@graphorin/mcp 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +73 -5
  3. package/dist/client/adapt-result.d.ts +9 -1
  4. package/dist/client/adapt-result.d.ts.map +1 -1
  5. package/dist/client/adapt-result.js +28 -10
  6. package/dist/client/adapt-result.js.map +1 -1
  7. package/dist/client/client-handlers.js +7 -1
  8. package/dist/client/client-handlers.js.map +1 -1
  9. package/dist/client/client.d.ts.map +1 -1
  10. package/dist/client/client.js +47 -71
  11. package/dist/client/client.js.map +1 -1
  12. package/dist/client/inbound-filters.js +101 -1
  13. package/dist/client/inbound-filters.js.map +1 -1
  14. package/dist/client/index.d.ts +2 -1
  15. package/dist/client/index.js +2 -1
  16. package/dist/client/managed.d.ts +35 -0
  17. package/dist/client/managed.d.ts.map +1 -0
  18. package/dist/client/managed.js +136 -0
  19. package/dist/client/managed.js.map +1 -0
  20. package/dist/client/mcp-resource-reader.js +1 -1
  21. package/dist/client/mcp-resource-reader.js.map +1 -1
  22. package/dist/client/to-tools-run.js +119 -0
  23. package/dist/client/to-tools-run.js.map +1 -0
  24. package/dist/client/to-tools.d.ts +8 -0
  25. package/dist/client/to-tools.d.ts.map +1 -1
  26. package/dist/client/to-tools.js +27 -4
  27. package/dist/client/to-tools.js.map +1 -1
  28. package/dist/client/types.d.ts +12 -3
  29. package/dist/client/types.d.ts.map +1 -1
  30. package/dist/errors/index.d.ts +3 -3
  31. package/dist/errors/index.js +1 -1
  32. package/dist/errors/index.js.map +1 -1
  33. package/dist/helpers/identity.d.ts +11 -0
  34. package/dist/helpers/identity.d.ts.map +1 -1
  35. package/dist/helpers/identity.js +13 -2
  36. package/dist/helpers/identity.js.map +1 -1
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +6 -6
  40. package/dist/index.js.map +1 -1
  41. package/dist/package.js +6 -0
  42. package/dist/package.js.map +1 -0
  43. package/dist/registry/json-schema.js +26 -8
  44. package/dist/registry/json-schema.js.map +1 -1
  45. package/dist/transport/types.d.ts +9 -0
  46. package/package.json +13 -12
  47. package/src/client/adapt-result.ts +302 -0
  48. package/src/client/client-handlers.ts +215 -0
  49. package/src/client/client.ts +725 -0
  50. package/src/client/defer-loading.ts +108 -0
  51. package/src/client/inbound-filters.ts +246 -0
  52. package/src/client/index.ts +42 -0
  53. package/src/client/managed.ts +222 -0
  54. package/src/client/mcp-resource-reader.ts +183 -0
  55. package/src/client/pinning.ts +48 -0
  56. package/src/client/to-tools-run.ts +178 -0
  57. package/src/client/to-tools.ts +294 -0
  58. package/src/client/transport-factory.ts +117 -0
  59. package/src/client/types.ts +422 -0
  60. package/src/errors/index.ts +170 -0
  61. package/src/helpers/identity.ts +128 -0
  62. package/src/helpers/index.ts +8 -0
  63. package/src/helpers/validate-config.ts +95 -0
  64. package/src/index.ts +49 -0
  65. package/src/oauth/bridge.ts +143 -0
  66. package/src/oauth/index.ts +18 -0
  67. package/src/oauth/library.ts +61 -0
  68. package/src/registry/json-schema.ts +402 -0
  69. package/src/transport/index.ts +15 -0
  70. package/src/transport/types.ts +108 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # @graphorin/mcp
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#154](https://github.com/o-stepper/graphorin/pull/154) [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04) Thanks [@o-stepper](https://github.com/o-stepper)! - W-072: every export map's `import` condition becomes `default`, and the Node floor rises to `>=22.12.0`.
8
+
9
+ CJS consumers previously hit a bewildering `ERR_PACKAGE_PATH_NOT_EXPORTED` instead of a clear ESM-only signal. With the `default` condition, plain `require('@graphorin/core')` works via Node's stable `require(esm)` - which shipped in 22.12, hence the engines bump across every workspace manifest (packages, examples, benchmarks, docs; enforced by the widened mvp-readiness sweep). No dual-instance hazard: there is no CJS build, `require()` returns the same ESM module instance. ESM consumers are unaffected (`default` serves both paths; `types` stays first). The pack gate now runs attw under the full `node16` profile (was `esm-only`) and adds a runtime `require(esm)` smoke against the packed tarballs. Installs on Node 22.0-22.11 with `engine-strict` will refuse - upgrade Node (see the migration guide).
10
+
11
+ - [#153](https://github.com/o-stepper/graphorin/pull/153) [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534) Thanks [@o-stepper](https://github.com/o-stepper)! - Security hardening (W-018, Invariant Labs tool-poisoning class): annotation strings inside MCP tool JSON Schemas (`description`, `title`, `$comment`, string `examples` - at any nesting depth: `properties`, `items` including tuples, `additionalProperties`, `oneOf`/`anyOf`/`allOf`, `$defs`/`definitions`, `patternProperties` and friends) are now stripped of imperative payloads before the schema reaches `buildJsonSchemaValidator`, whose `toJSON()` feeds the provider wire and the `tool_search` projection. Semantic keywords (`enum`, `const`, `pattern`, `required`, property names) are never modified, so input validation is byte-identical. The TOFU fingerprint (`computeToolDefinitionHash`) keeps hashing the RAW definition: existing pins stay valid, and two differently-poisoned schemas cannot collapse into one redacted hash. New counter `mcp.tool-schema.injection-flagged.total` (server/tool) signals hits at registration. An operator's `pass-through` override skips the pass, mirroring `sanitizeDescription`. Minor bump: model-facing schemas may change bytes and new counter increments may appear on existing deployments.
12
+
13
+ - [#154](https://github.com/o-stepper/graphorin/pull/154) [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04) Thanks [@o-stepper](https://github.com/o-stepper)! - W-016/W-140: MCP server identity is transport-derived by default; scoped handles are colon-safe.
14
+
15
+ `serverIdentity.id` previously defaulted to the name the remote server self-reported on `initialize` - and every security surface keys off that id: TOFU pins in the pinStore (a rug-pull server minted a fresh record by renaming itself), `mcp:<id>:<uri>` handle scoping (a malicious server claiming a trusted name had its resource_links resolve under the trusted scope), taint labels and audit rows. The id now derives ONLY from the operator-controlled transport config plus the explicit `serverInfoName` override; HTTP-family ids include a non-default port (localhost:3001 and :3002 no longer collide). The self-reported name survives as display-only `reportedServerName`. Because ':' is now routine in ids, `scopedResourceHandle` percent-encodes the id segment and the reader decodes before matching (handles are ephemeral - no migration). MIGRATION: pinStore records keyed by old server-controlled ids orphan and TOFU re-pins under the transport id on first `toTools()` - operators running `onPinMismatch: 'reject'` must re-pin; registry auto-prefix namespaces (derived from the identity) may change model-visible prefixed tool names.
16
+
17
+ - [#158](https://github.com/o-stepper/graphorin/pull/158) [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab) Thanks [@o-stepper](https://github.com/o-stepper)! - Full TOFU pin lifecycle (W-079). With a `pinStore`, a tool ADDED after the first-use recording is now rejected by default (`MCPToolPinningError`; `onPinMismatch: 'warn'` admits it with the new `mcp.tools.pin-added.total` counter) - previously post-approval additions entered the catalogue unchecked. Removals of pinned tools are observable via `mcp.tools.pin-removed.total` (never an exception - but they can hide a rename). The new `onPinMismatch: 'accept-and-update'` gives operators the documented path to accept a legitimate catalogue change: after the comparison it overwrites the store with the current snapshot (`mcp.tools.pins-updated.total` + log), ending the eternal-warn state without manual store mutation. Explicit `pinnedFingerprints` stay subset-pins and win over the store; servers that legitimately extend their catalogue under a pinStore should use `'warn'` or `'accept-and-update'`.
18
+
19
+ - [#160](https://github.com/o-stepper/graphorin/pull/160) [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156) Thanks [@o-stepper](https://github.com/o-stepper)! - W-080: new opt-in `createManagedMCPClient(options & { reconnect })` - a managed wrapper for long-running agents whose MCP tools must survive a dead stdio child or a lost HTTP session. It implements `MCPClient` by delegating to an inner client it rebuilds on transport close (exponential backoff + jitter; `mcp.reconnect.attempt/success/gave-up.total` counters), and - the key move - its `toTools()` binds adapted tools to the WRAPPER, so already-registered `Tool` objects keep working across a reconnect without re-registration (the full toTools pipeline was extracted into a shared, client-parameterized `runToTools` helper; the plain client uses it byte-identically). After a successful reconnect the wrapper re-runs `toTools()` with the last-used options so the pin comparison / TOFU store re-screens the post-reconnect catalogue (rug-pull caught). Deliberate contracts: an in-flight call is NEVER retried (only the connection heals), and the operator's `onTransportClose` fires once, on final (gave-up) failure. The default `createMCPClient` behaviour is unchanged.
20
+
21
+ - [#158](https://github.com/o-stepper/graphorin/pull/158) [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab) Thanks [@o-stepper](https://github.com/o-stepper)! - The ToolReturn envelope gets a symbol brand (W-115). New core exports: `TOOL_RETURN_BRAND` (`Symbol.for`, duplicate-copy safe), the `toolReturn()` factory, and the ONE shared guard `isToolReturnEnvelope` consumed by both the executor's unwrap and the registry's example-normalizer (the duplicated sniff is gone). The structural fallback for unbranded objects is deliberately narrow - own keys within `{output, contentParts, taint}` - so a tool legitimately returning `{output, exitCode, stderr}` now reaches the model whole instead of being silently stripped to `.output`; canonical unbranded literals keep unwrapping and increment `tool.result.envelope.unbranded-toolreturn.total` toward the sniff's future deprecation. First-party producers (MCP adaptCallResult, memory recall tools, toTool taint envelopes) now brand via `toolReturn()`. Downstream consumers relying on extra fields being dropped will now see them; plain data of exactly `{output: X}` remains ambiguous by contract - brand it or rename the field.
22
+
23
+ ### Patch Changes
24
+
25
+ - [#153](https://github.com/o-stepper/graphorin/pull/153) [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534) Thanks [@o-stepper](https://github.com/o-stepper)! - Security hardening (W-017): the text of an MCP `isError` result is now sanitized at the MCP boundary before it rides into `MCPToolExecutionError` and, through the executor, into the model-visible `ToolError.message`. Previously the error path bypassed inbound sanitization entirely (it only ran on the success path), so a malicious server could deliver a prompt-injection payload verbatim via `isError: true`. The error text now goes through the same per-server inbound policy the adapted tool declares (default `detect-and-strip-and-wrap`: imperatives stripped, body wrapped in the `<<<untrusted_content trust="mcp-derived">>>` envelope with embedded delimiters neutralized); an operator's explicit `pass-through` override is honored for parity with the success path. New counter `mcp.tool-error.injection-flagged.total` increments when a pattern fires.
26
+
27
+ - [#153](https://github.com/o-stepper/graphorin/pull/153) [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534) Thanks [@o-stepper](https://github.com/o-stepper)! - ReDoS guard hardening (W-078): `looksCatastrophic` now also rejects the alternation-overlap exponential family (`^(a|a)+$`, `^(\w|\d)*$`) - previously only groups whose body ends with a quantifier were caught, so these classic shapes ran on the raw engine and a ~1k-char non-matching input from an untrusted server could stall the event loop practically forever. Additionally, any pattern containing a quantified group now runs under a reduced 1000-char tested-string cap (defense-in-depth against polynomial backtracking). Conservative false positives degrade the pattern to permissive validation - the semantics already documented for guarded and malformed patterns; alternations without a quantified group still validate exactly as before. The TSDoc no longer overclaims: covered classes are enumerated and a linear-time engine (re2) is named as the exact solution.
28
+
29
+ - [#162](https://github.com/o-stepper/graphorin/pull/162) [`73b19ca`](https://github.com/o-stepper/graphorin/commit/73b19caeda388bda628a48138cb7d70b1db839a3) Thanks [@o-stepper](https://github.com/o-stepper)! - Remove phantom workspace dependencies that no source file imports: agent no longer depends on provider and observability, mcp/workflow/server no longer depend on observability, sessions no longer depends on security (and its memory edge moves to devDependencies where the single test import lives), skills no longer depends on tools. Dead tsdown `external` entries for the removed edges are gone too, so a future import can no longer build as external without a declared dependency. Consumer install graphs shrink accordingly; a new repo-wide `check-phantom-deps` CI gate keeps the manifest graph honest from here on.
30
+
31
+ - [#160](https://github.com/o-stepper/graphorin/pull/160) [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156) Thanks [@o-stepper](https://github.com/o-stepper)! - W-105: an operator downgrade of an MCP tool's `sideEffectClass` below the sink classes (`'read-only'` / `'pure'` via `sideEffectClassByTool`) is now visible: one WARN per tool at adaptation time (naming the server, the tool and every gate the tool leaves - dataflow sink gate, Rule-of-Two writer forbid, read-only capability gate) and a new additive frozen `AdaptedToolsResult.downgradedTools` list for operator audits. The guide documents the consequences and that the server's own `readOnlyHint` is deliberately never trusted for classification. Default behaviour without overrides is byte-identical.
32
+
33
+ - [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - TSDoc `{@link}` hygiene sweep (W-130): all 55 broken links found by TypeDoc's now-enabled `validation.invalidLink` are fixed - two resolved to their real targets (`GraphorinMCPError` was misnamed `MCPError`), the rest (cross-package, `import()`-form, unexported-constant, and DOM-type references that have never rendered as hrefs) converted to plain inline code. The docs build now fails on any new broken `{@link}` via a scoped gate.
34
+
35
+ - [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - Tarballs now ship `src/` so the published `dist/**/*.d.ts.map` files actually work (W-136): the maps referenced `../src/*.ts` that the `files` whitelist excluded, so go-to-definition fell back into `.d.ts` and the shipped maps were dead weight. The pack gate gains a `map-integrity` leg: every source referenced by a shipped map must resolve inside the tarball (or be embedded via `sourcesContent`), with an anti-vacuous guard - a package whose tsdown config emits declaration maps must contain a non-zero number of `.d.ts.map` files, so a cache-restored dist that silently dropped maps fails the gate instead of passing vacuously. `mvp-readiness` now requires `src` in every publishable `files` array.
36
+
37
+ - [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - Every published package now declares its tree-shaking contract via `sideEffects` (W-137): 18 packages audited to a pure module scope get `false`, the CLI declares its bin entry (`["./dist/bin/*"]`), and `@graphorin/security` gets an explicit `true` - its secrets subsystem registers built-in resolvers and the SecretValue caller-context provider at import time, so marking it pure would let bundlers drop those registrations. `mvp-readiness` now fails any publishable manifest without a declared `sideEffects`, closing the drift for future packages.
38
+
39
+ - [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - SDK error classification is code-based, not message-based (W-141): an `McpError`'s message is server-controlled text, so a server phrasing an ordinary failure as "request timed out" or "cancelled by user" could forge the typed `MCPCallTimeoutError`/`MCPCancelledError` classes and skew the operator counters keyed on them. Timeouts now map from `ErrorCode.RequestTimeout` (-32001), cancellation maps from the caller's own AbortSignal state (SDK 1.29 wraps local aborts in the same -32001 code, so the signal - not the error - is the trustworthy fact), and message heuristics remain only as a last resort for plain non-RPC errors plus the benign tool-not-found class. The advertised elicitation capability is now the explicit `{ form: {} }` sub-capability of the 2025-11-25 spec instead of the bare `{}` shorthand.
40
+
41
+ - Updated dependencies [[`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04)]:
42
+ - @graphorin/core@0.7.0
43
+ - @graphorin/security@0.7.0
44
+ - @graphorin/tools@0.7.0
45
+
46
+ ## 0.6.1
47
+
48
+ ### Patch Changes
49
+
50
+ - [#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.
51
+
52
+ - Updated dependencies [[`826ee59`](https://github.com/o-stepper/graphorin/commit/826ee5933ad38693b0dd2f20a110abfecba7d23d), [`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430)]:
53
+ - @graphorin/observability@0.6.1
54
+ - @graphorin/core@0.6.1
55
+ - @graphorin/security@0.6.1
56
+ - @graphorin/tools@0.6.1
57
+
3
58
  ## 0.6.0
4
59
 
5
60
  ### Minor Changes
package/README.md CHANGED
@@ -26,8 +26,23 @@ 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. The full pin
38
+ lifecycle (W-079): tools ADDED after the first recording are rejected
39
+ by default too (`mcp.tools.pin-added.total` under `'warn'`), removals
40
+ are observable (`mcp.tools.pin-removed.total`, never an exception),
41
+ and `onPinMismatch: 'accept-and-update'` is the explicit operator
42
+ path to re-trust a legitimately changed catalogue - it rewrites the
43
+ store with the current snapshot (`mcp.tools.pins-updated.total`) so
44
+ subsequent calls are clean. Explicit `pinnedFingerprints` remain
45
+ subset-pins and win over the store.
31
46
  - **Strict default for MCP-derived tools.** Every `Tool` produced
32
47
  by `MCPClient.toTools()` defaults to the
33
48
  `'detect-and-strip-and-wrap'` inbound prompt-injection
@@ -56,6 +71,15 @@ the existing outbound OAuth subsystem in `@graphorin/security`.
56
71
  `'auto-prefix'`), and the optional per-client `priority` field;
57
72
  the registry consumes the trio when its strategy-aware
58
73
  `assertNoDuplicates(strategy, ctx)` overload runs.
74
+ - **Transport-derived identity (W-016).** `serverIdentity.id` derives
75
+ from the operator-controlled transport config (HTTP ids include a
76
+ non-default port), never from the name a server self-reports on
77
+ `initialize` - TOFU pins, `mcp:<id>:<uri>` handle scoping and taint
78
+ labels all key off it, so a rug-pull rename cannot mint a fresh pin
79
+ and a malicious server cannot claim a trusted server's scope. The
80
+ self-reported name survives as display-only `reportedServerName`;
81
+ the explicit `serverInfoName` option remains the operator override.
82
+ Handle ids are percent-encoded (`:` is routine in ids now).
59
83
  - **OAuth integration.** `createOAuthAuthorizationProvider({...})`
60
84
  wraps the existing `refreshOAuthSession(...)` helper from
61
85
  `@graphorin/security/oauth`, resolves the bearer header on every
@@ -139,7 +163,19 @@ Tool descriptions are always sanitized at registration time using
139
163
  the `'detect-and-strip'` variant of the configured policy (the
140
164
  wrap envelope is reserved for tool-result bodies inside the
141
165
  conversation history; the description goes into the per-step tool
142
- catalogue verbatim aside from the strip pass).
166
+ catalogue verbatim aside from the strip pass). The same strip pass
167
+ covers the ANNOTATION strings inside the tool's JSON Schemas
168
+ (`description`, `title`, `$comment`, string `examples` at any
169
+ nesting depth) before the schema reaches the provider wire and the
170
+ `tool_search` projection - the Invariant Labs tool-poisoning
171
+ hiding place. Semantic keywords (`enum`, `const`, `pattern`,
172
+ `required`, property names) are never modified, and the TOFU
173
+ fingerprint keeps hashing the RAW definition, so existing pins
174
+ survive and drift detection still sees the original bytes
175
+ (`mcp.tool-schema.injection-flagged.total` counts hits). The text
176
+ of an `isError` result goes through the full configured policy
177
+ (strip + envelope by default) before it reaches the model as a
178
+ tool error message.
143
179
 
144
180
  ## OAuth integration
145
181
 
@@ -199,6 +235,38 @@ const issues = await createMCPClient({
199
235
  });
200
236
  ```
201
237
 
238
+ ## Managed client (auto-reconnect)
239
+
240
+ `createMCPClient` connections are deliberately **one-shot**: the SDK
241
+ heals transient Streamable HTTP hiccups itself (`Last-Event-ID`), but
242
+ a dead stdio child or a lost HTTP session kills the client for good -
243
+ and every `Tool` adapted from it closes over the corpse. For
244
+ long-running agents, opt into the managed wrapper:
245
+
246
+ ```ts
247
+ import { createManagedMCPClient } from '@graphorin/mcp';
248
+
249
+ const client = await createManagedMCPClient({
250
+ transport: { kind: 'stdio', command: 'linear-mcp' },
251
+ reconnect: { maxAttempts: 5, initialDelayMs: 500, maxDelayMs: 30_000 },
252
+ });
253
+ const tools = await client.toTools({ pinStore }); // register once
254
+ ```
255
+
256
+ The wrapper implements `MCPClient` by delegating to an inner client it
257
+ rebuilds on transport close (exponential backoff + jitter;
258
+ `mcp.reconnect.attempt/success/gave-up.total` counters). Tools from
259
+ `client.toTools()` close over the **wrapper**, so the same registered
260
+ `Tool` objects keep working across a reconnect - no re-registration.
261
+ After a successful reconnect the wrapper re-runs `toTools()` with the
262
+ last-used options, so the pin comparison / TOFU store re-screens the
263
+ post-reconnect catalogue (a rug-pull across a reconnect is caught and,
264
+ with a pin store, rejected + logged). Two contracts to know: an
265
+ **in-flight call is never retried** (the failed call stays failed;
266
+ retry policy belongs to the executor/model - only the connection
267
+ heals), and the operator's `onTransportClose` fires **once, on final
268
+ failure** (reconnect attempts exhausted), not per outage.
269
+
202
270
  ## Streamable HTTP sessions
203
271
 
204
272
  When the MCP server assigns an `Mcp-Session-Id` on `initialize`, the
@@ -267,8 +335,8 @@ field where applicable:
267
335
 
268
336
  ## Acceptance & testing
269
337
 
270
- - 109 unit + integration + property tests run under Vitest with
271
- no network calls (verified by the workspace
338
+ - The package's unit + integration + property tests run under Vitest
339
+ with no network calls (verified by the workspace
272
340
  `pnpm run check-no-network` script).
273
341
  - Integration tests use the SDK's `InMemoryTransport` linked-pair
274
342
  + a configurable in-process `Server` fixture; no child
@@ -293,4 +361,4 @@ MIT - © 2026 Oleksiy Stepurenko.
293
361
 
294
362
  ---
295
363
 
296
- **Project Graphorin** · v0.6.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
364
+ **Project Graphorin** · v0.7.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
@@ -1,6 +1,6 @@
1
1
  import { ServerIdentity } from "../transport/types.js";
2
2
  import { MCPCallToolResult } from "./types.js";
3
- import { ToolReturn, ZodLikeSchema } from "@graphorin/core";
3
+ import { InboundSanitizationPolicy, ToolReturn, ZodLikeSchema } from "@graphorin/core";
4
4
 
5
5
  //#region src/client/adapt-result.d.ts
6
6
 
@@ -10,6 +10,14 @@ interface AdaptCallResultArgs {
10
10
  readonly outputSchema?: ZodLikeSchema<unknown> | undefined;
11
11
  readonly serverIdentity: ServerIdentity;
12
12
  readonly toolName: string;
13
+ /**
14
+ * Effective per-server inbound-sanitization policy, applied to the
15
+ * `isError` text before it rides into `MCPToolExecutionError` (the
16
+ * executor never sanitizes the error path, so the trust-aware MCP
17
+ * boundary must). Defaults to the MCP-strict
18
+ * `'detect-and-strip-and-wrap'` when omitted.
19
+ */
20
+ readonly inboundSanitization?: InboundSanitizationPolicy;
13
21
  readonly logger?: (level: 'debug' | 'info' | 'warn' | 'error', message: string, fields?: Record<string, unknown>) => void;
14
22
  }
15
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"adapt-result.d.ts","names":[],"sources":["../../src/client/adapt-result.ts"],"sourcesContent":[],"mappings":";;;;;;;UAmBiB,mBAAA;mBACE;0BACO;2BACC;;2FAKd;;;;;;;;;iBAWG,eAAA,OAAsB,sBAAsB"}
1
+ {"version":3,"file":"adapt-result.d.ts","names":[],"sources":["../../src/client/adapt-result.ts"],"sourcesContent":[],"mappings":";;;;;;;UA0BiB,mBAAA;mBACE;0BACO;2BACC;;;;;;;;;iCASM;2FAIpB;;;;;;;;;iBAWG,eAAA,OAAsB,sBAAsB"}
@@ -1,5 +1,7 @@
1
1
  import { MCPToolExecutionError } from "../errors/index.js";
2
2
  import { incrementCounter } from "@graphorin/tools/audit";
3
+ import { toolReturn } from "@graphorin/core";
4
+ import { applyInboundSanitization } from "@graphorin/tools/inbound";
3
5
 
4
6
  //#region src/client/adapt-result.ts
5
7
  /**
@@ -17,7 +19,23 @@ function adaptCallResult(args) {
17
19
  server: serverIdentity.id,
18
20
  tool: toolName
19
21
  });
20
- throw new MCPToolExecutionError(errorText.length > 0 ? errorText : `MCP tool '${toolName}' reported an error result.`, { metadata: { tool: toolName } });
22
+ let message = errorText;
23
+ if (errorText.length > 0) {
24
+ const outcome = applyInboundSanitization({
25
+ body: errorText,
26
+ policy: args.inboundSanitization ?? "detect-and-strip-and-wrap",
27
+ trustClass: "mcp-derived",
28
+ toolName,
29
+ contentOrigin: `mcp:tool-error:${serverIdentity.id}`,
30
+ failClosed: false
31
+ });
32
+ if (outcome.patternsHit.length > 0) incrementCounter("mcp.tool-error.injection-flagged.total", {
33
+ server: serverIdentity.id,
34
+ tool: toolName
35
+ });
36
+ message = outcome.body;
37
+ }
38
+ throw new MCPToolExecutionError(message.length > 0 ? message : `MCP tool '${toolName}' reported an error result.`, { metadata: { tool: toolName } });
21
39
  }
22
40
  const contentParts = [];
23
41
  const resourceLinkPreviews = [];
@@ -62,10 +80,10 @@ function adaptCallResult(args) {
62
80
  type: "text",
63
81
  text: JSON.stringify(result.structuredContent)
64
82
  });
65
- return Object.freeze({
83
+ return Object.freeze(toolReturn({
66
84
  output: validation.data,
67
85
  contentParts: Object.freeze(finalParts)
68
- });
86
+ }));
69
87
  }
70
88
  incrementCounter("mcp.structured-content.validation.failure.total", {
71
89
  server: serverIdentity.id,
@@ -80,10 +98,10 @@ function adaptCallResult(args) {
80
98
  type: "text",
81
99
  text: JSON.stringify(result.structuredContent)
82
100
  });
83
- return Object.freeze({
101
+ return Object.freeze(toolReturn({
84
102
  output: concatenatedText.length > 0 ? concatenatedText : JSON.stringify(result.structuredContent),
85
103
  contentParts: Object.freeze(fallbackParts)
86
- });
104
+ }));
87
105
  } else {
88
106
  incrementCounter("mcp.structured-content.emitted.total", {
89
107
  server: serverIdentity.id,
@@ -94,15 +112,15 @@ function adaptCallResult(args) {
94
112
  type: "text",
95
113
  text: JSON.stringify(result.structuredContent)
96
114
  });
97
- return Object.freeze({
115
+ return Object.freeze(toolReturn({
98
116
  output: result.structuredContent,
99
117
  contentParts: Object.freeze(finalParts)
100
- });
118
+ }));
101
119
  }
102
- return Object.freeze({
120
+ return Object.freeze(toolReturn({
103
121
  output: concatenatedText,
104
122
  contentParts: Object.freeze(contentParts)
105
- });
123
+ }));
106
124
  }
107
125
  function mcpContentToMessageContent(part, serverId) {
108
126
  switch (part.type) {
@@ -166,7 +184,7 @@ function formatResourceLinkPreview(part, serverId) {
166
184
  * @stable
167
185
  */
168
186
  function scopedResourceHandle(serverId, uri) {
169
- return `mcp:${serverId}:${uri}`;
187
+ return `mcp:${encodeURIComponent(serverId)}:${uri}`;
170
188
  }
171
189
  /** Human-readable size of a base64 payload's decoded bytes (MC-8). */
172
190
  function approxDecodedSize(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, 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":";;;;;;;;;;;AAsCA,SAAgB,gBAAgB,MAAgD;CAC9E,MAAM,EAAE,QAAQ,cAAc,gBAAgB,aAAa;AAK3D,KAAI,OAAO,YAAY,MAAM;EAC3B,MAAM,aAAa,OAAO,WAAW,EAAE,EACpC,QAAQ,MAA2C,EAAE,SAAS,OAAO,CACrE,KAAK,MAAM,EAAE,KAAK,CAClB,KAAK,KAAK;AACb,mBAAiB,6BAA6B;GAC5C,QAAQ,eAAe;GACvB,MAAM;GACP,CAAC;AACF,QAAM,IAAI,sBACR,UAAU,SAAS,IAAI,YAAY,aAAa,SAAS,8BACzD,EAAE,UAAU,EAAE,MAAM,UAAU,EAAE,CACjC;;CAEH,MAAMA,eAAiC,EAAE;CAIzC,MAAMC,uBAAiC,EAAE;AACzC,MAAK,MAAM,QAAQ,OAAO,SAAS;AACjC,MAAI,KAAK,SAAS,iBAAiB;AACjC,oBAAiB,mCAAmC;IAClD,QAAQ,eAAe;IACvB,MAAM;IACP,CAAC;AACF,wBAAqB,KAAK,0BAA0B,MAAM,eAAe,GAAG,CAAC;;EAE/E,MAAM,cAAc,2BAA2B,MAAM,eAAe,GAAG;AACvE,MAAI,gBAAgB,OAAW,cAAa,KAAK,YAAY;;CAE/D,MAAM,aAAa,OAAO,WAAW,EAAE,EAAE,QACtC,MAA2C,EAAE,SAAS,OACxD;CAKD,MAAMC,iBAA2B,EAAE;AACnC,MAAK,MAAM,QAAQ,OAAO,WAAW,EAAE,CACrC,SAAQ,KAAK,MAAb;EACE,KAAK;AACH,kBAAe,KAAK,KAAK,KAAK;AAC9B;EACF,KAAK;EACL,KAAK;AACH,kBAAe,KAAK,mBAAmB,KAAK,MAAM,KAAK,UAAU,KAAK,KAAK,CAAC;AAC5E;EACF,KAAK;AACH,OAAI,KAAK,SAAS,SAAS,OACzB,gBAAe,KAAK,KAAK,SAAS,KAAK;YAC9B,KAAK,SAAS,SAAS,OAChC,gBAAe,KACb,aAAa,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS,YAAY,2BAA2B,KAAK,kBAAkB,KAAK,SAAS,KAAK,CAAC,kCACnI;OAED,gBAAe,KAAK,YAAY,KAAK,SAAS,MAAM;AAEtD;EAEF,KAAK,gBAEH;;CAGN,MAAM,mBAAmB,CAAC,GAAG,gBAAgB,GAAG,qBAAqB,CAAC,KAAK,KAAK;AAEhF,KAAI,OAAO,sBAAsB,OAC/B,KAAI,iBAAiB,QAAW;EAC9B,MAAM,aAAa,aAAa,UAAU,OAAO,kBAAkB;AACnE,MAAI,WAAW,SAAS;AACtB,oBAAiB,wCAAwC;IACvD,QAAQ,eAAe;IACvB,MAAM;IACP,CAAC;GACF,MAAM,aAAa,CAAC,GAAG,aAAa;AACpC,OAAI,UAAU,WAAW,EACvB,YAAW,KAAK;IAAE,MAAM;IAAQ,MAAM,KAAK,UAAU,OAAO,kBAAkB;IAAE,CAAC;AAEnF,UAAO,OAAO,OAAO;IACnB,QAAQ,WAAW;IACnB,cAAc,OAAO,OAAO,WAAW;IACxC,CAAC;;AAEJ,mBAAiB,mDAAmD;GAClE,QAAQ,eAAe;GACvB,MAAM;GACP,CAAC;AAIF,OAAK,SACH,QACA,2EACA;GAAE,QAAQ,eAAe;GAAI,MAAM;GAAU,CAC9C;EACD,MAAM,gBAAgB,CAAC,GAAG,aAAa;AACvC,MAAI,UAAU,WAAW,EACvB,eAAc,KAAK;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,OAAO,kBAAkB;GAAE,CAAC;AAEtF,SAAO,OAAO,OAAO;GACnB,QACE,iBAAiB,SAAS,IAAI,mBAAmB,KAAK,UAAU,OAAO,kBAAkB;GAC3F,cAAc,OAAO,OAAO,cAAc;GAC3C,CAAC;QACG;AACL,mBAAiB,wCAAwC;GACvD,QAAQ,eAAe;GACvB,MAAM;GACP,CAAC;EACF,MAAM,aAAa,CAAC,GAAG,aAAa;AACpC,MAAI,UAAU,WAAW,EACvB,YAAW,KAAK;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,OAAO,kBAAkB;GAAE,CAAC;AAEnF,SAAO,OAAO,OAAO;GACnB,QAAQ,OAAO;GACf,cAAc,OAAO,OAAO,WAAW;GACxC,CAAC;;AAIN,QAAO,OAAO,OAAO;EACnB,QAAQ;EACR,cAAc,OAAO,OAAO,aAAa;EAC1C,CAAC;;AAGJ,SAAS,2BACP,MACA,UAC4B;AAC5B,SAAQ,KAAK,MAAb;EACE,KAAK,OACH,QAAO;GAAE,MAAM;GAAQ,MAAM,KAAK;GAAM;EAC1C,KAAK,QACH,QAAO;GACL,MAAM;GACN,OAAO,aAAa,KAAK,KAAK;GAC9B,UAAU,KAAK;GAChB;EACH,KAAK,QACH,QAAO;GACL,MAAM;GACN,OAAO,aAAa,KAAK,KAAK;GAC9B,UAAU,KAAK;GAChB;EACH,KAAK,YAAY;GACf,MAAM,OAAO,KAAK,SAAS;AAC3B,OAAI,SAAS,OACX,QAAO;IAAE,MAAM;IAAQ;IAAM;GAE/B,MAAM,OAAO,KAAK,SAAS;AAC3B,OAAI,SAAS,OACX,QAAO;IACL,MAAM;IACN,MAAM,aAAa,KAAK;IACxB,UAAU,KAAK,SAAS,YAAY;IACrC;AAEH,UAAO;IAAE,MAAM;IAAQ,MAAM,YAAY,KAAK,SAAS;IAAO;;EAEhE,KAAK,gBACH,QAAO;GAAE,MAAM;GAAQ,MAAM,0BAA0B,MAAM,SAAS;GAAE;;;;;;;;;;;;AAa9E,SAAS,0BACP,MACA,UACQ;CACR,MAAM,QAAQ,KAAK,UAAU,UAAa,KAAK,MAAM,WAAW,IAAI,KAAK,OAAO,KAAK;CACrF,MAAMC,OAAiB,EAAE;AACzB,KAAI,KAAK,aAAa,OAAW,MAAK,KAAK,KAAK,SAAS;AACzD,KAAI,KAAK,SAAS,OAAW,MAAK,KAAK,GAAG,KAAK,KAAK,QAAQ;AAI5D,QAAO,mBAAmB,QAHV,KAAK,WAAW,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,CAAC,KAE5D,KAAK,gBAAgB,UAAa,KAAK,YAAY,WAAW,IAAI,KAAK,MAAM,KAAK,cACnC,gEAAgE,qBAAqB,UAAU,KAAK,IAAI;;;;;;;;AAS3J,SAAgB,qBAAqB,UAAkB,KAAqB;AAC1E,QAAO,OAAO,SAAS,GAAG;;;AAI5B,SAAS,kBAAkB,QAAwB;CACjD,MAAM,QAAQ,KAAK,MAAO,OAAO,SAAS,IAAK,EAAE;AACjD,QAAO,SAAS,OAAO,GAAG,KAAK,MAAM,QAAQ,KAAK,CAAC,MAAM,GAAG,MAAM;;;AAIpE,SAAS,mBAAmB,MAAyB,UAAkB,MAAsB;AAC3F,QAAO,IAAI,KAAK,GAAG,SAAS,KAAK,kBAAkB,KAAK,CAAC;;AAG3D,SAAS,aAAa,OAA2B;AAC/C,QAAO,WAAW,KAAK,OAAO,KAAK,OAAO,SAAS,CAAC"}
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 {\n InboundSanitizationPolicy,\n MessageContent,\n ToolReturn,\n ZodLikeSchema,\n} from '@graphorin/core';\nimport { toolReturn } from '@graphorin/core';\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport { applyInboundSanitization } from '@graphorin/tools/inbound';\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 /**\n * Effective per-server inbound-sanitization policy, applied to the\n * `isError` text before it rides into `MCPToolExecutionError` (the\n * executor never sanitizes the error path, so the trust-aware MCP\n * boundary must). Defaults to the MCP-strict\n * `'detect-and-strip-and-wrap'` when omitted.\n */\n readonly inboundSanitization?: InboundSanitizationPolicy;\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 // W-017: the error text is mcp-derived and reaches the model as\n // `ToolError.message` WITHOUT passing the executor's success-path\n // sanitization (`describe(executeError)` never sanitizes - it\n // cannot tell an untrusted MCP error apart from a trusted\n // first-party one). The trust class IS known here, so sanitize at\n // the source with the same policy the adapted tool declares:\n // parity with the success path, including an operator's explicit\n // `pass-through` override.\n let message = errorText;\n if (errorText.length > 0) {\n const outcome = applyInboundSanitization({\n body: errorText,\n policy: args.inboundSanitization ?? 'detect-and-strip-and-wrap',\n trustClass: 'mcp-derived',\n toolName,\n contentOrigin: `mcp:tool-error:${serverIdentity.id}`,\n failClosed: false,\n });\n if (outcome.patternsHit.length > 0) {\n incrementCounter('mcp.tool-error.injection-flagged.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n }\n message = outcome.body;\n }\n throw new MCPToolExecutionError(\n message.length > 0 ? message : `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 toolReturn({ output: validation.data, 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 toolReturn({\n output:\n concatenatedText.length > 0\n ? concatenatedText\n : JSON.stringify(result.structuredContent),\n contentParts: Object.freeze(fallbackParts),\n }),\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 toolReturn({ output: result.structuredContent, contentParts: Object.freeze(finalParts) }),\n );\n }\n }\n\n return Object.freeze(\n toolReturn({ output: concatenatedText, 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 // W-140: the transport-derived id (W-016) routinely contains ':'\n // (localhost:3001) which would break the `mcp:<id>:<uri>` grammar -\n // percent-encode the id segment; the reader decodes before matching.\n // Handles are ephemeral (minted per result), so no migration.\n return `mcp:${encodeURIComponent(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":";;;;;;;;;;;;;AAqDA,SAAgB,gBAAgB,MAAgD;CAC9E,MAAM,EAAE,QAAQ,cAAc,gBAAgB,aAAa;AAK3D,KAAI,OAAO,YAAY,MAAM;EAC3B,MAAM,aAAa,OAAO,WAAW,EAAE,EACpC,QAAQ,MAA2C,EAAE,SAAS,OAAO,CACrE,KAAK,MAAM,EAAE,KAAK,CAClB,KAAK,KAAK;AACb,mBAAiB,6BAA6B;GAC5C,QAAQ,eAAe;GACvB,MAAM;GACP,CAAC;EASF,IAAI,UAAU;AACd,MAAI,UAAU,SAAS,GAAG;GACxB,MAAM,UAAU,yBAAyB;IACvC,MAAM;IACN,QAAQ,KAAK,uBAAuB;IACpC,YAAY;IACZ;IACA,eAAe,kBAAkB,eAAe;IAChD,YAAY;IACb,CAAC;AACF,OAAI,QAAQ,YAAY,SAAS,EAC/B,kBAAiB,0CAA0C;IACzD,QAAQ,eAAe;IACvB,MAAM;IACP,CAAC;AAEJ,aAAU,QAAQ;;AAEpB,QAAM,IAAI,sBACR,QAAQ,SAAS,IAAI,UAAU,aAAa,SAAS,8BACrD,EAAE,UAAU,EAAE,MAAM,UAAU,EAAE,CACjC;;CAEH,MAAMA,eAAiC,EAAE;CAIzC,MAAMC,uBAAiC,EAAE;AACzC,MAAK,MAAM,QAAQ,OAAO,SAAS;AACjC,MAAI,KAAK,SAAS,iBAAiB;AACjC,oBAAiB,mCAAmC;IAClD,QAAQ,eAAe;IACvB,MAAM;IACP,CAAC;AACF,wBAAqB,KAAK,0BAA0B,MAAM,eAAe,GAAG,CAAC;;EAE/E,MAAM,cAAc,2BAA2B,MAAM,eAAe,GAAG;AACvE,MAAI,gBAAgB,OAAW,cAAa,KAAK,YAAY;;CAE/D,MAAM,aAAa,OAAO,WAAW,EAAE,EAAE,QACtC,MAA2C,EAAE,SAAS,OACxD;CAKD,MAAMC,iBAA2B,EAAE;AACnC,MAAK,MAAM,QAAQ,OAAO,WAAW,EAAE,CACrC,SAAQ,KAAK,MAAb;EACE,KAAK;AACH,kBAAe,KAAK,KAAK,KAAK;AAC9B;EACF,KAAK;EACL,KAAK;AACH,kBAAe,KAAK,mBAAmB,KAAK,MAAM,KAAK,UAAU,KAAK,KAAK,CAAC;AAC5E;EACF,KAAK;AACH,OAAI,KAAK,SAAS,SAAS,OACzB,gBAAe,KAAK,KAAK,SAAS,KAAK;YAC9B,KAAK,SAAS,SAAS,OAChC,gBAAe,KACb,aAAa,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS,YAAY,2BAA2B,KAAK,kBAAkB,KAAK,SAAS,KAAK,CAAC,kCACnI;OAED,gBAAe,KAAK,YAAY,KAAK,SAAS,MAAM;AAEtD;EAEF,KAAK,gBAEH;;CAGN,MAAM,mBAAmB,CAAC,GAAG,gBAAgB,GAAG,qBAAqB,CAAC,KAAK,KAAK;AAEhF,KAAI,OAAO,sBAAsB,OAC/B,KAAI,iBAAiB,QAAW;EAC9B,MAAM,aAAa,aAAa,UAAU,OAAO,kBAAkB;AACnE,MAAI,WAAW,SAAS;AACtB,oBAAiB,wCAAwC;IACvD,QAAQ,eAAe;IACvB,MAAM;IACP,CAAC;GACF,MAAM,aAAa,CAAC,GAAG,aAAa;AACpC,OAAI,UAAU,WAAW,EACvB,YAAW,KAAK;IAAE,MAAM;IAAQ,MAAM,KAAK,UAAU,OAAO,kBAAkB;IAAE,CAAC;AAEnF,UAAO,OAAO,OACZ,WAAW;IAAE,QAAQ,WAAW;IAAM,cAAc,OAAO,OAAO,WAAW;IAAE,CAAC,CACjF;;AAEH,mBAAiB,mDAAmD;GAClE,QAAQ,eAAe;GACvB,MAAM;GACP,CAAC;AAIF,OAAK,SACH,QACA,2EACA;GAAE,QAAQ,eAAe;GAAI,MAAM;GAAU,CAC9C;EACD,MAAM,gBAAgB,CAAC,GAAG,aAAa;AACvC,MAAI,UAAU,WAAW,EACvB,eAAc,KAAK;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,OAAO,kBAAkB;GAAE,CAAC;AAEtF,SAAO,OAAO,OACZ,WAAW;GACT,QACE,iBAAiB,SAAS,IACtB,mBACA,KAAK,UAAU,OAAO,kBAAkB;GAC9C,cAAc,OAAO,OAAO,cAAc;GAC3C,CAAC,CACH;QACI;AACL,mBAAiB,wCAAwC;GACvD,QAAQ,eAAe;GACvB,MAAM;GACP,CAAC;EACF,MAAM,aAAa,CAAC,GAAG,aAAa;AACpC,MAAI,UAAU,WAAW,EACvB,YAAW,KAAK;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,OAAO,kBAAkB;GAAE,CAAC;AAEnF,SAAO,OAAO,OACZ,WAAW;GAAE,QAAQ,OAAO;GAAmB,cAAc,OAAO,OAAO,WAAW;GAAE,CAAC,CAC1F;;AAIL,QAAO,OAAO,OACZ,WAAW;EAAE,QAAQ;EAAkB,cAAc,OAAO,OAAO,aAAa;EAAE,CAAC,CACpF;;AAGH,SAAS,2BACP,MACA,UAC4B;AAC5B,SAAQ,KAAK,MAAb;EACE,KAAK,OACH,QAAO;GAAE,MAAM;GAAQ,MAAM,KAAK;GAAM;EAC1C,KAAK,QACH,QAAO;GACL,MAAM;GACN,OAAO,aAAa,KAAK,KAAK;GAC9B,UAAU,KAAK;GAChB;EACH,KAAK,QACH,QAAO;GACL,MAAM;GACN,OAAO,aAAa,KAAK,KAAK;GAC9B,UAAU,KAAK;GAChB;EACH,KAAK,YAAY;GACf,MAAM,OAAO,KAAK,SAAS;AAC3B,OAAI,SAAS,OACX,QAAO;IAAE,MAAM;IAAQ;IAAM;GAE/B,MAAM,OAAO,KAAK,SAAS;AAC3B,OAAI,SAAS,OACX,QAAO;IACL,MAAM;IACN,MAAM,aAAa,KAAK;IACxB,UAAU,KAAK,SAAS,YAAY;IACrC;AAEH,UAAO;IAAE,MAAM;IAAQ,MAAM,YAAY,KAAK,SAAS;IAAO;;EAEhE,KAAK,gBACH,QAAO;GAAE,MAAM;GAAQ,MAAM,0BAA0B,MAAM,SAAS;GAAE;;;;;;;;;;;;AAa9E,SAAS,0BACP,MACA,UACQ;CACR,MAAM,QAAQ,KAAK,UAAU,UAAa,KAAK,MAAM,WAAW,IAAI,KAAK,OAAO,KAAK;CACrF,MAAMC,OAAiB,EAAE;AACzB,KAAI,KAAK,aAAa,OAAW,MAAK,KAAK,KAAK,SAAS;AACzD,KAAI,KAAK,SAAS,OAAW,MAAK,KAAK,GAAG,KAAK,KAAK,QAAQ;AAI5D,QAAO,mBAAmB,QAHV,KAAK,WAAW,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,CAAC,KAE5D,KAAK,gBAAgB,UAAa,KAAK,YAAY,WAAW,IAAI,KAAK,MAAM,KAAK,cACnC,gEAAgE,qBAAqB,UAAU,KAAK,IAAI;;;;;;;;AAS3J,SAAgB,qBAAqB,UAAkB,KAAqB;AAK1E,QAAO,OAAO,mBAAmB,SAAS,CAAC,GAAG;;;AAIhD,SAAS,kBAAkB,QAAwB;CACjD,MAAM,QAAQ,KAAK,MAAO,OAAO,SAAS,IAAK,EAAE;AACjD,QAAO,SAAS,OAAO,GAAG,KAAK,MAAM,QAAQ,KAAK,CAAC,MAAM,GAAG,MAAM;;;AAIpE,SAAS,mBAAmB,MAAyB,UAAkB,MAAsB;AAC3F,QAAO,IAAI,KAAK,GAAG,SAAS,KAAK,kBAAkB,KAAK,CAAC;;AAG3D,SAAS,aAAa,OAA2B;AAC/C,QAAO,WAAW,KAAK,OAAO,KAAK,OAAO,SAAS,CAAC"}
@@ -23,11 +23,17 @@ import { CreateMessageRequestSchema, ElicitRequestSchema, ErrorCode, McpError }
23
23
  * Compute the {@link ClientCapabilities} to advertise on `initialize`,
24
24
  * based on which server-initiated handlers the operator supplied.
25
25
  * Returns `undefined` when none are configured (advertise nothing).
26
+ *
27
+ * Elicitation is advertised as `{ form: {} }` (W-141): the handler
28
+ * supports form-mode only, and the 2025-11-25 spec expresses that as
29
+ * the `form` sub-capability. A bare `{}` is spec-equivalent through
30
+ * the backward-compat preprocess rule, but the explicit shape states
31
+ * what is actually supported (and matches the handler's contract).
26
32
  */
27
33
  function computeClientCapabilities(opts) {
28
34
  if (opts.elicitation === void 0 && opts.sampling === void 0) return void 0;
29
35
  return {
30
- ...opts.elicitation === void 0 ? {} : { elicitation: {} },
36
+ ...opts.elicitation === void 0 ? {} : { elicitation: { form: {} } },
31
37
  ...opts.sampling === void 0 ? {} : { sampling: {} }
32
38
  };
33
39
  }
@@ -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 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
+ {"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 *\n * Elicitation is advertised as `{ form: {} }` (W-141): the handler\n * supports form-mode only, and the 2025-11-25 spec expresses that as\n * the `form` sub-capability. A bare `{}` is spec-equivalent through\n * the backward-compat preprocess rule, but the explicit shape states\n * what is actually supported (and matches the handler's contract).\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: { form: {} } }),\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":";;;;;;;AA0EA;AASA;;;AAAwE,iBATxD,4BAAA,CAAA,CASwD,EAAA,IAAA;;;;;;iBAAlD,eAAA,UAAyB,yBAAyB,QAAQ"}