@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Angelo Cavallo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# `@forgewisp/mcp`
|
|
2
|
+
|
|
3
|
+
[](https://github.com/forgewisp/forgewisp-core)
|
|
4
|
+
[](https://github.com/forgewisp/forgewisp-core)
|
|
5
|
+
|
|
6
|
+
> ⭐ **Found Forgewisp useful? [Star the repo](https://github.com/forgewisp/forgewisp-core) on GitHub.**
|
|
7
|
+
|
|
8
|
+
> Model Context Protocol (MCP) client support for
|
|
9
|
+
> [Forgewisp](https://github.com/forgewisp/forgewisp-core) agents — connect an
|
|
10
|
+
> MCP server over Streamable HTTP and expose its tools as agent
|
|
11
|
+
> `FunctionDefinition`s, with JSON Schema validation, the risk-tier
|
|
12
|
+
> confirmation boundary, the audit log, and `runToolLoop` all applying
|
|
13
|
+
> unchanged.
|
|
14
|
+
|
|
15
|
+
MCP becomes just another source of `FunctionDefinition`s. This package
|
|
16
|
+
connects to an MCP server, lists its tools, and adapts each one into a
|
|
17
|
+
`FunctionDefinition` that is registered through the agent's existing
|
|
18
|
+
`registerFunction` path — so everything core already does for hand-authored
|
|
19
|
+
tools (registry, Ajv validation, two-phase executor, `onConfirmRequired`
|
|
20
|
+
invariant, audit log, abort propagation, the tool loop) applies to MCP tools
|
|
21
|
+
for free. **`@forgewisp/core` is not modified and is a types-only/peer
|
|
22
|
+
dependency here**; the only runtime dependency this package adds is
|
|
23
|
+
[`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk).
|
|
24
|
+
Isolating the SDK in this opt-in package keeps users who don't need MCP from
|
|
25
|
+
pulling it into their bundle.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm add @forgewisp/mcp @forgewisp/core
|
|
31
|
+
# or
|
|
32
|
+
npm install @forgewisp/mcp @forgewisp/core
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`@forgewisp/core` is a peer dependency. ESM and CommonJS builds are shipped
|
|
36
|
+
(`dist/index.mjs`, `dist/index.cjs`, `dist/index.d.ts`) — there is **no
|
|
37
|
+
self-contained browser global build**, since inlining the MCP SDK (which
|
|
38
|
+
relies on browser-native `fetch`/`EventSource`) into a single IIFE bundle
|
|
39
|
+
would be heavy and fragile. Consumers bundle it through their app bundler,
|
|
40
|
+
which resolves the externalized SDK and peer core. Node.js ≥ 18.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createAgent } from '@forgewisp/core';
|
|
46
|
+
import { registerMcpServer } from '@forgewisp/mcp';
|
|
47
|
+
|
|
48
|
+
const agent = createAgent({
|
|
49
|
+
llmEndpoint: 'https://api.openai.com/v1/chat/completions',
|
|
50
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
51
|
+
model: 'gpt-4o',
|
|
52
|
+
systemPrompt: 'You can use tools from the connected MCP server.',
|
|
53
|
+
// write/destructive tools require this — see "Risk tiers & confirmation".
|
|
54
|
+
onConfirmRequired: async (call) =>
|
|
55
|
+
window.confirm(`${call.functionName}: ${JSON.stringify(call.args)}`),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Connect, list tools, adapt, and register them on the agent in one call.
|
|
59
|
+
const handle = await registerMcpServer(
|
|
60
|
+
agent,
|
|
61
|
+
{
|
|
62
|
+
name: 'my-server',
|
|
63
|
+
url: 'https://example.com/mcp',
|
|
64
|
+
apiKey: process.env.MCP_API_KEY, // optional; omitted entirely when unset
|
|
65
|
+
defaultTier: 'read',
|
|
66
|
+
tierOverrides: { sendEmail: 'write' }, // assign tiers per original tool name
|
|
67
|
+
hasConfirmation: true, // required if any tool resolves to write/destructive
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
console.log(handle.toolNames); // e.g. ['my-server__search', 'my-server__sendEmail']
|
|
72
|
+
|
|
73
|
+
const result = await agent.run('Search the docs for "MCP" and email me a summary.');
|
|
74
|
+
|
|
75
|
+
// Later: deregister all tools and disconnect.
|
|
76
|
+
await handle.close();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use the lower-level `createMcpTools` when you want the adapted
|
|
80
|
+
`FunctionDefinition[]` in hand without touching any agent — for custom
|
|
81
|
+
registration, a `ToolSet`, or rendering a tier-grouped tool list:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { createMcpTools } from '@forgewisp/mcp';
|
|
85
|
+
|
|
86
|
+
const { tools, authState, close, finishAuth } = await createMcpTools({
|
|
87
|
+
name: 'my-server',
|
|
88
|
+
url: 'https://example.com/mcp',
|
|
89
|
+
defaultTier: 'read',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
for (const def of tools) agent.registerFunction(def); // register however you like
|
|
93
|
+
// `tools[i].name`, `.description`, and `.riskTier` are populated for UI rendering.
|
|
94
|
+
|
|
95
|
+
await close(); // disconnect when done
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API
|
|
99
|
+
|
|
100
|
+
### `registerMcpServer(agent, config, options?): Promise<McpServerHandle>`
|
|
101
|
+
|
|
102
|
+
Connect to an MCP server, list its tools, adapt each into a
|
|
103
|
+
`FunctionDefinition`, and register them on `agent` (any object with
|
|
104
|
+
`registerFunction` / `deregisterFunction` — `ReturnType<typeof createAgent>`
|
|
105
|
+
satisfies this structurally). Returns a handle for lifecycle management.
|
|
106
|
+
|
|
107
|
+
- **Confirmation preflight.** Before registering anything, if any adapted
|
|
108
|
+
tool resolves to `write`/`destructive` and `config.hasConfirmation` is not
|
|
109
|
+
`true`, the client is closed and a clear error is thrown — so a tier
|
|
110
|
+
mismatch never leaves a partially-registered server. Core's own
|
|
111
|
+
registration-time invariant still fires as a backstop inside
|
|
112
|
+
`agent.registerFunction`.
|
|
113
|
+
- **OAuth.** When the first connect needs auth, no tools are registered yet
|
|
114
|
+
and the preflight is deferred until `handle.finishAuth(code)` lists the
|
|
115
|
+
tools (see [OAuth 2.1 + PKCE](#oauth-21--pkce)).
|
|
116
|
+
|
|
117
|
+
### `createMcpTools(config, options?): Promise<McpToolsResult>`
|
|
118
|
+
|
|
119
|
+
The agent-free form: connect, list, and adapt without registering on any
|
|
120
|
+
agent. Returns `{ tools, authState, close, finishAuth }`. `tools` is empty
|
|
121
|
+
while `authState` is `'pending'` and is mutated in place once auth completes,
|
|
122
|
+
so the caller's reference stays valid. `close`/`finishAuth` are arrow
|
|
123
|
+
properties (they destructure cleanly).
|
|
124
|
+
|
|
125
|
+
### `McpServerConfig`
|
|
126
|
+
|
|
127
|
+
| Field | Type | Default | Description |
|
|
128
|
+
| ------------------- | ------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
129
|
+
| `name` | `string` | — | Logical server name. Used as the default `toolNamePrefix` and the attribution suffix on each tool's description. |
|
|
130
|
+
| `url` | `string` | — | Streamable HTTP endpoint URL of the MCP server. |
|
|
131
|
+
| `apiKey` | `string?` | — | Bearer token sent as `Authorization`. Omitted entirely when unset (so no-auth/proxy endpoints work). Mutually exclusive with `authProvider`; if both are set, `authProvider` wins and `apiKey` is ignored. |
|
|
132
|
+
| `authProvider` | `OAuthClientProvider?` | — | Drives the full OAuth 2.1 + PKCE flow via the SDK. Takes precedence over `apiKey`. |
|
|
133
|
+
| `requestTimeoutMs` | `number?` | `60000` | Per-call timeout for `listTools`/`callTool`, in ms. |
|
|
134
|
+
| `toolNamePrefix` | `string?` | `config.name` | Prefix for the registered (prefixed) tool names. |
|
|
135
|
+
| `defaultTier` | `RiskTier?` | `'read'` | Risk tier for tools not listed in `tierOverrides`. |
|
|
136
|
+
| `tierOverrides` | `Record<string, RiskTier>?` | `{}` | Map of original MCP tool name → risk tier (overrides `defaultTier`). |
|
|
137
|
+
| `onlyTools` | `string[]?` | — | Allowlist of original MCP tool names to register; all others are skipped. `[]` registers none. |
|
|
138
|
+
| `excludeTools` | `string[]?` | — | Blocklist of original MCP tool names to skip. |
|
|
139
|
+
| `hasConfirmation` | `boolean?` | — | Required to be `true` when any resolved tool tier is `write`/`destructive`; `registerMcpServer` throws up front otherwise. |
|
|
140
|
+
|
|
141
|
+
### `McpServerHandle`
|
|
142
|
+
|
|
143
|
+
| Field | Type | Description |
|
|
144
|
+
| ------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
145
|
+
| `toolNames` | `string[]` | The prefixed tool names registered on the agent. Empty while `authState` is `'pending'`. |
|
|
146
|
+
| `authState` | `'authorized' \| 'pending'` | `'pending'` means OAuth is required and `finishAuth` (or resume) is needed before tools exist. |
|
|
147
|
+
| `finishAuth` | `(code: string) => Promise<void>` | Complete the OAuth redirect-back: exchange the code, reconnect, list tools, run preflight, register. |
|
|
148
|
+
| `close` | `() => Promise<void>` | Deregister all of this server's tools and disconnect. |
|
|
149
|
+
|
|
150
|
+
### Re-exported types
|
|
151
|
+
|
|
152
|
+
The package re-exports the core types you need (`FunctionDefinition`,
|
|
153
|
+
`RiskTier`, `JSONSchema`, `JSONSchemaProperty`, `ToolContext`) and the SDK
|
|
154
|
+
OAuth shapes (`OAuthClientProvider`, `OAuthTokens`, `OAuthClientMetadata`,
|
|
155
|
+
`OAuthClientInformationMixed`) so you can import everything from
|
|
156
|
+
`@forgewisp/mcp`.
|
|
157
|
+
|
|
158
|
+
## How adaptation works
|
|
159
|
+
|
|
160
|
+
- **Risk tiers come from config, not MCP hints.** MCP has no tier concept.
|
|
161
|
+
`defaultTier` + `tierOverrides` assign tiers; MCP
|
|
162
|
+
`annotations.readOnlyHint`/`destructiveHint` are informational only and
|
|
163
|
+
deliberately **not** auto-mapped — the consumer owns the security boundary.
|
|
164
|
+
- **Schema pass-through.** MCP `inputSchema` is full draft-07 JSON Schema.
|
|
165
|
+
Forgewisp's `JSONSchema` type is a narrow subset that is **not** widened; the
|
|
166
|
+
adapter casts the raw `inputSchema` at the boundary. Runtime is correct:
|
|
167
|
+
core's Ajv is `strict: false` and validates draft-07 (`$ref`/`enum`/`oneOf`
|
|
168
|
+
…), the compiled validator is cached per schema-object identity, and the
|
|
169
|
+
wire payload to the LLM carries the full schema. (External `$ref`s won't
|
|
170
|
+
resolve — they don't appear in well-formed MCP schemas.)
|
|
171
|
+
- **Name namespacing.** Registered names are `${prefix}__<sanitized>`,
|
|
172
|
+
sanitized to OpenAI's `^[A-Za-z0-9_-]{1,64}$`, with numeric suffixes on
|
|
173
|
+
collision. Tools are called by their **original** (un-prefixed) name so the
|
|
174
|
+
MCP server sees the right handler.
|
|
175
|
+
- **Result flattening.** MCP `callTool` results are reduced to a single
|
|
176
|
+
JSON-serializable value: `structuredContent` preferred, then a single
|
|
177
|
+
`text` item's `.text`, then joined text, else `{ content }`. `isError`
|
|
178
|
+
throws so the executor records `function_errored`.
|
|
179
|
+
- **Abort.** The parent run's `AbortSignal` (threaded in via `ToolContext`) is
|
|
180
|
+
forwarded to `client.callTool`, merged with the per-server
|
|
181
|
+
`requestTimeoutMs` — aborting the parent run aborts in-flight MCP calls, and
|
|
182
|
+
the timeout covers both the connect handshake and each `callTool`/`listTools`.
|
|
183
|
+
|
|
184
|
+
## Risk tiers & confirmation
|
|
185
|
+
|
|
186
|
+
`@forgewisp/mcp` respects the same risk-tier boundary as `@forgewisp/core`.
|
|
187
|
+
**Tier discipline is a security boundary here too:**
|
|
188
|
+
|
|
189
|
+
- **`read`** tools run immediately, no confirmation.
|
|
190
|
+
- **`write` / `destructive`** tools require the consumer to configure
|
|
191
|
+
`onConfirmRequired` **and** pass `hasConfirmation: true` to
|
|
192
|
+
`registerMcpServer`. The package runs a preflight before registering
|
|
193
|
+
anything; core's own registration-time invariant fires as a backstop.
|
|
194
|
+
|
|
195
|
+
**Confirmation UI must be rendered only from the schema-validated
|
|
196
|
+
`PendingCall.args`** — never from raw LLM output. The executor validates args
|
|
197
|
+
against the JSON Schema before confirmation, so the args you get back are
|
|
198
|
+
guaranteed well-formed.
|
|
199
|
+
|
|
200
|
+
## OAuth 2.1 + PKCE
|
|
201
|
+
|
|
202
|
+
`config.authProvider` (an SDK `OAuthClientProvider`) takes precedence over
|
|
203
|
+
`apiKey` and drives the full OAuth 2.1 authorization-code + PKCE flow via the
|
|
204
|
+
SDK: RFC 9728/8414 discovery, dynamic client registration (RFC 7591), token
|
|
205
|
+
exchange, and refresh. The consumer implements the provider and owns
|
|
206
|
+
browser-redirect plumbing + token storage. `client/auth.js` (and its
|
|
207
|
+
`UnauthorizedError`) is dynamically imported **only on the OAuth path** —
|
|
208
|
+
non-OAuth consumers never load the auth module.
|
|
209
|
+
|
|
210
|
+
The consumer-visible state machine is `McpAuthState = 'authorized' | 'pending'`:
|
|
211
|
+
|
|
212
|
+
- A first connect that needs auth returns `authState: 'pending'` with an
|
|
213
|
+
empty `tools` array instead of throwing — the SDK has already called
|
|
214
|
+
`provider.redirectToAuthorization`. The consumer redirects the user to the
|
|
215
|
+
authorization server, then calls `handle.finishAuth(code)` once the
|
|
216
|
+
redirect-back arrives.
|
|
217
|
+
- **Fresh transport on resume.** `finishAuth(code)` is not "reconnect the
|
|
218
|
+
spent transport." The SDK's `Client.connect` catches the `UnauthorizedError`
|
|
219
|
+
thrown during the redirect, calls `void this.close()` (which aborts the
|
|
220
|
+
transport's `AbortController` but does not null it), and rethrows — so a
|
|
221
|
+
second `start()` on that transport throws `"already started"`. The same
|
|
222
|
+
transport cannot be re-`connect`ed. So `finishAuth(code)` exchanges the code
|
|
223
|
+
on the spent transport (its `finishAuth` only calls `auth()`, independent of
|
|
224
|
+
`start()`), disconnects it, builds a **fresh** transport whose `connect`
|
|
225
|
+
reads `provider.tokens()` and succeeds, then re-lists tools and splices them
|
|
226
|
+
into the caller's `tools` array in place. The same fresh-transport pattern
|
|
227
|
+
backs `McpConnectOptions.authorizationCode` (the page-reload resume path:
|
|
228
|
+
exchange the code on a fresh transport *before* connecting).
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const handle = await registerMcpServer(agent, {
|
|
232
|
+
name: 'oauth-server',
|
|
233
|
+
url: 'https://example.com/mcp',
|
|
234
|
+
authProvider: myOAuthProvider, // your OAuthClientProvider implementation
|
|
235
|
+
defaultTier: 'read',
|
|
236
|
+
hasConfirmation: true,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (handle.authState === 'pending') {
|
|
240
|
+
// The SDK already redirected the user to authorize. When they come back
|
|
241
|
+
// with `?code=...&state=...`, complete the flow:
|
|
242
|
+
await handle.finishAuth(authorizationCode);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// After a page reload, resume in one shot instead of re-prompting:
|
|
246
|
+
const handle = await registerMcpServer(agent, config, { authorizationCode: code });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
See `apps/mcp-demo` for a complete `OAuthClientProvider` backed by
|
|
250
|
+
`localStorage` (popup + same-tab fallback, `oauth-callback.html`, and a
|
|
251
|
+
load-resume path via `createMcpTools(config, { authorizationCode })`).
|
|
252
|
+
|
|
253
|
+
## Security in production
|
|
254
|
+
|
|
255
|
+
Connecting an agent to an MCP server exposes it to that server's tools, their
|
|
256
|
+
schemas, and the text those tools return. Treat everything coming from the
|
|
257
|
+
server as untrusted input, the same way you would treat model output. The
|
|
258
|
+
risk-tier and confirmation machinery above is necessary but not sufficient —
|
|
259
|
+
in production, also mind these points:
|
|
260
|
+
|
|
261
|
+
- **Treat MCP servers as a trust boundary.** The agent will call a
|
|
262
|
+
server's tools based on model decisions, with whatever tiers you assigned.
|
|
263
|
+
Only connect to servers you control or have vetted. Audit which tools a
|
|
264
|
+
server exposes (`onlyTools` / `excludeTools`) before granting it
|
|
265
|
+
`write`/`destructive` tiers — do not blanket-map an untrusted server's
|
|
266
|
+
tools to `destructive`.
|
|
267
|
+
- **Tiers come from you, not the server.** MCP has no risk concept, and this
|
|
268
|
+
adapter deliberately does **not** auto-map `annotations.readOnlyHint` /
|
|
269
|
+
`destructiveHint`. The server controls its own annotations; trusting them
|
|
270
|
+
would let a server self-promote a tool to `read` and skip confirmation.
|
|
271
|
+
Decide tiers in `McpServerConfig` (`defaultTier` + `tierOverrides`) per
|
|
272
|
+
tool you actually intend to expose.
|
|
273
|
+
- **Tool descriptions and results are prompt-injection vectors.** Each adapted
|
|
274
|
+
tool's `description` is the server's own description text, and tool results
|
|
275
|
+
flow back into the conversation as model input. A compromised or hostile
|
|
276
|
+
server can craft descriptions/results that steer the model to call other
|
|
277
|
+
tools, exfiltrate data, or bypass confirmation. This is inherent to letting
|
|
278
|
+
a model consume server-supplied text — keep `write`/`destructive` tiers
|
|
279
|
+
narrow and confirmation strict, and prefer `onlyTools` allowlists over
|
|
280
|
+
registering everything a server exposes.
|
|
281
|
+
- **Confirm only from validated args.** Confirmation UI must be rendered from
|
|
282
|
+
the schema-validated `PendingCall.args`, never from raw model or
|
|
283
|
+
server-supplied text. The executor validates before confirming, so the args
|
|
284
|
+
you render are well-formed — don't re-render unvalidated server content in a
|
|
285
|
+
confirm dialog.
|
|
286
|
+
- **Don't ship long-lived secrets to the browser.** `apiKey` is sent as
|
|
287
|
+
`Authorization: Bearer …` and is visible to the page (and to anything with
|
|
288
|
+
access to the page's memory). For servers requiring user-scoped access,
|
|
289
|
+
prefer `authProvider` (OAuth) so tokens are obtained and refreshed per user
|
|
290
|
+
with a defined lifetime, rather than baking a shared key into the bundle.
|
|
291
|
+
- **Always use HTTPS.** Connect to `https://` endpoints only. Plain HTTP leaks
|
|
292
|
+
the bearer token, tool arguments, and tool results; it also lets a
|
|
293
|
+
network attacker tamper with the tool list and results the agent acts on.
|
|
294
|
+
- **Bound resource use.** Set a sane `requestTimeoutMs` so a hung or slow
|
|
295
|
+
server can't stall a run indefinitely — the timeout covers the connect
|
|
296
|
+
handshake, `listTools`, and each `callTool`. Aborting the parent run (the
|
|
297
|
+
`AbortSignal` threaded in via `ToolContext`) cancels in-flight MCP calls.
|
|
298
|
+
- **Atomic registration.** The `hasConfirmation` preflight runs before any
|
|
299
|
+
tool is registered, and a partial-registration failure rolls back already
|
|
300
|
+
registered tools — so a tier mismatch never leaves a half-registered
|
|
301
|
+
server whose tools the model could already call. Keep `hasConfirmation: true`
|
|
302
|
+
whenever any tool can be `write`/`destructive`.
|
|
303
|
+
- **OAuth token storage is yours to harden.** The SDK calls your
|
|
304
|
+
`OAuthClientProvider` for token persistence; the demo uses `localStorage`,
|
|
305
|
+
which is reachable by any script on your origin (XSS). For real deployments,
|
|
306
|
+
store tokens somewhere less XSS-exposed and scope them per server name.
|
|
307
|
+
|
|
308
|
+
## Tests
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
pnpm install
|
|
312
|
+
pnpm --filter @forgewisp/mcp test # vitest run
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The adapter is layered so the mapping is unit-testable without a network:
|
|
316
|
+
`adaptMcpTool` / `adaptMcpTools` are pure given a `McpClient` (a plain fake
|
|
317
|
+
implementing `callTool` is enough). `register-mcp-server.test.ts` and
|
|
318
|
+
`oauth.test.ts` use the SDK's `InMemoryTransport` via an `@internal`
|
|
319
|
+
`options.transport` injection seam on `createMcpTools` / `registerMcpServer`
|
|
320
|
+
(a `Transport` or a `() => Transport` factory — the factory form lets the
|
|
321
|
+
OAuth tests share one in-memory pipe across the gated transports that
|
|
322
|
+
`finishAuth`/resume build). This seam is not part of the stable public
|
|
323
|
+
contract.
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
MIT
|