Skip to content

Commit 2544135

Browse files
authored
Merge branch 'main' into fix/1847-eager-schema-conversion
2 parents 978677f + 595652c commit 2544135

8 files changed

Lines changed: 282 additions & 10 deletions

File tree

.github/workflows/release.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,20 @@ jobs:
6363
node-version: 24
6464
cache: pnpm
6565
cache-dependency-path: pnpm-lock.yaml
66-
registry-url: 'https://registry.npmjs.org'
6766

6867
- name: Install dependencies
6968
run: pnpm install
7069

70+
# pnpm@10 delegates `pnpm publish` to the npm CLI; OIDC trusted publishing
71+
# requires npm >=11.5.1, which Node 24's bundled npm only satisfies from
72+
# ~24.6 onward. Install a recent-enough npm so we don't depend on which Node patch resolves.
73+
- name: Ensure npm CLI supports OIDC trusted publishing
74+
run: npm install -g npm@11.5.1
75+
7176
- name: Publish to npm
7277
uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1
7378
with:
7479
publish: pnpm run ci:publish
7580
env:
7681
GITHUB_TOKEN: ${{ github.token }}
77-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
78-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
7982
NPM_CONFIG_PROVENANCE: 'true'

.github/workflows/update-spec-types.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,17 @@ jobs:
5151
if: steps.check_changes.outputs.has_changes == 'true'
5252
env:
5353
GH_TOKEN: ${{ github.token }}
54+
# Skip lefthook pre-push (typecheck/lint/build); spec drift that breaks
55+
# typecheck should still open a PR so it can be fixed there.
56+
LEFTHOOK: 0
5457
run: |
5558
git config user.name "github-actions[bot]"
5659
git config user.email "github-actions[bot]@users.noreply.github.com"
5760
5861
git checkout -B update-spec-types
5962
git add packages/core/src/types/spec.types.ts
6063
git commit -m "chore: update spec.types.ts from upstream"
61-
git push -f origin update-spec-types
64+
git push -f --no-verify origin update-spec-types
6265
6366
# Create PR if it doesn't exist, or update if it does
6467
PR_BODY="This PR updates \`packages/core/src/types/spec.types.ts\` from the Model Context Protocol specification.
@@ -67,9 +70,11 @@ jobs:
6770
6871
This is an automated update triggered by the nightly cron job."
6972
70-
if gh pr view update-spec-types &>/dev/null; then
71-
echo "PR already exists, updating description..."
72-
gh pr edit update-spec-types --body "$PR_BODY"
73+
# `gh pr view <branch>` matches closed PRs too, so check for an *open* PR explicitly.
74+
EXISTING_PR=$(gh pr list --head update-spec-types --state open --json number --jq '.[0].number // empty')
75+
if [ -n "$EXISTING_PR" ]; then
76+
echo "PR #$EXISTING_PR already exists, updating description..."
77+
gh pr edit "$EXISTING_PR" --body "$PR_BODY"
7378
else
7479
gh pr create \
7580
--title "chore: update spec.types.ts from upstream" \

REVIEW.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# typescript-sdk Review Conventions
2+
3+
Guidance for reviewing pull requests on this repository. The first three sections are
4+
stable principles; the **Recurring Catches** section is auto-maintained from past human
5+
review rounds and grows over time.
6+
7+
## Guiding Principles
8+
9+
1. **Minimalism** — The SDK should do less, not more. Protocol correctness, transport
10+
lifecycle, types, and clean handler context belong in the SDK. Middleware engines,
11+
registry managers, builder patterns, and content helpers belong in userland.
12+
2. **Burden of proof is on addition** — The default answer to "should we add this?" is
13+
no. Removing something from the public API is far harder than not adding it.
14+
3. **Justify with concrete evidence** — Every new abstraction needs a concrete consumer
15+
today. Ask for real issues, benchmarks, real-world examples; apply the same standard
16+
to your own review (link spec sections, link code, show the simpler alternative).
17+
4. **Spec is the anchor** — The SDK implements the protocol spec. The further a feature
18+
drifts from the spec, the stronger the justification needs to be.
19+
5. **Kill at the highest level** — If the design is wrong, don't review the
20+
implementation. Lead with the highest-level concern; specific bugs are supporting
21+
detail.
22+
6. **Decompose by default** — A PR doing multiple things should be multiple PRs unless
23+
there's a strong reason to bundle.
24+
25+
## Review Ordering
26+
27+
1. **Design justification** — Is the overall approach sound? Is the complexity warranted?
28+
2. **Structural concerns** — Is the architecture right? Are abstractions justified?
29+
3. **Correctness** — Bugs, regressions, missing functionality.
30+
4. **Style and naming** — Nits, conventions, documentation.
31+
32+
## Checklist
33+
34+
**Protocol & spec**
35+
- Types match [`schema.ts`](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) exactly (optional vs required fields)
36+
- Correct `ProtocolError` codes (enum `ProtocolErrorCode`); HTTP status codes match spec (e.g., 404 vs 410)
37+
- Works for both stdio and Streamable HTTP transports — no transport-specific assumptions
38+
- Cross-SDK consistency: check what `python-sdk` does for the same feature
39+
40+
**API surface**
41+
- Every new export is intentional (see CLAUDE.md § Public API Exports); helpers users can write themselves belong in a cookbook, not the SDK
42+
- New abstractions have at least one concrete callsite in the PR
43+
- One way to do things — improving an existing API beats adding a parallel one
44+
45+
**Correctness**
46+
- Async: race conditions, cleanup on cancellation, unhandled rejections, missing `await`
47+
- Error propagation: caught/rethrown properly, resources cleaned up on error paths
48+
- Type safety: no unjustified `any`, no unsafe `as` assertions
49+
- Backwards compat: public-interface changes, default changes, removed exports — flagged and justified
50+
51+
**Tests & docs**
52+
- New behavior has vitest coverage including error paths
53+
- Breaking changes documented in `docs/migration.md` and `docs/migration-SKILL.md`
54+
- Bugfix or behavior change: check whether `docs/**/*.md` describes the old behavior and needs updating; flag prose that now contradicts the implementation
55+
- New feature: verify prose documentation is added (not just JSDoc), and assess whether `examples/` needs a new or updated example
56+
- Behavior change: assess whether existing `examples/` still compile and demonstrate the current API
57+
58+
## Reference
59+
60+
When verifying spec compliance, consult the spec directly rather than relying on memory:
61+
62+
- MCP documentation server: `https://modelcontextprotocol.io/mcp`
63+
- Full spec text (single file, LLM-friendly): `https://modelcontextprotocol.io/llms-full.txt` — fetch to a temp file and grep for the relevant section
64+
- Schema source of truth: [`schema.ts`](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts)
65+
66+
## Recurring Catches
67+
68+
### HTTP Transport
69+
70+
- When validating `Mcp-Session-Id`, return **400** for a missing header and **404** for an unknown/expired session — never conflate `!sessionId || !transports[sessionId]` into one status, because the client needs to distinguish "fix your request" from "start a new session". Flag any diff that branches on session-id presence/lookup with a single 4xx. (#1707, #1770)
71+
72+
### Error Handling
73+
74+
- Broad `catch` blocks must not emit client-fault JSON-RPC codes (`-32700` ParseError, `-32602` InvalidParams) for server-internal failures like stream setup, task-store misses, or polling errors — map those to `-32603` InternalError so clients don't retry/reformat pointlessly. Flag any catch-all that hard-codes ParseError/InvalidParams without discriminating the thrown cause. (#1752, #1769)
75+
76+
### Schema Compliance
77+
78+
- When editing Zod protocol schemas in `schemas.ts`, verify unknown-key handling matches the spec `schema.ts`: if the spec type has no `additionalProperties: false`, the SDK schema must use `z.looseObject()` / `.catchall(z.unknown())` rather than implicit strict — over-strict Zod (incl. `z.literal('object')` on `type`) rejects spec-valid payloads from other SDKs. Also confirm `spec.types.test.ts` still passes bidirectionally. (#1768, #1849, #1169)
79+
80+
### Async / Lifecycle
81+
82+
- In `close()` / shutdown paths, wrap user-supplied or chained callbacks (`onclose?.()`, cancel fns) in `try/finally` so a throw can't skip the remaining teardown (`abort()`, `_onclose()`, map clears) — otherwise the transport is left half-open. (#1735, #1763)
83+
- Deferred callbacks (`setTimeout`, `.finally()`, reconnect closures) must check closed/aborted state before mutating `this._*` or starting I/O — a callback scheduled pre-close can fire after close/reconnect and corrupt the new connection's state (e.g., delete the new request's `AbortController`). (#1735, #1763)
84+
85+
### Completeness
86+
87+
- When a PR replaces a pattern (error class, auth-flow step, catch shape), grep the package for surviving instances of the old form — partial migrations leave sibling code paths with the very bug the PR claims to fix. Flag every leftover site. (#1657, #1761, #1595)
88+
89+
### Documentation & Changesets
90+
91+
- Read added `.changeset/*.md` text and new inline comments against the implementation in the same diff — prose that promises behavior the code no longer ships misleads consumers and contradicts stated intent. Flag any claim the diff doesn't back. (#1718, #1838)
92+
93+
### CI & GitHub Actions
94+
95+
- Do **not** assert that a third-party GitHub Action or publish toolchain will fail or needs extra permissions/tokens without verifying its docs or source — `pnpm publish` delegates to the system npm CLI (so npm OIDC works), and `changesets/action` in publish mode has no PR-comment step requiring `pull-requests: write`. For diffs under `.github/workflows/`, confirm claimed behavior in the action's README/source before flagging. (#1838, #1836)

docs/migration-SKILL.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Notes:
8686
| `JSONRPCError` | `JSONRPCErrorResponse` |
8787
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
8888
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
89-
| `isJSONRPCResponse` | `isJSONRPCResultResponse` |
89+
| `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` (**not** v2's new `isJSONRPCResponse`, which correctly matches both result and error) |
9090
| `ResourceReference` | `ResourceTemplateReference` |
9191
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
9292
| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) |
@@ -98,7 +98,7 @@ Notes:
9898
| `StreamableHTTPError` | REMOVED (use `SdkError` with `SdkErrorCode.ClientHttp*`) |
9999
| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) |
100100

101-
All other symbols from `@modelcontextprotocol/sdk/types.js` retain their original names (e.g., `CallToolResultSchema`, `ListToolsResultSchema`, etc.).
101+
All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names. **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) are no longer part of the public API — they are internal to the SDK. For runtime validation, use type guard functions like `isCallToolResult` instead of `CallToolResultSchema.safeParse()`.
102102

103103
### Error class changes
104104

@@ -435,6 +435,13 @@ const tool = await client.callTool({ name: 'my-tool', arguments: {} });
435435

436436
Remove unused schema imports: `CallToolResultSchema`, `CompatibilityCallToolResultSchema`, `ElicitResultSchema`, `CreateMessageResultSchema`, etc., when they were only used in `request()`/`send()`/`callTool()` calls.
437437

438+
If `CallToolResultSchema` was used for **runtime validation** (not just as a `request()` argument), replace with the `isCallToolResult` type guard:
439+
440+
| v1 pattern | v2 replacement |
441+
| --------------------------------------------------- | -------------------------- |
442+
| `CallToolResultSchema.safeParse(value).success` | `isCallToolResult(value)` |
443+
| `CallToolResultSchema.parse(value)` | Use `isCallToolResult(value)` then cast, or use `CallToolResult` type |
444+
438445
## 12. Experimental: `TaskCreationParams.ttl` no longer accepts `null`
439446

440447
`TaskCreationParams.ttl` changed from `z.union([z.number(), z.null()]).optional()` to `z.number().optional()`. Per the MCP spec, `null` TTL (unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Omit `ttl` to let the server decide.

docs/migration.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,18 @@ const result = await client.callTool({ name: 'my-tool', arguments: {} });
442442

443443
The return type is now inferred from the method name via `ResultTypeMap`. For example, `client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult | CreateTaskResult>`.
444444

445+
If you were using `CallToolResultSchema` for **runtime validation** (not just in `request()`/`callTool()` calls), use the new `isCallToolResult` type guard instead:
446+
447+
```typescript
448+
// v1: runtime validation with Zod schema
449+
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
450+
if (CallToolResultSchema.safeParse(value).success) { /* ... */ }
451+
452+
// v2: use the type guard
453+
import { isCallToolResult } from '@modelcontextprotocol/client';
454+
if (isCallToolResult(value)) { /* ... */ }
455+
```
456+
445457
### Client list methods return empty results for missing capabilities
446458

447459
`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, and `listTools()` now return empty results when the server didn't advertise the corresponding capability, instead of sending the request. This respects the MCP spec's capability negotiation.
@@ -482,14 +494,16 @@ The following deprecated type aliases have been removed from `@modelcontextproto
482494
| `JSONRPCError` | `JSONRPCErrorResponse` |
483495
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
484496
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
485-
| `isJSONRPCResponse` | `isJSONRPCResultResponse` |
497+
| `isJSONRPCResponse` | `isJSONRPCResultResponse` (see note below) |
486498
| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` |
487499
| `ResourceReference` | `ResourceTemplateReference` |
488500
| `IsomorphicHeaders` | Use Web Standard `Headers` |
489501
| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) |
490502

491503
All other types and schemas exported from `@modelcontextprotocol/sdk/types.js` retain their original names — import them from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`.
492504

505+
> **Note on `isJSONRPCResponse`:** v1's `isJSONRPCResponse` was a deprecated alias that only checked for *result* responses (it was equivalent to `isJSONRPCResultResponse`). v2 removes the deprecated alias and introduces a **new** `isJSONRPCResponse` with corrected semantics — it checks for *any* response (either result or error). If you are migrating v1 code that used `isJSONRPCResponse`, rename it to `isJSONRPCResultResponse` to preserve the original behavior. Use the new `isJSONRPCResponse` only when you want to match both result and error responses.
506+
493507
**Before (v1):**
494508

495509
```typescript

packages/core/src/exports/public/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,13 @@ export { ProtocolError, UrlElicitationRequiredError } from '../../types/errors.j
104104
export {
105105
assertCompleteRequestPrompt,
106106
assertCompleteRequestResourceTemplate,
107+
isCallToolResult,
107108
isInitializedNotification,
108109
isInitializeRequest,
109110
isJSONRPCErrorResponse,
110111
isJSONRPCNotification,
111112
isJSONRPCRequest,
113+
isJSONRPCResponse,
112114
isJSONRPCResultResponse,
113115
isTaskAugmentedRequestParams,
114116
parseJSONRPCMessage
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { JSONRPC_VERSION } from './constants.js';
4+
import { isCallToolResult, isJSONRPCErrorResponse, isJSONRPCResponse, isJSONRPCResultResponse } from './guards.js';
5+
6+
describe('isJSONRPCResponse', () => {
7+
it('returns true for a valid result response', () => {
8+
expect(
9+
isJSONRPCResponse({
10+
jsonrpc: JSONRPC_VERSION,
11+
id: 1,
12+
result: {}
13+
})
14+
).toBe(true);
15+
});
16+
17+
it('returns true for a valid error response', () => {
18+
expect(
19+
isJSONRPCResponse({
20+
jsonrpc: JSONRPC_VERSION,
21+
id: 1,
22+
error: { code: -32_600, message: 'Invalid Request' }
23+
})
24+
).toBe(true);
25+
});
26+
27+
it('returns false for a request', () => {
28+
expect(
29+
isJSONRPCResponse({
30+
jsonrpc: JSONRPC_VERSION,
31+
id: 1,
32+
method: 'test'
33+
})
34+
).toBe(false);
35+
});
36+
37+
it('returns false for a notification', () => {
38+
expect(
39+
isJSONRPCResponse({
40+
jsonrpc: JSONRPC_VERSION,
41+
method: 'test'
42+
})
43+
).toBe(false);
44+
});
45+
46+
it('returns false for arbitrary objects', () => {
47+
expect(isJSONRPCResponse({ foo: 'bar' })).toBe(false);
48+
});
49+
50+
it('narrows the type correctly', () => {
51+
const value: unknown = {
52+
jsonrpc: JSONRPC_VERSION,
53+
id: 1,
54+
result: { content: [] }
55+
};
56+
if (isJSONRPCResponse(value)) {
57+
// Type should be narrowed to JSONRPCResponse
58+
expect(value.jsonrpc).toBe(JSONRPC_VERSION);
59+
expect(value.id).toBe(1);
60+
}
61+
});
62+
63+
it('agrees with isJSONRPCResultResponse || isJSONRPCErrorResponse', () => {
64+
const values = [
65+
{ jsonrpc: JSONRPC_VERSION, id: 1, result: {} },
66+
{ jsonrpc: JSONRPC_VERSION, id: 2, error: { code: -1, message: 'err' } },
67+
{ jsonrpc: JSONRPC_VERSION, id: 3, method: 'test' },
68+
{ jsonrpc: JSONRPC_VERSION, method: 'notify' },
69+
{ foo: 'bar' },
70+
null,
71+
42
72+
];
73+
for (const v of values) {
74+
expect(isJSONRPCResponse(v)).toBe(isJSONRPCResultResponse(v) || isJSONRPCErrorResponse(v));
75+
}
76+
});
77+
});
78+
79+
describe('isCallToolResult', () => {
80+
it('returns false for an empty object (content is required)', () => {
81+
expect(isCallToolResult({})).toBe(false);
82+
});
83+
84+
it('returns true for a result with content', () => {
85+
expect(
86+
isCallToolResult({
87+
content: [{ type: 'text', text: 'hello' }]
88+
})
89+
).toBe(true);
90+
});
91+
92+
it('returns true for a result with isError', () => {
93+
expect(
94+
isCallToolResult({
95+
content: [{ type: 'text', text: 'fail' }],
96+
isError: true
97+
})
98+
).toBe(true);
99+
});
100+
101+
it('returns true for a result with structuredContent', () => {
102+
expect(
103+
isCallToolResult({
104+
content: [],
105+
structuredContent: { key: 'value' }
106+
})
107+
).toBe(true);
108+
});
109+
110+
it('returns false for non-objects', () => {
111+
expect(isCallToolResult(null)).toBe(false);
112+
expect(isCallToolResult(42)).toBe(false);
113+
expect(isCallToolResult('string')).toBe(false);
114+
});
115+
116+
it('returns false for invalid content items', () => {
117+
expect(
118+
isCallToolResult({
119+
content: [{ type: 'invalid' }]
120+
})
121+
).toBe(false);
122+
});
123+
});

0 commit comments

Comments
 (0)