Running the HTTP server¶
33 tools over Streamable HTTP, with the authentication method chosen by
MCP_AUTH_MODE. This page is how to run each mode; what each one actually verifies is
in Authentication. What the repository side has to
provide is in Connecting to InvenioRDM — including the one thing keycloak
mode needs that stock InvenioRDM does not have.
invenio (PAT, default) |
keycloak |
|
|---|---|---|
| What the client presents | an InvenioRDM personal access token | a token obtained through OAuth 2.1 |
| Authorization server | not needed | Keycloak (realm mcp) |
| Browser login | never happens | happens |
| Where scopes come from | derived from InvenioRDM roles | the token's scope claim |
| Token exchange | none (same audience) | RFC 8693, into aud=invenio-api |
| Conforms to MCP 2026-07-28 | no — there is no authorization server | yes |
PAT mode¶
cd http
cp .env.example .env # point INVENIO_API / INVENIO_UI at your instance
docker compose up -d --build
If InvenioRDM uses a self-signed certificate, put the root CA at ./ca.crt (or set
CA_FILE). The compose file appends it to the system CA bundle rather than
replacing it — replacing the bundle outright breaks every other HTTPS call the server
makes.
Issue a token on the InvenioRDM side:
invenio tokens create -n mcp -u <email>
# or the UI: <InvenioRDM>/account/settings/applications/tokens/new/
Scopes from roles¶
A personal access token carries no scopes, so they are assembled from the roles
returned by GET /api/me. This uses stock InvenioRDM roles — no extra vocabulary, no
extension to install.
| Setting | Default | Meaning |
|---|---|---|
MCP_INVENIO_BASE_SCOPES |
mcp:read mcp:write |
granted to anyone who authenticates |
MCP_INVENIO_CURATE_ROLES |
admin |
only these roles also get mcp:curate |
MCP_INVENIO_VERIFY_TTL |
60 |
seconds to cache the result of /me |
The result of /me is cached for the TTL so that every request does not make a round
trip. Failures are never cached — deleting a token has to take effect.
PAT mode has no audience separation
The token you present is an InvenioRDM token, so there is nothing to exchange it into, and nothing stops it being used directly against the repository. That is the price of not needing an authorization server. Use keycloak mode where it matters.
Why not make InvenioRDM the authorization server?¶
invenio-oauth2server has no PKCE, no authorization server metadata (RFC 8414) and no
dynamic client registration — /.well-known/* returns 404 and there is no
code_challenge in the code. Putting it in that role would mean this server fabricating
metadata that does not exist, accepting the absence of PKCE, and needing somewhere to
register clients by hand — and it still would not reach MCP conformance.
Either way, what reaches InvenioRDM is an InvenioRDM token. Only the way of obtaining it differs. So the simpler route wins.
keycloak mode¶
export KC_BASE=https://<keycloak>
export KC_ADMIN_PASSWORD=<Keycloak admin password>
export MCP_SERVER_SECRET=<secret for the mcp-server client>
export MCP_RESOURCE=https://<mcp>/mcp
python3 keycloak/setup_mcp_realm.py
KC_ADMIN_PASSWORD and MCP_SERVER_SECRET have no defaults: stopping immediately
beats silently running on a guessable value. Pass the same MCP_SERVER_SECRET to the
server.
setup_mcp_realm.py deletes and recreates the realm if one exists
Recreating it changes Keycloak's sub, which breaks UserIdentity in InvenioRDM
and severs existing user links. To add something to a live realm, call
ensure_scope() and friends individually instead of going through
ensure_realm().
The demo users (researcher, rdmadmin) are not created by default. Add
MCP_DEMO_USERS=yes if you want them. Their passwords are weak, so only do this in a
throwaway realm.
keycloak/setup_gakunin_idp.py adds the GakuNin SAML broker, which is optional.
Deployment is covered in Deployment.
The canonical URI¶
MCP_RESOURCE must be character-for-character the URL the client actually calls.
That single string is what makes resource (RFC 8707), resource (RFC 9728) and the
token's aud line up. A trailing slash or localhost where the client says 127.0.0.1
is enough to make every token fail validation.
The audit log¶
One JSON object per line on stdout, so it lands in whatever collects container logs:
{"ts":"2026-08-29T12:00:00+0900","event":"call","path":"/mcp",
"method":"tools/call","tool":"create_record","status":200,"ms":412,
"sub":"3f2b...","azp":"mcp-client","scope":"mcp:read mcp:write"}
event is call, tool_error (the tool ran and failed — those come back as HTTP 200
with isError) or deny (the challenge was returned). The token is never logged.
Set MCP_AUDIT=off to turn it off.
Checking it¶
# the authorization specification, PASS/FAIL
MCP_RESOURCE=https://<mcp>/mcp MCP_TEST_USER=... MCP_TEST_PASSWORD=... \
python3 conformance/mcp_client.py
# all three file transfer paths, 16 assertions
MCP_RESOURCE=https://<mcp>/mcp INVENIO_UI=https://<invenio> \
python3 conformance/verify-mcp-files.py
# the authorization flow with nothing but curl, as a walkthrough
bash conformance/curl-tour.sh