@cyanheads/mcp-ts-core 0.13.8 → 0.13.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +51 -24
- package/CLAUDE.md +51 -24
- package/README.md +10 -10
- package/changelog/0.13.x/0.13.10.md +118 -0
- package/changelog/0.13.x/0.13.9.md +113 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +31 -9
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts +6 -3
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +21 -5
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +114 -21
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +40 -0
- package/dist/core/context.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/serverManifest.d.ts +6 -0
- package/dist/core/serverManifest.d.ts.map +1 -1
- package/dist/core/serverManifest.js +6 -0
- package/dist/core/serverManifest.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 +2 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +37 -3
- 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 +133 -12
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +192 -20
- package/dist/mcp-server/inputRequired.js.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.d.ts +10 -2
- package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +49 -11
- package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
- package/dist/mcp-server/resources/resource-registration.d.ts +4 -2
- package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
- package/dist/mcp-server/resources/resource-registration.js +6 -4
- package/dist/mcp-server/resources/resource-registration.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +4 -3
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +42 -14
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/server.d.ts +9 -0
- package/dist/mcp-server/server.d.ts.map +1 -1
- package/dist/mcp-server/server.js +14 -13
- package/dist/mcp-server/server.js.map +1 -1
- package/dist/mcp-server/tools/tool-registration.d.ts +7 -3
- package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-registration.js +9 -5
- package/dist/mcp-server/tools/tool-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +163 -40
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.js +330 -114
- package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +40 -19
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +387 -130
- 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/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.js +65 -9
- package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
- package/dist/mcp-server/transports/stdio/stdioTransport.d.ts +9 -5
- package/dist/mcp-server/transports/stdio/stdioTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/stdio/stdioTransport.js +9 -5
- package/dist/mcp-server/transports/stdio/stdioTransport.js.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.d.ts +6 -2
- package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.js +7 -3
- package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +82 -18
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +620 -328
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts +11 -7
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/exportWriter.js +19 -16
- package/dist/services/canvas/providers/duckdb/exportWriter.js.map +1 -1
- package/dist/services/mirror/core/defineMirror.d.ts +1 -0
- package/dist/services/mirror/core/defineMirror.d.ts.map +1 -1
- package/dist/services/mirror/core/defineMirror.js +1 -0
- package/dist/services/mirror/core/defineMirror.js.map +1 -1
- package/dist/testing/index.d.ts +17 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +21 -7
- package/dist/testing/index.js.map +1 -1
- package/dist/types-global/errors.d.ts +18 -15
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +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/performance.d.ts +4 -2
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +8 -6
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/internal/telemetryMessages.d.ts +0 -1
- package/dist/utils/internal/telemetryMessages.d.ts.map +1 -1
- package/dist/utils/internal/telemetryMessages.js +0 -1
- package/dist/utils/internal/telemetryMessages.js.map +1 -1
- package/dist/utils/network/pacer.d.ts +38 -5
- package/dist/utils/network/pacer.d.ts.map +1 -1
- package/dist/utils/network/pacer.js +87 -25
- package/dist/utils/network/pacer.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +10 -5
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +10 -5
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +3 -3
- package/framework-skills/add-export/SKILL.md +5 -16
- package/framework-skills/add-prompt/SKILL.md +7 -3
- package/framework-skills/add-resource/SKILL.md +7 -5
- package/framework-skills/add-service/SKILL.md +3 -12
- package/framework-skills/add-test/SKILL.md +6 -3
- package/framework-skills/add-tool/SKILL.md +40 -42
- package/framework-skills/api-auth/SKILL.md +2 -2
- package/framework-skills/api-canvas/SKILL.md +17 -8
- package/framework-skills/api-config/SKILL.md +5 -4
- package/framework-skills/api-context/SKILL.md +168 -42
- package/framework-skills/api-errors/SKILL.md +48 -51
- package/framework-skills/api-linter/SKILL.md +30 -35
- package/framework-skills/api-mirror/SKILL.md +2 -1
- package/framework-skills/api-telemetry/SKILL.md +14 -10
- package/framework-skills/api-testing/SKILL.md +43 -11
- package/framework-skills/api-utils/SKILL.md +2 -2
- package/framework-skills/api-workers/SKILL.md +3 -1
- package/framework-skills/design-mcp-server/SKILL.md +6 -6
- package/framework-skills/field-test/SKILL.md +5 -5
- package/framework-skills/git-wrapup/SKILL.md +8 -6
- package/framework-skills/orchestrations/SKILL.md +7 -6
- package/framework-skills/orchestrations/workflows/field-test-fix.md +9 -19
- package/framework-skills/orchestrations/workflows/fix-wrapup-release.md +7 -7
- package/framework-skills/orchestrations/workflows/greenfield-build.md +8 -5
- package/framework-skills/orchestrations/workflows/maintenance-release.md +8 -8
- package/framework-skills/polish-docs-meta/SKILL.md +4 -4
- package/framework-skills/release-and-publish/SKILL.md +8 -6
- package/framework-skills/release-pr-review/SKILL.md +38 -24
- package/framework-skills/report-issue-framework/SKILL.md +7 -5
- package/framework-skills/report-issue-local/SKILL.md +8 -6
- package/framework-skills/security-pass/SKILL.md +14 -13
- package/package.json +6 -5
- package/scripts/devcheck.ts +7 -6
- package/scripts/install-otel.ts +84 -0
- package/scripts/lint-mcp.ts +87 -27
- package/scripts/lint-packaging.ts +226 -4
- package/scripts/release-github.ts +117 -5
- package/templates/.env.example +2 -0
- package/templates/AGENTS.md +5 -4
- package/templates/CLAUDE.md +5 -4
- package/templates/Dockerfile +67 -50
- package/templates/_.mcpbignore +2 -0
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +3 -8
|
@@ -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.19"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -64,16 +64,11 @@ export const fetchTool = tool('fetch_articles', {
|
|
|
64
64
|
|:--------|:---------|
|
|
65
65
|
| Compile time | `ctx.fail('typo')` is a TS error. Auto-completes declared reasons. |
|
|
66
66
|
| Runtime | `ctx.fail(reason, msg?, data?, options?)` builds an `McpError(contract.code, msg, { ...data, reason }, options)` — `data.reason` is auto-populated from the contract and cannot be overridden by caller-supplied data (spread first, then `reason` written last), so observers see a stable identifier. `options` accepts `{ cause }` for ES2022 error chaining. |
|
|
67
|
+
| Runtime (recovery) | A failure whose `data.reason` names a declared entry and carries no `data.recovery` gets `data.recovery.hint` set to the entry's `recovery` at the handler boundary — see below. |
|
|
67
68
|
| Lint (devcheck) | Each `code` validated against `JsonRpcErrorCode`. Reasons validated as snake_case + unique within contract. `recovery` validated as non-empty and ≥ 5 words. Build-time only — not invoked at server startup. |
|
|
68
|
-
| Lint (conformance) | If the handler `throw new McpError(JsonRpcErrorCode.X)` outside `ctx.fail`, conformance check warns when X isn't declared. The inverse is checked too: a declared reason no `ctx.fail` in the handler names warns as `error-contract-unthrown` (mark it `thrownBy: 'service'` when the service layer produces it)
|
|
69
|
+
| Lint (conformance) | If the handler `throw new McpError(JsonRpcErrorCode.X)` outside `ctx.fail`, conformance check warns when X isn't declared. The inverse is checked too: a declared reason no `ctx.fail` in the handler names warns as `error-contract-unthrown` (mark it `thrownBy: 'service'` when the service layer produces it). |
|
|
69
70
|
|
|
70
|
-
> **`recovery` is
|
|
71
|
-
|
|
72
|
-
#### `ctx.recoveryFor` — opt-in contract resolution
|
|
73
|
-
|
|
74
|
-
`ctx.recoveryFor(reason)` returns `{ recovery: { hint: <contract.recovery> } }` for a declared reason, ready to spread into `data`. Always available on `Context` (returns `{}` when no contract is attached or the reason is unknown — spread-safe with no optional chaining). On `HandlerContext<R>` it tightens to a typed signature constrained to the declared reason union.
|
|
75
|
-
|
|
76
|
-
Spreading it into the data object and passing it as the data argument are the same call — `ctx.fail` spreads whatever `data` it receives. Spread when the site carries other keys, pass it directly when it carries nothing else. **Forwarding is lint-enforced per throw site:** a `ctx.fail` site that carries neither the resolver nor its own `recovery` key warns as `error-contract-recovery-unforwarded`, because the declared hint then reaches neither client surface and an error-path test asserting `code` and `reason` still passes.
|
|
71
|
+
> **`recovery` is the wire default for its reason.** The contract `recovery` is required metadata documenting the agent's next move when this failure mode fires (a forcing function for thoughtful guidance — placeholders like "Try again." get flagged by the linter), and it is what the caller receives. When a failure whose `data.reason` names a declared entry reaches the tool or resource handler factory with no `data.recovery`, the factory sets `data.recovery.hint` to that entry's `recovery` before it logs the failure and builds the envelope, so the `Error in tool:<name>` record, `structuredContent.error.data`, and the `Recovery:` line in `content[]` carry the same hint. It matches on the reason alone — a bare `ctx.fail('reason')`, a service throwing `notFound(msg, { reason })`, and a declared reason raised through a factory with a different code all get it. A throw-site `recovery` always wins, whatever its shape. An undeclared reason, a tool without `errors[]`, a non-`McpError` throw, and a cancelled call get nothing, and the framework-owned `invalid_arguments` / `client_capability_missing` refusals keep their own hints. The thrown `McpError` is never changed — a handler-level test of `ctx.fail` sees exactly what the throw site wrote — and `runToolContract` applies the same fill, so a contract test sees the production envelope. Prompts declare no contract.
|
|
77
72
|
|
|
78
73
|
```ts
|
|
79
74
|
export const calculateTool = tool('calculate', {
|
|
@@ -85,32 +80,29 @@ export const calculateTool = tool('calculate', {
|
|
|
85
80
|
],
|
|
86
81
|
handler(input, ctx) {
|
|
87
82
|
if (!input.expression.trim()) {
|
|
88
|
-
// Static recovery —
|
|
89
|
-
throw ctx.fail('empty_expression'
|
|
83
|
+
// Static recovery — the framework fills the contract's hint onto the wire.
|
|
84
|
+
throw ctx.fail('empty_expression');
|
|
90
85
|
}
|
|
91
86
|
// ...
|
|
92
87
|
},
|
|
93
88
|
});
|
|
94
89
|
```
|
|
95
90
|
|
|
96
|
-
Same
|
|
91
|
+
Same for a service, which needs no `ctx` to get the hint — the reason is enough:
|
|
97
92
|
|
|
98
93
|
```ts
|
|
99
94
|
export class MathService {
|
|
100
|
-
parse(expr: string
|
|
95
|
+
parse(expr: string) {
|
|
101
96
|
try {
|
|
102
97
|
return mathjs.parse(expr);
|
|
103
98
|
} catch (err) {
|
|
104
|
-
throw validationError(`Parse failed: ${err.message}`, {
|
|
105
|
-
reason: 'parse_failed',
|
|
106
|
-
...ctx.recoveryFor('parse_failed'), // {} if calling tool has no matching reason
|
|
107
|
-
});
|
|
99
|
+
throw validationError(`Parse failed: ${err.message}`, { reason: 'parse_failed' });
|
|
108
100
|
}
|
|
109
101
|
}
|
|
110
102
|
}
|
|
111
103
|
```
|
|
112
104
|
|
|
113
|
-
The contract is the single source of truth — write the recovery once, lint validates ≥5 words, the
|
|
105
|
+
The contract is the single source of truth — write the recovery once, lint validates ≥5 words, and the framework carries it to every failure with that reason. For runtime-context recovery (interpolating input values, attempted IDs, queue state), override at the throw site:
|
|
114
106
|
|
|
115
107
|
```ts
|
|
116
108
|
throw ctx.fail('no_match', `No item ${id}`, {
|
|
@@ -120,7 +112,11 @@ throw ctx.fail('no_match', `No item ${id}`, {
|
|
|
120
112
|
|
|
121
113
|
> **A recovery hint names a capability, never an internal method.** The reader is a model whose only reachable surface is this server's tool names — it cannot call a TypeScript method, set a library option, or re-run an internal function. `Re-stage the table via registerTable()` is unfollowable and invites a hallucinated tool call; `Re-run the tool that produced this table to stage it again, or list the currently staged tables with this server's dataframe-describe tool` is actionable from where the reader sits. Name a condition the caller cannot observe — an option flag they never set — and the hint is noise for the same reason. The framework holds its own throws to this rule: the canvas SQL gate's rejections point at the dataframe-query and dataframe-describe capabilities rather than the provider methods behind them.
|
|
122
114
|
|
|
123
|
-
`ctx.recoveryFor`
|
|
115
|
+
#### `ctx.recoveryFor` — the entry's hint at the throw site
|
|
116
|
+
|
|
117
|
+
`ctx.recoveryFor(reason)` returns `{ recovery: { hint: <contract.recovery> } }` for a declared reason, ready to spread into `data`. Always available on `Context` (returns `{}` when no contract is attached or the reason is unknown — spread-safe with no optional chaining). On `HandlerContext<R>` it tightens to a typed signature constrained to the declared reason union.
|
|
118
|
+
|
|
119
|
+
It is not needed to put a declared hint on the wire — the fill above does that. Reach for it when the hint has to ride the thrown error itself: a test asserting `data.recovery` on the handler's own throw, or a site that deliberately sends another entry's guidance (`ctx.fail('a', msg, ctx.recoveryFor('b'))`), which the fill respects as authored.
|
|
124
120
|
|
|
125
121
|
#### `severity` — log a modeled outcome below `error`
|
|
126
122
|
|
|
@@ -138,12 +134,14 @@ Values are the logger's own level names below `error` — `debug`, `info`, `noti
|
|
|
138
134
|
|
|
139
135
|
| Surface | Under a declared severity |
|
|
140
136
|
|:--------|:--------------------------|
|
|
141
|
-
| The `Error in tool:<name>` log record | Emitted at the declared level. Same message, same structured fields. |
|
|
137
|
+
| The `Error in tool:<name>` log record | Emitted at the declared level. Same message, same structured fields, the stack included. |
|
|
142
138
|
| `mcp.errors.classified` | Gains an `mcp.error.severity` attribute. The `reason` itself never becomes a metric attribute. |
|
|
143
139
|
| `isError`, the JSON-RPC code, `structuredContent.error`, `content[]` | Byte-identical to the undeclared case. |
|
|
144
140
|
| Span status, `mcp.tool.calls`, `mcp.tool.duration`, `mcp.tool.errors` | Unchanged — the call still failed, and splitting those series would redefine what an error rate means. |
|
|
145
141
|
|
|
146
|
-
**Tools only.** Resolution happens in the tool handler factory, against the thrown error's `data.reason`. Resources declare `errors[]` but
|
|
142
|
+
**Tools only.** Resolution happens in the tool handler factory, against the thrown error's `data.reason` — the same reason-to-entry lookup that fills `data.recovery`. Resources declare `errors[]` but write no failure record, so the field is accepted there and inert. A reason thrown below the handler that the contract never declared, an entry with no `severity`, and a non-`McpError` throw all keep `error`. A cancelled request keeps its own `info`, stack-free path regardless.
|
|
143
|
+
|
|
144
|
+
**The framework's own refusals log at `notice`.** An argument rejection (`invalid_arguments`, raised only by the schema gate before the handler runs) and a `ctx.requestInput` the connection cannot serve (`client_capability_missing`) are routine caller or connection traffic, not server faults, so their `Error in tool:<name>` record — and the failure-payload record when `LOG_TOOL_FAILURE_PAYLOADS=true` — is emitted at `notice`, and `mcp.errors.classified` counts them with `mcp.error.severity: "notice"`. Nothing to declare; an `errors[]` entry naming either reason with its own `severity` still wins. The wire envelope and `mcp.tool.rejections` are unchanged, and a schema that wrongly rejects valid calls still shows per tool on `mcp.tool.rejections`.
|
|
147
145
|
|
|
148
146
|
**Skip the contract** for one-off internal tools or quick prototypes — `ctx` is plain `Context` (no `fail`) and you throw via [factories](#error-factories-fallback) directly. Behavior is identical at the wire; the contract just adds compile-time safety.
|
|
149
147
|
|
|
@@ -173,7 +171,7 @@ errors: [
|
|
|
173
171
|
]
|
|
174
172
|
```
|
|
175
173
|
|
|
176
|
-
The handler doesn't catch and re-throw — letting service errors bubble unchanged keeps "logic throws, framework catches" intact. The wire payload
|
|
174
|
+
The handler doesn't catch and re-throw — letting service errors bubble unchanged keeps "logic throws, framework catches" intact. The wire payload carries `code`, `data.reason`, and the declared entry's `recovery` as `data.recovery.hint` (filled at the handler boundary, whatever code the service picked), so clients can switch on reason without parsing message text. What's lost is lint-time enforcement that every reason is reachable; compensate with one wire-shape test per reason.
|
|
177
175
|
|
|
178
176
|
**Mark the entries the service produces.** `error-contract-unthrown` reads the handler body alone, so in a handler that mixes one local precondition with service-thrown reasons it flags each service reason as dead. Add `thrownBy: 'service'` to those entries:
|
|
179
177
|
|
|
@@ -186,18 +184,7 @@ errors: [
|
|
|
186
184
|
]
|
|
187
185
|
```
|
|
188
186
|
|
|
189
|
-
The field is lint-only metadata — nothing at runtime reads it, so the entry is typed, advertised, and thrown exactly as an unmarked one, and its reason stays in the `ctx.fail` / `ctx.recoveryFor` union. It suppresses the one rule that cannot see below the handler, and only for the entries it marks; the handler's own reasons keep being checked.
|
|
190
|
-
|
|
191
|
-
To carry the contract `recovery` from a service throw, accept `ctx` and spread the resolver:
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
throw validationError(message, {
|
|
195
|
-
reason: 'parse_failed',
|
|
196
|
-
...ctx.recoveryFor('parse_failed'), // {} when calling tool has no matching reason
|
|
197
|
-
});
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
`ctx.recoveryFor` is always present on `Context` (no-op when no contract), so services don't need to know which tool called them — the spread is safe either way.
|
|
187
|
+
The field is lint-only metadata — nothing at runtime reads it, so the entry is typed, advertised, and thrown exactly as an unmarked one (its `recovery` filled like any other), and its reason stays in the `ctx.fail` / `ctx.recoveryFor` union. It suppresses the one rule that cannot see below the handler, and only for the entries it marks; the handler's own reasons keep being checked.
|
|
201
188
|
|
|
202
189
|
---
|
|
203
190
|
|
|
@@ -395,31 +382,42 @@ Checked before common patterns. Cover: AWS exception names, HTTP status codes, D
|
|
|
395
382
|
| Layer | Pattern |
|
|
396
383
|
|:------|:--------|
|
|
397
384
|
| Tool/resource handlers | Throw `McpError` — no try/catch |
|
|
398
|
-
| Handler factory (tools) | Catches all errors, normalizes to `McpError`, sets `isError: true`, mirrors error across both client surfaces (see [Error-path parity](#error-path-parity)) |
|
|
399
|
-
| Handler factory (resources) | Catches and re-throws to the SDK, which routes through the JSON-RPC error envelope |
|
|
385
|
+
| Handler factory (tools) | Catches all errors, fills a declared `recovery`, normalizes to `McpError`, sets `isError: true`, adds `data.requestId`, mirrors error across both client surfaces (see [Error-path parity](#error-path-parity)) |
|
|
386
|
+
| Handler factory (resources) | Catches, fills a declared `recovery`, adds `data.requestId`, and re-throws to the SDK, which routes through the JSON-RPC error envelope |
|
|
387
|
+
| Prompt registration, HTTP transport | Log the failure, then answer the JSON-RPC error with the thrown `McpError`'s `data` plus `data.requestId` |
|
|
400
388
|
| Services/setup code | `ErrorHandler.tryCatch` for structured logging and wrapping (always rethrows — never swallows) |
|
|
401
389
|
|
|
402
390
|
### Error-path parity
|
|
403
391
|
|
|
404
|
-
MCP clients differ in which `CallToolResult` surface they forward to the agent. Tool errors mirror the success-path `format-parity` invariant — the text carries the message, the recovery hint,
|
|
392
|
+
MCP clients differ in which `CallToolResult` surface they forward to the agent. Tool errors mirror the success-path `format-parity` invariant — the text carries the message, the recovery hint, the two fields a caller branches on, and the request id, while the numeric `code` and `data.issues` stay JSON-only:
|
|
405
393
|
|
|
406
394
|
| Surface | Content | Read by |
|
|
407
395
|
|:--------|:--------|:--------|
|
|
408
|
-
| `content[]` | Text rendering: `Error: <message>`, then `Recovery: <hint>` when `data.recovery.hint` adds something the message does not already say, then `(reason <reason> · not retryable)` for whichever of `data.reason` / `data.retryable` is present | Claude Desktop and other format()-only clients |
|
|
409
|
-
| `structuredContent.error` | JSON `{ code, message, data? }` carrying the error code, message,
|
|
396
|
+
| `content[]` | Text rendering: `Error: <message>`, then `Recovery: <hint>` when `data.recovery.hint` adds something the message does not already say, then `(reason <reason> · not retryable · request <id>)` for whichever of `data.reason` / `data.retryable` / `data.requestId` is present | Claude Desktop and other format()-only clients |
|
|
397
|
+
| `structuredContent.error` | JSON `{ code, message, data? }` carrying the error code, message, any structured data from the thrown `McpError` or `ZodError`, and `data.requestId` | Claude Code and other structuredContent-only clients |
|
|
398
|
+
|
|
399
|
+
```text
|
|
400
|
+
Error: No data for 3 PMIDs
|
|
401
|
+
|
|
402
|
+
Recovery: Use pubmed_search_articles to discover valid PMIDs.
|
|
403
|
+
|
|
404
|
+
(reason no_match · not retryable · request UTFAC-QE0MB)
|
|
405
|
+
```
|
|
410
406
|
|
|
411
407
|
Important properties:
|
|
412
408
|
- **`_meta.error` is NOT emitted.** Error code/data live on `structuredContent.error` instead. Don't read `_meta.error` in clients or tests — it doesn't exist.
|
|
413
|
-
- **`data` propagation is restricted** to explicitly-thrown `McpError.data
|
|
414
|
-
-
|
|
415
|
-
-
|
|
409
|
+
- **`data` propagation is restricted** to explicitly-thrown `McpError.data`, `ZodError.issues`, and the request id. Auto-classified plain errors (`TypeError`, network errors, etc.) emit `code`, `message`, and `data: { requestId }` only, so internal classification context never leaks to clients.
|
|
410
|
+
- **`data.requestId` names the request.** The framework sets it on every error envelope it builds — a tool result (handler throws, argument rejections, auth refusals, output-contract failures), a failed resource read, a failed prompt, and the JSON-RPC errors `httpErrorHandler` returns — to the `requestId` that call's log records carry. On a tool, resource, or prompt call that is a generated `XXXXX-XXXXX` token, or the client's JSON-RPC id when that id is a string; `httpErrorHandler` generates its own token, the one on its `Client error:` record. A failure reported from the client resolves to its `Error in tool:<name>` record by that value. A resource read refused before it is measured (an auth refusal, or URI variables that fail `params`) carries an id no log record shares, since resources write no failure record of their own. It is added where the envelope is built, never to the thrown `McpError.data`, so `ErrorHandler.handleError` / `tryCatch` results and the log record's `errorData` stay context-free; it replaces a thrown `data.requestId`, the way canonical fields win in log records. Two envelopes go without it: a resource `-32602` whose `data` is exactly `{ uri }` (the resource-not-found shape clients match exactly), and `runToolContract` results, which have no real request. It closes the `content[]` terms line, alone as `(request <id>)` when there is no `reason` or `retryable` — so a test pinning `content[0].text` exactly sees it.
|
|
411
|
+
- **Recovery hint mirroring is automatic, unless the hint repeats the message.** When the thrown `McpError` carries `data.recovery.hint`, the handler factory appends it to the `content[]` text so the markdown surface matches the JSON surface. Authors don't need to format the hint manually. The one exception is a hint the trimmed message already contains verbatim (case-sensitively) — an argument rejection whose every hint sentence restates an issue, where the hint is the message's issue text verbatim, and an author hint that restates its own message. There the line adds no next step, so it is dropped from the text; `structuredContent.error.data.recovery.hint` stays populated either way.
|
|
412
|
+
- **`reason`, `retryable`, and `requestId` render as a trailing term line.** `(reason malformed_id · not retryable · request UTFAC-QE0MB)` closes the text whenever `data.reason` is a non-empty string, `data.retryable` is a boolean, or `data.requestId` is a non-empty string — `retryable` for `true`, `not retryable` for `false`, in that order. None present (an `McpError` with no `data` built outside a request, as `runToolContract` does) appends nothing at all. The numeric `code` and `data.issues` stay JSON-only on purpose: the code is the one envelope field a model cannot act on, and the message already renders each issue as a sentence. A consumer test pinning `content[0].text` exactly, rather than asserting it contains the diagnostic, therefore moves for any error carrying a reason.
|
|
416
413
|
- **Argument-schema rejection is a tool error with the same envelope.** An unknown root key, a wrong type, a missing required field, or a failed constraint returns `isError: true` with `structuredContent.error.code = -32602` (`InvalidParams`) and the readable `Invalid arguments for tool <name>: …` diagnostic in `content[]`. The handler never runs. Two neighbouring failures keep the protocol error path instead, arriving as a JSON-RPC error rather than a tool result: an unknown or disabled tool name, and a malformed request envelope.
|
|
417
|
-
- **`invalid_arguments` is the framework-owned reason on every argument rejection.** The rejection carries `data.reason: "invalid_arguments"` and a `data.recovery.hint` the framework synthesizes from the Zod issues, the arguments as sent, and the root schema — an unknown key names the root properties the tool does accept, a wrong type names the type to send instead, missing fields collapse into one `Provide …` sentence, and anything else
|
|
418
|
-
- **`client_capability_missing` is the other framework-owned reason.** When a handler returns `ctx.requestInput({ inputRequests: … })` on a 2025-era connection whose client declared no matching capability, `ctx.requestInput` throws this failure in place of the input-required signal, before anything reaches the wire. It is an ordinary handler throw from there on
|
|
419
|
-
- **A schema constraint cannot carry a *declared* reason.** Because the handler never runs, a rejection by `.max()`, `.regex()`, `.min()`, or any other Zod refinement bypasses `errors[]` entirely: it arrives as `InvalidParams` with `data.issues` under the framework's `invalid_arguments`, never the `reason` and authored `recovery` of a contract entry — so a caller has nothing tool-specific to branch on and gets only the schema-derived hint. Decide per constraint which surface it belongs on. A bound that is purely structural — the input is the wrong shape and no guidance beyond the diagnostic would help — belongs on the schema, where it also advertises itself in `inputSchema`. A bound a caller is expected to recover from belongs in the handler as `ctx.fail('reason', message
|
|
414
|
+
- **`invalid_arguments` is the framework-owned reason on every argument rejection.** The rejection carries `data.reason: "invalid_arguments"` and a `data.recovery.hint` the framework synthesizes from the Zod issues, the arguments as sent, and the root schema — an unknown root key names the root properties the tool does accept, an unknown key inside a nested strict object names its full path and that object's own keys (`Unknown key opts.b. opts accepts: a.`, `Unknown key items.1.b. items.1 accepts: name.`), a wrong type names the type to send instead (a fractional number on an integer field reads `Send rows as an integer, not a fractional number.`), missing fields collapse into one `Provide …` sentence, and anything else restates its diagnostic line, field path included (`start: Must be a parseable ISO 8601 date`), so identical constraints on different fields stay distinguishable. When every sentence restates an issue, the hint is the message's issue text verbatim, and its `Recovery:` line is dropped from `content[]`. Beside a framework sentence each restatement is terminated and a repeated sentence appears once: `start: Must be a parseable ISO 8601 date. Send n as a number, not a string.` When pre-validation rewrote or dropped a key the caller wrote, the rejection says so, since its issues name only the keys that were validated: `data.input` carries `{ aliased: [{ alias, target }], ignored: [...] }` — keys only, in argument order, `ignored` holding the undeclared underscore-prefixed keys the drop discarded — and the hint closes with `Validated query as targetQuery.` / `Dropped undeclared key _max.`, framework sentences that keep the `Recovery:` line. An ignore-list drop (`_meta`, `toolCallId`, a server's `input.ignoreKeys`) is a client artifact and is never reported, and a rejection the step changed nothing on carries no `data.input`. The reason renders on the closing `(reason invalid_arguments · request <id>)`; this path sets no `retryable`. Its `Error in tool:<name>` record logs at `notice`, not `error`. Authors declare nothing for this: the rejection happens before the handler and the hint is derived from the schema.
|
|
415
|
+
- **`client_capability_missing` is the other framework-owned reason.** When a handler returns `ctx.requestInput({ inputRequests: … })` on a 2025-era connection whose client declared no matching capability, `ctx.requestInput` throws this failure in place of the input-required signal, before anything reaches the wire. It is an ordinary handler throw from there on, logged at `notice`: measured as the failed call it is, and shaped by the family's usual error path — a tool gets `structuredContent.error.code = -32600` (`InvalidRequest`), `data.reason: "client_capability_missing"`, and a `data.recovery.hint` naming the capability; a resource read gets the same code, reason, and hint through the JSON-RPC error envelope. The hint ends at reconnecting with a client that declares the capability, since a consent gate has no argument that could stand in for its answer; a handler whose arguments can appends its own sentence per call with `ctx.requestInput(spec, { fallbackHint })`. Like `invalid_arguments`, a definition cannot declare it in `errors[]`: it names a property of the connection, not a domain outcome. See `api-context`'s `ctx.requestInput`.
|
|
416
|
+
- **A schema constraint cannot carry a *declared* reason.** Because the handler never runs, a rejection by `.max()`, `.regex()`, `.min()`, or any other Zod refinement bypasses `errors[]` entirely: it arrives as `InvalidParams` with `data.issues` under the framework's `invalid_arguments`, never the `reason` and authored `recovery` of a contract entry — so a caller has nothing tool-specific to branch on and gets only the schema-derived hint. Decide per constraint which surface it belongs on. A bound that is purely structural — the input is the wrong shape and no guidance beyond the diagnostic would help — belongs on the schema, where it also advertises itself in `inputSchema`. A bound a caller is expected to recover from belongs in the handler as `ctx.fail('reason', message)` against a declared `errors[]` entry, whose `recovery` the framework puts on the wire, with the limit restated in the field's `.describe()` so it is still visible before the call. Enforcing the same bound in both places is the trap: the schema wins, and the contract entry becomes unreachable while still reading as covered.
|
|
420
417
|
- **A rejected value never reaches the client.** The rendered sentence distinguishes an omitted field from a wrong one (`what: Missing required field. Expected one of "os"|"cpu"` rather than the invalid-option text), and a union renders the branch that says what would have been accepted instead of Zod's `Invalid input` placeholder. Both read the arguments in-process for the absent/present bit and the arriving type only — `data.issues` ships the Zod issues as-is, and no value the caller sent is copied onto them.
|
|
421
|
-
- **A union branch names its own field.** Each branch issue is prefixed with the path it names relative to that branch, so two alternatives differing only in which field they require stay distinguishable: `spec: kind: Invalid option: expected one of "x"|"y"; n: Invalid input: expected number, received undefined or other: Invalid input: expected string, received undefined`. Issues *within* one branch join on `; `, across branches on ` or `, and top-level issues on `, ` — three nestings, three separators. A scalar branch carries no path and renders as before. `data.issues` still ships the raw nested Zod issues, and `data.recovery.hint`
|
|
422
|
-
- **
|
|
418
|
+
- **A union branch names its own field.** Each branch issue is prefixed with the path it names relative to that branch, so two alternatives differing only in which field they require stay distinguishable: `spec: kind: Invalid option: expected one of "x"|"y"; n: Invalid input: expected number, received undefined or other: Invalid input: expected string, received undefined`. Issues *within* one branch join on `; `, across branches on ` or `, and top-level issues on `, ` — three nestings, three separators. A scalar branch carries no path and renders as before. `data.issues` still ships the raw nested Zod issues, and `data.recovery.hint` restates the same line, field path included.
|
|
419
|
+
- **A one-or-many union renders like the field it wraps.** Once a union branch fails below its root, every branch whose only issue is a root type mismatch is dropped — for `z.union([z.array(Item), Item])` given a list, that is the object branch saying only that the value is an array. If one branch remains, its issues render and hint under the field's path exactly as they would on a non-union field: `items.1.name: Invalid input: expected string, received boolean`, hinted `Send items.1.name as a string, not a boolean.` A missing element field is hinted `Provide items.1.name.`, and the rule applies again at every nested level. When every branch fails at its root (`items: "x"`), all of them render, joined by ` or `. `data.issues` keeps Zod's single `invalid_union` issue.
|
|
420
|
+
- **Some rejections never happen at all.** An ordered pre-validation step wraps the parse: a client-added root key is dropped, a declared or case-style key alias is rewritten to its canonical name, and — only after a failed parse — a JSON-stringified array or object, or a safe integer sent for a string, is repaired and the arguments parsed once more. When that still fails and the drop discarded a key, the step retries alias-first. A call the step rescues succeeds outright and produces no error envelope; a call it cannot rescue throws the rejection above exactly as it would under `input: { coerce: false }` — same code, message, `data.issues`, `data.input`, and `data.recovery.hint` — so a discarded repair leaves no trace. An integer sent to a string field, or a stringified object to an object field, is therefore a success, not a wrong-type case — a test that needs a wrong-type rejection sends a boolean. See the `add-tool` skill for the boundaries and the per-server switches.
|
|
423
421
|
|
|
424
422
|
**Handler — throw freely, no try/catch:**
|
|
425
423
|
|
|
@@ -469,7 +467,7 @@ const parsed = await ErrorHandler.tryCatch(
|
|
|
469
467
|
|
|
470
468
|
`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())`.
|
|
471
469
|
|
|
472
|
-
**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 the caught `McpError`'s own `data`, `originalErrorName`, `originalMessage`, and `rootCause` (`{ name, message }`) — never a stack and never `context`: `originalStack`, the full `causeChain`, and every `context` field (`requestId`, `sessionId`, `traceId`, `tenantId`, `extra`, …) go to the log record only. A field the caller should act on belongs in the thrown `McpError`'s `data`, not in `context`.
|
|
470
|
+
**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 the caught `McpError`'s own `data`, `originalErrorName`, `originalMessage`, and `rootCause` (`{ name, message }`) — never a stack and never `context`: `originalStack`, the full `causeChain`, and every `context` field (`requestId`, `sessionId`, `traceId`, `tenantId`, `extra`, …) go to the log record only. A field the caller should act on belongs in the thrown `McpError`'s `data`, not in `context`. (The handler factory adds the call's own `data.requestId` when it builds the envelope; that value never comes from `context` here.)
|
|
473
471
|
|
|
474
472
|
**Options** (`Omit<ErrorHandlerOptions, 'rethrow'>`):
|
|
475
473
|
|
|
@@ -496,7 +494,7 @@ const response = await fetch(url, { signal: ctx.signal });
|
|
|
496
494
|
if (!response.ok) {
|
|
497
495
|
throw await httpErrorFromResponse(response, {
|
|
498
496
|
service: 'NCBI', // included in message
|
|
499
|
-
data: { endpoint,
|
|
497
|
+
data: { endpoint }, // the framework adds data.requestId
|
|
500
498
|
});
|
|
501
499
|
}
|
|
502
500
|
```
|
|
@@ -527,7 +525,7 @@ Also exports `httpStatusToErrorCode(status)` for sync mapping when you don't hav
|
|
|
527
525
|
|
|
528
526
|
## Handler-Body Lint Rules
|
|
529
527
|
|
|
530
|
-
The
|
|
528
|
+
The definition linter (`bun run lint:mcp`, and devcheck's MCP Definitions step) checks handler bodies for common anti-patterns. It runs at build time, never at server startup. All emit warnings (not errors): they show up in `devcheck` output but don't fail it.
|
|
531
529
|
|
|
532
530
|
| Rule | Catches |
|
|
533
531
|
|:-----|:--------|
|
|
@@ -569,7 +567,6 @@ The linter validates the structure of `errors[]` and (when present) cross-checks
|
|
|
569
567
|
| `error-contract-conformance` | warning | Handler throws a non-baseline code that isn't in the contract. Suggests adding it to `errors[]` so the contract is the canonical source of truth for declared failure modes. |
|
|
570
568
|
| `error-contract-prefer-fail` | warning | Handler throws a code that **is** in the contract directly (via factory or `new McpError`) instead of through `ctx.fail(reason, …)`. Encourages routing through the typed helper so observers see consistent `data.reason` values. |
|
|
571
569
|
| `error-contract-unthrown` | warning | A declared `reason` that no literal `ctx.fail('<reason>'` or `ctx.recoveryFor('<reason>'` in the handler names. Fires only when the handler already holds at least one literal `ctx.fail(`, and skips the definition entirely when either callee takes a non-literal first argument. Wire the throw, drop the entry, or mark it `thrownBy: 'service'`. |
|
|
572
|
-
| `error-contract-recovery-unforwarded` | warning | A literal `ctx.fail('<reason>', …)` site carrying neither `ctx.recoveryFor('<reason>')` nor its own `recovery` key, so the declared hint reaches neither client surface. One diagnostic per site; skips a site whose data argument the scan cannot read. |
|
|
573
570
|
|
|
574
571
|
### Baseline codes (auto-allowed)
|
|
575
572
|
|
|
@@ -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.21"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -15,10 +15,10 @@ The linter validates tool, resource, and prompt definitions against the MCP spec
|
|
|
15
15
|
|
|
16
16
|
| Entry point | When | On failure |
|
|
17
17
|
|:------------|:-----|:-----------|
|
|
18
|
-
| `bun run lint:mcp` | Manual or CI |
|
|
18
|
+
| `bun run lint:mcp` | Manual or CI | Imports every definition file, prints errors + warnings, exits non-zero on errors. A file that fails to import is an error ([`definition-import-failed`](#definition-import-failed)), never a skip. |
|
|
19
19
|
| `bun run devcheck` | Pre-commit workflow | Wraps `lint:mcp` alongside typecheck, format, `bun audit`, `bun outdated`. |
|
|
20
20
|
|
|
21
|
-
Both surface the same `LintReport` from `validateDefinitions()` (exported from `@cyanheads/mcp-ts-core/linter`)
|
|
21
|
+
Both surface the same `LintReport` from `validateDefinitions()` (exported from `@cyanheads/mcp-ts-core/linter`), plus two load errors the CLI raises itself, because `validateDefinitions()` only receives what already loaded: `definition-import-failed` and `server-json-parse`. Each diagnostic has a stable `rule` ID — that's the anchor you land on via the `See: framework-skills/api-linter/SKILL.md#<rule>` breadcrumb appended to every message.
|
|
22
22
|
|
|
23
23
|
**Severity:**
|
|
24
24
|
- **error** — MUST-level spec violation; blocks `devcheck`.
|
|
@@ -42,7 +42,7 @@ Grouped by family. Jump to any rule ID via its anchor.
|
|
|
42
42
|
|
|
43
43
|
| Family | Rules | Section |
|
|
44
44
|
|:-------|:------|:--------|
|
|
45
|
-
| Definition | `definition-invalid` | [Definition rules](#definition-rules) |
|
|
45
|
+
| Definition | `definition-invalid`, `definition-import-failed` | [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
47
|
| Schema | `schema-is-object`, `describe-on-fields`, `schema-serializable`, `schema-unsatisfiable`, `header-param-designation`, `schema-root-meta-discarded` | [Schema rules](#schema-rules) |
|
|
48
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) |
|
|
@@ -53,7 +53,7 @@ Grouped by family. Jump to any rule ID via its anchor.
|
|
|
53
53
|
| Prompts | `generate-required` | [Prompt rules](#prompt-rules) |
|
|
54
54
|
| Handler body | `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error` | [Handler body rules](#handler-body-rules) |
|
|
55
55
|
| Error contract (structural) | `error-contract-type`, `error-contract-empty`, `error-contract-entry-type`, `error-contract-code-type`, `error-contract-code-unknown`, `error-contract-code-unknown-error`, `error-contract-reason-required`, `error-contract-reason-format`, `error-contract-reason-unique`, `error-contract-when-required`, `error-contract-retryable-type`, `error-contract-severity-unknown`, `error-contract-recovery-required`, `error-contract-recovery-empty`, `error-contract-recovery-min-words` | [Error contract rules](#error-contract-rules) |
|
|
56
|
-
| Error contract (conformance) | `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown
|
|
56
|
+
| Error contract (conformance) | `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` | [Error contract rules](#error-contract-rules) |
|
|
57
57
|
| Enrichment | `enrichment-type`, `enrichment-empty`, `enrichment-field-type`, `enrichment-output-collision`, `enrichment-prefer-block`, `enrichment-trailer-render`, `enrichment-trailer-orphan`, `enrichment-trailer-unknown-field`, `capped-list-no-truncation` | [Enrichment rules](#enrichment-rules) |
|
|
58
58
|
| server.json | ~40 rules prefixed `server-json-*` | [server.json rules](#server-json-rules) |
|
|
59
59
|
|
|
@@ -69,6 +69,23 @@ Fires when a `tools`, `resources`, or `prompts` array passed to `validateDefinit
|
|
|
69
69
|
|
|
70
70
|
**Fix:** remove the empty slot, or ensure every element of the array is a real definition object (e.g. `[makeFooTool(), enabled ? makeBarTool() : null].filter(Boolean)`).
|
|
71
71
|
|
|
72
|
+
### definition-import-failed
|
|
73
|
+
|
|
74
|
+
**Severity:** error
|
|
75
|
+
|
|
76
|
+
Fires when a discovered definition file (`*.tool.ts`, `*.resource.ts`, `*.prompt.ts`, `*.app-tool.ts`, `*.app-resource.ts` under `src/mcp-server/` or `examples/mcp-server/`) rejects on `import()`: a package that the file, or anything it imports, needs cannot be resolved, the file has a syntax error, or code throws at module load. None of that file's definitions can be checked, so the run fails rather than passing without them. The other files are still imported and linted, so one run reports every import failure alongside the rule diagnostics. The `lint:mcp` CLI (`scripts/lint-mcp.ts`) raises it; `validateDefinitions()` never does, since a programmatic caller does its own imports.
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
✗ [definition-import-failed] src/mcp-server/tools/definitions/query.tool.ts: Cannot find package '@duckdb/node-api' imported from …
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Fix**, by cause:
|
|
83
|
+
|
|
84
|
+
- **An optional peer dependency** imported at the top of the definition or of a service it imports: install the peer wherever `lint:mcp` and `devcheck` run (a `devDependency` is enough), or make the import lazy — `await import('<pkg>')` inside the handler or service method that uses it, so loading the definition never touches the package. The framework's own Tier 3 subpaths already lazy-load their peers; importing them from a definition needs nothing installed.
|
|
85
|
+
- **A throw at module load** — reading config, constructing a client, or awaiting a network call at top level: move the work into `setup()`, a service's init, or the handler. Definitions must import without side effects.
|
|
86
|
+
- **A syntax error**: fix it. `devcheck`'s typecheck reports the same file with a location.
|
|
87
|
+
- **Running the script under plain `node`**: Node's type stripping does not rewrite a relative `./x.js` specifier to `x.ts`, so a definition that imports a sibling module fails to load. Run it under Bun — `bun run lint:mcp`, as `devcheck` does.
|
|
88
|
+
|
|
72
89
|
---
|
|
73
90
|
|
|
74
91
|
## Format parity
|
|
@@ -472,19 +489,20 @@ Catches `readOnlyHint: true` with **any** explicit `destructiveHint` value (even
|
|
|
472
489
|
|
|
473
490
|
Fires when a tool's `inputAliases` cannot resolve to exactly one declared input key. An alias is a one-to-one mapping fixed ahead of time — the reason it is accepted where nearest-key matching is not — so an alias resolving to none or to more than one is a definition error, not a runtime one. The runtime declines an ambiguous rewrite silently and the caller sees the ordinary strict rejection, which reads as the alias simply not working.
|
|
474
491
|
|
|
475
|
-
|
|
492
|
+
Six conditions, all decidable from the definition:
|
|
476
493
|
|
|
477
494
|
| Condition | Example |
|
|
478
495
|
|:--|:--|
|
|
479
496
|
| An alias must not equal a declared key | `input: z.object({ q, query })` with `inputAliases: { q: 'query' }` — a declared key is never rewritten, so the alias can never fire |
|
|
480
497
|
| An alias's target must be a declared key | `inputAliases: { q: 'searchQuery' }` when the schema declares `query` |
|
|
498
|
+
| An alias's target must not be `headerParam`-designated | `inputAliases: { region: 'regionCode' }` with `regionCode: headerParam(z.string(), 'Region')` — the rewrite never targets a header-mirrored field, since the SDK checks the `Mcp-Param-Region` header against the body the caller sent, so the caller gets `Unknown key region` |
|
|
481
499
|
| Two declared keys must not case-fold to one name | `z.object({ maxResults, max_results })` — no alias can resolve between them |
|
|
482
500
|
| An alias must not case-fold to a declared key other than its target | `inputAliases: { max_results: 'query' }` alongside a declared `maxResults` |
|
|
483
501
|
| Two aliases must not case-fold to one name with different targets | `inputAliases: { 'search-term': 'query', search_term: 'maxResults' }` |
|
|
484
502
|
|
|
485
|
-
Case-folding strips `-` and `_` and lowercases — the same fold the runtime rewrite applies, so the rule and the runtime cannot disagree. On a discriminated-union root, every variant's keys count as declared: a rewrite resolves against the selected variant, so an alias naming a key no variant declares can never fire.
|
|
503
|
+
Case-folding strips `-` and `_` and lowercases — the same fold the runtime rewrite applies, so the rule and the runtime cannot disagree. On a discriminated-union root, every variant's keys count as declared: a rewrite resolves against the selected variant, so an alias naming a key no variant declares can never fire. The header check reads each variant's own designations, so an alias onto a designated field inside one variant is reported beside that variant's `header-param-designation` error; a designation deeper than the root never blocks an alias, which only ever names a root key.
|
|
486
504
|
|
|
487
|
-
**Fix:** point the alias at an existing key, rename the key it shadows, or drop the alias. Also fires when `inputAliases` is not an object of non-empty string targets.
|
|
505
|
+
**Fix:** point the alias at an existing key, rename the key it shadows, or drop the alias. For a header target, drop the alias or the `headerParam` designation — the field cannot be both. Also fires when `inputAliases` is not an object of non-empty string targets.
|
|
488
506
|
|
|
489
507
|
Silent when no `inputAliases` is declared — the case-style half needs no declaration and declines ambiguity on its own.
|
|
490
508
|
|
|
@@ -596,6 +614,7 @@ Validates the `server.json` manifest at project root against the [MCP server man
|
|
|
596
614
|
|
|
597
615
|
| Rule ID | Severity | What it checks |
|
|
598
616
|
|:--------|:---------|:---------------|
|
|
617
|
+
| `server-json-parse` | error | `server.json` exists but does not parse as JSON, so none of the rules below can run. Raised by the `lint:mcp` CLI, which reads the file; a missing `server.json` is skipped |
|
|
599
618
|
| `server-json-type` | error | `server.json` must be a JSON object, not an array or primitive |
|
|
600
619
|
| `server-json-name-required` | error | `name` must be present and non-empty |
|
|
601
620
|
| `server-json-name-length` | error | `name` length 3–200 characters |
|
|
@@ -847,7 +866,7 @@ Fires when an entry's optional `severity` field is present but isn't one of `deb
|
|
|
847
866
|
|
|
848
867
|
**Severity:** error
|
|
849
868
|
|
|
850
|
-
Fires when an entry's `recovery` field is missing or not a string. `recovery` is the agent's next-move guidance when this failure fires — it
|
|
869
|
+
Fires when an entry's `recovery` field is missing or not a string. `recovery` is the agent's next-move guidance when this failure fires — the handler factory sends it as `data.recovery.hint` with any failure carrying the entry's reason and no hint of its own.
|
|
851
870
|
|
|
852
871
|
### error-contract-recovery-empty
|
|
853
872
|
|
|
@@ -929,37 +948,13 @@ async handler(input, ctx) {
|
|
|
929
948
|
}
|
|
930
949
|
```
|
|
931
950
|
|
|
932
|
-
The field is lint-only metadata: `ctx.fail`, `ctx.recoveryFor`, the `severity` lookup, and the advertised error envelope never read it, so a marked entry is typed, advertised, and thrown exactly as an unmarked one. Prefer it over the workarounds that also silence the rule — moving the literal `ctx.fail` into a module-level helper turns the whole tool off, handler-local reasons included.
|
|
951
|
+
The field is lint-only metadata: `ctx.fail`, `ctx.recoveryFor`, the recovery fill, the `severity` lookup, and the advertised error envelope never read it, so a marked entry is typed, advertised, and thrown exactly as an unmarked one. Prefer it over the workarounds that also silence the rule — moving the literal `ctx.fail` into a module-level helper turns the whole tool off, handler-local reasons included.
|
|
933
952
|
|
|
934
953
|
**Trigger.** Only when the handler holds at least one literal `ctx.fail(`. A handler with none produces its reasons somewhere the scan cannot reach, so firing there would warn on every service-layer definition. A `ctx.fail(` or `ctx.recoveryFor(` whose first argument is not a string literal — a variable, a template literal, a map lookup — makes the named set unknowable, and the whole definition is skipped rather than guessed at.
|
|
935
954
|
|
|
936
955
|
**Heuristic limitations:** the scan reads `handler.toString()` and matches call sites in the comment- and string-stripped text, so a `ctx.fail('…')` written inside a comment or nested in another literal does not count as thrown. A reason produced outside the handler closure is invisible to any `toString()` scan, which is why the rule can never prove absence and stays a warning. Still silent without a marker: a `createFail(errors)` resolver built outside the handler, and an aliased `const fail = ctx.fail`.
|
|
937
956
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
**Severity:** warning
|
|
941
|
-
|
|
942
|
-
Fires per literal `ctx.fail('<reason>', …)` site that does not put the contract's `recovery` on the wire.
|
|
943
|
-
|
|
944
|
-
`recovery` is required on every `errors[]` entry, but reaching the client with it is opt-in — the throw site forwards `ctx.recoveryFor('<reason>')`, or passes its own `recovery` key. A site that does neither ships `reason` and `retryable` with no hint, and since the framework mirrors `data.recovery.hint` into the error `content[]`, both client surfaces lose it together. Nothing else catches this: the contract is declared, `lint:mcp` passes, and an error-path test asserting `code` and `reason` passes with the hint absent.
|
|
945
|
-
|
|
946
|
-
**Fix:** forward the resolver at the site named in the diagnostic.
|
|
947
|
-
|
|
948
|
-
```ts
|
|
949
|
-
// warns
|
|
950
|
-
throw ctx.fail('rate_limited', 'Upstream rate limit exceeded');
|
|
951
|
-
|
|
952
|
-
// clean — any of
|
|
953
|
-
throw ctx.fail('rate_limited', msg, { ...ctx.recoveryFor('rate_limited') });
|
|
954
|
-
throw ctx.fail('rate_limited', msg, ctx.recoveryFor('rate_limited'));
|
|
955
|
-
throw ctx.fail('rate_limited', msg, { recovery: { hint: `Retry in ${waitSeconds}s.` } });
|
|
956
|
-
```
|
|
957
|
-
|
|
958
|
-
**Per site, not per reason.** A handler wiring one of six throws is covered at one of them, so each site is judged on its own argument list. Two sites naming one reason, one forwarding and one bare, produce exactly one diagnostic. A site whose only resolver names a *different* reason warns too, naming both — the caller would otherwise get another failure mode's guidance.
|
|
959
|
-
|
|
960
|
-
**Bails.** A non-literal first argument on either `ctx.fail(` or `ctx.recoveryFor(` skips the whole definition, as it does for `error-contract-unthrown`. A resolver sitting outside every fail span — a hoisted `const hint = ctx.recoveryFor('x')` — skips that reason, since the binding is assembled where the scan cannot follow it. A data argument the scan cannot read skips that one site: an identifier (`ctx.fail('r', msg, data)`), a call other than the resolver, or an object literal spreading another value (`{ ...details }`), any of which may carry `recovery` already. An object literal of plain keys carrying no `recovery` still warns.
|
|
961
|
-
|
|
962
|
-
**Heuristic limitations:** same `handler.toString()` scan as `error-contract-unthrown`, so a call written inside a comment or nested in another literal is not a site, and a failure thrown below the handler is invisible. The rule speaks only for the sites it sees, which is why it stays a warning.
|
|
957
|
+
No rule checks that a throw site forwards the declared `recovery`: the handler factory fills `data.recovery.hint` from the entry for any failure carrying its reason and no hint of its own, so a bare `ctx.fail('<reason>')` and a service throw both reach the client with it. See `api-errors`.
|
|
963
958
|
|
|
964
959
|
---
|
|
965
960
|
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Stand up a persistent, self-refreshing local mirror of a bulk upstream dataset with the MirrorService (@cyanheads/mcp-ts-core/mirror). Use when a server wraps a large or slow API and should query a synced local index (embedded SQLite + FTS5) instead of paginating the live API per request.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.3"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -24,6 +24,7 @@ const papers = defineMirror({
|
|
|
24
24
|
name: 'arxiv-papers',
|
|
25
25
|
store: sqliteMirrorStore({
|
|
26
26
|
path: config.mirrorPath,
|
|
27
|
+
table: 'papers', // primary table; FTS index is `papers_fts`
|
|
27
28
|
primaryKey: 'id',
|
|
28
29
|
columns: { id: 'TEXT', title: 'TEXT', authors: 'TEXT', abstract: 'TEXT', updated: 'TEXT' },
|
|
29
30
|
fts: ['title', 'authors', 'abstract'], // opt-in FTS5 external-content index
|
|
@@ -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.16"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -73,7 +73,7 @@ A failed flush is logged as a warning and the logger still closes, so the final
|
|
|
73
73
|
|:--------|:-----|:-----|
|
|
74
74
|
| `SIGTERM` / `SIGINT` | `shutdown(signal)`, then an explicit exit | `0`, or `1` when the backstop fires |
|
|
75
75
|
| `uncaughtException` / `unhandledRejection` | `shutdown(signal)`, then an explicit exit | `1` |
|
|
76
|
-
| stdin EOF, stdio transport | `shutdown('STDIN_EOF')
|
|
76
|
+
| stdin EOF, stdio transport | the SDK transport closes itself, aborting in-flight requests unanswered; then `shutdown('STDIN_EOF')` and an explicit exit | `0`, backstop or not |
|
|
77
77
|
| a second signal during shutdown | none — the handlers are already detached | the OS default (`143` / `130`) |
|
|
78
78
|
| `ServerHandle.shutdown()` called directly | the same drain | none — exit-free by contract |
|
|
79
79
|
|
|
@@ -138,7 +138,7 @@ All custom metrics are namespaced `mcp.*` (or `process.*` / `http.client.*` wher
|
|
|
138
138
|
| `mcp.tool.param.usage` | counter | `{uses}` | `mcp.tool.name`, `mcp.tool.param` (top-level keys supplied by caller) |
|
|
139
139
|
| `mcp.input.ignored_key` | counter | `{keys}` | `mcp.tool.name`, `mcp.input.ignore_rule` (the ignore-list entry that matched, or `underscore_prefix`) |
|
|
140
140
|
| `mcp.input.aliased` | counter | `{keys}` | `mcp.tool.name`, `mcp.input.target` (the declared key), `mcp.input.alias_kind` (`declared`/`case_style`) |
|
|
141
|
-
| `mcp.input.coerced` | counter | `{calls}` | `mcp.tool.name`, `mcp.input.coercion` (`stringified_array`) |
|
|
141
|
+
| `mcp.input.coerced` | counter | `{calls}` | `mcp.tool.name`, `mcp.input.coercion` (`stringified_array`/`stringified_object`/`integer_as_string`) |
|
|
142
142
|
| `mcp.resource.reads` | counter | `{reads}` | `mcp.resource.name`, `mcp.resource.success` |
|
|
143
143
|
| `mcp.resource.duration` | histogram | `ms` | `mcp.resource.name`, `mcp.resource.success` |
|
|
144
144
|
| `mcp.resource.errors` | counter | `{errors}` | `mcp.resource.name` |
|
|
@@ -153,7 +153,7 @@ All custom metrics are namespaced `mcp.*` (or `process.*` / `http.client.*` wher
|
|
|
153
153
|
|
|
154
154
|
**Rejections and cancellations.** A call refused before the handler runs — argument validation (`-32602`) or the inline `auth` check (`-32005` missing scope, `-32006` no auth context) — never reaches the measured region, so it is absent from `mcp.tool.calls`, `mcp.tool.duration`, and `mcp.tool.errors` and counts once on `mcp.tool.rejections` instead, labelled with the code and category the caller received. `mcp.tool.outcome` separates a caller hang-up from a failure: `cancelled` for a `RequestCancelled` (`-32011`, always paired with `error_category="client"`), `error` for any other failure, `ok` for a success or an `input_required` round. `mcp.tool.success` and `error_category` keep their meaning, so existing `sum()` queries are unchanged. An error rate that excludes hang-ups filters on `mcp.tool.outcome!="cancelled"`; the failure rate a caller sees is `(errors + rejections) / (calls + rejections)`. Resources and prompts carry neither split.
|
|
155
155
|
|
|
156
|
-
The three `mcp.input.*` counters are the
|
|
156
|
+
The three `mcp.input.*` counters are the pre-validation step's metrics. Each marks a call the strict `input` schema would otherwise have rejected: a key rewritten to its canonical spelling, a client-added root key dropped, or a value repaired after the parse failed — a stringified array or object, or an integer sent for a string. `mcp.input.coerced` adds one per repaired call per kind, not per repaired value: a call repairing an array and an object adds one to each `mcp.input.coercion` series, and a call repairing three arrays adds one. A call the step rescues carries nothing about it in its response, so a client artifact spreading across a fleet shows up here first. The counters describe the arguments the handler receives: when a call is retried with the alias stage first (see `add-tool`), the key the retry rewrote counts on `mcp.input.aliased` and never also on `mcp.input.ignored_key`, and a rejected call counts the attempt its rejection reports — the retry's when it ran. The counters are not the only record: every stage writes a debug log naming the key or the repair kinds, the opt-in failure-payload record ([below](#failed-call-payloads)) keeps a failed call's arguments as the caller sent them, and a rejected call reports its rewrites and underscore-rule drops to the caller as `data.input` (see `api-errors`). All three are lazy: a server whose callers never trip a stage emits no series at all.
|
|
157
157
|
|
|
158
158
|
**Every label is author- or framework-defined — the caller's own key text is never one.** `mcp.input.ignore_rule` is the ignore-list entry that matched or the fixed `underscore_prefix`, bounded by the list's length plus one. `mcp.input.aliased` is labelled by the canonical `mcp.input.target` (a declared property of the tool) and `mcp.input.alias_kind`, not by the alias the caller sent — the case-style half accepts every `-`/`_`/case permutation of a declared key, so labelling the alias would put a caller-controlled set on a permanent series. That is the unbounded-label leak removed from the rate-limiter counter in 0.9.0: a metric attribute set lives until process restart, so anything the caller names belongs on a span or in a log, never on a counter.
|
|
159
159
|
|
|
@@ -214,15 +214,17 @@ A dashboard reading `error_category` alone therefore no longer needs to special-
|
|
|
214
214
|
A definition may put `severity` on an `errors[]` entry — `debug`, `info`, `notice`, or `warning` — for an outcome it models rather than suffers. Two things move, and nothing else:
|
|
215
215
|
|
|
216
216
|
- The `Error in tool:<name>` log record is emitted at that level instead of `error`, with the same message and structured fields.
|
|
217
|
-
- `mcp.errors.classified` gains `mcp.error.severity` on that record. It is set only when a
|
|
217
|
+
- `mcp.errors.classified` gains `mcp.error.severity` on that record. It is set only when a severity resolved below `error`.
|
|
218
218
|
|
|
219
|
-
The
|
|
219
|
+
The framework's own refusals resolve one without a declaration: an argument rejection (`invalid_arguments`) and a `ctx.requestInput` the client connection cannot serve (`client_capability_missing`) log at `notice`, so even a server that declares no severity sees `mcp.error.severity: "notice"` on those `mcp.errors.classified` increments — a bounded split a dashboard can use to separate caller rejections from faults. An argument rejection opens no execution span and reaches no call counter either way; it still counts once on `mcp.tool.rejections`.
|
|
220
|
+
|
|
221
|
+
The call still failed: the execution span keeps `SpanStatusCode.ERROR` and its recorded exception, `mcp.tool.calls` / `mcp.tool.duration` / `mcp.tool.errors` record the same values, and the completion log still reads `isSuccess: false`. Splitting those series on an authoring decision would redefine what an error rate means. Tools only — resources write no failure record of their own. A cancelled request keeps its own `info`, stack-free path whatever the contract declares. See `api-errors`.
|
|
220
222
|
|
|
221
223
|
### Errors, rate limits, HTTP client
|
|
222
224
|
|
|
223
225
|
| Metric | Type | Unit | Attributes |
|
|
224
226
|
|:-------|:-----|:-----|:-----------|
|
|
225
|
-
| `mcp.errors.classified` | counter | `{errors}` | `mcp.error.classified_code` (JSON-RPC code), `mcp.error.category` (`upstream`/`server`/`client`, as in [Error category](#error-category)), `operation`, and `mcp.error.severity` when
|
|
227
|
+
| `mcp.errors.classified` | counter | `{errors}` | `mcp.error.classified_code` (JSON-RPC code), `mcp.error.category` (`upstream`/`server`/`client`, as in [Error category](#error-category)), `operation`, and `mcp.error.severity` when a tool failure's level resolved below `error` — a declared severity, or `notice` for an `invalid_arguments` / `client_capability_missing` refusal |
|
|
226
228
|
| `mcp.ratelimit.rejections` | counter | `{rejections}` | — (the rate-limit key is caller-supplied and typically per-client, so it would materialize an unbounded series in the meter; per-key attribution lives on the span instead) |
|
|
227
229
|
| `http.client.request.duration` | histogram | `s` | `http.request.method`, `server.address`, `http.response.status_code` (when > 0; absent on network errors before a response is received) |
|
|
228
230
|
|
|
@@ -245,17 +247,19 @@ Auto-registered when `process.memoryUsage` / `process.uptime` / `perf_hooks` are
|
|
|
245
247
|
|
|
246
248
|
Every framework log record carries `requestId`, `traceId`, `spanId`, and `tenantId` from the request context, so every log line is searchable by trace. `@opentelemetry/instrumentation-pino` does not touch these records: it patches only a `pino` loaded after the SDK starts. To ship the records to the same backend as traces, set `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` (see Enabling export).
|
|
247
249
|
|
|
248
|
-
For domain logging inside handlers, use `ctx.log` (`debug`/`info`/`notice`/`warning`/`error`) — auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. The completion log emitted at the end of every handler carries a `metrics` payload, with fields tuned to each surface:
|
|
250
|
+
For domain logging inside handlers, use `ctx.log` (`debug`/`info`/`notice`/`warning`/`error`) — auto-includes `requestId`, `traceId`, `tenantId`, `spanId`. The completion log emitted at the end of every handler — at `info`, whatever the outcome — carries a `metrics` payload, with fields tuned to each surface:
|
|
249
251
|
|
|
250
252
|
| Handler | Log message | `metrics` fields |
|
|
251
253
|
|:--------|:------------|:-----------------|
|
|
252
254
|
| Tool | `Tool execution finished.` | `durationMs`, `isSuccess`, `errorCode`, `inputBytes`, `outputBytes`, plus `partialSuccess` / `batchSucceeded` / `batchFailed` when the result is a partial-success batch |
|
|
253
255
|
| Resource | `Resource read finished.` | `durationMs`, `isSuccess`, `errorCode`, `outputBytes`, `uri`, `mimeType` |
|
|
254
|
-
| Prompt | `Prompt generation finished.`
|
|
256
|
+
| Prompt | `Prompt generation finished.` | `durationMs`, `isSuccess`, `errorCode`, `inputBytes` (0 for a prompt declaring no arguments), `outputBytes`, `messageCount` |
|
|
257
|
+
|
|
258
|
+
A failed tool call or prompt adds exactly one `Error in tool:<name>` / `Error in prompt:<name>` record. Each call — prompts included — logs under its own `requestId`, and the client receives that value as `data.requestId` on the call's error envelope, so a reported failure resolves to its records.
|
|
255
259
|
|
|
256
260
|
### Failed-call payloads
|
|
257
261
|
|
|
258
|
-
Off by default. With `LOG_TOOL_FAILURE_PAYLOADS=true`, a failed tool call writes one more record right after its `Error in tool:<name>` record: message `Tool failure payload: <name>`, the same request context (`requestId`, `traceId`, `spanId`, `toolName`), and the same level, a declared `severity` included.
|
|
262
|
+
Off by default. With `LOG_TOOL_FAILURE_PAYLOADS=true`, a failed tool call writes one more record right after its `Error in tool:<name>` record: message `Tool failure payload: <name>`, the same request context (`requestId`, `traceId`, `spanId`, `toolName`), and the same level, a declared `severity` and the `notice` of an argument rejection included. A payload record below `MCP_LOG_LEVEL` is dropped with its `Error in tool:` record, so at `warning` or above an argument rejection writes neither.
|
|
259
263
|
|
|
260
264
|
| Field | Content |
|
|
261
265
|
|:------|:--------|
|