@cyanheads/mcp-ts-core 0.8.4 → 0.8.6
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/CLAUDE.md +22 -10
- package/README.md +2 -2
- package/changelog/0.8.x/0.8.5.md +42 -0
- package/changelog/0.8.x/0.8.6.md +22 -0
- package/dist/core/context.d.ts +79 -8
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +46 -9
- package/dist/core/context.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/logs/combined.log +4 -4
- package/dist/logs/error.log +4 -4
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +5 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -3
- package/skills/add-app-tool/SKILL.md +2 -2
- package/skills/add-resource/SKILL.md +2 -2
- package/skills/add-service/SKILL.md +13 -2
- package/skills/add-tool/SKILL.md +43 -20
- package/skills/api-auth/SKILL.md +6 -5
- package/skills/api-context/SKILL.md +55 -6
- package/skills/api-errors/SKILL.md +62 -2
- package/skills/field-test/SKILL.md +2 -5
- package/skills/polish-docs-meta/references/readme.md +1 -8
- package/skills/report-issue-framework/SKILL.md +1 -1
- package/skills/report-issue-local/SKILL.md +1 -1
- package/templates/AGENTS.md +0 -2
- package/templates/CLAUDE.md +0 -2
- package/templates/package.json +0 -2
package/dist/logs/error.log
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{"level":50,"time":
|
|
2
|
-
{"level":50,"time":
|
|
3
|
-
{"level":50,"time":
|
|
4
|
-
{"level":50,"time":
|
|
1
|
+
{"level":50,"time":1777501046747,"env":"testing","version":"0.0.0-test","pid":87645,"requestId":"0C362-5BC6L","timestamp":"2026-04-29T22:17:26.747Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"06197534acbde11d6e26e711be49afbffc20b6a009f7ad4a0f88a0e85af5b057","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant"},"errorData":{"sessionId":"06197534acbde11d6e26e711be49afbffc20b6a009f7ad4a0f88a0e85af5b057","toolName":"scoped_echo","requestId":"0C362-5BC6L","timestamp":"2026-04-29T22:17:26.747Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant"},"originalErrorName":"McpError","originalMessage":"Insufficient permissions.","originalStack":"McpError: Insufficient permissions.\n at forbidden (/Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:84:58)\n at withRequiredScopes (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/lib/authUtils.js:61:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:133:17)\n at executeToolHandler (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:231:34)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43)\n at processTicksAndRejections (native:7:39)"},"stack":"McpError: Insufficient permissions.\n at handleError (/Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:169:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:168:26)\n at executeToolHandler (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:231:34)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43)\n at processTicksAndRejections (native:7:39)","msg":"Error in tool:scoped_echo: Insufficient permissions."}
|
|
2
|
+
{"level":50,"time":1777501047384,"env":"testing","version":"0.8.6","pid":87649,"requestId":"HGVJ8-F7EK7","timestamp":"2026-04-29T22:17:27.383Z","operation":"httpErrorHandler","critical":false,"errorCode":-32006,"originalErrorType":"McpError","finalErrorType":"McpError","path":"/mcp","method":"POST","errorData":{"path":"/mcp","method":"POST","requestId":"HGVJ8-F7EK7","timestamp":"2026-04-29T22:17:27.383Z","operation":"httpErrorHandler","originalErrorName":"McpError","originalMessage":"Missing or invalid Authorization header. Bearer scheme required.","originalStack":"McpError: Missing or invalid Authorization header. Bearer scheme required.\n at unauthorized (/Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:86:61)\n at authMiddleware (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/authMiddleware.js:64:19)\n at dispatch (/Users/casey/Developer/github/mcp-ts-core/node_modules/hono/dist/compose.js:22:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/http/httpTransport.js:232:22)\n at dispatch (/Users/casey/Developer/github/mcp-ts-core/node_modules/hono/dist/compose.js:22:23)\n at cors2 (/Users/casey/Developer/github/mcp-ts-core/node_modules/hono/dist/middleware/cors/index.js:82:11)\n at processTicksAndRejections (native:7:39)"},"stack":"McpError: Missing or invalid Authorization header. Bearer scheme required.\n at handleError (/Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:169:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/http/httpErrorHandler.js:59:39)\n at dispatch (/Users/casey/Developer/github/mcp-ts-core/node_modules/hono/dist/compose.js:26:25)\n at processTicksAndRejections (native:7:39)","msg":"Error in httpTransport: Missing or invalid Authorization header. Bearer scheme required."}
|
|
3
|
+
{"level":50,"time":1777501047397,"env":"testing","version":"0.8.6","pid":87649,"requestId":"W6P3E-BGMMZ","timestamp":"2026-04-29T22:17:27.397Z","operation":"httpErrorHandler","critical":false,"errorCode":-32006,"originalErrorType":"McpError","finalErrorType":"McpError","path":"/mcp","method":"POST","errorData":{"path":"/mcp","method":"POST","requestId":"W6P3E-BGMMZ","timestamp":"2026-04-29T22:17:27.397Z","operation":"httpErrorHandler","originalErrorName":"McpError","originalMessage":"Token has expired.","originalStack":"McpError: Token has expired.\n at unauthorized (/Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:86:61)\n at handleJoseVerifyError (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/lib/claimParser.js:56:11)\n at verify (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/strategies/jwtStrategy.js:91:13)\n at processTicksAndRejections (native:7:39)"},"stack":"McpError: Token has expired.\n at handleError (/Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:169:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/http/httpErrorHandler.js:59:39)\n at dispatch (/Users/casey/Developer/github/mcp-ts-core/node_modules/hono/dist/compose.js:26:25)\n at processTicksAndRejections (native:7:39)","msg":"Error in httpTransport: Token has expired."}
|
|
4
|
+
{"level":50,"time":1777501047400,"env":"testing","version":"0.8.6","pid":87649,"requestId":"AN6ZH-0RBVB","timestamp":"2026-04-29T22:17:27.400Z","operation":"httpErrorHandler","critical":false,"errorCode":-32006,"originalErrorType":"McpError","finalErrorType":"McpError","path":"/mcp","method":"GET","errorData":{"path":"/mcp","method":"GET","requestId":"AN6ZH-0RBVB","timestamp":"2026-04-29T22:17:27.400Z","operation":"httpErrorHandler","originalErrorName":"McpError","originalMessage":"Missing or invalid Authorization header. Bearer scheme required.","originalStack":"McpError: Missing or invalid Authorization header. Bearer scheme required.\n at unauthorized (/Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:86:61)\n at authMiddleware (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/authMiddleware.js:64:19)\n at dispatch (/Users/casey/Developer/github/mcp-ts-core/node_modules/hono/dist/compose.js:22:23)\n at dispatch (/Users/casey/Developer/github/mcp-ts-core/node_modules/hono/dist/compose.js:22:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/http/httpTransport.js:232:22)\n at dispatch (/Users/casey/Developer/github/mcp-ts-core/node_modules/hono/dist/compose.js:22:23)\n at cors2 (/Users/casey/Developer/github/mcp-ts-core/node_modules/hono/dist/middleware/cors/index.js:82:11)\n at processTicksAndRejections (native:7:39)"},"stack":"McpError: Missing or invalid Authorization header. Bearer scheme required.\n at handleError (/Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:169:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/http/httpErrorHandler.js:59:39)\n at dispatch (/Users/casey/Developer/github/mcp-ts-core/node_modules/hono/dist/compose.js:26:25)\n at processTicksAndRejections (native:7:39)","msg":"Error in httpTransport: Missing or invalid Authorization header. Bearer scheme required."}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EACZ,eAAe,EAChB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAW,CAAC,EAAE,MAAM,KAAK,CAAC;AACtC,OAAO,KAAK,EACV,WAAW,EACX,OAAO,EACP,aAAa,EAGb,YAAY,EACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,kDAAkD,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAM9D,MAAM,WAAW,kBAAkB;IACjC,oBAAoB;IACpB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,gCAAgC;IAChC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IACxF;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAClC,2CAA2C;IAC3C,yBAAyB,CAAC,EAAE,MAAM,IAAI,CAAC;IACvC,sCAAsC;IACtC,qBAAqB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6BAA6B;IAC7B,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,EAAE,EAAE,IAAI,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC5F,wEAAwE;IACxE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,GAAG,CAAC,EAAE,GAAG,CAAC;CACX;AAMD,iFAAiF;AACjF,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG;IAC9C,mFAAmF;IACnF,KAAK,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC9D,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,IAAI,iBAAiB,CAiBpD;AAwGD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EACZ,eAAe,EAChB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAW,CAAC,EAAE,MAAM,KAAK,CAAC;AACtC,OAAO,KAAK,EACV,WAAW,EACX,OAAO,EACP,aAAa,EAGb,YAAY,EACb,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,kDAAkD,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAM9D,MAAM,WAAW,kBAAkB;IACjC,oBAAoB;IACpB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,gCAAgC;IAChC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IACxF;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAClC,2CAA2C;IAC3C,yBAAyB,CAAC,EAAE,MAAM,IAAI,CAAC;IACvC,sCAAsC;IACtC,qBAAqB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6BAA6B;IAC7B,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,EAAE,EAAE,IAAI,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC5F,wEAAwE;IACxE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,GAAG,CAAC,EAAE,GAAG,CAAC;CACX;AAMD,iFAAiF;AACjF,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG;IAC9C,mFAAmF;IACnF,KAAK,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC9D,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,IAAI,iBAAiB,CAiBpD;AAwGD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CA4B3E;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,cAAc,CAEvF"}
|
package/dist/testing/index.js
CHANGED
|
@@ -171,9 +171,13 @@ export function createMockContext(options = {}) {
|
|
|
171
171
|
notifyResourceUpdated: options.notifyResourceUpdated,
|
|
172
172
|
progress,
|
|
173
173
|
uri: options.uri,
|
|
174
|
+
// No-op resolver for definitions without a contract. `attachTypedFail` below
|
|
175
|
+
// overwrites it with a contract-aware resolver when `options.errors` is set.
|
|
176
|
+
recoveryFor: () => ({}),
|
|
174
177
|
};
|
|
175
178
|
// Mirror the production handler factory: when a contract is declared, attach
|
|
176
|
-
// a typed `fail` keyed by the contract's reasons. Empty
|
|
179
|
+
// a typed `fail` and `recoveryFor` keyed by the contract's reasons. Empty
|
|
180
|
+
// contracts leave the no-op resolver in place.
|
|
177
181
|
return attachTypedFail(ctx, options.errors);
|
|
178
182
|
}
|
|
179
183
|
// ---------------------------------------------------------------------------
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/mcp-ts-core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"mcpName": "io.github.cyanheads/mcp-ts-core",
|
|
5
5
|
"description": "Agent-native TypeScript framework for building MCP servers. Declarative definitions with auth, multi-backend storage, OpenTelemetry, and first-class support for Bun/Node/Cloudflare Workers.",
|
|
6
6
|
"main": "dist/core/index.js",
|
|
@@ -122,8 +122,6 @@
|
|
|
122
122
|
"start:stdio": "MCP_TRANSPORT_TYPE=stdio bun ./dist/index.js",
|
|
123
123
|
"start:http": "MCP_TRANSPORT_TYPE=http bun ./dist/index.js",
|
|
124
124
|
"dev": "bun --watch src/index.ts",
|
|
125
|
-
"dev:stdio": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=stdio bun --watch src/index.ts",
|
|
126
|
-
"dev:http": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=http bun --watch src/index.ts",
|
|
127
125
|
"devdocs": "bun run scripts/devdocs.ts",
|
|
128
126
|
"devcheck": "bun run scripts/devcheck.ts",
|
|
129
127
|
"rebuild": "bun run scripts/clean.ts && bun run build",
|
|
@@ -40,7 +40,7 @@ For the full API, Context interface, and error codes, read `node_modules/@cyanhe
|
|
|
40
40
|
4. **Create the app resource** at `src/mcp-server/resources/definitions/{{tool-name}}-ui.app-resource.ts`
|
|
41
41
|
5. **Register both** in the project's existing `createApp()` arrays (directly in `src/index.ts` for fresh scaffolds, or via barrels if the repo already has them)
|
|
42
42
|
6. **Run `bun run devcheck`** — the linter validates `_meta.ui` and cross-checks tool/resource pairing
|
|
43
|
-
7. **Smoke-test** with `bun run
|
|
43
|
+
7. **Smoke-test** with `bun run rebuild && bun run start:stdio` (or `start:http`)
|
|
44
44
|
|
|
45
45
|
## App Tool Template
|
|
46
46
|
|
|
@@ -239,4 +239,4 @@ If the repo already uses `definitions/index.ts` barrels, update those instead of
|
|
|
239
239
|
- [ ] UI applies host context updates via `app.onhostcontextchanged`
|
|
240
240
|
- [ ] Both registered in the project's existing `createApp()` arrays (directly or via barrels)
|
|
241
241
|
- [ ] `bun run devcheck` passes (linter validates `_meta.ui` and tool/resource pairing)
|
|
242
|
-
- [ ] Smoke-tested with `bun run
|
|
242
|
+
- [ ] Smoke-tested with `bun run rebuild && bun run start:stdio` (or `start:http`)
|
|
@@ -24,7 +24,7 @@ For the full `resource()` API, pagination utilities, and `Context` interface, re
|
|
|
24
24
|
3. **Create the file** at `src/mcp-server/resources/definitions/{{resource-name}}.resource.ts`
|
|
25
25
|
4. **Register** the resource in the project's existing `createApp()` resource list (directly in `src/index.ts` for fresh scaffolds, or via a barrel if the repo already has one)
|
|
26
26
|
5. **Run `bun run devcheck`** to verify
|
|
27
|
-
6. **Smoke-test** with `bun run
|
|
27
|
+
6. **Smoke-test** with `bun run rebuild && bun run start:stdio` (or `start:http`)
|
|
28
28
|
|
|
29
29
|
## Template
|
|
30
30
|
|
|
@@ -159,4 +159,4 @@ Beyond `description`, `params`, `handler`, and `list`, the builder also supports
|
|
|
159
159
|
- [ ] Pagination used for large result sets (`extractCursor`/`paginateArray`)
|
|
160
160
|
- [ ] Registered in the project's existing `createApp()` resource list (directly or via barrel)
|
|
161
161
|
- [ ] `bun run devcheck` passes
|
|
162
|
-
- [ ] Smoke-tested with `bun run
|
|
162
|
+
- [ ] Smoke-tested with `bun run rebuild && bun run start:stdio` (or `start:http`)
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Scaffold a new service integration. Use when the user asks to add a service, integrate an external API, or create a reusable domain module with its own initialization and state.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.5"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -237,10 +237,21 @@ Services don't declare `errors: [...]` contracts and don't have `ctx.fail` — t
|
|
|
237
237
|
- **Carry contract `reason` via `data: { reason }`** when the calling tool declares an `errors[]` contract entry for this failure mode. Services can't call `ctx.fail`, but passing the reason in `data` flows through the auto-classifier untouched, so clients see the same `error.data.reason` they'd see from `ctx.fail` — no handler-side catch-and-rethrow needed:
|
|
238
238
|
|
|
239
239
|
```ts
|
|
240
|
-
// tool declares: errors: [{ reason: 'empty_expression', code: JsonRpcErrorCode.ValidationError, when: '…' }]
|
|
240
|
+
// tool declares: errors: [{ reason: 'empty_expression', code: JsonRpcErrorCode.ValidationError, when: '…', recovery: '…' }]
|
|
241
241
|
throw validationError('Expression cannot be empty.', { reason: 'empty_expression' });
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
+
- **Resolve contract `recovery` via `ctx.recoveryFor`** to land the contract's recovery hint on the wire without duplicating the string. Always-present on `Context`, returns `{}` when the calling tool has no matching reason — spread-safe regardless:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
throw validationError('Parse failed: ' + err.message, {
|
|
248
|
+
reason: 'parse_failed',
|
|
249
|
+
...ctx.recoveryFor('parse_failed'), // resolves from caller's contract
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The contract `recovery` (validated ≥5 words at lint time) is the single source of truth. Services that opt in via the resolver carry the same hint to the wire that handler-level `ctx.fail` callers do — no drift, no auto-population. For dynamic recovery (interpolating runtime values into the hint), pass an explicit `{ recovery: { hint: '…' } }` instead.
|
|
254
|
+
|
|
244
255
|
## API Efficiency
|
|
245
256
|
|
|
246
257
|
When a service wraps an external API, design methods to minimize upstream calls. These patterns compound — a tool calling 3 service methods that each make N requests is 3N calls; batching drops it to 3.
|
package/skills/add-tool/SKILL.md
CHANGED
|
@@ -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.4"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -23,7 +23,7 @@ For the full `tool()` API, `Context` interface, and error codes, read `node_modu
|
|
|
23
23
|
3. **Create the file** at `src/mcp-server/tools/definitions/{{tool-name}}.tool.ts`
|
|
24
24
|
4. **Register** the tool in the project's existing `createApp()` tool list (directly in `src/index.ts` for fresh scaffolds, or via a barrel if the repo already has one)
|
|
25
25
|
5. **Run `bun run devcheck`** to verify
|
|
26
|
-
6. **Smoke-test** with `bun run
|
|
26
|
+
6. **Smoke-test** with `bun run rebuild && bun run start:stdio` (or `start:http`)
|
|
27
27
|
|
|
28
28
|
## Naming
|
|
29
29
|
|
|
@@ -68,10 +68,9 @@ export const {{TOOL_EXPORT}} = tool('{{tool_name}}', {
|
|
|
68
68
|
//
|
|
69
69
|
// `recovery` is required (≥ 5 words) — it's the agent's next move when this
|
|
70
70
|
// failure fires. Forcing function for thoughtful guidance: placeholders like
|
|
71
|
-
// "Try again." get flagged by the linter.
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// throw site when dynamic context matters.
|
|
71
|
+
// "Try again." get flagged by the linter. The contract `recovery` is the
|
|
72
|
+
// single source of truth for what flows to the wire — opt in at the throw
|
|
73
|
+
// site by spreading `ctx.recoveryFor('reason')` into the `data` arg.
|
|
75
74
|
errors: [
|
|
76
75
|
{ reason: 'no_match', code: JsonRpcErrorCode.NotFound,
|
|
77
76
|
when: 'No items matched the query.',
|
|
@@ -87,7 +86,18 @@ export const {{TOOL_EXPORT}} = tool('{{tool_name}}', {
|
|
|
87
86
|
// With an `errors[]` contract: `throw ctx.fail('reason_id', message?, data?)`.
|
|
88
87
|
// Without: throw via factories (`notFound`, `validationError`, …) or plain `Error`.
|
|
89
88
|
const items = await search(input);
|
|
90
|
-
if (items.length === 0)
|
|
89
|
+
if (items.length === 0) {
|
|
90
|
+
// Dynamic recovery — interpolate runtime context, override the contract default.
|
|
91
|
+
throw ctx.fail('no_match', `No items matched "${input.query}"`, {
|
|
92
|
+
recovery: { hint: `Try a broader query than "${input.query}", or check the spelling.` },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (queue.full()) {
|
|
96
|
+
// Static recovery — resolve from the contract via ctx.recoveryFor('reason').
|
|
97
|
+
// Single source of truth: the string lives in errors[] above; this spread
|
|
98
|
+
// pulls it onto the wire so format()-only clients see the recovery hint.
|
|
99
|
+
throw ctx.fail('queue_full', undefined, { ...ctx.recoveryFor('queue_full') });
|
|
100
|
+
}
|
|
91
101
|
return { items };
|
|
92
102
|
},
|
|
93
103
|
|
|
@@ -297,39 +307,50 @@ export const fetchArticles = tool('fetch_articles', {
|
|
|
297
307
|
input: z.object({ pmids: z.array(z.string()).describe('PMIDs to fetch') }),
|
|
298
308
|
output: z.object({ articles: z.array(ArticleSchema).describe('Resolved articles') }),
|
|
299
309
|
async handler(input, ctx) {
|
|
300
|
-
|
|
310
|
+
// Static recovery — ctx.recoveryFor pulls the contract recovery onto the wire.
|
|
311
|
+
// The contract is the single source of truth; this spread surfaces it on the
|
|
312
|
+
// wire so format()-only clients see the hint mirrored into content[] text.
|
|
313
|
+
if (queue.full()) throw ctx.fail('queue_full', undefined, { ...ctx.recoveryFor('queue_full') });
|
|
314
|
+
|
|
301
315
|
const articles = await fetch(input.pmids);
|
|
302
316
|
if (articles.length === 0) {
|
|
303
|
-
|
|
317
|
+
// Dynamic recovery — interpolate runtime context, override the contract default.
|
|
318
|
+
throw ctx.fail('no_pmid_match', `No data for ${input.pmids.length} PMIDs`, {
|
|
319
|
+
pmids: input.pmids,
|
|
320
|
+
recovery: { hint: `Use pubmed_search_articles to discover valid PMIDs.` },
|
|
321
|
+
});
|
|
304
322
|
}
|
|
305
323
|
return { articles };
|
|
306
324
|
},
|
|
307
325
|
});
|
|
308
326
|
```
|
|
309
327
|
|
|
328
|
+
**`ctx.recoveryFor(reason)`** resolves the contract's `recovery` string into the wire shape `{ recovery: { hint } }` — safe to spread into `data` so format()-only clients see the same recovery hint that structuredContent clients read. Always available on `Context` (no-op `{}` when no contract), strictly typed on `HandlerContext<R>` against the declared reasons. Use it for static recovery; pass `{ recovery: { hint: \`…${dynamic}…\` } }` directly when you need runtime context. The contract is the single source of truth — write the recovery once, lint validates it ≥5 words, the resolver carries it to every throw site.
|
|
329
|
+
|
|
310
330
|
**Baseline codes** (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring. Wire-level behavior is identical when the contract is omitted, but you lose the type-checked `ctx.fail`, the `tools/list` advertisement, and conformance lint coverage — declare a contract whenever the tool has a domain-specific failure mode.
|
|
311
331
|
|
|
312
332
|
`ctx.fail` accepts an optional 4th `options` argument for ES2022 cause chaining: `throw ctx.fail('upstream_error', 'Upstream returned 500', { url }, { cause: e })`.
|
|
313
333
|
|
|
314
334
|
#### Service-layer throws
|
|
315
335
|
|
|
316
|
-
API-wrapping tools usually delegate to a service: `
|
|
336
|
+
API-wrapping tools usually delegate to a service: `await ncbi.fetch(input, ctx)`. The throw lives in the service, not the handler. Services accept `ctx` (the unified Context) so they can call `ctx.log`, `ctx.recoveryFor`, etc. The handler doesn't catch — it just bubbles, and the framework's auto-classifier preserves `data` on the wire.
|
|
317
337
|
|
|
318
|
-
The contract entry on the tool and the `data: { reason }` on the service throw need to use the **same reason string** so the two sides line up.
|
|
338
|
+
The contract entry on the tool and the `data: { reason }` on the service throw need to use the **same reason string** so the two sides line up. `ctx.recoveryFor('reason')` resolves the contract recovery from the calling tool's `errors[]` — same single-source-of-truth pattern that works in handlers.
|
|
319
339
|
|
|
320
340
|
```typescript
|
|
321
|
-
// service — passes data.reason
|
|
341
|
+
// service — receives ctx; passes data.reason and spreads ctx.recoveryFor
|
|
342
|
+
import type { Context } from '@cyanheads/mcp-ts-core';
|
|
322
343
|
import { serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
|
|
323
344
|
|
|
324
345
|
export class NcbiService {
|
|
325
|
-
async fetch(pmids: string[]) {
|
|
346
|
+
async fetch(pmids: string[], ctx: Context) {
|
|
326
347
|
const response = await fetchWithRetry(...);
|
|
327
348
|
if (!response.ok) {
|
|
328
|
-
throw serviceUnavailable(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
);
|
|
349
|
+
throw serviceUnavailable(`NCBI returned HTTP ${response.status}`, {
|
|
350
|
+
reason: 'ncbi_unreachable',
|
|
351
|
+
status: response.status,
|
|
352
|
+
...ctx.recoveryFor('ncbi_unreachable'), // resolves from caller's contract
|
|
353
|
+
});
|
|
333
354
|
}
|
|
334
355
|
return response.json();
|
|
335
356
|
}
|
|
@@ -343,11 +364,13 @@ export const fetchArticles = tool('fetch_articles', {
|
|
|
343
364
|
recovery: 'NCBI is degraded; retry in a few minutes.' },
|
|
344
365
|
],
|
|
345
366
|
async handler(input, ctx) {
|
|
346
|
-
return { articles: await ncbi.fetch(input.pmids) }; // throws bubble unchanged
|
|
367
|
+
return { articles: await ncbi.fetch(input.pmids, ctx) }; // throws bubble unchanged
|
|
347
368
|
},
|
|
348
369
|
});
|
|
349
370
|
```
|
|
350
371
|
|
|
372
|
+
`ctx.recoveryFor` returns `{}` when the calling tool has no contract or the reason isn't declared, so the spread is always safe — services don't have to know which tool called them.
|
|
373
|
+
|
|
351
374
|
See `add-service` for the full pattern.
|
|
352
375
|
|
|
353
376
|
#### Ad-hoc factory throws (fallback)
|
|
@@ -458,4 +481,4 @@ Large payloads burn the agent's context window. Default to curated summaries; of
|
|
|
458
481
|
- [ ] `task: true` added if the tool is long-running
|
|
459
482
|
- [ ] Registered in the project's existing `createApp()` tool list (directly or via barrel)
|
|
460
483
|
- [ ] `bun run devcheck` passes
|
|
461
|
-
- [ ] Smoke-tested with `bun run
|
|
484
|
+
- [ ] Smoke-tested with `bun run rebuild && bun run start:stdio` (or `start:http`)
|
package/skills/api-auth/SKILL.md
CHANGED
|
@@ -121,10 +121,11 @@ Set via `MCP_AUTH_MODE` environment variable.
|
|
|
121
121
|
|
|
122
122
|
### tenantId sources
|
|
123
123
|
|
|
124
|
-
|
|
|
125
|
-
|
|
126
|
-
|
|
|
127
|
-
|
|
|
124
|
+
| Mode | Source | Value |
|
|
125
|
+
|:-----|:-------|:------|
|
|
126
|
+
| Stdio (any auth mode) | Hardcoded default | `'default'` |
|
|
127
|
+
| HTTP + `MCP_AUTH_MODE=none` | Hardcoded default | `'default'` (single-tenant by design) |
|
|
128
|
+
| HTTP + `MCP_AUTH_MODE=jwt`/`oauth` | JWT `tid` claim | Auto-propagated from token; `undefined` if absent (fail-closed) |
|
|
128
129
|
|
|
129
130
|
### Tenant ID validation rules
|
|
130
131
|
|
|
@@ -148,7 +149,7 @@ handler: async (input, ctx) => {
|
|
|
148
149
|
},
|
|
149
150
|
```
|
|
150
151
|
|
|
151
|
-
`ctx.state` throws `McpError(InvalidRequest)` if `tenantId` is missing.
|
|
152
|
+
`ctx.state` throws `McpError(InvalidRequest)` if `tenantId` is missing. Stdio (any auth mode) and HTTP+`MCP_AUTH_MODE=none` default `tenantId` to `'default'` so `ctx.state` works without forcing operators to mint tokens. HTTP+`jwt`/`oauth` deliberately fails closed when the token lacks a `tid` claim — distinct authenticated callers must not silently share state.
|
|
152
153
|
|
|
153
154
|
---
|
|
154
155
|
|
|
@@ -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, all sub-APIs (`ctx.log`, `ctx.state`, `ctx.elicit`, `ctx.sample`, `ctx.progress`), and when to use each.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.2"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -26,7 +26,7 @@ interface Context {
|
|
|
26
26
|
// Identity & tracing
|
|
27
27
|
readonly requestId: string; // Unique per request, auto-generated
|
|
28
28
|
readonly timestamp: string; // ISO 8601 request start time
|
|
29
|
-
readonly tenantId?: string; //
|
|
29
|
+
readonly tenantId?: string; // JWT 'tid' claim; 'default' for stdio and HTTP+MCP_AUTH_MODE=none
|
|
30
30
|
readonly traceId?: string; // OTEL trace ID (present when OTEL enabled)
|
|
31
31
|
readonly spanId?: string; // OTEL span ID (present when OTEL enabled)
|
|
32
32
|
readonly auth?: AuthContext; // Parsed auth claims (clientId, scopes, sub)
|
|
@@ -53,10 +53,14 @@ interface Context {
|
|
|
53
53
|
|
|
54
54
|
// Raw URI — present only for resource handlers
|
|
55
55
|
readonly uri?: URL;
|
|
56
|
+
|
|
57
|
+
// Opt-in contract resolver — always present (returns {} when no contract is attached
|
|
58
|
+
// or the reason is unknown), strictly typed on HandlerContext<R> against declared reasons.
|
|
59
|
+
recoveryFor(reason: string): { recovery: { hint: string } } | {};
|
|
56
60
|
}
|
|
57
61
|
```
|
|
58
62
|
|
|
59
|
-
> **`ctx.fail` is on `HandlerContext<R>`, not `Context`.** When a definition declares `errors: [...]`, the handler receives `HandlerContext<R> = Context & { fail: TypedFail<R> }` — the typed `fail`
|
|
63
|
+
> **`ctx.fail` is on `HandlerContext<R>`, not `Context`.** When a definition declares `errors: [...]`, the handler receives `HandlerContext<R> = Context & { fail: TypedFail<R>; recoveryFor: TypedRecoveryFor<R> }` — both the typed `fail` and the strictly-typed `recoveryFor` live on the intersection. The bare `Context.recoveryFor` is the loose, always-present resolver. See [`ctx.fail`](#ctxfail) and [`ctx.recoveryFor`](#ctxrecoveryfor) below.
|
|
60
64
|
|
|
61
65
|
### Identity fields
|
|
62
66
|
|
|
@@ -64,7 +68,7 @@ interface Context {
|
|
|
64
68
|
|:------|:--------------|:-------|
|
|
65
69
|
| `requestId` | Yes | Auto-generated UUID per request |
|
|
66
70
|
| `timestamp` | Yes | ISO 8601, request start |
|
|
67
|
-
| `tenantId` |
|
|
71
|
+
| `tenantId` | Stdio and HTTP+`MCP_AUTH_MODE=none` (as `'default'`); JWT `tid` claim in HTTP+`jwt`/`oauth` | JWT / single-tenant default |
|
|
68
72
|
| `traceId` | When OTEL enabled | OTEL trace context |
|
|
69
73
|
| `spanId` | When OTEL enabled | OTEL trace context |
|
|
70
74
|
| `auth` | When auth enabled | Parsed JWT claims |
|
|
@@ -158,7 +162,7 @@ if (page.cursor) { /* more pages available */ }
|
|
|
158
162
|
|
|
159
163
|
### Behavior notes
|
|
160
164
|
|
|
161
|
-
- Throws `McpError(InvalidRequest)` if `tenantId` is missing
|
|
165
|
+
- 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).
|
|
162
166
|
- Keys are tenant-prefixed internally; handlers never need to namespace manually.
|
|
163
167
|
- **Workers persistence:** The `in-memory` provider loses data on cold starts. Use `cloudflare-kv`, `cloudflare-r2`, or `cloudflare-d1` for durable storage in Workers.
|
|
164
168
|
|
|
@@ -391,13 +395,57 @@ The contract is opt-in. See `skills/api-errors/SKILL.md` for the full type-drive
|
|
|
391
395
|
|
|
392
396
|
---
|
|
393
397
|
|
|
398
|
+
## `ctx.recoveryFor`
|
|
399
|
+
|
|
400
|
+
Always present on `Context`. Resolves the contract `recovery` for a given reason and returns the canonical wire shape `{ recovery: { hint } }`, ready to spread into `data`. The first member of a planned **family of opt-in resolution helpers** (future: `troubleshootingFor`, `userMessageFor`, …).
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
async handler(input, ctx) {
|
|
404
|
+
// Static recovery — pulled from the contract entry, no string duplication.
|
|
405
|
+
if (queue.full()) throw ctx.fail('queue_full', undefined, { ...ctx.recoveryFor('queue_full') });
|
|
406
|
+
|
|
407
|
+
// Dynamic recovery — interpolate runtime context, override the contract default.
|
|
408
|
+
if (!matched) throw ctx.fail('no_match', `No items for "${input.query}"`, {
|
|
409
|
+
recovery: { hint: `Try a broader query than "${input.query}", or check spelling.` },
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Signature
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
// Loose (always present on Context — works without a contract attached):
|
|
418
|
+
ctx.recoveryFor(reason: string): { recovery: { hint: string } } | {}
|
|
419
|
+
|
|
420
|
+
// Strict (HandlerContext<R> when the definition declares errors[]):
|
|
421
|
+
ctx.recoveryFor(reason: R): { recovery: { hint: string } }
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Behavior
|
|
425
|
+
|
|
426
|
+
| Aspect | Detail |
|
|
427
|
+
|:-------|:-------|
|
|
428
|
+
| No contract attached | Returns `{}` — spread is a no-op. Always safe. |
|
|
429
|
+
| Unknown reason | Returns `{}` (TS prevents this for typed callers; runtime is loose for JS / stale contracts). |
|
|
430
|
+
| Declared reason | Returns `{ recovery: { hint: <contract.recovery> } }` — spread into `data`. |
|
|
431
|
+
| Override | Caller can override by spreading `recoveryFor` first then writing `recovery: { hint: '...' }` after — last write wins. |
|
|
432
|
+
| Service usage | Services that accept `ctx: Context` can spread `ctx.recoveryFor('reason')` directly; the no-op fallback means they don't need to know which tool called them. |
|
|
433
|
+
|
|
434
|
+
### Why opt-in resolution, not auto-population
|
|
435
|
+
|
|
436
|
+
The framework never injects `data.recovery.hint` without an explicit signal at the throw site. Authors opt in by typing `ctx.recoveryFor('reason')` — the same way `ctx.fail('reason')` opts into resolving the contract `code`. The contract is the single source of truth for the recovery hint; the resolver is a typed lookup keyed by the same reason the author already typed. No magic, no hidden transformation.
|
|
437
|
+
|
|
438
|
+
The `≥5 words` lint rule on contract `recovery` (validated at lint time) makes this load-bearing — every `ctx.recoveryFor` call site benefits from the thoughtfulness the contract enforced.
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
394
442
|
## Quick reference
|
|
395
443
|
|
|
396
444
|
| Property | Type | Present when |
|
|
397
445
|
|:---------|:-----|:-------------|
|
|
398
446
|
| `ctx.requestId` | `string` | Always |
|
|
399
447
|
| `ctx.timestamp` | `string` | Always |
|
|
400
|
-
| `ctx.tenantId` | `string \| undefined` |
|
|
448
|
+
| `ctx.tenantId` | `string \| undefined` | Stdio (`'default'`); HTTP+`MCP_AUTH_MODE=none` (`'default'`); HTTP+`jwt`/`oauth` (JWT `tid` claim — undefined if absent) |
|
|
401
449
|
| `ctx.traceId` | `string \| undefined` | OTEL enabled |
|
|
402
450
|
| `ctx.spanId` | `string \| undefined` | OTEL enabled |
|
|
403
451
|
| `ctx.auth` | `AuthContext \| undefined` | Auth enabled |
|
|
@@ -411,3 +459,4 @@ The contract is opt-in. See `skills/api-errors/SKILL.md` for the full type-drive
|
|
|
411
459
|
| `ctx.progress` | `ContextProgress \| undefined` | Tool defined with `task: true` |
|
|
412
460
|
| `ctx.uri` | `URL \| undefined` | Resource handlers only |
|
|
413
461
|
| `ctx.fail` | `(reason, msg?, data?, opts?) => McpError` | Definition declares `errors[]` contract |
|
|
462
|
+
| `ctx.recoveryFor` | `(reason) => { recovery: { hint } } \| {}` | Always (no-op when no contract); strictly typed on `HandlerContext<R>` |
|
|
@@ -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.4"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -67,7 +67,56 @@ export const fetchTool = tool('fetch_articles', {
|
|
|
67
67
|
| Lint (startup) | Each `code` validated against `JsonRpcErrorCode`. Reasons validated as snake_case + unique within contract. `recovery` validated as non-empty and ≥ 5 words. |
|
|
68
68
|
| Lint (conformance) | If the handler `throw new McpError(JsonRpcErrorCode.X)` outside `ctx.fail`, conformance check warns when X isn't declared. |
|
|
69
69
|
|
|
70
|
-
> **`recovery` is
|
|
70
|
+
> **`recovery` is opt-in resolution, not auto-population.** 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). It does **not** automatically appear in runtime `data.recovery.hint` — the framework never injects it without an explicit signal at the throw site. Authors opt in by spreading `ctx.recoveryFor('reason')` into the `data` argument, the same way `ctx.fail('reason')` opts into resolving the contract `code`. What the author types at the throw site is what flows to the wire, with no hidden transformation; the resolver is just a typed lookup keyed by the same `reason` the author already typed.
|
|
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
|
+
```ts
|
|
77
|
+
export const calculateTool = tool('calculate', {
|
|
78
|
+
// ...
|
|
79
|
+
errors: [
|
|
80
|
+
{ reason: 'empty_expression', code: JsonRpcErrorCode.ValidationError,
|
|
81
|
+
when: 'Expression is empty or whitespace-only.',
|
|
82
|
+
recovery: 'Provide a non-empty mathematical expression to evaluate.' },
|
|
83
|
+
],
|
|
84
|
+
handler(input, ctx) {
|
|
85
|
+
if (!input.expression.trim()) {
|
|
86
|
+
// Static recovery — resolve from the contract.
|
|
87
|
+
throw ctx.fail('empty_expression', undefined, { ...ctx.recoveryFor('empty_expression') });
|
|
88
|
+
}
|
|
89
|
+
// ...
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Same pattern works inside services that accept `ctx`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
export class MathService {
|
|
98
|
+
parse(expr: string, ctx: Context) {
|
|
99
|
+
try {
|
|
100
|
+
return mathjs.parse(expr);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
throw validationError(`Parse failed: ${err.message}`, {
|
|
103
|
+
reason: 'parse_failed',
|
|
104
|
+
...ctx.recoveryFor('parse_failed'), // {} if calling tool has no matching reason
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The contract is the single source of truth — write the recovery once, lint validates ≥5 words, the resolver carries it to every throw site that opts in. For runtime-context recovery (interpolating input values, attempted IDs, queue state), override at the throw site:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
throw ctx.fail('no_match', `No item ${id}`, {
|
|
115
|
+
recovery: { hint: `No item ${id}; try IDs 1-100 instead.` },
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`ctx.recoveryFor` is the first member of a planned **family of opt-in resolution helpers**. Future contract-bound fields (`troubleshootingFor`, `userMessageFor`, …) follow the same shape: single-purpose, spreadable wire-shape, `{}` fallback when not applicable.
|
|
71
120
|
|
|
72
121
|
**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.
|
|
73
122
|
|
|
@@ -97,6 +146,17 @@ errors: [
|
|
|
97
146
|
|
|
98
147
|
The handler doesn't catch and re-throw — letting service errors bubble unchanged keeps "logic throws, framework catches" intact. The wire payload still carries `code` + `data.reason`, and 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.
|
|
99
148
|
|
|
149
|
+
To carry the contract `recovery` from a service throw, accept `ctx` and spread the resolver:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
throw validationError(message, {
|
|
153
|
+
reason: 'parse_failed',
|
|
154
|
+
...ctx.recoveryFor('parse_failed'), // {} when calling tool has no matching reason
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`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.
|
|
159
|
+
|
|
100
160
|
---
|
|
101
161
|
|
|
102
162
|
## Error Factories (fallback)
|
|
@@ -17,12 +17,9 @@ Unit tests (`add-test` skill) verify handler logic with mocked context. Field te
|
|
|
17
17
|
|
|
18
18
|
### Transport coverage
|
|
19
19
|
|
|
20
|
-
This skill drives an HTTP server because curl + JSON-RPC is the most reliable harness for shell-based agents.
|
|
20
|
+
This skill drives an HTTP server because curl + JSON-RPC is the most reliable harness for shell-based agents. The same handlers run on both transports — only the framing differs — so HTTP exercises the full functional surface.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
- **mcp-cli** (`uvx mcp-cli --stdio bun run dev:stdio`) — scriptable JSON-RPC client; best for batch/agentic testing
|
|
24
|
-
|
|
25
|
-
Adapt the test plan below the same way — universal battery on every definition, situational categories only when triggered, same error-contract verification — but call the tools through the inspector / mcp-cli rather than `mcp_call`. Pino startup + handler logs land on stderr in stdio mode (stdout is reserved for JSON-RPC), so tail with `2>/tmp/mcp-server.log` if you start the server yourself.
|
|
22
|
+
**Stdio coverage is a boot check only.** Run `bun run rebuild && bun run start:stdio`, confirm the startup logs look clean (banner, expected tool/resource counts, no errors/warnings, no missing-config gripes), then kill it. Pino logs go to stderr in stdio mode (stdout is reserved for JSON-RPC), so they print straight to the terminal when you run interactively. No need to call tools over stdio — the HTTP pass already covered handler behavior.
|
|
26
23
|
|
|
27
24
|
---
|
|
28
25
|
|
|
@@ -363,14 +363,7 @@ Separate from Getting Started. Show dev, build + run, and Workers/Docker deploym
|
|
|
363
363
|
|
|
364
364
|
### Local development
|
|
365
365
|
|
|
366
|
-
- **
|
|
367
|
-
|
|
368
|
-
\`\`\`sh
|
|
369
|
-
bun run dev:stdio
|
|
370
|
-
bun run dev:http
|
|
371
|
-
\`\`\`
|
|
372
|
-
|
|
373
|
-
- **Build and run the production version:**
|
|
366
|
+
- **Build and run:**
|
|
374
367
|
|
|
375
368
|
\`\`\`sh
|
|
376
369
|
# One-time build
|
|
@@ -180,7 +180,7 @@ Combine labels: `--label "bug" --label "regression"`.
|
|
|
180
180
|
For long output, write to a file and attach:
|
|
181
181
|
|
|
182
182
|
```bash
|
|
183
|
-
bun run
|
|
183
|
+
bun run rebuild && bun run start:stdio 2>&1 | head -100 > /tmp/mcp-error.log
|
|
184
184
|
|
|
185
185
|
# As part of a new issue
|
|
186
186
|
gh issue create -R cyanheads/mcp-ts-core \
|
|
@@ -173,7 +173,7 @@ gh label create breaking-change --color d93f0b --description "Change will break
|
|
|
173
173
|
### Attaching logs or large output
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
|
-
bun run
|
|
176
|
+
bun run rebuild && bun run start:stdio 2>&1 | head -200 > /tmp/server-error.log
|
|
177
177
|
|
|
178
178
|
# As part of a new issue
|
|
179
179
|
gh issue create \
|