mcp-starter-template
mcp-starter-template
多くの公開MCPサンプルが省略しているセキュリティパターンを組み込んだ、リファレンス用のMCPサーバースキャフォールドです。ユーザー単位の認証パススルー(共有サービスアカウントは不使用)、デフォルトで読み取り専用+明示的な書き込みオプトイン、書き込みツールのドライラン・モード、そしてセッションごとの支出/レート上限(無言のno-opやクラッシュではなく構造化された拒否応答)を備えています。すべてのガードレールは、docstringだけでなく自動テストによって裏付けられています。
これらのガードレールが一切ない本番用MCPフォークから、デッドツールハンドラーを監査して取り除いた後、ポートフォリオ用に構築しました。
関連プロジェクトである mcp-issue-tracker は、このまったく同じセキュリティアーキテクチャ(認証パススルー、許可リストで制御された書き込み、ドライラン、レート制限、監査証跡)を、実際のローカル issue-tracker ドメインに適用したものです。同じパターンが一度ならず二度実証されています。
これが存在する理由
ほとんどの公開MCPサーバーの例では、アシスタントをフル権限のサービスアカウントに直結し、ガードレールを設けていません。そのため、もっともな質問に答えるアシスタントが、質問者に見せるべきでないデータを漏らしたり、誰も承認していない書き込みを黙って実行したりすることになります。このリポジトリは、より安全なデフォルトがどのようなものかを示すものであり、一気に通読できる程度の小ささです。
ガードレールと、それぞれが防ぐもの
ガードレール | 場所 | 防ぐもの |
認証パススルー |
| ツール呼び出しが共有/包括的な資格情報で実行されることを防ぎます。すべての呼び出しは この特定の呼び出し元の アイデンティティを解決し、以降のすべてのチェックはそのアイデンティティを使用します(管理者/サービスアカウントではありません)。「誰が尋ねたかに関係なく、アシスタントがサービスアカウントに見えるすべてを閲覧できる」ことを防ぎます。 |
デフォルトで読み取り専用+明示的な書き込み許可リスト |
| 誰かが明示的にレビューして有効化する前に、新しく追加された、または誤って構成された書き込みツールが実行されることを防ぎます。ツールが書き込みとして呼び出し可能になるのは、その名前が |
ドライラン・モード |
| オペレーターが動作を検証している間に、書き込みツールの 実際の 下流の副作用が発動することを防ぎます。ドライランでは、実際のAPIクライアントは一切呼び出されません。これは、レスポンスを検査するだけでなく、クライアントメソッド自体をスパイしてテストで検証されています。「ドライランが密かに書き込んでいたため、本番でテストしてしまった」ことを防ぎます。 |
セッションごとのレート/支出上限 |
| 無制限または暴走したクライアントが支出を消費したり、下流のAPIを過剰に叩いたりすることを防ぎます。セッションのウィンドウ予算(呼び出し数またはコスト単位)が尽きると、そのウィンドウ内の後続のすべての呼び出しは、トリップした1回だけではなく、構造化されたエラーと |
構造化監査ログ |
| セキュリティインシデントが事後的に再構築できないことを防ぎます。すべての呼び出し(許可されたか拒否されたか、読み取りか書き込みか、ドライランか本番か)は、JSON Lines の1レコードとSQLiteの1行として書き込まれます:タイムスタンプ、セッション、ユーザー、ツール、読み取り/書き込み、ドライランフラグ、許可フラグ、レイテンシ。「実際に何が起こったのか分からない」ことを防ぎます。 |
アーキテクチャ
┌─────────────────────────────┐
MCP client ───────▶ │ transport adapter │
(stdio / HTTP) │ mcp_app.py / http_app.py │
└──────────────┬───────────────┘
│ token, session_id, tool_name, args
▼
┌─────────────────────────────┐
│ MCPStarterServer │ server.py — single
│ .call_tool() │ choke point every
└──────────────┬───────────────┘ call passes through
1) resolve tool ────┤
2) authenticate ────┤──▶ AuthMiddleware ──▶ MockIdentityProvider
3) allowlist check ─┤──▶ ToolRegistry
4) rate/spend check ┤──▶ SessionLimiter
5) execute ─────────┤──▶ tool handler (search_docs / create_ticket)
6) audit log ───────┴──▶ AuditLogger ──▶ audit.jsonl + SQLite認証ミドルウェア (
auth.py) は、MockIdentityProvider(identity.py) を介してベアラートークンをUserに解決します。これは明確に開発専用とラベル付けされ、2つの異なるテストユーザー(alice/engineering、bob/sales)と管理者がシードされています。欠落している、または認識されないトークンは拒否されます。フォールバックアイデンティティはありません。ツールレジストリ (
registry.py) は、すべてのツールの読み取り/書き込み分類が置かれる唯一の場所であり、登録時にserver.yamlのtools:セクションと相互チェックされます。コードが宣言している内容と設定が一致しない場合、起動を拒否します。書き込みツールは、その名前がallowed_write_toolsに含まれている場合にのみ 呼び出し可能 になります。どちらにせよlist_tools()では 表示 されるため、レビュアーは現在有効なものだけでなく、完全なサーフェスエリアを確認できます。ドライランラッパー: 各書き込みツールのハンドラーは
dry_run: boolを受け取り、create_ticketの場合、それが true のときはTicketSystemClient.create(代役の下流API)に一切触れず、代わりに合成のDRYRUN-...ID を返します。dryrun.pyは[DRY RUN]監査行を整形します。レート/支出リミッター (
limiter.py) は、session_idごとの固定ウィンドウカウンターです。calls_per_minとcost_per_session(ツールコストはレジストリから取得)は、window_secondsごとに一緒にリセットされます。拒否された呼び出し自体は予算を消費しません。監査ログ (
audit.py) は JSON Lines をファイルに書き込み、すべてのレコードを仕様のデータモデルに一致する SQLite のaudit_logテーブルにミラーリングするため、テキストとして tail したり、SQL でクエリしたりできます。
2つのトランスポートが同じ MCPStarterServer コアをラップしています:
mcp_app.py— 公式の MCP Python SDK (FastMCP) に基づく本格的な MCP stdio サーバーです。stdio は単一のローカルプロセスであり、リクエストごとのヘッダーがないため、tokenとsession_idは明示的なツール引数になります。これはローカル/開発用 MCP サーバーにおける一般的で文書化された簡略化です。実際の MCP クライアント(Claude Desktop、mcpCLI など)が接続するのはこれです。http_app.py— FastAPI の HTTP トランスポートで、トークンは実際のAuthorization: Bearer <token>ヘッダーから、セッションはX-Session-Idから取得します。これは実際のマルチテナント展開で使用される形状です。
ツール例
search_docs(query) -> list[DocResult]— 読み取り専用。小さな静的なインメモリコーパスを検索し、呼び出し元ユーザーの チームに表示可能なドキュメント(または全社ドキュメント)にフィルタリングします。これにより認証パススルーが証明可能になります。alice(engineering)とbob(sales)からの同じクエリは異なる結果を返します。create_ticket(title, body) -> TicketId— 書き込み、許可リストで制御されます。実際のチケットAPI(TicketSystemClient)の代役を務めます。ドライランはそのクライアントに触れる前にインターセプトします。
データモデル
audit_log:timestamp, session_id, user_id, tool_name, read_or_write, dry_run, allowed, latency_ms, error_code, detail— SQLite テーブル+JSON Lines ファイル。すべての呼び出しで書き込まれます。tool_registryの設定(server.yamlのtools:セクション): ツール名ごとにread_only, cost_units, description。session_limits: インメモリのセッションごとのウィンドウ(call_count, cost_used。window_secondsでリセット)。server.yamlのrate_limit:によって駆動されます。
エラー契約
すべての拒否は構造化された MCPError です。{code, message, retry_after?, details?} という形式で、素の例外や黙った no-op はありません。
code | 発生時 |
| トークンが欠落しているか認識されない |
| 書き込みツールが呼び出されたが |
| セッションが |
| 不明なツール名 |
| ハンドラーが指定された引数で |
HTTP では、これらはそれぞれ 401 / 403 / 429 / 404 / 400 にマッピングされ、レスポンスの detail に同じ {code, message, ...} ボディが含まれます。
インストール
Python 3.10+ が必要です(3.10 で開発・テスト済み。仕様では 3.11+ が求められていました。3.10 を使用した理由については、下記の 逸脱 を参照してください)。
git clone https://github.com/HamzaOuadid/mcp-starter-template.git
cd mcp-starter-template
pip install -e ".[dev]"使用方法
ツールレジストリの一覧表示(セキュリティレビュー)
mcp-starter toolsこのリポジトリからの実際の出力:
create_ticket WRITE [DISABLED (not allowlisted)] cost=5 Create a ticket in the downstream ticket system (write, allowlist-gated).
search_docs read-only cost=1 Search internal docs visible to the calling user's team (read-only).「これが防ぐもの」の実演を実行
これはマイルストーン4の成果物です。このリポジトリに同梱されている実際の server.yaml(dry_run: true、空の allowed_write_tools)を使用して、2人のテストユーザーシナリオをエンドツーエンドでシミュレートし、権限境界が維持されることを示します。
mcp-starter demoこのリポジトリの server.yaml(rate_limit.calls_per_min: 5)に対する実際の実行出力:
=== 1. Per-user auth passthrough: same tool, same query, different results ===
alice (engineering): sees docs ['eng-001', 'eng-002', 'all-001']
bob (sales): sees docs ['sales-001', 'sales-002', 'all-001']
=== 2. Missing/invalid identity is rejected, not defaulted ===
token=None -> ok=False error={'code': 'UNAUTHENTICATED', 'message': 'Missing or invalid identity token; call rejected.'}
=== 3. Write tool default posture ===
create_ticket denied: {'code': 'WRITE_NOT_ALLOWED', 'message': "Tool 'create_ticket' is a write tool and is not in allowed_write_tools. Add it to server.yaml's allowlist to enable it."}
=== 4. Rate limit: burst of calls past the cap ===
call 1/6: allowed
call 2/6: allowed
call 3/6: allowed
call 4/6: allowed
call 5/6: allowed
call 6/6: DENIED (RATE_LIMIT_EXCEEDED)
=== Audit log written to <repo>\demo_audit.jsonl ===
{"allowed": true, "detail": "", "dry_run": false, "error_code": null, "latency_ms": 0.0, "read_or_write": "read", "session_id": "demo-burst-session", ...}
{"allowed": true, ...}
{"allowed": false, "error_code": "RATE_LIMIT_EXCEEDED", "detail": "Session 'demo-burst-session' exceeded its rate/spend cap (5 calls or 10 cost units per 60s window).", ...}alice(engineering)と bob(sales)は、共有の全社ハンドブック(all-001)に加えて、互いに素なドキュメントセットを参照します。まったく同じツールとクエリからでも権限境界は維持されます。None トークンは即座に拒否されます。create_ticket は、許可リストがデフォルトで空であるため拒否されます。1分間に5回のセッションでは、6回目の呼び出しが構造化エラーで拒否されます。
書き込みツールを許可リストに登録して実行すると(設定のデフォルトのため、これもドライランになります)、ドライラン応答の形式を確認できます:
mcp-starter demo --allow-writes=== 3. Write tool default posture ===
create_ticket allowed (allowlisted): TicketId(ticket_id='DRYRUN-8ffc09d5', dry_run=True)実際のチケットは作成されません。ドライランモードでは TicketSystemClient.created は空のままです。これは tests/test_dry_run.py で、クライアントメソッド自体をスパイすることによって直接検証されています。
HTTP トランスポートを実行
mcp-starter serve-http --port 8000curl http://127.0.0.1:8000/tools
curl -X POST http://127.0.0.1:8000/tools/search_docs/call \
-H "Authorization: Bearer token-alice" \
-H "X-Session-Id: demo-1" \
-H "Content-Type: application/json" \
-d '{"arguments": {"query": ""}}'
# Write tool, denied by default (empty allowlist):
curl -i -X POST http://127.0.0.1:8000/tools/create_ticket/call \
-H "Authorization: Bearer token-alice" \
-H "X-Session-Id: demo-1" \
-H "Content-Type: application/json" \
-d '{"arguments": {"title": "Broken build", "body": "CI red on main"}}'
# -> HTTP 403, {"detail":{"code":"WRITE_NOT_ALLOWED", ...}}開発用トークン:token-alice(engineering)、token-bob(sales)、token-admin(engineering、管理者フラグ設定済み)。
実際の MCP stdio サーバーを実行
mcp-starter serve-stdioこれにより、実際の FastMCP stdio サーバーが起動します。MCPクライアント(例:mcp CLI の mcp dev、または Claude Desktop の設定)を python -m mcp_starter.mcp_app に向けてください。ツール:search_docs(query, token, session_id)、create_ticket(title, body, token, session_id)、list_tools()。
設定
server.yaml を編集:
dry_run: true # write tools log-and-simulate instead of executing
allowed_write_tools: [] # empty = no write tool is callable, by design
rate_limit:
calls_per_min: 5
cost_per_session: 10
window_seconds: 60
tools:
search_docs:
read_only: true
cost_units: 1
create_ticket:
read_only: false
cost_units: 5実際にチケット作成を有効にするには、create_ticket を allowed_write_tools に追加 かつ dry_run: false に設定します。どちらか一方だけでは、書き込み不可のまま、またはシミュレーションのままになります。
テスト
pytest tests/ -vこのリポジトリからの実際の出力(40テスト、すべて成功):
tests/test_audit_log.py::test_audit_jsonl_reconstructs_a_session PASSED
tests/test_audit_log.py::test_audit_sqlite_table_matches_data_model PASSED
tests/test_audit_log.py::test_query_filters_by_session PASSED
tests/test_audit_log.py::test_rate_limit_denial_is_also_audited PASSED
tests/test_auth_passthrough.py::test_two_users_see_different_results_from_same_tool PASSED
tests/test_auth_passthrough.py::test_missing_token_is_rejected_not_defaulted PASSED
tests/test_auth_passthrough.py::test_invalid_token_is_rejected PASSED
tests/test_auth_passthrough.py::test_empty_string_token_is_rejected PASSED
tests/test_auth_passthrough.py::test_unknown_tool_name_does_not_crash PASSED
tests/test_cli.py::test_tools_command_lists_both_example_tools PASSED
tests/test_cli.py::test_demo_command_runs_full_scenario PASSED
tests/test_cli.py::test_demo_command_with_allow_writes_flag PASSED
tests/test_dry_run.py::test_dry_run_never_invokes_the_real_downstream_client PASSED
tests/test_dry_run.py::test_dry_run_logs_the_would_be_action_with_marker PASSED
tests/test_dry_run.py::test_dry_run_off_with_allowlist_actually_calls_downstream PASSED
tests/test_dry_run.py::test_dry_run_plus_write_tool_never_executes_even_when_allowlisted_repeatedly PASSED
tests/test_dry_run.py::test_read_only_tool_is_unaffected_by_dry_run_flag PASSED
tests/test_http_transport.py::test_list_tools_endpoint PASSED
tests/test_http_transport.py::test_auth_header_passthrough_two_users_differ PASSED
tests/test_http_transport.py::test_missing_auth_header_returns_401 PASSED
tests/test_http_transport.py::test_write_not_allowed_returns_403 PASSED
tests/test_http_transport.py::test_rate_limit_returns_429 PASSED
tests/test_http_transport.py::test_unknown_tool_returns_404 PASSED
tests/test_mcp_stdio.py::test_stdio_server_lists_all_three_tools PASSED
tests/test_mcp_stdio.py::test_stdio_server_two_users_differ PASSED
tests/test_mcp_stdio.py::test_stdio_server_write_tool_denied_by_default PASSED
tests/test_mcp_stdio.py::test_stdio_server_missing_token_rejected PASSED
tests/test_rate_limit.py::test_burst_of_n_plus_one_rejects_the_last_call PASSED
tests/test_rate_limit.py::test_calls_keep_being_rejected_until_window_resets PASSED
tests/test_rate_limit.py::test_cost_cap_is_enforced_independent_of_call_count PASSED
tests/test_rate_limit.py::test_sessions_are_isolated_from_each_other PASSED
tests/test_rate_limit.py::test_rate_limit_via_server_returns_structured_error PASSED
tests/test_rate_limit.py::test_denied_write_does_not_consume_rate_budget PASSED
tests/test_registry_allowlist.py::test_registry_describes_every_tool_classification PASSED
tests/test_registry_allowlist.py::test_all_write_tools_default_to_disabled PASSED
tests/test_registry_allowlist.py::test_write_tool_not_in_allowlist_is_denied PASSED
tests/test_registry_allowlist.py::test_write_tool_in_allowlist_becomes_enabled PASSED
tests/test_registry_allowlist.py::test_registration_refuses_undeclared_tool PASSED
tests/test_registry_allowlist.py::test_registration_refuses_classification_mismatch PASSED
tests/test_registry_allowlist.py::test_unknown_tool_call_is_tool_not_found PASSED
======================== 40 passed, 1 warning in 6.91s ========================関心ごとのカバレッジ:
認証パススルー (
test_auth_passthrough.py) — 2つのモックユーザー、同じツール、異なる結果。欠落・不正・空のトークンは拒否され、決してデフォルトにフォールバックしない。不明なツール名はクラッシュせずクリーンに失敗する。レジストリ / 許可リスト (
test_registry_allowlist.py) — すべてのツールの分類が検査可能。すべての書き込みツールはデフォルトで無効。登録は、設定にないツール、またはコードと設定の分類が一致しないツールを拒否する。不明なツール名はクリーンなTOOL_NOT_FOUNDを返す。ドライラン (
test_dry_run.py) — レスポンスが合成されたものに見えるという確認だけではなく、ドライラン中に実際には呼び出されないことを検証するため、TicketSystemClient.createを直接スパイする。caplogを使って[DRY RUN]マーカーが実際にログに記録されることを確認。ドライランがオフになりツールが許可リストに載ると、実際のクライアントが呼び出されることを確認。ドライラン + 許可リスト済み書き込みの組み合わせを複数回繰り返してリグレッションを防止。読み取り専用ツールがフラグの影響を受けないことを確認。レート制限 (
test_rate_limit.py) — N+1 件のバーストでは、ちょうど (N+1) 番目が拒否される。フェイククロックを使い、ウィンドウの残り期間中も(トリガーした 1 回だけでなく)呼び出しが拒否され続けることを確認。コスト上限は呼び出し回数とは独立に強制される。セッションは互いに分離される。拒否された書き込みはそれ自体がレート予算を消費しない。監査ログ (
test_audit_log.py) — JSONL と SQLite の両方が、誰が/何を/許可されたか/ドライランかを再構築できる十分な詳細を含む完全なセッションを記録する。SQLite の行はセッションでフィルタリング可能。レート制限による拒否も、成功だけでなくトレイルに記録される。両トランスポート (
test_http_transport.py,test_mcp_stdio.py) — 同じガードレールは、トランスポート非依存のコアを通してだけでなく、FastAPI のTestClientや実物のFastMCPサーバーの非同期call_tool/list_toolsを通して駆動した場合も成立する。CLI (
test_cli.py) —toolsとdemo(--allow-writesの有無にかかわらず) がtyper.testing. CliRunnerを通じてエンドツーエンドでエラーなく実行される。
仕様からの逸脱とその理由
Python 3.10(3.11+ ではなく)。 dev/CI 環境は 3.10 を搭載している。このコードベースで 3.11 専用機能は使っていないため、インタプリタのアップグレードを待つのではなく
requires-pythonの下限を緩めた。CI は実際にテストされているバージョンに合わせて 3.10 に固定している。SQLite(PostgreSQL ではない)、監査ログ用に採用。仕様ではどちらでもよいとされている。この環境には Docker/Postgres がない。監査スキーマ(
audit.pyのaudit_logテーブル)は SQLite 専用構文を含まない素の SQL なので、後で Postgres に移行するのはドライバの交換(sqlite3.connect→psycopg2/asyncpg)とAUTOINCREMENT→SERIAL/IDENTITYだけで済み、再設計は不要。stdio 上の認証パススルーは、トランスポートヘッダーではなく明示的な
token引数を使う。 MCP の stdio トランスポートは単一のローカルプロセスであり、リクエストごとのヘッダーがないため、HTTP のAuthorizationヘッダーが HTTP トランスポート(http_app.py)に実際のリクエスト単位の資格情報を与えるのと違って、インターセプトできるものが何もない。トークンを明示的に渡すことで、効果(すべての呼び出しをゲートする、解決済みで非デフォルトのアイデンティティ)は両トランスポートで同一かつテスト可能に保たれる。これは文書化された簡略化であり、stdio に「本物の」マルチユーザー認証があるという主張ではない。本番のマルチユーザー展開では、HTTP トランスポートを実行するか、このコードの上流で実際の資格情報を注入する認証プロキシでラップした stdio トランスポートを実行すべきである。MockIdentityProviderには OAuth/JWT/mTLS なし。 これは静的な token→user ディクショナリであり、仕様自身のリスク注記の通り明らかに開発専用である。実際の検証に差し替えるには、AuthMiddleware.authenticateのトークンルックアップを実際の IdP に対して実装することを意味する。パイプラインの残り(レジストリ、リミッタ、ドライラン、監査)は、Userが返ってくることだけに依存するので影響を受けない。省略: v0.1 git タグ。 マイルストーン 4 では
v0.1リリースにタグを付けることが求められている。このリポジトリはマイルストーンごとの PR ではなくユーザーストーリーごとのコミットであるため、タグ付けは、CI がグリーンでデフォルトブランチに入った後にメンテナーが行うこと(git tag v0.1.0 && git push --tags)として残されており、どこにもプッシュされたことのないリポジトリに自己タグ付けはしない。省略: 永続的な
session_limits/tool_registryテーブルなし。 仕様のデータモデルは、audit_logと並んでsession_limitsとtool_registryをテーブルとして挙げている。tool_registryの分類はserver.yamlにあり(レビュアーが DB テーブルを照会するよりも、おそらくより良い単一の真実源である)、session_limitsはメモリ内のみ(limiter.py)で、シングルプロセススターターには正しいが、再起動を生き延びず、プロセスをまたいでスケールしない。複数のサーバープロセスの背後で実行する前に、最初に修正すべき点(例: Redis をバックエンドにしたカウンタ)として指摘しておく。2つの例示ツール(3以上ではない)。 仕様では「2〜3」とされているが、正確に2つ(読み取り1つ、書き込み1つ)を出荷した。3つ目の読み取り専用ツールは、最初の2つが既にカバーしているガードレールを新たに行使することはないため。
プロジェクトレイアウト
src/mcp_starter/
identity.py mock identity provider (dev-only) + User model
auth.py auth passthrough middleware
config.py server.yaml loading/validation (pydantic)
registry.py tool registry: classification + allowlist enforcement
limiter.py per-session fixed-window rate/spend limiter
audit.py JSONL + SQLite structured audit logging
dryrun.py "[DRY RUN]" audit-line formatting
errors.py structured MCPError + error codes
server.py MCPStarterServer.call_tool — the orchestration core
mcp_app.py real MCP stdio server (official MCP Python SDK)
http_app.py FastAPI HTTP transport (Authorization header passthrough)
cli.py `mcp-starter` CLI: tools / demo / serve-http / serve-stdio
tools/
docs.py search_docs (read-only example tool)
tickets.py create_ticket (write example tool) + TicketSystemClient
tests/ 37 tests across every guardrail and both transports
server.yaml tool classification, allowlist, dry-run, rate limitsライセンス
MIT — LICENSE を参照。
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
A paid remote MCP for CLI tool MCP, built to return verdicts, receipts, usage logs, and audit-ready
Hosted MCP server for agent governance: MCP config audits, injection scans, scope-policy checks.
The official MCP Server from Mia-Platform to interact with Mia-Platform Console
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/HamzaOuadid/mcp-starter-template'
If you have feedback or need assistance with the MCP directory API, please join our Discord server