@graphorin/mcp 0.6.1 → 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.
- package/CHANGELOG.md +43 -0
- package/README.md +64 -3
- package/dist/client/adapt-result.d.ts +9 -1
- package/dist/client/adapt-result.d.ts.map +1 -1
- package/dist/client/adapt-result.js +28 -10
- package/dist/client/adapt-result.js.map +1 -1
- package/dist/client/client-handlers.js +7 -1
- package/dist/client/client-handlers.js.map +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +45 -70
- package/dist/client/client.js.map +1 -1
- package/dist/client/inbound-filters.js +101 -1
- package/dist/client/inbound-filters.js.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +2 -1
- package/dist/client/managed.d.ts +35 -0
- package/dist/client/managed.d.ts.map +1 -0
- package/dist/client/managed.js +136 -0
- package/dist/client/managed.js.map +1 -0
- package/dist/client/mcp-resource-reader.js +1 -1
- package/dist/client/mcp-resource-reader.js.map +1 -1
- package/dist/client/to-tools-run.js +119 -0
- package/dist/client/to-tools-run.js.map +1 -0
- package/dist/client/to-tools.d.ts +8 -0
- package/dist/client/to-tools.d.ts.map +1 -1
- package/dist/client/to-tools.js +27 -4
- package/dist/client/to-tools.js.map +1 -1
- package/dist/client/types.d.ts +12 -3
- package/dist/client/types.d.ts.map +1 -1
- package/dist/errors/index.d.ts +3 -3
- package/dist/errors/index.js +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/helpers/identity.d.ts +11 -0
- package/dist/helpers/identity.d.ts.map +1 -1
- package/dist/helpers/identity.js +13 -2
- package/dist/helpers/identity.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/registry/json-schema.js +26 -8
- package/dist/registry/json-schema.js.map +1 -1
- package/dist/transport/types.d.ts +9 -0
- package/package.json +13 -12
- package/src/client/adapt-result.ts +302 -0
- package/src/client/client-handlers.ts +215 -0
- package/src/client/client.ts +725 -0
- package/src/client/defer-loading.ts +108 -0
- package/src/client/inbound-filters.ts +246 -0
- package/src/client/index.ts +42 -0
- package/src/client/managed.ts +222 -0
- package/src/client/mcp-resource-reader.ts +183 -0
- package/src/client/pinning.ts +48 -0
- package/src/client/to-tools-run.ts +178 -0
- package/src/client/to-tools.ts +294 -0
- package/src/client/transport-factory.ts +117 -0
- package/src/client/types.ts +422 -0
- package/src/errors/index.ts +170 -0
- package/src/helpers/identity.ts +128 -0
- package/src/helpers/index.ts +8 -0
- package/src/helpers/validate-config.ts +95 -0
- package/src/index.ts +49 -0
- package/src/oauth/bridge.ts +143 -0
- package/src/oauth/index.ts +18 -0
- package/src/oauth/library.ts +61 -0
- package/src/registry/json-schema.ts +402 -0
- package/src/transport/index.ts +15 -0
- package/src/transport/types.ts +108 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
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
|
+
|
|
3
46
|
## 0.6.1
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -34,7 +34,15 @@ the existing outbound OAuth subsystem in `@graphorin/security`.
|
|
|
34
34
|
first use and, when a pin store is present, **rejects** silent drift
|
|
35
35
|
by default (`onPinMismatch: 'warn'` downgrades to a warning);
|
|
36
36
|
description-injection heuristics at registration feed the
|
|
37
|
-
`mcp.tool-description.injection-flagged.total` counter.
|
|
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.
|
|
38
46
|
- **Strict default for MCP-derived tools.** Every `Tool` produced
|
|
39
47
|
by `MCPClient.toTools()` defaults to the
|
|
40
48
|
`'detect-and-strip-and-wrap'` inbound prompt-injection
|
|
@@ -63,6 +71,15 @@ the existing outbound OAuth subsystem in `@graphorin/security`.
|
|
|
63
71
|
`'auto-prefix'`), and the optional per-client `priority` field;
|
|
64
72
|
the registry consumes the trio when its strategy-aware
|
|
65
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).
|
|
66
83
|
- **OAuth integration.** `createOAuthAuthorizationProvider({...})`
|
|
67
84
|
wraps the existing `refreshOAuthSession(...)` helper from
|
|
68
85
|
`@graphorin/security/oauth`, resolves the bearer header on every
|
|
@@ -146,7 +163,19 @@ Tool descriptions are always sanitized at registration time using
|
|
|
146
163
|
the `'detect-and-strip'` variant of the configured policy (the
|
|
147
164
|
wrap envelope is reserved for tool-result bodies inside the
|
|
148
165
|
conversation history; the description goes into the per-step tool
|
|
149
|
-
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.
|
|
150
179
|
|
|
151
180
|
## OAuth integration
|
|
152
181
|
|
|
@@ -206,6 +235,38 @@ const issues = await createMCPClient({
|
|
|
206
235
|
});
|
|
207
236
|
```
|
|
208
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
|
+
|
|
209
270
|
## Streamable HTTP sessions
|
|
210
271
|
|
|
211
272
|
When the MCP server assigns an `Mcp-Session-Id` on `initialize`, the
|
|
@@ -300,4 +361,4 @@ MIT - © 2026 Oleksiy Stepurenko.
|
|
|
300
361
|
|
|
301
362
|
---
|
|
302
363
|
|
|
303
|
-
**Project Graphorin** · v0.
|
|
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":";;;;;;;
|
|
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
|
-
|
|
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":"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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 {\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":"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"}
|
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,SAAgB,0BAA0B,MAGP;AACjC,KAAI,KAAK,gBAAgB,UAAa,KAAK,aAAa,OAAW,QAAO;AAC1E,QAAO;EACL,GAAI,KAAK,gBAAgB,SAAY,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,EAAE;EACvE,GAAI,KAAK,aAAa,SAAY,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE;EACxD;;;;;;;;AASH,SAAgB,8BACd,WACA,MACM;CACN,MAAM,EAAE,gBAAgB;AAExB,KAAI,KAAK,gBAAgB,QAAW;EAClC,MAAM,UAAU,KAAK;AACrB,YAAU,kBAAkB,qBAAqB,OAAO,SAAS,UAAU;AACzE,oBAAiB,mCAAmC,EAAE,QAAQ,YAAY,SAAS,CAAC;GACpF,MAAM,SAAS,QAAQ;AAMvB,OAAK,OAAuC,SAAS,OAAO;AAC1D,qBAAiB,kCAAkC;KACjD,QAAQ,YAAY;KACpB,QAAQ;KACT,CAAC;AACF,UAAM,IAAI,SACR,UAAU,gBACV,oFACD;;GAEH,MAAM,kBACJ,qBAAqB,UAAU,OAAO,oBAAoB,SACrD,OAAO,kBACR;IAAE,MAAM;IAAU,YAAY,EAAE;IAAE;GACxC,MAAM,SAAS,MAAM,QACnB;IAAE,SAAS,OAAO;IAAS;IAAiB,EAC5C,EAAE,QAAQ,MAAM,QAAQ,CACzB;AACD,OAAI,OAAO,WAAW,UAAU;AAC9B,qBAAiB,kCAAkC,EAAE,QAAQ,YAAY,SAAS,CAAC;AACnF,WAAO,OAAO,YAAY,SACtB,EAAE,QAAQ,UAAU,GACpB;KAAE,QAAQ;KAAU,SAAS,OAAO;KAAS;;AAEnD,oBAAiB,kCAAkC;IACjD,QAAQ,YAAY;IACpB,QAAQ,OAAO;IAChB,CAAC;AACF,UAAO,EAAE,QAAQ,OAAO,QAAQ;IAChC;;AAGJ,KAAI,KAAK,aAAa,QAAW;EAC/B,MAAM,UAAU,KAAK;AACrB,YAAU,kBAAkB,4BAA4B,OAAO,SAAS,UAAU;AAChF,oBAAiB,gCAAgC,EAAE,QAAQ,YAAY,SAAS,CAAC;GACjF,MAAM,IAAI,QAAQ;GAMlB,MAAM,YAAY;AAClB,OAAI,UAAU,UAAU,UAAa,UAAU,eAAe,OAC5D,OAAM,IAAI,SACR,UAAU,gBACV,2GACD;GAEH,MAAM,KAAK,EAAE;GA4Bb,MAAM,SAAS,MAAM,QA3BuB;IAC1C,UAAU,EAAE,SAAS,KAAK,OAAO;KAC/B,MAAM,EAAE;KAGR,SAAS,cAAc,EAAE,QAAQ,CAAC,KAAK,UAAU,mBAAmB,MAAM,CAAC;KAC5E,EAAE;IACH,WAAW,EAAE;IACb,GAAI,EAAE,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc,EAAE,cAAc;IACxE,GAAI,EAAE,gBAAgB,SAAY,EAAE,GAAG,EAAE,aAAa,EAAE,aAAa;IACrE,GAAI,EAAE,kBAAkB,SAAY,EAAE,GAAG,EAAE,eAAe,EAAE,eAAe;IAC3E,GAAI,OAAO,SACP,EAAE,GACF,EACE,kBAAkB;KAChB,GAAI,GAAG,UAAU,SACb,EAAE,GACF,EAAE,OAAO,GAAG,MAAM,KAAK,MAAO,EAAE,SAAS,SAAY,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAE,EAAE;KAClF,GAAI,GAAG,iBAAiB,SAAY,EAAE,GAAG,EAAE,cAAc,GAAG,cAAc;KAC1E,GAAI,GAAG,kBAAkB,SAAY,EAAE,GAAG,EAAE,eAAe,GAAG,eAAe;KAC7E,GAAI,GAAG,yBAAyB,SAC5B,EAAE,GACF,EAAE,sBAAsB,GAAG,sBAAsB;KACtD,EACF;IACL,GAAI,EAAE,mBAAmB,SAAY,EAAE,GAAG,EAAE,gBAAgB,EAAE,gBAAgB;IAC/E,EAC6C,EAAE,QAAQ,MAAM,QAAQ,CAAC;AACvE,oBAAiB,gCAAgC,EAAE,QAAQ,YAAY,SAAS,CAAC;AACjF,UAAO;IACL,MAAM,OAAO;IACb,SAAS,OAAO;IAChB,OAAO,OAAO;IACd,GAAI,OAAO,eAAe,SAAY,EAAE,GAAG,EAAE,YAAY,OAAO,YAAY;IAC7E;IACD;;;;AAKN,SAAS,cACP,SACkE;CAElE,MAAM,UADM,MAAM,QAAQ,QAAQ,GAAG,UAAU,CAAC,QAAQ,EACrC,QAChB,MACC,MAAM,QAAQ,OAAO,MAAM,SAC9B;AACD,QAAO,OAAO,SAAS,IAAI,SAAS,CAAC;EAAE,MAAM;EAAQ,MAAM;EAAI,CAAC;;;AAIlE,SAAS,mBAAmB,OAGL;AACrB,KAAI,MAAM,SAAS,QACjB,QAAO;EACL,MAAM;EACN,MAAM,OAAO,MAAM,QAAQ,GAAG;EAC9B,UAAU,OAAO,MAAM,YAAY,2BAA2B;EAC/D;AAEH,KAAI,MAAM,SAAS,QACjB,QAAO;EACL,MAAM;EACN,MAAM,OAAO,MAAM,QAAQ,GAAG;EAC9B,UAAU,OAAO,MAAM,YAAY,2BAA2B;EAC/D;AAEH,KAAI,MAAM,SAAS,OACjB,QAAO;EAAE,MAAM;EAAQ,MAAM,OAAO,MAAM,QAAQ,GAAG;EAAE;AAEzD,QAAO;EAAE,MAAM;EAAQ,MAAM,KAAK,UAAU,MAAM;EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"client.d.ts","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":[],"mappings":";;;;;;;AA0EA;AASA;;;AAAwE,iBATxD,4BAAA,CAAA,CASwD,EAAA,IAAA;;;;;;iBAAlD,eAAA,UAAyB,yBAAyB,QAAQ"}
|