Claude Codeの「npm run dev問題」を解決するMCPサーバーを作った話

記事はこちらに移行しました。
以下魚拓
はじめに
皆さんvibe codingしてますか?
僕はClaude Codeに首ったけなんですが、開発してもらっている最中にしばしばnpm run devをClaudeが実行してタイムアウトまで待たされる事がありました。
最初は仕方がないと思い、一度escで処理を止めて、別のターミナルを開いて手動で開発サーバーを起動して、Claude Codeに引き継いでもらうようにしていました。が、明らかに非効率なのでなんとかしたく、以下のMCPサーバーをvibeしました。
https://github.com/shabaraba/devtools-mcp
ついでに、デバッグ時の情報提供も面倒でした。
「ブラウザのコンソールにエラーが出ています」と報告しても、Claude Codeからはブラウザの中を見ることができません。
開発者ツールのスクリーンショットを撮って共有するか、エラーメッセージを手動でコピペするか。これも明らかに非効率です。
なので、ClaudeCodeにはこのMCPを使ってサーバー管理だけでなく、ついでにブラウザのエラー況まで含めて、開発環境全体を把握できるようになってもらいます。
問題の本質を理解する
なぜnpm run devがClaude Codeをブロックしてしまうのか。答えは単純でした。開発サーバーは基本的にフォアグラウンドプロセスとして動作し、ターミナルの制御を占有し続けるからです。
そして、なぜブラウザの情報がClaude Codeに届かないのか。これも単純な理由でした。ブラウザとサーバーサイドの開発環境は、完全に分離されているからです。ブラウザのコンソールに出力されるエラーやログは、開発サーバーからは見えません。
私が欲しかったのは:
- 開発サーバーをバックグラウンドで起動し、すぐに制御を返してくれる仕組み
- サーバーの状態を監視したり、ログを確認したり、必要に応じて再起動できる機能
- ブラウザのコンソールログやネットワークエラーを、サーバーサイドから確認できる仕組み
つまり、開発環境全体の「可視性」を向上させることでした。
導入するとどうなるか
後述で設計とか書きますが、とりあえずどんな感じで動作するかをまず書きますね。
開発サーバーの起動と監視
Claude Codeに「開発サーバー起動して」と依頼すると、以下のような流れで処理されます。
Claude Code: start_dev_serverツールを呼び出し中...
引数: {
"command": "npm run dev",
"name": "nextjs-app",
"port": 3000,
"cwd": "/Users/dev/my-project"
}
結果:
Development server "nextjs-app" started successfully in background.
Command: npm run dev
Port: 3000
PID: 12345
Working Directory: /Users/dev/my-project
Server is running in the background. Use check_dev_server to monitor status.
この時点で、Claude Codeのターミナルは完全に自由になります。開発サーバーはバックグラウンドで動作し続けているのに、Claude Codeは次のタスクに移れるのです。
もちろん特に依頼しなくても、開発の流れで勝手にstart_dev_serverツールを使ってくれます。
ブラウザログの統合監視
Chrome拡張機能をインストールしていると、ブラウザでの操作が自動的にMCPサーバーに送信されます。Claude Codeに「最近のブラウザログを確認してください」と依頼すると:
{
"totalLogs": 157,
"portStats": {
"3000": 98,
"8080": 59
},
"recentErrors": [
{
"timestamp": "2024-06-10T15:23:45.123Z",
"level": "error",
"message": "POST <http://localhost:3000/api/users> 500 (Internal Server Error)",
"url": "<http://localhost:3000/dashboard>",
"port": "3000"
},
{
"timestamp": "2024-06-10T15:24:12.456Z",
"level": "error",
"message": "Uncaught TypeError: Cannot read properties of undefined (reading 'name')",
"url": "<http://localhost:3000/profile>",
"port": "3000"
}
]
}
みたいな感じで、開発者ツールを見に行かなくてもClaudeCodeが取得できるようになります。
サーバーログとブラウザログが統合されることで、フルスタックでの問題解決が可能になりました。
統合デバッグ体験
エラー解決体験も結構良くなりました。
ブラウザ側でエラーが発生しても、以下のような感じでブラウザ・サーバーを横断で確認してくれるようになります。
⏺ ブラウザでエラーが発生していることを検出しました。詳細を確認します。
⏺ devtools:get_browser_logs (MCP)(filter: {"level": ["error"], "limit": 10})
⎿ 最新のエラーログ:
- GET <http://localhost:3000/_next/static/css/app.css> 404 (Not Found)
- GET <http://localhost:3000/favicon.ico> 404 (Not Found)
⏺ 静的ファイルが見つからないエラーが発生しています。開発サーバーのログも確認してみましょう。
⏺ devtools:get_server_logs (MCP)(name: "nextjs-app", lines: 10)
⎿ サーバーログから、ビルドプロセスでエラーが発生していることがわかりました。
⏺ 開発サーバーを再起動してみましょう。
⏺ devtools:restart_dev_server (MCP)(name: "nextjs-app")
⎿ Development server "nextjs-app" started successfully in background.
Server is running in the background.
⏺ 開発サーバーを再起動しました。ブラウザでページをリロード(⌘+R)してみてください。
とか

とか。
もうコピペしなくて良い。やったね。
設計について
:::message
ここからの文章について、7割はClaudeによるもので、たたきを出してもらって僕本人が読んで「なるほどー」って学びながらリライトしています。
:::
ここから少しコード寄りの話です。
設計も何も全部vibeでやったのであれですが、考えられる方針としてはnohup npm run dev &を叩いてもらってバックグラウンドで起動する方向もあったと思います。
ただマイプロジェクトCLAUDE.mdにかくのも面倒だし、ユーザーレベルのメモリに書いたとて、プロセスを返さない全コマンドを書くのも大変です。プロセスが生きているかわからないし、ログも見づらい。
ブラウザログの収集についても色々選択肢がありました。
ラウザ拡張機能にするか、Webアプリ側にライブラリを組み込むか、それとも別のアプローチか。
最終的には、MCPサーバー x Chrome拡張機能に落ち着きました。
- 新しいツールをClaude Codeに教えるのも簡単
- 既存のWebアプリケーションに一切の変更を加えずに済む
実装とClaudeCodeとのラリー
プロセス管理まわり
最初のバージョンでは、単純にspawnでプロセスを起動していましたが、子プロセスが正常に起動したかの判定が難しいらしく、上手く取得できてなさそうでした。
// 最初の実装(問題あり)
const childProcess = spawn(cmd, cmdArgs, options);
// すぐに成功として返してしまう
return { success: true };
解決策として、起動チェック機能を実装しました。プロセスを起動後、2秒間待機して、その間にプロセスが異常終了しないかを監視します。500ミリ秒後に生存確認も行います。
const startupPromise = new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => resolve(true), 2000);childProcess.once('exit', () => {
clearTimeout(timeout);
resolve(false);
});
setTimeout(() => {
if (!childProcess.killed && childProcess.exitCode === null) {
clearTimeout(timeout);
resolve(true);
}
}, 500);
});
ブラウザログ収集
ブラウザのコンソールログを取得するのは、想像以上に複雑でした。Chrome拡張機能のManifest V3では、セキュリティ制約が厳しく、単純にconsole.logを傍受するだけでは不十分です。
最終的に、3層構造のアーキテクチャを採用しました:
- Injected Script: ページのコンテキストで動作し、console.*メソッドをオーバーライド
- Content Script: Injected Scriptからのメッセージを中継
- Background Service Worker: MCPサーバーへのHTTP通信を担当
// Injected Scriptでのconsole.logの傍受
Object.keys(originalConsole).forEach(method => {
console[method] = function(...args) {
// 元のメソッドを呼び出し
originalConsole[method].apply(console, args);
// ログ情報をContent Scriptに送信
sendLog(method, args);
};
});
さらに、HTTP通信エラーの検出も重要でした。fetchとXMLHttpRequestの両方をオーバーライドして、ネットワークエラーを自動的にキャッチします:
// fetchのエラー検出
const originalFetch = window.fetch;
window.fetch = function(...args) {
const request = originalFetch.apply(this, args);
return request.then(response => {
if (!response.ok) {
const url = args[0];
const method = args[1]?.method || 'GET';
sendLog('error', [`${method} ${url} ${response.status} (${response.statusText})`]);
}
return response;
}).catch(error => {
// ネットワークエラーをログに記録
const url = args[0];
const method = args[1]?.method || 'GET';
sendLog('error', [`Network Error: ${method} ${url}`, error.message]);
throw error;
});
};
fetch、XMLHttpRequestをオーバーライドしている点が気になりますが、なにか問題が起きたらその都度対応していきたいと思います。
ログ管理方法
開発サーバーのログをどう管理するかも悩みどころでした。すべてのログを保存していると、メモリを圧迫してしまいます。かといって、ログがないとデバッグが困難です。
最終的に、循環バッファ方式を採用しました。最新の100行だけを保持し、古いログは自動的に削除されます。また、長すぎるログ行は途中で切り捨てて、メモリ使用量を制御しています。
const timestamp = new Date().toISOString();
const truncatedLog = log.length > this.maxLogLength
? log.substring(0, this.maxLogLength) + '...[truncated]'
: log;
server.logs.push(`[${timestamp}] ${truncatedLog}`);
if (server.logs.length > this.maxLogLines) {
server.logs = server.logs.slice(-this.maxLogLines);
}
}
ブラウザログについても同様の仕組みを実装しましたが、さらにポート別の分類機能を追加しました。localhost:3000とlocalhost:8080で動作する複数のサービスのログを、別々に管理できるようにしたのです。
既存サーバーの発見機能
開発中に気づいたのが、「MCPサーバー経由で起動していないサーバーは見えない」という問題でした。既にnpm run devで起動済みのサーバーがあっても、MCPシステムからは認識されません。
この問題を解決するため、discover_running_servers機能を実装しました。lsofコマンドを使って、実際に動作しているサーバーを検出し、MCPの管理下に取り込む仕組みです:
// ポートスキャンによるサーバー発見
const { stdout } = await execAsync(`lsof -i :${port}`);
const lines = stdout.trim().split('\\n');
if (lines.length > 1) {
const firstLine = lines[1];
const parts = firstLine.trim().split(/\\s+/);
const pid = parseInt(parts[1]);
// プロセス情報を取得してサーバーリストに追加
const { stdout: commandOutput } = await execAsync(`ps -p ${pid} -o command=`);
const command = commandOutput.trim();
return { port, pid, command, protocol: 'tcp' };
}
クリーンアップの重要性
開発中に何度もプロセスが残ってしまう問題に遭遇しました。MCPサーバー自体が異常終了した際に、起動した開発サーバーがゾンビプロセスとして残ってしまうのです。
この問題を解決するため、プロセス終了時のクリーンアップ処理も実装してもらいました。
constructor() {
process.on('exit', () => this.cleanup());
process.on('SIGINT', () => this.cleanup());
process.on('SIGTERM', () => this.cleanup());
}
private cleanup(): void {
for (const [name, server] of this.servers) {
if (server.status === 'running' || server.status === 'starting') {
console.error(`Cleaning up server "${name}"`);
server.process.kill('SIGTERM');
}
}
}
おわりに
とまぁ後半は長々とAIの説明をペタペタ書いていきましたが、リライトしていて正直学びも多かったです。
vibe codingだとほぼコードなんか見ないので、どういう設計を選択したかとか、そういうのを出力させて勉強するのもvibe codingの一つの使い道のように思いました。別件だけど。
ということで、もしかしたらもっとスマートな解決方法があったかもしれませんが、devtools-mcpも割と便利だと思うので、ご興味あれば是非。