@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +23 -16
  3. package/dist/client/adapt-result.js +22 -10
  4. package/dist/client/adapt-result.js.map +1 -1
  5. package/dist/client/client-handlers.js +11 -2
  6. package/dist/client/client-handlers.js.map +1 -1
  7. package/dist/client/client.d.ts.map +1 -1
  8. package/dist/client/client.js +111 -27
  9. package/dist/client/client.js.map +1 -1
  10. package/dist/client/inbound-filters.js +8 -3
  11. package/dist/client/inbound-filters.js.map +1 -1
  12. package/dist/client/mcp-resource-reader.d.ts +10 -0
  13. package/dist/client/mcp-resource-reader.d.ts.map +1 -1
  14. package/dist/client/mcp-resource-reader.js +16 -5
  15. package/dist/client/mcp-resource-reader.js.map +1 -1
  16. package/dist/client/pinning.js +2 -2
  17. package/dist/client/pinning.js.map +1 -1
  18. package/dist/client/to-tools.js.map +1 -1
  19. package/dist/client/transport-factory.js +1 -1
  20. package/dist/client/transport-factory.js.map +1 -1
  21. package/dist/client/types.d.ts +58 -10
  22. package/dist/client/types.d.ts.map +1 -1
  23. package/dist/errors/index.d.ts +2 -2
  24. package/dist/errors/index.js +2 -2
  25. package/dist/errors/index.js.map +1 -1
  26. package/dist/helpers/validate-config.js +1 -1
  27. package/dist/helpers/validate-config.js.map +1 -1
  28. package/dist/index.d.ts +2 -3
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +4 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/oauth/bridge.d.ts +1 -1
  33. package/dist/oauth/bridge.js.map +1 -1
  34. package/dist/package.js +6 -0
  35. package/dist/package.js.map +1 -0
  36. package/dist/registry/json-schema.js +36 -7
  37. package/dist/registry/json-schema.js.map +1 -1
  38. package/dist/transport/types.d.ts +4 -4
  39. 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` / `getPrompt` /
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 the client
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` that pins a single
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 no client
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` transport could not be established or was dropped.
260
- - `MCPProtocolError` JSON-RPC / MCP protocol-level error.
261
- - `MCPAuthError` authentication / authorization failure.
262
- - `MCPToolNotFoundError` the requested tool is not registered with the server.
263
- - `MCPCallTimeoutError` tool call exceeded the configured timeout (variant `'session-lost'` for the resume-handshake-lost path).
264
- - `MCPCancelledError` call was cancelled by an `AbortSignal`.
265
- - `MCPInvalidConfigError` the supplied `MCPTransportConfig` is invalid.
266
- - `MCPTransportNotSupportedError` the supplied configuration requested an unsupported transport / capability combination (variant `'transport-resumable-not-supported'` for resumable sessions on `'stdio'` / `'sse'`).
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
- - 109 unit + integration + property tests run under Vitest with
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 © 2026 Oleksiy Stepurenko.
299
+ MIT - © 2026 Oleksiy Stepurenko.
293
300
 
294
301
  ---
295
302
 
296
- **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
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)} full payload in contentParts]`);
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
- * `uri` doubles as the `read_result` handle — an
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
- * resolves it on demand via {@link MCPClient.readResource}.
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 ? "" : ` ${part.description}`}\nFetch the full contents on demand with read_result, handle: ${part.uri}`;
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)} full payload in contentParts]`;
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 R4).
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":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;iBAsEgB,4BAAA,CAAA;;;;;;iBASM,eAAA,UAAyB,yBAAyB,QAAQ"}
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"}
@@ -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 = "0.5.0";
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 re-run toTools() to refresh + re-sanitize the catalogue", { server: serverIdRef.current });
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
- let result;
111
- try {
112
- result = await sdkClient.listTools({}, requestOptions);
113
- } catch (cause) {
114
- throw mapSdkError(cause, {});
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
- let result;
132
- try {
133
- result = await sdkClient.listResources({}, requestOptions);
134
- } catch (cause) {
135
- throw mapSdkError(cause, {});
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
- let result;
148
- try {
149
- result = await sdkClient.listPrompts({}, requestOptions);
150
- } catch (cause) {
151
- throw mapSdkError(cause, {});
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 readResource(uri, opts) {
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 first = (result.contents ?? [])[0];
210
- if (first === void 0) throw new MCPProtocolError(`MCP server returned no contents for resource '${uri}'.`, { metadata: { server: serverIdentity.id } });
211
- return Object.freeze(first);
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
- const pins = toolsOpts?.pinnedFingerprints;
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 (toolsOpts?.onPinMismatch === "reject") throw new MCPToolPinningError(`MCP tool '${name}' no longer matches its pinned definition fingerprint the server changed the definition behind an approved name.`, { metadata: {
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