@cyanheads/workflows-mcp-server 0.3.0 → 0.3.1

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/AGENTS.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/workflows-mcp-server
4
- **Version:** 0.3.0
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.3`
4
+ **Version:** 0.3.1
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.5`
6
6
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
8
- **Zod:** ^4.4.3
8
+ **Zod:** ^4.5.4
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
11
11
 
@@ -112,6 +112,14 @@ export function getServerConfig() {
112
112
 
113
113
  `parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`WORKFLOWS_DIR`) not the path (`workflowsDir`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
114
114
 
115
+ For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
116
+
117
+ ### Server identity
118
+
119
+ Framework identity (`name`, `version`, `description`, `keywords`) and a relative `LOGS_DIR` resolve against the **application root** — the nearest `package.json` at or above the process entry module — never the launching client's working directory. Server-specific paths like `WORKFLOWS_DIR` are this server's own config and still resolve against `process.cwd()`, so a relative default points at the caller's workflow library rather than the installed package.
120
+
121
+ `createApp()` declares `name` + `title` only. `description`, `version`, and `keywords` derive from `package.json` — restating them in the call is drift, not configuration. `instructions` is optional server-level orientation sent on every `initialize`; use it for deployment guidance instead of repeating the same context across tool descriptions.
122
+
115
123
  ---
116
124
 
117
125
  ## Context
@@ -120,12 +128,15 @@ Handlers receive a unified `ctx` object. Key properties:
120
128
 
121
129
  | Property | Description |
122
130
  |:---------|:------------|
123
- | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
124
- | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. |
131
+ | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
132
+ | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
133
+ | `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present, but a 2025-era HTTP client cannot answer under `MCP_SESSION_MODE=stateless` — treat an unanswered round as terminal, never as consent. |
134
+ | `ctx.inputs` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. Empty on the first round. |
125
135
  | `ctx.enrich` | Success-path agent context that reaches both `structuredContent` and `content[]` when the definition declares an `enrichment` block. |
136
+ | `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
126
137
  | `ctx.signal` | `AbortSignal` for cancellation. |
127
138
  | `ctx.requestId` | Unique request ID. |
128
- | `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio. |
139
+ | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
129
140
  | `ctx.fail` | Typed error factory for declared error contracts — `ctx.fail('reason', msg, ctx.recoveryFor('reason'))`. |
130
141
 
131
142
  ---
@@ -134,7 +145,7 @@ Handlers receive a unified `ctx` object. Key properties:
134
145
 
135
146
  Handlers throw — the framework catches, classifies, and formats.
136
147
 
137
- **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 (≥ 5 words, lint-validated) and is the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`data.recovery.hint`, mirrored into `content[]` text); override it explicitly when dynamic runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
148
+ **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 (≥ 5 words, lint-validated) and is the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`data.recovery.hint`, mirrored into `content[]` text); override it explicitly when dynamic runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
138
149
 
139
150
  ```ts
140
151
  errors: [
@@ -234,7 +245,7 @@ Available skills:
234
245
  | `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
235
246
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
236
247
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
237
- | `devcheck` | Lint, format, typecheck, audit |
248
+ | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
238
249
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
239
250
  | `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
240
251
  | `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
@@ -268,9 +279,14 @@ When you complete a skill's checklist, check the boxes and add a completion time
268
279
  | `bun run clean` | Remove build artifacts |
269
280
  | `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
270
281
  | `bun run audit:refresh` | Delete `bun.lock`, reinstall, re-audit. Use when `devcheck` flags a transitive advisory — stale lockfile can mask already-patched deps. If advisory survives, it's real. |
282
+ | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
283
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
284
+ | `bun run list-skills` | Print the skill registry |
271
285
  | `bun run tree` | Generate directory structure doc |
272
- | `bun run format` | Auto-fix formatting |
273
- | `bun run test` | Run tests |
286
+ | `bun run format` | Auto-fix formatting (safe fixes only) |
287
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
288
+ | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
289
+ | `bun run test:coverage` | Run tests with Istanbul coverage |
274
290
  | `bun run start:stdio` | Production mode (stdio) |
275
291
  | `bun run start:http` | Production mode (HTTP) |
276
292
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
@@ -281,44 +297,11 @@ When you complete a skill's checklist, check the boxes and add a completion time
281
297
 
282
298
  ## Bundling
283
299
 
284
- `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
300
+ `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
285
301
 
286
302
  **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.
287
303
 
288
- **README install badges.** Drop these into the project README to give users one-click install paths. Fill in `<OWNER>` / `<REPO>` / `<PACKAGE_NAME>` and encode the per-server config. Cursor + VS Code badges assume the server is published to npm; Claude Desktop downloads the `.mcpb` directly so npm publishing isn't required.
289
-
290
- | Client | Mechanism |
291
- |:-------|:----------|
292
- | 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. |
293
- | Cursor | Official `https://cursor.com/en/install-mcp` endpoint with base64 JSON config. |
294
- | 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. |
295
- | Claude Code / Codex | CLI only (`claude mcp add` / `codex mcp add`); no URL scheme. |
296
-
297
- ```markdown
298
- [![Install in Claude Desktop](https://img.shields.io/badge/Install_in-Claude_Desktop-D97757?style=for-the-badge&logo=anthropic&logoColor=white)](https://github.com/<OWNER>/<REPO>/releases/latest/download/<PACKAGE_NAME>.mcpb)
299
- [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=<PACKAGE_NAME>&config=<BASE64_CONFIG>)
300
- [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=for-the-badge&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?<URLENCODED_JSON>)
301
- ```
302
-
303
- 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.
304
-
305
- Generate the encoded configs (replace `<PACKAGE_NAME>` and add env vars for any required API keys):
306
-
307
- ```bash
308
- # Cursor: base64-encoded JSON. Split command/args, add env when keys are needed.
309
- echo -n '{"command":"npx","args":["-y","<PACKAGE_NAME>"],"env":{"API_KEY":"your-api-key"}}' | base64
310
- # Without env (no required keys):
311
- echo -n '{"command":"npx","args":["-y","<PACKAGE_NAME>"]}' | base64
312
-
313
- # VS Code: URL-encoded JSON. Same shape plus a `name` field.
314
- node -p 'encodeURIComponent(JSON.stringify({name:"<SHORT_NAME>",command:"npx",args:["-y","<PACKAGE_NAME>"],env:{API_KEY:"your-api-key"}}))'
315
- # Without env:
316
- node -p 'encodeURIComponent(JSON.stringify({name:"<SHORT_NAME>",command:"npx",args:["-y","<PACKAGE_NAME>"]}))'
317
- ```
318
-
319
- 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`.
320
-
321
- 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.
304
+ **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`.
322
305
 
323
306
  ---
324
307
 
@@ -332,14 +315,14 @@ Each per-version file opens with YAML frontmatter:
332
315
  ---
333
316
  summary: "One-line headline, ≤350 chars" # required — powers the rollup index
334
317
  breaking: false # optional — true flags breaking changes
335
- security: false # optional — true flags security fixes
318
+ security: false # optional — true ONLY for a source-code security fix, never a dependency CVE bump
336
319
  ---
337
320
 
338
321
  # 0.1.0 — YYYY-MM-DD
339
322
  ...
340
323
  ```
341
324
 
342
- `breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section. When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
325
+ `breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section — set it only for a security fix in this server's *own source code*, never for a routine dependency or transitive CVE bump (record those under `## Dependencies`). When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
343
326
 
344
327
  `agent-notes` is an optional free-form field for maintenance agents processing the release downstream. Content here won't appear in the rendered CHANGELOG — it's consumed by agents running the `maintenance` skill. Use it for adoption instructions that don't fit the human-facing sections: new files to create, fields to populate, one-time migration steps. Omit entirely when there's nothing to say.
345
328
 
package/CLAUDE.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** @cyanheads/workflows-mcp-server
4
- **Version:** 0.3.0
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.3`
4
+ **Version:** 0.3.1
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.12.5`
6
6
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
8
- **Zod:** ^4.4.3
8
+ **Zod:** ^4.5.4
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
11
11
 
@@ -112,6 +112,14 @@ export function getServerConfig() {
112
112
 
113
113
  `parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`WORKFLOWS_DIR`) not the path (`workflowsDir`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
114
114
 
115
+ For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
116
+
117
+ ### Server identity
118
+
119
+ Framework identity (`name`, `version`, `description`, `keywords`) and a relative `LOGS_DIR` resolve against the **application root** — the nearest `package.json` at or above the process entry module — never the launching client's working directory. Server-specific paths like `WORKFLOWS_DIR` are this server's own config and still resolve against `process.cwd()`, so a relative default points at the caller's workflow library rather than the installed package.
120
+
121
+ `createApp()` declares `name` + `title` only. `description`, `version`, and `keywords` derive from `package.json` — restating them in the call is drift, not configuration. `instructions` is optional server-level orientation sent on every `initialize`; use it for deployment guidance instead of repeating the same context across tool descriptions.
122
+
115
123
  ---
116
124
 
117
125
  ## Context
@@ -120,12 +128,15 @@ Handlers receive a unified `ctx` object. Key properties:
120
128
 
121
129
  | Property | Description |
122
130
  |:---------|:------------|
123
- | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
124
- | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. |
131
+ | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
132
+ | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
133
+ | `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present, but a 2025-era HTTP client cannot answer under `MCP_SESSION_MODE=stateless` — treat an unanswered round as terminal, never as consent. |
134
+ | `ctx.inputs` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. Empty on the first round. |
125
135
  | `ctx.enrich` | Success-path agent context that reaches both `structuredContent` and `content[]` when the definition declares an `enrichment` block. |
136
+ | `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
126
137
  | `ctx.signal` | `AbortSignal` for cancellation. |
127
138
  | `ctx.requestId` | Unique request ID. |
128
- | `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio. |
139
+ | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
129
140
  | `ctx.fail` | Typed error factory for declared error contracts — `ctx.fail('reason', msg, ctx.recoveryFor('reason'))`. |
130
141
 
131
142
  ---
@@ -134,7 +145,7 @@ Handlers receive a unified `ctx` object. Key properties:
134
145
 
135
146
  Handlers throw — the framework catches, classifies, and formats.
136
147
 
137
- **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 (≥ 5 words, lint-validated) and is the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`data.recovery.hint`, mirrored into `content[]` text); override it explicitly when dynamic runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
148
+ **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 (≥ 5 words, lint-validated) and is the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`data.recovery.hint`, mirrored into `content[]` text); override it explicitly when dynamic runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
138
149
 
139
150
  ```ts
140
151
  errors: [
@@ -234,7 +245,7 @@ Available skills:
234
245
  | `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
235
246
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
236
247
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
237
- | `devcheck` | Lint, format, typecheck, audit |
248
+ | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
238
249
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
239
250
  | `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
240
251
  | `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
@@ -268,9 +279,14 @@ When you complete a skill's checklist, check the boxes and add a completion time
268
279
  | `bun run clean` | Remove build artifacts |
269
280
  | `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
270
281
  | `bun run audit:refresh` | Delete `bun.lock`, reinstall, re-audit. Use when `devcheck` flags a transitive advisory — stale lockfile can mask already-patched deps. If advisory survives, it's real. |
282
+ | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
283
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
284
+ | `bun run list-skills` | Print the skill registry |
271
285
  | `bun run tree` | Generate directory structure doc |
272
- | `bun run format` | Auto-fix formatting |
273
- | `bun run test` | Run tests |
286
+ | `bun run format` | Auto-fix formatting (safe fixes only) |
287
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
288
+ | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
289
+ | `bun run test:coverage` | Run tests with Istanbul coverage |
274
290
  | `bun run start:stdio` | Production mode (stdio) |
275
291
  | `bun run start:http` | Production mode (HTTP) |
276
292
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
@@ -281,44 +297,11 @@ When you complete a skill's checklist, check the boxes and add a completion time
281
297
 
282
298
  ## Bundling
283
299
 
284
- `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
300
+ `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
285
301
 
286
302
  **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.
287
303
 
288
- **README install badges.** Drop these into the project README to give users one-click install paths. Fill in `<OWNER>` / `<REPO>` / `<PACKAGE_NAME>` and encode the per-server config. Cursor + VS Code badges assume the server is published to npm; Claude Desktop downloads the `.mcpb` directly so npm publishing isn't required.
289
-
290
- | Client | Mechanism |
291
- |:-------|:----------|
292
- | 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. |
293
- | Cursor | Official `https://cursor.com/en/install-mcp` endpoint with base64 JSON config. |
294
- | 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. |
295
- | Claude Code / Codex | CLI only (`claude mcp add` / `codex mcp add`); no URL scheme. |
296
-
297
- ```markdown
298
- [![Install in Claude Desktop](https://img.shields.io/badge/Install_in-Claude_Desktop-D97757?style=for-the-badge&logo=anthropic&logoColor=white)](https://github.com/<OWNER>/<REPO>/releases/latest/download/<PACKAGE_NAME>.mcpb)
299
- [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=<PACKAGE_NAME>&config=<BASE64_CONFIG>)
300
- [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=for-the-badge&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?<URLENCODED_JSON>)
301
- ```
302
-
303
- 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.
304
-
305
- Generate the encoded configs (replace `<PACKAGE_NAME>` and add env vars for any required API keys):
306
-
307
- ```bash
308
- # Cursor: base64-encoded JSON. Split command/args, add env when keys are needed.
309
- echo -n '{"command":"npx","args":["-y","<PACKAGE_NAME>"],"env":{"API_KEY":"your-api-key"}}' | base64
310
- # Without env (no required keys):
311
- echo -n '{"command":"npx","args":["-y","<PACKAGE_NAME>"]}' | base64
312
-
313
- # VS Code: URL-encoded JSON. Same shape plus a `name` field.
314
- node -p 'encodeURIComponent(JSON.stringify({name:"<SHORT_NAME>",command:"npx",args:["-y","<PACKAGE_NAME>"],env:{API_KEY:"your-api-key"}}))'
315
- # Without env:
316
- node -p 'encodeURIComponent(JSON.stringify({name:"<SHORT_NAME>",command:"npx",args:["-y","<PACKAGE_NAME>"]}))'
317
- ```
318
-
319
- 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`.
320
-
321
- 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.
304
+ **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`.
322
305
 
323
306
  ---
324
307
 
@@ -332,14 +315,14 @@ Each per-version file opens with YAML frontmatter:
332
315
  ---
333
316
  summary: "One-line headline, ≤350 chars" # required — powers the rollup index
334
317
  breaking: false # optional — true flags breaking changes
335
- security: false # optional — true flags security fixes
318
+ security: false # optional — true ONLY for a source-code security fix, never a dependency CVE bump
336
319
  ---
337
320
 
338
321
  # 0.1.0 — YYYY-MM-DD
339
322
  ...
340
323
  ```
341
324
 
342
- `breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section. When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
325
+ `breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section — set it only for a security fix in this server's *own source code*, never for a routine dependency or transitive CVE bump (record those under `## Dependencies`). When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
343
326
 
344
327
  `agent-notes` is an optional free-form field for maintenance agents processing the release downstream. Content here won't appear in the rendered CHANGELOG — it's consumed by agents running the `maintenance` skill. Use it for adoption instructions that don't fit the human-facing sections: new files to create, fields to populate, one-time migration steps. Omit entirely when there's nothing to say.
345
328
 
package/Dockerfile CHANGED
@@ -3,6 +3,16 @@
3
3
  #
4
4
  # This stage installs all dependencies (including dev), builds the TypeScript
5
5
  # source code into JavaScript, and prepares the production assets.
6
+ #
7
+ # Pinned to $BUILDPLATFORM rather than the target platform: `bun run build` emits
8
+ # JavaScript, and only `dist/` crosses into the production stage, which runs its
9
+ # own target-arch install. Built for the target instead, the non-native leg of a
10
+ # `--platform linux/amd64,linux/arm64` build runs under QEMU, where bun >= 1.4
11
+ # aborts with a JavaScriptCore allocator assertion and fails the multi-arch push.
12
+ #
13
+ # The constraint this assumes: the build stage produces platform-independent
14
+ # output. A stage that compiles a native addon needs the target-arch toolchain
15
+ # and cannot cross-compile this way — drop the flag there.
6
16
  # ==============================================================================
7
17
  FROM --platform=$BUILDPLATFORM oven/bun:1.4.0 AS build
8
18
 
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.3.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/workflows-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-v2-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/workflows-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/workflows-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.3.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/workflows-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-v2-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/workflows-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/workflows-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -0,0 +1,33 @@
1
+ ---
2
+ summary: "Server identity now resolves from the served package instead of the caller's working directory, pre-init logs are no longer dropped, and MCP_SESSION_MODE settles to stateless. mcp-ts-core bumps to 0.12.5, zod to 4.5.4."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.3.1 — 2026-09-04
8
+
9
+ ## Changed
10
+
11
+ - **`.env.example` `MCP_SESSION_MODE`: `auto` → `stateless`**, matching the Docker image — this server declares no `ctx.requestInput` call sites and so needs no session-backed 2025-era elicitation shim. Verified: `GET /mcp` now advertises `sessionMode: "stateless"`.
12
+ - **Server identity resolves from the served package, not the caller's working directory** (mcp-ts-core 0.12.5) — `resolveAppRoot()` reads the nearest `package.json` above the process entry module. Verified: launched from a scratch directory whose own `package.json` declared an unrelated project, `GET /mcp` still returned this server's own name, version, and description.
13
+ - **Pre-init log records are replayed instead of dropped** (mcp-ts-core 0.12.5) — verified in the startup log: `createStorageProvider` and `OpenRouterProvider.constructor` records now appear despite predating `loggerInit`.
14
+ - A caller disconnect mid-call now classifies as `RequestCancelled` (-32011) rather than `InternalError`, and the HTTP transport answers 499 (mcp-ts-core 0.12.4).
15
+ - `manifest.json` `author.name`: `Casey Hand` → `cyanheads`, matching fleet convention.
16
+ - Dockerfile build stage carries a new comment explaining why it pins `$BUILDPLATFORM`.
17
+ - Skills and agent docs synced with mcp-ts-core 0.12.5 — 11 `skills/` files plus the `CLAUDE.md`/`AGENTS.md` templates.
18
+
19
+ ## Dependencies
20
+
21
+ Runtime:
22
+
23
+ - `@cyanheads/mcp-ts-core` `^0.12.3` → `^0.12.5`
24
+ - `zod` `^4.4.3` → `^4.5.4`
25
+
26
+ Development:
27
+
28
+ - `@biomejs/biome` `2.5.9` → `2.5.11`
29
+ - `@types/node` `26.2.0` → `26.4.0`
30
+ - `ignore` `^7.0.6` → `^7.0.8`
31
+ - `tsc-alias` `^1.9.2` → `^1.9.3`
32
+
33
+ The lockfile re-resolve also cleared 8 transitive advisories (`fast-uri` `3.1.5` → `3.1.6`, `browserslist` `4.28.4` → `4.28.8`, `qs` `6.15.3` → `6.16.0`); `bun audit` now reports no vulnerabilities across 375 packages.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/workflows-mcp-server",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "mcpName": "io.github.cyanheads/workflows-mcp-server",
5
5
  "description": "Store, query, and create YAML workflow playbooks for LLM agents via MCP. STDIO or Streamable HTTP.",
6
6
  "type": "module",
@@ -85,22 +85,22 @@
85
85
  "access": "public"
86
86
  },
87
87
  "dependencies": {
88
- "@cyanheads/mcp-ts-core": "^0.12.3",
88
+ "@cyanheads/mcp-ts-core": "^0.12.5",
89
89
  "pino-pretty": "^13.1.3",
90
90
  "semver": "^7.8.5",
91
91
  "yaml": "^2.9.0",
92
- "zod": "^4.4.3"
92
+ "zod": "^4.5.4"
93
93
  },
94
94
  "devDependencies": {
95
- "@biomejs/biome": "2.5.9",
95
+ "@biomejs/biome": "2.5.11",
96
96
  "@socketsecurity/bun-security-scanner": "^1.1.2",
97
- "@types/node": "26.2.0",
97
+ "@types/node": "26.4.0",
98
98
  "@types/semver": "^7.8.0",
99
99
  "@vitest/coverage-istanbul": "4.1.11",
100
100
  "depcheck": "^1.4.7",
101
101
  "fast-check": "^4.9.0",
102
- "ignore": "^7.0.6",
103
- "tsc-alias": "^1.9.2",
102
+ "ignore": "^7.0.8",
103
+ "tsc-alias": "^1.9.3",
104
104
  "typescript": "^7.0.2",
105
105
  "vitest": "^4.1.11"
106
106
  }
package/server.json CHANGED
@@ -6,14 +6,14 @@
6
6
  "url": "https://github.com/cyanheads/workflows-mcp-server",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.3.0",
9
+ "version": "0.3.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "registryBaseUrl": "https://registry.npmjs.org",
14
14
  "identifier": "@cyanheads/workflows-mcp-server",
15
15
  "runtimeHint": "bun",
16
- "version": "0.3.0",
16
+ "version": "0.3.1",
17
17
  "packageArguments": [
18
18
  { "type": "positional", "value": "run" },
19
19
  { "type": "positional", "value": "start:stdio" }
@@ -57,7 +57,7 @@
57
57
  "registryBaseUrl": "https://registry.npmjs.org",
58
58
  "identifier": "@cyanheads/workflows-mcp-server",
59
59
  "runtimeHint": "bun",
60
- "version": "0.3.0",
60
+ "version": "0.3.1",
61
61
  "packageArguments": [
62
62
  { "type": "positional", "value": "run" },
63
63
  { "type": "positional", "value": "start:http" }