diff --git a/.gitignore b/.gitignore index e715fdb..65a6009 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ coverage/ # IDE .vscode/ -.idea/ \ No newline at end of file +.idea/ +.osgrep diff --git a/package-lock.json b/package-lock.json index 4f3a737..c0f8687 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@steipete/claude-code-mcp", - "version": "1.10.11", + "version": "1.10.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@steipete/claude-code-mcp", - "version": "1.10.11", + "version": "1.10.12", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.11.2", @@ -1182,6 +1182,7 @@ "version": "22.15.17", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1360,7 +1361,6 @@ "node_modules/accepts": { "version": "1.3.8", "license": "MIT", - "peer": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -1397,8 +1397,7 @@ }, "node_modules/array-flatten": { "version": "1.1.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/assertion-error": { "version": "2.0.1", @@ -1420,7 +1419,6 @@ "node_modules/body-parser": { "version": "1.20.3", "license": "MIT", - "peer": true, "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -1542,7 +1540,6 @@ "node_modules/content-disposition": { "version": "0.5.4", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -1566,8 +1563,7 @@ }, "node_modules/cookie-signature": { "version": "1.0.6", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cors": { "version": "2.8.5", @@ -1612,7 +1608,6 @@ "node_modules/debug": { "version": "2.6.9", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -1637,7 +1632,6 @@ "node_modules/destroy": { "version": "1.2.0", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -2235,7 +2229,6 @@ "node_modules/finalhandler": { "version": "1.3.1", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -2276,7 +2269,6 @@ "node_modules/fresh": { "version": "0.5.2", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -2429,7 +2421,6 @@ "node_modules/iconv-lite": { "version": "0.4.24", "license": "MIT", - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -2619,7 +2610,6 @@ "node_modules/media-typer": { "version": "0.3.0", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -2627,7 +2617,6 @@ "node_modules/merge-descriptors": { "version": "1.0.3", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -2635,7 +2624,6 @@ "node_modules/methods": { "version": "1.1.2", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -2643,7 +2631,6 @@ "node_modules/mime": { "version": "1.6.0", "license": "MIT", - "peer": true, "bin": { "mime": "cli.js" }, @@ -2654,7 +2641,6 @@ "node_modules/mime-db": { "version": "1.52.0", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -2662,7 +2648,6 @@ "node_modules/mime-types": { "version": "2.1.35", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -2698,8 +2683,7 @@ }, "node_modules/ms": { "version": "2.0.0", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", @@ -2723,7 +2707,6 @@ "node_modules/negotiator": { "version": "0.6.3", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -2802,8 +2785,7 @@ }, "node_modules/path-to-regexp": { "version": "0.1.12", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/pathe": { "version": "1.1.2", @@ -2879,7 +2861,6 @@ "node_modules/qs": { "version": "6.13.0", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "side-channel": "^1.0.6" }, @@ -2900,7 +2881,6 @@ "node_modules/raw-body": { "version": "2.5.2", "license": "MIT", - "peer": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -3037,7 +3017,6 @@ "node_modules/send": { "version": "0.19.0", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -3060,20 +3039,17 @@ "node_modules/send/node_modules/encodeurl": { "version": "1.0.2", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } }, "node_modules/send/node_modules/ms": { "version": "2.1.3", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/serve-static": { "version": "1.16.2", "license": "MIT", - "peer": true, "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -3424,7 +3400,6 @@ "node_modules/type-is": { "version": "1.6.18", "license": "MIT", - "peer": true, "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -3460,7 +3435,6 @@ "node_modules/utils-merge": { "version": "1.0.1", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -3478,6 +3452,7 @@ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -3642,6 +3617,7 @@ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", @@ -3849,6 +3825,7 @@ "node_modules/zod": { "version": "3.24.4", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/server.ts b/src/server.ts index 07cbf51..a793d3c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,11 +9,10 @@ import { type ServerResult, } from '@modelcontextprotocol/sdk/types.js'; import { spawn } from 'node:child_process'; -import { existsSync } from 'node:fs'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join, resolve as pathResolve } from 'node:path'; import * as path from 'path'; -import { readFileSync } from 'node:fs'; // Server version - update this when releasing new versions const SERVER_VERSION = "1.10.12"; @@ -35,29 +34,193 @@ export function debugLog(message?: any, ...optionalParams: any[]): void { } /** - * Determine the Claude CLI command/path. - * 1. Checks for CLAUDE_CLI_NAME environment variable: - * - If absolute path, uses it directly - * - If relative path, throws error - * - If simple name, continues with path resolution - * 2. Checks for Claude CLI at the local user path: ~/.claude/local/claude. - * 3. If not found, defaults to the CLI name (or 'claude'), relying on the system's PATH for lookup. + * Message format for conversation context + */ +interface ConversationMessage { + role: 'user' | 'assistant'; + content: string; +} + +/** + * Extended interface for Claude Code tool arguments with session support + */ +interface ClaudeCodeArgs { + prompt: string; + workFolder?: string; + sessionId?: string; + messages?: ConversationMessage[]; + stateless?: boolean; +} + +/** + * Claude CLI JSON output format + */ +interface ClaudeCliResponse { + type: string; + subtype: string; + is_error: boolean; + duration_ms: number; + duration_api_ms?: number; + num_turns?: number; + result: string; + session_id: string; + total_cost_usd?: number; + usage?: { + input_tokens: number; + output_tokens: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; + }; +} + +/** + * Session entry with timestamp for expiration + */ +interface SessionEntry { + claudeSessionId: string; + updatedAt: string; +} + +/** + * Session storage configuration + */ +const MAX_SESSIONS = 1000; +const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours +const SESSION_DIR = join(homedir(), '.config', 'claude-code-mcp'); +const SESSION_FILE = join(SESSION_DIR, 'sessions.json'); + +/** + * In-memory session cache, loaded from file on startup + */ +let sessionMap: Map = new Map(); + +/** + * Ensure the session directory exists + */ +function ensureSessionDir(): void { + if (!existsSync(SESSION_DIR)) { + mkdirSync(SESSION_DIR, { recursive: true }); + debugLog(`[Debug] Created session directory: ${SESSION_DIR}`); + } +} + +/** + * Load sessions from file into memory + */ +function loadSessions(): void { + try { + if (existsSync(SESSION_FILE)) { + const data = readFileSync(SESSION_FILE, 'utf-8'); + const parsed = JSON.parse(data); + sessionMap = new Map(Object.entries(parsed)); + debugLog(`[Debug] Loaded ${sessionMap.size} sessions from ${SESSION_FILE}`); + + // Clean up expired sessions on load + cleanExpiredSessions(); + } + } catch (e) { + debugLog('[Debug] Failed to load sessions file, starting fresh:', e); + sessionMap = new Map(); + } +} + +/** + * Save sessions from memory to file + */ +function saveSessions(): void { + try { + ensureSessionDir(); + const data = Object.fromEntries(sessionMap); + writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2)); + debugLog(`[Debug] Saved ${sessionMap.size} sessions to ${SESSION_FILE}`); + } catch (e) { + debugLog('[Debug] Failed to save sessions file:', e); + } +} + +/** + * Remove sessions older than TTL + */ +function cleanExpiredSessions(): void { + const now = Date.now(); + let cleaned = 0; + + for (const [parentId, entry] of sessionMap) { + const age = now - new Date(entry.updatedAt).getTime(); + if (age > SESSION_TTL_MS) { + sessionMap.delete(parentId); + cleaned++; + } + } + + if (cleaned > 0) { + debugLog(`[Debug] Cleaned ${cleaned} expired sessions`); + saveSessions(); + } +} + +/** + * Get a session mapping + */ +function getSessionMapping(parentId: string): string | undefined { + const entry = sessionMap.get(parentId); + if (entry) { + // Check if expired + const age = Date.now() - new Date(entry.updatedAt).getTime(); + if (age > SESSION_TTL_MS) { + sessionMap.delete(parentId); + saveSessions(); + return undefined; + } + return entry.claudeSessionId; + } + return undefined; +} + +/** + * Store a session mapping with LRU-style eviction and file persistence + */ +function setSessionMapping(parentId: string, claudeId: string): void { + // LRU eviction if at capacity + if (sessionMap.size >= MAX_SESSIONS) { + // Remove oldest entry (first inserted) + const firstKey = sessionMap.keys().next().value; + if (firstKey) sessionMap.delete(firstKey); + } + + sessionMap.set(parentId, { + claudeSessionId: claudeId, + updatedAt: new Date().toISOString() + }); + + // Persist to file + saveSessions(); +} + +// Load sessions on module initialization +loadSessions(); + +/** + * Determines the Claude CLI command/path based on the following precedence: + * 1. An absolute path specified in the `CLAUDE_CLI_NAME` environment variable. + * 2. The local user installation at `~/.claude/local/claude`. + * 3. A simple command name from `CLAUDE_CLI_NAME` (looked up in the system's PATH). + * 4. The default command `claude` (looked up in the system's PATH). + * + * Note: Relative paths in `CLAUDE_CLI_NAME` are not allowed and will cause an error. */ export function findClaudeCli(): string { debugLog('[Debug] Attempting to find Claude CLI...'); - // Check for custom CLI name from environment variable const customCliName = process.env.CLAUDE_CLI_NAME; if (customCliName) { debugLog(`[Debug] Using custom Claude CLI name from CLAUDE_CLI_NAME: ${customCliName}`); - // If it's an absolute path, use it directly if (path.isAbsolute(customCliName)) { debugLog(`[Debug] CLAUDE_CLI_NAME is an absolute path: ${customCliName}`); return customCliName; } - // If it starts with ~ or ./, reject as relative paths are not allowed if (customCliName.startsWith('./') || customCliName.startsWith('../') || customCliName.includes('/')) { throw new Error(`Invalid CLAUDE_CLI_NAME: Relative paths are not allowed. Use either a simple name (e.g., 'claude') or an absolute path (e.g., '/tmp/claude-test')`); } @@ -65,7 +228,6 @@ export function findClaudeCli(): string { const cliName = customCliName || 'claude'; - // Try local install path: ~/.claude/local/claude (using the original name for local installs) const userPath = join(homedir(), '.claude', 'local', 'claude'); debugLog(`[Debug] Checking for Claude CLI at local user path: ${userPath}`); @@ -76,26 +238,74 @@ export function findClaudeCli(): string { debugLog(`[Debug] Claude CLI not found at local user path: ${userPath}.`); } - // 3. Fallback to CLI name (PATH lookup) debugLog(`[Debug] Falling back to "${cliName}" command name, relying on spawn/PATH lookup.`); console.warn(`[Warning] Claude CLI not found at ~/.claude/local/claude. Falling back to "${cliName}" in PATH. Ensure it is installed and accessible.`); return cliName; } /** - * Interface for Claude Code tool arguments + * Format conversation messages into a context string for Claude CLI */ -interface ClaudeCodeArgs { - prompt: string; - workFolder?: string; +function formatConversationContext(messages: ConversationMessage[]): string { + if (!messages || messages.length === 0) return ''; + + const formatted = messages.map(msg => { + const roleLabel = msg.role === 'user' ? 'User' : 'Assistant'; + return `[${roleLabel}]: ${msg.content}`; + }).join('\n\n'); + + return `\n${formatted}\n\n\n`; +} + +/** + * Translate slash commands to @ mentions for Claude Code subagent invocation + * Only matches /command patterns that don't look like file paths (no / after the command name) + */ +function translateSlashCommands(prompt: string): string { + // Match /command at start of line, but only if followed by space, end of line, or end of string + // This avoids matching file paths like /tmp/foo or /usr/bin/something + return prompt.replace(/^\/([a-zA-Z][a-zA-Z0-9_-]*)(?=\s|$)/gm, '@$1'); +} + +/** + * Parse Claude CLI JSON response + * Handles both clean JSON output and output with extra text + */ +function parseClaudeResponse(stdout: string): ClaudeCliResponse | null { + try { + // Try parsing the entire stdout first (most common case with --output-format json) + const trimmed = stdout.trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + const parsed = JSON.parse(trimmed); + if (parsed.type === 'result') return parsed; + } + + // Fallback: try to find a JSON object line by line + const lines = stdout.split('\n'); + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i].trim(); + if (line.startsWith('{') && line.endsWith('}')) { + try { + const parsed = JSON.parse(line); + if (parsed.type === 'result') return parsed; + } catch { + // Continue to next line + } + } + } + + return null; + } catch (e) { + debugLog('[Debug] Failed to parse Claude CLI JSON response:', e); + return null; + } } -// Ensure spawnAsync is defined correctly *before* the class export async function spawnAsync(command: string, args: string[], options?: { timeout?: number, cwd?: string }): Promise<{ stdout: string; stderr: string }> { return new Promise((resolve, reject) => { debugLog(`[Spawn] Running command: ${command} ${args.join(' ')}`); const process = spawn(command, args, { - shell: false, // Reverted to false + shell: false, timeout: options?.timeout, cwd: options?.cwd, stdio: ['ignore', 'pipe', 'pipe'] @@ -137,17 +347,15 @@ export async function spawnAsync(command: string, args: string[], options?: { ti } /** - * MCP Server for Claude Code - * Provides a simple MCP tool to run Claude CLI in one-shot mode + * MCP Server for Claude Code with Session Continuity */ export class ClaudeCodeServer { private server: Server; - private claudeCliPath: string; // This now holds either a full path or just 'claude' - private packageVersion: string; // Add packageVersion property + private claudeCliPath: string; + private packageVersion: string; constructor() { - // Use the simplified findClaudeCli function - this.claudeCliPath = findClaudeCli(); // Removed debugMode argument + this.claudeCliPath = findClaudeCli(); console.error(`[Setup] Using Claude CLI command/path: ${this.claudeCliPath}`); this.packageVersion = SERVER_VERSION; @@ -172,59 +380,77 @@ export class ClaudeCodeServer { }); } - /** - * Set up the MCP tool handlers - */ private setupToolHandlers(): void { - // Define available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'claude_code', - description: `Claude Code Agent: Your versatile multi-modal assistant for code, file, Git, and terminal operations via Claude CLI. Use \`workFolder\` for contextual execution. - -• File ops: Create, read, (fuzzy) edit, move, copy, delete, list files, analyze/ocr images, file content analysis - └─ e.g., "Create /tmp/log.txt with 'system boot'", "Edit main.py to replace 'debug_mode = True' with 'debug_mode = False'", "List files in /src", "Move a specific section somewhere else" - -• Code: Generate / analyse / refactor / fix - └─ e.g. "Generate Python to parse CSV→JSON", "Find bugs in my_script.py" - -• Git: Stage ▸ commit ▸ push ▸ tag (any workflow) - └─ "Commit '/workspace/src/main.java' with 'feat: user auth' to develop." - -• Terminal: Run any CLI cmd or open URLs - └─ "npm run build", "Open https://developer.mozilla.org" - -• Web search + summarise content on-the-fly - -• Multi-step workflows (Version bumps, changelog updates, release tagging, etc.) - -• GitHub integration Create PRs, check CI status - -• Confused or stuck on an issue? Ask Claude Code for a second opinion, it might surprise you! - -**Prompt tips** - -1. Be concise, explicit & step-by-step for complex tasks. No need for niceties, this is a tool to get things done. -2. For multi-line text, write it to a temporary file in the project root, use that file, then delete it. -3. If you get a timeout, split the task into smaller steps. -4. **Seeking a second opinion/analysis**: If you're stuck or want advice, you can ask \`claude_code\` to analyze a problem and suggest solutions. Clearly state in your prompt that you are looking for analysis only and no actual file modifications should be made. -5. If workFolder is set to the project path, there is no need to repeat that path in the prompt and you can use relative paths for files. -6. Claude Code is really good at complex multi-step file operations and refactorings and faster than your native edit features. -7. Combine file operations, README updates, and Git commands in a sequence. -8. Claude can do much more, just ask it! - - `, + description: `Claude Code Agent with Session Continuity: Multi-modal assistant for code, file, Git, and terminal operations via Claude CLI. + +**Session Continuity** (Default behavior): +- Pass \`sessionId\` from your parent interface to maintain conversation context +- First call: optionally include \`messages\` array with conversation history +- Subsequent calls: just pass \`sessionId\` and \`prompt\` - context is maintained +- Response includes session ID for tracking + +**Subagent Invocation**: +- Use \`/commandname\` in prompts to invoke Claude Code subagents (translated to @mentions) + +**Stateless Mode** (Legacy): +- Set \`stateless: true\` for single-prompt behavior without session tracking + +**Capabilities**: +• File ops: Create, read, edit, move, copy, delete, list, analyze images +• Code: Generate, analyze, refactor, fix bugs +• Git: Stage, commit, push, tag, create PRs +• Terminal: Run CLI commands, open URLs +• Web search + summarization +• Multi-step workflows + +**Tips**: +1. Be concise and explicit for complex tasks +2. Use \`workFolder\` for file operations +3. For long tasks, split into smaller steps +4. Combine operations in sequences for efficiency +`, inputSchema: { type: 'object', properties: { prompt: { type: 'string', - description: 'The detailed natural language prompt for Claude to execute.', + description: 'The message or prompt for Claude to process.', }, workFolder: { type: 'string', - description: 'Mandatory when using file operations or referencing any file. The working directory for the Claude CLI execution. Must be an absolute path.', + description: 'Working directory for file operations. Must be an absolute path.', + }, + sessionId: { + type: 'string', + description: 'Session ID from the parent interface. Enables conversation continuity - subsequent calls with the same sessionId will resume the Claude Code session.', + }, + messages: { + type: 'array', + description: 'Full conversation history from the parent interface. Only needed on the first call - subsequent calls use sessionId for continuity.', + items: { + type: 'object', + properties: { + role: { + type: 'string', + enum: ['user', 'assistant'], + description: 'The role of the message sender.', + }, + content: { + type: 'string', + description: 'The message content.', + }, + }, + required: ['role', 'content'], + }, + }, + stateless: { + type: 'boolean', + description: 'Force single-prompt mode without session continuity. Default is false (sessions enabled).', + default: false, }, }, required: ['prompt'], @@ -233,20 +459,16 @@ export class ClaudeCodeServer { ], })); - // Handle tool calls const executionTimeoutMs = 1800000; // 30 minutes timeout this.server.setRequestHandler(CallToolRequestSchema, async (args, call): Promise => { debugLog('[Debug] Handling CallToolRequest:', args); - // Correctly access toolName from args.params.name const toolName = args.params.name; if (toolName !== 'claude_code') { - // ErrorCode.ToolNotFound should be ErrorCode.MethodNotFound as per SDK for tools throw new McpError(ErrorCode.MethodNotFound, `Tool ${toolName} not found`); } - // Robustly access prompt from args.params.arguments const toolArguments = args.params.arguments; let prompt: string; @@ -258,18 +480,36 @@ export class ClaudeCodeServer { ) { prompt = toolArguments.prompt; } else { - throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: prompt (must be an object with a string "prompt" property) for claude_code tool'); + throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: "prompt" must be a string.'); + } + + // Validate optional sessionId + const sessionId = toolArguments.sessionId; + if (sessionId !== undefined && typeof sessionId !== 'string') { + throw new McpError(ErrorCode.InvalidParams, 'Invalid parameter: "sessionId" must be a string if provided.'); } - // Determine the working directory - let effectiveCwd = homedir(); // Default CWD is user's home directory + // Validate optional messages array + const messages = toolArguments.messages; + if (messages !== undefined) { + if (!Array.isArray(messages)) { + throw new McpError(ErrorCode.InvalidParams, 'Invalid parameter: "messages" must be an array if provided.'); + } + for (const msg of messages) { + if (typeof msg !== 'object' || msg === null || + typeof msg.role !== 'string' || typeof msg.content !== 'string' || + (msg.role !== 'user' && msg.role !== 'assistant')) { + throw new McpError(ErrorCode.InvalidParams, 'Invalid parameter: each message must have "role" (user|assistant) and "content" (string).'); + } + } + } + + let effectiveCwd = homedir(); - // Check if workFolder is provided in the tool arguments if (toolArguments.workFolder && typeof toolArguments.workFolder === 'string') { const resolvedCwd = pathResolve(toolArguments.workFolder); debugLog(`[Debug] Specified workFolder: ${toolArguments.workFolder}, Resolved to: ${resolvedCwd}`); - // Check if the resolved path exists if (existsSync(resolvedCwd)) { effectiveCwd = resolvedCwd; debugLog(`[Debug] Using workFolder as CWD: ${effectiveCwd}`); @@ -283,19 +523,42 @@ export class ClaudeCodeServer { try { debugLog(`[Debug] Attempting to execute Claude CLI with prompt: "${prompt}" in CWD: "${effectiveCwd}"`); - // Print tool info on first use if (isFirstToolUse) { const versionInfo = `claude_code v${SERVER_VERSION} started at ${serverStartupTime}`; console.error(versionInfo); isFirstToolUse = false; } - const claudeProcessArgs = ['--dangerously-skip-permissions', '-p', prompt]; + // Use already-validated values + const parentSessionId = sessionId; + const validatedMessages = messages as ConversationMessage[] | undefined; + const stateless = toolArguments.stateless === true; + + let processedPrompt = translateSlashCommands(prompt); + + const claudeProcessArgs: string[] = ['--dangerously-skip-permissions']; + + if (!stateless && parentSessionId) { + const existingClaudeSessionId = getSessionMapping(parentSessionId); + + if (existingClaudeSessionId) { + debugLog(`[Debug] Resuming Claude CLI session: ${existingClaudeSessionId} for parent session: ${parentSessionId}`); + claudeProcessArgs.push('--resume', existingClaudeSessionId); + } else if (validatedMessages && validatedMessages.length > 0) { + debugLog(`[Debug] First call for parent session: ${parentSessionId}, injecting ${validatedMessages.length} messages as context`); + const contextPrefix = formatConversationContext(validatedMessages); + processedPrompt = contextPrefix + 'Continue the conversation. ' + processedPrompt; + } + } + + claudeProcessArgs.push('--output-format', 'json'); + claudeProcessArgs.push('-p', processedPrompt); + debugLog(`[Debug] Invoking Claude CLI: ${this.claudeCliPath} ${claudeProcessArgs.join(' ')}`); const { stdout, stderr } = await spawnAsync( - this.claudeCliPath, // Run the Claude CLI directly - claudeProcessArgs, // Pass the arguments + this.claudeCliPath, + claudeProcessArgs, { timeout: executionTimeoutMs, cwd: effectiveCwd } ); @@ -304,13 +567,47 @@ export class ClaudeCodeServer { debugLog('[Debug] Claude CLI stderr:', stderr.trim()); } - // Return stdout content, even if there was stderr, as claude-cli might output main result to stdout. - return { content: [{ type: 'text', text: stdout }] }; + const parsedResponse = parseClaudeResponse(stdout); + + let resultText: string; + let claudeSessionId: string | undefined; + + if (parsedResponse) { + resultText = parsedResponse.result; + claudeSessionId = parsedResponse.session_id; + + if (!stateless && parentSessionId && claudeSessionId) { + setSessionMapping(parentSessionId, claudeSessionId); + debugLog(`[Debug] Stored session mapping: ${parentSessionId} -> ${claudeSessionId}`); + } + + if (parsedResponse.usage) { + debugLog(`[Debug] Token usage - Input: ${parsedResponse.usage.input_tokens}, Output: ${parsedResponse.usage.output_tokens}`); + } + if (parsedResponse.total_cost_usd !== undefined) { + debugLog(`[Debug] Cost: $${parsedResponse.total_cost_usd.toFixed(4)}`); + } + } else { + resultText = stdout; + debugLog('[Debug] Could not parse JSON response, using raw stdout'); + } + + const responseContent: { type: 'text'; text: string }[] = [ + { type: 'text', text: resultText } + ]; + + if (!stateless && claudeSessionId) { + responseContent.push({ + type: 'text', + text: `\n---\n_Session ID: ${claudeSessionId}_` + }); + } + + return { content: responseContent }; } catch (error: any) { debugLog('[Error] Error executing Claude CLI:', error); let errorMessage = error.message || 'Unknown error'; - // Attempt to include stderr and stdout from the error object if spawnAsync attached them if (error.stderr) { errorMessage += `\nStderr: ${error.stderr}`; } @@ -319,26 +616,19 @@ export class ClaudeCodeServer { } if (error.signal === 'SIGTERM' || (error.message && error.message.includes('ETIMEDOUT')) || (error.code === 'ETIMEDOUT')) { - // Reverting to InternalError due to lint issues, but with a specific timeout message. throw new McpError(ErrorCode.InternalError, `Claude CLI command timed out after ${executionTimeoutMs / 1000}s. Details: ${errorMessage}`); } - // ErrorCode.ToolCallFailed should be ErrorCode.InternalError or a more specific execution error if available throw new McpError(ErrorCode.InternalError, `Claude CLI execution failed: ${errorMessage}`); } }); } - /** - * Start the MCP server - */ async run(): Promise { - // Revert to original server start logic if listen caused errors const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Claude Code MCP server running on stdio'); } } -// Create and run the server if this is the main module const server = new ClaudeCodeServer(); -server.run().catch(console.error); \ No newline at end of file +server.run().catch(console.error);