TL;DR: 私たちは 922 個の npm 公開 MCP サーバに対して npx -y <package> を実行し、JSON-RPC の initialize と tools/list 呼び出しを送って、それらが何をしたかを記録しました。359 件は応答しました。563 件は 15 通りの異なる失敗で、MCP 自体というより npm のパッケージング事情がよく分かる結果でした。
261 サーバを壊した stderr のシグネチャ
[stderr] connecting to upstream...
[stderr] (no further output)
[timeout after 120s]
これは現時点の npm MCP エコシステムで最も多い単一の失敗パターンです。プロセスは起動し、パッケージが読み込まれ、コンストラクタが実行されます。そして、プロトコルに応答する前にサーバが上流(upstream)の API に電話しようとします。イントロスペクション用の実行器は 120 秒待って諦めます。261 サーバ、公開された全体の 28% は、自身のスタートアップを越えることができませんでした。
あなたが MCP サーバをメンテしていて、initialize 中に外部へ接続するなら、このカテゴリに入ります。修正は、最初のツール呼び出しが来るまで上流接続を遅延させることです。
実際に私たちが実行したこと
プロトコルには、発見(ディスカバリ)メソッドがあります: tools/list。initialize の後に送ると、サーバは公開しているすべてのツールの完全な JSON スキーマを返します。README のスクレイピングは不要、LLM の解釈も不要、推測も不要です。これは、MCP クライアントが接続したときに行うのとまったく同じことです。
実行器はパッケージごとに次を行います:
1. spawn: npx -y <package>(stdio 経由)
2. write: {"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2024-11-05",
"capabilities":{},
"clientInfo":{"name":"introspector","version":"1.0"}}}
3. read: initialize の応答
4. write: {"jsonrpc":"2.0","method":"notifications/initialized"}
5. write: {"jsonrpc":"2.0","id":2,"method":"tools/list"}
6. read: ツール一覧
7. kill: stdin を閉じる、SIGTERM
並列度は 8、サーバごとのタイムアウトは 120 秒で、922 パッケージの総ウォールタイムは約 25 分でした。出力はサーバごとに JSONL 1 行で、ステータス、ツール数、そして(利用可能な場合は)すべてのツールの完全な入力スキーマを含みます。
15 個の失敗バケット
クリーンなツール配列を返さなかったすべてのサーバは、終了コード、stderr、そして(もしあれば)JSON-RPC エラーを調べることで分類しました。内訳:
| Status | Count | Meaning |
|---|---|---|
ok |
359 | クリーンな tools/list の応答 |
init_timeout |
261 | 起動したが initialize に応答しなかった
|
npm_install_generic |
172 |
npx -y 自体が失敗 |
needs_cli_args |
54 | 使用法エラーで終了 |
needs_env_var |
42 | 汎用の環境変数が不足 |
broken_install |
11 | 不正なパッケージ、main または bin が不適切
|
error |
8 | 非ゼロ終了で分類不能なクラッシュ |
needs_setup_step |
3 | 最初に CLI のセットアップウィザードの実行が必要 |
needs_slack_token |
2 | SLACK_BOT_TOKEN がないと拒否 |
needs_azure_creds |
2 | Azure 認証がないと拒否 |
tools_list_timeout |
2 | initialize には応答したが tools/list で停止(ハング) |
needs_google_creds |
2 | Google 認証がないと拒否 |
needs_stripe_key |
1 | STRIPE_API_KEY がないと拒否 |
needs_config_file |
1 | 設定ファイルのパスがないと拒否 |
needs_external_runtime |
1 | インストールされていないバイナリにシェルアウト |
needs_openai_key |
1 | OPENAI_API_KEY がないと拒否
|
資格情報(クレデンシャル)ウォールのバケット(すべての needs_*)は合計 109 サーバで、公開セットのほぼ 12% です。README を解析してエージェントのツール一覧を埋めている場合、これらのサーバはインデックス内で 0 ツールサーバとして登録されます。それらは 0 ツールサーバではありません。5 ツール、12 ツール、40 ツールのサーバで、あなたが渡していないキーを待っているだけです。
Windows 固有の落とし穴
実行器は Windows 上で動いています。問題になることが 2 つあります。
npx のスポーン。 Windows 上の npx は npx.cmd に解決されるため、バッチスクリプトになります。シェルなしでの Node の child_process.spawn は .cmd を呼び出しません。その結果、where npx が PATH 上にあることを示していても、フラットな ENOENT が発生します。修正は次の通り:
const child = spawn("cmd", ["/c", "npx", "-y", pkg], {
stdio: ["pipe", "pipe", "pipe"],
});
この同じパターンは、Windows のすべての claude_desktop_config.json に現れます。自作の MCP クライアントを書くなら、これも必要です。
UTF-8 の stdio。 Windows のデフォルトのコードページは UTF-8 ではありません。MCP サーバが、非 ASCII 文字(ドイツ語のウムラウト、em ダッシュ、カールした引用符など)を含むツール説明を書き込み、それを UTF-8 を強制せずに stdio から読み取ると、tools/list の途中で JSON のパースエラーになります。UTF-8 を強制してください:
proc = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
errors="replace",
)
今回の実行で、3つのサーバーがASCII以外の文字を含むツール説明を持っていました。encodingフラグがなければ、3つとも error として誤って数えられていたはずです。
サーバー作者に対してこれが伝えていること
データから見えてきたいくつかのパターンを、サーバーを配布する誰にでも向けてまとめます:
initializeの間に上流へ接続しないでください。 サーバーが起動時にAPIへ連絡する場合、イントロスペクションに失敗し、さらに、資格情報を最初に消費することなくツールを列挙しようとするクライアントも失敗します。最初のツール呼び出し時に遅延接続してください。ツール一覧を表示するためにCLI引数を要求しないでください。 54のサーバーが、何を公開しているのかをあなたに伝える前に、使用法エラーで終了します。設定パスが必要なら、環境変数から取得するか、それなしで
tools/listを受け付けてください。READMEで環境変数を文書化してください。 分類器は、stderrに明確な「missing X」という行を書いたサーバーに対して、資格情報の名前を正常に特定できました。そうでないものは、汎用の
needs_env_varバケットに入ってしまいました。このバケットはあなたのバグ報告キューです。実際の
binエントリを同梱してください。broken_installの11のサーバーでは、存在しないファイルを指しているpackage.jsonがありました。あるいは、require時にクラッシュするmainフィールドがありました。いずれも資格情報は必要ありませんでした。必要だったのは、公開時(publish時)のスモークテストです。
完全なデータセット
9,922個のツールスキーマと、922件のサーバー状態(status)行は、CC-BY-4.0のもとで HuggingFace に automatelab/mcp-servers-tool-catalog としてあります。パイプラインのソースは AutomateLab-tech/mcp-tool-catalog で、GitHub Actions を通じて毎月1日に再実行されます。
製品ページ: https://automatelab.tech/products/datasets/mcp-tool-catalog/
from datasets import load_dataset
servers = load_dataset(
"automatelab/mcp-servers-tool-catalog", "servers", split="train"
)
# 到達できないものだけを、失敗モードごとにまとめる
from collections import Counter
print(Counter(r["status"] for r in servers if r["status"] != "ok"))
失敗バケットに入っていて、本来そこに入るべきではない場合は、パイプラインのリポジトリでPRを開いてください。次回の月次実行でそれが拾われます。



