@cyanheads/mcp-ts-core 0.8.17 → 0.8.19

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 CHANGED
@@ -1,9 +1,28 @@
1
- # Agent Protocol
1
+ # Developer Protocol
2
2
 
3
- **Package:** `@cyanheads/mcp-ts-core` · **Version:** 0.8.17
4
- **npm:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) · **Docker:** [ghcr.io/cyanheads/mcp-ts-core](https://ghcr.io/cyanheads/mcp-ts-core)
3
+ **Package:** `@cyanheads/mcp-ts-core`
4
+ **Version:** 0.8.19
5
+ **Engines:** Bun ≥1.3.0, Node ≥24.0.0
6
+ **MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
7
+ **Zod:** ^4.4.3
8
+ **GitHub:** [cyanheads/mcp-ts-core](https://github.com/cyanheads/mcp-ts-core)
9
+ **npm:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
10
+ **Docker:** [ghcr.io/cyanheads/mcp-ts-core](https://ghcr.io/cyanheads/mcp-ts-core)
5
11
 
6
- > **Developer note:** Never assume. Read related files and docs before making changes. Read full file content for context. Never edit a file before reading it.
12
+ > **Developer note:** Never assume. Read related files and docs before making changes. Read full file content for context. Never try to edit a file before reading it.
13
+
14
+ ---
15
+
16
+ ## Consumers
17
+
18
+ This package serves two consumer paths. When making changes, know which audience your change affects:
19
+
20
+ | Path | On-ramp | Affected by changes to |
21
+ |:--|:--|:--|
22
+ | **Direct package import** — existing project pulls in the package | `bun add @cyanheads/mcp-ts-core` → `import { createApp, tool, z } from '@cyanheads/mcp-ts-core'` | Public API surface (`src/`) — existing consumers feel changes immediately on upgrade |
23
+ | **Init-scaffolded server** — fresh project bootstrapped from this repo's templates | `bunx @cyanheads/mcp-ts-core init [name]` copies `templates/` into the new directory | `templates/` — only affects newly scaffolded servers, not existing ones |
24
+
25
+ Both paths end up consuming the same public API. The init script bootstraps the project structure: starter `package.json`, `tsconfig`, `biome.json`, `vitest.config.ts`, `.env.example`, `Dockerfile`, `CLAUDE.md`/`AGENTS.md`, and example tool/resource/prompt definitions. Files in `templates/` prefixed with `_` (e.g. `_.gitignore`, `_tsconfig.json`) have the prefix stripped on copy. After init, agents should consult the `setup` skill.
7
26
 
8
27
  ---
9
28
 
@@ -13,7 +32,7 @@
13
32
  - **Full-stack observability.** The framework automatically instruments every tool/resource call — OTel span, duration/payload/memory metrics, structured completion log. Use `ctx.log` for additional domain-specific logging within handlers (external API calls, multi-step operations, business events). `requestId`, `traceId`, `tenantId` auto-correlated. No `console` calls.
14
33
  - **Unified Context.** Handlers receive `ctx` with logging (`ctx.log`), tenant-scoped storage (`ctx.state`), optional protocol capabilities (`ctx.elicit`, `ctx.sample`), and cancellation (`ctx.signal`).
15
34
  - **Decoupled storage.** `ctx.state` for tenant-scoped KV. Never access persistence backends directly.
16
- - **Canvas tokens are capabilities, not tenant-scoped state.** A `canvasId` is a 10-char URL-safe token; possession grants full read/write/drop on that canvas. Tokens are designed to be shareable — between agents in one session, and across users in single-tenant deployments (stdio, HTTP + `MCP_AUTH_MODE=none`, where every request resolves to `tenantId='default'`). Tools should accept the token in `input` (or omit to create fresh) and return it in `output`; collaboration is opt-in via explicit token exchange. **Do NOT cache canvasIds in `ctx.state` as a "default canvas"** — under shared `tenantId='default'` every concurrent user reads the same cached id and silently collides on one workspace. The same rule applies to any UUID-keyed primitive (datasets, derived row stores): the ID is the access grant, scoping happens by token possession, not by tenant. Public/free deployments of this framework run stateless and auth-mode-`none` — design accordingly.
35
+ - **Canvas tokens are capabilities, not tenant-scoped state.** A `canvasId` is a 10-char URL-safe token; possession grants full read/write/drop on that canvas. Tokens are designed to be shareable — between agents in one session, and across users in single-tenant deployments (see tenant resolution table under `ctx.state`). Tools should accept the token in `input` (or omit to create fresh) and return it in `output`; collaboration is opt-in via explicit token exchange.
17
36
  - **Runtime parity.** All features work with `stdio`/`http` and Worker bundle. Guard non-portable deps via `runtimeCaps` from `@cyanheads/mcp-ts-core/utils` — a frozen capability object (`isNode`, `isBun`, `isWorkerLike`, `hasBuffer`, `hasProcess`, etc.) computed once at module load. Prefer runtime-agnostic abstractions (Hono + `@hono/mcp`, Fetch APIs).
18
37
  - **Startup validation.** `createApp()` runs the definition linter before proceeding — errors (spec violations) throw `ConfigurationError` and block startup; warnings are logged. Also available standalone via `bun run lint:mcp` and as a devcheck step. Every diagnostic links to the rule reference in `api-linter` skill; see that skill for the full rule catalog.
19
38
  - **Elicitation for missing input.** Use `ctx.elicit` when the client supports it.
@@ -151,10 +170,6 @@ src/
151
170
 
152
171
  **File suffixes:** `.tool.ts` (standard or task), `.resource.ts`, `.prompt.ts`, `.app-tool.ts` (UI-enabled), `.app-resource.ts` (UI resource linked to app tool).
153
172
 
154
- **Scaffold a new server:** `npx @cyanheads/mcp-ts-core init [name]` copies `templates/` into a new project. After running, consult the `setup` skill.
155
-
156
- **`templates/` directory:** Scaffolding source for the init CLI. Contents are copied into new consumer servers — includes starter `package.json`, `tsconfig`, `biome.json`, `vitest.config.ts`, `.env.example`, `Dockerfile`, `CLAUDE.md`/`AGENTS.md`, and example tool/resource/prompt definitions. Files prefixed with `_` (e.g. `_.gitignore`, `_tsconfig.json`) are renamed on copy (strip `_` prefix). Changes here affect every newly scaffolded server.
157
-
158
173
  ---
159
174
 
160
175
  ## Adding a Tool
@@ -199,9 +214,14 @@ export const myTool = tool('my_tool', {
199
214
 
200
215
  **Schema constraint:** Input/output schemas must use JSON-Schema-serializable Zod types only. The MCP SDK converts schemas to JSON Schema for `tools/list` — non-serializable types (`z.custom()`, `z.date()`, `z.transform()`, `z.bigint()`, `z.symbol()`, `z.void()`, `z.map()`, `z.set()`, `z.function()`, `z.nan()`) cause a hard runtime failure. Use structural equivalents instead (e.g., `z.string()` with `.describe('ISO 8601 date')` instead of `z.date()`). The linter validates this at startup.
201
216
 
202
- **Form-client safety:** Form-based MCP clients (MCP Inspector, web UIs) send optional object fields with empty-string inner values instead of `undefined`. Don't reject with `.min(1)` on optional fields — guard for meaningful values in the handler (`if (input.dateRange?.minDate && input.dateRange?.maxDate)`). Test with both omitted and empty-value payloads. Alternative when schema-level constraints (regex/length) matter enough to surface in the JSON Schema: wrap in a union with a `z.literal('')` sentinel `z.union([z.literal(''), z.string().regex(...).describe(...)])`. The linter exempts literal variants from `describe-on-fields`; the LLM sees the regex/length via the non-literal branch.
217
+ **Form-client safety:** Form-based clients (MCP Inspector, web UIs) send optional fields as empty strings, not `undefined`. Don't reject with `.min(1)` on optional fields — guard for meaningful values in the handler (`if (input.dateRange?.minDate && input.dateRange?.maxDate)`). Test with both omitted and empty-value payloads. When schema-level constraints (regex/length) need to surface in the JSON Schema, wrap in a union with a `z.literal('')` sentinel: `z.union([z.literal(''), z.string().regex(...).describe(...)])` the linter exempts the literal variant from `describe-on-fields`.
218
+
219
+ **`format`**: Maps output to MCP `content[]`. Different clients forward different surfaces to the agent — some (Claude Code) read `structuredContent` from `output`, others (Claude Desktop) read `content[]` from `format()`. `format()` is the markdown twin of `structuredContent`, not a reduced summary.
203
220
 
204
- **`format`**: Maps output to MCP `content[]`. 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 be content-complete** so every client sees the same data — `format()` is the markdown-rendered twin of `structuredContent`, not a separate payload. A thin `format()` (count or title only) leaves `content[]`-only clients blind to data that `structuredContent` clients can see. Enforced at lint time: every terminal field in `output` must appear in `format()`'s rendered text (via sentinel injection), or startup fails with a `format-parity` error. Primary fix: render the missing field in `format()` (use `z.discriminatedUnion` for list/detail variants — each branch is validated separately). Escape hatch: if the output schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) rather than maintaining aspirational typing — passthrough still flows data to `structuredContent`. Omit `format` entirely for JSON stringify fallback. Additional formatters: `markdown()` (builder), `diffFormatter` (async), `tableFormatter`, `treeFormatter` from `/utils`.
221
+ - **Parity is enforced.** Every terminal field in `output` must appear in `format()`'s rendered text (via sentinel injection), or startup fails with a `format-parity` lint error.
222
+ - **Primary fix:** render the missing field in `format()`. Use `z.discriminatedUnion` for list/detail variants — each branch is validated separately.
223
+ - **Escape hatch:** if the schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) — passthrough still flows data to `structuredContent`.
224
+ - **Fallback:** omit `format` for JSON stringify. Additional formatters in `/utils`: `markdown()` (builder), `diffFormatter` (async), `tableFormatter`, `treeFormatter`.
205
225
 
206
226
  **Task tools:** Add `task: true` for long-running async operations. Framework manages lifecycle: creates task → returns ID immediately → runs handler in background with `ctx.progress` → stores result/error → `ctx.signal` for cancellation. See `add-tool` skill for full example.
207
227
 
@@ -396,11 +416,7 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
396
416
 
397
417
  **Auto-classification.** Plain `Error`, `ZodError`, and any other thrown value are caught and classified automatically. Resolution order: `McpError` code (preserved as-is) → JS constructor name (`TypeError` → `ValidationError`) → provider patterns (HTTP status codes, AWS errors, DB errors) → common message patterns → `AbortError` name → `InternalError` fallback.
398
418
 
399
- **Error-path parity.** Tool errors mirror the success-path `format-parity` invariant both surfaces clients forward to the agent carry the same payload:
400
- - `content[]` (read by clients like Claude Desktop) — markdown rendering of the error, with `data.recovery.hint` mirrored into the text when present
401
- - `structuredContent.error` (read by clients like Claude Code) — JSON `{ code, message, data? }` carrying the error code, message, and any structured data from the thrown `McpError` or `ZodError`
402
-
403
- `_meta.error` is **not** emitted on tool errors — error data lives on `structuredContent.error`. Resources re-throw to the SDK, which routes them through the JSON-RPC error envelope (no parity wiring needed there).
419
+ **Error-path parity.** Tool errors apply the same client-surface parity as success: `content[]` carries a markdown rendering with `data.recovery.hint` mirrored in, `structuredContent.error` carries `{ code, message, data? }`. `_meta.error` is not emitted. Resources re-throw to the SDK and route through the JSON-RPC error envelope (no parity wiring there).
404
420
 
405
421
  The startup linter checks handler bodies for `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, plus contract conformance (`error-contract-conformance` for undeclared non-baseline codes, `error-contract-prefer-fail` for declared codes thrown directly instead of via `ctx.fail`) — all warnings, surfaced in `bun run devcheck`.
406
422
 
@@ -494,7 +510,8 @@ Each `skills/<name>/SKILL.md` carries a `metadata.version` string in its frontma
494
510
 
495
511
  | Skill | Path | Covers |
496
512
  |:------|:-----|:-------|
497
- | `api-utils` | `skills/api-utils/SKILL.md` | formatting, parsing, security, network, pagination, runtime, scheduling, types, logger, requestContext, errorHandler, telemetry |
513
+ | `api-utils` | `skills/api-utils/SKILL.md` | formatting, parsing, security, network, pagination, runtime, scheduling, types, logger, requestContext, errorHandler, telemetry helpers (`withSpan`, `createCounter`, …) |
514
+ | `api-telemetry` | `skills/api-telemetry/SKILL.md` | OTel catalog: span names, metric names + attributes, completion log fields, env config, runtime support, cardinality rules |
498
515
  | `api-services` | `skills/api-services/SKILL.md` | LLM (OpenRouter), Speech (ElevenLabs TTS, Whisper STT), Graph (CRUD, traversal, pathfinding) |
499
516
  | `api-context` | `skills/api-context/SKILL.md` | Context interface, createContext, ContextLogger/State/Progress |
500
517
  | `api-errors` | `skills/api-errors/SKILL.md` | McpError, JsonRpcErrorCode, error handling patterns |
@@ -535,7 +552,7 @@ Each `skills/<name>/SKILL.md` carries a `metadata.version` string in its frontma
535
552
  - **JSDoc:** `@fileoverview` + `@module` required on every file
536
553
  - **No fabricated signal:** Don't invent synthetic scores or arbitrary "confidence percentages." Surface real signal.
537
554
  - **Builders:** `tool()`/`resource()`/`prompt()` with correct fields (`handler`, `input`, `output`, `format`, `auth`, `args`)
538
- - **`format()` completeness:** different clients forward different surfaces (Claude Code reads `structuredContent`, Claude Desktop reads `content[]`) — both must carry the same data; `format()` is the markdown twin of `structuredContent`, not a reduced summary
555
+ - **`format()` completeness:** must carry the same data as `output` (parity is lint-enforced see Adding a Tool)
539
556
  - **Auth:** via `auth: ['scope']` on definitions (not HOF wrapper)
540
557
  - **Presence checks:** `ctx.elicit`/`ctx.sample` checked before use
541
558
  - **Task tools:** use `task: true` flag
@@ -547,14 +564,6 @@ Each `skills/<name>/SKILL.md` carries a `metadata.version` string in its frontma
547
564
 
548
565
  ---
549
566
 
550
- ## Git
551
-
552
- **Safety:** NEVER `git stash`. NEVER destructive commands (`reset --hard`, `checkout -- .`, `restore .`, `clean -f`) unless user explicitly requests. Read-only is always safe.
553
-
554
- **Commits:** Plain `-m` strings, no heredoc/command substitution. [Conventional Commits](https://www.conventionalcommits.org/): `feat|fix|refactor|chore|docs|test|build(scope): message`. Group related changes atomically.
555
-
556
- ---
557
-
558
567
  ## Commands
559
568
 
560
569
  | Command | Purpose |
@@ -576,7 +585,9 @@ After `bun update --latest`, run the `maintenance` skill to investigate changelo
576
585
 
577
586
  ## Changelog
578
587
 
579
- Directory-based, grouped by minor series using the `.x` semver-wildcard convention. Source of truth is `changelog/<major.minor>.x/<version>.md` — one standalone file per released version (e.g. `changelog/0.5.x/0.5.4.md`), shipped in the npm package so agents can read a specific version from `node_modules/@cyanheads/mcp-ts-core/changelog/<major.minor>.x/<version>.md` without parsing a monolithic file. At release time, author the per-version file with a concrete version and date, then run `bun run changelog:build` to regenerate the rollup. `changelog/template.md` is a **pristine format reference** — never edited, never renamed, never moved. Read it to remember the frontmatter + section layout when scaffolding a new per-version file.
588
+ Directory-based, grouped by minor series via the `.x` semver-wildcard convention. Source of truth is `changelog/<major.minor>.x/<version>.md` — one standalone file per release (e.g. `changelog/0.5.x/0.5.4.md`). Per-version files ship in the npm package so agents can read a specific version directly from `node_modules` without parsing a monolithic file.
589
+
590
+ At release time, author the per-version file with a concrete version and date, then run `bun run changelog:build` to regenerate the rollup. `changelog/template.md` is a format reference — never edited; consult it for frontmatter and section layout when scaffolding a new file. Be concise and accurate.
580
591
 
581
592
  `CHANGELOG.md` is a **navigation index**, not a copy of bodies — each entry is a clickable header + one-line summary pulled from the per-version file's frontmatter. Regenerated by `bun run changelog:build`. Devcheck runs `changelog:check` and hard-fails on drift. Never hand-edit `CHANGELOG.md` — edit the per-version file and rerun the build.
582
593
 
@@ -584,14 +595,13 @@ Directory-based, grouped by minor series using the `.x` semver-wildcard conventi
584
595
 
585
596
  ```markdown
586
597
  ---
587
- summary: One-line headline, ≤250 chars, no markdown # required
588
- breaking: false # optional, default false
598
+ summary: "One-line headline, ≤250 chars, no markdown" # required
599
+ breaking: false # optional, default false
600
+ security: false # optional, default false
589
601
  ---
590
602
 
591
603
  # 0.5.4 — 2026-04-20
592
604
 
593
- Optional narrative intro (1-3 sentences).
594
-
595
605
  ## Added
596
606
 
597
607
  - ...
@@ -601,10 +611,13 @@ Optional narrative intro (1-3 sentences).
601
611
 
602
612
  | Field | Required | Purpose |
603
613
  |:------|:---------|:--------|
604
- | `summary` | yes | Rollup index line. Max 250 chars, no markdown, single line. Write like a GitHub Release title. |
614
+ | `summary` | yes | Rollup index line. 250 chars, no markdown, single line. Write like a GitHub Release title. |
605
615
  | `breaking` | no (default `false`) | Flags releases with breaking changes. Renders as `· ⚠️ Breaking` badge in the rollup. Agents running the `maintenance` skill read this to prioritize review. |
616
+ | `security` | no (default `false`) | Flags releases with security fixes. Renders as `· 🛡️ Security` badge in the rollup so users can triage upgrade urgency. Pairs with the `## Security` body section. |
617
+
618
+ When both flags are set, badges render in fixed order: `· ⚠️ Breaking · 🛡️ Security`. Summary > 250 chars or a malformed boolean fails `changelog:check`. Missing `summary` emits a warning and renders the rollup entry as header-only.
606
619
 
607
- Summary > 250 chars or malformed `breaking` fails `changelog:check`. Missing `summary` emits a warning and renders the rollup entry as header-only.
620
+ **Section order** (Keep a Changelog): Added, Changed, Deprecated, Removed, Fixed, Security. Include only sections with entries don't ship empty headers.
608
621
 
609
622
  Pre-release versions (`0.6.0-beta.1`, `0.6.0-rc.1`, etc.) are consolidated as `##`/`###` sub-headers inside the final version's per-version file (`changelog/0.6.x/0.6.0.md`) when the final ships — they share the final version's frontmatter, no separate files per pre-release.
610
623
 
@@ -612,16 +625,4 @@ Pre-release versions (`0.6.0-beta.1`, `0.6.0-rc.1`, etc.) are consolidated as `#
612
625
 
613
626
  ## Publishing
614
627
 
615
- Run the `release-and-publish` skill — it runs the verification gate (`devcheck`, `rebuild`, `test:all`), pushes commits and tags, and publishes to every applicable destination. The full reference:
616
-
617
- ```bash
618
- bun publish --access public
619
-
620
- docker buildx build --platform linux/amd64,linux/arm64 \
621
- -t ghcr.io/cyanheads/mcp-ts-core:<version> \
622
- -t ghcr.io/cyanheads/mcp-ts-core:latest \
623
- --push .
624
-
625
- mcp-publisher publish
626
- ```
627
-
628
+ If the user requests it, run the `release-and-publish` skill — it runs the verification gate (`devcheck`, `rebuild`, `test:all`), pushes commits and tags, and publishes to every applicable destination. The full reference:
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  <div align="center">
7
7
 
8
- [![Version](https://img.shields.io/badge/Version-0.8.17-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--11--25-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-11-25/changelog.mdx) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE)
8
+ [![Version](https://img.shields.io/badge/Version-0.8.18-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--11--25-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-11-25/changelog.mdx) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE)
9
9
 
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
@@ -15,7 +15,9 @@
15
15
 
16
16
  ## What is this?
17
17
 
18
- `@cyanheads/mcp-ts-core` is the infrastructure layer for TypeScript MCP servers. Install it as a dependency — don't fork it. You write tools, resources, and prompts; the framework handles transports, auth, storage, config, logging, telemetry, and lifecycle.
18
+ `@cyanheads/mcp-ts-core` is the infrastructure layer for TypeScript MCP servers. Install it as a dependency — don't fork it. Your agent collaborates with you to design and build the tools, resources, and prompts for your server.
19
+
20
+ The framework handles the plumbing: transports, auth, config, logging, telemetry, & more. Define your domain logic with the builders and let the framework take care of the rest.
19
21
 
20
22
  ```ts
21
23
  import { createApp, tool, z } from '@cyanheads/mcp-ts-core';
@@ -23,15 +25,21 @@ import { createApp, tool, z } from '@cyanheads/mcp-ts-core';
23
25
  const greet = tool('greet', {
24
26
  description: 'Greet someone by name and return a personalized message.',
25
27
  annotations: { readOnlyHint: true },
26
- input: z.object({ name: z.string().describe('Name of the person to greet') }),
27
- output: z.object({ message: z.string().describe('The greeting message') }),
28
- handler: async (input) => ({ message: `Hello, ${input.name}!` }),
28
+ input: z.object({
29
+ name: z.string().describe('Name of the person to greet'),
30
+ }),
31
+ output: z.object({
32
+ message: z.string().describe('The greeting message'),
33
+ }),
34
+ handler: async (input) => ({
35
+ message: `Hello, ${input.name}!`,
36
+ }),
29
37
  });
30
38
 
31
39
  await createApp({ tools: [greet] });
32
40
  ```
33
41
 
34
- That's a complete MCP server. Every tool call is automatically logged with duration, payload sizes, memory usage, and request correlation — no instrumentation code needed. `createApp()` handles config parsing, logger init, transport startup, signal handlers, and graceful shutdown.
42
+ That's a complete MCP server. Every tool call is automatically logged with duration, payload sizes, and request correlation — no instrumentation code needed. `createApp()` handles config parsing, logger init, transport startup, signal handlers, and graceful shutdown.
35
43
 
36
44
  ## Quick start
37
45
 
@@ -43,7 +51,7 @@ bun install
43
51
 
44
52
  You get a scaffolded project with `CLAUDE.md`, Agent Skills, and a `src/` tree ready for your tools. Infrastructure — transports, auth, storage, telemetry, lifecycle, linting — lives in `node_modules`. What's left is domain: which APIs to wrap, which workflows to expose.
45
53
 
46
- Start your coding agent (Claude Code, Codex, Cursor), describe the system you want to expose, and it drives the build. The included skills cover the full cycle: `setup`, `design-mcp-server`, scaffolding, testing, `security-pass`, `release-and-publish`.
54
+ Start your coding agent (i.e. Claude Code, Codex) and describe what you want. The agent knows what to do from there. The included Agent Skills cover the full cycle: `setup`, `design-mcp-server`, scaffolding, testing, `security-pass`, `release-and-publish`, `maintenance`, & more.
47
55
 
48
56
  ### What you get
49
57
 
@@ -58,7 +66,9 @@ export const search = tool('search', {
58
66
  query: z.string().describe('Search query'),
59
67
  limit: z.number().default(10).describe('Max results'),
60
68
  }),
61
- output: z.object({ items: z.array(z.string()).describe('Search results') }),
69
+ output: z.object({
70
+ items: z.array(z.string()).describe('Search results'),
71
+ }),
62
72
  async handler(input) {
63
73
  const results = await doSearch(input.query, input.limit);
64
74
  return { items: results };
@@ -73,7 +83,9 @@ import { resource, z } from '@cyanheads/mcp-ts-core';
73
83
 
74
84
  export const itemData = resource('items://{itemId}', {
75
85
  description: 'Retrieve item data by ID.',
76
- params: z.object({ itemId: z.string().describe('Item ID') }),
86
+ params: z.object({
87
+ itemId: z.string().describe('Item ID'),
88
+ }),
77
89
  async handler(params, ctx) {
78
90
  return await getItem(params.itemId);
79
91
  },
@@ -96,32 +108,17 @@ It also works on Cloudflare Workers with `createWorkerHandler()` — same defini
96
108
 
97
109
  ## Features
98
110
 
99
- - **Declarative definitions** — `tool()`, `resource()`, `prompt()` builders with Zod schemas. `appTool()`/`appResource()` add interactive HTML UIs.
111
+ - **Declarative definitions** — `tool()`, `resource()`, `prompt()` builders with Zod schemas; `appTool()`/`appResource()` add interactive HTML UIs.
100
112
  - **Unified Context** — one `ctx` for logging, tenant-scoped storage, elicitation, sampling, cancellation, and task progress.
101
- - **Inline auth** — `auth: ['scope']` on definitions. Framework checks scopes before dispatch no wrapper code.
113
+ - **Auth** — `auth: ['scope']` on definitions, checked before dispatch (no wrapper code). Modes: `none`, `jwt`, or `oauth` (local secret or JWKS).
102
114
  - **Task tools** — `task: true` for long-running ops; framework manages create/poll/progress/complete/cancel.
103
- - **Definition linter** — validates names, schemas, auth scopes, annotation coherence, and format-parity at startup. Standalone CLI (`lint:mcp`) and devcheck step.
104
- - **Typed error contracts** — declare `errors: [{ reason, code, when, retryable? }]` on a tool/resource and the handler receives a typed `ctx.fail(reason, …)` keyed against the declared reasons. The contract publishes in `tools/list` so clients preview failure modes; the linter cross-checks the handler body. Error factories (`notFound()`, `httpErrorFromResponse()`, …) for ad-hoc throws; plain `Error` works too — framework auto-classifies.
105
- - **Multi-backend storage** — `in-memory`, filesystem, Supabase, Cloudflare D1/KV/R2. Swap providers via env var; handlers don't change.
115
+ - **Definition linter** — validates names, schemas, auth scopes, annotations, and format-parity at startup. Standalone via `lint:mcp` or devcheck.
116
+ - **Typed error contracts** — declare `errors: [{ reason, code, when, retryable? }]` and handlers get a typed `ctx.fail(reason, …)`. Contracts publish in `tools/list` so clients preview failure modes; the linter cross-checks the handler. Factories (`notFound()`, `httpErrorFromResponse()`, …) cover ad-hoc throws; plain `Error` auto-classifies.
117
+ - **Multi-backend storage** — `in-memory`, filesystem, Supabase, Cloudflare D1/KV/R2. Swap via env var; handlers don't change.
106
118
  - **DataCanvas (optional)** — Tier 3 SQL/analytical workspace backed by DuckDB. Register tabular data from upstream APIs, run SQL across registered tables, export CSV/Parquet/JSON. Token-sharing model (opaque `canvas_id`) for multi-agent collaboration; sliding TTL + per-tenant scoping. Opt-in via `CANVAS_PROVIDER_TYPE=duckdb`; fails closed on Workers.
107
- - **Pluggable auth** — `none`, `jwt`, or `oauth`. Local secret or JWKS verification.
108
- - **Observability** — Pino logging, optional OpenTelemetry traces and metrics. Request correlation and tool metrics are automatic.
109
- - **Local + edge** — same definitions run on stdio, HTTP (Hono), and Cloudflare Workers.
110
- - **Tiered dependencies** — parsers, OTEL SDK, Supabase, and OpenAI are optional peers. Install what you use.
111
- - **Agent-first DX** — ships `CLAUDE.md` with the full exports catalog so AI agents ramp up without prompting.
112
-
113
- ### Storage Behavior Snapshot
114
-
115
- Provider behavior is intentionally normalized at the interface, but backend limits still matter:
116
-
117
- | Provider | Delete count accuracy | List TTL filtering | Notes |
118
- |:---------|:----------------------|:-------------------|:------|
119
- | `in-memory` | Exact | Exact | Volatile process memory |
120
- | `filesystem` | Exact | Exact | Node/Bun only |
121
- | `supabase` | Exact | Exact | Requires configured Supabase client |
122
- | `cloudflare-d1` | Exact | Exact | Workers D1 binding |
123
- | `cloudflare-kv` | Idempotent API success | Native/eventual | Delete cannot prove prior existence |
124
- | `cloudflare-r2` | Idempotent API success | Not applied during list | Expired envelopes are removed on read |
119
+ - **Observability** — Pino logging + optional OpenTelemetry traces/metrics. Request correlation and tool metrics automatic.
120
+ - **Tiered dependencies** — parsers, OTEL SDK, Supabase, OpenAI as optional peers. Install what you use.
121
+ - **Agent-first DX** — ships `CLAUDE.md` / `AGENTS.md` with the codebase documented throughout Agent Skills.
125
122
 
126
123
  ## Server structure
127
124
 
@@ -181,7 +178,6 @@ See [CLAUDE.md](CLAUDE.md) for the full configuration reference.
181
178
  | `prompt(name, options)` | Define a prompt with `generate(args)` |
182
179
  | `appTool(name, options)` | Define an MCP Apps tool with auto-populated `_meta.ui` |
183
180
  | `appResource(uriTemplate, options)` | Define an MCP Apps HTML resource with the correct MIME type and `_meta.ui` mirroring for read content |
184
- | `disabledTool(def, meta)` | Mark a tool present-in-manifest but skipped at registration — clients can't invoke; landing page renders it muted with the operator-facing reason and optional hint. Compose with feature-flag conditionals at definition time. |
185
181
 
186
182
  ### Context
187
183
 
@@ -215,7 +211,7 @@ import { createMockContext } from '@cyanheads/mcp-ts-core/testing';
215
211
  import { fuzzTool, fuzzResource, fuzzPrompt } from '@cyanheads/mcp-ts-core/testing/fuzz';
216
212
  ```
217
213
 
218
- See [CLAUDE.md](CLAUDE.md) for the complete exports reference.
214
+ See [CLAUDE.md/AGENTS.md](CLAUDE.md) for the complete exports reference.
219
215
 
220
216
  ## Examples
221
217
 
@@ -261,16 +257,18 @@ Also exports `fuzzResource`, `fuzzPrompt`, `zodToArbitrary`, and `ADVERSARIAL_ST
261
257
 
262
258
  ## Documentation
263
259
 
264
- - **[CLAUDE.md](CLAUDE.md)** — Framework reference: exports catalog, patterns, Context interface, error codes, auth, config, testing. Ships in the npm package.
265
- - **[CHANGELOG.md](CHANGELOG.md)** — Version history
260
+ - **[CLAUDE.md/AGENTS.md](CLAUDE.md)** — Framework reference: exports catalog, patterns, Context interface, error codes, auth, config, testing. Ships in the npm package.
261
+ - **[docs/telemetry/observability.md](docs/telemetry/observability.md)** — OpenTelemetry catalog: every span, metric, and attribute the framework emits, plus the env vars to wire export.
262
+ - **[docs/telemetry/dashboards.md](docs/telemetry/dashboards.md)** — Example Grafana dashboard JSON and vendor-agnostic query recipes (Datadog, New Relic, Honeycomb).
263
+ - **[CHANGELOG.md](CHANGELOG.md)** — Version history - Directory based for easier parsing by agents. Each entry includes a summary, migration notes, and links to commits/issues.
266
264
 
267
265
  ## Development
268
266
 
269
267
  ```bash
270
268
  bun run rebuild # clean + build (scripts/clean.ts + scripts/build.ts)
271
- bun run devcheck # lint, format, typecheck, MCP defs, audit, outdated
269
+ bun run devcheck # full gate: lint/format, typecheck, MCP defs, framework antipatterns, docs/skills/changelog sync, tests, audit, outdated, secrets/TODO scan
272
270
  bun run lint:mcp # validate MCP definitions against spec
273
- bun run test:all # vitest (unit + integration)
271
+ bun run test:all # vitest: unit + Workers pool + integration
274
272
  ```
275
273
 
276
274
  ## Contributing
@@ -0,0 +1,17 @@
1
+ ---
2
+ summary: "Fix `ctx.auth.token` strip in `toAuthContext` ([#121](https://github.com/cyanheads/mcp-ts-core/issues/121)) — typed `token?: string` on `AuthContext`, forwarded by `withAuthInfo` and the ALS bridge so handlers can relay the bearer upstream."
3
+ breaking: false
4
+ ---
5
+
6
+ # 0.8.18 — 2026-05-06
7
+
8
+ `toAuthContext` mapped `AuthInfo` → public `AuthContext` but silently dropped `info.token`, so `ctx.auth.token` was always `undefined` even though `claimParser.buildAuthInfoFromClaims()` set it from the verified JWT and the jwt/oauth strategies populated it. Tool handlers needing PAT pass-through / on-behalf-of forwarding had no public API path to the validated bearer (`authContext` ALS isn't re-exported), and the `[key: string]: unknown` index signature on `AuthContext` made `ctx.auth.token` compile cleanly — hiding the bug from typecheck and `createMockContext({ auth: { token } })` unit tests.
9
+
10
+ ## Fixed
11
+
12
+ - **`ctx.auth.token` reaches handlers** ([#121](https://github.com/cyanheads/mcp-ts-core/issues/121)) — `toAuthContext` in `src/utils/internal/requestContext.ts` now spreads `info.token` when present. Covers both call sites: `withAuthInfo` (initial request context, including auto-task `callerAuth` capture) and the ALS bridge in `createRequestContext` (handler-created child contexts). Logger pino-redact paths (`token`, `*.token`, `*.*.token`) already mask the field, so naive `ctx.log.info({ auth: ctx.auth })` doesn't leak the bearer.
13
+
14
+ ## Changed
15
+
16
+ - **`AuthContext.token`** — typed `token?: string` on the public interface. Consumers no longer need the `(ctx.auth as { token?: string }).token` cast.
17
+ - **Dependency bumps:** `hono` `^4.12.17` → `^4.12.18` (security release: GHSA-p77w-8qqv-26rm cache leakage, GHSA-qp7p-654g-cw7p `hono/jsx` CSS injection, GHSA-hm8q-7f3q-5f36 JWT `NumericDate`; framework doesn't use the affected surfaces, but the bumped peer floor protects downstream consumers); `chrono-node` `^2.9.0` → `^2.9.1`; `@opentelemetry/sdk-node` / `exporter-trace-otlp-http` / `exporter-metrics-otlp-http` / `instrumentation-http` `^0.216.0` → `^0.217.0`; `@opentelemetry/instrumentation-pino` `^0.62.0` → `^0.63.0`.
@@ -0,0 +1,33 @@
1
+ ---
2
+ summary: "Telemetry visualization docs ([#125](https://github.com/cyanheads/mcp-ts-core/issues/125)) — example Grafana dashboard JSON, vendor-agnostic query recipes, new `api-telemetry` skill. Engines bumped to Bun ≥1.3.0 / Node ≥24.0.0."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.8.19 — 2026-05-08
8
+
9
+ ## Added
10
+
11
+ - **`docs/telemetry/dashboards.md` + `mcp-ts-core-dashboard.json`** ([#125](https://github.com/cyanheads/mcp-ts-core/issues/125)) — example Grafana dashboard (54 panels, schemaVersion 39) plus an import quickstart and equivalent query recipes for Datadog / NRQL / Honeycomb. Single `$service` template variable, `(?:@[^/]+/)?` regex strips any npm-org prefix, no publisher-specific names. `docs/telemetry/observability.md` cross-links it. Framework-source-only — `docs/` is intentionally not shipped in the npm `dist`.
12
+ - **`api-telemetry` skill** (v1.0) — catalog of every span, metric, attribute, completion-log field, env var, and runtime caveat the framework emits. Cross-linked from `api-utils` (which now scopes to the helper API only) and from CLAUDE.md/AGENTS.md skill index.
13
+ - **Changelog frontmatter `security: boolean`** — pairs with the `## Security` section to render a `🛡️ Security` badge in the rollup. Both flags render in fixed order (`· ⚠️ Breaking · 🛡️ Security`) when set. `scripts/build-changelog.ts` parses and validates `security` like `breaking` (must be literal `true`/`false`).
14
+ - **`init` template substitutions** — `{{MCP_SDK_VERSION}}` and `{{ZOD_VERSION}}` now resolve from the framework `package.json` `dependencies` map. Joins existing `{{PACKAGE_NAME}}` and `{{FRAMEWORK_VERSION}}`.
15
+
16
+ ## Changed
17
+
18
+ - **Engines bumped:** Bun `>=1.2.0` → `>=1.3.0`, Node `>=22.0.0` → `>=24.0.0`. Mirrored in `templates/package.json` and `skills/polish-docs-meta/references/package-meta.md`.
19
+ - **Docker base image:** `oven/bun:1` → `oven/bun:1.3` (build + runtime stages, root and template Dockerfiles).
20
+ - **CLAUDE.md / AGENTS.md** — restructured: new "Consumers" section explains the package-import vs init-scaffolded paths up front; tool/format guidance condensed; standalone Git section dropped (the global protocol covers it). Now reflects 0.8.19 + Bun ≥1.3.0 / Node ≥24.0.0 in the header.
21
+ - **`changelog/template.md` / `templates/changelog/template.md`** — rewritten authoring guide: bold-the-symbol bullet style, Keep-a-Changelog section order, scaffolded `Deprecated` / `Removed` / `Security` sections (commented out by default).
22
+ - **`README.md`** — leaner feature list, reordered storage detail behind a single `node_modules` link, added cross-links to `docs/telemetry/observability.md` and `dashboards.md`.
23
+ - **Skill bumps:** `setup` 1.6 → 1.7 (rebrands `npx` examples to `bunx`, replaces `{{PACKAGE_NAME}}` placeholder guidance with substituted-name verification, adds `release-and-publish` to the rough progression). `maintenance` 2.0 → 2.1 — Phase C now also resyncs pristine reference files (`templates/changelog/template.md` → consumer `changelog/template.md`) on content-hash mismatch. `report-issue-framework` 1.5 → 1.6, `report-issue-local` 1.4 → 1.5 — terser issue-writing guidance, "cite cross-references once," Bun `1.3.x` examples. `api-utils` 2.1 → 2.2 — telemetry section header points readers to the new `api-telemetry` skill for the catalog.
24
+ - **Keywords:** added `bun`, `mcp-framework`; removed `edge`.
25
+ - **Dependency refresh:**
26
+ - `@hono/node-server` `^2.0.1` → `^2.0.2`
27
+ - `@cloudflare/vitest-pool-workers` `^0.16.0` → `^0.16.3`
28
+ - `@cloudflare/workers-types` `^4.20260506.1` → `^4.20260508.1`
29
+ - `@hono/otel` `^1.1.1` → `^1.1.2`
30
+ - `@types/node` `^25.6.0` → `^25.6.2`
31
+ - `openai` `^6.36.0` → `^6.37.0`
32
+ - `vite` `8.0.10` → `8.0.11`
33
+ - `fast-xml-parser` `^5.7.3` (new dev dep)
@@ -1,56 +1,71 @@
1
1
  ---
2
- # FORMAT REFERENCE — this file is never edited, never moved, never renamed.
3
- #
4
- # At release time, author a new per-version file at:
5
- # changelog/<major.minor>.x/<version>.md (e.g. changelog/0.6.x/0.6.6.md)
6
- # using this file's frontmatter and section layout as the starting point.
7
- # Set that new file's H1 to `# <version> — YYYY-MM-DD` with a concrete date.
8
-
9
- # Required. One-line headline describing the release. Max 250 chars. No markdown.
10
- # This line is what the CHANGELOG.md rollup shows write it like a GitHub Release title.
11
- # Keep the double quotes around the value — unquoted YAML treats `:` (colon-space)
12
- # inside the string as a key separator, which fails GitHub's strict YAML parser.
2
+ # FORMAT REFERENCE — do not edit. Copy this file to
3
+ # `changelog/<major.minor>.x/<version>.md` (e.g. `changelog/0.8.x/0.8.6.md`)
4
+ # to author a new release. Set that file's H1 to `# <version> — YYYY-MM-DD`
5
+ # with a concrete date.
6
+
7
+ # Required. One-line GitHub Release-style headline. ~250 character soft cap.
8
+ # Default short and scannable. Don't pad, don't stitch unrelated changes with
9
+ # semicolons pick the headline. Quotes required: unquoted YAML treats `: `
10
+ # inside the value as a key separator and fails GitHub's strict parser.
13
11
  summary: ""
14
12
 
15
- # Set to `true` only if this release has breaking changes (API removals, signature
16
- # changes, config renames, anything that requires consumer code changes on update).
17
- # Flagged as ⚠️ Breaking in the rollup.
13
+ # Set `true` when consumers must change code to upgrade: API removals,
14
+ # signature changes, config renames, behavior changes that break existing
15
+ # usage. Flagged as `Breaking` in the rollup.
18
16
  breaking: false
17
+
18
+ # Set `true` if this release contains any security fix. Pairs with the
19
+ # `## Security` section below. Flagged as `Security` in the rollup so
20
+ # users can triage upgrade urgency at a glance.
21
+ security: false
19
22
  ---
20
23
 
21
24
  # <version> — YYYY-MM-DD
22
25
 
23
26
  <!--
24
- FORMAT REFERENCEdo not edit this file. It exists so the per-version file
25
- structure has a single copy-source. Create new release notes at
26
- `changelog/<major.minor>.x/<version>.md` using this layout.
27
-
28
- Optional narrative intro 1-3 sentences framing the release theme. Delete if not needed.
29
-
30
- TONE: terse and fact-dense. 1-2 sentence(s) per bullet where possible —
31
- name the symbol, state what changed, stop. Drop "explains how it works" prose;
32
- that belongs in JSDoc, AGENTS.md, or the relevant skill. Drop ceremonial
33
- framings ("This release introduces…", "fully backwards compatible:" with a
34
- paragraph of justification). Prefer code/symbol names over English
35
- re-explanations. If a bullet runs more than ~2 lines, split it or cut it.
36
-
37
- WHAT TO INCLUDE: every distinct fact a reader needs to adopt or audit the
38
- release new exports, signatures, lint rule IDs, env vars, breaking
39
- changes, version bumps on shipped skills. WHAT TO CUT: mechanism walkthroughs,
40
- duplicate prose between Added and Changed, file-by-file test enumerations,
41
- internal implementation notes. Trust the reader to read the code or the docs.
42
-
43
- Linking issues/PRs: use full URLs so the link works everywhere (GitHub web UI,
44
- npm/node_modules reads, local editors). GitHub's bare `#NN` auto-link only
45
- resolves inside its own UI.
46
-
47
- [#38](https://github.com/cyanheads/mcp-ts-core/issues/38) ← issue
48
- [#42](https://github.com/cyanheads/mcp-ts-core/pull/42) ← PR
49
-
50
- Only link numbers you've verified exist (via `gh issue view NN` or
51
- `gh pr view NN`). Never speculate on a future number `#42` for "my
52
- upcoming PR" will quietly resolve to whatever real item already owns 42,
53
- and GitHub timeline previews will pull in that unrelated item's title.
27
+ AUTHORING GUIDEapplies to the new per-version file you create from this
28
+ template.
29
+
30
+ Audience: someone scanning release notes to decide what affects them. Lead
31
+ each bullet with the symbol or concept name in **bold** so they can skip
32
+ what's irrelevant and zoom in on what's not.
33
+
34
+ Tone: terse, fact-dense, not verbose. Default to one sentence per bullet
35
+ name the symbol, state what changed, stop. Use a second sentence only when
36
+ it carries weight. If a bullet feels long, it is.
37
+
38
+ Cut: mechanism walkthroughs (those belong in JSDoc, AGENTS.md, or the
39
+ relevant skill), ceremonial framings ("This release introduces…",
40
+ backwards-compat paragraphs), file-by-file test enumerations, internal
41
+ implementation notes. Prefer code/symbol names over English re-explanations.
42
+
43
+ Narrative intro: skip by default. Add one short sentence only when the
44
+ release theme genuinely needs framing the bullets can't carry.
45
+
46
+ Sections: Keep a Changelog order Added, Changed, Deprecated, Removed,
47
+ Fixed, Security. Include only sections with entries; delete the rest
48
+ (including the commented-out scaffolding below). Don't ship empty headers.
49
+
50
+ Include: every distinct fact a reader needs to adopt or audit the release —
51
+ new exports, signatures, lint rule IDs, env vars, breaking changes, version
52
+ bumps on shipped skills. Nothing more.
53
+
54
+ Links: link issues, PRs, docs, or skills where they help a reader jump to
55
+ context. Once per item per entry don't re-link the same issue in summary,
56
+ narrative, and bullet. Skip links for inline symbol names; code spans speak
57
+ for themselves.
58
+
59
+ Issue/PR URLs: use full URLs. GitHub's bare `#NN` auto-link only resolves
60
+ inside its own UI, not in npm reads or local editors.
61
+
62
+ [#38](https://github.com/cyanheads/mcp-ts-core/issues/38) ← issue
63
+ [#42](https://github.com/cyanheads/mcp-ts-core/pull/42) ← PR
64
+
65
+ Verify numbers exist before linking (`gh issue view NN`, `gh pr view NN`).
66
+ Never speculate on a future number — `#42` for an upcoming PR silently
67
+ resolves to whatever real item already owns 42, and timeline previews pull
68
+ in that unrelated item's metadata.
54
69
  -->
55
70
 
56
71
  ## Added
@@ -61,6 +76,18 @@ breaking: false
61
76
 
62
77
  -
63
78
 
79
+ <!-- ## Deprecated
80
+
81
+ - -->
82
+
83
+ <!-- ## Removed
84
+
85
+ - -->
86
+
64
87
  ## Fixed
65
88
 
66
89
  -
90
+
91
+ <!-- ## Security
92
+
93
+ - -->
package/dist/cli/init.js CHANGED
@@ -66,10 +66,16 @@ function init() {
66
66
  mkdirSync(dest, { recursive: true });
67
67
  }
68
68
  console.log(`\n Scaffolding${name ? ` ${name}` : ''} in ${dest}\n`);
69
+ const substitutions = {
70
+ PACKAGE_NAME: packageName,
71
+ FRAMEWORK_VERSION: PACKAGE_JSON.version,
72
+ MCP_SDK_VERSION: PACKAGE_JSON.dependencies?.['@modelcontextprotocol/sdk'] ?? '',
73
+ ZOD_VERSION: PACKAGE_JSON.dependencies?.zod ?? '',
74
+ };
69
75
  const created = [];
70
76
  const skipped = [];
71
77
  // Step 1: Copy templates
72
- copyTemplates(dest, packageName, PACKAGE_JSON.version, created, skipped);
78
+ copyTemplates(dest, substitutions, created, skipped);
73
79
  // Step 2: Copy scripts
74
80
  copyScripts(dest, created, skipped);
75
81
  // Step 3: Copy external skills
@@ -78,7 +84,7 @@ function init() {
78
84
  printSummary(created, skipped, name);
79
85
  }
80
86
  // ── Template copying ──────────────────────────────────────────────────
81
- function copyTemplates(dest, name, frameworkVersion, created, skipped) {
87
+ function copyTemplates(dest, substitutions, created, skipped) {
82
88
  const entries = walkDir(TEMPLATES_DIR);
83
89
  for (const srcPath of entries) {
84
90
  let relPath = relative(TEMPLATES_DIR, srcPath);
@@ -94,9 +100,10 @@ function copyTemplates(dest, name, frameworkVersion, created, skipped) {
94
100
  continue;
95
101
  }
96
102
  mkdirSync(dirname(destPath), { recursive: true });
97
- const content = readFileSync(srcPath, 'utf-8')
98
- .replace(/\{\{PACKAGE_NAME\}\}/g, name)
99
- .replace(/\{\{FRAMEWORK_VERSION\}\}/g, frameworkVersion);
103
+ let content = readFileSync(srcPath, 'utf-8');
104
+ for (const [key, value] of Object.entries(substitutions)) {
105
+ content = content.replaceAll(`{{${key}}}`, value);
106
+ }
100
107
  writeFileSync(destPath, content);
101
108
  created.push(relPath);
102
109
  }