ndl-mcp
ndl-mcp
An MCP server for searching 国立国会図書館サーチ (NDL Search), operated by the National Diet Library of Japan, over the SRU searchRetrieve interface.
Third in a series with cinii-mcp and jstage-mcp, and sharing their response envelope: typed query and script, matching mode, graduated breadth, per-item matched_in, typed diagnostics, a loggable receipt, attribution.
What this is for
The National Diet Library receives everything published in Japan, and this reaches five of its own catalogues: general holdings, the Japanese National Bibliography, the periodicals index, that index's online-materials companion, and the open-data digital collections.
Use the national bibliography when an imprint fact has to be right — a date, a publisher, an edition statement — because it is the authority other catalogues copy from. The periodicals index reaches article-level records for Japanese magazines and journals going back well beyond what CiNii or J-STAGE hold, which is where prewar and early postwar material becomes searchable. Single records resolve by JP number or NDL bibliographic ID.
Requests are issued one at a time, at a measured pace, under the undertakings filed with the library.
What the receipts are for
A search you cannot re-run is a claim you cannot check. When a footnote rests on a database query, say that no article in this index uses a term before a certain year, the reader is asked to take the search on trust: which term, in which script, on what date, against which index and which version of it, and how far down the results the author went. Ordinary searching leaves none of that behind. This server leaves all of it. Every query-answering tool returns its envelope through the ledger, which appends one line to an append-only file: the term actually sent and its script, how the source matched it, how many records existed and how many came back, the diagnostics, the tool and its parameters, the server version, a timestamp, and the hash of the previous line. The hash makes the file a chain: a line cannot be altered, removed or reordered afterwards without the verifier saying so.
What that gives a researcher:
A citable search. Name the receipt in the footnote (session slug, server, date, line hash) and a reader can see exactly what was asked and run it again against the same version.
Negative findings that carry weight. "Not found" is evidence only if the search that produced it is on record, with its term, its script and its breadth.
A method section that writes itself.
ndl-mcp-ledgermanifest <folder>summarises every query a project made, by server, script and session: the disclosure a journal, a data-availability statement or a research-integrity review asks for.A record of AI-mediated research. When a model chose the term, the receipt shows the term it chose and what came back, which is the thing to disclose about work done with an assistant.
Nothing interpreted. The receipt is the source's own answer with credentials removed. The server does not summarise, rank or paraphrase, so the record is of the source, not of the tool.
Receipts are off until you name a folder (MCP_RECEIPT_DIR); each server then writes its own
<server>.jsonl inside it, and MCP_RECEIPT_SESSION stamps a project or article slug on every
line so one folder can serve several projects. ndl-mcp-ledger verify-dir <folder> checks the chains.
The mechanics, the variables and what the envelope says when nothing is deposited are in the
receipts section below.
Before you run this
There is no credential. The NDL search APIs are open. No API key, no application ID, no token, nothing to paste into a config file. If you are waiting for something to arrive before you can use this, you are waiting for something that is not coming.
Registering is recommended. The author filed a notification of continuous use on 19 August 2026 through the form described in APIのご利用について. The library replied that registration is no longer required, though still welcome. A formal 利用申請 remains necessary only for revenue-generating use.
Register anyway. It costs a few minutes, it tells the library who is using the interface and for what, and a national library that can see its API being used by researchers has an argument for keeping it funded and open that it does not otherwise have. Scholarly infrastructure survives on evidence of use.
install.ps1 records the date to NDL-API-NOTIFICATION.txt when you pass it:
.\install.ps1 -NotificationFiled 2026-08-19Run it without the flag and it prints the form URL, offers to open it, and continues with the install.
Python. The pip and source routes need Python 3.10 or later; 3.10, 3.12, 3.13 and 3.14 are tested in CI on Windows, macOS and Linux. The Claude Desktop bundle uses whichever of these is already installed, and has uv download one only if none is.
Install
Three routes. All three give you the same server; pick by how much you want to see of it.
Getting Python
Every route needs Python 3.10 to 3.14. The Claude Desktop bundle uses one already on the machine
and has uv download one only if none is; the other routes also need the venv module, which the
official installers include.
Windows. Download the 64-bit installer from python.org/downloads and run it; tick "Add python.exe to PATH" on the first screen. Afterwards
py --version(the launcher the installer adds) orpython --versionin a new terminal should print 3.1x. If typingpythonopens the Microsoft Store instead, Windows has no Python yet: that Store page is a stub, and it is also what "'python' is not recognized" usually means.macOS. The python.org installer, or
brew install python@3.13with Homebrew. The/usr/bin/python3that Xcode's command-line tools provide may be older than 3.10;python3 --versionsays.Linux. Your distribution's package:
sudo apt install python3 python3-venvon Debian and Ubuntu,sudo dnf install python3on Fedora. Or let uv provide one (next line).Any platform, with uv. uv installs Python itself:
uv python install 3.13, thenuv venvor theuvxroute below.
One click: the Claude Desktop bundle
Download ndl-mcp-1.2.1.mcpb from the latest release and open it; Claude Desktop installs it. One bundle serves Windows, macOS (Apple Silicon and Intel) and Linux. Claude Desktop asks only for a receipts folder at install time.
The bundle carries the server's source and a lock file, nothing compiled, and needs no Python of its own: Claude Desktop runs it with uv, using a uv already on your PATH if there is one and otherwise the copy the app ships. On first launch uv uses a Python 3.10 or later already on the machine, downloading one only if there is none, and installs the locked libraries: roughly 40 MB, or 60 MB with an interpreter, which took 26 to 46 seconds on the author's connection; later launches take under a second. If the first launch is slow enough that Claude Desktop reports the server disconnected, restart the app: what uv already fetched is cached, and the second launch completes. Bundles before 1.2.0 vendored libraries compiled for CPython 3.12 only and failed on every other interpreter; see Troubleshooting.
From GitHub, pinned to a release
pip install "git+https://github.com/ckgerteis/ndl-mcp@v1.2.1"
# or, without an environment of your own:
uvx --from "git+https://github.com/ckgerteis/ndl-mcp@v1.2.1" ndl-mcpinstalls the ndl-mcp console script and ndl-mcp-ledger. The tag is the thing to cite; @main gets whatever is current. Then register it in Claude Desktop (below), or let install.py do that.
The whole family
pip install "git+https://github.com/ckgerteis/bibliograph-mcp@v1.0.3" && bibliograph installinstalls all six servers and registers them together — one receipts folder, credentials asked for once. See bibliograph-mcp. From a checkout of this repository, python install.py does the same for this server alone, python install.py --all for the six, on Windows, macOS and Linux; install.ps1 remains for Windows.
From source
python3 -m venv .venv
.venv/bin/pip install .
.venv/bin/python -c "import ndl_mcp; print(ndl_mcp.__version__)"That import fails loudly if the package or one of its vendored modules is
missing. Do not use ndl-mcp --help as the check: unknown arguments are
ignored, the server starts, reads end-of-input and exits 0, so it reports
success whatever the state of the code.
Installing more than this one
Six independent packages. None imports another, none depends on another, and
each installs and answers on its own — pip install . in this directory is a
complete install of this server and nothing else.
They do share three things: a response envelope, a query ledger, and — if you
run more than one — a receipts folder. install.ps1 is vendored byte-identical
into all six and handles that on Windows; install.py is its cross-platform port. Both install this server by default, because
cloning one repository is not a request for five more.
.\install.ps1 # this server
.\install.ps1 -All # all six
.\install.ps1 -Servers ndl,cinii # a chosen subsetNothing about where things go is decided for you. The script asks where to
install (the virtual environment Claude Desktop will be pointed at), which
folder receives the receipts, and which session slug to stamp on them,
offering a neutral suggestion for each that Enter accepts; run without a
terminal it does not guess, and stops unless --venv and --receipts-dir
(or --no-receipts; -VenvDir and -ReceiptsDir for install.ps1) say
so. Whatever subset you name is registered against one receipts folder, asked for
once. The script prefers a sibling checkout to the network, carries across
credentials already registered rather than asking again, leaves servers it was
not asked about alone, and stops rather than guessing where the servers already
registered disagree about the folder or the session slug. It also asserts that
ledger.py and mediation.py are byte-identical across everything it
installed, so two envelope versions cannot end up in one environment unnoticed.
Claude Desktop
install.ps1 writes this entry for you. By hand, add it to
%APPDATA%\Claude\claude_desktop_config.json under mcpServers, pointing at
the console script in the environment you installed into. On macOS or Linux use
the absolute path to .venv/bin/ndl-mcp. There is no credential to supply.
{
"mcpServers": {
"ndl": {
"command": "C:\\path\\to\\.venv\\Scripts\\ndl-mcp.exe",
"env": {
"MCP_RECEIPT_DIR": "C:\\path\\to\\receipts",
"MCP_RECEIPT_SESSION": "project-or-article-slug"
}
}
}
}Changed in 1.1.0. Earlier versions were registered by path —
"command": "…\\python.exe", "args": ["…\\server.py"]. That entry will not
start this version, because server.py is now a module inside a package rather
than a script beside its imports. Replace it with the console script above.
Restart Claude Desktop. The six tools should appear under "ndl" in the tool list.
Any other MCP client
Nothing here is specific to Claude. The server speaks the Model Context Protocol over stdio and nothing else: any client that can start a process and talk JSON-RPC to it (Claude Code, Cursor, VS Code and Continue, Zed, LibreChat, a script of your own using an MCP SDK) can use it. The Claude Desktop bundle and the installers are conveniences for one client; the server underneath is the same console script. Register it anywhere by giving the client the absolute path of the console script and, optionally, the environment:
{
"mcpServers": {
"ndl": {
"command": "/absolute/path/to/.venv/bin/ndl-mcp",
"env": {
"MCP_RECEIPT_DIR": "/absolute/path/to/receipts",
"MCP_RECEIPT_SESSION": "project-or-article-slug"
}
}
}
}Claude Code takes the same thing on the command line:
claude mcp add ndl -- /absolute/path/to/.venv/bin/ndl-mcpOn Windows the path ends in \.venv\Scripts\ndl-mcp.exe. MCP_RECEIPT_DIR and MCP_RECEIPT_SESSION
are optional; without them the server runs and every envelope says RECEIPT_NOT_DEPOSITED. The
stdio transport is the only one: there is no HTTP endpoint to expose, and nothing to host.
Troubleshooting
"Server disconnected" is all Claude Desktop says when the server process exited before or during the handshake, whatever the reason. The reason is in the log:
Windows:
%APPDATA%\Claude\logs\mcp-server-<name>.log(the extension's display name, or the key undermcpServers), withmcp.logbeside it for the app's side of the conversation.macOS:
~/Library/Logs/Claude/mcp-server-<name>.logandmcp.log.Linux:
~/.config/Claude/logs/.
Read the last launch from the bottom up. Three shapes account for nearly every report:
A Python traceback ending in
ImportErrororModuleNotFoundError(for exampleNo module named 'pydantic_core._pydantic_core'). The interpreter started, the code was found, and a compiled library did not match that interpreter. This is what every bundle before 1.2.0 did on any Python other than 3.12. Install the current bundle, or use the pip route, which resolves wheels for the interpreter you install into.'python' is not recognized,spawn python ENOENT, or a line from the Microsoft Store: no interpreter was found on the PATH Claude Desktop constructs. Nothing of this server ran. The current bundle does not launchpythonat all; for the pip route, register the console script by absolute path as shown above.A line from uv (
error: ..., or a download that never finished): the current bundle's runtime could not build its environment, usually because the first launch had no network or ran past Claude Desktop's sixty-second limit. Restart the app; uv keeps what it fetched. A uv older than 0.5 cannot read the lock file; upgrade it or remove it so the app uses its own.
The bundle's own entry point writes one line naming the interpreter, its path and the supported range before re-raising an import failure, so a log from 1.2.0 onwards says which of these it is.
What the server will not do
The undertakings below were filed with the NDL. They are implemented, not aspired to, and the installer's smoke test asserts the first three:
Undertaking | Implementation |
Requests issued serially; no concurrent access |
|
Minimum one-second interval |
|
A cap on records per search; no bulk retrieval |
|
The harvesting interface is not used | OAI-PMH is not implemented |
Credit on every response |
|
Metadata displayed, not accumulated | no cache, no local store |
Change any of them and you change what this server declares about itself. Update this table, the module comment in src/ndl_mcp/server.py, and NDL-API-NOTIFICATION.txt in the same commit, so the description a reader checks stays true to what the code does.
Providers
Only the five sets declared in the notification of 19 August 2026 are reachable. All are NDL-created, all are marked ○ in both the 非営利 and 営利 columns of the provider list, and none requires a usage application. Their metadata is governed by 公共データ利用規約(第1.0版)(PDL1.0), which the NDL states to be compatible with CC BY 4.0 — PDL1.0 is the licence, CC BY 4.0 the compatibility claim:
dpid | 名称 |
| 国立国会図書館蔵書 |
| 国立国会図書館全国書誌情報 |
| 国立国会図書館雑誌記事索引 |
| 国立国会図書館雑誌記事索引オンライン資料編 |
| 国立国会図書館デジタルコレクション(オープンデータ) |
ndl-dl and ndl-dl-online — the wider Digital Collections — are marked ○ for 非営利 and △ only for 営利 on the provider list, so scholarly use needs no usage application. They are out of scope here because they sit outside the set this server declares, and because their metadata carries no open licence — displayable, not redistributable. Adding them is a documentation change in this repository, not an application to the library. A request naming them is refused in process, with a DPID_NOT_PERMITTED diagnostic, rather than sent.
Tools
Tool | Set searched |
| 蔵書 |
| 全国書誌情報 |
| 雑誌記事索引 (both sets) |
| デジタルコレクション(オープンデータ) |
| all five |
| one record by |
Search fields: title, creator, publisher, subject, anywhere, ndc, isbn, issn, from_year, to_year. They are combined with AND; title, creator, publisher and subject match partially, ndc by prefix, identifiers exactly.
ndl_get_record is a fetch, so its envelope omits searched_for — no term was chosen.
Two things that will bite
A bare and or or between words makes NDL reject the whole query, in any case. Not "returns nothing" — rejects. NOT is not reserved and is left alone.
This was measured against the live API on 27 August 2026, because the rule stated here until then was wrong in both directions. It claimed the check was case-sensitive and that War and Peace passes. It does not: anywhere="War and Peace" is refused where anywhere="War Peace" returns 5,025. What "passed" was the guard, not the library — and the query then came back as API_ERROR with a total of zero, which reads exactly like an absence. The same sentence also treated NOT as reserved, so the server declined in process a query NDL answers: anywhere="cats NOT dogs" returns 96 records.
Position matters, and the check follows what the API does rather than what would be tidy: anywhere="and Peace" is tolerated (14,375), anywhere="Peace and" is refused, anywhere="cats and dogs" is refused. So whitespace is required before the word, and whitespace or the end of the term after it. A word that merely contains the letters is untouched — Thailand, Andorra and notation all return records.
The server checks before sending and returns a RESERVED_WORD_IN_QUERY diagnostic naming the offending field, rather than letting the library answer with a parse failure. The remedy is usually to drop the conjunction: "Civil Information and Education Section" is refused, "Civil Information Education Section" returns 21,690.
NDL enforces a rate limit it will not quantify, and answers HTTP 429. The help page says only 「同時リクエスト数には制限を設けています」 and declines to publish a figure. In testing on 19 August 2026 a 429 arrived at well under one sustained request per second — so the one-second floor filed with the library is a minimum, not a guarantee. A 429 buys one backoff, honouring Retry-After, and then the server stops rather than pressing. It reports RATE_LIMITED, deliberately distinct from API_ERROR, because the two mean different things to a reader: a rate-limited search has an unknown result, not an empty one, and must never be written up as an absence.
A romanised term will under-return. NDL Search indexes Japanese-language records in Japanese script. A Latin-script query against a Japanese corpus is the romaji trap, and the envelope raises SCRIPT_LATIN_QUERY for it. The searched_for headline exists so that the term the assistant actually chose is visible at the top of the response rather than buried — that is the whole point of the field, and the reason a disclosure can report the terms a search used.
Response format
Every tool returns the response envelope built by mediation.py and defined in response-schema.json, schema version 2.3.0. The module and the schema are vendored byte-identically across cinii-mcp, jstage-mcp, korea-scholarship-mcp and this server, so an envelope from one can be read by a consumer written for another.
Search operations carry searched_for — the term actually sent, its detected script, and the matching mode — hoisted to the top of the envelope so a relaying client cannot drop it. ndl_get_record omits it: a fetch is handed an identifier and chooses no term.
Receipts
mediation.emit() writes each response envelope to an append-only, hash-chained ledger. Unset the receipt variables and nothing is written and nothing fails — which is exactly what happened here between 19 and 22 August 2026: the code called emit() at every exit while no variable was set in this server's environment, so three days of queries went unrecorded behind well-formed envelopes. Since schema 2.3.0 the envelope reports it: RECEIPT_NOT_DEPOSITED when nothing is configured, RECEIPT_WRITE_FAILED when it is and the write did not land.
MCP_RECEIPT_DIR names a folder, and this server writes ndl.jsonl inside it. One file per server, because appending is read-the-last-hash-then-write and the lock around it does not hold between processes: six servers pointed at one file will fork the chain when two answer at once. Measured — six processes, 150 lines, fourteen forks. MCP_RECEIPT_LOG still names a single file and is honoured when MCP_RECEIPT_DIR is unset.
install.ps1 holds no path of its own. It asks for the folder, offering whatever the already-registered servers use and otherwise %APPDATA%\Claude\mcp-receipts, and registers every server it installs against the same one. MCP_RECEIPT_SESSION — the project slug that groups a project's queries — is taken from the registered servers or asked for, never invented per server. Where the registered servers disagree about either, the install stops and asks rather than putting NDL in one of two records. (Earlier revisions of this README said the installer defaulted to %APPDATA%\Claude\mcp-receipts.jsonl. It did not; the statement described an installer that no longer existed. What it did default to, until 1.1.0, was a path inside the author's own Dropbox folder, written into a public repository.)
Verify with ndl-mcp-ledger verify-dir <folder>, or write the citable manifest with ndl-mcp-ledger manifest <folder>. A failure is typed: a fork means concurrent writers and every line is still present; tamper means a line no longer hashes to its own content. The two are not the same finding and are no longer reported as though they were.
Note what the ledger holds and what it does not: the query, the normalised term, the parameters sent, the timestamp, a SHA-256 over query and parameters, and the identifiers of the records returned. It does not hold the bibliographic records themselves. Logging a query is not accumulating a database, and the undertaking against accumulation is not breached by keeping the receipt — but the distinction is worth stating rather than assuming, because the two look similar from outside.
Why SRU only
The application declares SRU and OpenSearch. This server implements SRU alone, which is less than was declared and therefore safe — you may always use less than you told the library you would.
The reason is evidential. The OpenSearch response format is not documented in the 第1.4版 specification: no element table, no sample, and the appendices cover SRU and OAI-PMH only. Worse, the spec states that a malformed parameter returns a zero-result response rather than an error — 「引数(パラメータ)誤りの場合には検索結果ゼロ件となる」 — so a typo in a field name is indistinguishable from a genuine absence. For a tool whose purpose is to let a historian trust that nothing was found, that is disqualifying. SRU returns typed diagnostics and a documented DC-NDL record schema. Adding OpenSearch later needs no new notification; it needs a documented response format.
Sources
国立国会図書館サーチ 外部提供インタフェース仕様書 第1.4版 (2026-03-31)
APIのご利用について — terms, credit requirement, concurrency, notification
API提供対象データプロバイダ一覧 — dpid values and licence conditions
Licence
MIT, for the server code. Metadata retrieved through this server is governed by 公共データ利用規約(第1.0版)(PDL1.0), read with 「国以外の者」 as 「国立国会図書館以外の者」; the NDL states that condition to be compatible with CC BY 4.0, which is a compatibility claim rather than a grant of CC BY 4.0 itself. The credit line the server emits is the attribution PDL1.0 requires, and it should survive into anything you publish from the results.
What has been tested, and what has not
Verified against the live API on 19 August 2026:
Japanese-script search across 蔵書 and 雑誌記事索引 — correct totals, correct records, correct years and identifiers.
DC-NDL parsing, including the manifestation-stub filter. NDL returns two
BibResourceelements per record; taking both doubled the result set with blanks until the filter went in.searched_forreports the term chosen, not the assembled CQL, so its script detection is meaningful; the exact CQL is carried inquery.paramsand is fixed by the receipt hash.The
DPID_NOT_PERMITTEDguard: a request namingndl-dlis refused in process.RESERVED_WORD_IN_QUERY, as it then stood:War AND Peacecaught,War and Peacepassed the guard. What the library did with it afterwards was not checked, and the entry should not have been written as though it had been — see 27 August below.The rate limiter, involuntarily — see HTTP 429 above.
Verified against the live API on 23 August 2026, closing two rows that had been read rather than run:
The "Record does not exist" passthrough. That string is not a fault. It is how NDL answers a search that matched nothing, and it answers that way for every provider —
zassaku,zassaku-online,iss-ndl-opacandndl-dl-openall return it for a term with no hits. The server maps it tototal: 0with aZERO_CONJUNCTIONdiagnostic rather than toAPI_ERROR, which is the distinction the whole envelope exists to preserve: a search that found nothing is not a search that failed.ndl_get_record.jpno=71009951, taken from a national-bibliography result, resolves to one record with anOKdiagnostic.The reserved-word rule, corrected.
andandorare refused by NDL in any case;NOTis not reserved at all. Measured across seventeen cases:cats and dogs,cats AND dogs,cats And dogs,cats or dogsandcats OR dogsall refused;cats not dogs,cats NOT dogs,war not peaceandtitle="not for sale"all answered;Thailand,Andorraandnotationunaffected. The guard now matches that behaviour exactly, having previously both missed every lowercase conjunction and refused a word the library accepts.Multi-provider search, after the CQL correction —
ndl_search_articlesreturns 23,766 fortitle="労働運動", matchingdpid="zassaku"alone, becausezassaku-onlineholds nothing under that title. The union total is a count, not a floor.
Still not verified, and read rather than run: the backoff path. Testing stopped at the 429 rather than continuing, because characterising an undisclosed rate limit by probing it is precisely the 継続して大量のアクセス the terms warn about, and the point of this server is not to be the thing the National Diet Library has to block. It will be exercised in ordinary use, a query at a time.
Verified on 4 September 2026 with tests/smoke_stdio.py, which starts the installed console script over stdio, performs the MCP handshake, checks tools/list against the tool table above, and with RUN_LIVE=1 … <tool> '<json params>' makes one live call: ndl_search_books for 軍艦島 answered 524 records with an OK diagnostic.