@cyanheads/mcp-ts-core 0.8.12 → 0.8.14

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 (39) hide show
  1. package/CLAUDE.md +3 -1
  2. package/README.md +14 -1
  3. package/changelog/0.8.x/0.8.13.md +27 -0
  4. package/changelog/0.8.x/0.8.14.md +33 -0
  5. package/dist/core/index.d.ts +2 -2
  6. package/dist/core/index.d.ts.map +1 -1
  7. package/dist/core/index.js +1 -1
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/logs/combined.log +4 -0
  10. package/dist/logs/error.log +4 -0
  11. package/dist/logs/interactions.log +0 -0
  12. package/dist/services/canvas/core/CanvasInstance.d.ts +13 -1
  13. package/dist/services/canvas/core/CanvasInstance.d.ts.map +1 -1
  14. package/dist/services/canvas/core/CanvasInstance.js +19 -0
  15. package/dist/services/canvas/core/CanvasInstance.js.map +1 -1
  16. package/dist/services/canvas/core/IDataCanvasProvider.d.ts +20 -2
  17. package/dist/services/canvas/core/IDataCanvasProvider.d.ts.map +1 -1
  18. package/dist/services/canvas/core/sqlGate.d.ts +9 -3
  19. package/dist/services/canvas/core/sqlGate.d.ts.map +1 -1
  20. package/dist/services/canvas/core/sqlGate.js +32 -4
  21. package/dist/services/canvas/core/sqlGate.js.map +1 -1
  22. package/dist/services/canvas/index.d.ts +1 -1
  23. package/dist/services/canvas/index.d.ts.map +1 -1
  24. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +21 -1
  25. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
  26. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +171 -45
  27. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
  28. package/dist/services/canvas/types.d.ts +32 -4
  29. package/dist/services/canvas/types.d.ts.map +1 -1
  30. package/dist/storage/core/IStorageProvider.d.ts +10 -3
  31. package/dist/storage/core/IStorageProvider.d.ts.map +1 -1
  32. package/package.json +9 -8
  33. package/skills/add-tool/SKILL.md +7 -1
  34. package/skills/api-canvas/SKILL.md +42 -8
  35. package/skills/api-errors/SKILL.md +3 -1
  36. package/skills/api-workers/SKILL.md +4 -4
  37. package/skills/tool-defs-analysis/SKILL.md +209 -0
  38. package/templates/AGENTS.md +2 -0
  39. package/templates/CLAUDE.md +2 -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.0"
7
+ version: "1.1"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -118,13 +118,42 @@ const joined = await instance.query(`
118
118
 
119
119
  `registerAs` rejects with `Conflict` if the target name already exists — drop it first.
120
120
 
121
- **Read-only enforcement** (three layers):
122
- 1. Statement count (must be 1) via `extractStatements`.
123
- 2. Statement type (must be `SELECT`) via `prepared.statementType`.
124
- 3. EXPLAIN-plan walk against an allowlisted set of physical operators.
121
+ **Read-only enforcement** (four layers):
122
+ 1. Text-level deny-list — pre-parse scan for file/HTTP-reading table functions (`read_csv*`, `read_json*`, `read_parquet*`, `read_text`, `read_blob`, `glob`, `iceberg_scan`, `delta_scan`, `postgres_scan`, `mysql_scan`, `sqlite_scan`, plus pre-staged spatial ones).
123
+ 2. Statement count (must be 1) via `extractStatements`.
124
+ 3. Statement type (must be `SELECT`) via `prepared.statementType`.
125
+ 4. EXPLAIN-plan walk against an allowlisted set of physical operators + a denied-function rescan over plan metadata strings.
125
126
 
126
127
  Any layer's rejection throws `ValidationError` with a structured `data.reason`. File-reading scans (`READ_CSV`, `READ_PARQUET`, `READ_JSON`), DDL (`CREATE_*`, `DROP_*`, `ALTER_*`), DML (`INSERT`, `UPDATE`, `DELETE`), exports (`COPY_TO_FILE`), and utility statements (`PRAGMA`, `ATTACH`, `LOAD`, `SET`) are all rejected.
127
128
 
129
+ ### `instance.registerView(name, selectSql, options?)`
130
+
131
+ Register a SQL view on the canvas. The `SELECT` runs through the same four-layer gate `query()` enforces, so a malicious definition fails at registration time, not later when the view is referenced.
132
+
133
+ ```ts
134
+ await instance.registerView(
135
+ 'sales_by_region',
136
+ 'SELECT region, SUM(amount) AS total FROM sales GROUP BY region',
137
+ );
138
+ // { viewName: 'sales_by_region', columns: ['region', 'total'] }
139
+
140
+ // Subsequent queries against the view inherit normal gate enforcement at execution time.
141
+ const result = await instance.query("SELECT total FROM sales_by_region WHERE region = 'a'");
142
+ ```
143
+
144
+ `CREATE OR REPLACE VIEW` semantics: re-registering the same name succeeds. Conflict with an existing base table throws `validationError({ reason: 'view_table_clash' })`.
145
+
146
+ ### `instance.importFrom(sourceCanvasId, sourceTableName, options?)`
147
+
148
+ Copy a table from another canvas the caller controls into this one. The lifecycle wrapper validates tenancy on both ids before the provider sees either. Round-trips through a sandbox-rooted Parquet temp file so `TIMESTAMP`/`DATE`/`BLOB` columns survive losslessly.
149
+
150
+ ```ts
151
+ const imported = await target.importFrom(source.canvasId, 'orders', { asName: 'orders_copy' });
152
+ // { tableName: 'orders_copy', rowCount: 2, columns: [...] }
153
+ ```
154
+
155
+ Idempotent on re-import (drop + create on the target). `asName` defaults to `sourceTableName`. Throws `validationError({ reason: 'import_same_canvas' })` if source and target are the same canvas — use `query({ registerAs })` to materialize within a single canvas. Throws `notFound` if the source table is missing; `validationError({ reason: 'import_view_clash' })` if the target name collides with an existing view.
156
+
128
157
  ### `instance.export(tableName, target, options?)`
129
158
 
130
159
  Export a canvas table. Path-based exports are sandboxed to `CANVAS_EXPORT_PATH` (default `./.canvas-exports`). Absolute paths and `..` traversal are rejected.
@@ -141,12 +170,17 @@ await instance.export('g_with_obs', { format: 'csv', stream: writableStream });
141
170
 
142
171
  ```ts
143
172
  const tables = await instance.describe();
144
- // [{ name: 'germplasm', rowCount: 200, columns: [...] }, ...]
173
+ // [{ name: 'germplasm', kind: 'table', rowCount: 200, columns: [...] }, ...]
174
+
175
+ // Filter by kind ('table' | 'view').
176
+ const onlyViews = await instance.describe({ kind: 'view' });
145
177
 
146
- await instance.drop('staging_table'); // false if missing
147
- await instance.clear(); // returns count dropped
178
+ await instance.drop('staging_table'); // detects kind, emits DROP TABLE or DROP VIEW; false if missing
179
+ await instance.clear(); // returns count dropped (drops views before tables to avoid dependency errors)
148
180
  ```
149
181
 
182
+ `TableInfo.kind` discriminates `'table'` vs `'view'`. For views, `rowCount` is materialized at describe time via `COUNT(*)` — not free; treat as an approximation if the view is expensive.
183
+
150
184
  ### Cancellation
151
185
 
152
186
  `registerTable`, `query`, and `export` accept `options.signal: AbortSignal`. The provider opens a fresh DuckDB connection per query/export so `connection.interrupt()` cancels exactly the in-flight work without disturbing other ops on the same canvas.
@@ -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.4"
7
+ version: "1.5"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -120,6 +120,8 @@ throw ctx.fail('no_match', `No item ${id}`, {
120
120
 
121
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.
122
122
 
123
+ > **Declare contracts inline on each tool, even when similar across tools.** The contract is part of the tool's documented public surface — reading one tool definition file should give the full picture (input, output, errors, handler, format). Don't extract a shared `errors[]` constant or contract module to deduplicate near-identical entries; per-tool repetition is the intended cost of locality, and dynamic `recovery` hints often need tool-specific runtime context anyway. If a code-cleanup pass suggests consolidating contracts, decline — the duplication is load-bearing for tool-def readability.
124
+
123
125
  > **Limits of the conformance lint.** The conformance and prefer-fail rules scan the handler's source text for `throw` statements. Errors thrown from called services (e.g. `await myService.fetch()` raising `RateLimited` internally) are invisible — the lint only sees what's lexically in the handler. Treat the contract as the *advertised* failure surface; bubbled-up codes still reach the client correctly via the auto-classifier, just without lint enforcement.
124
126
 
125
127
  ### Carrying contract `reason` from services
@@ -4,7 +4,7 @@ description: >
4
4
  Cloudflare Workers deployment using `createWorkerHandler` from `@cyanheads/mcp-ts-core/worker`. Covers the full handler signature, binding types, CloudflareBindings extensibility, runtime compatibility guards, and wrangler.toml requirements.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.2"
7
+ version: "1.3"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -126,10 +126,10 @@ In Workers, only these storage providers are allowed:
126
126
  | `cloudflare-r2` | R2 bucket binding — object storage |
127
127
  | `cloudflare-d1` | D1 database binding — SQLite-compatible |
128
128
 
129
- `filesystem` and `supabase` are not on the whitelist and behave differently:
129
+ `filesystem`, `supabase`, and unknown provider types are not on the whitelist:
130
130
 
131
- - **`filesystem`** and other unknown types are **silently forced to `in-memory`** (a warning is logged) in a serverless environment.
132
- - **`supabase`** does **not** silently fall back. The framework attempts to connect and throws `ConfigurationError` if credentials (`SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`) are missing or the client cannot be constructed. Do not set `STORAGE_PROVIDER_TYPE=supabase` in a Worker.
131
+ - **`filesystem`** and unknown types throw `ConfigurationError` in serverless environments.
132
+ - **`supabase`** does **not** silently fall back. The framework may validate Supabase credentials first, but Worker startup still fails with `ConfigurationError` because Supabase storage is not a supported serverless provider. Do not set `STORAGE_PROVIDER_TYPE=supabase` in a Worker.
133
133
 
134
134
  Set `STORAGE_PROVIDER_TYPE` to one of the four whitelisted values to avoid unexpected behavior.
135
135
 
@@ -0,0 +1,209 @@
1
+ ---
2
+ name: tool-defs-analysis
3
+ description: >
4
+ Read-only audit of MCP definition language across an existing surface — tools, resources, prompts. Walks every definition file and checks 10 categories the LLM reads to decide whether and how to call: voice & tense, internal leaks, audience leaks, defaults, recovery hints, output descriptions, cross-references, sparsity, examples, structure. Produces grouped findings with file:line citations and a numbered options list. Use during polish, after a refactor, or before a release. Complements `field-test` (behavior testing) and `security-pass` (security audit).
5
+ metadata:
6
+ author: cyanheads
7
+ version: "1.0"
8
+ audience: external
9
+ type: audit
10
+ ---
11
+
12
+ ## Context
13
+
14
+ Every string in a tool/resource/prompt definition is part of an LLM-facing API contract. The model reads the description, every parameter `.describe()`, the output schema, the recovery hints — and decides what to call and how. Definition language drifts: an internal mapping leaks into a parameter doc during a fix, a self-referential output description survives a refactor, a default that suited the developer at scaffold time stays after the typical call shape changes.
15
+
16
+ This skill is the **review-time pass** for that drift. Read each definition the way a mid-tier model with no project context would — can it pick the tool, fill the fields, and recover from errors using only the rendered schema?
17
+
18
+ | Skill | Lens |
19
+ |:---|:---|
20
+ | `design-mcp-server` | Authoring rules at write-time |
21
+ | `field-test` | Behavior testing + a narrow 3-category leak audit |
22
+ | `security-pass` | Injection, scopes, input sinks |
23
+ | `tool-defs-analysis` (this) | LLM-facing language across the existing surface |
24
+
25
+ `field-test` already audits descriptions for implementation leaks, meta-coaching, and consumer-aware phrasing during its catalog step — that's a fast shallow pass alongside live tool calls. This skill is the deeper review: 10 categories, every field, every recovery hint, every default value, with file:line citations.
26
+
27
+ **Read-only.** This skill produces a report; the maintainer applies fixes. While running it, do not run git, do not stage or commit, do not update the changelog, do not run `devcheck`, do not invoke wrapup or release workflows. Fixes flow through the normal authoring path (edit the definition, then re-run this skill if you want to verify).
28
+
29
+ ## When to Use
30
+
31
+ - After a polish session or refactor that touched definitions
32
+ - Before a release, alongside `polish-docs-meta` and `security-pass`
33
+ - When the user says "review my tool definitions", "audit descriptions", "are my tool descriptions any good"
34
+ - After scaffolding a new server but before it ships
35
+
36
+ Skip during initial authoring — `add-tool` and `design-mcp-server` cover that. Skip diff-only review — read each file in full so drift across the whole definition surfaces.
37
+
38
+ ## Inputs
39
+
40
+ Gather before starting. Ask if unclear:
41
+
42
+ 1. **Scope** — whole server, specific definitions, or a single directory?
43
+ 2. **Severity floor** — all findings (default), or skip nits?
44
+ 3. **Known concerns** — anything the user already wants emphasized?
45
+
46
+ ## Steps
47
+
48
+ ### 1. Build the inventory
49
+
50
+ ```bash
51
+ find src/mcp-server/tools/definitions -type f -name "*tool.ts" 2>/dev/null | sort
52
+ find src/mcp-server/resources/definitions -type f -name "*resource.ts" 2>/dev/null | sort
53
+ find src/mcp-server/prompts/definitions -type f -name "*.prompt.ts" 2>/dev/null | sort
54
+ ```
55
+
56
+ The `*tool.ts` / `*resource.ts` patterns also catch `*.app-tool.ts` / `*.app-resource.ts`. If the server's definitions live elsewhere (`examples/`, a packages workspace, …), audit those paths too.
57
+
58
+ Use `TaskCreate` — one task per file. Mark each complete after its findings are captured.
59
+
60
+ ### 2. Walk the 10 categories per file
61
+
62
+ Read each definition file in full. Apply every category — most files trip more than one. Capture each hit with `file:line`, the offending excerpt, and a one-line fix.
63
+
64
+ #### 1. Voice & tense
65
+
66
+ **Look in:** tool / resource / prompt `description`.
67
+
68
+ **Check:** imperative present-tense. "Search for trials" beats "Searches for trials" or "This tool will search trials".
69
+
70
+ **Smell:** "Allows you to…", "This tool…", "Provides functionality to…", "Searches for…", "Fetches…", "Will return…".
71
+
72
+ (Parameter `.describe()` text describes the *value*, not the tool — it doesn't need imperative voice.)
73
+
74
+ #### 2. Internal leaks
75
+
76
+ **Look in:** every `description` and `.describe()`.
77
+
78
+ **Check:** internal API routes, endpoint paths, API call counts, internal parameter mappings, sibling service names, version notes, TODOs.
79
+
80
+ **Smell:** "/api/v2/by-state", "Adds a second API call", "API requires `two_year_period`", "(deprecated; use bar_v2)", "TODO: support batch mode", "Used internally by FooService".
81
+
82
+ Prior art: #25.
83
+
84
+ #### 3. Audience leaks
85
+
86
+ **Look in:** every `description` and `.describe()`.
87
+
88
+ **Check:** reader-naming or meta-coaching directed at the LLM rather than describing the tool.
89
+
90
+ **Smell:** "suitable for LLM consumption", "Treat the returned ID as the canonical Y", "Agents should…", "Callers should…", "When you call this tool…", any reference to "LLM", "agent", "Claude", "the model".
91
+
92
+ Prior art: #74. Field-test catches this in its leak audit; this skill is the more thorough pass.
93
+
94
+ #### 4. Defaults
95
+
96
+ **Look in:** every `.default(...)` call in input schemas.
97
+
98
+ **Check:** the default matches the typical caller's case. A default that suited the developer at scaffold time often skews real calls — `limit: 1` makes default-args searches useless, `verbose: true` floods context, `dryRun: false` on a destructive op invites an irreversible accident.
99
+
100
+ **Smell:** dev-convenience values that survived the schema's first draft, dangerous defaults on destructive operations, defaults that contradict the description's framing of typical use.
101
+
102
+ #### 5. Recovery hints
103
+
104
+ **Look in:** `errors: [{ recovery: '…' }]` arrays, `data.recovery.hint` at throw sites in handler bodies.
105
+
106
+ **Check:** the hint directs the *agent* to its next action, not the developer to debugging. "Call `pubmed_search` with a narrower query" beats "Verify the configuration is correct" or "Internal error".
107
+
108
+ **Smell:** "Check the logs", "See documentation", "Contact admin", "Try again later" (with no condition), generic non-actionable text, hints that name internal classes, files, or env vars.
109
+
110
+ #### 6. Output descriptions
111
+
112
+ **Look in:** every field `.describe()` inside `output: z.object({ ... })`.
113
+
114
+ **Check:** the description tells the agent what the *value* is — not just the field name restated, not silent on dynamic shapes.
115
+
116
+ **Smell:**
117
+
118
+ - `name: z.string().describe('Name')` — tautology
119
+ - `description: z.string().describe('Description.')` — tautology
120
+ - `metadata: z.record(z.string(), z.unknown()).describe('Metadata')` — opaque dynamic shape with no hint about keys/values
121
+ - Optional fields with no note on when they're absent
122
+ - Enum fields with no `.describe()` on the variants
123
+
124
+ #### 7. Cross-references
125
+
126
+ **Look in:** tool descriptions, prompt content, recovery hints.
127
+
128
+ **Check:** when one tool/resource is mentioned, *when* to reach for it is explained — and the references cover the relevant siblings, not a partial sample.
129
+
130
+ **Smell:** "Use `foo_search` to find IDs" (no when); a prompt naming 3 of 7 landscape-relevant tools; a tool description listing one sibling but not the others that fit the same workflow.
131
+
132
+ #### 8. Sparsity
133
+
134
+ **Look in:** `output` schemas (especially fields wrapping external API data), `format()` rendering.
135
+
136
+ **Check:** optional upstream fields are acknowledged as such — not implied to always be present. `format()` doesn't print fabricated values for missing fields.
137
+
138
+ **Smell:**
139
+
140
+ - `pmid: z.string().describe('PubMed ID')` when only ~60% of records have one (should be `.optional()` and noted)
141
+ - `format()` printing `**PMID:** undefined`
142
+ - A required field in `output` for an upstream value the API doesn't always return
143
+
144
+ #### 9. Examples
145
+
146
+ **Look in:** parameter `.describe()` text containing "e.g.,", "(e.g. ...)", `.example(...)` calls.
147
+
148
+ **Check:** examples are domain-realistic — real-shaped IDs, real query strings, real values from the upstream domain. One example is usually enough.
149
+
150
+ **Smell:** `.describe('Item ID (e.g., "abc123")')` when real IDs have structure (`NCT12345678`); toy values ("foo", "bar"); padding multiple toy examples instead of one realistic one.
151
+
152
+ #### 10. Structure
153
+
154
+ **Look in:** tool / resource / prompt `description`.
155
+
156
+ **Check:** single cohesive paragraph. No bullet lists, no blank-line-separated sections, no markdown headers inside the description.
157
+
158
+ **Smell:** blank lines (`\n\n`) inside a description string, `- bullet` lines, `## Header` lines, "Operations:\n- foo: …" duplicating an enum's `.describe()` text.
159
+
160
+ Prior art: #33.
161
+
162
+ ### 3. Report
163
+
164
+ Three sections.
165
+
166
+ #### Summary (1 paragraph)
167
+
168
+ Definitions reviewed, categories with findings, total finding count. One sentence on the single most material finding.
169
+
170
+ #### Findings
171
+
172
+ Group by category. Within each category, list each finding:
173
+
174
+ ```
175
+ **<file>:<line> — <category> — (material|nit)**
176
+ Excerpt: `<the offending text>`
177
+ Issue: <one line: what's wrong>
178
+ Fix: <one line: what to change to>
179
+ ```
180
+
181
+ Two-level severity:
182
+
183
+ - **material** — affects agent decisions (will mis-select tool, mis-fill input, mis-handle output, swallow an irrecoverable error)
184
+ - **nit** — polish (style, voice consistency, minor phrasing)
185
+
186
+ Skip categories with no findings — don't list empty headers.
187
+
188
+ #### Options
189
+
190
+ Numbered, cherry-pickable. Map each item to a concrete change in a single file.
191
+
192
+ ```
193
+ 1. Tighten `metadata` description in `pubmed_fetch.tool.ts:42` — explain the dynamic shape (finding #3, material)
194
+ 2. Drop bullet list from `clinicaltrials_get_field_definitions.tool.ts:18` description — single paragraph (finding #5, material)
195
+ 3. Replace toy "abc123" example in `inventory_search.tool.ts:27` with real shape (finding #8, nit)
196
+ ```
197
+
198
+ End with:
199
+
200
+ > Pick by number (e.g. "do 1, 3, 5" or "expand on 2").
201
+
202
+ ## Checklist
203
+
204
+ - [ ] Scope confirmed (whole server / module / specific files)
205
+ - [ ] Inventory built — every `*.tool.ts`, `*.app-tool.ts`, `*.resource.ts`, `*.app-resource.ts`, `*.prompt.ts` listed
206
+ - [ ] Each file walked through all 10 categories (per-file, not 10 separate passes)
207
+ - [ ] **Read-only:** no git, no commits, no changelog edits, no `devcheck`, no wrapup invoked during the audit
208
+ - [ ] Findings carry file:line citation, excerpt, issue, fix
209
+ - [ ] Report: summary → grouped-by-category findings → numbered options
@@ -183,6 +183,8 @@ async handler(input, ctx) {
183
183
  }
184
184
  ```
185
185
 
186
+ **Declare contracts inline on each tool, even when similar across tools.** The contract is part of the tool's documented public surface — reading one tool definition file should give the full picture. Don't extract a shared `errors[]` constant or contract module to deduplicate; per-tool repetition is the intended cost of locality.
187
+
186
188
  **Fallback (no contract entry fits):** throw via factories or plain `Error`.
187
189
 
188
190
  ```ts
@@ -183,6 +183,8 @@ async handler(input, ctx) {
183
183
  }
184
184
  ```
185
185
 
186
+ **Declare contracts inline on each tool, even when similar across tools.** The contract is part of the tool's documented public surface — reading one tool definition file should give the full picture. Don't extract a shared `errors[]` constant or contract module to deduplicate; per-tool repetition is the intended cost of locality.
187
+
186
188
  **Fallback (no contract entry fits):** throw via factories or plain `Error`.
187
189
 
188
190
  ```ts