@forgewisp/mcp 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,355 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+
3
+ // src/mcp.ts
4
+
5
+ // package.json
6
+ var package_default = {
7
+ version: "0.5.0"};
8
+
9
+ // src/mcp.ts
10
+ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
11
+ var MAX_OPENAI_TOOL_NAME = 64;
12
+ var CLIENT_INFO = { name: "forgewisp-mcp", version: package_default.version };
13
+ function withTimeout(external, timeoutMs) {
14
+ const hasTimeout = timeoutMs !== void 0 && timeoutMs > 0;
15
+ if (!hasTimeout) return { signal: external, cleanup: () => {
16
+ } };
17
+ const ctrl = new AbortController();
18
+ const timer = setTimeout(
19
+ () => ctrl.abort(new Error("[Forgewisp MCP] call timed out.")),
20
+ timeoutMs
21
+ );
22
+ const propagate = () => {
23
+ clearTimeout(timer);
24
+ ctrl.abort(external.reason ?? new Error("Aborted"));
25
+ };
26
+ if (external) {
27
+ if (external.aborted) {
28
+ propagate();
29
+ } else {
30
+ external.addEventListener("abort", propagate, { once: true });
31
+ }
32
+ }
33
+ return {
34
+ signal: ctrl.signal,
35
+ cleanup: () => {
36
+ clearTimeout(timer);
37
+ if (external) external.removeEventListener("abort", propagate);
38
+ }
39
+ };
40
+ }
41
+ function sanitizeSegment(raw, maxLen, fallback) {
42
+ return raw.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, maxLen) || fallback;
43
+ }
44
+ function sanitizeToolName(raw) {
45
+ return sanitizeSegment(raw, MAX_OPENAI_TOOL_NAME - 4, "tool");
46
+ }
47
+ function flattenResult(result) {
48
+ if (result.structuredContent !== void 0 && result.structuredContent !== null) {
49
+ return result.structuredContent;
50
+ }
51
+ const items = result.content ?? [];
52
+ const textItems = items.filter((i) => i.type === "text");
53
+ if (items.length === 1 && textItems.length === 1) {
54
+ return textItems[0]?.text ?? "";
55
+ }
56
+ if (textItems.length > 0 && textItems.length === items.length) {
57
+ return textItems.map((t) => t.text ?? "").join("\n");
58
+ }
59
+ return { content: result.content };
60
+ }
61
+ function resolveTier(toolName, policy) {
62
+ return policy.tierOverrides[toolName] ?? policy.defaultTier ?? "read";
63
+ }
64
+ function adaptMcpTool(client, tool, policy, taken) {
65
+ const toolPart = sanitizeToolName(tool.name);
66
+ const prefixBudget = Math.max(0, MAX_OPENAI_TOOL_NAME - "__".length - toolPart.length);
67
+ const prefixPart = sanitizeSegment(policy.prefix, prefixBudget, "srv");
68
+ const base = `${prefixPart}__${toolPart}`;
69
+ let name = base.slice(0, MAX_OPENAI_TOOL_NAME);
70
+ if (taken.has(name)) {
71
+ let n = 2;
72
+ do {
73
+ name = `${base.slice(0, MAX_OPENAI_TOOL_NAME - String(n).length - 1)}_${n}`;
74
+ n += 1;
75
+ } while (taken.has(name));
76
+ }
77
+ taken.add(name);
78
+ const description = `${tool.description ?? tool.name} (MCP server: ${policy.serverName})`;
79
+ const riskTier = resolveTier(tool.name, policy);
80
+ const parameters = tool.inputSchema ?? {
81
+ type: "object",
82
+ properties: {}
83
+ };
84
+ return {
85
+ name,
86
+ description,
87
+ parameters,
88
+ riskTier,
89
+ handler: async (args, context) => {
90
+ const { signal, cleanup } = withTimeout(context?.signal, policy.requestTimeoutMs);
91
+ try {
92
+ const result = await client.callTool(
93
+ { name: tool.name, arguments: args },
94
+ void 0,
95
+ signal ? { signal } : void 0
96
+ );
97
+ if (result.isError) {
98
+ const firstText = result.content?.find((i) => i.type === "text");
99
+ throw new Error(firstText?.text ?? `MCP tool "${tool.name}" returned an error`);
100
+ }
101
+ return flattenResult(result);
102
+ } finally {
103
+ cleanup();
104
+ }
105
+ }
106
+ };
107
+ }
108
+ function adaptMcpTools(client, tools, policy) {
109
+ const allow = policy.onlyTools !== void 0 ? new Set(policy.onlyTools) : void 0;
110
+ const deny = new Set(policy.excludeTools ?? []);
111
+ const taken = /* @__PURE__ */ new Set();
112
+ const out = [];
113
+ for (const tool of tools) {
114
+ if (allow !== void 0 && !allow.has(tool.name)) continue;
115
+ if (deny.has(tool.name)) continue;
116
+ out.push(adaptMcpTool(client, tool, policy, taken));
117
+ }
118
+ return out;
119
+ }
120
+ function wrapClient(client) {
121
+ return {
122
+ // Always pass `undefined` for the result schema (we don't request output-schema validation),
123
+ // which sidesteps the SDK's Zod-typed `resultSchema` parameter.
124
+ callTool: (params, _resultSchema, options) => client.callTool(params, void 0, options)
125
+ };
126
+ }
127
+ function policyFromConfig(config) {
128
+ return {
129
+ serverName: config.name,
130
+ prefix: config.toolNamePrefix ?? config.name,
131
+ defaultTier: config.defaultTier ?? "read",
132
+ tierOverrides: config.tierOverrides ?? {},
133
+ requestTimeoutMs: config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
134
+ onlyTools: config.onlyTools,
135
+ excludeTools: config.excludeTools
136
+ };
137
+ }
138
+ async function buildTransport(config, override) {
139
+ if (override) return override;
140
+ const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
141
+ const opts = {};
142
+ if (config.authProvider) {
143
+ opts.authProvider = config.authProvider;
144
+ } else if (config.apiKey) {
145
+ opts.requestInit = { headers: { Authorization: `Bearer ${config.apiKey}` } };
146
+ }
147
+ return new StreamableHTTPClientTransport(new URL(config.url), opts);
148
+ }
149
+ async function connectClient(client, transport, config, authorizationCode, signal) {
150
+ if (authorizationCode && config.authProvider) {
151
+ await transport.finishAuth?.(authorizationCode);
152
+ }
153
+ try {
154
+ await client.connect(transport, signal ? { signal } : void 0);
155
+ return "authorized";
156
+ } catch (err) {
157
+ if (config.authProvider && !authorizationCode) {
158
+ const { UnauthorizedError } = await import('@modelcontextprotocol/sdk/client/auth.js');
159
+ if (err instanceof UnauthorizedError) return "pending";
160
+ }
161
+ throw err;
162
+ }
163
+ }
164
+ async function connectMcpServer(config, override, authorizationCode) {
165
+ const transport = await buildTransport(config, override);
166
+ const client = new Client(CLIENT_INFO, { capabilities: {} });
167
+ const { signal, cleanup } = withTimeout(
168
+ void 0,
169
+ config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
170
+ );
171
+ let authState;
172
+ try {
173
+ authState = await connectClient(client, transport, config, authorizationCode, signal);
174
+ } catch (err) {
175
+ await client.close().catch(() => {
176
+ });
177
+ throw err;
178
+ } finally {
179
+ cleanup();
180
+ }
181
+ const close = async () => {
182
+ try {
183
+ await transport.terminateSession?.();
184
+ } catch {
185
+ }
186
+ await client.close().catch(() => {
187
+ });
188
+ };
189
+ return { client, transport, authState, close };
190
+ }
191
+ async function listAllTools(client, config) {
192
+ const { signal, cleanup } = withTimeout(
193
+ void 0,
194
+ config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
195
+ );
196
+ try {
197
+ const all = [];
198
+ let cursor;
199
+ for (; ; ) {
200
+ const result = await client.listTools(
201
+ cursor ? { cursor } : void 0,
202
+ signal ? { signal } : void 0
203
+ );
204
+ all.push(...result.tools);
205
+ cursor = result.nextCursor;
206
+ if (!cursor) break;
207
+ }
208
+ return all;
209
+ } finally {
210
+ cleanup();
211
+ }
212
+ }
213
+ async function listAndAdapt(client, config) {
214
+ const tools = await listAllTools(client, config);
215
+ const policy = policyFromConfig(config);
216
+ return adaptMcpTools(wrapClient(client), tools, policy);
217
+ }
218
+ function preflightMessage(config, tools) {
219
+ return `[Forgewisp] Cannot register MCP server "${config.name}": it exposes ${tools.filter((t) => t.riskTier !== "read").length} write/destructive tool(s) but \`hasConfirmation\` is not set. Configure the agent's \`onConfirmRequired\` and pass \`hasConfirmation: true\` to \`registerMcpServer\`, or map those tools to \`read\` via \`tierOverrides\`.`;
220
+ }
221
+ function preflightAndRegister(agent, config, tools) {
222
+ const names = tools.map((t) => t.name);
223
+ const needsConfirmation = tools.some((t) => t.riskTier !== "read");
224
+ if (needsConfirmation && !config.hasConfirmation) {
225
+ throw new Error(preflightMessage(config, tools));
226
+ }
227
+ try {
228
+ for (const def of tools) agent.registerFunction(def);
229
+ } catch (err) {
230
+ for (const name of names) agent.deregisterFunction(name);
231
+ throw err;
232
+ }
233
+ return names;
234
+ }
235
+ async function createMcpTools(config, options) {
236
+ const transportSeam = options?.transport;
237
+ const resolveTransport = () => typeof transportSeam === "function" ? transportSeam() : transportSeam;
238
+ let connected = await connectMcpServer(
239
+ config,
240
+ await resolveTransport(),
241
+ options?.authorizationCode
242
+ );
243
+ const tools = [];
244
+ let authState = connected.authState;
245
+ if (authState === "authorized") {
246
+ try {
247
+ tools.push(...await listAndAdapt(connected.client, config));
248
+ } catch (err) {
249
+ await connected.close().catch(() => {
250
+ });
251
+ throw err;
252
+ }
253
+ }
254
+ let closed = false;
255
+ const close = async () => {
256
+ if (closed) return;
257
+ closed = true;
258
+ await connected.close().catch(() => {
259
+ });
260
+ };
261
+ const finishAuth = async (authorizationCode) => {
262
+ if (closed) throw new Error("[Forgewisp MCP] finishAuth called after close.");
263
+ if (authState === "authorized") return;
264
+ if (!config.authProvider) {
265
+ throw new Error("[Forgewisp MCP] finishAuth requires an `authProvider` in the config.");
266
+ }
267
+ await connected.transport.finishAuth?.(authorizationCode);
268
+ await connected.close().catch(() => {
269
+ });
270
+ const resumed = await connectMcpServer(config, await resolveTransport());
271
+ try {
272
+ if (resumed.authState !== "authorized") {
273
+ throw new Error(
274
+ "[Forgewisp MCP] finishAuth did not result in an authorized connection (the authorization code may be invalid or expired)."
275
+ );
276
+ }
277
+ const newTools = await listAndAdapt(resumed.client, config);
278
+ if (closed) {
279
+ await resumed.close().catch(() => {
280
+ });
281
+ throw new Error(
282
+ "[Forgewisp MCP] finishAuth aborted \u2014 close() was called during authorization."
283
+ );
284
+ }
285
+ tools.length = 0;
286
+ tools.push(...newTools);
287
+ authState = "authorized";
288
+ connected = resumed;
289
+ } catch (err) {
290
+ await resumed.close().catch(() => {
291
+ });
292
+ throw err;
293
+ }
294
+ };
295
+ return {
296
+ tools,
297
+ get authState() {
298
+ return authState;
299
+ },
300
+ close,
301
+ finishAuth
302
+ };
303
+ }
304
+ async function registerMcpServer(agent, config, options) {
305
+ const created = await createMcpTools(config, options);
306
+ const { tools, close: disconnect } = created;
307
+ const toolNames = [];
308
+ let closed = false;
309
+ if (created.authState === "authorized") {
310
+ try {
311
+ toolNames.push(...preflightAndRegister(agent, config, tools));
312
+ } catch (err) {
313
+ closed = true;
314
+ await disconnect().catch(() => {
315
+ });
316
+ throw err;
317
+ }
318
+ }
319
+ const close = async () => {
320
+ if (closed) return;
321
+ closed = true;
322
+ for (const name of toolNames) agent.deregisterFunction(name);
323
+ toolNames.length = 0;
324
+ await disconnect().catch(() => {
325
+ });
326
+ };
327
+ const finishAuth = async (authorizationCode) => {
328
+ if (closed) throw new Error("[Forgewisp MCP] finishAuth called after close.");
329
+ if (created.authState === "authorized") return;
330
+ try {
331
+ await created.finishAuth(authorizationCode);
332
+ const names = preflightAndRegister(agent, config, created.tools);
333
+ toolNames.length = 0;
334
+ toolNames.push(...names);
335
+ } catch (err) {
336
+ closed = true;
337
+ toolNames.length = 0;
338
+ await disconnect().catch(() => {
339
+ });
340
+ throw err;
341
+ }
342
+ };
343
+ return {
344
+ toolNames,
345
+ get authState() {
346
+ return created.authState;
347
+ },
348
+ finishAuth,
349
+ close
350
+ };
351
+ }
352
+
353
+ export { createMcpTools, registerMcpServer };
354
+ //# sourceMappingURL=index.mjs.map
355
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../package.json","../src/mcp.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["{\n \"name\": \"@forgewisp/mcp\",\n \"version\": \"0.5.0\",\n \"description\": \"Model Context Protocol (MCP) client support for Forgewisp agents — expose an MCP server's tools as agent FunctionDefinitions\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.mjs\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.cjs\",\n \"types\": \"./dist/index.d.ts\"\n }\n },\n \"files\": [\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"eslint --config ../../eslint.config.mjs src tests\"\n },\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.29.0\"\n },\n \"peerDependencies\": {\n \"@forgewisp/core\": \"workspace:^\"\n },\n \"devDependencies\": {\n \"@forgewisp/core\": \"workspace:^\",\n \"tsup\": \"^8.0.0\",\n \"vitest\": \"^1.6.0\",\n \"typescript\": \"^5.4.0\"\n },\n \"keywords\": [\n \"ai-agent\",\n \"function-calling\",\n \"tool-use\",\n \"forgewisp\",\n \"mcp\",\n \"model-context-protocol\"\n ]\n}\n","/**\n * MCP → Forgewisp adapter.\n *\n * Connects to an MCP server over the Streamable HTTP transport, lists its tools, and adapts\n * each into a Forgewisp `FunctionDefinition` that is registered through the agent's existing\n * `registerFunction` path. Because registration goes through core unchanged, the registry,\n * Ajv validation, the two-phase executor, the `onConfirmRequired` confirmation invariant, the\n * audit log, and `runToolLoop` all apply to MCP tools for free — MCP becomes just another source\n * of `FunctionDefinition`s.\n *\n * The adapter is layered so the mapping is unit-testable without a network:\n * - `adaptMcpTool` / `adaptMcpTools` — pure given a `McpClient` (the test seam; a plain fake\n * object implementing `callTool` is enough).\n * - `connectMcpServer` — builds the SDK `Client` + `StreamableHTTPClientTransport`.\n * - `createMcpTools` / `registerMcpServer` — the public wiring.\n *\n * ── Schema handling ──────────────────────────────────────────────────────────\n * MCP `inputSchema` is full draft-07 JSON Schema. Forgewisp's `JSONSchema` type (in core) is a\n * deliberately narrow subset enforcing strict hand-authored schemas in `@forgewisp/bundled-tools`;\n * we do NOT widen it. The adapter casts the raw `inputSchema` to `JSONSchema` at the boundary.\n * Runtime is correct: core's Ajv is `strict: false` and validates draft-07 (`$ref`/`enum`/`oneOf`\n * etc.), the compiled validator is cached per schema-object identity, and the wire payload sent\n * to the LLM carries the full schema, which OpenAI-compatible models accept. Limitation: any\n * external `$ref` (not present in well-formed MCP schemas) won't resolve.\n *\n * ── Risk tiers ────────────────────────────────────────────────────────────────\n * MCP has no risk-tier concept. Tiers come entirely from `McpServerConfig` (`defaultTier` +\n * `tierOverrides`); MCP `annotations.readOnlyHint`/`destructiveHint` are informational only and\n * deliberately NOT auto-mapped — the consumer owns the security boundary.\n *\n * ── Abort ─────────────────────────────────────────────────────────────────────\n * The parent run's `AbortSignal` is threaded into handlers by core's executor via `ToolContext`.\n * The handler forwards it to `client.callTool` (merged with a per-server timeout), so aborting\n * the parent run aborts in-flight MCP calls.\n *\n * ── OAuth ─────────────────────────────────────────────────────────────────────\n * `McpServerConfig.authProvider` (an SDK `OAuthClientProvider`) takes precedence over `apiKey` and\n * drives the full OAuth 2.1 authorization-code + PKCE flow (RFC 9728 / 8414 discovery, dynamic client\n * registration, token exchange, refresh) via the SDK transport — the consumer only implements the\n * provider (browser-redirect plumbing + token storage). `client/auth.js` is dynamically imported\n * only on the OAuth path; non-OAuth consumers never load it.\n *\n * State machine: the first `connect` that needs auth has the SDK call\n * `authProvider.redirectToAuthorization(url)` and then throw `UnauthorizedError` from `connect`.\n * `Client.connect` catches that, calls `void this.close()`, and rethrows. The transport is now\n * *spent* — `close()` aborts its `AbortController` but does not null it, so a second `start()`\n * throws \"already started\". **The same transport cannot be re-`connect`ed.** So this adapter\n * catches `UnauthorizedError` (via `instanceof`, dynamically importing the class only on this path —\n * the SDK's error doesn't set a custom `name`, so a name check won't work) and returns\n * `authState: 'pending'` (empty tools) instead of throwing, keeping the spent transport+client only\n * long enough to call `transport.finishAuth(code)` (which just calls `auth()` to exchange the code\n * and save tokens into the provider — it does not require `start()`). `finishAuth` then builds a\n * **fresh** transport + client whose `connect` reads the saved token via `provider.tokens()` and\n * succeeds. The same \"finishAuth-then-fresh-connect\" pattern powers the resume path\n * (`options.authorizationCode`), which exchanges the code on a single fresh transport *before*\n * connecting.\n *\n * For `registerMcpServer`, the `hasConfirmation` preflight runs only once tools are listed — i.e.\n * after auth completes (deferred when `authState` is `'pending'`), mirroring the non-OAuth path.\n */\n\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';\nimport type { FunctionDefinition, JSONSchema, RiskTier, ToolContext } from '@forgewisp/core';\n// Read the version from this package's own package.json so the SDK client identity (sent to\n// every MCP server on every connect) tracks the lockstep version bump automatically, instead of a\n// hardcoded string that drifts on every release. Bundled into dist at build time.\nimport pkg from '../package.json';\n\nimport type {\n AgentLike,\n McpAuthState,\n McpCallToolResult,\n McpClient,\n McpConnectOptions,\n McpContentItem,\n McpServerConfig,\n McpServerHandle,\n McpTool,\n} from './types.js';\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 60_000;\nconst MAX_OPENAI_TOOL_NAME = 64;\nconst CLIENT_INFO = { name: 'forgewisp-mcp', version: pkg.version } as const;\n\n// ─── OAuth helpers ───────────────────────────────────────────────────────────\n\n/**\n * A transport that may carry an OAuth `finishAuth(code)` method (the SDK's\n * `StreamableHTTPClientTransport` does; injected test transports may). `terminateSession` is also\n * optional — present on the Streamable HTTP transport, absent on `InMemoryTransport`. Both are\n * optional because non-OAuth transports don't implement them.\n */\ninterface AuthTransport extends Transport {\n finishAuth?(authorizationCode: string): Promise<void>;\n terminateSession?(): Promise<void>;\n}\n\n// ─── Abort helpers ───────────────────────────────────────────────────────────\n\n/**\n * Combine an external `AbortSignal` (the parent run's, from `ToolContext`) with a per-call\n * timeout into a single signal. Returns `undefined` when neither is in play. Mirrors the\n * `mergeAbortSignals` pattern in `packages/core/src/http.ts` (core's helper isn't exported, so\n * a minimal local copy lives here).\n *\n * Returns a `cleanup` that MUST be called once the operation bearing the signal completes\n * (in a `finally`), so the `setTimeout` timer and any external-`abort` listener are released\n * promptly instead of lingering for the full `timeoutMs` (and, for the listener, until the\n * parent run's signal is GC'd). Without the cleanup, a chatty run would accumulate one pending\n * timer + one `once` listener on the parent signal per MCP call.\n */\ninterface TimeoutHandle {\n signal: AbortSignal | undefined;\n cleanup: () => void;\n}\n\nfunction withTimeout(\n external: AbortSignal | undefined,\n timeoutMs: number | undefined,\n): TimeoutHandle {\n const hasTimeout = timeoutMs !== undefined && timeoutMs > 0;\n if (!hasTimeout) return { signal: external, cleanup: () => {} };\n\n const ctrl = new AbortController();\n const timer = setTimeout(\n () => ctrl.abort(new Error('[Forgewisp MCP] call timed out.')),\n timeoutMs,\n );\n const propagate = (): void => {\n clearTimeout(timer);\n ctrl.abort((external as AbortSignal & { reason?: unknown }).reason ?? new Error('Aborted'));\n };\n\n if (external) {\n if (external.aborted) {\n propagate();\n } else {\n external.addEventListener('abort', propagate, { once: true });\n }\n }\n return {\n signal: ctrl.signal,\n cleanup: () => {\n clearTimeout(timer);\n if (external) external.removeEventListener('abort', propagate);\n },\n };\n}\n\n// ─── Name sanitization ───────────────────────────────────────────────────────\n\n/**\n * Rewrite a name segment into chars safe for OpenAI's function-name constraint\n * (`^[A-Za-z0-9_-]{1,64}$`). Invalid chars become `_`; the result is truncated to `maxLen` (the\n * tool-name segment is truncated to leave room for a collision suffix added by `adaptMcpTools`).\n * An empty/blank segment collapses to `fallback`.\n */\nfunction sanitizeSegment(raw: string, maxLen: number, fallback: string): string {\n return raw.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, maxLen) || fallback;\n}\n\n/** Sanitize the tool-name half of the registered name (truncated to leave room for a suffix). */\nfunction sanitizeToolName(raw: string): string {\n return sanitizeSegment(raw, MAX_OPENAI_TOOL_NAME - 4, 'tool');\n}\n\n// ─── Result flattening ───────────────────────────────────────────────────────\n\n/**\n * Reduce an MCP `callTool` result to a single JSON-serializable value for core's loop, which\n * serializes handler results via `JSON.stringify` (with a non-serializable fallback). Order:\n * 1. `structuredContent` (machine-readable JSON) — preferred when present.\n * 2. Exactly one `text` item → its `.text` string.\n * 3. All items are `text` → joined with `\\n`.\n * 4. Otherwise → `{ content }` (plain JSON-serializable; image/audio data is base64).\n */\nfunction flattenResult(result: McpCallToolResult): unknown {\n if (result.structuredContent !== undefined && result.structuredContent !== null) {\n return result.structuredContent;\n }\n const items: McpContentItem[] = result.content ?? [];\n const textItems = items.filter((i) => i.type === 'text');\n if (items.length === 1 && textItems.length === 1) {\n return textItems[0]?.text ?? '';\n }\n if (textItems.length > 0 && textItems.length === items.length) {\n return textItems.map((t) => t.text ?? '').join('\\n');\n }\n return { content: result.content };\n}\n\n// ─── Adapt policy ────────────────────────────────────────────────────────────\n\ninterface AdaptPolicy {\n serverName: string;\n prefix: string;\n defaultTier: RiskTier;\n tierOverrides: Record<string, RiskTier>;\n requestTimeoutMs?: number;\n}\n\nfunction resolveTier(toolName: string, policy: AdaptPolicy): RiskTier {\n return policy.tierOverrides[toolName] ?? policy.defaultTier ?? 'read';\n}\n\n// ─── Pure adapter ───────────────────────────────────────────────────────────\n\n/**\n * Adapt a single MCP tool into a Forgewisp `FunctionDefinition`. The handler closes over the\n * `client` and forwards `(args, { signal })` to `client.callTool`, calling the tool by its\n * ORIGINAL (un-prefixed) name so the MCP server sees the right handler. Throws on `isError` so\n * core's executor records `function_errored`.\n *\n * `taken` is the set of already-issued prefixed names (within this server) so collisions after\n * sanitization/truncation get a numeric suffix.\n */\nfunction adaptMcpTool(\n client: McpClient,\n tool: McpTool,\n policy: AdaptPolicy,\n taken: Set<string>,\n): FunctionDefinition {\n // Build a name that prioritizes the tool half over the prefix when the two don't both fit in\n // OpenAI's 64-char limit. The tool name is what the LLM disambiguates on, so it wins: reserve\n // `__` + the (capped) tool name first, then give the prefix whatever room remains. A long prefix\n // (e.g. a 60+-char server name) is truncated rather than crowding the tool out of the name\n // entirely — which previously made `base.slice(0, 64)` yield just the prefix, slicing off `__`\n // and the whole tool half so every tool collapsed to a numeric suffix, hiding the tool identity.\n const toolPart = sanitizeToolName(tool.name);\n const prefixBudget = Math.max(0, MAX_OPENAI_TOOL_NAME - '__'.length - toolPart.length);\n const prefixPart = sanitizeSegment(policy.prefix, prefixBudget, 'srv');\n const base = `${prefixPart}__${toolPart}`;\n let name = base.slice(0, MAX_OPENAI_TOOL_NAME);\n // Disambiguate collisions (different MCP tool names that sanitize to the same prefixed name).\n if (taken.has(name)) {\n let n = 2;\n do {\n name = `${base.slice(0, MAX_OPENAI_TOOL_NAME - String(n).length - 1)}_${n}`;\n n += 1;\n } while (taken.has(name));\n }\n taken.add(name);\n\n const description = `${tool.description ?? tool.name} (MCP server: ${policy.serverName})`;\n const riskTier = resolveTier(tool.name, policy);\n\n // Cast: MCP `inputSchema` is full draft-07; core's `JSONSchema` is a narrow subset. See the\n // file header — runtime validation (Ajv `strict: false`) and the wire payload both accept it.\n const parameters = (tool.inputSchema ?? {\n type: 'object',\n properties: {},\n }) as unknown as JSONSchema;\n\n return {\n name,\n description,\n parameters,\n riskTier,\n handler: async (args: Record<string, unknown>, context?: ToolContext) => {\n const { signal, cleanup } = withTimeout(context?.signal, policy.requestTimeoutMs);\n try {\n const result = await client.callTool(\n { name: tool.name, arguments: args },\n undefined,\n signal ? { signal } : undefined,\n );\n if (result.isError) {\n const firstText = result.content?.find((i) => i.type === 'text');\n throw new Error(firstText?.text ?? `MCP tool \"${tool.name}\" returned an error`);\n }\n return flattenResult(result);\n } finally {\n // Release the timeout timer + external-`abort` listener promptly (see `withTimeout`).\n cleanup();\n }\n },\n };\n}\n\n/** Adapt policy extended with the `onlyTools`/`excludeTools` filters from `McpServerConfig`. */\nexport interface AdaptPolicyWithFilters extends AdaptPolicy {\n onlyTools?: string[];\n excludeTools?: string[];\n}\n\n/**\n * Filter and adapt a server's tools into `FunctionDefinition`s. Applies `onlyTools`/`excludeTools`\n * and assigns unique prefixed names. Pure given the `client` — the unit-test seam.\n */\nexport function adaptMcpTools(\n client: McpClient,\n tools: readonly McpTool[],\n policy: AdaptPolicyWithFilters,\n): FunctionDefinition[] {\n // `onlyTools: undefined` → register all (no allowlist). `onlyTools: []` → an explicit empty\n // allowlist → register none. Gating on `allow.size > 0` conflated the two and registered every\n // tool when the caller passed an empty array (the inverse of the allowlist intent).\n const allow = policy.onlyTools !== undefined ? new Set(policy.onlyTools) : undefined;\n const deny = new Set(policy.excludeTools ?? []);\n const taken = new Set<string>();\n const out: FunctionDefinition[] = [];\n for (const tool of tools) {\n if (allow !== undefined && !allow.has(tool.name)) continue;\n if (deny.has(tool.name)) continue;\n out.push(adaptMcpTool(client, tool, policy, taken));\n }\n return out;\n}\n\n// ─── Connection ──────────────────────────────────────────────────────────────\n\n/**\n * Adapt the SDK `Client` to the minimal `McpClient` the pure adapter depends on. The SDK's\n * `callTool` return type is a union that also includes a task-async variant (`{ toolResult }`\n * with no `content`); we never invoke task-based execution, so at runtime we always get the\n * `content` variant. The cast lands on that single variant and keeps the union out of the pure\n * adapter's types (so unit tests can use a plain fake object).\n */\nfunction wrapClient(client: Client): McpClient {\n return {\n // Always pass `undefined` for the result schema (we don't request output-schema validation),\n // which sidesteps the SDK's Zod-typed `resultSchema` parameter.\n callTool: (params, _resultSchema, options) =>\n client.callTool(params, undefined, options) as unknown as Promise<McpCallToolResult>,\n };\n}\n\n/** Resolve the effective policy fields from a config. */\nfunction policyFromConfig(config: McpServerConfig): AdaptPolicyWithFilters {\n return {\n serverName: config.name,\n prefix: config.toolNamePrefix ?? config.name,\n defaultTier: config.defaultTier ?? 'read',\n tierOverrides: config.tierOverrides ?? {},\n requestTimeoutMs: config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n onlyTools: config.onlyTools,\n excludeTools: config.excludeTools,\n };\n}\n\ninterface ConnectedServer {\n client: Client;\n transport: AuthTransport;\n authState: McpAuthState;\n close: () => Promise<void>;\n}\n\n/**\n * Build a transport for `config`. When `override` is given (the `@internal` test seam), use it as-is.\n * Otherwise dynamically import `StreamableHTTPClientTransport` (so the transport module, which\n * references browser-native `EventSource`, is only evaluated when actually connecting over HTTP)\n * and construct it from `config.url`:\n * - `authProvider` set → `{ authProvider }` (the SDK transport manages `Authorization` via the\n * provider's tokens; `apiKey` is ignored — the two are mutually exclusive).\n * - else `apiKey` set → `{ requestInit: { headers: { Authorization: Bearer <apiKey> } } }`\n * (header omitted entirely when unset, mirroring core's `HttpClient`).\n */\nasync function buildTransport(\n config: McpServerConfig,\n override?: Transport,\n): Promise<AuthTransport> {\n // `AuthTransport` only adds optional `finishAuth`/`terminateSession` on top of `Transport`, so a\n // plain `Transport` (or the SDK transport, which implements both) is structurally assignable to it\n // — no cast needed.\n if (override) return override;\n const { StreamableHTTPClientTransport } =\n await import('@modelcontextprotocol/sdk/client/streamableHttp.js');\n // `authProvider` and `apiKey` are mutually exclusive; authProvider wins (the SDK transport manages\n // the Authorization header from the provider's tokens). The header is omitted entirely when neither\n // is set, so no-auth/proxy endpoints work.\n const opts: { authProvider?: OAuthClientProvider; requestInit?: RequestInit } = {};\n if (config.authProvider) {\n opts.authProvider = config.authProvider;\n } else if (config.apiKey) {\n opts.requestInit = { headers: { Authorization: `Bearer ${config.apiKey}` } };\n }\n return new StreamableHTTPClientTransport(new URL(config.url), opts);\n}\n\n/**\n * Connect `client` to `transport`, returning the resulting auth state. When `authorizationCode` is\n * provided (the resume path, only with `authProvider`), it is exchanged on the transport via\n * `finishAuth` *before* connecting — the subsequent `connect` reads the saved token and succeeds.\n *\n * On a plain connect with `authProvider`, an `UnauthorizedError` from `client.connect` means the SDK\n * already called `redirectToAuthorization` and the transport is spent — translate that to\n * `'pending'` rather than throwing. The resume path never returns `'pending'`: it rethrows (a code\n * that doesn't yield a token is a hard failure, not an interactive redirect).\n */\nasync function connectClient(\n client: Client,\n transport: AuthTransport,\n config: McpServerConfig,\n authorizationCode?: string,\n signal?: AbortSignal,\n): Promise<McpAuthState> {\n if (authorizationCode && config.authProvider) {\n // The SDK transport's `finishAuth(code)` only calls `auth()` (token exchange) — its signature\n // takes no AbortSignal, so the per-call timeout can't cover the exchange itself; the subsequent\n // `client.connect` below is what the signal bounds.\n await transport.finishAuth?.(authorizationCode);\n }\n try {\n await client.connect(transport, signal ? { signal } : undefined);\n return 'authorized';\n } catch (err) {\n if (config.authProvider && !authorizationCode) {\n // The SDK's `UnauthorizedError` doesn't set a custom `name` (it stays `'Error'`), so a\n // name-based check can't detect it — use `instanceof`. The class is dynamically imported here,\n // *only* on the OAuth-pending path (when connect throws with an authProvider), so the non-OAuth\n // path never loads `client/auth.js`. The SDK is externalized and not minified by us, preserving\n // class identity across the test fake and this import.\n const { UnauthorizedError } = await import('@modelcontextprotocol/sdk/client/auth.js');\n if (err instanceof UnauthorizedError) return 'pending';\n }\n throw err;\n }\n}\n\n/**\n * Build a transport + `Client`, then connect. `override` is the `@internal` test seam.\n * `authorizationCode` enables the resume path (exchange-then-connect on a single fresh transport).\n *\n * The connect is bounded by `requestTimeoutMs` (a hung server during the initialize handshake no\n * longer hangs `createMcpTools` forever). If connect (or the resume `finishAuth` exchange) throws,\n * the freshly-built client/transport are closed so nothing leaks — `Client.close()` closes the\n * underlying transport.\n */\nasync function connectMcpServer(\n config: McpServerConfig,\n override?: Transport,\n authorizationCode?: string,\n): Promise<ConnectedServer> {\n const transport = await buildTransport(config, override);\n const client = new Client(CLIENT_INFO, { capabilities: {} });\n const { signal, cleanup } = withTimeout(\n undefined,\n config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n );\n let authState: McpAuthState;\n try {\n authState = await connectClient(client, transport, config, authorizationCode, signal);\n } catch (err) {\n await client.close().catch(() => {\n /* ignore — closing must never throw during cleanup */\n });\n throw err;\n } finally {\n cleanup();\n }\n\n const close = async (): Promise<void> => {\n try {\n await transport.terminateSession?.();\n } catch {\n // terminateSession is best-effort; ignore failures (server may already be gone).\n }\n await client.close().catch(() => {\n /* ignore — closing must never throw during cleanup */\n });\n };\n\n return { client, transport, authState, close };\n}\n\n/**\n * Paginate `client.listTools`, following `nextCursor`, into a flat `McpTool[]`. Bounded by\n * `config.requestTimeoutMs` — the docs promise the timeout covers `listTools` as well as `callTool`.\n */\nasync function listAllTools(client: Client, config: McpServerConfig): Promise<McpTool[]> {\n const { signal, cleanup } = withTimeout(\n undefined,\n config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n );\n try {\n const all: McpTool[] = [];\n let cursor: string | undefined;\n for (;;) {\n const result = await client.listTools(\n cursor ? { cursor } : undefined,\n signal ? { signal } : undefined,\n );\n all.push(...(result.tools as McpTool[]));\n cursor = (result as { nextCursor?: string }).nextCursor;\n if (!cursor) break;\n }\n return all;\n } finally {\n cleanup();\n }\n}\n\n/** List a server's tools and adapt them into `FunctionDefinition`s using `config`'s policy. */\nasync function listAndAdapt(\n client: Client,\n config: McpServerConfig,\n): Promise<FunctionDefinition[]> {\n const tools = await listAllTools(client, config);\n const policy = policyFromConfig(config);\n return adaptMcpTools(wrapClient(client), tools, policy);\n}\n\n/** Build the preflight error message for a server whose write/destructive tools lack confirmation. */\nfunction preflightMessage(config: McpServerConfig, tools: FunctionDefinition[]): string {\n return (\n `[Forgewisp] Cannot register MCP server \"${config.name}\": it exposes ` +\n `${tools.filter((t) => t.riskTier !== 'read').length} write/destructive tool(s) ` +\n `but \\`hasConfirmation\\` is not set. Configure the agent's \\`onConfirmRequired\\` and pass ` +\n `\\`hasConfirmation: true\\` to \\`registerMcpServer\\`, or map those tools to \\`read\\` via ` +\n `\\`tierOverrides\\`.`\n );\n}\n\n/**\n * Run the `hasConfirmation` preflight and register `tools` on the agent, rolling back on a partial\n * registration failure (core's own registration-time invariant is the backstop). Returns the\n * registered tool names. Throws (registering nothing) when the preflight fails.\n */\nfunction preflightAndRegister(\n agent: AgentLike,\n config: McpServerConfig,\n tools: FunctionDefinition[],\n): string[] {\n const names = tools.map((t) => t.name);\n const needsConfirmation = tools.some((t) => t.riskTier !== 'read');\n if (needsConfirmation && !config.hasConfirmation) {\n throw new Error(preflightMessage(config, tools));\n }\n try {\n for (const def of tools) agent.registerFunction(def);\n } catch (err) {\n for (const name of names) agent.deregisterFunction(name);\n throw err;\n }\n return names;\n}\n\n// ─── Public wiring ───────────────────────────────────────────────────────────\n\n/** Shape returned by `createMcpTools`: the adapted tools, a closer, and (for OAuth) the auth flow. */\nexport interface McpToolsResult {\n /** The adapted `FunctionDefinition`s. Empty while `authState` is `'pending'`; mutated in place by\n * `finishAuth` once auth completes, so the caller's reference stays valid. */\n tools: FunctionDefinition[];\n /** Authorization state. `'pending'` means the server requires OAuth and `finishAuth` (or a\n * resume via `options.authorizationCode`) is needed before tools are available. */\n authState: McpAuthState;\n /** Disconnects the underlying client. Idempotent. */\n close: () => Promise<void>;\n /** Completes the OAuth redirect-back. See `McpServerHandle.finishAuth`. */\n finishAuth: (authorizationCode: string) => Promise<void>;\n}\n\n/**\n * Connect to an MCP server, list its tools, and adapt them into `FunctionDefinition`s without\n * touching any agent. Useful for custom registration or `ToolSet` composition. The returned\n * `close()` disconnects the underlying client.\n *\n * OAuth: when `config.authProvider` is set and the server requires auth, the first connect returns\n * `authState: 'pending'` with an empty `tools` array (the SDK already redirected the user agent).\n * Call `finishAuth(code)` after the redirect-back to exchange the code and populate `tools`, or pass\n * `options.authorizationCode` to resume in one shot after a page reload.\n *\n * @internal `options.transport` is a test seam to inject a non-HTTP transport; not part of the\n * stable public contract.\n */\nexport async function createMcpTools(\n config: McpServerConfig,\n options?: McpConnectOptions,\n): Promise<McpToolsResult> {\n const transportSeam = options?.transport;\n const resolveTransport = (): Transport | Promise<Transport> | undefined =>\n typeof transportSeam === 'function' ? transportSeam() : transportSeam;\n\n let connected = await connectMcpServer(\n config,\n await resolveTransport(),\n options?.authorizationCode,\n );\n\n const tools: FunctionDefinition[] = [];\n let authState: McpAuthState = connected.authState;\n if (authState === 'authorized') {\n try {\n tools.push(...(await listAndAdapt(connected.client, config)));\n } catch (err) {\n // Connect succeeded but listing/adapting threw — close the open client/transport so the\n // session doesn't leak (the caller never receives a handle to close it).\n await connected.close().catch(() => {\n /* ignore — closing must never throw during cleanup */\n });\n throw err;\n }\n }\n let closed = false;\n\n const close = async (): Promise<void> => {\n if (closed) return;\n closed = true;\n await connected.close().catch(() => {\n /* closing must never throw during cleanup */\n });\n };\n\n const finishAuth = async (authorizationCode: string): Promise<void> => {\n if (closed) throw new Error('[Forgewisp MCP] finishAuth called after close.');\n if (authState === 'authorized') return; // idempotent — already authorized (e.g. double call)\n if (!config.authProvider) {\n throw new Error('[Forgewisp MCP] finishAuth requires an `authProvider` in the config.');\n }\n // Exchange the code on the spent transport (saves tokens into the provider), disconnect it, then\n // build a fresh transport whose connect reads the saved token. See the file header: a spent\n // transport cannot be re-`connect`ed, so a fresh one is required on resume. The transport seam is\n // re-resolved so a factory seam yields a fresh transport (a single-instance seam is only used on\n // the initial connect and never reaches here in practice, since `finishAuth` requires\n // `authProvider` and the non-OAuth tests never call it).\n await connected.transport.finishAuth?.(authorizationCode);\n await connected.close().catch(() => {\n /* the spent client/transport may already be closed by the failed connect */\n });\n const resumed = await connectMcpServer(config, await resolveTransport());\n try {\n if (resumed.authState !== 'authorized') {\n throw new Error(\n '[Forgewisp MCP] finishAuth did not result in an authorized connection ' +\n '(the authorization code may be invalid or expired).',\n );\n }\n const newTools = await listAndAdapt(resumed.client, config);\n // `close()` may have run during the multi-second exchange + reconnect above (the user hit\n // Disconnect mid-authorization). If so, drop the fresh connection and bail without adopting it\n // — otherwise `connected = resumed` would revive a server the user closed, and `close()` would\n // be a no-op (closed=true) so the fresh session could never be torn down. Throwing lets the\n // caller's catch path finish the cleanup it already started.\n if (closed) {\n await resumed.close().catch(() => {\n /* ignore */\n });\n throw new Error(\n '[Forgewisp MCP] finishAuth aborted — close() was called during authorization.',\n );\n }\n tools.length = 0;\n tools.push(...newTools);\n authState = 'authorized';\n connected = resumed;\n } catch (err) {\n // listAndAdapt threw, the post-auth state wasn't authorized, or close() raced — either way the\n // fresh `resumed` client/transport must not leak (the caller's close() still points at the old,\n // already-closed `connected`).\n await resumed.close().catch(() => {\n /* ignore */\n });\n throw err;\n }\n };\n\n return {\n tools,\n get authState(): McpAuthState {\n return authState;\n },\n close,\n finishAuth,\n };\n}\n\n/**\n * Connect to an MCP server and register all of its (adapted) tools on the given agent. Returns an\n * `McpServerHandle` whose `close()` deregisters every tool and disconnects.\n *\n * Confirmation preflight: before registering anything, if any adapted tool resolves to\n * `write`/`destructive` and `config.hasConfirmation` is not `true`, the client is closed and a\n * clear error is thrown — so a tier mismatch never leaves a partially-registered server. Core's\n * own registration-time invariant still fires as a backstop inside `agent.registerFunction`.\n *\n * OAuth: when the first connect returns `authState: 'pending'`, no tools are registered yet and the\n * preflight is deferred until `handle.finishAuth(code)` lists the tools. If the post-auth preflight\n * fails, `finishAuth` deregisters anything registered, disconnects, and throws the same error.\n *\n * @internal `options.transport` is a test seam to inject a non-HTTP transport.\n */\nexport async function registerMcpServer(\n agent: AgentLike,\n config: McpServerConfig,\n options?: McpConnectOptions,\n): Promise<McpServerHandle> {\n const created = await createMcpTools(config, options);\n const { tools, close: disconnect } = created;\n\n const toolNames: string[] = [];\n let closed = false;\n\n // Authorized path: preflight + register now.\n if (created.authState === 'authorized') {\n try {\n toolNames.push(...preflightAndRegister(agent, config, tools));\n } catch (err) {\n closed = true;\n await disconnect().catch(() => {\n /* ignore */\n });\n throw err;\n }\n }\n // Pending path: nothing registered; preflight + register happen in finishAuth.\n\n const close = async (): Promise<void> => {\n if (closed) return;\n closed = true;\n for (const name of toolNames) agent.deregisterFunction(name);\n toolNames.length = 0;\n await disconnect().catch(() => {\n /* ignore */\n });\n };\n\n const finishAuth = async (authorizationCode: string): Promise<void> => {\n if (closed) throw new Error('[Forgewisp MCP] finishAuth called after close.');\n if (created.authState === 'authorized') return; // idempotent\n try {\n await created.finishAuth(authorizationCode); // exchange + reconnect + populate created.tools\n const names = preflightAndRegister(agent, config, created.tools);\n toolNames.length = 0;\n toolNames.push(...names);\n } catch (err) {\n closed = true;\n toolNames.length = 0;\n await disconnect().catch(() => {\n /* ignore */\n });\n throw err;\n }\n };\n\n return {\n toolNames,\n get authState(): McpAuthState {\n return created.authState;\n },\n finishAuth,\n close,\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@forgewisp/mcp",
3
+ "version": "0.5.0",
4
+ "description": "Model Context Protocol (MCP) client support for Forgewisp agents — expose an MCP server's tools as agent FunctionDefinitions",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.29.0"
22
+ },
23
+ "peerDependencies": {
24
+ "@forgewisp/core": "^0.5.0"
25
+ },
26
+ "devDependencies": {
27
+ "tsup": "^8.0.0",
28
+ "vitest": "^1.6.0",
29
+ "typescript": "^5.4.0",
30
+ "@forgewisp/core": "^0.5.0"
31
+ },
32
+ "keywords": [
33
+ "ai-agent",
34
+ "function-calling",
35
+ "tool-use",
36
+ "forgewisp",
37
+ "mcp",
38
+ "model-context-protocol"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "dev": "tsup --watch",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "typecheck": "tsc --noEmit",
46
+ "lint": "eslint --config ../../eslint.config.mjs src tests"
47
+ }
48
+ }