@iflow-mcp/georgejeffers-uk-case-law-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/2288_process.log +4 -0
- package/LICENSE +131 -0
- package/README.md +112 -0
- package/bun.lock +213 -0
- package/dist/cases.d.ts +4 -0
- package/dist/cases.d.ts.map +1 -0
- package/dist/cases.js +32 -0
- package/dist/cases.js.map +1 -0
- package/dist/formatters.d.ts +21 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +164 -0
- package/dist/formatters.js.map +1 -0
- package/dist/search.d.ts +13 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +81 -0
- package/dist/search.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +191 -0
- package/dist/server.js.map +1 -0
- package/dist/test.d.ts +3 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +247 -0
- package/dist/test.js.map +1 -0
- package/dist/tna-client.d.ts +21 -0
- package/dist/tna-client.d.ts.map +1 -0
- package/dist/tna-client.js +394 -0
- package/dist/tna-client.js.map +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/language.json +1 -0
- package/package.json +1 -0
- package/package_name +1 -0
- package/push_info.json +5 -0
- package/src/cases.ts +36 -0
- package/src/formatters.ts +226 -0
- package/src/search.ts +108 -0
- package/src/server.ts +227 -0
- package/src/test.ts +283 -0
- package/src/tna-client.ts +495 -0
- package/src/types.ts +59 -0
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// src/formatters.ts
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// OUTPUT FORMATTERS
|
|
4
|
+
// ============================================================================
|
|
5
|
+
//
|
|
6
|
+
// Format search results and case content for Claude.
|
|
7
|
+
// Designed to be readable and include proper citations.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
import type { SearchResult, CaseContent, CaseParagraph } from './types.js';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// PARAGRAPH RANGE UTILITIES
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export function parseParaRange(range: string): { start: number; end: number } | null {
|
|
17
|
+
const match = range.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
18
|
+
if (!match || !match[1] || !match[2]) return null;
|
|
19
|
+
return {
|
|
20
|
+
start: parseInt(match[1], 10),
|
|
21
|
+
end: parseInt(match[2], 10),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Maximum characters to return (roughly 8000 tokens)
|
|
26
|
+
const MAX_OUTPUT_CHARS = 32000;
|
|
27
|
+
|
|
28
|
+
export function applyParaRange(
|
|
29
|
+
paragraphs: CaseParagraph[],
|
|
30
|
+
range: string | undefined
|
|
31
|
+
): { paragraphs: CaseParagraph[]; truncated: boolean; remaining: number } {
|
|
32
|
+
if (!range) {
|
|
33
|
+
// No range specified - apply token limit
|
|
34
|
+
return truncateToLimit(paragraphs);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const parsed = parseParaRange(range);
|
|
38
|
+
if (!parsed) {
|
|
39
|
+
return truncateToLimit(paragraphs);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const filtered = paragraphs.filter(
|
|
43
|
+
p => p.number >= parsed.start && p.number <= parsed.end
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return truncateToLimit(filtered);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function truncateToLimit(paragraphs: CaseParagraph[]): {
|
|
50
|
+
paragraphs: CaseParagraph[];
|
|
51
|
+
truncated: boolean;
|
|
52
|
+
remaining: number;
|
|
53
|
+
} {
|
|
54
|
+
let totalChars = 0;
|
|
55
|
+
const output: CaseParagraph[] = [];
|
|
56
|
+
|
|
57
|
+
for (const para of paragraphs) {
|
|
58
|
+
if (totalChars + para.text.length > MAX_OUTPUT_CHARS) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
output.push(para);
|
|
62
|
+
totalChars += para.text.length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
paragraphs: output,
|
|
67
|
+
truncated: output.length < paragraphs.length,
|
|
68
|
+
remaining: paragraphs.length - output.length,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// SEARCH RESULTS FORMATTER
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
export function formatSearchResults(results: SearchResult[]): string {
|
|
77
|
+
if (results.length === 0) {
|
|
78
|
+
return 'No cases found matching your search query.';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let output = `Found ${results.length} case${results.length === 1 ? '' : 's'}:\n\n`;
|
|
82
|
+
|
|
83
|
+
for (const result of results) {
|
|
84
|
+
// Citation or URI as header
|
|
85
|
+
const citation = result.neutralCitation || result.documentUri;
|
|
86
|
+
output += `**${citation}**\n`;
|
|
87
|
+
|
|
88
|
+
// Title
|
|
89
|
+
output += `${result.title}\n`;
|
|
90
|
+
|
|
91
|
+
// Court and date
|
|
92
|
+
const parts: string[] = [];
|
|
93
|
+
if (result.court) parts.push(result.court);
|
|
94
|
+
if (result.date) parts.push(result.date);
|
|
95
|
+
if (parts.length > 0) {
|
|
96
|
+
output += `${parts.join(' | ')}\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Snippet if available
|
|
100
|
+
if (result.snippet) {
|
|
101
|
+
const cleanSnippet = result.snippet
|
|
102
|
+
.replace(/\s+/g, ' ')
|
|
103
|
+
.trim()
|
|
104
|
+
.substring(0, 200);
|
|
105
|
+
output += `> ${cleanSnippet}${cleanSnippet.length >= 200 ? '...' : ''}\n`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Add document links
|
|
109
|
+
output += `[View on TNA](${result.urls.web}) | [PDF](${result.urls.pdf})\n`;
|
|
110
|
+
|
|
111
|
+
output += '\n';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return output.trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// CASE CONTENT FORMATTER
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
export interface FormatOptions {
|
|
122
|
+
includeMetadata?: boolean;
|
|
123
|
+
paragraphRange?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function formatCaseContent(
|
|
127
|
+
caseData: CaseContent,
|
|
128
|
+
options: FormatOptions = {}
|
|
129
|
+
): string {
|
|
130
|
+
const { includeMetadata = true, paragraphRange } = options;
|
|
131
|
+
|
|
132
|
+
let output = '';
|
|
133
|
+
|
|
134
|
+
// Metadata header
|
|
135
|
+
if (includeMetadata) {
|
|
136
|
+
output += `# ${caseData.metadata.title}\n\n`;
|
|
137
|
+
|
|
138
|
+
if (caseData.metadata.neutralCitation) {
|
|
139
|
+
output += `**Citation:** ${caseData.metadata.neutralCitation}\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
output += `**Court:** ${caseData.metadata.courtName || caseData.metadata.court}\n`;
|
|
143
|
+
|
|
144
|
+
if (caseData.metadata.date) {
|
|
145
|
+
output += `**Date:** ${caseData.metadata.date}\n`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (caseData.judges && caseData.judges.length > 0) {
|
|
149
|
+
output += `**Judges:** ${caseData.judges.join(', ')}\n`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (caseData.parties) {
|
|
153
|
+
if (caseData.parties.claimants.length > 0) {
|
|
154
|
+
output += `**Claimant(s):** ${caseData.parties.claimants.join(', ')}\n`;
|
|
155
|
+
}
|
|
156
|
+
if (caseData.parties.defendants.length > 0) {
|
|
157
|
+
output += `**Defendant(s):** ${caseData.parties.defendants.join(', ')}\n`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Add document links
|
|
162
|
+
output += `\n[View on TNA](${caseData.metadata.urls.web}) | [PDF](${caseData.metadata.urls.pdf})\n`;
|
|
163
|
+
|
|
164
|
+
output += '\n---\n\n';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Apply paragraph range and truncation
|
|
168
|
+
const { paragraphs, truncated, remaining } = applyParaRange(
|
|
169
|
+
caseData.paragraphs,
|
|
170
|
+
paragraphRange
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Format paragraphs with numbers for citation
|
|
174
|
+
for (const para of paragraphs) {
|
|
175
|
+
output += `[${para.number}] ${para.text}\n\n`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Truncation notice
|
|
179
|
+
if (truncated) {
|
|
180
|
+
output += `---\n\n`;
|
|
181
|
+
output += `*Output truncated. ${remaining} paragraph${remaining === 1 ? '' : 's'} remaining. `;
|
|
182
|
+
const lastPara = paragraphs[paragraphs.length - 1];
|
|
183
|
+
if (lastPara) {
|
|
184
|
+
output += `Use the paragraphs parameter (e.g., "${lastPara.number + 1}-${lastPara.number + 50}") to retrieve more.*\n`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return output.trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// CITATIONS FORMATTER
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
export function formatCitations(
|
|
196
|
+
citations: { citing: any[]; cited: any[] },
|
|
197
|
+
direction: 'citing' | 'cited' | 'both'
|
|
198
|
+
): string {
|
|
199
|
+
let output = '';
|
|
200
|
+
|
|
201
|
+
if (direction === 'citing' || direction === 'both') {
|
|
202
|
+
output += `## Cases Citing This Decision\n\n`;
|
|
203
|
+
if (citations.citing.length === 0) {
|
|
204
|
+
output += `No citing cases found.\n\n`;
|
|
205
|
+
} else {
|
|
206
|
+
for (const c of citations.citing) {
|
|
207
|
+
output += `- **${c.citation || c.title}** (${c.court}, ${c.date})\n`;
|
|
208
|
+
}
|
|
209
|
+
output += '\n';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (direction === 'cited' || direction === 'both') {
|
|
214
|
+
output += `## Cases Cited By This Decision\n\n`;
|
|
215
|
+
if (citations.cited.length === 0) {
|
|
216
|
+
output += `No cited cases found.\n\n`;
|
|
217
|
+
} else {
|
|
218
|
+
for (const c of citations.cited) {
|
|
219
|
+
output += `- **${c.citation || c.title}** (${c.court}, ${c.date})\n`;
|
|
220
|
+
}
|
|
221
|
+
output += '\n';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return output.trim();
|
|
226
|
+
}
|
package/src/search.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/search.ts
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// UNIFIED SEARCH LAYER
|
|
4
|
+
// ============================================================================
|
|
5
|
+
//
|
|
6
|
+
// Combines results from:
|
|
7
|
+
// 1. TNA API (2003+)
|
|
8
|
+
// 2. Local PostgreSQL (pre-2003 BAILII content) - optional, when database configured
|
|
9
|
+
//
|
|
10
|
+
// Results are merged using Reciprocal Rank Fusion (RRF).
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
import { searchTna, COURT_CODE_MAP, LEGAL_AREA_COURTS } from './tna-client.js';
|
|
14
|
+
import type { SearchResult } from './types.js';
|
|
15
|
+
|
|
16
|
+
export interface SearchParams {
|
|
17
|
+
query: string;
|
|
18
|
+
legalArea?: string;
|
|
19
|
+
court?: string;
|
|
20
|
+
yearFrom?: number;
|
|
21
|
+
yearTo?: number;
|
|
22
|
+
limit?: number;
|
|
23
|
+
page?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function searchCaseLaw(params: SearchParams): Promise<SearchResult[]> {
|
|
27
|
+
const limit = params.limit || 10;
|
|
28
|
+
|
|
29
|
+
// Determine which TNA courts to search
|
|
30
|
+
let tnaCourts: string[] | undefined;
|
|
31
|
+
|
|
32
|
+
if (params.court && params.court !== 'any') {
|
|
33
|
+
tnaCourts = COURT_CODE_MAP[params.court];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (params.legalArea && params.legalArea !== 'any') {
|
|
37
|
+
const areaCourts = LEGAL_AREA_COURTS[params.legalArea];
|
|
38
|
+
if (areaCourts) {
|
|
39
|
+
// Intersect with court filter if both specified
|
|
40
|
+
if (tnaCourts) {
|
|
41
|
+
tnaCourts = tnaCourts.filter(c => areaCourts.includes(c));
|
|
42
|
+
} else {
|
|
43
|
+
tnaCourts = areaCourts;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// For MVP, only use TNA API
|
|
49
|
+
// TODO: Add local database search when PostgreSQL is configured
|
|
50
|
+
const tnaResults = await searchTna({
|
|
51
|
+
query: params.query,
|
|
52
|
+
courts: tnaCourts,
|
|
53
|
+
yearFrom: params.yearFrom,
|
|
54
|
+
yearTo: params.yearTo,
|
|
55
|
+
limit: limit,
|
|
56
|
+
page: params.page,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return tnaResults.slice(0, limit);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// RECIPROCAL RANK FUSION (for future use with multiple sources)
|
|
64
|
+
// ============================================================================
|
|
65
|
+
//
|
|
66
|
+
// Combines ranked lists from different sources.
|
|
67
|
+
// Each document gets a score based on its rank in each list:
|
|
68
|
+
// score = sum(1 / (k + rank)) for each list
|
|
69
|
+
//
|
|
70
|
+
// k=60 is a standard constant that prevents top results from
|
|
71
|
+
// dominating too heavily.
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
export function reciprocalRankFusion(
|
|
75
|
+
resultLists: SearchResult[][],
|
|
76
|
+
limit: number,
|
|
77
|
+
k: number = 60
|
|
78
|
+
): SearchResult[] {
|
|
79
|
+
const scores = new Map<string, number>();
|
|
80
|
+
const docMap = new Map<string, SearchResult>();
|
|
81
|
+
|
|
82
|
+
for (const results of resultLists) {
|
|
83
|
+
for (let rank = 0; rank < results.length; rank++) {
|
|
84
|
+
const result = results[rank];
|
|
85
|
+
if (!result) continue;
|
|
86
|
+
|
|
87
|
+
const docId = result.documentUri || result.neutralCitation || result.title;
|
|
88
|
+
|
|
89
|
+
const currentScore = scores.get(docId) || 0;
|
|
90
|
+
scores.set(docId, currentScore + 1 / (k + rank + 1));
|
|
91
|
+
|
|
92
|
+
// Keep the first occurrence (usually has more complete metadata)
|
|
93
|
+
if (!docMap.has(docId)) {
|
|
94
|
+
docMap.set(docId, result);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Sort by RRF score
|
|
100
|
+
const sortedIds = [...scores.entries()]
|
|
101
|
+
.sort((a, b) => b[1] - a[1])
|
|
102
|
+
.map(([id]) => id);
|
|
103
|
+
|
|
104
|
+
return sortedIds
|
|
105
|
+
.slice(0, limit)
|
|
106
|
+
.map(id => docMap.get(id))
|
|
107
|
+
.filter((r): r is SearchResult => r !== undefined);
|
|
108
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// UK CASE LAW MCP SERVER
|
|
4
|
+
// ============================================================================
|
|
5
|
+
//
|
|
6
|
+
// Provides Claude with tools to search and retrieve UK case law.
|
|
7
|
+
//
|
|
8
|
+
// Tools provided:
|
|
9
|
+
// - uklaw_search: Search across all case law
|
|
10
|
+
// - uklaw_get_case: Get full text of a specific case
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
import { searchCaseLaw } from './search.js';
|
|
18
|
+
import { getCaseByUri, getCaseByCitation } from './cases.js';
|
|
19
|
+
import { formatSearchResults, formatCaseContent } from './formatters.js';
|
|
20
|
+
import type { SearchParams } from './search.js';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// SERVER INITIALIZATION
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const server = new McpServer({
|
|
27
|
+
name: 'uk-case-law',
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// TOOL: uklaw_search
|
|
33
|
+
// ============================================================================
|
|
34
|
+
//
|
|
35
|
+
// Primary search tool. Searches TNA API (2003+).
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
server.tool(
|
|
39
|
+
'uklaw_search',
|
|
40
|
+
`Search UK case law across all courts and tribunals.
|
|
41
|
+
|
|
42
|
+
Returns ranked results with:
|
|
43
|
+
- Neutral citations (e.g., [2024] UKSC 1)
|
|
44
|
+
- Case titles
|
|
45
|
+
- Courts and dates
|
|
46
|
+
- Brief snippets
|
|
47
|
+
|
|
48
|
+
Use filters to narrow results by court, legal area, or date range.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
- "patent obviousness test" - finds patent validity cases
|
|
52
|
+
- "unfair dismissal procedure" - finds employment cases
|
|
53
|
+
- "breach of fiduciary duty director" - finds company law cases`,
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
query: z.string()
|
|
57
|
+
.min(2)
|
|
58
|
+
.describe('Search terms - legal concepts, party names, or keywords'),
|
|
59
|
+
|
|
60
|
+
legal_area: z.enum([
|
|
61
|
+
'any',
|
|
62
|
+
'intellectual_property',
|
|
63
|
+
'commercial',
|
|
64
|
+
'company',
|
|
65
|
+
'employment',
|
|
66
|
+
'property',
|
|
67
|
+
'family',
|
|
68
|
+
'criminal',
|
|
69
|
+
'public_law',
|
|
70
|
+
'immigration',
|
|
71
|
+
'personal_injury'
|
|
72
|
+
])
|
|
73
|
+
.default('any')
|
|
74
|
+
.describe('Filter by area of law'),
|
|
75
|
+
|
|
76
|
+
court: z.enum([
|
|
77
|
+
'any',
|
|
78
|
+
'supreme_court',
|
|
79
|
+
'court_of_appeal',
|
|
80
|
+
'high_court',
|
|
81
|
+
'crown_court',
|
|
82
|
+
'tribunals'
|
|
83
|
+
])
|
|
84
|
+
.default('any')
|
|
85
|
+
.describe('Filter by court level'),
|
|
86
|
+
|
|
87
|
+
year_from: z.number()
|
|
88
|
+
.int()
|
|
89
|
+
.min(1800)
|
|
90
|
+
.max(2025)
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Earliest decision year'),
|
|
93
|
+
|
|
94
|
+
year_to: z.number()
|
|
95
|
+
.int()
|
|
96
|
+
.min(1800)
|
|
97
|
+
.max(2025)
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('Latest decision year'),
|
|
100
|
+
|
|
101
|
+
limit: z.number()
|
|
102
|
+
.int()
|
|
103
|
+
.min(1)
|
|
104
|
+
.max(50)
|
|
105
|
+
.default(10)
|
|
106
|
+
.describe('Maximum results to return'),
|
|
107
|
+
|
|
108
|
+
page: z.number()
|
|
109
|
+
.int()
|
|
110
|
+
.min(1)
|
|
111
|
+
.default(1)
|
|
112
|
+
.describe('Page number for pagination. If results seem truncated or date range is not met, try next page.'),
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async ({ query, legal_area, court, year_from, year_to, limit, page }) => {
|
|
116
|
+
try {
|
|
117
|
+
const params: SearchParams = {
|
|
118
|
+
query,
|
|
119
|
+
legalArea: legal_area === 'any' ? undefined : legal_area,
|
|
120
|
+
court: court === 'any' ? undefined : court,
|
|
121
|
+
yearFrom: year_from,
|
|
122
|
+
yearTo: year_to,
|
|
123
|
+
limit,
|
|
124
|
+
page,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const results = await searchCaseLaw(params);
|
|
128
|
+
const formatted = formatSearchResults(results);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: 'text', text: formatted }]
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
content: [{
|
|
136
|
+
type: 'text',
|
|
137
|
+
text: `Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
138
|
+
}],
|
|
139
|
+
isError: true
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// TOOL: uklaw_get_case
|
|
147
|
+
// ============================================================================
|
|
148
|
+
//
|
|
149
|
+
// Retrieves full text of a specific case. For TNA cases, fetches from API.
|
|
150
|
+
//
|
|
151
|
+
// Output includes numbered paragraphs for precise citation.
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
server.tool(
|
|
155
|
+
'uklaw_get_case',
|
|
156
|
+
`Retrieve the full text of a specific UK case.
|
|
157
|
+
|
|
158
|
+
Accepts either:
|
|
159
|
+
- Neutral citation: "[2007] EWCA Civ 588"
|
|
160
|
+
- Document URI: "ewca/civ/2007/588"
|
|
161
|
+
|
|
162
|
+
Returns the judgment with numbered paragraphs. Use paragraph numbers
|
|
163
|
+
when citing specific passages, e.g., "as stated at [23]".
|
|
164
|
+
|
|
165
|
+
For long judgments, use the paragraphs parameter to request a specific
|
|
166
|
+
range (e.g., "1-50" for the first 50 paragraphs).`,
|
|
167
|
+
|
|
168
|
+
{
|
|
169
|
+
citation: z.string()
|
|
170
|
+
.describe('Neutral citation (e.g., "[2024] UKSC 1") or document URI (e.g., "uksc/2024/1")'),
|
|
171
|
+
|
|
172
|
+
paragraphs: z.string()
|
|
173
|
+
.optional()
|
|
174
|
+
.describe('Paragraph range to retrieve, e.g., "1-50" or "23-45". Omit for full text.'),
|
|
175
|
+
|
|
176
|
+
include_metadata: z.boolean()
|
|
177
|
+
.default(true)
|
|
178
|
+
.describe('Include case metadata (judges, date, court)')
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async ({ citation, paragraphs, include_metadata }) => {
|
|
182
|
+
try {
|
|
183
|
+
// Determine if this is a neutral citation or URI
|
|
184
|
+
const isNeutralCitation = citation.startsWith('[');
|
|
185
|
+
|
|
186
|
+
const caseData = isNeutralCitation
|
|
187
|
+
? await getCaseByCitation(citation)
|
|
188
|
+
: await getCaseByUri(citation);
|
|
189
|
+
|
|
190
|
+
if (!caseData) {
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: 'text', text: `Case not found: ${citation}` }],
|
|
193
|
+
isError: true
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const formatted = formatCaseContent(caseData, {
|
|
198
|
+
includeMetadata: include_metadata,
|
|
199
|
+
paragraphRange: paragraphs,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: 'text', text: formatted }]
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return {
|
|
207
|
+
content: [{
|
|
208
|
+
type: 'text',
|
|
209
|
+
text: `Failed to retrieve case: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
210
|
+
}],
|
|
211
|
+
isError: true
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// START SERVER
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
async function main() {
|
|
222
|
+
const transport = new StdioServerTransport();
|
|
223
|
+
await server.connect(transport);
|
|
224
|
+
console.error('UK Case Law MCP server running');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
main().catch(console.error);
|