Skip to content

Commit 1bcf63c

Browse files
committed
fix(server): reject requests before initialization
1 parent e8c7180 commit 1bcf63c

3 files changed

Lines changed: 87 additions & 2 deletions

File tree

.changeset/green-dryers-film.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@modelcontextprotocol/server": patch
3+
---
4+
5+
fix(server): reject requests before initialization

packages/server/src/server/server.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export class Server extends Protocol<ServerContext> {
8787
private _capabilities: ServerCapabilities;
8888
private _instructions?: string;
8989
private _jsonSchemaValidator: jsonSchemaValidator;
90+
private _receivedInitialize = false;
9091

9192
/**
9293
* Callback for when initialization has fully completed (i.e., the client has sent an `notifications/initialized` notification).
@@ -184,8 +185,19 @@ export class Server extends Protocol<ServerContext> {
184185
method: string,
185186
handler: (request: JSONRPCRequest, ctx: ServerContext) => Promise<Result>
186187
): (request: JSONRPCRequest, ctx: ServerContext) => Promise<Result> {
188+
const lifecycleHandler: (request: JSONRPCRequest, ctx: ServerContext) => Promise<Result> = async (request, ctx) => {
189+
if (!ctx.http && ctx.sessionId === undefined && !this._receivedInitialize && method !== 'initialize' && method !== 'ping') {
190+
throw new ProtocolError(ProtocolErrorCode.InvalidRequest, 'Server not initialized');
191+
}
192+
const result = await handler(request, ctx);
193+
if (method === 'initialize') {
194+
this._receivedInitialize = true;
195+
}
196+
return result;
197+
};
198+
187199
if (method !== 'tools/call') {
188-
return handler;
200+
return lifecycleHandler;
189201
}
190202
return async (request, ctx) => {
191203
const validatedRequest = parseSchema(CallToolRequestSchema, request);
@@ -195,7 +207,7 @@ export class Server extends Protocol<ServerContext> {
195207
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
196208
}
197209

198-
const result = await handler(request, ctx);
210+
const result = await lifecycleHandler(request, ctx);
199211

200212
const validationResult = parseSchema(CallToolResultSchema, result);
201213
if (!validationResult.success) {

packages/server/test/server/server.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
InMemoryTransport,
55
isJSONRPCResultResponse,
66
LATEST_PROTOCOL_VERSION,
7+
ProtocolErrorCode,
78
SUPPORTED_PROTOCOL_VERSIONS
89
} from '@modelcontextprotocol/core';
910
import { Server } from '../../src/server/server.js';
@@ -84,6 +85,73 @@ describe('Server', () => {
8485

8586
await server.close();
8687
});
88+
89+
it('rejects requests before initialize', async () => {
90+
const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: { tools: {} } });
91+
92+
server.setRequestHandler('tools/list', async () => ({ tools: [] }));
93+
94+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
95+
await server.connect(serverTransport);
96+
97+
const responses: JSONRPCMessage[] = [];
98+
clientTransport.onmessage = message => responses.push(message);
99+
await clientTransport.start();
100+
101+
await clientTransport.send({
102+
jsonrpc: '2.0',
103+
method: 'notifications/initialized'
104+
} as JSONRPCMessage);
105+
106+
await clientTransport.send({
107+
jsonrpc: '2.0',
108+
id: 1,
109+
method: 'tools/list',
110+
params: {}
111+
} as JSONRPCMessage);
112+
113+
await vi.waitFor(() => expect(responses.some(message => 'id' in message && message.id === 1)).toBe(true));
114+
115+
const rejected = responses.find(message => 'id' in message && message.id === 1);
116+
expect(rejected).toMatchObject({
117+
error: {
118+
code: ProtocolErrorCode.InvalidRequest,
119+
message: 'Server not initialized'
120+
}
121+
});
122+
123+
await clientTransport.send({
124+
jsonrpc: '2.0',
125+
id: 2,
126+
method: 'initialize',
127+
params: {
128+
protocolVersion: LATEST_PROTOCOL_VERSION,
129+
capabilities: {},
130+
clientInfo: { name: 'test-client', version: '1.0.0' }
131+
}
132+
} as JSONRPCMessage);
133+
await vi.waitFor(() => expect(responses.some(message => 'id' in message && message.id === 2)).toBe(true));
134+
135+
await clientTransport.send({
136+
jsonrpc: '2.0',
137+
method: 'notifications/initialized'
138+
} as JSONRPCMessage);
139+
140+
await clientTransport.send({
141+
jsonrpc: '2.0',
142+
id: 3,
143+
method: 'tools/list',
144+
params: {}
145+
} as JSONRPCMessage);
146+
147+
await vi.waitFor(() => expect(responses.some(message => 'id' in message && message.id === 3)).toBe(true));
148+
149+
expect(responses.find(message => 'id' in message && message.id === 3)).toMatchObject({
150+
result: { tools: [] }
151+
});
152+
153+
await server.close();
154+
});
87155
});
88156

89157
describe('getNegotiatedProtocolVersion', () => {

0 commit comments

Comments
 (0)