@cyanheads/mcp-ts-core 0.8.8 → 0.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -1
- package/README.md +4 -1
- package/biome.json +1 -1
- package/changelog/0.8.x/0.8.10.md +23 -0
- package/changelog/0.8.x/0.8.9.md +34 -0
- package/dist/config/index.d.ts +51 -15
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +44 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts +8 -0
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +11 -0
- package/dist/core/app.js.map +1 -1
- package/dist/core/worker.d.ts +7 -0
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +1 -0
- package/dist/core/worker.js.map +1 -1
- package/dist/logs/combined.log +4 -4
- package/dist/logs/error.log +4 -4
- package/dist/services/canvas/core/CanvasInstance.d.ts +43 -0
- package/dist/services/canvas/core/CanvasInstance.d.ts.map +1 -0
- package/dist/services/canvas/core/CanvasInstance.js +63 -0
- package/dist/services/canvas/core/CanvasInstance.js.map +1 -0
- package/dist/services/canvas/core/CanvasRegistry.d.ts +96 -0
- package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -0
- package/dist/services/canvas/core/CanvasRegistry.js +250 -0
- package/dist/services/canvas/core/CanvasRegistry.js.map +1 -0
- package/dist/services/canvas/core/DataCanvas.d.ts +49 -0
- package/dist/services/canvas/core/DataCanvas.d.ts.map +1 -0
- package/dist/services/canvas/core/DataCanvas.js +85 -0
- package/dist/services/canvas/core/DataCanvas.js.map +1 -0
- package/dist/services/canvas/core/IDataCanvasProvider.d.ts +47 -0
- package/dist/services/canvas/core/IDataCanvasProvider.d.ts.map +1 -0
- package/dist/services/canvas/core/IDataCanvasProvider.js +10 -0
- package/dist/services/canvas/core/IDataCanvasProvider.js.map +1 -0
- package/dist/services/canvas/core/canvasFactory.d.ts +26 -0
- package/dist/services/canvas/core/canvasFactory.d.ts.map +1 -0
- package/dist/services/canvas/core/canvasFactory.js +63 -0
- package/dist/services/canvas/core/canvasFactory.js.map +1 -0
- package/dist/services/canvas/core/sqlGate.d.ts +107 -0
- package/dist/services/canvas/core/sqlGate.d.ts.map +1 -0
- package/dist/services/canvas/core/sqlGate.js +267 -0
- package/dist/services/canvas/core/sqlGate.js.map +1 -0
- package/dist/services/canvas/index.d.ts +21 -0
- package/dist/services/canvas/index.d.ts.map +1 -0
- package/dist/services/canvas/index.js +19 -0
- package/dist/services/canvas/index.js.map +1 -0
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +56 -0
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -0
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +600 -0
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -0
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts +48 -0
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts.map +1 -0
- package/dist/services/canvas/providers/duckdb/exportWriter.js +119 -0
- package/dist/services/canvas/providers/duckdb/exportWriter.js.map +1 -0
- package/dist/services/canvas/providers/duckdb/schemaSniffer.d.ts +44 -0
- package/dist/services/canvas/providers/duckdb/schemaSniffer.d.ts.map +1 -0
- package/dist/services/canvas/providers/duckdb/schemaSniffer.js +134 -0
- package/dist/services/canvas/providers/duckdb/schemaSniffer.js.map +1 -0
- package/dist/services/canvas/types.d.ts +134 -0
- package/dist/services/canvas/types.d.ts.map +1 -0
- package/dist/services/canvas/types.js +9 -0
- package/dist/services/canvas/types.js.map +1 -0
- package/package.json +14 -5
- package/skills/add-tool/SKILL.md +1 -0
- package/skills/api-canvas/SKILL.md +260 -0
- package/skills/api-config/SKILL.md +18 -0
- package/skills/api-workers/SKILL.md +6 -0
- package/skills/design-mcp-server/SKILL.md +3 -0
- package/skills/report-issue-framework/SKILL.md +1 -0
- package/skills/report-issue-local/SKILL.md +2 -0
- package/skills/security-pass/SKILL.md +24 -0
- package/templates/.github/ISSUE_TEMPLATE/bug_report.yml +1 -0
- package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +1 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-canvas
|
|
3
|
+
description: >
|
|
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
|
+
metadata:
|
|
6
|
+
author: cyanheads
|
|
7
|
+
version: "1.0"
|
|
8
|
+
audience: external
|
|
9
|
+
type: reference
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
`DataCanvas` is a primitive for **storage stashes, canvas computes**. The existing `IStorageProvider` is a key/value abstraction — it can stash blobs but exposes no analytical surface. `DataCanvas` is the analytical surface: register tabular data from upstream APIs, run SQL across multiple registered tables, and export results as CSV/Parquet/JSON.
|
|
15
|
+
|
|
16
|
+
**Tier 3** — `@duckdb/node-api` is an optional peer dependency. Servers that don't enable canvas pay zero install cost. Lazy-loaded on first use.
|
|
17
|
+
|
|
18
|
+
**Disabled by default.** Set `CANVAS_PROVIDER_TYPE=duckdb` to enable. Otherwise `core.canvas` is `undefined`.
|
|
19
|
+
|
|
20
|
+
**Cloudflare Workers:** unsupported. DuckDB has no V8-isolate build. Setting `CANVAS_PROVIDER_TYPE=duckdb` on a Worker fails closed with a `ConfigurationError` at init time.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Imports
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import type { DataCanvas, CanvasInstance, ColumnSchema } from '@cyanheads/mcp-ts-core/canvas';
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The framework wires the optional service onto `CoreServices`:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
interface CoreServices {
|
|
34
|
+
canvas?: DataCanvas; // present when CANVAS_PROVIDER_TYPE !== 'none'
|
|
35
|
+
// ... other services
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## The token-sharing model
|
|
42
|
+
|
|
43
|
+
A canvas is identified by an opaque 10-character URL-safe `canvasId` (~10¹⁸ keyspace). Tools that touch canvas state accept an optional `canvas_id` input parameter:
|
|
44
|
+
|
|
45
|
+
| Caller passes | Result |
|
|
46
|
+
|:--------------|:-------|
|
|
47
|
+
| **Omitted** | Framework mints a fresh canvasId, returns it in the tool output. Caller surfaces it to the user / next tool call / another agent. |
|
|
48
|
+
| **Existing id (own tenant)** | Resolves to that canvas, slides TTL forward, returns `isNew: false`. |
|
|
49
|
+
| **Existing id (other tenant)** | Throws `NotFound` — uniform with unknown to avoid leaking existence across tenants. |
|
|
50
|
+
| **Unknown id** | Throws `NotFound` with a hint to omit the parameter on retry. |
|
|
51
|
+
|
|
52
|
+
When auth is enabled, the effective scope is the composite `(tenantId, canvasId)`. In `MCP_AUTH_MODE=none`, `tenantId` collapses to `'default'` and the canvasId is the only differentiator — entropy + TTL + the framework's rate limiter make brute-force discovery operationally infeasible. **Designed for public-data servers (BrAPI, OpenFEC, etc.). Don't put PII on a no-auth canvas.**
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Lifecycle
|
|
57
|
+
|
|
58
|
+
| Behavior | Default | Override |
|
|
59
|
+
|:---------|:--------|:---------|
|
|
60
|
+
| Sliding TTL | 24 h, extended on every operation | `CANVAS_TTL_MS` |
|
|
61
|
+
| Absolute cap from creation | 7 days | `CANVAS_ABSOLUTE_CAP_MS` |
|
|
62
|
+
| Per-tenant active cap | 100 canvases | `CANVAS_MAX_CANVASES_PER_TENANT` |
|
|
63
|
+
| Sweeper interval | 60 s | `CANVAS_SWEEPER_INTERVAL_MS` (0 to disable) |
|
|
64
|
+
| Persistence | In-memory only | — (v1; restart drops all canvases) |
|
|
65
|
+
|
|
66
|
+
The sweeper runs as an `unref`'d `setInterval` — does not keep the event loop alive on its own. Shutdown via `core.canvas.shutdown(ctx)` (called automatically from `ServerHandle.shutdown()`) stops the sweeper and tears down every active DuckDB instance.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## API
|
|
71
|
+
|
|
72
|
+
### `canvas.acquire(maybeId, ctx, options?) → CanvasInstance`
|
|
73
|
+
|
|
74
|
+
Resolves an existing canvas or creates a new one. Returns a {@link CanvasInstance} bound to `(canvasId, tenantId)`. Subsequent operations don't repeat them.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const instance = await ctx.core.canvas!.acquire(input.canvas_id, ctx);
|
|
78
|
+
// instance.canvasId — surface to the agent
|
|
79
|
+
// instance.isNew — true on first call
|
|
80
|
+
// instance.expiresAt — ISO 8601 after sliding extension
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `instance.registerTable(name, rows, options?)`
|
|
84
|
+
|
|
85
|
+
Register an in-memory or async-iterable rowset as a canvas table.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
await instance.registerTable('germplasm', rows);
|
|
89
|
+
|
|
90
|
+
// Explicit schema for AsyncIterable (required — sniffer can't peek).
|
|
91
|
+
await instance.registerTable('big_dataset', asyncRows, {
|
|
92
|
+
schema: [
|
|
93
|
+
{ name: 'id', type: 'BIGINT' },
|
|
94
|
+
{ name: 'label', type: 'VARCHAR', nullable: true },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Schema inference** when `schema` is omitted: sniffer materializes the first 100 rows, unions JS-side types per column, and maps to DuckDB types. Fall-backs to `VARCHAR` for ambiguous unions (string mixed with numerics). Numeric widening: `INTEGER + DOUBLE → DOUBLE`, `INTEGER + BIGINT → BIGINT`. Column ordering follows first-appearance.
|
|
100
|
+
|
|
101
|
+
### `instance.query(sql, options?)`
|
|
102
|
+
|
|
103
|
+
Run SQL across registered tables. Returns at most `rowLimit` rows (default 10 000). For full result sets, pass `registerAs` — the result is materialized as a new canvas table; the response carries a `preview` slice plus the table reference.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const result = await instance.query(`
|
|
107
|
+
SELECT germplasmName, COUNT(*) AS n
|
|
108
|
+
FROM germplasm GROUP BY germplasmName ORDER BY n DESC
|
|
109
|
+
`);
|
|
110
|
+
|
|
111
|
+
// Materialize a join result for follow-up queries.
|
|
112
|
+
const joined = await instance.query(`
|
|
113
|
+
SELECT g.germplasmName, o.value
|
|
114
|
+
FROM germplasm g JOIN observations o ON g.germplasmDbId = o.germplasmDbId
|
|
115
|
+
`, { registerAs: 'g_with_obs', preview: 10 });
|
|
116
|
+
// joined.tableName === 'g_with_obs'; joined.rows.length === 10; joined.rowCount === <full count>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`registerAs` rejects with `Conflict` if the target name already exists — drop it first.
|
|
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.
|
|
125
|
+
|
|
126
|
+
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
|
+
### `instance.export(tableName, target, options?)`
|
|
129
|
+
|
|
130
|
+
Export a canvas table. Path-based exports are sandboxed to `CANVAS_EXPORT_PATH` (default `./.canvas-exports`). Absolute paths and `..` traversal are rejected.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// Path target — written inside the sandbox.
|
|
134
|
+
await instance.export('g_with_obs', { format: 'parquet', path: 'observations.parquet' });
|
|
135
|
+
|
|
136
|
+
// Stream target — copied to a temp file in the sandbox, piped to the stream, unlinked.
|
|
137
|
+
await instance.export('g_with_obs', { format: 'csv', stream: writableStream });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `instance.describe(options?)` / `instance.drop(name)` / `instance.clear()`
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const tables = await instance.describe();
|
|
144
|
+
// [{ name: 'germplasm', rowCount: 200, columns: [...] }, ...]
|
|
145
|
+
|
|
146
|
+
await instance.drop('staging_table'); // false if missing
|
|
147
|
+
await instance.clear(); // returns count dropped
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Cancellation
|
|
151
|
+
|
|
152
|
+
`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.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Result row shape
|
|
157
|
+
|
|
158
|
+
Rows are returned via DuckDB's `getRowObjectsJson()` for JSON-safe serialization:
|
|
159
|
+
|
|
160
|
+
| DuckDB type | JS type returned |
|
|
161
|
+
|:------------|:-----------------|
|
|
162
|
+
| `VARCHAR`, `JSON` | `string` |
|
|
163
|
+
| `INTEGER`, `DOUBLE` | `number` |
|
|
164
|
+
| `BIGINT` | `string` (lossless for values outside JS Number range) |
|
|
165
|
+
| `BOOLEAN` | `boolean` |
|
|
166
|
+
| `DATE`, `TIMESTAMP` | `string` |
|
|
167
|
+
| `BLOB` | `string` (base64) |
|
|
168
|
+
| `NULL` | `null` |
|
|
169
|
+
|
|
170
|
+
If your tool surfaces row data via `structuredContent`, the JSON-safe shape flows through unchanged.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Configuration
|
|
175
|
+
|
|
176
|
+
| Env Var | `AppConfig` field | Default |
|
|
177
|
+
|:--------|:-----------------|:--------|
|
|
178
|
+
| `CANVAS_PROVIDER_TYPE` | `canvas.providerType` | `none` (also: `duckdb`) |
|
|
179
|
+
| `CANVAS_DEFAULT_MEMORY_LIMIT_MB` | `canvas.defaultMemoryLimitMb` | `1024` |
|
|
180
|
+
| `CANVAS_EXPORT_PATH` | `canvas.exportRootPath` | `./.canvas-exports` |
|
|
181
|
+
| `CANVAS_MAX_CANVASES_PER_TENANT` | `canvas.maxCanvasesPerTenant` | `100` |
|
|
182
|
+
| `CANVAS_TTL_MS` | `canvas.ttlMs` | `86_400_000` (24 h) |
|
|
183
|
+
| `CANVAS_ABSOLUTE_CAP_MS` | `canvas.absoluteCapMs` | `604_800_000` (7 d) |
|
|
184
|
+
| `CANVAS_SWEEPER_INTERVAL_MS` | `canvas.sweeperIntervalMs` | `60_000` |
|
|
185
|
+
| `CANVAS_DEFAULT_ROW_LIMIT` | `canvas.defaultRowLimit` | `10_000` |
|
|
186
|
+
| `CANVAS_SCHEMA_SNIFF_ROWS` | `canvas.schemaSniffRows` | `100` |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Consumer tool template
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
194
|
+
|
|
195
|
+
export const fetchAndStage = tool('fetch_and_stage_germplasm', {
|
|
196
|
+
description: 'Fetch germplasm matching a query and stage it on a DataCanvas for follow-up SQL.',
|
|
197
|
+
input: z.object({
|
|
198
|
+
query: z.string().describe('Search query'),
|
|
199
|
+
canvas_id: z
|
|
200
|
+
.string()
|
|
201
|
+
.optional()
|
|
202
|
+
.describe(
|
|
203
|
+
'Optional 10-char canvas ID returned from a prior call. Omit on first call to start a fresh canvas; the response will include a new canvas_id you can pass to subsequent calls or share with another agent.',
|
|
204
|
+
),
|
|
205
|
+
}),
|
|
206
|
+
output: z.object({
|
|
207
|
+
canvas_id: z.string().describe('Canvas ID — pass to subsequent tool calls'),
|
|
208
|
+
is_new_canvas: z.boolean().describe('True if a new canvas was created'),
|
|
209
|
+
table_name: z.string().describe('Canvas table where rows were registered'),
|
|
210
|
+
row_count: z.number().describe('Rows registered'),
|
|
211
|
+
expires_at: z.string().describe('ISO 8601 expiry after sliding 24h window'),
|
|
212
|
+
}),
|
|
213
|
+
async handler(input, ctx) {
|
|
214
|
+
const canvas = ctx.core.canvas;
|
|
215
|
+
if (!canvas) {
|
|
216
|
+
throw new Error('DataCanvas is not enabled. Set CANVAS_PROVIDER_TYPE=duckdb.');
|
|
217
|
+
}
|
|
218
|
+
const instance = await canvas.acquire(input.canvas_id, ctx);
|
|
219
|
+
const rows = await fetchGermplasm(input.query);
|
|
220
|
+
const tableInfo = await instance.registerTable('germplasm', rows);
|
|
221
|
+
return {
|
|
222
|
+
canvas_id: instance.canvasId,
|
|
223
|
+
is_new_canvas: instance.isNew,
|
|
224
|
+
table_name: tableInfo.tableName,
|
|
225
|
+
row_count: tableInfo.rowCount,
|
|
226
|
+
expires_at: instance.expiresAt,
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Trade-offs
|
|
235
|
+
|
|
236
|
+
- **DuckDB only in v1.** Polars/SQLite/DataFusion don't fit the "agent writes ad-hoc SQL across N registered tables" shape.
|
|
237
|
+
- **In-memory only.** Server restart drops all canvases. For public-data servers, restart is rare and re-fetching upstream data is cheap. Disk persistence is a v2 concern.
|
|
238
|
+
- **Single process.** Tokens issued by one process are not portable to another. Multi-process distributed canvases are out of scope.
|
|
239
|
+
- **Read-only relative to upstream.** Canvas mutations (register, drop, clear, query+registerAs) all stay behind typed methods. Arbitrary SQL cannot mutate.
|
|
240
|
+
- **No OTel in v1.** Canvas operations are not instrumented at the framework level. Add manually via `ctx.log` if needed.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Platform support
|
|
245
|
+
|
|
246
|
+
| Platform | Status |
|
|
247
|
+
|:---------|:-------|
|
|
248
|
+
| Linux x64 / arm64 | Supported |
|
|
249
|
+
| macOS x64 / arm64 | Supported |
|
|
250
|
+
| Windows x64 | Supported |
|
|
251
|
+
| Windows arm64 | **Not supported** (DuckDB upstream limitation) |
|
|
252
|
+
| Cloudflare Workers | **Not supported** — fail-closed at init time |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Related skills
|
|
257
|
+
|
|
258
|
+
- `add-tool` — scaffold a new MCP tool definition (use the canvas template above)
|
|
259
|
+
- `api-config` — full env var reference
|
|
260
|
+
- `api-workers` — Worker fail-closed behavior
|
|
@@ -99,6 +99,24 @@ Activated when `OAUTH_PROXY_AUTHORIZATION_URL` or `OAUTH_PROXY_TOKEN_URL` is set
|
|
|
99
99
|
| `STORAGE_PROVIDER_TYPE` | `storage.providerType` | `in-memory` | `in-memory` \| `filesystem` \| `supabase` \| `cloudflare-r2` \| `cloudflare-kv` \| `cloudflare-d1`; aliases: `mem`, `fs` |
|
|
100
100
|
| `STORAGE_FILESYSTEM_PATH` | `storage.filesystemPath` | `./.storage` | Used only when `providerType` is `filesystem` |
|
|
101
101
|
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### Canvas (DataCanvas primitive — Tier 3, optional peer dep `@duckdb/node-api`)
|
|
105
|
+
|
|
106
|
+
| Env Var | `AppConfig` field | Default | Notes |
|
|
107
|
+
|:--------|:-----------------|:--------|:------|
|
|
108
|
+
| `CANVAS_PROVIDER_TYPE` | `canvas.providerType` | `none` | `none` \| `duckdb`. Set to `duckdb` to enable `core.canvas`. Fails closed on Cloudflare Workers (DuckDB has no V8-isolate build). |
|
|
109
|
+
| `CANVAS_DEFAULT_MEMORY_LIMIT_MB` | `canvas.defaultMemoryLimitMb` | `1024` | Per-canvas DuckDB `memory_limit` PRAGMA value, in MB. |
|
|
110
|
+
| `CANVAS_EXPORT_PATH` | `canvas.exportRootPath` | `./.canvas-exports` | Sandbox root for path-targeted exports. Absolute paths and `..` traversal are rejected. |
|
|
111
|
+
| `CANVAS_MAX_CANVASES_PER_TENANT` | `canvas.maxCanvasesPerTenant` | `100` | Active canvas cap per tenant; throws `RateLimited` when exceeded. |
|
|
112
|
+
| `CANVAS_TTL_MS` | `canvas.ttlMs` | `86400000` | Sliding TTL (24 h). Every operation extends the expiry. |
|
|
113
|
+
| `CANVAS_ABSOLUTE_CAP_MS` | `canvas.absoluteCapMs` | `604800000` | Absolute cap from creation (7 d). Sliding window clamps to this. |
|
|
114
|
+
| `CANVAS_SWEEPER_INTERVAL_MS` | `canvas.sweeperIntervalMs` | `60000` | Background sweep interval. Set to `0` to disable. |
|
|
115
|
+
| `CANVAS_DEFAULT_ROW_LIMIT` | `canvas.defaultRowLimit` | `10000` | Default cap on rows materialized into a query response. |
|
|
116
|
+
| `CANVAS_SCHEMA_SNIFF_ROWS` | `canvas.schemaSniffRows` | `100` | Rows to materialize for schema inference when `schema` is omitted. |
|
|
117
|
+
|
|
118
|
+
**Platform support:** Linux/macOS/Windows × x64 supported, Linux/macOS arm64 supported. Windows arm64 unsupported (DuckDB upstream). See `api-canvas` skill for the full DataCanvas reference.
|
|
119
|
+
|
|
102
120
|
#### Supabase (optional sub-object)
|
|
103
121
|
|
|
104
122
|
Activated when both `SUPABASE_URL` and `SUPABASE_ANON_KEY` are set.
|
|
@@ -171,3 +171,9 @@ export function getServerConfig() {
|
|
|
171
171
|
**`in-memory` storage is volatile.** Data stored with the `in-memory` provider is lost between cold starts and is not shared across Worker instances. Use `cloudflare-kv`, `cloudflare-r2`, or `cloudflare-d1` for any state that must persist or be shared.
|
|
172
172
|
|
|
173
173
|
**Node-only utilities throw in Workers.** `scheduler` (`node-cron`), `sanitizePath` (fs-based), and `filesystem` storage provider all throw `ConfigurationError` when called from a Worker. Guard with `runtimeCaps.isNode` or avoid entirely.
|
|
174
|
+
|
|
175
|
+
**DataCanvas is unavailable in Workers.** DuckDB has no V8-isolate build, so `core.canvas` is always `undefined` on Workers. Setting `CANVAS_PROVIDER_TYPE=duckdb` (the only non-default value) in `wrangler.toml` triggers a fail-closed `ConfigurationError` at init time:
|
|
176
|
+
|
|
177
|
+
> `DuckDB canvas requires Node.js or Bun. Set CANVAS_PROVIDER_TYPE=none or omit it for Cloudflare Workers deployment.`
|
|
178
|
+
|
|
179
|
+
Leave the env unset (or set to `none`) for Worker deployments. Tools that conditionally use canvas should check `if (!ctx.core.canvas) { ... }` and surface a clear "feature unavailable on this deployment" message. See `api-canvas` for the full DataCanvas reference.
|
|
@@ -408,6 +408,8 @@ Skip for purely data/action-oriented servers.
|
|
|
408
408
|
|
|
409
409
|
**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.
|
|
410
410
|
|
|
411
|
+
**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.
|
|
412
|
+
|
|
411
413
|
For services wrapping external APIs, plan the resilience layer.
|
|
412
414
|
|
|
413
415
|
| Concern | Decision |
|
|
@@ -551,4 +553,5 @@ Items without an `If …:` prefix apply to every design. Conditional items only
|
|
|
551
553
|
- [ ] **If the server exposes resources:** URIs use `{param}` templates, pagination planned for large lists
|
|
552
554
|
- [ ] **If the server has external deps or shared state:** service layer planned (or explicitly skipped with reasoning)
|
|
553
555
|
- [ ] **If services wrap external APIs:** resilience planned (retry boundary, backoff, parse classification)
|
|
556
|
+
- [ ] **If exposing a SQL/analytical workspace over tabular data is in scope:** DataCanvas considered (`api-canvas` skill) as one option before designing custom analytical state — register / query / export tools accepting an optional `canvas_id`, with `ctx.core.canvas?` reads
|
|
554
557
|
- [ ] **If the server needs runtime config:** env vars identified in `server-config.ts`
|
|
@@ -172,6 +172,7 @@ Every issue needs exactly one primary label. Stack secondary labels on top when
|
|
|
172
172
|
| `performance` | Memory, CPU, latency, or resource usage |
|
|
173
173
|
| `security` | Vulnerability, CVE, or hardening work |
|
|
174
174
|
| `breaking-change` | Fix/feature will break public API; requires a major bump |
|
|
175
|
+
| `surplus-token-idea` | Worth exploring when token budget allows |
|
|
175
176
|
|
|
176
177
|
Combine labels: `--label "bug" --label "regression"`.
|
|
177
178
|
|
|
@@ -158,6 +158,7 @@ Every issue needs exactly one primary label. Stack secondary labels on top when
|
|
|
158
158
|
| `performance` | Memory, CPU, latency, or resource usage |
|
|
159
159
|
| `security` | Vulnerability, CVE, or hardening work |
|
|
160
160
|
| `breaking-change` | Change will break public API; requires a major bump |
|
|
161
|
+
| `surplus-token-idea` | Worth exploring when token budget allows |
|
|
161
162
|
|
|
162
163
|
Combine labels: `--label "bug" --label "regression"`.
|
|
163
164
|
|
|
@@ -168,6 +169,7 @@ gh label create regression --color e99695 --description "Worked before, broken a
|
|
|
168
169
|
gh label create performance --color 5319e7 --description "Memory, CPU, latency, or resource usage"
|
|
169
170
|
gh label create security --color b60205 --description "Vulnerability, CVE, or hardening work"
|
|
170
171
|
gh label create breaking-change --color d93f0b --description "Change will break public API; requires a major bump"
|
|
172
|
+
gh label create surplus-token-idea --color FF10F0 --description "Worth exploring when token budget allows"
|
|
171
173
|
```
|
|
172
174
|
|
|
173
175
|
### Attaching logs or large output
|
|
@@ -54,6 +54,12 @@ Note: tool / resource / prompt counts, auth mode, storage provider, upstream API
|
|
|
54
54
|
- Any unauthenticated routes (`/healthz`, `/sse`, metadata endpoints) — do they leak tool lists or tenant hints?
|
|
55
55
|
- MCP Authorization spec: if implemented, PKCE enforced, token audience (`aud`) checked, resource indicators used
|
|
56
56
|
|
|
57
|
+
**If `CANVAS_PROVIDER_TYPE=duckdb` is set**, also capture:
|
|
58
|
+
|
|
59
|
+
- Auth mode — canvas in `MCP_AUTH_MODE=none` collapses the composite `(tenantId, canvasId)` scope to `('default', canvasId)`, where the ID is the only differentiator
|
|
60
|
+
- `CANVAS_MAX_CANVASES_PER_TENANT`, `CANVAS_TTL_MS`, `CANVAS_ABSOLUTE_CAP_MS`, `CANVAS_EXPORT_PATH` values
|
|
61
|
+
- Whether external rate limiting (CDN, reverse proxy, WAF) fronts the deployment — required to keep the ~10¹⁸ canvasId keyspace operationally infeasible to brute-force
|
|
62
|
+
|
|
57
63
|
Use `TaskCreate` — one task per axis. Mark complete as you go.
|
|
58
64
|
|
|
59
65
|
**Run `fuzzTool` in parallel.** `@cyanheads/mcp-ts-core/testing/fuzz` catches crashes, memory leaks, and prototype pollution automatically on each tool — start it now so results are ready when you reach Axis 5.
|
|
@@ -249,6 +255,23 @@ grep -rn "JSON.parse\b" src/
|
|
|
249
255
|
|
|
250
256
|
**Smell:** `while (cursor) { results.push(...); cursor = next; }` with no max count. Or: `JSON.parse(await req.text())` with no `Content-Length` check upstream.
|
|
251
257
|
|
|
258
|
+
#### Axis 9 — Canvas (only if `CANVAS_PROVIDER_TYPE=duckdb`)
|
|
259
|
+
|
|
260
|
+
DataCanvas is opt-in and deliberately trades isolation for cross-agent token-shareable working sets — designed for public-data tabular servers (BrAPI, OpenAlex, etc.) where session-pinning isn't desired. The trade only holds when the deployment matches that assumption. Skip this axis entirely when canvas is disabled (`CANVAS_PROVIDER_TYPE=none`, the default).
|
|
261
|
+
|
|
262
|
+
**Look in:** `src/config/server-config.ts`, every tool reading `ctx.core.canvas?`, deployment config (wrangler / Dockerfile / proxy).
|
|
263
|
+
|
|
264
|
+
**Check:**
|
|
265
|
+
|
|
266
|
+
- Data registered on canvases is **already public** or already-shared-out-of-band. The composite `(tenantId, canvasId)` scope collapses to `('default', canvasId)` in `MCP_AUTH_MODE=none` — anyone with the `canvasId` attaches.
|
|
267
|
+
- External rate limiting (CDN, reverse proxy, WAF) fronts the deployment so the ~10¹⁸ keyspace can't be brute-forced. Without it, the entropy assumption breaks and discovery becomes feasible.
|
|
268
|
+
- `CANVAS_MAX_CANVASES_PER_TENANT` sized for the memory budget — default 100 is the floor; raising it lets a single tenant exhaust memory faster.
|
|
269
|
+
- `CANVAS_TTL_MS` / `CANVAS_ABSOLUTE_CAP_MS` not absurdly long. Defaults (24 h sliding / 7 d absolute) are reasonable; longer widens the window an unreferenced `canvasId` stays guessable.
|
|
270
|
+
- `CANVAS_EXPORT_PATH` doesn't point into a shared mount, the repo, or a directory another service serves from. The path-sandbox blocks `..` traversal but doesn't prevent the configured root from being a bad choice.
|
|
271
|
+
- Axis 1 (description templating from canvas-supplied content), Axis 5 (no parallel service runs raw SQL outside the canvas API and bypasses `assertReadOnlyQuery`), and Axis 7 (errors from canvas operations don't leak the failed SQL string back through `McpError.data`) all apply.
|
|
272
|
+
|
|
273
|
+
**Smell:** `MCP_AUTH_MODE=none` deployment registering per-user data (recent activity, account state, cart contents) onto a canvas. Or: `CANVAS_EXPORT_PATH=/srv/static` with a static file server pointing at the same root.
|
|
274
|
+
|
|
252
275
|
### 3. Quick sanity pass
|
|
253
276
|
|
|
254
277
|
Fast, sometimes high-leverage. Outside the eight axes.
|
|
@@ -320,5 +343,6 @@ End with:
|
|
|
320
343
|
- [ ] Axis 6 — tenant isolation: module-scope state swept
|
|
321
344
|
- [ ] Axis 7 — leakage back: errors / outputs / `ctx.log` / `console.*` / telemetry / constant-time comparisons
|
|
322
345
|
- [ ] Axis 8 — resource bounds on loops / retries / pagination / parse size+depth / per-tenant rate
|
|
346
|
+
- [ ] **If `CANVAS_PROVIDER_TYPE=duckdb`:** Axis 9 — public-data assumption holds, external rate limiting in place, max-canvases-per-tenant + TTLs sized for the deployment, `CANVAS_EXPORT_PATH` doesn't escape into shared / served paths, `assertReadOnlyQuery` is the only SQL path
|
|
323
347
|
- [ ] Quick sanity pass: `bun audit`, lifecycle scripts, `.env.example`, config validation, new-dep provenance
|
|
324
348
|
- [ ] Report: summary → grouped findings → numbered options
|
|
@@ -13,6 +13,7 @@ body:
|
|
|
13
13
|
- `performance` — memory, CPU, latency, or resource usage
|
|
14
14
|
- `security` — vulnerability, CVE, or hardening work
|
|
15
15
|
- `breaking-change` — fix will break public API
|
|
16
|
+
- `surplus-token-idea` — worth exploring when token budget allows
|
|
16
17
|
|
|
17
18
|
If a label doesn't exist in this repo yet, create it once: `gh label create <name>`.
|
|
18
19
|
|
|
@@ -10,6 +10,7 @@ body:
|
|
|
10
10
|
- `performance` — improves memory, CPU, latency, or resource usage
|
|
11
11
|
- `security` — hardens the security posture
|
|
12
12
|
- `breaking-change` — will break public API; requires a major bump
|
|
13
|
+
- `surplus-token-idea` — worth exploring when token budget allows
|
|
13
14
|
|
|
14
15
|
If a label doesn't exist in this repo yet, create it once: `gh label create <name>`.
|
|
15
16
|
|