@cyanheads/mcp-ts-core 0.9.12 → 0.9.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.
- package/CLAUDE.md +6 -3
- package/README.md +30 -41
- package/changelog/0.9.x/0.9.13.md +33 -0
- package/changelog/0.9.x/0.9.14.md +31 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +20 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/context.d.ts +103 -14
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +66 -1
- package/dist/core/context.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/serverManifest.d.ts +12 -1
- package/dist/core/serverManifest.d.ts.map +1 -1
- package/dist/core/serverManifest.js +4 -1
- package/dist/core/serverManifest.js.map +1 -1
- package/dist/linter/rules/enrichment-rules.d.ts +32 -0
- package/dist/linter/rules/enrichment-rules.d.ts.map +1 -0
- package/dist/linter/rules/enrichment-rules.js +116 -0
- package/dist/linter/rules/enrichment-rules.js.map +1 -0
- package/dist/linter/rules/index.d.ts +1 -0
- package/dist/linter/rules/index.d.ts.map +1 -1
- package/dist/linter/rules/index.js +1 -0
- package/dist/linter/rules/index.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +4 -0
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-registration.js +7 -7
- package/dist/mcp-server/tools/tool-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolDefinition.d.ts +41 -7
- package/dist/mcp-server/tools/utils/toolDefinition.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolDefinition.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +23 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +90 -9
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.js +40 -0
- package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
- package/dist/testing/index.d.ts +13 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +21 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/utils/internal/performance.d.ts +5 -1
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +10 -1
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +2 -0
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +2 -0
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/package.json +2 -2
- package/scripts/check-docs-sync.ts +50 -32
- package/skills/add-tool/SKILL.md +57 -32
- package/skills/api-canvas/SKILL.md +106 -1
- package/skills/api-config/SKILL.md +2 -1
- package/skills/api-context/SKILL.md +65 -2
- package/skills/api-linter/SKILL.md +48 -1
- package/skills/design-mcp-server/SKILL.md +2 -1
- package/skills/orchestrations/SKILL.md +9 -5
- package/skills/polish-docs-meta/SKILL.md +1 -2
- package/skills/polish-docs-meta/references/readme.md +15 -1
- package/skills/report-issue-framework/SKILL.md +8 -3
- package/skills/report-issue-local/SKILL.md +8 -3
- package/templates/.env.example +1 -0
- package/templates/AGENTS.md +8 -35
- package/templates/CLAUDE.md +8 -36
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +10 -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.
|
|
7
|
+
version: "1.3"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -245,8 +245,113 @@ If your tool surfaces row data via `structuredContent`, the JSON-safe shape flow
|
|
|
245
245
|
|
|
246
246
|
---
|
|
247
247
|
|
|
248
|
+
## Minimum viable spillover server
|
|
249
|
+
|
|
250
|
+
Most canvas use cases are public-data analytics: fetch from an upstream API, stage the full result, let the agent SQL it. The primitives are domain-neutral — `canvas.acquire()`, `spillover()`, `instance.query()` — so the minimum viable shape is small and generic. Reach for it first; add scoping only when a real multi-tenant requirement appears.
|
|
251
|
+
|
|
252
|
+
### Simple-shape defaults
|
|
253
|
+
|
|
254
|
+
| Concern | Simple-shape answer |
|
|
255
|
+
|:--|:--|
|
|
256
|
+
| Canvas scoping | One shared canvas per tenant. Omit `canvas_id` on the first call to mint one; pass the returned id back to reuse it. |
|
|
257
|
+
| 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
|
+
| 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
|
+
| 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. |
|
|
261
|
+
| 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
|
+
|
|
263
|
+
> 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.
|
|
264
|
+
|
|
265
|
+
### Recipe
|
|
266
|
+
|
|
267
|
+
A fetcher that spills and a query tool that runs SQL across what was spilled — the whole surface. Swap `fetchUpstream` for any paginated or streamed source; nothing here is domain-specific.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
271
|
+
import { spillover } from '@cyanheads/mcp-ts-core/canvas';
|
|
272
|
+
import { getCanvas } from '@/services/canvas-accessor.js';
|
|
273
|
+
|
|
274
|
+
/** Fetch an upstream dataset, inline a preview, spill the full result to a canvas table. */
|
|
275
|
+
export const fetchDataset = tool('fetch_dataset', {
|
|
276
|
+
description:
|
|
277
|
+
'Fetch a dataset and stage it on a DataCanvas. Returns an inline preview plus a ' +
|
|
278
|
+
'canvas_id + table you can query with dataframe_query for the full result set.',
|
|
279
|
+
annotations: { readOnlyHint: true },
|
|
280
|
+
input: z.object({
|
|
281
|
+
query: z.string().describe('Upstream search/filter expression'),
|
|
282
|
+
canvas_id: z
|
|
283
|
+
.string()
|
|
284
|
+
.optional()
|
|
285
|
+
.describe('Canvas ID from a prior call. Omit to start fresh — the response returns a new one.'),
|
|
286
|
+
}),
|
|
287
|
+
output: z.object({
|
|
288
|
+
canvas_id: z.string().describe('Canvas ID — pass to dataframe_query or another fetch call'),
|
|
289
|
+
table_name: z.string().describe('Canvas table holding the full result (empty when not spilled)'),
|
|
290
|
+
spilled: z.boolean().describe('True when the result exceeded the preview and was staged'),
|
|
291
|
+
preview: z.array(z.record(z.string(), z.unknown())).describe('Inline rows — the immediate answer'),
|
|
292
|
+
row_count: z.number().describe('Rows staged on the canvas (preview length when not spilled)'),
|
|
293
|
+
}),
|
|
294
|
+
async handler(input, ctx) {
|
|
295
|
+
const canvas = getCanvas();
|
|
296
|
+
if (!canvas) throw new Error('DataCanvas is not enabled. Set CANVAS_PROVIDER_TYPE=duckdb.');
|
|
297
|
+
|
|
298
|
+
const instance = await canvas.acquire(input.canvas_id, ctx);
|
|
299
|
+
const result = await spillover({
|
|
300
|
+
canvas: instance,
|
|
301
|
+
source: fetchUpstream(input.query), // any AsyncIterable<Row> | Iterable<Row>
|
|
302
|
+
previewChars: 100_000, // ≈ 25k tokens inline
|
|
303
|
+
signal: ctx.signal,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
canvas_id: instance.canvasId,
|
|
308
|
+
table_name: result.spilled ? result.handle.tableName : '',
|
|
309
|
+
spilled: result.spilled,
|
|
310
|
+
preview: result.previewRows,
|
|
311
|
+
row_count: result.spilled ? result.handle.rowCount : result.previewRows.length,
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
/** Run read-only SQL across tables staged on a canvas. */
|
|
317
|
+
export const dataframeQuery = tool('dataframe_query', {
|
|
318
|
+
description: 'Run a read-only SQL SELECT against tables staged on a canvas by fetch_dataset.',
|
|
319
|
+
annotations: { readOnlyHint: true },
|
|
320
|
+
input: z.object({
|
|
321
|
+
canvas_id: z.string().describe('Canvas ID returned by fetch_dataset'),
|
|
322
|
+
sql: z.string().describe('Read-only SELECT. Reference tables by the names fetch_dataset returned.'),
|
|
323
|
+
}),
|
|
324
|
+
output: z.object({
|
|
325
|
+
rows: z.array(z.record(z.string(), z.unknown())).describe('Result rows (capped at the canvas row limit)'),
|
|
326
|
+
row_count: z.number().describe('Full result count before the row cap'),
|
|
327
|
+
}),
|
|
328
|
+
async handler(input, ctx) {
|
|
329
|
+
const canvas = getCanvas();
|
|
330
|
+
if (!canvas) throw new Error('DataCanvas is not enabled. Set CANVAS_PROVIDER_TYPE=duckdb.');
|
|
331
|
+
|
|
332
|
+
const instance = await canvas.acquire(input.canvas_id, ctx);
|
|
333
|
+
const result = await instance.query(input.sql, { signal: ctx.signal });
|
|
334
|
+
return { rows: result.rows, row_count: result.rowCount };
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### When the simple shape is enough
|
|
340
|
+
|
|
341
|
+
| Condition | Simple shape suffices? |
|
|
342
|
+
|:--|:--|
|
|
343
|
+
| Underlying data is publicly accessible | ✅ |
|
|
344
|
+
| Single-user deployment (stdio, or HTTP with one user) | ✅ — no cross-user surface regardless of data sensitivity |
|
|
345
|
+
| Use case is research / analytics, not multi-tenant SaaS | ✅ |
|
|
346
|
+
| Dataframes must age individually | ⚠️ TTL is canvas-level today (a hot canvas keeps stale tables alive); per-table TTL is tracked in [#140](https://github.com/cyanheads/mcp-ts-core/issues/140). Backstop with `ctx.state` bookkeeping in the interim. |
|
|
347
|
+
| Per-user row visibility matters in a multi-user deployment | ❌ — add session/tenant scoping at the server level |
|
|
348
|
+
|
|
349
|
+
The germplasm-flavored [consumer tool template](#consumer-tool-template) below is the same pattern with domain-specific naming.
|
|
350
|
+
|
|
248
351
|
## Consumer tool template
|
|
249
352
|
|
|
353
|
+
A domain-specific instance of the [minimum viable spillover server](#minimum-viable-spillover-server) above — the same `acquire → register → return handle` flow with germplasm naming.
|
|
354
|
+
|
|
250
355
|
```ts
|
|
251
356
|
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
252
357
|
import { getCanvas } from '@/services/canvas-accessor.js';
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Reference for core and server configuration in `@cyanheads/mcp-ts-core`. Covers env var tables with defaults, priority order, server-specific Zod schema pattern, and Workers lazy-parsing requirement.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.5"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -52,6 +52,7 @@ Managed by `@cyanheads/mcp-ts-core`. Validated via Zod from environment variable
|
|
|
52
52
|
| `MCP_HTTP_PORT` | `mcpHttpPort` | `3010` | Port for HTTP transport |
|
|
53
53
|
| `MCP_HTTP_HOST` | `mcpHttpHost` | `127.0.0.1` | Bind address |
|
|
54
54
|
| `MCP_HTTP_ENDPOINT_PATH` | `mcpHttpEndpointPath` | `/mcp` | HTTP endpoint path |
|
|
55
|
+
| `MCP_HTTP_MAX_BODY_BYTES` | `mcpHttpMaxBodyBytes` | `1048576` (1 MiB) | Max **inbound** JSON-RPC request body; oversized requests get `413` before per-request allocation. Does **not** cap upstream data staged into a canvas or response sizes. `0` disables (defer to runtime/proxy). |
|
|
55
56
|
| `MCP_HTTP_MAX_PORT_RETRIES` | `mcpHttpMaxPortRetries` | `15` | Retry count if port is busy |
|
|
56
57
|
| `MCP_HTTP_PORT_RETRY_DELAY_MS` | `mcpHttpPortRetryDelayMs` | `50` | Delay between port retries (ms) |
|
|
57
58
|
| `MCP_SESSION_MODE` | `mcpSessionMode` | `auto` | `stateless` \| `stateful` \| `auto` |
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: api-context
|
|
3
3
|
description: >
|
|
4
|
-
Canonical reference for the unified `Context` object passed to every tool and resource handler in `@cyanheads/mcp-ts-core`. Covers the full interface, all sub-APIs (`ctx.log`, `ctx.state`, `ctx.elicit`, `ctx.sample`, `ctx.progress`), and when to use each.
|
|
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.
|
|
7
|
+
version: "1.4"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -57,6 +57,12 @@ interface Context {
|
|
|
57
57
|
// Raw URI — present only for resource handlers
|
|
58
58
|
readonly uri?: URL;
|
|
59
59
|
|
|
60
|
+
// Agent-facing success-path enrichment — accumulates notices, query echo, totals
|
|
61
|
+
// onto the request; reaches structuredContent + content[]. Always present (no-op
|
|
62
|
+
// when no `enrichment` block), strictly typed on HandlerContext<R, E> against the
|
|
63
|
+
// declared fields. Kind-tagged helpers: enrich.notice / .total / .echo.
|
|
64
|
+
readonly enrich: Enrich;
|
|
65
|
+
|
|
60
66
|
// Opt-in contract resolver — always present (returns {} when no contract is attached
|
|
61
67
|
// or the reason is unknown), strictly typed on HandlerContext<R> against declared reasons.
|
|
62
68
|
recoveryFor(reason: string): { recovery: { hint: string } } | {};
|
|
@@ -520,6 +526,62 @@ The `≥5 words` lint rule on contract `recovery` (validated at lint time) makes
|
|
|
520
526
|
|
|
521
527
|
---
|
|
522
528
|
|
|
529
|
+
## `ctx.enrich`
|
|
530
|
+
|
|
531
|
+
Always present on `Context`. Accumulates agent-facing **success-path** context — empty-result notices, the query/filter as the server parsed it, pagination totals — onto the request. The framework merges it into `structuredContent`, advertises `output.extend(enrichment)` as the tool's `outputSchema`, and mirrors it into a `content[]` trailer. The success-path counterpart to `ctx.fail` / `ctx.recoveryFor`.
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
export const search = tool('search', {
|
|
535
|
+
description: 'Search the catalog.',
|
|
536
|
+
input: z.object({ query: z.string().describe('Search terms') }),
|
|
537
|
+
output: z.object({ items: z.array(z.string()).describe('Matching items') }),
|
|
538
|
+
enrichment: {
|
|
539
|
+
effectiveQuery: z.string().describe('Query as the server parsed it'),
|
|
540
|
+
totalCount: z.number().describe('Total matches before the limit'),
|
|
541
|
+
notice: z.string().optional().describe('Guidance when nothing matched'),
|
|
542
|
+
},
|
|
543
|
+
async handler(input, ctx) {
|
|
544
|
+
const res = await runSearch(input.query);
|
|
545
|
+
ctx.enrich.echo(res.parsed); // → effectiveQuery + "Query: …" trailer
|
|
546
|
+
ctx.enrich.total(res.total); // → totalCount + "N total" trailer
|
|
547
|
+
if (res.items.length === 0) ctx.enrich.notice(`No matches for "${input.query}".`);
|
|
548
|
+
return { items: res.items }; // enrichment never rides in the domain return
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Signature
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
// Loose (always present on Context — works without a block; service-callable):
|
|
557
|
+
ctx.enrich(fields: Record<string, unknown>): void
|
|
558
|
+
|
|
559
|
+
// Strict (HandlerContext<R, E> when the definition declares an enrichment block):
|
|
560
|
+
ctx.enrich(fields: Partial<z.infer<ZodObject<E>>>): void
|
|
561
|
+
|
|
562
|
+
// Kind-tagged field-helpers (always present) — write a conventional key and tag
|
|
563
|
+
// the content[] trailer rendering:
|
|
564
|
+
ctx.enrich.notice(text: string): void // writes `notice` → blockquote
|
|
565
|
+
ctx.enrich.total(count: number): void // writes `totalCount` → "N total"
|
|
566
|
+
ctx.enrich.echo(query: string): void // writes `effectiveQuery` → "Query: …"
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Behavior
|
|
570
|
+
|
|
571
|
+
| Aspect | Detail |
|
|
572
|
+
|:-------|:-------|
|
|
573
|
+
| Accumulation | Each call merges its fields onto the request; later calls override earlier keys. |
|
|
574
|
+
| Both surfaces | Merged into `structuredContent` (validated against `output.extend(enrichment)`) and appended to `content[]` as a trailer — even when the tool defines no `format()`. |
|
|
575
|
+
| Domain payload untouched | `content[]` renders the handler's return via `format()` (or the JSON default); enrichment is a separate trailer, never double-rendered. The handler return must NOT carry enrichment fields. |
|
|
576
|
+
| Required-field guard | A required enrichment field never populated fails the effective-output parse — the bug surfaces loudly rather than dropping silently. |
|
|
577
|
+
| No block | Calling `ctx.enrich` on a tool that declared no `enrichment` is a silent no-op (values are stripped by the parse) — the price of service-layer callability. |
|
|
578
|
+
| Service usage | Services accepting `ctx: Context` can call `ctx.enrich(...)`; the value reaches `structuredContent` exactly as if the handler had. |
|
|
579
|
+
| `format-parity` | Enrichment lives outside `output`, so the `format-parity` lint never requires it in `format()`. |
|
|
580
|
+
|
|
581
|
+
See `add-tool`'s **Tool Response Design** and `skills/api-linter` (`enrichment-*` rules) for the full pattern. Test enrichment with `getEnrichment(ctx)` from `@cyanheads/mcp-ts-core/testing`.
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
523
585
|
## Quick reference
|
|
524
586
|
|
|
525
587
|
| Property | Type | Present when |
|
|
@@ -534,6 +596,7 @@ The `≥5 words` lint rule on contract `recovery` (validated at lint time) makes
|
|
|
534
596
|
| `ctx.log` | `ContextLogger` | Always |
|
|
535
597
|
| `ctx.state` | `ContextState` | Always (throws if `tenantId` missing) |
|
|
536
598
|
| `ctx.signal` | `AbortSignal` | Always |
|
|
599
|
+
| `ctx.enrich` | `Enrich` | Always; typed on `HandlerContext<R, E>` when an `enrichment` block is declared |
|
|
537
600
|
| `ctx.elicit` | `function \| undefined` | Client supports elicitation |
|
|
538
601
|
| `ctx.sample` | `function \| undefined` | Client supports sampling |
|
|
539
602
|
| `ctx.notifyResourceListChanged` | `function \| undefined` | Transport supports resource notifications |
|
|
@@ -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.
|
|
7
|
+
version: "1.4"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -53,6 +53,7 @@ Grouped by family. Jump to any rule ID via its anchor.
|
|
|
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 rules](#enrichment-rules) |
|
|
56
57
|
| server.json | ~40 rules prefixed `server-json-*` | [server.json rules](#server-json-rules) |
|
|
57
58
|
|
|
58
59
|
---
|
|
@@ -708,6 +709,52 @@ The diagnostic message includes the declared reason(s) for the code so you can c
|
|
|
708
709
|
|
|
709
710
|
---
|
|
710
711
|
|
|
712
|
+
## Enrichment rules
|
|
713
|
+
|
|
714
|
+
Validate the `enrichment` block — the success-path counterpart to `errors[]`. Enrichment fields are merged into `structuredContent` and advertised as `output.extend(enrichment)`, so the linter guards the block's shape and its disjointness from `output`. See `api-context`'s `ctx.enrich` and `add-tool`'s **Tool Response Design**.
|
|
715
|
+
|
|
716
|
+
### enrichment-type
|
|
717
|
+
|
|
718
|
+
**Severity:** error
|
|
719
|
+
|
|
720
|
+
Fires when `enrichment` is present but isn't a plain object mapping field names to Zod schemas (a `ZodRawShape`) — e.g. an array or a primitive.
|
|
721
|
+
|
|
722
|
+
**Fix:** declare `enrichment: { <name>: <ZodType>, … }`.
|
|
723
|
+
|
|
724
|
+
### enrichment-empty
|
|
725
|
+
|
|
726
|
+
**Severity:** warning
|
|
727
|
+
|
|
728
|
+
Fires when `enrichment: {}` is declared with no fields — a no-op.
|
|
729
|
+
|
|
730
|
+
**Fix:** drop the field, or declare the agent-facing fields `ctx.enrich(...)` will populate.
|
|
731
|
+
|
|
732
|
+
### enrichment-field-type
|
|
733
|
+
|
|
734
|
+
**Severity:** error
|
|
735
|
+
|
|
736
|
+
Fires when an enrichment field's value isn't a Zod schema.
|
|
737
|
+
|
|
738
|
+
**Fix:** use a Zod type (`z.string().describe(…)`, `z.number().describe(…)`, …) for every enrichment field.
|
|
739
|
+
|
|
740
|
+
### enrichment-output-collision
|
|
741
|
+
|
|
742
|
+
**Severity:** error
|
|
743
|
+
|
|
744
|
+
Fires when an enrichment key matches an `output` key. The effective output schema is `output.extend(enrichment)`, so a collision silently overrides the `output` field.
|
|
745
|
+
|
|
746
|
+
**Fix:** rename one side so enrichment keys are disjoint from output keys.
|
|
747
|
+
|
|
748
|
+
### enrichment-prefer-block
|
|
749
|
+
|
|
750
|
+
**Severity:** warning
|
|
751
|
+
|
|
752
|
+
Advisory. Fires when a tool has **no** `enrichment` block but an `output` field whose name strongly signals agent-facing context (`notice`, `effectiveQuery`, `queryEcho`) rather than domain payload.
|
|
753
|
+
|
|
754
|
+
**Fix:** move the field into an `enrichment` block and populate it via `ctx.enrich(...)` — it reaches both client surfaces without a `format()` entry. Ignore if the field is genuinely domain data. Deliberately conservative — common domain fields like `totalCount` are not flagged.
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
711
758
|
## Escape hatches
|
|
712
759
|
|
|
713
760
|
### Dynamic upstream data
|
|
@@ -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.
|
|
7
|
+
version: "2.13"
|
|
8
8
|
audience: external
|
|
9
9
|
type: workflow
|
|
10
10
|
---
|
|
@@ -351,6 +351,7 @@ output: z.object({
|
|
|
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
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.
|
|
353
353
|
- **`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.
|
|
354
|
+
- **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. See `add-tool`'s **Tool Response Design**.
|
|
354
355
|
|
|
355
356
|
#### Batch input design
|
|
356
357
|
|
|
@@ -69,7 +69,7 @@ The orchestrator owns the goals. Workflow phases are not "run skill X" — they
|
|
|
69
69
|
Before running a phase (or spawning a sub-agent for it), write down four things:
|
|
70
70
|
|
|
71
71
|
1. **Goal** — the verifiable end state this phase must produce. Concrete and testable: "v0.5.2 tag exists at HEAD with structured-markdown annotation; `bun run devcheck` green; `npm view <pkg>@0.5.2` resolves." Not fuzzy: "ran the release-and-publish skill."
|
|
72
|
-
2. **Primary sources** — the specific files, GH issues, and reference docs the sub-agent must read directly. Inlining content into the prompt is a paraphrase that loses nuance; agents grounded in the source catch details the orchestrator's summary missed. For GH issues, instruct `gh issue view N --comments` — body alone misses
|
|
72
|
+
2. **Primary sources** — the specific files, GH issues, and reference docs the sub-agent must read directly. Inlining content into the prompt is a paraphrase that loses nuance; agents grounded in the source catch details the orchestrator's summary missed. For GH issues, instruct both `gh issue view N --comments` (the comment thread) and the timeline cross-reference query in the Orient block (what references the issue, including cross-repo) — the body alone misses both. The orchestrator reads these sources too (to construct the prompt), but that's prompt construction, not a substitute for the sub-agent reading them.
|
|
73
73
|
3. **Path** — the Tier 1 skill(s) and steps that get to the goal. This is what gets handed to the sub-agent.
|
|
74
74
|
4. **Verification** — the read-only checks that confirm the goal was hit. Defined upfront, not as an afterthought.
|
|
75
75
|
|
|
@@ -91,6 +91,8 @@ Sub-agents are optional. Match the mechanism to your platform's capability — t
|
|
|
91
91
|
|
|
92
92
|
Phases, gates, goals, and constraints are identical across all three tiers — only the fanout mechanism changes. Use the most capable tier available, and don't hand-roll what the platform does natively (e.g., rolling concurrency). Choose by scope and capability, not by default.
|
|
93
93
|
|
|
94
|
+
**Model tier ≠ orchestration tier.** A higher orchestration tier is not automatically the right choice. On some platforms, programmatically-orchestrated or *nested* sub-agents (an agent spawning agents) silently run on a cheaper/downgraded model, while the strongest model is reachable only by sub-agents the **main loop spawns directly** (tier 2). When a phase needs the top model (heavy generation, design, framework adoption), prefer direct main-loop fanout even when a more "capable" orchestration primitive exists — the primitive can cost you the model. Verify the model your platform actually assigned via its UI/telemetry; never infer it from a sub-agent's transcript, which interleaves auxiliary calls (titles, summaries) on cheaper models and will mislead you.
|
|
95
|
+
|
|
94
96
|
The decision tree below is orthogonal to tier — it governs *whether* a given phase fans out, by target count and conflict risk:
|
|
95
97
|
|
|
96
98
|
| Situation | Strategy |
|
|
@@ -119,10 +121,12 @@ order. If any file does not exist, note it and continue.
|
|
|
119
121
|
available skills with descriptions and locations.
|
|
120
122
|
5. Read the skill file(s) for this task: `[Tier 1 skill paths]`.
|
|
121
123
|
6. Read the primary sources for this task directly — design docs (`docs/design.md`),
|
|
122
|
-
GH issues
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
GH issues, handoff documents, reference/gold-standard files. For a GH issue, read
|
|
125
|
+
both the comment thread and its cross-references — the body alone misses both:
|
|
126
|
+
- `gh issue view <N> --comments` — description + comment thread
|
|
127
|
+
- `gh api 'repos/{owner}/{repo}/issues/<N>/timeline' --paginate --jq '.[] | select(.event=="cross-referenced") | .source.issue | "\(.repository.full_name)#\(.number) — \(.title)"'` — issues/PRs that reference this one, including from other repos
|
|
128
|
+
List each source explicitly: `[primary source paths and gh commands]`. Skip this
|
|
129
|
+
step only if no primary source applies (rare).
|
|
126
130
|
|
|
127
131
|
Only after that, begin the task below.
|
|
128
132
|
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Finalize documentation and project metadata for a ship-ready MCP server. Use after implementation is complete, tests pass, and devcheck is clean. Safe to run at any stage — each step checks current state and only acts on what still needs work.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.4"
|
|
8
8
|
audience: external
|
|
9
9
|
type: workflow
|
|
10
10
|
---
|
|
@@ -212,7 +212,6 @@ If the project ships as an `.mcpb` bundle for Claude Desktop (check for `manifes
|
|
|
212
212
|
- If `manifest.json` exists, the README should include the Claude Desktop install badge linking to `releases/latest/download/<name>.mcpb`
|
|
213
213
|
- If the package is published to npm, include Cursor and VS Code install badges
|
|
214
214
|
- See `references/readme.md` for badge format and config generation commands
|
|
215
|
-
- See the **Bundling** section of `templates/CLAUDE.md` for `base64` / `encodeURIComponent` generation
|
|
216
215
|
|
|
217
216
|
### 12. `LICENSE`
|
|
218
217
|
|
|
@@ -54,7 +54,21 @@ Centered HTML. The `<h1>` is the server name — use the scoped package name if
|
|
|
54
54
|
</div>
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Generate the `<BASE64_CONFIG>` (Cursor) and `<URLENCODED_JSON>` (VS Code) payloads — replace `<PACKAGE_NAME>` / `<SHORT_NAME>` and add `env` only for required API keys:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Cursor: base64-encoded JSON. Split command/args, add env when keys are needed.
|
|
61
|
+
echo -n '{"command":"npx","args":["-y","<PACKAGE_NAME>"],"env":{"API_KEY":"your-api-key"}}' | base64
|
|
62
|
+
# Without env (no required keys):
|
|
63
|
+
echo -n '{"command":"npx","args":["-y","<PACKAGE_NAME>"]}' | base64
|
|
64
|
+
|
|
65
|
+
# VS Code: URL-encoded JSON. Same shape plus a `name` field.
|
|
66
|
+
node -p 'encodeURIComponent(JSON.stringify({name:"<SHORT_NAME>",command:"npx",args:["-y","<PACKAGE_NAME>"],env:{API_KEY:"your-api-key"}}))'
|
|
67
|
+
# Without env:
|
|
68
|
+
node -p 'encodeURIComponent(JSON.stringify({name:"<SHORT_NAME>",command:"npx",args:["-y","<PACKAGE_NAME>"]}))'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Both clients use the same `{command, args, env}` shape; VS Code adds a top-level `name`. Omit `env` entirely when no API keys are needed — don't include empty objects or framework-only vars like `MCP_TRANSPORT_TYPE`. Install links route through HTTPS endpoints (`cursor.com/en/install-mcp`, `vscode.dev/redirect`) because GitHub-rendered markdown strips non-HTTP schemes — a raw `cursor://` or `vscode:` link won't click through. Omit any install badge whose target doesn't apply (e.g. no `.mcpb` bundle → drop the Claude Desktop badge).
|
|
58
72
|
|
|
59
73
|
The header tagline must match the `package.json` `description`.
|
|
60
74
|
|
|
@@ -30,7 +30,12 @@ For general `gh` CLI workflows outside issue filing (PRs, workflows, API access)
|
|
|
30
30
|
4. **Search existing issues** — don't file duplicates:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
gh issue list -R cyanheads/mcp-ts-core --search "your error message or keyword"
|
|
33
|
+
gh issue list -R cyanheads/mcp-ts-core --search "your error message or keyword" --state all
|
|
34
|
+
|
|
35
|
+
# Assess a close match before commenting — is it already linked to a fix or referenced elsewhere?
|
|
36
|
+
gh issue view <number> -R cyanheads/mcp-ts-core --comments
|
|
37
|
+
gh api 'repos/cyanheads/mcp-ts-core/issues/<number>/timeline' --paginate \
|
|
38
|
+
--jq '.[] | select(.event=="cross-referenced") | .source.issue | "\(.repository.full_name)#\(.number) — \(.title)"'
|
|
34
39
|
```
|
|
35
40
|
|
|
36
41
|
5. **For documentation- or contract-shaped requests, audit all three doc layers first** — proposals to add reference docs, public-API conventions, attribute/event catalogs, or stability commitments often duplicate surface that already exists. Check `src/` for behavior, `docs/` for human-facing reference, and `skills/` for agent-facing reference. Skill files marked `audience: external` are the framework's public contract — treat them as authoritative when evaluating whether a documentation gap exists. Also verify the constants or types you'd reference aren't already exported from `@cyanheads/mcp-ts-core` or one of its subpaths.
|
|
@@ -273,8 +278,8 @@ ISSUE
|
|
|
273
278
|
## Following Up
|
|
274
279
|
|
|
275
280
|
```bash
|
|
276
|
-
# Check issue status
|
|
277
|
-
gh issue view <number> -R cyanheads/mcp-ts-core
|
|
281
|
+
# Check issue status (with comment thread)
|
|
282
|
+
gh issue view <number> -R cyanheads/mcp-ts-core --comments
|
|
278
283
|
|
|
279
284
|
# Add context or respond to maintainer questions
|
|
280
285
|
gh issue comment <number> -R cyanheads/mcp-ts-core --body "Additional context..."
|
|
@@ -35,7 +35,12 @@ gh repo view --json nameWithOwner -q '.nameWithOwner'
|
|
|
35
35
|
2. **Search existing issues** — if a close match exists (same symptom, different tool; same tool, different symptom; closed issue that might cover the new case), add a comment on that issue instead of filing a new one — unless the symptom or scope is distinct enough to warrant separate tracking:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
gh issue list --search "your error message or keyword"
|
|
38
|
+
gh issue list --search "your error message or keyword" --state all
|
|
39
|
+
|
|
40
|
+
# Assess a close match before commenting — is it already linked to a fix or referenced elsewhere?
|
|
41
|
+
gh issue view <number> --comments
|
|
42
|
+
gh api 'repos/{owner}/{repo}/issues/<number>/timeline' --paginate \
|
|
43
|
+
--jq '.[] | select(.event=="cross-referenced") | .source.issue | "\(.repository.full_name)#\(.number) — \(.title)"'
|
|
39
44
|
```
|
|
40
45
|
|
|
41
46
|
3. **Reproduce the issue** — confirm it's reproducible. Note the exact input, transport mode, and any relevant env vars.
|
|
@@ -280,8 +285,8 @@ When genuinely ambiguous, file against this server's repo and note that it might
|
|
|
280
285
|
## Following Up
|
|
281
286
|
|
|
282
287
|
```bash
|
|
283
|
-
# View issue details
|
|
284
|
-
gh issue view <number>
|
|
288
|
+
# View issue details (with comment thread)
|
|
289
|
+
gh issue view <number> --comments
|
|
285
290
|
|
|
286
291
|
# Add context
|
|
287
292
|
gh issue comment <number> --body "Additional findings..."
|
package/templates/.env.example
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# MCP_HTTP_PORT=3010 # HTTP port (default: 3010)
|
|
4
4
|
# MCP_HTTP_HOST=localhost # HTTP host (default: localhost)
|
|
5
5
|
# MCP_HTTP_ENDPOINT_PATH=/mcp # HTTP endpoint path (default: /mcp)
|
|
6
|
+
# MCP_HTTP_MAX_BODY_BYTES=1048576 # Max request body bytes; 413 over limit, 0 disables (default: 1048576)
|
|
6
7
|
# MCP_PUBLIC_URL= # Public origin behind a TLS-terminating proxy (e.g. https://mcp.example.com)
|
|
7
8
|
|
|
8
9
|
# ── Auth ──────────────────────────────────────────────────────────────
|
package/templates/AGENTS.md
CHANGED
|
@@ -50,6 +50,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
50
50
|
- **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
|
|
51
51
|
- **Check `ctx.elicit` / `ctx.sample`** for presence before calling.
|
|
52
52
|
- **Secrets in env vars only** — never hardcoded.
|
|
53
|
+
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
53
54
|
|
|
54
55
|
---
|
|
55
56
|
|
|
@@ -151,6 +152,10 @@ export function getServerConfig() {
|
|
|
151
152
|
|
|
152
153
|
`parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`MY_API_KEY`) not the path (`apiKey`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
|
|
153
154
|
|
|
155
|
+
### Server instructions
|
|
156
|
+
|
|
157
|
+
`createApp({ instructions })` — optional server-level orientation, sent to clients on every `initialize` as session-level context. Use it for deployment guidance (connection aliases, regional notes, scope hints) instead of repeating the same context across tool descriptions. Client adoption is uneven, but there's no downside when set.
|
|
158
|
+
|
|
154
159
|
---
|
|
155
160
|
|
|
156
161
|
## Context
|
|
@@ -177,6 +182,8 @@ Handlers throw — the framework catches, classifies, and formats.
|
|
|
177
182
|
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required descriptive metadata for the agent's next move (≥ 5 words, lint-validated); for the wire `data.recovery.hint` (mirrored into `content[]` text), pass explicitly at the throw site when dynamic context matters: `ctx.fail('reason', msg, { recovery: { hint: '...' } })`. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
|
|
178
183
|
|
|
179
184
|
```ts
|
|
185
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
186
|
+
|
|
180
187
|
errors: [
|
|
181
188
|
{ reason: 'no_match', code: JsonRpcErrorCode.NotFound,
|
|
182
189
|
when: 'No item matched the query',
|
|
@@ -271,7 +278,6 @@ Available skills:
|
|
|
271
278
|
| `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
|
|
272
279
|
| `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
|
|
273
280
|
| `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
|
|
274
|
-
| `migrate-mcp-ts-template` | One-time migration of an existing project to the current framework template |
|
|
275
281
|
| `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
|
|
276
282
|
| `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
|
|
277
283
|
| `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
|
|
@@ -319,40 +325,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
319
325
|
|
|
320
326
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match.
|
|
321
327
|
|
|
322
|
-
**README install badges
|
|
323
|
-
|
|
324
|
-
| Client | Mechanism |
|
|
325
|
-
|:-------|:----------|
|
|
326
|
-
| Claude Desktop | Browser downloads the `.mcpb` from the latest GitHub Release; OS file handler routes it to Claude Desktop, which opens the install dialog. No deep-link URL scheme yet — this is the canonical path. |
|
|
327
|
-
| Cursor | Official `https://cursor.com/en/install-mcp` endpoint with base64 JSON config. |
|
|
328
|
-
| VS Code / Insiders | Official `vscode:mcp/install?...` deep link, wrapped in `https://vscode.dev/redirect?url=` so GitHub-rendered markdown doesn't strip the non-HTTP scheme. |
|
|
329
|
-
| Claude Code / Codex | CLI only (`claude mcp add` / `codex mcp add`); no URL scheme. |
|
|
330
|
-
|
|
331
|
-
```markdown
|
|
332
|
-
[](https://github.com/<OWNER>/<REPO>/releases/latest/download/<PACKAGE_NAME>.mcpb)
|
|
333
|
-
[](https://cursor.com/en/install-mcp?name=<PACKAGE_NAME>&config=<BASE64_CONFIG>)
|
|
334
|
-
[](https://vscode.dev/redirect?url=vscode:mcp/install?<URLENCODED_JSON>)
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
Both install links route through HTTPS endpoints (`cursor.com/en/install-mcp` and `vscode.dev/redirect`) — GitHub-rendered markdown strips non-HTTP URL schemes from anchors, so a raw `cursor://` or `vscode:` link won't click through from github.com.
|
|
338
|
-
|
|
339
|
-
Generate the encoded configs (replace `<PACKAGE_NAME>` and add env vars for any required API keys):
|
|
340
|
-
|
|
341
|
-
```bash
|
|
342
|
-
# Cursor: base64-encoded JSON. Split command/args, add env when keys are needed.
|
|
343
|
-
echo -n '{"command":"npx","args":["-y","<PACKAGE_NAME>"],"env":{"API_KEY":"your-api-key"}}' | base64
|
|
344
|
-
# Without env (no required keys):
|
|
345
|
-
echo -n '{"command":"npx","args":["-y","<PACKAGE_NAME>"]}' | base64
|
|
346
|
-
|
|
347
|
-
# VS Code: URL-encoded JSON. Same shape plus a `name` field.
|
|
348
|
-
node -p 'encodeURIComponent(JSON.stringify({name:"<SHORT_NAME>",command:"npx",args:["-y","<PACKAGE_NAME>"],env:{API_KEY:"your-api-key"}}))'
|
|
349
|
-
# Without env:
|
|
350
|
-
node -p 'encodeURIComponent(JSON.stringify({name:"<SHORT_NAME>",command:"npx",args:["-y","<PACKAGE_NAME>"]}))'
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
Both clients use the same `{command, args, env}` shape (matching `mcp.json` schema). VS Code adds a top-level `name` field. Omit `env` entirely when no API keys are needed — don't include empty objects or framework-only vars like `MCP_TRANSPORT_TYPE`.
|
|
354
|
-
|
|
355
|
-
The Claude Desktop badge requires the bundle to ship with a stable filename — `bun run bundle` outputs `dist/<PACKAGE_NAME>.mcpb`, and `release-and-publish` attaches that file to the GitHub Release. `releases/latest/download/<PACKAGE_NAME>.mcpb` then redirects to the most recent release.
|
|
328
|
+
**README install badges** (Claude Desktop `.mcpb`, Cursor, VS Code) and the `base64` / `encodeURIComponent` config-generation commands are ship-time concerns — run the `polish-docs-meta` skill, which carries the badge format, layout, and generation snippets in `skills/polish-docs-meta/references/readme.md`.
|
|
356
329
|
|
|
357
330
|
---
|
|
358
331
|
|