// src/server.ts // ============================================================================ // UK CASE LAW MCP SERVER // ============================================================================ // // Provides Claude with tools to search and retrieve UK case law. // // Tools provided: // - uklaw_search: Search across all case law // - uklaw_get_case: Get full text of a specific case // ============================================================================ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { searchCaseLaw } from './search.js'; import { getCaseByUri, getCaseByCitation } from './cases.js'; import { formatSearchResults, formatCaseContent } from './formatters.js'; import type { SearchParams } from './search.js'; // ============================================================================ // SERVER INITIALIZATION // ============================================================================ const server = new McpServer({ name: 'uk-case-law', version: '1.0.0', }); // ============================================================================ // TOOL: uklaw_search // ============================================================================ // // Primary search tool. Searches TNA API (2003+). // ============================================================================ server.tool( 'uklaw_search', `Search UK case law across all courts and tribunals. Returns ranked results with: - Neutral citations (e.g., [2024] UKSC 1) - Case titles - Courts and dates - Brief snippets Use filters to narrow results by court, legal area, or date range. Examples: - "patent obviousness test" - finds patent validity cases - "unfair dismissal procedure" - finds employment cases - "breach of fiduciary duty director" - finds company law cases`, { query: z.string() .min(2) .describe('Search terms - legal concepts, party names, or keywords'), legal_area: z.enum([ 'any', 'intellectual_property', 'commercial', 'company', 'employment', 'property', 'family', 'criminal', 'public_law', 'immigration', 'personal_injury' ]) .default('any') .describe('Filter by area of law'), court: z.enum([ 'any', 'supreme_court', 'court_of_appeal', 'high_court', 'crown_court', 'tribunals' ]) .default('any') .describe('Filter by court level'), year_from: z.number() .int() .min(1800) .max(2025) .optional() .describe('Earliest decision year'), year_to: z.number() .int() .min(1800) .max(2025) .optional() .describe('Latest decision year'), limit: z.number() .int() .min(1) .max(50) .default(10) .describe('Maximum results to return'), page: z.number() .int() .min(1) .default(1) .describe('Page number for pagination. If results seem truncated or date range is not met, try next page.'), }, async ({ query, legal_area, court, year_from, year_to, limit, page }) => { try { const params: SearchParams = { query, legalArea: legal_area === 'any' ? undefined : legal_area, court: court === 'any' ? undefined : court, yearFrom: year_from, yearTo: year_to, limit, page, }; const results = await searchCaseLaw(params); const formatted = formatSearchResults(results); return { content: [{ type: 'text', text: formatted }] }; } catch (error) { return { content: [{ type: 'text', text: `Search failed: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } ); // ============================================================================ // TOOL: uklaw_get_case // ============================================================================ // // Retrieves full text of a specific case. For TNA cases, fetches from API. // // Output includes numbered paragraphs for precise citation. // ============================================================================ server.tool( 'uklaw_get_case', `Retrieve the full text of a specific UK case. Accepts either: - Neutral citation: "[2007] EWCA Civ 588" - Document URI: "ewca/civ/2007/588" Returns the judgment with numbered paragraphs. Use paragraph numbers when citing specific passages, e.g., "as stated at [23]". For long judgments, use the paragraphs parameter to request a specific range (e.g., "1-50" for the first 50 paragraphs).`, { citation: z.string() .describe('Neutral citation (e.g., "[2024] UKSC 1") or document URI (e.g., "uksc/2024/1")'), paragraphs: z.string() .optional() .describe('Paragraph range to retrieve, e.g., "1-50" or "23-45". Omit for full text.'), include_metadata: z.boolean() .default(true) .describe('Include case metadata (judges, date, court)') }, async ({ citation, paragraphs, include_metadata }) => { try { // Determine if this is a neutral citation or URI const isNeutralCitation = citation.startsWith('['); const caseData = isNeutralCitation ? await getCaseByCitation(citation) : await getCaseByUri(citation); if (!caseData) { return { content: [{ type: 'text', text: `Case not found: ${citation}` }], isError: true }; } const formatted = formatCaseContent(caseData, { includeMetadata: include_metadata, paragraphRange: paragraphs, }); return { content: [{ type: 'text', text: formatted }] }; } catch (error) { return { content: [{ type: 'text', text: `Failed to retrieve case: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } ); // ============================================================================ // START SERVER // ============================================================================ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('UK Case Law MCP server running'); } main().catch(console.error);