Skip to content

Commit 59786e0

Browse files
committed
draft: request context utility - utility methods, lifespan context as per python sdk
1 parent e0de082 commit 59786e0

File tree

6 files changed

+726
-146
lines changed

6 files changed

+726
-146
lines changed

src/server/context.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import {
2+
CreateMessageRequest,
3+
CreateMessageResultSchema,
4+
ElicitRequest,
5+
ElicitResultSchema,
6+
LoggingMessageNotification,
7+
Notification,
8+
Request,
9+
RequestId,
10+
RequestInfo,
11+
RequestMeta,
12+
Result,
13+
ServerNotification,
14+
ServerRequest,
15+
ServerResult
16+
} from '../types.js';
17+
import { RequestHandlerExtra, RequestOptions } from '../shared/protocol.js';
18+
import { ZodType } from 'zod';
19+
import type { z } from 'zod';
20+
import { Server } from './index.js';
21+
import { LifespanContext, RequestContext } from '../shared/requestContext.js';
22+
import { AuthInfo } from './auth/types.js';
23+
24+
/**
25+
* A context object that is passed to request handlers.
26+
*
27+
* Implements the RequestHandlerExtra interface for backwards compatibility.
28+
* Notes:
29+
* Keeps this backwards compatible with the old RequestHandlerExtra interface and provides getter methods for backwards compatibility.
30+
* In a breaking change, this can be removed and the RequestContext can be used directly via ctx.requestContext.
31+
*
32+
* TODO(Konstantin): Could be restructured better when breaking changes are allowed. More
33+
*/
34+
export class Context<
35+
LifespanContextT extends LifespanContext | undefined = undefined,
36+
RequestT extends Request = Request,
37+
NotificationT extends Notification = Notification,
38+
ResultT extends Result = Result
39+
> implements RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>
40+
{
41+
private readonly server: Server<RequestT, NotificationT, ResultT, LifespanContextT>;
42+
43+
/**
44+
* The request context.
45+
* A type-safe context that is passed to request handlers.
46+
*/
47+
public readonly requestCtx: RequestContext<
48+
LifespanContextT,
49+
RequestT | ServerRequest,
50+
NotificationT | ServerNotification,
51+
ResultT | ServerResult
52+
>;
53+
54+
constructor(args: {
55+
server: Server<RequestT, NotificationT, ResultT, LifespanContextT>;
56+
requestCtx: RequestContext<LifespanContextT, RequestT | ServerRequest, NotificationT | ServerNotification, ResultT | ServerResult>;
57+
}) {
58+
this.server = args.server;
59+
this.requestCtx = args.requestCtx;
60+
}
61+
62+
public get requestId(): RequestId {
63+
return this.requestCtx.requestId;
64+
}
65+
66+
public get signal(): AbortSignal {
67+
return this.requestCtx.signal;
68+
}
69+
70+
public get authInfo(): AuthInfo | undefined {
71+
return this.requestCtx.authInfo;
72+
}
73+
74+
public get requestInfo(): RequestInfo | undefined {
75+
return this.requestCtx.requestInfo;
76+
}
77+
78+
public get _meta(): RequestMeta | undefined {
79+
return this.requestCtx._meta;
80+
}
81+
82+
public get sessionId(): string | undefined {
83+
return this.requestCtx.sessionId;
84+
}
85+
86+
/**
87+
* Sends a notification that relates to the current request being handled.
88+
*
89+
* This is used by certain transports to correctly associate related messages.
90+
*/
91+
public sendNotification = (notification: NotificationT | ServerNotification): Promise<void> => {
92+
return this.requestCtx.sendNotification(notification);
93+
};
94+
95+
/**
96+
* Sends a request that relates to the current request being handled.
97+
*
98+
* This is used by certain transports to correctly associate related messages.
99+
*/
100+
public sendRequest = <U extends ZodType<object>>(
101+
request: RequestT | ServerRequest,
102+
resultSchema: U,
103+
options?: RequestOptions
104+
): Promise<z.infer<U>> => {
105+
return this.requestCtx.sendRequest(request, resultSchema, { ...options, relatedRequestId: this.requestId });
106+
};
107+
108+
/**
109+
* Sends a request to sample an LLM via the client.
110+
*/
111+
public requestSampling(params: CreateMessageRequest['params'], options?: RequestOptions) {
112+
const request: CreateMessageRequest = {
113+
method: 'sampling/createMessage',
114+
params
115+
};
116+
return this.server.request(request, CreateMessageResultSchema, { ...options, relatedRequestId: this.requestId });
117+
}
118+
119+
/**
120+
* Sends an elicitation request to the client.
121+
*/
122+
public elicit(params: ElicitRequest['params'], options?: RequestOptions) {
123+
const request: ElicitRequest = {
124+
method: 'elicitation/create',
125+
params
126+
};
127+
return this.server.request(request, ElicitResultSchema, { ...options, relatedRequestId: this.requestId });
128+
}
129+
130+
/**
131+
* Sends a logging message.
132+
*/
133+
public async log(params: LoggingMessageNotification['params'], sessionId?: string) {
134+
await this.server.sendLoggingMessage(params, sessionId);
135+
}
136+
137+
/**
138+
* Sends a debug log message.
139+
*/
140+
public async debug(message: string, extraLogData?: Record<string, unknown>, sessionId?: string) {
141+
await this.log(
142+
{
143+
level: 'debug',
144+
data: {
145+
...extraLogData,
146+
message
147+
},
148+
logger: 'server'
149+
},
150+
sessionId
151+
);
152+
}
153+
154+
/**
155+
* Sends an info log message.
156+
*/
157+
public async info(message: string, extraLogData?: Record<string, unknown>, sessionId?: string) {
158+
await this.log(
159+
{
160+
level: 'info',
161+
data: {
162+
...extraLogData,
163+
message
164+
},
165+
logger: 'server'
166+
},
167+
sessionId
168+
);
169+
}
170+
171+
/**
172+
* Sends a warning log message.
173+
*/
174+
public async warning(message: string, extraLogData?: Record<string, unknown>, sessionId?: string) {
175+
await this.log(
176+
{
177+
level: 'warning',
178+
data: {
179+
...extraLogData,
180+
message
181+
},
182+
logger: 'server'
183+
},
184+
sessionId
185+
);
186+
}
187+
188+
/**
189+
* Sends an error log message.
190+
*/
191+
public async error(message: string, extraLogData?: Record<string, unknown>, sessionId?: string) {
192+
await this.log(
193+
{
194+
level: 'error',
195+
data: {
196+
...extraLogData,
197+
message
198+
},
199+
logger: 'server'
200+
},
201+
sessionId
202+
);
203+
}
204+
}

src/server/index.ts

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mergeCapabilities, Protocol, ProtocolOptions, RequestOptions } from '../shared/protocol.js';
1+
import { mergeCapabilities, Protocol, ProtocolOptions, RequestHandlerExtra, RequestOptions } from '../shared/protocol.js';
22
import {
33
ClientCapabilities,
44
CreateMessageRequest,
@@ -29,11 +29,18 @@ import {
2929
SUPPORTED_PROTOCOL_VERSIONS,
3030
LoggingLevel,
3131
SetLevelRequestSchema,
32-
LoggingLevelSchema
32+
LoggingLevelSchema,
33+
JSONRPCRequest,
34+
MessageExtraInfo
3335
} from '../types.js';
3436
import Ajv from 'ajv';
37+
import { Context } from './context.js';
38+
import { LifespanContext, RequestContext } from '../shared/requestContext.js';
39+
import { ZodLiteral, ZodObject } from 'zod';
40+
import { z } from 'zod';
41+
import { Transport } from 'src/shared/transport.js';
3542

36-
export type ServerOptions = ProtocolOptions & {
43+
export type ServerOptions<LifespanContextT extends LifespanContext | undefined = undefined> = ProtocolOptions & {
3744
/**
3845
* Capabilities to advertise as being supported by this server.
3946
*/
@@ -43,6 +50,11 @@ export type ServerOptions = ProtocolOptions & {
4350
* Optional instructions describing how to use the server and its features.
4451
*/
4552
instructions?: string;
53+
54+
/**
55+
* Optional lifespan context type.
56+
*/
57+
lifespan?: LifespanContextT;
4658
};
4759

4860
/**
@@ -73,12 +85,14 @@ export type ServerOptions = ProtocolOptions & {
7385
export class Server<
7486
RequestT extends Request = Request,
7587
NotificationT extends Notification = Notification,
76-
ResultT extends Result = Result
88+
ResultT extends Result = Result,
89+
LifespanContextT extends LifespanContext | undefined = undefined
7790
> extends Protocol<ServerRequest | RequestT, ServerNotification | NotificationT, ServerResult | ResultT> {
7891
private _clientCapabilities?: ClientCapabilities;
7992
private _clientVersion?: Implementation;
8093
private _capabilities: ServerCapabilities;
8194
private _instructions?: string;
95+
private _lifespan?: LifespanContextT;
8296

8397
/**
8498
* Callback for when initialization has fully completed (i.e., the client has sent an `initialized` notification).
@@ -90,12 +104,12 @@ export class Server<
90104
*/
91105
constructor(
92106
private _serverInfo: Implementation,
93-
options?: ServerOptions
107+
options?: ServerOptions<LifespanContextT>
94108
) {
95109
super(options);
96110
this._capabilities = options?.capabilities ?? {};
97111
this._instructions = options?.instructions;
98-
112+
this._lifespan = options?.lifespan;
99113
this.setRequestHandler(InitializeRequestSchema, request => this._oninitialize(request));
100114
this.setNotificationHandler(InitializedNotificationSchema, () => this.oninitialized?.());
101115

@@ -125,6 +139,13 @@ export class Server<
125139
return currentLevel ? this.LOG_LEVEL_SEVERITY.get(level)! < this.LOG_LEVEL_SEVERITY.get(currentLevel)! : false;
126140
};
127141

142+
// Runtime type guard: ensure extra is our Context
143+
private isContextExtra(
144+
extra: RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>
145+
): extra is Context<LifespanContextT, RequestT, NotificationT, ResultT> {
146+
return extra instanceof Context;
147+
}
148+
128149
/**
129150
* Registers new capabilities. This can only be called before connecting to a transport.
130151
*
@@ -277,6 +298,65 @@ export class Server<
277298
return this._capabilities;
278299
}
279300

301+
/**
302+
* Registers a handler where `extra` is typed as `Context` for ergonomic server-side usage.
303+
* Internally, this wraps the handler and forwards to the base implementation.
304+
*/
305+
public override setRequestHandler<
306+
T extends ZodObject<{
307+
method: ZodLiteral<string>;
308+
}>
309+
>(
310+
requestSchema: T,
311+
handler: (
312+
request: z.infer<T>,
313+
extra: Context<LifespanContextT, RequestT, NotificationT, ResultT>
314+
) => ServerResult | ResultT | Promise<ServerResult | ResultT>
315+
): void {
316+
super.setRequestHandler(requestSchema, (request, extra) => {
317+
if (!this.isContextExtra(extra)) {
318+
throw new Error('Internal error: Expected Context for request handler extra');
319+
}
320+
return handler(request, extra);
321+
});
322+
}
323+
324+
protected override createRequestExtra(
325+
request: JSONRPCRequest,
326+
abortController: AbortController,
327+
capturedTransport: Transport | undefined,
328+
extra?: MessageExtraInfo
329+
): RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT> {
330+
const base = super.createRequestExtra(request, abortController, capturedTransport, extra) as RequestHandlerExtra<
331+
ServerRequest | RequestT,
332+
ServerNotification | NotificationT
333+
>;
334+
335+
// Wrap base in Context to add server utilities while preserving shape
336+
const requestCtx = new RequestContext<
337+
LifespanContextT,
338+
ServerRequest | RequestT,
339+
ServerNotification | NotificationT,
340+
ServerResult | ResultT
341+
>({
342+
signal: base.signal,
343+
authInfo: base.authInfo,
344+
requestInfo: base.requestInfo,
345+
requestId: base.requestId,
346+
_meta: base._meta,
347+
sessionId: base.sessionId,
348+
lifespanContext: this._lifespan as LifespanContextT,
349+
protocol: this
350+
});
351+
352+
const ctx = new Context<LifespanContextT, RequestT, NotificationT, ResultT>({
353+
server: this,
354+
requestCtx
355+
});
356+
357+
return ctx;
358+
}
359+
280360
async ping() {
281361
return this.request({ method: 'ping' }, EmptyResultSchema);
282362
}

0 commit comments

Comments
 (0)