Skip to main content
Glama
clankercode

mcp-fetch-patents

by clankercode
README.md
# mcp-fetch-patents

**The last patent data tool your agent will ever need.**

Give any AI agent instant access to the entire global patent corpus. One MCP tool call. Throw any patent ID at it — US, EP, WO, JP, CN, KR, AU, CA, NZ, BR, IN — in any format, with or without kind codes, even raw Google Patents URLs. Get back PDF, Markdown, and structured metadata in seconds. Ask for it again? Instant, from cache.

Your agent shouldn't have to know that US patents live on USPTO PPUBS, European ones require EPO OPS OAuth2, and PCT applications need to be scraped from WIPO PatentScope. It shouldn't care that Espacenet has different HTML than CIPO, or that Google Patents needs a headless browser. This server ate all of that complexity so your agent never has to think about it.

```
"Fetch US7654321, EP1234567B1, and WO2024/123456"
  → 3 PDFs, 3 Markdown files, 3 metadata objects — cached forever
```

9 patent sources. 4 PDF-to-Markdown converters with OCR. Two-layer SQLite cache. Automatic retry with exponential backoff. Web search fallback when everything else fails. Zero configuration required — works out of the box with 6 of 9 sources.

---

## Why this exists

Patent data is the most fragmented public dataset on the planet. Every national patent office has its own API, auth scheme, document format, and rate limits. An agent doing patent analysis shouldn't need to know any of that — it should just say "get me this patent" and get it.

**9 patent sources, 1 tool call:**

| Source | Coverage | Auth required? |
|---|---|---|
| USPTO PPUBS | US granted + applications | No (session-based) |
| EPO OPS | EP, WO, 100+ offices via exchange data | Yes (OAuth2) |
| Espacenet | EP + EPO member states | No (scraped) |
| WIPO PatentScope | WO / PCT international | No (scraped) |
| IP Australia | AU patents | No (REST API) |
| CIPO | Canadian patents | No (scraped) |
| Google Patents | All jurisdictions | No |
| Google BigQuery | Bulk patent data | Yes (GCP credentials) |
| Web search fallback | Anything missed | No (DuckDuckGo) / Optional (SerpAPI) |

Sources are tried in priority order. First success wins (unless you set `PATENT_FETCH_ALL_SOURCES=true` to aggregate from all). If every structured source fails, the web search fallback finds the PDF anyway. We really don't like returning empty-handed.

## Quick start

### Install

```bash
# Rust (recommended)
cargo install patent-mcp-server

# or build from source:
cargo build --release --manifest-path src/rust/Cargo.toml

# Python (reference implementation, also works standalone)
pip install patent-mcp-server
```

### Configure your MCP client

Add to your Claude Desktop, Claude Code, Cursor, or Cline config:

```json
{
  "mcpServers": {
    "patents": {
      "command": "patent-mcp-server"
    }
  }
}
```

That's the whole setup. No API keys needed — 6 of 9 sources work without auth (USPTO, Espacenet, WIPO, IP Australia, CIPO, DuckDuckGo). Add keys later to unlock EPO OPS, BigQuery, and SerpAPI. See [docs/api-keys.md](docs/api-keys.md).

For an installed Rust server, secrets can live in `~/.patents.toml`, normal environment variables, or `~/.patents-mcp.env` autoloaded by the server. A repo-local `.env` is only useful when the server is launched from that checkout.

If your MCP client launches stdio servers inside a sandboxed subprocess, use the localhost HTTP transport instead:

```bash
patent-mcp-server serve-http
```

Then configure the client to connect to:

```text
http://127.0.0.1:38473/mcp
```

The repo pins the Python HTTP endpoint to `/mcp`, and the Rust server uses the same default URL. The shared contract today is that URL and localhost-only binding, not byte-for-byte HTTP transport parity.

### Use it

```
"Fetch patents US7654321 and EP1234567B1, then summarize the key claims."
```

The agent calls `fetch_patents`, gets back file paths and metadata, reads the Markdown, and does its thing. You don't configure sources. You don't pick formats. You don't manage cache. It just works.

## Tools

### `fetch_patents`

Fetch one or more patents by ID. Accepts any format — bare numbers, jurisdiction-prefixed, with kind codes, even Google Patents URLs.

**Parameters:**
| Name | Type | Required | Description |
|---|---|---|---|
| `patent_ids` | `string[]` | Yes | Patent IDs in any format |
| `force_refresh` | `bool` | No | Bypass cache, re-fetch from sources |
| `formats` | `string[]` | No | Requested formats (default: `["pdf", "txt", "md"]`) |
| `postprocess_query` | `string` | No | Reserved for v2 post-processing |

**Response:**
```json
{
  "results": [
    {
      "canonical_id": "US7654321",
      "success": true,
      "from_cache": false,
      "files": {
        "pdf": "~/.local/share/patent-cache/patents/US7654321/US7654321.pdf",
        "md": "~/.local/share/patent-cache/patents/US7654321/US7654321.md"
      },
      "metadata": {
        "title": "Method and apparatus for ...",
        "inventors": ["Jane Doe", "John Smith"],
        "assignee": "Acme Corp",
        "filing_date": "2005-03-15",
        "publication_date": "2010-02-02",
        "jurisdiction": "US",
        "doc_type": "patent"
      },
      "sources": [
        {"source": "USPTO", "success": true, "elapsed_ms": 1842}
      ]
    }
  ],
  "summary": {
    "total": 1,
    "success": 1,
    "cached": 0,
    "errors": 0,
    "total_duration_ms": 2105
  }
}
```

### `list_cached_patents`

List all cached patents. No parameters, returns `{patents: [...], count: N}`.

### `get_patent_metadata`

Cache-only metadata lookup — no network calls, instant response.

**Parameters:** `patent_ids: string[]`
**Returns:** `{results: [{patent_id, canonical_id, metadata}]}`

## Search tools (patent-search MCP server)

The search server (`python -m patent_mcp.search`) provides tools for natural language patent search, structured queries, citation chains, and research sessions.

It also supports localhost Streamable HTTP:

```bash
python -m patent_mcp.search serve-http
```

Search startup includes a best-effort browser prewarm only when the optional browser dependencies are installed. It is safe to ignore if unavailable, and it does not guarantee that later search calls reuse the same browser/profile.

### `patent_search_natural`

Search using plain English. The planner expands your description into multiple query variants using keyword/synonym expansion, runs them against Google Patents (browser and/or SerpAPI), and reranks results.

**Parameters:**
| Name | Type | Required | Description |
|---|---|---|---|
| `description` | `string` | Yes | Natural language description of the technology |
| `date_cutoff` | `string` | No | ISO date — only return patents before this date |
| `jurisdictions` | `string[]` | No | Filter by jurisdiction (e.g. `["US", "EP"]`) |
| `session_id` | `string` | No | Auto-save results to a research session |
| `max_results` | `int` | No | Maximum results after ranking (default 25) |
| `backend` | `string` | No | `"browser"`, `"serpapi"`, or `"auto"` (default) |
| `enrich_top_n` | `int` | No | Enrich top N hits with full metadata (default 5) |

### Other search tools

- **`patent_search_structured`** — Expert Boolean query syntax against USPTO, EPO OPS, Google Patents
- **`patent_citation_chain`** — Follow citations forward/backward (depth 1-3)
- **`patent_classification_search`** — Search by IPC/CPC code
- **`patent_family_search`** — Find patent family members across jurisdictions
- **`patent_suggest_queries`** — Generate search strategy without executing
- **`patent_search_profile_login_start`** — Launch headed browser for Google login

### Research sessions

Sessions persist search results across multiple tool calls:
- **`patent_session_create`** — Start a new research session
- **`patent_session_load/list/note/annotate`** — Manage session state
- **`patent_session_export`** — Generate Markdown report

## Patent ID formats

Don't worry about formatting. The canonicalizer has seen it all:

| Input | Canonical form | Jurisdiction |
|---|---|---|
| `US7654321` | `US7654321` | US |
| `US7654321B2` | `US7654321` | US (with kind code) |
| `US20230001234A1` | `US20230001234` | US application |
| `7654321` | `US7654321` | US (inferred) |
| `EP1234567B1` | `EP1234567` | EP |
| `WO2024/123456` | `WO2024/123456` | International (PCT) |
| `JP2023-123456` | `JP2023123456` | Japan |
| `CN202310001234A` | `CN202310001234` | China |
| `KR10-1234567` | `KR10-1234567` | South Korea |
| `AU2023123456` | `AU2023123456` | Australia |
| `CA3123456` | `CA3123456` | Canada |
| `https://patents.google.com/patent/US7654321/en` | `US7654321` | (extracted) |

Also handles NZ, BR, and IN formats. 22+ patterns total. If it looks like a patent number, we'll figure it out.

## How it works

```
Agent                    MCP Server (Rust)
  │                           │
  ├─ fetch_patents ──────────►│
  │                           ├─ cache lookup (SQLite)
  │                           │   HIT? return immediately
  │                           │   MISS?
  │                           │     ├─ try sources in priority order
  │                           │     │   USPTO → EPO → Espacenet → WIPO → ...
  │                           │     ├─ download PDF (async, concurrent)
  │                           │     ├─ convert PDF → Markdown
  │                           │     │   (pymupdf4llm → pdfplumber → pdftotext → marker)
  │                           │     ├─ extract metadata
  │                           │     └─ store in cache
  │◄──────────────────────────┤
  │   files + metadata        │
```

**Cache:** Single global SQLite DB at `~/.local/share/patent-cache/index.db` shared across all projects. Patent files live in `~/.local/share/patent-cache/patents/{CANONICAL_ID}/`. First fetch takes seconds. Every subsequent fetch is instant — even across different repos.

**HTTP transport:** Rust, Python fetch, and Python search can each run on localhost HTTP at the default URL `http://127.0.0.1:38473/mcp` when launched with `serve-http`. Stdio remains the default when you launch a server without `serve-http`. The implementations share the same default URL and localhost-only bind, but this repo does not currently claim wire-level Rust/Python HTTP parity.

**Activity journal:** Each tool call is logged to `.patent-activity.jsonl` in the current working directory (JSONL format). This file is meant to be git-tracked so each project retains a record of what was searched, fetched, and accessed. Disable with `PATENT_ACTIVITY_JOURNAL=""`.

**PDF → Markdown:** Four converter backends tried in order (pymupdf4llm → pdfplumber → pdftotext → marker). If one fails, the next picks up. Tables extracted and merged. OCR via tesseract for scanned patent figures. The output is clean enough for an LLM to read directly.

**Dual implementation, cross-verified:** Rust is the production server — standalone, async, with native HTTP fetchers and retry logic. Python is the reference implementation. 39 cross-implementation parity tests verify both produce identical results for ID canonicalization, source ordering, converter output, and web search queries. When we say they match, we mean it — it's tested.

## Configuration

All config via autoloaded env files, `~/.patents.toml`, or environment variables (env vars take precedence). The Rust server autoloads `~/.patents-mcp.env` and then `.env` from the current working directory before reading `~/.patents.toml` and explicit environment variables.

| Env var | Default | Description |
|---|---|---|
| `PATENT_CACHE_DIR` | `~/.local/share/patent-cache/patents` | Cache directory for patent files |
| `PATENT_CONCURRENCY` | `10` | Max concurrent fetches |
| `PATENT_TIMEOUT` | `30.0` | HTTP timeout (seconds) |
| `PATENT_EPO_KEY` | — | EPO OPS `client_id:client_secret` |
| `PATENT_LENS_KEY` | — | Lens.org API key |
| `PATENT_SERPAPI_KEY` | — | SerpAPI key (web search fallback) |
| `PATENT_BIGQUERY_PROJECT` | — | GCP project for BigQuery source |
| `PATENT_FETCH_ALL_SOURCES` | `true` | Try all sources even after first success |
| `PATENT_ACTIVITY_JOURNAL` | `.patent-activity.jsonl` | Per-repo activity journal (empty = disabled) |
| `PATENT_SEARCH_BACKEND_DEFAULT` | `browser` | Default search backend (`browser`, `serpapi`, `auto`) |
| `PATENT_SEARCH_BROWSER_HEADLESS` | `true` | Run Playwright in headless mode |
| `PATENT_SEARCH_BROWSER_IDLE_TIMEOUT` | `1800` | Browser idle timeout in seconds (default 30 min) |
| `PATENT_SEARCH_BROWSER_MAX_PAGES` | `3` | Max Google Patents result pages per query |
| `PATENT_SEARCH_ENRICH_TOP_N` | `5` | Enrich top N search results with full metadata |

## Development

### Running tests

```bash
# Rust tests (237 tests, <0.1s)
cargo test --manifest-path src/rust/Cargo.toml

# Start the Rust server over localhost HTTP
just serve-rust-http

# Start the Python fetch server over localhost HTTP
just serve-http

# Start the Python search server over localhost HTTP
just serve-search-http

# Direct stdio MCP smoke test against the Rust dev server
just mcp-smoke-rust

# Direct stdio MCP smoke test against the installed Rust binary
just mcp-smoke-rust-installed

# Fast Python unit tests (<1s, all I/O mocked)
pytest tests/python/ -m "not browser and not integration and not slow"

# Full Python suite (includes fuzz tests via Hypothesis, slow tests)
pytest tests/python/

# Cross-implementation parity — verifies Python == Rust (39 tests)
pytest tests/cross_impl/

# Manual E2E tests — automated script (31 tests)
python3 run_manual_e2e.py
```

### Project structure

```
src/
  python/patent_mcp/       # Python reference implementation
    id_canon.py            #   Patent ID canonicalization (22+ formats)
    cache.py               #   Dual-layer SQLite cache
    config.py              #   TOML + env var config loading
    fetchers/
      http.py              #   All HTTP/API source implementations
      web_search.py        #   DuckDuckGo / SerpAPI fallback
      orchestrator.py      #   Priority-ordered source orchestration
    converters/            #   PDF → Markdown (4 backends + OCR)
    server.py              #   MCP server (FastMCP, stdin/stdout)
  rust/                    # Rust production server (standalone)
    src/
      id_canon/            #   Canonicalization (must match Python)
      cache/               #   Two-layer SQLite cache (rusqlite)
      config/              #   TOML + env var config loading
      converters/          #   PDF → Markdown (4 backends + OCR)
      fetchers/
        http/              #   9 native async HTTP sources with retry
        web_search/        #   DuckDuckGo + SerpAPI fallback
        browser.rs         #   Native Google Patents HTML/microdata fetcher
        mod.rs             #   Priority-ordered source orchestrator
      server/              #   JSON-RPC 2.0 over stdin/stdout
tests/
  python/                  # 16 test files: unit, integration, fuzz
  cross_impl/              # Python == Rust parity tests
  fixtures/                # Shared test data
docs/
  api-keys.md              # API key setup for each source
```

## What makes this different

Most patent tools give you an API wrapper for one database. This gives your agent the entire global patent system behind a single function call. It handles the authentication, the format differences, the fallbacks, the caching, and the PDF-to-text conversion. Your agent asks for a patent and gets back something it can read. That's it. That's the product.

## MCP Debugging

If your editor or MCP client seems to be using a stale server process, test the Rust server directly over stdio JSON-RPC instead of guessing whether the client reloaded:

```bash
# Current checkout / dev binary
just mcp-smoke-rust

# Installed binary from ~/.cargo/bin
just mcp-smoke-rust-installed

# Different patent
just mcp-smoke-rust US7654321B2
```

These recipes send `initialize` and `tools/call` requests straight to the server process, so they verify the real binary behavior without depending on Claude/OpenCode reload state.

## License

CC0 1.0 Universal