Add MCP bridge to claude-agent so Hermes can delegate to the real Claude Code CLI

New POST /mcp endpoint (Streamable HTTP transport, stateless — fresh
McpServer+transport per request) exposing one tool, ask_claude_code: runs
the real `claude` binary against a prompt, billed against the Pro/Max
subscription rather than API credits. This works specifically because it's
the actual claude CLI making the request server-side — the same reason
Hermes itself can't authenticate with the subscription directly (proven
earlier: Anthropic rejects the OAuth token from any client that isn't the
real CLI's exact request fingerprint). Read-only: no Edit/Write/git-push/
git-commit tools, since this is a quick-answer bridge, not a repo editor.

Tested end-to-end locally (built + ran the image, curled the full MCP
handshake: initialize -> tools/list -> tools/call) before pushing — got a
real 'pong' back from the actual claude CLI through the MCP protocol.

To register it with Hermes (lives in its own data volume, not git — see
.env.example comment):
  docker exec hermes hermes config set mcp_servers.claude-code.url http://claude-agent:3001/mcp
  docker exec hermes hermes config set 'mcp_servers.claude-code.headers.Authorization' 'Bearer <MCP_BRIDGE_KEY>'
This commit is contained in:
2026-08-23 17:24:46 +00:00
parent ec37245b37
commit 732246d4fd
5 changed files with 116 additions and 4 deletions
+29
View File
@@ -1,7 +1,9 @@
import express from "express";
import crypto from "node:crypto";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { postPRComment } from "./gitea.js";
import { reviewPullRequest } from "./runner.js";
import { createMcpServer, mcpAuthMiddleware } from "./mcpBridge.js";
const app = express();
app.use(
@@ -56,6 +58,33 @@ app.post("/webhooks/gitea", async (req, res) => {
}
});
// MCP bridge — lets Hermes (or anything else speaking MCP) delegate a question to the
// real Claude Code CLI, billed against the subscription. Stateless: a fresh server +
// transport per request, no session tracking needed for a single-tool bridge like this.
app.post("/mcp", mcpAuthMiddleware, async (req, res) => {
const mcpServer = createMcpServer();
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on("close", () => {
transport.close();
mcpServer.close();
});
try {
await mcpServer.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (err) {
console.error("MCP request handling failed:", err);
if (!res.headersSent) res.status(500).send("internal error");
}
});
app.get("/mcp", mcpAuthMiddleware, (_req, res) => {
res.status(405).set("Allow", "POST").send("Method Not Allowed");
});
app.delete("/mcp", mcpAuthMiddleware, (_req, res) => {
res.status(405).set("Allow", "POST").send("Method Not Allowed");
});
app.listen(PORT, () => {
console.log(`claude-agent listening on :${PORT}`);
});