HTTP 版を動かす¶
Streamable HTTP で 33 ツール。認証方式は MCP_AUTH_MODE で選ぶ。ここに書くのは各モードの
動かし方で、それぞれが実際に何を検証しているかは認証に
ある。リポジトリ側に要るものはInvenioRDM と繋ぐにある——keycloak モード
だけが必要とする、素の InvenioRDM には無いものも含めて。
invenio(PAT・既定) |
keycloak |
|
|---|---|---|
| クライアントが提示するもの | InvenioRDM の個人アクセストークン | OAuth 2.1 で得たトークン |
| 認可サーバ | 不要 | Keycloak(realm mcp) |
| ブラウザでのログイン | 起きない | 起きる |
| scope の出どころ | InvenioRDM のロールから導く | トークンの scope クレーム |
| トークン交換 | なし(宛先が同じ) | RFC 8693 で aud=invenio-api へ |
| MCP 2026-07-28 適合 | ×(認可サーバが無い) | ○ |
PAT モード¶
InvenioRDM が自己署名証明書なら、ルート CA を ./ca.crt に置く(CA_FILE で変更可)。
compose ファイルはそれをシステムの CA 束に足す。束ごと置き換えると、サーバが行う
他のすべての HTTPS 通信が壊れるからである。
InvenioRDM 側でトークンを発行する。
invenio tokens create -n mcp -u <メールアドレス>
# UI なら <InvenioRDM>/account/settings/applications/tokens/new/
ロールから scope を導く¶
個人アクセストークンに scope の概念は無いので、GET /api/me が返すロールから
組み立てる。素の InvenioRDM が持つロールをそのまま使うので、追加の語彙も拡張も要らない。
| 設定 | 既定 | 意味 |
|---|---|---|
MCP_INVENIO_BASE_SCOPES |
mcp:read mcp:write |
認証できた全員に与える |
MCP_INVENIO_CURATE_ROLES |
admin |
このロールにだけ mcp:curate も与える |
MCP_INVENIO_VERIFY_TTL |
60 |
/me の結果を保持する秒数 |
/me の結果は TTL のあいだ保持する。毎リクエスト往復させないためである。
失敗はキャッシュしない——トークンを消したら効かなくなる必要がある。
PAT モードに audience による分離は無い
提示されるトークンがそもそも InvenioRDM のトークンなので、交換する先が無く、 リポジトリに直接使うことも止められない。それが認可サーバを要らなくすることの代価 である。そこが問題になるなら keycloak モードを使う。
なぜ InvenioRDM を認可サーバにしないのか¶
invenio-oauth2server は PKCE も認可サーバメタデータ(RFC 8414)も動的登録も持たない。
/.well-known/* は 404 を返し、コードに code_challenge は無い。認可サーバに据えると、
本サーバが在りもしないメタデータを捏造し、PKCE 無しを飲み、クライアントを手で登録する
足場を用意することになる——そのうえで、それでも MCP 適合には届かない。
どちらの道でも InvenioRDM に届くのは InvenioRDM のトークンであり、違うのは入手方法 だけである。ならば手間の少ないほうを採る。
keycloak モード¶
export KC_BASE=https://<keycloak>
export KC_ADMIN_PASSWORD=<Keycloak の管理者パスワード>
export MCP_SERVER_SECRET=<mcp-server クライアントのシークレット>
export MCP_RESOURCE=https://<mcp>/mcp
python3 keycloak/setup_mcp_realm.py
KC_ADMIN_PASSWORD と MCP_SERVER_SECRET に既定値は無い。推測可能な値で黙って
動いてしまうより、その場で止まるほうがよい。同じ MCP_SERVER_SECRET をサーバにも渡す。
setup_mcp_realm.py は既存の realm を削除して作り直す
作り直すと Keycloak の sub が変わり、InvenioRDM の UserIdentity が壊れて
既存の利用者の紐付けが切れる。稼働中の realm に何かを足すときは、ensure_realm() を
通さず ensure_scope() などを個別に呼ぶこと。
デモ利用者(researcher・rdmadmin)は既定では作らない。要るなら
MCP_DEMO_USERS=yes を付ける。パスワードが弱いので、捨ててよい realm に限ること。
keycloak/setup_gakunin_idp.py は学認 SAML ブローカを足す(任意)。
デプロイはデプロイにまとめてある。
canonical URI¶
MCP_RESOURCE はクライアントが実際に叩く URL と一字一句同じでなければならない。
resource(RFC 8707)・resource(RFC 9728)・トークンの aud が揃うのは、この1本の
文字列によってである。末尾のスラッシュ1つ、クライアントが 127.0.0.1 と言うところの
localhost 1つで、すべてのトークンが検証に落ちる。
監査ログ¶
標準出力に 1行1 JSON。コンテナのログ収集にそのまま載る。
{"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 は call・tool_error(ツールは走って失敗した——これは HTTP 200 + isError で
返る)・deny(チャレンジを返した)のいずれか。トークンは決して出さない。
止めるには MCP_AUDIT=off。