@cyanheads/mcp-ts-core 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/AGENTS.md +2 -2
  2. package/CLAUDE.md +2 -2
  3. package/README.md +1 -1
  4. package/changelog/0.10.x/0.10.2.md +35 -0
  5. package/dist/core/context.d.ts +22 -0
  6. package/dist/core/context.d.ts.map +1 -1
  7. package/dist/core/context.js +12 -0
  8. package/dist/core/context.js.map +1 -1
  9. package/dist/linter/rules/enrichment-rules.d.ts +20 -0
  10. package/dist/linter/rules/enrichment-rules.d.ts.map +1 -1
  11. package/dist/linter/rules/enrichment-rules.js +74 -9
  12. package/dist/linter/rules/enrichment-rules.js.map +1 -1
  13. package/dist/linter/rules/index.d.ts +2 -2
  14. package/dist/linter/rules/index.d.ts.map +1 -1
  15. package/dist/linter/rules/index.js +2 -2
  16. package/dist/linter/rules/index.js.map +1 -1
  17. package/dist/linter/rules/schema-rules.d.ts +4 -0
  18. package/dist/linter/rules/schema-rules.d.ts.map +1 -1
  19. package/dist/linter/rules/schema-rules.js +13 -0
  20. package/dist/linter/rules/schema-rules.js.map +1 -1
  21. package/dist/linter/rules/tool-rules.d.ts +12 -0
  22. package/dist/linter/rules/tool-rules.d.ts.map +1 -1
  23. package/dist/linter/rules/tool-rules.js +48 -1
  24. package/dist/linter/rules/tool-rules.js.map +1 -1
  25. package/dist/linter/types.d.ts +17 -0
  26. package/dist/linter/types.d.ts.map +1 -1
  27. package/dist/linter/validate.d.ts.map +1 -1
  28. package/dist/linter/validate.js +55 -1
  29. package/dist/linter/validate.js.map +1 -1
  30. package/dist/logs/combined.log +4 -0
  31. package/dist/logs/error.log +2 -0
  32. package/dist/logs/interactions.log +0 -0
  33. package/dist/services/canvas/core/CanvasInstance.d.ts +1 -1
  34. package/dist/services/canvas/core/CanvasInstance.d.ts.map +1 -1
  35. package/dist/services/canvas/core/CanvasInstance.js +36 -11
  36. package/dist/services/canvas/core/CanvasInstance.js.map +1 -1
  37. package/dist/services/canvas/core/CanvasRegistry.d.ts +50 -3
  38. package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -1
  39. package/dist/services/canvas/core/CanvasRegistry.js +163 -6
  40. package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
  41. package/dist/services/canvas/spillover.d.ts +6 -0
  42. package/dist/services/canvas/spillover.d.ts.map +1 -1
  43. package/dist/services/canvas/spillover.js +1 -0
  44. package/dist/services/canvas/spillover.js.map +1 -1
  45. package/dist/services/canvas/types.d.ts +21 -0
  46. package/dist/services/canvas/types.d.ts.map +1 -1
  47. package/package.json +6 -6
  48. package/skills/add-tool/SKILL.md +38 -1
  49. package/skills/api-canvas/SKILL.md +23 -3
  50. package/skills/api-context/SKILL.md +41 -1
  51. package/skills/api-linter/SKILL.md +77 -3
  52. package/skills/api-mirror/SKILL.md +30 -1
  53. package/skills/design-mcp-server/SKILL.md +2 -1
  54. package/templates/Dockerfile +17 -0
@@ -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.6"
7
+ version: "1.7"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -46,14 +46,14 @@ Grouped by family. Jump to any rule ID via its anchor.
46
46
  | Schema | `schema-is-object`, `describe-on-fields`, `schema-serializable` | [Schema rules](#schema-rules) |
47
47
  | Portability | `schema-format-portability`, `schema-anyof-needs-type`, `schema-no-discriminator-keyword`, `schema-no-defs`, `schema-dialect-tag` | [Portability rules](#portability-rules) |
48
48
  | Names | `name-required`, `name-format`, `name-unique` | [Name rules](#name-rules) |
49
- | Tools | `description-required`, `handler-required`, `auth-type`, `auth-scope-format`, `annotation-type`, `annotation-coherence`, `meta-ui-type`, `meta-ui-resource-uri-required`, `meta-ui-resource-uri-scheme`, `app-tool-resource-pairing` | [Tool rules](#tool-rules) |
49
+ | Tools | `description-required`, `handler-required`, `auth-type`, `auth-scope-format`, `annotation-type`, `annotation-coherence`, `meta-ui-type`, `meta-ui-resource-uri-required`, `meta-ui-resource-uri-scheme`, `app-tool-resource-pairing`, `canvas-consumer-missing` | [Tool rules](#tool-rules) |
50
50
  | Resources | `uri-template-required`, `uri-template-valid`, `resource-name-not-uri`, `template-params-align` | [Resource rules](#resource-rules) |
51
51
  | Landing | `landing-*` (23 rules — shape, tagline, logo, links, repo, envExample, connectSnippets, theme) | [Landing config rules](#landing-config-rules) |
52
52
  | Prompts | `generate-required` | [Prompt rules](#prompt-rules) |
53
53
  | Handler body | `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error` | [Handler body rules](#handler-body-rules) |
54
54
  | Error contract (structural) | `error-contract-type`, `error-contract-empty`, `error-contract-entry-type`, `error-contract-code-type`, `error-contract-code-unknown`, `error-contract-code-unknown-error`, `error-contract-reason-required`, `error-contract-reason-format`, `error-contract-reason-unique`, `error-contract-when-required`, `error-contract-retryable-type`, `error-contract-recovery-required`, `error-contract-recovery-empty`, `error-contract-recovery-min-words` | [Error contract rules](#error-contract-rules) |
55
55
  | Error contract (conformance) | `error-contract-conformance`, `error-contract-prefer-fail` | [Error contract rules](#error-contract-rules) |
56
- | Enrichment | `enrichment-type`, `enrichment-empty`, `enrichment-field-type`, `enrichment-output-collision`, `enrichment-prefer-block`, `enrichment-trailer-render`, `enrichment-trailer-orphan`, `enrichment-trailer-unknown-field` | [Enrichment rules](#enrichment-rules) |
56
+ | Enrichment | `enrichment-type`, `enrichment-empty`, `enrichment-field-type`, `enrichment-output-collision`, `enrichment-prefer-block`, `enrichment-trailer-render`, `enrichment-trailer-orphan`, `enrichment-trailer-unknown-field`, `capped-list-no-truncation` | [Enrichment rules](#enrichment-rules) |
57
57
  | server.json | ~40 rules prefixed `server-json-*` | [server.json rules](#server-json-rules) |
58
58
 
59
59
  ---
@@ -367,6 +367,29 @@ An app tool's `_meta.ui.resourceUri` must match the `uriTemplate` of a registere
367
367
 
368
368
  **Fix:** either correct the `resourceUri` to match an existing resource, or register the resource it references. Use the `add-app-tool` skill's paired scaffold to avoid this.
369
369
 
370
+ ### canvas-consumer-missing
371
+
372
+ **Severity:** warning
373
+
374
+ Fires when the registered tool set contains at least one tool whose output schema has a depth-0 field named `canvas_id` or `canvasId`, but no consumer tool is registered — that is, no tool name ends with `_dataframe_query` and no extra names are listed in `canvasConsumers`.
375
+
376
+ A canvas token with no query path is dead output: the agent receives the token but has no tool to send it to. The fix runs in either direction:
377
+
378
+ - **Complete the integration** — add the standard `<prefix>_dataframe_query` and `<prefix>_dataframe_describe` consumers (see `api-canvas`).
379
+ - **Remove the staging** — when the data isn't row-shaped (nested, heterogeneous, single-record payloads), SQL access adds nothing. Drop the DataCanvas integration rather than adding tools to justify it.
380
+
381
+ **Knob:** suppress via `LintInput.canvasConsumers`:
382
+
383
+ ```ts
384
+ // Accept a non-standard query tool name:
385
+ validateDefinitions({ tools, canvasConsumers: ['my_sql_query'] });
386
+
387
+ // Disable the rule entirely:
388
+ validateDefinitions({ tools, canvasConsumers: false });
389
+ ```
390
+
391
+ **Env var:** `MCP_LINT_CANVAS_CONSUMERS` — comma-separated tool names; the literal `false` disables. A programmatic `LintInput.canvasConsumers` takes precedence over the env var. Servers that need the knob set `MCP_LINT_CANVAS_CONSUMERS=my_query_tool` in their `.env` or CI environment.
392
+
370
393
  ---
371
394
 
372
395
  ## Resource rules
@@ -777,6 +800,57 @@ Fires when an `enrichmentTrailer` key doesn't match any declared `enrichment` fi
777
800
 
778
801
  **Fix:** rename the trailer key to a declared enrichment field, or remove it.
779
802
 
803
+ ### capped-list-no-truncation
804
+
805
+ **Severity:** warning
806
+
807
+ Fires when a tool:
808
+ 1. has a depth-0 input field named `limit`, `per_page`, `page_size`, `max_results`, or `max_items` (case-insensitive; camelCase twins like `perPage`, `maxResults` match too), AND
809
+ 2. has at least one depth-0 array-typed `output` field, AND
810
+ 3. declares no truncation disclosure.
811
+
812
+ **Disclosure-present (rule silent) when** any of the following is true:
813
+ - The declared `enrichment` shape has a `truncated` or `totalCount` key (`ctx.enrich.truncated()` and `ctx.enrich.total()` satisfy this).
814
+ - The `output` schema has a depth-0 `truncated` or `totalCount` field.
815
+
816
+ A silently capped list leaves the agent unaware that results were cut off — it may treat a partial set as complete. Use `ctx.enrich.truncated({ shown, cap })` for the one-liner:
817
+
818
+ ```ts
819
+ // In the enrichment block:
820
+ enrichment: {
821
+ truncated: z.boolean().describe('True when the list was capped at the limit.'),
822
+ shown: z.number().describe('Number of items returned.'),
823
+ cap: z.number().describe('The limit applied.'),
824
+ },
825
+
826
+ // In the handler:
827
+ if (items.length >= input.limit) {
828
+ ctx.enrich.truncated({ shown: items.length, cap: input.limit });
829
+ }
830
+ ```
831
+
832
+ Or use `ctx.enrich.total(n)` when the upstream total is known — that writes `totalCount`, which is also recognized as honest disclosure.
833
+
834
+ **Threshold bound:** when the list is sorted by the cap key and the upstream total is unknowable (e.g. an API returning only the page), the smallest shown value upper-bounds all omitted items. Pass it as `ceiling`:
835
+
836
+ ```ts
837
+ ctx.enrich.truncated({ shown: items.length, cap: input.limit, ceiling: items.at(-1)?.count });
838
+ ```
839
+
840
+ Declare `truncationCeiling: z.number().optional()` in the `enrichment` block to surface it.
841
+
842
+ **Knob:** suppress via `LintInput.truncationAllowlist`:
843
+
844
+ ```ts
845
+ // Exempt a specific tool:
846
+ validateDefinitions({ tools, truncationAllowlist: ['my_search_tool'] });
847
+
848
+ // Disable the rule entirely:
849
+ validateDefinitions({ tools, truncationAllowlist: false });
850
+ ```
851
+
852
+ **Env var:** `MCP_LINT_TRUNCATION_ALLOWLIST` — comma-separated tool names; the literal `false` disables. A programmatic `LintInput.truncationAllowlist` takes precedence.
853
+
780
854
  ---
781
855
 
782
856
  ## Escape hatches
@@ -4,7 +4,7 @@ description: >
4
4
  Stand up a persistent, self-refreshing local mirror of a bulk upstream dataset with the MirrorService (@cyanheads/mcp-ts-core/mirror). Use when a server wraps a large or slow API and should query a synced local index (embedded SQLite + FTS5) instead of paginating the live API per request.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.0"
7
+ version: "1.1"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -92,6 +92,35 @@ The service owns `runSync` + state; it does not schedule. Wire "self-refreshing"
92
92
  - **Refresh** — register `runSync({ mode: 'refresh' })` on a cron via `schedulerService` from `@cyanheads/mcp-ts-core/utils`, inside `setup()`. Gate on transport (HTTP) when stdio operators run it out-of-band.
93
93
  - **Init** — run out-of-band (a CLI script / one-shot), never on startup: a full init can take hours and must not block the server. It is idempotent and resumable — re-running after an interrupt continues from the persisted cursor.
94
94
 
95
+ ### Shipping the mirror CLI in a production Docker image
96
+
97
+ The scaffold `Dockerfile` copies only `dist/` to the runtime stage. A mirror lifecycle script (`mirror:init`, `mirror:refresh`, `mirror:verify`) that imports through the `@/` path alias fails under `docker exec` — `@/` resolves to `src/` via the source `tsconfig.json`, and `src/` never reaches the image.
98
+
99
+ On the Bun runtime image (`oven/bun`), two stanzas fix it — no build change, no `rootDir` surgery, and the `mirror:*` package scripts stay identical between a dev checkout and the image.
100
+
101
+ Add the following to the runtime stage of `Dockerfile`, after the `COPY --from=build .../dist ./dist` line:
102
+
103
+ ```dockerfile
104
+ # Copy mirror lifecycle scripts. The shared context shim (_mirror-context.ts)
105
+ # is imported by the three named scripts, so it must travel with them.
106
+ COPY --from=build /usr/src/app/scripts/<your>-mirror-init.ts \
107
+ /usr/src/app/scripts/<your>-mirror-refresh.ts \
108
+ /usr/src/app/scripts/<your>-mirror-verify.ts \
109
+ /usr/src/app/scripts/_mirror-context.ts \
110
+ ./scripts/
111
+
112
+ # Bun honors tsconfig `paths` at runtime — map `@/` to the compiled `./dist/`
113
+ # so the .ts scripts resolve their alias imports against the build output.
114
+ # In a dev checkout the source tsconfig.json maps @/* → ./src/*; in the image
115
+ # this emitted one maps @/* → ./dist/*. Same `bun run mirror:*` command, both
116
+ # environments — the only lever is which tsconfig.json is on disk.
117
+ RUN echo '{"compilerOptions":{"baseUrl":".","paths":{"@/*":["./dist/*"]}}}' > tsconfig.json
118
+ ```
119
+
120
+ **Caveat:** this relies on Bun's runtime `paths` resolution. A Node runtime image (no native `.ts` execution) needs the scripts compiled into `dist/` instead — a separate tsconfig pass with a different `rootDir` is required in that case.
121
+
122
+ **`package.json` `files[]`:** add `scripts/_mirror-context.ts` and the three named lifecycle scripts so the npm tarball and `.mcpb` bundle carry them. Consumers installing from npm need them for `docker exec` access.
123
+
95
124
  ## Checklist
96
125
 
97
126
  - [ ] `defineMirror({ name, store, sync })`; the server holds the instance (one per mirror)
@@ -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.17"
7
+ version: "2.18"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -348,6 +348,7 @@ output: z.object({
348
348
  }),
349
349
  ```
350
350
 
351
+ - **Capped lists disclose truncation.** When a tool accepts a cap-like input (`limit`, `per_page`, `page_size`, `max_results`, `max_items`) and returns an array, the handler must disclose when the cap was hit. Standard fields: `truncated: true`, `shown`, `cap` in the `enrichment` block via `ctx.enrich.truncated({ shown, cap })`. `ctx.enrich.total(n)` (writes `totalCount`) is also recognized. Silent caps leave the agent treating a partial set as complete. The `capped-list-no-truncation` lint rule enforces this; see `api-linter` and `api-context`'s `ctx.enrich.truncated()` section.
351
352
  - **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
353
  - **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
354
  - **Outline one large *document* into sections.** When a single tool call returns one document-shaped record (not many rows) that can exceed context — a ~130KB FDA drug label, a big API entity dominated by a few fat fields — return a section *outline* (top-level keys + per-section byte size) instead of truncating, and let the agent re-call with `sections: [...]` to pull only what it needs. The `outlineOnOverflow()` helper (`@cyanheads/mcp-ts-core/utils`) measures the payload and returns a `full | outline` discriminated union; declare its `OUTLINE_VARIANT` as a branch of the tool's `output` so `format()`-parity is enforced per branch. Pure measure + key-slice — Workers-portable, unlike canvas-bound `spillover()`. Distinct from spillover on *shape*: spillover splits a row collection, this outlines one fat record. See the `techniques` skill's `outline-on-overflow` reference.
@@ -71,12 +71,29 @@ RUN if [ "$OTEL_ENABLED" = "true" ]; then \
71
71
  # Copy the compiled application code from the build stage
72
72
  COPY --from=build /usr/src/app/dist ./dist
73
73
 
74
+ # Mirror CLI (MirrorService adopters only — Tier 3, opt-in):
75
+ # Copy your mirror lifecycle scripts and emit a runtime tsconfig so Bun resolves
76
+ # the @/ path alias against ./dist/ rather than ./src/.
77
+ # See the api-mirror skill for the full recipe.
78
+ #
79
+ # COPY --from=build /usr/src/app/scripts/<your>-mirror-init.ts \
80
+ # /usr/src/app/scripts/<your>-mirror-refresh.ts \
81
+ # /usr/src/app/scripts/<your>-mirror-verify.ts \
82
+ # /usr/src/app/scripts/_mirror-context.ts \
83
+ # ./scripts/
84
+ # RUN echo '{"compilerOptions":{"baseUrl":".","paths":{"@/*":["./dist/*"]}}}' > tsconfig.json
85
+
74
86
  # The 'oven/bun' image already provides a non-root user named 'bun'.
75
87
  # We will use this existing user for enhanced security.
76
88
 
77
89
  # Create and set permissions for the log directory, assigning ownership to the 'bun' user.
78
90
  RUN mkdir -p /var/log/{{PACKAGE_NAME}} && chown -R bun:bun /var/log/{{PACKAGE_NAME}}
79
91
 
92
+ # Writable data dirs for on-disk SQLite stores (catalog index / observations
93
+ # mirror), owned by the runtime user. Mount a volume over either in production.
94
+ RUN mkdir -p /usr/src/app/.cache /usr/src/app/.mirror \
95
+ && chown -R bun:bun /usr/src/app/.cache /usr/src/app/.mirror
96
+
80
97
  # Switch to the non-root user
81
98
  USER bun
82
99