transkribus-mcp-server
# transkribus-mcp-server
[](https://github.com/lazyants/transkribus-mcp-server/actions/workflows/test.yml)
MCP server for the [Transkribus REST API](https://transkribus.eu/). Manage collections, documents, HTR/OCR recognition, models, and more through the Model Context Protocol.
**304 tools** across 23 resource domains, with 9 entry points so you can pick the right server for your MCP client's tool limit.
> **API scope:** This server covers **two** Transkribus APIs:
>
> - the **legacy TrpServer REST API** (`https://transkribus.eu/TrpServer/rest`), session-based — 300 tools;
> - the **Metagrapho Processing API** (`https://transkribus.eu/processing/v1`), OIDC bearer auth via `account.readcoop.eu` — the 4 `transkribus_processing_*` tools.
>
> Mind the version. Some Transkribus material still shows `/processing/v2` and a `config.modelId` field. That path returns 404; the live service is `/processing/v1` and takes `config.textRecognition.htrId`.
## Installation
```bash
npm install -g @lazyants/transkribus-mcp-server
```
Or run directly:
```bash
npx @lazyants/transkribus-mcp-server
```
## Configuration
Transkribus uses session-based authentication. Credentials are resolved in this
order, per value:
1. **OS keyring** (recommended — nothing is written to a config file in clear text)
2. **Environment variable** (`TRANSKRIBUS_USER` + `TRANSKRIBUS_PASSWORD`, or `TRANSKRIBUS_SESSION_ID`)
Either a user name and password (the server logs in and manages the session) or
a session id you already hold. A session id takes precedence when both are
available; it expires, so a user name and password is the better choice for a
long-running setup — and is what lets the server re-authenticate after a 401.
The keyring is never required: if it is unavailable — a headless Linux box with
no Secret Service, an unsupported platform, an install with `--omit=optional` —
or if it does not answer within 5 seconds, the server falls back to the
environment.
### Store the credentials in the OS keyring
Three entries under one service name, `transkribus-mcp` by default:
`user`, `password` and `session-id` (store only what you use).
> [!IMPORTANT]
> The commands below read the value from an interactive prompt rather than
> taking it as an argument, so it never lands in your shell history, in a
> command line, or in the launch environment of another process. Avoid pasting a
> password directly onto the command line.
#### macOS
Omitting the value after `-w` makes `security` prompt for it:
```bash
security add-generic-password -s "transkribus-mcp" -a "user" -w
security add-generic-password -s "transkribus-mcp" -a "password" -w
```
> [!NOTE]
> A login-keychain item belongs to the program that created it. The first time
> the server reads an item created by `security`, macOS shows a "…wants to use
> your confidential information stored in transkribus-mcp" dialog — choose
> **Always Allow** and it will not ask again. Until that is granted the read
> cannot complete: the server waits 5 seconds, then falls back to the
> environment variables, so a server started where nobody can answer the dialog
> behaves as if the keyring were empty rather than hanging.
>
> To avoid the dialog entirely, write the entry from the same Node.js runtime
> that will read it. The value is piped in on standard input, so it appears
> neither in a command line nor in a process environment (`ps -E` shows those).
> The prompt below is plain POSIX, so it behaves the same in `zsh` and `bash`:
>
> ```bash
> npm install -g @lazyants/transkribus-mcp-server # the keyring module ships with it
> cd "$(npm root -g)/@lazyants/transkribus-mcp-server"
> printf 'Transkribus password: ' >&2; stty -echo; IFS= read -r TK_SECRET; stty echo; printf '\n' >&2
> printf '%s' "$TK_SECRET" | node -e '
> const { Entry } = require("@napi-rs/keyring");
> let value = "";
> process.stdin.setEncoding("utf8");
> process.stdin.on("data", (chunk) => { value += chunk; });
> process.stdin.on("end", () => {
> new Entry("transkribus-mcp", "password").setPassword(value);
> console.log("stored");
> });
> '
> unset TK_SECRET
> ```
>
> Repeat with `"user"` in place of `"password"`. A different Node.js
> installation later (a `nvm` switch, say) is a different program to the
> keychain, so the dialog can appear once more for it.
#### Windows (PowerShell)
`cmdkey` can only take the value as a command-line argument, which exposes it in
the process list. Read it from a hidden prompt instead and write it straight into
Windows Credential Manager via `CredWrite`. The credential's target name is
`<account>.<service>` — `user.transkribus-mcp` and `password.transkribus-mcp`
for the default service — which is exactly what the server reads back:
```powershell
Add-Type -Namespace TranskribusKeyring -Name Native -MemberDefinition @'
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct CREDENTIAL {
public uint Flags;
public uint Type;
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
[MarshalAs(UnmanagedType.LPWStr)] public string Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public uint CredentialBlobSize;
public IntPtr CredentialBlob;
public uint Persist;
public uint AttributeCount;
public IntPtr Attributes;
[MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
}
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CredWriteW(ref CREDENTIAL credential, uint flags);
'@
function Set-TranskribusCredential {
param([Parameter(Mandatory)][string]$Account, [Parameter(Mandatory)][string]$Prompt)
$secure = Read-Host -AsSecureString $Prompt
$blob = [Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($secure)
try {
$cred = New-Object TranskribusKeyring.Native+CREDENTIAL
$cred.Type = 1 # CRED_TYPE_GENERIC
$cred.Persist = 2 # CRED_PERSIST_LOCAL_MACHINE
$cred.TargetName = "$Account.transkribus-mcp" # "<account>.<service>"
$cred.UserName = $Account
$cred.CredentialBlob = $blob
$cred.CredentialBlobSize = $secure.Length * 2 # UTF-16 bytes, no terminator
if (-not [TranskribusKeyring.Native]::CredWriteW([ref]$cred, 0)) {
throw "CredWrite failed (Win32 error $([Runtime.InteropServices.Marshal]::GetLastWin32Error()))"
}
Write-Host "Stored '$Account' in Windows Credential Manager."
} finally {
[Runtime.InteropServices.Marshal]::ZeroFreeCoTaskMemUnicode($blob)
$secure.Dispose()
Remove-Variable secure, blob
}
}
Set-TranskribusCredential -Account 'user' -Prompt 'Transkribus user (e-mail)'
Set-TranskribusCredential -Account 'password' -Prompt 'Transkribus password'
```
> Using a custom `TRANSKRIBUS_KEYRING_SERVICE` (e.g. `acme`)? Set `TargetName` to
> `user.acme` / `password.acme` to match — the server looks each value up under
> `<account>.<service>`.
#### Linux
```bash
secret-tool store --label="Transkribus user" service transkribus-mcp username user
secret-tool store --label="Transkribus password" service transkribus-mcp username password
# (each prompts for the value)
```
Once stored, MCP config files need no credentials at all.
### Use environment variables instead
```bash
export TRANSKRIBUS_USER=your-email@example.com
export TRANSKRIBUS_PASSWORD=your-password
```
Or, with a session you already hold:
```bash
export TRANSKRIBUS_SESSION_ID=your-session-id
```
### Environment variables
| Variable | Default | Description |
| --- | --- | --- |
| `TRANSKRIBUS_USER` | — | Account e-mail; used when the keyring has no `user` entry for the configured service |
| `TRANSKRIBUS_PASSWORD` | — | Account password; used when the keyring has no `password` entry |
| `TRANSKRIBUS_SESSION_ID` | — | An existing session id; used when the keyring has no `session-id` entry |
| `TRANSKRIBUS_KEYRING_SERVICE` | `transkribus-mcp` | Keyring service name. Override to connect to several Transkribus accounts at once — run one server instance per account, each with its own service name |
### Processing API credentials
The `transkribus_processing_*` tools talk to a different service with a different
auth scheme, but they need **no extra configuration**: the same
`TRANSKRIBUS_USER` + `TRANSKRIBUS_PASSWORD` are exchanged for an OIDC bearer token
(READCOOP SSO password grant, client `processing-api-client`) and refreshed
automatically. `TRANSKRIBUS_SESSION_ID` does not apply to them.
Two optional overrides:
```bash
export TRANSKRIBUS_ACCESS_TOKEN=your-bearer-token # skip the token exchange entirely
export TRANSKRIBUS_PROCESSING_CLIENT_ID=custom-client # non-default OIDC client
```
## Entry Points
| Command | Domains | Tools |
|---|---|---|
| `transkribus-mcp-server` | All 23 domains | 304 |
| `transkribus-mcp-collections` | Auth, Collections (core/docs/pages/users/crowd/editdecl/credits/stats/labels/activity/tags) | 131 |
| `transkribus-mcp-admin` | Auth, Admin, Credits, Uploads, Labels, Files, System, Root | 62 |
| `transkribus-mcp-transcription` | Auth, Recognition, Layout Analysis, PyLaia, P2PaLA, DU | 47 |
| `transkribus-mcp-users` | Auth, Users, Crowdsourcing, eLearning | 29 |
| `transkribus-mcp-models` | Auth, Models | 26 |
| `transkribus-mcp-jobs` | Auth, Jobs, Actions | 19 |
| `transkribus-mcp-search` | Auth, Search, KWS | 16 |
| `transkribus-mcp-processing` | Processing (Metagrapho) — no legacy auth tools | 4 |
Use split servers to reduce context size — pick only the splits you need.
## Uploading a document
To ingest a document, use this three-step flow:
1. `transkribus_upload_create_structure` — give it `collId`, a `title`, and a `pages` array of
`{ fileName, pageNr }` (one entry per page image you are about to send). Returns an upload with
an `uploadId`.
2. `transkribus_upload_page` — call once per page with the `uploadId` and `imagePath` (a path to a
local image file), optionally `pageXmlPath` for an existing PAGE XML transcript.
3. `transkribus_upload_get_status` — poll with the `uploadId` until the document appears in the
collection.
These upload tools ship in the full `transkribus-mcp-server` and in the `transkribus-mcp-admin`
split — not in `transkribus-mcp-collections`. PDF ingestion is not supported; convert the PDF to
page images first and use the flow above.
## Claude Code
Add to `~/.claude/settings.json`. With the credentials in the OS keyring under
the default service name (recommended), no `env` key is needed:
```json
{
"mcpServers": {
"transkribus": {
"command": "npx",
"args": ["-y", "@lazyants/transkribus-mcp-server"]
}
}
}
```
Or use split servers (pick the splits you need):
```json
{
"mcpServers": {
"transkribus-collections": {
"command": "npx",
"args": ["-y", "-p", "@lazyants/transkribus-mcp-server", "transkribus-mcp-collections"]
},
"transkribus-transcription": {
"command": "npx",
"args": ["-y", "-p", "@lazyants/transkribus-mcp-server", "transkribus-mcp-transcription"]
}
}
}
```
Two Transkribus accounts at once — one instance per account, each pointed at its
own keyring service name:
```json
{
"mcpServers": {
"transkribus-team-a": {
"command": "npx",
"args": ["-y", "@lazyants/transkribus-mcp-server"],
"env": { "TRANSKRIBUS_KEYRING_SERVICE": "transkribus-team-a" }
},
"transkribus-team-b": {
"command": "npx",
"args": ["-y", "@lazyants/transkribus-mcp-server"],
"env": { "TRANSKRIBUS_KEYRING_SERVICE": "transkribus-team-b" }
}
}
}
```
Without a keyring, pass the credentials in `env` instead:
```json
{
"mcpServers": {
"transkribus": {
"command": "npx",
"args": ["-y", "@lazyants/transkribus-mcp-server"],
"env": {
"TRANSKRIBUS_USER": "your-email@example.com",
"TRANSKRIBUS_PASSWORD": "your-password"
}
}
}
}
```
## Claude Desktop
Add to `claude_desktop_config.json`. With the credentials in the OS keyring
(recommended — assumes the default service name `transkribus-mcp`):
```json
{
"mcpServers": {
"transkribus": {
"command": "npx",
"args": ["-y", "@lazyants/transkribus-mcp-server"]
}
}
}
```
Without a keyring:
```json
{
"mcpServers": {
"transkribus": {
"command": "npx",
"args": ["-y", "@lazyants/transkribus-mcp-server"],
"env": {
"TRANSKRIBUS_USER": "your-email@example.com",
"TRANSKRIBUS_PASSWORD": "your-password"
}
}
}
}
```
## Security
- **Use the OS keyring** to keep your password out of config files and shell history entirely (see [Configuration](#configuration))
- **Never commit your credentials** to version control
- Session IDs expire — prefer a user name and password for long-running setups; a session id alone cannot be renewed after a 401
## Disclaimer
This is an unofficial MCP server for Transkribus. The authors are not affiliated with READ-COOP SCE. Use at your own risk.
## Releasing
Releases ship via the GitHub Release event. Maintainer flow:
1. 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.
2. Update `CHANGELOG.md`.
3. 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:
```bash
V=X.Y.Z && PR=<release-pr-number> &&
SHA="$(gh pr view "$PR" --json mergeCommit -q .mergeCommit.oid)" && test -n "$SHA" &&
git fetch origin main && git merge-base --is-ancestor "$SHA" origin/main &&
PKG="$(git show "$SHA:package.json")" &&
test "$(printf '%s' "$PKG" | node -pe 'JSON.parse(require("fs").readFileSync(0,"utf8")).version')" = "$V" &&
CL="$(git show "$SHA:CHANGELOG.md")" &&
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" &&
grep -q '[^[:space:]]' "/tmp/notes-v$V.md" &&
git tag -a "v$V" "$SHA" -m "v$V" &&
git push origin "v$V" &&
gh release create "v$V" --verify-tag --notes-file "/tmp/notes-v$V.md"
```
**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.
Each element is load-bearing:
- **`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`.
- **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.
- **`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.
- **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.
- **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.
- **`--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.
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.
4. 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`.
The workflow skips `npm publish` cleanly if the version is already on npm (cutover guard for releases that were partially published manually).
### npm authentication
Publishing 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.
The binding is configured in the npm web UI (package → Trusted Publishers): provider `GitHub Actions`, organization `lazyants`, repository `transkribus-mcp-server`, workflow `publish-registry.yml`.
## License
[FSL-1.1-MIT](LICENSE) — see [LICENSE](LICENSE) for the full terms. Versions `1.x` remain MIT-licensed.
TDQS
Scored across 304 tools
Many tools have identical or near-identical purposes, e.g. transkribus_coll_remove_doc and transkribus_doc_remove_from_collection both say 'Remove a document from a collection without deleting it'; transkribus_coll_user_remove and transkribus_coll_remove_user duplicate user removal; and multiple metadata update endpoints exist (transkribus_doc_update_metadata vs transkribus_doc_update_metadata_v2). An agent cannot reliably select the correct tool without external knowledge.
All names use snake_case with a consistent transkribus_ prefix, but action ordering and suffixing are inconsistent: transkribus_coll_list vs transkribus_coll_list_paged vs transkribus_coll_list_xml; transkribus_doc_get_metadata vs transkribus_doc_update_metadata_v2; and legacy/new variants coexist. The pattern is readable but not fully predictable.
304 tools is an extreme mismatch for an agent-friendly MCP server. Even accounting for a large underlying API, this vastly exceeds the 50+ threshold where tool selection becomes impractical, and many tools are redundant rather than essential.
The surface covers collections, documents, pages, transcripts, models, recognition, training, search, jobs, users, admin, credits, crowdsourcing, e-learning, and uploads, with create/get/update/delete operations for most resources. It appears to wrap the entire Transkribus API exhaustively, leaving no obvious domain gaps.