{
  "markdown": "# transkribus-mcp-server\n\n[![Tests](https://github.com/lazyants/transkribus-mcp-server/actions/workflows/test.yml/badge.svg)](https://github.com/lazyants/transkribus-mcp-server/actions/workflows/test.yml)\n\nMCP server for the [Transkribus REST API](https://transkribus.eu/). Manage collections, documents, HTR/OCR recognition, models, and more through the Model Context Protocol.\n\n**300 tools** across 22 resource domains, with 8 entry points so you can pick the right server for your MCP client's tool limit.\n\n> **API scope:** This server covers the **legacy Transkribus TrpServer REST API**. The newer **Processing API v2** (OIDC auth, `/processing/v2`, `account.readcoop.eu`) is intentionally out of scope.\n\n## Installation\n\n```bash\nnpm install -g @lazyants/transkribus-mcp-server\n```\n\nOr run directly:\n\n```bash\nnpx @lazyants/transkribus-mcp-server\n```\n\n## Configuration\n\nTranskribus uses session-based authentication. You can authenticate in two ways:\n\n### Option 1: Username + Password (auto-login)\n\n```bash\nexport TRANSKRIBUS_USER=your-email@example.com\nexport TRANSKRIBUS_PASSWORD=your-password\n```\n\nThe server will automatically log in and manage the session.\n\n### Option 2: Direct session ID\n\n```bash\nexport TRANSKRIBUS_SESSION_ID=your-session-id\n```\n\nUse this if you already have a valid session from the Transkribus platform.\n\n## Entry Points\n\n| Command | Domains | Tools |\n|---|---|---|\n| `transkribus-mcp-server` | All 22 domains | 300 |\n| `transkribus-mcp-collections` | Auth, Collections (core/docs/pages/users/crowd/editdecl/credits/stats/labels/activity/tags) | 132 |\n| `transkribus-mcp-admin` | Auth, Admin, Credits, Uploads, Labels, Files, System, Root | 62 |\n| `transkribus-mcp-transcription` | Auth, Recognition, Layout Analysis, PyLaia, P2PaLA, DU | 47 |\n| `transkribus-mcp-users` | Auth, Users, Crowdsourcing, eLearning | 29 |\n| `transkribus-mcp-models` | Auth, Models | 26 |\n| `transkribus-mcp-jobs` | Auth, Jobs, Actions | 18 |\n| `transkribus-mcp-search` | Auth, Search, KWS | 16 |\n\nUse split servers to reduce context size — pick only the splits you need.\n\n## Claude Code\n\nAdd to `~/.claude/settings.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"transkribus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@lazyants/transkribus-mcp-server\"],\n      \"env\": {\n        \"TRANSKRIBUS_USER\": \"your-email@example.com\",\n        \"TRANSKRIBUS_PASSWORD\": \"your-password\"\n      }\n    }\n  }\n}\n```\n\nOr use split servers (pick the splits you need):\n\n```json\n{\n  \"mcpServers\": {\n    \"transkribus-collections\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"-p\", \"@lazyants/transkribus-mcp-server\", \"transkribus-mcp-collections\"],\n      \"env\": {\n        \"TRANSKRIBUS_USER\": \"your-email@example.com\",\n        \"TRANSKRIBUS_PASSWORD\": \"your-password\"\n      }\n    },\n    \"transkribus-transcription\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"-p\", \"@lazyants/transkribus-mcp-server\", \"transkribus-mcp-transcription\"],\n      \"env\": {\n        \"TRANSKRIBUS_USER\": \"your-email@example.com\",\n        \"TRANSKRIBUS_PASSWORD\": \"your-password\"\n      }\n    }\n  }\n}\n```\n\n## Claude Desktop\n\nAdd to `claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"transkribus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@lazyants/transkribus-mcp-server\"],\n      \"env\": {\n        \"TRANSKRIBUS_USER\": \"your-email@example.com\",\n        \"TRANSKRIBUS_PASSWORD\": \"your-password\"\n      }\n    }\n  }\n}\n```\n\n## Security\n\n- **Never commit your credentials** to version control\n- Use environment variables or a `.env` file (excluded via `.gitignore`)\n- Session IDs expire — prefer username/password for long-running setups\n\n## Disclaimer\n\nThis is an unofficial MCP server for Transkribus. The authors are not affiliated with READ-COOP SCE. Use at your own risk.\n\n## Releasing\n\nReleases ship via the GitHub Release event. Maintainer flow:\n\n1. Bump the version in `package.json`, `package-lock.json`, and `server.json` (`npm version <x.y.z> --no-git-tag-version` updates the first two together). `npm run check-versions` **hard-fails** unless `package.json#/version` and `server.json#/packages[0].version` agree. `server.json#/version` is checked loosely: it must be present, and it only fails when it *regresses* below `packages[0].version` — a value left behind at the previous release passes with a `WARN:` line and exit 0. The script does **not** look at `package-lock.json` or `CHANGELOG.md` at all, so read its output rather than trusting its exit code.\n2. Update `CHANGELOG.md`.\n3. Commit, and **merge the version bump to `main` before creating the release**. Then create the tag yourself, on a SHA you have checked, and only then create the release from it:\n\n   ```bash\n   V=X.Y.Z && PR=<release-pr-number> &&\n     SHA=\"$(gh pr view \"$PR\" --json mergeCommit -q .mergeCommit.oid)\" && test -n \"$SHA\" &&\n     git fetch origin main && git merge-base --is-ancestor \"$SHA\" origin/main &&\n     PKG=\"$(git show \"$SHA:package.json\")\" &&\n     test \"$(printf '%s' \"$PKG\" | node -pe 'JSON.parse(require(\"fs\").readFileSync(0,\"utf8\")).version')\" = \"$V\" &&\n     CL=\"$(git show \"$SHA:CHANGELOG.md\")\" &&\n     printf '%s\\n' \"$CL\" | awk -v v=\"$V\" 'index($0,\"## [\"v\"]\")==1{f=1;next} /^## \\[/{f=0} /^\\[[0-9]+\\.[0-9]+\\.[0-9]+\\]:/{f=0} f' > \"/tmp/notes-v$V.md\" &&\n     grep -q '[^[:space:]]' \"/tmp/notes-v$V.md\" &&\n     git tag -a \"v$V\" \"$SHA\" -m \"v$V\" &&\n     git push origin \"v$V\" &&\n     gh release create \"v$V\" --verify-tag --notes-file \"/tmp/notes-v$V.md\"\n   ```\n\n   **The failure this prevents:** with no existing tag, `gh release create vX.Y.Z` places one on the **tip of the default branch**. Run it while the bump is still on a release branch and it tags the *previous* release's commit; the workflow then publishes whatever version it finds in that commit's `package.json`, producing a `vX.Y.Z` GitHub Release that silently republishes the old version. The publish workflow now refuses to continue when `GITHUB_REF_NAME` is not `v<package.json version>`, so that exact scenario fails before `npm publish` rather than silently republishing. The sequence above is still required, and guards a case the workflow cannot: the workflow guard only runs once a release already exists, and it passes for any commit carrying the right version — so it catches a *mis-tagged* release, not the *wrong commit* being tagged.\n\n   Each element is load-bearing:\n\n   - **`gh pr view … .mergeCommit.oid`** names the release PR's own squash commit. Do not substitute `git rev-parse origin/main`: that is merely whatever sits on `main` at the moment you look, so an unrelated merge landing in the gap gets tagged and shipped instead. `gh` exits 0 and prints nothing for an unmerged PR, hence the explicit `test -n`.\n   - **The `&&` chain** stops at the first failure instead of falling through to the irreversible step. Both `git show` calls are assigned to a variable rather than piped directly, so their exit status is actually checked — a pipeline reports only its *last* command's status unless `pipefail` is set, which is not assumed here.\n   - **`git merge-base --is-ancestor`** proves the commit is reachable from `main`. Mere existence is not enough: a commit can be present locally because another branch was fetched, and if its version files happen to match it would otherwise pass every remaining check.\n   - **The version test reads `package.json` out of the target commit**, not the working tree — which would still show the right version while `$SHA` pointed elsewhere.\n   - **The `awk`** lifts that version's section out of the commit's `CHANGELOG.md` for `--notes-file`. Without it the release body is whatever `--notes-from-tag` finds in the annotation — here the literal string `vX.Y.Z`, a poor release note for any version and a misleading one for a release carrying a breaking change. It stops at the next `## [` heading *or* at the first link-reference definition, because the oldest entry has no heading after it and would otherwise swallow the whole link-reference block. `grep -q` rather than `test -s` guards the result: a section empty apart from its blank line still produces a one-byte file, which `test -s` accepts.\n   - **`--verify-tag`** makes `gh` abort rather than invent a tag if the push did not land — the guard against the tip-of-default-branch fallback described above.\n\n   If `gh release create` fails after the tag is already pushed, do not rerun the whole block; it will stop at `git tag`, which is correct. Rerun only the final command.\n4. The `Publish to npm + MCP Registry` workflow runs automatically: it `npm publish`es with provenance, polls the registry until the tarball is available, then pushes the matching `server.json` to the MCP Registry via `mcp-publisher`.\n\nThe workflow skips `npm publish` cleanly if the version is already on npm (cutover guard for releases that were partially published manually).\n\n### npm authentication\n\nPublishing uses **npm Trusted Publishing**: the workflow's GitHub OIDC token (`id-token: write`) is exchanged for a one-shot publish token at runtime. No `NPM_TOKEN` secret needs to live in the repo.\n\nThe binding is configured in the npm web UI (package → Trusted Publishers): provider `GitHub Actions`, organization `lazyants`, repository `transkribus-mcp-server`, workflow `publish-registry.yml`.\n\n## License\n\n[FSL-1.1-MIT](LICENSE) — see [LICENSE](LICENSE) for the full terms. Versions `1.x` remain MIT-licensed.\n",
  "bytes": 9356,
  "sha": "83448ea91cf078d437a492c22b0e50363e9c0183c225b646aa04a0dbc7a12859",
  "repo_slug": "lazyants/transkribus-mcp-server",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_lazyants_transkribus_972d1ed1/readme"
}