@cyanheads/mcp-ts-core 0.12.6 → 0.12.7

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.
Files changed (33) hide show
  1. package/AGENTS.md +2 -1
  2. package/CLAUDE.md +2 -1
  3. package/README.md +124 -76
  4. package/biome.json +1 -1
  5. package/changelog/0.12.x/0.12.6.md +2 -2
  6. package/changelog/0.12.x/0.12.7.md +39 -0
  7. package/dist/core/worker.d.ts +1 -1
  8. package/dist/core/worker.d.ts.map +1 -1
  9. package/dist/core/worker.js.map +1 -1
  10. package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
  11. package/dist/mcp-server/tools/tool-registration.js +7 -1
  12. package/dist/mcp-server/tools/tool-registration.js.map +1 -1
  13. package/dist/mcp-server/tools/utils/deferredInputSchema.d.ts +39 -0
  14. package/dist/mcp-server/tools/utils/deferredInputSchema.d.ts.map +1 -0
  15. package/dist/mcp-server/tools/utils/deferredInputSchema.js +33 -0
  16. package/dist/mcp-server/tools/utils/deferredInputSchema.js.map +1 -0
  17. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +10 -2
  18. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  19. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +25 -3
  20. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  21. package/dist/services/speech/providers/whisper.provider.d.ts.map +1 -1
  22. package/dist/services/speech/providers/whisper.provider.js +4 -2
  23. package/dist/services/speech/providers/whisper.provider.js.map +1 -1
  24. package/package.json +21 -20
  25. package/scripts/check-framework-antipatterns.ts +4 -1
  26. package/scripts/devcheck.ts +303 -33
  27. package/scripts/lint-packaging.ts +28 -6
  28. package/skills/add-tool/SKILL.md +33 -1
  29. package/skills/api-errors/SKILL.md +2 -1
  30. package/skills/design-mcp-server/SKILL.md +6 -1
  31. package/skills/field-test/SKILL.md +70 -30
  32. package/skills/git-wrapup/SKILL.md +4 -3
  33. package/templates/package.json +6 -6
@@ -4,7 +4,7 @@ description: >
4
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.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.9"
7
+ version: "2.10"
8
8
  audience: external
9
9
  type: debug
10
10
  ---
@@ -17,7 +17,7 @@ Unit tests (`add-test` skill) verify handler logic with mocked context. Field te
17
17
 
18
18
  ### Transport coverage
19
19
 
20
- This skill drives an HTTP server because curl + JSON-RPC is the most reliable harness for shell-based agents. The same handlers run on both transports — only the framing differs — so HTTP exercises the full functional surface.
20
+ This skill drives an HTTP server because curl + JSON-RPC is the most reliable harness for shell-based agents. The same handlers run on both transports — only the framing differs — so HTTP exercises the full functional surface. Both HTTP session modes are covered: a durable `Mcp-Session-Id` session, and the sessionless initialization a `MCP_SESSION_MODE=stateless` server performs.
21
21
 
22
22
  **Stdio coverage is a boot check only — run this before Step 1.** Run `bun run rebuild && bun run start:stdio`, confirm the startup logs look clean (banner, expected tool/resource counts, no errors/warnings, no missing-config gripes), then kill it. Pino logs go to stderr in stdio mode (stdout is reserved for JSON-RPC), so they print straight to the terminal when you run interactively. No need to call tools over stdio — the HTTP pass already covered handler behavior.
23
23
 
@@ -93,9 +93,26 @@ mcp_start() {
93
93
  echo "ready pid=$pid url=$url port=$port log=$server_log"
94
94
  }
95
95
 
96
+ # Internal: report a failed initialize with the raw exchange, then clean up.
97
+ _mcp_init_fail() {
98
+ local msg="$1"; local body_file="$2"; local hdr="$3"
99
+ echo "init failed — $msg" >&2
100
+ echo "--- response body ---" >&2
101
+ if [ -s "$body_file" ]; then cat "$body_file" >&2; else echo "(empty)" >&2; fi
102
+ echo "--- response headers ---" >&2
103
+ if [ -s "$hdr" ]; then cat "$hdr" >&2; else echo "(none)" >&2; fi
104
+ rm -f "$hdr" "$body_file"
105
+ return 1
106
+ }
107
+
96
108
  # Usage: mcp_init <url>
97
109
  # Runs `initialize`, sends `notifications/initialized`, prints:
98
- # ready sid=<id> protocol=<negotiated-version>
110
+ # ready sid=<id-or-empty> protocol=<negotiated-version> requested=<want> (HTTP <code>)
111
+ # The initialize *result* is what decides success — a session ID is optional.
112
+ # A server started with MCP_SESSION_MODE=stateless mints none, and the session
113
+ # header is then omitted from every later request. Capture BOTH `sid` and
114
+ # `protocol`: mcp_call takes the protocol as its 5th arg, which is what carries
115
+ # the negotiated revision when there is no session to carry it.
99
116
  # A negotiated version older than the requested one means the server capped it
100
117
  # — note that in the report; you are then testing an older protocol than a
101
118
  # current client would use.
@@ -105,41 +122,57 @@ mcp_init() {
105
122
  local want="${MCP_FIELD_TEST_PROTOCOL:-2025-11-25}"
106
123
  local hdr; hdr=$(mktemp)
107
124
  local body_file; body_file=$(mktemp)
108
- local code
125
+ local code curl_rc
109
126
  code=$(curl -sS -D "$hdr" -o "$body_file" -w '%{http_code}' -X POST "$url" \
110
127
  -H "Content-Type: application/json" \
111
128
  -H "Accept: application/json, text/event-stream" \
112
129
  -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"$want\",\"capabilities\":{},\"clientInfo\":{\"name\":\"field-test\",\"version\":\"1.0.0\"}}}")
113
- local sid; sid=$(grep -i '^mcp-session-id:' "$hdr" | awk '{print $2}' | tr -d '\r\n')
114
- if [ -z "$sid" ]; then
115
- echo "init failed — HTTP $code, no Mcp-Session-Id header returned" >&2
116
- echo "--- response body ---" >&2
117
- cat "$body_file" >&2
118
- echo "--- response headers ---" >&2
119
- cat "$hdr" >&2
120
- rm -f "$hdr" "$body_file"
130
+ curl_rc=$?
131
+ if [ "$curl_rc" -ne 0 ] || [ -z "$code" ] || [ "$code" = "000" ]; then
132
+ _mcp_init_fail "transport failure — curl exit $curl_rc, http_code '${code:-none}'; nothing listening at $url" "$body_file" "$hdr"
121
133
  return 1
122
134
  fi
123
- local got; got=$(sed -n 's/^data: //p' "$body_file" | grep -o '"protocolVersion":"[^"]*"' | head -1 | cut -d'"' -f4)
124
- [ -z "$got" ] && got=$(grep -o '"protocolVersion":"[^"]*"' "$body_file" | head -1 | cut -d'"' -f4)
125
- curl -sS -X POST "$url" \
126
- -H "Content-Type: application/json" \
127
- -H "Accept: application/json, text/event-stream" \
128
- -H "Mcp-Session-Id: $sid" \
135
+ [ "$code" -ge 400 ] && { _mcp_init_fail "HTTP $code" "$body_file" "$hdr"; return 1; }
136
+ # Unwrap SSE framing when present; a plain JSON body is used as-is.
137
+ local payload; payload=$(sed -n 's/^data: //p' "$body_file")
138
+ [ -z "$payload" ] && payload=$(cat "$body_file")
139
+ local reply; reply=$(printf '%s\n' "$payload" | grep -E '"(result|error)"' | head -1)
140
+ [ -z "$reply" ] && reply="$payload"
141
+ if printf '%s' "$reply" | grep -q '"error"'; then
142
+ _mcp_init_fail "server returned a JSON-RPC error" "$body_file" "$hdr"
143
+ return 1
144
+ fi
145
+ if ! printf '%s' "$reply" | grep -q '"result"'; then
146
+ _mcp_init_fail "HTTP $code but no JSON-RPC result in the body" "$body_file" "$hdr"
147
+ return 1
148
+ fi
149
+ local got; got=$(printf '%s' "$reply" | grep -o '"protocolVersion":"[^"]*"' | head -1 | cut -d'"' -f4)
150
+ if [ -z "$got" ]; then
151
+ _mcp_init_fail "initialize result declares no protocolVersion" "$body_file" "$hdr"
152
+ return 1
153
+ fi
154
+ local sid; sid=$(grep -i '^mcp-session-id:' "$hdr" | awk '{print $2}' | tr -d '\r\n')
155
+ local init_headers=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "MCP-Protocol-Version: $got")
156
+ [ -n "$sid" ] && init_headers+=(-H "Mcp-Session-Id: $sid")
157
+ curl -sS -X POST "$url" "${init_headers[@]}" \
129
158
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
130
159
  rm -f "$hdr" "$body_file"
131
- echo "ready sid=$sid protocol=${got:-unknown} requested=$want (HTTP $code)"
160
+ echo "ready sid=$sid protocol=$got requested=$want (HTTP $code)"
132
161
  }
133
162
 
134
- # Usage: mcp_call <url> <sid> <method> [JSON_PARAMS]
163
+ # Usage: mcp_call <url> <sid> <method> [JSON_PARAMS] [protocol]
135
164
  # Prints the JSON-RPC response. SSE framing is stripped when present, and only
136
165
  # the reply is emitted (a single POST can also carry progress notifications, so
137
166
  # emitting every event would break `| jq .result`). A transport failure or an
138
167
  # HTTP >= 400 prints the details and returns non-zero — it never returns 0 with
139
168
  # empty output. Pipe to `jq`.
169
+ # `sid` may be empty ('') for a stateless server; the session header is then
170
+ # omitted. Pass the `protocol` mcp_init printed as the 5th arg — with no
171
+ # session carrying the negotiation, MCP-Protocol-Version is what tells the
172
+ # server which revision the request speaks.
140
173
  mcp_call() {
141
- local url="$1"; local sid="$2"; local method="$3"; local params="${4:-}"
142
- [ -z "$url" ] || [ -z "$sid" ] || [ -z "$method" ] && { echo "usage: mcp_call <url> <sid> <method> [params]" >&2; return 1; }
174
+ local url="$1"; local sid="$2"; local method="$3"; local params="${4:-}"; local protocol="${5:-}"
175
+ [ -z "$url" ] || [ -z "$method" ] && { echo "usage: mcp_call <url> <sid> <method> [params] [protocol]" >&2; return 1; }
143
176
  local body
144
177
  if [ -z "$params" ]; then
145
178
  body=$(printf '{"jsonrpc":"2.0","id":%d,"method":"%s"}' "$RANDOM" "$method")
@@ -148,11 +181,10 @@ mcp_call() {
148
181
  fi
149
182
  local resp_file; resp_file=$(mktemp)
150
183
  local code curl_rc
151
- code=$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST "$url" \
152
- -H "Content-Type: application/json" \
153
- -H "Accept: application/json, text/event-stream" \
154
- -H "Mcp-Session-Id: $sid" \
155
- -d "$body")
184
+ local headers=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream")
185
+ [ -n "$sid" ] && headers+=(-H "Mcp-Session-Id: $sid")
186
+ [ -n "$protocol" ] && headers+=(-H "MCP-Protocol-Version: $protocol")
187
+ code=$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST "$url" "${headers[@]}" -d "$body")
156
188
  curl_rc=$?
157
189
  if [ "$curl_rc" -ne 0 ] || [ -z "$code" ] || [ "$code" = "000" ]; then
158
190
  echo "TRANSPORT FAILURE calling $method — curl exit $curl_rc, http_code '${code:-none}'." >&2
@@ -241,11 +273,19 @@ Capture `pid`, `url`, `port`, `log` from the `mcp_start` output — every later
241
273
  mcp_init <url-from-mcp_start>
242
274
  ```
243
275
 
244
- Runs `initialize`, sends `notifications/initialized`, prints `sid=<id>` to capture for `mcp_call`, plus the protocol version the server negotiated.
276
+ Runs `initialize`, sends `notifications/initialized`, prints the `sid` and `protocol` to capture for `mcp_call`. 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.
245
277
 
246
278
  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`.
247
279
 
248
- This exercises the **2025-era arm**: `initialize` negotiates the revision, the server mints an `Mcp-Session-Id`, and every later call rides that session. The [2026-07-28 revision](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports) is not in the `initialize` ladder at all — it is selected per request by the `io.modelcontextprotocol/protocolVersion` key in the request's own `_meta` envelope, and carries no session. Curl-based field testing therefore covers the sessionful leg; exercise the per-request leg from a real 2026-era client or an integration test.
280
+ **`sid=` may come back empty — that is a pass, not a failure.** Under `MCP_SESSION_MODE=stateless` the server mints no `Mcp-Session-Id`, and the helper then omits the session header from every later request. Thread the empty value through positionally and pass the negotiated protocol, which is what identifies the revision when no session carries it:
281
+
282
+ ```bash
283
+ mcp_call <url> '' tools/list '' <protocol-from-mcp_init>
284
+ ```
285
+
286
+ To exercise both session modes, start the server twice — once with the project's default, once with `MCP_SESSION_MODE=stateless` — and run the same calls against each.
287
+
288
+ Both modes exercise the **2025-era arm**: `initialize` negotiates the revision, and the session (when there is one) carries it. The [2026-07-28 revision](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports) is a different thing from a sessionless 2025 handshake — it does not initialize at all, and is selected per request by the `io.modelcontextprotocol/protocolVersion` key in the request's own `_meta` envelope. This helper does not reach it; exercise the per-request leg from a real 2026-era client or an integration test.
249
289
 
250
290
  ### 3. Surface the catalog
251
291
 
@@ -379,7 +419,7 @@ End with:
379
419
 
380
420
  - [ ] Stdio boot check completed — `bun run rebuild && bun run start:stdio` shows clean startup (banner, expected counts, no errors)
381
421
  - [ ] HTTP server built and started; real port parsed from log
382
- - [ ] Session initialized; `notifications/initialized` sent; negotiated protocol version matches the requested one (a downgrade is a finding)
422
+ - [ ] 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)
383
423
  - [ ] Catalog surfaced and presented; descriptions audited for leaks (implementation details, meta-coaching, consumer-aware phrasing)
384
424
  - [ ] Universal battery run on every definition (happy path, parity against the full `content[]` array, input error)
385
425
  - [ ] Situational categories applied only when triggered
@@ -4,7 +4,7 @@ description: >
4
4
  Land working-tree changes as logical commits — the work grouped by concern, topped by a release commit (version bump, changelog, regenerated artifacts) and an annotated tag. Verify, commit, tag. Stops at "committed and tagged locally" — no push, no publish. The release-and-publish skill picks up from here. Distilled from the git_wrapup_instructions protocol.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.11"
7
+ version: "1.12"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -69,6 +69,7 @@ Every file that declares a version must be updated. Skip any file that doesn't e
69
69
  - `package.json` — `version`
70
70
  - `server.json` — top-level `version` AND every `packages[].version` entry
71
71
  - `manifest.json` (if present) — `version`. Verify `name` is the bare package name (e.g. `bls-mcp-server`, not `@cyanheads/bls-mcp-server`)
72
+ - `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json` (if present) — `version`. Packaging validation fails on a mismatch; `.codex-plugin/mcp.json` is connection config and carries none
72
73
  - `README.md` — version badge
73
74
  - `CLAUDE.md` / `AGENTS.md` — if they pin a version string
74
75
  - `Dockerfile` — OCI labels if they pin the version
@@ -129,7 +130,7 @@ bun run test:package # only if the script exists — NOT part of test:all
129
130
  Do NOT `git add -A` into one commit. Group the working tree into a handful of logical commits — never one blob:
130
131
 
131
132
  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.
132
- 2. **The release commit — last, on top.** Version bumps (`package.json`, `server.json`, README badge, `CLAUDE.md`/`AGENTS.md`), the changelog entry, `CHANGELOG.md`, and `docs/tree.md` go in a single final commit that sits on top of the work stack — never mixed into a feature commit.
133
+ 2. **The release commit — last, on top.** Version bumps (`package.json`, `server.json`, `manifest.json`, the plugin manifests, README badge, `CLAUDE.md`/`AGENTS.md`), the changelog entry, `CHANGELOG.md`, and `docs/tree.md` go in a single final commit that sits on top of the work stack — never mixed into a feature commit.
133
134
 
134
135
  Stage each group explicitly, commit it, then move to the next — the release commit goes last:
135
136
 
@@ -230,7 +231,7 @@ If the working tree isn't clean or the tag doesn't point at HEAD, something went
230
231
  ## Checklist
231
232
 
232
233
  - [ ] Diff reviewed end-to-end before version bump
233
- - [ ] Version bumped in every declaring file (`package.json`, `server.json`, `manifest.json`, README badge, `CLAUDE.md`/`AGENTS.md` if they pin a version)
234
+ - [ ] Version bumped in every declaring file (`package.json`, `server.json`, `manifest.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, README badge, `CLAUDE.md`/`AGENTS.md` if they pin a version)
234
235
  - [ ] GH issues addressed by this work commented with what landed (if working from GH issues)
235
236
  - [ ] Docs updated for any new or changed features
236
237
  - [ ] Changelog authored at `changelog/<major.minor>.x/<version>.md`
@@ -65,15 +65,15 @@
65
65
  "zod": "{{ZOD_VERSION}}"
66
66
  },
67
67
  "devDependencies": {
68
- "@biomejs/biome": "2.5.5",
68
+ "@biomejs/biome": "2.5.12",
69
69
  "@socketsecurity/bun-security-scanner": "^1.1.2",
70
- "@types/node": "26.1.1",
71
- "@vitest/coverage-istanbul": "4.1.10",
70
+ "@types/node": "26.4.0",
71
+ "@vitest/coverage-istanbul": "4.1.11",
72
72
  "depcheck": "^1.4.7",
73
73
  "fast-check": "^4.9.0",
74
- "ignore": "^7.0.6",
75
- "tsc-alias": "^1.9.1",
74
+ "ignore": "^7.0.7",
75
+ "tsc-alias": "^1.9.2",
76
76
  "typescript": "^7.0.2",
77
- "vitest": "^4.1.10"
77
+ "vitest": "^4.1.11"
78
78
  }
79
79
  }