@cyanheads/mcp-ts-core 0.12.0 → 0.12.2
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 +7 -3
- package/CLAUDE.md +7 -3
- package/README.md +1 -1
- package/changelog/0.12.x/0.12.1.md +26 -0
- package/changelog/0.12.x/0.12.2.md +29 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +21 -3
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts +45 -1
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +18 -3
- package/dist/core/app.js.map +1 -1
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/serverManifest.d.ts +11 -2
- package/dist/core/serverManifest.d.ts.map +1 -1
- package/dist/core/serverManifest.js +30 -12
- package/dist/core/serverManifest.js.map +1 -1
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +2 -2
- package/dist/core/worker.js.map +1 -1
- package/dist/linter/rules/enrichment-rules.d.ts.map +1 -1
- package/dist/linter/rules/enrichment-rules.js +5 -2
- package/dist/linter/rules/enrichment-rules.js.map +1 -1
- package/dist/linter/rules/portability-rules.d.ts +4 -2
- package/dist/linter/rules/portability-rules.d.ts.map +1 -1
- package/dist/linter/rules/portability-rules.js +33 -4
- package/dist/linter/rules/portability-rules.js.map +1 -1
- package/dist/linter/rules/schema-rules.d.ts +28 -3
- package/dist/linter/rules/schema-rules.d.ts.map +1 -1
- package/dist/linter/rules/schema-rules.js +87 -25
- package/dist/linter/rules/schema-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +9 -3
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/linter/types.d.ts +2 -1
- package/dist/linter/types.d.ts.map +1 -1
- package/dist/logs/combined.log +10 -10
- package/dist/logs/error.log +6 -6
- package/dist/mcp-server/cacheHints.d.ts +35 -0
- package/dist/mcp-server/cacheHints.d.ts.map +1 -0
- package/dist/mcp-server/cacheHints.js +43 -0
- package/dist/mcp-server/cacheHints.js.map +1 -0
- package/dist/mcp-server/notifications.d.ts +68 -1
- package/dist/mcp-server/notifications.d.ts.map +1 -1
- package/dist/mcp-server/notifications.js +67 -0
- package/dist/mcp-server/notifications.js.map +1 -1
- package/dist/mcp-server/resources/resource-registration.d.ts +2 -2
- package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
- package/dist/mcp-server/resources/resource-registration.js +6 -2
- package/dist/mcp-server/resources/resource-registration.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceDefinition.d.ts +14 -1
- package/dist/mcp-server/resources/utils/resourceDefinition.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceDefinition.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +13 -5
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +28 -26
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/server.d.ts +16 -1
- package/dist/mcp-server/server.d.ts.map +1 -1
- package/dist/mcp-server/server.js +7 -2
- package/dist/mcp-server/server.js.map +1 -1
- package/dist/mcp-server/tools/tool-registration.d.ts +2 -2
- package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-registration.js +2 -1
- package/dist/mcp-server/tools/tool-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/headerParam.d.ts +113 -0
- package/dist/mcp-server/tools/utils/headerParam.d.ts.map +1 -0
- package/dist/mcp-server/tools/utils/headerParam.js +280 -0
- package/dist/mcp-server/tools/utils/headerParam.js.map +1 -0
- package/dist/mcp-server/tools/utils/schemaShape.d.ts +27 -0
- package/dist/mcp-server/tools/utils/schemaShape.d.ts.map +1 -0
- package/dist/mcp-server/tools/utils/schemaShape.js +43 -0
- package/dist/mcp-server/tools/utils/schemaShape.js.map +1 -0
- package/dist/mcp-server/tools/utils/toolDefinition.d.ts +28 -5
- package/dist/mcp-server/tools/utils/toolDefinition.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolDefinition.js +22 -3
- package/dist/mcp-server/tools/utils/toolDefinition.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +13 -5
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +35 -28
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/http/eventStore.d.ts +57 -0
- package/dist/mcp-server/transports/http/eventStore.d.ts.map +1 -0
- package/dist/mcp-server/transports/http/eventStore.js +114 -0
- package/dist/mcp-server/transports/http/eventStore.js.map +1 -0
- package/dist/mcp-server/transports/http/httpServer.d.ts +2 -1
- package/dist/mcp-server/transports/http/httpServer.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpServer.js +2 -2
- package/dist/mcp-server/transports/http/httpServer.js.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.d.ts +8 -3
- package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.js +30 -5
- package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
- package/dist/mcp-server/transports/http/landing-page/sections/tools.js +22 -2
- package/dist/mcp-server/transports/http/landing-page/sections/tools.js.map +1 -1
- package/dist/mcp-server/transports/manager.d.ts +10 -1
- package/dist/mcp-server/transports/manager.d.ts.map +1 -1
- package/dist/mcp-server/transports/manager.js +6 -2
- package/dist/mcp-server/transports/manager.js.map +1 -1
- package/dist/mcp-server/types.d.ts +20 -1
- package/dist/mcp-server/types.d.ts.map +1 -1
- package/dist/mcp-server/types.js +14 -0
- package/dist/mcp-server/types.js.map +1 -1
- package/dist/testing/fuzz.d.ts +7 -4
- package/dist/testing/fuzz.d.ts.map +1 -1
- package/dist/testing/fuzz.js +13 -5
- package/dist/testing/fuzz.js.map +1 -1
- package/dist/utils/internal/performance.d.ts +12 -4
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +23 -8
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/internal/requestContext.d.ts +13 -0
- package/dist/utils/internal/requestContext.d.ts.map +1 -1
- package/dist/utils/internal/requestContext.js +43 -8
- package/dist/utils/internal/requestContext.js.map +1 -1
- package/package.json +2 -2
- package/skills/add-resource/SKILL.md +31 -1
- package/skills/add-tool/SKILL.md +70 -1
- package/skills/api-config/SKILL.md +30 -3
- package/skills/api-context/SKILL.md +47 -15
- package/skills/api-linter/SKILL.md +39 -3
- package/skills/api-telemetry/SKILL.md +3 -1
- package/skills/api-workers/SKILL.md +9 -1
- package/skills/tool-defs-analysis/SKILL.md +15 -5
- package/templates/.env.example +4 -0
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Canonical reference for the unified `Context` object passed to every tool and resource handler in `@cyanheads/mcp-ts-core`. Covers the full interface, its `RequestContext` base, all sub-APIs (`ctx.log`, `ctx.state`, `ctx.requestInput`, `ctx.inputs`, `ctx.enrich`, `ctx.content`), and when to use each.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.1"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -28,8 +28,8 @@ interface Context extends RequestContext {
|
|
|
28
28
|
readonly timestamp: string; // ISO 8601 request start time
|
|
29
29
|
readonly tenantId?: string; // JWT 'tid' claim; 'default' for stdio and HTTP+MCP_AUTH_MODE=none
|
|
30
30
|
readonly sessionId?: string; // Mcp-Session-Id (HTTP stateful/auto); undefined elsewhere unless opted in
|
|
31
|
-
readonly traceId?: string; //
|
|
32
|
-
readonly spanId?: string; //
|
|
31
|
+
readonly traceId?: string; // Trace containing this handler execution
|
|
32
|
+
readonly spanId?: string; // The handler's own execution span
|
|
33
33
|
readonly auth?: AuthContext; // Parsed auth claims (clientId, scopes, sub)
|
|
34
34
|
readonly operation?: string; // Label for the operation this context belongs to
|
|
35
35
|
readonly extra?: Readonly<Record<string, unknown>>; // Correlation bag — the one open field
|
|
@@ -86,8 +86,8 @@ interface Context extends RequestContext {
|
|
|
86
86
|
| `timestamp` | Yes | ISO 8601, request start |
|
|
87
87
|
| `tenantId` | Stdio and HTTP+`MCP_AUTH_MODE=none` (as `'default'`); JWT `tid` claim in HTTP+`jwt`/`oauth` | JWT / single-tenant default |
|
|
88
88
|
| `sessionId` | HTTP `stateful` / `auto` mode; undefined for stdio and stateless HTTP unless opted in | `Mcp-Session-Id` header (or server-minted) — see [§ `ctx.sessionId`](#ctxsessionid) |
|
|
89
|
-
| `traceId` | When OTEL enabled |
|
|
90
|
-
| `spanId` | When OTEL enabled |
|
|
89
|
+
| `traceId` | When OTEL enabled | Trace containing this handler execution |
|
|
90
|
+
| `spanId` | When OTEL enabled | The active `tool_execution:*` / `resource_read:*` span |
|
|
91
91
|
| `auth` | When auth enabled | Parsed JWT claims |
|
|
92
92
|
|
|
93
93
|
---
|
|
@@ -256,6 +256,8 @@ Optional HTTP session identifier. Surfaced when the request carries a durable se
|
|
|
256
256
|
|
|
257
257
|
In `stateful` / `auto` mode, the value mirrors the `Mcp-Session-Id` HTTP header (or a server-minted token for new sessions). Each subsequent request from the same client reuses it; reconnects after disconnect bind to the same session as long as it hasn't expired.
|
|
258
258
|
|
|
259
|
+
The in-flight SSE stream is resumable too: stateful sessions carry a bounded event store, so a client reconnecting with `Last-Event-ID` gets the frames it missed replayed before the live stream resumes. On by default — selecting the session mode is the opt-in — with `MCP_HTTP_RESUMABILITY=false` as the kill switch and retention capped by both event count and TTL (`api-config` has the knobs). The buffer is released when its session is evicted.
|
|
260
|
+
|
|
259
261
|
### Stateless-mode opt-in
|
|
260
262
|
|
|
261
263
|
In stateless HTTP mode the SDK still hands the framework a freshly generated token for every request, but it has request-lifetime semantics (no `SessionStore`, no continuity). The framework hides this from handlers by default — `ctx.sessionId` is `undefined` so any handler treating it as durable fails closed.
|
|
@@ -442,20 +444,50 @@ async handler(input, ctx) {
|
|
|
442
444
|
|
|
443
445
|
### Delivery
|
|
444
446
|
|
|
445
|
-
|
|
447
|
+
Which channel a notification takes depends on the protocol era the request is being served under, because the two eras opt a client in differently.
|
|
448
|
+
|
|
449
|
+
| Era | Client opts in via | `ctx.notify*` routes to |
|
|
450
|
+
|:----|:-------------------|:------------------------|
|
|
451
|
+
| 2026-07-28 | a `subscriptions/listen` stream, whose filter names the types (and, for resources, the URIs) it wants | the change-event bus, where the SDK's listen router applies that filter |
|
|
452
|
+
| 2025-era | `resources/subscribe`, for resource updates only | the request's own channel, stamped with `relatedRequestId` |
|
|
453
|
+
|
|
454
|
+
The modern routing is not an optimization — the spec is explicit that a server MUST NOT send notification types the client has not requested, and that filter only sees what reaches the bus. A modern handler sending through its own request scope would deliver to a client that opened no stream at all.
|
|
446
455
|
|
|
447
456
|
| Fired from | stdio | HTTP / Workers |
|
|
448
457
|
|:-----------|:------|:---------------|
|
|
449
|
-
| A tool / resource handler | ✅ delivered | ✅ delivered
|
|
450
|
-
| A `setup()` hook, cron job, or any non-request scope | ✅ delivered | ⚠️ dropped
|
|
458
|
+
| A tool / resource handler | ✅ delivered | ✅ delivered — on the listen stream (2026) or the request's SSE response stream (2025) |
|
|
459
|
+
| A `setup()` hook, cron job, or any non-request scope | ✅ delivered | ✅ to 2026 clients, via `core.notify` — see below. ⚠️ still dropped for 2025 clients: there is no out-of-request channel on that era |
|
|
460
|
+
|
|
461
|
+
### Emitting outside a request: `core.notify`
|
|
462
|
+
|
|
463
|
+
Under HTTP there is no long-lived server instance a background emitter can send through, so `CoreServices.notify` publishes straight to the bus:
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
createApp({
|
|
467
|
+
tools,
|
|
468
|
+
setup(core) {
|
|
469
|
+
watchUpstream(() => core.notify.resourcesChanged());
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
It is the same `ServerNotifier` the handler path publishes to, captured before serving starts; publishing while nothing is listening is a no-op, not an error. Delivery reaches 2026-07-28 clients with an open `subscriptions/listen` stream.
|
|
475
|
+
|
|
476
|
+
**Supply your own bus on a multi-isolate runtime.** The default is in-process, which covers a single container. On Cloudflare Workers a background emission would otherwise reach only the isolate that produced it:
|
|
451
477
|
|
|
452
|
-
|
|
478
|
+
```ts
|
|
479
|
+
createApp({ tools, eventBus: myDurableObjectBackedBus });
|
|
480
|
+
```
|
|
453
481
|
|
|
454
482
|
### `notifyResourceUpdated` is subscription-scoped
|
|
455
483
|
|
|
456
|
-
|
|
484
|
+
On both eras, but through different registries.
|
|
485
|
+
|
|
486
|
+
On 2025-era connections the framework advertises `resources: { subscribe: true }` and backs it with real `resources/subscribe` / `resources/unsubscribe` handlers, so `notifyResourceUpdated(uri)` emits only for URIs the connected client actually subscribed to; an unsubscribed URI logs at debug and sends nothing. Both handlers are idempotent — re-subscribing is a no-op, and unsubscribing from a URI that was never subscribed succeeds.
|
|
487
|
+
|
|
488
|
+
That registry's scope is the `McpServer` instance, which is also the connection: one persistent instance per session on the sessionful arm, one per request under per-request serving. On the per-request leg a subscription cannot outlive the request that created it, so a handler-time `ctx.notifyResourceUpdated(uri)` delivers only when that same exchange subscribed first.
|
|
457
489
|
|
|
458
|
-
|
|
490
|
+
On 2026-07-28 there is no `resources/subscribe` method and no registry. The URI ships with the published event, and the listen filter's `resourceSubscriptions` field decides who receives it — upstream's job, not the framework's.
|
|
459
491
|
|
|
460
492
|
---
|
|
461
493
|
|
|
@@ -761,8 +793,8 @@ Test content blocks with `getContentBlocks(ctx)` from `@cyanheads/mcp-ts-core/te
|
|
|
761
793
|
| `ctx.timestamp` | `string` | Always |
|
|
762
794
|
| `ctx.tenantId` | `string \| undefined` | Stdio (`'default'`); HTTP+`MCP_AUTH_MODE=none` (`'default'`); HTTP+`jwt`/`oauth` (JWT `tid` claim — undefined if absent) |
|
|
763
795
|
| `ctx.sessionId` | `string \| undefined` | HTTP `stateful` / `auto` mode; stateless HTTP only when `createApp({ context: { exposeStatelessSessionId: true } })`; never in stdio or on the session-less 2026-07-28 leg |
|
|
764
|
-
| `ctx.traceId` | `string \| undefined` | OTEL enabled |
|
|
765
|
-
| `ctx.spanId` | `string \| undefined` | OTEL enabled |
|
|
796
|
+
| `ctx.traceId` | `string \| undefined` | OTEL enabled — the trace containing this handler execution |
|
|
797
|
+
| `ctx.spanId` | `string \| undefined` | OTEL enabled — the handler's own execution span, not the enclosing request span |
|
|
766
798
|
| `ctx.auth` | `AuthContext \| undefined` | Auth enabled |
|
|
767
799
|
| `ctx.operation` | `string \| undefined` | Set by the context that created it (`'HandleToolRequest'` for tool calls) |
|
|
768
800
|
| `ctx.extra` | `Readonly<Record<string, unknown>> \| undefined` | When correlation data was attached — the one open bag on the closed shape |
|
|
@@ -774,9 +806,9 @@ Test content blocks with `getContentBlocks(ctx)` from `@cyanheads/mcp-ts-core/te
|
|
|
774
806
|
| `ctx.requestInput` | `(spec) => never` | Always — suspends the handler and asks the caller for more input |
|
|
775
807
|
| `ctx.inputs` | `ContextInputs` | Always; empty until the request is retried with responses |
|
|
776
808
|
| `ctx.notifyResourceListChanged` | `function \| undefined` | Always in handler ctx; delivery request-scoped (see [§ list-changed notifications](#list-changed-notifications-ctxnotify)) |
|
|
777
|
-
| `ctx.notifyResourceUpdated` | `function \| undefined` | Always in handler ctx;
|
|
809
|
+
| `ctx.notifyResourceUpdated` | `function \| undefined` | Always in handler ctx; limited to URIs the client subscribed to, through the listen filter (2026) or the subscribe registry (2025) |
|
|
778
810
|
| `ctx.notifyPromptListChanged` | `function \| undefined` | Always in handler ctx; delivery request-scoped |
|
|
779
|
-
| `ctx.notifyToolListChanged` | `function \| undefined` | Always in handler ctx;
|
|
811
|
+
| `ctx.notifyToolListChanged` | `function \| undefined` | Always in handler ctx; delivered on the client's listen stream (2026) or its own request scope (2025) |
|
|
780
812
|
| `ctx.uri` | `URL \| undefined` | Resource handlers only |
|
|
781
813
|
| `ctx.fail` | `(reason, msg?, data?, opts?) => McpError` | Definition declares `errors[]` contract |
|
|
782
814
|
| `ctx.recoveryFor` | `(reason) => { recovery: { hint } } \| {}` | Always (no-op when no contract); strictly typed on `HandlerContext<R>` |
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
MCP definition linter rules reference. Use when `bun run lint:mcp` or `bun run devcheck` reports a lint error or warning (`format-parity`, `schema-is-object`, `name-format`, `server-json-*`, etc.) and you need to understand the rule, its severity, and how to fix it. Every rule ID the linter emits has an entry in this doc.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.12"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -44,8 +44,8 @@ Grouped by family. Jump to any rule ID via its anchor.
|
|
|
44
44
|
|:-------|:------|:--------|
|
|
45
45
|
| Definition | `definition-invalid` | [Definition rules](#definition-rules) |
|
|
46
46
|
| Format parity | `format-parity`, `format-parity-threw`, `format-parity-walk-failed`, `format-parity-depth-limit` | [Format parity](#format-parity) |
|
|
47
|
-
| Schema | `schema-is-object`, `describe-on-fields`, `schema-serializable`, `schema-unsatisfiable` | [Schema rules](#schema-rules) |
|
|
48
|
-
| Portability | `schema-format-portability`, `schema-anyof-needs-type`, `schema-no-discriminator-keyword`, `schema-no-defs`, `schema-dialect-tag` | [Portability rules](#portability-rules) |
|
|
47
|
+
| Schema | `schema-is-object`, `describe-on-fields`, `schema-serializable`, `schema-unsatisfiable`, `header-param-designation` | [Schema rules](#schema-rules) |
|
|
48
|
+
| Portability | `schema-format-portability`, `schema-anyof-needs-type`, `schema-no-discriminator-keyword`, `schema-no-defs`, `schema-root-oneof-portability`, `schema-dialect-tag` | [Portability rules](#portability-rules) |
|
|
49
49
|
| Names | `name-required`, `name-format`, `name-unique` | [Name rules](#name-rules) |
|
|
50
50
|
| Tools | `description-required`, `handler-required`, `auth-type`, `auth-scope-format`, `annotation-type`, `annotation-coherence`, `meta-ui-type`, `meta-ui-resource-uri-required`, `meta-ui-resource-uri-scheme`, `app-tool-resource-pairing`, `canvas-consumer-missing` | [Tool rules](#tool-rules) |
|
|
51
51
|
| Resources | `uri-template-required`, `uri-template-valid`, `resource-name-not-uri`, `template-params-align` | [Resource rules](#resource-rules) |
|
|
@@ -169,6 +169,10 @@ input: z.array(z.string())
|
|
|
169
169
|
input: z.object({ items: z.array(z.string()).describe('List of items') })
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
+
**One exception, on tool `input` only:** a `z.discriminatedUnion(...)` of object variants is accepted, for a multi-mode tool with mutually exclusive argument sets. It advertises as `{"type": "object", "oneOf": [...]}` — the object requirement holds, and each branch keeps its own `required` list and `const`-tagged discriminator. A bare `z.union(...)` is still rejected: with no discriminator the model has no key to pick a branch by. Output roots stay object-only — the 2025-era projection rewrites a non-object output root and wraps `structuredContent` to match.
|
|
173
|
+
|
|
174
|
+
The other schema rules walk every variant, so a missing `.describe()`, a non-serializable type, or an unsatisfiable node inside one branch is reported at `input|<i>.<field>`.
|
|
175
|
+
|
|
172
176
|
### describe-on-fields
|
|
173
177
|
|
|
174
178
|
**Severity:** warning
|
|
@@ -186,6 +190,7 @@ Every field in `input`, `output`, `params`, or `args` needs a `.describe('...')`
|
|
|
186
190
|
| `z.array(primitive)` element — string, number, enum, regex-branded primitive, etc. | **No** | No — outer array describe is sufficient |
|
|
187
191
|
| `z.union([a, b, ...])` non-literal option | Yes | Yes, on each option |
|
|
188
192
|
| `z.union([..., z.literal(X), ...])` literal option | **No** | No — outer union describe is sufficient |
|
|
193
|
+
| A tool `input` root that is a `z.discriminatedUnion(...)` — its variant objects | Yes, their **fields** | No, not on the variant itself — it is a root, and roots carry no describe |
|
|
189
194
|
|
|
190
195
|
The asymmetry that catches agents: inside `z.union([z.string(), z.array(z.string())])`, the outer `z.string()` option **does** need a describe (unions walk non-literal options), but the `z.string()` inside the inner array does **not** (arrays don't walk primitive elements). If the linter didn't flag a path, don't add a describe there — the redundant describe ships to the JSON Schema as clutter.
|
|
191
196
|
|
|
@@ -242,6 +247,29 @@ Evaluated on the emitted schema rather than on the Zod schema, because the two d
|
|
|
242
247
|
|
|
243
248
|
Not flagged, deliberately: `allOf: []` is vacuously true (matches everything), and empty `required` / `properties` / `prefixItems` are absent constraints rather than impossible ones.
|
|
244
249
|
|
|
250
|
+
### header-param-designation
|
|
251
|
+
|
|
252
|
+
**Severity:** error
|
|
253
|
+
|
|
254
|
+
Fires when a tool's `input` carries an `x-mcp-header` designation — from `headerParam(schema, 'Name')` or a hand-written `.meta({ 'x-mcp-header': 'Name' })` — that violates one of the constraints protocol revision 2026-07-28 places on it.
|
|
255
|
+
|
|
256
|
+
| Constraint | Example violation |
|
|
257
|
+
|:--|:--|
|
|
258
|
+
| Statically reachable through a chain of `properties` keys | A designation on an array element, a `z.record()` value, any field of a **discriminated-union input root** (the root advertises `oneOf`), or a schema hoisted into `$defs` by `.meta({ id })` |
|
|
259
|
+
| Primitive-typed property — `string`, `integer`, `number`, `boolean` | `headerParam(z.object({ … }), 'Region')` |
|
|
260
|
+
| Non-empty RFC 9110 token | `headerParam(z.string(), 'Bad Name')` — spaces, control characters, and HTTP delimiters are all rejected |
|
|
261
|
+
| Case-insensitively unique across the whole input schema | `'Region'` and `'REGION'` on two sibling fields |
|
|
262
|
+
|
|
263
|
+
Evaluated on the emitted JSON Schema, which is the same input the SDK's own scan reads — so a verdict here is the SDK's verdict.
|
|
264
|
+
|
|
265
|
+
The message names the offending field in the linter's path vocabulary: `input.rows[].region` for an array element, `input.map.<key>` for a record value, `input|0.region` for a union branch.
|
|
266
|
+
|
|
267
|
+
**Fix:** move the designation to a top-level or nested object property. For a multi-mode tool, there is no placement that works — a union input root puts every field behind `oneOf`; flatten the schema or drop the designation.
|
|
268
|
+
|
|
269
|
+
**Why it is an error, not a warning:** the SDK enforces this with a `console.warn`. The tool still registers, and conforming Streamable HTTP clients then exclude it from `tools/list` — it silently disappears with nothing reporting the gap. `tool()` throws on the same condition at definition time, so this rule normally fires only for a definition assembled without the builder.
|
|
270
|
+
|
|
271
|
+
Silent when the schema cannot be converted to JSON Schema at all — that is `schema-serializable`'s diagnostic.
|
|
272
|
+
|
|
245
273
|
---
|
|
246
274
|
|
|
247
275
|
## Portability rules
|
|
@@ -309,6 +337,14 @@ Fires when emitted output contains `$defs` or `$ref`. Gemini rejects these (`400
|
|
|
309
337
|
|
|
310
338
|
**Fix:** inline the recursive type with bounded depth, or accept the Gemini limitation if you target only Anthropic clients.
|
|
311
339
|
|
|
340
|
+
### schema-root-oneof-portability
|
|
341
|
+
|
|
342
|
+
**Severity:** warning (only when `portability: 'strict'`)
|
|
343
|
+
|
|
344
|
+
Fires when a tool's advertised `inputSchema` has a root-level `oneOf` — that is, when `input` is a `z.discriminatedUnion(...)`. The emitted shape is valid 2020-12, every branch is a typed object, and the bytes are identical on both MCP protocol revisions. What is unmeasured is vendor handling of a `oneOf` at the *parameter* root: a client that reads only `type` and `properties` would see a parameterless tool and drop the constraint silently rather than erroring. Opt-in, because for Anthropic clients the union is the better shape.
|
|
345
|
+
|
|
346
|
+
**Fix (only if you need the widest vendor reach):** flatten to a single `z.object()` with a discriminator field and optional per-mode fields, and validate the combination in the handler.
|
|
347
|
+
|
|
312
348
|
### schema-dialect-tag
|
|
313
349
|
|
|
314
350
|
**Severity:** warning (only when `portability: 'strict'`)
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Catalog of OpenTelemetry instrumentation built into framework `@cyanheads/mcp-ts-core` — spans, metrics, completion logs, env config, runtime caveats, custom instrumentation patterns, and cardinality rules. Use when enabling OTel export, adding custom spans or metrics in services, debugging missing telemetry, looking up attribute names, or deciding what's safe to put on a metric attribute vs. a span.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.5"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -15,6 +15,8 @@ The framework auto-instruments every tool, resource, prompt, storage, LLM, speec
|
|
|
15
15
|
|
|
16
16
|
`requestId`, `traceId`, and `tenantId` correlate automatically across spans, metrics, and logs. Pino logs get `trace_id`/`span_id` injected when a span is active.
|
|
17
17
|
|
|
18
|
+
A handler's `ctx.traceId` / `ctx.spanId` name the execution span it runs in — `tool_execution:<name>` or `resource_read:<name>` — not the enclosing HTTP request span. Under HTTP the trace ID is the request's, so handler logs join to the request; the span ID is the child execution's, so they join to that span's attributes and duration. On stdio, where no transport span exists, both are still populated from the execution span the framework opens. Both are `undefined` when telemetry is disabled: the non-recording span a disabled pipeline produces carries all-zero IDs, and the framework reports no correlation rather than IDs that correlate to nothing.
|
|
19
|
+
|
|
18
20
|
For the helper API surface (`withSpan`, `createCounter`, `createHistogram`, `buildTraceparent`, etc.) — see the `api-utils` skill, `Telemetry` section. This skill is the catalog of **what** is emitted; that one is the reference for **how** to emit your own.
|
|
19
21
|
|
|
20
22
|
---
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Cloudflare Workers deployment using `createWorkerHandler` from `@cyanheads/mcp-ts-core/worker`. Covers the full handler signature, binding types, CloudflareBindings extensibility, runtime compatibility guards, and wrangler.toml requirements.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.7"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -212,6 +212,14 @@ export function getServerConfig() {
|
|
|
212
212
|
|
|
213
213
|
Leave the env unset (or set to `none`) for Worker deployments. Tools that conditionally use canvas should check the module-level accessor (`if (!getCanvas()) { ... }`) and surface a clear "feature unavailable on this deployment" message. See `api-canvas` for the full DataCanvas reference and setup wiring pattern.
|
|
214
214
|
|
|
215
|
+
**The default notification bus is per-isolate.** `core.notify` and the modern era's `subscriptions/listen` streams share a change-event bus that defaults to in-process — which on Workers means one bus per isolate, so a background emission reaches only the listeners that happen to live in the isolate that produced it. Handler-time `ctx.notify*` is unaffected (the listen stream and the handler are the same request). For fan-out that must cross isolates, supply a bus of your own, backed by a Durable Object or another shared channel:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
createWorkerHandler({ tools, eventBus: myDurableObjectBackedBus });
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The interface is the SDK's `ServerEventBus` — `publish(event)` and `subscribe(listener)`. See `api-context`'s notification section for what publishes onto it.
|
|
222
|
+
|
|
215
223
|
---
|
|
216
224
|
|
|
217
225
|
## Testing Workers with miniflare
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tool-defs-analysis
|
|
3
3
|
description: >
|
|
4
|
-
Read-only audit of MCP definition language across an existing surface — tools, resources, prompts, server instructions. Walks every definition file and checks
|
|
4
|
+
Read-only audit of MCP definition language across an existing surface — tools, resources, prompts, server instructions. Walks every definition file and checks 16 categories the LLM reads to decide whether and how to call: voice & tense, internal leaks, audience leaks, defaults, recovery hints, field descriptions, cross-references, sparsity, examples, structure, mutator observability, unit-bearing numeric names, validator-enforced constraints, annotations truthfulness, single-line strings, exclusive modes in the schema — then a cross-surface pass: naming taxonomy, parameter vocabulary, tool overlap, instructions drift, length outliers. Produces grouped findings with file:line citations and a numbered options list. Use during polish, after a refactor, or before a release. Complements `field-test` (behavior testing) and `security-pass` (security audit).
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.5"
|
|
8
8
|
audience: external
|
|
9
9
|
type: audit
|
|
10
10
|
---
|
|
@@ -22,7 +22,7 @@ This skill is the **review-time pass** for that drift. Read each definition the
|
|
|
22
22
|
| `security-pass` | Injection, scopes, input sinks |
|
|
23
23
|
| `tool-defs-analysis` (this) | LLM-facing language across the existing surface |
|
|
24
24
|
|
|
25
|
-
`field-test` already audits descriptions for implementation leaks, meta-coaching, and consumer-aware phrasing during its catalog step — that's a fast shallow pass alongside live tool calls. This skill is the deeper review:
|
|
25
|
+
`field-test` already audits descriptions for implementation leaks, meta-coaching, and consumer-aware phrasing during its catalog step — that's a fast shallow pass alongside live tool calls. This skill is the deeper review: 16 categories, every field, every recovery hint, every default value, with file:line citations — plus a cross-surface pass for the drift no single file shows.
|
|
26
26
|
|
|
27
27
|
**Read-only.** This skill produces a report; the maintainer applies fixes. While running it, do not run git, do not stage or commit, do not update the changelog, do not run `devcheck`, do not invoke wrapup or release workflows. Fixes flow through the normal authoring path (edit the definition, then re-run this skill if you want to verify).
|
|
28
28
|
|
|
@@ -57,7 +57,7 @@ The `*tool.ts` / `*resource.ts` patterns also catch `*.app-tool.ts` / `*.app-res
|
|
|
57
57
|
|
|
58
58
|
Use `TaskCreate` — one task per file. Mark each complete after its findings are captured.
|
|
59
59
|
|
|
60
|
-
### 2. Walk the
|
|
60
|
+
### 2. Walk the 16 categories per file
|
|
61
61
|
|
|
62
62
|
Read each definition file in full. Apply every category — most files trip more than one. Capture each hit with `file:line`, the offending excerpt, and a one-line fix.
|
|
63
63
|
|
|
@@ -201,6 +201,16 @@ Field-test catches this in its leak audit; this skill is the more thorough pass.
|
|
|
201
201
|
|
|
202
202
|
**Fix:** collapse to one single-line string literal.
|
|
203
203
|
|
|
204
|
+
#### 16. Exclusive modes in the schema
|
|
205
|
+
|
|
206
|
+
**Look in:** input schemas where two or more optional fields are alternatives rather than additions — the describes say "provide either X or Y", "ignored when Z is set", "one of".
|
|
207
|
+
|
|
208
|
+
**Check:** the exclusivity is in the schema, as a `z.discriminatedUnion` on a mode literal, so each branch advertises its own `required` list and the model reads which arguments go together. A mutex that lives only in prose reaches a weaker model unreliably: it fills both, or neither, and the handler answers with a validation error for a rule the schema said nothing about.
|
|
209
|
+
|
|
210
|
+
**Smell:** every field optional with a describe explaining when to omit it; a handler opening with `if (!a && !b) throw` / `if (a && b) throw`; a `mode`/`kind`/`by` enum whose value decides which *other* fields are required.
|
|
211
|
+
|
|
212
|
+
**Fix:** `input: z.discriminatedUnion('mode', [...])` — see the `add-tool` skill. Not every all-optional schema qualifies: fields that genuinely compose (independent filters, pagination) stay flat.
|
|
213
|
+
|
|
204
214
|
### 3. Cross-surface pass
|
|
205
215
|
|
|
206
216
|
The per-file walk misses drift that only shows between files. After it, sweep the whole surface:
|
|
@@ -260,7 +270,7 @@ End with:
|
|
|
260
270
|
- [ ] Scope confirmed (whole server / module / specific files)
|
|
261
271
|
- [ ] Severity floor applied — nits suppressed if user requested
|
|
262
272
|
- [ ] Inventory built — every `*.tool.ts`, `*.app-tool.ts`, `*.resource.ts`, `*.app-resource.ts`, `*.prompt.ts` listed; server `instructions` located if set
|
|
263
|
-
- [ ] Each file walked through all
|
|
273
|
+
- [ ] Each file walked through all 16 categories (per-file, not 16 separate passes)
|
|
264
274
|
- [ ] Cross-surface pass run — naming taxonomy, parameter vocabulary, tool overlap, instructions drift, length outliers
|
|
265
275
|
- [ ] **Read-only:** no git, no commits, no changelog edits, no `devcheck`, no wrapup invoked during the audit
|
|
266
276
|
- [ ] Findings carry file:line citation, excerpt, issue, fix — excerpts verbatim, line numbers verified
|
package/templates/.env.example
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
# ── Session ──────────────────────────────────────────────────────────
|
|
17
17
|
# MCP_SESSION_MODE=stateful # stateful | stateless (default: stateful)
|
|
18
|
+
# MCP_HTTP_RESUMABILITY=false # SSE replay on a dropped stateful stream. Default: true.
|
|
19
|
+
# Kill switch only — no effect on stateless serving.
|
|
20
|
+
# MCP_HTTP_RESUMABILITY_MAX_EVENTS=512 # Retained per session, oldest evicted first
|
|
21
|
+
# MCP_HTTP_RESUMABILITY_TTL_MS=300000 # How long a retained event stays replayable
|
|
18
22
|
|
|
19
23
|
# ── Memory ───────────────────────────────────────────────────────────
|
|
20
24
|
# MCP_GC_PRESSURE_INTERVAL_MS=0 # Opt-in forced Bun.gc(true) interval, ms (Bun only). Drains old-gen backlog
|