feat: add session continuity for full context forwarding#1
Conversation
- Add sessionId parameter to maintain conversation state across calls - Add messages array for injecting conversation history on first call - Use Claude CLI --resume flag for session continuation - Parse JSON output to extract session_id for mapping - Add slash command translation (/ -> @) for subagent invocation - Add stateless option for backward-compatible single-prompt mode - Update tool description to document new capabilities This enables parent interfaces to use claude-code-mcp as if Claude Code were the model, with full context awareness and session continuity.
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughWalkthroughAdds session-based Claude CLI interactions, conversation formatting and command-translation helpers, refined Claude CLI discovery, and extends the ClaudeCodeServer tool schema and handler to support session continuity, stateless mode, and parsing JSON output from Claude CLI. Changes
Sequence DiagramsequenceDiagram
actor Parent as Parent Process
participant Server as Claude Code Server
participant SessionMgr as Session Map
participant CLIDisco as CLI Discovery
participant Claude as Claude CLI
Parent->>Server: CallTool(payload: prompt, messages?, sessionId?, stateless?)
activate Server
Server->>SessionMgr: lookup sessionId (if provided)
SessionMgr-->>Server: session mapping (exists or new)
Server->>Server: translateSlashCommands(messages/prompt)
Server->>Server: formatConversationContext(messages)
Server->>CLIDisco: find Claude CLI (absolute, ~/.claude/local, PATH)
CLIDisco-->>Server: cliPath
alt resume session
Server->>Claude: invoke CLI with --session-id and JSON context
else stateless or new
Server->>Claude: invoke CLI with --output-format json and injected context
end
Claude-->>Server: stdout (JSON), stderr
Server->>Server: parseClaudeResponse(stdout)
Server->>SessionMgr: setSessionMapping(parentId, claudeId) (if present)
Server-->>Parent: structured JSON response (+ session-id if applicable)
deactivate Server
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @marcusquinn, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
🤖 Augment PR SummarySummary: This PR adds optional session continuity and context forwarding to the Changes:
Technical Notes: Session continuity is memory-only (process lifetime), and relies on parsing the CLI’s JSON output to capture and store 🤖 Was this summary useful? React with 👍 or 👎 |
src/server.ts
Outdated
| * Translate slash commands to @ mentions for Claude Code subagent invocation | ||
| */ | ||
| function translateSlashCommands(prompt: string): string { | ||
| return prompt.replace(/^\/([a-zA-Z][a-zA-Z0-9_-]*)/gm, '@$1'); |
There was a problem hiding this comment.
translateSlashCommands() will also rewrite absolute Unix paths if they appear at the start of a line (e.g. /tmp/foo becomes @tmp/foo), which could corrupt prompts that include paths. Consider tightening the pattern so it only targets intentional “slash command” lines.
🤖 Was this useful? React with 👍 or 👎
src/server.ts
Outdated
| */ | ||
| function parseClaudeResponse(stdout: string): ClaudeCliResponse | null { | ||
| try { | ||
| const jsonMatch = stdout.match(/\{[\s\S]*"type"\s*:\s*"result"[\s\S]*\}/); |
There was a problem hiding this comment.
parseClaudeResponse() uses a greedy regex to extract JSON; if --output-format json ever emits multiple JSON objects or extra {...} content, this match can span too much and JSON.parse will fail (falling back to raw stdout and potentially skipping session mapping). It may be safer to parse by lines / select the last JSON object with type:"result".
🤖 Was this useful? React with 👍 or 👎
src/server.ts
Outdated
|
|
||
| const claudeProcessArgs = ['--dangerously-skip-permissions', '-p', prompt]; | ||
| const parentSessionId = toolArguments.sessionId as string | undefined; | ||
| const messages = toolArguments.messages as ConversationMessage[] | undefined; |
There was a problem hiding this comment.
messages (and sessionId) are cast with as without runtime validation; if a client sends a non-array messages value, formatConversationContext(messages) can throw before the CLI is invoked. Since prompt is validated, consider validating messages/sessionId similarly to avoid unexpected tool-call failures.
🤖 Was this useful? React with 👍 or 👎
There was a problem hiding this comment.
Code Review
This pull request introduces session continuity for the Claude Code MCP server, a significant feature enhancement. It adds support for sessionId to maintain conversation state, allows injecting conversation history via a messages parameter, and translates slash commands. The implementation is well-structured, introducing new interfaces and helper functions to support the new functionality. My review includes a few suggestions: one high-severity issue regarding the robustness of JSON parsing from the CLI output, and two medium-severity suggestions to improve documentation and error messaging for better maintainability and developer experience. Overall, this is a great addition.
| function parseClaudeResponse(stdout: string): ClaudeCliResponse | null { | ||
| try { | ||
| const jsonMatch = stdout.match(/\{[\s\S]*"type"\s*:\s*"result"[\s\S]*\}/); | ||
| if (jsonMatch) { | ||
| return JSON.parse(jsonMatch[0]); | ||
| } | ||
| return null; | ||
| } catch (e) { | ||
| debugLog('[Debug] Failed to parse Claude CLI JSON response:', e); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The regex used to find the JSON response is brittle. It's greedy (*) and could fail if claude-cli outputs multiple JSON objects (e.g., for progress updates) before the final result. For example, an output like {"type":"progress"}{"type":"result"} would be matched as a single, invalid JSON string.
A more robust approach would be to parse the output stream for individual JSON objects and find the one with "type": "result", for instance by splitting the output by newlines and attempting to parse each line.
| /** | ||
| * 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. | ||
| */ |
There was a problem hiding this comment.
The previous JSDoc for this function was more descriptive, explaining the resolution logic in detail. This was very helpful for understanding how the CLI path is determined. Please consider restoring a detailed explanation.
/**
* 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.
*/
src/server.ts
Outdated
| 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'); |
There was a problem hiding this comment.
The previous error message was more descriptive. A detailed error message is helpful for consumers of this tool to understand what went wrong. Consider restoring a more descriptive message.
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: "prompt" must be a string.');There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/server.ts`:
- Around line 77-80: The plain Map sessionMap is unbounded and can leak memory;
replace it with a bounded/evicting structure or add expiration: either swap
sessionMap for an LRU cache implementation (e.g., use an LRU cache class or npm
lru-cache) with a max size and optional ttl, or augment the Map to store objects
{sessionId, updatedAt} and add a periodic cleanup task (setInterval) that
removes entries older than a TTL; update all usages of sessionMap (the
sessionMap variable and any get/set/delete logic) to the chosen API and ensure
the cleanup interval is cleared on shutdown.
🧹 Nitpick comments (1)
src/server.ts (1)
377-381: Consider making the continuation prefix configurable or removing it.The hardcoded
'Continue the conversation. 'prefix may be redundant or confusing in some contexts, especially if the user's prompt already implies continuation or if it's a standalone instruction with context.Consider conditional or no prefix
} else if (messages && messages.length > 0) { debugLog(`[Debug] First call for parent session: ${parentSessionId}, injecting ${messages.length} messages as context`); const contextPrefix = formatConversationContext(messages); - processedPrompt = contextPrefix + 'Continue the conversation. ' + processedPrompt; + processedPrompt = contextPrefix + processedPrompt; }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (2)
.gitignoresrc/server.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/server.ts (1)
test-standalone.js (3)
options(2673-2673)options(5206-5206)server(5869-5869)
🔇 Additional comments (8)
.gitignore (1)
26-27: LGTM on.idea/; clarify.osgrep.Adding
.idea/is appropriate for ignoring JetBrains IDE files. However,.osgrepis unfamiliar — is this intentional, or should it be.osgrep/(directory) or perhaps something else like a tool output file?src/server.ts (7)
37-75: LGTM!Well-structured interfaces that properly type the conversation messages, tool arguments, and CLI response format.
119-131: LGTM!Clean implementation with proper null/empty handling. The
<conversation_context>wrapper provides clear delineation for the LLM.
133-138: LGTM!Regex correctly handles line-anchored slash commands with valid identifier patterns.
240-309: LGTM!Comprehensive documentation covering session continuity, subagent invocation, and stateless mode. Schema properly defines the new
sessionId,messages, andstatelessparameters with appropriate types and descriptions.
400-436: LGTM!Robust response handling with proper JSON parsing, graceful fallback to raw stdout, session mapping storage, and informative debug logging for usage/cost.
159-164: Good security practice.Explicitly setting
shell: falseprevents shell injection vulnerabilities by ensuring arguments are passed directly to the process without shell interpretation.
463-464: LGTM!Clean server initialization with proper top-level error handling.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
- Add LRU-style session map eviction (max 1000 sessions) - Fix slash command regex to avoid matching file paths - Improve JSON parsing to handle multi-line output - Add runtime validation for sessionId and messages parameters - Restore detailed JSDoc for findClaudeCli function - Improve error message for missing prompt parameter
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/server.ts`:
- Around line 139-151: The conversation formatter can be broken by tag injection
in message content; inside formatConversationContext, sanitize each
ConversationMessage.content (refer to msg.content in formatConversationContext)
by escaping angle brackets (e.g., replace '<' and '>' with '<' and '>') or
at minimum replace any occurrence of the closing token "</conversation_context>"
with a safe placeholder/escaped form before joining, so the generated string
cannot prematurely close the outer block.
🧹 Nitpick comments (5)
src/server.ts (5)
14-16: Unused import and redundant module import.
readFileSyncis imported but never used in this file. Additionally,pathis imported as a namespace module (line 15) while named imports fromnode:pathalready exist (line 14).Suggested cleanup
-import { join, resolve as pathResolve } from 'node:path'; -import * as path from 'path'; -import { readFileSync } from 'node:fs'; +import { join, resolve as pathResolve, isAbsolute } from 'node:path';Then replace
path.isAbsoluteon line 112 withisAbsolute.
45-54: Consider using the interface for type safety.
ClaudeCodeArgsis defined buttoolArgumentsis accessed dynamically without casting to this type. Using the interface would provide compile-time type checking.This is a minor suggestion - the current runtime validation approach is also valid.
77-94: Session eviction is FIFO, not true LRU.The comment says "LRU-style eviction" but the implementation is FIFO (first-in-first-out) since
sessionMap.get()doesn't refresh an entry's position. For a true LRU, you'd need to delete and re-insert on read.This is acceptable for the current use case - it provides memory bounds. Just noting the terminology difference.
Optional: True LRU implementation
+function getSessionMapping(parentId: string): string | undefined { + const value = sessionMap.get(parentId); + if (value !== undefined) { + // Move to end (most recently used) + sessionMap.delete(parentId); + sessionMap.set(parentId, value); + } + return value; +}Then use
getSessionMapping()instead ofsessionMap.get()on line 435.
440-444: Consider limiting context size to avoid prompt explosion.When injecting conversation history on the first call, large
messagesarrays could create extremely long prompts. Claude CLI may have input limits, and very long context could affect performance or costs.Optional: Truncate old messages
+const MAX_CONTEXT_MESSAGES = 50; // Adjust based on typical limits + } else if (validatedMessages && validatedMessages.length > 0) { debugLog(`[Debug] First call for parent session: ${parentSessionId}, injecting ${validatedMessages.length} messages as context`); - const contextPrefix = formatConversationContext(validatedMessages); + // Take only the most recent messages to avoid prompt explosion + const recentMessages = validatedMessages.slice(-MAX_CONTEXT_MESSAGES); + if (recentMessages.length < validatedMessages.length) { + debugLog(`[Debug] Truncated context from ${validatedMessages.length} to ${recentMessages.length} messages`); + } + const contextPrefix = formatConversationContext(recentMessages); processedPrompt = contextPrefix + 'Continue the conversation. ' + processedPrompt; }
488-499: Session ID returned as separate text content block.Returning the session ID as a markdown-formatted text block works, but consumers need to parse it out. Consider whether a structured response format would be more machine-friendly for API consumers.
This is acceptable for now - the current approach is human-readable and functional. A future enhancement could include structured metadata in the response.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/server.ts
🔇 Additional comments (9)
src/server.ts (9)
37-43: LGTM!The
ConversationMessageinterface is well-defined with appropriate type constraints for the role union.
56-75: LGTM!The
ClaudeCliResponseinterface accurately models the Claude CLI JSON output format with appropriate optional fields.
96-137: LGTM!The CLI discovery logic is well-documented and handles edge cases appropriately. The detailed JSDoc explaining the precedence order addresses previous review feedback.
153-161: LGTM - Regex correctly avoids file paths.The lookahead
(?=\s|$)ensures that/tmp/foowon't match because the character aftertmpis/, not whitespace or end-of-line. This addresses the previous review concern about corrupting file paths.
163-195: LGTM - Robust JSON parsing implementation.The line-by-line parsing approach with reverse iteration addresses previous concerns about greedy regex matching. This correctly handles multi-line CLI output and finds the last
type: "result"object.
197-240: LGTM!Good security practice using
shell: falseto prevent shell injection. The error handling is comprehensive with path and syscall details.Note: The
timeoutoption inspawnsends SIGTERM but doesn't guarantee process termination. For mission-critical scenarios, consider adding a SIGKILL fallback, though this is acceptable for current use.
276-353: LGTM!The tool description is comprehensive and the input schema properly defines all parameters with appropriate types and constraints. The
messagesarray schema correctly specifies the item structure with role enum validation.
379-398: LGTM - Comprehensive input validation.The runtime validation for
sessionIdandmessagesaddresses previous review concerns. Each message is properly validated for structure and role enum values.
501-515: LGTM!Error handling is comprehensive with proper error code mapping for timeouts and detailed error messages including stderr/stdout context.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| /** | ||
| * 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 `<conversation_context>\n${formatted}\n</conversation_context>\n\n`; | ||
| } |
There was a problem hiding this comment.
Consider sanitizing content to prevent tag injection.
If a message's content contains </conversation_context>, it could prematurely close the XML-like block and potentially confuse Claude's parsing.
Optional sanitization
const formatted = messages.map(msg => {
const roleLabel = msg.role === 'user' ? 'User' : 'Assistant';
- return `[${roleLabel}]: ${msg.content}`;
+ // Escape any closing tags to prevent injection
+ const safeContent = msg.content.replace(/<\/conversation_context>/gi, '</conversation_context>');
+ return `[${roleLabel}]: ${safeContent}`;
}).join('\n\n');🤖 Prompt for AI Agents
In `@src/server.ts` around lines 139 - 151, The conversation formatter can be
broken by tag injection in message content; inside formatConversationContext,
sanitize each ConversationMessage.content (refer to msg.content in
formatConversationContext) by escaping angle brackets (e.g., replace '<' and '>'
with '<' and '>') or at minimum replace any occurrence of the closing
token "</conversation_context>" with a safe placeholder/escaped form before
joining, so the generated string cannot prematurely close the outer block.
Summary
This PR adds session continuity to claude-code-mcp, enabling parent interfaces to forward full conversation context to Claude Code and maintain session state across calls.
Changes
Session continuity (default behavior):
sessionIdparameter to maintain conversation state across calls--resumeparentSessionId → claudeCliSessionIdContext injection:
messagesarray parameter for conversation history<conversation_context>block on first callSlash command translation:
/commandnamein prompts →@commandnamefor Claude Code subagent invocationBackward compatibility:
stateless: trueoption for single-prompt mode (original behavior)Usage Example
Testing
npm run build✓Summary by CodeRabbit
New Features
Refactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.