@cyanheads/mcp-ts-core 0.13.6 → 0.13.7
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 +3 -3
- package/CLAUDE.md +3 -3
- package/README.md +55 -52
- package/biome.json +1 -1
- package/changelog/0.13.x/0.13.7.md +77 -0
- package/config/tsconfig.base.json +2 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +42 -11
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +21 -4
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +9 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +4 -13
- package/dist/core/context.js.map +1 -1
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +7 -1
- package/dist/core/worker.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +6 -0
- package/dist/mcp-server/handlerContext.d.ts.map +1 -1
- package/dist/mcp-server/handlerContext.js +3 -0
- package/dist/mcp-server/handlerContext.js.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +6 -3
- package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +5 -1
- 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 +15 -5
- package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
- package/dist/storage/core/IStorageProvider.d.ts +5 -2
- package/dist/storage/core/IStorageProvider.d.ts.map +1 -1
- package/dist/storage/core/providerHelpers.d.ts +29 -8
- package/dist/storage/core/providerHelpers.d.ts.map +1 -1
- package/dist/storage/core/providerHelpers.js +49 -11
- package/dist/storage/core/providerHelpers.js.map +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.js +4 -4
- package/dist/storage/providers/cloudflare/d1Provider.js.map +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.d.ts +2 -0
- package/dist/storage/providers/cloudflare/kvProvider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.js +11 -9
- package/dist/storage/providers/cloudflare/kvProvider.js.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.js +8 -5
- package/dist/storage/providers/cloudflare/r2Provider.js.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts +1 -0
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.js +10 -8
- package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts +5 -0
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.js +9 -5
- package/dist/storage/providers/inMemory/inMemoryProvider.js.map +1 -1
- package/dist/storage/providers/supabase/supabaseProvider.d.ts.map +1 -1
- package/dist/storage/providers/supabase/supabaseProvider.js +5 -1
- package/dist/storage/providers/supabase/supabaseProvider.js.map +1 -1
- package/dist/testing/index.d.ts +6 -4
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +6 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/utils/formatting/partialResult.d.ts +28 -2
- package/dist/utils/formatting/partialResult.d.ts.map +1 -1
- package/dist/utils/formatting/partialResult.js +46 -2
- package/dist/utils/formatting/partialResult.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +8 -2
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +27 -16
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/mappings.d.ts +1 -0
- package/dist/utils/internal/error-handler/mappings.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/mappings.js +1 -0
- package/dist/utils/internal/error-handler/mappings.js.map +1 -1
- package/dist/utils/internal/performance.d.ts +5 -1
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +13 -8
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +15 -15
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +108 -88
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/telemetry/instrumentation.d.ts +6 -2
- package/dist/utils/telemetry/instrumentation.d.ts.map +1 -1
- package/dist/utils/telemetry/instrumentation.js +23 -8
- package/dist/utils/telemetry/instrumentation.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +12 -18
- package/framework-skills/add-prompt/SKILL.md +3 -1
- package/framework-skills/add-provider/SKILL.md +14 -4
- package/framework-skills/add-resource/SKILL.md +3 -3
- package/framework-skills/add-tool/SKILL.md +22 -7
- package/framework-skills/api-canvas/SKILL.md +2 -2
- package/framework-skills/api-config/SKILL.md +4 -3
- package/framework-skills/api-context/SKILL.md +8 -5
- package/framework-skills/api-errors/SKILL.md +7 -3
- package/framework-skills/api-linter/SKILL.md +8 -8
- package/framework-skills/api-telemetry/SKILL.md +9 -4
- package/framework-skills/api-testing/SKILL.md +21 -13
- package/framework-skills/api-utils/SKILL.md +3 -3
- package/framework-skills/api-utils/references/security.md +7 -6
- package/framework-skills/code-simplifier/SKILL.md +31 -18
- package/framework-skills/design-mcp-server/SKILL.md +62 -35
- package/framework-skills/git-wrapup/SKILL.md +16 -10
- package/framework-skills/maintenance/SKILL.md +2 -2
- package/framework-skills/orchestrations/SKILL.md +1 -1
- package/framework-skills/orchestrations/workflows/greenfield-build.md +15 -8
- package/framework-skills/polish-docs-meta/SKILL.md +2 -2
- package/framework-skills/polish-docs-meta/references/package-meta.md +1 -1
- package/framework-skills/polish-docs-meta/references/readme.md +3 -3
- package/framework-skills/release-and-publish/SKILL.md +6 -4
- package/framework-skills/release-pr-review/SKILL.md +18 -1
- package/framework-skills/report-issue-framework/SKILL.md +2 -2
- package/framework-skills/report-issue-local/SKILL.md +3 -3
- package/framework-skills/security-pass/SKILL.md +11 -3
- package/framework-skills/tool-defs-analysis/SKILL.md +3 -3
- package/package.json +15 -36
- package/templates/.env.example +3 -1
- package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +1 -1
- package/templates/AGENTS.md +2 -2
- package/templates/CLAUDE.md +2 -2
- package/templates/Dockerfile +4 -4
- package/templates/package.json +3 -3
- package/templates/src/mcp-server/prompts/definitions/echo.prompt.ts +2 -4
- package/templates/src/mcp-server/resources/definitions/echo-app-ui.app-resource.ts +51 -14
- package/templates/src/mcp-server/resources/definitions/echo.resource.ts +1 -1
- package/templates/src/mcp-server/tools/definitions/echo-app.app-tool.ts +2 -3
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +1 -1
- package/dist/utils/telemetry/index.d.ts +0 -12
- package/dist/utils/telemetry/index.d.ts.map +0 -1
- package/dist/utils/telemetry/index.js +0 -12
- package/dist/utils/telemetry/index.js.map +0 -1
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Scaffold a new MCP tool definition. Use when the user asks to add a tool, create a new tool, or implement a new capability for the server.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.30"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -24,13 +24,13 @@ Tools use the `tool()` builder from `@cyanheads/mcp-ts-core`. Each tool lives in
|
|
|
24
24
|
|
|
25
25
|
## Naming
|
|
26
26
|
|
|
27
|
-
Tools use lowercase snake_case with a canonical server/domain prefix
|
|
27
|
+
Tools use lowercase snake_case with a canonical server/domain prefix, `{server}_{verb}_{noun}` by default. Drop the noun only when the verb is a complete action on its own (`git_pull`, `git_status`): if `{server}_{verb}` leaves "…what?" unanswered — `search` what? `connect` to what? — the noun is missing. The full rule is the Name row of the `design-mcp-server` Design table.
|
|
28
28
|
|
|
29
29
|
Examples: `pubmed_search_articles`, `pubmed_fetch_fulltext`, `clinicaltrials_find_eligible`.
|
|
30
30
|
|
|
31
31
|
The server prefix is judged on clarity, not length: the brand name or the plain well-known word for the domain both pass (`pubmed_`, `patents_`); an abbreviation fails only when it reads as something else out of context (`loc_`, `ct_`). A fourth segment is fine when the noun is inherently two words (`openfda_search_device_clearances`). When a name resists the schema — can't pick a verb, noun feels generic, the *verb* wants a second word — that's usually a signal the scope is fuzzy; split the tool, rename, or reconsider.
|
|
32
32
|
|
|
33
|
-
For shape selection (Workflow or
|
|
33
|
+
For shape selection (Workflow, Instruction, or Reference variants — standard single-action tools are the default), see the `design-mcp-server` skill's Tool shapes section.
|
|
34
34
|
|
|
35
35
|
## Template
|
|
36
36
|
|
|
@@ -243,7 +243,7 @@ const { enableWrites } = getServerConfig();
|
|
|
243
243
|
|
|
244
244
|
// The suggestion is emitted only under the config that registers its target.
|
|
245
245
|
const nextToolSuggestions = enableWrites
|
|
246
|
-
? [{ toolName: 'brapi_submit_observations', args: { studyDbId } }]
|
|
246
|
+
? [{ toolName: 'brapi_submit_observations', reason: 'Record the observations collected for this study.', args: { studyDbId } }]
|
|
247
247
|
: [];
|
|
248
248
|
|
|
249
249
|
return {
|
|
@@ -344,11 +344,11 @@ The handler dispatches on the discriminator and TypeScript narrows `input` to th
|
|
|
344
344
|
|
|
345
345
|
What reaches the wire is `{"type": "object", "oneOf": [<branch>, …]}`: branches intact, each with its own `required` list and a `const`-tagged discriminator, `additionalProperties: false` on every one. Identical bytes on a 2025-11-25 and a 2026-07-28 connection — the legacy projection inspects `outputSchema` alone and never rewrites an input root.
|
|
346
346
|
|
|
347
|
-
|
|
347
|
+
Four constraints:
|
|
348
348
|
|
|
349
349
|
- **The union must be discriminated.** A bare `z.union(...)` is rejected: with no literal-tagged key the model has nothing to choose a branch by, and every variant's `required` would read as applying at once.
|
|
350
350
|
- **`output` stays a flat `z.object`** — see the widening section below for why a non-object output root breaks the success path. When the *result* shape varies by mode, use a `kind` discriminator with presence-based optional fields and render each arm on field presence in `format()`.
|
|
351
|
-
- **
|
|
351
|
+
- **Claude clients flatten the union root.** The Anthropic Messages API rejects a top-level `oneOf` in `input_schema`, so Claude clients rewrite the root before the model sees it — and the rewrite keeps only the first branch's properties, with `required: []`. A tool that must work in Claude clients takes a flat `z.object()` with an enum discriminator, optional per-mode fields, each mode's required fields named in the discriminator's `.describe()`, and the combination checked in the handler. `schema-root-oneof-portability` (strict mode only) flags the union root. Tracked in [#510](https://github.com/cyanheads/mcp-ts-core/issues/510).
|
|
352
352
|
- **A union root rules out `headerParam`.** See below — the branches sit under `oneOf`, which the reachability rule excludes.
|
|
353
353
|
|
|
354
354
|
### `headerParam` mirrors an argument into a request header
|
|
@@ -539,7 +539,7 @@ async handler(input, ctx) {
|
|
|
539
539
|
|
|
540
540
|
Single-item tools don't need this — they either succeed or throw. The partial success question only arises with array inputs.
|
|
541
541
|
|
|
542
|
-
**Telemetry:** The framework automatically detects this pattern — when a handler result contains a non-empty `failed` array, the span gets `mcp.tool.partial_success`, `mcp.tool.batch.succeeded_count
|
|
542
|
+
**Telemetry:** The framework automatically detects this pattern — when a handler result contains a non-empty `failed` array, the span gets `mcp.tool.partial_success`, `mcp.tool.batch.succeeded_count` (from the `succeeded` array), and `mcp.tool.batch.failed_count` attributes. No manual instrumentation needed. An `output` built with `partialResultSchema()` from `/utils` is read under its own `failedKey`/`succeededKey` instead — also after `.extend()`, `.pick()`, `.omit()`, or a `.shape` spread. `.partial()` and `.required()` rebuild the fields, so a schema derived that way falls back to the literal keys.
|
|
543
543
|
|
|
544
544
|
### Empty results need context
|
|
545
545
|
|
|
@@ -811,6 +811,21 @@ async handler(input, ctx) {
|
|
|
811
811
|
|
|
812
812
|
The same applies to optional arrays — use `?.length` guards so empty arrays are skipped, not passed through.
|
|
813
813
|
|
|
814
|
+
When an optional string field carries a validator (`.regex()` for a date, `.min(1)` for a cursor), a permissive schema would drop the validator and a strict one would reject the blank. Keep both by mapping the blank to `undefined` *before* the validator runs:
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
/** A blank from a form client is "unset", never a value to validate. */
|
|
818
|
+
const blankAsUnset = <T extends z.ZodType>(schema: T) =>
|
|
819
|
+
z.preprocess((value) => (value === '' ? undefined : value), schema);
|
|
820
|
+
|
|
821
|
+
input: z.object({
|
|
822
|
+
d1: blankAsUnset(z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()).describe('Earliest date, YYYY-MM-DD.'),
|
|
823
|
+
cursor: blankAsUnset(z.string().regex(/^[1-9]\d*$/).optional()).describe('Opaque continuation from the previous page.'),
|
|
824
|
+
}),
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
`toJSONSchema` emits only the inner schema for a preprocess pipe in both `io` modes, so the advertised `pattern` is unchanged; `''` parses to an absent key, a real value still hits the validator, and the handler needs no extra guard.
|
|
828
|
+
|
|
814
829
|
**Required fields are different.** If a string field is required and must be non-empty to be meaningful, `.min(1)` is correct — the client shouldn't have submitted the form without filling it in.
|
|
815
830
|
|
|
816
831
|
### Match response density to context budget
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
DataCanvas primitive reference — a Tier 3 SQL/analytical workspace for tabular MCP servers, backed by DuckDB. Use when registering tables from upstream APIs, running ad-hoc SQL across them, and exporting results. Covers the acquire → register → query → export flow, per-table TTL, the token-sharing pattern for multi-agent collaboration, env config, and Cloudflare Workers fail-closed behavior.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.4"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -511,7 +511,7 @@ The merged iterable streams — the helper does not double-buffer the full sourc
|
|
|
511
511
|
| Sync or async | Caller-supplied | Forwarded to `registerTable` as-is |
|
|
512
512
|
| Sync or async | Omitted | Helper infers via `inferSchemaFromRows` over preview buffer + sentinel |
|
|
513
513
|
|
|
514
|
-
|
|
514
|
+
Pass `schema` explicitly whenever a column's type can't be read off the first rows — a fractional column whose leading values are all `0` or `null` sniffs as `BIGINT` (or `VARCHAR`), and the appender then truncates or stringifies every later value without an error. The sniff window is only as large as the preview budget, so shrinking `previewChars` widens the exposure; the same applies to `registerTable` called without a schema. Derive the schema from the row type once and pass it to both calls.
|
|
515
515
|
|
|
516
516
|
### Cancellation and partial state
|
|
517
517
|
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Reference for core and server configuration in `@cyanheads/mcp-ts-core`. Covers env var tables with defaults, priority order, server-specific Zod schema pattern, and Workers lazy-parsing requirement.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.20"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -209,8 +209,9 @@ Activated when `SUPABASE_URL` is set.
|
|
|
209
209
|
| `OTEL_ENABLED` | `openTelemetry.enabled` | `false` | Enable OpenTelemetry export |
|
|
210
210
|
| `OTEL_SERVICE_NAME` | `openTelemetry.serviceName` | `createApp` `name` → `package.json` `name` | Seeded from `createApp({ name })` when unset; an env value wins |
|
|
211
211
|
| `OTEL_SERVICE_VERSION` | `openTelemetry.serviceVersion` | `package.json` `version` | |
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
212
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | — | — | OTLP/HTTP base URL; resolves `tracesEndpoint` to `<base>/v1/traces` and `metricsEndpoint` to `<base>/v1/metrics` (path prefix kept) when the signal-specific variable is unset |
|
|
213
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `openTelemetry.tracesEndpoint` | — | OTLP traces endpoint URL; overrides the base, used as-is |
|
|
214
|
+
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | `openTelemetry.metricsEndpoint` | — | OTLP metrics endpoint URL; overrides the base, used as-is |
|
|
214
215
|
| `OTEL_TRACES_SAMPLER_ARG` | `openTelemetry.samplingRatio` | `1.0` | 0–1; fraction of traces to export |
|
|
215
216
|
| `OTEL_LOG_LEVEL` | `openTelemetry.logLevel` | `INFO` | OTel SDK internal log level: `NONE` \| `ERROR` \| `WARN` \| `INFO` \| `DEBUG` \| `VERBOSE` \| `ALL` |
|
|
216
217
|
|
|
@@ -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.6"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -210,7 +210,7 @@ interface ContextState {
|
|
|
210
210
|
### Usage
|
|
211
211
|
|
|
212
212
|
```ts
|
|
213
|
-
// Store — accepts any serializable value, no manual JSON.stringify needed
|
|
213
|
+
// Store — accepts any JSON-serializable value, no manual JSON.stringify needed
|
|
214
214
|
await ctx.state.set('item/123', { name: 'Widget', count: 42 });
|
|
215
215
|
await ctx.state.set('session/xyz', token, { ttl: 3600 }); // TTL in seconds
|
|
216
216
|
|
|
@@ -236,6 +236,7 @@ if (page.cursor) { /* more pages available */ }
|
|
|
236
236
|
|
|
237
237
|
- Throws `McpError(InvalidRequest)` if `tenantId` is missing. Won't happen in stdio (any auth mode) or HTTP+`MCP_AUTH_MODE=none` — both default to `'default'`. Can happen in HTTP+`MCP_AUTH_MODE=jwt`/`oauth` when the token lacks a `tid` claim (intentional fail-closed: distinct authenticated callers must not silently share state).
|
|
238
238
|
- Keys are tenant-prefixed internally; handlers never need to namespace manually.
|
|
239
|
+
- **Values round-trip as JSON** on every provider, `in-memory` included: reads return the JSON form, so a `Date` comes back as its ISO string, a `Map` as `{}`, and a returned object never shares identity with the one written. Validate reads with a schema that matches the stored form (`z.string()` for a date, not `z.date()`). A `bigint`, a cyclic reference, or a top-level `undefined`, function, or symbol throws `McpError(SerializationError)` before anything is written; in `setMany`, one such value rejects the whole batch.
|
|
239
240
|
- **Key charset:** `^[a-zA-Z0-9_.\-/]+$`, 1024 chars max, no `..`. Slashes are the namespace separator — a colon (`item:123`) throws `McpError(ValidationError)` on every call. The rule covers `list` prefixes and every key in a batch operation. `createMockContext().state` enforces it identically, so an illegal key fails in the test rather than in a deployment.
|
|
240
241
|
- **Workers persistence:** The `in-memory` provider loses data on cold starts. Use `cloudflare-kv`, `cloudflare-r2`, or `cloudflare-d1` for durable storage in Workers.
|
|
241
242
|
|
|
@@ -712,9 +713,11 @@ For tools that cap a list (i.e. have a `limit`/`per_page`/`page_size`/`max_resul
|
|
|
712
713
|
|
|
713
714
|
```ts
|
|
714
715
|
enrichment: {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
716
|
+
// Optional: truncated() writes these only when the cap is hit, and a required
|
|
717
|
+
// enrichment field left unset fails the effective-output parse on every complete result.
|
|
718
|
+
truncated: z.boolean().optional().describe('True when the list was capped.'),
|
|
719
|
+
shown: z.number().optional().describe('Number of items returned.'),
|
|
720
|
+
cap: z.number().optional().describe('The limit that was applied.'),
|
|
718
721
|
truncationCeiling: z.number().optional().describe('Upper bound for omitted items (threshold bound).'),
|
|
719
722
|
},
|
|
720
723
|
async handler(input, ctx) {
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
McpError constructor, JsonRpcErrorCode reference, and error handling patterns for `@cyanheads/mcp-ts-core`. Use when looking up error codes, understanding where errors should be thrown vs. caught, or using ErrorHandler.tryCatch in services.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.16"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -205,6 +205,8 @@ throw validationError(message, {
|
|
|
205
205
|
|
|
206
206
|
Throw when the server has authoritative classification — auth failure, rate limit, schema violation, upstream 5xx, missing required input. Don't throw when "this looks wrong" depends on intent the server can't see. For mutators, surface raw pre- and post-mutation observable state in the response and let the agent decide whether it matches intent — the server can detect that the file shrunk, but only the agent knows whether it was supposed to. Tell: defensive code justified as a free rider on other work — audit it standalone, and it usually doesn't earn its keep.
|
|
207
207
|
|
|
208
|
+
A best-effort call that catches and degrades must still rethrow on `ctx.signal?.aborted`: `catch (err) { if (ctx.signal?.aborted) throw err; return degraded(); }`. One example is an enrichment lookup whose failure should return the primary result with a notice. The factory maps a cancelled handler to `RequestCancelled` only when the handler throws. A catch-all degrade turns the caller's cancellation into a "successful" response and logs a false failure warning.
|
|
209
|
+
|
|
208
210
|
---
|
|
209
211
|
|
|
210
212
|
## Error Factories (fallback)
|
|
@@ -312,7 +314,7 @@ Use factories or `McpError` directly when the code must be exact — auto-classi
|
|
|
312
314
|
|
|
313
315
|
The framework applies these steps in order — first match wins:
|
|
314
316
|
|
|
315
|
-
1. **Request signal aborted** — `ctx.signal.aborted` is `true` when the handler unwinds → `RequestCancelled`. Resolved by the tool and resource handler factories
|
|
317
|
+
1. **Request signal aborted** — `ctx.signal.aborted` is `true` when the handler unwinds → `RequestCancelled`. Resolved before the thrown value is classified at all — by the tool and resource handler factories, and by the HTTP transport's error handler against the inbound request's signal, which catches a caller that hangs up before any handler runs (mid-body, say) and answers it 499 — so it outranks every step below, `McpError` included: the caller withdrew the request, and what the handler threw on the way out does not change that. Covers every shape an abort leaves behind — a `notifications/cancelled` `reason` string, the `DOMException` named `AbortError` a reason-less cancellation produces, a service's own `McpError`, and the SDK's `SdkError(ConnectionClosed)` on transport close. The accepted cost is that an unrelated fault raised after the abort is recorded as a cancellation too; it is bounded, because the SDK writes no response for a request whose signal it aborted. A handler that throws while the signal is live is untouched by this step.
|
|
316
318
|
2. **`McpError` instance** — `error.code` is preserved as-is; no classification needed.
|
|
317
319
|
3. **SDK transport-closed rejection** — an `SdkError` carrying `SdkErrorCode.ConnectionClosed` → `RequestCancelled`. The SDK rejects every in-flight request when the transport closes, which is what a client disconnect looks like from inside a handler. Matched on the code, not the message: one of its wordings says "aborted" and would otherwise be caught by the generic abort pattern in step 6 and read as a `Timeout`. Still the rule for a throw raised where no request signal is in scope — a service, an outbound leg, a background task.
|
|
318
320
|
4. **JS constructor name** — matched against a fixed table (e.g. `ZodError` → `ValidationError`, `SyntaxError` → `ValidationError`). Note: `TypeError` is intentionally excluded — runtime TypeErrors are programmer errors, not validation failures.
|
|
@@ -462,12 +464,14 @@ const parsed = await ErrorHandler.tryCatch(
|
|
|
462
464
|
|
|
463
465
|
`tryCatch` always logs and rethrows — it never swallows errors. The `fn` argument may be synchronous or return a `Promise`; both are handled via `Promise.resolve(fn())`.
|
|
464
466
|
|
|
467
|
+
**The thrown error's `data` is wire-visible.** A handler that lets it propagate forwards it as `structuredContent.error.data` (tools) or JSON-RPC `error.data` (resources, prompts). It carries `originalErrorName`, `originalMessage`, `rootCause` (`{ name, message }`), and the canonical fields and `extra` of `context`, but never a stack: `originalStack` and the full `causeChain` go to the log record only.
|
|
468
|
+
|
|
465
469
|
**Options** (`Omit<ErrorHandlerOptions, 'rethrow'>`):
|
|
466
470
|
|
|
467
471
|
| Option | Type | Required | Purpose |
|
|
468
472
|
|:-------|:-----|:--------:|:--------|
|
|
469
473
|
| `operation` | `string` | Yes | Name logged with the error |
|
|
470
|
-
| `context` | `ErrorContext` | No |
|
|
474
|
+
| `context` | `ErrorContext` | No | Structured fields merged into the log record and the thrown error's client-visible `data`; `requestId` and `timestamp` receive special treatment |
|
|
471
475
|
| `errorCode` | `JsonRpcErrorCode` | No | Code used if the caught error is not already an `McpError` |
|
|
472
476
|
| `input` | `unknown` | No | Input value sanitized and logged alongside the error |
|
|
473
477
|
| `critical` | `boolean` | No | Marks the error as critical in logs (default `false`) |
|
|
@@ -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.18"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -85,7 +85,7 @@ Why this family exists: different MCP clients forward different surfaces of a to
|
|
|
85
85
|
|
|
86
86
|
Two consequences worth knowing when writing a `format()`:
|
|
87
87
|
|
|
88
|
-
- **The string sentinel is alphanumeric so escaping does not break it.** `content[]` is markdown carrying upstream text you do not control, so escaping
|
|
88
|
+
- **The string sentinel is alphanumeric so escaping does not break it.** `content[]` is markdown carrying upstream text you do not control, so escaping at the render boundary is correct — and it leaves an alphanumeric probe byte-identical. Escape a character only where it would change rendering, per CommonMark/GFM rules: intraword `_` (`snake_case`), a `<` that cannot open a tag (`p<0.05`), and a `[` that cannot form a link all stay raw. Agents read `content[]` as text and copy spans out of it, so a blanket escape set turns into backslash noise in their output. Markdown escaping, HTML escaping, and URL encoding all pass. You never need to carve an exception into your escape set to keep `lint:mcp` green.
|
|
89
89
|
- **Schema-dictated values must render as their own token.** A required `kind: z.enum(['full', 'outline'])` that `format()` never renders is not satisfied by the letters `full` appearing inside a longer word elsewhere in the output — `case_name_full`, `inactive`, `listing`. Render the field, or render its key name as a label.
|
|
90
90
|
|
|
91
91
|
### format-parity
|
|
@@ -373,9 +373,9 @@ Fires when emitted output contains `$defs` or `$ref`. Gemini rejects these (`400
|
|
|
373
373
|
|
|
374
374
|
**Severity:** warning (only when `portability: 'strict'`)
|
|
375
375
|
|
|
376
|
-
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.
|
|
376
|
+
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. Vendor handling of a `oneOf` at the *parameter* root varies: a client that reads only `type` and `properties` sees a parameterless tool and drops the constraint silently rather than erroring, and Claude clients — the Anthropic Messages API rejects a top-level `oneOf` — rewrite the root to its first branch's properties, hiding every other mode from the model. Opt-in for now; whether it warns by default is tracked in [#510](https://github.com/cyanheads/mcp-ts-core/issues/510).
|
|
377
377
|
|
|
378
|
-
**Fix (
|
|
378
|
+
**Fix (for any tool that must work in Claude clients):** flatten to a single `z.object()` with a discriminator field and optional per-mode fields, and validate the combination in the handler.
|
|
379
379
|
|
|
380
380
|
### schema-dialect-tag
|
|
381
381
|
|
|
@@ -1065,11 +1065,11 @@ Singularization covers only the bounded suffixes above (`ies` → `y`, `ses`/`xe
|
|
|
1065
1065
|
A silently capped list leaves the agent unaware that results were cut off — it may treat a partial set as complete. Use `ctx.enrich.truncated({ shown, cap })` for the one-liner:
|
|
1066
1066
|
|
|
1067
1067
|
```ts
|
|
1068
|
-
// In the enrichment block:
|
|
1068
|
+
// In the enrichment block — optional, since truncated() fires only on a capped page:
|
|
1069
1069
|
enrichment: {
|
|
1070
|
-
truncated: z.boolean().describe('True when the list was capped at the limit.'),
|
|
1071
|
-
shown: z.number().describe('Number of items returned.'),
|
|
1072
|
-
cap: z.number().describe('The limit applied.'),
|
|
1070
|
+
truncated: z.boolean().optional().describe('True when the list was capped at the limit.'),
|
|
1071
|
+
shown: z.number().optional().describe('Number of items returned.'),
|
|
1072
|
+
cap: z.number().optional().describe('The limit applied.'),
|
|
1073
1073
|
},
|
|
1074
1074
|
|
|
1075
1075
|
// In the handler:
|
|
@@ -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.13"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -28,8 +28,9 @@ OTel is **off by default**. `OTEL_ENABLED=true` alone does nothing — you also
|
|
|
28
28
|
| Env var | Default | Purpose |
|
|
29
29
|
|:--------|:--------|:--------|
|
|
30
30
|
| `OTEL_ENABLED` | `false` | Master switch. Must be `true` to start the SDK. |
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
31
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | — | OTLP/HTTP base URL (e.g. `http://localhost:4318`). Traces go to `<base>/v1/traces`, metrics to `<base>/v1/metrics`; a path prefix is kept. |
|
|
32
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | — | OTLP/HTTP traces endpoint (e.g. `http://localhost:4318/v1/traces`). Overrides the base for traces; used as-is. |
|
|
33
|
+
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | — | OTLP/HTTP metrics endpoint (e.g. `http://localhost:4318/v1/metrics`). Overrides the base for metrics; used as-is. |
|
|
33
34
|
| `OTEL_SERVICE_NAME` | `createApp` `name` → `package.json` `name` | `service.name` resource attribute. Seeded from `createApp({ name })` when unset; an env value wins. |
|
|
34
35
|
| `OTEL_SERVICE_VERSION` | `package.json` `version` | `service.version` resource attribute. |
|
|
35
36
|
| `OTEL_TRACES_SAMPLER_ARG` | `1.0` | Trace sampling ratio (0–1) for `TraceIdRatioBasedSampler`. |
|
|
@@ -37,6 +38,8 @@ OTel is **off by default**. `OTEL_ENABLED=true` alone does nothing — you also
|
|
|
37
38
|
|
|
38
39
|
Metrics push via `PeriodicExportingMetricReader` every **15 seconds**. Traces use `BatchSpanProcessor`.
|
|
39
40
|
|
|
41
|
+
Endpoints resolve per the [OTLP exporter spec](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#endpoint-urls-for-otlphttp): the signal-specific variable as-is, else the base plus the signal path. Those two exporters are the only export path. A signal with no resolved endpoint exports nothing, OTel log records are never exported, and `NodeSDK`'s own `OTEL_METRICS_EXPORTER` / `OTEL_LOGS_EXPORTER` defaults are not consulted.
|
|
42
|
+
|
|
40
43
|
---
|
|
41
44
|
|
|
42
45
|
## Runtime support
|
|
@@ -61,6 +64,8 @@ Cloud platform detection auto-populates resource attributes:
|
|
|
61
64
|
|
|
62
65
|
Spans batch and metrics push on a 15-second cycle, so a process that exits between cycles takes its telemetry with it. `ServerHandle.shutdown()` is the drain: it stops the transport, runs the `teardown` hook, then force-flushes traces and metrics through the OTLP exporters and closes the logger.
|
|
63
66
|
|
|
67
|
+
A failed flush is logged as a warning and the logger still closes, so the final log lines survive. The usual cause is an exporter that can't reach its collector and hits the 5 s OTel shutdown ceiling.
|
|
68
|
+
|
|
64
69
|
| Trigger | Path | Exit |
|
|
65
70
|
|:--------|:-----|:-----|
|
|
66
71
|
| `SIGTERM` / `SIGINT` | `shutdown(signal)`, then an explicit exit | `0`, or `1` when the backstop fires |
|
|
@@ -107,7 +112,7 @@ Two consequences worth knowing when reading a dashboard:
|
|
|
107
112
|
| `mcp.tool.duration` / `mcp.resource.duration` | The handler **plus** validation, formatting, and the enrichment merge — time to produce the result, not time spent in handler code. An expensive `format()` shows up here. |
|
|
108
113
|
| `mcp.tool.output_bytes` / `mcp.resource.output_bytes` | The handler's returned domain value, not the assembled result. `content[]` re-renders the data the structured payload already carries, so measuring the assembly would double-count it. Nothing is recorded for a call that fails after the handler. |
|
|
109
114
|
|
|
110
|
-
`mcp.tool.partial_success` and the `mcp.tool.batch.*` counts read the same domain value, so a batch envelope (`{ succeeded, failed }`) is still detected once the result has been assembled around it.
|
|
115
|
+
`mcp.tool.partial_success` and the `mcp.tool.batch.*` counts read the same domain value, so a batch envelope (`{ succeeded, failed }`) is still detected once the result has been assembled around it. For an `output` built with `partialResultSchema()`, the arrays are read under its `failedKey`/`succeededKey`, resolved once per definition from the output schema.
|
|
111
116
|
|
|
112
117
|
Trace context propagates across boundaries via W3C `traceparent` headers. See `api-utils` → `telemetry/trace` for `withSpan`, `buildTraceparent`, `extractTraceparent`, `createContextWithParentTrace`, `injectCurrentContextInto`, `runInContext` signatures.
|
|
113
118
|
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Testing patterns for MCP tool/resource handlers using `createMockContext` and Vitest. Covers mock context options, handler testing, McpError assertions, format testing, Vitest config setup, and test isolation conventions.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.11"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -13,7 +13,7 @@ metadata:
|
|
|
13
13
|
|
|
14
14
|
Tests target handler behavior directly — call `handler(input, ctx)`, assert on the return value or thrown error. The framework's handler factory (try/catch, formatting, telemetry) is not involved. Use `createMockContext` from `@cyanheads/mcp-ts-core/testing` to construct the `ctx` argument.
|
|
15
15
|
|
|
16
|
-
**Additional exports from `/testing`:** `createMockSession()` binds a mock handler context to an HTTP session; `createFetchMock()` provides a strict upstream HTTP fake; `runToolContract()` executes a definition through schema, handler, formatting, enrichment/content, and production-shaped error-envelope checks. `createMockLogger()` returns a standalone `MockContextLogger`,
|
|
16
|
+
**Additional exports from `/testing`:** `createMockSession()` binds a mock handler context to an HTTP session; `createFetchMock()` provides a strict upstream HTTP fake; `runToolContract()` executes a definition through schema, handler, formatting, enrichment/content, and production-shaped error-envelope checks. `createMockLogger()` returns a standalone `MockContextLogger`, `createInMemoryStorage(options?)` provides a real `StorageService` backed by `InMemoryProvider`, and `expectInputRequired(run)` returns the `input_required` result a multi-round-trip handler asked for (see [Mock inputs](#mock-inputs)).
|
|
17
17
|
|
|
18
18
|
**Philosophy:** Test behavior, not implementation. Refactors should not break tests. Match the repo's existing test layout: fresh scaffolds use `tests/`, while colocated `src/**/*.test.ts` files are also supported. Integration tests at I/O boundaries over unit tests of internals.
|
|
19
19
|
|
|
@@ -101,6 +101,15 @@ try {
|
|
|
101
101
|
|
|
102
102
|
Routes match in registration order. `match` accepts an exact URL, `RegExp`, or request predicate; `respond` accepts a clonable `Response` or response factory. Set `once: true` for one-shot behavior. Unmatched requests throw unless `onUnhandled` is provided.
|
|
103
103
|
|
|
104
|
+
**A request predicate routes on the URL's origin, never a prefix.** `req.url.startsWith(BASE_URL)` also matches a lookalike host (`https://api.example.test.evil.com/...`), which CodeQL reports as high-severity incomplete URL substring sanitization — it scans test files as readily as `src/`, so a suite that is green locally still fails the security check on a pull request. Parse the URL and compare origins, matching the path separately:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
match: (req) => {
|
|
108
|
+
const url = new URL(req.url);
|
|
109
|
+
return url.origin === new URL(BASE_URL).origin && url.pathname.startsWith('/items/');
|
|
110
|
+
},
|
|
111
|
+
```
|
|
112
|
+
|
|
104
113
|
---
|
|
105
114
|
|
|
106
115
|
## Tool conformance with `toolContractSuite`
|
|
@@ -188,6 +197,7 @@ interface MockContextOptions<TErrors extends readonly ErrorContract[] | undefine
|
|
|
188
197
|
`ctx.state` is a real `StorageService` over an `InMemoryProvider` — the production storage path, not a `Map`. A test therefore sees the same rules a deployed server enforces:
|
|
189
198
|
|
|
190
199
|
- **Keys** match `^[a-zA-Z0-9_.\-/]+$` and may not contain `..`. Colons are rejected, so `cache:v1:abc` throws `McpError(ValidationError)` in the test exactly as it would in a deployment; use `cache/v1/abc`.
|
|
200
|
+
- **Values** round-trip as JSON, as on every persistent provider. A read returns a fresh object in its JSON form — a `Date` reads back as its ISO string — so a test cannot pass on identity or on a `Date`/`Map` surviving storage. A value JSON cannot encode (`bigint`, a cyclic reference, a top-level `undefined`, function, or symbol) rejects with `McpError(SerializationError)`.
|
|
191
201
|
- **TTL** is honored. An entry written with `{ ttl: 30 }` reads back as `null` once 30 seconds elapse — drive the clock with `vi.useFakeTimers()` to assert expiry.
|
|
192
202
|
- **`getMany` / `setMany` / `deleteMany` / `list`** validate every key and prefix, and `list` paginates with the same opaque cursors.
|
|
193
203
|
- **Cancellation** applies: once `ctx.signal` aborts, state operations reject.
|
|
@@ -198,6 +208,9 @@ const ctx = createMockContext();
|
|
|
198
208
|
await ctx.state.set('cache/v1/abc', { hits: 1 }, { ttl: 30 });
|
|
199
209
|
await expect(ctx.state.get('cache/v1/abc')).resolves.toEqual({ hits: 1 });
|
|
200
210
|
await expect(ctx.state.set('cache:v1:abc', {})).rejects.toThrow(McpError);
|
|
211
|
+
|
|
212
|
+
await ctx.state.set('seen/abc', { at: new Date('2026-01-01T00:00:00Z') });
|
|
213
|
+
await expect(ctx.state.get('seen/abc')).resolves.toEqual({ at: '2026-01-01T00:00:00.000Z' });
|
|
201
214
|
```
|
|
202
215
|
|
|
203
216
|
Reach for `createInMemoryStorage()` when a service takes a `StorageService` directly — it builds the same pair.
|
|
@@ -216,21 +229,16 @@ it('asks for confirmation on the first round', async () => {
|
|
|
216
229
|
});
|
|
217
230
|
```
|
|
218
231
|
|
|
219
|
-
To assert on *what* was requested,
|
|
232
|
+
To assert on *what* was requested, use `expectInputRequired` from `/testing`. It runs the handler and returns the `input_required` result the handler factory would have returned; it throws when the handler returns normally, and any other error propagates untouched:
|
|
220
233
|
|
|
221
234
|
```ts
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (isInputRequiredSignal(error)) return error.result;
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
229
|
-
throw new Error('Expected the handler to request input.');
|
|
230
|
-
}
|
|
235
|
+
import { createMockContext, expectInputRequired } from '@cyanheads/mcp-ts-core/testing';
|
|
236
|
+
|
|
237
|
+
const asked = await expectInputRequired(() => myTool.handler(input, createMockContext()));
|
|
238
|
+
expect(asked.inputRequests?.confirm?.method).toBe('elicitation/create');
|
|
231
239
|
```
|
|
232
240
|
|
|
233
|
-
`inputResponses` drives the second round. `ctx.inputs.accepted(key, schema)` and `.view(key)` read it with the same helpers production uses, so a wrong response shape fails in the test:
|
|
241
|
+
Pass `asked.requestState` back as `createMockContext({ requestState })` when the handler reads state from the prior round. `inputResponses` drives the second round. `ctx.inputs.accepted(key, schema)` and `.view(key)` read it with the same helpers production uses, so a wrong response shape fails in the test:
|
|
234
242
|
|
|
235
243
|
```ts
|
|
236
244
|
it('proceeds once the user accepts', async () => {
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
API reference for all utilities exported from `@cyanheads/mcp-ts-core/utils`. Use when looking up utility method signatures, options, peer dependencies, or usage patterns.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.12"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -47,7 +47,7 @@ Utility exports from `@cyanheads/mcp-ts-core/utils`. Utilities with complex APIs
|
|
|
47
47
|
| Export | API | Notes |
|
|
48
48
|
|:-------|:----|:------|
|
|
49
49
|
| `extractCursor` | `(params?) -> string \| undefined` | Extracts opaque cursor string from MCP request params. Checks `params.cursor` then `params._meta.cursor`. Returns `undefined` when no cursor is present. Does not decode. |
|
|
50
|
-
| `paginateArray` | `<T>(items, cursorStr, defaultPageSize, maxPageSize, context: RequestContext) -> PaginatedResult<T>` | Decodes cursor, slices array, returns `{ items, nextCursor?, totalCount }`. `nextCursor` omitted on last page. Throws `McpError(InvalidParams)` on invalid cursor. |
|
|
50
|
+
| `paginateArray` | `<T>(items, cursorStr, defaultPageSize, maxPageSize, context: RequestContext) -> PaginatedResult<T>` | Decodes cursor, slices array, returns `{ items, nextCursor?, totalCount }`. `nextCursor` omitted on last page. Throws `McpError(InvalidParams)` on invalid cursor. On a continued call the page size comes from the cursor, not `defaultPageSize` — a tool with a caller-facing `limit` input must slice on `decodeCursor(...).offset` itself to honor `limit` past page 1. |
|
|
51
51
|
| `encodeCursor` | `(state: PaginationState) -> string` | Encodes `{ offset, limit, ...extra }` to opaque base64url string. |
|
|
52
52
|
| `decodeCursor` | `(cursor, context: RequestContext) -> PaginationState` | Decodes opaque base64url cursor. Throws `McpError(InvalidParams)` if malformed. |
|
|
53
53
|
|
|
@@ -177,6 +177,6 @@ Helper API only. For the catalog of what the framework auto-emits (span names, m
|
|
|
177
177
|
|
|
178
178
|
MCP-specific `ATTR_*` constant exports for span and metric attributes. Covers: code execution (`code.function.name`, `code.namespace`), MCP tool execution (name, input/output bytes, duration, success, error code, error category, partial success, batch succeeded/failed counts), MCP resource (URI, name, MIME type, size, duration, success, error code), MCP request context (tenant ID, client ID), MCP session events, MCP storage, GenAI semantic conventions, speech, graph, auth, task, and error classification attributes.
|
|
179
179
|
|
|
180
|
-
Batch/partial success attributes (`mcp.tool.partial_success`, `mcp.tool.batch.succeeded_count`, `mcp.tool.batch.failed_count`) are set automatically by the framework when a tool handler returns a result containing a non-empty `failed` array — matching the batch response pattern from the design skill.
|
|
180
|
+
Batch/partial success attributes (`mcp.tool.partial_success`, `mcp.tool.batch.succeeded_count`, `mcp.tool.batch.failed_count`) are set automatically by the framework when a tool handler returns a result containing a non-empty `failed` array — matching the batch response pattern from the design skill. A tool whose `output` is built with `partialResultSchema()` is read under its `failedKey`/`succeededKey` instead, including after `.extend()`, `.pick()`, `.omit()`, or a `.shape` spread.
|
|
181
181
|
|
|
182
182
|
Standard OTel semantic conventions (HTTP, cloud, service, network, etc.) are NOT re-exported — import those directly from `@opentelemetry/semantic-conventions` if needed.
|
|
@@ -8,16 +8,16 @@ import { sanitization, RateLimiter, IdGenerator, idGenerator, generateUUID, gene
|
|
|
8
8
|
|
|
9
9
|
## `sanitization`
|
|
10
10
|
|
|
11
|
-
Pre-constructed singleton of `Sanitization`. Tier 3
|
|
11
|
+
Pre-constructed singleton of `Sanitization`. Tier 3 peer: `sanitize-html` (HTML handling only); URL and number validation are built in.
|
|
12
12
|
|
|
13
13
|
### Methods
|
|
14
14
|
|
|
15
15
|
| Method | Async | Peer dep | Signature |
|
|
16
16
|
|:-------|:------|:---------|:----------|
|
|
17
17
|
| `sanitizeHtml` | yes | `sanitize-html` | `(input, config?) -> Promise<string>` |
|
|
18
|
-
| `sanitizeString` | yes | `sanitize-html`
|
|
19
|
-
| `sanitizeUrl` | yes |
|
|
20
|
-
| `sanitizeNumber` | yes |
|
|
18
|
+
| `sanitizeString` | yes | `sanitize-html` (`'text'`, `'html'`, `'attribute'` contexts) | `(input, options?) -> Promise<string>` |
|
|
19
|
+
| `sanitizeUrl` | yes | none | `(input, allowedProtocols?) -> Promise<string>` |
|
|
20
|
+
| `sanitizeNumber` | yes | none | `(input, min?, max?) -> Promise<number>` |
|
|
21
21
|
| `sanitizePath` | **no** | Node.js only | `(input, options?) -> SanitizedPathInfo` |
|
|
22
22
|
| `sanitizeJson` | **no** | none | `<T>(input, maxSize?) -> T` |
|
|
23
23
|
| `sanitizeForLogging` | **no** | none | `(input) -> unknown` |
|
|
@@ -59,7 +59,8 @@ interface SanitizedPathInfo {
|
|
|
59
59
|
|
|
60
60
|
- `sanitizeHtml`: returns `''` for falsy input; `<a>` tags get `rel="noopener noreferrer"` by default
|
|
61
61
|
- `sanitizeString`: `'javascript'` context always throws `McpError(ValidationError)` — no JavaScript allowed
|
|
62
|
-
- `sanitizeUrl`: default protocols `['http', 'https']`; always blocks `javascript:`, `data:`, `vbscript:`
|
|
62
|
+
- `sanitizeUrl`: default protocols `['http', 'https']`; requires a host (domain, single-label name such as `localhost`, IPv4 dotted quad, or bracketed IPv6), so host-less schemes like `mailto:` never pass; rejects whitespace, `<`, `>`, and URLs over 2084 characters; always blocks `javascript:`, `data:`, `vbscript:`
|
|
63
|
+
- `sanitizeNumber`: string input must be a plain decimal — optional sign and fraction, no exponent (`1e5`) or separators (`1,000`)
|
|
63
64
|
- `sanitizePath`: **Node-only** — throws `McpError(InternalError)` in Workers. Throws `McpError(ValidationError)` on path traversal or null bytes.
|
|
64
65
|
- `sanitizeJson`: `maxSize` is bytes (UTF-8); uses `Buffer.byteLength` / `TextEncoder` / `string.length` fallback chain
|
|
65
66
|
- `sanitizeNumber`: `NaN`/`Infinity` always rejected; out-of-range values silently clamped with debug log
|
|
@@ -81,7 +82,7 @@ const clean = await sanitization.sanitizeHtml(userHtml, {
|
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
// URL validation
|
|
84
|
-
const safeUrl = await sanitization.sanitizeUrl(userUrl, ['http', 'https', '
|
|
85
|
+
const safeUrl = await sanitization.sanitizeUrl(userUrl, ['http', 'https', 'ftp']);
|
|
85
86
|
|
|
86
87
|
// Path sanitization (Node-only)
|
|
87
88
|
const info = sanitization.sanitizePath(userPath, { rootDir: '/app/data', allowAbsolute: false });
|