Skip to content

Initial implementation of TypeScript SDK - #13

Open
BobDickinson wants to merge 25 commits into
modelcontextprotocol:mainfrom
BobDickinson:main
Open

Initial implementation of TypeScript SDK#13
BobDickinson wants to merge 25 commits into
modelcontextprotocol:mainfrom
BobDickinson:main

Conversation

@BobDickinson

Copy link
Copy Markdown

Initial implementation of MCP Interceptor TypeScript SDK

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

See typescript/sdk/docs/design-and-implementation.md for design rationale and implementation details.

@BobDickinson BobDickinson changed the title Intiial implementation of TypeScript SDK Initial implementation of TypeScript SDK May 24, 2026
@BobDickinson

Copy link
Copy Markdown
Author

Here's a video with an overview and some demos: https://youtu.be/1El_XjABrbA

Comment thread typescript/sdk/examples/transparent-proxy/src/index.ts

@jeongukjae jeongukjae left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks really nice! thanks a lot!

Comment thread typescript/sdk/vitest.config.js Outdated
Comment thread typescript/sdk/src/server/reflection.ts
Comment thread typescript/sdk/src/client/chain-orchestrator.ts
Comment thread typescript/sdk/src/server/register-interceptors.ts Outdated
Comment thread typescript/sdk/src/gateway/gateway-protocol-bridge.ts Outdated
Comment thread typescript/sdk/src/client/intercepting-client.ts
Comment thread typescript/sdk/src/client/intercepting-client.ts
Comment thread typescript/sdk/src/client/merge-interceptor-chain-entries.ts
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
jeongukjae previously approved these changes Jun 15, 2026

@jeongukjae jeongukjae left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks solid for me. Thanks a lot!

@wi-ski

wi-ski commented Jul 15, 2026

Copy link
Copy Markdown

@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.

olaservo and others added 10 commits July 23, 2026 07:10
# 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>
@olaservo

Copy link
Copy Markdown
Member

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):

  • Merged main and resolved conflicts; SEP-1763 → SEP-2624 references updated.
  • Mode values aligned to the spec: 'active' is canonical, 'enforce' accepted read-only as a legacy value (the design doc had these inverted).
  • Capability key moved to capabilities.extensions["io.modelcontextprotocol/interceptors"] (SEP-2133 format per Align capability key to SEP-2133 extensions format #25, same key as C#). Side benefit: the v1 SDK's ServerCapabilitiesSchema types extensions, so the capability now survives stock-client initialize parsing — the old top-level interceptor key was silently stripped.
  • Implemented InterceptorOverrides (Introduce InterceptorOverrides in the chain execution model #24): per-entry failOpen / priorityHint / mode / timeoutMs / hook narrowing (widening rejected per the SEP MUST), plumbed through the orchestrator, executeInterceptorChainOnClients, and InterceptorChainRunner. First SDK in the repo to implement it.

Correctness fixes found in deep review:

  • Chain timeoutMs is now actually enforced against real hosts — the orchestrator signal was previously dropped before reaching client.request, so a hung host blocked for the SDK default 60s. Caller cancellation now rejects with the abort reason instead of reporting status timeout.
  • Server-side invoke handler races the interceptor against the timeout signal (a handler ignoring the signal could previously hang forever) and reports SEP -32000 with {interceptor, timeoutMs, phase}.
  • Mutations that remove fields (e.g. argument redaction) are forwarded as-is — ?? args fallbacks were silently restoring the original unredacted arguments.
  • interceptors/list pagination is drained everywhere (listAllInterceptors); a paginating host's page-2 interceptors were silently skipped.
  • Gateway: empty-resolver requests pass through instead of erroring; partial connect failures no longer leak clients; a transient failure during routing-table build no longer poisons every later interceptor/invoke; resources/updated, notifications/message, and tools/call progress are now forwarded (subscribe was a silent no-op through the proxy); per-interceptor config is forwarded to interceptor/invoke.
  • Packaging: dropped the require export condition (ESM-only package; it produced ERR_REQUIRE_ESM), added an AbortSignal.any fallback for Node 20.0–20.2, CI uses npm ci.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • timeoutMs accepts negative values, but registerInterceptorsOnServer uses it with AbortSignal.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.

Comment thread typescript/sdk/src/protocol/zod-schemas.ts Outdated
Comment thread typescript/sdk/src/server/register-interceptors.ts
Comment thread typescript/sdk/src/protocol/zod-schemas.ts Outdated
Comment thread typescript/sdk/src/protocol/types.ts Outdated
Comment thread typescript/sdk/src/protocol/results.ts Outdated
Comment thread typescript/sdk/src/protocol/constants.ts Outdated
Comment thread typescript/sdk/src/client/client-extensions.ts Outdated
Comment thread typescript/sdk/src/client/chain-orchestrator.ts Outdated
Comment thread typescript/sdk/docs/design-and-implementation.md Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
  }

Copilot AI review requested due to automatic review settings August 19, 2026 00:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 InterceptorResult object 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 InterceptorResult object 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

  • parseInterceptorResult uses Boolean(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 anySignal fallback attaches abort listeners to each input signal but never removes them when the combined signal is no longer needed. If a long-lived caller AbortSignal is 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 InterceptorResult object 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
  • parseInterceptorResult uses Boolean(obj.valid) which will treat non-boolean truthy values (e.g. the string "false") as true. 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.

Comment thread typescript/sdk/src/gateway/gateway-protocol-bridge.ts
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>
Copilot AI review requested due to automatic review settings August 19, 2026 00:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.any polyfill adds an abort listener 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>
Copilot AI review requested due to automatic review settings August 19, 2026 02:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 coerces required boolean fields with Boolean(...), so missing/invalid wire fields (e.g. { type: 'validation', ... } without valid) get silently treated as false instead 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 long timeoutMs), this can leave many timers pending until they elapse. Prefer an AbortController + setTimeout that you clearTimeout in finally.

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>
Copilot AI review requested due to automatic review settings August 19, 2026 17:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 like payload => { ... } have no parentheses. In that case the function falls back to arity-based positional binding, which can silently mis-bind (e.g. phase => ... receives payload). Consider parsing single-identifier arrow params so named binding works consistently.

olaservo and others added 2 commits August 19, 2026 14:33
`(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>
Copilot AI review requested due to automatic review settings August 19, 2026 21:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • parseInterceptorResult coerces modified with Boolean(...), which will treat any truthy non-boolean value as true. This should require a boolean so wire parsing can't misinterpret malformed results.
    typescript/sdk/src/protocol/results.ts:87
  • parseInterceptorResult coerces recorded with Boolean(...), which will treat any truthy non-boolean value as true. This should require a boolean so wire parsing can't misinterpret malformed results.
    typescript/sdk/src/client/interceptor-chain-runner.ts:49
  • shouldIntercept doesn't treat the wildcard event ("*", i.e. InterceptionEvents.All) as "intercept everything". With events: ['*'], this currently returns false for all real event names, so interception is silently disabled.
    typescript/sdk/src/protocol/results.ts:71
  • parseInterceptorResult coerces valid with Boolean(...), which will treat any truthy non-boolean value (e.g. the string "false") as true, and missing values as false. 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>
Copilot AI review requested due to automatic review settings August 20, 2026 04:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 recorded flag 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 modified flag 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>
Copilot AI review requested due to automatic review settings August 20, 2026 04:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/invokeHandlerFunction parameter binding relies on Function.prototype.toString() parsing in getParameterNames(), but it only matches parameter lists wrapped in parentheses. For single-parameter arrow functions without parentheses (e.g. params => { ... }), getParameterNames() returns [], causing bindHandlerArguments() to fall back to positional binding and pass request.payload instead of the full InvokeInterceptorRequestParams (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:50
  • GatewayInterceptorClientPool caches a single Promise<Client> per connectionId, but it also passes the per-request signal into connectInterceptorClient(). If multiple requests concurrently resolve the same connectionId, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants