598369e894
- Hook system for customization - Documentation and installation scripts - Multi-platform support via GitHub releases - Binaries available for Windows, Linux (x64/ARM64), macOS (Intel/Apple Silicon) Generated with Claude Code via Happy
7.9 KiB
7.9 KiB
MCP TypeScript SDK Stdio Transport Guide
Documentation Source
- SDK Version: @modelcontextprotocol/sdk v1.0.0
- Last Verified: 2025-09-01
- Official Docs: https://github.com/modelcontextprotocol/typescript-sdk
Stdio Transport Overview
The StdioServerTransport enables MCP servers to communicate via standard input/output streams, making them ideal for CLI tools and direct integrations with Claude Code.
Communication Channels
Stream Usage
- stdin: Receives MCP protocol messages (JSON-RPC)
- stdout: Sends MCP protocol responses (JSON-RPC)
- stderr: Logging and diagnostic output
Important Rules
- Never write non-protocol data to stdout - This will break the protocol
- Always use console.error() for logging - Goes to stderr
- Handle binary data carefully - Protocol is text-based JSON
Implementation Patterns
Basic Stdio Server Setup
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
// Create server
const server = new Server(
{ name: 'my-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
// Create and connect transport
const transport = new StdioServerTransport();
await server.connect(transport);
// Server is now listening on stdin/stdout
console.error('Server started'); // Note: console.error for logging
With McpServer (High-Level API)
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new McpServer({
name: 'my-server',
version: '1.0.0'
});
// Register tools, resources, prompts...
server.registerTool(...);
// Connect to stdio
const transport = new StdioServerTransport();
await server.connect(transport);
CLI Entry Point Pattern
Proper Module Detection (ES Modules)
#!/usr/bin/env node
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Only run if executed directly
if (process.argv[1] === __filename ||
process.argv[1].endsWith('server.js')) {
main().catch((error) => {
console.error('Startup error:', error);
process.exit(1);
});
}
Main Function Pattern
async function main(): Promise<void> {
try {
// 1. Initialize dependencies
await initializeDatabase();
// 2. Create server
const server = createServer();
// 3. Create transport
const transport = new StdioServerTransport();
// 4. Connect
await server.connect(transport);
// 5. Setup shutdown handlers
setupShutdownHandlers();
// 6. Log readiness to stderr
console.error('MCP server ready on stdio');
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
Shutdown Handling
Graceful Shutdown Pattern
function setupShutdownHandlers(): void {
const shutdown = async (signal: string) => {
console.error(`\nReceived ${signal}, shutting down...`);
try {
// Clean up resources
await cleanupResources();
// Note: Transport cleanup is handled automatically
process.exit(0);
} catch (error) {
console.error('Shutdown error:', error);
process.exit(1);
}
};
// Handle termination signals
process.on('SIGINT', () => shutdown('SIGINT')); // Ctrl+C
process.on('SIGTERM', () => shutdown('SIGTERM')); // Kill
process.on('SIGHUP', () => shutdown('SIGHUP')); // Terminal closed
// Handle errors
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
process.exit(1);
});
}
Logging Best Practices
Do's and Don'ts
// ✅ DO: Log to stderr
console.error('Server initialized');
console.error('Processing request:', requestId);
console.error('Debug info:', { data });
// ❌ DON'T: Log to stdout
console.log('This breaks the protocol!'); // NEVER DO THIS
// ✅ DO: Use structured logging to stderr
const log = (level: string, message: string, data?: any) => {
console.error(JSON.stringify({
timestamp: new Date().toISOString(),
level,
message,
...data
}));
};
log('info', 'Server started', { port: 'stdio' });
Debug Mode Pattern
const DEBUG = process.env.DEBUG === 'true';
const debug = (message: string, ...args: any[]) => {
if (DEBUG) {
console.error(`[DEBUG] ${message}`, ...args);
}
};
// Usage
debug('Request received:', request);
Testing Stdio Servers
Manual Testing
# Start server and interact manually
node dist/server.js
# With debug logging
DEBUG=true node dist/server.js
# Pipe test input
echo '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}' | node dist/server.js
Automated Testing Pattern
import { spawn } from 'child_process';
function testServer() {
const server = spawn('node', ['dist/server.js']);
// Capture stderr for logs
server.stderr.on('data', (data) => {
console.log('Server log:', data.toString());
});
// Capture stdout for protocol
let response = '';
server.stdout.on('data', (data) => {
response += data.toString();
// Parse and validate response
});
// Send test request
server.stdin.write(JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: {},
id: 1
}));
server.stdin.end();
}
Common Issues and Solutions
Issue 1: Protocol Corruption
Problem: Random text in stdout breaks communication Solution: Always use console.error() for logging
// Wrong
console.log('Debug:', data); // Breaks protocol
// Right
console.error('Debug:', data); // Safe for debugging
Issue 2: Server Not Responding
Problem: Server starts but doesn't respond to requests Solution: Ensure transport is connected
// Check connection is awaited
await server.connect(transport); // Must await!
console.error('Transport connected');
Issue 3: Premature Exit
Problem: Server exits immediately Solution: Don't close stdin/stdout
// Wrong
process.stdin.end(); // Don't do this
// Right
// Let the transport manage streams
Integration with Claude Code
Configuration in .claude.json
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/dist/server.js"],
"env": {
"DEBUG": "false"
}
}
}
}
Best Practices for Claude Code Integration
- Startup Messages: Log clear startup messages to stderr
- Error Messages: Provide actionable error messages
- Ready Signal: Log when server is ready to accept requests
- Version Info: Include version in startup logs
console.error(`Starting ${serverName} v${version}`);
console.error('Initializing...');
// ... initialization ...
console.error(`${serverName} ready on stdio`);
Performance Considerations
Buffering and Streaming
// For large responses, consider streaming
import { Transform } from 'stream';
class ResponseStream extends Transform {
_transform(chunk: any, encoding: string, callback: Function) {
// Process chunk
this.push(JSON.stringify(chunk));
callback();
}
}
Memory Management
// Clear large objects after use
let largeData = await processData();
// Use data...
largeData = null; // Allow GC