TL;DR
-
rust-mcp-serverv0.4.1 をcargo installして、MCP の stdio トランスポートに素の JSON-RPC で直接つなぎ、33 個のツールを実測しました -
tools/listの応答は 65,934 バイト。ツールを 1 つも呼ばない段階で、これだけがエージェントのコンテキストに乗ります - 一方で ツール応答そのものは素の
cargoとほぼ同サイズ(cargo clippyは 1,317 バイト対 1,352 バイト)。削減されるのは出力量ではなく手順です -
--disable-toolを 20 個並べて 13 ツールに絞ると 28,118 バイト(-57%) まで落ちました - 依存 193 パッケージのクレートでは、
cargo-metadataツールが 946,959 バイト を素通しで返す一方、workspace-infoは 202 バイト でした。どのツール名を選ばせるかでコンテキスト消費が 4,600 倍変わります - 引数名を間違えると エラーにならず空文字が返る(
isError: falseのまま)という挙動を踏みました
はじめに
対象読者は、Rust プロジェクトを AI コーディングエージェントに触らせたい開発者です。
エージェントに cargo check を走らせるだけなら、シェル実行を許可すれば済みます。わざわざ MCP サーバーを挟む意味はどこにあるのか。「構造化されて返ってくるから効率が良い」という説明はよく見かけますが、それが本当なら 出力バイト数が減っているはず です。手元で測ってみると、減っていたのは別のところでした。
rust-mcp-server は何をするものか
rust-mcp-server は、cargo / rustc / rustup の各コマンドを Model Context Protocol のツールとして公開する Rust 製サーバーです。crates.io の API で確認したところ、最新版は 0.4.1(2026-08-01 公開)、累計ダウンロードは 5,462 でした。
curl -sS "https://crates.io/api/v1/crates/rust-mcp-server" | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(d['crate']['newest_version'], d['crate']['updated_at'])"
# 0.4.1 2026-08-01T14:23:52.238639Z
公開されるツールは cargo-build / cargo-check / cargo-test / cargo-clippy といったコア機能に加え、cargo-deny-check(セキュリティアドバイザリとライセンス確認)、cargo-machete(未使用依存の検出)、rustc-explain(エラーコードの解説)などが並びます。詳細は公式 README にあります。
エージェント側から見た構造はこうなります。
検証環境とセットアップ
検証は Linux コンテナ上で行いました。
| 項目 | 値 |
|---|---|
| cargo | 1.94.1 (29ea6fb6a 2026-03-24) |
| rustc | 1.94.1 (e408947bf 2026-03-25) |
| rust-mcp-server | 0.4.1 |
| Node.js(クライアント側) | v22.22.2 |
| 測定日 | 2026-09-03(JST) |
インストールは cargo install 一発です。HTTP トランスポートも試せるように http フィーチャーを有効にしました。
cargo install rust-mcp-server --features http
# Finished `release` profile [optimized] target(s) in 1m 15s
# Installed package `rust-mcp-server v0.4.1` (executable `rust-mcp-server`)
release ビルドで 1 分 15 秒でした。依存に axum と clap が入るぶん、単機能の CLI としては重めですが、待てない長さではありません。
検証対象には、警告が確実に出る小さなクレートを用意しました。
// src/main.rs
fn compute_total(items: &Vec<i32>) -> i32 {
let mut total = 0;
for i in 0..items.len() {
total += items[i];
}
total
}
fn main() {
let items = vec![1, 2, 3];
let unused_value = 42;
println!("total = {}", compute_total(&items));
}
unused_value で unused_variables、&Vec<i32> で clippy の ptr_arg、インデックスループで needless_range_loop が出ます。
MCP に素の JSON-RPC でつなぐ
MCP クライアントを実装したエディタを経由すると、どこまでがサーバーの挙動でどこからがクライアントの加工か分かりません。そこで stdio に直接 JSON-RPC を流す最小クライアントを書きました。
// mcp-probe.mjs(抜粋)
import { spawn } from 'node:child_process';
const child = spawn(binPath, ['--workspace', workspace], {
stdio: ['pipe', 'pipe', 'pipe'],
});
let buf = '';
const pending = new Map();
child.stdout.on('data', (chunk) => {
buf += chunk.toString();
let idx;
while ((idx = buf.indexOf('\n')) >= 0) {
const line = buf.slice(0, idx).trim();
buf = buf.slice(idx + 1);
if (!line) continue;
const msg = JSON.parse(line);
if (msg.id && pending.has(msg.id)) {
pending.get(msg.id)(msg);
pending.delete(msg.id);
}
}
});
let nextId = 1;
function send(method, params) {
const id = nextId++;
return new Promise((resolve) => {
pending.set(id, resolve);
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
});
}
const init = await send('initialize', {
protocolVersion: '2025-06-18',
capabilities: {},
clientInfo: { name: 'mcp-probe', version: '0.1.0' },
});
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }) + '\n');
const list = await send('tools/list', {});
改行区切りの JSON をやり取りするだけなので、100 行に満たないコードで足ります。initialize の応答は 1 ミリ秒、返ってきた protocolVersion は 2025-06-18 でした。
実測1: ツール定義だけで 65,934 バイト
tools/list の応答を丸ごとバイト数で測ると、こうなりました。
{
"initMs": 1,
"protocolVersion": "2025-06-18",
"toolCount": 33,
"listPayloadBytes": 65934
}
33 ツールで 65,934 バイト。1 ツールあたり約 2KB です。内訳を見ると、cargo-check 単体のスキーマが 3,618 バイトあり、入力プロパティは 29 個ありました。
| ツール | スキーマのバイト数 | 入力プロパティ数 |
|---|---|---|
cargo-check |
3,618 | 29 |
cargo-clippy |
3,650 | 29 |
rustc-explain |
702 | 2 |
cargo-check のプロパティには all_features / bench / benches / bin / bins / example / examples / exclude / jobs / keep_going …と、cargo check --help にあるフラグがほぼ網羅されています。網羅性は正しさの証ですが、エージェントは 1 回の cargo check のために 29 個の選択肢を読まされます。
この 66KB は、ツールを 1 つも呼ばない段階でコンテキストに載ります。会話が始まる前の固定費です。
実測2: 応答サイズは素の cargo とほぼ変わらない
では実際にツールを呼んだ結果はどうか。同じクレートに対して、素の CLI と MCP 経由でバイト数を比較しました。
| 実行内容 | 素の CLI | MCP 経由 | 差 |
|---|---|---|---|
cargo clippy(警告 3 件) |
1,352 バイト | 1,317 バイト | -35 |
cargo check(コンパイルエラー) |
902 バイト | 934 バイト | +32 |
cargo metadata --no-deps |
974 バイト | 204 バイト | -770 |
clippy と check は誤差の範囲です。MCP 側は Executed command: の 1 行と結果マーカーが増えるぶんだけ差し引きが起きています。実際の応答はこうでした。
Executed command: `cargo clippy --locked --quiet`
warning: unused variable: `unused_value`
--> src/main.rs:11:9
(中略)
✅ cargo-clippy: Success
RECOMMENDATION: Run #cargo-clippy with the `fix` and `allow_dirty` options to automatically fix the issues
コンパイラの出力はそのまま素通しで、前後にメタ情報が付く形です。「構造化されて返る」という言い方から JSON 化された診断結果を想像していましたが、返ってくるのはテキストでした。
唯一はっきり差が出たのが workspace-info です。内部で cargo metadata --format-version 1 --no-deps を実行しつつ、返すのはパッケージ名・マニフェストパス・ターゲット種別に絞った 204 バイトでした。
Executed command: `cargo metadata --format-version 1 --no-deps`
✅ workspace-info: Success
{"packages":[{"name":"demo-crate","manifest_path":"/tmp/mcp-lab/demo-crate/Cargo.toml","target_types":["bin"]}]}
素の cargo metadata は依存なしでも 974 バイト、依存込みだと 1,125 バイトあります。ただしこれは 1 クレートの最小構成での話です。差が本当に効くのはどこかを確かめるため、serde / tokio / clap / axum / reqwest を追加した別のクレート(推移的依存を含めて Cargo.lock に 193 パッケージ)でも同じ 3 者を測り直しました。
| 実行内容 | 依存 0 個のクレート | 依存 193 パッケージのクレート |
|---|---|---|
素の cargo metadata(依存込み) |
1,125 バイト | 946,681 バイト |
MCP cargo-metadata ツール |
(同上相当) | 946,959 バイト |
MCP workspace-info ツール |
204 バイト | 202 バイト |
実プロジェクト規模になると cargo metadata は 900KB を超えました。ここで 同じサーバーの中でも挙動が分かれます。cargo-metadata ツールは cargo metadata --format-version 1 --quiet --locked をそのまま実行して 946,959 バイトを丸ごと返します(素の出力との差は 278 バイト)。一方 workspace-info は依存が 193 個あっても 202 バイトのままでした。
つまり「MCP を挟めば出力が小さくなる」のではなく、サーバーの実装者が要約するツールを別に用意しているかどうか で決まります。エージェントに cargo-metadata を選ばせると 900KB がコンテキストに流れ込みますが、workspace-info を選ばせれば 202 バイトで済みます。ツール名の選択がそのままコンテキスト消費を 4,600 倍変えるということです。
実測3: 失敗の伝え方は素の CLI より明確
コンパイルエラーを仕込んで cargo-check を呼ぶと、応答に終了コードが載りました。
Executed command: `cargo check --locked --quiet`
error[E0308]: mismatched types
--> src/main.rs:11:42
(中略)
❌ cargo-check: Failure, exit code: 101
JSON-RPC の結果側にも isError: true が立ちます。シェル実行だとエージェントは標準エラー出力の中身から成否を推測するか、echo $? を別途走らせる必要があります。ここは MCP を挟む実利がはっきり出た部分でした。
続けて rustc-explain にエラーコードを渡すと、rustc --explain E0308 の全文が 761 バイトで返ります。エラー検出から解説の取得までを同じ経路で完結できるのは、シェル実行にはない導線です。
もう 1 つ気づいたのが --locked です。ログを見ると、サーバーが組み立てるコマンドには既定で --locked が付いていました。エージェントが cargo check を走らせたつもりで Cargo.lock が更新される事故を防ぐ設計で、これはシェル実行を許可しただけでは得られない安全側の既定値です。
実測4: 引数名を間違えても成功扱いで返ってくる
最初、rustc-explain を {"code": "E0308"} で呼びました。結果がこれです。
{ "tool": "rustc-explain", "callMs": 1, "isError": false, "payloadBytes": 0 }
isError は false のまま、応答テキストは空でした。正しい引数名は error_code で、スキーマの required にもそう書かれています。
=== rustc-explain | schemaBytes: 702
required: ["error_code"]
props: error_code, toolchain
必須引数が欠けているのにバリデーションエラーが返らず、空文字が成功として返る。エージェントから見ると「ツールは成功したが説明は得られなかった」という解釈になり、rustc --explain にはこのエラーコードの解説が無いのだ、と誤った結論に進む余地があります。ツールを呼ぶ前にスキーマの required を読ませる作りになっているかが、ここでは効いてきます。
コンテキストを削る: --disable-tool
66KB の固定費は --disable-tool で削れます。使わないツールを 20 個無効化してみました。
rust-mcp-server --workspace /path/to/project \
--disable-tool cargo-deny-check --disable-tool cargo-deny-init \
--disable-tool cargo-deny-list --disable-tool cargo-deny-install \
--disable-tool cargo-machete --disable-tool cargo-machete-install \
--disable-tool cargo-hack --disable-tool cargo-hack-install \
--disable-tool cargo-insta-update-snapshots \
--disable-tool rustup-update --disable-tool rustup-toolchain-add \
--disable-tool cargo-new --disable-tool cargo-package \
--disable-tool cargo-clean --disable-tool cargo-expand \
--disable-tool cargo-doc --disable-tool cargo-search \
--disable-tool cargo-info --disable-tool cargo-update \
--disable-tool cargo-generate_lockfile
結果は以下のとおりです。
| 構成 | ツール数 |
tools/list バイト数 |
|---|---|---|
| 既定 | 33 | 65,934 |
| 20 ツール無効化 | 13 | 28,118(-57%) |
残したのは workspace-info / cargo-metadata / rustc-explain / cargo-list / cargo-build / cargo-check / cargo-test / cargo-add / cargo-remove / cargo-clippy / cargo-tree / rustup-show / cargo-fmt の 13 個です。日常的な開発ループはこれで回ります。
--no-recommendations を付ければ、応答末尾の RECOMMENDATION: 行も止められます。エージェントに次の一手を示唆させたいかどうかで選ぶ設定です。
著者視点の発見ポイント
実際に測ってみて、事前の想定と一番ずれたのは 「MCP 化すると出力が減る」ではなかった ことです。減っていたのは出力量ではなく、エージェントが踏む手順とその不確実性でした。整理するとこうなります。
-
手順が減る:
cargo checkを走らせるのに、シェルの引数を組み立てて exit code を別途確認して、という往復が 1 回のtools/callに畳まれます -
失敗が明示される:
isErrorと終了コードが応答に含まれるので、成否の推測が不要です -
安全側の既定値が付く:
--lockedが既定で付き、Cargo.lockの意図しない更新を防ぎます -
出力量は減らない: コンパイラ出力は素通しなので、警告が 100 件あれば 100 件ぶんそのまま返ります。
cargo-metadataに至っては 900KB を丸ごと返します -
要約するツールは別に用意されている:
workspace-infoのように要約専用のツールが混ざっており、どちらをエージェントに選ばせるかで消費が桁違いに変わります - 固定費は増える: ツール定義の 66KB が会話の頭に乗ります
トークン予算という一点だけで見るなら、5 回程度しかツールを呼ばないセッションでは、66KB の固定費を回収できません。逆に、長時間のリファクタリングのようにビルドとテストを何十回も往復するセッションでは、手順の削減と失敗判定の確実さが効いてきます。判断の分かれ目は「そのセッションで cargo を何回叩くか」であって、「構造化されているか」ではありませんでした。
導入するなら、まず --disable-tool で使うツールだけに絞るところから始めるのが素直です。cargo-deny や cargo-machete はツール自体を別途インストールしないと動かない(そのための *-install ツールが用意されている)ので、使う予定がないなら定義ごと落として構いません。ワークスペース把握には cargo-metadata ではなく workspace-info を使わせたいので、この 2 つを同時に有効にしておくかどうかも先に決めておくと安全です。
使いどころの整理
rust-mcp-server は、エージェントに cargo を安全に触らせるための薄い層でした。魔法のような圧縮は起きませんが、--locked の既定付与と isError による明示的な失敗通知は、シェル実行を丸ごと許可する構成より確実に一段安全です。
一方で、既定の 33 ツール構成をそのまま使うと、cargo を数回叩くだけのセッションではコンテキストの持ち出しになります。--disable-tool はドキュメントでは補助的な機能に見えますが、実際には 導入時に最初に触るべき設定 だと感じました。
なお、ここに書いた数値はすべて 2026-09-03 時点・rust-mcp-server 0.4.1・cargo 1.94.1 での測定です。ツール数もスキーマも版によって変わるため、導入時は tools/list のバイト数を自分の環境で測り直すことをおすすめします。今回使った 100 行足らずの probe スクリプトで足ります。
関連記事
- Coro Code 実践 — Rust製OSSコーディングエージェントをDeepSeekバックエンドで動かす
- mcp-use 実践 — 複数MCPサーバーを1つのエージェントに束ねPythonでLLM非依存のツール実行を実装する
- MCP新仕様がGo SDKで通らない、Stateless=trueに加え必須が3つあった