@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/LICENSE +21 -0
- package/README.md +327 -0
- package/dist/index.cjs +358 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +230 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.mjs +355 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +48 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { FunctionDefinition, RiskTier } from '@forgewisp/core';
|
|
2
|
+
export { FunctionDefinition, JSONSchema, JSONSchemaProperty, RiskTier, ToolContext } from '@forgewisp/core';
|
|
3
|
+
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
4
|
+
export { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
5
|
+
export { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
6
|
+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Structural view of a Forgewisp agent: the two methods the MCP integration needs. The real
|
|
10
|
+
* `ForgewispAgent` (from `createAgent`) satisfies this structurally, so consumers pass their
|
|
11
|
+
* agent instance directly: `await registerMcpServer(agent, { ... })`. Declaring a structural
|
|
12
|
+
* subset (rather than importing the not-exported `ForgewispAgent` class) keeps this package
|
|
13
|
+
* decoupled from core's internals — same technique `apps/bundled-demo` uses for its `ToolMeta`.
|
|
14
|
+
*/
|
|
15
|
+
interface AgentLike {
|
|
16
|
+
/** Registers a tool. Throws at registration time for `write`/`destructive` tools when the
|
|
17
|
+
* agent's `onConfirmRequired` is unset (core enforces this — see `packages/core/src/agent.ts`). */
|
|
18
|
+
registerFunction(def: FunctionDefinition): void;
|
|
19
|
+
/** Deregisters a tool by name. No-op if unknown. */
|
|
20
|
+
deregisterFunction(name: string): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for connecting an MCP server and adapting its tools into Forgewisp
|
|
24
|
+
* `FunctionDefinition`s.
|
|
25
|
+
*/
|
|
26
|
+
interface McpServerConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Logical server name. Used as the default `toolNamePrefix` and as the audit/UI attribution
|
|
29
|
+
* suffix on each tool's description. Also identifies the server in error messages.
|
|
30
|
+
*/
|
|
31
|
+
name: string;
|
|
32
|
+
/** Streamable HTTP endpoint URL of the MCP server. */
|
|
33
|
+
url: string;
|
|
34
|
+
/**
|
|
35
|
+
* Optional bearer token sent to the MCP endpoint as `Authorization: Bearer <apiKey>`. The
|
|
36
|
+
* header is omitted entirely when unset, so no-auth/proxy endpoints work — mirrors core's
|
|
37
|
+
* `HttpClient` (which only sends `Authorization` when an `apiKey` is configured). Mutually
|
|
38
|
+
* exclusive with `authProvider`; when both are set, `authProvider` wins and `apiKey` is ignored.
|
|
39
|
+
* Use this when you already hold an access token; use `authProvider` when the server requires a
|
|
40
|
+
* full OAuth flow to *obtain* one.
|
|
41
|
+
*/
|
|
42
|
+
apiKey?: string;
|
|
43
|
+
/**
|
|
44
|
+
* OAuth client provider. When set, takes precedence over `apiKey` and drives the full OAuth 2.1
|
|
45
|
+
* authorization-code + PKCE flow via the SDK: RFC 9728 protected-resource discovery, RFC 8414
|
|
46
|
+
* authorization-server metadata discovery, dynamic client registration (RFC 7591) when the
|
|
47
|
+
* server supports it, token exchange, and refresh. The consumer owns browser-redirect plumbing
|
|
48
|
+
* and token storage by implementing `OAuthClientProvider`.
|
|
49
|
+
*
|
|
50
|
+
* On the first connect, if the server requires auth and no usable token exists, the SDK calls
|
|
51
|
+
* `authProvider.redirectToAuthorization(url)` and the connect returns `authState: 'pending'`
|
|
52
|
+
* with an empty `tools` array (instead of throwing). The consumer completes the redirect-back and
|
|
53
|
+
* calls `finishAuth(code)` on the handle (or, after a page reload, calls
|
|
54
|
+
* `createMcpTools`/`registerMcpServer` again with `options.authorizationCode`). See `mcp.ts` for
|
|
55
|
+
* why a fresh transport is built on resume (the SDK cannot re-`connect` a spent transport).
|
|
56
|
+
*/
|
|
57
|
+
authProvider?: OAuthClientProvider;
|
|
58
|
+
/** Per-call timeout for MCP `listTools`/`callTool` requests, in milliseconds. Default 60_000. */
|
|
59
|
+
requestTimeoutMs?: number;
|
|
60
|
+
/** Prefix for the registered (prefixed) tool names. Default: `config.name`. */
|
|
61
|
+
toolNamePrefix?: string;
|
|
62
|
+
/** Default risk tier for tools not listed in `tierOverrides`. Default: `'read'`. */
|
|
63
|
+
defaultTier?: RiskTier;
|
|
64
|
+
/** Map of original MCP tool name → risk tier (overrides `defaultTier` for that tool). */
|
|
65
|
+
tierOverrides?: Record<string, RiskTier>;
|
|
66
|
+
/** Allowlist of original MCP tool names to register; all others are skipped. */
|
|
67
|
+
onlyTools?: string[];
|
|
68
|
+
/** Blocklist of original MCP tool names to skip. */
|
|
69
|
+
excludeTools?: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Whether the agent has `onConfirmRequired` configured. Required to be `true` when any
|
|
72
|
+
* resolved tool tier is `write` or `destructive`; `registerMcpServer` throws a clear error
|
|
73
|
+
* up front (before registering anything) otherwise — so a tier mismatch never leaves a
|
|
74
|
+
* partially-registered server. Core's own registration-time invariant still fires as a
|
|
75
|
+
* backstop inside `agent.registerFunction`.
|
|
76
|
+
*/
|
|
77
|
+
hasConfirmation?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** Handle returned by `registerMcpServer` for managing a server's lifecycle. */
|
|
80
|
+
interface McpServerHandle {
|
|
81
|
+
/** The prefixed tool names that were registered on the agent. Empty while `authState` is
|
|
82
|
+
* `'pending'` (tools are not listed until auth completes). */
|
|
83
|
+
toolNames: string[];
|
|
84
|
+
/** Authorization state. `'pending'` means the server requires OAuth and the consumer must complete
|
|
85
|
+
* the redirect-back via `finishAuth` (or resume with `options.authorizationCode`). */
|
|
86
|
+
authState: McpAuthState;
|
|
87
|
+
/**
|
|
88
|
+
* Complete the OAuth redirect-back: exchange `authorizationCode` for tokens (via the provider),
|
|
89
|
+
* build a fresh transport, reconnect, list the server's tools, run the `hasConfirmation`
|
|
90
|
+
* preflight, and register the tools on the agent. Mutates `toolNames`/`authState` in place.
|
|
91
|
+
*
|
|
92
|
+
* Rejects if already authorized or closed, or if the post-auth preflight fails (in which case
|
|
93
|
+
* nothing remains registered). Idempotent guard: a second `finishAuth` is a no-op.
|
|
94
|
+
*/
|
|
95
|
+
finishAuth(authorizationCode: string): Promise<void>;
|
|
96
|
+
/** Deregisters all of this server's tools from the agent and disconnects from the MCP server. */
|
|
97
|
+
close(): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
/** Authorization state of a connected MCP server. */
|
|
100
|
+
type McpAuthState = 'authorized' | 'pending';
|
|
101
|
+
/**
|
|
102
|
+
* Options for `createMcpTools` / `registerMcpServer`.
|
|
103
|
+
*/
|
|
104
|
+
interface McpConnectOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Resume path: an authorization code obtained from the redirect-back after a page reload (or a
|
|
107
|
+
* blocked-popup same-tab navigation). When set alongside `config.authProvider`, the code is
|
|
108
|
+
* exchanged for tokens on a fresh transport *before* connecting, so the (re)connect reads the
|
|
109
|
+
* saved token and returns `authState: 'authorized'` directly — skipping the pending state.
|
|
110
|
+
* Ignored when `authProvider` is not set.
|
|
111
|
+
*/
|
|
112
|
+
authorizationCode?: string;
|
|
113
|
+
/**
|
|
114
|
+
* @internal Test seam to inject a non-HTTP transport (e.g. `InMemoryTransport` or an OAuth-gated
|
|
115
|
+
* fake), so tests never dynamic-import `StreamableHTTPClientTransport`. May be a single
|
|
116
|
+
* `Transport` (used once for the initial connect — fine for non-OAuth tests, where `finishAuth`
|
|
117
|
+
* is never called) or a factory `() => Transport | Promise<Transport>` invoked each time a
|
|
118
|
+
* transport is needed (initial connect AND each `finishAuth` reconnect), which is how OAuth
|
|
119
|
+
* tests supply a fresh gated transport that shares one in-memory pipe. Not part of the stable
|
|
120
|
+
* public contract.
|
|
121
|
+
*/
|
|
122
|
+
transport?: Transport | (() => Transport | Promise<Transport>);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* MCP → Forgewisp adapter.
|
|
127
|
+
*
|
|
128
|
+
* Connects to an MCP server over the Streamable HTTP transport, lists its tools, and adapts
|
|
129
|
+
* each into a Forgewisp `FunctionDefinition` that is registered through the agent's existing
|
|
130
|
+
* `registerFunction` path. Because registration goes through core unchanged, the registry,
|
|
131
|
+
* Ajv validation, the two-phase executor, the `onConfirmRequired` confirmation invariant, the
|
|
132
|
+
* audit log, and `runToolLoop` all apply to MCP tools for free — MCP becomes just another source
|
|
133
|
+
* of `FunctionDefinition`s.
|
|
134
|
+
*
|
|
135
|
+
* The adapter is layered so the mapping is unit-testable without a network:
|
|
136
|
+
* - `adaptMcpTool` / `adaptMcpTools` — pure given a `McpClient` (the test seam; a plain fake
|
|
137
|
+
* object implementing `callTool` is enough).
|
|
138
|
+
* - `connectMcpServer` — builds the SDK `Client` + `StreamableHTTPClientTransport`.
|
|
139
|
+
* - `createMcpTools` / `registerMcpServer` — the public wiring.
|
|
140
|
+
*
|
|
141
|
+
* ── Schema handling ──────────────────────────────────────────────────────────
|
|
142
|
+
* MCP `inputSchema` is full draft-07 JSON Schema. Forgewisp's `JSONSchema` type (in core) is a
|
|
143
|
+
* deliberately narrow subset enforcing strict hand-authored schemas in `@forgewisp/bundled-tools`;
|
|
144
|
+
* we do NOT widen it. The adapter casts the raw `inputSchema` to `JSONSchema` at the boundary.
|
|
145
|
+
* Runtime is correct: core's Ajv is `strict: false` and validates draft-07 (`$ref`/`enum`/`oneOf`
|
|
146
|
+
* etc.), the compiled validator is cached per schema-object identity, and the wire payload sent
|
|
147
|
+
* to the LLM carries the full schema, which OpenAI-compatible models accept. Limitation: any
|
|
148
|
+
* external `$ref` (not present in well-formed MCP schemas) won't resolve.
|
|
149
|
+
*
|
|
150
|
+
* ── Risk tiers ────────────────────────────────────────────────────────────────
|
|
151
|
+
* MCP has no risk-tier concept. Tiers come entirely from `McpServerConfig` (`defaultTier` +
|
|
152
|
+
* `tierOverrides`); MCP `annotations.readOnlyHint`/`destructiveHint` are informational only and
|
|
153
|
+
* deliberately NOT auto-mapped — the consumer owns the security boundary.
|
|
154
|
+
*
|
|
155
|
+
* ── Abort ─────────────────────────────────────────────────────────────────────
|
|
156
|
+
* The parent run's `AbortSignal` is threaded into handlers by core's executor via `ToolContext`.
|
|
157
|
+
* The handler forwards it to `client.callTool` (merged with a per-server timeout), so aborting
|
|
158
|
+
* the parent run aborts in-flight MCP calls.
|
|
159
|
+
*
|
|
160
|
+
* ── OAuth ─────────────────────────────────────────────────────────────────────
|
|
161
|
+
* `McpServerConfig.authProvider` (an SDK `OAuthClientProvider`) takes precedence over `apiKey` and
|
|
162
|
+
* drives the full OAuth 2.1 authorization-code + PKCE flow (RFC 9728 / 8414 discovery, dynamic client
|
|
163
|
+
* registration, token exchange, refresh) via the SDK transport — the consumer only implements the
|
|
164
|
+
* provider (browser-redirect plumbing + token storage). `client/auth.js` is dynamically imported
|
|
165
|
+
* only on the OAuth path; non-OAuth consumers never load it.
|
|
166
|
+
*
|
|
167
|
+
* State machine: the first `connect` that needs auth has the SDK call
|
|
168
|
+
* `authProvider.redirectToAuthorization(url)` and then throw `UnauthorizedError` from `connect`.
|
|
169
|
+
* `Client.connect` catches that, calls `void this.close()`, and rethrows. The transport is now
|
|
170
|
+
* *spent* — `close()` aborts its `AbortController` but does not null it, so a second `start()`
|
|
171
|
+
* throws "already started". **The same transport cannot be re-`connect`ed.** So this adapter
|
|
172
|
+
* catches `UnauthorizedError` (via `instanceof`, dynamically importing the class only on this path —
|
|
173
|
+
* the SDK's error doesn't set a custom `name`, so a name check won't work) and returns
|
|
174
|
+
* `authState: 'pending'` (empty tools) instead of throwing, keeping the spent transport+client only
|
|
175
|
+
* long enough to call `transport.finishAuth(code)` (which just calls `auth()` to exchange the code
|
|
176
|
+
* and save tokens into the provider — it does not require `start()`). `finishAuth` then builds a
|
|
177
|
+
* **fresh** transport + client whose `connect` reads the saved token via `provider.tokens()` and
|
|
178
|
+
* succeeds. The same "finishAuth-then-fresh-connect" pattern powers the resume path
|
|
179
|
+
* (`options.authorizationCode`), which exchanges the code on a single fresh transport *before*
|
|
180
|
+
* connecting.
|
|
181
|
+
*
|
|
182
|
+
* For `registerMcpServer`, the `hasConfirmation` preflight runs only once tools are listed — i.e.
|
|
183
|
+
* after auth completes (deferred when `authState` is `'pending'`), mirroring the non-OAuth path.
|
|
184
|
+
*/
|
|
185
|
+
|
|
186
|
+
/** Shape returned by `createMcpTools`: the adapted tools, a closer, and (for OAuth) the auth flow. */
|
|
187
|
+
interface McpToolsResult {
|
|
188
|
+
/** The adapted `FunctionDefinition`s. Empty while `authState` is `'pending'`; mutated in place by
|
|
189
|
+
* `finishAuth` once auth completes, so the caller's reference stays valid. */
|
|
190
|
+
tools: FunctionDefinition[];
|
|
191
|
+
/** Authorization state. `'pending'` means the server requires OAuth and `finishAuth` (or a
|
|
192
|
+
* resume via `options.authorizationCode`) is needed before tools are available. */
|
|
193
|
+
authState: McpAuthState;
|
|
194
|
+
/** Disconnects the underlying client. Idempotent. */
|
|
195
|
+
close: () => Promise<void>;
|
|
196
|
+
/** Completes the OAuth redirect-back. See `McpServerHandle.finishAuth`. */
|
|
197
|
+
finishAuth: (authorizationCode: string) => Promise<void>;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Connect to an MCP server, list its tools, and adapt them into `FunctionDefinition`s without
|
|
201
|
+
* touching any agent. Useful for custom registration or `ToolSet` composition. The returned
|
|
202
|
+
* `close()` disconnects the underlying client.
|
|
203
|
+
*
|
|
204
|
+
* OAuth: when `config.authProvider` is set and the server requires auth, the first connect returns
|
|
205
|
+
* `authState: 'pending'` with an empty `tools` array (the SDK already redirected the user agent).
|
|
206
|
+
* Call `finishAuth(code)` after the redirect-back to exchange the code and populate `tools`, or pass
|
|
207
|
+
* `options.authorizationCode` to resume in one shot after a page reload.
|
|
208
|
+
*
|
|
209
|
+
* @internal `options.transport` is a test seam to inject a non-HTTP transport; not part of the
|
|
210
|
+
* stable public contract.
|
|
211
|
+
*/
|
|
212
|
+
declare function createMcpTools(config: McpServerConfig, options?: McpConnectOptions): Promise<McpToolsResult>;
|
|
213
|
+
/**
|
|
214
|
+
* Connect to an MCP server and register all of its (adapted) tools on the given agent. Returns an
|
|
215
|
+
* `McpServerHandle` whose `close()` deregisters every tool and disconnects.
|
|
216
|
+
*
|
|
217
|
+
* Confirmation preflight: before registering anything, if any adapted tool resolves to
|
|
218
|
+
* `write`/`destructive` and `config.hasConfirmation` is not `true`, the client is closed and a
|
|
219
|
+
* clear error is thrown — so a tier mismatch never leaves a partially-registered server. Core's
|
|
220
|
+
* own registration-time invariant still fires as a backstop inside `agent.registerFunction`.
|
|
221
|
+
*
|
|
222
|
+
* OAuth: when the first connect returns `authState: 'pending'`, no tools are registered yet and the
|
|
223
|
+
* preflight is deferred until `handle.finishAuth(code)` lists the tools. If the post-auth preflight
|
|
224
|
+
* fails, `finishAuth` deregisters anything registered, disconnects, and throws the same error.
|
|
225
|
+
*
|
|
226
|
+
* @internal `options.transport` is a test seam to inject a non-HTTP transport.
|
|
227
|
+
*/
|
|
228
|
+
declare function registerMcpServer(agent: AgentLike, config: McpServerConfig, options?: McpConnectOptions): Promise<McpServerHandle>;
|
|
229
|
+
|
|
230
|
+
export { type AgentLike, type McpAuthState, type McpConnectOptions, type McpServerConfig, type McpServerHandle, type McpToolsResult, createMcpTools, registerMcpServer };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { FunctionDefinition, RiskTier } from '@forgewisp/core';
|
|
2
|
+
export { FunctionDefinition, JSONSchema, JSONSchemaProperty, RiskTier, ToolContext } from '@forgewisp/core';
|
|
3
|
+
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
4
|
+
export { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
5
|
+
export { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
6
|
+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Structural view of a Forgewisp agent: the two methods the MCP integration needs. The real
|
|
10
|
+
* `ForgewispAgent` (from `createAgent`) satisfies this structurally, so consumers pass their
|
|
11
|
+
* agent instance directly: `await registerMcpServer(agent, { ... })`. Declaring a structural
|
|
12
|
+
* subset (rather than importing the not-exported `ForgewispAgent` class) keeps this package
|
|
13
|
+
* decoupled from core's internals — same technique `apps/bundled-demo` uses for its `ToolMeta`.
|
|
14
|
+
*/
|
|
15
|
+
interface AgentLike {
|
|
16
|
+
/** Registers a tool. Throws at registration time for `write`/`destructive` tools when the
|
|
17
|
+
* agent's `onConfirmRequired` is unset (core enforces this — see `packages/core/src/agent.ts`). */
|
|
18
|
+
registerFunction(def: FunctionDefinition): void;
|
|
19
|
+
/** Deregisters a tool by name. No-op if unknown. */
|
|
20
|
+
deregisterFunction(name: string): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for connecting an MCP server and adapting its tools into Forgewisp
|
|
24
|
+
* `FunctionDefinition`s.
|
|
25
|
+
*/
|
|
26
|
+
interface McpServerConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Logical server name. Used as the default `toolNamePrefix` and as the audit/UI attribution
|
|
29
|
+
* suffix on each tool's description. Also identifies the server in error messages.
|
|
30
|
+
*/
|
|
31
|
+
name: string;
|
|
32
|
+
/** Streamable HTTP endpoint URL of the MCP server. */
|
|
33
|
+
url: string;
|
|
34
|
+
/**
|
|
35
|
+
* Optional bearer token sent to the MCP endpoint as `Authorization: Bearer <apiKey>`. The
|
|
36
|
+
* header is omitted entirely when unset, so no-auth/proxy endpoints work — mirrors core's
|
|
37
|
+
* `HttpClient` (which only sends `Authorization` when an `apiKey` is configured). Mutually
|
|
38
|
+
* exclusive with `authProvider`; when both are set, `authProvider` wins and `apiKey` is ignored.
|
|
39
|
+
* Use this when you already hold an access token; use `authProvider` when the server requires a
|
|
40
|
+
* full OAuth flow to *obtain* one.
|
|
41
|
+
*/
|
|
42
|
+
apiKey?: string;
|
|
43
|
+
/**
|
|
44
|
+
* OAuth client provider. When set, takes precedence over `apiKey` and drives the full OAuth 2.1
|
|
45
|
+
* authorization-code + PKCE flow via the SDK: RFC 9728 protected-resource discovery, RFC 8414
|
|
46
|
+
* authorization-server metadata discovery, dynamic client registration (RFC 7591) when the
|
|
47
|
+
* server supports it, token exchange, and refresh. The consumer owns browser-redirect plumbing
|
|
48
|
+
* and token storage by implementing `OAuthClientProvider`.
|
|
49
|
+
*
|
|
50
|
+
* On the first connect, if the server requires auth and no usable token exists, the SDK calls
|
|
51
|
+
* `authProvider.redirectToAuthorization(url)` and the connect returns `authState: 'pending'`
|
|
52
|
+
* with an empty `tools` array (instead of throwing). The consumer completes the redirect-back and
|
|
53
|
+
* calls `finishAuth(code)` on the handle (or, after a page reload, calls
|
|
54
|
+
* `createMcpTools`/`registerMcpServer` again with `options.authorizationCode`). See `mcp.ts` for
|
|
55
|
+
* why a fresh transport is built on resume (the SDK cannot re-`connect` a spent transport).
|
|
56
|
+
*/
|
|
57
|
+
authProvider?: OAuthClientProvider;
|
|
58
|
+
/** Per-call timeout for MCP `listTools`/`callTool` requests, in milliseconds. Default 60_000. */
|
|
59
|
+
requestTimeoutMs?: number;
|
|
60
|
+
/** Prefix for the registered (prefixed) tool names. Default: `config.name`. */
|
|
61
|
+
toolNamePrefix?: string;
|
|
62
|
+
/** Default risk tier for tools not listed in `tierOverrides`. Default: `'read'`. */
|
|
63
|
+
defaultTier?: RiskTier;
|
|
64
|
+
/** Map of original MCP tool name → risk tier (overrides `defaultTier` for that tool). */
|
|
65
|
+
tierOverrides?: Record<string, RiskTier>;
|
|
66
|
+
/** Allowlist of original MCP tool names to register; all others are skipped. */
|
|
67
|
+
onlyTools?: string[];
|
|
68
|
+
/** Blocklist of original MCP tool names to skip. */
|
|
69
|
+
excludeTools?: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Whether the agent has `onConfirmRequired` configured. Required to be `true` when any
|
|
72
|
+
* resolved tool tier is `write` or `destructive`; `registerMcpServer` throws a clear error
|
|
73
|
+
* up front (before registering anything) otherwise — so a tier mismatch never leaves a
|
|
74
|
+
* partially-registered server. Core's own registration-time invariant still fires as a
|
|
75
|
+
* backstop inside `agent.registerFunction`.
|
|
76
|
+
*/
|
|
77
|
+
hasConfirmation?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** Handle returned by `registerMcpServer` for managing a server's lifecycle. */
|
|
80
|
+
interface McpServerHandle {
|
|
81
|
+
/** The prefixed tool names that were registered on the agent. Empty while `authState` is
|
|
82
|
+
* `'pending'` (tools are not listed until auth completes). */
|
|
83
|
+
toolNames: string[];
|
|
84
|
+
/** Authorization state. `'pending'` means the server requires OAuth and the consumer must complete
|
|
85
|
+
* the redirect-back via `finishAuth` (or resume with `options.authorizationCode`). */
|
|
86
|
+
authState: McpAuthState;
|
|
87
|
+
/**
|
|
88
|
+
* Complete the OAuth redirect-back: exchange `authorizationCode` for tokens (via the provider),
|
|
89
|
+
* build a fresh transport, reconnect, list the server's tools, run the `hasConfirmation`
|
|
90
|
+
* preflight, and register the tools on the agent. Mutates `toolNames`/`authState` in place.
|
|
91
|
+
*
|
|
92
|
+
* Rejects if already authorized or closed, or if the post-auth preflight fails (in which case
|
|
93
|
+
* nothing remains registered). Idempotent guard: a second `finishAuth` is a no-op.
|
|
94
|
+
*/
|
|
95
|
+
finishAuth(authorizationCode: string): Promise<void>;
|
|
96
|
+
/** Deregisters all of this server's tools from the agent and disconnects from the MCP server. */
|
|
97
|
+
close(): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
/** Authorization state of a connected MCP server. */
|
|
100
|
+
type McpAuthState = 'authorized' | 'pending';
|
|
101
|
+
/**
|
|
102
|
+
* Options for `createMcpTools` / `registerMcpServer`.
|
|
103
|
+
*/
|
|
104
|
+
interface McpConnectOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Resume path: an authorization code obtained from the redirect-back after a page reload (or a
|
|
107
|
+
* blocked-popup same-tab navigation). When set alongside `config.authProvider`, the code is
|
|
108
|
+
* exchanged for tokens on a fresh transport *before* connecting, so the (re)connect reads the
|
|
109
|
+
* saved token and returns `authState: 'authorized'` directly — skipping the pending state.
|
|
110
|
+
* Ignored when `authProvider` is not set.
|
|
111
|
+
*/
|
|
112
|
+
authorizationCode?: string;
|
|
113
|
+
/**
|
|
114
|
+
* @internal Test seam to inject a non-HTTP transport (e.g. `InMemoryTransport` or an OAuth-gated
|
|
115
|
+
* fake), so tests never dynamic-import `StreamableHTTPClientTransport`. May be a single
|
|
116
|
+
* `Transport` (used once for the initial connect — fine for non-OAuth tests, where `finishAuth`
|
|
117
|
+
* is never called) or a factory `() => Transport | Promise<Transport>` invoked each time a
|
|
118
|
+
* transport is needed (initial connect AND each `finishAuth` reconnect), which is how OAuth
|
|
119
|
+
* tests supply a fresh gated transport that shares one in-memory pipe. Not part of the stable
|
|
120
|
+
* public contract.
|
|
121
|
+
*/
|
|
122
|
+
transport?: Transport | (() => Transport | Promise<Transport>);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* MCP → Forgewisp adapter.
|
|
127
|
+
*
|
|
128
|
+
* Connects to an MCP server over the Streamable HTTP transport, lists its tools, and adapts
|
|
129
|
+
* each into a Forgewisp `FunctionDefinition` that is registered through the agent's existing
|
|
130
|
+
* `registerFunction` path. Because registration goes through core unchanged, the registry,
|
|
131
|
+
* Ajv validation, the two-phase executor, the `onConfirmRequired` confirmation invariant, the
|
|
132
|
+
* audit log, and `runToolLoop` all apply to MCP tools for free — MCP becomes just another source
|
|
133
|
+
* of `FunctionDefinition`s.
|
|
134
|
+
*
|
|
135
|
+
* The adapter is layered so the mapping is unit-testable without a network:
|
|
136
|
+
* - `adaptMcpTool` / `adaptMcpTools` — pure given a `McpClient` (the test seam; a plain fake
|
|
137
|
+
* object implementing `callTool` is enough).
|
|
138
|
+
* - `connectMcpServer` — builds the SDK `Client` + `StreamableHTTPClientTransport`.
|
|
139
|
+
* - `createMcpTools` / `registerMcpServer` — the public wiring.
|
|
140
|
+
*
|
|
141
|
+
* ── Schema handling ──────────────────────────────────────────────────────────
|
|
142
|
+
* MCP `inputSchema` is full draft-07 JSON Schema. Forgewisp's `JSONSchema` type (in core) is a
|
|
143
|
+
* deliberately narrow subset enforcing strict hand-authored schemas in `@forgewisp/bundled-tools`;
|
|
144
|
+
* we do NOT widen it. The adapter casts the raw `inputSchema` to `JSONSchema` at the boundary.
|
|
145
|
+
* Runtime is correct: core's Ajv is `strict: false` and validates draft-07 (`$ref`/`enum`/`oneOf`
|
|
146
|
+
* etc.), the compiled validator is cached per schema-object identity, and the wire payload sent
|
|
147
|
+
* to the LLM carries the full schema, which OpenAI-compatible models accept. Limitation: any
|
|
148
|
+
* external `$ref` (not present in well-formed MCP schemas) won't resolve.
|
|
149
|
+
*
|
|
150
|
+
* ── Risk tiers ────────────────────────────────────────────────────────────────
|
|
151
|
+
* MCP has no risk-tier concept. Tiers come entirely from `McpServerConfig` (`defaultTier` +
|
|
152
|
+
* `tierOverrides`); MCP `annotations.readOnlyHint`/`destructiveHint` are informational only and
|
|
153
|
+
* deliberately NOT auto-mapped — the consumer owns the security boundary.
|
|
154
|
+
*
|
|
155
|
+
* ── Abort ─────────────────────────────────────────────────────────────────────
|
|
156
|
+
* The parent run's `AbortSignal` is threaded into handlers by core's executor via `ToolContext`.
|
|
157
|
+
* The handler forwards it to `client.callTool` (merged with a per-server timeout), so aborting
|
|
158
|
+
* the parent run aborts in-flight MCP calls.
|
|
159
|
+
*
|
|
160
|
+
* ── OAuth ─────────────────────────────────────────────────────────────────────
|
|
161
|
+
* `McpServerConfig.authProvider` (an SDK `OAuthClientProvider`) takes precedence over `apiKey` and
|
|
162
|
+
* drives the full OAuth 2.1 authorization-code + PKCE flow (RFC 9728 / 8414 discovery, dynamic client
|
|
163
|
+
* registration, token exchange, refresh) via the SDK transport — the consumer only implements the
|
|
164
|
+
* provider (browser-redirect plumbing + token storage). `client/auth.js` is dynamically imported
|
|
165
|
+
* only on the OAuth path; non-OAuth consumers never load it.
|
|
166
|
+
*
|
|
167
|
+
* State machine: the first `connect` that needs auth has the SDK call
|
|
168
|
+
* `authProvider.redirectToAuthorization(url)` and then throw `UnauthorizedError` from `connect`.
|
|
169
|
+
* `Client.connect` catches that, calls `void this.close()`, and rethrows. The transport is now
|
|
170
|
+
* *spent* — `close()` aborts its `AbortController` but does not null it, so a second `start()`
|
|
171
|
+
* throws "already started". **The same transport cannot be re-`connect`ed.** So this adapter
|
|
172
|
+
* catches `UnauthorizedError` (via `instanceof`, dynamically importing the class only on this path —
|
|
173
|
+
* the SDK's error doesn't set a custom `name`, so a name check won't work) and returns
|
|
174
|
+
* `authState: 'pending'` (empty tools) instead of throwing, keeping the spent transport+client only
|
|
175
|
+
* long enough to call `transport.finishAuth(code)` (which just calls `auth()` to exchange the code
|
|
176
|
+
* and save tokens into the provider — it does not require `start()`). `finishAuth` then builds a
|
|
177
|
+
* **fresh** transport + client whose `connect` reads the saved token via `provider.tokens()` and
|
|
178
|
+
* succeeds. The same "finishAuth-then-fresh-connect" pattern powers the resume path
|
|
179
|
+
* (`options.authorizationCode`), which exchanges the code on a single fresh transport *before*
|
|
180
|
+
* connecting.
|
|
181
|
+
*
|
|
182
|
+
* For `registerMcpServer`, the `hasConfirmation` preflight runs only once tools are listed — i.e.
|
|
183
|
+
* after auth completes (deferred when `authState` is `'pending'`), mirroring the non-OAuth path.
|
|
184
|
+
*/
|
|
185
|
+
|
|
186
|
+
/** Shape returned by `createMcpTools`: the adapted tools, a closer, and (for OAuth) the auth flow. */
|
|
187
|
+
interface McpToolsResult {
|
|
188
|
+
/** The adapted `FunctionDefinition`s. Empty while `authState` is `'pending'`; mutated in place by
|
|
189
|
+
* `finishAuth` once auth completes, so the caller's reference stays valid. */
|
|
190
|
+
tools: FunctionDefinition[];
|
|
191
|
+
/** Authorization state. `'pending'` means the server requires OAuth and `finishAuth` (or a
|
|
192
|
+
* resume via `options.authorizationCode`) is needed before tools are available. */
|
|
193
|
+
authState: McpAuthState;
|
|
194
|
+
/** Disconnects the underlying client. Idempotent. */
|
|
195
|
+
close: () => Promise<void>;
|
|
196
|
+
/** Completes the OAuth redirect-back. See `McpServerHandle.finishAuth`. */
|
|
197
|
+
finishAuth: (authorizationCode: string) => Promise<void>;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Connect to an MCP server, list its tools, and adapt them into `FunctionDefinition`s without
|
|
201
|
+
* touching any agent. Useful for custom registration or `ToolSet` composition. The returned
|
|
202
|
+
* `close()` disconnects the underlying client.
|
|
203
|
+
*
|
|
204
|
+
* OAuth: when `config.authProvider` is set and the server requires auth, the first connect returns
|
|
205
|
+
* `authState: 'pending'` with an empty `tools` array (the SDK already redirected the user agent).
|
|
206
|
+
* Call `finishAuth(code)` after the redirect-back to exchange the code and populate `tools`, or pass
|
|
207
|
+
* `options.authorizationCode` to resume in one shot after a page reload.
|
|
208
|
+
*
|
|
209
|
+
* @internal `options.transport` is a test seam to inject a non-HTTP transport; not part of the
|
|
210
|
+
* stable public contract.
|
|
211
|
+
*/
|
|
212
|
+
declare function createMcpTools(config: McpServerConfig, options?: McpConnectOptions): Promise<McpToolsResult>;
|
|
213
|
+
/**
|
|
214
|
+
* Connect to an MCP server and register all of its (adapted) tools on the given agent. Returns an
|
|
215
|
+
* `McpServerHandle` whose `close()` deregisters every tool and disconnects.
|
|
216
|
+
*
|
|
217
|
+
* Confirmation preflight: before registering anything, if any adapted tool resolves to
|
|
218
|
+
* `write`/`destructive` and `config.hasConfirmation` is not `true`, the client is closed and a
|
|
219
|
+
* clear error is thrown — so a tier mismatch never leaves a partially-registered server. Core's
|
|
220
|
+
* own registration-time invariant still fires as a backstop inside `agent.registerFunction`.
|
|
221
|
+
*
|
|
222
|
+
* OAuth: when the first connect returns `authState: 'pending'`, no tools are registered yet and the
|
|
223
|
+
* preflight is deferred until `handle.finishAuth(code)` lists the tools. If the post-auth preflight
|
|
224
|
+
* fails, `finishAuth` deregisters anything registered, disconnects, and throws the same error.
|
|
225
|
+
*
|
|
226
|
+
* @internal `options.transport` is a test seam to inject a non-HTTP transport.
|
|
227
|
+
*/
|
|
228
|
+
declare function registerMcpServer(agent: AgentLike, config: McpServerConfig, options?: McpConnectOptions): Promise<McpServerHandle>;
|
|
229
|
+
|
|
230
|
+
export { type AgentLike, type McpAuthState, type McpConnectOptions, type McpServerConfig, type McpServerHandle, type McpToolsResult, createMcpTools, registerMcpServer };
|