courtlistener-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/README.md +24 -0
- package/dist/api.d.ts +24 -0
- package/dist/api.js +75 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +58 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# CourtListener MCP
|
|
2
|
+
|
|
3
|
+
Keyless MCP server for CourtListener: search court opinions and federal dockets (RECAP), case status, citations, judges.
|
|
4
|
+
|
|
5
|
+
Pairs well with `court-records-mcp`, `federal-register-mcp`, and `police-transparency-mcp`.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run build
|
|
12
|
+
node dist/index.js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code, MCP Inspector, or another compatible MCP client.
|
|
16
|
+
|
|
17
|
+
## Tools at a glance
|
|
18
|
+
|
|
19
|
+
- `search_opinions`: Opinions with court, status, citations, syllabus.
|
|
20
|
+
- `search_dockets`: RECAP federal dockets with docket numbers.
|
|
21
|
+
|
|
22
|
+
## Limits and privacy
|
|
23
|
+
|
|
24
|
+
This project is intentionally narrow. It should be treated as a practical helper, not a complete certification or security audit. Check the implementation and the returned data before using it with sensitive material. No credentials are required for search; full opinion/docket detail endpoints need a free CourtListener API key and are not included.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare class CourtListenerError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export interface OpinionHit {
|
|
4
|
+
caseName?: string;
|
|
5
|
+
court?: string;
|
|
6
|
+
dateFiled?: string;
|
|
7
|
+
status?: string;
|
|
8
|
+
citation?: string;
|
|
9
|
+
citeCount?: number;
|
|
10
|
+
judge?: string;
|
|
11
|
+
syllabus?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function searchOpinions(query: string, limit?: number): Promise<OpinionHit[]>;
|
|
15
|
+
export interface DocketHit {
|
|
16
|
+
caseName?: string;
|
|
17
|
+
court?: string;
|
|
18
|
+
docketNumber?: string;
|
|
19
|
+
dateFiled?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function searchDockets(query: string, limit?: number): Promise<DocketHit[]>;
|
|
23
|
+
export declare function formatOpinion(o: OpinionHit, index?: number): string;
|
|
24
|
+
export declare function formatDocket(d: DocketHit, index?: number): string;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CourtListener API v4 client, keyless for search.
|
|
3
|
+
* Docs: https://www.courtlistener.com/help/api/rest/
|
|
4
|
+
* Full opinion/docket detail endpoints need an API key; search does not.
|
|
5
|
+
*/
|
|
6
|
+
const BASE = "https://www.courtlistener.com/api/rest/v4";
|
|
7
|
+
export class CourtListenerError extends Error {
|
|
8
|
+
}
|
|
9
|
+
async function searchApi(query, type, limit) {
|
|
10
|
+
const url = `${BASE}/search/?q=${encodeURIComponent(query)}&type=${type}`;
|
|
11
|
+
const res = await fetch(url, {
|
|
12
|
+
headers: { "User-Agent": "courtlistener-mcp/1.0" },
|
|
13
|
+
signal: AbortSignal.timeout(20000),
|
|
14
|
+
});
|
|
15
|
+
if (!res.ok)
|
|
16
|
+
throw new CourtListenerError(`CourtListener error ${res.status}`);
|
|
17
|
+
const data = (await res.json());
|
|
18
|
+
const results = Array.isArray(data.results) ? data.results : [];
|
|
19
|
+
return results.slice(0, limit);
|
|
20
|
+
}
|
|
21
|
+
const fullUrl = (path) => typeof path === "string" && path ? `https://www.courtlistener.com${path}` : undefined;
|
|
22
|
+
const clean = (s, max) => {
|
|
23
|
+
if (typeof s !== "string" || !s)
|
|
24
|
+
return undefined;
|
|
25
|
+
const t = s.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
26
|
+
return t ? (t.length > max ? t.slice(0, max) + "..." : t) : undefined;
|
|
27
|
+
};
|
|
28
|
+
export async function searchOpinions(query, limit = 5) {
|
|
29
|
+
const rows = await searchApi(query, "o", limit);
|
|
30
|
+
return rows.map((r) => ({
|
|
31
|
+
caseName: typeof r.caseName === "string" ? r.caseName : undefined,
|
|
32
|
+
court: typeof r.court === "string" ? r.court : undefined,
|
|
33
|
+
dateFiled: typeof r.dateFiled === "string" ? r.dateFiled.slice(0, 10) : undefined,
|
|
34
|
+
status: typeof r.status === "string" ? r.status : undefined,
|
|
35
|
+
citation: Array.isArray(r.citation) ? String(r.citation[0]) : typeof r.citation === "string" ? r.citation : undefined,
|
|
36
|
+
citeCount: typeof r.citeCount === "number" ? r.citeCount : undefined,
|
|
37
|
+
judge: typeof r.judge === "string" && r.judge ? r.judge : undefined,
|
|
38
|
+
syllabus: clean(r.syllabus, 280),
|
|
39
|
+
url: fullUrl(r.absolute_url),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
export async function searchDockets(query, limit = 5) {
|
|
43
|
+
const rows = await searchApi(query, "r", limit);
|
|
44
|
+
return rows.map((r) => ({
|
|
45
|
+
caseName: typeof r.caseName === "string" ? r.caseName : undefined,
|
|
46
|
+
court: typeof r.court === "string" ? r.court : undefined,
|
|
47
|
+
docketNumber: typeof r.docketNumber === "string" ? r.docketNumber : undefined,
|
|
48
|
+
dateFiled: typeof r.dateFiled === "string" ? r.dateFiled.slice(0, 10) : undefined,
|
|
49
|
+
url: fullUrl(r.absolute_url),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
export function formatOpinion(o, index) {
|
|
53
|
+
const prefix = index !== undefined ? `${index + 1}. ` : "";
|
|
54
|
+
const lines = [
|
|
55
|
+
`${prefix}${o.caseName ?? "(unnamed case)"}${o.status ? ` [${o.status}]` : ""}`,
|
|
56
|
+
o.court ? `Court: ${o.court}` : "",
|
|
57
|
+
o.dateFiled ? `Filed: ${o.dateFiled}` : "",
|
|
58
|
+
o.citation ? `Cite: ${o.citation}` : "",
|
|
59
|
+
o.citeCount !== undefined ? `Cited by: ${o.citeCount}` : "",
|
|
60
|
+
o.judge ? `Judge: ${o.judge}` : "",
|
|
61
|
+
o.syllabus ? `Syllabus: ${o.syllabus}` : "",
|
|
62
|
+
o.url ? `More: ${o.url}` : "",
|
|
63
|
+
].filter(Boolean);
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
export function formatDocket(d, index) {
|
|
67
|
+
const prefix = index !== undefined ? `${index + 1}. ` : "";
|
|
68
|
+
const lines = [
|
|
69
|
+
`${prefix}${d.caseName ?? "(unnamed docket)"}${d.docketNumber ? ` (${d.docketNumber})` : ""}`,
|
|
70
|
+
d.court ? `Court: ${d.court}` : "",
|
|
71
|
+
d.dateFiled ? `Filed: ${d.dateFiled}` : "",
|
|
72
|
+
d.url ? `More: ${d.url}` : "",
|
|
73
|
+
].filter(Boolean);
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
console.error("MCP server running on stdio");
|
|
8
|
+
}
|
|
9
|
+
main().catch((err) => {
|
|
10
|
+
console.error("Fatal error:", err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CourtListenerError, formatDocket, formatOpinion, searchDockets, searchOpinions, } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
|
+
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
7
|
+
export function createServer() {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "courtlistener-mcp",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
});
|
|
12
|
+
server.registerTool("search_opinions", {
|
|
13
|
+
title: "Search court opinions",
|
|
14
|
+
description: "Search CourtListener opinions: case name, court, filing date, status, citations, judge, syllabus.",
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
query: z.string().describe("Search text, e.g. 'roe wade', ' Miranda rights', 'patent exhaustion'"),
|
|
17
|
+
limit: z.number().int().min(1).max(20).default(5),
|
|
18
|
+
}),
|
|
19
|
+
annotations: READ_ONLY,
|
|
20
|
+
}, async ({ query, limit }) => {
|
|
21
|
+
try {
|
|
22
|
+
const results = await searchOpinions(query, limit);
|
|
23
|
+
if (results.length === 0)
|
|
24
|
+
return text(`No CourtListener opinions match "${query}".`);
|
|
25
|
+
return text(`CourtListener opinions for "${query}":\n\n${results.map((o, i) => formatOpinion(o, i)).join("\n\n")}`);
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
return textError(errorMessage(e));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
server.registerTool("search_dockets", {
|
|
32
|
+
title: "Search federal dockets",
|
|
33
|
+
description: "Search CourtListener RECAP federal dockets: case name, court, docket number, filing date.",
|
|
34
|
+
inputSchema: z.object({
|
|
35
|
+
query: z.string().describe("Search text, e.g. a party name or case topic"),
|
|
36
|
+
limit: z.number().int().min(1).max(20).default(5),
|
|
37
|
+
}),
|
|
38
|
+
annotations: READ_ONLY,
|
|
39
|
+
}, async ({ query, limit }) => {
|
|
40
|
+
try {
|
|
41
|
+
const results = await searchDockets(query, limit);
|
|
42
|
+
if (results.length === 0)
|
|
43
|
+
return text(`No CourtListener dockets match "${query}".`);
|
|
44
|
+
return text(`CourtListener dockets for "${query}":\n\n${results.map((d, i) => formatDocket(d, i)).join("\n\n")}`);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return textError(errorMessage(e));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return server;
|
|
51
|
+
}
|
|
52
|
+
function errorMessage(e) {
|
|
53
|
+
if (e instanceof CourtListenerError)
|
|
54
|
+
return `Error: ${e.message}`;
|
|
55
|
+
if (e instanceof Error)
|
|
56
|
+
return `Error: ${e.message}`;
|
|
57
|
+
return `Error: ${String(e)}`;
|
|
58
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "courtlistener-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Keyless MCP server for CourtListener: search court opinions and federal dockets (RECAP), case status, citations, judges.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/courtlistener-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/mrfentmen/awesome-mcps.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"courtlistener-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"dev": "rm -rf dist && tsc -p tsconfig.json && node dist/index.js",
|
|
22
|
+
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"courtlistener",
|
|
27
|
+
"courts",
|
|
28
|
+
"api"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
33
|
+
"zod": "^3.23.8"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"typescript": "^5.6.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
}
|
|
42
|
+
}
|