✨ 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:
7
mcp-server/.gitignore
vendored
Normal file
7
mcp-server/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.js
|
||||||
|
*.d.ts
|
||||||
|
*.js.map
|
||||||
|
*.d.ts.map
|
||||||
|
!api/
|
||||||
29
mcp-server/Dockerfile
Normal file
29
mcp-server/Dockerfile
Normal 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
21
mcp-server/api/index.ts
Normal 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";
|
||||||
|
|
||||||
96
mcp-server/api/vercelApp.ts
Normal file
96
mcp-server/api/vercelApp.ts
Normal 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
13
mcp-server/railway.json
Normal 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
172
mcp-server/src/http.ts
Normal 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);
|
||||||
|
});
|
||||||
@@ -22,557 +22,3 @@ main().catch((err) => {
|
|||||||
console.error("Fatal error:", err);
|
console.error("Fatal error:", err);
|
||||||
process.exit(1);
|
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function formatX402Cost(): string {
|
|||||||
if (!byDomain[domain]) {
|
if (!byDomain[domain]) {
|
||||||
byDomain[domain] = { count: 0, totalUSD: 0 }
|
byDomain[domain] = { count: 0, totalUSD: 0 }
|
||||||
}
|
}
|
||||||
byDomain[domain].count += payment.count ?? 1
|
byDomain[domain].count += 1
|
||||||
byDomain[domain].totalUSD += payment.amountUSD
|
byDomain[domain].totalUSD += payment.amountUSD
|
||||||
} catch {
|
} catch {
|
||||||
// Skip malformed URLs
|
// Skip malformed URLs
|
||||||
|
|||||||
28
src/shims/bun-bundle.ts
Normal file
28
src/shims/bun-bundle.ts
Normal 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
26
src/shims/macro.ts
Normal 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
6
src/shims/preload.ts
Normal 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
|
||||||
@@ -324,6 +324,43 @@ export async function getWithPermittedRedirects(
|
|||||||
throw new EgressBlockedError(hostname)
|
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
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/types/macro.d.ts
vendored
Normal file
5
src/types/macro.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare const MACRO: {
|
||||||
|
VERSION: string
|
||||||
|
PACKAGE_URL: string
|
||||||
|
ISSUES_EXPLAINER: string
|
||||||
|
}
|
||||||
20
vercel.json
Normal file
20
vercel.json
Normal 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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user