Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,15 @@ As an agent, before completing a task, verify that your work adheres to ALL stan
11. Validation output includes count of failed tests out of total tests run

If any standard is not met, fix the issue before submitting the work.

## Known Issues

### Claude.ai Stale MCP Session

**Symptom:** All tool calls from Claude.ai return "Error occurred during tool execution" but the server is running fine (health check passes via curl, logs show no incoming requests).

**Root cause:** Claude.ai caches MCP session state client-side. When sessions go stale (server restart, supergateway session cleanup after idle timeout, long gaps between tool calls), Claude.ai's tools/call requests fail before reaching the server. Initialize and tools/list still work (tools appear in UI) but execution silently fails.

**Fix:** Disconnect and reconnect the MCP connector in Claude.ai → Settings → Connected Apps. Takes 10 seconds. This forces a fresh session.

**Diagnostic proof:** If `tail -f /tmp/claude-code-mcp.stdout.log` shows no new lines when Claude.ai calls a tool, it's a stale session — not a server issue. Do not debug the server/supergateway/funnel.
21 changes: 21 additions & 0 deletions dist/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { debugLog } from './config.js';
/**
* Determine the Claude CLI command/path.
* 1. Checks ~/.claude/local/claude
* 2. Falls back to 'claude' on PATH
*/
export function findClaudeCli() {
debugLog('[Debug] Attempting to find Claude CLI...');
const userPath = join(homedir(), '.claude', 'local', 'claude');
debugLog(`[Debug] Checking for Claude CLI at local user path: ${userPath}`);
if (existsSync(userPath)) {
debugLog(`[Debug] Found Claude CLI at local user path: ${userPath}. Using this path.`);
return userPath;
}
debugLog('[Debug] Falling back to "claude" command name, relying on spawn/PATH lookup.');
console.warn('[Warning] Claude CLI not found at ~/.claude/local/claude. Falling back to "claude" in PATH.');
return 'claude';
}
29 changes: 29 additions & 0 deletions dist/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Environment-driven configuration and named constants
export const DEBUG_MODE = process.env.MCP_CLAUDE_DEBUG === 'true';
export const HEARTBEAT_INTERVAL_MS = parseInt(process.env.MCP_HEARTBEAT_INTERVAL_MS || '15000', 10);
export const EXECUTION_TIMEOUT_MS = parseInt(process.env.MCP_EXECUTION_TIMEOUT_MS || '1800000', 10);
export const USE_ROO_MODES = process.env.MCP_USE_ROOMODES === 'true';
export const MAX_RETRIES = parseInt(process.env.MCP_MAX_RETRIES || '3', 10);
export const RETRY_DELAY_MS = parseInt(process.env.MCP_RETRY_DELAY_MS || '1000', 10);
export const WATCH_ROO_MODES = process.env.MCP_WATCH_ROOMODES === 'true';
// Named constants (formerly magic numbers)
export const CACHE_TTL_MS = 60_000;
export const SHUTDOWN_TIMEOUT_MS = 10_000;
export const SHUTDOWN_POLL_MS = 100;
export const HEALTH_CHECK_TIMEOUT_MS = 5_000;
export const CONVERTER_TIMEOUT_MS = 30_000;
export const TASK_TTL_MS = parseInt(process.env.MCP_TASK_TTL_MS || '1800000', 10);
export const TASK_CLEANUP_INTERVAL_MS = 300_000;
export const TASK_PARTIAL_OUTPUT_LIMIT = 10_000;
// Tool name constants
export const TOOL_NAMES = {
HEALTH: 'health',
CLAUDE_CODE: 'claude_code',
CONVERT_TASK: 'convert_task_markdown',
GET_TASK_RESULT: 'get_task_result',
};
export function debugLog(message, ...optionalParams) {
if (DEBUG_MODE) {
console.error(message, ...optionalParams);
}
}
41 changes: 41 additions & 0 deletions dist/descriptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Claude Code tool description — separated to keep tool schema definitions scannable
export const CLAUDE_CODE_DESCRIPTION = `ASYNC Claude Code Agent for code, file, Git, and terminal operations via Claude CLI.

IMPORTANT — THIS TOOL IS ASYNCHRONOUS:
This tool returns IMMEDIATELY with a taskId. It does NOT return the result directly.
You MUST poll get_task_result with that taskId to get the actual result.

REQUIRED WORKFLOW:
1. Call claude_code with your prompt → receives {"taskId": "...", "status": "running"}
2. Call get_task_result with {"taskId": "..."} (wait ~30 seconds between polls)
3. If status is "running", go back to step 2
4. If status is "completed", the "output" field contains the full result
5. If status is "failed", the "error" field explains what went wrong

NEVER assume the task is done after calling claude_code. ALWAYS poll get_task_result.
NEVER fabricate or guess results. ONLY use the output from get_task_result.

Capabilities:
• File ops: Create, read, edit, move, copy, delete, list, analyze images
• Code: Generate, analyse, refactor, fix
• Git: Stage, commit, push, tag, create PRs
• Terminal: Run any CLI command
• Web search + summarise
• Multi-step workflows (version bumps, changelogs, releases)

Prompt tips:
1. Be concise and explicit. Step-by-step for complex tasks.
2. Set workFolder to the project path, then use relative paths.
3. For analysis only, state "no file modifications" in your prompt.
4. For task orchestration, use parentTaskId and returnMode: "summary".`;
export const GET_TASK_RESULT_DESCRIPTION = `Poll for the result of an async claude_code task.

Returns the current status and output of a task started by claude_code.

Statuses:
- "running": Task is still executing. partialOutput shows recent output.
- "completed": Task finished successfully. output contains the full result.
- "failed": Task encountered an error. error explains what went wrong.
- "not_found": No task with this ID exists (expired or invalid).

Call this tool every 30 seconds after starting a task with claude_code until status is "completed" or "failed".`;
62 changes: 62 additions & 0 deletions dist/roomodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { existsSync, readFileSync, statSync, watch } from 'node:fs';
import * as path from 'node:path';
import { USE_ROO_MODES, WATCH_ROO_MODES, CACHE_TTL_MS, debugLog, } from './config.js';
export function createRooModesManager() {
let cache = null;
let watcher = null;
function init() {
if (!USE_ROO_MODES || !WATCH_ROO_MODES)
return;
const roomodesPath = path.join(process.cwd(), '.roomodes');
if (!existsSync(roomodesPath)) {
console.error(`[Warning] Cannot watch .roomodes file as it doesn't exist at: ${roomodesPath}`);
return;
}
try {
watcher = watch(roomodesPath, (eventType) => {
if (eventType === 'change') {
cache = null;
console.error(`[Info] .roomodes file changed, cache invalidated`);
}
});
process.on('exit', () => {
try {
watcher?.close();
}
catch { /* ignore during shutdown */ }
});
console.error(`[Setup] Watching .roomodes file for changes`);
}
catch (error) {
console.error(`[Warning] Failed to set up watcher for .roomodes file:`, error);
}
}
function load() {
try {
const roomodesPath = path.join(process.cwd(), '.roomodes');
if (!existsSync(roomodesPath))
return null;
const fileModifiedTime = statSync(roomodesPath).mtimeMs;
if (cache &&
cache.timestamp > fileModifiedTime &&
Date.now() - cache.timestamp < CACHE_TTL_MS) {
debugLog('[Debug] Using cached .roomodes configuration');
return cache.data;
}
const content = readFileSync(roomodesPath, 'utf8');
const parsedData = JSON.parse(content);
cache = { data: parsedData, timestamp: Date.now() };
debugLog('[Debug] Loaded fresh .roomodes configuration');
return parsedData;
}
catch (error) {
debugLog('[Error] Failed to load .roomodes file:', error);
return null;
}
}
return { init, load };
}
// Default singleton for backward compatibility
const defaultManager = createRooModesManager();
export const initRooModesWatcher = defaultManager.init;
export const loadRooModes = defaultManager.load;
Loading