@combycode/llm-sdk 1.7.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +462 -1
  2. package/MIGRATION.md +93 -0
  3. package/README.md +17 -2
  4. package/dist/agent/loop-config.d.ts +22 -0
  5. package/dist/agent/loop-step-state.d.ts +2 -0
  6. package/dist/agent/loop.d.ts +7 -0
  7. package/dist/agent/reflect-retry.d.ts +56 -0
  8. package/dist/agent/tool-key.d.ts +3 -0
  9. package/dist/agent/types.d.ts +8 -2
  10. package/dist/helpers/mcp.d.ts +24 -2
  11. package/dist/helpers/provenance-types.d.ts +63 -0
  12. package/dist/helpers/provenance.d.ts +12 -0
  13. package/dist/helpers/transcribe.d.ts +35 -6
  14. package/dist/index.browser.js +2997 -708
  15. package/dist/index.d.ts +19 -7
  16. package/dist/index.js +2997 -708
  17. package/dist/llm/providers/anthropic/constants.d.ts +2 -0
  18. package/dist/llm/providers/openai/completions.d.ts +9 -0
  19. package/dist/llm/providers/openai/provenance.d.ts +26 -0
  20. package/dist/llm/providers/openai/responses.d.ts +4 -2
  21. package/dist/llm/providers/openai/transcription.d.ts +39 -2
  22. package/dist/llm/types/audio.d.ts +31 -0
  23. package/dist/llm/types/messages.d.ts +89 -1
  24. package/dist/llm/types/options.d.ts +15 -0
  25. package/dist/llm/types/request.d.ts +13 -1
  26. package/dist/llm/types/response.d.ts +31 -1
  27. package/dist/llm/types/stream.d.ts +14 -1
  28. package/dist/llm/types/tiers.d.ts +6 -6
  29. package/dist/network/queue-state-config.d.ts +5 -0
  30. package/dist/network/queue-state.d.ts +7 -0
  31. package/dist/network/types.d.ts +23 -0
  32. package/dist/plugins/context-guard/strategies/anchored.d.ts +47 -0
  33. package/dist/plugins/context-measurer/counter/hybrid.d.ts +4 -1
  34. package/dist/plugins/context-measurer/counter/tiktoken.d.ts +8 -1
  35. package/dist/plugins/mcp/base-transport.d.ts +16 -0
  36. package/dist/plugins/mcp/client.d.ts +144 -7
  37. package/dist/plugins/mcp/input-required.d.ts +35 -0
  38. package/dist/plugins/mcp/jsonrpc.d.ts +7 -0
  39. package/dist/plugins/mcp/oauth.d.ts +21 -1
  40. package/dist/plugins/mcp/protocol-version.d.ts +61 -0
  41. package/dist/plugins/mcp/result-cache.d.ts +31 -0
  42. package/dist/plugins/mcp/subscriptions.d.ts +69 -0
  43. package/dist/plugins/mcp/transport-http.d.ts +31 -0
  44. package/dist/plugins/mcp/transport-stdio.d.ts +2 -0
  45. package/dist/plugins/mcp/transport-ws.d.ts +11 -1
  46. package/dist/plugins/mcp/transport.d.ts +11 -0
  47. package/dist/plugins/mcp/types.d.ts +54 -2
  48. package/dist/plugins/telemetry/telemetry.d.ts +12 -0
  49. package/dist/util/http.d.ts +8 -0
  50. package/package.json +9 -6
@@ -5,7 +5,9 @@
5
5
  import type { HookBus } from '../../bus/hook-bus';
6
6
  import type { McpTransport } from './transport';
7
7
  import type { TraceContext } from '../../network/types';
8
- import type { McpCallResult, McpCompletionRef, McpCompletionResult, McpGetPromptResult, McpInitializeResult, McpLogLevel, McpPrompt, McpResource, McpResourceContent, McpResourceTemplate, McpTask, McpTaskMetadata, McpToolDef } from './types';
8
+ import { type McpEra } from './protocol-version';
9
+ import { McpSubscription, type McpServerEvent, type McpSubscriptionFilter } from './subscriptions';
10
+ import type { McpCallResult, McpCompletionRef, McpCompletionResult, McpDiscoverResult, McpGetPromptResult, McpInitializeResult, McpLogLevel, McpPrompt, McpResource, McpResourceContent, McpResourceTemplate, McpTask, McpTaskMetadata, McpToolDef } from './types';
9
11
  export interface McpClientOptions {
10
12
  clientInfo?: {
11
13
  name: string;
@@ -29,23 +31,92 @@ export interface McpClientOptions {
29
31
  hooks: HookBus;
30
32
  server: string;
31
33
  };
32
- /** Send a `ping` every N ms to keep the connection alive (0/undefined = off). */
34
+ /** Send a `ping` every N ms to keep the connection alive (0/undefined = off).
35
+ * Ignored on a 2026-07-28 session, where `ping` no longer exists. */
33
36
  keepAliveMs?: number;
37
+ /** How to negotiate the protocol revision.
38
+ *
39
+ * - `'auto'` (default) — probe `server/discover`; anything that is not positive evidence of a
40
+ * modern server falls back to the `initialize` handshake.
41
+ * - `'legacy'` — skip the probe and run the handshake, byte-identical to pre-2.0 behaviour.
42
+ * Use this for a server that mishandles unknown methods.
43
+ * - a version string (e.g. `'2026-07-28'`) — adopt that revision directly, no probe.
44
+ *
45
+ * Fallback is a DENYLIST: every JSON-RPC error falls back to the handshake except a
46
+ * `-32022` naming only modern versions we do not share. Transport/network errors are never
47
+ * treated as an era verdict — an outage must not silently downgrade the wire. */
48
+ protocolMode?: 'auto' | 'legacy' | (string & {});
49
+ /** Cap on `input_required` retry rounds before giving up (default 10, matching every other SDK).
50
+ * A handler that never satisfies the server would otherwise loop forever. */
51
+ inputRequiredMaxRounds?: number;
52
+ /** Honour the server's `ttlMs` / `cacheScope` hints on list and read results (2026-07-28).
53
+ *
54
+ * **Off by default.** Caching changes when a caller observes a server-side change, which is the
55
+ * caller's call to make. A server that sends no hints caches nothing either way, so this is a
56
+ * no-op against every pre-2026 server. Cache entries are dropped automatically on the matching
57
+ * `*_changed` notification. */
58
+ cacheResults?: boolean;
34
59
  }
35
60
  export declare class McpClient {
36
61
  private readonly transport;
37
62
  private readonly opts;
38
63
  private serverInfo;
39
64
  private pingTimer;
65
+ private negotiatedVersion;
66
+ private discovery;
67
+ private readonly cache;
68
+ private readonly subscriptions;
40
69
  constructor(transport: McpTransport, opts?: McpClientOptions);
41
- /** The server's `initialize` result, or null before `connect()`. */
70
+ /** Drop cached list/read results. Called automatically on the matching `*_changed`
71
+ * notification; exposed because a caller may know the server moved before it says so. */
72
+ invalidateCache(method?: string): void;
73
+ /** The server's `initialize` result, or null before `connect()`.
74
+ *
75
+ * On a 2026-07-28 session there is no `initialize`, so this is SYNTHESISED from the
76
+ * `server/discover` result (capabilities, instructions, and the `_meta` serverInfo stamp).
77
+ * Deliberate: the shape a caller reads must not depend on which wire was negotiated
78
+ * (CONSTITUTION.md R2 — absorb the difference, never expose a union). */
42
79
  get info(): McpInitializeResult | null;
80
+ /** The revision actually negotiated. Defaults to the handshake-era version until `connect()`. */
81
+ get protocolVersion(): string;
82
+ /** Which wire this session speaks. `'handshake'` for 2025-11-25 and earlier. */
83
+ get era(): McpEra;
84
+ /** The raw `server/discover` result on a modern session; `null` on a handshake session.
85
+ * Carries the fields that have no handshake equivalent (`supportedVersions`, `ttlMs`,
86
+ * `cacheScope`). */
87
+ get discoverResult(): McpDiscoverResult | null;
43
88
  /** Open the transport, run the initialize handshake, and start listening for
44
89
  * server-initiated messages. */
45
90
  connect(): Promise<McpInitializeResult>;
91
+ /** The pre-2026 path, unchanged: `initialize` + `notifications/initialized`. */
92
+ private handshake;
93
+ /** One `server/discover` at `version`. No retry, no adoption — the caller decides.
94
+ *
95
+ * The transport is told the version FIRST because on HTTP it is not just bookkeeping:
96
+ * a modern server routes by the `MCP-Protocol-Version` header, so a probe sent without
97
+ * it lands on the legacy handler and is rejected with `400 Missing session ID` — the
98
+ * connection fails outright instead of negotiating. Invisible over stdio, which has no
99
+ * headers; found against mcp-py 2.0.0 Streamable HTTP (2026-08-09). */
100
+ private sendDiscover;
101
+ /** Install a discover result as the live session. */
102
+ private adoptModern;
103
+ /** `mode: 'auto'` — probe modern, fall back to the handshake on anything that is not positive
104
+ * evidence of a modern server.
105
+ *
106
+ * The fallback is a DENYLIST, matching mcp-py 2.0.0 `client/_probe.py`: every JSON-RPC error
107
+ * falls back EXCEPT a `-32022` whose `supported` list is modern-only and disjoint from ours —
108
+ * that one is a real incompatibility and must surface. Transport/network failures are rethrown
109
+ * untouched: an outage is not an era verdict, and silently downgrading the wire because a
110
+ * socket blipped would be the worst possible failure mode. */
111
+ private negotiateAuto;
46
112
  /** List every tool the server exposes (follows cursor pagination). */
47
113
  listTools(): Promise<McpToolDef[]>;
48
- /** Follow cursor pagination for a list method, collecting `field` from each page. */
114
+ /** Follow cursor pagination for a list method, collecting `field` from each page.
115
+ *
116
+ * When result caching is enabled and the server sent a `ttlMs`, the assembled list is reused
117
+ * until it expires. A paginated list is only as fresh as its shortest-lived page, so the
118
+ * effective TTL is the MINIMUM across pages — taking the last page's value would let an early,
119
+ * more volatile page go stale unnoticed. */
49
120
  private paginate;
50
121
  /** Invoke a tool by its (un-namespaced) server name.
51
122
  * Pass `trace` when the call originates from an AgentLoop run so that
@@ -57,14 +128,35 @@ export declare class McpClient {
57
128
  listResourceTemplates(): Promise<McpResourceTemplate[]>;
58
129
  /** Read a resource's contents by URI. */
59
130
  readResource(uri: string): Promise<McpResourceContent[]>;
60
- /** Subscribe to updates for a resource (server sends `notifications/resources/updated`). */
131
+ /** Subscribe to updates for a resource (server sends `notifications/resources/updated`).
132
+ *
133
+ * Handshake era only — 2026-07-28 replaces per-resource subscription with the single
134
+ * `subscriptions/listen` stream. */
61
135
  subscribeResource(uri: string): Promise<void>;
62
136
  unsubscribeResource(uri: string): Promise<void>;
137
+ /** Open a `subscriptions/listen` stream (2026-07-28) — the single channel that replaces
138
+ * `resources/subscribe` and the standalone notification stream.
139
+ *
140
+ * Every kind is **opt-in**: the server may not send what was not requested, and it acknowledges
141
+ * with the subset it actually honoured — which can be narrower than what was asked for. Check
142
+ * `subscription.honored` / `isHonored(kind)` rather than assuming the request was granted.
143
+ *
144
+ * Events are level triggers ("this changed, re-fetch if you care"), so they carry no payload
145
+ * beyond the change itself. When result caching is on, the matching entries are dropped before
146
+ * `onEvent` runs, so a handler that immediately re-lists sees fresh data.
147
+ *
148
+ * Requires a modern session and a duplex transport (stdio / WebSocket) — see
149
+ * `sendLongLivedRequest` on the HTTP transport for why. */
150
+ listen(filter: McpSubscriptionFilter, onEvent: (event: McpServerEvent) => void): Promise<McpSubscription>;
63
151
  /** List the server's prompts (follows cursor pagination). */
64
152
  listPrompts(): Promise<McpPrompt[]>;
65
153
  /** Render a prompt by name with arguments → its messages. */
66
154
  getPrompt(name: string, args?: Record<string, string>): Promise<McpGetPromptResult>;
67
- /** Set the server's log verbosity (it then sends `notifications/message`). */
155
+ /** Set the server's log verbosity (it then sends `notifications/message`).
156
+ *
157
+ * Handshake era only — `logging/setLevel` was removed at 2026-07-28, where verbosity rides in
158
+ * each request's `_meta` instead. Throws rather than silently no-opping: a caller who asked for
159
+ * debug logging and got none would have no way to tell. */
68
160
  setLogLevel(level: McpLogLevel): Promise<void>;
69
161
  /** Argument autocompletion for a prompt or resource template. */
70
162
  completeArgument(ref: McpCompletionRef, argument: {
@@ -85,8 +177,53 @@ export declare class McpClient {
85
177
  pollIntervalMs?: number;
86
178
  timeoutMs?: number;
87
179
  }): Promise<McpTask>;
88
- /** Low-level escape hatch: send any request method. */
180
+ /** Low-level escape hatch: send any request method. Carries the modern-era identity
181
+ * envelope like every other request. */
89
182
  request(method: string, params?: unknown): Promise<unknown>;
183
+ /** Every request goes through here.
184
+ *
185
+ * At 2026-07-28 there is no handshake and no session id, so each request states its
186
+ * own identity: `_meta` MUST carry the protocol version and the client capabilities
187
+ * (client info is optional). Only `server/discover` used to build that envelope, so
188
+ * every later call on a modern session was rejected:
189
+ *
190
+ * -32602 params._meta must be an object carrying the required
191
+ * 'io.modelcontextprotocol/protocolVersion' and
192
+ * 'io.modelcontextprotocol/clientCapabilities' envelope keys
193
+ *
194
+ * Unreachable without a real 2026-07-28 server — no public one exists yet; found by
195
+ * running mcp-py 2.0.0 over stdio (2026-08-09).
196
+ *
197
+ * Handshake-era sessions are untouched: identity lives in `initialize` there, and an
198
+ * unexpected `_meta` is exactly the sort of thing an older server can reject. */
199
+ private send;
200
+ /** Stamp the modern identity envelope onto a request's params.
201
+ *
202
+ * Shared by `send()` and the long-lived `subscriptions/listen`, because EVERY request
203
+ * needs it — and the long-lived path is the one where a missing envelope hides: the
204
+ * rejection arrives as the stream's end rather than a thrown error, so `listen()`
205
+ * returned a subscription that looked alive and delivered nothing. */
206
+ private withEnvelope;
90
207
  close(): Promise<void>;
208
+ /** Resolve an `input_required` result to a terminal one.
209
+ *
210
+ * The dispatcher is `handleServerRequest` — the SAME path that serves a handshake-era server
211
+ * pushing `sampling/createMessage` at us. That is the whole point of routing MRTR through here:
212
+ * a caller wires up sampling once and it works on either wire, without knowing which is in play.
213
+ *
214
+ * Skips instantly when `resultType` is absent or `'complete'`, so the handshake path pays
215
+ * nothing. */
216
+ private driveInputRequired;
217
+ /** Guard a method the 2026-07-28 revision removed. Naming the negotiated version and the
218
+ * replacement matters: without it the caller sees a bare -32601 from the server and has no way
219
+ * to know the method existed until the session happened to negotiate modern. */
220
+ private requireHandshakeEra;
221
+ /** Map a change notification onto the cache entries it invalidates. Same vocabulary on both
222
+ * eras — these methods ride the `subscriptions/listen` stream at 2026-07-28 and the
223
+ * back-channel before it, but the meaning ("refetch if you care") is identical. */
224
+ private invalidateOnChange;
225
+ /** Same invalidation as `invalidateOnChange`, driven from a typed listen-stream event. */
226
+ private invalidateForEvent;
227
+ private clientInfo;
91
228
  private handleServerRequest;
92
229
  }
@@ -0,0 +1,35 @@
1
+ /** Multi-round-trip requests (MRTR, SEP-2322) — the 2026-07-28 replacement for the server→client
2
+ * back-channel.
3
+ *
4
+ * On the handshake wire a server that needs sampling/elicitation/roots PUSHES a request at us
5
+ * mid-call. At 2026-07-28 there is no back-channel: the server instead RETURNS
6
+ * `resultType: 'input_required'` carrying the requests it needs answered, and the client re-issues
7
+ * the *same* call with the answers plus the server's opaque `requestState`.
8
+ *
9
+ * Both mechanisms feed the SAME callbacks — a caller who wired up sampling once gets it on either
10
+ * wire without knowing which is in play. Algorithm mirrors mcp-py 2.0.0 `client/_input_required.py`.
11
+ */
12
+ import type { McpInputRequest } from './types';
13
+ /** Cap on retry rounds before the driver gives up. Matches the TypeScript SDK's default; the C#
14
+ * and Go SDKs use the same value as a hard constant. */
15
+ export declare const DEFAULT_INPUT_REQUIRED_MAX_ROUNDS = 10;
16
+ /** Answer one embedded request through the client's sampling / elicitation / roots handling. */
17
+ export type InputRequestDispatcher = (key: string, request: McpInputRequest) => Promise<unknown>;
18
+ /** Re-issue the original call with the collected answers and the latest state. */
19
+ export type InputRequiredRetry<T> = (responses: Record<string, unknown> | undefined, requestState: string | undefined) => Promise<T>;
20
+ /** True when a result is the server asking for more input rather than a final answer.
21
+ *
22
+ * The discriminant is `resultType`. Per spec an ABSENT `resultType` MUST be read as `'complete'`:
23
+ * servers on earlier revisions never send the field, and treating absent as anything else would
24
+ * make every legacy result look like a question. */
25
+ export declare function isInputRequired(result: unknown): boolean;
26
+ /** Drive an `input_required` result to a terminal one.
27
+ *
28
+ * Each round either answers every embedded request and retries with the responses, or — when the
29
+ * server sent state but no questions — backs off and retries empty. `requestState` is echoed back
30
+ * byte-exact and never inspected: it is the server's sealed continuation token. */
31
+ export declare function runInputRequiredDriver<T>(first: T, opts: {
32
+ dispatch: InputRequestDispatcher;
33
+ retry: InputRequiredRetry<T>;
34
+ maxRounds?: number;
35
+ }): Promise<T>;
@@ -9,6 +9,13 @@ export declare const McpErrorCode: {
9
9
  readonly MethodNotFound: -32601;
10
10
  readonly InvalidParams: -32602;
11
11
  readonly InternalError: -32603;
12
+ /** A routing header disagrees with the request body. */
13
+ readonly HeaderMismatch: -32020;
14
+ /** The server requires a client capability this client did not declare. */
15
+ readonly MissingRequiredClientCapability: -32021;
16
+ /** The server does not speak the requested revision; `data.supported` lists the ones it does.
17
+ * The ONE error carrying era information, so negotiation reads it specifically. */
18
+ readonly UnsupportedProtocolVersion: -32022;
12
19
  };
13
20
  /** A JSON-RPC / transport level error (server unreachable, error response,
14
21
  * timeout). Tool execution failures arrive as a normal result with
@@ -27,6 +27,9 @@ export interface McpOAuthClientMetadata {
27
27
  grant_types?: string[];
28
28
  response_types?: string[];
29
29
  token_endpoint_auth_method?: string;
30
+ /** OIDC Registration §2 application type (SEP-837). Defaults to `'native'` at registration, since
31
+ * an MCP client is normally a local process with a loopback redirect. Set explicitly to override. */
32
+ application_type?: 'web' | 'native';
30
33
  }
31
34
  /** Consumer-implemented storage + interactive redirect. */
32
35
  export interface McpAuthProvider {
@@ -51,7 +54,24 @@ export interface AuthServerMetadata {
51
54
  authorization_endpoint: string;
52
55
  token_endpoint: string;
53
56
  registration_endpoint?: string;
57
+ /** The authorization server's issuer identifier (RFC 8414), used to validate the RFC 9207 `iss`
58
+ * returned with the authorization code. */
59
+ issuer?: string;
60
+ /** RFC 9207: the server states it returns `iss` on authorization responses. When true, a response
61
+ * WITHOUT `iss` is rejected — otherwise an attacker could simply strip the parameter to dodge
62
+ * the check. */
63
+ authorization_response_iss_parameter_supported?: boolean;
54
64
  }
65
+ /** Validate the RFC 9207 authorization-response issuer.
66
+ *
67
+ * This is the mix-up-attack defence: without it a malicious authorization server can hand back a
68
+ * code minted by a DIFFERENT server, and the client will dutifully redeem it — replaying the
69
+ * user's credentials against a party they never intended to authorize.
70
+ *
71
+ * Comparison is **exact string equality** per RFC 9207 §2.4 (RFC 3986 §6.2.1) — deliberately NOT
72
+ * URL-normalised. Normalising would make `https://as.example.com` and `https://as.example.com/`
73
+ * compare equal, and that leniency is precisely what an attacker looks for. */
74
+ export declare function validateAuthorizationResponseIss(iss: string | undefined, meta: Pick<AuthServerMetadata, 'issuer' | 'authorization_response_iss_parameter_supported'>): void;
55
75
  /** Security options for the OAuth flow. All fields default to the most
56
76
  * restrictive posture. Re-exported from `url-guard` for consumer convenience. */
57
77
  export type { SsrfGuardOptions as McpOAuthSecurityOptions };
@@ -114,7 +134,7 @@ export declare class McpOAuth {
114
134
  reauthorize(): Promise<boolean>;
115
135
  /** Finish the interactive flow: exchange the callback code for tokens.
116
136
  * The `returnedState` MUST match the state persisted during redirect (CSRF guard). */
117
- finish(code: string, returnedState: string): Promise<void>;
137
+ finish(code: string, returnedState: string, iss?: string): Promise<void>;
118
138
  private startRedirect;
119
139
  private tryRefresh;
120
140
  private ensureMetadata;
@@ -0,0 +1,61 @@
1
+ /** MCP protocol-version registry and era helpers.
2
+ *
3
+ * MCP has two ERAS, not just two versions:
4
+ * - **handshake** (2024-11-05 … 2025-11-25) — `initialize` + `notifications/initialized`, an
5
+ * `Mcp-Session-Id`, and a server→client back-channel (ping, logging/setLevel,
6
+ * resources/subscribe, push sampling / roots / elicitation).
7
+ * - **modern** (2026-07-28+) — no handshake and no session id. One `server/discover` probe,
8
+ * then every request carries its own identity in `_meta` and routing headers.
9
+ *
10
+ * Both are supported and neither is preferred: real servers overwhelmingly still speak
11
+ * 2025-11-25, so the handshake path must keep working byte-for-byte. See CONSTITUTION.md
12
+ * standing decisions (2026-08-08).
13
+ */
14
+ /** Every released revision, oldest to newest. Verified against mcp-py 2.0.0
15
+ * (`mcp_types/version.py`, KNOWN_PROTOCOL_VERSIONS). */
16
+ export declare const MCP_KNOWN_PROTOCOL_VERSIONS: readonly ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25", "2026-07-28"];
17
+ /** Revisions reachable via the `initialize` handshake. */
18
+ export declare const MCP_HANDSHAKE_PROTOCOL_VERSIONS: readonly ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"];
19
+ /** Revisions that use the stateless per-request envelope (`server/discover`). */
20
+ export declare const MCP_MODERN_PROTOCOL_VERSIONS: readonly ["2026-07-28"];
21
+ /** Newest revision reachable via the handshake — what we offer in `initialize`. */
22
+ export declare const MCP_LATEST_HANDSHAKE_VERSION = "2025-11-25";
23
+ /** Newest per-request-envelope revision — what the `server/discover` probe asks for. */
24
+ export declare const MCP_LATEST_MODERN_VERSION = "2026-07-28";
25
+ /** Which wire a negotiated version implies. */
26
+ export type McpEra = 'handshake' | 'modern';
27
+ /** Version strings are an ENUMERATED SET, not an ordered scalar.
28
+ *
29
+ * Released revisions happen to be dates that sort lexicographically, but future identifiers are
30
+ * not guaranteed to be date-shaped, and an unrecognised peer string must compare conservatively
31
+ * rather than accidentally (`'zzz' > '2025-11-25'` is true and meaningless). So era questions go
32
+ * through the lists above — never through `<` / `>`. Upstream calls this out explicitly, having
33
+ * been bitten by it. An unknown version reads as `handshake`: the older, safer wire. */
34
+ export declare function mcpEraOf(version: string): McpEra;
35
+ /** True when `version` is a revision this client can speak on the modern wire. */
36
+ export declare function isModernMcpVersion(version: string): boolean;
37
+ /** True when `version` is a revision reachable via the `initialize` handshake. */
38
+ export declare function isHandshakeMcpVersion(version: string): boolean;
39
+ /** The newest modern revision BOTH sides speak, or undefined if they share none. */
40
+ export declare function newestMutualModernVersion(theirs: readonly string[]): string | undefined;
41
+ /** Required on every modern request: the revision this request is written at. */
42
+ export declare const MCP_PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion";
43
+ /** Required on every modern request: what the client can do (replaces the handshake exchange). */
44
+ export declare const MCP_CLIENT_CAPABILITIES_META_KEY = "io.modelcontextprotocol/clientCapabilities";
45
+ /** Optional client identity, display-only. */
46
+ export declare const MCP_CLIENT_INFO_META_KEY = "io.modelcontextprotocol/clientInfo";
47
+ /** Server identity stamped on modern results, display-only (absent and malformed both read as
48
+ * "unknown" rather than failing the connection). */
49
+ export declare const MCP_SERVER_INFO_META_KEY = "io.modelcontextprotocol/serverInfo";
50
+ /** Stamped on every `subscriptions/listen` stream frame; the value is that request's JSON-RPC id,
51
+ * which is how a frame is attributed to one subscription. */
52
+ export declare const MCP_SUBSCRIPTION_ID_META_KEY = "io.modelcontextprotocol/subscriptionId";
53
+ export declare const MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version";
54
+ export declare const MCP_METHOD_HEADER = "mcp-method";
55
+ export declare const MCP_NAME_HEADER = "mcp-name";
56
+ /** Methods whose primary subject goes in the `Mcp-Name` routing header, and the param it comes
57
+ * from. Lets an intermediary route/authorize without parsing the body. */
58
+ export declare const MCP_NAME_BEARING_METHODS: Readonly<Record<string, string>>;
59
+ /** Header values must be ASCII; a tool name or URI may not be. Encode out-of-range characters
60
+ * rather than emitting a header the runtime will reject. */
61
+ export declare function encodeMcpHeaderValue(value: string): string;
@@ -0,0 +1,31 @@
1
+ /** Opt-in response cache honouring the 2026-07-28 `ttlMs` / `cacheScope` hints (SEP-2578).
2
+ *
3
+ * A server tells the client how long a list/read result stays fresh. Without this, `listTools()`
4
+ * re-fetches on every call — the churn this hint exists to remove.
5
+ *
6
+ * Deliberately conservative:
7
+ * - **Off unless asked for.** Caching changes when a caller observes a server change; that is the
8
+ * caller's decision, not ours.
9
+ * - **No hint, no caching.** `ttlMs` absent (every pre-2026 server) means nothing is stored, so
10
+ * behaviour is byte-identical to before.
11
+ * - **`ttlMs: 0` means immediately stale**, which is a real instruction — not a missing value to
12
+ * be replaced with a default.
13
+ * - **`cacheScope` is recorded, never used to widen sharing.** This cache lives inside one client
14
+ * with one credential, so `public` buys nothing here; storing the scope keeps the entry honest
15
+ * if the cache is ever shared.
16
+ */
17
+ import type { McpCacheHints } from './types';
18
+ export declare class McpResultCache {
19
+ private readonly entries;
20
+ /** Cache key: the method plus its arguments. Two `resources/read` calls for different URIs are
21
+ * different entries. */
22
+ static key(method: string, params?: unknown): string;
23
+ get(key: string, now?: number): unknown | undefined;
24
+ /** Store only when the server actually asked for it. Returns whether anything was stored. */
25
+ set(key: string, value: unknown, hints: McpCacheHints | undefined, now?: number): boolean;
26
+ /** Drop everything — e.g. after a `*_changed` notification says the server moved on. */
27
+ clear(): void;
28
+ /** Drop every entry for one method, leaving the others. */
29
+ clearMethod(method: string): void;
30
+ get size(): number;
31
+ }
@@ -0,0 +1,69 @@
1
+ /** `subscriptions/listen` (2026-07-28, SEP-2575) — one stream for every change notification.
2
+ *
3
+ * At 2026-07-28 the per-resource `resources/subscribe` RPC and the standalone notification channel
4
+ * are both replaced by a single long-lived `subscriptions/listen` request. The client names the
5
+ * notification kinds it wants; **every kind is opt-in and the server MUST NOT send one that was
6
+ * not asked for**. The server acknowledges with the subset it actually honoured, which can be
7
+ * smaller than what was requested — so "I asked for it" never implies "I will receive it".
8
+ *
9
+ * Every frame on the stream is stamped with the listen request's id under
10
+ * `io.modelcontextprotocol/subscriptionId`, which is how frames are attributed to a subscription.
11
+ *
12
+ * Events are **level triggers**: "this changed, re-fetch if you care". They carry no payload
13
+ * beyond the fact of the change, so consecutive identical events collapse safely.
14
+ */
15
+ /** The notification kinds a client may opt into. Mirrors the wire `SubscriptionFilter`. */
16
+ export interface McpSubscriptionFilter {
17
+ toolsListChanged?: boolean;
18
+ promptsListChanged?: boolean;
19
+ resourcesListChanged?: boolean;
20
+ /** Resource URIs to watch — the replacement for the `resources/subscribe` RPC. */
21
+ resourceSubscriptions?: string[];
22
+ }
23
+ /** A change the server announced. Level triggers: re-fetch if you care. */
24
+ export type McpServerEvent = {
25
+ type: 'tools_list_changed';
26
+ } | {
27
+ type: 'prompts_list_changed';
28
+ } | {
29
+ type: 'resources_list_changed';
30
+ } | {
31
+ type: 'resource_updated';
32
+ uri: string;
33
+ };
34
+ /** The notification methods that ride a listen stream at 2026-07-28. */
35
+ export declare const MCP_LISTEN_STREAM_METHODS: readonly ["notifications/tools/list_changed", "notifications/prompts/list_changed", "notifications/resources/list_changed", "notifications/resources/updated"];
36
+ /** The ack frame that carries the honoured subset. */
37
+ export declare const MCP_SUBSCRIPTIONS_ACKNOWLEDGED = "notifications/subscriptions/acknowledged";
38
+ /** The event a raw stream frame announces, or undefined when it carries none. */
39
+ export declare function eventFromWire(method: string, params: unknown): McpServerEvent | undefined;
40
+ /** The subscription id stamped on a frame, or undefined when it is not a listen frame. */
41
+ export declare function subscriptionIdFrom(params: unknown): string | number | undefined;
42
+ /** A live subscription: the honoured filter once acknowledged, plus event delivery. */
43
+ export declare class McpSubscription {
44
+ readonly id: string | number;
45
+ readonly requested: McpSubscriptionFilter;
46
+ private readonly onEvent;
47
+ private readonly onClose;
48
+ /** The subset the server agreed to send. `null` until the ack arrives — and it can be NARROWER
49
+ * than what was requested, so check it rather than assuming. */
50
+ honored: McpSubscriptionFilter | null;
51
+ private closed;
52
+ private endState;
53
+ constructor(id: string | number, requested: McpSubscriptionFilter, onEvent: (event: McpServerEvent) => void, onClose: () => void);
54
+ /** Feed a raw stream frame. Returns true when it belonged to this subscription. */
55
+ handleFrame(method: string, params: unknown): boolean;
56
+ /** True when the server acknowledged this kind. Unacknowledged ⇒ do not expect events. */
57
+ isHonored(kind: keyof McpSubscriptionFilter): boolean;
58
+ /** Set once the stream ends: `undefined` for a clean server-side teardown, otherwise the error
59
+ * that killed it. A subscription that stopped delivering is otherwise indistinguishable from one
60
+ * where nothing has changed yet. */
61
+ get ended(): {
62
+ error?: unknown;
63
+ } | null;
64
+ /** True while the subscription can still deliver. */
65
+ get active(): boolean;
66
+ /** Called by the transport when the underlying stream finishes. */
67
+ markEnded(error?: unknown): void;
68
+ close(): void;
69
+ }
@@ -26,11 +26,18 @@ export declare class HttpTransport extends BaseJsonRpcTransport implements McpTr
26
26
  private nextHttpId;
27
27
  private sessionId;
28
28
  private protocolVersion;
29
+ /** Handshake until negotiation says otherwise — an un-negotiated connection must behave exactly
30
+ * as it did before 2026 support existed. */
31
+ private era;
29
32
  private eventAbort;
30
33
  private lastEventId;
34
+ /** Live `subscriptions/listen` streams, so `close()` can tear them down. Without this a closed
35
+ * client would leave the POST hanging until the server gave up on it. */
36
+ private readonly streamAborts;
31
37
  constructor(config: McpHttpConfig, deps: HttpTransportDeps);
32
38
  start(): Promise<void>;
33
39
  setProtocolVersion(version: string): void;
40
+ setEra(era: 'handshake' | 'modern'): void;
34
41
  /** Open the server->client GET SSE stream (best-effort: a 405 means the server
35
42
  * is request/response only). Reconnects with backoff + Last-Event-ID until
36
43
  * close(). Runs in the background. */
@@ -43,8 +50,32 @@ export declare class HttpTransport extends BaseJsonRpcTransport implements McpTr
43
50
  close(): Promise<void>;
44
51
  /** Send a JSON-RPC response back via POST (used by handleRequest from base). */
45
52
  protected sendMessage(obj: unknown): Promise<void>;
53
+ /** Open a long-lived request whose RESPONSE BODY is the event stream.
54
+ *
55
+ * This is how `subscriptions/listen` works on Streamable HTTP: unlike an ordinary call, the POST
56
+ * does not return a single JSON-RPC message — the server holds the response open and writes
57
+ * notification frames as they occur, closing it only when the subscription ends. So it goes
58
+ * through `fetchStream` (streaming response) rather than the buffered `post()` path, which would
59
+ * surface the frames only after the stream closed.
60
+ *
61
+ * Frames are routed exactly like the GET channel's, so the client sees no difference between the
62
+ * two. The eventual JSON-RPC response has no pending entry to settle — its only meaning is "the
63
+ * stream ended", which is reported through `onEnd`. */
64
+ sendLongLivedRequest(method: string, params?: unknown, onEnd?: (error?: unknown) => void): Promise<number>;
46
65
  private headers;
66
+ /** Modern-era routing headers: `Mcp-Method` on every request, plus `Mcp-Name` carrying the
67
+ * method's subject (tool name / prompt name / resource URI) so a gateway can route and
68
+ * authorize without parsing the body. No-op on the handshake wire. */
69
+ private routingHeaders;
47
70
  /** Base headers + any OAuth bearer + per-call extras. */
48
71
  private authedHeaders;
49
72
  private post;
50
73
  }
74
+ /** Extract the JSON-RPC response matching `id` from a JSON or SSE body. */
75
+ /** The media-type ESSENCE of a Content-Type: type/subtype, lowercased, parameters stripped.
76
+ *
77
+ * A substring test misroutes anything that merely *contains* the token — `application/json` with a
78
+ * vendor parameter mentioning it, or a hypothetical `application/vnd.text/event-stream+json` —
79
+ * while still needing to cope with the ordinary `text/event-stream; charset=utf-8`. Comparing the
80
+ * essence handles both. Matches the fix upstream shipped in mcp-ts 1.30. */
81
+ export declare function mediaTypeEssence(contentType: string): string;
@@ -9,8 +9,10 @@ export declare class StdioTransport extends BaseJsonRpcTransport implements McpT
9
9
  private proc;
10
10
  private buffer;
11
11
  private readonly timeoutMs;
12
+ private readonly maxBufferSize;
12
13
  constructor(config: McpStdioConfig, opts?: {
13
14
  timeoutMs?: number;
15
+ maxBufferSize?: number;
14
16
  });
15
17
  start(): Promise<void>;
16
18
  setProtocolVersion(): void;
@@ -1,6 +1,16 @@
1
1
  /** WebSocket MCP transport — JSON-RPC messages as text frames over a duplex
2
2
  * socket (naturally bidirectional). Uses the engine's `connect` so it shares
3
- * the engine's WebSocket factory + hooks. Cross-env (browser + Node/Bun). */
3
+ * the engine's WebSocket factory + hooks. Cross-env (browser + Node/Bun).
4
+ *
5
+ * **NON-STANDARD, and deliberately kept.** The MCP SDK removed its own WebSocket transport in
6
+ * `mcp` 2.0.0, reasoning that it "was never part of the MCP specification". Ours stays: it is
7
+ * public API we shipped, and deleting an exported transport on an upstream style call would break
8
+ * consumers for no protocol reason (CONSTITUTION.md R7 — an upstream deletion is not our
9
+ * deletion). Treat it as a supported extra rather than a spec transport: the server has to opt
10
+ * into JSON-RPC over a socket, and the standard transports remain stdio and Streamable HTTP.
11
+ *
12
+ * Practical upside of keeping it: being duplex, it supports `subscriptions/listen` today —
13
+ * which Streamable HTTP does not yet. */
4
14
  import type { EngineConnect } from '../../network/types';
5
15
  import type { McpTransport } from './transport';
6
16
  import { BaseJsonRpcTransport } from './base-transport';
@@ -21,6 +21,17 @@ export interface McpTransport {
21
21
  setHandlers(handlers: IncomingMcpHandlers): void;
22
22
  /** Record the negotiated protocol version (HTTP sets a header; stdio ignores). */
23
23
  setProtocolVersion?(version: string): void;
24
+ /** Record the negotiated ERA. At `'modern'` (2026-07-28+) the wire is stateless: no
25
+ * `Mcp-Session-Id`, and every request carries `Mcp-Method` / `Mcp-Name` routing headers so an
26
+ * intermediary can route without parsing the body. Optional — stdio has no headers to set. */
27
+ setEra?(era: 'handshake' | 'modern'): void;
28
+ /** Send a long-lived request (`subscriptions/listen`) and return its id, without arming the
29
+ * normal response timeout.
30
+ *
31
+ * `onEnd` fires when the stream finishes — cleanly (the server tore the subscription down) or
32
+ * with the error that killed it. A subscription that stops delivering is otherwise invisible to
33
+ * the caller. */
34
+ sendLongLivedRequest?(method: string, params?: unknown, onEnd?: (error?: unknown) => void): Promise<string | number>;
24
35
  /** Open the server->client channel (HTTP GET SSE stream). Stdio is already
25
36
  * duplex, so this is a no-op there. Call after the initialize handshake. */
26
37
  listen?(): Promise<void> | void;
@@ -52,7 +52,40 @@ export type McpContentBlock = {
52
52
  type: string;
53
53
  [k: string]: unknown;
54
54
  };
55
- export interface McpCallResult {
55
+ /** One server→client request embedded in an `input_required` result: a `sampling/createMessage`,
56
+ * `elicitation/create` or `roots/list`, in JSON-RPC request shape. Identical in content to what a
57
+ * handshake-era server pushes over the back-channel — only the delivery differs. */
58
+ export interface McpInputRequest {
59
+ method: string;
60
+ params?: unknown;
61
+ }
62
+ /** Client-side caching directives carried by 2026-07-28 list/read results (`CacheableResult`).
63
+ *
64
+ * Optional here because every pre-2026 server omits them, and a server that sends no hints must
65
+ * behave exactly as before (CONSTITUTION.md R3). */
66
+ export interface McpCacheHints {
67
+ /** How long (ms) the client MAY reuse this result. **`0` means immediately stale** — re-fetch
68
+ * every time — so it is NOT the same as "absent" and must not be coerced to a default. */
69
+ ttlMs?: number;
70
+ /** `'public'`: no user-specific data, any cache may serve it across authorization contexts.
71
+ * `'private'`: reusable only within the same authorization context. */
72
+ cacheScope?: 'private' | 'public';
73
+ }
74
+ /** The 2026-07-28 multi-round-trip fields (SEP-2322).
75
+ *
76
+ * Upstream models this as a separate `InputRequiredResult` type, making every result a union. We
77
+ * attach it as OPTIONAL fields on the existing results instead (CONSTITUTION.md R2): code reading
78
+ * `result.content` keeps compiling, and callers who never meet a modern server never see them. */
79
+ export interface McpInputRequiredFields {
80
+ /** `'complete'` | `'input_required'`. **Absent MUST be read as `'complete'`** — earlier revisions
81
+ * never send it. Open by R1: a future revision may add a third kind. */
82
+ resultType?: 'complete' | 'input_required' | (string & {});
83
+ /** Server-assigned key → the request to answer. Present when the server has questions. */
84
+ inputRequests?: Record<string, McpInputRequest>;
85
+ /** Opaque continuation token. Echoed back byte-exact and never inspected. */
86
+ requestState?: string;
87
+ }
88
+ export interface McpCallResult extends McpInputRequiredFields {
56
89
  content: McpContentBlock[];
57
90
  isError?: boolean;
58
91
  structuredContent?: Record<string, unknown>;
@@ -92,7 +125,7 @@ export interface McpPromptMessage {
92
125
  role: 'user' | 'assistant';
93
126
  content: McpContentBlock;
94
127
  }
95
- export interface McpGetPromptResult {
128
+ export interface McpGetPromptResult extends McpInputRequiredFields {
96
129
  description?: string;
97
130
  messages: McpPromptMessage[];
98
131
  }
@@ -180,6 +213,25 @@ export interface McpInitializeResult {
180
213
  serverInfo: McpServerInfo;
181
214
  instructions?: string;
182
215
  }
216
+ /** Result of the 2026-07-28 `server/discover` probe — the modern replacement for the `initialize`
217
+ * handshake. Shape verified against mcp-py 2.0.0 (`_v2026_07_28.DiscoverResult`).
218
+ *
219
+ * `McpClient.info` synthesises an `McpInitializeResult` from this, so a caller never has to branch
220
+ * on the era (CONSTITUTION.md R2). This type exposes the fields that have no handshake equivalent. */
221
+ export interface McpDiscoverResult {
222
+ capabilities: Record<string, unknown>;
223
+ /** Revisions the server speaks; the client picks one from this list. */
224
+ supportedVersions: string[];
225
+ /** How long (ms) the client MAY cache this result. `0` = treat as immediately stale. */
226
+ ttlMs?: number;
227
+ /** `public` = cacheable across authorization contexts; `private` = same context only. */
228
+ cacheScope?: 'private' | 'public';
229
+ instructions?: string;
230
+ /** Absent on servers implementing an earlier revision, which MUST be read as `'complete'`. */
231
+ resultType?: string;
232
+ /** Carries the display-only `io.modelcontextprotocol/serverInfo` stamp. */
233
+ _meta?: Record<string, unknown>;
234
+ }
183
235
  export interface McpHttpConfig {
184
236
  /** Streamable-HTTP MCP endpoint URL. Cross-env (browser needs server CORS). */
185
237
  url: string;