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?

#180 CursorにBrainfuckデバッガを自律操作させてみる(MCP × DAP)

0
Posted at

はじめに

以前の記事では、VSCodeのDebug Adapter Protocol(DAP)に準拠したBrainfuckデバッガを作成しました。

そこで思ったのが、最近のAIエディタ(Cursor等)は、ファイルの読み書きやターミナルコマンドの実行を自律的に行えます。であれば、デバッガの操作も自律的にできそうです。

調べてみたところ、DAP x MCPの組み合わせは既に試行している方がある程度いるみたいだったのですが、せっかくなので前回作ったDAP向けのMCPサーバーを自作してみました。

やりたいこと

通常、デバッグは人間が以下のような操作を繰り返します:

  1. ブレークポイントを設定
  2. プログラムを実行
  3. 停止したら変数を確認
  4. ステップ実行で進める
  5. 問題を発見

これをCursorに「このBrainfuckプログラムが期待値通りに出力されないのでデバッグして」と指示するだけで、自律的に実行してほしい。

ポイントは以下の2点です:

  • どこで止めるべきかはAIが判断する: ブレークポイントの位置はデバッグの目的や状況によって変わるため、AIが自律的に判断する
  • 効率的に進める手段を提供する: 1命令ずつステップ実行すると毎回AIとのやり取りが発生してトークンを大量消費するので、ループ全体をスキップするなどの「粗い粒度」で進める手段も用意する

提供するツール

MCPサーバーは、前回作成したBrainfuckランタイムをベースに、以下のツールを提供します:

ツール名 説明
debug_load Brainfuckファイルをロード
debug_start デバッグ開始
debug_step_in 1命令実行(最も細かいステップ)
debug_step_over ループをスキップして次へ
debug_step_out 現在のループから抜ける
debug_continue ブレークポイントまで実行
debug_set_breakpoint ブレークポイント設定
debug_get_state 現在の実行状態を取得

さらに、基本ツールに加えて、トークン使用量抑制のために以下のツールも追加しました:

ツール名 説明
debug_set_breakpoints AIが決めた複数のBPを一括設定
debug_run_to_output 出力が変わるまで実行

AIはソースコードを読んで「ここが怪しそう」と自分で判断し、debug_set_breakpoints でBPを設定します。そして実行時の状態を debug_get_state で確認しながらデバッグを進めます。

実装

依存関係の追加

まず、MCP SDKを追加します:

npm install @modelcontextprotocol/sdk

ランタイムへの追加

前回作成した brainfuckRuntime.ts に、複数ブレークポイントを一括設定するメソッドを追加します:

// brainfuckRuntime.ts に追加
public setBreakpoints(lines: number[]): BrainfuckBreakpoint[] {
  return lines.map(line => this.setBreakpoint(line));
}

MCPサーバー(mcpServer.ts)

MCPサーバーの全体構造は以下の通りです:

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { BrainfuckRuntime, RuntimeState } from './brainfuckRuntime';
import * as fs from 'fs';

export class BrainfuckMCPServer {
  private server: Server;
  private runtime: BrainfuckRuntime;
  private currentFile: string = '';

  constructor() {
    this.runtime = new BrainfuckRuntime();
    
    this.server = new Server(
      { name: 'brainfuck-debugger', version: '1.0.0' },
      { capabilities: { tools: {} } }
    );

    this.setupHandlers();
  }

  private setupHandlers(): void {
    // ツール一覧を返す
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      const tools: Tool[] = [
        // ... ツール定義(後述)
      ];
      return { tools };
    });

    // ツール実行
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;
      switch (name) {
        case 'debug_load':
          return this.handleLoad(args);
        case 'debug_start':
          return this.handleStart(args);
        // ... 他のハンドラ
      }
    });
  }

  async start(): Promise<void> {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
  }
}

// スタンドアロンで実行
const server = new BrainfuckMCPServer();
server.start();

ツール定義

各ツールの定義です。description はAIがツールの使い方を判断する材料になるので、具体的に書きます:

const tools: Tool[] = [
  // ファイル操作
  {
    name: 'debug_load',
    description: 'Load a Brainfuck file for debugging. Call this first.',
    inputSchema: {
      type: 'object',
      properties: {
        filePath: { type: 'string', description: 'Path to the Brainfuck file' },
        input: { type: 'string', description: 'Optional input string' },
      },
      required: ['filePath'],
    },
  },
  // 実行制御
  {
    name: 'debug_start',
    description: 'Start debugging. Use stopOnEntry=true to stop at first instruction.',
    inputSchema: {
      type: 'object',
      properties: {
        stopOnEntry: { type: 'boolean', description: 'Stop on first instruction (default: true)' },
      },
    },
  },
  { name: 'debug_step_in', description: 'Execute one instruction.', inputSchema: { type: 'object', properties: {} } },
  { name: 'debug_step_over', description: 'Step over. If at "[", executes entire loop.', inputSchema: { type: 'object', properties: {} } },
  { name: 'debug_step_out', description: 'Step out of current loop.', inputSchema: { type: 'object', properties: {} } },
  { name: 'debug_continue', description: 'Continue until breakpoint or end.', inputSchema: { type: 'object', properties: {} } },
  {
    name: 'debug_run_to_output',
    description: 'Run until output changes. Useful for checking output without stopping at every instruction.',
    inputSchema: { type: 'object', properties: {} },
  },
  // ブレークポイント
  {
    name: 'debug_set_breakpoint',
    description: 'Set a breakpoint at a line.',
    inputSchema: {
      type: 'object',
      properties: { line: { type: 'number', description: 'Line number (1-indexed)' } },
      required: ['line'],
    },
  },
  {
    name: 'debug_set_breakpoints',
    description: 'Set multiple breakpoints at once.',
    inputSchema: {
      type: 'object',
      properties: { lines: { type: 'array', items: { type: 'number' } } },
      required: ['lines'],
    },
  },
  {
    name: 'debug_clear_breakpoint',
    description: 'Clear a breakpoint at a line.',
    inputSchema: {
      type: 'object',
      properties: { line: { type: 'number', description: 'Line number (1-indexed)' } },
      required: ['line'],
    },
  },
  // 状態取得
  {
    name: 'debug_get_state',
    description: 'Get current execution state: memory, pointer, output, current line.',
    inputSchema: { type: 'object', properties: {} },
  },
];

ハンドラ実装

各ツールのハンドラ実装です:

private handleLoad(args: { filePath: string; input?: string }) {
  const sourceCode = fs.readFileSync(args.filePath, 'utf8');
  this.runtime.load(args.filePath, sourceCode, args.input || '');
  this.currentFile = args.filePath;
  return { content: [{ type: 'text', text: JSON.stringify({ success: true }) }] };
}

private handleStart(args: { stopOnEntry?: boolean }) {
  this.runtime.start(args.stopOnEntry !== false);
  return { content: [{ type: 'text', text: JSON.stringify({ state: this.formatState() }) }] };
}

private handleStepIn() {
  this.runtime.stepIn();
  return { content: [{ type: 'text', text: JSON.stringify({ state: this.formatState() }) }] };
}

private handleStepOver() {
  this.runtime.stepOver();
  return { content: [{ type: 'text', text: JSON.stringify({ state: this.formatState() }) }] };
}

private handleContinue() {
  const hitBreakpoint = this.runtime.continue();
  return { content: [{ type: 'text', text: JSON.stringify({ hitBreakpoint, state: this.formatState() }) }] };
}

private handleRunToOutput() {
  const initialOutput = this.runtime.getState().output;
  const maxIterations = 100000;
  let iterations = 0;

  while (iterations < maxIterations) {
    const continued = this.runtime.stepIn();
    if (!continued) break;
    if (this.runtime.getState().output !== initialOutput) break;
    iterations++;
  }

  const state = this.runtime.getState();
  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        newOutput: state.output.slice(initialOutput.length),
        state: this.formatState(),
      }),
    }],
  };
}

private handleSetBreakpoints(args: { lines: number[] }) {
  const breakpoints = this.runtime.setBreakpoints(args.lines);
  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        breakpoints: breakpoints.map(bp => ({ line: bp.line, verified: bp.verified })),
      }),
    }],
  };
}

private handleGetState() {
  return { content: [{ type: 'text', text: JSON.stringify({ state: this.formatState() }) }] };
}

private formatState() {
  const state = this.runtime.getState();
  return {
    line: state.currentLine,
    pointer: state.pointer,
    cell: state.memory[state.pointer],
    output: state.output,
    memory: state.memory.slice(0, Math.max(10, state.pointer + 3)),
    finished: state.finished,
  };
}

Cursorへの登録

ビルド後、~/.cursor/mcp.json に以下を追加します:

npm run compile
{
  "mcpServers": {
    "brainfuck-debugger": {
      "command": "node",
      "args": ["/path/to/out/mcpServer.js"]
    }
  }
}

Cursorを再起動すると、MCPツールとして認識されます。

実際に動かしてみる

前回記事のHello Worldプログラムにバグを仕込んで、Cursorにデバッグさせてみます。

Cursorへの指示

「このプログラムは 'Hello World!' を出力するはずが 'Hello World"' になる。デバッガを使って原因を調べて」と指示すると、Cursorからの回答は以下の通りでした。

Brainfuckデバッガを使って問題を調査します。まずプログラムを読み込んで分析してみましょう。

[2 tools called]

問題は39行目にあるようです。`!` (ASCII 33) を出力するはずが `"` (ASCII 34) を出力しています。この行の直前までプログラムを実行して、メモリの状態を確認しましょう。

[1 tool called]

39行目(問題の `!` を出力する行)にブレークポイントを設定して、そこまで実行します。

[2 tools called]

39行目に到達しました。現在の状態を確認しましょう:
- **出力**: `"Hello World"` (まだ `!` が出力されていない)
- **ポインタ**: セル3 (値: 101 = 'e')
- **メモリ**: `[0, 0, 72('H'), 101('e'), 87('W'), 32(' '), 8, 0, 0, 0]`

39行目のコード `>>++.` をステップ実行して詳しく見てみましょう:

[3 tools called]

最初の `+` で 32 → **33 ('!')** になりました。次のステップを見てみましょう:

[1 tool called]

**原因が特定できました!**

2つ目の `+` で 33 → **34 ('"')** になってしまいました。次の `.` で `"` が出力されます。

## 問題の原因

| ステップ | 操作 | セル[5]の値 | 文字 |
|---------|------|------------|------|
| 初期値 | - | 32 | (スペース) |
| 1つ目の `+` | +1 | 33 | `!` ✅ |
| 2つ目の `+` | +1 | **34** | `"` ❌ |

39行目の `>>++.` で `++` (2回インクリメント) していますが、`!` (ASCII 33) を出力するには `+` (1回) で十分です。

## 修正方法

39行目を以下のように変更してください:

- >>++.
+ >>+.

修正しますか?

怪しいところに自己判断でブレークポイントを張り、デバッグを進めてくれていることがわかります。

まとめ

デバッガのMCPサーバーを構築することで、CursorのAIが自律的にデバッグ戦略を立てて実行できるようになりました。

今回は試しやすいBrainfuckを用いましたが、同様の仕組みで他言語のデバッグについてもMCPサーバーを介してAI任せにすることが可能です。

なお、今回は小規模なBrainfuckプログラムのため、
大規模コードベースでは以下の課題が考えられます。

  • 状態取得コストの増大
  • ツール設計が甘いとAIが迷走する
  • 副作用の多い言語では粒度設計が難しい

参考になれば幸いです。最後まで読んでいただきありがとうございました。

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?