![[Typescript] MCP ServerでRustのデバッグをする[CodeLLDB]](https://images.ctfassets.net/ct0aopd36mqt/4AJd3M26yescaNaY21CYmQ/9730e95c53a7b3049c6da6d4c0b1dec9/mcp.png?w=3840&fm=webp)
[Typescript] MCP ServerでRustのデバッグをする[CodeLLDB]
Introduction
Model Context Protocol (MCP)によってAgent(Claude codeやcodexなど)から
各種リソースにアクセスしたりツール実行を簡単に実行できるようになりました。
じゃあデバッガもつかってみよう、ということで
本記事では、MCPを使ってRustプログラムをデバッグする方法について解説します。
Claude CodeなどがRustプログラムのデバッグを行えるようにすることで、
変数の確認やステップ実行などを対話的に制御できるようになります。
この記事では以下の内容について解説します。
- CodeLLDB (LLDB) を使った Rust デバッグ方法
- Debug Adapter Protocol (DAP) の基本
- MCP サーバーの実装パターン
- 実際に動作するMCP Rustデバッガ実装例
Terminology
Model Context Protocol (MCP)
MCPは、LLMとツール・データソースを接続するための標準化されたプロトコルです。
以下のようにClaude CodeからMCPサーバーを使います。
┌─────────────┐
│ AI Agent │ ← Claude, GPT-4など
│ (Claude) │
└──────┬──────┘
│ MCP Protocol
│
▼
┌─────────────┐
│ MCP Server │ ← 今回実装するRustデバッガー
└─────────────┘
ブレークポイントの設定やステップオーバーなどの処理をツールとして登録し、
Claude Codeなどがそれらのツールをつかって処理をしていきます。
Debug Adapter Protocol (DAP)
DAPは、
エディタとデバッガーの間の通信プロトコルです。VS Codeなどで使用されています。
MCPサーバからDAPをつかってデバッガに接続します。
┌──────────────┐
│ MCP Server │
└──────┬───────┘
│ DAP Protocol
│
▼
┌──────────────┐
│ CodeLLDB │ ← LLDBのDAPアダプター
└──────┬───────┘
│ LLDB Commands
▼
┌──────────────┐
│ LLDB │ ← 実際のデバッガエンジン
└──────┬───────┘
│ Process Control
▼
┌──────────────┐
│ Rust Program │
└──────────────┘
LLDB (Low Level Debugger)
LLDB)は、LLVMプロジェクトの一部として開発されたデバッガです。
概要は以下のとおり。
- 開発: 主にAppleが主導する LLVM プロジェクト
- 対応言語: C, C++, Objective-C, Swift, Rustなど
- プラットフォーム: macOS, Linux, Windows他
ブレークポイントの設定やステップ実行、式の評価などの
機能をもっています。
Rustが使える環境であれば、lldbのRust用ラッパーであるrust-lldbが使えます。
こちらは所有権に起因する問題の解析とかもできるみたいです。
LLDBは↓みたいにCLIで使います。
% lldb ./target/debug/hoge-program
(lldb) target create "・・・・・・・"
Current executable set to '・・・・・' (arm64).
#ブレークポイント設定
(lldb) breakpoint set --file main.rs --line 10
(lldb) run
#変数表示
(lldb) frame variable
#ステップ実行
(lldb) step
CodeLLDB
CodeLLDBはVS Code用のLLDBフロントエンドであり、
LLDBデバッガを DAPでラップしたものです。
これはVS Code拡張としてインストールされますが、バイナリ自体はVS Codeに依存してません。
Rust, C, C++, Swiftなどに対応しています。
LLDB を DAP 経由で利用可能にするアダプターとして動作し、
JSON-RPCでプログラムから制御可能になります。
以下のようなコマンドでVS Codeにインストールできます。
% code --install-extension vadimcn.vscode-lldb
インストール後、macの場合に実行ファイルは以下に配置されます。
~/Library/Application\ Support/Code/User/globalStorage/vadimcn.vscode-lldb/lldb/extension/adapter/codelldb
なお、codelldbは単独でもインストール可能ですが、あまり一般的ではないようです。
※GitHubからダウンロード可能
┌──────────────┐
│ DAPクライアント│ ← VS Codeや自作MCPサーバーなど
└──────┬───────┘
│ DAP Protocol
┌──────▼──────┐
│ CodeLLDB │ ← DAPアダプター(サーバー)
└──────┬──────┘
│
┌──────▼──────┐
│ LLDB │ ← デバッガ
└─────────────┘
本記事ではCodeLLDBをDAPサーバーとして起動し、
MCPサーバーがDAPクライアントとしてアクセスします。
アーキテクチャ全体
全体像は↓のような感じです。
┌──────────────────────────────────────────────────────┐
│ AI Agent (Claude Code) │
└───────────────────┬──────────────────────────────────┘
│ MCP (stdin/stdout)
│
┌───────────────────▼──────────────────────────────────┐
│ Rust Debugging MCP Server │
│ ┌───────────────────────────────────────────────┐ │
│ │ tools: │ │
│ │ - debug_launch() │ │
│ │ - debug_step_over() │ │
│ │ - debug_get_variables() │ │
│ │ - debug_continue() │ │
│ └──────────────────┬────────────────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────────────┐ │
│ │ RustDebugger Class │ │
│ │ - DAPメッセージング │ │
│ │ - イベント処理 │ │
│ │ - スタック/変数管理 │ │
│ └──────────────────┬───────────────────────────┘ │
└────────────────────┬─────────────────────────────────┘
│ DAP (stdin/stdout)
│
┌────────────────────▼─────────────────────────────────┐
│ CodeLLDB │
│ ┌────────────────────────────────────────────┐ │
│ │ DAP Adapter │ │
│ │ - initialize, launch, continue │ │
│ │ - breakpoints, stackTrace, variables │ │
│ └────────────────┬───────────────────────────┘ │
└────────────────────┬─────────────────────────────────┘
│ LLDB Commands
│
┌────────────────────▼─────────────────────────────────┐
│ LLDB (Debugger Engine) │
│ - ブレークポイント管理 │
│ - プロセス制御 (step, continue) │
│ - メモリ/レジスタ操作 │
│ - DWARF デバッグ情報解析 │
└────────────────────┬─────────────────────────────────┘
│ Process Control (ptrace/等)
│
┌────────────────────▼──────────────────────────────────┐
│ Rust Binary (Debug Build) │
└───────────────────────────────────────────────────────┘
Environment
- MacBook Pro (14-inch, M3, 2023)
- OS : MacOS 15.7
- Node : v24.9.0
- LLDB : lldb-1703.0.31.2
- Claude Code : 2.0.19
- VSCode : 1.105.1 (Universal)
Setup
では、RustデバッグMCPサーバーを実装するための準備をしていきましょう。
まずはCodeLLDBをインストールします。
さきほど↑で言ったようにVSCode拡張としてインストール。
% code --install-extension vadimcn.vscode-lldb
RustとNodeも環境があることを確認。
% rustc --version # Rust 1.70+
% node --version # Node.js 20+
プロジェクトセットアップ
Githubにサンプル実装のリポジトリがあるのでcloneします。
% git clone https://github.com/nakamura-shuta/mcp-rust-debugger-example
各種ビルドを実行します。
% cd mcp-rust-debugger-example
# 依存関係のインストール
npm install
# TypeScript のビルド
npm run build
# サンプル Rust プログラムのビルド (デバッグモード)
cd example
cargo build
Implements MCP
MCP Rustデバッガのコードを見てみます。
DAP メッセージングの実装
送信側の処理です。
DAPはContent-Lengthヘッダ付きJSONを使用します。
/** src/rust-debugger.ts */
private async sendRequest(command: string, args: unknown): Promise<unknown> {
const seq = this.requestSeq++;
const message = {
type: 'request',
seq,
command,
arguments: args
};
return new Promise((resolve, reject) => {
this.pendingRequests.set(seq, { resolve, reject });
const content = JSON.stringify(message);
const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
// CodeLLDBのstdinに送信
this.codelldbProcess?.stdin?.write(header + content);
setTimeout(() => {
if (this.pendingRequests.has(seq)) {
this.pendingRequests.delete(seq);
reject(new Error(`Request ${command} timed out`));
}
}, 30000);
});
}
受信側では以下のような実装。
parseMessagesは、受信したDAPのメッセージをパースします。
/** src/rust-debugger.ts */
private parseMessages(): void {
while (true) {
const headerEnd = this.messageBuffer.indexOf('\r\n\r\n');
if (headerEnd === -1) break;
const header = this.messageBuffer.slice(0, headerEnd);
const match = /Content-Length: (\d+)/.exec(header);
if (!match) break;
const contentLength = parseInt(match[1], 10);
const messageStart = headerEnd + 4;
const messageEnd = messageStart + contentLength;
if (this.messageBuffer.length < messageEnd) break;
const content = this.messageBuffer.slice(messageStart, messageEnd);
this.messageBuffer = this.messageBuffer.slice(messageEnd);
const message = JSON.parse(content);
this.handleMessage(message);
}
}
DAPメッセージはストリームで届くため、1回のイベントで複数メッセージがきたり
1つのメッセージが分割されて届くこともあります。
この処理により、届いたデータからメッセージ境界を判定してます。
デバッグの開始
Rustプログラムのデバッグ開始時には
決まった順序でリクエストを送る必要があります。
/** src/rust-debugger.ts */
async launch(config: LaunchConfig): Promise<ToolResponse> {
// 1. CodeLLDBプロセス起動
this.codelldbProcess = spawn(codelldbPath, []);
// 2. initializeリクエスト
await this.sendRequest('initialize', {
clientID: 'mcp-rust-debugger',
adapterID: 'lldb',
pathFormat: 'path'
});
// 3. initialized イベントを待機
await new Promise<void>(resolve => {
this.once('initialized', resolve);
});
// 4. launch リクエスト(プログラム起動)
this.sendRequest('launch', {
program: config.program,
args: config.args ?? [],
stopOnEntry: true // 最初の行で停止
});
// 5. stopped イベントを待機してthreadIdを取得
await new Promise<void>(resolve => {
const onStopped = (body: DAP.StoppedEvent['body']) => {
this.threadId = body.threadId; // ← threadIdを保存
this.off('stopped', onStopped);
resolve();
};
this.on('stopped', onStopped);
});
// 6. configurationDone
await this.sendRequest('configurationDone', {});
return { success: true };
}
stopOnEntry: true
でプログラム開始時に自動で停止するようにします。
その後、threadIdを保存しておきます。(ステップ実行で必要)
変数の取得
変数取得は、スタックトレース → スコープ → 変数 の順に取得します。
/** src/rust-debugger.ts */
async getVariables(): Promise<ToolResponse<{ variables: Variable[] }>> {
// 1. スタックトレース取得
const stackTraceResponse = await this.sendRequest('stackTrace', {
threadId: this.threadId,
startFrame: 0,
levels: 1 // 最上位フレームのみ
}) as DAP.StackTraceResponse;
const frames = stackTraceResponse.body.stackFrames ?? [];
if (frames.length === 0) {
return { success: true, data: { variables: [] } };
}
this.frameId = frames[0].id;
// 2. スコープ取得
const scopesResponse = await this.sendRequest('scopes', {
frameId: this.frameId
}) as DAP.ScopesResponse;
const scopes = scopesResponse.body.scopes ?? [];
const allVariables: Variable[] = [];
// 3. 各スコープの変数を取得
for (const scope of scopes) {
if (scope.variablesReference) {
const vars = await this.getVariablesFromReference(scope.variablesReference);
allVariables.push(...vars);
}
}
return { success: true, data: { variables: allVariables } };
}
以下のような階層構造になってます。
stackTrace (threadId指定)
└─> stackFrames[0]
└─> scopes (frameId指定)
├─> Local Variables (variablesReference)
│ └─> variables
└─> Arguments (variablesReference)
└─> variables
ブレークポイントの設定
指定したファイル・行番号にブレークポイントを設定する処理も実装しています。
setBreakpointsコマンドでCodeLLDBにブレークポイントを設定し、
DAPのレスポンスから必要な情報を取得して変換しています。
/** src/rust-debugger.ts */
async setBreakpoint(config: SetBreakpointConfig): Promise<ToolResponse<{ breakpoints: BreakpointInfo[] }>> {
try {
const response = await this.sendRequest('setBreakpoints', {
source: { path: config.file },
breakpoints: [{ line: config.line }]
}) as DebugProtocol.SetBreakpointsResponse;
const breakpoints: BreakpointInfo[] = (response.body.breakpoints ?? []).map((bp, index) => ({
id: bp.id ?? index,
file: config.file,
line: bp.line ?? config.line,
verified: bp.verified ?? false //ブレークポイントが有効か確認
}));
return { success: true, data: { breakpoints } };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
実行継続とブレークポイントヒット
次のブレークポイントまで実行を行うためにcontinue
を使います。
これは、DAPのcontinueコマンドを送って指定したスレッドで実行を再開します。
continueリクエストが送信されるとプログラムが走り始め、
その後stoppedイベントが受信されてブレークポイントで停止します。
以下のように処理が流れるイメージです。
[stopped at entry]
↓
continue() 呼び出し
↓
continue リクエスト送信
↓
プログラム実行中...
↓
ブレークポイントヒット
↓
stopped イベント受信
↓
continue() から return
↓
[stopped at breakpoint] ← 変数取得可能
continue
実行後、ブレークポイントでプログラムが停止すると stopped
イベントがemitされます。
このイベントをハンドリングすることにより、停止位置で変数を取得できます。
ブレークポイントで停止していないと、変数の取得などの操作が失敗します。
/** src/rust-debugger.ts */
async continue(): Promise<ToolResponse> {
if (!this.threadId) {
return { success: false, error: 'No thread ID' };
}
try {
await this.sendRequest('continue', { threadId: this.threadId });
// 次のstopped イベントを待機
await new Promise<void>((resolve) => {
this.once('stopped', () => resolve());
});
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
ステップ実行
ステップオーバー処理は、現在の行を実行して次の行に移動します。
DAPのnextコマンドをつかってステップオーバーを行います。
continueと同じく、「実行コマンド送信 → stopped イベント待機」パターンで、
確実に停止してから次の操作に移れるようにしています。
実行フロー例は以下です。
10: let x = 10; ← 現在ここ
11: let y = 20;
12: let sum = x + y;
stepOver() 呼び出し
↓
next リクエスト送信
↓
10行目を実行
↓
stopped イベント受信
↓
11: let y = 20; ← 次の行で停止
/** src/rust-debugger.ts */
async stepOver(): Promise<ToolResponse> {
await this.sendRequest('next', { threadId: this.threadId });
// 次のstopped イベントを待機
await new Promise<void>(resolve => {
this.once('stopped', () => resolve());
});
return { success: true };
}
next
リクエスト送信後、必ず stopped
イベントを待機することにより、
次の行で確実に停止します。
MCP ツールの登録
src/index.tsはMCPサーバーのエントリーポイントです。
エージェントが呼び出せるデバッグツールを登録やMCPサーバの起動を行います。
ツールの登録は以下のように、各種デバッグ用ツールを登録しています。
server.registerTool(
'debug_launch', // ツール名
{
title: '...', // 表示名
description: '...', // 説明
inputSchema: {...} // 引数のスキーマ
},
async (rawArgs) => { // 実行ハンドラ
// 1. 引数バリデーション
const args = launchSchema.parse(rawArgs);
// 2. RustDebuggerのメソッド呼び出し
const result = await rustDebugger.xxxxxx(args);
// 3. MCP形式のレスポンス
return {
content: [{ type: 'text', text: '...' }]
};
}
);
Try it out
では実際にMCP InspectorからMCPデバッグツールを呼び出して
Rustプログラムをデバッグしてみましょう。
サンプルプログラムのビルド
まずはサンプルのRustプロジェクトをbuildします。
% cd /path/your/mcp-rust-debugger-example/example
% cargo build
生成されるバイナリはexample/target/debug/rust-debug-sample
になります。
MCP Inspectorで動作確認
MCP Inspectorは、MCPサーバーをブラウザで対話的にテストできるツールです。
これをつかって動作確認してみます。
# MCP Inspectorを起動
npx @modelcontextprotocol/inspector node dist/index.js
実行するとコンソールにURLが表示されるので、ブラウザでアクセスします。
デバッグフローの実行
実際に実行してみましょう。
画面左のconnectボタンを押し、List toolsボタンをおします。
① デバッグセッションの開始
debug_launchを選択し、以下の内容を入力します。
{
"program": "/path/your/example/target/debug/rust-debug-sample",
"cwd": "/absolute/path/to/example"
}
Tool Resultに「デバッグセッションを開始しました」と表示されればOKです。
② ブレークポイントの設定
debug_set_breakpointツールを実行します。
{
"file": "/absolute/path/to/example/src/main.rs",
"line": 8
}
結果は以下。
ブレークポイントを設定しました:
/path/to/example/src/main.rs:8 (設定完了)
main関数内の変数計算部分(8行目)にブレークポイントを設定します。
break
③ 実行継続(ブレークポイントまで)
debug_continueツールを実行します。
結果は以下。
実行を継続しました
プログラムは設定したブレークポイント(8行目)まで実行され、そこで停止します。
④ 変数の確認
debug_get_variablesツールを実行すると以下の結果になりました。
変数 (6件):
x: 10 (int)
y: 20 (int)
sum: 30 (int)
General Purpose Registers: {...} (unknown)
Floating Point Registers: {...} (unknown)
Exception State Registers: {...} (unknown)
ブレークポイントで停止した時点での変数の値が確認できます。
一応、レジスタ情報も取得されます。
⑤ ステップオーバー
ステップオーバーはdebug_step_overツールを実行します。
以下の結果になりました。
次の行へ移動しました
⑥ セッション終了
最後はdebug_terminateを実行して終了します。
デバッグセッションを終了しました
デバッグセッションを終了し、対象プロセスも停止します。
なお、これらの操作はすべてClaude Codeなどのエージェントからも実行可能です。
そうすることで対話的にデバッグを実施できたりします。
※Claude Codeから実行したい場合、このへん参照
Summary
本記事では、MCPサーバーを使ってRustプログラムをデバッグする実装を解説しました。
とりあえずの基本機能のみですが、対話的にデバッグできるので、ちゃんと実装すれば
「@hoge.rs#13にブレークポイント設定してXXという条件のときの変数Zの状態をチェックして」
みたいな確認も簡単にできたりするかもしれません。(未確認)
なお、RustだけでなくDAP対応デバッガが存在するか、
言語固有のデバッグプロトコル(ex.Chrome DevTools Protocol)があれば
他の言語のデバッグもできます。
(TypescriptはCDP使ってデバッグできました)
GithubにもMCPサーバのデバッガがいろいろあるので、確認してみてください。