Initial implementation of TypeScript SDK - #13
Conversation
|
Here's a video with an overview and some demos: https://youtu.be/1El_XjABrbA |
jeongukjae
left a comment
There was a problem hiding this comment.
this looks really nice! thanks a lot!
Return processed list responses from InterceptingMcpClient; use MCP -32602 for unknown interceptors; add gateway name→client routing cache with invalidation on invalid-params; parallelize interceptors/list across hosts; fix defineInterceptor destructuring to receive full invoke params.
jeongukjae
left a comment
There was a problem hiding this comment.
Looks solid for me. Thanks a lot!
|
@BobDickinson this has been approved-but-conflicting for a few weeks, and I would like to help get it in. Happy to take a pass at the conflict resolution and send it to your branch (PR-to-your-fork or a plain diff, whichever you prefer). Motive is simple: I have been building conformance fixtures over on #20 and want a merged mainline TypeScript SDK as the canonical target to certify against. |
# Conflicts: # typescript/sdk/src/index.ts # typescript/sdk/src/interceptors.ts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 'enforce' legacy-read-only) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aligns with sep.md (PR modelcontextprotocol#25) and the C# SDK: capability now lives at capabilities.extensions["io.modelcontextprotocol/interceptors"] instead of a top-level 'interceptor' key. The extensions record is typed in the v1 SDK's ServerCapabilitiesSchema, so the capability now also survives stock v1 Client initialize parsing (the old key was silently stripped). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion semantics - ChainInterceptorEntry with per-entry InterceptorOverrides (failOpen, priorityHint, mode, timeoutMs, hook narrowing); widening overrides are rejected with InterceptorOverrideHookError per the SEP MUST. - resolvePriority takes override precedence (overrides.priorityHint ?? interceptor.priorityHint). - Chain timeout is now actually enforced against real interceptor hosts: the orchestrator's signal is threaded through invokeInterceptor into client.request, with already-aborted signals short-circuited. - Per-interceptor timeoutMs aborts just that invoke and routes through resolved failOpen; the chain-aggregate timeout maps to status 'timeout'. - Caller cancellation now rejects with the abort reason instead of being misreported as chain status 'timeout'. - All validations complete before rejecting; blocked chains aggregate every validation result and summary count (SEP aggregation rule). - Per-interceptor config from ExecuteChainRequestParams.config is forwarded to interceptor/invoke (was silently dropped). - AbortSignal.any fallback for Node 20.0-20.2. - Empty host list is a no-op success chain (enables gateway pass-through). - Overrides plumbed through executeInterceptorChainOnClients options and InterceptorChainRunner options, keyed by interceptor name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… error
The invoke handler now races the interceptor handler against the timeout
signal, so a handler that ignores the signal can no longer hold the request
past timeoutMs. Timeouts surface as JSON-RPC -32000 with
{ interceptor, timeoutMs, phase } data per SEP-2624. The client-side wire
timeout becomes a backstop with headroom so the host's richer error wins.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A mutation that removed fields (e.g. argument redaction) previously had the original arguments silently restored via '?? args' fallbacks, forwarding the exact data the interceptor stripped. Malformed mutated payloads (no string name) now fail loudly instead of silently discarding the mutation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y routing listAllInterceptors drains every page (nextCursor) from a host; chain discovery, the gateway routing table, and the bridge's list aggregation all use it, so interceptors beyond page one can no longer be silently skipped. The bridge also rejects foreign cursors and no longer forwards one host's cursor to every other host. A failed routing-table build is dropped instead of poisoning every subsequent interceptor/invoke with the cached rejection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Resolver-only gateways pass requests through instead of erroring when the resolver returns no connections for a request. - Partial connect failures in the client pool close already-connected owned clients instead of leaking live transports per request. - Resolved-client dispose settles every owned client even if one close throws, and a failing per-request dispose no longer masks the request outcome. - Backend notifications/resources/updated and notifications/message are now relayed to proxied clients (subscribe was a silent end-to-end no-op), and tools/call progress is relayed under the caller's progressToken. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…overrides
The package is ESM-only ('type': 'module'); the 'require' condition pointed
at the ESM build and produced ERR_REQUIRE_ESM at runtime for CJS consumers.
CI now uses npm ci against the committed lockfile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed a batch of commits on top of @BobDickinson's work to unblock this and merge. Bob's original commits are untouched; everything lands as additive commits. Suite is green: 91 tests (up from 74), typecheck, lint, and build. Merge + SEP alignment (main moved under this PR — #17, #21, #24, #25):
Correctness fixes found in deep review:
Not addressed (follow-ups): sink semantics should be re-checked against #28 once that merges; reflection param-name binding under minification; pool eviction for dead cached clients; descriptor caching for the transparent path. 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Pull request overview
Implements the initial end-to-end TypeScript SDK for MCP Interceptors (SEP-2624), including protocol types/schemas, client-side chain orchestration, server-side interceptor hosting, and a transparent gateway/proxy with runnable examples and CI wiring.
Changes:
- Added protocol layer (types, Zod schemas, parsing/helpers) plus error types aligned to interceptor chain outcomes.
- Added client APIs for list/invoke/chain execution (single-host + multi-host merge), plus
InterceptingMcpClient. - Added server registration helpers (
registerInterceptorsOnServer), reflection-style interceptor definition, gateway/proxy implementation, docs/examples, and TypeScript CI/lint/build config.
Reviewed changes
Copilot reviewed 62 out of 63 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| typescript/sdk/tsconfig.eslint.json | Adds ESLint-specific TS project config. |
| typescript/sdk/tsconfig.build.json | Build config excluding tests from compilation output. |
| typescript/sdk/src/server/register-interceptors.ts | Registers interceptor list/invoke handlers on an MCP server and enforces timeouts. |
| typescript/sdk/src/server/register-interceptors.test.ts | Tests server registration, list filtering, invoke routing, and timeouts. |
| typescript/sdk/src/server/reflection.ts | Adds defineInterceptor and handler-argument binding/normalization utilities. |
| typescript/sdk/src/server/reflection.test.ts | Tests reflection/binding behavior and boolean-to-validation normalization. |
| typescript/sdk/src/server/interceptor-definition.ts | Builds interceptor descriptors from ergonomic definition options. |
| typescript/sdk/src/server/capabilities.ts | Computes and registers SEP extensions capability for supported events. |
| typescript/sdk/src/protocol/zod-schemas.ts | Defines wire schemas for interceptor methods and results (Zod). |
| typescript/sdk/src/protocol/types.ts | Defines TypeScript protocol DTOs for interceptors and chain execution. |
| typescript/sdk/src/protocol/results.ts | Adds helpers/type guards and parsing for interceptor results. |
| typescript/sdk/src/protocol/resolve-priority.ts | Resolves scalar/per-phase priority hints for mutation ordering. |
| typescript/sdk/src/protocol/protocol.test.ts | Tests protocol parsing and minimal descriptor JSON shape. |
| typescript/sdk/src/protocol/protocol-serialization.test.ts | Zod round-trip tests for wire shapes and union parsing. |
| typescript/sdk/src/protocol/mcp-errors.ts | MCP error helpers for interceptor not-found and timeout cases. |
| typescript/sdk/src/protocol/llm-payload.ts | Adds payload types for llm/completion lifecycle events. |
| typescript/sdk/src/protocol/errors.ts | Defines chain/validation exceptions and formatting/collection helpers. |
| typescript/sdk/src/protocol/errors.test.ts | Tests chain failure message formatting and exception throwing behavior. |
| typescript/sdk/src/protocol/constants.ts | Adds method names, capability key, and well-known event constants. |
| typescript/sdk/src/interceptors.ts | Removes prior placeholder module. |
| typescript/sdk/src/interceptors.test.ts | Removes prior placeholder test. |
| typescript/sdk/src/index.ts | Replaces placeholder export with full public SDK surface. |
| typescript/sdk/src/gateway/proxy-request.ts | Utility to wrap a backend call with request/response chain phases. |
| typescript/sdk/src/gateway/mcp-interceptor-server-connection-options.ts | Types for defining outbound interceptor-host connections. |
| typescript/sdk/src/gateway/mcp-interceptor-gateway.ts | Transparent proxy gateway implementation and lifecycle management. |
| typescript/sdk/src/gateway/gateway-resolved-interceptor-clients.ts | Wraps resolved interceptor clients and owned disposables. |
| typescript/sdk/src/gateway/gateway-proxy-configurator.ts | Wires proxy request handlers and runs interception around backend calls. |
| typescript/sdk/src/gateway/gateway-protocol-bridge.ts | Optionally exposes aggregated interceptor list/invoke via the proxy server. |
| typescript/sdk/src/gateway/gateway-message-context.ts | Defines resolver context passed to dynamic interceptor connection resolution. |
| typescript/sdk/src/gateway/gateway-interceptor-client-provider.ts | Resolves static + dynamic interceptor clients (resolver + pooling). |
| typescript/sdk/src/gateway/gateway-interceptor-client-pool.ts | Pools clients by connectionId and manages lifecycle/cleanup. |
| typescript/sdk/src/gateway/connect-interceptor-client.ts | Connects an interceptor client with defaults and a provided transport. |
| typescript/sdk/src/client/merge-interceptor-chain-entries.ts | Lists/merges multi-host interceptors and handles duplicate-name policy. |
| typescript/sdk/src/client/matches-event.test.ts | Tests event matching logic (exact + wildcard). |
| typescript/sdk/src/client/interceptor-chain-runner.ts | High-level chain runner for client/gateway flows with filters/timeouts. |
| typescript/sdk/src/client/interceptor-chain-runner.test.ts | Tests chain runner event filtering and multi-host chaining. |
| typescript/sdk/src/client/interceptor-chain-entry.ts | Defines chain entry/host DTOs and duplicate-name policy type. |
| typescript/sdk/src/client/intercepting-client.ts | Implements a gateway-style client that intercepts before calling backend methods. |
| typescript/sdk/src/client/execute-interceptor-chain-on-clients.ts | Runs SEP multi-host chain via list+invoke with per-interceptor routing. |
| typescript/sdk/src/client/execute-interceptor-chain-on-clients.test.ts | Tests ordering/duplicates/timeout/overrides in multi-host execution. |
| typescript/sdk/src/client/client-extensions.ts | Client helpers for interceptors/list, interceptor/invoke, and chain execution. |
| typescript/sdk/src/client/chain-orchestrator.ts | Core chain orchestration (ordering, audit/failOpen, timeouts, summaries). |
| typescript/sdk/src/client/chain-orchestrator.test.ts | Comprehensive unit tests for orchestrator semantics and overrides. |
| typescript/sdk/src/tests/v1-server-wiring.test.ts | Integration smoke test for v1 server wiring + extensions capability survival. |
| typescript/sdk/src/tests/integration/mcp-interceptor-gateway.test.ts | End-to-end tests for gateway proxying, interception, and protocol exposure. |
| typescript/sdk/src/tests/integration/intercepting-client.test.ts | End-to-end tests for InterceptingMcpClient behavior and blocking flows. |
| typescript/sdk/src/tests/integration/client-extensions.test.ts | Integration tests for list/invoke/chain against an in-memory host. |
| typescript/sdk/src/tests/fixtures/hosts.ts | Shared in-memory fixtures for interceptor hosts and backend servers. |
| typescript/sdk/README.md | Expanded package docs with quick starts, capability notes, and examples. |
| typescript/sdk/package.json | Renames package, adds scripts/examples tooling, adjusts exports and CI-friendly scripts. |
| typescript/sdk/examples/transparent-proxy/src/index.ts | Runnable transparent proxy example using stdio transports. |
| typescript/sdk/examples/interceptor-server/src/sample-interceptors.ts | Sample interceptors (PII validator, email redactor, logger sink). |
| typescript/sdk/examples/interceptor-server/src/index.ts | Stdio interceptor host example entrypoint. |
| typescript/sdk/examples/interceptor-server/package.json | Example package wiring (local file dep + SDK peer). |
| typescript/sdk/examples/interceptor-client/src/index.ts | Stdio client example exercising list/invoke/chain. |
| typescript/sdk/examples/interceptor-client/package.json | Example package wiring for client sample. |
| typescript/sdk/examples/gateway/src/index.ts | Runnable InterceptingMcpClient gateway sample. |
| typescript/sdk/examples/gateway-chain/src/index.ts | Runnable notes/sample for chained gateway/hosts usage guidance. |
| typescript/sdk/docs/design-and-implementation.md | Design rationale and structure/compatibility guidance for the SDK. |
| typescript/sdk/.eslintrc.json | Points ESLint to the new tsconfig.eslint.json. |
| README.md | Updates root repo status table for TypeScript package name/status. |
| .github/workflows/typescript.yml | Switches to npm ci and adds explicit typecheck step. |
Suppressed comments (1)
typescript/sdk/src/protocol/zod-schemas.ts:81
timeoutMsaccepts negative values, butregisterInterceptorsOnServeruses it withAbortSignal.timeout(...), which will throw for negative durations. Constrain it to a non-negative integer so invalid params are rejected as invalid params.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replace the v1 monolith (@modelcontextprotocol/sdk ^1.x) with the v2
package split: peer dependencies on @modelcontextprotocol/client and
@modelcontextprotocol/server ^2.0.0, with @modelcontextprotocol/core and
zod ^4.2.0 as direct dependencies.
Mechanical changes via the official v1-to-v2 codemod; manual changes:
- interceptors/list and interceptor/invoke use the v2 3-arg
setRequestHandler(method, { params }, handler) form with new
ListInterceptorsParamsSchema / InvokeInterceptorParamsSchema. Result
schemas are deliberately omitted so the server never re-validates or
transforms handler output (the mode schema normalizes enforce->active
on parse).
- Gateway forwards paginated list methods via client.request() instead
of the typed list verbs, which auto-aggregate every page in v2 and
would break page-faithful transparent proxying.
- InterceptingMcpClient.callTool uses request() with
CompatibilityCallToolResultSchema since v2 callTool() dropped the
result-schema parameter.
- v1-server-wiring.test.ts rewritten as server-wiring.test.ts against
the v2 packages.
- README and design doc updated for the v2 runtime; design doc 4.3 now
states the actual SEP-2133 extensions capability key.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 62 out of 63 changed files in this pull request and generated no new comments.
Suppressed comments (2)
typescript/sdk/src/protocol/results.ts:89
- parseInterceptorResult currently coerces boolean fields with Boolean(...), which will silently accept invalid wire shapes (e.g. "false" becomes true) instead of rejecting malformed results. Since this function is intended to parse untrusted wire JSON, it should validate that these fields are actual booleans and throw otherwise.
typescript/sdk/src/client/chain-orchestrator.ts:152 - The anySignal() polyfill attaches abort listeners to every input signal but never removes them if a different signal aborts first (or if the returned signal is abandoned). On Node versions where AbortSignal.any is unavailable, repeated chain runs can accumulate listeners and retain memory unnecessarily.
/** `AbortSignal.any` with a fallback for Node < 20.3. */
function anySignal(signals: AbortSignal[]): AbortSignal {
if (typeof AbortSignal.any === 'function') {
return AbortSignal.any(signals);
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 62 out of 63 changed files in this pull request and generated 1 comment.
Suppressed comments (7)
typescript/sdk/src/client/chain-orchestrator.ts:334
- This mutates the
InterceptorResultobject returned by the invoker (interceptor,durationMs). If the invoker returns a shared object, this can corrupt subsequent requests. Consider wrapping the returned result instead of mutating it.
const invokeParams = createInvokeParams(entry, chainParams, currentPayload);
const sw = Date.now();
const result = await invoker(invokeParams, entrySignal(chainCt, entry));
result.interceptor = entry.descriptor.name;
result.durationMs = Date.now() - sw;
typescript/sdk/src/client/chain-orchestrator.ts:423
- This mutates the
InterceptorResultobject returned by the invoker (interceptor,durationMs). For safety (and to avoid issues with frozen/shared objects), wrap the result in a new object instead of mutating it in-place.
const invokeParams = createInvokeParams(entry, chainParams, currentPayload);
const sw = Date.now();
const result = await invoker(invokeParams, entrySignal(chainCt, entry));
result.interceptor = entry.descriptor.name;
result.durationMs = Date.now() - sw;
typescript/sdk/src/protocol/results.ts:80
parseInterceptorResultusesBoolean(obj.modified)/Boolean(obj.recorded), which can incorrectly coerce non-boolean values (e.g. "false" -> true). These should be validated as booleans and rejected if malformed.
typescript/sdk/src/client/chain-orchestrator.ts:150- The
anySignalfallback attachesabortlisteners to each input signal but never removes them when the combined signal is no longer needed. If a long-lived callerAbortSignalis reused across many chains without aborting, this will accumulate listeners and leak memory.
controller.abort(s.reason);
break;
}
s.addEventListener('abort', () => controller.abort(s.reason), { once: true });
}
typescript/sdk/src/client/chain-orchestrator.ts:273
- This mutates the
InterceptorResultobject returned by the invoker (interceptor,durationMs). If a handler reuses a cached/frozen result object across invocations, this can cause cross-request contamination or runtime errors. Prefer creating a new result object when adding metadata.
This issue also appears in the following locations of the same file:
- line 330
- line 419
const sw = Date.now();
const result = await invoker(invokeParams, entrySignal(chainCt, entry));
result.interceptor = descriptor.name;
result.durationMs = Date.now() - sw;
results.push(result);
typescript/sdk/src/server/register-interceptors.ts:102
- This mutates the interceptor result object returned by the handler (
interceptor,phase). If a handler returns a shared or frozen object, this can cause cross-request contamination or throw at runtime. Prefer returning a new object with the extra fields set.
typescript/sdk/src/protocol/results.ts:75 parseInterceptorResultusesBoolean(obj.valid)which will treat non-boolean truthy values (e.g. the string "false") astrue. Since this function is meant to parse untrusted wire JSON, it should validate these fields are actual booleans (and throw otherwise) to avoid silently misclassifying results.
This issue also appears on line 76 of the same file.
The C# SDK advertises `extensions["io.modelcontextprotocol/interceptors"]`, not `extensions["interceptors"]`. Both SDKs use the same key, so the capability row in the known-gaps table is not a gap and is dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 62 out of 63 changed files in this pull request and generated no new comments.
Suppressed comments (1)
typescript/sdk/src/client/chain-orchestrator.ts:152
- The
AbortSignal.anypolyfill adds anabortlistener to every input signal but never unregisters the listeners when a different signal aborts (e.g. the per-invocation timeout). In long-running processes, repeated chain executions can accumulate listeners on the caller-provided signal until the timeout elapses, increasing memory usage and potentially hitting MaxListeners warnings.
if (s.aborted) {
controller.abort(s.reason);
break;
}
s.addEventListener('abort', () => controller.abort(s.reason), { once: true });
The bridge routing table kept the first host that listed a name and dropped the rest, so an invoke could reach either host. `interceptor/invoke` carries only the name, which is why the client chain merge already defaults to `duplicateNamePolicy: 'error'`. Ambiguous names now fail with InvalidParams; names unique across hosts route as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 62 out of 63 changed files in this pull request and generated no new comments.
Suppressed comments (2)
typescript/sdk/src/protocol/results.ts:89
parseInterceptorResultcoerces required boolean fields withBoolean(...), so missing/invalid wire fields (e.g.{ type: 'validation', ... }withoutvalid) get silently treated asfalseinstead of rejecting as an invalid result shape. This can turn malformed data into a false negative/positive and makes debugging harder.
typescript/sdk/src/server/register-interceptors.ts:109- Using
AbortSignal.timeout(...)creates a non-cancelable timer even if the handler returns quickly. On busy hosts (or with longtimeoutMs), this can leave many timers pending until they elapse. Prefer anAbortController+setTimeoutthat youclearTimeoutinfinally.
Node 20 reached end of life on 2026-04-30. Node 22 is supported until 2027-04-30. `engines`, `.nvmrc`, and `@types/node` move to 22; the CI test matrix becomes 22 and 24, and status-check.yml is updated to require those job names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 64 out of 65 changed files in this pull request and generated no new comments.
Suppressed comments (2)
typescript/sdk/src/gateway/gateway-protocol-bridge.ts:61
- GatewayInterceptorProtocolBridge registers the aggregated interceptor capability with
supportedEvents: [...allEvents], which preserves insertion order and can vary based on host ordering / each host's advertised order. Other capability builders in this SDK return a sorted list (e.g. collectSupportedEvents in server/capabilities.ts), so sorting here would keep the wire capability stable and consistent.
typescript/sdk/src/server/reflection.ts:135 - getParameterNames() only extracts names from signatures that include parentheses (e.g.
(payload) =>), but common arrow forms likepayload => { ... }have no parentheses. In that case the function falls back to arity-based positional binding, which can silently mis-bind (e.g.phase => ...receivespayload). Consider parsing single-identifier arrow params so named binding works consistently.
`(params) =>` is the shape the docstring recommends, but `params` was not in the name map, so the handler received undefined. Any unrecognized name did the same, because the arity fallback only ran when no name was extracted at all. `params` and `request` now resolve to the full invoke params, and binding falls back to positional when none of the names are recognized. Also drops the `anySignal` polyfill, which only covered Node < 20.3, and sorts the gateway's aggregated `supportedEvents` to match `collectSupportedEvents`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 64 out of 65 changed files in this pull request and generated no new comments.
Suppressed comments (4)
typescript/sdk/src/protocol/results.ts:80
parseInterceptorResultcoercesmodifiedwithBoolean(...), which will treat any truthy non-boolean value astrue. This should require a boolean so wire parsing can't misinterpret malformed results.
typescript/sdk/src/protocol/results.ts:87parseInterceptorResultcoercesrecordedwithBoolean(...), which will treat any truthy non-boolean value astrue. This should require a boolean so wire parsing can't misinterpret malformed results.
typescript/sdk/src/client/interceptor-chain-runner.ts:49shouldInterceptdoesn't treat the wildcard event ("*", i.e.InterceptionEvents.All) as "intercept everything". Withevents: ['*'], this currently returns false for all real event names, so interception is silently disabled.
typescript/sdk/src/protocol/results.ts:71parseInterceptorResultcoercesvalidwithBoolean(...), which will treat any truthy non-boolean value (e.g. the string "false") astrue, and missing values asfalse. Since this is parsing untrusted wire JSON, it should require an actual boolean for correctness.
This issue also appears in the following locations of the same file:
- line 80
- line 87
`events: ['*']` returned false for every real event name, so the one value that reads as "intercept everything" turned interception off, while omitting `events` intercepted everything. `shouldIntercept` now uses `matchesEvent`, which already treats `InterceptionEvents.All` as a wildcard. Reaches InterceptingMcpClient and the gateway proxy path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 64 out of 65 changed files in this pull request and generated no new comments.
Suppressed comments (4)
typescript/sdk/src/protocol/results.ts:88
- parseInterceptorResult coerces sink result's required
recordedflag with Boolean(...), which can misinterpret non-boolean wire values (e.g. "false" becomes true). Validate it is a boolean and throw on invalid shapes.
typescript/sdk/src/protocol/results.ts:81 - parseInterceptorResult coerces mutation result's required
modifiedflag with Boolean(...), which can misinterpret non-boolean wire values (e.g. "false" becomes true). Validate it is a boolean and throw on invalid shapes.
typescript/sdk/src/protocol/results.ts:75 - parseInterceptorResult coerces required boolean fields with Boolean(...). This will treat non-boolean inputs like the string "false" as true, and missing fields as false, silently accepting malformed wire data and potentially inverting results. Prefer validating these fields are booleans and throwing on invalid shapes.
This issue also appears in the following locations of the same file:
- line 78
- line 84
typescript/sdk/src/server/register-interceptors.test.ts:10
- The same module is imported twice here (Server and ProtocolErrorCode). Consolidating avoids duplication and keeps imports easier to maintain.
`parseInterceptorResult` hand-rolled a second, looser parser for the shape `InterceptorResultSchema` already describes: `Boolean(obj.valid)` turned the string "false" into true and a missing flag into false, while the schema `interceptor/invoke` responses are validated against requires a real boolean. The exported helper now delegates to that schema, so both paths reject the same input. Also merges a duplicate import in register-interceptors.test.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 64 out of 65 changed files in this pull request and generated no new comments.
Suppressed comments (2)
typescript/sdk/src/server/reflection.ts:136
defineInterceptor/invokeHandlerFunctionparameter binding relies onFunction.prototype.toString()parsing ingetParameterNames(), but it only matches parameter lists wrapped in parentheses. For single-parameter arrow functions without parentheses (e.g.params => { ... }),getParameterNames()returns[], causingbindHandlerArguments()to fall back to positional binding and passrequest.payloadinstead of the fullInvokeInterceptorRequestParams(contrary to the documented(params) =>behavior). Supporting the common no-parens arrow form avoids surprising runtime behavior.
typescript/sdk/src/gateway/gateway-interceptor-client-pool.ts:50GatewayInterceptorClientPoolcaches a singlePromise<Client>perconnectionId, but it also passes the per-requestsignalintoconnectInterceptorClient(). If multiple requests concurrently resolve the sameconnectionId, one request aborting can cancel the shared connection attempt for all waiters (they await the same promise). For pooled connections, the connect should not be bound to a request-scoped signal; instead, keep the shared connect running while allowing each request to stop waiting when its own signal aborts.
Initial implementation of MCP Interceptor TypeScript SDK
Checklist
Additional context
See typescript/sdk/docs/design-and-implementation.md for design rationale and implementation details.