@cyanheads/mcp-ts-core 0.13.9 → 0.13.11
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 +41 -15
- package/CLAUDE.md +41 -15
- package/README.md +37 -11
- package/biome.json +1 -1
- package/changelog/0.13.x/0.13.10.md +118 -0
- package/changelog/0.13.x/0.13.11.md +113 -0
- package/dist/cli/app-render.d.ts +11 -0
- package/dist/cli/app-render.d.ts.map +1 -0
- package/dist/cli/app-render.js +150 -0
- package/dist/cli/app-render.js.map +1 -0
- package/dist/cli/init.d.ts +2 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +10 -2
- package/dist/cli/init.js.map +1 -1
- 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 +92 -22
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +65 -16
- 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 +60 -21
- 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/services/mirror/sqlite/handle.d.ts +11 -5
- package/dist/services/mirror/sqlite/handle.d.ts.map +1 -1
- package/dist/services/mirror/sqlite/handle.js +46 -8
- package/dist/services/mirror/sqlite/handle.js.map +1 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts +6 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts.map +1 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.js.map +1 -1
- package/dist/testing/apps/browser.d.ts +27 -0
- package/dist/testing/apps/browser.d.ts.map +1 -0
- package/dist/testing/apps/browser.js +162 -0
- package/dist/testing/apps/browser.js.map +1 -0
- package/dist/testing/apps/cdp-pipe.d.ts +79 -0
- package/dist/testing/apps/cdp-pipe.d.ts.map +1 -0
- package/dist/testing/apps/cdp-pipe.js +200 -0
- package/dist/testing/apps/cdp-pipe.js.map +1 -0
- package/dist/testing/apps/csp.d.ts +17 -0
- package/dist/testing/apps/csp.d.ts.map +1 -0
- package/dist/testing/apps/csp.js +50 -0
- package/dist/testing/apps/csp.js.map +1 -0
- package/dist/testing/apps/host-pages.d.ts +47 -0
- package/dist/testing/apps/host-pages.d.ts.map +1 -0
- package/dist/testing/apps/host-pages.js +138 -0
- package/dist/testing/apps/host-pages.js.map +1 -0
- package/dist/testing/apps/index.d.ts +167 -0
- package/dist/testing/apps/index.d.ts.map +1 -0
- package/dist/testing/apps/index.js +36 -0
- package/dist/testing/apps/index.js.map +1 -0
- package/dist/testing/apps/partial-json.d.ts +14 -0
- package/dist/testing/apps/partial-json.d.ts.map +1 -0
- package/dist/testing/apps/partial-json.js +71 -0
- package/dist/testing/apps/partial-json.js.map +1 -0
- package/dist/testing/apps/run.d.ts +13 -0
- package/dist/testing/apps/run.d.ts.map +1 -0
- package/dist/testing/apps/run.js +659 -0
- package/dist/testing/apps/run.js.map +1 -0
- 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/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.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/logger.d.ts +21 -1
- package/dist/utils/internal/logger.d.ts.map +1 -1
- package/dist/utils/internal/logger.js +31 -14
- package/dist/utils/internal/logger.js.map +1 -1
- package/dist/utils/internal/performance.d.ts +7 -3
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +16 -9
- 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/security/sanitization.d.ts +16 -13
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +74 -33
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +12 -5
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +12 -5
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +2 -1
- package/framework-skills/add-service/SKILL.md +3 -12
- package/framework-skills/add-test/SKILL.md +20 -17
- package/framework-skills/add-tool/SKILL.md +30 -34
- package/framework-skills/api-config/SKILL.md +12 -2
- package/framework-skills/api-context/SKILL.md +156 -41
- package/framework-skills/api-errors/SKILL.md +43 -47
- package/framework-skills/api-linter/SKILL.md +5 -29
- package/framework-skills/api-mirror/SKILL.md +4 -3
- package/framework-skills/api-telemetry/SKILL.md +16 -10
- package/framework-skills/api-testing/SKILL.md +47 -11
- package/framework-skills/api-utils/SKILL.md +3 -3
- package/framework-skills/api-workers/SKILL.md +3 -1
- package/framework-skills/code-simplifier/SKILL.md +2 -2
- package/framework-skills/design-mcp-server/SKILL.md +5 -5
- package/framework-skills/field-test/SKILL.md +53 -8
- package/framework-skills/git-wrapup/SKILL.md +2 -2
- package/framework-skills/orchestrations/SKILL.md +1 -1
- package/framework-skills/orchestrations/workflows/greenfield-build.md +2 -2
- package/framework-skills/polish-docs-meta/SKILL.md +4 -3
- package/framework-skills/polish-docs-meta/references/server-json.md +24 -26
- package/framework-skills/release-and-publish/SKILL.md +3 -3
- package/framework-skills/release-pr-review/SKILL.md +6 -6
- package/framework-skills/security-pass/SKILL.md +8 -7
- package/framework-skills/setup/SKILL.md +2 -2
- package/package.json +38 -22
- package/scripts/devcheck.ts +43 -21
- package/scripts/install-otel.ts +84 -0
- package/scripts/lint-packaging.ts +279 -28
- package/scripts/prune-musl-packages.ts +146 -0
- package/templates/.env.example +2 -0
- package/templates/.github/workflows/codeql.yml +10 -1
- package/templates/AGENTS.md +5 -4
- package/templates/CLAUDE.md +5 -4
- package/templates/Dockerfile +76 -50
- package/templates/_.dockerignore +3 -5
- package/templates/_.gitignore +4 -4
- package/templates/package.json +4 -4
- package/templates/server.json +7 -21
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +3 -8
package/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Package:** `@cyanheads/mcp-ts-core`
|
|
4
|
-
**Version:** 0.13.
|
|
4
|
+
**Version:** 0.13.11
|
|
5
5
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
6
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
6
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.2.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.6.5
|
|
8
8
|
**GitHub:** [cyanheads/mcp-ts-core](https://github.com/cyanheads/mcp-ts-core)
|
|
9
9
|
**npm:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
|
|
@@ -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 |
|
|
@@ -61,6 +61,7 @@ Both paths share the same public API. Init copies starter `package.json`, config
|
|
|
61
61
|
| `/services` | `OpenRouterProvider`, `SpeechService`, `createSpeechProvider`, `ElevenLabsProvider`, `WhisperProvider`, `GraphService`, provider interfaces and types | LLM, Speech (TTS/STT), Graph services |
|
|
62
62
|
| `/linter` | `validateDefinitions`, `LintReport`, `LintDiagnostic`, `LintInput`, `LintSeverity` | Definition validation |
|
|
63
63
|
| `/testing` | `createMockContext`, `createMockSession`, `createFetchMock`, `runToolContract`, `createMockLogger`, `getEnrichment`, `getContentBlocks`, `createInMemoryStorage`, `expectInputRequired` | Test kit for handlers and upstream HTTP boundaries |
|
|
64
|
+
| `/testing/apps` | `renderAppTool`, `RenderAppToolOptions`, `AppRenderReport`, `AppRenderStep`, `AppServerTarget`, `AppHostOptions` | Headless MCP Apps host that renders an app tool's `ui://` view and reports on it (optional peers `@modelcontextprotocol/client`, `@modelcontextprotocol/ext-apps`) |
|
|
64
65
|
| `/testing/fuzz` | `fuzzTool`, `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, `adversarialArbitrary`, `ADVERSARIAL_STRINGS` | Fuzz testing |
|
|
65
66
|
| `/testing/vitest` | `mcpTest`, `toolContractSuite`, `McpTestFixtures` (+ re-exported `/testing` helpers) | Vitest fixtures and tool conformance suites (optional peer `vitest`) |
|
|
66
67
|
|
|
@@ -300,10 +301,11 @@ interface Context {
|
|
|
300
301
|
readonly traceId?: string;
|
|
301
302
|
readonly spanId?: string;
|
|
302
303
|
readonly auth?: AuthContext;
|
|
304
|
+
readonly clientCapabilities: ClientCapabilities | undefined; // what the client declared; undefined when no view (2025-era stateless HTTP)
|
|
303
305
|
readonly log: ContextLogger; // auto-correlated: requestId, traceId, tenantId
|
|
304
306
|
readonly state: ContextState; // tenant-scoped KV storage
|
|
305
307
|
readonly requestInput: RequestInputFn; // (spec, options?) => never — suspends and asks the caller for input
|
|
306
|
-
readonly inputs: ContextInputs; //
|
|
308
|
+
readonly inputs: ContextInputs; // the request's responses, limited to what the client declared
|
|
307
309
|
readonly notifyPromptListChanged?: (() => void) | undefined; // prompt list changed
|
|
308
310
|
readonly notifyResourceListChanged?: (() => void) | undefined; // resource list changed
|
|
309
311
|
readonly notifyResourceUpdated?: ((uri: string) => void) | undefined; // resource content changed
|
|
@@ -312,13 +314,13 @@ interface Context {
|
|
|
312
314
|
readonly uri?: URL; // present for resource handlers
|
|
313
315
|
readonly content: ContentCollect; // media blocks → prepended to content[]; never in structuredContent
|
|
314
316
|
readonly enrich: Enrich; // success-path agent context → structuredContent + content[]; typed on HandlerContext<R, E>
|
|
315
|
-
recoveryFor(reason: string): { recovery: { hint: string } } | Record<string, never>; //
|
|
317
|
+
recoveryFor(reason: string): { recovery: { hint: string } } | Record<string, never>; // a declared entry's hint, in wire shape
|
|
316
318
|
}
|
|
317
319
|
```
|
|
318
320
|
|
|
319
321
|
### `ctx.log`
|
|
320
322
|
|
|
321
|
-
Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `error`. Auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. Use `ctx.log` in handlers; global `logger` for startup/shutdown/background.
|
|
323
|
+
Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `error`. Auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. Each record also reaches the client as `notifications/message` — only at or above `MCP_LOG_LEVEL` (RFC 5424 order; a client's own level can only narrow it), with sensitive fields masked as `[REDACTED]`. Use `ctx.log` in handlers; global `logger` for startup/shutdown/background.
|
|
322
324
|
|
|
323
325
|
### `ctx.state`
|
|
324
326
|
|
|
@@ -367,11 +369,33 @@ useFormat(answer.format);
|
|
|
367
369
|
cancelled prompt is terminal, not a round to retry. `inputRequired.elicitUrl({ message, url })`
|
|
368
370
|
hands the user an external link instead of a form.
|
|
369
371
|
|
|
372
|
+
A client can send responses on a call nothing asked for, so only what it declared reaches
|
|
373
|
+
`ctx.inputs`, at the mode level the request would need: an elicit result needs `elicitation` —
|
|
374
|
+
`elicitation.form` when it carries `content` — a sampling result `sampling` (`sampling.tools` when it
|
|
375
|
+
holds a tool block), a roots result `roots`, and a request with no capability view gets none.
|
|
376
|
+
`ctx.clientCapabilities` — the SDK-parsed `initialize` value on 2025-era connections (a bare
|
|
377
|
+
`elicitation: {}` reads back as `{ elicitation: { form: {} } }`), the request's envelope as sent on
|
|
378
|
+
2026-07-28 — decides whether to *ask* for optional context (request roots only when `roots` is
|
|
379
|
+
declared, else fall through); it is never a reason to skip a consent prompt.
|
|
380
|
+
|
|
370
381
|
A 2025-era client that declared no matching capability is refused as `client_capability_missing`,
|
|
371
382
|
with a hint that ends at reconnecting. When the tool's own arguments can stand in for the answer,
|
|
372
383
|
say so per call — `ctx.requestInput(spec, { fallbackHint: 'Or call again with noun supplied.' })` —
|
|
373
384
|
and the sentence is appended to that hint. A consent gate passes none: it has no such field.
|
|
374
385
|
|
|
386
|
+
**Consent gates redeem a server record.** A capable client can still pre-answer, and any
|
|
387
|
+
`requestState` can be replayed within its lifetime, so a destructive handler first redeems (reads and
|
|
388
|
+
deletes) a `ctx.state` record `{ operation, clientId, subject, target, contentHash }` stored under a
|
|
389
|
+
random id — the id is all `requestState` carries — and asks again on an unknown, used, or expired id,
|
|
390
|
+
or when any field differs from this call. Carrying the target in `requestState` and comparing on
|
|
391
|
+
re-entry is replayable. Redeeming stops a sequential replay, not concurrent retries: until an atomic
|
|
392
|
+
`ctx.state.take` exists (#593), an action that must not repeat is made idempotent per record. The
|
|
393
|
+
record's storage is shared by every instance a 2026-07-28 retry can reach — `filesystem`, `supabase`,
|
|
394
|
+
or `cloudflare-d1`, never `cloudflare-kv`. `MCP_REQUEST_STATE_KEY` (≥ 32 bytes, the same on every
|
|
395
|
+
instance) opts into sealing: the framework signs the string a handler returns, the SDK rejects any
|
|
396
|
+
other as `-32602` `invalid_request_state` before the handler runs, and `ctx.inputs.state()` still
|
|
397
|
+
returns the original string. See `api-context` for the full pattern.
|
|
398
|
+
|
|
375
399
|
### `ctx.content`
|
|
376
400
|
|
|
377
401
|
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 +406,7 @@ See `api-context` skill for full details.
|
|
|
382
406
|
|
|
383
407
|
## Error Handling
|
|
384
408
|
|
|
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
|
|
409
|
+
**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
410
|
|
|
387
411
|
```ts
|
|
388
412
|
errors: [
|
|
@@ -394,8 +418,8 @@ errors: [
|
|
|
394
418
|
recovery: 'Wait 30 seconds before retrying or reduce batch size.' },
|
|
395
419
|
],
|
|
396
420
|
async handler(input, ctx) {
|
|
397
|
-
// Static recovery —
|
|
398
|
-
if (queue.full()) throw ctx.fail('queue_full'
|
|
421
|
+
// Static recovery — the framework fills the contract's hint onto the wire.
|
|
422
|
+
if (queue.full()) throw ctx.fail('queue_full');
|
|
399
423
|
// Dynamic recovery — interpolate runtime context, override the contract default.
|
|
400
424
|
if (!matched) throw ctx.fail('no_match', `No data for ${input.pmids.length} PMIDs`, {
|
|
401
425
|
pmids: input.pmids,
|
|
@@ -404,7 +428,7 @@ async handler(input, ctx) {
|
|
|
404
428
|
}
|
|
405
429
|
```
|
|
406
430
|
|
|
407
|
-
**`ctx.recoveryFor(reason)`** returns `{}` when no contract exists (spread-safe). Typed against the declared reason union on `HandlerContext<R>`.
|
|
431
|
+
**`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
432
|
|
|
409
433
|
**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
434
|
|
|
@@ -422,9 +446,9 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
|
|
|
422
446
|
|
|
423
447
|
**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
448
|
|
|
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.
|
|
449
|
+
**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
450
|
|
|
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'`)
|
|
451
|
+
**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
452
|
|
|
429
453
|
See `api-errors` skill for the full pattern-matching table, error code reference, and detailed examples.
|
|
430
454
|
|
|
@@ -458,7 +482,7 @@ Managed by `@cyanheads/mcp-ts-core`. Validated via Zod. Precedence: `createApp()
|
|
|
458
482
|
| Category | Key Variables |
|
|
459
483
|
|:---------|:-------------|
|
|
460
484
|
| 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_
|
|
485
|
+
| Auth | `MCP_AUTH_MODE`, `MCP_AUTH_SECRET_KEY`, `MCP_AUTH_DISABLE_SCOPE_CHECKS`, `OAUTH_*`, `MCP_REQUEST_STATE_KEY` (opt-in `requestState` sealing) |
|
|
462
486
|
| Storage | `STORAGE_PROVIDER_TYPE` (`in-memory`\|`filesystem`\|`supabase`\|`cloudflare-r2`\|`cloudflare-kv`\|`cloudflare-d1`) |
|
|
463
487
|
| LLM | `OPENROUTER_API_KEY`, `OPENROUTER_APP_URL/NAME`, `LLM_DEFAULT_*` |
|
|
464
488
|
| Telemetry | `OTEL_ENABLED`, `OTEL_SERVICE_NAME/VERSION`, `OTEL_EXPORTER_OTLP_*` |
|
|
@@ -485,7 +509,7 @@ describe('myTool', () => {
|
|
|
485
509
|
});
|
|
486
510
|
```
|
|
487
511
|
|
|
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.
|
|
512
|
+
**`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
513
|
|
|
490
514
|
**`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
515
|
|
|
@@ -495,7 +519,7 @@ describe('myTool', () => {
|
|
|
495
519
|
|
|
496
520
|
**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
521
|
|
|
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
|
|
522
|
+
**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
523
|
|
|
500
524
|
**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
525
|
|
|
@@ -512,6 +536,8 @@ it('survives fuzz testing', async () => {
|
|
|
512
536
|
|
|
513
537
|
Options: `numRuns` (valid inputs, default 50), `numAdversarial` (adversarial inputs, default 30), `seed` (reproducibility), `timeout` (per-call ms, default 5000), `ctx` (`MockContextOptions` for stateful handlers). Also exports `zodToArbitrary(schema)` for custom property-based tests and `ADVERSARIAL_STRINGS` for targeted injection testing.
|
|
514
538
|
|
|
539
|
+
**App views:** `renderAppTool` from `/testing/apps` connects to a server as an MCP Apps client, calls an app tool, and renders its `ui://` view in `chrome-headless-shell` inside the spec's double-iframe sandbox and CSP. The report carries `initialized`, `errors`, `cspViolations`, every view↔host `messages` entry, the rendered `text`, and `screenshots`; only setup failures (missing peer, no browser, server unreachable, tool missing, UI resource absent or unreadable, a `_meta.ui.csp` entry that is not a plain origin) throw. `mcp-ts-core app-render` is the CLI form. Needs the optional peers `@modelcontextprotocol/client` and `@modelcontextprotocol/ext-apps`, plus a `chrome-headless-shell` build — the newest in `~/.cache/puppeteer`, or `browserPath` / `MCP_APPS_BROWSER_PATH`; the `field-test` skill covers installing one and reading a report.
|
|
540
|
+
|
|
515
541
|
**Vitest config:** Extend core config, add `@/` alias: `resolve: { alias: { '@/': new URL('./src/', import.meta.url).pathname } }`. Construct deps in `beforeEach`. Re-init services per suite.
|
|
516
542
|
|
|
517
543
|
---
|
package/CLAUDE.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Package:** `@cyanheads/mcp-ts-core`
|
|
4
|
-
**Version:** 0.13.
|
|
4
|
+
**Version:** 0.13.11
|
|
5
5
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
6
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
6
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.2.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.6.5
|
|
8
8
|
**GitHub:** [cyanheads/mcp-ts-core](https://github.com/cyanheads/mcp-ts-core)
|
|
9
9
|
**npm:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
|
|
@@ -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 |
|
|
@@ -61,6 +61,7 @@ Both paths share the same public API. Init copies starter `package.json`, config
|
|
|
61
61
|
| `/services` | `OpenRouterProvider`, `SpeechService`, `createSpeechProvider`, `ElevenLabsProvider`, `WhisperProvider`, `GraphService`, provider interfaces and types | LLM, Speech (TTS/STT), Graph services |
|
|
62
62
|
| `/linter` | `validateDefinitions`, `LintReport`, `LintDiagnostic`, `LintInput`, `LintSeverity` | Definition validation |
|
|
63
63
|
| `/testing` | `createMockContext`, `createMockSession`, `createFetchMock`, `runToolContract`, `createMockLogger`, `getEnrichment`, `getContentBlocks`, `createInMemoryStorage`, `expectInputRequired` | Test kit for handlers and upstream HTTP boundaries |
|
|
64
|
+
| `/testing/apps` | `renderAppTool`, `RenderAppToolOptions`, `AppRenderReport`, `AppRenderStep`, `AppServerTarget`, `AppHostOptions` | Headless MCP Apps host that renders an app tool's `ui://` view and reports on it (optional peers `@modelcontextprotocol/client`, `@modelcontextprotocol/ext-apps`) |
|
|
64
65
|
| `/testing/fuzz` | `fuzzTool`, `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, `adversarialArbitrary`, `ADVERSARIAL_STRINGS` | Fuzz testing |
|
|
65
66
|
| `/testing/vitest` | `mcpTest`, `toolContractSuite`, `McpTestFixtures` (+ re-exported `/testing` helpers) | Vitest fixtures and tool conformance suites (optional peer `vitest`) |
|
|
66
67
|
|
|
@@ -300,10 +301,11 @@ interface Context {
|
|
|
300
301
|
readonly traceId?: string;
|
|
301
302
|
readonly spanId?: string;
|
|
302
303
|
readonly auth?: AuthContext;
|
|
304
|
+
readonly clientCapabilities: ClientCapabilities | undefined; // what the client declared; undefined when no view (2025-era stateless HTTP)
|
|
303
305
|
readonly log: ContextLogger; // auto-correlated: requestId, traceId, tenantId
|
|
304
306
|
readonly state: ContextState; // tenant-scoped KV storage
|
|
305
307
|
readonly requestInput: RequestInputFn; // (spec, options?) => never — suspends and asks the caller for input
|
|
306
|
-
readonly inputs: ContextInputs; //
|
|
308
|
+
readonly inputs: ContextInputs; // the request's responses, limited to what the client declared
|
|
307
309
|
readonly notifyPromptListChanged?: (() => void) | undefined; // prompt list changed
|
|
308
310
|
readonly notifyResourceListChanged?: (() => void) | undefined; // resource list changed
|
|
309
311
|
readonly notifyResourceUpdated?: ((uri: string) => void) | undefined; // resource content changed
|
|
@@ -312,13 +314,13 @@ interface Context {
|
|
|
312
314
|
readonly uri?: URL; // present for resource handlers
|
|
313
315
|
readonly content: ContentCollect; // media blocks → prepended to content[]; never in structuredContent
|
|
314
316
|
readonly enrich: Enrich; // success-path agent context → structuredContent + content[]; typed on HandlerContext<R, E>
|
|
315
|
-
recoveryFor(reason: string): { recovery: { hint: string } } | Record<string, never>; //
|
|
317
|
+
recoveryFor(reason: string): { recovery: { hint: string } } | Record<string, never>; // a declared entry's hint, in wire shape
|
|
316
318
|
}
|
|
317
319
|
```
|
|
318
320
|
|
|
319
321
|
### `ctx.log`
|
|
320
322
|
|
|
321
|
-
Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `error`. Auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. Use `ctx.log` in handlers; global `logger` for startup/shutdown/background.
|
|
323
|
+
Opt-in domain-specific logging. Methods: `debug`, `info`, `notice`, `warning`, `error`. Auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. Each record also reaches the client as `notifications/message` — only at or above `MCP_LOG_LEVEL` (RFC 5424 order; a client's own level can only narrow it), with sensitive fields masked as `[REDACTED]`. Use `ctx.log` in handlers; global `logger` for startup/shutdown/background.
|
|
322
324
|
|
|
323
325
|
### `ctx.state`
|
|
324
326
|
|
|
@@ -367,11 +369,33 @@ useFormat(answer.format);
|
|
|
367
369
|
cancelled prompt is terminal, not a round to retry. `inputRequired.elicitUrl({ message, url })`
|
|
368
370
|
hands the user an external link instead of a form.
|
|
369
371
|
|
|
372
|
+
A client can send responses on a call nothing asked for, so only what it declared reaches
|
|
373
|
+
`ctx.inputs`, at the mode level the request would need: an elicit result needs `elicitation` —
|
|
374
|
+
`elicitation.form` when it carries `content` — a sampling result `sampling` (`sampling.tools` when it
|
|
375
|
+
holds a tool block), a roots result `roots`, and a request with no capability view gets none.
|
|
376
|
+
`ctx.clientCapabilities` — the SDK-parsed `initialize` value on 2025-era connections (a bare
|
|
377
|
+
`elicitation: {}` reads back as `{ elicitation: { form: {} } }`), the request's envelope as sent on
|
|
378
|
+
2026-07-28 — decides whether to *ask* for optional context (request roots only when `roots` is
|
|
379
|
+
declared, else fall through); it is never a reason to skip a consent prompt.
|
|
380
|
+
|
|
370
381
|
A 2025-era client that declared no matching capability is refused as `client_capability_missing`,
|
|
371
382
|
with a hint that ends at reconnecting. When the tool's own arguments can stand in for the answer,
|
|
372
383
|
say so per call — `ctx.requestInput(spec, { fallbackHint: 'Or call again with noun supplied.' })` —
|
|
373
384
|
and the sentence is appended to that hint. A consent gate passes none: it has no such field.
|
|
374
385
|
|
|
386
|
+
**Consent gates redeem a server record.** A capable client can still pre-answer, and any
|
|
387
|
+
`requestState` can be replayed within its lifetime, so a destructive handler first redeems (reads and
|
|
388
|
+
deletes) a `ctx.state` record `{ operation, clientId, subject, target, contentHash }` stored under a
|
|
389
|
+
random id — the id is all `requestState` carries — and asks again on an unknown, used, or expired id,
|
|
390
|
+
or when any field differs from this call. Carrying the target in `requestState` and comparing on
|
|
391
|
+
re-entry is replayable. Redeeming stops a sequential replay, not concurrent retries: until an atomic
|
|
392
|
+
`ctx.state.take` exists (#593), an action that must not repeat is made idempotent per record. The
|
|
393
|
+
record's storage is shared by every instance a 2026-07-28 retry can reach — `filesystem`, `supabase`,
|
|
394
|
+
or `cloudflare-d1`, never `cloudflare-kv`. `MCP_REQUEST_STATE_KEY` (≥ 32 bytes, the same on every
|
|
395
|
+
instance) opts into sealing: the framework signs the string a handler returns, the SDK rejects any
|
|
396
|
+
other as `-32602` `invalid_request_state` before the handler runs, and `ctx.inputs.state()` still
|
|
397
|
+
returns the original string. See `api-context` for the full pattern.
|
|
398
|
+
|
|
375
399
|
### `ctx.content`
|
|
376
400
|
|
|
377
401
|
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 +406,7 @@ See `api-context` skill for full details.
|
|
|
382
406
|
|
|
383
407
|
## Error Handling
|
|
384
408
|
|
|
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
|
|
409
|
+
**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
410
|
|
|
387
411
|
```ts
|
|
388
412
|
errors: [
|
|
@@ -394,8 +418,8 @@ errors: [
|
|
|
394
418
|
recovery: 'Wait 30 seconds before retrying or reduce batch size.' },
|
|
395
419
|
],
|
|
396
420
|
async handler(input, ctx) {
|
|
397
|
-
// Static recovery —
|
|
398
|
-
if (queue.full()) throw ctx.fail('queue_full'
|
|
421
|
+
// Static recovery — the framework fills the contract's hint onto the wire.
|
|
422
|
+
if (queue.full()) throw ctx.fail('queue_full');
|
|
399
423
|
// Dynamic recovery — interpolate runtime context, override the contract default.
|
|
400
424
|
if (!matched) throw ctx.fail('no_match', `No data for ${input.pmids.length} PMIDs`, {
|
|
401
425
|
pmids: input.pmids,
|
|
@@ -404,7 +428,7 @@ async handler(input, ctx) {
|
|
|
404
428
|
}
|
|
405
429
|
```
|
|
406
430
|
|
|
407
|
-
**`ctx.recoveryFor(reason)`** returns `{}` when no contract exists (spread-safe). Typed against the declared reason union on `HandlerContext<R>`.
|
|
431
|
+
**`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
432
|
|
|
409
433
|
**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
434
|
|
|
@@ -422,9 +446,9 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
|
|
|
422
446
|
|
|
423
447
|
**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
448
|
|
|
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.
|
|
449
|
+
**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
450
|
|
|
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'`)
|
|
451
|
+
**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
452
|
|
|
429
453
|
See `api-errors` skill for the full pattern-matching table, error code reference, and detailed examples.
|
|
430
454
|
|
|
@@ -458,7 +482,7 @@ Managed by `@cyanheads/mcp-ts-core`. Validated via Zod. Precedence: `createApp()
|
|
|
458
482
|
| Category | Key Variables |
|
|
459
483
|
|:---------|:-------------|
|
|
460
484
|
| 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_
|
|
485
|
+
| Auth | `MCP_AUTH_MODE`, `MCP_AUTH_SECRET_KEY`, `MCP_AUTH_DISABLE_SCOPE_CHECKS`, `OAUTH_*`, `MCP_REQUEST_STATE_KEY` (opt-in `requestState` sealing) |
|
|
462
486
|
| Storage | `STORAGE_PROVIDER_TYPE` (`in-memory`\|`filesystem`\|`supabase`\|`cloudflare-r2`\|`cloudflare-kv`\|`cloudflare-d1`) |
|
|
463
487
|
| LLM | `OPENROUTER_API_KEY`, `OPENROUTER_APP_URL/NAME`, `LLM_DEFAULT_*` |
|
|
464
488
|
| Telemetry | `OTEL_ENABLED`, `OTEL_SERVICE_NAME/VERSION`, `OTEL_EXPORTER_OTLP_*` |
|
|
@@ -485,7 +509,7 @@ describe('myTool', () => {
|
|
|
485
509
|
});
|
|
486
510
|
```
|
|
487
511
|
|
|
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.
|
|
512
|
+
**`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
513
|
|
|
490
514
|
**`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
515
|
|
|
@@ -495,7 +519,7 @@ describe('myTool', () => {
|
|
|
495
519
|
|
|
496
520
|
**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
521
|
|
|
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
|
|
522
|
+
**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
523
|
|
|
500
524
|
**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
525
|
|
|
@@ -512,6 +536,8 @@ it('survives fuzz testing', async () => {
|
|
|
512
536
|
|
|
513
537
|
Options: `numRuns` (valid inputs, default 50), `numAdversarial` (adversarial inputs, default 30), `seed` (reproducibility), `timeout` (per-call ms, default 5000), `ctx` (`MockContextOptions` for stateful handlers). Also exports `zodToArbitrary(schema)` for custom property-based tests and `ADVERSARIAL_STRINGS` for targeted injection testing.
|
|
514
538
|
|
|
539
|
+
**App views:** `renderAppTool` from `/testing/apps` connects to a server as an MCP Apps client, calls an app tool, and renders its `ui://` view in `chrome-headless-shell` inside the spec's double-iframe sandbox and CSP. The report carries `initialized`, `errors`, `cspViolations`, every view↔host `messages` entry, the rendered `text`, and `screenshots`; only setup failures (missing peer, no browser, server unreachable, tool missing, UI resource absent or unreadable, a `_meta.ui.csp` entry that is not a plain origin) throw. `mcp-ts-core app-render` is the CLI form. Needs the optional peers `@modelcontextprotocol/client` and `@modelcontextprotocol/ext-apps`, plus a `chrome-headless-shell` build — the newest in `~/.cache/puppeteer`, or `browserPath` / `MCP_APPS_BROWSER_PATH`; the `field-test` skill covers installing one and reading a report.
|
|
540
|
+
|
|
515
541
|
**Vitest config:** Extend core config, add `@/` alias: `resolve: { alias: { '@/': new URL('./src/', import.meta.url).pathname } }`. Construct deps in `beforeEach`. Re-init services per suite.
|
|
516
542
|
|
|
517
543
|
---
|
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
<div align="center">
|
|
8
8
|
|
|
9
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://modelcontextprotocol.io/specification/2026-07-28)
|
|
10
10
|
|
|
11
|
-
[](https://modelcontextprotocol.io/) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
12
12
|
|
|
13
13
|
[Quick start](#quick-start) · [Capabilities](#what-comes-with-it) · [API reference](#api-overview) · [Examples](#examples)
|
|
14
14
|
|
|
@@ -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 |
|
|
@@ -304,6 +304,7 @@ import { validateDefinitions } from '@cyanheads/mcp-ts-core/linter';
|
|
|
304
304
|
import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
|
|
305
305
|
import { mcpTest, toolContractSuite } from '@cyanheads/mcp-ts-core/testing/vitest';
|
|
306
306
|
import { fuzzTool, fuzzResource, fuzzPrompt } from '@cyanheads/mcp-ts-core/testing/fuzz';
|
|
307
|
+
import { renderAppTool } from '@cyanheads/mcp-ts-core/testing/apps';
|
|
307
308
|
```
|
|
308
309
|
|
|
309
310
|
See [CLAUDE.md/AGENTS.md](CLAUDE.md) for the complete exports reference.
|
|
@@ -334,7 +335,7 @@ const input = myTool.input.parse({ query: 'test' });
|
|
|
334
335
|
const result = await myTool.handler(input, ctx);
|
|
335
336
|
```
|
|
336
337
|
|
|
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`,
|
|
338
|
+
`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
339
|
|
|
339
340
|
`/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
341
|
|
|
@@ -351,6 +352,31 @@ expect(report.prototypePollution).toBe(false);
|
|
|
351
352
|
|
|
352
353
|
It also exports `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, and `ADVERSARIAL_STRINGS` for custom property-based tests.
|
|
353
354
|
|
|
355
|
+
`/testing/apps` is a headless MCP Apps host. `renderAppTool` connects to your server as an MCP Apps client, calls an app tool, loads its `ui://` view into `chrome-headless-shell` inside the sandbox and CSP the MCP Apps spec prescribes, runs scripted steps against the view, and returns a report:
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { renderAppTool } from '@cyanheads/mcp-ts-core/testing/apps';
|
|
359
|
+
|
|
360
|
+
const run = await renderAppTool({
|
|
361
|
+
server: { command: 'bun', args: ['run', 'dist/index.js'] }, // or { url }
|
|
362
|
+
tool: 'my_app_tool',
|
|
363
|
+
arguments: { query: 'probe' },
|
|
364
|
+
steps: [{ click: '#action-btn' }, { screenshot: 'after-click' }],
|
|
365
|
+
});
|
|
366
|
+
expect(run.initialized).toBe(true);
|
|
367
|
+
expect(run.errors).toHaveLength(0);
|
|
368
|
+
expect(run.cspViolations).toHaveLength(0);
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The report also carries every message between the view and the host, the view's rendered text, and the screenshot paths. The same run from the command line, writing `report.json` and the screenshots under `--out`:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
bunx @cyanheads/mcp-ts-core app-render --tool my_app_tool --args '{"query":"probe"}' \
|
|
375
|
+
--click '#action-btn' --out ./app-run -- bun run dist/index.js
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
It needs the optional peers `@modelcontextprotocol/client` and `@modelcontextprotocol/ext-apps`, and a `chrome-headless-shell` build: the newest one in Puppeteer's cache (`npx @puppeteer/browsers install chrome-headless-shell@stable --path ~/.cache/puppeteer`), or an explicit executable path through `browserPath`, `--browser`, or `MCP_APPS_BROWSER_PATH`. Installed Chrome, Edge, Brave, and Chromium are never searched for.
|
|
379
|
+
|
|
354
380
|
## Documentation
|
|
355
381
|
|
|
356
382
|
- **[CLAUDE.md/AGENTS.md](CLAUDE.md)**: the framework reference, covering exports, patterns, `Context`, error codes, auth, config, and testing. It ships in the npm package, so your agent reads it from `node_modules` after `init`.
|
|
@@ -361,7 +387,7 @@ It also exports `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, and `ADVERSARIAL
|
|
|
361
387
|
|
|
362
388
|
```bash
|
|
363
389
|
bun run rebuild # clean + build (scripts/clean.ts + scripts/build.ts)
|
|
364
|
-
bun run devcheck # full gate: lint/format, typecheck, MCP defs, framework antipatterns, docs/skills/changelog sync, audit, outdated, secrets
|
|
390
|
+
bun run devcheck # full gate: lint/format, typecheck, MCP defs, packaging, framework antipatterns, docs/skills/changelog sync, audit, outdated, tracked-secrets and to-do marker scans
|
|
365
391
|
bun run lint:mcp # validate MCP definitions against spec
|
|
366
392
|
bun run test:all # rebuild + coverage + Node.js + Workers + integration
|
|
367
393
|
bun run test:package # pack the tarball and consume it as an external project would
|
package/biome.json
CHANGED