← ブログに戻る

MCPサーバーを公開する前のpre-flight — 4層で採点する mcp-scorecard の設計

MCP エコシステムは、レビュールールが追いつかない速度で成長しました。この数週間で私は自分のMCPサーバーを3本 (domain-pre-flight / rag-db-advisor / opencut-mcp) 公開しましたが、毎回 publish の前に同じチェックリストを頭の中で回していました。登録されているだけで毎ターン何トークン消費しているか。ツール記述はLLMに正しく選ばれる程度に絞れているか。description 文字列に秘密情報が漏れていないか。ツール名は他の何かと衝突していないか。

そのチェックリストをツールにして PyPI に公開しました。mcp-scorecard は登録済みMCPサーバーの表層に4種の pre-flight チェックを走らせ、A–Fの grade をツール別 findings 付きで返します。CLI は1コマンドで、同じ4層は5本のMCPツールとしても提供されるので、Claude Code の中のLLMがセッションを離れずに別のMCPを監査できます。

pip install mcp-scorecard
mcp-scorecard scan ./your-server.py

成果物: kenimo49/mcp-scorecard v0.1.1 を PyPI に公開 (MIT)。デモGIFと MCP-Scan / MCP Inspector との比較表を含むフル LP は /ja/products/mcp-scorecard/ にあります。

この記事の目的は、4つの層それぞれの設計意図と、なぜこの順番で並んでいるか、初回に自分のMCPを scan したときに何が引っかかったかを書くことです。すでに MCP-ScanMCP Inspector を回している場合は、末尾に「この3つの位置関係」を書きました。

なぜMCPに pre-flight が必要か

MCPサーバーをレビューするときの既定の視点は、call time に危険があるという想定です。tool response 内の prompt injection、shell exec 経由の credential 漏れ、モデルを誤誘導する tool shadowing。どれも実在する事故で、MCP-Scan が runtime で担当しています。ただ、この視点がすくいきれないのは、ツールが呼ばれる にLLMが見ている surface です。

tools/list の各エントリは毎ターンモデルに送られます。理由は単純で、どのツールを呼ぶか決めるためには一覧が必要だからです。これがサーバーを登録している受動的なコストです。そして各エントリの description フィールドは、モデルが選ぶために読むテキストです。これがサーバーの受動的な品質です。両方とも著者が書いたときに決まり、runtime のトラフィックに依存しません。両方とも runtime scanner からは見えません。

pre-flight はここを扱います。あなたのMCPについてLLMが何を見るか、リクエストが1本も飛ぶ前に。以下の4層は、それをコンパクトに答える1つの試みです。

Layer A — Passive Footprint (受動トークンフットプリント)

冗長なMCPサーバーが1本あるだけで、1ターンあたり静かに 5,000 トークン以上を消費することがあります。しかも誰もツールを呼んでいない状態でです。消費の内訳は3つです。各ツールの description 文字列、各ツールの入力 JSON Schema、そしてツール名そのもの。3つとも tools/list に連結されて、毎ターンモデルに送られます。

Layer A はこれを tiktokencl100k_base エンコーディングで数えます (OpenAI GPT-4 系のトークナイザーですが、Claude 系の近似としても実用範囲です)。出力はツール別の内訳とグローバルな initial_token_load です。

                per-tool footprint (top 10 by total)
┏━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┓
┃ Tool                   ┃ Desc tok ┃ Schema tok ┃ Name tok ┃ Total ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━┩
│ check_domain           │      182 │         88 │        2 │   272 │
│ list_typo_permutations │       79 │         54 │        5 │   138 │
│ check_trademark        │       83 │         41 │        4 │   128 │
│ check_handles          │       76 │         40 │        2 │   118 │
└────────────────────────┴──────────┴────────────┴──────────┴───────┘
findings
  · 1 tool(s) have description > 150 tokens: check_domain

これは domain-pre-flightmcp-scorecard で scan した実出力です。合計 initial_token_load は 4ツールで 656 トークン、GREEN 帯に十分収まっています。ただし1本 (check_domain) が引っかかっています。description が 182 トークンで、150 トークンの bloat しきい値を超えているためです。こういうサーバーを5本同時に登録すると、受動的な消費は積み上がり、「使わないMCPを棚卸ししよう」というレビューが入るまで見えないままになります。

しきい値はキャリブレーション済みですが神聖不可侵ではありません。v0.1 では以下を採用しています。

計測値GREENYELLOWORANGERED
initial_token_load≤ 1500≤ 4000≤ 8000> 8000
Description tokens per tool≤ 150超えたら bloat として flag
Tool count≤ 15≥ 30 で tool_count warning

initial_token_load の RED しきい値は、サーバー1本が長い会話のコンテキスト予算を食い始める境界です。150トークンの bloat しきい値は、description がドキュメントのように長くなり始める境界です。

bloat findings を消すいちばん素直な方法は、description を単一目的の1文にトリムして、例文はツール表層から docs に逃がすことです。「単一目的の1文」に何を書けばよいかは、次の Layer B が答えます。

Layer B — Use-Case Scoping (ユースケースの絞り込み)

Passive footprint は「LLMが どれだけ を見るか」を答えます。Layer B は「見ているものが どれだけ的を絞っているか」を答えます。想定する事故はこうです。モデルにもっともらしい説明のツールが3本あって、どれを使うか決めきれず、ヒューリスティクスで選んで外す。これは security の話ではなく、あなたのMCPの UX チェックです。ユーザーはLLMです。

v0.1 では4つのルールが走ります。

Vague verbs (曖昧な動詞)。 description が run / handle / process / manage / execute / do / perform / work with / deal with / 汎用の helper / utility で始まっているとaction hintで flag します。上の scan で私自身の check_domainrun が引っかかった例が分かりやすい。description が「run pre-flight checks on a domain」と書かれていて、ドキュメントとしては読めますが、何を run するかの signal がLLMに残りません。「check availability, run WHOIS, resolve DNS, and score TLD risk for a domain candidate」と書き直すと、同じ情報量で、しかも兄弟ツールと弁別する材料がモデルに渡ります。

When-to-use trigger (使うときのトリガー)。 各ツールの description に、明示的な使用文脈フレーズ (use this / use when / call when / useful for および日本語の 使う / 使用 / 呼び出) があるかをチェックします。私の domain-pre-flight 4本中3本にはこれが無く、上の scan の ORANGE 帯はこれについての苦情でした。この trigger は魔法の keyword ではなく、「この状況では私が正しいツールです」とモデルに宣言するマーカーです。trigger が無いと、コンテキスト推論だのみでツールが呼ばれることが増え、これは安定した pattern ではありません。

Overlap detection (重複検出)。 ツール記述のペアごとに、共通するステムトークンの重複率を計ります。2本のツールが description で同じような content words を使っていると、モデルにはだいたい同じ話に見え、実質ランダム選択になります。v0.1 はしきい値超えペアを flag しますが fail はしません。修正は普通、片方の description を書き直して、この2本を分ける軸を名指しすることです。

Naming style consistency (命名スタイルの一貫性)。 バンドルされたチェックが各ツール名を snake_case / camelCase / kebab-case / flat に分類し、複数スタイルが混在するサーバーを flag します。混在自体はトークンを直接消費しませんが、モデルの注意予算を消費します。同じ list 内で命名規則が切り替わると、モデルはどのツールがどの規則だったかを覚える予算を割きます。その予算は本来 reasoning に使えたはずです。

Layer B は、このツールが自分のMCPを厳しく採点する層です。mcp-scorecard 自身のMCPサーバーに mcp-scorecard scan をかけると overall grade D (ORANGE) が返ります。preflight_* ツールの docstring に scoping findings が5件立つためです。うち4件は vague verb (handle / process / manage / execute) がヒットしていますが、これらは docstring 内で「scoping check が catch する例」として挙げているので、文脈的には false positive です。プロダクトLPの caveat セクションにその旨も書きました。ただ、ここでホワイトリストを入れて自分の docstring だけ検出を止めることはしませんでした。それは同じ物差しをツール自身に当てるルールを無効化する類の細工で、ルールセットを役立たずにするからです。

Layer C — Security own rules (セキュリティ独自ルール)

Layer C は既存のツールともっとも overlap する層なので、意図的にスコープをいちばん狭く取っています。v0.1 は宣言済み表層に対して3系統の独自ルールを走らせるだけで、MCP-Scan がよく担当している runtime カバレッジは v0.2 の wrap 用に残してあります。

description 内の prompt injection マーカー。 ツール description は毎ターンLLMが読むテキストなので、そこに命令調が入ると、著者の意図と関係なくモデルが steer されます。既知の注入パターン (ignore previous instructions / disregard / system: prefix / 閉じ </system> タグ / you must および同等の日本語表現) を catch します。このルールは書籍『MCP実践セキュリティ』(インプレス NextPublishing) のレビューチェックリストから直接来ています。本のチェック項目のうち Layer C に関わるものを、そのままルール化しました。

Tool shadowing (ツール名の shadowing)。 リモートオブジェクト一覧を返す ls という名前のツールがあると、モデルが「list the files here」のような意図が緩い指示を読んだときに、shell の ls の代わりにこちらを呼びます。ルールは、ツール名を一般的な shell / filesystem / process 名 (ls / cat / rm / cp / curl / sudo / exec / eval / shell / execute) と照合し、衝突を flag します。修正は namespace hygiene で、list_objectss3_ls にリネームすれば曖昧さは消えます。

description 内の hardcoded secrets (ハードコードされた秘密情報)。 AWS access key (AKIA...) / GitHub PAT (ghp_...) / OpenAI key (sk-...) / Anthropic key (sk-ant-...) / Google API key (AIza...) / Slack token (xox...) / PEM ブロック (-----BEGIN PRIVATE KEY-----) / hardcoded Bearer パターンの regex sweep です。想定する事故は「credential がリポジトリに commit された」ではありません。そちらは gitleaks / trufflehog の担当領域で、意図的に重複させていません。ここでの想定事故は「毎ターンLLMに送られる description 文字列に credential が含まれている」で、これは別種のより深刻な漏れです。credential がモデルのコンテキスト経由でリクエストごとに exfiltrate されるためです。

v0.1 でこの層が しない こと: フルソースの secret scan、live traffic に対する runtime injection、protocol レベルの validation。3つとも既存ツールの守備範囲で、v0.2 のロードマップは runtime 側について MCP-Scan の wrap を提供することで、書き直しではありません。

Layer D — Name Safety (命名安全性)

Layer D は、著者が既に決めた命名が publish して安全かを問う層です。4つのルールがあります。

Case collision (大文字小文字衝突)。 バンドルされたブランド名約50件と既知MCP名 23件を lowercase に正規化し、候補名を同じ正規化ルールで照合します。GitHub-mcpgithub-mcp とこの規則で衝突し、findings は「PEP 503 style normalization で case 衝突。パッケージ化後は github-mcp と区別不能」となります。

Brand Levenshtein similarity (ブランドとの編集距離)。 既知ブランドとの Levenshtein 距離が2以下 (対象ブランドが4文字以上) の候補名で typosquat 警告を上げます。これは domain-pre-flight の domain typosquat 検出と同じルールを、パッケージ名の文字数にスケールダウンしたものです。既知MCPのリストは意図的に小さくしています。ヒットしたときの修正はリストを大きくすることではなく、他と隣接する必要のない名前を選び直すことです。

Separator variants (区切り文字の異形)。 mcp_scorecard / mcp-scorecard / mcpscorecard は PyPI の PEP 503 ルールで同じパッケージ名に正規化されるので、既存のPyPI名の異形を選ぶと namespace が衝突します。この層は publish 前にそれを catch します。実は mcp-scorecard 自身にも同種の問題がありました。元の名前 mcp-preflight は既に別プロジェクトに占有されており、mcp-pre-flight は PyPI 側の類似判定で拒否されました。Layer D はこの2つ目の候補を、PyPI validator にぶつける前に「既存MCPに近すぎる」と flag していたはずです。

Namespace hygiene (命名空間の衛生)。 候補名に対する regex が、小文字 kebab-case と optional な @vendor/tool scoping を検査します。大文字混在や特殊文字が含まれる名前は「kebab-case または @vendor/tool を推奨」で flag します。この項は装飾的で、findings は warn レベルで error ではありません。存在理由は、LLM のツール選択ヒューリスティクスが一貫した書式で書かれた名前でよく働くからで、そこは Layer B に循環します。

既知MCP 23件のリストは静的データとして bundle しています。明らかなブランド名 (github / google / anthropic / openai / aws / cloudflare / stripe / hubspot) に加えて、よくあるMCP名 (mcp-scan / mcp-inspector / mcp-validator と、このツール自身の名前) を含みます。追加は src/mcp_preflight/data/known_brands.py への素の PR で足せます。

この4層でカバーしないもの

上の4層はMCPサーバーの LLM-facing quality を扱います。モデルが何を見るか、それがどれだけコストになるか、正しいツールを選べるか、名前が既知のものと衝突しないか。この4層がカバーしないのは、MCP-Scan がよく扱っている runtime security surface です。call time におけるツール 出力 内の prompt injection、exec 時の credential handling、request time の tool shadowing。これらは live サーバーを読む別 scanner の仕事で、mcp-scorecard v0.1 は AST と manifest の read で、対象を実行しません。プロダクトLPの compare セクション に、MCP-Scan と MCP Inspector との3方向の位置関係を、各ツールが cover する軸と cover しない軸を正直にマークして書いてあります。

CI 向けの gate はロードマップに入っています。--format sarif 出力と ORANGE / RED での非ゼロ exit code はすでに v0.1 で動きますし、JSON schema はローカル用途に耐える程度に安定しています。v0.1 で まだ不安定 なのは band しきい値そのものです。もっと多くの実MCPで scan を回して、現在の数字が緩すぎるか厳しすぎるかを見ながら動かす予定です。v0.1 に alpha ラベルを付けているのはそこが理由です。

Install

pip install mcp-scorecard              # CLI + library
pip install "mcp-scorecard[mcp]"       # + MCP server (stdio)

mcp-scorecard scan ./your-server.py    # 4層フルスキャン
mcp-scorecard footprint ./server.py    # Layer A のみ
mcp-scorecard scoping ./server.py      # Layer B のみ
mcp-scorecard security ./server.py     # Layer C のみ
mcp-scorecard name my-new-mcp          # Layer D のみ (候補名に対して)

TypeScript / Node のMCPは manifest JSON 経由でサポートしています。tools/list 出力 (または保存したコピー) を JSON として --target に渡すと、Layers A / B / C / D すべてが同じルールで走ります。AST 経路だけが Python 専用で、層のロジック自体は言語非依存です。

Claude Code / Cursor / Windsurf の中のLLMに別のMCPを監査させるためのMCPサーバーとしての使い方は次の通りです。

{
  "mcpServers": {
    "mcp-scorecard": {
      "command": "mcp-scorecard-mcp"
    }
  }
}

あとはモデルにパスを渡して「score this MCP」と頼むだけで、5本のツール (preflight_scan / preflight_footprint / preflight_scoping / preflight_security / preflight_name_check) が CLI と同じ4層に routing されます。

関連

mcp-scorecard は、エコシステムに既にある runtime security ツールと protocol ツールに対する LLM-facing quality のコンパニオンで、CLI + MCP の命名線では domain-pre-flight の pre-flight コンパニオンです。もう一つの位置付けは、刊行予定の書籍『MCP実践セキュリティ』(インプレス NextPublishing) とのコンパニオンで、本がチェック項目を教え、ツールがそれを走らせます。

ルールとしきい値に対するフィードバックは v0.1 alpha が今いちばん欲しいものです。Issues は kenimo49/mcp-scorecard にどうぞ。