Keep agent MCP transport across daemon restarts
This commit is contained in:
parent
26093ace0e
commit
bb74fc849f
8 changed files with 119 additions and 5 deletions
56
scripts/serve-voidbot-mcp-bridge.mjs
Normal file
56
scripts/serve-voidbot-mcp-bridge.mjs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env node
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
const endpoint = new URL(process.env.VOIDBOT_MCP_HTTP_URL ?? "http://127.0.0.1:17875/mcp");
|
||||
let cachedTools;
|
||||
|
||||
const server = new Server(
|
||||
{ name: "voidbot-transport-bridge", version: "1.0.0" },
|
||||
{ capabilities: { tools: {} } },
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
try {
|
||||
cachedTools = await withDaemon((client) => client.listTools());
|
||||
return cachedTools;
|
||||
} catch (error) {
|
||||
if (cachedTools) {
|
||||
return cachedTools;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
return await withDaemon((client) => client.callTool(request.params));
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `VoidBot daemon is temporarily unavailable: ${describeError(error)}`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function withDaemon(operation) {
|
||||
const client = new Client({ name: "voidbot-transport-bridge-upstream", version: "1.0.0" });
|
||||
try {
|
||||
await client.connect(new StreamableHTTPClientTransport(endpoint));
|
||||
return await operation(client);
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function describeError(error) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
await server.connect(new StdioServerTransport());
|
||||
53
scripts/smoke-voidbot-mcp-bridge-recovery.mjs
Normal file
53
scripts/smoke-voidbot-mcp-bridge-recovery.mjs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
|
||||
import { createVoidbotMcpContext } from "../apps/worker/dist/mcp-server-context.js";
|
||||
import { startVoidbotMcpHttpServer } from "../apps/worker/dist/mcp-http-server.js";
|
||||
|
||||
const port = Number.parseInt(process.env.VOIDBOT_MCP_BRIDGE_SMOKE_PORT ?? "17876", 10);
|
||||
const endpoint = `http://127.0.0.1:${port}/mcp`;
|
||||
const context = createVoidbotMcpContext();
|
||||
let listener = await startVoidbotMcpHttpServer(context, { host: "127.0.0.1", port });
|
||||
const client = new Client({ name: "voidbot-mcp-bridge-recovery-smoke", version: "1.0.0" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: ["scripts/serve-voidbot-mcp-bridge.mjs"],
|
||||
env: { ...process.env, VOIDBOT_MCP_HTTP_URL: endpoint },
|
||||
stderr: "inherit",
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const before = await client.callTool({ name: "list_indexed_repos", arguments: {} });
|
||||
assertSuccessful(before, "before outage");
|
||||
|
||||
await closeListener(listener);
|
||||
const during = await client.callTool({ name: "list_indexed_repos", arguments: {} });
|
||||
if (!during.isError) {
|
||||
throw new Error("Bridge did not lower daemon outage into a tool error.");
|
||||
}
|
||||
|
||||
listener = await startVoidbotMcpHttpServer(context, { host: "127.0.0.1", port });
|
||||
const after = await client.callTool({ name: "list_indexed_repos", arguments: {} });
|
||||
assertSuccessful(after, "after recovery");
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
beforeRepoCount: before.structuredContent?.repoCount,
|
||||
outageWasToolError: during.isError,
|
||||
afterRepoCount: after.structuredContent?.repoCount,
|
||||
})}\n`);
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
await closeListener(listener).catch(() => undefined);
|
||||
}
|
||||
|
||||
function assertSuccessful(result, phase) {
|
||||
if (result.isError || !result.structuredContent?.repoCount) {
|
||||
throw new Error(`Bridge call failed ${phase}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function closeListener(server) {
|
||||
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||
}
|
||||
Loading…
Reference in a new issue