@cyanheads/mcp-ts-core 0.13.7 → 0.13.9
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 +19 -15
- package/CLAUDE.md +19 -15
- package/README.md +4 -2
- package/changelog/0.13.x/0.13.8.md +101 -0
- package/changelog/0.13.x/0.13.9.md +113 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +39 -9
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts +6 -3
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +20 -6
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +25 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +13 -3
- package/dist/core/context.js.map +1 -1
- package/dist/core/serverManifest.d.ts +6 -0
- package/dist/core/serverManifest.d.ts.map +1 -1
- package/dist/core/serverManifest.js +6 -0
- package/dist/core/serverManifest.js.map +1 -1
- package/dist/core/worker.d.ts +2 -0
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +2 -0
- package/dist/core/worker.js.map +1 -1
- package/dist/linter/rules/enrichment-rules.d.ts +3 -2
- package/dist/linter/rules/enrichment-rules.d.ts.map +1 -1
- package/dist/linter/rules/enrichment-rules.js +9 -2
- package/dist/linter/rules/enrichment-rules.js.map +1 -1
- package/dist/linter/rules/handler-body-rules.d.ts.map +1 -1
- package/dist/linter/rules/handler-body-rules.js +10 -4
- package/dist/linter/rules/handler-body-rules.js.map +1 -1
- package/dist/linter/rules/schema-rules.d.ts +5 -0
- package/dist/linter/rules/schema-rules.d.ts.map +1 -1
- package/dist/linter/rules/schema-rules.js +44 -17
- package/dist/linter/rules/schema-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts +2 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +36 -1
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/mcp-server/inputRequired.d.ts +14 -5
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +15 -8
- package/dist/mcp-server/inputRequired.js.map +1 -1
- package/dist/mcp-server/outputContract.d.ts +33 -0
- package/dist/mcp-server/outputContract.d.ts.map +1 -0
- package/dist/mcp-server/outputContract.js +43 -0
- package/dist/mcp-server/outputContract.js.map +1 -0
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +10 -2
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +163 -40
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.js +330 -114
- package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +39 -15
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +361 -93
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js +4 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +1 -1
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +2 -5
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.js +65 -9
- package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
- package/dist/mcp-server/transports/http/sessionStore.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/sessionStore.js +2 -2
- package/dist/mcp-server/transports/http/sessionStore.js.map +1 -1
- package/dist/mcp-server/transports/stdio/stdioTransport.d.ts +9 -5
- package/dist/mcp-server/transports/stdio/stdioTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/stdio/stdioTransport.js +9 -5
- package/dist/mcp-server/transports/stdio/stdioTransport.js.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.d.ts +6 -2
- package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.js +8 -4
- package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
- package/dist/services/canvas/core/DataCanvas.d.ts.map +1 -1
- package/dist/services/canvas/core/DataCanvas.js +7 -5
- package/dist/services/canvas/core/DataCanvas.js.map +1 -1
- package/dist/services/canvas/core/canvasFactory.d.ts.map +1 -1
- package/dist/services/canvas/core/canvasFactory.js +2 -2
- package/dist/services/canvas/core/canvasFactory.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +82 -18
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +645 -344
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts +11 -7
- package/dist/services/canvas/providers/duckdb/exportWriter.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/exportWriter.js +19 -16
- package/dist/services/canvas/providers/duckdb/exportWriter.js.map +1 -1
- package/dist/services/llm/providers/openrouter.provider.js +1 -1
- package/dist/services/llm/providers/openrouter.provider.js.map +1 -1
- package/dist/services/mirror/core/defineMirror.d.ts +1 -0
- package/dist/services/mirror/core/defineMirror.d.ts.map +1 -1
- package/dist/services/mirror/core/defineMirror.js +1 -0
- package/dist/services/mirror/core/defineMirror.js.map +1 -1
- package/dist/services/speech/providers/elevenlabs.provider.js +3 -3
- package/dist/services/speech/providers/elevenlabs.provider.js.map +1 -1
- package/dist/services/speech/providers/whisper.provider.d.ts.map +1 -1
- package/dist/services/speech/providers/whisper.provider.js +5 -5
- package/dist/services/speech/providers/whisper.provider.js.map +1 -1
- package/dist/storage/core/StorageService.d.ts.map +1 -1
- package/dist/storage/core/StorageService.js +3 -6
- package/dist/storage/core/StorageService.js.map +1 -1
- package/dist/storage/core/storageFactory.d.ts.map +1 -1
- package/dist/storage/core/storageFactory.js +12 -15
- package/dist/storage/core/storageFactory.js.map +1 -1
- package/dist/storage/core/storageValidation.d.ts +13 -13
- package/dist/storage/core/storageValidation.d.ts.map +1 -1
- package/dist/storage/core/storageValidation.js +49 -125
- package/dist/storage/core/storageValidation.js.map +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.js +5 -3
- package/dist/storage/providers/cloudflare/d1Provider.js.map +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.js +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.js.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.js +3 -3
- package/dist/storage/providers/cloudflare/r2Provider.js.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.js +4 -4
- package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.js +6 -5
- package/dist/storage/providers/inMemory/inMemoryProvider.js.map +1 -1
- package/dist/testing/fuzz.d.ts.map +1 -1
- package/dist/testing/fuzz.js +7 -1
- package/dist/testing/fuzz.js.map +1 -1
- package/dist/testing/index.d.ts +15 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +51 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/types-global/errors.d.ts +7 -4
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/types-global/errors.js.map +1 -1
- package/dist/utils/formatting/codeSpan.d.ts +27 -0
- package/dist/utils/formatting/codeSpan.d.ts.map +1 -0
- package/dist/utils/formatting/codeSpan.js +42 -0
- package/dist/utils/formatting/codeSpan.js.map +1 -0
- package/dist/utils/formatting/diffFormatter.d.ts.map +1 -1
- package/dist/utils/formatting/diffFormatter.js +7 -15
- package/dist/utils/formatting/diffFormatter.js.map +1 -1
- package/dist/utils/formatting/markdownBuilder.d.ts +12 -5
- package/dist/utils/formatting/markdownBuilder.d.ts.map +1 -1
- package/dist/utils/formatting/markdownBuilder.js +14 -2
- package/dist/utils/formatting/markdownBuilder.js.map +1 -1
- package/dist/utils/formatting/tableFormatter.d.ts.map +1 -1
- package/dist/utils/formatting/tableFormatter.js +5 -9
- package/dist/utils/formatting/tableFormatter.js.map +1 -1
- package/dist/utils/formatting/treeFormatter.d.ts.map +1 -1
- package/dist/utils/formatting/treeFormatter.js +5 -9
- package/dist/utils/formatting/treeFormatter.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +17 -10
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +47 -26
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/mappings.d.ts +17 -1
- package/dist/utils/internal/error-handler/mappings.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/mappings.js +22 -1
- package/dist/utils/internal/error-handler/mappings.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +2 -0
- package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
- package/dist/utils/internal/logger.d.ts +75 -3
- package/dist/utils/internal/logger.d.ts.map +1 -1
- package/dist/utils/internal/logger.js +181 -52
- package/dist/utils/internal/logger.js.map +1 -1
- package/dist/utils/internal/performance.d.ts +11 -0
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +46 -12
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/network/fetchWithTimeout.d.ts +11 -5
- package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
- package/dist/utils/network/fetchWithTimeout.js +50 -23
- package/dist/utils/network/fetchWithTimeout.js.map +1 -1
- package/dist/utils/network/pacer.d.ts +38 -5
- package/dist/utils/network/pacer.d.ts.map +1 -1
- package/dist/utils/network/pacer.js +87 -25
- package/dist/utils/network/pacer.js.map +1 -1
- package/dist/utils/network/retry.d.ts +16 -8
- package/dist/utils/network/retry.d.ts.map +1 -1
- package/dist/utils/network/retry.js +19 -8
- package/dist/utils/network/retry.js.map +1 -1
- package/dist/utils/overflow/outlineOnOverflow.d.ts +18 -2
- package/dist/utils/overflow/outlineOnOverflow.d.ts.map +1 -1
- package/dist/utils/overflow/outlineOnOverflow.js +28 -3
- package/dist/utils/overflow/outlineOnOverflow.js.map +1 -1
- package/dist/utils/pagination/pagination.d.ts +3 -1
- package/dist/utils/pagination/pagination.d.ts.map +1 -1
- package/dist/utils/pagination/pagination.js +10 -2
- package/dist/utils/pagination/pagination.js.map +1 -1
- package/dist/utils/parsing/csvParser.d.ts.map +1 -1
- package/dist/utils/parsing/csvParser.js +4 -2
- package/dist/utils/parsing/csvParser.js.map +1 -1
- package/dist/utils/parsing/htmlExtractor.js +1 -1
- package/dist/utils/parsing/htmlExtractor.js.map +1 -1
- package/dist/utils/parsing/jsonParser.d.ts.map +1 -1
- package/dist/utils/parsing/jsonParser.js +3 -1
- package/dist/utils/parsing/jsonParser.js.map +1 -1
- package/dist/utils/parsing/xmlParser.d.ts.map +1 -1
- package/dist/utils/parsing/xmlParser.js +3 -1
- package/dist/utils/parsing/xmlParser.js.map +1 -1
- package/dist/utils/parsing/yamlParser.d.ts.map +1 -1
- package/dist/utils/parsing/yamlParser.js +3 -1
- package/dist/utils/parsing/yamlParser.js.map +1 -1
- package/dist/utils/security/idGenerator.d.ts.map +1 -1
- package/dist/utils/security/idGenerator.js +20 -4
- package/dist/utils/security/idGenerator.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +31 -0
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +98 -11
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +21 -2
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +21 -2
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/dist/utils/telemetry/instrumentation.d.ts +9 -3
- package/dist/utils/telemetry/instrumentation.d.ts.map +1 -1
- package/dist/utils/telemetry/instrumentation.js +85 -13
- package/dist/utils/telemetry/instrumentation.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +3 -3
- package/framework-skills/add-export/SKILL.md +5 -16
- package/framework-skills/add-prompt/SKILL.md +7 -3
- package/framework-skills/add-resource/SKILL.md +7 -5
- package/framework-skills/add-tool/SKILL.md +12 -10
- package/framework-skills/api-auth/SKILL.md +4 -2
- package/framework-skills/api-canvas/SKILL.md +19 -10
- package/framework-skills/api-config/SKILL.md +9 -6
- package/framework-skills/api-context/SKILL.md +16 -5
- package/framework-skills/api-errors/SKILL.md +23 -17
- package/framework-skills/api-linter/SKILL.md +32 -9
- package/framework-skills/api-mirror/SKILL.md +2 -1
- package/framework-skills/api-telemetry/SKILL.md +34 -14
- package/framework-skills/api-testing/SKILL.md +5 -3
- package/framework-skills/api-utils/SKILL.md +10 -10
- package/framework-skills/api-utils/references/formatting.md +1 -1
- package/framework-skills/api-utils/references/parsing.md +2 -2
- package/framework-skills/api-utils/references/security.md +6 -4
- package/framework-skills/design-mcp-server/SKILL.md +2 -2
- package/framework-skills/field-test/SKILL.md +4 -4
- package/framework-skills/git-wrapup/SKILL.md +12 -7
- package/framework-skills/maintenance/SKILL.md +2 -2
- package/framework-skills/orchestrations/SKILL.md +7 -6
- package/framework-skills/orchestrations/workflows/field-test-fix.md +9 -19
- package/framework-skills/orchestrations/workflows/fix-wrapup-release.md +7 -7
- package/framework-skills/orchestrations/workflows/greenfield-build.md +8 -5
- package/framework-skills/orchestrations/workflows/maintenance-release.md +8 -8
- package/framework-skills/polish-docs-meta/SKILL.md +4 -4
- package/framework-skills/polish-docs-meta/references/readme.md +1 -0
- package/framework-skills/release-and-publish/SKILL.md +7 -5
- package/framework-skills/release-pr-review/SKILL.md +37 -23
- package/framework-skills/report-issue-framework/SKILL.md +7 -5
- package/framework-skills/report-issue-local/SKILL.md +8 -6
- package/framework-skills/security-pass/SKILL.md +8 -8
- package/framework-skills/techniques/SKILL.md +1 -1
- package/framework-skills/techniques/references/outline-on-overflow.md +12 -7
- package/package.json +20 -5
- package/scripts/check-skill-versions.ts +103 -22
- package/scripts/devcheck.ts +11 -9
- package/scripts/lint-mcp.ts +87 -27
- package/scripts/lint-packaging.ts +99 -1
- package/scripts/release-github.ts +117 -5
- package/templates/.env.example +4 -0
- package/templates/Dockerfile +26 -6
- package/templates/_.mcpbignore +2 -0
- package/templates/package.json +1 -0
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
File a bug or feature request against @cyanheads/mcp-ts-core when you hit a framework issue. Use when a builder, utility, context method, or config behaves contrary to the documented API — not for server-specific application bugs.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.14"
|
|
8
8
|
audience: external
|
|
9
9
|
type: workflow
|
|
10
10
|
---
|
|
@@ -33,7 +33,8 @@ For general `gh` CLI workflows outside issue filing (PRs, workflows, API access)
|
|
|
33
33
|
gh issue list -R cyanheads/mcp-ts-core --search "your error message or keyword" --state all
|
|
34
34
|
|
|
35
35
|
# Assess a close match before commenting — is it already linked to a fix or referenced elsewhere?
|
|
36
|
-
gh issue view <number> -R cyanheads/mcp-ts-core
|
|
36
|
+
gh issue view <number> -R cyanheads/mcp-ts-core # body
|
|
37
|
+
gh issue view <number> -R cyanheads/mcp-ts-core --comments # thread only — without a TTY it prints no body
|
|
37
38
|
gh api 'repos/cyanheads/mcp-ts-core/issues/<number>/timeline' --paginate \
|
|
38
39
|
--jq '.[] | select(.event=="cross-referenced") | .source.issue | "\(.repository.full_name)#\(.number) — \(.title)"'
|
|
39
40
|
```
|
|
@@ -80,7 +81,7 @@ gh issue create -R cyanheads/mcp-ts-core \
|
|
|
80
81
|
--body "$(cat <<'ISSUE'
|
|
81
82
|
### mcp-ts-core version
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
<installed version from node_modules/@cyanheads/mcp-ts-core/package.json — not the ^ range>
|
|
84
85
|
|
|
85
86
|
### Runtime
|
|
86
87
|
|
|
@@ -88,7 +89,7 @@ Bun
|
|
|
88
89
|
|
|
89
90
|
### Runtime version
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
<bun --version>
|
|
92
93
|
|
|
93
94
|
### Transport
|
|
94
95
|
|
|
@@ -249,7 +250,8 @@ ISSUE
|
|
|
249
250
|
## Following Up
|
|
250
251
|
|
|
251
252
|
```bash
|
|
252
|
-
# Check issue status
|
|
253
|
+
# Check issue status, then its comment thread (--comments without a TTY prints no body)
|
|
254
|
+
gh issue view <number> -R cyanheads/mcp-ts-core
|
|
253
255
|
gh issue view <number> -R cyanheads/mcp-ts-core --comments
|
|
254
256
|
|
|
255
257
|
# Add context or respond to maintainer questions
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
File a bug or feature request against this MCP server's own repo. Use for server-specific issues — tool logic, service integrations, config problems, or domain bugs that aren't caused by the framework.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.12"
|
|
8
8
|
audience: external
|
|
9
9
|
type: workflow
|
|
10
10
|
---
|
|
@@ -38,7 +38,8 @@ gh repo view --json nameWithOwner -q '.nameWithOwner'
|
|
|
38
38
|
gh issue list --search "your error message or keyword" --state all
|
|
39
39
|
|
|
40
40
|
# Assess a close match before commenting — is it already linked to a fix or referenced elsewhere?
|
|
41
|
-
gh issue view <number>
|
|
41
|
+
gh issue view <number> # body
|
|
42
|
+
gh issue view <number> --comments # thread only — without a TTY it prints no body
|
|
42
43
|
gh api 'repos/{owner}/{repo}/issues/<number>/timeline' --paginate \
|
|
43
44
|
--jq '.[] | select(.event=="cross-referenced") | .source.issue | "\(.repository.full_name)#\(.number) — \(.title)"'
|
|
44
45
|
```
|
|
@@ -87,11 +88,11 @@ gh issue create \
|
|
|
87
88
|
--body "$(cat <<'ISSUE'
|
|
88
89
|
### Server version
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
<package.json version>
|
|
91
92
|
|
|
92
93
|
### mcp-ts-core version
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
<installed version from node_modules/@cyanheads/mcp-ts-core/package.json — not the ^ range>
|
|
95
96
|
|
|
96
97
|
### Runtime
|
|
97
98
|
|
|
@@ -99,7 +100,7 @@ Bun
|
|
|
99
100
|
|
|
100
101
|
### Runtime version
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
<bun --version>
|
|
103
104
|
|
|
104
105
|
### Transport
|
|
105
106
|
|
|
@@ -258,7 +259,8 @@ When genuinely ambiguous, file against this server's repo and note that it might
|
|
|
258
259
|
## Following Up
|
|
259
260
|
|
|
260
261
|
```bash
|
|
261
|
-
# View issue
|
|
262
|
+
# View the issue body, then its comment thread (--comments without a TTY prints no body)
|
|
263
|
+
gh issue view <number>
|
|
262
264
|
gh issue view <number> --comments
|
|
263
265
|
|
|
264
266
|
# Add context
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Review an MCP server for common security gaps: LLM-facing surfaces as injection vector (tools, resources, prompts, descriptions), scope blast radius, destructive ops without consent, upstream auth shape, input sinks (URL / path / roots / shell / schema strictness / ReDoS), tenant isolation, leakage through errors and telemetry, unbounded resources, and HTTP-mode deployment surface. Use before a release, after a batch of handler changes, or when the user asks for a security review, audit, or hardening pass. Produces grouped findings and a numbered options list.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.11"
|
|
8
8
|
audience: external
|
|
9
9
|
type: audit
|
|
10
10
|
---
|
|
@@ -116,7 +116,7 @@ grep -rn "auth: \[" src/mcp-server/tools/definitions/
|
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
118
|
grep -rn "destructiveHint" src/mcp-server/tools/definitions/
|
|
119
|
-
grep -
|
|
119
|
+
grep -rnE "ctx\.requestInput|ctx\.inputs" src/mcp-server/tools/definitions/
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
**Check:**
|
|
@@ -127,7 +127,7 @@ grep -rn "ctx.requestInput\|ctx.inputs" src/mcp-server/tools/definitions/
|
|
|
127
127
|
- Consent is scoped to the specific target (e.g., record ID rendered in the message), not a generic "proceed?"
|
|
128
128
|
- Any `requestState` carried across rounds is integrity-protected if it influences authorization, resource access, or which target gets mutated. It round-trips through the client and comes back attacker-controlled; the SDK does not sign or verify it.
|
|
129
129
|
- **A consent gate's state is server-issued and single-use.** A client can send `inputResponses` plus a `requestState` of its own on the very FIRST call — honored on 2025-era connections, even from a client that declared no `elicitation` — so a handler that only *compares* client-carried state against a fresh resolution deletes on a forged "accepted" answer without ever prompting. A signed state closes forgery but not replay within its TTL. Keep the confirmed target (plus a content hash, so a same-path swap is caught) in a server-side record keyed by a random id, send only the id, redeem it before anything else in the handler, and refuse an unknown, used, or expired id.
|
|
130
|
-
- **The weak point is answerability, not availability.** `ctx.requestInput` is present on every transport and both protocol eras — the 2025-era shim issues the real `elicitation/create` round trip, the 2026-07-28 client fulfils the embedded request directly. A client that never retries simply leaves the destructive step un-run, which fails safe. Keep `destructiveHint: true` so client-side approval flows still surface the risk, and do not accept "proceed anyway when the round is unavailable" as a fallback
|
|
130
|
+
- **The weak point is answerability, not availability.** `ctx.requestInput` is present on every transport and both protocol eras — the 2025-era shim issues the real `elicitation/create` round trip, the 2026-07-28 client fulfils the embedded request directly. A client that never retries simply leaves the destructive step un-run, which fails safe. Keep `destructiveHint: true` so client-side approval flows still surface the risk, and do not accept "proceed anyway when the round is unavailable" as a fallback. On a 2025-era connection whose client lacks the capability, `ctx.requestInput` throws `client_capability_missing` inside the handler; catching that to run the side effect is exactly this bypass — let it propagate.
|
|
131
131
|
|
|
132
132
|
**Smell:** `destructiveHint: true` file with no `ctx.requestInput` in it. Or `ctx.inputs.accepted('confirm')` with no schema argument — the content could be anything. Or a handler that re-issues the same request after a `decline`.
|
|
133
133
|
|
|
@@ -157,22 +157,22 @@ LLM-supplied inputs feel internal but aren't. Classic sinks apply, amplified. Sa
|
|
|
157
157
|
grep -rn "z.string().url()" src/
|
|
158
158
|
|
|
159
159
|
# Path sinks — traversal
|
|
160
|
-
grep -
|
|
160
|
+
grep -rnE "readFile|writeFile|readdirSync|createReadStream|statSync" src/
|
|
161
161
|
|
|
162
162
|
# Shell sinks — command injection
|
|
163
163
|
grep -rnE "\b(exec|spawn|execSync|spawnSync)\b" src/
|
|
164
164
|
|
|
165
165
|
# Merges — prototype pollution
|
|
166
|
-
grep -
|
|
166
|
+
grep -rnE "Object\.assign\b|structuredClone" src/
|
|
167
167
|
|
|
168
168
|
# Lookups — prototype chain read through an object literal
|
|
169
169
|
grep -rnE "\[[a-zA-Z_$][a-zA-Z0-9_$.]*\] *\?\? |\[[a-zA-Z_$][a-zA-Z0-9_$.]*\] *\|\| " src/
|
|
170
170
|
|
|
171
171
|
# Roots — client-shared filesystem
|
|
172
|
-
grep -
|
|
172
|
+
grep -rnE "roots/list|ctx\.roots" src/
|
|
173
173
|
|
|
174
174
|
# Schema laxity — fields sneaking past validation
|
|
175
|
-
grep -
|
|
175
|
+
grep -rnE "\.passthrough\(\)|\.loose\(\)|looseObject\(|\.catchall\(" src/mcp-server/
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
**Check:**
|
|
@@ -245,7 +245,7 @@ Unbounded = DoS of self, upstream, or the LLM's context window (billing-DoS is r
|
|
|
245
245
|
|
|
246
246
|
```bash
|
|
247
247
|
grep -rnE "while\s*\(|for\s*\(.*of" src/mcp-server/tools/definitions/
|
|
248
|
-
grep -
|
|
248
|
+
grep -rnE "cursor|nextPage|paginate" src/
|
|
249
249
|
grep -rn "JSON.parse\b" src/
|
|
250
250
|
```
|
|
251
251
|
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Catalog of reusable response- and data-shaping techniques for MCP servers built on `@cyanheads/mcp-ts-core` — overflow handling, payload shaping, retrieval patterns. Use when a tool's payload is too large, awkwardly shaped, or expensive to retrieve and you want a proven pattern instead of inventing one. Each technique has a self-contained reference under `references/`.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "0.
|
|
7
|
+
version: "0.4"
|
|
8
8
|
audience: external
|
|
9
9
|
type: reference
|
|
10
10
|
---
|
|
@@ -45,9 +45,12 @@ export const getLabel = tool('get_label', {
|
|
|
45
45
|
.describe('Sections to return. Omit for the full label (or an outline if it overflows).'),
|
|
46
46
|
}),
|
|
47
47
|
output: z.object({
|
|
48
|
-
kind: z.enum(['full', 'outline']),
|
|
48
|
+
kind: z.enum(['full', 'outline']).describe('Whether the full label or a section outline was returned'),
|
|
49
49
|
...FullLabel.partial().shape, // full arm — every field optional
|
|
50
|
-
sections:
|
|
50
|
+
sections: z // outline arm
|
|
51
|
+
.array(OUTLINE_VARIANT.shape.sections.element.describe('One section of the label'))
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Available sections, largest first'),
|
|
51
54
|
notice: OUTLINE_VARIANT.shape.notice.optional(),
|
|
52
55
|
}),
|
|
53
56
|
// Render each arm on field presence, independently — never branch on `kind` (see below).
|
|
@@ -66,6 +69,8 @@ export const getLabel = tool('get_label', {
|
|
|
66
69
|
});
|
|
67
70
|
```
|
|
68
71
|
|
|
72
|
+
The shape lints clean as written. The section item is described in place (`.element.describe(…)`, then the array re-described) because `describe-on-fields` asks every array-of-object element for a description and `OUTLINE_VARIANT`'s item carries none; folding `OUTLINE_VARIANT.shape.sections` in directly warns on `output.sections[]`. The `notice` beside a `sections` array is exempt from `enrichment-prefer-block` — it is the re-call instruction, main-body payload by design.
|
|
73
|
+
|
|
69
74
|
`format()`-parity holds because every terminal field in `output` must appear in the rendered text. With a flat object the linter builds **one** synthetic sample with every optional field populated at once, so render each arm on field presence, independently — a mutually-exclusive `if (kind === 'outline') … else …` renders only one arm against that all-fields sample and fails parity for the other. `formatOutline` is the shipped renderer for the `outline` arm; you supply the `full` renderer. That keeps the two client surfaces in lockstep.
|
|
70
75
|
|
|
71
76
|
## The helper
|
|
@@ -75,9 +80,9 @@ export const getLabel = tool('get_label', {
|
|
|
75
80
|
| Export | Purpose |
|
|
76
81
|
|:--|:--|
|
|
77
82
|
| `outlineOnOverflow(doc, options?)` | Returns `{ kind: 'full', ...doc }` under budget (or with `< 2` sections), else `{ kind: 'outline', sections, notice }`. |
|
|
78
|
-
| `OUTLINE_VARIANT` | The reusable `outline`-arm Zod schema; fold `.shape.sections` / `.shape.notice` into your flat `output` object as optional arms. |
|
|
79
|
-
| `selectSections(doc, want, { alwaysKeep })` | Projects the document to requested keys plus always-kept metadata. The selection-path counterpart. |
|
|
80
|
-
| `formatOutline(outline)` | Renders the outline to `content[]` for `format()`. |
|
|
83
|
+
| `OUTLINE_VARIANT` | The reusable `outline`-arm Zod schema; fold `.shape.sections` (item described in place, as above) / `.shape.notice` into your flat `output` object as optional arms. |
|
|
84
|
+
| `selectSections(doc, want, { alwaysKeep })` | Projects the document to requested keys plus always-kept metadata. The selection-path counterpart. A requested name that is not a key of `doc` throws `InvalidParams`; the message names the unmatched names and the available keys, and `data` carries both (`unmatched`, `available`). An `alwaysKeep` key absent from `doc` is ignored. |
|
|
85
|
+
| `formatOutline(outline)` | Renders the outline to `content[]` for `format()`. Each section name is a code span sized past any backtick in it, so the name reads back exactly as the caller must pass it in `sections`. |
|
|
81
86
|
| `DEFAULT_OUTLINE_BUDGET_BYTES` | The default budget (`24_000`) when `options.budget` is omitted. |
|
|
82
87
|
|
|
83
88
|
`outlineOnOverflow` options:
|
|
@@ -93,14 +98,14 @@ The flow:
|
|
|
93
98
|
3. **Over budget, ≥ 2 sections** → the outline (sections sorted largest-first). The agent re-calls with `sections: [...]`.
|
|
94
99
|
4. **Over budget, < 2 sections** → `full` anyway (nothing to pick between). A single section that *alone* exceeds budget is a known limitation — sub-section outlining is out of scope.
|
|
95
100
|
|
|
96
|
-
The budget bounds the **disclosure**, not the selection. `selectSections` returns whatever the agent named, so a selection over several sections — or one section larger than the budget — comes back whole. That is deliberate: the agent asked for those sections by name, and truncating the answer is the thing this technique exists to avoid. The default notice reports each example's size so the selection can be sized before it is made.
|
|
101
|
+
The budget bounds the **disclosure**, not the selection. `selectSections` returns whatever the agent named, so a selection over several sections — or one section larger than the budget — comes back whole. That is deliberate: the agent asked for those sections by name, and truncating the answer is the thing this technique exists to avoid. The default notice reports each example's size so the selection can be sized before it is made. The selection is not lenient about names, though: a name the document does not carry is rejected with the valid names, not dropped, so a stale or mistyped section never comes back as a quietly smaller answer.
|
|
97
102
|
|
|
98
103
|
## Re-retrieval — why the selection call is stateless
|
|
99
104
|
|
|
100
105
|
The re-call is **self-contained**, so nothing is stored between the outline call and the selection call:
|
|
101
106
|
|
|
102
107
|
- The selection call sends the **same input** as the outline call, plus `sections: [...]`.
|
|
103
|
-
- The handler **re-fetches** the document — input-minus-`sections` is identical and the upstream query is deterministic, so it reproduces the exact same record — then applies `selectSections` (a pure projection: requested keys + `alwaysKeep` metadata).
|
|
108
|
+
- The handler **re-fetches** the document — input-minus-`sections` is identical and the upstream query is deterministic, so it reproduces the exact same record — then applies `selectSections` (a pure projection: requested keys + `alwaysKeep` metadata; an unknown requested name throws `InvalidParams` naming the available keys).
|
|
104
109
|
- You **reconstruct rather than remember**. The agent holds the continuity (it passes `sections`); the upstream holds the document.
|
|
105
110
|
|
|
106
111
|
The only cost is the redundant fetch. For a **rate-limited or expensive upstream**, trade it for an optional cache:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/mcp-ts-core",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.9",
|
|
4
4
|
"mcpName": "io.github.cyanheads/mcp-ts-core",
|
|
5
5
|
"description": "Agent-native TypeScript framework for MCP servers. Includes runtime infrastructure and agent skills for building, testing, and shipping servers.",
|
|
6
6
|
"files": [
|
|
@@ -201,18 +201,21 @@
|
|
|
201
201
|
"@cloudflare/workers-types": "5.20260922.1",
|
|
202
202
|
"@duckdb/node-api": "^1.5.5-r.5",
|
|
203
203
|
"@hono/otel": "^1.1.2",
|
|
204
|
-
"@modelcontextprotocol/client": "^2.
|
|
204
|
+
"@modelcontextprotocol/client": "^2.1.0",
|
|
205
|
+
"@opentelemetry/api-logs": "^0.222.0",
|
|
206
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.222.0",
|
|
205
207
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.222.0",
|
|
206
208
|
"@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
|
|
207
209
|
"@opentelemetry/instrumentation-http": "^0.222.0",
|
|
208
210
|
"@opentelemetry/instrumentation-pino": "^0.68.0",
|
|
209
211
|
"@opentelemetry/resources": "^2.11.0",
|
|
212
|
+
"@opentelemetry/sdk-logs": "^0.222.0",
|
|
210
213
|
"@opentelemetry/sdk-metrics": "^2.11.0",
|
|
211
214
|
"@opentelemetry/sdk-node": "^0.222.0",
|
|
212
215
|
"@opentelemetry/sdk-trace-node": "^2.11.0",
|
|
213
216
|
"@opentelemetry/semantic-conventions": "^1.43.0",
|
|
214
217
|
"@socketsecurity/bun-security-scanner": "^1.1.3",
|
|
215
|
-
"@supabase/supabase-js": "^2.117.
|
|
218
|
+
"@supabase/supabase-js": "^2.117.1",
|
|
216
219
|
"@types/bun": "^1.4.2",
|
|
217
220
|
"@types/node": "26.6.2",
|
|
218
221
|
"@types/papaparse": "^5.5.2",
|
|
@@ -232,7 +235,7 @@
|
|
|
232
235
|
"js-yaml": "^5.4.2",
|
|
233
236
|
"linkedom": "^0.18.13",
|
|
234
237
|
"node-cron": "^4.6.0",
|
|
235
|
-
"openai": "^7.
|
|
238
|
+
"openai": "^7.23.0",
|
|
236
239
|
"papaparse": "^5.7.0",
|
|
237
240
|
"partial-json": "^0.1.7",
|
|
238
241
|
"pdf-lib": "^1.17.1",
|
|
@@ -287,7 +290,7 @@
|
|
|
287
290
|
},
|
|
288
291
|
"dependencies": {
|
|
289
292
|
"@hono/node-server": "^2.1.1",
|
|
290
|
-
"@modelcontextprotocol/server": "^2.
|
|
293
|
+
"@modelcontextprotocol/server": "^2.1.0",
|
|
291
294
|
"@opentelemetry/api": "^1.9.1",
|
|
292
295
|
"hono": "^4.13.8",
|
|
293
296
|
"jose": "^6.2.12",
|
|
@@ -297,11 +300,14 @@
|
|
|
297
300
|
"peerDependencies": {
|
|
298
301
|
"@duckdb/node-api": "^1.5.5-r.1",
|
|
299
302
|
"@hono/otel": "^1.1.2",
|
|
303
|
+
"@opentelemetry/api-logs": "^0.222.0",
|
|
304
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.222.0",
|
|
300
305
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.222.0",
|
|
301
306
|
"@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
|
|
302
307
|
"@opentelemetry/instrumentation-http": "^0.222.0",
|
|
303
308
|
"@opentelemetry/instrumentation-pino": "^0.68.0",
|
|
304
309
|
"@opentelemetry/resources": "^2.10.0",
|
|
310
|
+
"@opentelemetry/sdk-logs": "^0.222.0",
|
|
305
311
|
"@opentelemetry/sdk-metrics": "^2.10.0",
|
|
306
312
|
"@opentelemetry/sdk-node": "^0.222.0",
|
|
307
313
|
"@opentelemetry/sdk-trace-node": "^2.10.0",
|
|
@@ -332,6 +338,12 @@
|
|
|
332
338
|
"@hono/otel": {
|
|
333
339
|
"optional": true
|
|
334
340
|
},
|
|
341
|
+
"@opentelemetry/api-logs": {
|
|
342
|
+
"optional": true
|
|
343
|
+
},
|
|
344
|
+
"@opentelemetry/exporter-logs-otlp-http": {
|
|
345
|
+
"optional": true
|
|
346
|
+
},
|
|
335
347
|
"@opentelemetry/instrumentation-http": {
|
|
336
348
|
"optional": true
|
|
337
349
|
},
|
|
@@ -347,6 +359,9 @@
|
|
|
347
359
|
"@opentelemetry/resources": {
|
|
348
360
|
"optional": true
|
|
349
361
|
},
|
|
362
|
+
"@opentelemetry/sdk-logs": {
|
|
363
|
+
"optional": true
|
|
364
|
+
},
|
|
350
365
|
"@opentelemetry/sdk-metrics": {
|
|
351
366
|
"optional": true
|
|
352
367
|
},
|
|
@@ -20,10 +20,19 @@
|
|
|
20
20
|
*
|
|
21
21
|
* A bare name (`add-tool`) and the file path (`add-tool/SKILL.md`) both match.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* In the framework repo, a skill whose version already moved since the last `v*`
|
|
24
|
+
* release tag — or that is new since it — is covered: a later body edit in the
|
|
25
|
+
* same cycle shares that bump instead of needing another.
|
|
26
|
+
*
|
|
27
|
+
* The inverse also holds: a skill moves at most one step per release. In the
|
|
28
|
+
* framework repo itself, each skill's version is compared with its version at the
|
|
29
|
+
* last `v*` release tag, and anything past the next minor (`1.4` → `1.5`) or the
|
|
30
|
+
* next major (`1.4` → `2.0`) is a violation — repeated edits in one cycle share a
|
|
31
|
+
* single bump. Consumer repos skip this: a skill sync can legitimately jump
|
|
32
|
+
* several framework releases at once.
|
|
33
|
+
*
|
|
25
34
|
* Severity mirrors `check-skills-sync.ts` — exits 1, demoted to a warning by
|
|
26
|
-
* devcheck. New skills (no
|
|
35
|
+
* devcheck. New skills (no prior version) and non-git trees are skipped.
|
|
27
36
|
*
|
|
28
37
|
* Runs standalone (`bun run scripts/check-skill-versions.ts`) and as a devcheck step.
|
|
29
38
|
*
|
|
@@ -36,6 +45,7 @@ import process from 'node:process';
|
|
|
36
45
|
|
|
37
46
|
const ROOT = resolve('.');
|
|
38
47
|
const SKILL_MD_RE = /^framework-skills\/[^/]+\/SKILL\.md$/;
|
|
48
|
+
const FRAMEWORK_PACKAGE = '@cyanheads/mcp-ts-core';
|
|
39
49
|
|
|
40
50
|
interface DevcheckConfig {
|
|
41
51
|
skillVersions?: { ignore?: string[] };
|
|
@@ -60,10 +70,10 @@ function isIgnored(relPath: string, patterns: string[]): boolean {
|
|
|
60
70
|
);
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
/** Skill `SKILL.md` files that differ from `
|
|
64
|
-
function changedSkillFiles(): string[] {
|
|
65
|
-
const result = spawnSync('git', ['diff', '--name-only',
|
|
66
|
-
if (result.status !== 0) return []; // not a git repo / no
|
|
73
|
+
/** Skill `SKILL.md` files that differ from `ref` in the working tree (staged + unstaged). */
|
|
74
|
+
function changedSkillFiles(ref: string): string[] {
|
|
75
|
+
const result = spawnSync('git', ['diff', '--name-only', ref, '--'], { encoding: 'utf-8' });
|
|
76
|
+
if (result.status !== 0) return []; // not a git repo / no such ref
|
|
67
77
|
return result.stdout
|
|
68
78
|
.trim()
|
|
69
79
|
.split('\n')
|
|
@@ -71,18 +81,50 @@ function changedSkillFiles(): string[] {
|
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
/**
|
|
74
|
-
* Content of a path at `
|
|
75
|
-
* A tree renamed from the pre-0.13 `skills/` reads its
|
|
84
|
+
* Content of a path at `ref`, or null when it didn't exist there (new file).
|
|
85
|
+
* A tree renamed from the pre-0.13 `skills/` reads its old copy from the old
|
|
76
86
|
* path, so the release that carries the rename still checks every body edit.
|
|
77
87
|
*/
|
|
78
|
-
function
|
|
79
|
-
const show = (p: string) => spawnSync('git', ['show',
|
|
88
|
+
function contentAt(ref: string, relPath: string): string | null {
|
|
89
|
+
const show = (p: string) => spawnSync('git', ['show', `${ref}:${p}`], { encoding: 'utf-8' });
|
|
80
90
|
const result = show(relPath);
|
|
81
91
|
if (result.status === 0) return result.stdout;
|
|
82
92
|
const legacy = show(relPath.replace(/^framework-skills\//, 'skills/'));
|
|
83
93
|
return legacy.status === 0 ? legacy.stdout : null;
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
/** True when this tree is the framework itself, where skill versions are authored. */
|
|
97
|
+
function isFrameworkRepo(): boolean {
|
|
98
|
+
try {
|
|
99
|
+
const pkg = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')) as {
|
|
100
|
+
name?: string;
|
|
101
|
+
};
|
|
102
|
+
return pkg.name === FRAMEWORK_PACKAGE;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The latest `v*` release tag reachable from `HEAD`, or null when there is none. */
|
|
109
|
+
function lastReleaseTag(): string | null {
|
|
110
|
+
const result = spawnSync('git', ['describe', '--tags', '--abbrev=0', '--match', 'v*'], {
|
|
111
|
+
encoding: 'utf-8',
|
|
112
|
+
});
|
|
113
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** True when `to` is `from`, its next minor, or its next major at `.0`. */
|
|
117
|
+
function withinOneStep(from: string, to: string): boolean {
|
|
118
|
+
const [fromMajor, fromMinor] = from.split('.').map(Number);
|
|
119
|
+
const [toMajor, toMinor] = to.split('.').map(Number);
|
|
120
|
+
if ([fromMajor, fromMinor, toMajor, toMinor].some((n) => n === undefined || Number.isNaN(n))) {
|
|
121
|
+
return true; // not `X.Y` — out of this check's scope
|
|
122
|
+
}
|
|
123
|
+
if (from === to) return true;
|
|
124
|
+
if (toMajor === fromMajor) return toMinor === (fromMinor as number) + 1;
|
|
125
|
+
return toMajor === (fromMajor as number) + 1 && toMinor === 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
86
128
|
/** `metadata.version` from skill frontmatter, or null when absent/unparseable. */
|
|
87
129
|
function extractVersion(content: string): string | null {
|
|
88
130
|
const block = content.match(/^---\n([\s\S]*?)\n---/)?.[1];
|
|
@@ -108,11 +150,24 @@ if (!existsSync(resolve(ROOT, 'framework-skills'))) {
|
|
|
108
150
|
}
|
|
109
151
|
|
|
110
152
|
const ignore = loadIgnorePatterns();
|
|
111
|
-
const
|
|
153
|
+
const tag = isFrameworkRepo() ? lastReleaseTag() : null;
|
|
112
154
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
155
|
+
/**
|
|
156
|
+
* True when the skill's current version already differs from its version at the
|
|
157
|
+
* release tag — or the skill is new since it — so this cycle's one step is taken
|
|
158
|
+
* and a further body edit shares it rather than needing another bump.
|
|
159
|
+
*/
|
|
160
|
+
function bumpedThisRelease(file: string, version: string | null): boolean {
|
|
161
|
+
if (tag === null) return false;
|
|
162
|
+
const released = contentAt(tag, file);
|
|
163
|
+
if (released === null) return true;
|
|
164
|
+
const releasedVersion = extractVersion(released);
|
|
165
|
+
return releasedVersion !== null && releasedVersion !== version;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const missing: { file: string; version: string }[] = [];
|
|
169
|
+
for (const file of changedSkillFiles('HEAD').filter((f) => !isIgnored(f, ignore))) {
|
|
170
|
+
const oldContent = contentAt('HEAD', file);
|
|
116
171
|
if (oldContent === null) continue; // new skill — no prior version to compare
|
|
117
172
|
if (!existsSync(resolve(ROOT, file))) continue; // deleted in worktree — no body to compare, can't violate
|
|
118
173
|
const newContent = readFileSync(resolve(ROOT, file), 'utf-8');
|
|
@@ -121,25 +176,51 @@ for (const file of changed) {
|
|
|
121
176
|
|
|
122
177
|
const oldVersion = extractVersion(oldContent);
|
|
123
178
|
const newVersion = extractVersion(newContent);
|
|
124
|
-
if (oldVersion !== null && oldVersion === newVersion) {
|
|
125
|
-
|
|
179
|
+
if (oldVersion !== null && oldVersion === newVersion && !bumpedThisRelease(file, newVersion)) {
|
|
180
|
+
missing.push({ file, version: oldVersion });
|
|
126
181
|
}
|
|
127
182
|
}
|
|
128
183
|
|
|
129
|
-
|
|
184
|
+
const overshot: { file: string; tag: string; released: string; version: string }[] = [];
|
|
185
|
+
if (tag !== null) {
|
|
186
|
+
for (const file of changedSkillFiles(tag)) {
|
|
187
|
+
const released = contentAt(tag, file);
|
|
188
|
+
if (released === null || !existsSync(resolve(ROOT, file))) continue;
|
|
189
|
+
const releasedVersion = extractVersion(released);
|
|
190
|
+
const version = extractVersion(readFileSync(resolve(ROOT, file), 'utf-8'));
|
|
191
|
+
if (releasedVersion === null || version === null) continue;
|
|
192
|
+
if (!withinOneStep(releasedVersion, version)) {
|
|
193
|
+
overshot.push({ file, tag, released: releasedVersion, version });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const count = missing.length + overshot.length;
|
|
199
|
+
if (count === 0) {
|
|
130
200
|
console.log('Skill versions are in step with body changes.');
|
|
131
201
|
process.exit(0);
|
|
132
202
|
}
|
|
133
203
|
|
|
134
204
|
const lines = [
|
|
135
|
-
`${
|
|
205
|
+
`${count} skill${count === 1 ? '' : 's'} out of step with the versioning policy:`,
|
|
136
206
|
'',
|
|
137
207
|
];
|
|
138
|
-
for (const v of
|
|
208
|
+
for (const v of missing) {
|
|
139
209
|
lines.push(` - ${v.file} body changed but metadata.version is still "${v.version}"`);
|
|
140
210
|
}
|
|
211
|
+
for (const v of overshot) {
|
|
212
|
+
lines.push(
|
|
213
|
+
` - ${v.file} is "${v.version}", more than one step past "${v.released}" at ${v.tag}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
141
216
|
lines.push('');
|
|
142
|
-
|
|
143
|
-
lines.push('
|
|
217
|
+
if (missing.length > 0) {
|
|
218
|
+
lines.push('Fix: bump metadata.version in the SKILL.md frontmatter, or add the skill to');
|
|
219
|
+
lines.push(' devcheck.config.json `skillVersions.ignore` for the typo/whitespace carve-out.');
|
|
220
|
+
}
|
|
221
|
+
if (overshot.length > 0) {
|
|
222
|
+
lines.push('Fix: set metadata.version to one step past the release tag — edits in one');
|
|
223
|
+
lines.push(' release cycle share a single bump.');
|
|
224
|
+
}
|
|
144
225
|
console.log(lines.join('\n'));
|
|
145
226
|
process.exit(1);
|
package/scripts/devcheck.ts
CHANGED
|
@@ -711,12 +711,12 @@ const ALL_CHECKS: Check[] = [
|
|
|
711
711
|
canFix: false,
|
|
712
712
|
// Validates env var alignment between manifest.json (MCPB bundle) and
|
|
713
713
|
// server.json (MCP Registry), plus plugin marketplace manifests (#240), the
|
|
714
|
-
// bundle-content guards on .mcpbignore (#343),
|
|
715
|
-
// (#418). Runs when any of those inputs
|
|
716
|
-
// none exist — consumers on an HTTP-only
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
// only covered incidentally.
|
|
714
|
+
// bundle-content guards on .mcpbignore (#343), the README version badge
|
|
715
|
+
// (#418), and the Dockerfile build platform. Runs when any of those inputs
|
|
716
|
+
// is present; skipped cleanly when none exist — consumers on an HTTP-only
|
|
717
|
+
// deploy are unaffected. README.md and Dockerfile are triggers in their own
|
|
718
|
+
// right: each check must gate a project that carries no bundle or plugin
|
|
719
|
+
// metadata at all, which the other inputs only covered incidentally.
|
|
720
720
|
getCommand: () => {
|
|
721
721
|
const inputs = [
|
|
722
722
|
'manifest.json',
|
|
@@ -725,6 +725,7 @@ const ALL_CHECKS: Check[] = [
|
|
|
725
725
|
'.codex-plugin/mcp.json',
|
|
726
726
|
'.mcpbignore',
|
|
727
727
|
'README.md',
|
|
728
|
+
'Dockerfile',
|
|
728
729
|
];
|
|
729
730
|
if (!inputs.some((input) => existsSync(path.join(ROOT_DIR, input)))) return null;
|
|
730
731
|
return ['bun', 'run', 'scripts/lint-packaging.ts'];
|
|
@@ -811,7 +812,8 @@ const ALL_CHECKS: Check[] = [
|
|
|
811
812
|
flag: '--no-skill-versions',
|
|
812
813
|
canFix: false,
|
|
813
814
|
// Flags framework-skills/<name>/SKILL.md body changes (vs HEAD) that lack a metadata.version
|
|
814
|
-
// bump (#99)
|
|
815
|
+
// bump (#99), and, in the framework repo, a skill bumped more than one step past the last
|
|
816
|
+
// release tag. Skipped when framework-skills/ is absent. Drift is demoted to a warning via
|
|
815
817
|
// isSuccess — the typo/whitespace carve-out lives in devcheck.config.json
|
|
816
818
|
// `skillVersions.ignore`.
|
|
817
819
|
getCommand: () => {
|
|
@@ -821,11 +823,11 @@ const ALL_CHECKS: Check[] = [
|
|
|
821
823
|
isSuccess: (result) => {
|
|
822
824
|
if (result.exitCode === 0) return true;
|
|
823
825
|
const firstLine =
|
|
824
|
-
result.stdout.split('\n')[0]?.trim() || 'Skill
|
|
826
|
+
result.stdout.split('\n')[0]?.trim() || 'Skill versions are out of step with the policy.';
|
|
825
827
|
return { success: true, warning: firstLine };
|
|
826
828
|
},
|
|
827
829
|
tip: (c) =>
|
|
828
|
-
`Bump ${c.bold('metadata.version')} in the changed ${c.bold('SKILL.md')}, or add it to ${c.bold('devcheck.config.json')} ${c.bold('skillVersions.ignore')}.`,
|
|
830
|
+
`Bump ${c.bold('metadata.version')} once per release in the changed ${c.bold('SKILL.md')}, or add it to ${c.bold('devcheck.config.json')} ${c.bold('skillVersions.ignore')}.`,
|
|
829
831
|
},
|
|
830
832
|
{
|
|
831
833
|
name: 'Changelog Sync',
|