@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.
- package/AGENTS.md +2 -1
- package/CLAUDE.md +2 -1
- package/README.md +124 -76
- package/biome.json +1 -1
- package/changelog/0.12.x/0.12.6.md +2 -2
- package/changelog/0.12.x/0.12.7.md +39 -0
- package/dist/core/worker.d.ts +1 -1
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js.map +1 -1
- package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
- package/dist/mcp-server/tools/tool-registration.js +7 -1
- package/dist/mcp-server/tools/tool-registration.js.map +1 -1
- package/dist/mcp-server/tools/utils/deferredInputSchema.d.ts +39 -0
- package/dist/mcp-server/tools/utils/deferredInputSchema.d.ts.map +1 -0
- package/dist/mcp-server/tools/utils/deferredInputSchema.js +33 -0
- package/dist/mcp-server/tools/utils/deferredInputSchema.js.map +1 -0
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +10 -2
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +25 -3
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/services/speech/providers/whisper.provider.d.ts.map +1 -1
- package/dist/services/speech/providers/whisper.provider.js +4 -2
- package/dist/services/speech/providers/whisper.provider.js.map +1 -1
- package/package.json +21 -20
- package/scripts/check-framework-antipatterns.ts +4 -1
- package/scripts/devcheck.ts +303 -33
- package/scripts/lint-packaging.ts +28 -6
- package/skills/add-tool/SKILL.md +33 -1
- package/skills/api-errors/SKILL.md +2 -1
- package/skills/design-mcp-server/SKILL.md +6 -1
- package/skills/field-test/SKILL.md +70 -30
- package/skills/git-wrapup/SKILL.md +4 -3
- 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.
|
|
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
|
-
|
|
114
|
-
if [ -z "$
|
|
115
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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=$
|
|
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 "$
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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`
|
package/templates/package.json
CHANGED
|
@@ -65,15 +65,15 @@
|
|
|
65
65
|
"zod": "{{ZOD_VERSION}}"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@biomejs/biome": "2.5.
|
|
68
|
+
"@biomejs/biome": "2.5.12",
|
|
69
69
|
"@socketsecurity/bun-security-scanner": "^1.1.2",
|
|
70
|
-
"@types/node": "26.
|
|
71
|
-
"@vitest/coverage-istanbul": "4.1.
|
|
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.
|
|
75
|
-
"tsc-alias": "^1.9.
|
|
74
|
+
"ignore": "^7.0.7",
|
|
75
|
+
"tsc-alias": "^1.9.2",
|
|
76
76
|
"typescript": "^7.0.2",
|
|
77
|
-
"vitest": "^4.1.
|
|
77
|
+
"vitest": "^4.1.11"
|
|
78
78
|
}
|
|
79
79
|
}
|