polish-academic-mcp
AGENTS.md4 starsChanged 8 months agoMITView on GitHub
This file is written for AI coding agents (GitHub Copilot, Claude, Cursor, etc.). It describes the codebase architecture, conventions, and step-by-step instructions for extending the server so that agents can work autonomously without guessing. polish-academic-mcp is a local MCP server (Model Context Protocol) that exposes 125 tools letting any MCP-compatible LLM (Claude, GPT-4, etc.) search Polish academic, public, and cultural databases. It runs as a local Node.js process communicating via stdio, distributed exclusively through npm as the polish-academic-mcp package. All…
- Installs packages
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,542 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository5 other files it gives its agents.
- ai-prompt-engineering-safety-reviewSkill
- autoresearchSkill
- first-askSkill
- mcp-cliSkill
- mcp-security-auditSkill
The file
Show its rules and text
We split this AGENTS.md into the rules it gives an agent
26 rules. About 0 are common, 7 are distinctive, and 19 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
19
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 138
Only tools/call JSON-RPC requests are counted.
- Very specific: line 194
When EVAL_WEBDAV_ENABLED=true, the Worker uploads one JSON file for every incoming MCP JSON-RPC request with method: "tools/call".
- Distinctive: line 216
Follow this template exactly:
- Very specific: line 254
Every parameter must have a .describe() string — this is the LLM's only hint.
- Distinctive: line 255
Always return raw API text (JSON or XML) rather than parsing it — saves CPU.
- Very specific: line 256
Always wrap the handler body in try/catch returning isError: true on failure.
- Very specific: line 257
Use makeCacheKey(toolName, paramsObject) to build deterministic cache keys.
- Very specific: line 261
Add an entry to SOURCES in src/sources.ts.
- Very specific: line 281
Every tool is automatically annotated readOnlyHint: true, openWorldHint: true.
- Very specific: line 282
If you add tools, update the expected tool list in tests/mcp-contract.test.ts and the counts in README.md / docs/AGENT-GUIDE.md.
- Very specific: line 297
Import paths always end in .js (required for ESM Worker output).
- Distinctive: line 302
No classes except where required by the SDK.
- Distinctive: line 308
All tool handlers must use this pattern:
- Very specific: line 333
If a new tool requires its own KV namespace (rare — prefer reusing CACHE_KV):
- Very specific: line 335
Add it to wrangler.jsonc under kv_namespaces.
- Very specific: line 336
Add the binding name to the Env interface in src/types.ts.
- Very specific: line 337
Document the one-time wrangler kv namespace create command in README.md.
- Distinctive: line 349
Test with MCP Inspector:
- Distinctive: line 373
Do not parse XML or large JSON inside tool handlers — return raw text from the source API to the LLM and let the model interpret it.
- Very specific: line 375
Do not share a single McpServer instance across requests — createServer(env) is called per-request in src/index.ts.
- Very specific: line 377
Do not bump @modelcontextprotocol/sdk without checking the bundled SDK version in agents (npm deduplicates.
- Very specific: line 380
Do not npm install -g polish-academic-mcp or recommend global install — this package is designed to be invoked through npx -y …
- Distinctive: line 383
Do not re-introduce Cloudflare / MCPB / research-evaluation / telemetry dependencies.
- Very specific: line 385
Do not write to stdout from tools or server code (console.log, process.stdout): stdout carries the MCP protocol.
- Very specific: line 388
Do not add runtime dependencies for the setup wizard (prompts, TOML/YAML parsers): it uses Node built-ins only, and npx downloads every …
- Very specific: line 390
Do not commit secrets. The only optional secrets are PBN_APP_ID, PBN_APP_TOKEN, BDL_CLIENT_ID.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md — AI Coding Agent Reference
This file is written for AI coding agents (GitHub Copilot, Claude, Cursor, etc.). It describes the codebase architecture, conventions, and step-by-step instructions for extending the server so that agents can work autonomously without guessing.
Project overview
polish-academic-mcp is a local MCP server (Model Context Protocol) that exposes
125 tools letting any MCP-compatible LLM (Claude, GPT-4, etc.) search Polish
academic, public, and cultural databases. It runs as a local Node.js process
communicating via stdio, distributed exclusively through npm as the
polish-academic-mcp package. All databases offer unauthenticated read access
except for the three PBN tools (require PBN_APP_ID + PBN_APP_TOKEN).
This file describes the v1.1.0 architecture (npm-only, local stdio). Historical sections that reference Cloudflare Workers, MCPB bundles, or research evaluation apparatus are obsolete and have been removed; if you see references to those in the wild, treat them as out of date.
For the complete tool catalogue (125 entries with Polish descriptions) see
README.md. For per-client configuration snippets see docs/CLIENTS.md.
For AI agents that need to configure the server themselves see
docs/AGENT-GUIDE.md.
File map
package.json npm-only distribution; entry "bin": { "polish-academic-mcp": "dist/index.js" }
engines: "node": ">=18"; no MCPB; no eval; no telemetry deps
src/
├── index.ts Stdio entry: --help, --version, --list-sources, setup/uninstall/doctor
│ dispatch, --sources / POLISH_ACADEMIC_SOURCES, then the MCP transport
├── server.ts createServer(env, { sources }) — registers the selected sources (default:
│ all 125 tools), MCP instructions, readOnlyHint/openWorldHint on every tool
├── sources.ts catalogue of 33 sources in 5 groups (nauka, dane, prawo, normy, kultura)
│ + parseSourceSelection()
├── clients.ts MCP client catalogue for setup: config paths per OS, entry formats,
│ JSON/TOML config edits (pure functions over a HostContext)
├── cli.ts setup / uninstall wizard, doctor, --list-sources, --help text
├── cache.ts in-process TTL cache + 30s timeout + single retry on transient errors;
│ POLISH_ACADEMIC_DEBUG=1 logs every request to stderr
├── tool-error-handling.ts structured error classification without OTel
├── types.ts Env interface (BDL_CLIENT_ID, PBN_APP_ID, PBN_APP_TOKEN optional)
└── tools/ 33 files, one per database / source
├── biblioteka-nauki.ts → bn_search_publications, bn_search_articles, bn_get_article
├── ruj.ts → ruj_search, ruj_get_item
├── agh.ts → agh_search, agh_get_item
├── amu.ts → amu_search, amu_get_item
├── uafm.ts → uafm_search, uafm_get_item (currently: HTTP 404 from eRIKA)
├── icm.ts → icm_search, icm_get_item
├── rodbuk.ts → rodbuk_search
├── repod.ts → repod_search, repod_get_dataset
├── dane.ts → dane_search, dane_get_dataset
├── polon.ts → polon_search
├── pbn.ts → pbn_search_publications, pbn_search_persons, pbn_get_publication
├── bdl.ts → bdl_search_subjects, bdl_search_variables, bdl_search_units,
│ bdl_get_variable, bdl_get_data_by_variable, bdl_get_data_by_unit
├── imgw.ts → imgw_synop, imgw_hydro, imgw_meteo, imgw_warnings
├── pkn.ts → pkn_search
├── wiedza.ts → wiedza_search_norms, wiedza_get_standard
├── blz.ts → blz_search, blz_get_listing, blz_listing_categories
├── baztol.ts → baztol_search, baztol_browse_domain, baztol_get_resource
├── nac.ts → nac_news_rss, nac_site_search, nac_get_post, nac_get_page
├── sum.ts → sum_aleph_find, sum_aleph_present
├── ludzie-nauki.ts → ludzie_search, ludzie_semantic_search, ludzie_get_scientist
├── pauart.ts → pauart_search, pauart_get_artwork
├── isap.ts → isap_search_acts, isap_get_act
├── sejm-bs.ts → bs_sejm_search, bs_sejm_get_item
├── saos.ts → saos_search_judgments, saos_get_judgment, saos_dump_services,
│ saos_dump_common_courts, saos_dump_sc_chambers,
│ saos_dump_judgments, saos_dump_enrichments
├── wolne-lektury.ts → wolnelektury_list_taxonomy, wolnelektury_filter_books,
│ wolnelektury_get_book, wolnelektury_get_collection
├── ninateka.ts → ninateka_search, ninateka_get_vod
├── gapla.ts → gapla_search, gapla_get_poster
├── fototeka.ts → fototeka_search, fototeka_get_photo
├── filmpolski.ts → filmpolski_search, filmpolski_get_item
├── fototekaslaska.ts → fototekaslaska_search, fototekaslaska_get_photo
├── filmoteka-repo.ts → fn_repo_search, fn_repo_get_node, fn_repo_film_index, fn_repo_browse_kind
├── rcin.ts → rcin_search, rcin_get_record
└── dokumenty-slaska.ts → dokumenty_slaska_get_page, dokumenty_slaska_medieval_catalog
tests/
├── package-contract.test.ts version, --help, --version, npm-only, no MCPB / eval / telemetry
├── mcp-contract.test.ts 125 tool IDs stable, all tool + parameter descriptions in Polish
├── fetch-policy.test.ts 30s timeout, single retry on transient, no retry on 4xx
├── sources.test.ts source selection, catalogue ↔ tool prefixes, filtered server
└── setup.test.ts client config paths per OS, JSON/TOML edits, setup CLI end-to-end
scripts/
└── smoke-tools.ts live smoke (82 tools + 5 dynamic search→get pairs)
tsconfig.json TypeScript config (strict, module: ES2022, target: ES2022)
docs/
├── CLIENTS.md per-client MCP configuration (what `setup` writes + manual steps)
├── AGENT-GUIDE.md prompt for AI agents configuring the server (uses `setup --yes`)
├── CLOUDFLARE.md remote Workers+Pages (OAuth 2.1, AI Search, connectors)
└── plans/ implementation plans
cloud/ optional Cloudflare Workers + Static Assets (separate package.json).
Shares src/tools/* via tool-bridge; not part of the npm CLI package.
OAuth 2.1 (/mcp), ORCID/email/passkey, D1 quotas, AI Gateway Haiku.
See docs/CLOUDFLARE.md.Architecture decisions (HISTORICAL — see v1.1.0 note at top)
These sections describe the pre-v1.1.0 Cloudflare Workers / MCPB / eval-pipeline architecture that no longer ships with
polish-academic-mcp. They are kept as a historical reference only. Do not change the codebase to match them.
1. Stateless — one McpServer instance per request
createServer(env) in server.ts is called inside the fetch handler so that every
HTTP request gets a fresh McpServer. This is mandatory since SDK v1.26.0: reusing a
global instance leaks state across clients.
// index.ts — correct pattern
export default {
async fetch(request, env, ctx) {
const handler = createMcpHandler(createServer(env)); // fresh each time
return handler(request, env, ctx);
},
};2. Rate limiting before MCP dispatch
Only tools/call JSON-RPC requests are counted. The body is cloned before reading so
the stream is still available for the MCP handler. Rejected requests never write to KV.
Limit: 10 tool calls per hour per client IP (CF-Connecting-IP header).
3. cachedFetch() wraps every external API call
Signature:
cachedFetch(
env: Env,
cacheKey: string,
url: string,
ttlSeconds: number,
fetchOptions?: RequestInit,
): Promise<string>Cache misses execute fetch(), store the raw response text in CACHE_KV with TTL, and
return the text. Writes are fire-and-forget (ctx.waitUntil is not available in
tool handlers, so writes are detached with .catch(() => {})).
TTL conventions:
86_400(24 h) for academic repositories (RODBuK, RePOD, RUJ, Biblioteka Nauki)3_600(1 h) for dane.gov.pl (frequently updated government data)
4. SDK pinning
@modelcontextprotocol/sdk is pinned to exactly 1.26.0 in package.json to match
the version bundled inside the agents package. npm deduplicates to one copy and
eliminates the private-field type conflict. Do not bump this without also checking
the agents package's bundled SDK version.
5. RQ3/RQ4 hardening: normalized fields + optional PII minimization
Recent tool updates add two important behaviors used by the evaluation harness:
- Normalized Dataverse search output in
rodbuk_searchandrepod_search. Both tools now return a normalized item shape containing stable keys used by evaluation (title,author,date,doi) while preserving source fields insource_raw. - Optional privacy mode for sensitive use cases:
ruj_searchsupportsminimize_pii: boolean(defaultfalse) and removes direct personal fields (authors,affiliation) plus redacts common PII patterns.bn_search_articlessupportsminimize_pii: boolean(defaultfalse) and redacts common PII patterns in XML (ORCID, email, PESEL-like, phone-like).
- Robust retrieval fallback in
bn_search_articles: when a restrictive OAI set returnsnoRecordsMatch, the tool retries once withoutset(same date range and metadata format) to improve practical recall in evaluation scenarios.
Eval export to Nextcloud WebDAV
When EVAL_WEBDAV_ENABLED=true, the Worker uploads one JSON file for every
incoming MCP JSON-RPC request with method: "tools/call".
Uploaded record includes:
request.toolNameandrequest.arguments- raw MCP response body (truncated to
EVAL_WEBDAV_MAX_JSON_BYTES) _spanspan attributes (attached bysrc/tracing.tswrappers)rqEval— computed RQ1–RQ4 scores/report when the tool call arguments match an eval test case fromscripts/eval/test-cases.ts
Configuration is done via wrangler.jsonc vars:
NEXTCLOUD_WEBDAV_BASE_URL, NEXTCLOUD_WEBDAV_USERNAME, NEXTCLOUD_WEBDAV_PASSWORD,
NEXTCLOUD_WEBDAV_PATH_PREFIX, and EVAL_WEBDAV_MAX_JSON_BYTES.
RQ metrics are computed server-side using the same metric functions as the
local evaluator (scripts/eval/metrics.ts).
How to add a new database tool
Step 1 — Create src/tools/my-database.ts
Follow this template exactly:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { Env } from "../types.js";
import { cachedFetch, makeCacheKey } from "../cache.js";
const API_BASE = "https://api.example.com";
const CACHE_TTL = 86_400; // seconds
export function registerMyDatabaseTools(server: McpServer, env: Env): void {
server.tool(
"mydb_search", // snake_case: prefix = short db name
"One-paragraph description that tells the LLM WHEN to call this tool," +
" what arguments it expects, and what shape the response has.",
{
query: z.string().describe("Search terms"),
page: z.number().int().min(1).default(1).describe("Page number (1-based)"),
},
async ({ query, page }) => {
try {
const url = `${API_BASE}/search?q=${encodeURIComponent(query)}&page=${page}`;
const key = makeCacheKey("mydb_search", { query, page });
const text = await cachedFetch(env.CACHE_KV, key, url, {}, CACHE_TTL);
return { content: [{ type: "text", text }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
}
},
);
}Rules:
- Tool name must be globally unique and follow
{prefix}_{action}naming. - Every parameter must have a
.describe()string — this is the LLM's only hint. - Always return raw API text (JSON or XML) rather than parsing it — saves CPU.
- Always wrap the handler body in
try/catchreturningisError: trueon failure. - Use
makeCacheKey(toolName, paramsObject)to build deterministic cache keys.
Step 2 — Add the source to the catalogue and register it
Add an entry to SOURCES in src/sources.ts. The id must equal the tool-name
prefix (mydb for mydb_search) and group must be one of the existing groups
(nauka, dane, prawo, normy, kultura):
{ id: "mydb", group: "nauka", name: "My Database" },Then map it in src/server.ts (the REGISTER record is typed over all source
ids, so a missing entry fails to compile):
import { registerMyDatabaseTools } from "./tools/my-database.js";
const REGISTER: Record<SourceId, RegisterFn> = {
// ...
mydb: registerMyDatabaseTools,
};Every tool is automatically annotated readOnlyHint: true, openWorldHint: true.
If you add tools, update the expected tool list in tests/mcp-contract.test.ts
and the counts in README.md / docs/AGENT-GUIDE.md.
Step 3 — Verify
npx tsc --noEmit
npm test # includes the catalogue ↔ tool-prefix check in tests/sources.test.tsTypeScript conventions
strict: trueeverywhere — no implicitany, no unused variables.- Import paths always end in
.js(required for ESM Worker output). Envinterface lives intypes.ts— add new KV/secret bindings there and inwrangler.jsonc.- Zod schemas are defined inline; do not extract them to separate files unless shared across two or more tools.
- No classes except where required by the SDK. Prefer plain functions.
Error handling pattern
All tool handlers must use this pattern:
async (params) => {
try {
// ... business logic ...
return { content: [{ type: "text", text: responseText }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {
content: [{ type: "text", text: `Error calling <tool>: ${msg}` }],
isError: true,
};
}
};Returning isError: true inside the result (not throwing) allows the LLM to see and
potentially handle the error message. Throwing from a tool handler causes a JSON-RPC
protocol error that is opaque to the LLM.
wrangler.jsonc changes needed for a new KV namespace
If a new tool requires its own KV namespace (rare — prefer reusing CACHE_KV):
- Add it to
wrangler.jsoncunderkv_namespaces. - Add the binding name to the
Envinterface insrc/types.ts. - Document the one-time
wrangler kv namespace createcommand inREADME.md.
Local development
npm run dev # starts the stdio MCP server from src/index.tsThe package uses an in-memory cache store in local development, so the server runs without Cloudflare bindings.
Test with MCP Inspector:
npx @modelcontextprotocol/inspector@latest
# Connect the inspector to the local `npm run dev` processDeployment
See README.md (Polish) or the GitHub Actions workflow at
.github/workflows/deploy.yml for full deployment instructions.
Quick reference:
npx wrangler kv namespace create "CACHE_KV" # copy ID → wrangler.jsonc
npx wrangler kv namespace create "RATE_LIMIT_KV" # copy ID → wrangler.jsonc
npm run deployWhat NOT to do (CURRENT for v1.1.0)
- Do not parse XML or large JSON inside tool handlers — return raw text from the source API to the LLM and let the model interpret it.
- Do not share a single
McpServerinstance across requests —createServer(env)is called per-request insrc/index.ts; SDK v1.26+ leaks state otherwise. - Do not bump
@modelcontextprotocol/sdkwithout checking the bundled SDK version inagents(npm deduplicates; private-field type conflicts are otherwise invisible until CI breaks). - Do not
npm install -g polish-academic-mcpor recommend global install — this package is designed to be invoked throughnpx -y polish-academic-mcpso each MCP client can pin its own version. - Do not re-introduce Cloudflare / MCPB / research-evaluation / telemetry dependencies. v1.1.0 is local stdio + npm-only by design.
- Do not write to stdout from tools or server code (
console.log,process.stdout): stdout carries the MCP protocol. Log to stderr; thesetup/doctorcommands insrc/cli.tsare the only code that prints to stdout, and they never start the transport. - Do not add runtime dependencies for the setup wizard (prompts, TOML/YAML parsers):
it uses Node built-ins only, and
npxdownloads every dependency on first run. - Do not commit secrets. The only optional secrets are
PBN_APP_ID,PBN_APP_TOKEN,BDL_CLIENT_ID; they live in the user's environment, never in the repo.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

