python-docs MCP server
Commit 19 Okt 2026Menyediakan dokumentasi live CPython standard library dan metadata package PyPI dalam format markdown yang bersih untuk AI agent.
Juga dikenal sebagai KEEPEE/python-docs-mcp
- Stars
- 0
- Tools
- 5
- Unduhan, 30 hari
- —
- Versi terbaru
- —
- Commit terakhir
- 19 Okt 2026
- Lisensi
- MIT
Apa itu python-docs MCP?
python-docs adalah MCP server. Menyediakan dokumentasi live CPython standard library dan metadata package PyPI dalam format markdown yang bersih untuk AI agent. Server ini menyediakan 5 tool, seperti python_docs, python_search, pypi_package.
Ringkasan dibuat dari README proyek
Tentang python-docs MCP server
MCP server ini memungkinkan Anda memberikan dokumentasi live CPython standard library dan metadata package PyPI kepada AI agent. Dengan ini, coding assistant dapat mengakses API dunia nyata yang terkini dalam format markdown yang bersih, alih-alih hanya mengandalkan data training yang sudah usang atau berisiko halusinasi. Developer dapat menggunakannya via stdio melalui client seperti Claude Desktop, Cursor, atau Cline untuk memastikan kode Python yang dihasilkan menargetkan modul dan versi yang valid.
Server ini menyertakan tools untuk resolve identifier modul spesifik, mencari melalui index lokal dari 229 modul standard library, serta mengambil detail package PyPI termasuk README dan metadata versi. Server ini juga dilengkapi dengan local SQLite TTL cache dan politeness layer yang menghormati robots.txt serta budget request. Anda dapat menginstalnya dengan cepat menggunakan uvx atau melalui local git checkout untuk keperluan development. Sistem ini mampu menangani lookup yang kompleks, seperti memetakan istilah umum seperti ElementTree ke full module path miliknya, serta menyediakan health checks untuk memantau status cache dan jaringan.
Apa yang bisa Anda lakukan dengannya
- 1Mengambil dokumentasi standard library yang akuratMinta AI untuk mengambil dokumentasi markdown untuk modul spesifik seperti json atau zipfile guna memastikan penggunaan API yang benar.
- 2Mencari modul berdasarkan fungsionalitasTemukan modul Python yang tepat saat Anda hanya mengetahui tugas yang ingin dilakukan, misalnya saat bekerja dengan file zip.
- 3Memeriksa detail package PyPIDapatkan README, lisensi, dan versi Python yang dibutuhkan untuk package pihak ketiga tertentu dari PyPI.
- 4Verifikasi kesehatan server dan jaringanPeriksa status cache dokumentasi dan konektivitas ke sumber dokumentasi eksternal.
Ditulis berdasarkan README proyek. Setiap use case diperiksa terhadap kutipan dari README tersebut.
Tool python-docs MCP 5
Terdeteksi dari READMEpython_docspython_searchpypi_packagepython_statushealth_check
| python_docs | — | |
| python_search | — | |
| pypi_package | — | |
| python_status | — | |
| health_check | — |
Konfigurasi
PYTHON_DOCS_MCP_CACHE_DIRPYTHON_DOCS_MCP_POLITENESS_DISABLEDPYTHON_DOCS_MCP_PYPI_JSON_OPT_IN
| Nama | Deskripsi | Tipe |
|---|---|---|
| PYTHON_DOCS_MCP_CACHE_DIR | — | |
| PYTHON_DOCS_MCP_POLITENESS_DISABLED | — | |
| PYTHON_DOCS_MCP_PYPI_JSON_OPT_IN | — |
README
Lihat di GitHubpython-docs-mcp
An MCP (Model Context Protocol) server that gives AI coding agents live CPython standard-library documentation and PyPI package information instead of whatever the model happens to remember. Module pages are scraped from docs.python.org at request time, cached locally, and returned as clean markdown — so generated Python targets real, current APIs rather than deprecated or invented ones.
- Stdlib modules — every module page fetched live from docs.python.org/3/library and converted to markdown (the
/3/line is whatever CPython currently documents — measured3.14.8) - PyPI packages — metadata + README read from the package's robots-allowed project page, optionally pinned to a version (see PyPI source)
- Local search index over all 229 stdlib modules (name + description recorded, so
ElementTreeresolves toxml.etree.elementtreeon its own), rebuilt at most every 7 days with a stale fallback when the network fails - SQLite TTL cache so repeated lookups are instant
- Politeness layer — robots.txt (RFC 9309), per-host throttle,
Retry-After, conditional GET and request budgets (details)
The five tools
| Tool | What it does |
|---|---|
python_docs |
Resolve one identifier (json, xml.etree.ElementTree, ElementTree) to a single stdlib module page as markdown. |
python_search |
Ranked search over the local index of all 229 stdlib modules (name and description). |
pypi_package |
PyPI package metadata + README: resolved version, summary, Requires-Python, license, homepage, project URLs. The README is capped by max_tokens (default 6 000 estimated tokens). |
python_status |
Real health check: index size/age, cache stats, live probes of docs.python.org and pypi.org, and politeness counters. |
health_check |
Server liveness and version. |
Every tool returns a plain dict. Failures come back as {"ok": false, "error": …, "suggestion": …} — a bad lookup never raises into the MCP layer, and no tool delegates to another tool.
Requirements
- Python 3.10 or newer (developed and tested on 3.12)
uvfor the one-command install below (uvxships with it)- The MCP Python SDK 1.x is pinned (
mcp>=1.2.0,<2.0); this package does not support the mcp 2.x rename ofFastMCP
Install & run
One command — no checkout, no token
uvx --from git+https://github.com/KEEPEE/python-docs-mcp.git python-docs
This builds the package in an isolated environment and starts the stdio MCP server. It prints nothing on purpose: stdout is the protocol channel. Stop it with Ctrl-C.
After the repository is updated, force uv to re-resolve the commit:
uvx --refresh --from git+https://github.com/KEEPEE/python-docs-mcp.git python-docs
Local checkout — fastest startup, editable while developing
git clone https://github.com/KEEPEE/python-docs-mcp.git
cd python-docs-mcp
python3 -m venv .venv
.venv/bin/pip install -e .
.venv/bin/python-docs # stdio MCP server
MCP client configuration
Generic stdio client (Claude Desktop, Cursor, Cline, …)
{
"mcpServers": {
"python-docs": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/KEEPEE/python-docs-mcp.git",
"python-docs"
]
}
}
}
With an explicit cache directory (any env you set is passed straight through to the server):
{
"mcpServers": {
"python-docs": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/KEEPEE/python-docs-mcp.git",
"python-docs"
],
"env": {
"PYTHON_DOCS_MCP_CACHE_DIR": "/tmp/python-docs-cache"
}
}
}
}
DeepSeek Harness (cordis-style plugin list)
- id: mcp-python-docs
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: python-docs
transport: stdio
command: uvx
args:
[
'--from',
'git+https://github.com/KEEPEE/python-docs-mcp.git',
'python-docs'
]
To skip the build at client start, point command at the console script of a checkout instead — command: /path/to/python-docs-mcp/.venv/bin/python-docs with args: [].
Tools reference
python_docs(identifier, topic=None, max_tokens=8000)
Resolves an identifier to one module page. Identifier forms, tried in this order:
| Form | Meaning |
|---|---|
xml.etree.ElementTree (any dotted name) |
fetched directly; the docs site only has lowercase filenames, so the lowercased name is tried first and the spelling you gave only as a fallback |
json, zipfile (plain lowercase path) |
fetched directly |
ElementTree (any other plain name) |
resolved through the local index: the unique entry whose full name or last dotted segment matches case-insensitively is fetched; ambiguous or missing matches return an error dict pointing at python_search |
topic keeps only the section whose heading matches it (Basic Usage, Command-line interface, …) plus the page title and description; if nothing matches, the full page is returned with a note listing the available headings. max_tokens is a rough budget (1 token ≈ 4 characters): the markdown is cut at a line boundary and truncated is set.
Returns {"ok", "identifier", "url", "title", "markdown", "truncated", "cached"}, plus an optional note.
// python_docs({ "identifier": "xml.etree.ElementTree", "max_tokens": 250 })
{
"ok": true,
"identifier": "xml.etree.ElementTree",
"url": "https://docs.python.org/3/library/xml.etree.elementtree.html",
"markdown": "# `xml.etree.ElementTree` — The ElementTree XML API **Source code:** [Lib/xml/etree/ElementTree.py](https://github.com/python/cpython/tree/3.14/Lib/xml/etree/ElementTree.py) --- The `xml.etree.ElementTree` module implements a simple and efficient API for pa … [truncated]",
"truncated": true,
"cached": false,
"title": "xml.etree.ElementTree — The ElementTree XML API"
}
A bare name is resolved through the index — same page, no guessing:
// python_docs({ "identifier": "ElementTree", "max_tokens": 160 })
{
"ok": true,
"identifier": "ElementTree",
"url": "https://docs.python.org/3/library/xml.etree.elementtree.html",
"title": "xml.etree.ElementTree — The ElementTree XML API",
"…": "…"
}
A topic that matches nothing tells you what the page does have:
// python_docs({ "identifier": "json", "topic": "nonexistent-section", "max_tokens": 120 })
{
"ok": true,
"identifier": "json",
"url": "https://docs.python.org/3/library/json.html",
"title": "json — JSON encoder and decoder",
"…": "…",
"note": "no section matching 'nonexistent-section'; available headings: `json` — JSON encoder and decoder, Basic Usage, Encoders and Decoders, Let the base class default method raise the TypeError, Exceptions, Standard Compliance and Interoperability, Character Encodings, Infinite and NaN Number Values, Repeated Names Within an Object, Top-level Non-Object, Non-Array Values, Implementation Limitations, Command-line interface, Command-line options"
}
A name that is not a module is an error dict, not an empty page:
// python_docs({ "identifier": "nope-not-a-real-module-xyz" })
{
"ok": false,
"error": "no stdlib module matching 'nope-not-a-real-module-xyz' found in the search index",
"suggestion": "use python_search('nope-not-a-real-module-xyz') to find the right module name"
}
python_search(query, limit=8)
Multi-token ranked search over the local index: every token must match, and both the module name and its one-line description are searched. Returns {"ok", "query", "results": [{module, description, score}], "index_count", "stale"}.
// python_search({ "query": "zip file", "limit": 3 })
{
"ok": true,
"query": "zip file",
"results": [
{ "module": "zipfile", "description": "Work with ZIP archives", "score": 14.0 },
{ "module": "gzip", "description": "Support for gzip files", "score": 9.0 }
],
"index_count": 229,
"stale": false
}
Description matching is what makes this useful when you know the job, not the name:
// python_search({ "query": "element tree", "limit": 3 })
{ "results": [ { "module": "xml.etree.elementtree", "description": "The ElementTree XML API", "score": 12.0 } ], "…": "…" }
// python_search({ "query": "tokeniz", "limit": 3 }) // prefix match
{ "results": [ { "module": "tokenize", "description": "Tokenizer for Python source", "score": 8.0 } ], "…": "…" }
python_search costs no requests while the cached index is fresh; if the index is stale the call rebuilds it (one robots fetch + one library-index page) inside the same budget.
pypi_package(name, version=None, max_tokens=6000)
PyPI metadata + README. name is the distribution name; version pins one release, which is how you read the README a specific version shipped; max_tokens caps the README. Returns {"ok", "name", "version" (resolved), "summary", "requires_python", "license", "homepage", "project_urls", "readme_markdown", "url"} — url is always the human-facing project page — plus truncated when there was a README to cap. Cached 1 day, keyed by name + version.
The README is capped, like python_docs's markdown. A README is the author's own document, and for a lot of packages it is the changelog: measured live 2026-10-07, fetch_pypi_package("pydantic") returns a 109 110-byte README (≈ 27 277 estimated tokens) and zope.interface a 27 KB one. This path had no cap at all while python_docs had one, so one tool call could spill a hundred kilobytes into a context window. max_tokens applies the same rule as python_docs — len(text)//4 estimate, cut at a line boundary, truncated: true, and the same [truncated: showing ~X of ~Y estimated tokens] note inside the body:
// pypi_package({ "name": "pydantic" }) — live output, 2026-10-07
{
"ok": true,
"name": "pydantic",
"version": "2.13.5",
"…": "…",
"readme_markdown": "# Pydantic Validation\n…\n\n[truncated: showing ~5999 of ~27277 estimated tokens]",
"url": "https://pypi.org/project/pydantic/",
"cached": false,
"truncated": true
}
A README that already fits is returned byte-for-byte with truncated: false, and a package with no README makes no truncation claim at all (no truncated key). The cache stores the uncapped document, so a later call with a bigger max_tokens gets more text without another request.
// pypi_package({ "name": "attrs" })
{
"ok": true,
"name": "attrs",
"version": "26.1.0",
"summary": "Classes Without Boilerplate",
"requires_python": ">=3.9",
"license": "MIT",
"homepage": "https://github.com/python-attrs/attrs",
"project_urls": {
"GitHub": "https://github.com/python-attrs/attrs",
"Changelog": "https://www.attrs.org/en/stable/changelog.html",
"Documentation": "https://www.attrs.org/",
"Funding": "https://github.com/sponsors/hynek",
"Tidelift": "https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=pypi"
},
"readme_markdown": "[] …",
"url": "https://pypi.org/project/attrs/26.1.0/",
"cached": false
}
A pinned version resolves to exactly that release:
// pypi_package({ "name": "six", "version": "1.16.0" })
{
"ok": true,
"name": "six",
"version": "1.16.0",
"summary": "Python 2 and 3 compatibility utilities",
"requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*",
"license": "MIT License (MIT)",
"homepage": "https://github.com/benjaminp/six",
"url": "https://pypi.org/project/six/1.16.0/",
"…": "…"
}
An unknown name is an error dict — and on PyPI the honest error message is two-sided, because PyPI does not answer unknown names with a plain 404 (see the challenge note):
// pypi_package({ "name": "nope-not-a-real-package-xyz" })
{
"ok": false,
"error": "pypi.org answered with a JavaScript bot-verification page (HTTP 200, not a 404) instead of the project page. Either PyPI is serving its anti-bot challenge for this URL, or the package does not exist — PyPI also answers unknown package names with that page, so a challenge is not proof the name is valid",
"suggestion": "retry later (PyPI serves that page for some cold URLs), check the name (case-sensitive) or drop the version pin, or use the PYTHON_DOCS_MCP_PYPI_JSON_OPT_IN=1 opt-in knowing that PyPI's robots.txt disallows the JSON API"
}
python_status()
Probes docs.python.org and pypi.org with a light GET (both on robots-allowed paths) and reports the index and cache state. overall is ok, degraded or error. The politeness block is diagnostics only and never changes overall.
// python_status()
{
"server": "python-docs-mcp",
"version": "0.2.0",
"checks": {
"search_index": { "status": "ok", "entries": 229, "built_at": "2026-10-07T08:04:39.802131+00:00", "stale": false },
"cache": { "status": "ok", "entries": 11, "expired": 0 },
"docs_python_org": { "status": "ok", "http_status": 200 },
"pypi_org": { "status": "ok", "http_status": 200 }
},
"politeness": {
"status": "ok", "disabled": false,
"requests": 10, "robots_requests": 2, "robots_rows": 2, "robots_fetches": 2, "robots_cache_hits": 7,
"blocked_by_robots": 0, "throttle_waits": 8, "throttle_sleep_s": 3.626,
"challenge_detected": 2, "challenge_retries": 1,
"host_delays": { "docs.python.org": 0.0, "pypi.org": 0.913 },
"budgets": { "tool:pypi_package": [2, 6], "tool:python_status": [2, 8] },
"budget_denied": 0, "conditional": 1, "conditional_skipped": 0, "revalidated_304": 1,
"retries_429": 0, "retries_transport": 0, "stalls": 0
},
"overall": "ok"
}
health_check()
{"status": "ok", "server": "python-docs-mcp", "version": "0.2.0"} — no network, no cache; safe as a liveness probe.
PyPI source (and robots.txt)
This is the one place where this server deliberately does not use the API you might expect, and it is worth understanding before you change it.
PyPI's robots.txt (325 bytes, 12 Disallow lines for User-agent: *) says:
Disallow: /simple/
Disallow: /packages/
Disallow: /_includes/authed/
Disallow: /project/*/submit-malware-report/
Disallow: /pypi/*/json
Disallow: /pypi/*/*/json
Disallow: /pypi*?
Disallow: /search*
Disallow: /_/
Disallow: /integrity/
Disallow: /account/
Disallow: /admin/
pypi_package() reads its metadata from the robots-allowed project page, https://pypi.org/project/<name>/ (or /project/<name>/<version>/), because Disallow: /pypi/*/json and Disallow: /pypi/*/*/json forbid the JSON API this package used to call. /simple/ is not a way around it — it is disallowed too, and it carries no README or project URLs. (Python's own urllib.robotparser did not catch the original violation: on Python < 3.14 it reads /pypi/*/json as a literal prefix and reports that URL as allowed. The RFC 9309 matcher in politeness.py reports it as blocked — verified live: can_fetch("https://pypi.org/pypi/attrs/json") → False, can_fetch("https://pypi.org/project/attrs/") → True.)
The page parser pulls the same fields out of the rendered HTML — name, resolved version, summary, Requires-Python, license (both the modern "License expression" block and the legacy "License" sidebar entry), homepage, project URLs and the README (RST → markdown) — and pypi_package returns exactly the same keys as before, so nothing downstream changed.
The JSON API is opt-in only
export PYTHON_DOCS_MCP_PYPI_JSON_OPT_IN=1
With that set, pypi_package uses https://pypi.org/pypi/<name>/json again. That endpoint is disallowed by robots.txt, so using it is your responsibility, and every result produced that way carries a notice field saying so — verbatim:
{
"…": "…",
"notice": "PyPI's robots.txt disallows /pypi/*/json and /pypi/*/*/json for every user-agent. The JSON API was used because PYTHON_DOCS_MCP_PYPI_JSON_OPT_IN=1 is set; that is a deliberate, user-authorised bypass of the politeness layer and it is the user's responsibility."
}
With the opt-in off (the default) the JSON endpoint is never requested — not even as a fallback when the project page fails, and the politeness layer blocks that URL independently, so the violation cannot come back through another door. With the opt-in on, the project page is never requested. The two sources never fall back into one another.
Known trade-off: the JS "Client Challenge"
PyPI's CDN (Fastly) answers some project-page URLs with a ~3 KB JavaScript Client Challenge instead of the page — HTTP 200, not a 404. Measured on this repo's fixtures: the challenge page is 3,038 bytes, a real attrs project page is 130,507 bytes.
This is a real trade-off of reading the allowed HTML page instead of the JSON API, and it has a sharp edge you should know about:
A package that does not exist can look exactly like a bot challenge. PyPI answers unknown package names with that same challenge page — reproduced live on 2026-10-07:
pypi.org/project/nope-not-a-real-package-xyz/returned HTTP 200 with the challenge body. A challenge is therefore not proof the name is valid, and a 404 is not the only way a name can be wrong.
The tool reports both possibilities instead of guessing:
// the error text, verbatim
"pypi.org answered with a JavaScript bot-verification page (HTTP 200, not a 404) instead of the project page. Either PyPI is serving its anti-bot challenge for this URL, or the package does not exist — PyPI also answers unknown package names with that page, so a challenge is not proof the name is valid"
What the code does about it:
- the politeness layer's optional
challenge_detectorhook is wired to a PyPI-specific predicate (fetchers.pypi_challenge_detector): small body + HTML + a challenge marker (_fs-ch-, "Client Challenge") + none of the markers a real project page always carries (project-header__name,package-header__name,pip-command); - when it fires, the layer waits (escalated per-host delay) and retries that same URL once — never another endpoint. Measured:
challenge_detected: 2,challenge_retries: 1, and the whole lookup spends 3 of its 6 budget units; - if the answer is still a challenge, the body is never parsed into a fake package, never cached, and never answered with a fallback to the robots-disallowed JSON endpoint. A genuine page is fetched exactly once — the detector does not add a request for it (covered by
tests/test_politeness_wiring.py).
Caching & politeness
Cache
Everything lives in one directory: ~/.cache/python-docs-mcp by default, overridable with PYTHON_DOCS_MCP_CACHE_DIR.
| File | Contents | TTL |
|---|---|---|
cache.db |
module pages (parsed result + raw body + ETag / Last-Modified), the search index, PyPI metadata |
module docs 7 days, search index 7 days, PyPI metadata 1 day |
robots.db |
the politeness layer's robots.txt cache | 7 days per host |
Delete the files to force a full refresh. A cache problem is never a tool failure: if the directory is unwritable the server simply runs without a cache and says so in python_status().checks.cache.
Pertanyaan tentang python-docs MCP
Tool apa yang disediakan python-docs MCP?
Server ini menyediakan 5 tool, termasuk python_docs, python_search, pypi_package, python_status, health_check.
Bagaimana cara kerja fungsi pencarian untuk modul standard library?
Server ini menggunakan local ranked search index pada seluruh 229 modul stdlib, yang mencari berdasarkan nama modul maupun deskripsinya.
Apakah server ini bisa menangani README PyPI yang berukuran besar?
Ya, konten README dibatasi menggunakan parameter max_tokens untuk mencegah context window AI menjadi terlalu penuh.
Apakah tersedia cache untuk mempercepat request yang berulang?
Ya, server ini menggunakan SQLite TTL cache sehingga pencarian yang berulang dapat dilakukan secara instan.