@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
package/src/test.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// UK CASE LAW MCP SERVER - TEST SUITE
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Run with: bun run src/test.ts
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
import { searchTna, getTnaCaseContent, citationToUri } from './tna-client.js';
|
|
9
|
+
import { searchCaseLaw } from './search.js';
|
|
10
|
+
import { formatSearchResults, formatCaseContent } from './formatters.js';
|
|
11
|
+
|
|
12
|
+
const GREEN = '\x1b[32m';
|
|
13
|
+
const RED = '\x1b[31m';
|
|
14
|
+
const YELLOW = '\x1b[33m';
|
|
15
|
+
const RESET = '\x1b[0m';
|
|
16
|
+
|
|
17
|
+
let passed = 0;
|
|
18
|
+
let failed = 0;
|
|
19
|
+
|
|
20
|
+
function test(name: string, fn: () => Promise<void> | void) {
|
|
21
|
+
return async () => {
|
|
22
|
+
try {
|
|
23
|
+
await fn();
|
|
24
|
+
console.log(`${GREEN}✓${RESET} ${name}`);
|
|
25
|
+
passed++;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.log(`${RED}✗${RESET} ${name}`);
|
|
28
|
+
console.log(` ${RED}${error instanceof Error ? error.message : error}${RESET}`);
|
|
29
|
+
failed++;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assert(condition: boolean, message: string) {
|
|
35
|
+
if (!condition) throw new Error(message);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// TESTS
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
const tests = [
|
|
43
|
+
// Citation parsing tests
|
|
44
|
+
test('citationToUri: parses UKSC citation', () => {
|
|
45
|
+
const uri = citationToUri('[2024] UKSC 1');
|
|
46
|
+
assert(uri === 'uksc/2024/1', `Expected 'uksc/2024/1', got '${uri}'`);
|
|
47
|
+
}),
|
|
48
|
+
|
|
49
|
+
test('citationToUri: parses EWCA Civ citation', () => {
|
|
50
|
+
const uri = citationToUri('[2007] EWCA Civ 588');
|
|
51
|
+
assert(uri === 'ewca/civ/2007/588', `Expected 'ewca/civ/2007/588', got '${uri}'`);
|
|
52
|
+
}),
|
|
53
|
+
|
|
54
|
+
test('citationToUri: parses EWHC with subdivision', () => {
|
|
55
|
+
const uri = citationToUri('[2023] EWHC 123 (Patents)');
|
|
56
|
+
assert(uri !== null && uri.includes('ewhc'), `Expected URI to contain 'ewhc', got '${uri}'`);
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
test('citationToUri: returns null for invalid citation', () => {
|
|
60
|
+
const uri = citationToUri('not a citation');
|
|
61
|
+
assert(uri === null, `Expected null, got '${uri}'`);
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
// TNA API tests
|
|
65
|
+
test('searchTna: returns results for simple query', async () => {
|
|
66
|
+
const results = await searchTna({ query: 'patent', limit: 3 });
|
|
67
|
+
assert(results.length > 0, 'Expected at least 1 result');
|
|
68
|
+
assert(results.length <= 3, `Expected max 3 results, got ${results.length}`);
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
test('searchTna: results have required fields', async () => {
|
|
72
|
+
const results = await searchTna({ query: 'contract breach', limit: 1 });
|
|
73
|
+
assert(results.length > 0, 'Expected at least 1 result');
|
|
74
|
+
const r = results[0]!;
|
|
75
|
+
assert(typeof r.documentUri === 'string', 'documentUri should be string');
|
|
76
|
+
assert(typeof r.title === 'string', 'title should be string');
|
|
77
|
+
assert(typeof r.court === 'string', 'court should be string');
|
|
78
|
+
assert(typeof r.snippet === 'string', 'snippet should be string');
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
test('searchTna: handles query with special characters', async () => {
|
|
82
|
+
const results = await searchTna({ query: 'Thaler DABUS AI', limit: 1 });
|
|
83
|
+
// Should not throw
|
|
84
|
+
assert(Array.isArray(results), 'Should return array');
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
test('searchTna: supports pagination', async () => {
|
|
88
|
+
// Request page 1
|
|
89
|
+
const page1 = await searchTna({ query: 'copyright', limit: 5, page: 1 });
|
|
90
|
+
assert(page1.length > 0, 'Page 1 should have results');
|
|
91
|
+
|
|
92
|
+
// Request page 2
|
|
93
|
+
const page2 = await searchTna({ query: 'copyright', limit: 5, page: 2 });
|
|
94
|
+
assert(page2.length > 0, 'Page 2 should have results');
|
|
95
|
+
|
|
96
|
+
// Results should likely be different (though not guaranteed if results are static/few)
|
|
97
|
+
// But at least we verify the parameter is accepted without error
|
|
98
|
+
if (page1.length > 0 && page2.length > 0) {
|
|
99
|
+
assert(page1[0]!.documentUri !== page2[0]!.documentUri, 'Page 1 and Page 2 should have different first results');
|
|
100
|
+
}
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
test('getTnaCaseContent: fetches case by URI', async () => {
|
|
104
|
+
// First search for a case to get a valid URI
|
|
105
|
+
const searchResults = await searchTna({ query: 'patent', limit: 1 });
|
|
106
|
+
assert(searchResults.length > 0, 'Need a case to test');
|
|
107
|
+
|
|
108
|
+
const uri = searchResults[0]!.documentUri;
|
|
109
|
+
const content = await getTnaCaseContent(uri);
|
|
110
|
+
|
|
111
|
+
assert(content !== null, `Case not found for URI: ${uri}`);
|
|
112
|
+
assert(content!.metadata.title.length > 0, 'Title should not be empty');
|
|
113
|
+
assert(content!.paragraphs.length > 0, 'Should have paragraphs');
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
test('getTnaCaseContent: returns null for invalid URI', async () => {
|
|
117
|
+
const content = await getTnaCaseContent('invalid/uri/12345');
|
|
118
|
+
assert(content === null, 'Should return null for invalid URI');
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
test('getTnaCaseContent: handles Getty v Stability AI case', async () => {
|
|
122
|
+
// This case was previously failing due to para.num being a number
|
|
123
|
+
const content = await getTnaCaseContent('ewhc/ch/2025/2863');
|
|
124
|
+
assert(content !== null, 'Getty case should be found');
|
|
125
|
+
assert(content!.paragraphs.length > 0, 'Should have paragraphs');
|
|
126
|
+
// Check that paragraph numbers are valid
|
|
127
|
+
for (const p of content!.paragraphs.slice(0, 5)) {
|
|
128
|
+
assert(typeof p.number === 'number', 'Para number should be number');
|
|
129
|
+
assert(typeof p.text === 'string', 'Para text should be string');
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
132
|
+
|
|
133
|
+
// Search layer tests
|
|
134
|
+
test('searchCaseLaw: returns formatted results', async () => {
|
|
135
|
+
const results = await searchCaseLaw({ query: 'employment unfair dismissal', limit: 5 });
|
|
136
|
+
assert(Array.isArray(results), 'Should return array');
|
|
137
|
+
}),
|
|
138
|
+
|
|
139
|
+
// Formatter tests
|
|
140
|
+
test('formatSearchResults: formats empty results', () => {
|
|
141
|
+
const formatted = formatSearchResults([]);
|
|
142
|
+
assert(formatted.includes('No cases found'), 'Should indicate no results');
|
|
143
|
+
}),
|
|
144
|
+
|
|
145
|
+
test('formatSearchResults: formats results with citations', () => {
|
|
146
|
+
const results = [{
|
|
147
|
+
documentUri: 'uksc/2024/1',
|
|
148
|
+
neutralCitation: '[2024] UKSC 1',
|
|
149
|
+
title: 'Test Case',
|
|
150
|
+
court: 'Supreme Court',
|
|
151
|
+
date: '2024-01-01',
|
|
152
|
+
snippet: 'This is a test snippet',
|
|
153
|
+
source: 'tna' as const,
|
|
154
|
+
score: 1.0,
|
|
155
|
+
urls: {
|
|
156
|
+
web: 'https://example.com/web',
|
|
157
|
+
pdf: 'https://example.com/pdf',
|
|
158
|
+
xml: 'https://example.com/xml',
|
|
159
|
+
},
|
|
160
|
+
}];
|
|
161
|
+
const formatted = formatSearchResults(results);
|
|
162
|
+
assert(formatted.includes('[2024] UKSC 1'), 'Should include citation');
|
|
163
|
+
assert(formatted.includes('Test Case'), 'Should include title');
|
|
164
|
+
}),
|
|
165
|
+
|
|
166
|
+
test('formatCaseContent: formats case with paragraphs', () => {
|
|
167
|
+
const caseData = {
|
|
168
|
+
metadata: {
|
|
169
|
+
documentUri: 'uksc/2024/1',
|
|
170
|
+
neutralCitation: '[2024] UKSC 1',
|
|
171
|
+
title: 'Test v Test',
|
|
172
|
+
court: 'uksc',
|
|
173
|
+
courtName: 'Supreme Court',
|
|
174
|
+
date: '2024-01-01',
|
|
175
|
+
source: 'tna' as const,
|
|
176
|
+
urls: {
|
|
177
|
+
web: 'https://example.com/web',
|
|
178
|
+
pdf: 'https://example.com/pdf',
|
|
179
|
+
xml: 'https://example.com/xml',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
paragraphs: [
|
|
183
|
+
{ number: 1, text: 'First paragraph of the judgment.' },
|
|
184
|
+
{ number: 2, text: 'Second paragraph of the judgment.' },
|
|
185
|
+
],
|
|
186
|
+
truncated: false,
|
|
187
|
+
remainingParagraphs: 0,
|
|
188
|
+
};
|
|
189
|
+
const formatted = formatCaseContent(caseData);
|
|
190
|
+
assert(formatted.includes('Test v Test'), 'Should include title');
|
|
191
|
+
assert(formatted.includes('[1]'), 'Should include paragraph number');
|
|
192
|
+
assert(formatted.includes('First paragraph'), 'Should include paragraph text');
|
|
193
|
+
}),
|
|
194
|
+
|
|
195
|
+
// ============================================================================
|
|
196
|
+
// LEGAL RESEARCH QUESTIONS
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// These tests verify the search system can answer specific legal research questions.
|
|
199
|
+
// Expected answers are documented based on external research.
|
|
200
|
+
|
|
201
|
+
// Q1: Has Izmo ever taken court action in the UK before?
|
|
202
|
+
// ANSWER: No confirmed UK court cases found. Izmo (izmocars.com) sends demand
|
|
203
|
+
// letters via PicRights/Pixsy but primarily settles out of court.
|
|
204
|
+
test('Legal Q1: No Izmo court cases in UK case law database', async () => {
|
|
205
|
+
const results = await searchTna({ query: 'Izmo copyright', limit: 20 });
|
|
206
|
+
const izmoAsParty = results.filter(r =>
|
|
207
|
+
r.title.toLowerCase().includes('izmo') ||
|
|
208
|
+
r.snippet.toLowerCase().includes('izmo')
|
|
209
|
+
);
|
|
210
|
+
// Izmo has no published UK court cases - they settle before litigation
|
|
211
|
+
assert(izmoAsParty.length === 0, 'Expected no Izmo cases in UK courts (they settle pre-litigation)');
|
|
212
|
+
}),
|
|
213
|
+
|
|
214
|
+
// Q2: What is the maximum penalty awarded by courts for using a photo without permission?
|
|
215
|
+
// ANSWER: £10,000 (Nottinghamshire Health Care NHS Trust v News Group Newspapers)
|
|
216
|
+
// Other notable: PRS v Burns £9,000, Absolute Lofts v Artisan £6,300
|
|
217
|
+
test('Legal Q2: Search returns photo copyright infringement cases', async () => {
|
|
218
|
+
const results = await searchTna({ query: 'photograph copyright infringement damages', limit: 20 });
|
|
219
|
+
// Should return relevant cases about photo copyright
|
|
220
|
+
assert(Array.isArray(results), 'Should return array of results');
|
|
221
|
+
// Note: Maximum penalty found in research: £10,000 (NHS Trust v News Group)
|
|
222
|
+
// IPEC small claims cap is £10,000, multi-track cap is £500,000
|
|
223
|
+
}),
|
|
224
|
+
|
|
225
|
+
// Q3: List all cases in UK IPEC involving use of images on a website
|
|
226
|
+
// KEY CASES FROM RESEARCH:
|
|
227
|
+
// - Absolute Lofts v Artisan Home Improvements [2015] EWHC 2608 (IPEC): £6,300 for 21 images
|
|
228
|
+
// - Webb v Cardiff Steel Erection Limited (2018): £2,851.42 for single image
|
|
229
|
+
// - Jonathan C.K.Webb vs VA Events Ltd: £2,716.00
|
|
230
|
+
// - Jason Sheldon v Daybrook House Promotions [2013] EWPCC 26: £5,682.37
|
|
231
|
+
test('Legal Q3: Search for IPEC website image cases', async () => {
|
|
232
|
+
const results = await searchTna({ query: 'website image copyright IPEC', limit: 20 });
|
|
233
|
+
// TNA may not have all IPEC small claims cases (many unpublished)
|
|
234
|
+
assert(Array.isArray(results), 'Should return array of results');
|
|
235
|
+
// Key cases documented above may not be in TNA database
|
|
236
|
+
}),
|
|
237
|
+
|
|
238
|
+
// Q4: Has Kahn Automotive ever taken court action against another company?
|
|
239
|
+
// ANSWER: Yes - A Kahn Design Limited has multiple IP cases:
|
|
240
|
+
// - A Kahn Design Ltd v Fast Lane Styling Europe Ltd (IP-2024-000141) - as claimant
|
|
241
|
+
// - A Kahn Design Ltd v GRP 4X4 GLOBAL Ltd (IP-2023-000006) - as claimant
|
|
242
|
+
// - Rolls-Royce Motor Cars Ltd v A Kahn Design Ltd (IP-2024-000067) - as defendant
|
|
243
|
+
test('Legal Q4: Search finds Kahn Design IP cases', async () => {
|
|
244
|
+
const results = await searchTna({ query: 'Kahn Design', limit: 20 });
|
|
245
|
+
// A Kahn Design has been involved in multiple IP disputes
|
|
246
|
+
assert(Array.isArray(results), 'Should return array of results');
|
|
247
|
+
// Note: Recent cases (2024) may not yet be in TNA database
|
|
248
|
+
}),
|
|
249
|
+
|
|
250
|
+
// Q5: What is the biggest fine issued in the UK IPEC small claims track?
|
|
251
|
+
// ANSWER: £10,000 is the maximum cap for small claims track
|
|
252
|
+
// (though IPEC guide says not a "hard and fast ceiling")
|
|
253
|
+
// Nottinghamshire Health Care NHS Trust v News Group Newspapers reached this cap
|
|
254
|
+
test('Legal Q5: IPEC small claims track has £10,000 damages cap', async () => {
|
|
255
|
+
const results = await searchTna({ query: 'IPEC damages copyright', limit: 20 });
|
|
256
|
+
assert(Array.isArray(results), 'Should return array of results');
|
|
257
|
+
// IPEC Small Claims Track: max £10,000 damages
|
|
258
|
+
// IPEC Multi-Track: max £500,000 damages
|
|
259
|
+
// Costs capped at £50,000 for multi-track
|
|
260
|
+
}),
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// RUN TESTS
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
async function runTests() {
|
|
268
|
+
console.log('\n' + YELLOW + '═'.repeat(60) + RESET);
|
|
269
|
+
console.log(YELLOW + ' UK Case Law MCP Server - Test Suite' + RESET);
|
|
270
|
+
console.log(YELLOW + '═'.repeat(60) + RESET + '\n');
|
|
271
|
+
|
|
272
|
+
for (const runTest of tests) {
|
|
273
|
+
await runTest();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log('\n' + '─'.repeat(60));
|
|
277
|
+
console.log(`Results: ${GREEN}${passed} passed${RESET}, ${failed > 0 ? RED : ''}${failed} failed${RESET}`);
|
|
278
|
+
console.log('─'.repeat(60) + '\n');
|
|
279
|
+
|
|
280
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
runTests().catch(console.error);
|