3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

この記事は何?

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

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

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

MCP Server のログを取るツール

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

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

使い方

Command line だとこう使えます。

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

例: コマンドライン

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

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...)が出力されます。

~/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 向けに直したものです。

~/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/"
      ]
    }
  }
}

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

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

Pasted_Image_2025_05_15__8_07.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

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

#!/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: node index.js <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}`);
  }
});
3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?