0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust製MCPサーバーをcargoに繋いだら、ツール定義だけで66KB消えた

0
Posted at

TL;DR

  • rust-mcp-server v0.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-info202 バイト でした。どのツール名を選ばせるかでコンテキスト消費が 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 秒でした。依存に axumclap が入るぶん、単機能の 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_valueunused_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 ミリ秒、返ってきた protocolVersion2025-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

clippycheck は誤差の範囲です。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 }

isErrorfalse のまま、応答テキストは空でした。正しい引数名は 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-denycargo-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 スクリプトで足ります。

関連記事

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?