@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 +1 -1
- package/CLAUDE.md +1 -1
- package/changelog/0.10.x/0.10.5.md +5 -5
- package/changelog/0.10.x/0.10.6.md +37 -0
- package/dist/logs/combined.log +8 -0
- package/dist/logs/error.log +4 -0
- package/dist/logs/interactions.log +0 -0
- package/package.json +2 -1
- package/scripts/clean-mcpb.ts +118 -0
- package/scripts/lint-packaging.ts +312 -66
- package/skills/orchestrations/SKILL.md +1 -1
- package/skills/orchestrations/workflows/fix-wrapup-release.md +9 -5
- package/skills/polish-docs-meta/SKILL.md +4 -2
- package/templates/AGENTS.md +2 -2
- package/templates/CLAUDE.md +2 -2
- package/templates/manifest.json +0 -1
- package/templates/package.json +1 -1
- package/templates/src/index.ts +2 -0
package/AGENTS.md
CHANGED
package/CLAUDE.md
CHANGED
|
@@ -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.
|
|
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. ([#
|
|
23
|
-
- **`skills/api-workers` `v1.
|
|
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`** —
|
|
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** —
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
27
|
-
*
|
|
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 {
|
|
35
|
-
import {
|
|
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
|
|
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
|
-
|
|
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:
|
|
145
|
+
let createIgnore: IgnoreFactory;
|
|
112
146
|
try {
|
|
113
147
|
// Dynamic import so the rest of the linter still runs when `ignore` is absent.
|
|
114
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
const manifestEnv = manifest.server?.mcp_config?.env ?? {};
|
|
220
|
-
const manifestEnvKeys = new Set(Object.keys(manifestEnv));
|
|
363
|
+
return { errors, warnings };
|
|
364
|
+
}
|
|
221
365
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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 (
|
|
395
|
+
if (manifest.name?.includes('/')) {
|
|
240
396
|
errors.push(
|
|
241
|
-
`manifest.json
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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 <
|
|
144
|
-
gh issue close "$n" -R "<owner>/<repo>" --reason completed
|
|
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:
|
|
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.
|
|
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` (
|
|
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:**
|
package/templates/AGENTS.md
CHANGED
|
@@ -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
|
|
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
|
|
package/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
|
package/templates/manifest.json
CHANGED
package/templates/package.json
CHANGED
|
@@ -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",
|
package/templates/src/index.ts
CHANGED
|
@@ -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],
|