chaite
Version:
core for chatgpt-plugin and karin-plugin-chatgpt
104 lines (103 loc) • 2.96 kB
JavaScript
//#region src/agent/tool-executors/mcp.ts
/**
* MCP client-side ToolExecutor.
* Communicates with an MCP server over JSON-RPC 2.0 (HTTP transport).
* Optional fallback executor is called when the remote call fails.
*
* Install @modelcontextprotocol/sdk for full lifecycle support;
* this implementation uses raw fetch for minimal overhead.
*/
var McpToolExecutor = class {
toolCache = null;
constructor(endpoint, fallback) {
this.endpoint = endpoint;
this.fallback = fallback;
}
async execute(call, ctx) {
const timeoutMs = ctx.timeoutMs ?? this.endpoint.defaultTimeoutMs ?? 15e3;
try {
const outputText = ((await this.rpcCall("tools/call", {
name: call.name,
arguments: call.arguments
}, call.id, timeoutMs))?.content ?? []).filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n").trim();
return {
id: call.id,
name: call.name,
ok: true,
outputText
};
} catch (err) {
const isFatal = err instanceof McpError && err.code !== "TIMEOUT" && err.code !== "NETWORK";
if (!isFatal && this.fallback) return this.fallback.execute(call, ctx);
return {
id: call.id,
name: call.name,
ok: false,
outputText: "",
error: {
kind: isFatal ? "FATAL" : "RETRYABLE",
message: err instanceof Error ? err.message : String(err),
cause: err
}
};
}
}
async listAvailableTools(_ctx) {
if (this.toolCache) return this.toolCache;
try {
const tools = ((await this.rpcCall("tools/list", {}, "list", 1e4))?.tools ?? []).map((t) => ({
name: t.name,
description: t.description,
schema: t.inputSchema
}));
this.toolCache = tools;
return tools;
} catch {
return [];
}
}
/** Invalidate tool list cache (call after server reconnect) */
invalidateCache() {
this.toolCache = null;
}
async rpcCall(method, params, id, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const headers = { "content-type": "application/json" };
if (this.endpoint.authHeader) headers["authorization"] = this.endpoint.authHeader;
const res = await fetch(this.endpoint.baseUrl, {
method: "POST",
headers,
body: JSON.stringify({
jsonrpc: "2.0",
id,
method,
params
}),
signal: controller.signal
});
if (!res.ok) {
const kind = res.status >= 500 ? "NETWORK" : "FATAL";
throw new McpError(`MCP HTTP ${res.status}`, kind);
}
const json = await res.json();
if (json.error) throw new McpError(json.error.message, "FATAL");
return json.result;
} catch (err) {
if (err.name === "AbortError") throw new McpError(`MCP request timeout after ${timeoutMs}ms`, "TIMEOUT");
throw err;
} finally {
clearTimeout(timer);
}
}
};
var McpError = class extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "McpError";
}
};
//#endregion
export { McpToolExecutor as t };