Skip to content

Commit 7fc93c2

Browse files
committed
fix(server): reject requests before initialization
1 parent 78fbe27 commit 7fc93c2

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
@@ -94,6 +94,7 @@ export class Server extends Protocol<ServerContext> {
9494
private _capabilities: ServerCapabilities;
9595
private _instructions?: string;
9696
private _jsonSchemaValidator: jsonSchemaValidator;
97+
private _receivedInitialize = false;
9798

9899
/**
99100
* Callback for when initialization has fully completed (i.e., the client has sent an `notifications/initialized` notification).
@@ -201,8 +202,19 @@ export class Server extends Protocol<ServerContext> {
201202
method: string,
202203
handler: (request: JSONRPCRequest, ctx: ServerContext) => Promise<Result>
203204
): (request: JSONRPCRequest, ctx: ServerContext) => Promise<Result> {
205+
const lifecycleHandler: (request: JSONRPCRequest, ctx: ServerContext) => Promise<Result> = async (request, ctx) => {
206+
if (!ctx.http && ctx.sessionId === undefined && !this._receivedInitialize && method !== 'initialize' && method !== 'ping') {
207+
throw new ProtocolError(ProtocolErrorCode.InvalidRequest, 'Server not initialized');
208+
}
209+
const result = await handler(request, ctx);
210+
if (method === 'initialize') {
211+
this._receivedInitialize = true;
212+
}
213+
return result;
214+
};
215+
204216
if (method !== 'tools/call') {
205-
return handler;
217+
return lifecycleHandler;
206218
}
207219
return async (request, ctx) => {
208220
const validatedRequest = parseSchema(CallToolRequestSchema, request);
@@ -212,7 +224,7 @@ export class Server extends Protocol<ServerContext> {
212224
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
213225
}
214226

215-
const result = await handler(request, ctx);
227+
const result = await lifecycleHandler(request, ctx);
216228

217229
const validationResult = parseSchema(CallToolResultSchema, result);
218230
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)