この記事は何?
最近は MCP (Model Context Protocol) も大分普及してきており、様々な便利 MCP Server が登場してきましたね!
そんなとき、なにか新しい MCP Server が出てきたら気軽に試してみたいですが、実際に Cursor や Claude 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...)が出力されます。
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
向けに直したものです。
{
"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 に認識されます。
仕組み
プロセスツリー上、mcp-server-logger は MPC Server のプロキシとして稼働します。
- MPC Client から見て、mcp-server-logger が MCP Server そのもの
- mcp-server-logger は、引数で与えられたコマンドを元に実際の MCP Server を子プロセスとして起動
- mcp-server-logger は、MCP Client ⇔ MCP Server 間の通信(標準入出力、シグナル...)を伝送
コードも 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}`);
}
});