@cyanheads/mcp-ts-core 0.9.19 → 0.9.21

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.
Files changed (51) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.md +1 -1
  4. package/changelog/0.9.x/0.9.20.md +27 -0
  5. package/changelog/0.9.x/0.9.21.md +15 -0
  6. package/dist/linter/rules/error-contract-rules.d.ts +5 -3
  7. package/dist/linter/rules/error-contract-rules.d.ts.map +1 -1
  8. package/dist/linter/rules/error-contract-rules.js +11 -5
  9. package/dist/linter/rules/error-contract-rules.js.map +1 -1
  10. package/dist/mcp-server/notifications.d.ts +55 -0
  11. package/dist/mcp-server/notifications.d.ts.map +1 -0
  12. package/dist/mcp-server/notifications.js +51 -0
  13. package/dist/mcp-server/notifications.js.map +1 -0
  14. package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
  15. package/dist/mcp-server/resources/resource-registration.js +5 -3
  16. package/dist/mcp-server/resources/resource-registration.js.map +1 -1
  17. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +7 -1
  18. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  19. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +11 -4
  20. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  21. package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/tool-registration.js +6 -3
  23. package/dist/mcp-server/tools/tool-registration.js.map +1 -1
  24. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +8 -1
  25. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +11 -4
  27. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  28. package/dist/mcp-server/transports/http/httpTransport.d.ts +1 -1
  29. package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
  30. package/dist/mcp-server/transports/http/httpTransport.js +33 -16
  31. package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
  32. package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
  33. package/dist/utils/network/fetchWithTimeout.js +33 -7
  34. package/dist/utils/network/fetchWithTimeout.js.map +1 -1
  35. package/package.json +11 -11
  36. package/skills/add-tool/SKILL.md +30 -3
  37. package/skills/api-canvas/SKILL.md +16 -2
  38. package/skills/api-context/SKILL.md +32 -6
  39. package/skills/api-linter/SKILL.md +3 -3
  40. package/skills/api-utils/SKILL.md +2 -2
  41. package/skills/design-mcp-server/SKILL.md +20 -4
  42. package/skills/orchestrations/SKILL.md +1 -1
  43. package/skills/orchestrations/workflows/field-test-fix.md +1 -1
  44. package/skills/orchestrations/workflows/fix-wrapup-release.md +1 -1
  45. package/skills/orchestrations/workflows/greenfield-build.md +1 -1
  46. package/skills/orchestrations/workflows/maintenance-release.md +1 -1
  47. package/templates/AGENTS.md +4 -1
  48. package/templates/CLAUDE.md +4 -1
  49. package/dist/logs/combined.log +0 -8
  50. package/dist/logs/error.log +0 -4
  51. package/dist/logs/interactions.log +0 -0
@@ -4,7 +4,7 @@ description: >
4
4
  DataCanvas primitive reference — a Tier 3 SQL/analytical workspace for tabular MCP servers, backed by DuckDB. Use when registering tables from upstream APIs, running ad-hoc SQL across them, and exporting results. Covers the acquire → register → query → export flow, the token-sharing pattern for multi-agent collaboration, env config, and Cloudflare Workers fail-closed behavior.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.3"
7
+ version: "1.4"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -21,6 +21,17 @@ metadata:
21
21
 
22
22
  ---
23
23
 
24
+ ## When canvas earns its keep
25
+
26
+ Two gates before wiring canvas in — **both** must be yes. Canvas that fails either is a SQL surface nobody queries.
27
+
28
+ 1. **Is the data analytical, not just large?** Canvas is for tabular/numeric result sets an agent runs SQL over — aggregate, group, join, time-series filter. A **discovery/search surface** returning categorical metadata (titles, IDs, types, dates) where the workflow is *find the record, then drill into it* does **not** qualify, regardless of row count. A 5,000-row search result is still discovery. The gate is **shape, not size**: the right question is "would an agent write `SELECT … GROUP BY` against this?", not "does it have many rows?" For name→ID resolution over a bounded list, reach for MCP-side list filtering (see the `design-mcp-server` skill) instead.
29
+ 2. **Is it too big to inline?** A result that fits the response (≤ ~100 rows of compact data) just gets inlined — no canvas. Canvas is the third option only when shape *and* size both call for it.
30
+
31
+ If canvas earns its keep, it carries an obligation: **a tool that emits a `canvas_id` MUST ship a `dataframe_query` tool in the same server's surface** (see the [simple-shape Tools row](#simple-shape-defaults) and the [Checklist](#checklist)). A `canvas_id` with no query tool is dead output — the agent literally cannot reach the staged data.
32
+
33
+ ---
34
+
24
35
  ## Imports
25
36
 
26
37
  ```ts
@@ -257,7 +268,7 @@ Most canvas use cases are public-data analytics: fetch from an upstream API, sta
257
268
  | Table naming | `spillover()` auto-names the table `spilled_<id>`; pass `tableName` for a stable handle. A dataframe-query surface commonly adds its own `df_<id>` convention. |
258
269
  | Access control | Possession of the `canvas_id` is access — unguessable in practice (see [token-sharing model](#the-token-sharing-model)). TTL + the framework rate limiter backstop brute force. |
259
270
  | Enable flag | None of your own — canvas presence is the gate (`CANVAS_PROVIDER_TYPE=duckdb`; `getCanvas()` returns `undefined` otherwise). |
260
- | Tools | A fetcher that spills, plus `dataframe_query` for SQL. `dataframe_describe` / `dataframe_drop` are optional consumer conventions, not framework-provided. |
271
+ | Tools | A fetcher that spills **plus a `dataframe_query` tool — mandatory once anything emits a `canvas_id`**: a token with no query tool in the same server is dead output (the agent can't reach the staged data). `dataframe_describe` is strongly recommended — it lets the agent discover staged table and column names before writing SQL. `dataframe_drop` is optional. None are framework-provided; you register them. |
261
272
  | Fetcher output | Two things in one response: the inline preview (answer to the immediate question) and the table handle (escape hatch for follow-up SQL via `dataframe_query`). Neither replaces the other. |
262
273
 
263
274
  > The `MCP_HTTP_MAX_BODY_BYTES` request-body cap is **inbound-only** — it bounds the JSON-RPC request, not the upstream data a handler stages into the canvas or the rows it returns. Canvas servers send small requests (queries, SQL, canvas IDs) regardless of dataset size, so the cap never constrains canvas ingestion.
@@ -458,6 +469,7 @@ When the preview budget is small (single-digit rows) and the sniff window matter
458
469
 
459
470
  ### When *not* to use spillover
460
471
 
472
+ - **Discovery/search surfaces.** A result that's categorical metadata for *find-then-drill-in* — search hits, ID lookups, catalog browsing — is not analytical and doesn't earn a canvas regardless of row count (see [When canvas earns its keep](#when-canvas-earns-its-keep)). Use MCP-side list filtering or plain pagination instead.
461
473
  - **Tiny known result.** If the upstream call returns ≤ 100 rows, just inline them — no canvas needed.
462
474
  - **Headless register** (caller wants the full set on canvas with zero preview rows). Call `canvas.registerTable` directly. `previewChars` is rejected at `0`; spillover always implies a visible preview.
463
475
  - **Workers runtime.** Canvas requires DuckDB native; spillover is a canvas-coupled helper. For Workers parity, persist via `ctx.state` instead.
@@ -501,6 +513,8 @@ When the preview budget is small (single-digit rows) and the sniff window matter
501
513
  - [ ] Accessor wired in `setup()` callback via `setCanvas(core.canvas)`
502
514
  - [ ] Handler guards for canvas availability (`if (!canvas) throw ...`)
503
515
  - [ ] `canvas_id` accepted as optional input, returned in output
516
+ - [ ] A `dataframe_query` tool is registered in this server whenever any tool emits a `canvas_id` — a token with no query tool is dead output. Register `dataframe_describe` too (lets the agent discover staged table/column names)
517
+ - [ ] Canvas earns its keep: the staged data is analytical (an agent would SQL it), not a discovery/search surface of categorical metadata
504
518
  - [ ] SQL queries are read-only (enforced by the four-layer gate, but don't attempt writes)
505
519
  - [ ] Testing: mock the module-level `getCanvas()` accessor with `vi.spyOn` or a test setup that calls `setCanvas(mockCanvas)`
506
520
  - [ ] `bun run devcheck` passes
@@ -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`, `ctx.enrich`), and when to use each.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.5"
7
+ version: "1.6"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -42,7 +42,8 @@ interface Context {
42
42
  readonly elicit?: (message: string, schema: z.ZodObject<z.ZodRawShape>) => Promise<ElicitResult>;
43
43
  readonly sample?: (messages: SamplingMessage[], opts?: SamplingOpts) => Promise<CreateMessageResult>;
44
44
 
45
- // Notificationspresent when transport supports them
45
+ // List-changed / resource-updated notifications wired in every handler ctx;
46
+ // delivery is request-scoped (see § list-changed notifications)
46
47
  readonly notifyResourceListChanged?: () => void;
47
48
  readonly notifyResourceUpdated?: (uri: string) => void;
48
49
  readonly notifyPromptListChanged?: () => void;
@@ -328,6 +329,31 @@ interface SamplingOpts {
328
329
 
329
330
  ---
330
331
 
332
+ ## List-changed notifications (`ctx.notify*`)
333
+
334
+ Fire-and-forget signals that the tool / resource / prompt list changed (the client should re-list), or that a specific resource was updated. The framework advertises the matching `listChanged` capabilities on every `initialize`. All four are wired in every tool and resource handler context — call with optional chaining (`?.`), the type is optional for mock / forward-compat only.
335
+
336
+ ```ts
337
+ async handler(input, ctx) {
338
+ await enableFeatureTools();
339
+ ctx.notifyToolListChanged?.(); // tells the client to re-fetch tools/list
340
+ return { ok: true };
341
+ }
342
+ ```
343
+
344
+ ### Delivery
345
+
346
+ A notification fired **from inside a handler** routes through that request's own channel (`relatedRequestId`), so it reaches the client on **every transport** — stdio, HTTP, and Workers — even though HTTP/Workers run a per-request `McpServer` with no long-lived notification channel.
347
+
348
+ | Fired from | stdio | HTTP / Workers |
349
+ |:-----------|:------|:---------------|
350
+ | A tool / resource handler | ✅ delivered | ✅ delivered (on the request's SSE response stream) |
351
+ | A `task: true` background handler, cron, or any non-request scope | ✅ delivered | ⚠️ dropped — no request scope to route through |
352
+
353
+ The background-under-HTTP gap is a known limitation; a session-scoped notification bus would close it. `notifyResourceUpdated` routes to the calling request, not to clients that subscribed to the URI — the framework tracks no subscription state.
354
+
355
+ ---
356
+
331
357
  ## `ctx.signal`
332
358
 
333
359
  Standard `AbortSignal`. Present on every context. Set when the client cancels the request or when a task tool is cancelled.
@@ -601,10 +627,10 @@ See `add-tool`'s **Tool Response Design** and `skills/api-linter` (`enrichment-*
601
627
  | `ctx.enrich` | `Enrich` | Always; typed on `HandlerContext<R, E>` when an `enrichment` block is declared |
602
628
  | `ctx.elicit` | `function \| undefined` | Client supports elicitation |
603
629
  | `ctx.sample` | `function \| undefined` | Client supports sampling |
604
- | `ctx.notifyResourceListChanged` | `function \| undefined` | Transport supports resource notifications |
605
- | `ctx.notifyResourceUpdated` | `function \| undefined` | Transport supports resource notifications |
606
- | `ctx.notifyPromptListChanged` | `function \| undefined` | Transport supports prompt notifications |
607
- | `ctx.notifyToolListChanged` | `function \| undefined` | Transport supports tool notifications |
630
+ | `ctx.notifyResourceListChanged` | `function \| undefined` | Always in handler ctx; delivery request-scoped (see [§ list-changed notifications](#list-changed-notifications-ctxnotify)) |
631
+ | `ctx.notifyResourceUpdated` | `function \| undefined` | Always in handler ctx; delivery request-scoped |
632
+ | `ctx.notifyPromptListChanged` | `function \| undefined` | Always in handler ctx; delivery request-scoped |
633
+ | `ctx.notifyToolListChanged` | `function \| undefined` | Always in handler ctx; delivery request-scoped |
608
634
  | `ctx.progress` | `ContextProgress \| undefined` | Tool defined with `task: true` |
609
635
  | `ctx.uri` | `URL \| undefined` | Resource handlers only |
610
636
  | `ctx.fail` | `(reason, msg?, data?, opts?) => McpError` | Definition declares `errors[]` contract |
@@ -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.5"
7
+ version: "1.6"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -682,13 +682,13 @@ Fires when `recovery` has fewer than 5 words. Short recoveries like "Try again."
682
682
 
683
683
  **Severity:** warning
684
684
 
685
- Cross-check rule. Fires when a handler throws a non-baseline code (via `JsonRpcErrorCode.X` or a factory like `notFound()`) that isn't declared in `errors[]`.
685
+ Cross-check rule. Fires when a handler throws a non-baseline code (via `new McpError(JsonRpcErrorCode.X, …)` or a factory like `notFound()`) that isn't declared in `errors[]`.
686
686
 
687
687
  Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) are auto-allowed because they bubble from anywhere — services, framework utilities, the auto-classifier — and are implicitly always-possible on any tool. Only domain-specific codes need declaring.
688
688
 
689
689
  **Fix:** add the missing code to `errors[]` with a stable reason, or route through `ctx.fail(reason, …)` if it maps to an existing entry.
690
690
 
691
- **Heuristic limitations:** the scan reads `handler.toString()` and only catches direct `throw new McpError(JsonRpcErrorCode.X, …)` and `throw factory(…)` patterns. Indirect throws (`const e = notFound(); throw e;`), throws from called services, and throws via runtime helpers like `httpErrorFromResponse(...)` are invisible.
691
+ **Heuristic limitations:** the scan reads `handler.toString()` and only counts code *construction* sites — `new McpError(JsonRpcErrorCode.X, …)` and `throw factory(…)`. A bare `JsonRpcErrorCode.X` reference in a comparison (`err.code === JsonRpcErrorCode.X`) or a `case` label is not a throw and is correctly ignored. Indirect throws (`const e = notFound(); throw e;`), throws from called services, and throws via runtime helpers like `httpErrorFromResponse(...)` are invisible.
692
692
 
693
693
  ### error-contract-prefer-fail
694
694
 
@@ -4,7 +4,7 @@ description: >
4
4
  API reference for all utilities exported from `@cyanheads/mcp-ts-core/utils`. Use when looking up utility method signatures, options, peer dependencies, or usage patterns.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.2"
7
+ version: "2.3"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -29,7 +29,7 @@ Utility exports from `@cyanheads/mcp-ts-core/utils`. Utilities with complex APIs
29
29
 
30
30
  | Export | API | Notes |
31
31
  |:-------|:----|:------|
32
- | `fetchWithTimeout` | `(url, timeoutMs, context: RequestContext, options?: FetchWithTimeoutOptions) -> Promise<Response>` | Wraps `fetch` with `AbortController` timeout. `FetchWithTimeoutOptions` extends `RequestInit` (minus `signal`) and adds `rejectPrivateIPs?: boolean` and `signal?: AbortSignal` (external cancellation). SSRF guard (best-effort, not hard isolation): blocks RFC 1918, loopback, link-local, CGNAT, cloud metadata. DNS validation on Node, Bun, and Cloudflare Workers under `nodejs_compat`; hostname-only fallback otherwise. Manual redirect following (max 5) with per-hop SSRF check. **DNS rebinding / TOCTOU gap** — the validation lookup and `fetch`'s own resolution are independent; pair with egress controls or a DNS-pinning fetch proxy for strong isolation. |
32
+ | `fetchWithTimeout` | `(url, timeoutMs, context: RequestContext, options?: FetchWithTimeoutOptions) -> Promise<Response>` | Wraps `fetch` with `AbortController` timeout. `FetchWithTimeoutOptions` extends `RequestInit` (minus `signal`) and adds `rejectPrivateIPs?: boolean` and `signal?: AbortSignal` (external cancellation). SSRF guard (best-effort, not hard isolation): blocks RFC 1918, loopback, link-local, CGNAT, cloud metadata. DNS validation on Node, Bun, and Cloudflare Workers under `nodejs_compat`; hostname-only fallback otherwise. Manual redirect following (max 5) with per-hop SSRF check. **DNS rebinding / TOCTOU gap** — the validation lookup and `fetch`'s own resolution are independent; pair with egress controls or a DNS-pinning fetch proxy for strong isolation. **Error/log redaction:** URLs written into thrown errors and log lines are reduced to `origin + pathname` — the query string (where API keys commonly ride: `?api-key=…`, `?api_key=…`) never reaches the client or the logs. The actual request still uses the full URL. |
33
33
  | `withRetry` | `<T>(fn: () => Promise<T>, options?: RetryOptions) -> Promise<T>` | Executes `fn` with exponential backoff. Retries on transient errors (`ServiceUnavailable`, `Timeout`, `RateLimited`); non-transient errors fail immediately. On exhaustion, enriches the final error with attempt count in message and `data.retryAttempts`. **Place the retry boundary around the full pipeline** (fetch + parse), not just the network call. `RetryOptions`: `maxRetries` (default `3`), `baseDelayMs` (default `1000`), `maxDelayMs` (default `30000`), `jitter` (default `0.25`), `operation` (log label), `context` (RequestContext), `signal` (AbortSignal), `isTransient` (custom predicate). |
34
34
  | `httpErrorFromResponse` | `(response: Response, options?: HttpErrorFromResponseOptions) -> Promise<McpError>` | Maps an HTTP `Response` to a properly classified `McpError` — full status table including 401/403/408/422/429/5xx, body capture (truncated), `retry-after` header, optional `cause`. Use this instead of hand-rolling `if (status === 429) ...` ladders. Reads the response body — `clone()` first if you need it elsewhere. `HttpErrorFromResponseOptions`: `service?` (logical name in message, e.g. `'NCBI'`), `captureBody?` (default `true`), `bodyLimit?` (default `500`), `data?` (extra fields merged into `error.data`), `cause?`, `codeOverride?` (per-status mapping override). Pairs naturally with `withRetry` — both classify codes the same way. |
35
35
  | `httpStatusToErrorCode` | `(status: number) -> JsonRpcErrorCode \| undefined` | Sync status → code lookup. Returns `undefined` for 1xx/2xx/3xx. Use when you need just the code without a `Response` object handy. |
@@ -4,7 +4,7 @@ description: >
4
4
  Design the tool surface, resources, and service layer for a new MCP server. Use when starting a new server, planning a major feature expansion, or when the user describes a domain/API they want to expose via MCP. Produces a design doc at docs/design.md that drives implementation.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.15"
7
+ version: "2.16"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -349,7 +349,7 @@ output: z.object({
349
349
  ```
350
350
 
351
351
  - **Truncate large output with counts.** When a list exceeds a reasonable display size, show the top N and append "...and X more". Don't silently drop results.
352
- - **Spill big tabular results to a queryable surface.** When a tool's row set can exceed any reasonable context budget — paginated APIs, streamed exports, big query results — pair an inline preview with a `DataCanvas` table holding the full set, returned as a token the agent can SQL. Compute distributions or refinement hints across the full result, not the preview, so aggregate signal stays honest. See `api-canvas` for the `spillover()` helper.
352
+ - **Spill big *analytical* results to a queryable surface.** When a tool's row set is something an agent would run SQL over (aggregate, group, join) *and* can exceed any reasonable context budget — paginated APIs, streamed exports, big query results — pair an inline preview with a `DataCanvas` table holding the full set. **Two rules gate this:** (1) it must earn its keep on *shape, not size* a discovery/search surface of categorical metadata (titles, IDs) is not analytical and doesn't get a canvas regardless of row count; for name→ID resolution over a bounded list use [MCP-side list filtering](#mcp-side-list-filtering); (2) the `canvas_id` is reachable only if the same server **also exposes a `dataframe_query` tool** — emit one without the other and the handle is dead output. Compute distributions or refinement hints across the full result, not the preview, so aggregate signal stays honest. See `api-canvas` for the `spillover()` helper and both rules in full.
353
353
  - **Mirror a bulk upstream instead of paginating it live.** When the server wraps a large or slow API whose corpus is queried far more than it changes, sync it once into a persistent local index and query that as the primary data path — not the live API per request. Match the backend to corpus size: ≲ tens of thousands of rows → an in-memory index (server-level, no primitive); ~10⁴–10⁷ → the `MirrorService` (embedded SQLite + FTS5; declare a schema + a `sync` ingester via `defineMirror`/`sqliteMirrorStore`, then `runSync`/`query`, see `api-mirror`); ≳ 10⁸ → an external store. Distinct lifecycle from DataCanvas: a mirror is long-lived and cross-session, refreshed on a schedule; canvas is ephemeral and per-session.
354
354
  - **`format()` is the markdown twin of `structuredContent` — make both content-complete.** Different MCP clients forward different surfaces to the model: some (e.g., Claude Code) read `structuredContent` from `output`, others (e.g., Claude Desktop) read `content[]` from `format()`. Both must carry the same data so every client sees the same picture — `format()` just dresses it up with markdown. A thin `format()` that returns only a count or title leaves `content[]`-only clients blind to data that `structuredContent` clients can see. Render all fields the LLM needs, with structured markdown (headers, bold labels, lists) for readability.
355
355
  - **Agent-facing context must reach both client surfaces — put it in `enrichment`.** `structuredContent` (from `output`) and `content[]` (from `format()`) are read by different clients. Empty-result notices, the query/filter as the server parsed it, and pagination totals — the context the agent *reasons with*, distinct from the domain payload — reach only `content[]` if hand-authored into `format()` text alone, leaving `structuredContent`-only clients (Claude Code) blind. (The reverse can't happen: `format-parity` drags every `output` field into `format()`, so `output`-authored context already reaches both.) An `enrichment` block — the success-path counterpart to `errors[]`, populated via `ctx.enrich(...)` — reaches both automatically: merged into `structuredContent`, advertised as `output.extend(enrichment)`, mirrored into a `content[]` trailer, no `format()` entry needed. How each field renders in that trailer is a per-tool call — a kind-tag (`notice`/`total`/`echo`/`delta`) when a canonical form fits, a domain key like `totalFound` otherwise, and an `enrichmentTrailer.render` for any structured (object/array) field so it doesn't ship as a JSON blob. See `add-tool`'s **Tool Response Design**.
@@ -400,6 +400,21 @@ query: z.record(z.unknown()).optional()
400
400
 
401
401
  The pattern: name the shortcut for what it does (`text_search`, `name_search`), document what it expands to, and point to the full parameter for advanced use. Validate that at least one of the two is provided.
402
402
 
403
+ #### MCP-side list filtering
404
+
405
+ **Applies when:** an upstream API has no native search, the relevant set is bounded (fits one or a few fetches), and an agent needs to resolve a name → opaque ID. Skip when the API already searches, or when the set is unbounded (bills, votes, filings) — that belongs in the DataCanvas dataframe layer (`*_dataframe_query`), not an in-memory filter.
406
+
407
+ Two params, two behaviors — keep them named distinctly:
408
+
409
+ - **`query`** → **upstream** full-text search. The API does the work; it may honor operators and ranking.
410
+ - **a local filter param** → **fetched-then-filtered on our side**. Name it for the mechanic: `filter` or `nameContains` (the latter self-documents the local, name-keyed half of the split). Don't overload `query` for it — the two have different semantics and different cost.
411
+
412
+ **Earns-its-keep gate — all must hold:** bounded set; no native upstream search; real scan pain (opaque IDs, a large/unordered list, or a default page that hides relevant rows); and it filters the natural lookup key (name/title). When any fails, skip it — paginate, or send the agent to upstream `query`.
413
+
414
+ **Correctness: filter the *complete* bounded set, not the current page.** Fetch up to the cap (or page through) before filtering — filtering one page returns a misleading partial slice.
415
+
416
+ **Matching: strict token match is the default.** Normalize (lowercase, strip punctuation/diacritics) and require every query token to appear, so word order and missing interior words still match. That strict core is the ~90% case, needs no fuzzy library, and is too small to centralize (~6 lines — guidance, not a shared helper). Add a fuzzy fallback **only when a caller genuinely needs typo tolerance** (an LLM caller rarely does): fire it only when the strict match is empty, score against the best-matching *token* in each name (not the whole string) and **cap** the results — or one short query clears the threshold against dozens of long multi-word names — and label its hits `approximate`. Often a bare "no match — call the unfiltered list to browse" beats an `approximate` guess: it lets the model self-correct instead of committing to the wrong record. See `add-tool` for the param + handler implementation.
417
+
403
418
  #### Error design
404
419
 
405
420
  Errors are part of the tool's interface — design them during the design phase, not as an afterthought. Three aspects: **the contract** (which failures are public), **classification** (what error code), and **messaging** (what the LLM reads).
@@ -489,7 +504,7 @@ Skip for purely data/action-oriented servers.
489
504
 
490
505
  **Server-as-service.** When the server IS the source of truth (knowledge graph, in-memory task tracker, local scratchpad, embedded inference wrapper), the resilience table below doesn't apply — there's no upstream to retry. The design questions shift to state management: what's tenant-scoped vs. global, what TTLs apply, what survives a restart, what the storage backend is. Plan persistence via `ctx.state` for tenant-scoped KV (auto-namespaced by `tenantId`), or use a `StorageService` provider directly when data must cross tenants. Service init still happens in `setup()`, accessed via `getMyService()` at request time. Calls within the server are local and synchronous-ish — the API-efficiency table below also doesn't apply.
491
506
 
492
- **Tabular API servers: DataCanvas is one option.** For servers that fetch tabular data and want to expose a SQL/analytical workspace register tables, run cross-table queries, export results the framework's optional `DataCanvas` primitive (Tier 3, opt-in via `CANVAS_PROVIDER_TYPE=duckdb`) handles lifecycle, ID generation, eviction, and export wiring so you don't design your own. If you opt in, surface `canvas_id` as an optional input on register/query/export tools; the framework mints on omit and resolves on match. Tools access it via `ctx.core.canvas?` (undefined when disabled or running on Workers — DuckDB has no V8-isolate build). See `api-canvas` for the full reference.
507
+ **Analytical API servers: DataCanvas is one option.** For servers that fetch **analytical** data result sets an agent runs SQL over (aggregate, group, join, time-series) and want to expose a SQL workspace, the framework's optional `DataCanvas` primitive (Tier 3, opt-in via `CANVAS_PROVIDER_TYPE=duckdb`) handles lifecycle, ID generation, eviction, and export wiring so you don't design your own. **It earns its keep on shape, not size:** a discovery/search surface returning categorical metadata (titles, IDs, types) — where the workflow is find-the-record-then-drill-in — does *not* qualify even when the result is large; resolve names over a bounded set with [MCP-side list filtering](#mcp-side-list-filtering) instead. **If you opt in, the consumer tools are mandatory:** a tool that emits a `canvas_id` MUST be paired with a `dataframe_query` (and `dataframe_describe`) tool in the same surface — a `canvas_id` with no query tool is dead output the agent can't reach. Surface `canvas_id` as an optional input on register/query/export tools; the framework mints on omit and resolves on match. Tools access it via `ctx.core.canvas?` (undefined when disabled or running on Workers — DuckDB has no V8-isolate build). See `api-canvas` for the full reference.
493
508
 
494
509
  For services wrapping external APIs, plan the resilience layer.
495
510
 
@@ -629,6 +644,7 @@ Items without an `If …:` prefix apply to every design. Conditional items only
629
644
  - [ ] Design doc written to `docs/design.md`
630
645
  - [ ] Design confirmed with user (or user pre-authorized implementation)
631
646
  - [ ] **If ops share a noun:** related operations consolidated under one tool with `mode`/`operation` enum
647
+ - [ ] **If an upstream API has no native search but the relevant set is bounded:** MCP-side list filtering considered — a distinct local filter param (`filter`/`nameContains`, not `query`), filtering the full set, strict token match (fuzzy only when a caller needs typo tolerance)
632
648
  - [ ] **If the server has workflow tools:** call-flow documented (upstream sequence + mode arms) in design doc's Workflow Analysis
633
649
  - [ ] **If state-aware procedural guidance adds value:** instruction tool considered with `nextToolSuggestions` pre-filled from diagnostics
634
650
  - [ ] **If workflow tools have destructive modes:** destructive arm guarded by `ctx.elicit` when available, with `destructiveHint` annotation as fallback for non-interactive clients
@@ -639,5 +655,5 @@ Items without an `If …:` prefix apply to every design. Conditional items only
639
655
  - [ ] **If the server has external deps or shared state:** service layer planned (or explicitly skipped with reasoning)
640
656
  - [ ] **If services wrap external APIs:** resilience planned (retry boundary, backoff, parse classification)
641
657
  - [ ] **If multi-source server:** each source has its own service with independent auth/retry/rate-limit config. Fallback chains or fan-out strategy documented per tool. Output includes source provenance.
642
- - [ ] **If exposing a SQL/analytical workspace over tabular data is in scope:** DataCanvas considered (`api-canvas` skill) as one option before designing custom analytical stateregister / query / export tools accepting an optional `canvas_id`, with `ctx.core.canvas?` reads
658
+ - [ ] **If exposing a SQL/analytical workspace is in scope:** DataCanvas considered (`api-canvas` skill), and it earns its keep on *analytical* fit (an agent would SQL it), not row count a discovery/search surface of categorical metadata doesn't qualify. Any tool emitting a `canvas_id` is paired with a `dataframe_query` (+ `dataframe_describe`) tool in the same surface — a token with no query tool is dead output
643
659
  - [ ] **If the server needs runtime config:** env vars identified in `server-config.ts`
@@ -5,7 +5,7 @@ description: >
5
5
  metadata:
6
6
  author: cyanheads
7
7
  version: "1.2"
8
- audience: internal
8
+ audience: external
9
9
  type: workflow
10
10
  ---
11
11
 
@@ -5,7 +5,7 @@ description: >
5
5
  metadata:
6
6
  author: cyanheads
7
7
  version: "1.0"
8
- audience: internal
8
+ audience: external
9
9
  type: workflow
10
10
  ---
11
11
 
@@ -5,7 +5,7 @@ description: >
5
5
  metadata:
6
6
  author: cyanheads
7
7
  version: "1.0"
8
- audience: internal
8
+ audience: external
9
9
  type: workflow
10
10
  ---
11
11
 
@@ -5,7 +5,7 @@ description: >
5
5
  metadata:
6
6
  author: cyanheads
7
7
  version: "1.0"
8
- audience: internal
8
+ audience: external
9
9
  type: workflow
10
10
  ---
11
11
 
@@ -5,7 +5,7 @@ description: >
5
5
  metadata:
6
6
  author: cyanheads
7
7
  version: "1.1"
8
- audience: internal
8
+ audience: external
9
9
  type: workflow
10
10
  ---
11
11
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  ## First Session
15
15
 
16
- This project was just scaffolded with `bunx @cyanheads/mcp-ts-core init`. The framework, skills, and example definitions are in place — the domain isn't. The user's first messages will set direction; wait for them before proceeding.
16
+ This project was just scaffolded with `bunx @cyanheads/mcp-ts-core init`. You're holding a production-grade MCP framework with the hard parts already solved — error handling, telemetry, auth, transport, validation, lifecycle. What's missing is the **domain**. Your job: design the tool, resource, and service surface with the user, then implement it as small pure handlers that throw — the framework catches, classifies, and instruments the rest. Design before code; the user's first messages set direction, so wait for them before scaffolding definitions.
17
17
 
18
18
  > **Remove this section** from CLAUDE.md / AGENTS.md after completing these steps. The skills and conventions below remain — this block is one-time onboarding only.
19
19
 
@@ -279,6 +279,7 @@ Available skills:
279
279
  | `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
280
280
  | `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
281
281
  | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
282
+ | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
282
283
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
283
284
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
284
285
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
@@ -293,6 +294,8 @@ Available skills:
293
294
  | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
294
295
  | `api-workers` | Cloudflare Workers runtime |
295
296
 
297
+ **Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
298
+
296
299
  When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
297
300
 
298
301
  ---
@@ -13,7 +13,7 @@
13
13
 
14
14
  ## First Session
15
15
 
16
- This project was just scaffolded with `bunx @cyanheads/mcp-ts-core init`. The framework, skills, and example definitions are in place — the domain isn't. The user's first messages will set direction; wait for them before proceeding.
16
+ This project was just scaffolded with `bunx @cyanheads/mcp-ts-core init`. You're holding a production-grade MCP framework with the hard parts already solved — error handling, telemetry, auth, transport, validation, lifecycle. What's missing is the **domain**. Your job: design the tool, resource, and service surface with the user, then implement it as small pure handlers that throw — the framework catches, classifies, and instruments the rest. Design before code; the user's first messages set direction, so wait for them before scaffolding definitions.
17
17
 
18
18
  > **Remove this section** from CLAUDE.md / AGENTS.md after completing these steps. The skills and conventions below remain — this block is one-time onboarding only.
19
19
 
@@ -279,6 +279,7 @@ Available skills:
279
279
  | `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
280
280
  | `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
281
281
  | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
282
+ | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
282
283
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
283
284
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
284
285
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
@@ -293,6 +294,8 @@ Available skills:
293
294
  | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
294
295
  | `api-workers` | Cloudflare Workers runtime |
295
296
 
297
+ **Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
298
+
296
299
  When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
297
300
 
298
301
  ---
@@ -1,8 +0,0 @@
1
- {"level":40,"time":1780225427928,"env":"testing","version":"0.9.18","pid":81052,"transport":"http","requestId":"VVRRG-H0319","timestamp":"2026-05-31T11:03:47.928Z","operation":"TransportManager.start","component":"HttpTransportSetup","msg":"MCP_ALLOWED_ORIGINS is not set — CORS is wildcard for CLI clients; browser Origin headers are restricted to loopback. Set MCP_ALLOWED_ORIGINS for production deployments accepting remote browser origins."}
2
- {"level":40,"time":1780225429690,"env":"testing","version":"0.9.18","pid":81052,"transport":"http","requestId":"VVRRG-H0319","timestamp":"2026-05-31T11:03:47.928Z","operation":"TransportManager.start","component":"HttpTransportSetup","sessionId":"not-a-real-session-1780225429690","msg":"Session validation failed - invalid or hijacked session"}
3
- {"level":50,"time":1780225433452,"env":"testing","version":"0.0.0-test","pid":81242,"requestId":"JKUAU-THL6D","timestamp":"2026-05-31T11:03:53.451Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"f6bd7a9479f95e541e8fc4ff349f26e58f52d11e0adb27c998363281fc996cc0","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"f6bd7a9479f95e541e8fc4ff349f26e58f52d11e0adb27c998363281fc996cc0","toolName":"scoped_echo","requestId":"JKUAU-THL6D","timestamp":"2026-05-31T11:03:53.451Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"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:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:251: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:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:293: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."}
4
- {"level":50,"time":1780225433460,"env":"testing","version":"0.0.0-test","pid":81242,"requestId":"NZFY2-LDFDA","timestamp":"2026-05-31T11:03:53.460Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"9cb9c32a32ff6dff5a162b462fb458ef271bb64711acab8467014cc411071dcb","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"9cb9c32a32ff6dff5a162b462fb458ef271bb64711acab8467014cc411071dcb","toolName":"scoped_echo","requestId":"NZFY2-LDFDA","timestamp":"2026-05-31T11:03:53.460Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"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:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:251: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:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:293: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."}
5
- {"level":40,"time":1780225576585,"env":"testing","version":"0.9.19","pid":84938,"transport":"http","requestId":"IJUO1-6Q9UC","timestamp":"2026-05-31T11:06:16.584Z","operation":"TransportManager.start","component":"HttpTransportSetup","msg":"MCP_ALLOWED_ORIGINS is not set — CORS is wildcard for CLI clients; browser Origin headers are restricted to loopback. Set MCP_ALLOWED_ORIGINS for production deployments accepting remote browser origins."}
6
- {"level":40,"time":1780225578334,"env":"testing","version":"0.9.19","pid":84938,"transport":"http","requestId":"IJUO1-6Q9UC","timestamp":"2026-05-31T11:06:16.584Z","operation":"TransportManager.start","component":"HttpTransportSetup","sessionId":"not-a-real-session-1780225578334","msg":"Session validation failed - invalid or hijacked session"}
7
- {"level":50,"time":1780225582015,"env":"testing","version":"0.0.0-test","pid":85073,"requestId":"RROQC-F3QA4","timestamp":"2026-05-31T11:06:22.014Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"074c0eb19abb7162d7e9535ee0f84ab27ff7b26d97b00cc186a0bdb3be13c85e","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"074c0eb19abb7162d7e9535ee0f84ab27ff7b26d97b00cc186a0bdb3be13c85e","toolName":"scoped_echo","requestId":"RROQC-F3QA4","timestamp":"2026-05-31T11:06:22.014Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"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:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:251: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:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:293: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."}
8
- {"level":50,"time":1780225582022,"env":"testing","version":"0.0.0-test","pid":85073,"requestId":"KL3EP-RS5AO","timestamp":"2026-05-31T11:06:22.022Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"211605bd738f777e38d582853d167849c027f5d280e6e0622ca9946ab08da583","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"211605bd738f777e38d582853d167849c027f5d280e6e0622ca9946ab08da583","toolName":"scoped_echo","requestId":"KL3EP-RS5AO","timestamp":"2026-05-31T11:06:22.022Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"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:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:251: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:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:293: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."}
@@ -1,4 +0,0 @@
1
- {"level":50,"time":1780225433452,"env":"testing","version":"0.0.0-test","pid":81242,"requestId":"JKUAU-THL6D","timestamp":"2026-05-31T11:03:53.451Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"f6bd7a9479f95e541e8fc4ff349f26e58f52d11e0adb27c998363281fc996cc0","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"f6bd7a9479f95e541e8fc4ff349f26e58f52d11e0adb27c998363281fc996cc0","toolName":"scoped_echo","requestId":"JKUAU-THL6D","timestamp":"2026-05-31T11:03:53.451Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"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:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:251: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:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:293: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":1780225433460,"env":"testing","version":"0.0.0-test","pid":81242,"requestId":"NZFY2-LDFDA","timestamp":"2026-05-31T11:03:53.460Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"9cb9c32a32ff6dff5a162b462fb458ef271bb64711acab8467014cc411071dcb","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"9cb9c32a32ff6dff5a162b462fb458ef271bb64711acab8467014cc411071dcb","toolName":"scoped_echo","requestId":"NZFY2-LDFDA","timestamp":"2026-05-31T11:03:53.460Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"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:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:251: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:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:293: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."}
3
- {"level":50,"time":1780225582015,"env":"testing","version":"0.0.0-test","pid":85073,"requestId":"RROQC-F3QA4","timestamp":"2026-05-31T11:06:22.014Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"074c0eb19abb7162d7e9535ee0f84ab27ff7b26d97b00cc186a0bdb3be13c85e","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"074c0eb19abb7162d7e9535ee0f84ab27ff7b26d97b00cc186a0bdb3be13c85e","toolName":"scoped_echo","requestId":"RROQC-F3QA4","timestamp":"2026-05-31T11:06:22.014Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"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:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:251: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:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:293: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."}
4
- {"level":50,"time":1780225582022,"env":"testing","version":"0.0.0-test","pid":85073,"requestId":"KL3EP-RS5AO","timestamp":"2026-05-31T11:06:22.022Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"211605bd738f777e38d582853d167849c027f5d280e6e0622ca9946ab08da583","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"211605bd738f777e38d582853d167849c027f5d280e6e0622ca9946ab08da583","toolName":"scoped_echo","requestId":"KL3EP-RS5AO","timestamp":"2026-05-31T11:06:22.022Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"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:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:251: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:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:293: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."}
File without changes