@cyanheads/mcp-ts-core 0.12.7 → 0.12.8
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 +8 -3
- package/CLAUDE.md +8 -3
- package/README.md +11 -4
- package/changelog/0.12.x/0.12.8.md +55 -0
- package/dist/config/index.d.ts +3 -34
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +4 -26
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts +0 -8
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +0 -7
- package/dist/core/app.js.map +1 -1
- package/dist/core/serverManifest.d.ts +0 -7
- package/dist/core/serverManifest.d.ts.map +1 -1
- package/dist/core/serverManifest.js +1 -13
- package/dist/core/serverManifest.js.map +1 -1
- package/dist/linter/rules/enrichment-rules.js +2 -2
- package/dist/linter/rules/enrichment-rules.js.map +1 -1
- package/dist/linter/rules/format-parity-rules.d.ts.map +1 -1
- package/dist/linter/rules/format-parity-rules.js +14 -36
- package/dist/linter/rules/format-parity-rules.js.map +1 -1
- package/dist/linter/rules/prompt-rules.d.ts +1 -1
- package/dist/linter/rules/prompt-rules.d.ts.map +1 -1
- package/dist/linter/rules/prompt-rules.js +2 -19
- package/dist/linter/rules/prompt-rules.js.map +1 -1
- package/dist/linter/rules/resource-rules.d.ts +1 -1
- package/dist/linter/rules/resource-rules.d.ts.map +1 -1
- package/dist/linter/rules/resource-rules.js +9 -39
- package/dist/linter/rules/resource-rules.js.map +1 -1
- package/dist/linter/rules/schema-rules.d.ts +22 -2
- package/dist/linter/rules/schema-rules.d.ts.map +1 -1
- package/dist/linter/rules/schema-rules.js +28 -5
- package/dist/linter/rules/schema-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts +1 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +13 -41
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/linter/validate.d.ts.map +1 -1
- package/dist/linter/validate.js +22 -42
- package/dist/linter/validate.js.map +1 -1
- package/dist/mcp-server/apps/appBuilders.d.ts.map +1 -1
- package/dist/mcp-server/apps/appBuilders.js +2 -16
- package/dist/mcp-server/apps/appBuilders.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +66 -0
- package/dist/mcp-server/handlerContext.d.ts.map +1 -0
- package/dist/mcp-server/handlerContext.js +71 -0
- package/dist/mcp-server/handlerContext.js.map +1 -0
- package/dist/mcp-server/inputRequired.d.ts +7 -1
- package/dist/mcp-server/inputRequired.d.ts.map +1 -1
- package/dist/mcp-server/inputRequired.js +10 -3
- package/dist/mcp-server/inputRequired.js.map +1 -1
- package/dist/mcp-server/resources/resource-registration.d.ts +2 -2
- package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
- package/dist/mcp-server/resources/resource-registration.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +14 -43
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +11 -50
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/tools/tool-registration.d.ts +5 -9
- package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-registration.js +9 -11
- package/dist/mcp-server/tools/tool-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/schemaShape.d.ts +21 -0
- package/dist/mcp-server/tools/utils/schemaShape.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/schemaShape.js +8 -6
- package/dist/mcp-server/tools/utils/schemaShape.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +15 -43
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +31 -72
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js +2 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
- package/dist/mcp-server/transports/http/landing-page/handler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/landing-page/handler.js +2 -1
- package/dist/mcp-server/transports/http/landing-page/handler.js.map +1 -1
- package/dist/mcp-server/transports/http/protectedResourceMetadata.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/protectedResourceMetadata.js +2 -1
- package/dist/mcp-server/transports/http/protectedResourceMetadata.js.map +1 -1
- package/dist/mcp-server/transports/http/publicOrigin.d.ts +11 -0
- package/dist/mcp-server/transports/http/publicOrigin.d.ts.map +1 -0
- package/dist/mcp-server/transports/http/publicOrigin.js +13 -0
- package/dist/mcp-server/transports/http/publicOrigin.js.map +1 -0
- package/dist/mcp-server/transports/http/serverCard.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/serverCard.js +2 -1
- package/dist/mcp-server/transports/http/serverCard.js.map +1 -1
- package/dist/mcp-server/transports/http/sessionIdUtils.d.ts +4 -0
- package/dist/mcp-server/transports/http/sessionIdUtils.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/sessionIdUtils.js +3 -13
- package/dist/mcp-server/transports/http/sessionIdUtils.js.map +1 -1
- package/dist/mcp-server/transports/manager.d.ts +0 -3
- package/dist/mcp-server/transports/manager.d.ts.map +1 -1
- package/dist/mcp-server/transports/manager.js +0 -7
- package/dist/mcp-server/transports/manager.js.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.d.ts +14 -0
- package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.js +3 -2
- package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +16 -0
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +78 -103
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
- package/dist/services/graph/core/GraphService.d.ts +3 -3
- package/dist/services/graph/core/GraphService.js +3 -3
- package/dist/services/graph/types.d.ts +2 -79
- package/dist/services/graph/types.d.ts.map +1 -1
- package/dist/services/graph/types.js +2 -2
- package/dist/services/index.d.ts +1 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +0 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/mirror/sqlite/handle.d.ts.map +1 -1
- package/dist/services/mirror/sqlite/handle.js +22 -36
- package/dist/services/mirror/sqlite/handle.js.map +1 -1
- package/dist/services/speech/core/ISpeechProvider.d.ts +0 -24
- package/dist/services/speech/core/ISpeechProvider.d.ts.map +1 -1
- package/dist/services/speech/core/ISpeechProvider.js +1 -28
- package/dist/services/speech/core/ISpeechProvider.js.map +1 -1
- package/dist/services/speech/core/SpeechService.d.ts.map +1 -1
- package/dist/services/speech/core/SpeechService.js +5 -8
- package/dist/services/speech/core/SpeechService.js.map +1 -1
- package/dist/services/speech/providers/elevenlabs.provider.d.ts.map +1 -1
- package/dist/services/speech/providers/elevenlabs.provider.js +1 -0
- package/dist/services/speech/providers/elevenlabs.provider.js.map +1 -1
- package/dist/services/speech/types.d.ts +2 -19
- package/dist/services/speech/types.d.ts.map +1 -1
- package/dist/storage/core/providerHelpers.d.ts +52 -0
- package/dist/storage/core/providerHelpers.d.ts.map +1 -0
- package/dist/storage/core/providerHelpers.js +96 -0
- package/dist/storage/core/providerHelpers.js.map +1 -0
- package/dist/storage/providers/cloudflare/d1Provider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.js +1 -4
- 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 +4 -31
- package/dist/storage/providers/cloudflare/kvProvider.js.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.d.ts +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.js +8 -48
- package/dist/storage/providers/cloudflare/r2Provider.js.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.js +17 -86
- package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.js +5 -38
- package/dist/storage/providers/inMemory/inMemoryProvider.js.map +1 -1
- package/dist/storage/providers/supabase/supabaseProvider.d.ts.map +1 -1
- package/dist/storage/providers/supabase/supabaseProvider.js +1 -4
- package/dist/storage/providers/supabase/supabaseProvider.js.map +1 -1
- package/dist/testing/fuzz.d.ts.map +1 -1
- package/dist/testing/fuzz.js +17 -31
- package/dist/testing/fuzz.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +4 -26
- package/dist/testing/index.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +0 -4
- package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
- package/dist/utils/internal/logger.d.ts.map +1 -1
- package/dist/utils/internal/logger.js +2 -16
- package/dist/utils/internal/logger.js.map +1 -1
- package/dist/utils/internal/performance.d.ts +8 -31
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +173 -295
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/security/idGenerator.d.ts.map +1 -1
- package/dist/utils/security/idGenerator.js +24 -43
- package/dist/utils/security/idGenerator.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +0 -7
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +4 -31
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/security/sensitiveFields.d.ts +14 -0
- package/dist/utils/security/sensitiveFields.d.ts.map +1 -0
- package/dist/utils/security/sensitiveFields.js +31 -0
- package/dist/utils/security/sensitiveFields.js.map +1 -0
- package/dist/utils/telemetry/trace.d.ts +8 -10
- package/dist/utils/telemetry/trace.d.ts.map +1 -1
- package/dist/utils/telemetry/trace.js +19 -18
- package/dist/utils/telemetry/trace.js.map +1 -1
- package/dist/utils/types/guards.d.ts +0 -102
- package/dist/utils/types/guards.d.ts.map +1 -1
- package/dist/utils/types/guards.js +0 -114
- package/dist/utils/types/guards.js.map +1 -1
- package/package.json +6 -6
- package/skills/add-provider/SKILL.md +18 -4
- package/skills/api-config/SKILL.md +4 -18
- package/skills/api-services/SKILL.md +1 -1
- package/skills/api-services/references/speech.md +1 -2
- package/skills/api-telemetry/SKILL.md +2 -2
- package/skills/api-utils/SKILL.md +2 -2
- package/skills/code-simplifier/SKILL.md +47 -20
- package/skills/field-test/SKILL.md +93 -14
- package/skills/git-wrapup/SKILL.md +64 -27
- package/skills/orchestrations/SKILL.md +17 -6
- package/skills/orchestrations/workflows/field-test-fix.md +6 -4
- package/skills/orchestrations/workflows/fix-wrapup-release.md +6 -4
- package/skills/orchestrations/workflows/greenfield-build.md +2 -2
- package/skills/orchestrations/workflows/maintenance-release.md +4 -2
- package/skills/release-and-publish/SKILL.md +101 -23
- package/skills/release-pr-review/SKILL.md +147 -0
- package/templates/AGENTS.md +4 -2
- package/templates/CLAUDE.md +4 -2
- package/dist/mcp-server/transports/ITransport.d.ts +0 -15
- package/dist/mcp-server/transports/ITransport.d.ts.map +0 -1
- package/dist/mcp-server/transports/ITransport.js +0 -2
- package/dist/mcp-server/transports/ITransport.js.map +0 -1
- package/dist/services/llm/types.d.ts +0 -16
- package/dist/services/llm/types.d.ts.map +0 -1
- package/dist/services/llm/types.js +0 -9
- package/dist/services/llm/types.js.map +0 -1
- package/dist/utils/internal/health.d.ts +0 -60
- package/dist/utils/internal/health.d.ts.map +0 -1
- package/dist/utils/internal/health.js +0 -46
- package/dist/utils/internal/health.js.map +0 -1
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Post-session code review and cleanup against a working tree of changes. Analyzes `git diff` to simplify, consolidate, and align changed code with the existing codebase — modernize syntax, remove unnecessary complexity, consolidate duplicated logic, catch efficiency issues. Use after a substantive working session, or when asked to clean up, simplify, reduce slop, consolidate, modernize, tighten up, or de-slop code. For `@cyanheads/mcp-ts-core` projects, includes specific transformations for tool/resource/prompt definitions, the ctx pattern, error factories, and framework idioms.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.4"
|
|
8
8
|
audience: external
|
|
9
9
|
type: workflow
|
|
10
10
|
---
|
|
@@ -21,7 +21,7 @@ Post-session cleanup pass. Reviews what changed, understands how it fits the exi
|
|
|
21
21
|
|
|
22
22
|
### Phase 1: Identify changes
|
|
23
23
|
|
|
24
|
-
Run `git status` to see the shape of the working tree, then `git diff HEAD` for all uncommitted changes (staged and unstaged). Untracked files never appear in the diff — read new files directly. If the
|
|
24
|
+
Run `git status` to see the shape of the working tree, then `git diff HEAD` for all uncommitted changes (staged and unstaged). Untracked files never appear in the diff — read new files directly. If the diff is empty and there are no untracked files, review the last commit (`git diff HEAD~1 HEAD`); if that is also empty, say the tree is clean and stop. Don't go hunting through the codebase for files to improve.
|
|
25
25
|
|
|
26
26
|
### Phase 2: Understand the surrounding codebase
|
|
27
27
|
|
|
@@ -29,7 +29,8 @@ Don't review changes in isolation. Before any modifications:
|
|
|
29
29
|
|
|
30
30
|
1. **Read the full files** containing changes — not just the diff hunks. Understand imports, surrounding logic, module structure.
|
|
31
31
|
2. **Identify the project language(s)** and select the relevant transformation rules. Discard inapplicable rules.
|
|
32
|
-
3. **Survey adjacent code** — shared utilities, sibling modules, common patterns. You need to know what already exists before deciding something is missing.
|
|
32
|
+
3. **Survey adjacent code** — shared utilities, sibling modules, common patterns. You need to know what already exists before deciding something is missing.
|
|
33
|
+
4. **Run the project's gate once before editing** to establish a baseline. Find it in `package.json` scripts — `devcheck` if present, else `check`, else the separate `typecheck` / `lint` / `test` scripts; Python projects gate on `uv run ruff check`, `uv run ruff format --check`, and the configured type checker and test runner. In a Bun project that tests with Vitest, run `bun run test` — bare `bun test` bypasses the script and runs Bun's own runner. If the gate is already red, say so in the summary and don't attribute the failure to your changes.
|
|
33
34
|
|
|
34
35
|
### Phase 3: Review
|
|
35
36
|
|
|
@@ -37,29 +38,35 @@ Evaluate the changes across these dimensions. Not every dimension applies to eve
|
|
|
37
38
|
|
|
38
39
|
#### Codebase cohesion
|
|
39
40
|
|
|
40
|
-
- **Reuse** — Search for existing utilities, helpers, and patterns that could replace newly-written code.
|
|
41
|
+
- **Reuse** — Search for existing utilities, helpers, and patterns that could replace newly-written code. Check utility directories, shared modules, and files adjacent to the changed ones. If a function already exists that does what the new code does, use it.
|
|
41
42
|
- **Consolidation** — Flag copy-paste-with-variation: near-duplicate code blocks that should be unified. Only unify if the shared abstraction is genuinely simpler than the duplicated code.
|
|
42
43
|
- **Consistency** — Check that new code follows the same patterns as the rest of the codebase: naming conventions, error handling style, import patterns, type annotation style. Normalize toward the better variant when the project is inconsistent.
|
|
43
|
-
- **Stringly-typed code** — Flag raw strings where constants, string-union types, branded types
|
|
44
|
+
- **Stringly-typed code** — Flag raw strings where constants, string-union types, or branded types already exist in the codebase.
|
|
44
45
|
|
|
45
46
|
#### Code quality
|
|
46
47
|
|
|
47
48
|
- **Redundant state** — State that duplicates existing state, cached values that could be derived.
|
|
48
49
|
- **Unnecessary complexity** — Deep nesting that could be guard clauses, premature abstractions, over-engineered solutions to simple problems.
|
|
49
|
-
- **Dead code** — Unreachable branches, unused variables, commented-out code
|
|
50
|
-
- **Defensive code for impossible states** — Guards for cases the type system or
|
|
50
|
+
- **Dead code** — Unreachable branches, unused variables, commented-out code. An export nothing imports is dead in an application or a package-internal module; on a published package's public surface it is API — leave it and note it in the summary.
|
|
51
|
+
- **Defensive code for impossible states** — Guards for cases the type system or upstream validation already prevents. Drop them.
|
|
52
|
+
- **Type escapes** — `any`, `as` casts that paper over a mismatch, non-null `!`, and `@ts-ignore`. Each is a claim the compiler couldn't check: replace with a narrowed type, a type guard, or a parse at the boundary. Keep the ones documenting a genuine type-system or third-party-types limitation, and prefer `@ts-expect-error` with a one-line reason over `@ts-ignore`.
|
|
53
|
+
- **Swallowed errors** — Empty `catch {}`, `catch { return null }`, and `try` blocks that log and continue. A fallback that hides a failure is worse than the crash it prevents: rethrow or let it propagate. When wrapping, preserve the chain (`new Error(msg, { cause })`, `raise X from err`).
|
|
54
|
+
- **Comment noise** — Strip comments that restate the code, commented-out code, and comments describing behavior the diff removed. Keep file headers, export JSDoc, and any comment carrying a *why* — a constraint, a workaround, an upstream bug reference.
|
|
51
55
|
- **Outdated patterns** — Verbose or legacy syntax where modern equivalents exist. See the transformation tables below.
|
|
52
56
|
|
|
53
57
|
#### Efficiency
|
|
54
58
|
|
|
55
59
|
- **Redundant work** — Repeated computations, duplicate file reads, duplicate network/API calls, N+1 query patterns.
|
|
56
60
|
- **Missed concurrency** — Independent async operations run sequentially that could run in parallel with `Promise.all` / `Promise.allSettled`.
|
|
61
|
+
- **Unbounded fan-out** — `Promise.all` / `asyncio.gather` over a caller-sized or otherwise unbounded array fires everything at once. Cap it with the project's existing concurrency helper or a batched loop. A fixed handful of independent calls needs no limit.
|
|
57
62
|
- **No-op updates** — State/store updates inside loops or event handlers that fire unconditionally. Add change-detection so downstream consumers aren't notified when nothing changed.
|
|
58
63
|
- **TOCTOU** — Pre-checking file/resource existence before operating on it. Operate directly and handle the error instead.
|
|
59
64
|
- **Overly broad operations** — Reading entire files when only a portion is needed, loading all items when filtering for one.
|
|
60
65
|
|
|
61
66
|
#### mcp-ts-core-specific
|
|
62
67
|
|
|
68
|
+
- **Gate** — `bun run devcheck` plus the test suite (`bun run test`) is the project gate in Phase 2 step 4 and Phase 4 step 5.
|
|
69
|
+
- **Framework-provided utilities** — Before hand-rolling, check `src/utils/` and `src/errors/` in the project and `node_modules/@cyanheads/mcp-ts-core/` for framework exports: pagination helpers, schema builders, retry primitives, and the `ATTR_*` OTel attribute constants are framework-provided. Raw OTel attribute keys should be `ATTR_*` imports from `@cyanheads/mcp-ts-core/utils`.
|
|
63
70
|
- **Error throwing patterns** — Prefer framework error factories (`McpError`, `validationError`, `notFound`, `httpErrorFromResponse`) over raw `throw new Error()`. Tool handlers should throw — the framework catches, classifies, and instruments.
|
|
64
71
|
- **Error codes** — `InvalidParams` only for malformed JSON-RPC params shape. `ValidationError` for domain validation. `NotFound` for missing entities. Don't conflate them.
|
|
65
72
|
- **Ctx usage** — Use `ctx.log`, `ctx.state`, `ctx.enrich` — don't reach for global loggers or request-scoped storage directly. The `ctx` pattern carries tenant scope and OTel context.
|
|
@@ -67,19 +74,24 @@ Evaluate the changes across these dimensions. Not every dimension applies to eve
|
|
|
67
74
|
- **Tool annotations** — `readOnlyHint`, `idempotentHint`, `openWorldHint` should reflect reality. A read-only tool with `readOnlyHint: false` gives clients the wrong picture.
|
|
68
75
|
- **`exactOptionalPropertyTypes` boundaries** — If a downstream type insists on the field being present-or-not-present (not present-as-undefined), use a mapped widening type at the boundary. The pattern is documented in the framework.
|
|
69
76
|
- **`format()` ↔ `structuredContent` parity** — Different MCP clients forward different surfaces. Tests should assert both surfaces carry equivalent data.
|
|
77
|
+
- **Defensive code** — the "impossible states" the framework already prevents include malformed params (Zod-validated before the handler runs) and unclassified errors (caught and classified after it throws). Guards for either are dead.
|
|
78
|
+
- **Public surface** — the MCP surface (every tool input/output schema advertised to clients) is public API for the "API compatibility" rule; changing one is a breaking change, not a refactor.
|
|
70
79
|
|
|
71
80
|
### Phase 4: Apply transformations
|
|
72
81
|
|
|
73
82
|
1. **Filter findings ruthlessly.** If a finding is a false positive or not worth the churn, skip it. Don't argue with yourself about borderline cases — move on.
|
|
74
|
-
2. **
|
|
75
|
-
3. **
|
|
76
|
-
4. **
|
|
83
|
+
2. **Stay in scope.** Edit only files in the diff or new this session. Touch a file outside that set only when a finding requires it — importing an existing helper, deleting a private export the diff just orphaned — and only on the lines that finding names. Anything broader goes in the summary as a recommendation, not into the tree.
|
|
84
|
+
3. **Correctness bugs are not this pass's job.** A real defect doesn't get folded into a cleanup diff — name it in the summary with file and line so it can be handled as its own change.
|
|
85
|
+
4. **Transform incrementally** — one category of change at a time (modernize syntax, then reduce nesting, then consolidate).
|
|
86
|
+
5. **Verify equivalence** — all functionality, types, and public interfaces must remain unchanged. Re-run the gate from Phase 2 after transforming; a simplification that breaks the build is worse than the verbosity it removed.
|
|
87
|
+
6. **Keep the diff minimal.** Only touch lines that have a real reason to change. Don't reformat untouched code, add comments to code you didn't modify, or "improve" things that are already fine. Formatting belongs to the formatter (Biome, ruff): never hand-adjust whitespace, quotes, or import order, and never let a formatting-only hunk into the diff.
|
|
88
|
+
7. **Never stage, commit, tag, or push.** This pass ends with a dirty working tree and a summary; landing the changes is the caller's call.
|
|
77
89
|
|
|
78
|
-
When done, briefly summarize what was fixed or confirm the code was already clean.
|
|
90
|
+
When done, briefly summarize what was fixed, what was deliberately skipped, and any defects or out-of-scope recommendations — or confirm the code was already clean.
|
|
79
91
|
|
|
80
92
|
## Common transformations
|
|
81
93
|
|
|
82
|
-
The tables below cover TypeScript and Python. For other languages, apply analogous principles: prefer modern idioms, reduce nesting, eliminate dead code, follow project conventions.
|
|
94
|
+
The tables below cover TypeScript and Python. For other languages, apply analogous principles: prefer modern idioms, reduce nesting, eliminate dead code, follow project conventions. Check the project's language floor (`tsconfig` target/lib, `pyproject` `requires-python`) before applying a version-gated row.
|
|
83
95
|
|
|
84
96
|
### TypeScript (modern ESM, TS 5.x+)
|
|
85
97
|
|
|
@@ -90,11 +102,17 @@ The tables below cover TypeScript and Python. For other languages, apply analogo
|
|
|
90
102
|
| `if (x !== null && x !== undefined)` | `if (x != null)` | Idiomatic null/undefined check |
|
|
91
103
|
| `arr.filter(x => x !== null) as T[]` | `arr.filter(x => x != null)` | TS 5.5+ infers the type predicate — no cast; on older TS use an explicit `(x): x is T` predicate |
|
|
92
104
|
| `export { foo } from './foo/index.js'` | Direct imports at call sites | Avoid barrel re-exports inside the package; barrel exports are for public APIs only |
|
|
105
|
+
| `import { readFile } from 'fs/promises'` | `import { readFile } from 'node:fs/promises'` | `node:` protocol — unambiguous, lint-enforced in Biome |
|
|
93
106
|
| `async function f() { const a = await x(); const b = await y(); }` | `const [a, b] = await Promise.all([x(), y()])` | Parallel when independent |
|
|
94
|
-
| `
|
|
107
|
+
| `value \|\| fallback` | `value ?? fallback` | `\|\|` also swallows `0`, `''`, and `false` — use `??` unless every falsy value really should take the fallback |
|
|
108
|
+
| `obj.x !== undefined ? obj.x : fallback` | `obj.x ?? fallback` | Nullish coalescing — equivalent only when `null` should take the fallback too |
|
|
95
109
|
| `if (a) { if (b) { if (c) { ... } } }` | Guard clauses with early returns | Reduce nesting |
|
|
96
|
-
| `try { risky() } catch (e: any) { ... }` | `try { risky() } catch (e
|
|
97
|
-
| `
|
|
110
|
+
| `try { risky() } catch (e: any) { ... }` | `try { risky() } catch (e) { ... }` | Under `strict` the catch binding is already `unknown`; narrow with a type guard before use |
|
|
111
|
+
| `catch (err) { throw new Error('load failed') }` | `throw new Error('load failed', { cause: err })` | Preserve the cause chain |
|
|
112
|
+
| `[...arr].sort(cmp)` / `arr.slice().sort(cmp)` | `arr.toSorted(cmp)` | Non-mutating array methods (ES2023) — also `toReversed`, `toSpliced`, `with` |
|
|
113
|
+
| `const c = new AbortController(); setTimeout(() => c.abort(), ms)` | `AbortSignal.timeout(ms)` | Built-in timeout signal; combine with a caller's signal via `AbortSignal.any([...])` |
|
|
114
|
+
| `JSON.parse(JSON.stringify(x))` | `structuredClone(x)` | Deep clone that preserves Date, Map, Set, and cycles |
|
|
115
|
+
| `enum Status { A, B, C }` | `const Status = { A: 'A', B: 'B', C: 'C' } as const` | `enum`, `namespace`, and constructor parameter properties are non-erasable syntax rejected by TS 5.8 `erasableSyntaxOnly` and Node type-stripping — but switching numeric values to strings changes serialized output; keep values stable if they're persisted |
|
|
98
116
|
| `function f(a: string, b: string, c: string, d?: string)` | `function f(opts: FnOptions)` | Options object when >3 params |
|
|
99
117
|
| `throw new Error('Bad input')` (in a tool handler) | `throw validationError('Bad input', { field: 'x' })` | Use framework error factories so the framework can classify and instrument |
|
|
100
118
|
| `const ATTR_KEY = 'mcp.tool.name'` | `import { ATTR_MCP_TOOL_NAME } from '@cyanheads/mcp-ts-core/utils'` | Use framework attribute constants |
|
|
@@ -105,16 +123,20 @@ The tables below cover TypeScript and Python. For other languages, apply analogo
|
|
|
105
123
|
| --- | --- | --- |
|
|
106
124
|
| `Optional[str]` | `str \| None` | Modern union syntax (3.10+) |
|
|
107
125
|
| `List[str]`, `Dict[str, int]` | `list[str]`, `dict[str, int]` | Built-in generics (3.9+) |
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
126
|
+
| `T = TypeVar("T")` + `def f(x: T) -> T` | `def f[T](x: T) -> T` | PEP 695 generics (3.12+) — also `class C[T]:` |
|
|
127
|
+
| `TypeAlias = Union[A, B, C]` | `type ABC = A \| B \| C` | `type` statement (3.12+) |
|
|
128
|
+
| `if isinstance(x, Foo): a = x.a; b = x.b` | `match x: case Foo(a=a, b=b): ...` | Structural pattern matching (3.10+) where it destructures — not as a replacement for a flat equality `if/elif` chain |
|
|
129
|
+
| `class Config: def __init__(self, a, b, c): self.a = a ...` | `@dataclass(slots=True) class Config: a: str; b: int; c: float` | Less boilerplate, built-in eq/repr; `frozen=True` when instances shouldn't mutate |
|
|
110
130
|
| `results = []; for item in items: results.append(transform(item))` | `results = [transform(item) for item in items]` | Idiomatic comprehension |
|
|
111
131
|
| `f = open('x'); try: ... finally: f.close()` | `with open('x') as f: ...` | Context manager for resources |
|
|
132
|
+
| `os.path.join(d, n)`, `os.path.exists(p)`, `open(p).read()` | `Path(d) / n`, `p.exists()`, `p.read_text()` | `pathlib` over `os.path` string juggling |
|
|
133
|
+
| `datetime.utcnow()` / `datetime.utcfromtimestamp(t)` | `datetime.now(UTC)` / `datetime.fromtimestamp(t, UTC)` | Deprecated in 3.12 — the old calls return naive datetimes that compare wrong against aware ones |
|
|
134
|
+
| `zip(a, b)` | `zip(a, b, strict=True)` | 3.10+ — silently truncating to the shorter input hides bugs |
|
|
112
135
|
| `m = pattern.match(s)` then `if m: use(m)` | `if (m := pattern.match(s)): use(m)` | Walrus operator where it removes a throwaway assignment |
|
|
113
136
|
| `"Hello " + name + "!"` | `f"Hello {name}!"` | f-string over concatenation |
|
|
114
137
|
| `except Exception as e: pass` | `except SpecificError as e: log(e)` | Catch specific, never bare except/pass |
|
|
115
138
|
| `from module import *` | `from module import specific_name` | Explicit imports only |
|
|
116
|
-
| `
|
|
117
|
-
| Sequential `await` for independent I/O | `await asyncio.gather(a(), b())` | Parallel when independent |
|
|
139
|
+
| Sequential `await` for independent I/O | `async with asyncio.TaskGroup() as tg: tg.create_task(a()); tg.create_task(b())` | Structured concurrency (3.11+) — cancels siblings on failure and raises an `ExceptionGroup`; `asyncio.gather(..., return_exceptions=True)` stays correct when every result is wanted regardless of failures |
|
|
118
140
|
|
|
119
141
|
## When NOT to simplify
|
|
120
142
|
|
|
@@ -124,7 +146,12 @@ Leave code alone when:
|
|
|
124
146
|
- **The change is cosmetic.** Renaming a variable from `data` to `result` isn't worth the churn.
|
|
125
147
|
- **Intentional verbosity for debugging.** Verbose code may exist to make stack traces or logging clearer.
|
|
126
148
|
- **Performance-critical paths.** A less readable version may exist for measured performance reasons — check before simplifying.
|
|
127
|
-
- **API compatibility.** Don't change public function signatures, export shapes, or return types that callers depend on.
|
|
149
|
+
- **API compatibility.** Don't change public function signatures, export shapes, or return types that callers depend on.
|
|
128
150
|
- **Tests.** Don't DRY up test code aggressively — test readability and isolation matter more than deduplication.
|
|
129
151
|
- **Type workarounds.** Sometimes an `as` cast or `# type: ignore` exists because of a genuine type system limitation — verify before removing.
|
|
130
152
|
- **The abstraction isn't proven.** Don't create a shared utility for two similar blocks of code. Wait until there are three, and even then only if the abstraction is genuinely simpler than the duplication.
|
|
153
|
+
- **`return await` inside `try` / `finally`.** Collapsing it to `return` is not equivalent — the promise settles outside the block, so `catch` never fires and `finally` runs early. Only strip `await` from a `return` in plain function-body position.
|
|
154
|
+
- **Lazy logging arguments.** `logger.info("loaded %s in %sms", name, ms)` defers formatting until the record is emitted — don't turn it into an f-string.
|
|
155
|
+
- **Awaits that only look independent.** Sequential I/O may be sequential on purpose: rate limits, upstream ordering, a write that must land before the next read. Confirm independence from the code, not from the shape of the calls, before reaching for `Promise.all`.
|
|
156
|
+
- **Generated and vendored files.** Lockfiles, generated clients and schemas, migrations, snapshots, and anything under `dist/` are regenerated, not edited — skip them even when they appear in the diff.
|
|
157
|
+
- **Tool descriptions and `.describe()` prose.** They are the contract an LLM client reads — tightening them for brevity degrades the surface. Treat them as API text, not as comments.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: field-test
|
|
3
3
|
description: >
|
|
4
|
-
Exercise tools, resources, and prompts against a live HTTP server via MCP JSON-RPC over curl. Starts the server, surfaces the catalog, runs real and adversarial inputs, and produces a tight report with concrete findings and numbered follow-up options. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface.
|
|
4
|
+
Exercise tools, resources, and prompts against a live HTTP server via MCP JSON-RPC over curl. Starts the server, surfaces the catalog, runs real and adversarial inputs, measures every call (bytes, token estimate, wall-clock) and weighs the catalog, and produces a tight report with concrete findings and numbered follow-up options. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.12"
|
|
8
8
|
audience: external
|
|
9
9
|
type: debug
|
|
10
10
|
---
|
|
@@ -45,7 +45,8 @@ cat > /tmp/<project-name>-field-test-9DJ73-K103L.sh <<'HELPER_EOF'
|
|
|
45
45
|
#
|
|
46
46
|
# Surfaces failures aggressively — field test is for finding things that fail,
|
|
47
47
|
# so the helper auto-tails logs and prints HTTP status/body on errors instead
|
|
48
|
-
# of swallowing them.
|
|
48
|
+
# of swallowing them. It also measures: every mcp_call prints a one-line
|
|
49
|
+
# size/latency reading on stderr, and mcp_catalog_size weighs tools/list.
|
|
49
50
|
|
|
50
51
|
# Usage: mcp_start /path/to/server [startup-timeout-seconds] (default: 30)
|
|
51
52
|
# Builds, starts the HTTP server in the background, waits for the listen line,
|
|
@@ -107,7 +108,10 @@ _mcp_init_fail() {
|
|
|
107
108
|
|
|
108
109
|
# Usage: mcp_init <url>
|
|
109
110
|
# Runs `initialize`, sends `notifications/initialized`, prints:
|
|
110
|
-
# ready sid=<id-or-empty> protocol=<negotiated-version> requested=<want> (HTTP <code>)
|
|
111
|
+
# ready sid=<id-or-empty> protocol=<negotiated-version> requested=<want> instructions=<bytes>B (HTTP <code>)
|
|
112
|
+
# `instructions=` is the byte size of the server's `instructions` string — it
|
|
113
|
+
# loads into every client session alongside tools/list, so it is the other half
|
|
114
|
+
# of the per-session context tax mcp_catalog_size weighs.
|
|
111
115
|
# The initialize *result* is what decides success — a session ID is optional.
|
|
112
116
|
# A server started with MCP_SESSION_MODE=stateless mints none, and the session
|
|
113
117
|
# header is then omitted from every later request. Capture BOTH `sid` and
|
|
@@ -151,13 +155,44 @@ mcp_init() {
|
|
|
151
155
|
_mcp_init_fail "initialize result declares no protocolVersion" "$body_file" "$hdr"
|
|
152
156
|
return 1
|
|
153
157
|
fi
|
|
158
|
+
local instr; instr=$(printf '%s' "$reply" | jq -r '.result.instructions // "" | utf8bytelength' 2>/dev/null || echo 0)
|
|
154
159
|
local sid; sid=$(grep -i '^mcp-session-id:' "$hdr" | awk '{print $2}' | tr -d '\r\n')
|
|
155
160
|
local init_headers=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "MCP-Protocol-Version: $got")
|
|
156
161
|
[ -n "$sid" ] && init_headers+=(-H "Mcp-Session-Id: $sid")
|
|
157
162
|
curl -sS -X POST "$url" "${init_headers[@]}" \
|
|
158
163
|
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
|
|
159
164
|
rm -f "$hdr" "$body_file"
|
|
160
|
-
echo "ready sid=$sid protocol=$got requested=$want (HTTP $code)"
|
|
165
|
+
echo "ready sid=$sid protocol=$got requested=$want instructions=${instr}B (HTTP $code)"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Internal: one stderr line per call — reply bytes, the content/structured
|
|
169
|
+
# split, a token estimate, wall-clock. Bytes are the reply as delivered (SSE
|
|
170
|
+
# framing stripped). `content` is every text block's bytes, `structured` is
|
|
171
|
+
# structuredContent serialized. The token figure is bytes/4 — an estimate, not
|
|
172
|
+
# a tokenizer. Wall-clock is curl's time_total for the whole exchange.
|
|
173
|
+
_mcp_measure() {
|
|
174
|
+
local method="$1"; local params="$2"; local reply="$3"; local code="$4"; local secs="$5"
|
|
175
|
+
local total; total=$(printf '%s' "$reply" | wc -c | tr -d ' ')
|
|
176
|
+
local ms; ms=$(awk -v s="$secs" 'BEGIN { printf "%d", s * 1000 }')
|
|
177
|
+
local label="$method"
|
|
178
|
+
local split=""
|
|
179
|
+
case "$method" in
|
|
180
|
+
tools/call)
|
|
181
|
+
local name; name=$(printf '%s' "$params" | jq -r '.name // empty' 2>/dev/null)
|
|
182
|
+
[ -n "$name" ] && label="$method $name"
|
|
183
|
+
split=$(printf '%s' "$reply" | jq -r '
|
|
184
|
+
(.result // {}) as $r
|
|
185
|
+
| ([$r.content[]? | select(.type == "text") | .text] | join("") | utf8bytelength) as $c
|
|
186
|
+
| (if $r.structuredContent == null then "none" else ($r.structuredContent | tojson | utf8bytelength | tostring) end) as $s
|
|
187
|
+
| "content \($c) · structured \($s)"' 2>/dev/null)
|
|
188
|
+
;;
|
|
189
|
+
resources/read)
|
|
190
|
+
split=$(printf '%s' "$reply" | jq -r '
|
|
191
|
+
"text \([.result.contents[]? | .text // ""] | join("") | utf8bytelength)"' 2>/dev/null)
|
|
192
|
+
;;
|
|
193
|
+
esac
|
|
194
|
+
local tok; tok=$(awk -v b="$total" 'BEGIN { if (b >= 1000) printf "~%.1fk", b / 4000; else printf "~%d", b / 4 }')
|
|
195
|
+
echo "⏱ $label · HTTP $code · ${total} B${split:+ ($split)} · $tok tok · ${ms} ms" >&2
|
|
161
196
|
}
|
|
162
197
|
|
|
163
198
|
# Usage: mcp_call <url> <sid> <method> [JSON_PARAMS] [protocol]
|
|
@@ -166,6 +201,9 @@ mcp_init() {
|
|
|
166
201
|
# emitting every event would break `| jq .result`). A transport failure or an
|
|
167
202
|
# HTTP >= 400 prints the details and returns non-zero — it never returns 0 with
|
|
168
203
|
# empty output. Pipe to `jq`.
|
|
204
|
+
# Every call also prints one measurement line on stderr, e.g.
|
|
205
|
+
# ⏱ tools/call gbif_search_species · HTTP 200 · 18412 B (content 9100 · structured 8900) · ~4.6k tok · 812 ms
|
|
206
|
+
# Read it on every call — it is the size/latency evidence the report cites.
|
|
169
207
|
# `sid` may be empty ('') for a stateless server; the session header is then
|
|
170
208
|
# omitted. Pass the `protocol` mcp_init printed as the 5th arg — with no
|
|
171
209
|
# session carrying the negotiation, MCP-Protocol-Version is what tells the
|
|
@@ -180,12 +218,13 @@ mcp_call() {
|
|
|
180
218
|
body=$(printf '{"jsonrpc":"2.0","id":%d,"method":"%s","params":%s}' "$RANDOM" "$method" "$params")
|
|
181
219
|
fi
|
|
182
220
|
local resp_file; resp_file=$(mktemp)
|
|
183
|
-
local code curl_rc
|
|
221
|
+
local stats code secs curl_rc
|
|
184
222
|
local headers=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream")
|
|
185
223
|
[ -n "$sid" ] && headers+=(-H "Mcp-Session-Id: $sid")
|
|
186
224
|
[ -n "$protocol" ] && headers+=(-H "MCP-Protocol-Version: $protocol")
|
|
187
|
-
|
|
225
|
+
stats=$(curl -sS -o "$resp_file" -w '%{http_code} %{time_total}' -X POST "$url" "${headers[@]}" -d "$body")
|
|
188
226
|
curl_rc=$?
|
|
227
|
+
read -r code secs <<< "$stats"
|
|
189
228
|
if [ "$curl_rc" -ne 0 ] || [ -z "$code" ] || [ "$code" = "000" ]; then
|
|
190
229
|
echo "TRANSPORT FAILURE calling $method — curl exit $curl_rc, http_code '${code:-none}'." >&2
|
|
191
230
|
echo "Server not reachable at $url (check it is still running: mcp_log <log>)." >&2
|
|
@@ -198,14 +237,44 @@ mcp_call() {
|
|
|
198
237
|
rm -f "$resp_file"
|
|
199
238
|
return 1
|
|
200
239
|
fi
|
|
240
|
+
local reply
|
|
201
241
|
local sse; sse=$(sed -n 's/^data: //p' "$resp_file")
|
|
202
242
|
if [ -n "$sse" ]; then
|
|
203
|
-
|
|
204
|
-
|
|
243
|
+
reply=$(printf '%s\n' "$sse" | grep -E '"(result|error)"')
|
|
244
|
+
reply="${reply:-$sse}"
|
|
205
245
|
else
|
|
206
|
-
cat "$resp_file"
|
|
246
|
+
reply=$(cat "$resp_file")
|
|
207
247
|
fi
|
|
208
248
|
rm -f "$resp_file"
|
|
249
|
+
_mcp_measure "$method" "$params" "$reply" "$code" "$secs"
|
|
250
|
+
printf '%s\n' "$reply"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Usage: mcp_catalog_size <url> <sid> [protocol]
|
|
254
|
+
# Weighs the catalog: the bytes of the tools/list reply — what every client
|
|
255
|
+
# loads into context per session before a single call — then each tool's
|
|
256
|
+
# serialized entry, largest first, split into description / inputSchema /
|
|
257
|
+
# outputSchema so the row says WHERE the weight is. A fat outputSchema costs as
|
|
258
|
+
# much as a fat description and is the usual surprise. Prints:
|
|
259
|
+
# catalog: 12 tools · 48210 B · ~12.1k tok
|
|
260
|
+
# <bytes> <~tok> <name> desc <b> · input <b> · output <b|none> (one row per tool)
|
|
261
|
+
mcp_catalog_size() {
|
|
262
|
+
local url="$1"; local sid="$2"; local protocol="${3:-}"
|
|
263
|
+
[ -z "$url" ] && { echo "usage: mcp_catalog_size <url> <sid> [protocol]" >&2; return 1; }
|
|
264
|
+
local reply; reply=$(mcp_call "$url" "$sid" tools/list '' "$protocol") || return 1
|
|
265
|
+
printf '%s' "$reply" | jq -r '
|
|
266
|
+
def tok: if . >= 1000 then "~\(. / 4000 * 10 | round / 10)k" else "~\(. / 4 | floor)" end;
|
|
267
|
+
def bytes_or_none: if . == null then "none" else (tojson | utf8bytelength | tostring) end;
|
|
268
|
+
(.result.tools // []) as $t
|
|
269
|
+
| (. | tojson | utf8bytelength) as $total
|
|
270
|
+
| "catalog: \($t | length) tools · \($total) B · \($total | tok) tok",
|
|
271
|
+
($t
|
|
272
|
+
| map({name, b: (tojson | utf8bytelength),
|
|
273
|
+
d: ((.description // "") | utf8bytelength),
|
|
274
|
+
i: (.inputSchema | bytes_or_none),
|
|
275
|
+
o: (.outputSchema | bytes_or_none)})
|
|
276
|
+
| sort_by(-.b) | .[]
|
|
277
|
+
| "\(.b)\t\(.b | tok)\t\(.name)\tdesc \(.d) · input \(.i) · output \(.o)")'
|
|
209
278
|
}
|
|
210
279
|
|
|
211
280
|
# Usage: mcp_log <server-log-path> [N] (default: 50 lines)
|
|
@@ -273,7 +342,7 @@ Capture `pid`, `url`, `port`, `log` from the `mcp_start` output — every later
|
|
|
273
342
|
mcp_init <url-from-mcp_start>
|
|
274
343
|
```
|
|
275
344
|
|
|
276
|
-
Runs `initialize`, sends `notifications/initialized`, prints the `sid` and `protocol` to capture for `mcp_call
|
|
345
|
+
Runs `initialize`, sends `notifications/initialized`, prints the `sid` and `protocol` to capture for `mcp_call`, plus `instructions=` — the byte size of the server's `instructions` string, which every client loads per session alongside the catalog (record it with the catalog total in Step 3). Success is decided by the initialize *result*, so a transport failure, a non-2xx status, a JSON-RPC error, a malformed body, or a result with no `protocolVersion` all fail loudly with the raw exchange.
|
|
277
346
|
|
|
278
347
|
The helper requests the newest `initialize`-negotiated revision the SDK supports (`2025-11-25`). **If `protocol=` comes back older than `requested=`, the server capped it** — every call after that exercises an older protocol than a current client would negotiate. Note it as a `bug` finding and check the pinned `@modelcontextprotocol/server` version; don't quietly test the downgraded surface. To deliberately test an older version, set `MCP_FIELD_TEST_PROTOCOL`.
|
|
279
348
|
|
|
@@ -294,8 +363,11 @@ Both modes exercise the **2025-era arm**: `initialize` negotiates the revision,
|
|
|
294
363
|
mcp_call <url> <sid> tools/list | jq '.result.tools[] | {name, description, inputSchema, outputSchema}'
|
|
295
364
|
mcp_call <url> <sid> resources/list | jq '.result.resources[] | {uri, name, mimeType}'
|
|
296
365
|
mcp_call <url> <sid> prompts/list | jq '.result.prompts[] | {name, description, arguments}'
|
|
366
|
+
mcp_catalog_size <url> <sid> <protocol>
|
|
297
367
|
```
|
|
298
368
|
|
|
369
|
+
**Weigh the catalog.** `mcp_catalog_size` prints the `tools/list` bytes — the context every client loads per session before a single call — and each tool's entry, largest first, split into description / `inputSchema` / `outputSchema`. Record the total alongside the `instructions=` bytes from Step 2; together they are the per-session tax. The split says where a heavy tool's weight lives: an `outputSchema` narrating every field of a 60-field record is the common surprise, an over-long description the obvious one. Hand the outliers to `tool-defs-analysis` (its length-outliers pass) rather than trimming blind.
|
|
370
|
+
|
|
299
371
|
Present a compact catalog to the user: each definition's name + 1-line description. Flag vague or missing descriptions as you go — those feed into the report. Use this to build the test plan.
|
|
300
372
|
|
|
301
373
|
**Audit every description for leaks** — tool description, every parameter `.describe()` in `inputSchema`, and every field `.describe()` in `outputSchema` (the `outputSchema` projection above is what surfaces these; don't skim past it). Three categories:
|
|
@@ -317,6 +389,7 @@ Treat any hit as a `ux` finding in the report. The authoring rule lives under *T
|
|
|
317
389
|
| Happy path | One realistic input. Output shape matches schema. `content[]` text reads clearly to a human. |
|
|
318
390
|
| `structuredContent` ↔ `content[]` parity | Dump the whole array (`jq '.result.content'`) and check every `structuredContent` field is surfaced *somewhere* in it — enrichment lands in its own trailing block, not in `content[0]`. Parity gap = client-specific blindness. |
|
|
319
391
|
| Input error | One invalid input (wrong type or missing required). Error text says *what*, *why*, *how to fix*. |
|
|
392
|
+
| Size & latency | Read the `⏱` line `mcp_call` prints on every call. A happy-path response over **24,000 B** (the framework's `DEFAULT_OUTLINE_BUDGET_BYTES` — the line at which it would outline a document itself) with no truncation disclosure and no retrieval path (cursor, offset, `sections`, canvas handle) is a `ux` finding: the agent pays the whole payload with no way to ask for less. `content` ≈ `structured` with the text starting `{` means the JSON is on the wire twice — a missing `format()`. A call over ~5 s on a happy-path input is worth a `mcp_log` look before calling it upstream latency. |
|
|
320
393
|
|
|
321
394
|
**Situational — add only when triggered**
|
|
322
395
|
|
|
@@ -349,7 +422,7 @@ Treat any hit as a `ux` finding in the report. The authoring rule lives under *T
|
|
|
349
422
|
|
|
350
423
|
Use `TaskCreate` — one task per definition. Mark complete as you go. Don't batch.
|
|
351
424
|
|
|
352
|
-
For each call, capture: input sent, response (trim huge payloads to files), whether `isError: true` appeared, anything surprising (slow response, parity drift, unhelpful text, crash).
|
|
425
|
+
For each call, capture: input sent, the `⏱` line (bytes, split, ms), response (trim huge payloads to files), whether `isError: true` appeared, anything surprising (slow response, parity drift, unhelpful text, crash).
|
|
353
426
|
|
|
354
427
|
When a call surprises you — slow, hangs, returns terse output, surfaces an unhelpful error — run `. /tmp/<project-name>-field-test-<ID>.sh && mcp_log <log>` to tail the server log. The pino startup banner, request handler errors, upstream API call traces, and rate-limit warnings all land in the per-server log (read via `mcp_log`) rather than coming back through `mcp_call`. Don't guess at runtime behavior from response text alone.
|
|
355
428
|
|
|
@@ -374,12 +447,16 @@ Kills the background server and its port-holding child, removes the server log,
|
|
|
374
447
|
|
|
375
448
|
### 7. Report
|
|
376
449
|
|
|
377
|
-
|
|
450
|
+
Four sections. Tight. The user should be able to skim the summary, scan the numbers, read details only for what matters, and act on numbered options.
|
|
378
451
|
|
|
379
452
|
#### Summary (1 paragraph)
|
|
380
453
|
|
|
381
454
|
One paragraph. How many definitions exercised, how many passed clean, how many have issues, and the single most important finding. No tables, no lists.
|
|
382
455
|
|
|
456
|
+
#### Size & latency
|
|
457
|
+
|
|
458
|
+
Per-session tax on its own line (`instructions` bytes + catalog bytes, with the heaviest tool named), then one row per tool exercised, sorted by happy-path bytes descending: tool · bytes · ~tok · ms. Over 15 tools, keep every row over 24,000 B plus the three slowest and fold the rest into one line ("N more under budget, median X B"). Numbers only — what they mean goes in Findings.
|
|
459
|
+
|
|
383
460
|
#### Findings
|
|
384
461
|
|
|
385
462
|
Only include definitions with issues. Group by severity. Each finding is 2–4 lines unless it genuinely needs more. A parity finding cites the full `content[]` dump as its evidence — a quote from one index doesn't establish drift.
|
|
@@ -421,6 +498,8 @@ End with:
|
|
|
421
498
|
- [ ] HTTP server built and started; real port parsed from log
|
|
422
499
|
- [ ] Session initialized (a stateless server returns an empty `sid` — still a pass); `notifications/initialized` sent; negotiated protocol version matches the requested one (a downgrade is a finding)
|
|
423
500
|
- [ ] Catalog surfaced and presented; descriptions audited for leaks (implementation details, meta-coaching, consumer-aware phrasing)
|
|
501
|
+
- [ ] Catalog weighed (`mcp_catalog_size`); total + `instructions=` bytes recorded for the report
|
|
502
|
+
- [ ] Every call's `⏱` line read; any happy-path response over 24,000 B with no disclosure + retrieval path filed as `ux`
|
|
424
503
|
- [ ] Universal battery run on every definition (happy path, parity against the full `content[]` array, input error)
|
|
425
504
|
- [ ] Situational categories applied only when triggered
|
|
426
505
|
- [ ] **If >15 tools:** sampled 30–40% for situational testing; skipped definitions listed in report
|
|
@@ -429,4 +508,4 @@ End with:
|
|
|
429
508
|
- [ ] **If any tool truncates, caps, or spills its output:** truncation forced; disclosure + a retrieval path (cursor, offset, selector, canvas handle) verified
|
|
430
509
|
- [ ] External-state / auth-gated tools handled explicitly (run, skip, or confirm)
|
|
431
510
|
- [ ] Server stopped (port confirmed free); server log and helper script removed
|
|
432
|
-
- [ ] Report: summary paragraph → grouped findings → numbered options
|
|
511
|
+
- [ ] Report: summary paragraph → size & latency table → grouped findings → numbered options
|
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: git-wrapup
|
|
3
3
|
description: >
|
|
4
|
-
Land working-tree changes as logical commits — the work grouped by concern, topped by a release commit (version bump, changelog, regenerated artifacts)
|
|
4
|
+
Land working-tree changes as logical commits — the work grouped by concern, topped by a release commit (version bump, changelog, regenerated artifacts). Verify, commit. Stops at "committed locally on main" — or, when the project releases through a release PR, at "release branch pushed, PR open". No tag, no push to main, no publish: the release-and-publish skill merges, tags, and ships from here. Distilled from the git_wrapup_instructions protocol.
|
|
5
5
|
metadata:
|
|
6
6
|
author: cyanheads
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.13"
|
|
8
8
|
audience: external
|
|
9
9
|
type: workflow
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## When to use
|
|
13
13
|
|
|
14
|
-
Working-tree or staged changes are ready to ship as a new version. This skill lands them as a stack of logical commits — the work grouped by concern, topped by a release commit (version + changelog + tree)
|
|
14
|
+
Working-tree or staged changes are ready to ship as a new version. This skill lands them as a stack of logical commits — the work grouped by concern, topped by a release commit (version + changelog + tree). It does NOT tag, push to main, or publish — `release-and-publish` does all three.
|
|
15
15
|
|
|
16
16
|
Common triggers:
|
|
17
17
|
- Feature work, bug fixes, or dependency updates are done and tested
|
|
18
18
|
- A maintenance or polish pass left changes in the working tree
|
|
19
19
|
- An orchestrator says "wrapup this project"
|
|
20
20
|
|
|
21
|
+
## Release PR mode
|
|
22
|
+
|
|
23
|
+
A project can route every release through a pull request — one PR per version, for the audit trail and a stable review target. The mode is declared in the project's `CLAUDE.md`/`AGENTS.md` or in the caller's brief; when neither says anything, there is no release PR and the stack lands on `main` directly.
|
|
24
|
+
|
|
25
|
+
| Mode | Wrapup ends at | Then |
|
|
26
|
+
|:--|:--|:--|
|
|
27
|
+
| *(none — default)* | commit stack on `main`, tree clean | `release-and-publish` tags HEAD and ships |
|
|
28
|
+
| **gated** | commit stack on `release/<version>`, branch pushed, PR open | a review pass on the PR (`release-pr-review` skill), then a separate `release-and-publish` run fast-forwards `main`, tags, and ships |
|
|
29
|
+
| **straight-through** | same as gated | the same agent continues straight into `release-and-publish` |
|
|
30
|
+
|
|
31
|
+
The branch is created at wrapup time, never before: work happens on `main` until the version is known, then the uncommitted tree moves to `release/<version>` in one step (step 7). The commit stack, the release commit, and the tag format are identical in every mode — the PR adds an artifact around them, it does not change them.
|
|
32
|
+
|
|
21
33
|
## Pre-wrapup gate checklist
|
|
22
34
|
|
|
23
35
|
Every item must be true before starting wrapup. Committing means releasing — a commit only happens when the work is ready to ship, not just "the edits are done." Each item is a goal to verify.
|
|
@@ -127,6 +139,15 @@ bun run test:package # only if the script exists — NOT part of test:all
|
|
|
127
139
|
|
|
128
140
|
### 7. Commit — group by concern, release artifacts on top
|
|
129
141
|
|
|
142
|
+
**Release PR mode only — move to the release branch first, before the first commit:**
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
git branch --show-current # must be main
|
|
146
|
+
git switch -c release/<version> # uncommitted work rides along
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Commits never land on `main` in this mode. If a `release/*` branch already exists locally, a prior release PR was never merged — halt and report it rather than stacking a second release on top.
|
|
150
|
+
|
|
130
151
|
Do NOT `git add -A` into one commit. Group the working tree into a handful of logical commits — never one blob:
|
|
131
152
|
|
|
132
153
|
1. **The work — one commit per concern.** A feature spanning multiple layers splits by layer: runtime/logic, linter/tooling, docs/skills. Unrelated changes (two separate fixes, an incidental doc tweak) are their own commits. Work commits do not carry the version.
|
|
@@ -172,59 +193,74 @@ The changelog carries the depth, the tag carries the headline, the commit carrie
|
|
|
172
193
|
|
|
173
194
|
**Right-size it.** "Group by concern" is not "always split." A genuinely single-concern change — one fix, a dependency bump, a small doc edit — is one work commit plus the release commit; when the change and its version bump are inseparable for a tiny patch, a single commit whose subject leads with the version is fine. The failure mode to prevent is the inverse: a large, multi-layer feature crammed into one commit alongside the release artifacts.
|
|
174
195
|
|
|
175
|
-
### 8.
|
|
196
|
+
### 8. Open the release PR (release PR mode only)
|
|
197
|
+
|
|
198
|
+
Skip this step entirely when the project has no release PR mode — go to step 9.
|
|
176
199
|
|
|
177
200
|
```bash
|
|
178
|
-
git
|
|
201
|
+
git push -u origin release/<version>
|
|
202
|
+
gh pr create --base main --head release/<version> --title "<release commit subject>" --body-file <path-to-body.md>
|
|
179
203
|
```
|
|
180
204
|
|
|
181
|
-
|
|
205
|
+
**Title:** the release commit's subject, verbatim — `chore(release): <version> — <theme>`.
|
|
182
206
|
|
|
183
|
-
|
|
207
|
+
**Body — always via `--body-file`, never an inline `--body` string** (backticks inside a double-quoted argument are command substitution and silently vanish). Write the file to a scratch location, not into the repo.
|
|
184
208
|
|
|
185
|
-
|
|
209
|
+
The body is the release digest — the same headline digest the annotated tag will carry, plus a gates record. It is written here, reviewed on the PR, and copied into the tag at release time, so it is the one place the release notes get reviewed before they become permanent. Format:
|
|
186
210
|
|
|
187
211
|
```
|
|
188
|
-
<theme —
|
|
212
|
+
<theme — the changelog entry's summary: line, plain prose, one line>
|
|
213
|
+
|
|
214
|
+
## Changes
|
|
189
215
|
|
|
190
216
|
- <notable user-facing change> (#N)
|
|
191
217
|
- <notable user-facing change> (#N)
|
|
192
218
|
- <ONE compact grouped line for the minor/internal changes — build config, repo hygiene, metadata>
|
|
193
219
|
- deps: `@cyanheads/mcp-ts-core` ^0.10.6 → ^0.10.14 (+ dev-dep bumps)
|
|
194
220
|
|
|
221
|
+
## Gates
|
|
222
|
+
|
|
223
|
+
- `bun run devcheck` — clean
|
|
224
|
+
- `bun run rebuild` — ok
|
|
225
|
+
- `bun run test:all` — <N> passed
|
|
226
|
+
- `bun run test:package` — <N> passed (only where the project defines it)
|
|
227
|
+
|
|
195
228
|
[CHANGELOG v<version>](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/<version>.md)
|
|
196
229
|
```
|
|
197
230
|
|
|
198
231
|
**Rules:**
|
|
199
|
-
-
|
|
200
|
-
- **
|
|
201
|
-
-
|
|
202
|
-
- **
|
|
203
|
-
- **
|
|
204
|
-
-
|
|
205
|
-
|
|
206
|
-
-
|
|
207
|
-
|
|
208
|
-
|
|
232
|
+
- **`## Changes` follows the tag rules exactly** (`release-and-publish` step 4): flat bullets, never Keep-a-Changelog section headers; complete at headline granularity — notable changes get their own bullet, minor/internal items share ONE grouped bullet; deps one line max, naming only what earns it; no narrative, no marketing adjectives. Depth lives in the changelog entry, which is in this PR's diff and linked on the last line.
|
|
233
|
+
- **Every claim traces to the diff and to the changelog entry.** The body is derived from the entry you authored in step 4, never written independently of it.
|
|
234
|
+
- **`## Gates` is the one release surface that carries gate results** — the exact commands from step 6 with their outcomes. It never enters the tag.
|
|
235
|
+
- **Issue references are bare `(#N)` backlinks — never a closing keyword** (`Closes #N`, `Fixes #N`); the merge would close the issue before its close-out comment lands.
|
|
236
|
+
- **Changelog link is the final line**, same form as the tag, blank line above it.
|
|
237
|
+
- Length is earned — a theme, two bullets, gates, and the link is a complete body for a small patch.
|
|
238
|
+
|
|
239
|
+
If the review pass changes what ships, `release-pr-review` updates `## Changes` and `## Gates` to match; `release-and-publish` then lifts `## Changes` plus the final link into the tag verbatim.
|
|
240
|
+
|
|
241
|
+
**Gated mode: halt here.** Report the PR URL, the branch, and the commit stack. Do not tag, do not merge, do not touch `main`. The review pass and `release-and-publish` run as separate steps after this one.
|
|
242
|
+
|
|
243
|
+
**Straight-through mode:** continue directly into `release-and-publish`.
|
|
209
244
|
|
|
210
245
|
### 9. Verify end state
|
|
211
246
|
|
|
212
247
|
```bash
|
|
213
248
|
git log --oneline -8 # confirm the commit stack: work commits + release commit on top
|
|
214
|
-
git show v<version> --stat | head -20 # confirm tag points at HEAD (the release commit)
|
|
215
249
|
git status # must be clean
|
|
216
|
-
git tag -
|
|
250
|
+
git tag --points-at HEAD # must print nothing — tagging is release-and-publish's job
|
|
251
|
+
git branch --show-current # main, or release/<version> in release PR mode
|
|
252
|
+
gh pr view --json number,url,state # release PR mode: OPEN, head = the branch above
|
|
217
253
|
```
|
|
218
254
|
|
|
219
|
-
If the working tree isn't clean or the
|
|
255
|
+
If the working tree isn't clean or the release commit isn't at HEAD, something went wrong — investigate before proceeding.
|
|
220
256
|
|
|
221
|
-
**Do NOT push.** This skill stops here.
|
|
257
|
+
**Do NOT tag, push `main`, or publish.** This skill stops here. `release-and-publish` merges the release branch when there is one, creates the tag, pushes, and publishes.
|
|
222
258
|
|
|
223
259
|
## Constraints
|
|
224
260
|
|
|
225
|
-
- **
|
|
261
|
+
- **No push to `main`, no tag, no publish.** The only remote writes this skill makes are the release-branch push and the PR create in release PR mode
|
|
226
262
|
- **Never stash.** Not for quick checks, not for testing, not for any reason
|
|
227
|
-
- **Never destructive.** No `git reset --hard`, `git restore .`, `git clean -f`, `git checkout --
|
|
263
|
+
- **Never destructive.** No `git reset --hard`, `git restore .`, `git clean -f`, `git checkout -- .`, no force-push
|
|
228
264
|
- **Bash git only.** Drive every git operation through the shell
|
|
229
265
|
- If `v<version>` already exists as a tag, **halt and report the conflict** — include the version string, existing tag SHA, and current HEAD SHA so the caller can resolve it. Do not delete or move tags without explicit authorization
|
|
230
266
|
|
|
@@ -240,8 +276,9 @@ If the working tree isn't clean or the tag doesn't point at HEAD, something went
|
|
|
240
276
|
- [ ] `bun run devcheck` passes
|
|
241
277
|
- [ ] `bun run test:all` (or `test`) passes
|
|
242
278
|
- [ ] `bun run test:package` passes, when the project defines it — it guards the public-export manifest and `test:all` does not run it
|
|
279
|
+
- [ ] Release PR mode: stack committed on `release/<version>`, never on `main`
|
|
243
280
|
- [ ] Work grouped into logical commits (large features split by layer); release artifacts (version + changelog + tree) committed separately on top, subject leading with the version
|
|
244
281
|
- [ ] Every commit carries a body, and every body is one or two lines — none subject-only, none a paragraph
|
|
245
|
-
- [ ]
|
|
282
|
+
- [ ] Release PR mode: branch pushed, PR open — title = release commit subject; body = theme line, `## Changes` in tag rules, `## Gates`, changelog link last (via `--body-file`, no closing keywords)
|
|
246
283
|
- [ ] Working tree clean
|
|
247
|
-
- [ ]
|
|
284
|
+
- [ ] No tag at HEAD, nothing pushed to `main` — `release-and-publish` owns both
|