MCP Server のデバッグ/ロギングツールを作りました

[MCP]:https://qiita.com/megmogmog1965/items/79ec6a47d9c223e8cffc
[Cursor]:https://www.cursor.com/
[Claude for Desktop]:https://claude.ai/download
[@modelcontextprotocol/server-filesystem]:https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
[mcp-server-logger]:https://github.com/megmogmog1965/mcp-server-logger

# この記事は何？

最近は [MCP] (Model Context Protocol) も大分普及してきており、様々な便利 MCP Server が登場してきましたね！

そんなとき、なにか新しい MCP Server が出てきたら気軽に試してみたいですが、実際に [Cursor] や [Claude for Desktop] で動かしてみようとすると「なんか上手く起動しない...」ってなることは多いのではないかと...😰

うごかない原因の調査をしようにも、ログはおろか、Console出力も見れないので MCP Server のデバッグは困難です。

# MCP Server のログを取るツール

そこで、MCP Server のログ（標準入出力、signal, exit status...）を取るツールを作りました。

https://github.com/megmogmog1965/mcp-server-logger

https://www.npmjs.com/package/mcp-server-logger

:::note warn
* このツールは Mac や Unix-base OS 向けです。Win でも動くかもしれませんが...
* STDIO Transport のみサポートしています（SSE 非対応）
:::

# 使い方

Command line だとこう使えます。

```bash
npx mcp-server-logger "{log-file-path}" {command} {args...}
```

## 例: コマンドライン

一例として、ローカルファイルの読み書き機能を提供する MCP Server [@modelcontextprotocol/server-filesystem] を起動する場合はこうです。

```bash
npx mcp-server-logger ~/Desktop/mcp-server.log npx -y @modelcontextprotocol/server-filesystem "/Users/{username}/Desktop/"
```

下記のように、引数で指定したファイル `~/Desktop/mcp-server.log` に MCP Server のログ（標準入出力、signal, exit status...）が出力されます。

```text:~/Desktop/mcp-server.log
2025-05-14T22:53:22.478Z [INFO] Subprocess launched: /Users/{username}/.volta/bin/npx -y @modelcontextprotocol/server-filesystem /Users/{username}/Desktop/
2025-05-14T22:53:22.480Z [STDIN] {"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-ai","version":"0.1.0"}},"jsonrpc":"2.0","id":0}
2025-05-14T22:53:23.098Z [STDERR] Secure MCP Filesystem Server running on stdio
2025-05-14T22:53:23.098Z [STDERR] Allowed directories: [ '/Users/{username}/Desktop' ]
2025-05-14T22:53:23.100Z [STDOUT] {"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"secure-filesystem-server","version":"0.2.0"}},"jsonrpc":"2.0","id":0}
2025-05-14T22:53:23.102Z [STDIN] {"method":"notifications/initialized","jsonrpc":"2.0"}
2025-05-14T22:53:23.103Z [STDIN] {"method":"tools/list","params":{},"jsonrpc":"2.0","id":1}
2025-05-14T22:53:23.103Z [STDIN] {"method":"tools/list","params":{},"jsonrpc":"2.0","id":2}
2025-05-14T22:53:23.103Z [STDIN] {"method":"resources/list","params":{},"jsonrpc":"2.0","id":3}
2025-05-14T22:53:23.104Z [STDOUT] {"result":{"tools":[{"name":"read_file", ... }, ... ]},"jsonrpc":"2.0","id":1}
2025-05-14T22:53:23.105Z [STDOUT] {"jsonrpc":"2.0","id":3,"error":{"code":-32601,"message":"Method not found"}}
2025-05-14T22:53:23.104Z [STDOUT] {"result":{"tools":[{"name":"read_file", ... }, ... ]},"jsonrpc":"2.0","id":1}
2025-05-14T22:53:23.107Z [STDIN] {"method":"prompts/list","params":{},"jsonrpc":"2.0","id":4}
2025-05-14T22:53:23.107Z [STDOUT] {"jsonrpc":"2.0","id":4,"error":{"code":-32601,"message":"Method not found"}}
2025-05-14T22:53:41.401Z [STDIN] {"method":"tools/call","params":{"name":"list_allowed_directories","arguments":{}},"jsonrpc":"2.0","id":5}
2025-05-14T22:53:41.402Z [STDOUT] {"result":{"content":[{"type":"text","text":"Allowed directories:\n/Users/{username}/Desktop"}]},"jsonrpc":"2.0","id":5}
2025-05-14T22:53:44.466Z [STDIN] {"method":"tools/call","params":{"name":"list_directory","arguments":{"path":"/Users/{username}/Desktop"}},"jsonrpc":"2.0","id":6}
2025-05-14T22:53:44.530Z [STDOUT] {"result":{"content":[{"type":"text","text":"[FILE] .DS_Store\n[FILE] .localized\n[DIR] test"}]},"jsonrpc":"2.0","id":6}
```

## 例: [Claude for Desktop]

前述のコマンドラインでの例を `~/Library/Application Support/Claude/claude_desktop_config.json` 向けに直したものです。

```json:~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "filesystem": {
      "command": "/Users/{username}/.volta/bin/npx",
      "args": [
        "mcp-server-logger",
        "/Users/{username}/Desktop/mcp-server.log",
        "/Users/{username}/.volta/bin/npx",
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/{username}/Desktop/"
      ]
    }
  }
}
```

:::note
`npx` のパスは各々のローカル環境で異なります。`which npx` 等で調べてください。
:::

うまく動けば、下図の様に [Claude for Desktop] に認識されます。

![Pasted_Image_2025_05_15__8_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/16a4cfb6-0158-40bf-a537-3cf6443e0952.png)

# 仕組み

プロセスツリー上、[mcp-server-logger] は MPC Server のプロキシとして稼働します。

* MPC Client から見て、[mcp-server-logger] が MCP Server そのもの
* [mcp-server-logger] は、引数で与えられたコマンドを元に実際の MCP Server を子プロセスとして起動
* [mcp-server-logger] は、MCP Client ⇔ MCP Server 間の通信（標準入出力、シグナル...）を伝送

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/3daa0dd2-7ab0-4599-903b-b7ecd2fd7a98.png)

コードも 100 行程度で、通信を伝送してログファイルに記録しているだけです。

```typescript
#!/usr/bin/env node

import { spawn } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';

// Validate command line arguments
if (process.argv.length < 4) {
  console.error('Error: Not enough arguments');
  console.error('Usage: npx mcp-server-logger <log_file> <command> [arguments...]');
  process.exit(1);
}

const logFile = process.argv[2];
const command = process.argv[3];
const args = process.argv.slice(4);

// Create a stream for the log file
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
const log = (message: string) => {
  const timestamp = new Date().toISOString();
  const formattedMessage = `${timestamp} ${message}${message.endsWith(os.EOL) ? '' : os.EOL}`;
  logStream.write(formattedMessage);
};

// Handle log file errors
logStream.on('error', (error) => {
  console.error(`Error: Failed to open log file.`);
  console.error(`${error.message}`);
  process.exit(1);
});

// Launch subprocess
const childProcess = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
log(`[INFO] Subprocess launched: ${command} ${args.join(' ')}`);

childProcess.on('error', (error) => {
  console.error(`Failed to start subprocess: ${error.message}`);
  log(`[ERROR] Failed to start subprocess: ${error.message}`);
  process.exit(1);
});

childProcess.on('close', (code) => {
  log(`[INFO] Subprocess finished with code ${code}`);
  logStream.end(); // Close the stream
  process.exit(code);
});

// Pass subprocess standard output to parent process standard output
childProcess.stdout.on('data', (chunk) => {
  process.stdout.write(chunk);
  log(`[STDOUT] ${chunk}`); // Also write to log file
});

// Pass subprocess standard error to parent process standard error
childProcess.stderr.on('data', (chunk) => {
  process.stderr.write(chunk);
  log(`[STDERR] ${chunk}`); // Also write to log file
});

// When data is input
process.stdin.on('data', (chunk) => {
  childProcess.stdin.write(chunk);
  log(`[STDIN] ${chunk}`); // Write standard input to log file
});

// List linux signals.
const signals = [
  'SIGABRT', 'SIGALRM', 'SIGBUS', 'SIGCHLD', 'SIGCONT', 'SIGFPE', 'SIGHUP',
  'SIGILL', 'SIGINT', 'SIGIO', 'SIGIOT', 'SIGPIPE', 'SIGPOLL',
  'SIGPROF', 'SIGPWR', 'SIGQUIT', 'SIGSEGV', 'SIGSTKFLT', 'SIGSYS',
  'SIGTERM', 'SIGTRAP', 'SIGTSTP', 'SIGTTIN', 'SIGTTOU', 'SIGUNUSED', 'SIGURG',
  'SIGUSR1', 'SIGUSR2', 'SIGVTALRM', 'SIGWINCH', 'SIGXCPU', 'SIGXFSZ'
];

// Logging signals.
signals.forEach(signal => {
  try {
    process.on(signal as NodeJS.Signals, () => {
      log(`[SIGNAL] ${signal} received.`);
    });
  } catch (err) {
    // for windows os.
    log(`[ERROR] ${err}`);
  }
});

// Forward signals from parent process to child process
['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGTERM'].forEach(signal => {
  try {
    process.on(signal as NodeJS.Signals, () => {
      log(`[SIGNAL] ${signal} received from parent process, forwarding to child process.`);

      try {
        childProcess.kill(signal as NodeJS.Signals);
      } catch (err) {
        log(`[ERROR] ${err}`);
      }
    });
  } catch (err) {
    // for windows os.
    log(`[ERROR] ${err}`);
  }
});
```