mcp-courtwatch
This MCP server provides free U.S. case-law and court-docket search via CourtListener, including full-text opinion search, docket lookup, citation verification, citator checks, court/judge lookups, docket entries, and oral-argument transcripts.
opinion_search – full-text case-law search with filters (court, date range, sort) and pagination
docket_lookup – search federal dockets by case name, free text, or docket number
court_list – list courts and their ids, filterable by jurisdiction and name
case_detail – fetch a full case cluster (name, citations, judges, opinion ids) or an opinion’s full text (token required)
citation_lookup – verify citations in free text against real cases, flagging fabricated or unknown ones (token required)
cited_by – find every opinion citing a given opinion (free citator check; no treatment classification)
case_authorities – list the authorities an opinion relies on, with citation depth (token required)
docket_entries – retrieve a federal docket’s RECAP filing history, including archived PACER documents (token required)
oral_arguments – search oral-argument audio with court/date filters and link to MP3s
oral_argument_transcript – retrieve machine-generated Whisper transcripts of oral arguments, with transcription-status flags and paging
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-courtwatch search for tenant rights opinions in California"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-courtwatch
MCP server for free U.S. case-law and court-docket search, over CourtListener (the Free Law Project's open legal database). Built for legal-aid orgs, tenant-defense and pro-se litigants, and public-interest lawyers who cannot afford Westlaw or PACER.
It wraps CourtListener's REST API v4, normalizing the raw JSON (caseName, dateFiled, cluster_id, docket_absolute_url, and so on) instead of passing the envelope through; see the field map below.
Tools
Tool | Arguments | Returns |
|
| Full-text case-law search ( |
|
| Docket search ( |
|
| Courts and their ids ( |
|
| Full case by id. A cluster ( |
|
| Verify citations ( |
|
| Judges / people ( |
|
| Every opinion citing a given opinion, via the |
|
| The authorities an opinion relies on (its table of authorities) with a per-authority citation depth, via |
|
| A federal docket's filing history from the RECAP archive: numbered entries, dates, descriptions, archived PACER documents with page counts and availability. Token required. RECAP holds what its users bought from PACER. |
|
| Oral-argument audio search ( |
|
| The recording's Whisper transcript text ( |
order_by for opinion_search is one of relevance (default), newest, oldest, most_cited. Court jurisdiction codes include F (federal appellate and other), FD (federal district), FB (bankruptcy), S (state), SA (state appellate), SS (state supreme).
Related MCP server: courtlistener-mcp
Data source
Base URL:
https://www.courtlistener.com/api/rest/v4Auth: a free API token, sent as the header
Authorization: Token <token>.Envelope: search and list endpoints return the DRF shape
{ count, next, previous, results: [...] }./search/and/people/paginate by opaquecursor;/courts/paginates by page number (?page=N). Detail endpoints return a bare object.POST /citation-lookup/returns a bare JSON array (one item per citation recognized in the text).Access:
/search/,/courts/,/people/, and/audio/{id}/answer without a token at a low rate limit, so those tools attach the token only when it is set (a token raises the limit)./clusters/{id}/,/opinions/{id}/,/opinions-cited/,/docket-entries/, andPOST /citation-lookup/return HTTP 401 without a token, socase_detail,citation_lookup,case_authorities, anddocket_entriesrequire one. The/docket-entries/filter parameter isdocket, notdocket_id(the API answers 400unknown_paramsotherwise; found live).Citation-lookup limits (server-side):
textmax 64,000 characters, counted after surrounding whitespace is stripped, since the serializer strips before it validates (enforced pre-flight here with a clear error; the tool never truncates, since a dropped tail would mean unchecked citations); the first 250 citations per call are looked up and any beyond that come back flagged per-item as not checked; rate limit 60 citations/min.
Sources:
REST API v4 overview and auth: https://www.courtlistener.com/help/api/rest/ (redirects to https://wiki.free.law/c/courtlistener/help/api/rest/v4/overview)
Search API (params,
typeenum, response fields): https://www.courtlistener.com/help/api/rest/search/ (redirects to https://wiki.free.law/c/courtlistener/help/api/rest/v4/search)Case Law API (clusters, opinions): https://www.courtlistener.com/help/api/rest/case-law/ (redirects to https://wiki.free.law/c/courtlistener/help/api/rest/v4/case-law)
Citation Lookup API: https://www.courtlistener.com/help/api/rest/citation-lookup/ (redirects to https://wiki.free.law/c/courtlistener/help/api/rest/v4/citation-lookup), plus the endpoint's source in the CourtListener repo:
cl/citations/api_views.pyandapi_serializers.py(request/response fields, per-citation status codes) andcl/settings/project/citations.py(the 250-citations-per-request cap). The endpoint returns HTTP 401 without a token.The live API itself, for the search / courts / people field names:
/search/?type=o,/search/?type=r,/courts/,/people/(all answer unauthenticated GETs).
Field map (CourtListener to normalized output)
CourtListener field | Normalized field | Where |
|
| opinion_search, docket_lookup |
|
| search hits |
|
| search hits |
|
| opinion_search |
|
| search hits |
|
| opinion_search |
|
| opinion_search, cited_by |
|
| search hits |
|
| search hits |
|
| docket_lookup |
|
| court_list |
|
| case_detail (cluster) |
|
| case_detail (cluster) |
|
| case_detail (opinion) |
| same names; the two offsets are re-anchored onto the caller's own string | citation_lookup (per citation) |
|
| citation_lookup (per citation) |
|
| citation_lookup (per citation) |
|
| cited_by |
|
| case_authorities |
| same names; | docket_entries |
|
| oral_arguments |
|
| oral_arguments |
|
| oral_argument_transcript, oral_arguments |
|
| oral_argument_transcript |
|
| oral_argument_transcript |
Install
Bundle (.mcpb). For a client that installs MCP bundles, build one and open it. No JSON to edit, and the API token is an optional field in the install dialog:
npm ci && npm run mcpb:pack # writes build/mcp-courtwatch-<version>.mcpbThe bundle is self-contained (manifest, dist/, and the one runtime dependency) and runs node dist/index.js. npm run verify:mcpb packs it, unpacks it, launches it through the manifest's own mcp_config with no token in the environment, and checks that the tools it serves match the ones the manifest declares. CI runs that on every push.
npm. Nothing to clone. Point your MCP client at it and npm fetches it on first run:
{
"mcpServers": {
"courtwatch": {
"command": "npx",
"args": ["-y", "@haksanlulz/mcp-courtwatch"],
"env": { "COURTLISTENER_API_TOKEN": "your-courtlistener-token" }
}
}
}git clone https://github.com/haksanlulz/mcp-courtwatch
cd mcp-courtwatch
npm install
npm run build # emits dist/; the published bin is dist/index.jsnpm start runs the TypeScript directly via tsx without building.
API token
case_detail, citation_lookup, case_authorities, and docket_entries need a free CourtListener token, and the other tools run faster (higher rate limit) with one. Create a free account, open Profile then the API page, and copy the token. Docs: https://www.courtlistener.com/help/api/rest/
Expose it as COURTLISTENER_API_TOKEN:
export COURTLISTENER_API_TOKEN=your-token-here # macOS / Linux
setx COURTLISTENER_API_TOKEN your-token-here # Windows (new shells)Without the token, opinion_search, docket_lookup, court_list, judge_lookup, cited_by, oral_arguments, and oral_argument_transcript still work at CourtListener's unauthenticated rate limit. The token-gated tools return a clear error telling you to set the token. The token is never logged.
New-account rate limit, and it has three parts. Fresh CourtListener accounts are throttled at 5 requests/minute (it rises as the account ages); set COURTWATCH_THROTTLE_MS=15000 in the server's env to pace under that; the default spacing is 200ms.
Pacing doesn't buy you the other two. There is also a fixed hourly ceiling and a fixed daily one, and no throttle value spends under either: HTTP 429 ... Rate limit exceeded: 50/hour and HTTP 429 ... Rate limit exceeded: 125/day, both observed live on this repo's own token on 2026-09-14 while correctly paced at 15000ms. The 429 body names the seconds until reset, counting down. A single request against the daily ceiling that day answered {"detail":"Request was throttled. Rate limit exceeded: 125/day. Expected available in 46707 seconds."}, about 13 hours.
npm run smoke makes twelve requests at minimum, eleven checks plus one to resolve a docket id, and up to two more when the docket_entries rung walks past a docket RECAP holds nothing for. So a single run spends a meaningful share of the hour's budget and roughly a tenth of the day's: a second run inside the same hour reports FAIL on whatever the hourly ceiling refuses, and after about ten runs in a day the rung is blocked until the daily one resets. A 429 is not retried when its stated wait is longer than RETRY_WINDOW_MS (retrying would spend two more requests of a budget CourtListener has already said is gone). RETRY_WINDOW_MS is how far into the retry loop the last attempt starts: it walks RETRY_BACKOFF_MS = [500, 2000] under the same deadline test withRetry uses, so with the default three attempts (CL_HTTP_ATTEMPTS) it is 2,500 ms. It is not RETRY_DEADLINE_MS (40,000 ms), which only bounds how late an attempt may start. A 429 that names a wait of 2.5 seconds or less, or names no wait, still gets its three attempts.
There is no workaround but waiting it out, or an account old enough to have higher limits.
Example
Call opinion_search with { "q": "warrantless search", "court": "scotus", "order_by": "most_cited", "limit": 1 } (output captured live, 2026-09-14; counts drift as CourtListener grows):
{
"query": { "q": "warrantless search", "court": "scotus", "filed_after": null, "filed_before": null, "order_by": "most_cited", "cursor": null },
"total_matches": 282,
"returned": 1,
"dropped_from_this_page": 19,
"next_cursor": "cz01MTM2JnM9MTA4NTcxJnQ9byZkPTIwMjYtMDktMTQmcD0y",
"note": "limit kept 1 of the 20 rows CourtListener sent for this page. next_cursor begins at the next page, so paging with it skips the 19 dropped row(s) — raise limit to 20 when you intend to page.",
"results": [
{
"case_name": "Monell v. New York City Dept. of Social Servs.",
"court": "Supreme Court of the United States",
"court_id": "scotus",
"date_filed": "1978-06-06",
"citations": ["56 L. Ed. 2d 611", "98 S. Ct. 2018", "436 U.S. 658", "1978 U.S. LEXIS 100", "16 Empl. Prac. Dec. (CCH) 8345", "17 Fair Empl. Prac. Cas. (BNA) 873"],
"docket_number": "75-1914",
"cite_count": 42979,
"status": "Published",
"snippet": "\n436 U.S. 658 (1978)\nMONELL ET AL.\nv.\nDEPARTMENT OF SOCIAL SERVICES OF THE CITY OF NEW YORK ET AL.\nNo. 75-1914.\nSupreme Court of the United States. ...",
"opinions": [{ "id": 109881, "type": "combined-opinion" }],
"cluster_id": 109881,
"docket_id": 266243,
"absolute_url": "https://www.courtlistener.com/opinion/109881/monell-v-new-york-city-dept-of-social-servs/"
}
],
"disclaimer": "Raw public court records from CourtListener, reproduced as published. This is not legal advice and is not a substitute for a lawyer. Docket entries record filings, not rulings; the absence of a record is not evidence that nothing happened."
}The disclaimer is attached to every response, in the payload and not only in the tool description, because a model composing an answer has the payload in hand and may no longer be holding the description.
dropped_from_this_page and the note are why limit: 1 is a bad way to page. /search/ serves one fixed ~20-row page and ignores page_size, so limit slices client-side while next_cursor starts after the whole page, so paging from here would skip the 19 rows the slice dropped. Raise limit to 20 when you intend to page.
Then pass the cluster_id to case_detail ({ "id": 109881 }) for the citations, judges, and opinion ids, or case_detail with { "id": <opinion id>, "type": "opinion" } for the full opinion text. Both need a token. Without one, opinions[0].id from the hit above goes straight to cited_by. For the next page, pass next_cursor back as cursor.
Example: verifying citations before filing
Courts have sanctioned filings built on citations that do not exist. Run a draft's citations through citation_lookup before filing.
Call citation_lookup with { "text": "Tenants are protected here. See Roe v. Wade, 410 U.S. 113 (1973); Smith v. Imaginary, 999 U.S. 9999 (2099)." }. The output below is illustrative of the response shape (the live smoke runs this exact real-plus-fabricated check and fails unless the real one resolves and the fake flags NOT_FOUND):
{
"query": { "text_chars": 107 },
"citations_checked": 2,
"found": 1,
"not_found": 1,
"invalid": 0,
"not_checked": 0,
"all_verified": false,
"coverage_note": "Only citations in recognized reporter formats are checked. A citation-like string with an unrecognized or invented reporter is invisible to this check — it is not counted and not flagged. all_verified covers recognized citations only.",
"warning": "1 of 2 citation(s) did NOT verify: 1 not found in CourtListener (likely fabricated or mis-cited). Do not cite unverified authorities — check them by hand before filing.",
"results": [
{
"citation": "410 U.S. 113",
"verified": true,
"verdict": "FOUND",
"status": 200,
"error_message": null,
"normalized_citations": ["410 U.S. 113"],
"start_index": 45,
"end_index": 57,
"matches": [
{
"cluster_id": 108713,
"case_name": "Roe v. Wade",
"date_filed": "1973-01-22",
"citations": ["410 U.S. 113", "93 S. Ct. 705", "35 L. Ed. 2d 147"],
"precedential_status": "Published",
"citation_count": 12030,
"judges": "Blackmun",
"docket_id": 4463,
"absolute_url": "https://www.courtlistener.com/opinion/108713/roe-v-wade/"
}
]
},
{
"citation": "999 U.S. 9999",
"verified": false,
"verdict": "NOT_FOUND",
"status": 404,
"error_message": "Citation not found: '999 U.S. 9999'",
"normalized_citations": ["999 U.S. 9999"],
"start_index": 86,
"end_index": 99,
"matches": []
}
],
"disclaimer": "Raw public court records from CourtListener, reproduced as published. This is not legal advice and is not a substitute for a lawyer. Docket entries record filings, not rulings; the absence of a record is not evidence that nothing happened."
}The fabricated citation comes back NOT_FOUND with a top-level warning. Per-citation status mirrors the API's own codes: 200 found, 300 found with multiple matching clusters (FOUND_MULTIPLE: a real citation, ambiguous mapping), 400 unknown reporter, 404 not found, 429 past the 250-citations-per-call cap (NOT_CHECKED_OVER_CAP: split the text and re-run the rest). A lookup that recognizes zero citations says so in a note instead of pretending to have verified anything.
The blind spot to know about: the extractor can only flag what it can recognize. 999 U.S. 9999 is caught (real reporter, fake volume: NOT_FOUND), but a cite with an invented reporter (live example 999 A.D.9th 999) isn't recognized as a citation at all, so it is neither counted nor flagged. Every response carries a coverage_note stating this; treat all_verified as covering recognized citations only.
Worked example: is this tenant case still good law?
A caseworker is writing an intake note on a habitability defense in New York and needs the leading case plus some sign it is still being relied on. No token, no account, three tool calls. Every figure below was captured live on 2026-09-14 and will drift as CourtListener grows.
1. Find the leading case. opinion_search with { "q": "warranty of habitability eviction", "court": "ny", "order_by": "most_cited" } → 8 results. The fields that matter from the first one:
Field | Value |
| Park West Management Corp. v. Mitchell |
| 1979-06-07 |
| 393 |
| 5683523 |
| 5532217 ( |
cite_count is how many opinions cite it, which is why it sorts first under most_cited.
2. Take the opinion id, not the cluster id. cited_by wants an opinion id, and opinions[0].id above is it. case_detail would also give you one, in sub_opinion_ids, but that endpoint needs a token; the search hit does not.
3. Ask who cites it. cited_by with { "opinion_id": 5532217 } → total_citing 174, newest first:
Field | Value |
| 174 |
| Fiondella v. 345 W. 70th Tenants Corp. |
| 2023-06-13 |
| nyappdiv |
What a caseworker can paste into an intake note:
The leading New York case on the warranty of habitability is Park West Management Corp. v. Mitchell, decided 1979-06-07. As of 2026-09-14, CourtListener records 174 opinions citing it, the most recent being Fiondella v. 345 W. 70th Tenants Corp. (App. Div., 2023-06-13). That means the case is still being engaged with by New York courts. It does not mean those 174 opinions agreed with it:
cited_byreports who cites a case, never whether they followed, distinguished, or criticized it. Read the citing opinions before relying on any of them, and confirm current law with a lawyer.
That last caveat is not decoration. It rides every cited_by response as a note, because the difference between "cited 174 times" and "good law 174 times" is the whole of a citator's value.
Caveats and verification state
Every tool has been run live against the real API with a real token: npm run smoke, 11/11, 2026-09-14 at COURTWATCH_THROTTLE_MS=15000. That is one real call per tool, all eleven of them since that day. oral_argument_transcript got its own check then and passes on audio 102928 (COMPLETE, 98,073 characters on the record); until then it was verified only by hand, against audio 106247 and 106047, byte-compared with a direct read of /audio/<id>/. A second run inside the same hour will not repeat that result on a new token: the hourly ceiling refuses the rest (and after about ten runs in a day, the daily one does), and those checks read FAIL with the 429, never green (see the rate-limit note in the token section). Two contract facts were only discoverable live and are baked in: the /docket-entries/ filter parameter is docket (an unauthenticated probe cannot see this, since auth is checked before params), and new-account rate limiting is 5 requests/minute (see the token section). Standing caveats that are properties of the API, not gaps in verification:
The citation checker has a structural blind spot, named in every payload. It can only check citations whose reporter it recognizes. A fabricated cite with an invented reporter (live example:
999 A.D.9th 999) is not recognized, not counted, and not flagged, soall_verified: truemeans "every recognized citation resolved" and never "nothing in this text is fake." Everycitation_lookupresponse carries acoverage_notesaying exactly this.The clusters returned by
citation_lookupdo not include the court (in CourtListener's model the court hangs off the docket, not the cluster). For the court, follow the match'sabsolute_urlor pass itscluster_idtocase_detail. Deliberately not auto-fetched: a 250-citation brief would fan out into hundreds of extra docket calls.A
docket_numberargument is sent through the fieldeddocketNumber:"..."search operator (live: 6 matches where the free-text form matched thousands). Combine withqfor case-name context when a docket number alone under-returns.court_listwith a name filter walks the full courts table one page at a time (the/courts/endpoint ignorespage_size; ~3,400 courts over ~170 pages). A complete walk is cached in-process for 24 hours, so it happens at most once per day per server process; scope byjurisdictionto avoid it entirely. On a 5-req/min account, a full walk can't finish inside one client timeout; filter by jurisdiction until the account limit rises.opinion_search,docket_lookup,cited_by, andoral_argumentsreturn one fixed/search/page of ~20 results; for more, passnext_cursorback ascursor. The endpoint ignorespage_size, solimitcaps at 20 rather than advertising an unreachable number.A
limitbelow the page size drops rows thatnext_cursorthen skips past. The cursor comes from the envelope, so it begins after the whole page, not after the last row returned:limit: 3against a 20-row page returns 3 and a cursor starting at row 21. Those responses carrydropped_from_this_pageand say so innote. Leavelimitat 20 when you intend to page.judge_lookupis the same shape one endpoint over and caps at 20 for the same reason:/people/?name_last=Smithand the same query withpage_size=50both return 20 rows with anext(live 2026-09-14). It exposes no cursor, so when more people match than one page holds the response carriesmore_available: trueand a note.returnedis that page, never the total. Narrow withname_first./docket-entries/and/opinions-cited/use v4 cursor pagination, which DEFERS the total count instead of omitting it:countarrives as a URL string pointing at the same query with?count=on, sototal_entries/total_authoritiescome backnullwithtotal_reported: false. That means "not reported here", never zero. Re-request the endpoint with?count=onfor a real total. (Verified live 2026-09-14 on/audio/?stt_status=3:countwas the literal?count=onURL, and that URL returned{"count": 708}.)Oral-argument transcripts are machine speech-to-text, and CourtListener says so per record.
stt_statusis an enum, not a flag, and its failure values are not rare: of 103,278 audio records on 2026-09-14, 708 were status 3 (TRANSCRIPTION_DOES_NOT_MATCH_AUDIO, a Whisper hallucination), 154 status 4 (file over the 25 MB limit), 77 status 5 (no audio file), 46 status 2 (failed) and 16 status 0 (not yet transcribed).oral_argument_transcriptmaps the enum to a namedstt_verdictand returns text only for a completed transcription; a status-3 record still carries text and it's withheld instead of presented as a transcript (audio 106047 holds 9,450 characters that are the case caption repeated over and over). Every response carriesprovenancesaying the text is Whisper output, not a court reporter's certified transcript. Enum names and numbers from the vendor source,cl/audio/models.py.Transcripts are too long to ride a search result, so they are paged: a 6,385-second SCOTUS argument (audio 104586) is 101,707 characters, a 1,051-second circuit argument (audio 106247) is 13,974 (both measured live 2026-09-14).
max_charsdefaults to 15,000 and caps at 50,000; a truncated page reportstruncated: trueand anext_offsetto resume from.oral_arguments'transcript_statusisNOT_CHECKEDunless you passinclude_transcript_status, which costs one extra API request per result row. The search index does not carrystt_status, and/audio/has no batch id filter:?id__in=is rejected as an unknown parameter (the vendor'sAudioFiltergivesidtheINTEGER_LOOKUPSset, which has noin). On a 5-request/minute account, lowerlimitbefore turning it on. A check that fails readsCHECK_FAILED, which means not looked up, never that no transcript exists.
Testing
npm test # offline: vitest, fetch mocked with the documented response shapes (no token needed)
npm run smoke # live: one real call per tool (needs COURTLISTENER_API_TOKEN; skips cleanly without)
npm run typecheck
npm run verify:pack # packs, installs into a temp project, spawns the bin shim over real stdio
npm run verify:mcpb # packs the .mcpb, unpacks it, launches it through the manifest's own mcp_configTwo tiers, split by script rather than by marker. npm test is the offline suite; CI runs it plus npm run typecheck, npm run build, npm run verify:pack and npm run verify:mcpb (ci.yml jobs test, package, consume). npm run smoke is the live upstream contract, token-gated, and not run in CI.
Counts as of 2026-09-25: 224 tests in 4 files (npm test), 2919 lines of app source and packaging scripts, 2678 lines of test source.
find . -path ./node_modules -prune -o -path ./dist -prune -o -path ./build -prune -o -path ./test -prune -o \( -name '*.ts' -o -name '*.mjs' \) -print | grep -v smoke.ts | xargs wc -l
find test -name '*.test.ts' | xargs wc -lWhat the tests cover, by layer. test/server.test.ts drives every tool through a real MCP client over an in-memory transport with fetch stubbed, and asserts the outgoing request (path, query params, Authorization header, POST body) and the normalized response shape, plus the retry policy (5xx and 429 retried three times, 4xx and non-JSON not retried, and two exceptions that exist to stop wasting a nonprofit's budget: a 5xx whose DRF detail says review your query is the search parser refusing the query, and a 429 that names a wait longer than RETRY_WINDOW_MS), the response cache, the argument validators that must fail before any network call, and the two shapes that must never be confused with each other, an empty results array (a real answer) versus a body with no results array (an error), which is also pinned as never being cached. The validator tests are table-driven over the population, not one example: every tool that takes an id, against every way the guard can be wrong; every tool that takes an order_by, against the keys a plain object inherits from Object.prototype; every search tool's free-text q, against a trailing backslash. test/throttle.test.ts measures the outbound throttle against a fake clock, asserting call-START timestamps, and pins that an empty or unparseable COURTWATCH_THROTTLE_MS falls back to the 200ms default instead of to no throttle at all (Number("") is 0). test/env-validation.test.ts re-imports the server with a garbage CL_HTTP_ATTEMPTS / CL_CACHE_TTL_MS / CL_CACHE_MAX and asserts each falls back instead of poisoning arithmetic. test/no-http-stack.test.ts pins the dependency surface. The live smoke checks each tool once against the real API.
The offline suite runs with COURTWATCH_THROTTLE_MS=0 (vitest.config.ts): fetch is stubbed, so paying the real 200ms outbound gap bought nothing but 14 seconds of sleeping.
Every fix was mutation-probed: the code was put back to the defect and the fix's own tests had to go red, while neighbouring tests that should not care stayed green. The live smoke's verdict path was fault-injected the same way, and exits 0 on pass, 1 on failure and 2 when nothing could be measured. The per-probe record is in GAUNTLET.md, under Mutation-probe record.
The 60 toHaveBeenCalled* assertions (counted 2026-09-25 with grep -o 'toHaveBeenCalled[A-Za-z]*' test/*.ts | wc -l) each pin a named contract (no request leaves on a validation or missing-token error, retry count, cache hit, two-page walk, one status check per row). Policy: assert behavior and payloads, never merely that a function was called.
AI assistance
This project was built with AI assistance (Claude). Correctness was established by the test suite and typecheck (npm test, npm run typecheck): every tool is driven through a real MCP client over an in-memory transport with fetch stubbed to the documented CourtListener response shapes, and the unauthenticated search / courts / people surfaces were additionally checked against the live API. As of 2026-08-23 every tool, including the token-gated four, has additionally been verified live with a real token (npm run smoke, plus persona-driven scenario probes that surfaced and fixed the docket filter-param and fake-reporter-coverage findings). The author is accountable for what ships here.
License
MIT. See LICENSE. Data from CourtListener / the Free Law Project (public court records and openly licensed legal data). Unofficial, not affiliated with CourtListener or the Free Law Project.
Available Tools
10 toolscase_authoritiesA
The reverse of cited_by: every authority a given opinion RELIES ON (its table of authorities), with a depth count of how many times each is cited. Pass the citing opinion's id. Requires COURTLISTENER_API_TOKEN (authentication-only endpoint). Returns cited opinion ids; fetch interesting ones with case_detail (type opinion) or their clusters.
Inputs: opinion_id (required), limit.
case_detailA
Fetch a full case by id: a cluster (the case: name, citations, date, judges, and its opinion ids) or a single opinion (its full text). Requires COURTLISTENER_API_TOKEN (these endpoints are authentication-only). Use the cluster_id from opinion_search results.
Inputs: id (required), type.
citation_lookupA
Verify legal citations against CourtListener's database of real cases before relying on them; catches fabricated or mangled citations. Pass free text (a brief, memo, or draft) or a single citation string; every citation recognized in the text is checked. Per citation: FOUND (with case name, date, and link) or an explicit NOT_FOUND / UNKNOWN_REPORTER flag. Requires COURTLISTENER_API_TOKEN (authentication-only endpoint). Caps: 64000 characters of text and 250 citations per call (server rate limit: 60 citations/min).
Inputs: text (required).
cited_byA
Every opinion that CITES a given opinion — the free version of a citator check ('is this case still being relied on, and by whom'). Pass an opinion id (from case_detail's sub_opinion_ids, or an opinion_search hit's cluster via case_detail). Returns citing opinions newest-first or most-cited-first with the same fields as opinion_search. Works without a token. NOTE: this reports who cites the case; it does NOT classify the treatment (followed/distinguished/overruled) — read the citing opinions.
Inputs: opinion_id (required), limit, cursor, order_by.
court_listA
List CourtListener courts and their ids (the values used as the court filter in opinion_search / docket_lookup). Optionally filter by jurisdiction code and/or a name substring. Jurisdiction codes include "F" (federal appellate/other), "FD" (federal district), "FB" (bankruptcy), "S" (state), "SA" (state appellate), "SS" (state supreme). Works without a token.
Inputs: q, limit, jurisdiction.
docket_entriesA
The actual filing history of a federal docket from the RECAP archive: numbered entries, dates, descriptions, and any archived PACER documents (with page counts and availability). Pass a docket_id from docket_lookup. Requires COURTLISTENER_API_TOKEN (authentication-only endpoint). Coverage note: RECAP holds what its users have bought from PACER — an entry or document not present may still exist on PACER.
Inputs: docket_id (required), limit, cursor.
docket_lookupA
Search court dockets (CourtListener type=r) by case name, free text, and/or docket number, optionally scoped to a court. Returns case name, court, docket number, filed/terminated dates, nature of suit, and a link. Works without a token.
Inputs: q, court, limit, cursor, +1 more.
judge_lookupA
Look up judges / people in CourtListener's judiciary database (type via /people/) by last and/or first name. Returns id, assembled name, birth/death dates and place, gender, and how many positions are on file. Works without a token.
Inputs: limit, name_last, name_first.
opinion_searchA
Full-text search of U.S. case law / court opinions (CourtListener type=o). Returns the top matching page (up to limit) with case name, court, date filed, citations, docket number, a snippet, citation count, and a link. Works without a token; set COURTLISTENER_API_TOKEN for a higher rate limit.
Inputs: q (required), court, limit, cursor, +3 more.
oral_argumentsA
Search oral-argument audio recordings (CourtListener type=oa): case name, court, argue date, judges on the panel, duration, and an MP3 download link. Useful for hearing how an issue was actually argued. Works without a token.
Inputs: q (required), court, limit, cursor, +3 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 10 tools
Each tool targets a distinct CourtListener data type (opinions, dockets, courts, citations, judges, oral arguments), so there is little overlap in primary purpose. The only potentially confusing pair is cited_by vs case_authorities—both take an opinion id and concern citation networks—but their descriptions explicitly frame them as reverse directions.
Five tools follow a clean resource_action pattern (opinion_search, docket_lookup, court_list, citation_lookup, judge_lookup), but the other five diverge into noun phrases or other forms (case_detail, cited_by, case_authorities, docket_entries, oral_arguments). The mix is readable but not consistent.
Ten tools is right-sized for a legal research server: opinion, docket, court, citation, judge, and oral-argument functionality each have dedicated tools without redundancy. The count matches the breadth of the CourtListener API surface without feeling bloated.
Core research workflows are covered end-to-end: find opinions, retrieve full text, follow the citation graph in both directions, verify citations, search dockets, and view docket histories. Minor gaps remain—no retrieval of docket entry document content (PACER) and no citator treatment classification—but these are documented limitations.
Maintenance
Related MCP Connectors
MCP for CourtListener: US federal and state opinions, dockets, judges, plus eCFR regulations.
CourtListener MCP — Free Law Project's CourtListener API (v4).
Search US court opinions, federal dockets, judges, citations, and oral arguments via CourtListener.
Related MCP Servers
- AlicenseAqualityDmaintenanceMCP Server providing AI agents access to over 3 million US court decisions via CourtListener and EU legislation via EUR-Lex, with tools for searching cases, citations, judges, and court information.621 PyPIMIT
- AlicenseNot gradedqualityDmaintenanceAn MCP server providing AI agents access to CourtListener's comprehensive legal database, featuring semantic search, hybrid search, citation verification, and research tools.7MIT
- AlicenseNot gradedqualityDmaintenanceA remotely callable MCP server for US legal research that provides tools to search, retrieve, and analyze US case law from the CourtListener API, enabling agents to build evidence packs from primary sources without generating legal content.MIT
- AlicenseAqualityCmaintenanceAn MCP server that provides AI assistants access to the CourtListener legal database (US court opinions, dockets, RECAP, PACER, oral arguments, judges) and the full eCFR federal regulations via the official CourtListener API v4. It supports bring-your-own-key authentication and is designed for use with MCP-compatible clients like Claude, VS Code, and Cursor.344MIT