agentleFS
asterixixasterixix/polish-academic-mcp/AGENTS.md

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.

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
  1. Very specific:

    Only tools/call JSON-RPC requests are counted.

    line 138
  2. Very specific:

    When EVAL_WEBDAV_ENABLED=true, the Worker uploads one JSON file for every incoming MCP JSON-RPC request with method: "tools/call".

    line 194
  3. Distinctive:

    Follow this template exactly:

    line 216
  4. Very specific:

    Every parameter must have a .describe() string — this is the LLM's only hint.

    line 254
  5. Distinctive:

    Always return raw API text (JSON or XML) rather than parsing it — saves CPU.

    line 255
  6. Very specific:

    Always wrap the handler body in try/catch returning isError: true on failure.

    line 256
  7. Very specific:

    Use makeCacheKey(toolName, paramsObject) to build deterministic cache keys.

    line 257
  8. Very specific:

    Add an entry to SOURCES in src/sources.ts.

    line 261
  9. Very specific:

    Every tool is automatically annotated readOnlyHint: true, openWorldHint: true.

    line 281
  10. Very specific:

    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.

    line 282
  11. Very specific:

    Import paths always end in .js (required for ESM Worker output).

    line 297
  12. Distinctive:

    No classes except where required by the SDK.

    line 302
  13. Distinctive:

    All tool handlers must use this pattern:

    line 308
  14. Very specific:

    If a new tool requires its own KV namespace (rare — prefer reusing CACHE_KV):

    line 333
  15. Very specific:

    Add it to wrangler.jsonc under kv_namespaces.

    line 335
  16. Very specific:

    Add the binding name to the Env interface in src/types.ts.

    line 336
  17. Very specific:

    Document the one-time wrangler kv namespace create command in README.md.

    line 337
  18. Distinctive:

    Test with MCP Inspector:

    line 349
  19. Distinctive:

    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.

    line 373
  20. Very specific:

    Do not share a single McpServer instance across requests — createServer(env) is called per-request in src/index.ts.

    line 375
  21. Very specific:

    Do not bump @modelcontextprotocol/sdk without checking the bundled SDK version in agents (npm deduplicates.

    line 377
  22. Very specific:

    Do not npm install -g polish-academic-mcp or recommend global install — this package is designed to be invoked through npx -y …

    line 380
  23. Distinctive:

    Do not re-introduce Cloudflare / MCPB / research-evaluation / telemetry dependencies.

    line 383
  24. Very specific:

    Do not write to stdout from tools or server code (console.log, process.stdout): stdout carries the MCP protocol.

    line 385
  25. Very specific:

    Do not add runtime dependencies for the setup wizard (prompts, TOML/YAML parsers): it uses Node built-ins only, and npx downloads every …

    line 388
  26. Very specific:

    Do not commit secrets. The only optional secrets are PBN_APP_ID, PBN_APP_TOKEN, BDL_CLIENT_ID.

    line 390

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

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_search and repod_search. Both tools now return a normalized item shape containing stable keys used by evaluation (title, author, date, doi) while preserving source fields in source_raw.
  • Optional privacy mode for sensitive use cases:
    • ruj_search supports minimize_pii: boolean (default false) and removes direct personal fields (authors, affiliation) plus redacts common PII patterns.
    • bn_search_articles supports minimize_pii: boolean (default false) 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 returns noRecordsMatch, the tool retries once without set (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.toolName and request.arguments
  • raw MCP response body (truncated to EVAL_WEBDAV_MAX_JSON_BYTES)
  • _span span attributes (attached by src/tracing.ts wrappers)
  • rqEval — computed RQ1–RQ4 scores/report when the tool call arguments match an eval test case from scripts/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/catch returning isError: true on 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.ts

TypeScript conventions

  • strict: true everywhere — no implicit any, no unused variables.
  • Import paths always end in .js (required for ESM Worker output).
  • Env interface lives in types.ts — add new KV/secret bindings there and in wrangler.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):

  1. Add it to wrangler.jsonc under kv_namespaces.
  2. Add the binding name to the Env interface in src/types.ts.
  3. Document the one-time wrangler kv namespace create command in README.md.

Local development

npm run dev          # starts the stdio MCP server from src/index.ts

The 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` process

Deployment

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 deploy

What 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 McpServer instance across requests — createServer(env) is called per-request in src/index.ts; SDK v1.26+ leaks state otherwise.
  • Do not bump @modelcontextprotocol/sdk without checking the bundled SDK version in agents (npm deduplicates; private-field type conflicts are otherwise invisible until CI breaks).
  • Do not npm install -g polish-academic-mcp or recommend global install — this package is designed to be invoked through npx -y polish-academic-mcp so 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; the setup/doctor commands in src/cli.ts are 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 npx downloads 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.