@cyanheads/mcp-ts-core 0.13.9 → 0.13.10
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/AGENTS.md +36 -13
- package/CLAUDE.md +36 -13
- package/README.md +9 -9
- package/changelog/0.13.x/0.13.10.md +118 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +15 -1
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +89 -20
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +40 -0
- package/dist/core/context.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/worker.d.ts +6 -0
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +1 -0
- package/dist/core/worker.js.map +1 -1
- package/dist/linter/rules/error-contract-rules.d.ts +3 -44
- package/dist/linter/rules/error-contract-rules.d.ts.map +1 -1
- package/dist/linter/rules/error-contract-rules.js +8 -144
- package/dist/linter/rules/error-contract-rules.js.map +1 -1
- package/dist/linter/rules/index.d.ts +1 -1
- package/dist/linter/rules/index.d.ts.map +1 -1
- package/dist/linter/rules/index.js +1 -1
- package/dist/linter/rules/index.js.map +1 -1
- package/dist/linter/rules/resource-rules.d.ts.map +1 -1
- package/dist/linter/rules/resource-rules.js +1 -2
- package/dist/linter/rules/resource-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +1 -2
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +26 -13
- package/dist/mcp-server/handlerContext.d.ts.map +1 -1
- package/dist/mcp-server/handlerContext.js +32 -17
- package/dist/mcp-server/handlerContext.js.map +1 -1
- package/dist/mcp-server/inputRequired.d.ts +120 -8
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +177 -12
- package/dist/mcp-server/inputRequired.js.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.d.ts +10 -2
- package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +49 -11
- package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
- package/dist/mcp-server/resources/resource-registration.d.ts +4 -2
- package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
- package/dist/mcp-server/resources/resource-registration.js +6 -4
- package/dist/mcp-server/resources/resource-registration.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +4 -3
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +42 -14
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/server.d.ts +9 -0
- package/dist/mcp-server/server.d.ts.map +1 -1
- package/dist/mcp-server/server.js +14 -13
- package/dist/mcp-server/server.js.map +1 -1
- package/dist/mcp-server/tools/tool-registration.d.ts +7 -3
- package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-registration.js +9 -5
- package/dist/mcp-server/tools/tool-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +17 -9
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +91 -50
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js +7 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
- package/dist/testing/index.d.ts +17 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +21 -7
- package/dist/testing/index.js.map +1 -1
- package/dist/types-global/errors.d.ts +18 -15
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +5 -4
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +9 -7
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +3 -1
- package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
- package/dist/utils/internal/performance.d.ts +4 -2
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +8 -6
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/internal/telemetryMessages.d.ts +0 -1
- package/dist/utils/internal/telemetryMessages.d.ts.map +1 -1
- package/dist/utils/internal/telemetryMessages.js +0 -1
- package/dist/utils/internal/telemetryMessages.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +5 -4
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +5 -4
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-service/SKILL.md +3 -12
- package/framework-skills/add-test/SKILL.md +6 -3
- package/framework-skills/add-tool/SKILL.md +29 -33
- package/framework-skills/api-config/SKILL.md +2 -1
- package/framework-skills/api-context/SKILL.md +155 -40
- package/framework-skills/api-errors/SKILL.md +43 -47
- package/framework-skills/api-linter/SKILL.md +5 -29
- package/framework-skills/api-telemetry/SKILL.md +11 -7
- package/framework-skills/api-testing/SKILL.md +43 -11
- package/framework-skills/api-workers/SKILL.md +3 -1
- package/framework-skills/design-mcp-server/SKILL.md +5 -5
- package/framework-skills/field-test/SKILL.md +2 -2
- package/framework-skills/polish-docs-meta/SKILL.md +2 -2
- package/framework-skills/release-and-publish/SKILL.md +3 -3
- package/framework-skills/release-pr-review/SKILL.md +2 -2
- package/framework-skills/security-pass/SKILL.md +8 -7
- package/package.json +4 -3
- package/scripts/install-otel.ts +84 -0
- package/scripts/lint-packaging.ts +188 -27
- package/templates/.env.example +2 -0
- package/templates/AGENTS.md +5 -4
- package/templates/CLAUDE.md +5 -4
- package/templates/Dockerfile +67 -50
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +3 -8
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Package:** `@cyanheads/mcp-ts-core`
|
|
4
|
-
**Version:** 0.13.
|
|
4
|
+
**Version:** 0.13.10
|
|
5
5
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
6
6
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.1.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.6.5
|
|
@@ -45,7 +45,7 @@ Both paths share the same public API. Init copies starter `package.json`, config
|
|
|
45
45
|
|
|
46
46
|
| Subpath | Key Exports | Purpose |
|
|
47
47
|
|:--------|:------------|:--------|
|
|
48
|
-
| `@cyanheads/mcp-ts-core` | `createApp`, `tool`, `resource`, `prompt`, `appTool`, `appResource`, `APP_RESOURCE_MIME_TYPE`, `headerParam`, `Context`, `createFail`, `createRecoveryFor`, `TypedFail`, `TypedRecoveryFor`, `ReasonOf`, `HandlerContext`, `Enrich`, `EnrichHelpers`, `TypedEnrich`, `ContentCollect`, `ContentBlock`, `z`, `inputRequired`, `completable`, `isCompletable`, `CompleteCallback`, `CompleteResourceTemplateCallback`, `CacheHint`, `CacheHints`, `CacheScope`, `SessionMode`, `ResolvedSessionMode` | Main entry point |
|
|
48
|
+
| `@cyanheads/mcp-ts-core` | `createApp`, `tool`, `resource`, `prompt`, `appTool`, `appResource`, `APP_RESOURCE_MIME_TYPE`, `headerParam`, `Context`, `ClientCapabilities`, `createFail`, `createRecoveryFor`, `TypedFail`, `TypedRecoveryFor`, `ReasonOf`, `HandlerContext`, `Enrich`, `EnrichHelpers`, `TypedEnrich`, `ContentCollect`, `ContentBlock`, `z`, `inputRequired`, `completable`, `isCompletable`, `CompleteCallback`, `CompleteResourceTemplateCallback`, `CacheHint`, `CacheHints`, `CacheScope`, `SessionMode`, `ResolvedSessionMode` | Main entry point |
|
|
49
49
|
| `/worker` | `createWorkerHandler`, `CloudflareBindings` | Cloudflare Workers entry |
|
|
50
50
|
| `/tools` | `ToolDefinition`, `AnyToolDefinition`, `ToolAnnotations` | Tool definition types |
|
|
51
51
|
| `/resources` | `ResourceDefinition`, `AnyResourceDefinition` | Resource definition types |
|
|
@@ -300,10 +300,11 @@ interface Context {
|
|
|
300
300
|
readonly traceId?: string;
|
|
301
301
|
readonly spanId?: string;
|
|
302
302
|
readonly auth?: AuthContext;
|
|
303
|
+
readonly clientCapabilities: ClientCapabilities | undefined; // what the client declared; undefined when no view (2025-era stateless HTTP)
|
|
303
304
|
readonly log: ContextLogger; // auto-correlated: requestId, traceId, tenantId
|
|
304
305
|
readonly state: ContextState; // tenant-scoped KV storage
|
|
305
306
|
readonly requestInput: RequestInputFn; // (spec, options?) => never — suspends and asks the caller for input
|
|
306
|
-
readonly inputs: ContextInputs; //
|
|
307
|
+
readonly inputs: ContextInputs; // the request's responses, limited to what the client declared
|
|
307
308
|
readonly notifyPromptListChanged?: (() => void) | undefined; // prompt list changed
|
|
308
309
|
readonly notifyResourceListChanged?: (() => void) | undefined; // resource list changed
|
|
309
310
|
readonly notifyResourceUpdated?: ((uri: string) => void) | undefined; // resource content changed
|
|
@@ -312,7 +313,7 @@ interface Context {
|
|
|
312
313
|
readonly uri?: URL; // present for resource handlers
|
|
313
314
|
readonly content: ContentCollect; // media blocks → prepended to content[]; never in structuredContent
|
|
314
315
|
readonly enrich: Enrich; // success-path agent context → structuredContent + content[]; typed on HandlerContext<R, E>
|
|
315
|
-
recoveryFor(reason: string): { recovery: { hint: string } } | Record<string, never>; //
|
|
316
|
+
recoveryFor(reason: string): { recovery: { hint: string } } | Record<string, never>; // a declared entry's hint, in wire shape
|
|
316
317
|
}
|
|
317
318
|
```
|
|
318
319
|
|
|
@@ -367,11 +368,33 @@ useFormat(answer.format);
|
|
|
367
368
|
cancelled prompt is terminal, not a round to retry. `inputRequired.elicitUrl({ message, url })`
|
|
368
369
|
hands the user an external link instead of a form.
|
|
369
370
|
|
|
371
|
+
A client can send responses on a call nothing asked for, so only what it declared reaches
|
|
372
|
+
`ctx.inputs`, at the mode level the request would need: an elicit result needs `elicitation` —
|
|
373
|
+
`elicitation.form` when it carries `content` — a sampling result `sampling` (`sampling.tools` when it
|
|
374
|
+
holds a tool block), a roots result `roots`, and a request with no capability view gets none.
|
|
375
|
+
`ctx.clientCapabilities` — the SDK-parsed `initialize` value on 2025-era connections (a bare
|
|
376
|
+
`elicitation: {}` reads back as `{ elicitation: { form: {} } }`), the request's envelope as sent on
|
|
377
|
+
2026-07-28 — decides whether to *ask* for optional context (request roots only when `roots` is
|
|
378
|
+
declared, else fall through); it is never a reason to skip a consent prompt.
|
|
379
|
+
|
|
370
380
|
A 2025-era client that declared no matching capability is refused as `client_capability_missing`,
|
|
371
381
|
with a hint that ends at reconnecting. When the tool's own arguments can stand in for the answer,
|
|
372
382
|
say so per call — `ctx.requestInput(spec, { fallbackHint: 'Or call again with noun supplied.' })` —
|
|
373
383
|
and the sentence is appended to that hint. A consent gate passes none: it has no such field.
|
|
374
384
|
|
|
385
|
+
**Consent gates redeem a server record.** A capable client can still pre-answer, and any
|
|
386
|
+
`requestState` can be replayed within its lifetime, so a destructive handler first redeems (reads and
|
|
387
|
+
deletes) a `ctx.state` record `{ operation, clientId, subject, target, contentHash }` stored under a
|
|
388
|
+
random id — the id is all `requestState` carries — and asks again on an unknown, used, or expired id,
|
|
389
|
+
or when any field differs from this call. Carrying the target in `requestState` and comparing on
|
|
390
|
+
re-entry is replayable. Redeeming stops a sequential replay, not concurrent retries: until an atomic
|
|
391
|
+
`ctx.state.take` exists (#593), an action that must not repeat is made idempotent per record. The
|
|
392
|
+
record's storage is shared by every instance a 2026-07-28 retry can reach — `filesystem`, `supabase`,
|
|
393
|
+
or `cloudflare-d1`, never `cloudflare-kv`. `MCP_REQUEST_STATE_KEY` (≥ 32 bytes, the same on every
|
|
394
|
+
instance) opts into sealing: the framework signs the string a handler returns, the SDK rejects any
|
|
395
|
+
other as `-32602` `invalid_request_state` before the handler runs, and `ctx.inputs.state()` still
|
|
396
|
+
returns the original string. See `api-context` for the full pattern.
|
|
397
|
+
|
|
375
398
|
### `ctx.content`
|
|
376
399
|
|
|
377
400
|
Accumulates non-text content blocks — image/audio bytes, embedded resources, resource links — onto the response: `ctx.content.image(data, mimeType)`, `ctx.content.audio(data, mimeType)`, or `ctx.content(block)` for a raw `ContentBlock`. Blocks are prepended to `content[]` after `format()` runs and never enter `structuredContent`, so a handler can emit media for the calling model without the base64 duplicating into typed output. Always present (no-op when unused); callable from handler and service layer.
|
|
@@ -382,7 +405,7 @@ See `api-context` skill for full details.
|
|
|
382
405
|
|
|
383
406
|
## Error Handling
|
|
384
407
|
|
|
385
|
-
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the
|
|
408
|
+
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the wire hint for its reason: when a failure whose `data.reason` names the entry reaches the tool or resource factory without `data.recovery`, the framework fills `data.recovery.hint` from it — a bare `ctx.fail('reason')` and a service throw carrying `{ reason }` alike, matched on the reason alone — and mirrors it into `content[]` text. Override with explicit `{ recovery: { hint: '...' } }` when runtime context matters; a throw-site `recovery` always wins, and the thrown `McpError` itself is never changed. Optional `severity` (`debug`/`info`/`notice`/`warning`) logs a modeled outcome below `error` — tools only, logging only: the wire envelope, the span status, and the `mcp.tool.*` metrics are untouched. The framework's own `invalid_arguments` and `client_capability_missing` refusals log at `notice` unless an entry naming them declares otherwise. Optional `thrownBy: 'service'` marks an entry the service layer produces, so `error-contract-unthrown` skips it while still checking the handler's own reasons — lint-only metadata, nothing at runtime reads it.
|
|
386
409
|
|
|
387
410
|
```ts
|
|
388
411
|
errors: [
|
|
@@ -394,8 +417,8 @@ errors: [
|
|
|
394
417
|
recovery: 'Wait 30 seconds before retrying or reduce batch size.' },
|
|
395
418
|
],
|
|
396
419
|
async handler(input, ctx) {
|
|
397
|
-
// Static recovery —
|
|
398
|
-
if (queue.full()) throw ctx.fail('queue_full'
|
|
420
|
+
// Static recovery — the framework fills the contract's hint onto the wire.
|
|
421
|
+
if (queue.full()) throw ctx.fail('queue_full');
|
|
399
422
|
// Dynamic recovery — interpolate runtime context, override the contract default.
|
|
400
423
|
if (!matched) throw ctx.fail('no_match', `No data for ${input.pmids.length} PMIDs`, {
|
|
401
424
|
pmids: input.pmids,
|
|
@@ -404,7 +427,7 @@ async handler(input, ctx) {
|
|
|
404
427
|
}
|
|
405
428
|
```
|
|
406
429
|
|
|
407
|
-
**`ctx.recoveryFor(reason)`** returns `{}` when no contract exists (spread-safe). Typed against the declared reason union on `HandlerContext<R>`.
|
|
430
|
+
**`ctx.recoveryFor(reason)`** returns the entry's hint in wire shape, `{}` when no contract exists (spread-safe). Typed against the declared reason union on `HandlerContext<R>`. Not needed to put a declared hint on the wire — the fill does that — only when the hint must ride the thrown error itself, as in a test of the handler's own throw.
|
|
408
431
|
|
|
409
432
|
**Contracts are inline, per-tool.** Don't extract shared `errors[]` constants — locality is the point, and dynamic `recovery` hints need tool-specific context. Declare domain-specific failures only; **baseline codes** (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) are auto-allowed by conformance lint. The lint scans handler source only — service-layer throws still reach clients via auto-classification.
|
|
410
433
|
|
|
@@ -422,9 +445,9 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
|
|
|
422
445
|
|
|
423
446
|
**Auto-classification.** Plain `Error`, `ZodError`, and any other thrown value are caught and classified automatically. Resolution order: request signal already aborted (→ `RequestCancelled`, outranking the thrown value's own code, `McpError` included) → `McpError` code (preserved as-is) → SDK `ConnectionClosed` (→ `RequestCancelled`) → engine resource-limit `RangeError` by whole message — stack overflow, maximum string size (→ `InternalError`) → JS constructor name (`SyntaxError` → `ValidationError`; `TypeError` is excluded) → provider patterns (HTTP status codes, AWS errors, DB errors) → common message patterns → `AbortError` name (→ `Timeout`) → `InternalError` fallback. A result that breaks the definition's own `output` or `enrichment` schema fails as `InternalError` naming that contract, not `ValidationError`.
|
|
424
447
|
|
|
425
|
-
**Error-path parity.** Tool errors: `content[]` carries `Error: <message>`, then `Recovery: <hint>` when the hint says something the message does not already contain, then a closing `(reason … · not retryable)` for whichever of `data.reason` / `data.retryable` is present; the numeric code and `data.issues` stay JSON-only. `structuredContent.error` carries `{ code, message, data? }`. No `_meta.error`. Resources re-throw via JSON-RPC error envelope. An argument rejection is one of them: `-32602` with `data.issues`, plus `data.reason: 'invalid_arguments'` and a hint synthesized from the issues and the root schema — never a tool-declared `reason`, since the handler never ran. When pre-validation rewrote or dropped a key the caller wrote, `data.input` (`{ aliased: [{ alias, target }], ignored }`) names it and the hint closes with `Validated … as ….` / `Dropped undeclared key ….`; an ignore-list drop is never reported. `client_capability_missing` is the second framework-owned reason: a `ctx.requestInput` return a 2025-era connection cannot serve is refused before any wire traffic as `-32600` carrying that reason and a hint naming the capability.
|
|
448
|
+
**Error-path parity.** Tool errors: `content[]` carries `Error: <message>`, then `Recovery: <hint>` when the hint says something the message does not already contain, then a closing `(reason … · not retryable · request <id>)` for whichever of `data.reason` / `data.retryable` / `data.requestId` is present; the numeric code and `data.issues` stay JSON-only. `structuredContent.error` carries `{ code, message, data? }`. No `_meta.error`. Resources re-throw via JSON-RPC error envelope. An argument rejection is one of them: `-32602` with `data.issues`, plus `data.reason: 'invalid_arguments'` and a hint synthesized from the issues and the root schema — never a tool-declared `reason`, since the handler never ran. When pre-validation rewrote or dropped a key the caller wrote, `data.input` (`{ aliased: [{ alias, target }], ignored }`) names it and the hint closes with `Validated … as ….` / `Dropped undeclared key ….`; an ignore-list drop is never reported. `client_capability_missing` is the second framework-owned reason: a `ctx.requestInput` return a 2025-era connection cannot serve is refused before any wire traffic as `-32600` carrying that reason and a hint naming the capability. **`data.requestId`** is set on every error envelope the framework builds — tool results, resource reads, prompts, `httpErrorHandler`'s JSON-RPC errors — equal to the `requestId` of that call's log records: a generated token, or the client's JSON-RPC id when it is a string (`httpErrorHandler` always generates its own). A resource read refused before it is measured (auth, `params`) carries an id no record shares. It replaces a thrown `data.requestId`, is never added to the thrown `McpError`, and is left off a resource `-32602` whose `data` is exactly `{ uri }` and off `runToolContract` results.
|
|
426
449
|
|
|
427
|
-
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names, unless marked `thrownBy: 'service'`)
|
|
450
|
+
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names, unless marked `thrownBy: 'service'`). See `api-linter` skill.
|
|
428
451
|
|
|
429
452
|
See `api-errors` skill for the full pattern-matching table, error code reference, and detailed examples.
|
|
430
453
|
|
|
@@ -458,7 +481,7 @@ Managed by `@cyanheads/mcp-ts-core`. Validated via Zod. Precedence: `createApp()
|
|
|
458
481
|
| Category | Key Variables |
|
|
459
482
|
|:---------|:-------------|
|
|
460
483
|
| Transport | `MCP_TRANSPORT_TYPE` (`stdio`\|`http`), `MCP_HTTP_PORT`, `MCP_HTTP_HOST`, `MCP_HTTP_ENDPOINT_PATH` |
|
|
461
|
-
| Auth | `MCP_AUTH_MODE`, `MCP_AUTH_SECRET_KEY`, `MCP_AUTH_DISABLE_SCOPE_CHECKS`, `OAUTH_
|
|
484
|
+
| Auth | `MCP_AUTH_MODE`, `MCP_AUTH_SECRET_KEY`, `MCP_AUTH_DISABLE_SCOPE_CHECKS`, `OAUTH_*`, `MCP_REQUEST_STATE_KEY` (opt-in `requestState` sealing) |
|
|
462
485
|
| Storage | `STORAGE_PROVIDER_TYPE` (`in-memory`\|`filesystem`\|`supabase`\|`cloudflare-r2`\|`cloudflare-kv`\|`cloudflare-d1`) |
|
|
463
486
|
| LLM | `OPENROUTER_API_KEY`, `OPENROUTER_APP_URL/NAME`, `LLM_DEFAULT_*` |
|
|
464
487
|
| Telemetry | `OTEL_ENABLED`, `OTEL_SERVICE_NAME/VERSION`, `OTEL_EXPORTER_OTLP_*` |
|
|
@@ -485,7 +508,7 @@ describe('myTool', () => {
|
|
|
485
508
|
});
|
|
486
509
|
```
|
|
487
510
|
|
|
488
|
-
**`createMockContext` options:** `createMockContext()` (state included), `{ tenantId: 'test-tenant' }` (explicit tenant; defaults to `'default'`, as stdio resolves it), `{ errors: myTool.errors }` (typed `ctx.fail`), `{ inputResponses }` / `{ requestState }` (seed `ctx.inputs` to drive a multi-round-trip handler's second round directly), plus `auth`, `sessionId`, `signal`, `requestId`, `uri`, and the four `notify*` callbacks.
|
|
511
|
+
**`createMockContext` options:** `createMockContext()` (state included), `{ tenantId: 'test-tenant' }` (explicit tenant; defaults to `'default'`, as stdio resolves it), `{ errors: myTool.errors }` (typed `ctx.fail`), `{ inputResponses }` / `{ requestState }` (seed `ctx.inputs` to drive a multi-round-trip handler's second round directly), `{ clientCapabilities }` (seeds `ctx.clientCapabilities` and filters the seeded responses to the declared kinds, as production does — omitted, nothing is filtered), plus `auth`, `sessionId`, `signal`, `requestId`, `uri`, and the four `notify*` callbacks.
|
|
489
512
|
|
|
490
513
|
**`ctx.state` in tests is the production path.** The mock backs it with a real `StorageService` over an `InMemoryProvider`, so key validation (`[a-zA-Z0-9_.\-/]+` — colons rejected), the JSON round-trip of every value, and TTL expiry behave exactly as they do in a deployment. Passing `errors` narrows the return type to `HandlerContext<ReasonOf<…>>`, which is what a definition declaring a contract types its handler's `ctx` as — so `definition.handler(input, ctx)` typechecks.
|
|
491
514
|
|
|
@@ -495,7 +518,7 @@ describe('myTool', () => {
|
|
|
495
518
|
|
|
496
519
|
**Fixture-based tests:** `mcpTest` from `/testing/vitest` (optional peer `vitest`) extends Vitest's `test` with per-test fixtures: `ctx` (fresh mock context), `session` (session-bound context), `fetchMock` (strict fetch fake, installed/restored around the test), and `storage` (fresh in-memory `StorageService`). Override fixtures via `.extend` with the function form only — a bare value would share one mutable context across every test in the file.
|
|
497
520
|
|
|
498
|
-
**Tool contracts:** `runToolContract(definition, input)` from `/testing` validates input/output, invokes the handler, formats content, and returns production-shaped success/error surfaces without transport auth or
|
|
521
|
+
**Tool contracts:** `runToolContract(definition, input)` from `/testing` validates input/output, invokes the handler, formats content, and returns production-shaped success/error surfaces — the declared `recovery` fill included — without transport auth, telemetry, or `data.requestId`. `toolContractSuite(definition, { success, errors })` from `/testing/vitest` registers reusable schema/handler/error-envelope conformance cases for a server's own tool definitions.
|
|
499
522
|
|
|
500
523
|
**Fuzz testing:** `fuzzTool`/`fuzzResource`/`fuzzPrompt` from `/testing/fuzz` generate valid and adversarial inputs from Zod schemas via `fast-check`, then assert handler invariants (no crashes, no prototype pollution, no stack trace leaks). Returns a `FuzzReport` for custom assertions.
|
|
501
524
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Package:** `@cyanheads/mcp-ts-core`
|
|
4
|
-
**Version:** 0.13.
|
|
4
|
+
**Version:** 0.13.10
|
|
5
5
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
6
6
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.1.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.6.5
|
|
@@ -45,7 +45,7 @@ Both paths share the same public API. Init copies starter `package.json`, config
|
|
|
45
45
|
|
|
46
46
|
| Subpath | Key Exports | Purpose |
|
|
47
47
|
|:--------|:------------|:--------|
|
|
48
|
-
| `@cyanheads/mcp-ts-core` | `createApp`, `tool`, `resource`, `prompt`, `appTool`, `appResource`, `APP_RESOURCE_MIME_TYPE`, `headerParam`, `Context`, `createFail`, `createRecoveryFor`, `TypedFail`, `TypedRecoveryFor`, `ReasonOf`, `HandlerContext`, `Enrich`, `EnrichHelpers`, `TypedEnrich`, `ContentCollect`, `ContentBlock`, `z`, `inputRequired`, `completable`, `isCompletable`, `CompleteCallback`, `CompleteResourceTemplateCallback`, `CacheHint`, `CacheHints`, `CacheScope`, `SessionMode`, `ResolvedSessionMode` | Main entry point |
|
|
48
|
+
| `@cyanheads/mcp-ts-core` | `createApp`, `tool`, `resource`, `prompt`, `appTool`, `appResource`, `APP_RESOURCE_MIME_TYPE`, `headerParam`, `Context`, `ClientCapabilities`, `createFail`, `createRecoveryFor`, `TypedFail`, `TypedRecoveryFor`, `ReasonOf`, `HandlerContext`, `Enrich`, `EnrichHelpers`, `TypedEnrich`, `ContentCollect`, `ContentBlock`, `z`, `inputRequired`, `completable`, `isCompletable`, `CompleteCallback`, `CompleteResourceTemplateCallback`, `CacheHint`, `CacheHints`, `CacheScope`, `SessionMode`, `ResolvedSessionMode` | Main entry point |
|
|
49
49
|
| `/worker` | `createWorkerHandler`, `CloudflareBindings` | Cloudflare Workers entry |
|
|
50
50
|
| `/tools` | `ToolDefinition`, `AnyToolDefinition`, `ToolAnnotations` | Tool definition types |
|
|
51
51
|
| `/resources` | `ResourceDefinition`, `AnyResourceDefinition` | Resource definition types |
|
|
@@ -300,10 +300,11 @@ interface Context {
|
|
|
300
300
|
readonly traceId?: string;
|
|
301
301
|
readonly spanId?: string;
|
|
302
302
|
readonly auth?: AuthContext;
|
|
303
|
+
readonly clientCapabilities: ClientCapabilities | undefined; // what the client declared; undefined when no view (2025-era stateless HTTP)
|
|
303
304
|
readonly log: ContextLogger; // auto-correlated: requestId, traceId, tenantId
|
|
304
305
|
readonly state: ContextState; // tenant-scoped KV storage
|
|
305
306
|
readonly requestInput: RequestInputFn; // (spec, options?) => never — suspends and asks the caller for input
|
|
306
|
-
readonly inputs: ContextInputs; //
|
|
307
|
+
readonly inputs: ContextInputs; // the request's responses, limited to what the client declared
|
|
307
308
|
readonly notifyPromptListChanged?: (() => void) | undefined; // prompt list changed
|
|
308
309
|
readonly notifyResourceListChanged?: (() => void) | undefined; // resource list changed
|
|
309
310
|
readonly notifyResourceUpdated?: ((uri: string) => void) | undefined; // resource content changed
|
|
@@ -312,7 +313,7 @@ interface Context {
|
|
|
312
313
|
readonly uri?: URL; // present for resource handlers
|
|
313
314
|
readonly content: ContentCollect; // media blocks → prepended to content[]; never in structuredContent
|
|
314
315
|
readonly enrich: Enrich; // success-path agent context → structuredContent + content[]; typed on HandlerContext<R, E>
|
|
315
|
-
recoveryFor(reason: string): { recovery: { hint: string } } | Record<string, never>; //
|
|
316
|
+
recoveryFor(reason: string): { recovery: { hint: string } } | Record<string, never>; // a declared entry's hint, in wire shape
|
|
316
317
|
}
|
|
317
318
|
```
|
|
318
319
|
|
|
@@ -367,11 +368,33 @@ useFormat(answer.format);
|
|
|
367
368
|
cancelled prompt is terminal, not a round to retry. `inputRequired.elicitUrl({ message, url })`
|
|
368
369
|
hands the user an external link instead of a form.
|
|
369
370
|
|
|
371
|
+
A client can send responses on a call nothing asked for, so only what it declared reaches
|
|
372
|
+
`ctx.inputs`, at the mode level the request would need: an elicit result needs `elicitation` —
|
|
373
|
+
`elicitation.form` when it carries `content` — a sampling result `sampling` (`sampling.tools` when it
|
|
374
|
+
holds a tool block), a roots result `roots`, and a request with no capability view gets none.
|
|
375
|
+
`ctx.clientCapabilities` — the SDK-parsed `initialize` value on 2025-era connections (a bare
|
|
376
|
+
`elicitation: {}` reads back as `{ elicitation: { form: {} } }`), the request's envelope as sent on
|
|
377
|
+
2026-07-28 — decides whether to *ask* for optional context (request roots only when `roots` is
|
|
378
|
+
declared, else fall through); it is never a reason to skip a consent prompt.
|
|
379
|
+
|
|
370
380
|
A 2025-era client that declared no matching capability is refused as `client_capability_missing`,
|
|
371
381
|
with a hint that ends at reconnecting. When the tool's own arguments can stand in for the answer,
|
|
372
382
|
say so per call — `ctx.requestInput(spec, { fallbackHint: 'Or call again with noun supplied.' })` —
|
|
373
383
|
and the sentence is appended to that hint. A consent gate passes none: it has no such field.
|
|
374
384
|
|
|
385
|
+
**Consent gates redeem a server record.** A capable client can still pre-answer, and any
|
|
386
|
+
`requestState` can be replayed within its lifetime, so a destructive handler first redeems (reads and
|
|
387
|
+
deletes) a `ctx.state` record `{ operation, clientId, subject, target, contentHash }` stored under a
|
|
388
|
+
random id — the id is all `requestState` carries — and asks again on an unknown, used, or expired id,
|
|
389
|
+
or when any field differs from this call. Carrying the target in `requestState` and comparing on
|
|
390
|
+
re-entry is replayable. Redeeming stops a sequential replay, not concurrent retries: until an atomic
|
|
391
|
+
`ctx.state.take` exists (#593), an action that must not repeat is made idempotent per record. The
|
|
392
|
+
record's storage is shared by every instance a 2026-07-28 retry can reach — `filesystem`, `supabase`,
|
|
393
|
+
or `cloudflare-d1`, never `cloudflare-kv`. `MCP_REQUEST_STATE_KEY` (≥ 32 bytes, the same on every
|
|
394
|
+
instance) opts into sealing: the framework signs the string a handler returns, the SDK rejects any
|
|
395
|
+
other as `-32602` `invalid_request_state` before the handler runs, and `ctx.inputs.state()` still
|
|
396
|
+
returns the original string. See `api-context` for the full pattern.
|
|
397
|
+
|
|
375
398
|
### `ctx.content`
|
|
376
399
|
|
|
377
400
|
Accumulates non-text content blocks — image/audio bytes, embedded resources, resource links — onto the response: `ctx.content.image(data, mimeType)`, `ctx.content.audio(data, mimeType)`, or `ctx.content(block)` for a raw `ContentBlock`. Blocks are prepended to `content[]` after `format()` runs and never enter `structuredContent`, so a handler can emit media for the calling model without the base64 duplicating into typed output. Always present (no-op when unused); callable from handler and service layer.
|
|
@@ -382,7 +405,7 @@ See `api-context` skill for full details.
|
|
|
382
405
|
|
|
383
406
|
## Error Handling
|
|
384
407
|
|
|
385
|
-
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the
|
|
408
|
+
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the wire hint for its reason: when a failure whose `data.reason` names the entry reaches the tool or resource factory without `data.recovery`, the framework fills `data.recovery.hint` from it — a bare `ctx.fail('reason')` and a service throw carrying `{ reason }` alike, matched on the reason alone — and mirrors it into `content[]` text. Override with explicit `{ recovery: { hint: '...' } }` when runtime context matters; a throw-site `recovery` always wins, and the thrown `McpError` itself is never changed. Optional `severity` (`debug`/`info`/`notice`/`warning`) logs a modeled outcome below `error` — tools only, logging only: the wire envelope, the span status, and the `mcp.tool.*` metrics are untouched. The framework's own `invalid_arguments` and `client_capability_missing` refusals log at `notice` unless an entry naming them declares otherwise. Optional `thrownBy: 'service'` marks an entry the service layer produces, so `error-contract-unthrown` skips it while still checking the handler's own reasons — lint-only metadata, nothing at runtime reads it.
|
|
386
409
|
|
|
387
410
|
```ts
|
|
388
411
|
errors: [
|
|
@@ -394,8 +417,8 @@ errors: [
|
|
|
394
417
|
recovery: 'Wait 30 seconds before retrying or reduce batch size.' },
|
|
395
418
|
],
|
|
396
419
|
async handler(input, ctx) {
|
|
397
|
-
// Static recovery —
|
|
398
|
-
if (queue.full()) throw ctx.fail('queue_full'
|
|
420
|
+
// Static recovery — the framework fills the contract's hint onto the wire.
|
|
421
|
+
if (queue.full()) throw ctx.fail('queue_full');
|
|
399
422
|
// Dynamic recovery — interpolate runtime context, override the contract default.
|
|
400
423
|
if (!matched) throw ctx.fail('no_match', `No data for ${input.pmids.length} PMIDs`, {
|
|
401
424
|
pmids: input.pmids,
|
|
@@ -404,7 +427,7 @@ async handler(input, ctx) {
|
|
|
404
427
|
}
|
|
405
428
|
```
|
|
406
429
|
|
|
407
|
-
**`ctx.recoveryFor(reason)`** returns `{}` when no contract exists (spread-safe). Typed against the declared reason union on `HandlerContext<R>`.
|
|
430
|
+
**`ctx.recoveryFor(reason)`** returns the entry's hint in wire shape, `{}` when no contract exists (spread-safe). Typed against the declared reason union on `HandlerContext<R>`. Not needed to put a declared hint on the wire — the fill does that — only when the hint must ride the thrown error itself, as in a test of the handler's own throw.
|
|
408
431
|
|
|
409
432
|
**Contracts are inline, per-tool.** Don't extract shared `errors[]` constants — locality is the point, and dynamic `recovery` hints need tool-specific context. Declare domain-specific failures only; **baseline codes** (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) are auto-allowed by conformance lint. The lint scans handler source only — service-layer throws still reach clients via auto-classification.
|
|
410
433
|
|
|
@@ -422,9 +445,9 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
|
|
|
422
445
|
|
|
423
446
|
**Auto-classification.** Plain `Error`, `ZodError`, and any other thrown value are caught and classified automatically. Resolution order: request signal already aborted (→ `RequestCancelled`, outranking the thrown value's own code, `McpError` included) → `McpError` code (preserved as-is) → SDK `ConnectionClosed` (→ `RequestCancelled`) → engine resource-limit `RangeError` by whole message — stack overflow, maximum string size (→ `InternalError`) → JS constructor name (`SyntaxError` → `ValidationError`; `TypeError` is excluded) → provider patterns (HTTP status codes, AWS errors, DB errors) → common message patterns → `AbortError` name (→ `Timeout`) → `InternalError` fallback. A result that breaks the definition's own `output` or `enrichment` schema fails as `InternalError` naming that contract, not `ValidationError`.
|
|
424
447
|
|
|
425
|
-
**Error-path parity.** Tool errors: `content[]` carries `Error: <message>`, then `Recovery: <hint>` when the hint says something the message does not already contain, then a closing `(reason … · not retryable)` for whichever of `data.reason` / `data.retryable` is present; the numeric code and `data.issues` stay JSON-only. `structuredContent.error` carries `{ code, message, data? }`. No `_meta.error`. Resources re-throw via JSON-RPC error envelope. An argument rejection is one of them: `-32602` with `data.issues`, plus `data.reason: 'invalid_arguments'` and a hint synthesized from the issues and the root schema — never a tool-declared `reason`, since the handler never ran. When pre-validation rewrote or dropped a key the caller wrote, `data.input` (`{ aliased: [{ alias, target }], ignored }`) names it and the hint closes with `Validated … as ….` / `Dropped undeclared key ….`; an ignore-list drop is never reported. `client_capability_missing` is the second framework-owned reason: a `ctx.requestInput` return a 2025-era connection cannot serve is refused before any wire traffic as `-32600` carrying that reason and a hint naming the capability.
|
|
448
|
+
**Error-path parity.** Tool errors: `content[]` carries `Error: <message>`, then `Recovery: <hint>` when the hint says something the message does not already contain, then a closing `(reason … · not retryable · request <id>)` for whichever of `data.reason` / `data.retryable` / `data.requestId` is present; the numeric code and `data.issues` stay JSON-only. `structuredContent.error` carries `{ code, message, data? }`. No `_meta.error`. Resources re-throw via JSON-RPC error envelope. An argument rejection is one of them: `-32602` with `data.issues`, plus `data.reason: 'invalid_arguments'` and a hint synthesized from the issues and the root schema — never a tool-declared `reason`, since the handler never ran. When pre-validation rewrote or dropped a key the caller wrote, `data.input` (`{ aliased: [{ alias, target }], ignored }`) names it and the hint closes with `Validated … as ….` / `Dropped undeclared key ….`; an ignore-list drop is never reported. `client_capability_missing` is the second framework-owned reason: a `ctx.requestInput` return a 2025-era connection cannot serve is refused before any wire traffic as `-32600` carrying that reason and a hint naming the capability. **`data.requestId`** is set on every error envelope the framework builds — tool results, resource reads, prompts, `httpErrorHandler`'s JSON-RPC errors — equal to the `requestId` of that call's log records: a generated token, or the client's JSON-RPC id when it is a string (`httpErrorHandler` always generates its own). A resource read refused before it is measured (auth, `params`) carries an id no record shares. It replaces a thrown `data.requestId`, is never added to the thrown `McpError`, and is left off a resource `-32602` whose `data` is exactly `{ uri }` and off `runToolContract` results.
|
|
426
449
|
|
|
427
|
-
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names, unless marked `thrownBy: 'service'`)
|
|
450
|
+
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names, unless marked `thrownBy: 'service'`). See `api-linter` skill.
|
|
428
451
|
|
|
429
452
|
See `api-errors` skill for the full pattern-matching table, error code reference, and detailed examples.
|
|
430
453
|
|
|
@@ -458,7 +481,7 @@ Managed by `@cyanheads/mcp-ts-core`. Validated via Zod. Precedence: `createApp()
|
|
|
458
481
|
| Category | Key Variables |
|
|
459
482
|
|:---------|:-------------|
|
|
460
483
|
| Transport | `MCP_TRANSPORT_TYPE` (`stdio`\|`http`), `MCP_HTTP_PORT`, `MCP_HTTP_HOST`, `MCP_HTTP_ENDPOINT_PATH` |
|
|
461
|
-
| Auth | `MCP_AUTH_MODE`, `MCP_AUTH_SECRET_KEY`, `MCP_AUTH_DISABLE_SCOPE_CHECKS`, `OAUTH_
|
|
484
|
+
| Auth | `MCP_AUTH_MODE`, `MCP_AUTH_SECRET_KEY`, `MCP_AUTH_DISABLE_SCOPE_CHECKS`, `OAUTH_*`, `MCP_REQUEST_STATE_KEY` (opt-in `requestState` sealing) |
|
|
462
485
|
| Storage | `STORAGE_PROVIDER_TYPE` (`in-memory`\|`filesystem`\|`supabase`\|`cloudflare-r2`\|`cloudflare-kv`\|`cloudflare-d1`) |
|
|
463
486
|
| LLM | `OPENROUTER_API_KEY`, `OPENROUTER_APP_URL/NAME`, `LLM_DEFAULT_*` |
|
|
464
487
|
| Telemetry | `OTEL_ENABLED`, `OTEL_SERVICE_NAME/VERSION`, `OTEL_EXPORTER_OTLP_*` |
|
|
@@ -485,7 +508,7 @@ describe('myTool', () => {
|
|
|
485
508
|
});
|
|
486
509
|
```
|
|
487
510
|
|
|
488
|
-
**`createMockContext` options:** `createMockContext()` (state included), `{ tenantId: 'test-tenant' }` (explicit tenant; defaults to `'default'`, as stdio resolves it), `{ errors: myTool.errors }` (typed `ctx.fail`), `{ inputResponses }` / `{ requestState }` (seed `ctx.inputs` to drive a multi-round-trip handler's second round directly), plus `auth`, `sessionId`, `signal`, `requestId`, `uri`, and the four `notify*` callbacks.
|
|
511
|
+
**`createMockContext` options:** `createMockContext()` (state included), `{ tenantId: 'test-tenant' }` (explicit tenant; defaults to `'default'`, as stdio resolves it), `{ errors: myTool.errors }` (typed `ctx.fail`), `{ inputResponses }` / `{ requestState }` (seed `ctx.inputs` to drive a multi-round-trip handler's second round directly), `{ clientCapabilities }` (seeds `ctx.clientCapabilities` and filters the seeded responses to the declared kinds, as production does — omitted, nothing is filtered), plus `auth`, `sessionId`, `signal`, `requestId`, `uri`, and the four `notify*` callbacks.
|
|
489
512
|
|
|
490
513
|
**`ctx.state` in tests is the production path.** The mock backs it with a real `StorageService` over an `InMemoryProvider`, so key validation (`[a-zA-Z0-9_.\-/]+` — colons rejected), the JSON round-trip of every value, and TTL expiry behave exactly as they do in a deployment. Passing `errors` narrows the return type to `HandlerContext<ReasonOf<…>>`, which is what a definition declaring a contract types its handler's `ctx` as — so `definition.handler(input, ctx)` typechecks.
|
|
491
514
|
|
|
@@ -495,7 +518,7 @@ describe('myTool', () => {
|
|
|
495
518
|
|
|
496
519
|
**Fixture-based tests:** `mcpTest` from `/testing/vitest` (optional peer `vitest`) extends Vitest's `test` with per-test fixtures: `ctx` (fresh mock context), `session` (session-bound context), `fetchMock` (strict fetch fake, installed/restored around the test), and `storage` (fresh in-memory `StorageService`). Override fixtures via `.extend` with the function form only — a bare value would share one mutable context across every test in the file.
|
|
497
520
|
|
|
498
|
-
**Tool contracts:** `runToolContract(definition, input)` from `/testing` validates input/output, invokes the handler, formats content, and returns production-shaped success/error surfaces without transport auth or
|
|
521
|
+
**Tool contracts:** `runToolContract(definition, input)` from `/testing` validates input/output, invokes the handler, formats content, and returns production-shaped success/error surfaces — the declared `recovery` fill included — without transport auth, telemetry, or `data.requestId`. `toolContractSuite(definition, { success, errors })` from `/testing/vitest` registers reusable schema/handler/error-envelope conformance cases for a server's own tool definitions.
|
|
499
522
|
|
|
500
523
|
**Fuzz testing:** `fuzzTool`/`fuzzResource`/`fuzzPrompt` from `/testing/fuzz` generate valid and adversarial inputs from Zod schemas via `fast-check`, then assert handler invariants (no crashes, no prototype pollution, no stack trace leaks). Returns a `FuzzReport` for custom assertions.
|
|
501
524
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<div align="center">
|
|
8
8
|
|
|
9
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://modelcontextprotocol.io/specification/2026-07-28)
|
|
10
10
|
|
|
11
11
|
[](https://modelcontextprotocol.io/) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
12
12
|
|
|
@@ -127,9 +127,7 @@ const search = tool('search', {
|
|
|
127
127
|
],
|
|
128
128
|
handler: async (input, ctx) => {
|
|
129
129
|
const res = await runSearch(input.query, input.limit);
|
|
130
|
-
if (!res)
|
|
131
|
-
throw ctx.fail('index_unavailable', undefined, ctx.recoveryFor('index_unavailable'));
|
|
132
|
-
}
|
|
130
|
+
if (!res) throw ctx.fail('index_unavailable');
|
|
133
131
|
ctx.enrich({ effectiveQuery: res.parsed, totalCount: res.total });
|
|
134
132
|
if (res.items.length === 0) {
|
|
135
133
|
ctx.enrich({ notice: `No matches for "${input.query}". Try broader terms.` });
|
|
@@ -141,7 +139,7 @@ const search = tool('search', {
|
|
|
141
139
|
await createApp({ tools: [search] });
|
|
142
140
|
```
|
|
143
141
|
|
|
144
|
-
Both contracts are advertised in `tools/list`, so clients see them before calling, and the definition linter checks the handler against them.
|
|
142
|
+
Both contracts are advertised in `tools/list`, so clients see them before calling, and the definition linter checks the handler against them. A failure with a declared reason reaches the client carrying that entry's recovery hint, and every tool error carries the request ID its server log records share.
|
|
145
143
|
|
|
146
144
|
### Same data across client surfaces
|
|
147
145
|
|
|
@@ -236,6 +234,7 @@ Core config comes from environment variables, validated with Zod. Server-specifi
|
|
|
236
234
|
| `MCP_HTTP_HOST` | HTTP server hostname | `127.0.0.1` |
|
|
237
235
|
| `MCP_AUTH_MODE` | `none`, `jwt`, or `oauth` | `none` |
|
|
238
236
|
| `MCP_AUTH_SECRET_KEY` | JWT signing secret (required for `jwt` mode) | — |
|
|
237
|
+
| `MCP_REQUEST_STATE_KEY` | Opt-in key (≥ 32 bytes, the same on every instance) that seals the `requestState` handlers return and rejects any a client did not get from this server | — |
|
|
239
238
|
| `STORAGE_PROVIDER_TYPE` | `in-memory`, `filesystem`, `supabase`, `cloudflare-d1`/`kv`/`r2` | `in-memory` |
|
|
240
239
|
| `CANVAS_PROVIDER_TYPE` | `none` or `duckdb` (optional peer dependency `@duckdb/node-api`) | `none` |
|
|
241
240
|
| `OTEL_ENABLED` | Enable OpenTelemetry | `false` |
|
|
@@ -273,17 +272,18 @@ Tool and resource handlers receive a `Context`. `ctx.enrich` and `ctx.fail` are
|
|
|
273
272
|
| `ctx.log` | `ContextLogger` | Request-scoped logger (auto-correlates requestId, traceId, tenantId); also mirrored to the client as `notifications/message` |
|
|
274
273
|
| `ctx.state` | `ContextState` | Tenant-scoped key-value storage |
|
|
275
274
|
| `ctx.requestInput` | `(spec) => never` | Suspend and ask the caller for more input; the handler is re-entered with the answers |
|
|
276
|
-
| `ctx.inputs` | `ContextInputs` |
|
|
275
|
+
| `ctx.inputs` | `ContextInputs` | The request's responses, limited to the kinds the client declared — `.accepted()`, `.view()`, `.state()`, `.dropped` |
|
|
276
|
+
| `ctx.clientCapabilities` | `ClientCapabilities \| undefined` | What the client declared for this request; decides whether to ask for optional context, never whether to skip a consent prompt |
|
|
277
277
|
| `ctx.enrich` | `Enrich` / `TypedEnrich<E>` | Add declared result context to structured output and text content |
|
|
278
278
|
| `ctx.content` | `ContentCollect` | Attach image/audio blocks to `content[]` — `content.image(data, mimeType)`, `content.audio(...)`, or a raw block |
|
|
279
279
|
| `ctx.fail` | `(reason, msg?, data?) => McpError` | Creates an error for `throw ctx.fail(...)`; available with a declared `errors` contract |
|
|
280
|
-
| `ctx.recoveryFor` | `(reason) => object` | Resolves a declared recovery hint to `{ recovery: { hint } }
|
|
280
|
+
| `ctx.recoveryFor` | `(reason) => object` | Resolves a declared recovery hint to `{ recovery: { hint } }`; the framework already sends it with any failure carrying that reason and no hint of its own |
|
|
281
281
|
| `ctx.signal` | `AbortSignal` | Cancellation signal |
|
|
282
282
|
| `ctx.notifyResourceUpdated` | `Function?` | Notify subscribed clients a resource changed |
|
|
283
283
|
| `ctx.notifyResourceListChanged` | `Function?` | Notify clients the resource list changed |
|
|
284
284
|
| `ctx.notifyPromptListChanged` | `Function?` | Notify clients the prompt list changed |
|
|
285
285
|
| `ctx.notifyToolListChanged` | `Function?` | Notify clients the tool list changed |
|
|
286
|
-
| `ctx.requestId` | `string` |
|
|
286
|
+
| `ctx.requestId` | `string` | Request ID — shared by the call's log records and returned on its errors as `data.requestId` |
|
|
287
287
|
| `ctx.tenantId` | `string?` | Tenant ID (JWT `tid` claim, or `'default'` for stdio and HTTP+`MCP_AUTH_MODE=none`) |
|
|
288
288
|
| `ctx.auth` | `AuthContext?` | Token claims and scopes when the request is authenticated |
|
|
289
289
|
| `ctx.sessionId` | `string?` | HTTP session ID in stateful/`auto` session mode — a scoping key, not an authorization principal |
|
|
@@ -334,7 +334,7 @@ const input = myTool.input.parse({ query: 'test' });
|
|
|
334
334
|
const result = await myTool.handler(input, ctx);
|
|
335
335
|
```
|
|
336
336
|
|
|
337
|
-
`createMockContext()` gives you a recording `log`, a `signal`, and a `state` backed by a real `StorageService` over an in-memory provider, so key validation, TTL expiry, and the JSON round-trip of stored values behave as they do in production: a `Date` reads back as its ISO string, and a value JSON cannot encode rejects. It uses tenant `'default'` unless you pass `{ tenantId }`. Pass `{ errors: myTool.errors }` for a typed `ctx.fail`,
|
|
337
|
+
`createMockContext()` gives you a recording `log`, a `signal`, and a `state` backed by a real `StorageService` over an in-memory provider, so key validation, TTL expiry, and the JSON round-trip of stored values behave as they do in production: a `Date` reads back as its ISO string, and a value JSON cannot encode rejects. It uses tenant `'default'` unless you pass `{ tenantId }`. Pass `{ errors: myTool.errors }` for a typed `ctx.fail`, `{ inputResponses, requestState }` to start a multi-round-trip handler at its second round, or `{ clientCapabilities }` to set what the client declared (seeded responses are then filtered to the declared kinds, as in production).
|
|
338
338
|
|
|
339
339
|
`/testing` also exports `createMockSession()` for session-bound contexts, `createFetchMock()` as a strict fake for upstream HTTP, and `runToolContract()`, which runs a definition through schema, handler, formatting, and error-envelope checks. `/testing/vitest` adds the `mcpTest` fixtures (`ctx`, `session`, `fetchMock`, `storage`) and `toolContractSuite()`.
|
|
340
340
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "A client can no longer pre-answer a consent gate with a response its declared capabilities do not cover, framework-built errors carry their request ID and fill a declared recovery hint, and multi-arch Docker builds stop running Bun under QEMU."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: true
|
|
5
|
+
agent-notes: |
|
|
6
|
+
Adoption steps for a consumer upgrading from 0.13.9.
|
|
7
|
+
|
|
8
|
+
1. Dockerfile — move every install into a `deps` stage. These steps
|
|
9
|
+
replace 0.13.8's agent-notes step 7. A production stage that copies
|
|
10
|
+
`bunfig.toml` (the 0.13.8 / 0.13.9 scaffold) fails `lint:packaging`
|
|
11
|
+
check 15 once `scripts/lint-packaging.ts` is resynced, and its
|
|
12
|
+
multi-arch build aborts on the non-native leg. `init` never overwrites
|
|
13
|
+
`Dockerfile`, so port by hand:
|
|
14
|
+
a. Resync the framework scripts (maintenance Phase C, or `init` in
|
|
15
|
+
place) so `scripts/install-otel.ts` exists. Commit it, and keep
|
|
16
|
+
`scripts/` out of `.dockerignore`: the image build copies it.
|
|
17
|
+
b. Copy the scaffold `Dockerfile`'s `deps` stage — its
|
|
18
|
+
`FROM --platform=$BUILDPLATFORM` on the build stage's `oven/bun` tag,
|
|
19
|
+
manifest copy, scanner seed, `TARGETARCH` → `.bun-cpu` mapping,
|
|
20
|
+
cross-install, `install-otel.ts` step, and scanner removal — in
|
|
21
|
+
between the build and production stages.
|
|
22
|
+
c. In the production stage, delete the `package.json`/`bun.lock`/
|
|
23
|
+
`bunfig.toml` copy, the scanner seed, the `bun install`,
|
|
24
|
+
`ARG OTEL_ENABLED`, and the OTel `RUN` (the `bun -e` range lookup
|
|
25
|
+
plus `bun add`, or an older plain `bun add` list). Before the `dist`
|
|
26
|
+
copy, add `COPY package.json ./` and
|
|
27
|
+
`COPY --from=deps /usr/src/app/node_modules ./node_modules`. Keep
|
|
28
|
+
everything else.
|
|
29
|
+
d. Move any other build-time `bun` / `bunx` call out of a stage not
|
|
30
|
+
pinned to `$BUILDPLATFORM`; check 15 names each line. Verify with
|
|
31
|
+
`bun run lint:packaging`.
|
|
32
|
+
A server still on the pre-0.13.8 production stage lints clean, but its
|
|
33
|
+
image installs skip the release-age gate and scanner; port it too.
|
|
34
|
+
2. Drop redundant recovery forwards. `ctx.fail('x', msg, ctx.recoveryFor('x'))`
|
|
35
|
+
becomes `ctx.fail('x', msg)`, and a service throw keeps only
|
|
36
|
+
`{ reason: 'x', … }` (a service that took `ctx` only for the spread can
|
|
37
|
+
drop it). Keep explicit `recovery: { hint }` overrides, deliberate
|
|
38
|
+
cross-reason forwards, and forwards a test asserts on a direct
|
|
39
|
+
`definition.handler(...)` throw — or move that assertion to
|
|
40
|
+
`runToolContract`, which applies the fill. The lint rule
|
|
41
|
+
`error-contract-recovery-unforwarded` is gone: swap its mentions in
|
|
42
|
+
`CLAUDE.md` / `AGENTS.md` for the template's Errors wording, and drop it
|
|
43
|
+
from any script that greps lint output.
|
|
44
|
+
3. Tests pinning exact error text or `data`: a tool error's `content[]`
|
|
45
|
+
now closes with `request <id>`, and tool, resource, and prompt error
|
|
46
|
+
`data` gain `requestId` (a classified plain `Error` returns
|
|
47
|
+
`data: { requestId }`). Assert with the envelope's own value or
|
|
48
|
+
`expect.any(String)`. `runToolContract` adds no request id but now
|
|
49
|
+
fills a declared hint, adding a `Recovery:` line. A server that put an
|
|
50
|
+
upstream id in `data.requestId` renames it (`upstreamRequestId`); one
|
|
51
|
+
that copied `ctx.requestId` there drops it.
|
|
52
|
+
4. Alerts: `invalid_arguments` and `client_capability_missing` records
|
|
53
|
+
now log at `notice`, tagged `mcp.error.severity: "notice"`. The
|
|
54
|
+
`Prompt generation failed.` record is gone — key prompt alerts on
|
|
55
|
+
`Error in prompt:<name>`.
|
|
56
|
+
5. Consent gates: a destructive handler that acts on `ctx.inputs` alone,
|
|
57
|
+
compares a target carried in `requestState`, or stores a record without
|
|
58
|
+
the operation and caller moves to the `api-context` § Consent gates
|
|
59
|
+
pattern: redeem a `ctx.state` record
|
|
60
|
+
`{ operation, clientId, subject, target, contentHash }` first and ask
|
|
61
|
+
again on any mismatch. Keep the record where every instance a retry
|
|
62
|
+
reaches can read it (`filesystem`, `supabase`, or `cloudflare-d1`,
|
|
63
|
+
never `cloudflare-kv`), and make an action that must not repeat
|
|
64
|
+
idempotent per record until an atomic `ctx.state.take` exists (#593).
|
|
65
|
+
Tests copy the round-one record into the round-two mock context
|
|
66
|
+
(`api-testing` § Mock inputs).
|
|
67
|
+
6. Set `MCP_REQUEST_STATE_KEY` (≥ 32 bytes, e.g. `openssl rand -base64 32`,
|
|
68
|
+
the same on every instance and Worker isolate) on any server whose
|
|
69
|
+
`requestState` drives a mutation or carries ids across rounds. A
|
|
70
|
+
startup `info` record naming the variable confirms it took. A client
|
|
71
|
+
or test that builds `requestState` by hand then gets `-32602`
|
|
72
|
+
`invalid_request_state`.
|
|
73
|
+
7. Replace a catch of `client_capability_missing` used to fall through to
|
|
74
|
+
another source with a `ctx.clientCapabilities` check before
|
|
75
|
+
`ctx.requestInput`, after reading `ctx.inputs`. Treat `undefined` as
|
|
76
|
+
cannot-ask, and never use it to skip a consent prompt.
|
|
77
|
+
8. `Context` gains a required `clientCapabilities` key, so a hand-built
|
|
78
|
+
`Context` literal (a custom mock typed `Context`) needs it.
|
|
79
|
+
`createMockContext` tests change only when they seed
|
|
80
|
+
`clientCapabilities`, which applies the production filter.
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
# 0.13.10 — 2026-09-26
|
|
84
|
+
|
|
85
|
+
## Added
|
|
86
|
+
|
|
87
|
+
- **`ctx.clientCapabilities`** ([#580](https://github.com/cyanheads/mcp-ts-core/issues/580)) — what the client declared for this request (`undefined` for a 2025-era request under stateless HTTP), for deciding whether to ask for optional context, never whether to skip a consent prompt. `ClientCapabilities` is exported from the main entry, and `createMockContext({ clientCapabilities })` seeds it.
|
|
88
|
+
- **`MCP_REQUEST_STATE_KEY`** ([#496](https://github.com/cyanheads/mcp-ts-core/issues/496)) — opt-in key (≥ 32 bytes, the same on every instance) that seals the `requestState` a handler returns, bound to the caller for 900 s. Any other state is rejected as `-32602` `invalid_request_state` before the handler runs; also a Worker core binding.
|
|
89
|
+
- **`scripts/install-otel.ts`** ([#578](https://github.com/cyanheads/mcp-ts-core/issues/578)) — ships in the package and the scaffold, installing every `@opentelemetry/*` peer plus `@hono/otel` at the framework's declared ranges and passing extra arguments (`--os`, `--cpu`) to its `bun install`. The Dockerfiles run it in place of a hand-kept list.
|
|
90
|
+
|
|
91
|
+
## Changed
|
|
92
|
+
|
|
93
|
+
- **A declared `recovery` reaches the wire without `ctx.recoveryFor`** ([#579](https://github.com/cyanheads/mcp-ts-core/issues/579)) — a tool or resource failure whose `data.reason` names an `errors[]` entry and carries no `data.recovery` gets that entry's hint, service throws included, and `runToolContract` does the same. A throw-site hint still wins.
|
|
94
|
+
- **Error envelopes carry `data.requestId`** ([#576](https://github.com/cyanheads/mcp-ts-core/issues/576)) — tool, resource, prompt, and `httpErrorHandler` errors return the `requestId` their log records carry, and a tool's `content[]` closes with `request <id>`. A resource `-32602` whose `data` is exactly `{ uri }` and `runToolContract` results go without.
|
|
95
|
+
- **Framework refusals log at `notice`** ([#567](https://github.com/cyanheads/mcp-ts-core/issues/567)) — `Error in tool:<name>` for `invalid_arguments` and `client_capability_missing` drops from `error` to `notice`, and `mcp.errors.classified` tags them `mcp.error.severity: "notice"`. A declared `severity` naming either reason still wins.
|
|
96
|
+
- **`lint:packaging` check 15 flags JavaScript in any target-platform stage** ([#575](https://github.com/cyanheads/mcp-ts-core/issues/575)) — a Dockerfile stage not starting `FROM --platform=$BUILDPLATFORM` fails when a `RUN` invokes `bun` for anything but `install`/`add`, or installs once `bunfig.toml` is in the stage, naming each line.
|
|
97
|
+
- **The release review pass may ship caller-handed work** — `release-pr-review` commits uncommitted work the caller explicitly hands over on top of the stack and adds it to the changelog entry; `release-and-publish` admits those commits above the release commit.
|
|
98
|
+
- Skill versions: `add-service` 1.11 → 1.12, `add-test` 1.7 → 1.8, `add-tool` 2.31 → 2.32, `api-config` 1.22 → 1.23, `api-context` 2.8 → 2.9, `api-errors` 1.18 → 1.19, `api-linter` 1.20 → 1.21, `api-telemetry` 1.15 → 1.16, `api-testing` 1.12 → 1.13, `api-workers` 1.8 → 1.9, `design-mcp-server` 2.30 → 2.31, `field-test` 2.17 → 2.18, `polish-docs-meta` 2.20 → 2.21, `release-and-publish` 2.22 → 2.23, `release-pr-review` 1.6 → 1.7, `security-pass` 1.11 → 1.12.
|
|
99
|
+
|
|
100
|
+
## Removed
|
|
101
|
+
|
|
102
|
+
- **`error-contract-recovery-unforwarded`** ([#579](https://github.com/cyanheads/mcp-ts-core/issues/579)) — the lint rule is gone; with the recovery fill, a bare `ctx.fail('<reason>')` reaches the client with its hint.
|
|
103
|
+
|
|
104
|
+
## Fixed
|
|
105
|
+
|
|
106
|
+
- **Multi-arch Docker builds no longer run Bun under QEMU** ([#575](https://github.com/cyanheads/mcp-ts-core/issues/575)) — the framework and scaffold Dockerfiles install production dependencies in a `deps` stage on `$BUILDPLATFORM`, cross-installing with `--os`/`--cpu`, so the production stage's only Bun calls are `HEALTHCHECK` and `CMD`. Existing servers port by hand.
|
|
107
|
+
- **An argless prompt's `generate` receives `{}`** ([#581](https://github.com/cyanheads/mcp-ts-core/issues/581)) — not the SDK's request context, and its input measures 0 bytes.
|
|
108
|
+
- **A failed prompt writes one error record, under its own request id** ([#582](https://github.com/cyanheads/mcp-ts-core/issues/582)) — `Error in prompt:<name>` at `error`, and a completion record of `Prompt generation finished.` at `info` with `metrics.isSuccess: false` in place of `Prompt generation failed.` at `error`. Each `prompts/get` logs under its own `requestId`.
|
|
109
|
+
|
|
110
|
+
## Security
|
|
111
|
+
|
|
112
|
+
- **`ctx.inputs` keeps only responses the client declared** ([#496](https://github.com/cyanheads/mcp-ts-core/issues/496)) — a client that did not declare `elicitation`, or declared only its URL mode, could pre-answer a consent gate on its first call. A response now reaches `ctx.inputs` only when the client's declared capabilities and modes cover it, on both protocol eras.
|
|
113
|
+
- **Consent gates redeem a server record** — a client that declared `elicitation` can still pre-answer, so `api-context` § *Consent gates* documents storing `{ operation, clientId, subject, target, contentHash }` in `ctx.state` under a random id and redeeming it before acting. That stops a sequential replay, not concurrent retries, until an atomic `ctx.state.take` exists ([#593](https://github.com/cyanheads/mcp-ts-core/issues/593)).
|
|
114
|
+
|
|
115
|
+
## Dependencies
|
|
116
|
+
|
|
117
|
+
- `hono` ^4.13.8 → ^4.13.9.
|
|
118
|
+
- Dev: `@cloudflare/workers-types` 5.20260922.1 → 5.20260924.1.
|
package/dist/config/index.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
60
60
|
mcpGcPressureIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
61
61
|
mcpAllowedOrigins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
62
62
|
mcpAuthSecretKey: z.ZodOptional<z.ZodString>;
|
|
63
|
+
mcpRequestStateKey: z.ZodOptional<z.ZodString>;
|
|
63
64
|
mcpJwtExpectedIssuer: z.ZodOptional<z.ZodString>;
|
|
64
65
|
mcpJwtExpectedAudience: z.ZodOptional<z.ZodString>;
|
|
65
66
|
mcpAuthMode: z.ZodPreprocess<z.ZodDefault<z.ZodEnum<{
|
|
@@ -197,6 +198,7 @@ declare const parseConfig: (envOverrides?: Record<string, string | undefined>) =
|
|
|
197
198
|
mcpGcPressureIntervalMs: number;
|
|
198
199
|
mcpAllowedOrigins?: string[] | undefined;
|
|
199
200
|
mcpAuthSecretKey?: string | undefined;
|
|
201
|
+
mcpRequestStateKey?: string | undefined;
|
|
200
202
|
mcpJwtExpectedIssuer?: string | undefined;
|
|
201
203
|
mcpJwtExpectedAudience?: string | undefined;
|
|
202
204
|
mcpAuthMode: "jwt" | "none" | "oauth";
|
|
@@ -325,6 +327,7 @@ declare const config: {
|
|
|
325
327
|
mcpGcPressureIntervalMs: number;
|
|
326
328
|
mcpAllowedOrigins?: string[] | undefined;
|
|
327
329
|
mcpAuthSecretKey?: string | undefined;
|
|
330
|
+
mcpRequestStateKey?: string | undefined;
|
|
328
331
|
mcpJwtExpectedIssuer?: string | undefined;
|
|
329
332
|
mcpJwtExpectedAudience?: string | undefined;
|
|
330
333
|
mcpAuthMode: "jwt" | "none" | "oauth";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,wEAAwE;AACxE,eAAO,MAAM,cAAc,2BAA2B,CAAC;AACvD,eAAO,MAAM,iBAAiB,QAAkC,CAAC;AA6DjE,QAAA,MAAM,YAAY
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,wEAAwE;AACxE,eAAO,MAAM,cAAc,2BAA2B,CAAC;AACvD,eAAO,MAAM,iBAAiB,QAAkC,CAAC;AA6DjE,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2Yd,CAAC;AAGL,QAAA,MAAM,WAAW,kBAAmB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+MrE,CAAC;AAIF;;;;;;;;GAQG;AACH,QAAA,MAAM,WAAW,kBAAmB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KAAG,IAExE,CAAC;AAEF;;;;;;GAMG;AACH,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuBV,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAErD;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/config/index.js
CHANGED
|
@@ -204,6 +204,16 @@ const ConfigSchema = z
|
|
|
204
204
|
mcpGcPressureIntervalMs: z.coerce.number().min(0).default(0),
|
|
205
205
|
mcpAllowedOrigins: z.array(z.string()).optional(),
|
|
206
206
|
mcpAuthSecretKey: z.string().optional(),
|
|
207
|
+
/**
|
|
208
|
+
* Opt-in HMAC key for multi-round-trip `requestState`. When set, the
|
|
209
|
+
* framework seals the state a handler returns with `ctx.requestInput` and
|
|
210
|
+
* the SDK verifies every echoed state before the handler runs, so a retry
|
|
211
|
+
* can only carry state this server minted, for the same principal, within
|
|
212
|
+
* 900 s. Every instance a retry can reach needs the same key. Unset, state
|
|
213
|
+
* round-trips raw. At least 32 bytes; the length is checked where the
|
|
214
|
+
* codec is built, so the startup error names this variable.
|
|
215
|
+
*/
|
|
216
|
+
mcpRequestStateKey: z.string().optional(),
|
|
207
217
|
mcpJwtExpectedIssuer: z.string().optional(),
|
|
208
218
|
mcpJwtExpectedAudience: z.string().optional(),
|
|
209
219
|
mcpAuthMode: z.preprocess(emptyStringAsUndefined, z.enum(['jwt', 'oauth', 'none']).default('none')),
|
|
@@ -481,6 +491,7 @@ const parseConfig = (envOverrides) => {
|
|
|
481
491
|
.map((o) => o.trim())
|
|
482
492
|
.filter(Boolean),
|
|
483
493
|
mcpAuthSecretKey: env.MCP_AUTH_SECRET_KEY,
|
|
494
|
+
mcpRequestStateKey: env.MCP_REQUEST_STATE_KEY,
|
|
484
495
|
mcpJwtExpectedIssuer: env.MCP_JWT_EXPECTED_ISSUER,
|
|
485
496
|
mcpJwtExpectedAudience: env.MCP_JWT_EXPECTED_AUDIENCE,
|
|
486
497
|
mcpAuthMode: env.MCP_AUTH_MODE,
|