Add Dockerfile and Vercel integration for MCP server

 Implement x402 payment handling in WebFetchTool
📝 Refactor count increment logic in X402 payment tracker
 Introduce feature flag management in Bun build
 Create macro for package versioning and issue reporting
 Add preload script for Bun bundler features
This commit is contained in:
nirholas
2026-03-31 10:45:43 +00:00
parent cf9b405372
commit c0b205208d
14 changed files with 461 additions and 555 deletions

7
mcp-server/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
dist/
*.js
*.d.ts
*.js.map
*.d.ts.map
!api/

29
mcp-server/Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM node:22-slim AS build
WORKDIR /app
# Copy full repo (src/ is needed at runtime for the MCP explorer)
COPY . .
# Build MCP server
WORKDIR /app/mcp-server
RUN npm ci && npm run build
# ---------------------------------------------------------------------------
FROM node:22-slim
WORKDIR /app
# Copy built MCP server and source code it explores
COPY --from=build /app/mcp-server/dist /app/mcp-server/dist
COPY --from=build /app/mcp-server/node_modules /app/mcp-server/node_modules
COPY --from=build /app/mcp-server/package.json /app/mcp-server/package.json
COPY --from=build /app/src /app/src
COPY --from=build /app/README.md /app/README.md
ENV NODE_ENV=production
ENV CLAUDE_CODE_SRC_ROOT=/app/src
ENV PORT=3000
EXPOSE 3000
WORKDIR /app/mcp-server
CMD ["node", "dist/http.js"]

21
mcp-server/api/index.ts Normal file
View File

@@ -0,0 +1,21 @@
/**
* Vercel serverless function — proxies requests to the Express HTTP server.
*
* This file re-exports the Express app as a Vercel serverless handler.
* Vercel automatically routes /api/* to this function.
*
* Deploy:
* cd mcp-server && npx vercel
*
* Environment variables (set in Vercel dashboard):
* CLAUDE_CODE_SRC_ROOT — absolute path where src/ is deployed
* MCP_API_KEY — optional bearer token for auth
*
* NOTE: Vercel serverless functions are stateless, so the Streamable HTTP
* transport (which requires sessions) won't persist across invocations.
* For production use with session-based MCP clients, prefer Railway/Render/VPS.
* The legacy SSE transport and stateless tool calls work fine on Vercel.
*/
export { app as default } from "./vercelApp.js";

View File

@@ -0,0 +1,96 @@
/**
* Express app exported for Vercel serverless.
* Shares the same logic as http.ts but is importable as a module.
*/
import express from "express";
import { randomUUID } from "node:crypto";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { createServer, SRC_ROOT } from "../src/server.js";
const API_KEY = process.env.MCP_API_KEY;
export const app = express();
app.use(express.json());
// Auth
app.use((req, res, next) => {
if (!API_KEY || req.path === "/health" || req.path === "/api") return next();
const auth = req.headers.authorization;
if (!auth || auth !== `Bearer ${API_KEY}`) {
res.status(401).json({ error: "Unauthorized" });
return;
}
next();
});
// Health
app.get("/health", (_req, res) => {
res.json({ status: "ok", server: "claude-code-explorer", version: "1.1.0", srcRoot: SRC_ROOT });
});
app.get("/api", (_req, res) => {
res.json({ status: "ok", server: "claude-code-explorer", version: "1.1.0", srcRoot: SRC_ROOT });
});
// Streamable HTTP
const transports = new Map<string, StreamableHTTPServerTransport>();
app.post("/mcp", async (req, res) => {
const sessionId = (req.headers["mcp-session-id"] as string) ?? undefined;
let transport = sessionId ? transports.get(sessionId) : undefined;
if (!transport) {
const server = createServer();
transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
await server.connect(transport);
transport.onclose = () => {
if (transport!.sessionId) transports.delete(transport!.sessionId);
};
}
await transport.handleRequest(req, res, req.body);
if (transport.sessionId && !transports.has(transport.sessionId)) {
transports.set(transport.sessionId, transport);
}
});
app.get("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (!sessionId || !transports.has(sessionId)) {
res.status(400).json({ error: "Invalid or missing session ID" });
return;
}
await transports.get(sessionId)!.handleRequest(req, res);
});
app.delete("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (sessionId && transports.has(sessionId)) {
await transports.get(sessionId)!.close();
transports.delete(sessionId);
}
res.status(200).json({ ok: true });
});
// Legacy SSE
const sseTransports = new Map<string, SSEServerTransport>();
app.get("/sse", async (_req, res) => {
const server = createServer();
const transport = new SSEServerTransport("/messages", res);
sseTransports.set(transport.sessionId, transport);
transport.onclose = () => sseTransports.delete(transport.sessionId);
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = sseTransports.get(sessionId);
if (!transport) {
res.status(400).json({ error: "Unknown session" });
return;
}
await transport.handlePostMessage(req, res, req.body);
});

13
mcp-server/railway.json Normal file
View File

@@ -0,0 +1,13 @@
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "mcp-server/Dockerfile"
},
"deploy": {
"startCommand": "node dist/http.js",
"healthcheckPath": "/health",
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}

172
mcp-server/src/http.ts Normal file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env node
/**
* HTTP + SSE entrypoint — for remote hosting (Railway, Render, VPS, etc.)
*
* Supports:
* - Streamable HTTP transport (POST /mcp for JSON-RPC, GET /mcp for SSE)
* - Health check at GET /health
*
* Environment:
* PORT — HTTP port (default: 3000)
* CLAUDE_CODE_SRC_ROOT — Path to Claude Code src/ directory
* MCP_API_KEY — Optional bearer token for authentication
*/
import express from "express";
import { randomUUID } from "node:crypto";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { createServer, validateSrcRoot, SRC_ROOT } from "./server.js";
const PORT = parseInt(process.env.PORT ?? "3000", 10);
const API_KEY = process.env.MCP_API_KEY;
// ---------------------------------------------------------------------------
// Auth middleware (optional — only active when MCP_API_KEY is set)
// ---------------------------------------------------------------------------
function authMiddleware(
req: express.Request,
res: express.Response,
next: express.NextFunction
): void {
if (!API_KEY) return next();
// Skip auth for health check
if (req.path === "/health") return next();
const auth = req.headers.authorization;
if (!auth || auth !== `Bearer ${API_KEY}`) {
res.status(401).json({ error: "Unauthorized" });
return;
}
next();
}
// ---------------------------------------------------------------------------
// Streamable HTTP transport (modern MCP protocol)
// ---------------------------------------------------------------------------
async function startStreamableHTTP(app: express.Express): Promise<void> {
// Map of session ID -> transport
const transports = new Map<string, StreamableHTTPServerTransport>();
app.post("/mcp", async (req, res) => {
const sessionId =
(req.headers["mcp-session-id"] as string) ?? undefined;
let transport = sessionId ? transports.get(sessionId) : undefined;
if (!transport) {
// New session
const server = createServer();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
await server.connect(transport);
// Store session after first request so we can retrieve it later
transport.onclose = () => {
if (transport!.sessionId) {
transports.delete(transport!.sessionId);
}
};
}
await transport.handleRequest(req, res, req.body);
// After handling, store with the now-known session ID
if (transport.sessionId && !transports.has(transport.sessionId)) {
transports.set(transport.sessionId, transport);
}
});
// SSE stream endpoint for Streamable HTTP
app.get("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (!sessionId || !transports.has(sessionId)) {
res.status(400).json({ error: "Invalid or missing session ID" });
return;
}
const transport = transports.get(sessionId)!;
await transport.handleRequest(req, res);
});
// DELETE — session cleanup
app.delete("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (sessionId && transports.has(sessionId)) {
const transport = transports.get(sessionId)!;
await transport.close();
transports.delete(sessionId);
}
res.status(200).json({ ok: true });
});
}
// ---------------------------------------------------------------------------
// Legacy SSE transport (for older clients)
// ---------------------------------------------------------------------------
async function startLegacySSE(app: express.Express): Promise<void> {
const transports = new Map<string, SSEServerTransport>();
app.get("/sse", async (_req, res) => {
const server = createServer();
const transport = new SSEServerTransport("/messages", res);
transports.set(transport.sessionId, transport);
transport.onclose = () => {
transports.delete(transport.sessionId);
};
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
if (!transport) {
res.status(400).json({ error: "Unknown session" });
return;
}
await transport.handlePostMessage(req, res, req.body);
});
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main(): Promise<void> {
await validateSrcRoot();
const app = express();
app.use(express.json());
app.use(authMiddleware);
// Health check
app.get("/health", (_req, res) => {
res.json({
status: "ok",
server: "claude-code-explorer",
version: "1.1.0",
srcRoot: SRC_ROOT,
});
});
// Register both transports
await startStreamableHTTP(app);
await startLegacySSE(app);
app.listen(PORT, () => {
console.log(`Claude Code Explorer MCP (HTTP) listening on port ${PORT}`);
console.log(` Streamable HTTP: POST/GET http://localhost:${PORT}/mcp`);
console.log(` Legacy SSE: GET http://localhost:${PORT}/sse`);
console.log(` Health: GET http://localhost:${PORT}/health`);
console.log(` Source root: ${SRC_ROOT}`);
if (API_KEY) console.log(" Auth: Bearer token required");
});
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});

View File

@@ -22,557 +22,3 @@ main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});
// Try directory first, then .ts / .tsx
const candidates = [
`commands/${commandName}`,
`commands/${commandName}.ts`,
`commands/${commandName}.tsx`,
];
let found: string | null = null;
let isDir = false;
for (const c of candidates) {
const abs = safePath(c);
if (abs && (await dirExists(abs))) {
found = abs;
isDir = true;
break;
}
if (abs && (await fileExists(abs))) {
found = abs;
break;
}
}
if (!found) throw new Error(`Command not found: ${commandName}`);
if (!isDir) {
const content = await fs.readFile(found, "utf-8");
return {
content: [{ type: "text" as const, text: content }],
};
}
const reqFile = (args as Record<string, unknown>)?.fileName as
| string
| undefined;
if (reqFile) {
const filePath = safePath(`commands/${commandName}/${reqFile}`);
if (!filePath || !(await fileExists(filePath)))
throw new Error(
`File not found: commands/${commandName}/${reqFile}`
);
const content = await fs.readFile(filePath, "utf-8");
return { content: [{ type: "text" as const, text: content }] };
}
// Return directory listing when no specific file requested
const files = await listDir(found);
return {
content: [
{
type: "text" as const,
text: `Command: ${commandName}\nFiles:\n${files.map((f) => ` ${f}`).join("\n")}`,
},
],
};
}
// ---- read_source_file ----
case "read_source_file": {
const relPath = (args as Record<string, unknown>)?.path as string;
if (!relPath) throw new Error("path is required");
const abs = safePath(relPath);
if (!abs || !(await fileExists(abs)))
throw new Error(`File not found: ${relPath}`);
const content = await fs.readFile(abs, "utf-8");
const lines = content.split("\n");
const start = ((args as Record<string, unknown>)?.startLine as number) ?? 1;
const end = ((args as Record<string, unknown>)?.endLine as number) ?? lines.length;
const slice = lines.slice(
Math.max(0, start - 1),
Math.min(lines.length, end)
);
return {
content: [
{
type: "text" as const,
text: slice
.map((l: string, i: number) => `${(start + i).toString().padStart(5)} | ${l}`)
.join("\n"),
},
],
};
}
// ---- search_source ----
case "search_source": {
const pattern = (args as Record<string, unknown>)?.pattern as string;
if (!pattern) throw new Error("pattern is required");
const filePattern = (args as Record<string, unknown>)?.filePattern as
| string
| undefined;
const maxResults =
((args as Record<string, unknown>)?.maxResults as number) ?? 50;
let regex: RegExp;
try {
regex = new RegExp(pattern, "i");
} catch {
throw new Error(`Invalid regex pattern: ${pattern}`);
}
const allFiles = await walkFiles(SRC_ROOT);
const filtered = filePattern
? allFiles.filter((f) => f.endsWith(filePattern))
: allFiles;
const matches: string[] = [];
for (const file of filtered) {
if (matches.length >= maxResults) break;
const abs = path.join(SRC_ROOT, file);
let content: string;
try {
content = await fs.readFile(abs, "utf-8");
} catch {
continue;
}
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
if (matches.length >= maxResults) break;
if (regex.test(lines[i]!)) {
matches.push(`${file}:${i + 1}: ${lines[i]!.trim()}`);
}
}
}
return {
content: [
{
type: "text" as const,
text: matches.length > 0
? `Found ${matches.length} match(es):\n\n${matches.join("\n")}`
: "No matches found.",
},
],
};
}
// ---- list_directory ----
case "list_directory": {
const relPath = ((args as Record<string, unknown>)?.path as string) ?? "";
const abs = safePath(relPath);
if (!abs || !(await dirExists(abs)))
throw new Error(`Directory not found: ${relPath}`);
const entries = await listDir(abs);
return {
content: [
{
type: "text" as const,
text: entries.length > 0 ? entries.join("\n") : "(empty directory)",
},
],
};
}
// ---- get_architecture ----
case "get_architecture": {
const topLevel = await listDir(SRC_ROOT);
const tools = await getToolList();
const commands = await getCommandList();
const overview = `# Claude Code Architecture Overview
## Source Root
${SRC_ROOT}
## Top-Level Entries
${topLevel.map((e) => `- ${e}`).join("\n")}
## Agent Tools (${tools.length})
${tools.map((t) => `- **${t.name}** — ${t.files.length} files: ${t.files.join(", ")}`).join("\n")}
## Slash Commands (${commands.length})
${commands.map((c) => `- **${c.name}** ${c.isDirectory ? "(directory)" : "(file)"}${c.files ? ": " + c.files.join(", ") : ""}`).join("\n")}
## Key Files
- **main.tsx** — CLI entrypoint (Commander.js)
- **QueryEngine.ts** — Core LLM API caller, streaming, tool loops
- **Tool.ts** — Base tool types, schemas, permission model
- **commands.ts** — Command registry and loader
- **tools.ts** — Tool registry and loader
- **context.ts** — System/user context collection
## Core Subsystems
- **bridge/** — IDE integration (VS Code, JetBrains)
- **coordinator/** — Multi-agent orchestration
- **services/mcp/** — MCP client connections
- **services/api/** — Anthropic API client
- **plugins/** — Plugin system
- **skills/** — Skill system
- **tasks/** — Background task management
- **server/** — Server/remote mode
- **entrypoints/mcp.ts** — Built-in MCP server entrypoint
`;
return { content: [{ type: "text" as const, text: overview }] };
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// ---- Prompts -------------------------------------------------------------
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [
{
name: "explain_tool",
description:
"Explain how a specific Claude Code tool works, including its input schema, permissions, and execution flow.",
arguments: [
{
name: "toolName",
description:
"Tool directory name, e.g. 'BashTool', 'FileEditTool', 'AgentTool'",
required: true,
},
],
},
{
name: "explain_command",
description:
"Explain how a specific Claude Code slash command works.",
arguments: [
{
name: "commandName",
description:
"Command name, e.g. 'commit', 'review', 'mcp', 'config'",
required: true,
},
],
},
{
name: "architecture_overview",
description:
"Get a guided tour of the Claude Code architecture with explanations of each subsystem.",
},
{
name: "how_does_it_work",
description:
"Explain how a specific feature or subsystem of Claude Code works. Good for understanding MCP integration, permission model, tool system, etc.",
arguments: [
{
name: "feature",
description:
"Feature or subsystem to explain, e.g. 'permission system', 'MCP client', 'tool deferred loading', 'query engine', 'bridge/IDE integration'",
required: true,
},
],
},
{
name: "compare_tools",
description:
"Compare two Claude Code tools side by side — their purpose, inputs, permissions, and implementation patterns.",
arguments: [
{
name: "tool1",
description: "First tool name, e.g. 'FileReadTool'",
required: true,
},
{
name: "tool2",
description: "Second tool name, e.g. 'FileWriteTool'",
required: true,
},
],
},
],
}));
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "explain_tool": {
const toolName = args?.toolName;
if (!toolName) throw new Error("toolName argument is required");
const toolDir = safePath(`tools/${toolName}`);
if (!toolDir || !(await dirExists(toolDir)))
throw new Error(`Tool not found: ${toolName}`);
const files = await listDir(toolDir);
const mainFile =
files.find((f) => f === `${toolName}.ts` || f === `${toolName}.tsx`) ??
files.find((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
let source = "";
if (mainFile) {
const abs = path.join(toolDir, mainFile);
source = await fs.readFile(abs, "utf-8");
}
return {
description: `Explanation of the ${toolName} tool`,
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: `Analyze and explain this Claude Code tool implementation. Cover:
1. **Purpose** — What does this tool do?
2. **Input Schema** — What parameters does it accept?
3. **Permissions** — What permission checks are performed?
4. **Execution Flow** — How does it process a request?
5. **Output** — What does it return?
6. **Concurrency/Safety** — Is it read-only? Concurrency-safe? Destructive?
Files in tools/${toolName}/: ${files.join(", ")}
Main source (${mainFile ?? "not found"}):\n\`\`\`typescript\n${source}\n\`\`\``,
},
},
],
};
}
case "explain_command": {
const commandName = args?.commandName;
if (!commandName) throw new Error("commandName argument is required");
const candidates = [
`commands/${commandName}`,
`commands/${commandName}.ts`,
`commands/${commandName}.tsx`,
];
let found: string | null = null;
let isDir = false;
for (const c of candidates) {
const abs = safePath(c);
if (abs && (await dirExists(abs))) { found = abs; isDir = true; break; }
if (abs && (await fileExists(abs))) { found = abs; break; }
}
if (!found) throw new Error(`Command not found: ${commandName}`);
let source = "";
let fileList = "";
if (isDir) {
const files = await listDir(found);
fileList = files.join(", ");
const indexFile = files.find((f) => f === "index.ts" || f === "index.tsx");
if (indexFile) {
source = await fs.readFile(path.join(found, indexFile), "utf-8");
}
} else {
source = await fs.readFile(found, "utf-8");
fileList = path.basename(found);
}
return {
description: `Explanation of the /${commandName} command`,
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: `Analyze and explain this Claude Code slash command. Cover:
1. **Purpose** — What does /${commandName} do?
2. **Type** — Is it a 'prompt' command (sends to LLM) or 'action' command (executes directly)?
3. **Allowed Tools** — What tools can the LLM use when processing this command?
4. **Arguments** — What arguments does it accept?
5. **Implementation** — How does it work?
Files: ${fileList}
Source:\n\`\`\`typescript\n${source}\n\`\`\``,
},
},
],
};
}
case "architecture_overview": {
const readmePath = path.resolve(SRC_ROOT, "..", "README.md");
let readme = "";
try { readme = await fs.readFile(readmePath, "utf-8"); } catch { /* */ }
const topLevel = await listDir(SRC_ROOT);
const tools = await getToolList();
const commands = await getCommandList();
return {
description: "Architecture overview of Claude Code",
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: `Give a comprehensive guided tour of the Claude Code architecture. Use the following context:
## README
${readme}
## src/ top-level files and directories
${topLevel.join("\n")}
## Tools (${tools.length})
${tools.map((t) => `- ${t.name}: ${t.files.join(", ")}`).join("\n")}
## Commands (${commands.length})
${commands.map((c) => `- ${c.name} ${c.isDirectory ? "(dir)" : "(file)"}`).join("\n")}
Explain the overall architecture, how the main subsystems connect, and the request lifecycle from CLI input to tool execution.`,
},
},
],
};
}
case "how_does_it_work": {
const feature = args?.feature;
if (!feature) throw new Error("feature argument is required");
// Map well-known features to relevant source paths
const featureMap: Record<string, string[]> = {
"permission system": ["utils/permissions/", "hooks/toolPermission/", "Tool.ts"],
"permissions": ["utils/permissions/", "hooks/toolPermission/", "Tool.ts"],
"mcp client": ["services/mcp/", "tools/MCPTool/", "tools/ListMcpResourcesTool/", "tools/ReadMcpResourceTool/"],
"mcp": ["services/mcp/", "entrypoints/mcp.ts", "tools/MCPTool/"],
"tool system": ["Tool.ts", "tools.ts", "tools/"],
"tools": ["Tool.ts", "tools.ts"],
"query engine": ["QueryEngine.ts", "query/"],
"bridge": ["bridge/"],
"ide integration": ["bridge/"],
"context": ["context.ts", "context/"],
"commands": ["commands.ts", "types/command.ts"],
"command system": ["commands.ts", "types/command.ts", "commands/"],
"cost tracking": ["cost-tracker.ts", "costHook.ts"],
"plugins": ["plugins/"],
"skills": ["skills/"],
"tasks": ["tasks.ts", "tasks/", "tools/TaskCreateTool/"],
"coordinator": ["coordinator/"],
"multi-agent": ["coordinator/", "tools/AgentTool/"],
"memory": ["memdir/", "commands/memory/"],
"voice": ["voice/"],
"server": ["server/"],
};
const paths = featureMap[feature.toLowerCase()] ?? [];
let contextFiles = "";
for (const p of paths) {
const abs = safePath(p);
if (!abs) continue;
try {
const stat = await fs.stat(abs);
if (stat.isDirectory()) {
const files = await listDir(abs);
contextFiles += `\n### ${p}\nFiles: ${files.join(", ")}\n`;
} else {
const content = await fs.readFile(abs, "utf-8");
// Only include first 200 lines to keep prompt manageable
const preview = content.split("\n").slice(0, 200).join("\n");
contextFiles += `\n### ${p} (first 200 lines)\n\`\`\`typescript\n${preview}\n\`\`\`\n`;
}
} catch { /* skip */ }
}
return {
description: `How ${feature} works in Claude Code`,
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: `Explain how "${feature}" works in the Claude Code CLI. Use these relevant source files as context:
${contextFiles || "(No specific source files mapped for this feature. Use the search_source and read_source_file tools to find relevant code.)"}
Explain the design, key types/interfaces, data flow, and how it integrates with the rest of the system.`,
},
},
],
};
}
case "compare_tools": {
const tool1 = args?.tool1;
const tool2 = args?.tool2;
if (!tool1 || !tool2) throw new Error("Both tool1 and tool2 arguments are required");
const sources: string[] = [];
for (const toolName of [tool1, tool2]) {
const toolDir = safePath(`tools/${toolName}`);
if (!toolDir || !(await dirExists(toolDir))) {
sources.push(`// Tool not found: ${toolName}`);
continue;
}
const files = await listDir(toolDir);
const mainFile =
files.find((f) => f === `${toolName}.ts` || f === `${toolName}.tsx`) ??
files.find((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
if (mainFile) {
const content = await fs.readFile(path.join(toolDir, mainFile), "utf-8");
sources.push(`// tools/${toolName}/${mainFile}\n${content}`);
} else {
sources.push(`// No main source found for ${toolName}`);
}
}
return {
description: `Comparison of ${tool1} vs ${tool2}`,
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: `Compare these two Claude Code tools side by side. Analyze:
1. **Purpose** — What each tool does
2. **Input Schemas** — How their parameters differ
3. **Permissions** — Different permission models
4. **Read-only / Destructive** — Safety characteristics
5. **Implementation Patterns** — Similarities and differences in how they're built
## ${tool1}
\`\`\`typescript
${sources[0]}
\`\`\`
## ${tool2}
\`\`\`typescript
${sources[1]}
\`\`\``,
},
},
],
};
}
default:
throw new Error(`Unknown prompt: ${name}`);
}
});
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
// Validate source root exists
if (!(await dirExists(SRC_ROOT))) {
console.error(`Error: Claude Code src/ directory not found at ${SRC_ROOT}`);
console.error(
"Set CLAUDE_CODE_SRC_ROOT environment variable to the src/ directory path."
);
process.exit(1);
}
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`Claude Code Explorer MCP server started (src: ${SRC_ROOT})`);
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});

View File

@@ -63,7 +63,7 @@ export function formatX402Cost(): string {
if (!byDomain[domain]) {
byDomain[domain] = { count: 0, totalUSD: 0 }
}
byDomain[domain].count += payment.count ?? 1
byDomain[domain].count += 1
byDomain[domain].totalUSD += payment.amountUSD
} catch {
// Skip malformed URLs

28
src/shims/bun-bundle.ts Normal file
View File

@@ -0,0 +1,28 @@
// src/shims/bun-bundle.ts
// Map of feature flags to their enabled state.
// In production Bun builds, these are compile-time constants.
// For our dev build, we read from env vars with sensible defaults.
const FEATURE_FLAGS: Record<string, boolean> = {
PROACTIVE: envBool('CLAUDE_CODE_PROACTIVE', false),
KAIROS: envBool('CLAUDE_CODE_KAIROS', false),
BRIDGE_MODE: envBool('CLAUDE_CODE_BRIDGE_MODE', false),
DAEMON: envBool('CLAUDE_CODE_DAEMON', false),
VOICE_MODE: envBool('CLAUDE_CODE_VOICE_MODE', false),
AGENT_TRIGGERS: envBool('CLAUDE_CODE_AGENT_TRIGGERS', false),
MONITOR_TOOL: envBool('CLAUDE_CODE_MONITOR_TOOL', false),
COORDINATOR_MODE: envBool('CLAUDE_CODE_COORDINATOR_MODE', false),
ABLATION_BASELINE: false, // always off for external builds
DUMP_SYSTEM_PROMPT: envBool('CLAUDE_CODE_DUMP_SYSTEM_PROMPT', false),
BG_SESSIONS: envBool('CLAUDE_CODE_BG_SESSIONS', false),
}
function envBool(key: string, fallback: boolean): boolean {
const v = process.env[key]
if (v === undefined) return fallback
return v === '1' || v === 'true'
}
export function feature(name: string): boolean {
return FEATURE_FLAGS[name] ?? false
}

26
src/shims/macro.ts Normal file
View File

@@ -0,0 +1,26 @@
// src/shims/macro.ts
// Read version from package.json at startup
import { readFileSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const pkgPath = resolve(dirname(__filename), '..', '..', 'package.json')
let version = '0.0.0-dev'
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
version = pkg.version || version
} catch {}
const MACRO_OBJ = {
VERSION: version,
PACKAGE_URL: '@anthropic-ai/claude-code',
ISSUES_EXPLAINER:
'report issues at https://github.com/anthropics/claude-code/issues',
}
// Install as global
;(globalThis as any).MACRO = MACRO_OBJ
export default MACRO_OBJ

6
src/shims/preload.ts Normal file
View File

@@ -0,0 +1,6 @@
// src/shims/preload.ts
// Must be loaded before any application code.
// Provides runtime equivalents of Bun bundler build-time features.
import './macro.js'
// bun:bundle is resolved via the build alias, not imported here

View File

@@ -324,6 +324,43 @@ export async function getWithPermittedRedirects(
throw new EgressBlockedError(hostname)
}
// Handle HTTP 402 Payment Required via x402 protocol
if (
axios.isAxiosError(error) &&
error.response?.status === 402
) {
try {
const { isX402Enabled, handlePaymentRequired, getX402SessionSpentUSD } =
require('../../services/x402/index.js') as typeof import('../../services/x402/index.js')
const paymentHeader = error.response.headers['x-payment-required']
if (isX402Enabled() && paymentHeader) {
const result = handlePaymentRequired(
paymentHeader,
getX402SessionSpentUSD(),
)
if (result) {
// Retry request with payment header
return await axios.get(url, {
signal,
timeout: FETCH_TIMEOUT_MS,
maxRedirects: 0,
responseType: 'arraybuffer',
maxContentLength: MAX_HTTP_CONTENT_LENGTH,
headers: {
Accept: 'text/markdown, text/html, */*',
'User-Agent': getWebFetchUserAgent(),
'x-payment': result.paymentHeader,
},
})
}
}
} catch {
// x402 handling failed, fall through to original error
}
}
throw error
}
}

5
src/types/macro.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare const MACRO: {
VERSION: string
PACKAGE_URL: string
ISSUES_EXPLAINER: string
}

20
vercel.json Normal file
View File

@@ -0,0 +1,20 @@
{
"version": 2,
"builds": [
{
"src": "mcp-server/api/index.ts",
"use": "@vercel/node",
"config": {
"includeFiles": ["src/**", "mcp-server/src/**", "mcp-server/dist/**", "README.md"]
}
}
],
"routes": [
{ "src": "/health", "dest": "mcp-server/api/index.ts" },
{ "src": "/mcp", "dest": "mcp-server/api/index.ts" },
{ "src": "/sse", "dest": "mcp-server/api/index.ts" },
{ "src": "/messages", "dest": "mcp-server/api/index.ts" },
{ "src": "/api", "dest": "mcp-server/api/index.ts" },
{ "src": "/(.*)", "dest": "mcp-server/api/index.ts" }
]
}