@graphorin/mcp 0.5.0 → 0.6.1
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/CHANGELOG.md +57 -0
- package/README.md +23 -16
- package/dist/client/adapt-result.js +22 -10
- package/dist/client/adapt-result.js.map +1 -1
- package/dist/client/client-handlers.js +11 -2
- package/dist/client/client-handlers.js.map +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +111 -27
- package/dist/client/client.js.map +1 -1
- package/dist/client/inbound-filters.js +8 -3
- package/dist/client/inbound-filters.js.map +1 -1
- package/dist/client/mcp-resource-reader.d.ts +10 -0
- package/dist/client/mcp-resource-reader.d.ts.map +1 -1
- package/dist/client/mcp-resource-reader.js +16 -5
- package/dist/client/mcp-resource-reader.js.map +1 -1
- package/dist/client/pinning.js +2 -2
- package/dist/client/pinning.js.map +1 -1
- package/dist/client/to-tools.js.map +1 -1
- package/dist/client/transport-factory.js +1 -1
- package/dist/client/transport-factory.js.map +1 -1
- package/dist/client/types.d.ts +58 -10
- package/dist/client/types.d.ts.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +2 -2
- package/dist/errors/index.js.map +1 -1
- package/dist/helpers/validate-config.js +1 -1
- package/dist/helpers/validate-config.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/oauth/bridge.d.ts +1 -1
- package/dist/oauth/bridge.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/registry/json-schema.js +36 -7
- package/dist/registry/json-schema.js.map +1 -1
- package/dist/transport/types.d.ts +4 -4
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# @graphorin/mcp
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#142](https://github.com/o-stepper/graphorin/pull/142) [`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430) Thanks [@o-stepper](https://github.com/o-stepper)! - Version constants and version-bearing strings now derive from each package's manifest at build time (`VERSION = pkg.version`; writer ids, client/server info, OTLP framework attributes, build-info metrics interpolate it). No behavioral change at the current version: the rendered strings are byte-identical. A release bump no longer edits source; the new `check-version-consistency` gate fails any reintroduced hardcoded framework version.
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`826ee59`](https://github.com/o-stepper/graphorin/commit/826ee5933ad38693b0dd2f20a110abfecba7d23d), [`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430)]:
|
|
10
|
+
- @graphorin/observability@0.6.1
|
|
11
|
+
- @graphorin/core@0.6.1
|
|
12
|
+
- @graphorin/security@0.6.1
|
|
13
|
+
- @graphorin/tools@0.6.1
|
|
14
|
+
|
|
15
|
+
## 0.6.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- [#136](https://github.com/o-stepper/graphorin/pull/136) [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84) Thanks [@o-stepper](https://github.com/o-stepper)! - Deterministic security adoptions (audit 2026-07-04 Wave C, cluster C6).
|
|
20
|
+
|
|
21
|
+
- Derived-taint propagation: opt-in `dataFlowPolicy.derivedTaint: 'strict'` fires the paraphrase-robust `derived-untrusted-to-sink` flow for every model-driven sink call once untrusted content entered the run (CaMeL-style control-flow integrity); the agent also records each tainted step's assistant text as `llm-derived` spans so model-echoed phrasing trips the verbatim probe.
|
|
22
|
+
- Taint into memory (cross-session MINJA leg): `ToolReturn` gains a widen-only `taint` override honoured through the executor record path; `fact_search` / `deep_recall` / `recall_episodes` attach it when any returned item is quarantined or foreign-provenance, re-arming the ledger at recall. `RunState.taintSummary` additionally carries one-way FNV-1a span-tile hashes (no plaintext), so a resumed run re-detects pre-suspend verbatim copies.
|
|
23
|
+
- MCP pinning completed: `toTools({ pinStore })` records fingerprints on first use and REJECTS drift by default when a store is present (rug-pull defense; `onPinMismatch: 'warn'` downgrades); tool-description injection hits at registration are counted (`mcp.tool-description.injection-flagged.total`).
|
|
24
|
+
- Signal-only heuristics + Unicode pre-pass: shared `normalizeForMatching` (NFKC + zero-width strip) applied in the guardrails injection catalogue and the memory quarantine heuristics; security.md repositions all pattern catalogues as best-effort signal, never a sole gate. `TaintLabel.sourceKind` widened to `string` for the new descriptive kinds.
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- [#134](https://github.com/o-stepper/graphorin/pull/134) [`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6) Thanks [@o-stepper](https://github.com/o-stepper)! - `listTools`/`listResources`/`listPrompts` now follow `nextCursor` and drain the full cursor chain (mcp-skills-02). MCP list operations are paginated since protocol 2024-11-05 and the SDK does not auto-paginate, so a paginating server's catalogue was silently truncated to page 1 - tools beyond it never reached `toTools()`, defer-loading thresholds counted a partial catalogue, and pin fingerprints covered a partial catalogue. A defensive 100-page cap (with a WARN) bounds buggy or adversarial servers that never terminate the chain.
|
|
29
|
+
|
|
30
|
+
- [#134](https://github.com/o-stepper/graphorin/pull/134) [`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6) Thanks [@o-stepper](https://github.com/o-stepper)! - Fix critical tool-schema wire bug (tools-01): plain Zod schemas were never converted to JSON Schema, so OpenAI-shaped/Ollama/vercel providers received `{"_def":...}` internals as tool `parameters` and MCP tools serialized to `{}`. Adds a shared structural Zod v3/v4 to JSON Schema converter (`@graphorin/tools/schema`, no new dependencies) used by the agent's `toolToDefinition`, the code-mode signature projection, and `ToolSearchMatch`; MCP's `buildJsonSchemaValidator` now retains the source JSON Schema and exposes it via `toJSON()`. Unprojectable schemas degrade loudly (WARN + permissive `{}`) instead of shipping serialized validator internals.
|
|
31
|
+
|
|
32
|
+
- [#135](https://github.com/o-stepper/graphorin/pull/135) [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3) Thanks [@o-stepper](https://github.com/o-stepper)! - MCP correctness tail (audit 2026-07-04 Wave B, cluster B8).
|
|
33
|
+
|
|
34
|
+
- mcp-skills-05: a `sampling/createMessage` carrying `tools` / `toolChoice` is rejected with an `McpError` (the 2025-11-25 MUST - the client does not declare `sampling.tools`; previously it silently answered as a plain completion). URL-mode elicitation is declined honestly instead of being surfaced as an empty form with the URL invisible.
|
|
35
|
+
- mcp-skills-06: MCP resource handles are scoped to their originating server (`mcp:<serverId>:<uri>`); `createMcpResourceReader` consults ONLY the matching client, closing the cross-server confused-deputy hop where server A's link (or a prompt-injected model) fetched a resource from trusted server B. Bare URIs are refused unless `allowCrossServer: true` is opted in.
|
|
36
|
+
- mcp-skills-07: server-supplied JSON-Schema `pattern`s are guarded before compilation - pattern/tested-string length caps plus a nested-quantifier heuristic (`(a+)+`-class) - so a malicious server can no longer stall the event loop with catastrophic backtracking; guarded-out patterns degrade to permissive like malformed ones.
|
|
37
|
+
- mcp-skills-10: new `onTransportClose` / `onTransportError` callbacks (plus `mcp.transport.closed|error.total` counters and a WARN log) surface a dead stdio child / dropped HTTP session; previously a disconnect was observable only as protocol errors on later calls.
|
|
38
|
+
- mcp-skills-11: new `MCPClient.readResourceContents(uri)` returns every content item; the single-content `readResource` convenience now WARNs + counts when it truncates a multi-content response.
|
|
39
|
+
- mcp-skills-04 (adjusted): a same-source tool re-registration increments `tool.registry.same-source-replaced.total`, so two server instances colliding on one identity are observable churn instead of a silent swap.
|
|
40
|
+
- mcp-skills-09 (F-10): the documented NESTED `metadata.graphorin` frontmatter form now actually resolves (flat dotted keys still win when both are present); skills.md fixes `sandboxTier` to `sandbox` and the `parseSlashCommand` output shape.
|
|
41
|
+
- mcp-skills-08 (F-9): mcp-client.md rewritten to the real observability surface (counters, no `mcp.tool.invoke` span, no per-call audit rows), the real executor error mapping, and the `.`-namespaced `sideEffectClassByTool` keys; sampling-with-tools / tasks / icons documented as known-unsupported.
|
|
42
|
+
|
|
43
|
+
- [#138](https://github.com/o-stepper/graphorin/pull/138) [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a) Thanks [@o-stepper](https://github.com/o-stepper)! - Documentation-truth sweep (audit 2026-07-04 Wave E, cluster E10): stale npm package descriptions rewritten (cli "Phase 14a three commands", mcp "upcoming auth CLI", store-sqlite's nonexistent WorkerPool), the store-sqlite WorkerPool TSDoc and the cipher-pragma ordering comment corrected, the executor timeout-precedence JSDoc fixed to the real `inlineToolTimeoutMs > tier timeoutMs > default` order, the `rrf.<label>` explain signals documented, and the skills spec-snapshot wording no longer claims a CI cron refreshes it (the diff is a manual `--upstream` pass; the release gate only parses the bundled snapshot).
|
|
44
|
+
|
|
45
|
+
- Updated dependencies [[`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6), [`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6), [`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6), [`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a), [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a)]:
|
|
46
|
+
- @graphorin/tools@0.6.0
|
|
47
|
+
- @graphorin/core@0.6.0
|
|
48
|
+
- @graphorin/security@0.6.0
|
|
49
|
+
- @graphorin/observability@0.6.0
|
|
50
|
+
|
|
51
|
+
## 0.5.0
|
|
52
|
+
|
|
53
|
+
First version published to the npm registry (with Sigstore build
|
|
54
|
+
provenance). The 0.2.0, 0.3.0, and 0.4.0 versions were internal lockstep
|
|
55
|
+
milestones and were never published. All `@graphorin/*` packages release
|
|
56
|
+
lockstep at the same version; the full release notes for 0.2.0-0.5.0 live
|
|
57
|
+
in the repository-level
|
|
58
|
+
[CHANGELOG](https://github.com/o-stepper/graphorin/blob/main/CHANGELOG.md).
|
|
59
|
+
|
|
3
60
|
## 0.1.0
|
|
4
61
|
|
|
5
62
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -26,8 +26,15 @@ the existing outbound OAuth subsystem in `@graphorin/security`.
|
|
|
26
26
|
that have not migrated yet (the runtime emits one WARN per
|
|
27
27
|
process on selection).
|
|
28
28
|
- **Typed `MCPClient` surface.** `listTools` / `listResources` /
|
|
29
|
-
`listPrompts` / `callTool` / `readResource` /
|
|
29
|
+
`listPrompts` / `callTool` / `readResource` /
|
|
30
|
+
`readResourceContents` (multi-content resources) / `getPrompt` /
|
|
30
31
|
`close` plus the strategy-aware `toTools(...)` adapter.
|
|
32
|
+
- **Tool pinning (TOFU rug-pull defense).** `toTools({ pinStore })`
|
|
33
|
+
records a fingerprint of each tool's name / description / schema on
|
|
34
|
+
first use and, when a pin store is present, **rejects** silent drift
|
|
35
|
+
by default (`onPinMismatch: 'warn'` downgrades to a warning);
|
|
36
|
+
description-injection heuristics at registration feed the
|
|
37
|
+
`mcp.tool-description.injection-flagged.total` counter.
|
|
31
38
|
- **Strict default for MCP-derived tools.** Every `Tool` produced
|
|
32
39
|
by `MCPClient.toTools()` defaults to the
|
|
33
40
|
`'detect-and-strip-and-wrap'` inbound prompt-injection
|
|
@@ -65,7 +72,7 @@ the existing outbound OAuth subsystem in `@graphorin/security`.
|
|
|
65
72
|
- **Streamable HTTP sessions.** The client persists the assigned
|
|
66
73
|
`Mcp-Session-Id` and the SDK transport auto-reconnects with
|
|
67
74
|
`Last-Event-ID` after a transient disconnect. Event replay is the
|
|
68
|
-
SERVER's responsibility per the Streamable HTTP spec
|
|
75
|
+
SERVER's responsibility per the Streamable HTTP spec - the client
|
|
69
76
|
surfaces `sessionIdPresent` (stateful routing detected; not a
|
|
70
77
|
replay guarantee).
|
|
71
78
|
|
|
@@ -182,7 +189,7 @@ const issues = await createMCPClient({
|
|
|
182
189
|
// `authProvider.resolveHeader()` on every outgoing request, so the
|
|
183
190
|
// refresh-ahead window fires automatically and a long-lived session
|
|
184
191
|
// survives token expiry without re-creating the client. Do **not**
|
|
185
|
-
// resolve the token once into static `headers`
|
|
192
|
+
// resolve the token once into static `headers` - that pins a single
|
|
186
193
|
// token and defeats the refresh.
|
|
187
194
|
authProvider,
|
|
188
195
|
});
|
|
@@ -207,7 +214,7 @@ client persists it for stateful routing and exposes
|
|
|
207
214
|
alias). A session id is **not** a replay guarantee: per the
|
|
208
215
|
Streamable HTTP spec, event replay is the **server's**
|
|
209
216
|
responsibility, and the SDK transport already auto-reconnects with
|
|
210
|
-
the `Last-Event-ID` header after a transient disconnect
|
|
217
|
+
the `Last-Event-ID` header after a transient disconnect - no client
|
|
211
218
|
configuration needed. (The former client-side `eventStore` option
|
|
212
219
|
was removed: a client-held store cannot drive replay; passing the
|
|
213
220
|
legacy option logs a warning.)
|
|
@@ -256,19 +263,19 @@ Every error class extends `GraphorinMCPError` and carries a
|
|
|
256
263
|
stable lowercase `kind` discriminator and an actionable `hint`
|
|
257
264
|
field where applicable:
|
|
258
265
|
|
|
259
|
-
- `MCPConnectionError`
|
|
260
|
-
- `MCPProtocolError`
|
|
261
|
-
- `MCPAuthError`
|
|
262
|
-
- `MCPToolNotFoundError`
|
|
263
|
-
- `MCPCallTimeoutError`
|
|
264
|
-
- `MCPCancelledError`
|
|
265
|
-
- `MCPInvalidConfigError`
|
|
266
|
-
- `MCPTransportNotSupportedError`
|
|
266
|
+
- `MCPConnectionError` - transport could not be established or was dropped.
|
|
267
|
+
- `MCPProtocolError` - JSON-RPC / MCP protocol-level error.
|
|
268
|
+
- `MCPAuthError` - authentication / authorization failure.
|
|
269
|
+
- `MCPToolNotFoundError` - the requested tool is not registered with the server.
|
|
270
|
+
- `MCPCallTimeoutError` - tool call exceeded the configured timeout (variant `'session-lost'` for the resume-handshake-lost path).
|
|
271
|
+
- `MCPCancelledError` - call was cancelled by an `AbortSignal`.
|
|
272
|
+
- `MCPInvalidConfigError` - the supplied `MCPTransportConfig` is invalid.
|
|
273
|
+
- `MCPTransportNotSupportedError` - the supplied configuration requested an unsupported transport / capability combination (variant `'transport-resumable-not-supported'` for resumable sessions on `'stdio'` / `'sse'`).
|
|
267
274
|
|
|
268
275
|
## Acceptance & testing
|
|
269
276
|
|
|
270
|
-
-
|
|
271
|
-
no network calls (verified by the workspace
|
|
277
|
+
- The package's unit + integration + property tests run under Vitest
|
|
278
|
+
with no network calls (verified by the workspace
|
|
272
279
|
`pnpm run check-no-network` script).
|
|
273
280
|
- Integration tests use the SDK's `InMemoryTransport` linked-pair
|
|
274
281
|
+ a configurable in-process `Server` fixture; no child
|
|
@@ -289,8 +296,8 @@ field where applicable:
|
|
|
289
296
|
|
|
290
297
|
## License
|
|
291
298
|
|
|
292
|
-
MIT
|
|
299
|
+
MIT - © 2026 Oleksiy Stepurenko.
|
|
293
300
|
|
|
294
301
|
---
|
|
295
302
|
|
|
296
|
-
**Project Graphorin** · v0.
|
|
303
|
+
**Project Graphorin** · v0.6.1 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
|
|
@@ -27,9 +27,9 @@ function adaptCallResult(args) {
|
|
|
27
27
|
server: serverIdentity.id,
|
|
28
28
|
tool: toolName
|
|
29
29
|
});
|
|
30
|
-
resourceLinkPreviews.push(formatResourceLinkPreview(part));
|
|
30
|
+
resourceLinkPreviews.push(formatResourceLinkPreview(part, serverIdentity.id));
|
|
31
31
|
}
|
|
32
|
-
const messagePart = mcpContentToMessageContent(part);
|
|
32
|
+
const messagePart = mcpContentToMessageContent(part, serverIdentity.id);
|
|
33
33
|
if (messagePart !== void 0) contentParts.push(messagePart);
|
|
34
34
|
}
|
|
35
35
|
const textParts = (result.content ?? []).filter((p) => p.type === "text");
|
|
@@ -44,7 +44,7 @@ function adaptCallResult(args) {
|
|
|
44
44
|
break;
|
|
45
45
|
case "resource":
|
|
46
46
|
if (part.resource.text !== void 0) outputSegments.push(part.resource.text);
|
|
47
|
-
else if (part.resource.blob !== void 0) outputSegments.push(`[resource ${part.resource.uri} ${part.resource.mimeType ?? "application/octet-stream"}, ~${approxDecodedSize(part.resource.blob)}
|
|
47
|
+
else if (part.resource.blob !== void 0) outputSegments.push(`[resource ${part.resource.uri} ${part.resource.mimeType ?? "application/octet-stream"}, ~${approxDecodedSize(part.resource.blob)} - full payload in contentParts]`);
|
|
48
48
|
else outputSegments.push(`Resource ${part.resource.uri}`);
|
|
49
49
|
break;
|
|
50
50
|
case "resource_link": break;
|
|
@@ -104,7 +104,7 @@ function adaptCallResult(args) {
|
|
|
104
104
|
contentParts: Object.freeze(contentParts)
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
|
-
function mcpContentToMessageContent(part) {
|
|
107
|
+
function mcpContentToMessageContent(part, serverId) {
|
|
108
108
|
switch (part.type) {
|
|
109
109
|
case "text": return {
|
|
110
110
|
type: "text",
|
|
@@ -139,22 +139,34 @@ function mcpContentToMessageContent(part) {
|
|
|
139
139
|
}
|
|
140
140
|
case "resource_link": return {
|
|
141
141
|
type: "text",
|
|
142
|
-
text: formatResourceLinkPreview(part)
|
|
142
|
+
text: formatResourceLinkPreview(part, serverId)
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
147
|
* Render the compact, model-facing preview for a `resource_link`. The
|
|
148
|
-
*
|
|
148
|
+
* handle is SCOPED to the originating server (mcp-skills-06):
|
|
149
|
+
* `mcp:<serverId>:<uri>` - an
|
|
149
150
|
* {@link import('./mcp-resource-reader.js').createMcpResourceReader}
|
|
150
|
-
*
|
|
151
|
+
* parses the prefix and consults ONLY that server, so a malicious
|
|
152
|
+
* server's link (or a prompt-injected model) cannot fetch a resource
|
|
153
|
+
* from a different, more-trusted server through the handle.
|
|
151
154
|
*/
|
|
152
|
-
function formatResourceLinkPreview(part) {
|
|
155
|
+
function formatResourceLinkPreview(part, serverId) {
|
|
153
156
|
const label = part.title === void 0 || part.title.length === 0 ? part.name : part.title;
|
|
154
157
|
const meta = [];
|
|
155
158
|
if (part.mimeType !== void 0) meta.push(part.mimeType);
|
|
156
159
|
if (part.size !== void 0) meta.push(`${part.size} bytes`);
|
|
157
|
-
return `[resource_link] ${label}${meta.length === 0 ? "" : ` (${meta.join(", ")})`}${part.description === void 0 || part.description.length === 0 ? "" : `
|
|
160
|
+
return `[resource_link] ${label}${meta.length === 0 ? "" : ` (${meta.join(", ")})`}${part.description === void 0 || part.description.length === 0 ? "" : ` - ${part.description}`}\nFetch the full contents on demand with read_result, handle: ${scopedResourceHandle(serverId, part.uri)}`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Build the server-scoped `read_result` handle for an MCP resource
|
|
164
|
+
* (mcp-skills-06): `mcp:<serverId>:<uri>`.
|
|
165
|
+
*
|
|
166
|
+
* @stable
|
|
167
|
+
*/
|
|
168
|
+
function scopedResourceHandle(serverId, uri) {
|
|
169
|
+
return `mcp:${serverId}:${uri}`;
|
|
158
170
|
}
|
|
159
171
|
/** Human-readable size of a base64 payload's decoded bytes (MC-8). */
|
|
160
172
|
function approxDecodedSize(base64) {
|
|
@@ -163,7 +175,7 @@ function approxDecodedSize(base64) {
|
|
|
163
175
|
}
|
|
164
176
|
/** Text descriptor for a non-text content part (MC-8). */
|
|
165
177
|
function describeBinaryPart(kind, mimeType, data) {
|
|
166
|
-
return `[${kind} ${mimeType}, ~${approxDecodedSize(data)}
|
|
178
|
+
return `[${kind} ${mimeType}, ~${approxDecodedSize(data)} - full payload in contentParts]`;
|
|
167
179
|
}
|
|
168
180
|
function decodeBase64(value) {
|
|
169
181
|
return Uint8Array.from(Buffer.from(value, "base64"));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapt-result.js","names":["contentParts: MessageContent[]","resourceLinkPreviews: string[]","outputSegments: string[]","meta: string[]"],"sources":["../../src/client/adapt-result.ts"],"sourcesContent":["/**\n * Result adaptation for the `toTools()` adapter (extracted from\n * `to-tools.ts` per F-MCP-001).\n *\n * Converts an MCP `CallToolResult` into a typed Graphorin `ToolReturn`,\n * handling the structured-content + outputSchema round-trip and the\n * backward-compatible TextContent mirror, and mapping each MCP content\n * part onto a Graphorin {@link MessageContent}.\n *\n * @packageDocumentation\n */\n\nimport type { MessageContent, ToolReturn, ZodLikeSchema } from '@graphorin/core';\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport { MCPToolExecutionError } from '../errors/index.js';\nimport type { ServerIdentity } from '../transport/types.js';\nimport type { MCPCallToolResult, MCPContentPart } from './types.js';\n\n/** Arguments for {@link adaptCallResult}. */\nexport interface AdaptCallResultArgs {\n readonly result: MCPCallToolResult;\n readonly outputSchema?: ZodLikeSchema<unknown> | undefined;\n readonly serverIdentity: ServerIdentity;\n readonly toolName: string;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n}\n\n/**\n * Convert an MCP `CallToolResult` into a typed Graphorin `ToolReturn`,\n * handling the structured-content + outputSchema round-trip and the\n * backward-compatible TextContent mirror.\n *\n * @internal\n */\nexport function adaptCallResult(args: AdaptCallResultArgs): ToolReturn<unknown> {\n const { result, outputSchema, serverIdentity, toolName } = args;\n // MC-4: the SDK deliberately does NOT throw on isError results — the\n // failure marker must not launder into a successful ToolReturn. Throw\n // the typed error so the executor records a real failure; the server's\n // text rides in the message for model self-correction.\n if (result.isError === true) {\n const errorText = (result.content ?? [])\n .filter((p): p is { type: 'text'; text: string } => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n incrementCounter('mcp.call.tool-error.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n throw new MCPToolExecutionError(\n errorText.length > 0 ? errorText : `MCP tool '${toolName}' reported an error result.`,\n { metadata: { tool: toolName } },\n );\n }\n const contentParts: MessageContent[] = [];\n // `resource_link` parts are NOT inlined: each contributes a compact\n // preview (carrying the `uri` as a result handle) so the model fetches\n // the body on demand via `read_result` instead of inflating context.\n const resourceLinkPreviews: string[] = [];\n for (const part of result.content) {\n if (part.type === 'resource_link') {\n incrementCounter('mcp.resource-link.emitted.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n resourceLinkPreviews.push(formatResourceLinkPreview(part));\n }\n const messagePart = mcpContentToMessageContent(part);\n if (messagePart !== undefined) contentParts.push(messagePart);\n }\n const textParts = (result.content ?? []).filter(\n (p): p is { type: 'text'; text: string } => p.type === 'text',\n );\n // MC-8: the typed `output` is the ONLY channel the agent loop\n // serialises into the tool message — non-text parts must leave a\n // text trace there (the full payloads stay in `contentParts`), and\n // embedded resource TEXT joins the concatenation outright.\n const outputSegments: string[] = [];\n for (const part of result.content ?? []) {\n switch (part.type) {\n case 'text':\n outputSegments.push(part.text);\n break;\n case 'image':\n case 'audio':\n outputSegments.push(describeBinaryPart(part.type, part.mimeType, part.data));\n break;\n case 'resource': {\n if (part.resource.text !== undefined) {\n outputSegments.push(part.resource.text);\n } else if (part.resource.blob !== undefined) {\n outputSegments.push(\n `[resource ${part.resource.uri} ${part.resource.mimeType ?? 'application/octet-stream'}, ~${approxDecodedSize(part.resource.blob)} — full payload in contentParts]`,\n );\n } else {\n outputSegments.push(`Resource ${part.resource.uri}`);\n }\n break;\n }\n case 'resource_link':\n // Joined below via the read_result preview.\n break;\n }\n }\n const concatenatedText = [...outputSegments, ...resourceLinkPreviews].join('\\n');\n\n if (result.structuredContent !== undefined) {\n if (outputSchema !== undefined) {\n const validation = outputSchema.safeParse(result.structuredContent);\n if (validation.success) {\n incrementCounter('mcp.structured-content.emitted.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n const finalParts = [...contentParts];\n if (textParts.length === 0) {\n finalParts.push({ type: 'text', text: JSON.stringify(result.structuredContent) });\n }\n return Object.freeze({\n output: validation.data,\n contentParts: Object.freeze(finalParts),\n });\n }\n incrementCounter('mcp.structured-content.validation.failure.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n // MC-11: a schema mismatch must not silently drop the payload —\n // log and mirror the structured content as JSON text, exactly\n // like the no-schema branch does.\n args.logger?.(\n 'warn',\n 'mcp.structured-content.validation.failed: payload mirrored as JSON text',\n { server: serverIdentity.id, tool: toolName },\n );\n const fallbackParts = [...contentParts];\n if (textParts.length === 0) {\n fallbackParts.push({ type: 'text', text: JSON.stringify(result.structuredContent) });\n }\n return Object.freeze({\n output:\n concatenatedText.length > 0 ? concatenatedText : JSON.stringify(result.structuredContent),\n contentParts: Object.freeze(fallbackParts),\n });\n } else {\n incrementCounter('mcp.structured-content.emitted.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n const finalParts = [...contentParts];\n if (textParts.length === 0) {\n finalParts.push({ type: 'text', text: JSON.stringify(result.structuredContent) });\n }\n return Object.freeze({\n output: result.structuredContent,\n contentParts: Object.freeze(finalParts),\n });\n }\n }\n\n return Object.freeze({\n output: concatenatedText,\n contentParts: Object.freeze(contentParts),\n });\n}\n\nfunction mcpContentToMessageContent(part: MCPContentPart): MessageContent | undefined {\n switch (part.type) {\n case 'text':\n return { type: 'text', text: part.text };\n case 'image':\n return {\n type: 'image',\n image: decodeBase64(part.data),\n mimeType: part.mimeType,\n };\n case 'audio':\n return {\n type: 'audio',\n audio: decodeBase64(part.data),\n mimeType: part.mimeType,\n };\n case 'resource': {\n const text = part.resource.text;\n if (text !== undefined) {\n return { type: 'text', text };\n }\n const blob = part.resource.blob;\n if (blob !== undefined) {\n return {\n type: 'file',\n file: decodeBase64(blob),\n mimeType: part.resource.mimeType ?? 'application/octet-stream',\n };\n }\n return { type: 'text', text: `Resource ${part.resource.uri}` };\n }\n case 'resource_link':\n return { type: 'text', text: formatResourceLinkPreview(part) };\n }\n}\n\n/**\n * Render the compact, model-facing preview for a `resource_link`. The\n * `uri` doubles as the `read_result` handle — an\n * {@link import('./mcp-resource-reader.js').createMcpResourceReader}\n * resolves it on demand via {@link MCPClient.readResource}.\n */\nfunction formatResourceLinkPreview(\n part: Extract<MCPContentPart, { type: 'resource_link' }>,\n): string {\n const label = part.title === undefined || part.title.length === 0 ? part.name : part.title;\n const meta: string[] = [];\n if (part.mimeType !== undefined) meta.push(part.mimeType);\n if (part.size !== undefined) meta.push(`${part.size} bytes`);\n const metaStr = meta.length === 0 ? '' : ` (${meta.join(', ')})`;\n const desc =\n part.description === undefined || part.description.length === 0 ? '' : ` — ${part.description}`;\n return `[resource_link] ${label}${metaStr}${desc}\\nFetch the full contents on demand with read_result, handle: ${part.uri}`;\n}\n\n/** Human-readable size of a base64 payload's decoded bytes (MC-8). */\nfunction approxDecodedSize(base64: string): string {\n const bytes = Math.floor((base64.length * 3) / 4);\n return bytes >= 1024 ? `${Math.round(bytes / 1024)}kB` : `${bytes}B`;\n}\n\n/** Text descriptor for a non-text content part (MC-8). */\nfunction describeBinaryPart(kind: 'image' | 'audio', mimeType: string, data: string): string {\n return `[${kind} ${mimeType}, ~${approxDecodedSize(data)} — full payload in contentParts]`;\n}\n\nfunction decodeBase64(value: string): Uint8Array {\n return Uint8Array.from(Buffer.from(value, 'base64'));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"adapt-result.js","names":["contentParts: MessageContent[]","resourceLinkPreviews: string[]","outputSegments: string[]","meta: string[]"],"sources":["../../src/client/adapt-result.ts"],"sourcesContent":["/**\n * Result adaptation for the `toTools()` adapter (extracted from\n * `to-tools.ts` per F-MCP-001).\n *\n * Converts an MCP `CallToolResult` into a typed Graphorin `ToolReturn`,\n * handling the structured-content + outputSchema round-trip and the\n * backward-compatible TextContent mirror, and mapping each MCP content\n * part onto a Graphorin {@link MessageContent}.\n *\n * @packageDocumentation\n */\n\nimport type { MessageContent, ToolReturn, ZodLikeSchema } from '@graphorin/core';\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport { MCPToolExecutionError } from '../errors/index.js';\nimport type { ServerIdentity } from '../transport/types.js';\nimport type { MCPCallToolResult, MCPContentPart } from './types.js';\n\n/** Arguments for {@link adaptCallResult}. */\nexport interface AdaptCallResultArgs {\n readonly result: MCPCallToolResult;\n readonly outputSchema?: ZodLikeSchema<unknown> | undefined;\n readonly serverIdentity: ServerIdentity;\n readonly toolName: string;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n}\n\n/**\n * Convert an MCP `CallToolResult` into a typed Graphorin `ToolReturn`,\n * handling the structured-content + outputSchema round-trip and the\n * backward-compatible TextContent mirror.\n *\n * @internal\n */\nexport function adaptCallResult(args: AdaptCallResultArgs): ToolReturn<unknown> {\n const { result, outputSchema, serverIdentity, toolName } = args;\n // MC-4: the SDK deliberately does NOT throw on isError results - the\n // failure marker must not launder into a successful ToolReturn. Throw\n // the typed error so the executor records a real failure; the server's\n // text rides in the message for model self-correction.\n if (result.isError === true) {\n const errorText = (result.content ?? [])\n .filter((p): p is { type: 'text'; text: string } => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n incrementCounter('mcp.call.tool-error.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n throw new MCPToolExecutionError(\n errorText.length > 0 ? errorText : `MCP tool '${toolName}' reported an error result.`,\n { metadata: { tool: toolName } },\n );\n }\n const contentParts: MessageContent[] = [];\n // `resource_link` parts are NOT inlined: each contributes a compact\n // preview (carrying the `uri` as a result handle) so the model fetches\n // the body on demand via `read_result` instead of inflating context.\n const resourceLinkPreviews: string[] = [];\n for (const part of result.content) {\n if (part.type === 'resource_link') {\n incrementCounter('mcp.resource-link.emitted.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n resourceLinkPreviews.push(formatResourceLinkPreview(part, serverIdentity.id));\n }\n const messagePart = mcpContentToMessageContent(part, serverIdentity.id);\n if (messagePart !== undefined) contentParts.push(messagePart);\n }\n const textParts = (result.content ?? []).filter(\n (p): p is { type: 'text'; text: string } => p.type === 'text',\n );\n // MC-8: the typed `output` is the ONLY channel the agent loop\n // serialises into the tool message - non-text parts must leave a\n // text trace there (the full payloads stay in `contentParts`), and\n // embedded resource TEXT joins the concatenation outright.\n const outputSegments: string[] = [];\n for (const part of result.content ?? []) {\n switch (part.type) {\n case 'text':\n outputSegments.push(part.text);\n break;\n case 'image':\n case 'audio':\n outputSegments.push(describeBinaryPart(part.type, part.mimeType, part.data));\n break;\n case 'resource': {\n if (part.resource.text !== undefined) {\n outputSegments.push(part.resource.text);\n } else if (part.resource.blob !== undefined) {\n outputSegments.push(\n `[resource ${part.resource.uri} ${part.resource.mimeType ?? 'application/octet-stream'}, ~${approxDecodedSize(part.resource.blob)} - full payload in contentParts]`,\n );\n } else {\n outputSegments.push(`Resource ${part.resource.uri}`);\n }\n break;\n }\n case 'resource_link':\n // Joined below via the read_result preview.\n break;\n }\n }\n const concatenatedText = [...outputSegments, ...resourceLinkPreviews].join('\\n');\n\n if (result.structuredContent !== undefined) {\n if (outputSchema !== undefined) {\n const validation = outputSchema.safeParse(result.structuredContent);\n if (validation.success) {\n incrementCounter('mcp.structured-content.emitted.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n const finalParts = [...contentParts];\n if (textParts.length === 0) {\n finalParts.push({ type: 'text', text: JSON.stringify(result.structuredContent) });\n }\n return Object.freeze({\n output: validation.data,\n contentParts: Object.freeze(finalParts),\n });\n }\n incrementCounter('mcp.structured-content.validation.failure.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n // MC-11: a schema mismatch must not silently drop the payload -\n // log and mirror the structured content as JSON text, exactly\n // like the no-schema branch does.\n args.logger?.(\n 'warn',\n 'mcp.structured-content.validation.failed: payload mirrored as JSON text',\n { server: serverIdentity.id, tool: toolName },\n );\n const fallbackParts = [...contentParts];\n if (textParts.length === 0) {\n fallbackParts.push({ type: 'text', text: JSON.stringify(result.structuredContent) });\n }\n return Object.freeze({\n output:\n concatenatedText.length > 0 ? concatenatedText : JSON.stringify(result.structuredContent),\n contentParts: Object.freeze(fallbackParts),\n });\n } else {\n incrementCounter('mcp.structured-content.emitted.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n const finalParts = [...contentParts];\n if (textParts.length === 0) {\n finalParts.push({ type: 'text', text: JSON.stringify(result.structuredContent) });\n }\n return Object.freeze({\n output: result.structuredContent,\n contentParts: Object.freeze(finalParts),\n });\n }\n }\n\n return Object.freeze({\n output: concatenatedText,\n contentParts: Object.freeze(contentParts),\n });\n}\n\nfunction mcpContentToMessageContent(\n part: MCPContentPart,\n serverId: string,\n): MessageContent | undefined {\n switch (part.type) {\n case 'text':\n return { type: 'text', text: part.text };\n case 'image':\n return {\n type: 'image',\n image: decodeBase64(part.data),\n mimeType: part.mimeType,\n };\n case 'audio':\n return {\n type: 'audio',\n audio: decodeBase64(part.data),\n mimeType: part.mimeType,\n };\n case 'resource': {\n const text = part.resource.text;\n if (text !== undefined) {\n return { type: 'text', text };\n }\n const blob = part.resource.blob;\n if (blob !== undefined) {\n return {\n type: 'file',\n file: decodeBase64(blob),\n mimeType: part.resource.mimeType ?? 'application/octet-stream',\n };\n }\n return { type: 'text', text: `Resource ${part.resource.uri}` };\n }\n case 'resource_link':\n return { type: 'text', text: formatResourceLinkPreview(part, serverId) };\n }\n}\n\n/**\n * Render the compact, model-facing preview for a `resource_link`. The\n * handle is SCOPED to the originating server (mcp-skills-06):\n * `mcp:<serverId>:<uri>` - an\n * {@link import('./mcp-resource-reader.js').createMcpResourceReader}\n * parses the prefix and consults ONLY that server, so a malicious\n * server's link (or a prompt-injected model) cannot fetch a resource\n * from a different, more-trusted server through the handle.\n */\nfunction formatResourceLinkPreview(\n part: Extract<MCPContentPart, { type: 'resource_link' }>,\n serverId: string,\n): string {\n const label = part.title === undefined || part.title.length === 0 ? part.name : part.title;\n const meta: string[] = [];\n if (part.mimeType !== undefined) meta.push(part.mimeType);\n if (part.size !== undefined) meta.push(`${part.size} bytes`);\n const metaStr = meta.length === 0 ? '' : ` (${meta.join(', ')})`;\n const desc =\n part.description === undefined || part.description.length === 0 ? '' : ` - ${part.description}`;\n return `[resource_link] ${label}${metaStr}${desc}\\nFetch the full contents on demand with read_result, handle: ${scopedResourceHandle(serverId, part.uri)}`;\n}\n\n/**\n * Build the server-scoped `read_result` handle for an MCP resource\n * (mcp-skills-06): `mcp:<serverId>:<uri>`.\n *\n * @stable\n */\nexport function scopedResourceHandle(serverId: string, uri: string): string {\n return `mcp:${serverId}:${uri}`;\n}\n\n/** Human-readable size of a base64 payload's decoded bytes (MC-8). */\nfunction approxDecodedSize(base64: string): string {\n const bytes = Math.floor((base64.length * 3) / 4);\n return bytes >= 1024 ? `${Math.round(bytes / 1024)}kB` : `${bytes}B`;\n}\n\n/** Text descriptor for a non-text content part (MC-8). */\nfunction describeBinaryPart(kind: 'image' | 'audio', mimeType: string, data: string): string {\n return `[${kind} ${mimeType}, ~${approxDecodedSize(data)} - full payload in contentParts]`;\n}\n\nfunction decodeBase64(value: string): Uint8Array {\n return Uint8Array.from(Buffer.from(value, 'base64'));\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { incrementCounter } from "@graphorin/tools/audit";
|
|
2
|
-
import { CreateMessageRequestSchema, ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { CreateMessageRequestSchema, ElicitRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
|
|
4
4
|
//#region src/client/client-handlers.ts
|
|
5
5
|
/**
|
|
@@ -11,7 +11,7 @@ import { CreateMessageRequestSchema, ElicitRequestSchema } from "@modelcontextpr
|
|
|
11
11
|
* handler *only* when the operator supplies the matching callback on
|
|
12
12
|
* {@link CreateMCPClientOptions}. A conforming server will not issue a
|
|
13
13
|
* request for an un-advertised capability, so the default client is inert
|
|
14
|
-
* (no implicit prompting, no implicit model calls
|
|
14
|
+
* (no implicit prompting, no implicit model calls - R4).
|
|
15
15
|
*
|
|
16
16
|
* The SDK request/result schemas are kept inside this module; the public
|
|
17
17
|
* surface speaks only the Graphorin-typed {@link MCPElicitationRequest} /
|
|
@@ -44,6 +44,13 @@ function registerClientRequestHandlers(sdkClient, opts) {
|
|
|
44
44
|
sdkClient.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
|
|
45
45
|
incrementCounter("mcp.elicitation.requested.total", { server: serverIdRef.current });
|
|
46
46
|
const params = request.params;
|
|
47
|
+
if (params.mode === "url") {
|
|
48
|
+
incrementCounter("mcp.elicitation.declined.total", {
|
|
49
|
+
server: serverIdRef.current,
|
|
50
|
+
action: "decline"
|
|
51
|
+
});
|
|
52
|
+
throw new McpError(ErrorCode.InvalidRequest, "URL-mode elicitation is not supported by this client (only 'form' is advertised).");
|
|
53
|
+
}
|
|
47
54
|
const requestedSchema = "requestedSchema" in params && params.requestedSchema !== void 0 ? params.requestedSchema : {
|
|
48
55
|
type: "object",
|
|
49
56
|
properties: {}
|
|
@@ -71,6 +78,8 @@ function registerClientRequestHandlers(sdkClient, opts) {
|
|
|
71
78
|
sdkClient.setRequestHandler(CreateMessageRequestSchema, async (request, extra) => {
|
|
72
79
|
incrementCounter("mcp.sampling.requested.total", { server: serverIdRef.current });
|
|
73
80
|
const p = request.params;
|
|
81
|
+
const withTools = p;
|
|
82
|
+
if (withTools.tools !== void 0 || withTools.toolChoice !== void 0) throw new McpError(ErrorCode.InvalidRequest, "sampling with tools is not supported by this client (ClientCapabilities.sampling.tools is not declared).");
|
|
74
83
|
const mp = p.modelPreferences;
|
|
75
84
|
const result = await handler({
|
|
76
85
|
messages: p.messages.map((m) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-handlers.js","names":[],"sources":["../../src/client/client-handlers.ts"],"sourcesContent":["/**\n * Client-side request handlers for server-initiated MCP requests\n * (WI-13 / P2-2): **elicitation** (`elicitation/create`) and **sampling**\n * (`sampling/createMessage`).\n *\n * Both are gated: the client advertises a capability and registers a\n * handler *only* when the operator supplies the matching callback on\n * {@link CreateMCPClientOptions}. A conforming server will not issue a\n * request for an un-advertised capability, so the default client is inert\n * (no implicit prompting, no implicit model calls — R4).\n *\n * The SDK request/result schemas are kept inside this module; the public\n * surface speaks only the Graphorin-typed {@link MCPElicitationRequest} /\n * {@link MCPSamplingRequest} boundary.\n *\n * @packageDocumentation\n */\n\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport {\n type ClientCapabilities,\n CreateMessageRequestSchema,\n ElicitRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js';\nimport type {\n MCPElicitationHandler,\n MCPSamplingContent,\n MCPSamplingHandler,\n MCPSamplingRequest,\n} from './types.js';\n\n/** Mutable holder so handlers can label counters with the server id. */\nexport interface ServerIdRef {\n current: string;\n}\n\n/** Options for {@link registerClientRequestHandlers}. */\nexport interface ClientRequestHandlerOptions {\n readonly elicitation?: MCPElicitationHandler;\n readonly sampling?: MCPSamplingHandler;\n readonly serverIdRef: ServerIdRef;\n}\n\n/**\n * Compute the {@link ClientCapabilities} to advertise on `initialize`,\n * based on which server-initiated handlers the operator supplied.\n * Returns `undefined` when none are configured (advertise nothing).\n */\nexport function computeClientCapabilities(opts: {\n readonly elicitation?: unknown;\n readonly sampling?: unknown;\n}): ClientCapabilities | undefined {\n if (opts.elicitation === undefined && opts.sampling === undefined) return undefined;\n return {\n ...(opts.elicitation === undefined ? {} : { elicitation: {} }),\n ...(opts.sampling === undefined ? {} : { sampling: {} }),\n };\n}\n\n/**\n * Register the elicitation / sampling request handlers on `sdkClient`,\n * one per supplied callback. Must be called **before** `connect(...)` so\n * the handlers are in place when the session begins delivering\n * server-initiated requests.\n */\nexport function registerClientRequestHandlers(\n sdkClient: Client,\n opts: ClientRequestHandlerOptions,\n): void {\n const { serverIdRef } = opts;\n\n if (opts.elicitation !== undefined) {\n const handler = opts.elicitation;\n sdkClient.setRequestHandler(ElicitRequestSchema, async (request, extra) => {\n incrementCounter('mcp.elicitation.requested.total', { server: serverIdRef.current });\n const params = request.params;\n const requestedSchema =\n 'requestedSchema' in params && params.requestedSchema !== undefined\n ? (params.requestedSchema as Readonly<Record<string, unknown>>)\n : { type: 'object', properties: {} };\n const result = await handler(\n { message: params.message, requestedSchema },\n { signal: extra.signal },\n );\n if (result.action === 'accept') {\n incrementCounter('mcp.elicitation.accepted.total', { server: serverIdRef.current });\n return result.content === undefined\n ? { action: 'accept' }\n : { action: 'accept', content: result.content };\n }\n incrementCounter('mcp.elicitation.declined.total', {\n server: serverIdRef.current,\n action: result.action,\n });\n return { action: result.action };\n });\n }\n\n if (opts.sampling !== undefined) {\n const handler = opts.sampling;\n sdkClient.setRequestHandler(CreateMessageRequestSchema, async (request, extra) => {\n incrementCounter('mcp.sampling.requested.total', { server: serverIdRef.current });\n const p = request.params;\n const mp = p.modelPreferences;\n const samplingRequest: MCPSamplingRequest = {\n messages: p.messages.map((m) => ({\n role: m.role,\n // MC-13: keep EVERY block — a text+image message must reach the\n // operator's handler whole, not truncated to its first block.\n content: contentBlocks(m.content).map((block) => toGraphorinContent(block)),\n })),\n maxTokens: p.maxTokens,\n ...(p.systemPrompt === undefined ? {} : { systemPrompt: p.systemPrompt }),\n ...(p.temperature === undefined ? {} : { temperature: p.temperature }),\n ...(p.stopSequences === undefined ? {} : { stopSequences: p.stopSequences }),\n ...(mp === undefined\n ? {}\n : {\n modelPreferences: {\n ...(mp.hints === undefined\n ? {}\n : { hints: mp.hints.map((h) => (h.name === undefined ? {} : { name: h.name })) }),\n ...(mp.costPriority === undefined ? {} : { costPriority: mp.costPriority }),\n ...(mp.speedPriority === undefined ? {} : { speedPriority: mp.speedPriority }),\n ...(mp.intelligencePriority === undefined\n ? {}\n : { intelligencePriority: mp.intelligencePriority }),\n },\n }),\n ...(p.includeContext === undefined ? {} : { includeContext: p.includeContext }),\n };\n const result = await handler(samplingRequest, { signal: extra.signal });\n incrementCounter('mcp.sampling.completed.total', { server: serverIdRef.current });\n return {\n role: result.role,\n content: result.content,\n model: result.model,\n ...(result.stopReason === undefined ? {} : { stopReason: result.stopReason }),\n };\n });\n }\n}\n\n/** Normalise SDK message content (block | block[]) to a block array (MC-13). */\nfunction contentBlocks(\n content: unknown,\n): ReadonlyArray<{ readonly type?: unknown; [k: string]: unknown }> {\n const raw = Array.isArray(content) ? content : [content];\n const blocks = raw.filter(\n (b): b is { readonly type?: unknown; [k: string]: unknown } =>\n b !== null && typeof b === 'object',\n );\n return blocks.length > 0 ? blocks : [{ type: 'text', text: '' }];\n}\n\n/** Map an SDK content block to the Graphorin sampling-content union. */\nfunction toGraphorinContent(block: {\n readonly type?: unknown;\n [k: string]: unknown;\n}): MCPSamplingContent {\n if (block.type === 'image') {\n return {\n type: 'image',\n data: String(block.data ?? ''),\n mimeType: String(block.mimeType ?? 'application/octet-stream'),\n };\n }\n if (block.type === 'audio') {\n return {\n type: 'audio',\n data: String(block.data ?? ''),\n mimeType: String(block.mimeType ?? 'application/octet-stream'),\n };\n }\n if (block.type === 'text') {\n return { type: 'text', text: String(block.text ?? '') };\n }\n return { type: 'text', text: JSON.stringify(block) };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client-handlers.js","names":[],"sources":["../../src/client/client-handlers.ts"],"sourcesContent":["/**\n * Client-side request handlers for server-initiated MCP requests\n * (WI-13 / P2-2): **elicitation** (`elicitation/create`) and **sampling**\n * (`sampling/createMessage`).\n *\n * Both are gated: the client advertises a capability and registers a\n * handler *only* when the operator supplies the matching callback on\n * {@link CreateMCPClientOptions}. A conforming server will not issue a\n * request for an un-advertised capability, so the default client is inert\n * (no implicit prompting, no implicit model calls - R4).\n *\n * The SDK request/result schemas are kept inside this module; the public\n * surface speaks only the Graphorin-typed {@link MCPElicitationRequest} /\n * {@link MCPSamplingRequest} boundary.\n *\n * @packageDocumentation\n */\n\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport {\n type ClientCapabilities,\n CreateMessageRequestSchema,\n ElicitRequestSchema,\n ErrorCode,\n McpError,\n} from '@modelcontextprotocol/sdk/types.js';\nimport type {\n MCPElicitationHandler,\n MCPSamplingContent,\n MCPSamplingHandler,\n MCPSamplingRequest,\n} from './types.js';\n\n/** Mutable holder so handlers can label counters with the server id. */\nexport interface ServerIdRef {\n current: string;\n}\n\n/** Options for {@link registerClientRequestHandlers}. */\nexport interface ClientRequestHandlerOptions {\n readonly elicitation?: MCPElicitationHandler;\n readonly sampling?: MCPSamplingHandler;\n readonly serverIdRef: ServerIdRef;\n}\n\n/**\n * Compute the {@link ClientCapabilities} to advertise on `initialize`,\n * based on which server-initiated handlers the operator supplied.\n * Returns `undefined` when none are configured (advertise nothing).\n */\nexport function computeClientCapabilities(opts: {\n readonly elicitation?: unknown;\n readonly sampling?: unknown;\n}): ClientCapabilities | undefined {\n if (opts.elicitation === undefined && opts.sampling === undefined) return undefined;\n return {\n ...(opts.elicitation === undefined ? {} : { elicitation: {} }),\n ...(opts.sampling === undefined ? {} : { sampling: {} }),\n };\n}\n\n/**\n * Register the elicitation / sampling request handlers on `sdkClient`,\n * one per supplied callback. Must be called **before** `connect(...)` so\n * the handlers are in place when the session begins delivering\n * server-initiated requests.\n */\nexport function registerClientRequestHandlers(\n sdkClient: Client,\n opts: ClientRequestHandlerOptions,\n): void {\n const { serverIdRef } = opts;\n\n if (opts.elicitation !== undefined) {\n const handler = opts.elicitation;\n sdkClient.setRequestHandler(ElicitRequestSchema, async (request, extra) => {\n incrementCounter('mcp.elicitation.requested.total', { server: serverIdRef.current });\n const params = request.params;\n // mcp-skills-05 (SEP-1036): URL-mode elicitation carries a URL,\n // not a form schema. The old handler surfaced it as an\n // empty-schema FORM with the URL invisible - decline it honestly\n // until URL elicitation is supported. (We advertise the `form`\n // sub-capability only, so a conforming server never sends this.)\n if ((params as { readonly mode?: unknown }).mode === 'url') {\n incrementCounter('mcp.elicitation.declined.total', {\n server: serverIdRef.current,\n action: 'decline',\n });\n throw new McpError(\n ErrorCode.InvalidRequest,\n \"URL-mode elicitation is not supported by this client (only 'form' is advertised).\",\n );\n }\n const requestedSchema =\n 'requestedSchema' in params && params.requestedSchema !== undefined\n ? (params.requestedSchema as Readonly<Record<string, unknown>>)\n : { type: 'object', properties: {} };\n const result = await handler(\n { message: params.message, requestedSchema },\n { signal: extra.signal },\n );\n if (result.action === 'accept') {\n incrementCounter('mcp.elicitation.accepted.total', { server: serverIdRef.current });\n return result.content === undefined\n ? { action: 'accept' }\n : { action: 'accept', content: result.content };\n }\n incrementCounter('mcp.elicitation.declined.total', {\n server: serverIdRef.current,\n action: result.action,\n });\n return { action: result.action };\n });\n }\n\n if (opts.sampling !== undefined) {\n const handler = opts.sampling;\n sdkClient.setRequestHandler(CreateMessageRequestSchema, async (request, extra) => {\n incrementCounter('mcp.sampling.requested.total', { server: serverIdRef.current });\n const p = request.params;\n // mcp-skills-05: protocol 2025-11-25 - \"The client MUST return an\n // error if this field is provided but\n // ClientCapabilities.sampling.tools is not declared\". We do not\n // declare it; pre-fix a tools-carrying request was silently\n // answered as a plain completion.\n const withTools = p as { readonly tools?: unknown; readonly toolChoice?: unknown };\n if (withTools.tools !== undefined || withTools.toolChoice !== undefined) {\n throw new McpError(\n ErrorCode.InvalidRequest,\n 'sampling with tools is not supported by this client (ClientCapabilities.sampling.tools is not declared).',\n );\n }\n const mp = p.modelPreferences;\n const samplingRequest: MCPSamplingRequest = {\n messages: p.messages.map((m) => ({\n role: m.role,\n // MC-13: keep EVERY block - a text+image message must reach the\n // operator's handler whole, not truncated to its first block.\n content: contentBlocks(m.content).map((block) => toGraphorinContent(block)),\n })),\n maxTokens: p.maxTokens,\n ...(p.systemPrompt === undefined ? {} : { systemPrompt: p.systemPrompt }),\n ...(p.temperature === undefined ? {} : { temperature: p.temperature }),\n ...(p.stopSequences === undefined ? {} : { stopSequences: p.stopSequences }),\n ...(mp === undefined\n ? {}\n : {\n modelPreferences: {\n ...(mp.hints === undefined\n ? {}\n : { hints: mp.hints.map((h) => (h.name === undefined ? {} : { name: h.name })) }),\n ...(mp.costPriority === undefined ? {} : { costPriority: mp.costPriority }),\n ...(mp.speedPriority === undefined ? {} : { speedPriority: mp.speedPriority }),\n ...(mp.intelligencePriority === undefined\n ? {}\n : { intelligencePriority: mp.intelligencePriority }),\n },\n }),\n ...(p.includeContext === undefined ? {} : { includeContext: p.includeContext }),\n };\n const result = await handler(samplingRequest, { signal: extra.signal });\n incrementCounter('mcp.sampling.completed.total', { server: serverIdRef.current });\n return {\n role: result.role,\n content: result.content,\n model: result.model,\n ...(result.stopReason === undefined ? {} : { stopReason: result.stopReason }),\n };\n });\n }\n}\n\n/** Normalise SDK message content (block | block[]) to a block array (MC-13). */\nfunction contentBlocks(\n content: unknown,\n): ReadonlyArray<{ readonly type?: unknown; [k: string]: unknown }> {\n const raw = Array.isArray(content) ? content : [content];\n const blocks = raw.filter(\n (b): b is { readonly type?: unknown; [k: string]: unknown } =>\n b !== null && typeof b === 'object',\n );\n return blocks.length > 0 ? blocks : [{ type: 'text', text: '' }];\n}\n\n/** Map an SDK content block to the Graphorin sampling-content union. */\nfunction toGraphorinContent(block: {\n readonly type?: unknown;\n [k: string]: unknown;\n}): MCPSamplingContent {\n if (block.type === 'image') {\n return {\n type: 'image',\n data: String(block.data ?? ''),\n mimeType: String(block.mimeType ?? 'application/octet-stream'),\n };\n }\n if (block.type === 'audio') {\n return {\n type: 'audio',\n data: String(block.data ?? ''),\n mimeType: String(block.mimeType ?? 'application/octet-stream'),\n };\n }\n if (block.type === 'text') {\n return { type: 'text', text: String(block.text ?? '') };\n }\n return { type: 'text', text: JSON.stringify(block) };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,0BAA0B,MAGP;AACjC,KAAI,KAAK,gBAAgB,UAAa,KAAK,aAAa,OAAW,QAAO;AAC1E,QAAO;EACL,GAAI,KAAK,gBAAgB,SAAY,EAAE,GAAG,EAAE,aAAa,EAAE,EAAE;EAC7D,GAAI,KAAK,aAAa,SAAY,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE;EACxD;;;;;;;;AASH,SAAgB,8BACd,WACA,MACM;CACN,MAAM,EAAE,gBAAgB;AAExB,KAAI,KAAK,gBAAgB,QAAW;EAClC,MAAM,UAAU,KAAK;AACrB,YAAU,kBAAkB,qBAAqB,OAAO,SAAS,UAAU;AACzE,oBAAiB,mCAAmC,EAAE,QAAQ,YAAY,SAAS,CAAC;GACpF,MAAM,SAAS,QAAQ;AAMvB,OAAK,OAAuC,SAAS,OAAO;AAC1D,qBAAiB,kCAAkC;KACjD,QAAQ,YAAY;KACpB,QAAQ;KACT,CAAC;AACF,UAAM,IAAI,SACR,UAAU,gBACV,oFACD;;GAEH,MAAM,kBACJ,qBAAqB,UAAU,OAAO,oBAAoB,SACrD,OAAO,kBACR;IAAE,MAAM;IAAU,YAAY,EAAE;IAAE;GACxC,MAAM,SAAS,MAAM,QACnB;IAAE,SAAS,OAAO;IAAS;IAAiB,EAC5C,EAAE,QAAQ,MAAM,QAAQ,CACzB;AACD,OAAI,OAAO,WAAW,UAAU;AAC9B,qBAAiB,kCAAkC,EAAE,QAAQ,YAAY,SAAS,CAAC;AACnF,WAAO,OAAO,YAAY,SACtB,EAAE,QAAQ,UAAU,GACpB;KAAE,QAAQ;KAAU,SAAS,OAAO;KAAS;;AAEnD,oBAAiB,kCAAkC;IACjD,QAAQ,YAAY;IACpB,QAAQ,OAAO;IAChB,CAAC;AACF,UAAO,EAAE,QAAQ,OAAO,QAAQ;IAChC;;AAGJ,KAAI,KAAK,aAAa,QAAW;EAC/B,MAAM,UAAU,KAAK;AACrB,YAAU,kBAAkB,4BAA4B,OAAO,SAAS,UAAU;AAChF,oBAAiB,gCAAgC,EAAE,QAAQ,YAAY,SAAS,CAAC;GACjF,MAAM,IAAI,QAAQ;GAMlB,MAAM,YAAY;AAClB,OAAI,UAAU,UAAU,UAAa,UAAU,eAAe,OAC5D,OAAM,IAAI,SACR,UAAU,gBACV,2GACD;GAEH,MAAM,KAAK,EAAE;GA4Bb,MAAM,SAAS,MAAM,QA3BuB;IAC1C,UAAU,EAAE,SAAS,KAAK,OAAO;KAC/B,MAAM,EAAE;KAGR,SAAS,cAAc,EAAE,QAAQ,CAAC,KAAK,UAAU,mBAAmB,MAAM,CAAC;KAC5E,EAAE;IACH,WAAW,EAAE;IACb,GAAI,EAAE,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc,EAAE,cAAc;IACxE,GAAI,EAAE,gBAAgB,SAAY,EAAE,GAAG,EAAE,aAAa,EAAE,aAAa;IACrE,GAAI,EAAE,kBAAkB,SAAY,EAAE,GAAG,EAAE,eAAe,EAAE,eAAe;IAC3E,GAAI,OAAO,SACP,EAAE,GACF,EACE,kBAAkB;KAChB,GAAI,GAAG,UAAU,SACb,EAAE,GACF,EAAE,OAAO,GAAG,MAAM,KAAK,MAAO,EAAE,SAAS,SAAY,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAE,EAAE;KAClF,GAAI,GAAG,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc,GAAG,cAAc;KAC1E,GAAI,GAAG,kBAAkB,SAAY,EAAE,GAAG,EAAE,eAAe,GAAG,eAAe;KAC7E,GAAI,GAAG,yBAAyB,SAC5B,EAAE,GACF,EAAE,sBAAsB,GAAG,sBAAsB;KACtD,EACF;IACL,GAAI,EAAE,mBAAmB,SAAY,EAAE,GAAG,EAAE,gBAAgB,EAAE,gBAAgB;IAC/E,EAC6C,EAAE,QAAQ,MAAM,QAAQ,CAAC;AACvE,oBAAiB,gCAAgC,EAAE,QAAQ,YAAY,SAAS,CAAC;AACjF,UAAO;IACL,MAAM,OAAO;IACb,SAAS,OAAO;IAChB,OAAO,OAAO;IACd,GAAI,OAAO,eAAe,SAAY,EAAE,GAAG,EAAE,YAAY,OAAO,YAAY;IAC7E;IACD;;;;AAKN,SAAS,cACP,SACkE;CAElE,MAAM,UADM,MAAM,QAAQ,QAAQ,GAAG,UAAU,CAAC,QAAQ,EACrC,QAChB,MACC,MAAM,QAAQ,OAAO,MAAM,SAC9B;AACD,QAAO,OAAO,SAAS,IAAI,SAAS,CAAC;EAAE,MAAM;EAAQ,MAAM;EAAI,CAAC;;;AAIlE,SAAS,mBAAmB,OAGL;AACrB,KAAI,MAAM,SAAS,QACjB,QAAO;EACL,MAAM;EACN,MAAM,OAAO,MAAM,QAAQ,GAAG;EAC9B,UAAU,OAAO,MAAM,YAAY,2BAA2B;EAC/D;AAEH,KAAI,MAAM,SAAS,QACjB,QAAO;EACL,MAAM;EACN,MAAM,OAAO,MAAM,QAAQ,GAAG;EAC9B,UAAU,OAAO,MAAM,YAAY,2BAA2B;EAC/D;AAEH,KAAI,MAAM,SAAS,OACjB,QAAO;EAAE,MAAM;EAAQ,MAAM,OAAO,MAAM,QAAQ,GAAG;EAAE;AAEzD,QAAO;EAAE,MAAM;EAAQ,MAAM,KAAK,UAAU,MAAM;EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":[],"mappings":";;;;;;;AAuEA;AASA;;;AAAwE,iBATxD,4BAAA,CAAA,CASwD,EAAA,IAAA;;;;;;iBAAlD,eAAA,UAAyB,yBAAyB,QAAQ"}
|
package/dist/client/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { version } from "../package.js";
|
|
1
2
|
import { MCPCallTimeoutError, MCPCancelledError, MCPConnectionError, MCPInvalidConfigError, MCPProtocolError, MCPToolNotFoundError, MCPToolPinningError } from "../errors/index.js";
|
|
2
3
|
import { deriveServerIdentity } from "../helpers/identity.js";
|
|
3
4
|
import { validateMCPServerConfig } from "../helpers/validate-config.js";
|
|
@@ -10,7 +11,7 @@ import { ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/typ
|
|
|
10
11
|
|
|
11
12
|
//#region src/client/client.ts
|
|
12
13
|
const DEFAULT_CLIENT_NAME = "graphorin-mcp-client";
|
|
13
|
-
const DEFAULT_CLIENT_VERSION =
|
|
14
|
+
const DEFAULT_CLIENT_VERSION = version;
|
|
14
15
|
/**
|
|
15
16
|
* Process-scoped dedup flag for the deprecated SSE transport WARN.
|
|
16
17
|
* Once set, subsequent {@link createMCPClient} calls with the SSE
|
|
@@ -51,7 +52,9 @@ async function createMCPClient(options) {
|
|
|
51
52
|
...options.clientName === void 0 ? {} : { clientName: options.clientName },
|
|
52
53
|
...options.clientVersion === void 0 ? {} : { clientVersion: options.clientVersion },
|
|
53
54
|
...options.elicitation === void 0 ? {} : { elicitation: options.elicitation },
|
|
54
|
-
...options.sampling === void 0 ? {} : { sampling: options.sampling }
|
|
55
|
+
...options.sampling === void 0 ? {} : { sampling: options.sampling },
|
|
56
|
+
...options.onTransportClose === void 0 ? {} : { onTransportClose: options.onTransportClose },
|
|
57
|
+
...options.onTransportError === void 0 ? {} : { onTransportError: options.onTransportError }
|
|
55
58
|
});
|
|
56
59
|
}
|
|
57
60
|
/**
|
|
@@ -79,7 +82,7 @@ async function createMCPClientFromSdkTransport(options) {
|
|
|
79
82
|
});
|
|
80
83
|
sdkClient.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
|
|
81
84
|
incrementCounter("mcp.tools.list-changed.total", { server: serverIdRef.current ?? "unknown" });
|
|
82
|
-
options.logger?.("warn", "mcp.tools.list_changed received
|
|
85
|
+
options.logger?.("warn", "mcp.tools.list_changed received - re-run toTools() to refresh + re-sanitize the catalogue", { server: serverIdRef.current });
|
|
83
86
|
});
|
|
84
87
|
try {
|
|
85
88
|
await sdkClient.connect(options.transport);
|
|
@@ -95,6 +98,15 @@ async function createMCPClientFromSdkTransport(options) {
|
|
|
95
98
|
};
|
|
96
99
|
const serverIdentity = deriveServerIdentity(options.transportConfig, options.serverInfoName ?? serverInfo.name);
|
|
97
100
|
serverIdRef.current = serverIdentity.id;
|
|
101
|
+
sdkClient.onclose = () => {
|
|
102
|
+
incrementCounter("mcp.transport.closed.total", { server: serverIdentity.id });
|
|
103
|
+
options.logger?.("warn", "MCP transport closed - rebuild the client to reconnect", { server: serverIdentity.id });
|
|
104
|
+
options.onTransportClose?.({ server: serverIdentity.id });
|
|
105
|
+
};
|
|
106
|
+
sdkClient.onerror = (error) => {
|
|
107
|
+
incrementCounter("mcp.transport.error.total", { server: serverIdentity.id });
|
|
108
|
+
options.onTransportError?.(error, { server: serverIdentity.id });
|
|
109
|
+
};
|
|
98
110
|
const collisionStrategy = options.collisionStrategy ?? "auto-prefix";
|
|
99
111
|
if (options.eventStore !== void 0) options.logger?.("warn", "the client 'eventStore' option was removed (MC-1): event replay is a server responsibility; the SDK transport auto-reconnects with Last-Event-ID. Remove the option.", { server: serverIdentity.id });
|
|
100
112
|
const sessionIdPresent = isStreamableHttp(options.transport) && options.transport.sessionId !== void 0;
|
|
@@ -105,15 +117,40 @@ async function createMCPClientFromSdkTransport(options) {
|
|
|
105
117
|
source: sessionIdPresent ? "session-id-present" : "transport-default"
|
|
106
118
|
});
|
|
107
119
|
let structuredContentSeenLogged = false;
|
|
120
|
+
/**
|
|
121
|
+
* mcp-skills-02: MCP list operations are cursor-paginated (protocol
|
|
122
|
+
* 2024-11-05+) and the SDK does NOT auto-paginate - a single call
|
|
123
|
+
* returns page 1 only. Ignoring `nextCursor` silently truncated the
|
|
124
|
+
* catalogue: tools beyond the first page never reached `toTools()`,
|
|
125
|
+
* defer-loading thresholds counted a partial catalogue, pin
|
|
126
|
+
* fingerprints covered a partial catalogue, and resources/prompts were
|
|
127
|
+
* under-listed. Every list surface now drains the cursor chain, with a
|
|
128
|
+
* defensive page cap against buggy/adversarial servers that never
|
|
129
|
+
* terminate it.
|
|
130
|
+
*/
|
|
131
|
+
const MAX_LIST_PAGES = 100;
|
|
132
|
+
function warnListPageCap(what, collected) {
|
|
133
|
+
options.logger?.("warn", `mcp.list.${what}.page-cap-reached: server kept returning nextCursor after ${MAX_LIST_PAGES} pages; the ${what} catalogue is truncated at ${collected} entries.`, { server: serverIdentity.id });
|
|
134
|
+
}
|
|
108
135
|
async function listTools(opts) {
|
|
109
136
|
const requestOptions = opts?.signal === void 0 ? {} : { signal: opts.signal };
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
137
|
+
const tools = [];
|
|
138
|
+
let cursor;
|
|
139
|
+
for (let page = 0;; page++) {
|
|
140
|
+
if (page >= MAX_LIST_PAGES) {
|
|
141
|
+
warnListPageCap("tools", tools.length);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
let result;
|
|
145
|
+
try {
|
|
146
|
+
result = await sdkClient.listTools(cursor === void 0 ? {} : { cursor }, requestOptions);
|
|
147
|
+
} catch (cause) {
|
|
148
|
+
throw mapSdkError(cause, {});
|
|
149
|
+
}
|
|
150
|
+
tools.push(...result.tools ?? []);
|
|
151
|
+
cursor = result.nextCursor;
|
|
152
|
+
if (cursor === void 0) break;
|
|
115
153
|
}
|
|
116
|
-
const tools = result.tools ?? [];
|
|
117
154
|
if (!structuredContentSeenLogged && tools.some((t) => t.outputSchema !== void 0)) {
|
|
118
155
|
structuredContentSeenLogged = true;
|
|
119
156
|
if (options.logger !== void 0) options.logger("info", "mcp.server.structured-content.detected", { server: serverIdentity.id });
|
|
@@ -128,13 +165,23 @@ async function createMCPClientFromSdkTransport(options) {
|
|
|
128
165
|
}
|
|
129
166
|
async function listResources(opts) {
|
|
130
167
|
const requestOptions = opts?.signal === void 0 ? {} : { signal: opts.signal };
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
const items = [];
|
|
169
|
+
let cursor;
|
|
170
|
+
for (let page = 0;; page++) {
|
|
171
|
+
if (page >= MAX_LIST_PAGES) {
|
|
172
|
+
warnListPageCap("resources", items.length);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
let result;
|
|
176
|
+
try {
|
|
177
|
+
result = await sdkClient.listResources(cursor === void 0 ? {} : { cursor }, requestOptions);
|
|
178
|
+
} catch (cause) {
|
|
179
|
+
throw mapSdkError(cause, {});
|
|
180
|
+
}
|
|
181
|
+
items.push(...result.resources ?? []);
|
|
182
|
+
cursor = result.nextCursor;
|
|
183
|
+
if (cursor === void 0) break;
|
|
136
184
|
}
|
|
137
|
-
const items = result.resources ?? [];
|
|
138
185
|
return Object.freeze(items.map((r) => Object.freeze({
|
|
139
186
|
uri: r.uri,
|
|
140
187
|
...r.name === void 0 ? {} : { name: r.name },
|
|
@@ -144,13 +191,23 @@ async function createMCPClientFromSdkTransport(options) {
|
|
|
144
191
|
}
|
|
145
192
|
async function listPrompts(opts) {
|
|
146
193
|
const requestOptions = opts?.signal === void 0 ? {} : { signal: opts.signal };
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
194
|
+
const items = [];
|
|
195
|
+
let cursor;
|
|
196
|
+
for (let page = 0;; page++) {
|
|
197
|
+
if (page >= MAX_LIST_PAGES) {
|
|
198
|
+
warnListPageCap("prompts", items.length);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
let result;
|
|
202
|
+
try {
|
|
203
|
+
result = await sdkClient.listPrompts(cursor === void 0 ? {} : { cursor }, requestOptions);
|
|
204
|
+
} catch (cause) {
|
|
205
|
+
throw mapSdkError(cause, {});
|
|
206
|
+
}
|
|
207
|
+
items.push(...result.prompts ?? []);
|
|
208
|
+
cursor = result.nextCursor;
|
|
209
|
+
if (cursor === void 0) break;
|
|
152
210
|
}
|
|
153
|
-
const items = result.prompts ?? [];
|
|
154
211
|
return Object.freeze(items.map((p) => Object.freeze({
|
|
155
212
|
name: p.name,
|
|
156
213
|
...p.description === void 0 ? {} : { description: p.description },
|
|
@@ -198,7 +255,7 @@ async function createMCPClientFromSdkTransport(options) {
|
|
|
198
255
|
...result.isError === void 0 ? {} : { isError: Boolean(result.isError) }
|
|
199
256
|
});
|
|
200
257
|
}
|
|
201
|
-
async function
|
|
258
|
+
async function readResourceContents(uri, opts) {
|
|
202
259
|
const requestOptions = opts?.signal === void 0 ? {} : { signal: opts.signal };
|
|
203
260
|
let result;
|
|
204
261
|
try {
|
|
@@ -206,9 +263,17 @@ async function createMCPClientFromSdkTransport(options) {
|
|
|
206
263
|
} catch (cause) {
|
|
207
264
|
throw mapSdkError(cause, {});
|
|
208
265
|
}
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
-
return Object.freeze(
|
|
266
|
+
const contents = result.contents ?? [];
|
|
267
|
+
if (contents.length === 0) throw new MCPProtocolError(`MCP server returned no contents for resource '${uri}'.`, { metadata: { server: serverIdentity.id } });
|
|
268
|
+
return Object.freeze(contents.map((c) => Object.freeze(c)));
|
|
269
|
+
}
|
|
270
|
+
async function readResource(uri, opts) {
|
|
271
|
+
const contents = await readResourceContents(uri, opts);
|
|
272
|
+
if (contents.length > 1) {
|
|
273
|
+
incrementCounter("mcp.resource.multi-content-truncated.total", { server: serverIdentity.id });
|
|
274
|
+
options.logger?.("warn", `resource '${uri}' returned ${contents.length} content items; readResource() surfaces only the first - use readResourceContents() for all of them`, { server: serverIdentity.id });
|
|
275
|
+
}
|
|
276
|
+
return contents[0];
|
|
212
277
|
}
|
|
213
278
|
async function getPrompt(name, args, opts) {
|
|
214
279
|
const requestOptions = opts?.signal === void 0 ? {} : { signal: opts.signal };
|
|
@@ -252,11 +317,29 @@ async function createMCPClientFromSdkTransport(options) {
|
|
|
252
317
|
}
|
|
253
318
|
}
|
|
254
319
|
lastToolFingerprints = adapted.fingerprints;
|
|
255
|
-
|
|
320
|
+
let pins = toolsOpts?.pinnedFingerprints;
|
|
321
|
+
let mismatchAction = toolsOpts?.onPinMismatch ?? "warn";
|
|
322
|
+
const pinStore = toolsOpts?.pinStore;
|
|
323
|
+
if (pins === void 0 && pinStore !== void 0) {
|
|
324
|
+
const stored = await pinStore.get(serverIdentity.id);
|
|
325
|
+
if (stored === void 0) {
|
|
326
|
+
const recorded = {};
|
|
327
|
+
for (const [name, hash] of adapted.fingerprints) recorded[name] = hash;
|
|
328
|
+
await pinStore.set(serverIdentity.id, recorded);
|
|
329
|
+
incrementCounter("mcp.tools.pins-recorded.total", { server: serverIdentity.id });
|
|
330
|
+
options.logger?.("info", "mcp.tools.pins-recorded: first-use fingerprints stored", {
|
|
331
|
+
server: serverIdentity.id,
|
|
332
|
+
tools: Object.keys(recorded).length
|
|
333
|
+
});
|
|
334
|
+
} else {
|
|
335
|
+
pins = stored;
|
|
336
|
+
mismatchAction = toolsOpts?.onPinMismatch ?? "reject";
|
|
337
|
+
}
|
|
338
|
+
}
|
|
256
339
|
if (pins !== void 0) for (const [name, pinned] of Object.entries(pins)) {
|
|
257
340
|
const current = adapted.fingerprints.get(name);
|
|
258
341
|
if (current !== void 0 && current !== pinned) {
|
|
259
|
-
if (
|
|
342
|
+
if (mismatchAction === "reject") throw new MCPToolPinningError(`MCP tool '${name}' no longer matches its pinned definition fingerprint - the server changed the definition behind an approved name.`, { metadata: {
|
|
260
343
|
server: serverIdentity.id,
|
|
261
344
|
tool: name
|
|
262
345
|
} });
|
|
@@ -299,6 +382,7 @@ async function createMCPClientFromSdkTransport(options) {
|
|
|
299
382
|
listPrompts,
|
|
300
383
|
callTool,
|
|
301
384
|
readResource,
|
|
385
|
+
readResourceContents,
|
|
302
386
|
getPrompt,
|
|
303
387
|
toTools,
|
|
304
388
|
close
|