@cyanheads/courtlistener-mcp-server
Search and retrieve US court opinions, federal dockets, judge records, citation networks, and oral arguments from CourtListener's 9M+ opinion corpus via MCP. STDIO or Streamable HTTP.
14 Tools • 2 Resources • 1 Prompt
Overview
US case law and federal court records from CourtListener's 9M+ opinion corpus and its RECAP mirror of PACER. Search opinions and dockets, trace citation networks, resolve citations, look up judges and courts, and read oral argument transcripts and judicial financial disclosures.
Tools
Resources
The jurisdiction codes are also listed on the jurisdiction parameter of courtlistener_lookup_courts, for clients that don't surface resources.
Prompts
Capability reference
Result-page sizes accept positive integers and bound outgoing requests: 20 for searches/citations, 10 for parties, and 50 for docket entries. Both response surfaces carry page_sizing (requested, bounded, actual, upstream). Upstream search/disclosure pages have a 20-row minimum, parties/entries use fixed 20-row pages, and citing slices cited IDs before searching. Returned pages are preserved; empty/final pages and page-local filters can produce fewer rows. Continue with the native cursor rather than increasing the requested size.
courtlistener_search_opinions tool
q is required and takes CourtListener field syntax (caseName:"roe v wade", court_id:scotus, judge:"Alito") and AND / OR / NOT; filter by court, filed_after / filed_before, and status, and sort with order_by (score desc default, dateFiled desc, dateFiled asc, citeCount desc); 20 results per page, continued with next_cursor
- Each result carries
cluster_id (for courtlistener_get_opinion and courtlistener_get_citations), docket_id, cite_count, a snippet, and opinions[] variants with opinion ID, type, author_id, and local_path; totalCount and effectiveQuery report the match
- Omit
status for CourtListener's Published default, or select one publication status, including In-chambers and Relating-to.
courtlistener_get_opinion tool
cluster_id is required; after an outline response, sections: ["opinion_<id>", ...] pulls specific variants, and a name the cluster doesn't have fails as unknown_section
- Cluster metadata (case name,
court_id, citations, cite_count, precedential_status, syllabus, posture, docket_id) plus each variant's html_text, plain_text, cites[], download_url, and type_label, the same label courtlistener_search_opinions reports
- Small responses use
kind: "full". Oversized variants or metadata return a bounded outline, then typed page responses through retrieval.next_cursor; repeat the original ID and selection with cursor until null.
- Supplied
plain_text is preserved; markup-only variants gain readable text with paragraphs, tables, lists, footnotes, and decoded entities. html_text retains raw source and renders as fenced text. Search/citation snippets use the same readable normalization.
variants_completion reports whether the bounded source walk finished and its remaining upstream variant cursor. docket_enrichment distinguishes not_linked, resolved, source_fields_absent, and lookup_failed with a sanitized failure category; cancellation propagates.
- Primary continuation remains stable when optional docket enrichment changes. Oversized enrichment has its own
enrichment_retrieval.next_cursor, passed as enrichment_cursor with the same cluster and selection. Keep enrichment chunks only while source_id is unchanged; a changed source restarts that stream. A metadata-only partial response can have a null primary cursor while enrichment continues. Legacy v1 cursors retain their original enrichment-sensitive behavior.
courtlistener_get_citations tool
cluster_id is required; direction is cited_by (default, opinions that cite this one) or citing (opinions it cites); optional court and filed_after filters; up to 20 results per page, continued with cursor
- Rows carry
cluster_id, court_id, date_filed, cite_count, and snippet; for citing, totals count the unfiltered cited-opinion list, and a filtered page can come back empty while next_cursor is still set. source_variants_completion and total_is_partial qualify observed_total; incomplete variants make this a lower bound and omit the exact totalCount trailer.
- For
citing, cursor is a nonnegative safe decimal integer offset (01, +1, 1.0, and 1. all mean 1); malformed offsets fail as invalid_cursor before any request. An offset at or past the end returns an empty page with the source name and unfiltered distinct-opinion total, without searching. cited_by cursors stay opaque.
courtlistener_lookup_citation tool
citation takes one citation or a passage of up to 64,000 characters, and every citation in it resolves; max_court_lookups (default 4, max 20, 0 to skip) caps the docket requests spent on court names
- One
matches[] entry per citation, with status (200 one case, 300 several candidates, 400 unrecognized reporter, 404 no match, 429 past the per-request citation cap) and status_label; each of its clusters[] carries cluster_id, docket_id, court_id, and court_resolution (resolved, no_docket, lookup_failed, over_budget, only the last worth retrying with a larger budget)
- Unresolved citations come back as entries, not errors; only text with no parseable citation fails, as
not_found. CourtListener meters this endpoint per citation submitted, separately from the per-request limits
courtlistener_search_dockets tool
q is required and matches case name, docket number, party, and attorney names; party_name (ANDed with q), court, and filed_after / filed_before narrow it; 20 results per page, continued with next_cursor
- Each docket carries
docket_id, parties, attorneys, firms, suit_nature, assigned_to / referred_to, and up to 3 sample_documents with is_available and a RECAP filepath_local URL; every response adds a coverage_note on RECAP's partial PACER coverage
courtlistener_get_docket tool
docket_id is required; entries come 20 per page (CourtListener ignores entries_page_size), walked with entries_page and top-level next_cursor. Without a document selection, returns docket metadata and entries[]; total_entries is null when upstream supplies no numeric total.
document_ids selects documents from the requested entries page. Selected responses carry kind, selected_documents, source/storage links, and raw plain_text; large records use retrieval.next_cursor, passed as cursor with the same docket ID, entries page, and selection. Listing-only fields are optional in selected responses.
text_status is available, unavailable, not_extracted, or unknown; extracted text can exist independently of is_available. IDs absent from the page fail as document_not_on_page; an entry without a matching docket relation fails as document_ownership_unverified. Selection adds no document-detail requests or page walks.
courtlistener_get_parties tool
docket_id is required; cursor takes the opaque next_cursor token, not a page number; outgoing page_size is bounded to 10 and CourtListener may not honor it
- Each party carries
name, its docket-scoped role, and attorneys[] with name, contact_raw, role_code / role (codes 5–9 mean no longer of record), and date_action; total_parties uses the upstream total when supplied, otherwise counts a single-page result or stays null
attorney_resolution reports resolved, exhausted, or capped lookups, unresolved IDs, and the remaining upstream roster cursor. Resolution stops after five roster pages or when the local request budget blocks a later page; already fetched records are retained. Per-attorney resolution distinguishes a resolved record with empty source fields from not_found after exhaustion and not_fetched after the cap. Upstream roster/variant cursors describe completeness; these tools do not accept them as new retrieval inputs.
courtlistener_search_judges tool
q is required; filter by appointer (the president's last name), court, and political_affiliation (d, r, i, l, g, u); 20 results per page, continued with next_cursor
- Results carry
person_id (for courtlistener_get_judge), political_affiliation and aba_rating as labels rather than codes, and a current_position: the one with no termination date, or the latest-starting when several or none qualify
courtlistener_get_judge tool
person_id is required; positions are fetched up to a page bound, and truncated with positionsShown reports when a long record ran past it
- Positions (judicial, plus non-judicial roles described by
job_title and organization_name), education, political affiliations, ABA ratings, and fjc_id; coded fields keep the raw code beside a label (position_type_label, termination_reason_label, degree_label)
- Dates carry the precision CourtListener recorded in
dob_granularity, dod_granularity, date_start_granularity, and date_termination_granularity (year, month, or day)
courtlistener_lookup_courts tool
- Filter by
jurisdiction (one of 22 CourtListener codes such as F, FD, FB, S, SA; the full table is in courtlistener://reference/courts), status (active default, inactive, or any), and has_opinion_scraper; live records come 20 per page, walked with page
courts[] carry id (the court ID every other tool filters on), citation_string, jurisdiction, and scraper flags; all_matching_court_ids lists every match from a bundled snapshot at no request cost, and is empty when more than 1,000 match, so check all_matching_court_ids_complete
courtlistener_search_oral_arguments tool
q is required and matches case name and transcript text; filter by court and argued_after / argued_before; 20 results per page, continued with next_cursor
- Results carry
audio_id (for courtlistener_get_oral_argument), panel_ids (for courtlistener_get_judge), duration_seconds, snippet, and two MP3 links: download_url at the originating court and local_path, CourtListener's durable copy
courtlistener_get_oral_argument tool
id (an audio_id from a search) is required; sections: ["transcript"] selects the transcript. Selected responses obey the same byte limits.
- Returns
panel_ids, duration_seconds, download_url, docket_id, has_transcript, and transcript; oversized text or metadata uses outline and cursor-driven page responses. The argument date is not on this record; take it from the search result or the docket.
courtlistener_search_financial_disclosures tool
- Filter by
judge_id (a person_id) and year; 20 filings per page, continued with cursor. year filters only the fetched page, because CourtListener has no server-side year filter, so keep paging when a page comes back empty with next_cursor set
- Each filing carries
disclosure_id, report_type, has_been_extracted, is_amended, pdf_url, per-category counts (investments, gifts, debts, positions, reimbursements, agreements, non-investment and spouse income), and itemized gifts
courtlistener_get_financial_disclosure tool
disclosure_id is required; categories selects from investments, debts, positions, reimbursements, non_investment_incomes, spouse_incomes, agreements, and gifts (omit for all)
- Returns filing metadata,
counts, and the requested line-item rows, with coded value and income columns decoded to dollar ranges (N → $250,001 - $500,000); oversized selections use outline and cursor-driven page responses, including when a single row exceeds the budget.
Opinion, audio, disclosure, and selected docket-document retrieval bound each serialized response surface (structuredContent and complete content[]) to 24,000 UTF-8 bytes. Repeat the original identity and selection with cursor: retrieval.next_cursor; docket selections also repeat entries_page. Catalog pages lead automatically to data pages. retrieval.ranges uses JSON Pointer paths and absolute exclusive ranges (rows, code_points, or sections); merge typed rows by absolute index, and concatenate text_chunks by their code-point ranges. Partial rows retain their source identity; omitted text is recoverable through its original field path. Metadata can also be paged. Structured strings remain unescaped; presentation escaping happens after slicing. A changed normalized source returns stale_cursor; restart without cursor. Source-completion cursors remain distinct from these retrieval cursors.
courtlistener://reference/courts resource
- Markdown reference: the 22 jurisdiction codes (rendered from the set
courtlistener_lookup_courts validates against), common court IDs, search type codes, the published free-tier rate limits, and the weekly maintenance window (Thursdays 21:00–23:59 PT)
- Static content, cacheable publicly for 24 hours
skill://courtlistener-guide/SKILL.md resource
- Identifier chains and research gates for case law, RECAP filings, judges, disclosures, and oral arguments
- Also included in plugin installs, npm, Docker, and the desktop bundle as courtlistener-guide
courtlistener_research_topic prompt
- Arguments:
topic required; jurisdiction optional (a court ID such as scotus or ca9); depth optional, overview (default, 3–5 key cases) or deep (adds citation-network traversal and judge lookup)
- Returns one user message laying out the research workflow and a findings template: key cases, precedent trajectory, research gaps
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
CourtListener-specific:
- CourtListener REST API v4: search, opinions and clusters, dockets and entries, parties and attorneys, people and positions, courts, audio, financial disclosures, and citation lookup
- One request queue for the whole process, paced to
COURTLISTENER_RATE_LIMIT_PER_MINUTE and COURTLISTENER_RATE_LIMIT_PER_HOUR, so a burst waits instead of failing; a 429 pauses the queue for every waiting call, and a Retry-After is honored within a 45-second wait budget
- Request cost per call: one for most tools; two for
courtlistener_get_docket; two or more for courtlistener_get_parties and courtlistener_get_judge (one per extra page of a long roster or career); three for courtlistener_get_opinion and courtlistener_get_citations (plus one per extra page of opinion variants); and for courtlistener_lookup_citation, the lookup plus up to max_court_lookups docket requests
- Input that can't succeed (an empty
q, a malformed date, a citation over 64,000 characters, an unknown oral argument section) is rejected before a request is spent
Agent-friendly output:
- Chaining IDs:
cluster_id, docket_id, person_id, audio_id, and disclosure_id appear wherever they feed a follow-up call, with field descriptions naming the tool that takes each
- Typed rate-limit errors:
reason: "rate_limited" with retryAfter in seconds, whether CourtListener returned the 429 or the queue gave up before sending, in which case the message says no request was spent
- Bounded retrieval: opinion, audio, disclosure, and selected docket-document responses preserve small full records and stream oversized selections through an outline and resumable typed pages without truncating source text
- Coverage caveats inline: RECAP
coverage_note, court_resolution on citation lookups, truncated on position history, and notices when a page-local filter empties a page that has more pages behind it
Getting started
Connect to the hosted instance, no install:
Or run it locally:
For other clients, add this to the MCP config file:
Generate a token in your CourtListener account settings. Published free-tier limits are 5 requests per minute, 50 per hour, and 125 per day; limits vary by token tier, and Free Law Project membership raises them. For Docker or a source build, see Running the server.
Configuration
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
-
Clone and install:
-
Build and run:
-
Run checks and tests:
Docker
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/courtlistener-mcp-server. For stdio, use docker run -i --rm -e MCP_TRANSPORT_TYPE=stdio -e COURTLISTENER_API_TOKEN=your-token ghcr.io/cyanheads/courtlistener-mcp-server:latest. To build locally, run docker build -t courtlistener-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catch in tool logic
- Use
ctx.log for request-scoped logging, ctx.state for tenant-scoped storage
- Register new tools, resources, and prompts by importing them in
src/index.ts and adding them to the createApp({ tools, resources, prompts }) arrays
- Wrap CourtListener API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
License
Apache-2.0 — see LICENSE for details.