Skip to content

Commit e99adbb

Browse files
Merge remote-tracking branch 'origin/fweinberger/v2-bc-ctx-flat-getters' into fweinberger/v2-bc-d1-base
# Conflicts: # packages/core/src/exports/public/index.ts
2 parents fa0b0b6 + 5b74969 commit e99adbb

6 files changed

Lines changed: 146 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modelcontextprotocol/client': patch
3+
'@modelcontextprotocol/server': patch
4+
---
5+
6+
v1-compat: add `@deprecated` flat fields (`signal`, `requestId`, `_meta`, `authInfo`, `sendNotification`, `sendRequest`, `taskStore`, `taskId`, `taskRequestedTtl`) on the handler context (`ClientContext`/`ServerContext`) mirroring the nested `ctx.mcpReq` / `ctx.http` / `ctx.task` fields, plus the `RequestHandlerExtra` type alias. Covers the common v1 `extra.*` accesses; HTTP-transport-specific fields (`requestInfo`, `closeSSEStream`, `closeStandaloneSSEStream`) are not shimmed and require migration to `ctx.http?.req` / `ctx.http?.closeSSE` / `ctx.http?.closeStandaloneSSE`.

packages/client/src/client/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
JsonSchemaType,
1414
JsonSchemaValidator,
1515
jsonSchemaValidator,
16+
LegacyContextFields,
1617
ListChangedHandlers,
1718
ListChangedOptions,
1819
ListPromptsRequest,
@@ -266,7 +267,7 @@ export class Client extends Protocol<ClientContext> {
266267
}
267268
}
268269

269-
protected override buildContext(ctx: BaseContext, _transportInfo?: MessageExtraInfo): ClientContext {
270+
protected override buildContext(ctx: BaseContext & LegacyContextFields, _transportInfo?: MessageExtraInfo): ClientContext {
270271
return ctx;
271272
}
272273

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ export { getDisplayName } from '../../shared/metadataUtils.js';
4242
export type {
4343
BaseContext,
4444
ClientContext,
45+
LegacyContextFields,
4546
NotificationOptions,
4647
ProgressCallback,
4748
ProtocolOptions,
49+
RequestHandlerExtra,
4850
RequestHandlerSchemas,
4951
RequestOptions,
5052
ServerContext

packages/core/src/shared/protocol.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,32 @@ export type NotificationOptions = {
162162
relatedTask?: RelatedTaskMetadata;
163163
};
164164

165+
/**
166+
* v1-compat flat aliases — added by `withLegacyContextFields`.
167+
* The v2 nested forms (`ctx.mcpReq.*`, `ctx.http?.*`, `ctx.task?.*`) are preferred.
168+
* Do not add new fields here.
169+
*/
170+
export interface LegacyContextFields {
171+
/** @deprecated Use `ctx.mcpReq.signal` */
172+
signal: AbortSignal;
173+
/** @deprecated Use `ctx.mcpReq.id` */
174+
requestId: RequestId;
175+
/** @deprecated Use `ctx.mcpReq._meta` */
176+
_meta?: RequestMeta;
177+
/** @deprecated Use `ctx.http?.authInfo` */
178+
authInfo?: AuthInfo;
179+
/** @deprecated Use `ctx.mcpReq.notify` */
180+
sendNotification: (notification: Notification) => Promise<void>;
181+
/** @deprecated Use `ctx.mcpReq.send` */
182+
sendRequest: <T extends AnySchema>(request: Request, resultSchema: T, options?: RequestOptions) => Promise<SchemaOutput<T>>;
183+
/** @deprecated Use `ctx.task?.store` */
184+
taskStore?: TaskContext['store'];
185+
/** @deprecated Use `ctx.task?.id` */
186+
taskId?: TaskContext['id'];
187+
/** @deprecated Use `ctx.task?.requestedTtl` */
188+
taskRequestedTtl?: TaskContext['requestedTtl'];
189+
}
190+
165191
/**
166192
* Base context provided to all request handlers.
167193
*/
@@ -282,12 +308,49 @@ export type ServerContext = BaseContext & {
282308
*/
283309
closeStandaloneSSE?: () => void;
284310
};
285-
};
311+
} & LegacyContextFields;
286312

287313
/**
288314
* Context provided to client-side request handlers.
289315
*/
290-
export type ClientContext = BaseContext;
316+
export type ClientContext = BaseContext & LegacyContextFields;
317+
318+
/**
319+
* @deprecated Use {@linkcode ServerContext} (server side) or {@linkcode ClientContext} (client side).
320+
*
321+
* v1 name for the handler context. v2 also exposes the same data under
322+
* `ctx.mcpReq` / `ctx.http`; the flat fields remain available so existing
323+
* handlers using them compile and run unchanged. HTTP-transport-specific fields
324+
* (`requestInfo`, `closeSSEStream`, `closeStandaloneSSEStream`) are not shimmed
325+
* and require migration to `ctx.http?.req` / `ctx.http?.closeSSE` / `ctx.http?.closeStandaloneSSE`. See {@linkcode BaseContext}.
326+
*/
327+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- phantom params kept for v1 source compatibility
328+
export type RequestHandlerExtra<_Req = unknown, _Notif = unknown> = ServerContext;
329+
330+
/**
331+
* Returns a copy of `ctx` with v1's flat `extra.*` aliases populated as plain properties
332+
* mirroring the nested v2 fields. Intersected onto `ClientContext`/`ServerContext` so
333+
* existing handlers that read `extra.signal` etc. compile and run unchanged.
334+
*
335+
* @internal
336+
*/
337+
function withLegacyContextFields<T extends BaseContext>(
338+
ctx: T,
339+
sendRequest: <S extends AnySchema>(r: Request, s: S, o?: RequestOptions) => Promise<SchemaOutput<S>>
340+
): T & LegacyContextFields {
341+
return {
342+
...ctx,
343+
signal: ctx.mcpReq.signal,
344+
requestId: ctx.mcpReq.id,
345+
_meta: ctx.mcpReq._meta,
346+
authInfo: ctx.http?.authInfo,
347+
sendNotification: ctx.mcpReq.notify,
348+
sendRequest,
349+
taskStore: ctx.task?.store,
350+
taskId: ctx.task?.id,
351+
taskRequestedTtl: ctx.task?.requestedTtl
352+
};
353+
}
291354

292355
/**
293356
* Information about a request's timeout state
@@ -406,7 +469,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
406469
* Builds the context object for request handlers. Subclasses must override
407470
* to return the appropriate context type (e.g., ServerContext adds HTTP request info).
408471
*/
409-
protected abstract buildContext(ctx: BaseContext, transportInfo?: MessageExtraInfo): ContextT;
472+
protected abstract buildContext(ctx: BaseContext & LegacyContextFields, transportInfo?: MessageExtraInfo): ContextT;
410473

411474
private async _oncancel(notification: CancelledNotification): Promise<void> {
412475
if (!notification.params.requestId) {
@@ -630,7 +693,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
630693
http: extra?.authInfo ? { authInfo: extra.authInfo } : undefined,
631694
task: taskContext
632695
};
633-
const ctx = this.buildContext(baseCtx, extra);
696+
const ctx = this.buildContext(withLegacyContextFields(baseCtx, sendRequest), extra);
634697

635698
// Starting with Promise.resolve() puts any synchronous errors into the monad as well.
636699
Promise.resolve()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, test } from 'vitest';
2+
3+
import type { BaseContext, ClientContext, LegacyContextFields, RequestHandlerExtra, ServerContext } from '../../src/shared/protocol.js';
4+
import { Protocol } from '../../src/shared/protocol.js';
5+
import type { Transport } from '../../src/shared/transport.js';
6+
import type { JSONRPCMessage } from '../../src/types/index.js';
7+
8+
class TestProtocolImpl extends Protocol<ClientContext> {
9+
protected assertCapabilityForMethod(): void {}
10+
protected assertNotificationCapability(): void {}
11+
protected assertRequestHandlerCapability(): void {}
12+
protected assertTaskCapability(): void {}
13+
protected assertTaskHandlerCapability(): void {}
14+
protected buildContext(ctx: BaseContext & LegacyContextFields): ClientContext {
15+
return ctx;
16+
}
17+
}
18+
19+
class MockTransport implements Transport {
20+
onclose?: () => void;
21+
onerror?: (error: Error) => void;
22+
onmessage?: (message: unknown) => void;
23+
24+
async start(): Promise<void> {}
25+
async close(): Promise<void> {
26+
this.onclose?.();
27+
}
28+
async send(_message: JSONRPCMessage): Promise<void> {}
29+
}
30+
31+
describe('v1-compat: flat ctx.* fields', () => {
32+
test('flat fields mirror nested v2 fields', async () => {
33+
const protocol = new TestProtocolImpl();
34+
const transport = new MockTransport();
35+
await protocol.connect(transport);
36+
37+
let captured: ClientContext | undefined;
38+
const done = new Promise<void>(resolve => {
39+
protocol.setRequestHandler('ping', (_request, ctx) => {
40+
captured = ctx;
41+
resolve();
42+
return {};
43+
});
44+
});
45+
46+
transport.onmessage?.({ jsonrpc: '2.0', id: 1, method: 'ping', params: {} });
47+
await done;
48+
49+
expect(captured).toBeDefined();
50+
const ctx = captured!;
51+
52+
expect(ctx.signal).toBe(ctx.mcpReq.signal);
53+
expect(ctx.requestId).toBe(ctx.mcpReq.id);
54+
expect(ctx._meta).toBe(ctx.mcpReq._meta);
55+
expect(ctx.authInfo).toBe(ctx.http?.authInfo);
56+
expect(ctx.sendNotification).toBe(ctx.mcpReq.notify);
57+
expect(ctx.sendRequest).toBeTypeOf('function');
58+
expect(ctx.taskStore).toBe(ctx.task?.store);
59+
expect(ctx.taskId).toBe(ctx.task?.id);
60+
expect(ctx.taskRequestedTtl).toBe(ctx.task?.requestedTtl);
61+
});
62+
63+
test('RequestHandlerExtra<R, N> is a ServerContext alias (type-level)', () => {
64+
const check = (ctx: ServerContext): RequestHandlerExtra<unknown, unknown> => ctx;
65+
void check;
66+
});
67+
});

packages/server/src/server/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
JSONRPCRequest,
1616
JsonSchemaType,
1717
jsonSchemaValidator,
18+
LegacyContextFields,
1819
ListRootsRequest,
1920
LoggingLevel,
2021
LoggingMessageNotification,
@@ -152,7 +153,7 @@ export class Server extends Protocol<ServerContext> {
152153
});
153154
}
154155

155-
protected override buildContext(ctx: BaseContext, transportInfo?: MessageExtraInfo): ServerContext {
156+
protected override buildContext(ctx: BaseContext & LegacyContextFields, transportInfo?: MessageExtraInfo): ServerContext {
156157
// Only create http when there's actual HTTP transport info or auth info
157158
const hasHttpInfo = ctx.http || transportInfo?.request || transportInfo?.closeSSEStream || transportInfo?.closeStandaloneSSEStream;
158159
return {

0 commit comments

Comments
 (0)