Skip to main content
Glama
haksanlulz

mcp-courtwatch

by haksanlulz

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

opinion_search

q (required), court, filed_after, filed_before, order_by, limit, cursor

Full-text case-law search (/search/?type=o). Per hit: case name, court, date filed, citations, docket number, snippet, citation count, cluster id, link, and opinions, the {id, type} of each opinion in the case, which is the keyless input to cited_by. Also returns next_cursor (pass it back as cursor for the next page).

docket_lookup

q and/or docket_number (at least one), court, limit, cursor

Docket search (/search/?type=r). Per hit: case name, court, docket number, filed/terminated dates, nature of suit, docket id, link. Also returns next_cursor (pass it back as cursor for the next page).

court_list

jurisdiction, q, limit

Courts and their ids (/courts/), the values used as the court filter above. Optional jurisdiction filter and a name substring filter applied across the full courts table (paged server-side).

case_detail

id (required), type (cluster or opinion)

Full case by id. A cluster (/clusters/{id}/) gives case name, citations, date, judges, and its opinion ids. An opinion (/opinions/{id}/) gives the full opinion text. Requires a token.

citation_lookup

text (required)

Verify citations (POST /citation-lookup/). Pass free text (a brief, a draft) or a single citation string; every citation recognized is checked against the database of real cases. Per citation: FOUND (with matched case name, date, link) or an explicit NOT_FOUND / UNKNOWN_REPORTER flag. Requires a token.

judge_lookup

name_last and/or name_first (at least one), limit

Judges / people (/people/). Per person: id, assembled name, birth and death dates and place, gender, count of positions on file. One page of 20 max; more_available says whether more match.

cited_by

opinion_id (required), order_by, limit, cursor

Every opinion citing a given opinion, via the cites:() search operator. A free citator check (who still relies on this case). Newest-first by default. No treatment classification. Keyless, and the id comes from opinion_search's opinions[].id so the whole chain works without a token (case_detail's sub_opinion_ids is the token-gated alternative).

case_authorities

opinion_id (required), limit, cursor

The authorities an opinion relies on (its table of authorities) with a per-authority citation depth, via /opinions-cited/. Token required. Also returns next_cursor; pass it back as cursor for the next page. /opinions-cited/ defers its count, so the cursor is the only signal that the table is longer than one page.

docket_entries

docket_id (required), limit, cursor

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_arguments

q (required), court, order_by (relevance/newest/oldest), argued_after, argued_before, include_transcript_status, limit, cursor

Oral-argument audio search (type=oa): case, court, argue date, panel judges, duration, audio id, MP3 link, and snippet, an excerpt of the recording's machine-generated Whisper transcript (not a certified transcript), and 708 of CourtListener's audio records are flagged as transcripts that do not match their audio. Each row also carries transcript_status, which reads NOT_CHECKED unless include_transcript_status is set (that costs one extra request per row; see the caveats). Keyless.

oral_argument_transcript

audio_id (required), offset, max_chars

The recording's Whisper transcript text (/audio/{id}/), paged. Returns stt_verdict (CourtListener's own transcription status, mapped), transcript_usable, transcript_chars, truncated, next_offset, and provenance. Only a COMPLETE transcription returns text: one flagged TRANSCRIPTION_DOES_NOT_MATCH_AUDIO has text on the record and it is withheld here. Keyless.

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/v4

  • Auth: 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 opaque cursor; /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/, and POST /citation-lookup/ return HTTP 401 without a token, so case_detail, citation_lookup, case_authorities, and docket_entries require one. The /docket-entries/ filter parameter is docket, not docket_id (the API answers 400 unknown_params otherwise; found live).

  • Citation-lookup limits (server-side): text max 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:

Field map (CourtListener to normalized output)

CourtListener field

Normalized field

Where

caseName (fallback: caseNameFull on opinion hits, case_name_full on docket hits)

case_name

opinion_search, docket_lookup

court_id, court

court_id, court

search hits

dateFiled

date_filed

search hits

citation (array)

citations

opinion_search

docketNumber

docket_number

search hits

opinions[].snippet

snippet

opinion_search

opinions[] (id, type)

opinions (array of {id, type})

opinion_search, cited_by

cluster_id, docket_id

cluster_id, docket_id

search hits

absolute_url / docket_absolute_url

absolute_url (made a full link)

search hits

dateTerminated, suitNature

date_terminated, nature_of_suit

docket_lookup

id, full_name, short_name, jurisdiction, citation_string, url

id, full_name, short_name, jurisdiction, citation_string, website

court_list

citations (array of {volume, reporter, page})

citations (formatted strings)

case_detail (cluster)

sub_opinions (array of URLs)

sub_opinion_ids

case_detail (cluster)

plain_text (fallback html_with_citations)

text (+ text_source, text_truncated)

case_detail (opinion)

citation, normalized_citations, start_index, end_index

same names; the two offsets are re-anchored onto the caller's own string

citation_lookup (per citation)

status (200/300/400/404/429), error_message

status + verdict (FOUND, FOUND_MULTIPLE, UNKNOWN_REPORTER, NOT_FOUND, NOT_CHECKED_OVER_CAP) + verified, error_message

citation_lookup (per citation)

clusters (array of cluster objects)

matches (cluster id, case name, date, citations, link)

citation_lookup (per citation)

count on a cites:() search

total_citing (hits normalized exactly as opinion_search hits)

cited_by

cited_opinion / citing_opinion (resource URLs), depth

cited_opinion_id, citing_opinion_id, depth

case_authorities

entry_number, date_filed, description, recap_documents[] (document_number, page_count, is_available, filepath_local)

same names; description_short falls back to short_description

docket_entries

dateArgued, dateReargued, judge, duration, id, download_url

date_argued, date_reargued, judges, duration_seconds, audio_id, download_url

oral_arguments

snippet (the indexed text for type=oa IS the Whisper transcript, so this is machine speech-to-text, not a certified transcript; live 2026-09-14, audio 102928's 497-character snippet is byte-identical to the opening of its stt_transcript)

snippet

oral_arguments

stt_status (0-5)

stt_verdict (TRANSCRIPTION_NEEDED, COMPLETE, TRANSCRIPTION_FAILED, TRANSCRIPTION_DOES_NOT_MATCH_AUDIO, AUDIO_FILE_TOO_BIG, AUDIO_FILE_MISSING) + transcript_usable; on a search row, transcript_status

oral_argument_transcript, oral_arguments

stt_source (1 / 2)

stt_source (OpenAI API whisper-1 / self-hosted Whisper)

oral_argument_transcript

stt_transcript

text (+ transcript_chars, offset, returned_chars, truncated, next_offset)

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>.mcpb

The 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.js

npm 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 that names a wait longer than the retry deadline is not retried (it would spend two more requests of a budget CourtListener has already said is gone); a 429 that names a short 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

case_name

Park West Management Corp. v. Mitchell

date_filed

1979-06-07

cite_count

393

cluster_id

5683523

opinions[0].id

5532217 (lead-opinion)

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

total_citing

174

results[0].case_name

Fiondella v. 345 W. 70th Tenants Corp.

results[0].date_filed

2023-06-13

results[0].court_id

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_by reports 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, so all_verified: true means "every recognized citation resolved" and never "nothing in this text is fake." Every citation_lookup response carries a coverage_note saying exactly this.

  • The clusters returned by citation_lookup do not include the court (in CourtListener's model the court hangs off the docket, not the cluster). For the court, follow the match's absolute_url or pass its cluster_id to case_detail. Deliberately not auto-fetched: a 250-citation brief would fan out into hundreds of extra docket calls.

  • A docket_number argument is sent through the fielded docketNumber:"..." search operator (live: 6 matches where the free-text form matched thousands). Combine with q for case-name context when a docket number alone under-returns.

  • court_list with a name filter walks the full courts table one page at a time (the /courts/ endpoint ignores page_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 by jurisdiction to 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, and oral_arguments return one fixed /search/ page of ~20 results; for more, pass next_cursor back as cursor. The endpoint ignores page_size, so limit caps at 20 rather than advertising an unreachable number.

  • A limit below the page size drops rows that next_cursor then skips past. The cursor comes from the envelope, so it begins after the whole page, not after the last row returned: limit: 3 against a 20-row page returns 3 and a cursor starting at row 21. Those responses carry dropped_from_this_page and say so in note. Leave limit at 20 when you intend to page.

  • judge_lookup is the same shape one endpoint over and caps at 20 for the same reason: /people/?name_last=Smith and the same query with page_size=50 both return 20 rows with a next (live 2026-09-14). It exposes no cursor, so when more people match than one page holds the response carries more_available: true and a note. returned is that page, never the total. Narrow with name_first.

  • /docket-entries/ and /opinions-cited/ use v4 cursor pagination, which DEFERS the total count instead of omitting it: count arrives as a URL string pointing at the same query with ?count=on, so total_entries / total_authorities come back null with total_reported: false. That means "not reported here", never zero. Re-request the endpoint with ?count=on for a real total. (Verified live 2026-09-14 on /audio/?stt_status=3: count was the literal ?count=on URL, and that URL returned {"count": 708}.)

  • Oral-argument transcripts are machine speech-to-text, and CourtListener says so per record. stt_status is 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_transcript maps the enum to a named stt_verdict and 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 carries provenance saying 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_chars defaults to 15,000 and caps at 50,000; a truncated page reports truncated: true and a next_offset to resume from.

  • oral_arguments' transcript_status is NOT_CHECKED unless you pass include_transcript_status, which costs one extra API request per result row. The search index does not carry stt_status, and /audio/ has no batch id filter: ?id__in= is rejected as an unknown parameter (the vendor's AudioFilter gives id the INTEGER_LOOKUPS set, which has no in). On a 5-request/minute account, lower limit before turning it on. A check that fails reads CHECK_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_config

Two 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-15 (fix round 2): 217 tests in 4 files (npm test), 3143 lines of app source and packaging scripts, 2635 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 -l

What 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 the retry deadline), 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.

Mutation probes (2026-09-14), each run and then reverted: restoring the bare Number(process.env.CL_HTTP_ATTEMPTS ?? 3) read turns 4 env-validation tests red; restoring String(err) in the CallTool catch turns the non-Error test red with expected 'Error: undefined' not to be 'Error: undefined'; restoring the permissive extractResults turns 6 of the 8 envelope tests red while leaving the legitimate-empty test green; neutering throttled() to a pass-through turns both throttle tests red; removing the transcript usability gate turns 8 transcript tests red; and pointing the manifest's entry_point at a file that exists but is not an entry point turns verify:mcpb red with no initialize response. The live smoke's verdict path was probed the same way: with every tool stubbed and opinion_search returning an empty page, it exits 1 reporting 8 passed / 1 failed / 1 skipped, where the earlier version printed two green lines. Its exit codes are 0 pass, 1 failure, 2 unmeasured. Forcing every check to skip exits 2, and exited 0 before that landed, so a run where nothing could resolve its input read to a wrapper exactly like a clean one. Later probes, same method: making stripDanglingEscape a pass-through turns 4 query-sanitizer tests red; restoring the ad-hoc COURTWATCH_THROTTLE_MS read turns 2 red, one on a measured zero gap; dropping the pre-cache envelope validation turns the not-cached test red while the still-cached companion stays green; restoring the truthiness and in checks for order_by turns all 5 inherited-key cases red; weakening the id guard to a null check in cited_by, case_authorities and docket_entries turns exactly those three red and leaves case_detail green. Fix round 2, same method: neutering cleanQuery's empty-query throw turns 4 red; restoring Array.isArray(json) ? json : [] in citation_lookup turns 2 red while the bare-array test that pins its real shape stays green; accepting any object in expectDetail instead of requiring the asked-for id turns 9 red; restoring clampLimit(args.limit, 10) in judge_lookup turns 1 red and dropping its more_available another; inverting droppedNote's guard turns 4 red across the four search tools; making court_list's note claim a complete walk unconditionally turns 1 red; and reporting 1.1.0 from a 1.2.0 bundle turns verify:mcpb red with server reports version 1.1.0, bundle says 1.2.0. The live smoke was fault-injected once more: forcing case_authorities to return zero rows turns that rung FAIL and the run exits 1, where its two previous assertions (both server invariants) stayed green. (An unset token still exits 0: that is the whole suite declining to run, which is how it stays wireable into CI without a secret.) Fix round 1, same method: restoring the bare HANDLERS[name] dispatch turns all 6 inherited-tool-name cases red (before the fix, tools/call with name constructor answered isError: false with the caller's own arguments and the records-only disclaimer attached); dropping the pre-cache expectDetail turns 2 red, on the repeat call that must reach upstream after a 200-served error body; disabling the review your query permanent-error branch turns 6 red; trimming citation_lookup's outgoing text again turns 3 red while the whitespace-only refusals stay green; removing the 429 stated-wait check turns 2 red while the plain-429 retry test stays green; and neutering normDate's calendar check turns 4 red while the real dates, leap day included, stay green. The live smoke's docket_entries rung was fault-injected offline with fetch stubbed: an empty /docket-entries/ page makes the new rung SKIP and the run exit 2 (10 passed, 0 failed, 1 skipped), where the old rung printed ok docket_entries and exited 0 on the same input; renaming entry_number / date_filed / description upstream makes it FAIL and exit 1, where it used to pass. Fix round 2 (2026-09-15), same method: dropping the re-anchoring offset in citation_lookup turns the offset test red, narrowing its leading-whitespace class to a bare \s turns 2 red, and measuring the 64,000-character cap against the untrimmed string turns 1 red; removing the caller-query narrowing on the review your query branch turns 3 red (and disabling that branch entirely still turns the 5 refusal tests red, so it can fail in both directions); dropping case_authorities's outgoing cursor, its schema property, and its conditional note each turn exactly 1 red; and restoring RETRY_DEADLINE_MS as the 429 comparison turns the 30-second and 3-second cases red while the 13-hour and 40-minute ones stay green. Those two exceed both constants, which is why the round-1 tests could not see the bug they were written for.

The 46 toHaveBeenCalled* assertions 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 tools
case_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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax authorities to return (1-50, default 50).
opinion_idYesNumeric OPINION id whose authorities to list.

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It discloses that this is an authentication-only endpoint requiring COURTLISTENER_API_TOKEN, which is important behavioral context. It also explains the output shape at a high level: cited opinion ids with a depth count.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded with the core concept: 'The reverse of cited_by'. Every sentence earns its place, covering the key behavior, required authentication, and downstream next-step guidance without unnecessary filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given low complexity (2 parameters, no output schema, no annotations), the description is complete enough for correct invocation. It specifies the required input, the auth requirement, the return behavior, and even suggests how to fetch fuller details using case_detail or clusters.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents both parameters clearly. The description adds modest reinforcement by explaining that opinion_id is the citing opinion's id and that the result includes depth counts, but it does not add substantial new meaning beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: it returns the authorities an opinion relies on, with a citation depth count. It also explicitly contrasts itself with cited_by, allowing an agent to distinguish it from a closely related sibling.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly implies when to use this tool by labeling it 'the reverse of cited_by' and explaining that the caller passes the citing opinion's id. It gives useful context and names the main alternative, though it does not explicitly state when-not-to-use or provide explicit exclusion conditions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
idYesNumeric cluster id (default) or opinion id.
typeNoWhich resource `id` refers to (default "cluster"). A cluster is the case; an opinion is one document within it.

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description must carry the full burden. It discloses the authentication need and outlines the response structure (cluster vs. opinion fields). However, it lacks details on error behavior (e.g., missing id, invalid type) and does not mention any side effects or rate limits. For a read-only fetch, this is adequate but not thorough.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise: three sentences covering purpose, auth, and usage. No unnecessary words or repetition. It is well front-loaded with the core action.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (two resource types) and no output schema, the description manages expectations well by listing what each resource returns (cluster fields: name, citations, date, judges, opinion ids; opinion: full text). It could mention potential errors, but for a fetch tool, the core information is present.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already covers both parameters with clear descriptions (id: numeric, type: enum). The description adds the context of using cluster_id from search results, but does not significantly enhance the understanding of the parameters themselves. With 100% schema coverage, the baseline is 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Fetch a full case by id' and distinguishes between two resource types: a cluster (the case) with its fields, or a single opinion. This specific verb-resource pairing and the differentiation from other tools like 'opinion_search' makes the purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description tells when to use the tool (to get full details of a case or opinion), mentions the authentication requirement, and even suggests how to obtain the id ('Use the cluster_id from opinion_search results'). It does not explicitly list when not to use it, but the context and sibling tools imply alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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).

ParametersJSON Schema
NameRequiredDescriptionDefault
textYesFree text to scan for citations (max 64000 characters), or a single citation string like "410 U.S. 113". The first 250 citations recognized are looked up; any beyond that are returned flagged NOT_CHECKED_OVER_CAP.

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses the required authentication token, character and citation caps, rate limit, and output flags (FOUND, NOT_FOUND, UNKNOWN_REPORTER, NOT_CHECKED_OVER_CAP). It does not cover error handling for invalid tokens, but the disclosed behavior is sufficient.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, well-structured paragraph of five sentences. It is front-loaded with the primary purpose, followed by usage instructions, then behavioral details and limitations. Every sentence adds value with no redundancy or fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has a single parameter, no output schema, and no annotations, the description covers all necessary aspects: purpose, usage, authentication, limits, and output type. It is complete for an agent to decide when and how to invoke the tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already provides a description for the 'text' parameter, achieving 100% coverage. The description adds significant value by explaining the two input forms (free text or single citation string), the maximum character length, and the citation cap behavior. This goes beyond the schema definition.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: verifying legal citations against CourtListener's database to catch fabricated or mangled citations. It specifies the verb 'verify' and the resource 'legal citations', and distinguishes itself from sibling tools like case_detail or opinion_search.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context on when to use the tool ('before relying on legal citations'), how to use it (pass free text or a single citation string), and operational limits (characters, citations, rate limit). It does not explicitly list when not to use it or mention alternatives, but the purpose is distinct enough.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax results from this page (1-20, default 20).
cursorNoOpaque cursor from a previous response's next_cursor.
order_byNoSort order (default newest).
opinion_idYesNumeric OPINION id (not a cluster id). case_detail on a cluster lists its sub_opinion_ids.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden, and it delivers: sorts are disclosed, token requirements are stated, the lack of treatment classification is explicitly highlighted, and the note to 'read the citing opinions' sets proper expectations. This is unusually transparent for a tool definition.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose, then supplies indispensable usage details, then ends with the critical limitation. Every sentence earns its place, and the caveat is clearly separated with 'NOTE' so an agent does not miss the non-classification behavior.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers ID sourcing, sorting, authentication, output similarity to opinion_search, and important limitations. It relies on 'same fields as opinion_search' to describe the return shape rather than enumerating fields, which is a minor gap given there is no output schema, but the sibling tool is available for reference.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds valuable context beyond the schema by explaining that opinion_id must be a numeric OPINION id and not a cluster id, and by showing where to obtain that ID from related tools. This meaningfully helps an agent pass the correct value.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a clear, specific statement: 'Every opinion that CITES a given opinion.' It also positions this as a citator-check tool, which distinguishes it from related siblings like citation_lookup and clarifies that it focuses on citing opinions, not treatment classification.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives concrete guidance on where to obtain the opinion_id (case_detail's sub_opinion_ids or via opinion_search's cluster through case_detail), clearly distinguishes opinion IDs from cluster IDs, and notes that it works without a token. The explicit caveat that it does NOT classify treatment gives a clear when-not-to-use signal, though it does not name a specific alternative tool for treatment analysis.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
qNoOptional case-insensitive substring matched against court id / full name / short name / citation string (applied to the fetched page).
limitNoMax courts to return (1-50, default 25).
jurisdictionNoOptional jurisdiction code filter (e.g. "F", "FD", "S").

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so description carries full burden. Discloses that it lists courts with ids, supports optional filtering, and requires no authentication. As a read-only list operation, this is sufficiently transparent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Description is concise (three sentences) and well-structured: first sentence states purpose, second explains filters, third explains jurisdiction codes. No filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema, description adequately covers return (list of courts and ids). Covers all three parameters with clear context. Also connects to sibling tools (opinion_search, docket_lookup). Complete for a list tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Adds meaning beyond 100% schema coverage: explains jurisdiction codes in detail, notes that 'q' is case-insensitive and matches multiple fields, and clarifies that limit controls page size. This helps the agent use parameters effectively.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states 'List CourtListener courts and their ids' and explains their use in other tools. Distinguishes from siblings by focusing on listing courts rather than searching opinions, dockets, etc.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says 'Works without a token' and explains optional filters. Implies usage for obtaining court ids before using opinion_search/docket_lookup. Could add explicit when-to-use vs alternatives, but context is clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax entries to return (1-50, default 50).
cursorNoOpaque cursor from a previous response's next_cursor.
docket_idYesNumeric docket id (from docket_lookup results).

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the behavioral disclosure burden. It reveals the authentication-only nature and flags an important data caveat: RECAP holdings depend on what users have purchased on PACER, so absence of an entry/document may not mean it doesn't exist on PACER. It also explains what document metadata is returned. It does not mention rate limits or error behavior, but the key non-obvious behaviors are covered.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, each with a distinct purpose: what the tool returns, how to invoke it (docket_id source and auth), and the data coverage caveat. No repetition or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The definition is self-sufficient for an agent to call the tool correctly: input source, auth requirement, output contents, and data limitations are all described. Pagination is handled by the schema's cursor description, so no gap. The coverage caveat is especially valuable for interpreting results.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so all three parameters are already documented, including docket_id's origin from docket_lookup results. The description adds no new parameter semantics beyond repeating the docket_id source, so the baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly identifies the resource as 'the actual filing history of a federal docket from the RECAP archive' and itemizes the return contents: numbered entries, dates, descriptions, and archived PACER documents with page counts and availability. It also positions itself relative to docket_lookup by stating that a docket_id comes from that tool's results.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It provides concrete usage context: pass a docket_id from docket_lookup, and requires COURTLISTENER_API_TOKEN. It does not explicitly name sibling alternatives or state when not to use this tool, but the workflow cue ('from docket_lookup') makes the intended usage clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
qNoCase name or free-text query (e.g. an employer or agency name).
courtNoOptional court id filter (e.g. "nysd"). Get ids from court_list.
limitNoMax results from this page (1-20, default 20). The /search/ endpoint returns one fixed page of ~20; to get more, pass the response's next_cursor back as cursor.
cursorNoOpaque pagination cursor from a previous response's next_cursor; fetches the next page (keep the other args the same).
docket_numberNoDocket number to match (e.g. "1:20-cv-03590").

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Since no annotations are provided, the description carries full burden. It discloses that the tool works without authentication and returns specific fields. However, it does not mention pagination behavior or any rate limits, which are relevant for a search tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences, front-loading the core purpose and return values. Every sentence contributes value, with no fluff or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the tool's purpose, parameters, and returned fields. However, it omits mention of pagination cursors in the return structure, which are referenced in the parameter description but not in the output summary.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% coverage with descriptions for all 5 parameters. The tool description adds a summary that aligns with parameters but provides no additional meaning beyond the schema definitions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states this tool searches court dockets by case name, free text, or docket number, with optional court scoping. It distinguishes itself from siblings like opinion_search or case_detail by focusing specifically on docket records.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage (search dockets) but does not explicitly state when to use this tool versus siblings like citation_lookup or judge_lookup. No 'when not to use' guidance is provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMax people to return (1-50, default 10).
name_lastNoLast name to match (e.g. "Ginsburg").
name_firstNoFirst name to match (e.g. "Ruth").

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations, but description explicitly lists return fields (id, name, dates, gender, positions count) and states no token required, providing good behavioral insight.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences, front-loaded with purpose and return details, no redundant information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 3-parameter look-up tool with no output schema or annotations, description provides return field info and auth requirement, but lacks error handling or pagination details.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema covers all 3 parameters with descriptions; description adds value by detailing return fields beyond schema, helping agent understand outputs.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool looks up judges/people in CourtListener's judiciary database by name, distinguishing it from siblings that deal with cases, citations, courts, dockets, and opinions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No explicit when-to-use or alternatives, but sibling tools are unrelated so usage is clear. Mentions it works without a token, implying ease of use.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
qYesSearch query (case name, party, or topic).
courtNoOptional court id filter (e.g. "scotus", "ca2"). Get ids from court_list.
limitNoMax results from this page (1-20, default 20).
cursorNoOpaque cursor from a previous response's next_cursor.
order_byNoSort order (default relevance; newest/oldest sort by argue date).
argued_afterNoOptional ISO date (YYYY-MM-DD); only arguments on/after.
argued_beforeNoOptional ISO date (YYYY-MM-DD); only arguments on/before.

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the behavioral burden. It clarifies the operation is a search (read-only), specifies the result fields including the MP3 download link, and discloses that no authentication token is needed. It doesn't mention pagination or rate limits, but for a read-only search tool these are minor omissions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences provide the essential information with zero filler. The primary action and resource are front-loaded, followed by a use case and the auth note, making it easy for an agent to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having no output schema or annotations, the description covers what is searched, what is returned, the intended use case, and authentication requirements. The 100% schema coverage handles parameter details, so nothing critical is missing for an agent to invoke this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so every parameter is already documented with its own description. The tool description does not add parameter-level syntax or clarification beyond restating the domain fields, so the baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource ('Search oral-argument audio recordings'), names the CourtListener type (oa), and enumerates the returned fields (case name, court, argue date, judges, duration, MP3 link). This clearly differentiates it from siblings like opinion_search, which focus on written opinions, and docket_lookup.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives a concrete usage scenario—'hearing how an issue was actually argued'—and notes that it works without a token. It does not explicitly name alternatives or state when not to use it, but the sibling tool list and the framing provide enough context for an agent to route correctly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 4 tool updatesv1.1.0
    • Addedcase_authorities
    • Addedcited_by
    • Addeddocket_entries
    • Addedoral_arguments
  2. 6 tool updatesv1.0.0
    • First observedcase_detail
    • First observedcitation_lookup
    • First observedcourt_list
    • First observeddocket_lookup
    • First observedjudge_lookup
    • First observedopinion_search

TDQS

A4.1/5.0

Scored across 10 tools

Disambiguation4/5

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.

Naming Consistency3/5

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.

Tool Count5/5

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.

Completeness4/5

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

ActivityActive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A 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
  • A
    license
    A
    quality
    D
    maintenance
    An 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.
    34
    3
    MIT