@cyanheads/mcp-ts-core 0.10.5 → 0.10.6

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,7 +1,7 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Package:** `@cyanheads/mcp-ts-core`
4
- **Version:** 0.10.5
4
+ **Version:** 0.10.6
5
5
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
6
6
  **MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
7
7
  **Zod:** ^4.4.3
package/CLAUDE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Package:** `@cyanheads/mcp-ts-core`
4
- **Version:** 0.10.5
4
+ **Version:** 0.10.6
5
5
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
6
6
  **MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
7
7
  **Zod:** ^4.4.3
@@ -1,5 +1,5 @@
1
1
  ---
2
- summary: "mcpTest fixture-based Vitest subpath; platform-native utils sweep under Node≥24/Bun≥1.3 (AbortSignal.any, Uint8Array base64, Symbol.Dispose, performance.now); workerd R2/D1 storage suites; typecheck project with @ts-expect-error negative cases; R2 list cap fix"
2
+ summary: "mcpTest fixture-based Vitest subpath; platform-native utils sweep under Node≥24/Bun≥1.3 (AbortSignal.any, Uint8Array base64, Symbol.dispose, performance.now); workerd R2/D1 storage suites; typecheck project with @ts-expect-error negative cases; R2 list cap fix"
3
3
  breaking: false
4
4
  security: false
5
5
  agent-notes: |
@@ -19,8 +19,8 @@ agent-notes: |
19
19
  - **`CanvasRegistry[Symbol.asyncDispose]`** — enables `await using registry = new CanvasRegistry(...)`. ([#216](https://github.com/cyanheads/mcp-ts-core/issues/216))
20
20
  - **Workerd R2 storage suite** (`tests/worker/storage-r2.worker.test.ts`) — set/get/delete/list/TTL exercised through the worker handler via miniflare `r2Buckets` binding. ([#228](https://github.com/cyanheads/mcp-ts-core/issues/228))
21
21
  - **Workerd D1 storage suite** (`tests/worker/storage-d1.worker.test.ts`) — set/get/delete/list/TTL exercised through the worker handler via miniflare `d1Databases` binding. ([#228](https://github.com/cyanheads/mcp-ts-core/issues/228))
22
- - **Encoding worker tests** (`tests/worker/encoding.worker.test.ts`) — `arrayBufferToBase64` / `base64ToString` verified in the workerd runtime. ([#228](https://github.com/cyanheads/mcp-ts-core/issues/228))
23
- - **`skills/api-workers` `v1.3`** — documents the `r2Buckets` / `d1Databases` miniflare binding pattern for workerd storage tests. ([#228](https://github.com/cyanheads/mcp-ts-core/issues/228))
22
+ - **Encoding worker tests** (`tests/worker/encoding.worker.test.ts`) — `arrayBufferToBase64` / `base64ToString` verified in the workerd runtime. ([#216](https://github.com/cyanheads/mcp-ts-core/issues/216))
23
+ - **`skills/api-workers` `v1.5`** — documents the `r2Buckets` / `d1Databases` miniflare binding pattern for workerd storage tests. ([#228](https://github.com/cyanheads/mcp-ts-core/issues/228))
24
24
  - **`skills/api-testing` `v1.5`** — documents `mcpTest`, `McpTestFixtures`, and the `./testing/vitest` subpath. ([#227](https://github.com/cyanheads/mcp-ts-core/issues/227))
25
25
 
26
26
  ## Changed
@@ -34,12 +34,12 @@ agent-notes: |
34
34
  - **`templates/tests/tools/echo.tool.test.ts`** — updated to use `mcpTest` from `@cyanheads/mcp-ts-core/testing/vitest`. ([#227](https://github.com/cyanheads/mcp-ts-core/issues/227))
35
35
  - **`export-map` allowlist test** — updated to include the `./testing/vitest` subpath. ([#227](https://github.com/cyanheads/mcp-ts-core/issues/227))
36
36
  - **`optional-peer-deps` test** — adds an integration-entrypoint exemption for `./testing/vitest` (its `vitest` peer is expected). ([#227](https://github.com/cyanheads/mcp-ts-core/issues/227))
37
- - **`worker-runtime.fixture`** — expanded with `r2Buckets` / `d1Databases` miniflare bindings for storage test suites. ([#228](https://github.com/cyanheads/mcp-ts-core/issues/228))
37
+ - **`worker-runtime.fixture`** — adds `storage_set` / `storage_get` / `storage_delete` / `storage_list` probe tools backing the workerd storage suites; the miniflare `r2Buckets` / `d1Databases` bindings live in `vitest.worker.ts`. ([#228](https://github.com/cyanheads/mcp-ts-core/issues/228))
38
38
 
39
39
  ## Fixed
40
40
 
41
41
  - **R2 `list()` cap** — `limit + 1` page-probe clamped to `R2_MAX_LIST_LIMIT = 1000`; R2 rejects requests above this cap. At the limit, pagination falls back to `listed.truncated` for has-more detection. ([#228](https://github.com/cyanheads/mcp-ts-core/issues/228))
42
- - **`lintCappedListTruncation` allowlist check** — guarded with `Array.isArray()` before calling `.includes()`; previously crashed on non-array `truncationAllowlist` values.
42
+ - **`lintCappedListTruncation` allowlist check** — truthiness guard replaced with `Array.isArray()` before calling `.includes()` (lint conformance; behavior unchanged).
43
43
 
44
44
  ## Deprecated
45
45
 
@@ -0,0 +1,37 @@
1
+ ---
2
+ summary: "Post-pack bundle cleaner strips dependency-shipped agent docs; packaging linter adds post-bundle content and entrypoint identity checks; template scaffolds name/title pair"
3
+ breaking: false
4
+ security: false
5
+ agent-notes: |
6
+ New file to sync: `scripts/clean-mcpb.ts` is now listed in `package.json` `files[]` and is invoked by the template `bundle` script after `mcpb pack`. Consumer servers that previously added the clean-mcpb script manually should verify their `bundle` script matches the template (`bun run build && npx -y @anthropic-ai/mcpb pack . dist/{{PACKAGE_NAME}}.mcpb && bun run scripts/clean-mcpb.ts dist/{{PACKAGE_NAME}}.mcpb`).
7
+
8
+ The `bundle` step for newly-scaffolded servers now includes the clean step automatically. Existing servers using the old `bundle` script (without `scripts/clean-mcpb.ts`) should update `package.json` to match the template.
9
+
10
+ Identity adoption (check 9): add explicit `name: '<unscoped-package-name>'` and `title: '<unscoped-package-name>'` to `createApp()` / `createWorkerHandler()` in `src/index.ts` (and `src/worker.ts` if present). `devcheck` now warns when the pair is partial and errors when a literal is set to the wrong value. Run `bun run lint:packaging` to confirm.
11
+
12
+ `templates/manifest.json` no longer includes an empty `repository` stub — `mcpb pack` rejects it on fresh scaffolds. Existing servers: remove `"repository": { "type": "git", "url": "" }` from `manifest.json` if present.
13
+
14
+ Re-sync the `skills/polish-docs-meta` skill (now v2.7) — it documents the `name`/`title` identity requirement and the updated `bundle` script description.
15
+ ---
16
+
17
+ # 0.10.6 — 2026-06-11
18
+
19
+ ## Added
20
+
21
+ - **`scripts/clean-mcpb.ts`** — post-pack bundle cleaner: runs `mcpb clean` (dev-dependency prune + manifest validation), then strips `node_modules/**` agent-doc entries (`skills/`, `.claude/`, `.agents/` trees and `SKILL.md` files) that root-anchored `.mcpbignore` patterns cannot reach. Batched `zip -d -nw` for literal matching; re-lists and asserts zero matches remain. ([#230](https://github.com/cyanheads/mcp-ts-core/issues/230))
22
+ - **`lint-packaging.ts` check 8** — post-bundle content guard: lists `.mcpb` files under `dist/` with `unzip -Z1` and flags any `node_modules/**` agent-doc entries, directing to `scripts/clean-mcpb.ts`. Cross-script regex `AGENT_DOC_ENTRY` is exported from both files; a unit test asserts they are identical. ([#230](https://github.com/cyanheads/mcp-ts-core/issues/230))
23
+ - **`lint-packaging.ts` check 9** — identity checks: `name`/`title` string literals in `createApp()` / `createWorkerHandler()` (`src/index.ts`, `src/worker.ts`) and `manifest.json` `display_name` must equal the unscoped `package.json` name; partial `name`/`title` pair warns without failing. Line-based brace-depth parser excludes nested object literals (setup bodies, extension configs) to avoid false positives. ([#231](https://github.com/cyanheads/mcp-ts-core/issues/231))
24
+ - **`scripts/clean-mcpb.ts` added to `package.json` `files[]`** — exported in the npm package so consumer servers can reference it via the framework install. ([#230](https://github.com/cyanheads/mcp-ts-core/issues/230))
25
+
26
+ ## Changed
27
+
28
+ - **`templates/package.json` `bundle` script** — appended `&& bun run scripts/clean-mcpb.ts dist/{{PACKAGE_NAME}}.mcpb` after `mcpb pack`; newly-scaffolded servers get the clean step automatically. ([#230](https://github.com/cyanheads/mcp-ts-core/issues/230))
29
+ - **`templates/src/index.ts`** — scaffolded `createApp()` now includes an explicit `name: '{{PACKAGE_NAME}}'` / `title: '{{PACKAGE_NAME}}'` identity pair, so new servers pass check 9 out of the box. ([#231](https://github.com/cyanheads/mcp-ts-core/issues/231))
30
+ - **`templates/manifest.json`** — removed empty `repository: { type: "git", url: "" }` stub; `mcpb pack` rejects a blank URL on fresh scaffolds. ([#230](https://github.com/cyanheads/mcp-ts-core/issues/230))
31
+ - **`skills/polish-docs-meta` v2.7** — adds the `name`/`title` identity requirement (`createApp()` / `createWorkerHandler()` must match the unscoped package name; `lint:packaging` enforces) and updates the `bundle` script description to reflect the clean step. ([#231](https://github.com/cyanheads/mcp-ts-core/issues/231))
32
+ - **`skills/orchestrations` v1.3** — Phase 4 close-loop wording: each shipped issue gets exactly one what-landed comment (concrete changes + version), then a bare close; bare "Fixed in v\<version\>." trailer replaced by the fuller comment-then-close pattern that also covers enhancements. ([#230](https://github.com/cyanheads/mcp-ts-core/issues/230))
33
+ - **`templates/CLAUDE.md` / `templates/AGENTS.md`** — `bundle` command description updated to reflect the clean step.
34
+ - **`lint-packaging.ts` `checkBundleContent`** — refactored to accept raw `.mcpbignore` content (string) instead of a file path, enabling direct unit testing without filesystem fixtures; `main()` still reads the file and passes the string.
35
+ - **`lint-packaging.ts` `KNOWN_DEV_DIRS` / `CRITICAL_RUNTIME_PATHS`** — exported for cross-module use and unit test access.
36
+ - **`tests/unit/scripts/lint-packaging.test.ts`** — rewritten to import `checkBundleContent`, `checkBundleEntries`, `checkEntrypointIdentity`, `checkManifestIdentity` from the real script; no inline logic mirror. ([#230](https://github.com/cyanheads/mcp-ts-core/issues/230), [#231](https://github.com/cyanheads/mcp-ts-core/issues/231))
37
+ - **`tests/unit/scripts/clean-mcpb.test.ts`** (new) — imports `AGENT_DOC_ENTRY` and `filterAgentDocEntries` from the real `scripts/clean-mcpb.ts`; covers match/keep/near-miss/order cases plus the cross-script sync assertion. ([#230](https://github.com/cyanheads/mcp-ts-core/issues/230))
@@ -0,0 +1,8 @@
1
+ {"level":40,"time":1781209564918,"env":"testing","version":"0.10.6","pid":48621,"transport":"http","requestId":"LASBX-RIEWV","timestamp":"2026-06-11T20:26:04.918Z","operation":"TransportManager.start","component":"HttpTransportSetup","msg":"MCP_ALLOWED_ORIGINS is not set — CORS is wildcard for CLI clients; browser Origin headers are restricted to loopback. Set MCP_ALLOWED_ORIGINS for production deployments accepting remote browser origins."}
2
+ {"level":40,"time":1781209567046,"env":"testing","version":"0.10.6","pid":48621,"component":"HttpTransport","requestId":"OSGGV-UYTJ4","timestamp":"2026-06-11T20:26:07.046Z","operation":"HttpRpcRequest","sessionId":"not-a-real-session-1781209567045","msg":"Session validation failed - invalid or hijacked session"}
3
+ {"level":50,"time":1781209571208,"env":"testing","version":"0.0.0-test","pid":48800,"requestId":"4QZIB-ASBOA","timestamp":"2026-06-11T20:26:11.207Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"87dfc0a1ba692591b1e5da5dabd7cdd49ce892c67e4c79bb8e96cb04679d7d3d","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"87dfc0a1ba692591b1e5da5dabd7cdd49ce892c67e4c79bb8e96cb04679d7d3d","toolName":"scoped_echo","requestId":"4QZIB-ASBOA","timestamp":"2026-06-11T20:26:11.207Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"originalErrorName":"McpError","originalMessage":"Insufficient permissions.","originalStack":"McpError: Insufficient permissions.\n at forbidden (file:///Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:84:54)\n at withRequiredScopes (file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/lib/authUtils.js:68:15)\n at file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:283:17\n at McpServer.executeToolHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:233:42)\n at file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43\n at async wrappedHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js:125:32)"},"stack":"McpError: Insufficient permissions.\n at ErrorHandler.handleError (file:///Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:170:19)\n at file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:324:26\n at McpServer.executeToolHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:233:42)\n at file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43\n at async wrappedHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js:125:32)","msg":"Error in tool:scoped_echo: Insufficient permissions."}
4
+ {"level":50,"time":1781209571217,"env":"testing","version":"0.0.0-test","pid":48800,"requestId":"XO135-J8PA0","timestamp":"2026-06-11T20:26:11.216Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"a6686259851faf1c145f82876f3441bd48445a032e3048b878d7c630901a1777","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"a6686259851faf1c145f82876f3441bd48445a032e3048b878d7c630901a1777","toolName":"scoped_echo","requestId":"XO135-J8PA0","timestamp":"2026-06-11T20:26:11.216Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"originalErrorName":"McpError","originalMessage":"Insufficient permissions.","originalStack":"McpError: Insufficient permissions.\n at forbidden (file:///Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:84:54)\n at withRequiredScopes (file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/lib/authUtils.js:68:15)\n at file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:283:17\n at McpServer.executeToolHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:233:42)\n at file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43\n at async wrappedHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js:125:32)"},"stack":"McpError: Insufficient permissions.\n at ErrorHandler.handleError (file:///Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:170:19)\n at file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:324:26\n at McpServer.executeToolHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:233:42)\n at file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43\n at async wrappedHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js:125:32)","msg":"Error in tool:scoped_echo: Insufficient permissions."}
5
+ {"level":50,"time":1781209584779,"env":"testing","version":"0.0.0-test","pid":49130,"requestId":"V3FOX-QXTR8","timestamp":"2026-06-11T20:26:24.778Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"b6231acc40fa0bfd377fd144250ade5c96a6f23385360c133635d0fd3ec6596b","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"b6231acc40fa0bfd377fd144250ade5c96a6f23385360c133635d0fd3ec6596b","toolName":"scoped_echo","requestId":"V3FOX-QXTR8","timestamp":"2026-06-11T20:26:24.778Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"originalErrorName":"McpError","originalMessage":"Insufficient permissions.","originalStack":"McpError: Insufficient permissions.\n at forbidden (/Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:84:58)\n at withRequiredScopes (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/lib/authUtils.js:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:283:17)\n at executeToolHandler (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:231:34)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43)\n at processTicksAndRejections (native:7:39)"},"stack":"McpError: Insufficient permissions.\n at handleError (/Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:324:26)\n at executeToolHandler (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:231:34)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43)\n at processTicksAndRejections (native:7:39)","msg":"Error in tool:scoped_echo: Insufficient permissions."}
6
+ {"level":50,"time":1781209584786,"env":"testing","version":"0.0.0-test","pid":49130,"requestId":"7G9CX-A4TMR","timestamp":"2026-06-11T20:26:24.786Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"0766a5d191de861c515f741834955c90f2761a08ac8dc84bdc1d70b1d7034134","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"0766a5d191de861c515f741834955c90f2761a08ac8dc84bdc1d70b1d7034134","toolName":"scoped_echo","requestId":"7G9CX-A4TMR","timestamp":"2026-06-11T20:26:24.786Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"originalErrorName":"McpError","originalMessage":"Insufficient permissions.","originalStack":"McpError: Insufficient permissions.\n at forbidden (/Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:84:58)\n at withRequiredScopes (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/lib/authUtils.js:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:283:17)\n at executeToolHandler (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:231:34)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43)\n at processTicksAndRejections (native:7:39)"},"stack":"McpError: Insufficient permissions.\n at handleError (/Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:324:26)\n at executeToolHandler (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:231:34)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43)\n at processTicksAndRejections (native:7:39)","msg":"Error in tool:scoped_echo: Insufficient permissions."}
7
+ {"level":40,"time":1781209585283,"env":"testing","version":"0.10.6","pid":49133,"transport":"http","requestId":"6YT5B-SCXYV","timestamp":"2026-06-11T20:26:25.282Z","operation":"TransportManager.start","component":"HttpTransportSetup","msg":"MCP_ALLOWED_ORIGINS is not set — CORS is wildcard for CLI clients; browser Origin headers are restricted to loopback. Set MCP_ALLOWED_ORIGINS for production deployments accepting remote browser origins."}
8
+ {"level":40,"time":1781209587210,"env":"testing","version":"0.10.6","pid":49133,"component":"HttpTransport","requestId":"WPW2S-K0V65","timestamp":"2026-06-11T20:26:27.210Z","operation":"HttpRpcRequest","sessionId":"not-a-real-session-1781209587210","msg":"Session validation failed - invalid or hijacked session"}
@@ -0,0 +1,4 @@
1
+ {"level":50,"time":1781209571208,"env":"testing","version":"0.0.0-test","pid":48800,"requestId":"4QZIB-ASBOA","timestamp":"2026-06-11T20:26:11.207Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"87dfc0a1ba692591b1e5da5dabd7cdd49ce892c67e4c79bb8e96cb04679d7d3d","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"87dfc0a1ba692591b1e5da5dabd7cdd49ce892c67e4c79bb8e96cb04679d7d3d","toolName":"scoped_echo","requestId":"4QZIB-ASBOA","timestamp":"2026-06-11T20:26:11.207Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"originalErrorName":"McpError","originalMessage":"Insufficient permissions.","originalStack":"McpError: Insufficient permissions.\n at forbidden (file:///Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:84:54)\n at withRequiredScopes (file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/lib/authUtils.js:68:15)\n at file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:283:17\n at McpServer.executeToolHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:233:42)\n at file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43\n at async wrappedHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js:125:32)"},"stack":"McpError: Insufficient permissions.\n at ErrorHandler.handleError (file:///Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:170:19)\n at file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:324:26\n at McpServer.executeToolHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:233:42)\n at file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43\n at async wrappedHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js:125:32)","msg":"Error in tool:scoped_echo: Insufficient permissions."}
2
+ {"level":50,"time":1781209571217,"env":"testing","version":"0.0.0-test","pid":48800,"requestId":"XO135-J8PA0","timestamp":"2026-06-11T20:26:11.216Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"a6686259851faf1c145f82876f3441bd48445a032e3048b878d7c630901a1777","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"a6686259851faf1c145f82876f3441bd48445a032e3048b878d7c630901a1777","toolName":"scoped_echo","requestId":"XO135-J8PA0","timestamp":"2026-06-11T20:26:11.216Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"originalErrorName":"McpError","originalMessage":"Insufficient permissions.","originalStack":"McpError: Insufficient permissions.\n at forbidden (file:///Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:84:54)\n at withRequiredScopes (file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/lib/authUtils.js:68:15)\n at file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:283:17\n at McpServer.executeToolHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:233:42)\n at file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43\n at async wrappedHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js:125:32)"},"stack":"McpError: Insufficient permissions.\n at ErrorHandler.handleError (file:///Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:170:19)\n at file:///Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:324:26\n at McpServer.executeToolHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:233:42)\n at file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43\n at async wrappedHandler (file:///Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js:125:32)","msg":"Error in tool:scoped_echo: Insufficient permissions."}
3
+ {"level":50,"time":1781209584779,"env":"testing","version":"0.0.0-test","pid":49130,"requestId":"V3FOX-QXTR8","timestamp":"2026-06-11T20:26:24.778Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"b6231acc40fa0bfd377fd144250ade5c96a6f23385360c133635d0fd3ec6596b","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"b6231acc40fa0bfd377fd144250ade5c96a6f23385360c133635d0fd3ec6596b","toolName":"scoped_echo","requestId":"V3FOX-QXTR8","timestamp":"2026-06-11T20:26:24.778Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["tool:other:read"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"originalErrorName":"McpError","originalMessage":"Insufficient permissions.","originalStack":"McpError: Insufficient permissions.\n at forbidden (/Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:84:58)\n at withRequiredScopes (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/lib/authUtils.js:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:283:17)\n at executeToolHandler (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:231:34)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43)\n at processTicksAndRejections (native:7:39)"},"stack":"McpError: Insufficient permissions.\n at handleError (/Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:324:26)\n at executeToolHandler (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:231:34)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43)\n at processTicksAndRejections (native:7:39)","msg":"Error in tool:scoped_echo: Insufficient permissions."}
4
+ {"level":50,"time":1781209584786,"env":"testing","version":"0.0.0-test","pid":49130,"requestId":"7G9CX-A4TMR","timestamp":"2026-06-11T20:26:24.786Z","operation":"HandleToolRequest","critical":false,"errorCode":-32005,"originalErrorType":"McpError","finalErrorType":"McpError","sessionId":"0766a5d191de861c515f741834955c90f2761a08ac8dc84bdc1d70b1d7034134","toolName":"scoped_echo","tenantId":"authz-tenant","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"errorData":{"sessionId":"0766a5d191de861c515f741834955c90f2761a08ac8dc84bdc1d70b1d7034134","toolName":"scoped_echo","requestId":"7G9CX-A4TMR","timestamp":"2026-06-11T20:26:24.786Z","tenantId":"authz-tenant","operation":"HandleToolRequest","auth":{"sub":"authz-user","scopes":["openid","email","profile","offline_access"],"clientId":"authz-client","tenantId":"authz-tenant","token":"[REDACTED]"},"originalErrorName":"McpError","originalMessage":"Insufficient permissions.","originalStack":"McpError: Insufficient permissions.\n at forbidden (/Users/casey/Developer/github/mcp-ts-core/dist/types-global/errors.js:84:58)\n at withRequiredScopes (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/transports/auth/lib/authUtils.js:68:15)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:283:17)\n at executeToolHandler (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:231:34)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43)\n at processTicksAndRejections (native:7:39)"},"stack":"McpError: Insufficient permissions.\n at handleError (/Users/casey/Developer/github/mcp-ts-core/dist/utils/internal/error-handler/errorHandler.js:170:23)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/dist/mcp-server/tools/utils/toolHandlerFactory.js:324:26)\n at executeToolHandler (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:231:34)\n at <anonymous> (/Users/casey/Developer/github/mcp-ts-core/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js:126:43)\n at processTicksAndRejections (native:7:39)","msg":"Error in tool:scoped_echo: Insufficient permissions."}
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/mcp-ts-core",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "mcpName": "io.github.cyanheads/mcp-ts-core",
5
5
  "description": "Agent-native TypeScript framework for building MCP servers. Declarative definitions with auth, multi-backend storage, OpenTelemetry, and first-class support for Bun/Node/Cloudflare Workers.",
6
6
  "main": "dist/core/index.js",
@@ -14,6 +14,7 @@
14
14
  "scripts/check-framework-antipatterns.ts",
15
15
  "scripts/check-skill-versions.ts",
16
16
  "scripts/check-skills-sync.ts",
17
+ "scripts/clean-mcpb.ts",
17
18
  "scripts/clean.ts",
18
19
  "scripts/devcheck.ts",
19
20
  "scripts/lint-mcp.ts",
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview Post-pack MCPB bundle cleaner. Runs after `mcpb pack` (wired
4
+ * into the `bundle` package script) to remove bundle content the pack step
5
+ * cannot exclude:
6
+ *
7
+ * 1. `mcpb clean <bundle>` — official dev-dependency prune + manifest
8
+ * validation (a measured real-world bundle dropped 61 MB → 12.9 MB).
9
+ * 2. Exact-name strip of agent-doc entries nested under `node_modules/` —
10
+ * dependency-shipped `skills/`, `.claude/`, `.agents/` trees and stray
11
+ * `SKILL.md` files. Root-anchored `.mcpbignore` patterns cannot reach
12
+ * these by design (issues #146/#207); this is issue #230.
13
+ * 3. Re-list and assert zero matching entries remain.
14
+ *
15
+ * Entry names are passed to `zip -d` with `-nw` (no-wildcard) so they match
16
+ * literally — bracketed runtime filenames like `pages/[slug].js` exist in
17
+ * real packages and must not glob. Bundles are unsigned in this flow; if
18
+ * `mcpb sign` is ever adopted, this script must run before signing.
19
+ *
20
+ * Usage: `bun run scripts/clean-mcpb.ts dist/<name>.mcpb`
21
+ *
22
+ * @module scripts/clean-mcpb
23
+ */
24
+ import { execFileSync } from 'node:child_process';
25
+ import { existsSync, statSync } from 'node:fs';
26
+ import { resolve } from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ /**
30
+ * Agent-doc entries under `node_modules/` that must not ship in a bundle.
31
+ * KEEP IN SYNC with `AGENT_DOC_ENTRY` in `scripts/lint-packaging.ts`
32
+ * (post-bundle content check) — a unit test asserts the two are identical.
33
+ */
34
+ export const AGENT_DOC_ENTRY =
35
+ /^node_modules\/.*(?:\/skills\/|\/\.claude\/|\/\.agents\/|\/SKILL\.md$)/;
36
+
37
+ /** Filter a bundle entry listing down to the agent-doc entries to strip. */
38
+ export function filterAgentDocEntries(entries: string[]): string[] {
39
+ return entries.filter((entry) => AGENT_DOC_ENTRY.test(entry));
40
+ }
41
+
42
+ /** Listing a 12k-entry bundle exceeds execFileSync's 1 MB default buffer. */
43
+ const MAX_LIST_BUFFER = 64 * 1024 * 1024;
44
+
45
+ /** `zip -d` argv batch size — stays clear of ARG_MAX with long entry names. */
46
+ const DELETE_BATCH = 200;
47
+
48
+ function listEntries(bundle: string): string[] {
49
+ return execFileSync('unzip', ['-Z1', bundle], { encoding: 'utf-8', maxBuffer: MAX_LIST_BUFFER })
50
+ .split('\n')
51
+ .filter((line) => line.length > 0);
52
+ }
53
+
54
+ function run(cmd: string, args: string[]): void {
55
+ execFileSync(cmd, args, { stdio: 'inherit' });
56
+ }
57
+
58
+ function mb(bytes: number): string {
59
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
60
+ }
61
+
62
+ function main(): void {
63
+ const bundleArg = process.argv[2];
64
+ if (!bundleArg) {
65
+ console.error('Usage: clean-mcpb.ts <bundle.mcpb> — run after `mcpb pack`.');
66
+ process.exit(1);
67
+ }
68
+ const bundle = resolve(bundleArg);
69
+ if (!existsSync(bundle)) {
70
+ console.error(`No bundle at ${bundle} — run \`mcpb pack\` first (see the \`bundle\` script).`);
71
+ process.exit(1);
72
+ }
73
+
74
+ const sizeBefore = statSync(bundle).size;
75
+
76
+ // 1. Official prune: removes dev dependencies, validates the manifest.
77
+ try {
78
+ run('npx', ['-y', '@anthropic-ai/mcpb', 'clean', bundle]);
79
+ } catch {
80
+ console.error('✗ `mcpb clean` failed — bundle left as packed.');
81
+ process.exit(1);
82
+ }
83
+
84
+ // 2. Exact-name strip of dependency-shipped agent docs.
85
+ let doomed: string[] = [];
86
+ try {
87
+ doomed = filterAgentDocEntries(listEntries(bundle));
88
+ for (let i = 0; i < doomed.length; i += DELETE_BATCH) {
89
+ run('zip', ['-q', '-d', '-nw', bundle, ...doomed.slice(i, i + DELETE_BATCH)]);
90
+ }
91
+ } catch (err) {
92
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
93
+ console.error(
94
+ '✗ Info-ZIP `zip`/`unzip` not found on PATH — required for the agent-doc strip.',
95
+ );
96
+ } else {
97
+ console.error(`✗ Agent-doc strip failed: ${err instanceof Error ? err.message : err}`);
98
+ }
99
+ process.exit(1);
100
+ }
101
+
102
+ // 3. Verify: zero matching entries remain.
103
+ const remaining = filterAgentDocEntries(listEntries(bundle));
104
+ if (remaining.length > 0) {
105
+ console.error(`✗ ${remaining.length} agent-doc entries still present after strip, e.g.:`);
106
+ for (const entry of remaining.slice(0, 5)) console.error(` ${entry}`);
107
+ process.exit(1);
108
+ }
109
+
110
+ const sizeAfter = statSync(bundle).size;
111
+ console.log(
112
+ `Bundle cleaned: ${mb(sizeBefore)} → ${mb(sizeAfter)} (${doomed.length} agent-doc entries stripped).`,
113
+ );
114
+ }
115
+
116
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
117
+ main();
118
+ }
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * @fileoverview MCPB packaging linter — validates env var alignment between
4
4
  * `manifest.json` (MCPB bundle install UX) and `server.json` (MCP Registry
5
- * discovery) for stdio packages, and guards against bundle-content mistakes.
5
+ * discovery) for stdio packages, and guards against bundle-content and
6
+ * identity mistakes.
6
7
  *
7
8
  * Used by devcheck and as a standalone script: `bun run lint:packaging` /
8
9
  * `npm run lint:packaging`.
@@ -22,17 +23,26 @@
22
23
  * `node_modules/x/skills/` (runtime path bypass, issues #172/#207).
23
24
  * 7. Bundle-content guard: `.mcpbignore` patterns must not strip critical
24
25
  * runtime package paths (e.g. `node_modules/@opentelemetry/api/build/src/`).
26
+ * 8. Post-bundle content: a built `.mcpb` under `dist/` must contain zero
27
+ * `node_modules/**` agent-doc entries (dependency-shipped `skills/`,
28
+ * `.claude/`, `.agents/`, `SKILL.md`) — unreachable by root-anchored
29
+ * `.mcpbignore` patterns; `scripts/clean-mcpb.ts` strips them at bundle
30
+ * time (issue #230).
31
+ * 9. Identity: `name`/`title` literals in `createApp()` /
32
+ * `createWorkerHandler()` (src/index.ts, src/worker.ts) and manifest
33
+ * `display_name` must equal the unscoped package name; a partial
34
+ * `name`/`title` pair warns without failing (issue #231).
25
35
  *
26
- * Checks 1–2 run with `manifest.json` alone; 3–4 require `server.json`.
27
- * Checks 5–7 run when `.mcpbignore` is present (silently skip otherwise).
28
- *
29
- * Skips cleanly when `manifest.json` is absent — consumers who deleted it for
30
- * an HTTP-only deploy should not fail this check.
36
+ * Every check skips cleanly when its input is absent — consumers who deleted
37
+ * `manifest.json` for an HTTP-only deploy, or who haven't built a bundle,
38
+ * should not fail the checks that need those files.
31
39
  *
32
40
  * @module scripts/lint-packaging
33
41
  */
34
- import { existsSync, readFileSync } from 'node:fs';
35
- import { resolve } from 'node:path';
42
+ import { execFileSync } from 'node:child_process';
43
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
44
+ import { join, resolve } from 'node:path';
45
+ import { fileURLToPath } from 'node:url';
36
46
 
37
47
  interface ServerJsonEnvVar {
38
48
  default?: string;
@@ -56,6 +66,7 @@ interface ManifestUserConfigEntry {
56
66
  }
57
67
 
58
68
  interface Manifest {
69
+ display_name?: unknown;
59
70
  name?: string;
60
71
  server?: { mcp_config?: { env?: Record<string, string> } };
61
72
  user_config?: Record<string, ManifestUserConfigEntry>;
@@ -69,20 +80,31 @@ const USER_CONFIG_REF = /^\$\{user_config\.([\w-]+)\}$/;
69
80
  * stripping nested runtime paths like `node_modules/x/skills/`. Keep in step
70
81
  * with the directory entries in `templates/_.mcpbignore`.
71
82
  */
72
- const KNOWN_DEV_DIRS = ['skills/', '.agents/', '.claude/'];
83
+ export const KNOWN_DEV_DIRS = ['skills/', '.agents/', '.claude/'];
73
84
 
74
85
  /**
75
86
  * Critical runtime paths that must NOT be stripped by any `.mcpbignore` pattern.
76
87
  * These are sampled representative paths — enough to catch a bare `skills/`
77
88
  * pattern accidentally stripping `node_modules/…/skills/`.
78
89
  */
79
- const CRITICAL_RUNTIME_PATHS = [
90
+ export const CRITICAL_RUNTIME_PATHS = [
80
91
  'node_modules/@opentelemetry/api/build/src/',
81
92
  'node_modules/@modelcontextprotocol/sdk/dist/',
82
93
  'node_modules/@cyanheads/mcp-ts-core/dist/',
83
94
  'dist/index.js',
84
95
  ];
85
96
 
97
+ /**
98
+ * Agent-doc entries under `node_modules/` that must not ship in a bundle.
99
+ * KEEP IN SYNC with `AGENT_DOC_ENTRY` in `scripts/clean-mcpb.ts` (the strip
100
+ * step this check verifies) — a unit test asserts the two are identical.
101
+ */
102
+ export const AGENT_DOC_ENTRY =
103
+ /^node_modules\/.*(?:\/skills\/|\/\.claude\/|\/\.agents\/|\/SKILL\.md$)/;
104
+
105
+ /** The canonical in-code identity pair — both must equal the unscoped package name. */
106
+ const IDENTITY_PAIR = ['name', 'title'] as const;
107
+
86
108
  function tryReadJson<T>(path: string): T | undefined {
87
109
  try {
88
110
  if (!existsSync(path)) return;
@@ -94,7 +116,7 @@ function tryReadJson<T>(path: string): T | undefined {
94
116
  }
95
117
 
96
118
  /**
97
- * Run bundle-content checks (5–7) against the .mcpbignore patterns.
119
+ * Run bundle-content checks (5–7) against raw `.mcpbignore` content.
98
120
  *
99
121
  * Uses the `ignore` package (already a devDependency in scaffolded servers)
100
122
  * to evaluate which paths survive the ignore rules. Returns an array of error
@@ -105,20 +127,32 @@ function tryReadJson<T>(path: string): T | undefined {
105
127
  * devDependencies (`^7.0.5`) and is therefore available in the server's
106
128
  * `node_modules` when `bun run lint:packaging` is invoked there.
107
129
  */
108
- async function checkBundleContent(mcpbignorePath: string): Promise<string[]> {
130
+ interface IgnoreMatcher {
131
+ add(patterns: string[]): IgnoreMatcher;
132
+ ignores(path: string): boolean;
133
+ }
134
+
135
+ /**
136
+ * The `ignore` package's factory, typed structurally — the CJS interop shape
137
+ * of `import('ignore')` differs between the script and test tsconfig programs,
138
+ * so naming the module's own types breaks one or the other.
139
+ */
140
+ type IgnoreFactory = (options?: unknown) => IgnoreMatcher;
141
+
142
+ export async function checkBundleContent(raw: string): Promise<string[]> {
109
143
  const errors: string[] = [];
110
144
 
111
- let createIgnore: typeof import('ignore')['default'];
145
+ let createIgnore: IgnoreFactory;
112
146
  try {
113
147
  // Dynamic import so the rest of the linter still runs when `ignore` is absent.
114
- createIgnore = ((await import('ignore')) as typeof import('ignore')).default;
148
+ const mod: unknown = await import('ignore');
149
+ createIgnore = ((mod as { default?: unknown }).default ?? mod) as IgnoreFactory;
115
150
  } catch {
116
151
  // `ignore` not installed — skip the guard without failing (e.g. in a minimal
117
152
  // CI environment that omits devDependencies).
118
153
  return errors;
119
154
  }
120
155
 
121
- const raw = readFileSync(mcpbignorePath, 'utf-8');
122
156
  const lines = raw
123
157
  .split('\n')
124
158
  .map((l) => l.trim())
@@ -180,81 +214,291 @@ async function checkBundleContent(mcpbignorePath: string): Promise<string[]> {
180
214
  return errors;
181
215
  }
182
216
 
183
- async function main(): Promise<void> {
184
- const manifestPath = resolve('manifest.json');
185
- if (!existsSync(manifestPath)) {
186
- console.log('No manifest.json skipping lint:packaging.');
187
- process.exit(0);
188
- }
217
+ /**
218
+ * Check 8: a built bundle must contain zero `node_modules/**` agent-doc
219
+ * entries. `scripts/clean-mcpb.ts` (wired into the `bundle` script) strips
220
+ * them after `mcpb pack`.
221
+ */
222
+ export function checkBundleEntries(entries: string[], bundleLabel: string): string[] {
223
+ const offending = entries.filter((entry) => AGENT_DOC_ENTRY.test(entry));
224
+ if (offending.length === 0) return [];
225
+ const sample = offending
226
+ .slice(0, 5)
227
+ .map((entry) => `\n ${entry}`)
228
+ .join('');
229
+ return [
230
+ `${bundleLabel} contains ${offending.length} node_modules agent-doc entries ` +
231
+ `(dependency-shipped skills/, .claude/, .agents/, SKILL.md) — re-run the \`bundle\` ` +
232
+ `script (scripts/clean-mcpb.ts strips them):${sample}`,
233
+ ];
234
+ }
235
+
236
+ /**
237
+ * Collect the direct (depth-1) property lines of the options object passed to
238
+ * `createApp()` / `createWorkerHandler()`. Returns undefined when no call with
239
+ * an inline options object exists (e.g. the framework's bare `createApp()`
240
+ * dev entry).
241
+ *
242
+ * Line-based, no AST: nested object literals (a `setup(core) { … }` body,
243
+ * `extensions: { … }`) are excluded by brace-depth tracking so an inner
244
+ * `name:`/`title:` key can't false-positive the identity check. Reliable for
245
+ * the template-scaffolded entrypoint shape with single-line string literals.
246
+ */
247
+ function identityCandidateLines(source: string): string[] | undefined {
248
+ const call = source.match(/\b(?:createApp|createWorkerHandler)\s*\(\s*\{/);
249
+ if (call?.index === undefined) return;
189
250
 
190
- const manifest = tryReadJson<Manifest>(manifestPath);
191
- if (!manifest) {
192
- console.error('manifest.json is unreadable or malformed.');
193
- process.exit(1);
251
+ const lines: string[] = [];
252
+ let depth = 0;
253
+ let lineStartDepth = 0;
254
+ let buf = '';
255
+ let inString: string | undefined;
256
+ let inBlockComment = false;
257
+
258
+ const flush = (): void => {
259
+ const trimmed = buf.trim();
260
+ if (
261
+ lineStartDepth === 1 &&
262
+ trimmed.length > 0 &&
263
+ !trimmed.startsWith('//') &&
264
+ !trimmed.startsWith('*')
265
+ ) {
266
+ lines.push(trimmed);
267
+ }
268
+ buf = '';
269
+ };
270
+
271
+ for (let i = call.index + call[0].length - 1; i < source.length; i++) {
272
+ const ch = source[i] as string;
273
+ if (ch === '\n') {
274
+ flush();
275
+ lineStartDepth = depth;
276
+ continue;
277
+ }
278
+ buf += ch;
279
+ if (inBlockComment) {
280
+ if (ch === '/' && source[i - 1] === '*') inBlockComment = false;
281
+ continue;
282
+ }
283
+ if (inString) {
284
+ if (ch === '\\') {
285
+ buf += source[i + 1] ?? '';
286
+ i++;
287
+ continue;
288
+ }
289
+ if (ch === inString) inString = undefined;
290
+ continue;
291
+ }
292
+ if (ch === '/' && source[i + 1] === '/') {
293
+ const nl = source.indexOf('\n', i);
294
+ const end = nl === -1 ? source.length : nl;
295
+ buf += source.slice(i + 1, end);
296
+ i = end - 1;
297
+ continue;
298
+ }
299
+ if (ch === '/' && source[i + 1] === '*') {
300
+ inBlockComment = true;
301
+ continue;
302
+ }
303
+ if (ch === "'" || ch === '"' || ch === '`') {
304
+ inString = ch;
305
+ continue;
306
+ }
307
+ if (ch === '{' || ch === '(' || ch === '[') {
308
+ depth++;
309
+ } else if (ch === '}' || ch === ')' || ch === ']') {
310
+ depth--;
311
+ if (depth === 0) {
312
+ flush();
313
+ return lines;
314
+ }
315
+ }
194
316
  }
317
+ flush();
318
+ return lines;
319
+ }
195
320
 
321
+ /**
322
+ * Check 9 (entrypoint surface): `name`/`title` literals passed to
323
+ * `createApp()` / `createWorkerHandler()` must equal the unscoped package
324
+ * name. A partial pair (one or both missing) warns without failing — explicit
325
+ * `name` also keeps scoped npm names out of the served `server_name`.
326
+ */
327
+ export function checkEntrypointIdentity(
328
+ source: string,
329
+ unscopedName: string,
330
+ fileLabel: string,
331
+ ): { errors: string[]; warnings: string[] } {
196
332
  const errors: string[] = [];
333
+ const warnings: string[] = [];
197
334
 
198
- if (manifest.name?.includes('/')) {
199
- errors.push(
200
- `manifest.json "name" contains a scope prefix ("${manifest.name}") — use the bare package name (e.g. "${manifest.name.split('/').pop()}")`,
201
- );
335
+ const optionLines = identityCandidateLines(source);
336
+ if (!optionLines) return { errors, warnings };
337
+
338
+ const present = new Set<string>();
339
+ for (const field of IDENTITY_PAIR) {
340
+ const fieldRe = new RegExp(`^['"\`]?${field}['"\`]?\\s*:`);
341
+ const literalRe = new RegExp(`^['"\`]?${field}['"\`]?\\s*:\\s*(['"\`])((?:(?!\\1).)*)\\1`);
342
+ for (const line of optionLines) {
343
+ if (!fieldRe.test(line)) continue;
344
+ present.add(field);
345
+ const literal = line.match(literalRe)?.[2];
346
+ if (literal !== undefined && literal !== unscopedName) {
347
+ errors.push(
348
+ `${fileLabel} sets ${field}: "${literal}" — must equal the unscoped package name ` +
349
+ `"${unscopedName}" (display identity is the machine name on every surface)`,
350
+ );
351
+ }
352
+ }
202
353
  }
203
354
 
204
- const userConfig = manifest.user_config ?? {};
205
- for (const [key, entry] of Object.entries(userConfig)) {
206
- if (typeof entry !== 'object' || entry === null) continue;
207
- const missing = (['title', 'type'] as const).filter(
208
- (f) => typeof entry[f] !== 'string' || (entry[f] as string).length === 0,
355
+ const missing = IDENTITY_PAIR.filter((field) => !present.has(field));
356
+ if (missing.length > 0) {
357
+ warnings.push(
358
+ `${fileLabel} identity pair is partial — missing: ${missing.join(', ')} ` +
359
+ `(set both name and title to the unscoped package name "${unscopedName}")`,
209
360
  );
210
- if (missing.length > 0) {
211
- errors.push(
212
- `manifest.json user_config["${key}"] is missing required field(s): ${missing.join(', ')} — mcpb pack will reject this`,
213
- );
214
- }
215
361
  }
216
362
 
217
- const serverJson = tryReadJson<ServerJson>(resolve('server.json'));
218
- if (serverJson) {
219
- const manifestEnv = manifest.server?.mcp_config?.env ?? {};
220
- const manifestEnvKeys = new Set(Object.keys(manifestEnv));
363
+ return { errors, warnings };
364
+ }
221
365
 
222
- const manifestUserConfigKeys = new Set(
223
- Object.entries(manifestEnv)
224
- .filter(([, v]) => typeof v === 'string' && USER_CONFIG_REF.test(v))
225
- .map(([k]) => k),
226
- );
366
+ /** Check 9 (manifest surface): `display_name`, when present, must be the unscoped package name. */
367
+ export function checkManifestIdentity(manifest: Manifest, unscopedName: string): string[] {
368
+ if (typeof manifest.display_name === 'string' && manifest.display_name !== unscopedName) {
369
+ return [
370
+ `manifest.json "display_name" is "${manifest.display_name}" — must equal the unscoped ` +
371
+ `package name "${unscopedName}"`,
372
+ ];
373
+ }
374
+ return [];
375
+ }
227
376
 
228
- const stdioEnvVars = (serverJson.packages ?? [])
229
- .filter((p) => p.transport?.type === 'stdio')
230
- .flatMap((p) => p.environmentVariables ?? []);
231
- const stdioEnvNames = new Set(stdioEnvVars.map((v) => v.name));
232
- const requiredStdioEnvNames = new Set(
233
- stdioEnvVars.filter((v) => v.isRequired === true && v.default == null).map((v) => v.name),
234
- );
377
+ async function main(): Promise<void> {
378
+ const errors: string[] = [];
379
+ const warnings: string[] = [];
380
+ const notes: string[] = [];
381
+
382
+ const pkg = tryReadJson<{ name?: string }>(resolve('package.json'));
383
+ const unscopedName = pkg?.name?.split('/').pop();
235
384
 
236
- const missingInServerJson = [...manifestUserConfigKeys].filter((k) => !stdioEnvNames.has(k));
237
- const missingInManifest = [...requiredStdioEnvNames].filter((k) => !manifestEnvKeys.has(k));
385
+ // ── Manifest-dependent checks (1–4 + manifest identity) ──
386
+ const manifestPath = resolve('manifest.json');
387
+ let manifest: Manifest | undefined;
388
+ if (existsSync(manifestPath)) {
389
+ manifest = tryReadJson<Manifest>(manifestPath);
390
+ if (!manifest) {
391
+ console.error('manifest.json is unreadable or malformed.');
392
+ process.exit(1);
393
+ }
238
394
 
239
- if (missingInServerJson.length > 0) {
395
+ if (manifest.name?.includes('/')) {
240
396
  errors.push(
241
- `manifest.json references user_config env var(s) not advertised in server.json stdio environmentVariables[]: ${missingInServerJson.join(', ')}`,
397
+ `manifest.json "name" contains a scope prefix ("${manifest.name}") use the bare package name (e.g. "${manifest.name.split('/').pop()}")`,
242
398
  );
243
399
  }
244
- if (missingInManifest.length > 0) {
245
- errors.push(
246
- `server.json declares required stdio env var(s) without default missing from manifest.json mcp_config.env: ${missingInManifest.join(', ')}`,
400
+
401
+ const userConfig = manifest.user_config ?? {};
402
+ for (const [key, entry] of Object.entries(userConfig)) {
403
+ if (typeof entry !== 'object' || entry === null) continue;
404
+ const missing = (['title', 'type'] as const).filter(
405
+ (f) => typeof entry[f] !== 'string' || (entry[f] as string).length === 0,
247
406
  );
407
+ if (missing.length > 0) {
408
+ errors.push(
409
+ `manifest.json user_config["${key}"] is missing required field(s): ${missing.join(', ')} — mcpb pack will reject this`,
410
+ );
411
+ }
248
412
  }
413
+
414
+ const serverJson = tryReadJson<ServerJson>(resolve('server.json'));
415
+ if (serverJson) {
416
+ const manifestEnv = manifest.server?.mcp_config?.env ?? {};
417
+ const manifestEnvKeys = new Set(Object.keys(manifestEnv));
418
+
419
+ const manifestUserConfigKeys = new Set(
420
+ Object.entries(manifestEnv)
421
+ .filter(([, v]) => typeof v === 'string' && USER_CONFIG_REF.test(v))
422
+ .map(([k]) => k),
423
+ );
424
+
425
+ const stdioEnvVars = (serverJson.packages ?? [])
426
+ .filter((p) => p.transport?.type === 'stdio')
427
+ .flatMap((p) => p.environmentVariables ?? []);
428
+ const stdioEnvNames = new Set(stdioEnvVars.map((v) => v.name));
429
+ const requiredStdioEnvNames = new Set(
430
+ stdioEnvVars.filter((v) => v.isRequired === true && v.default == null).map((v) => v.name),
431
+ );
432
+
433
+ const missingInServerJson = [...manifestUserConfigKeys].filter((k) => !stdioEnvNames.has(k));
434
+ const missingInManifest = [...requiredStdioEnvNames].filter((k) => !manifestEnvKeys.has(k));
435
+
436
+ if (missingInServerJson.length > 0) {
437
+ errors.push(
438
+ `manifest.json references user_config env var(s) not advertised in server.json stdio environmentVariables[]: ${missingInServerJson.join(', ')}`,
439
+ );
440
+ }
441
+ if (missingInManifest.length > 0) {
442
+ errors.push(
443
+ `server.json declares required stdio env var(s) without default missing from manifest.json mcp_config.env: ${missingInManifest.join(', ')}`,
444
+ );
445
+ }
446
+ }
447
+
448
+ if (unscopedName) {
449
+ errors.push(...checkManifestIdentity(manifest, unscopedName));
450
+ }
451
+ } else {
452
+ notes.push('No manifest.json — skipping manifest/server.json alignment checks.');
249
453
  }
250
454
 
251
- // Bundle-content guard (checks 5–7).
455
+ // ── Bundle-content guard (checks 5–7) ──
252
456
  const mcpbignorePath = resolve('.mcpbignore');
253
457
  if (existsSync(mcpbignorePath)) {
254
- const bundleErrors = await checkBundleContent(mcpbignorePath);
255
- errors.push(...bundleErrors);
458
+ errors.push(...(await checkBundleContent(readFileSync(mcpbignorePath, 'utf-8'))));
256
459
  }
257
460
 
461
+ // ── Post-bundle content check (8) ──
462
+ const distDir = resolve('dist');
463
+ if (existsSync(distDir)) {
464
+ for (const file of readdirSync(distDir).filter((f) => f.endsWith('.mcpb'))) {
465
+ try {
466
+ const listing = execFileSync('unzip', ['-Z1', join(distDir, file)], {
467
+ encoding: 'utf-8',
468
+ maxBuffer: 64 * 1024 * 1024,
469
+ });
470
+ errors.push(
471
+ ...checkBundleEntries(
472
+ listing.split('\n').filter((line) => line.length > 0),
473
+ `dist/${file}`,
474
+ ),
475
+ );
476
+ } catch (err) {
477
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
478
+ notes.push(`unzip not available — skipping bundle content check for dist/${file}.`);
479
+ } else {
480
+ errors.push(
481
+ `failed to list entries of dist/${file}: ${err instanceof Error ? err.message : err}`,
482
+ );
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ // ── Entrypoint identity check (9) ──
489
+ if (unscopedName) {
490
+ for (const entry of ['src/index.ts', 'src/worker.ts']) {
491
+ const entryPath = resolve(entry);
492
+ if (!existsSync(entryPath)) continue;
493
+ const result = checkEntrypointIdentity(readFileSync(entryPath, 'utf-8'), unscopedName, entry);
494
+ errors.push(...result.errors);
495
+ warnings.push(...result.warnings);
496
+ }
497
+ }
498
+
499
+ for (const note of notes) console.log(note);
500
+ for (const warning of warnings) console.warn(` ⚠ ${warning}`);
501
+
258
502
  if (errors.length === 0) {
259
503
  console.log('Packaging alignment OK.');
260
504
  process.exit(0);
@@ -263,4 +507,6 @@ async function main(): Promise<void> {
263
507
  process.exit(1);
264
508
  }
265
509
 
266
- await main();
510
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
511
+ await main();
512
+ }
@@ -4,7 +4,7 @@ description: >
4
4
  Pick and run a multi-phase workflow that chains foundational task skills (`git-wrapup`, `release-and-publish`, `maintenance`, `field-test`, `setup`, etc.) end-to-end. Routes user intent to a workflow file under `workflows/` — greenfield builds, maintenance + release, field-test + fix, or known-work + release. Single source for the universal rules (no commits without authorization, no destructive git, no marketing language), the orchestrator posture (own the goal, ground sub-agents in primary sources, verify against the goal), and the sub-agent strategy (orient block, parallel fanout, isolation, normalization) that apply across every workflow. Sub-agents are an optional capability — workflows run linearly when fanout isn't available.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.2"
7
+ version: "1.3"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -63,7 +63,7 @@ Each phase's Objective column is the goal state per target — the verifiable en
63
63
  | 1b | Fix | Per target: targeted issues fixed in source, tests updated/added, `devcheck` + `rebuild` + `test` green, each fixed issue commented with fix details, working tree dirty for review | parallel fanout (one sub-agent per target — hard constraint) | **barrier** — orchestrator reviews diffs before verify (explicit gate in checklist) |
64
64
  | 2 | Verify | Per target: full diff cold-reviewed; simplified if warranted; each fix re-exercised against the running server with actual tool output in the summary | parallel fanout | **barrier** — orchestrator reviews simplified diff and verified outputs; release authorization required |
65
65
  | 3 | Wrap-up + release | Per target: fixes split into per-file commits with a release commit on top; annotated tag; published per repo visibility; tag annotation is structured markdown with issue backlinks | parallel fanout (Bash git only) | gate-free |
66
- | 4 | Issue cleanup | Every GH issue that shipped a fix closed with "Fixed in v\<version\>" comment | orchestrator (serial) | — |
66
+ | 4 | Issue cleanup | Every shipped issue closed (reason: completed) carrying exactly one what-landed comment that cites the version | orchestrator (serial) | — |
67
67
 
68
68
  Phase 1a is conditional — only runs when the input is a handoff document or otherwise unvalidated. When the input is already tracked GH issues, skip directly to Phase 1b. The release portion of Phase 3 is conditional on user authorization to ship.
69
69
 
@@ -137,14 +137,18 @@ The tag annotation and changelog cover ALL fixes — the commit split is about g
137
137
  **Tag annotations** are for end users — internal dev cleanup (lockfile refreshes, linter fixes, build config) belongs in commit bodies, not the tag annotation.
138
138
 
139
139
  ### Phase 4: Issue cleanup
140
- Close issues that shipped fixes — only those. Skipped issues stay open.
140
+ Close issues that shipped — only those. Skipped issues stay open.
141
+
142
+ Each issue gets exactly ONE substantive comment recording what landed — concrete changes, file paths, and the version — written either by the fix sub-agent (Phase 1b) or by the orchestrator here. Then close without an additional comment:
141
143
 
142
144
  ```bash
143
- for n in <fixed-issue-numbers>; do
144
- gh issue close "$n" -R "<owner>/<repo>" --reason completed --comment "Fixed in v<version>."
145
+ for n in <shipped-issue-numbers>; do
146
+ gh issue close "$n" -R "<owner>/<repo>" --reason completed
145
147
  done
146
148
  ```
147
149
 
150
+ If no what-landed comment exists yet, the version belongs in that one comment ("Shipped in v\<version\>: …"). Never stack a bare "Fixed in v\<version\>" trailer on top of an existing summary — it duplicates the record, and "fixed" misdescribes enhancements (enhancements ship/land; only bugs are fixed).
151
+
148
152
  ## Workflow-specific gotchas
149
153
 
150
154
  | # | Gotcha | Mitigation |
@@ -170,6 +174,6 @@ done
170
174
  - [ ] Orchestrator gate after Phase 2: simplified diff reviewed, field-test claims verified
171
175
  - [ ] Phase 3: version bumped, fix commits + release commit, annotated tag per target — scope matches private/public status
172
176
  - [ ] Phase 3: published per scope (push, npm if public, MCP Registry if applicable, GH release, Docker if applicable)
173
- - [ ] Phase 4: fixed issues closed; skipped issues remain open
177
+ - [ ] Phase 4: shipped issues closed, one what-landed comment each; skipped issues remain open
174
178
  - [ ] Post-workflow verification: `git ls-remote --tags origin`, `npm view <pkg>@<version>` if public, GH release artifacts attached
175
179
  - [ ] Tag/release quality review: tag subject omits version number, structured markdown, no marketing adjectives, issue backlinks present
@@ -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.6"
7
+ version: "2.7"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -76,6 +76,8 @@ Key fields: `name`, `description`, `repository`, `author`, `homepage`, `bugs`, `
76
76
 
77
77
  **`name` must communicate the server's domain at a glance.** See `references/package-meta.md` for the naming convention — ambiguous abbreviations and acronym-only names fail the scannability test for humans and agents alike.
78
78
 
79
+ **`name` and `title` in `createApp()` / `createWorkerHandler()` must match the unscoped `package.json` `name`** — display identity is the machine name on every surface; `lint:packaging` (run by `devcheck`) enforces the match and warns when the pair is partial. `description` is never duplicated into the entrypoint — `package.json` is the canonical source (the framework derives the served description from it).
80
+
79
81
  **`description` is the canonical source.** Every other surface (README header, `server.json`, Dockerfile OCI label, GitHub repo description) derives from it. Write it here first, then propagate.
80
82
 
81
83
  ### 6. `server.json`
@@ -193,7 +195,7 @@ If the project ships as an `.mcpb` bundle for Claude Desktop (check for `manifes
193
195
 
194
196
  **`package.json` scripts:**
195
197
 
196
- - `bundle` — builds the `.mcpb` (e.g., `mcpb pack --output dist/`)
198
+ - `bundle` — builds the `.mcpb` (`mcpb pack`, then `scripts/clean-mcpb.ts` prunes dev deps and strips dependency-shipped agent docs)
197
199
  - `lint:packaging` — validates `manifest.json` ↔ `server.json` env var consistency (run by `devcheck`)
198
200
 
199
201
  **Cross-file consistency:**
@@ -335,13 +335,13 @@ When you complete a skill's checklist, check the boxes and add a completion time
335
335
  | `npm run start:http` | Production mode (HTTP) |
336
336
  | `npm run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
337
337
  | `npm run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
338
- | `npm run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
338
+ | `npm run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
339
339
 
340
340
  ---
341
341
 
342
342
  ## Bundling
343
343
 
344
- `npm 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.
344
+ `npm 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 dependency-shipped agent docs (`node_modules/**` `skills/`, `.claude/`, `.agents/`, `SKILL.md`) that root-anchored `.mcpbignore` patterns cannot reach. 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.
345
345
 
346
346
  **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.
347
347
 
@@ -335,13 +335,13 @@ When you complete a skill's checklist, check the boxes and add a completion time
335
335
  | `npm run start:http` | Production mode (HTTP) |
336
336
  | `npm run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
337
337
  | `npm run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
338
- | `npm run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
338
+ | `npm run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
339
339
 
340
340
  ---
341
341
 
342
342
  ## Bundling
343
343
 
344
- `npm 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.
344
+ `npm 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 dependency-shipped agent docs (`node_modules/**` `skills/`, `.claude/`, `.agents/`, `SKILL.md`) that root-anchored `.mcpbignore` patterns cannot reach. 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.
345
345
 
346
346
  **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.
347
347
 
@@ -22,6 +22,5 @@
22
22
  },
23
23
  "tools_generated": true,
24
24
  "prompts_generated": true,
25
- "repository": { "type": "git", "url": "" },
26
25
  "user_config": {}
27
26
  }
@@ -30,7 +30,7 @@
30
30
  "format:unsafe": "biome check --write --unsafe .",
31
31
  "lint:mcp": "bun run scripts/lint-mcp.ts",
32
32
  "lint:packaging": "bun run scripts/lint-packaging.ts",
33
- "bundle": "bun run build && npx -y @anthropic-ai/mcpb pack . dist/{{PACKAGE_NAME}}.mcpb",
33
+ "bundle": "bun run build && npx -y @anthropic-ai/mcpb pack . dist/{{PACKAGE_NAME}}.mcpb && bun run scripts/clean-mcpb.ts dist/{{PACKAGE_NAME}}.mcpb",
34
34
  "changelog:build": "bun run scripts/build-changelog.ts",
35
35
  "changelog:check": "bun run scripts/build-changelog.ts --check",
36
36
  "release:github": "bun run scripts/release-github.ts",
@@ -12,6 +12,8 @@ import { echoAppUiResource } from './mcp-server/resources/definitions/echo-app-u
12
12
  import { echoPrompt } from './mcp-server/prompts/definitions/echo.prompt.js';
13
13
 
14
14
  await createApp({
15
+ name: '{{PACKAGE_NAME}}',
16
+ title: '{{PACKAGE_NAME}}',
15
17
  tools: [echoTool, echoAppTool],
16
18
  resources: [echoResource, echoAppUiResource],
17
19
  prompts: [echoPrompt],