Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/short-ducks-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': patch
---

Allow `@modelcontextprotocol/server` list-changed helpers to forward notification options so stateless Streamable HTTP servers can send inline resource, tool, and prompt list updates on the active POST response.
38 changes: 21 additions & 17 deletions packages/server/src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ListResourcesResult,
ListToolsResult,
LoggingMessageNotification,
NotificationOptions,
Prompt,
PromptReference,
ReadResourceResult,
Expand Down Expand Up @@ -599,7 +600,7 @@ export class McpServer {
);

this.setResourceRequestHandlers();
this.sendResourceListChanged();
void this.sendResourceListChanged();
return registeredResource;
} else {
if (this._registeredResourceTemplates[name]) {
Expand All @@ -615,7 +616,7 @@ export class McpServer {
);

this.setResourceRequestHandlers();
this.sendResourceListChanged();
void this.sendResourceListChanged();
return registeredResourceTemplate;
}
}
Expand Down Expand Up @@ -646,7 +647,7 @@ export class McpServer {
if (updates.metadata !== undefined) registeredResource.metadata = updates.metadata;
if (updates.callback !== undefined) registeredResource.readCallback = updates.callback;
if (updates.enabled !== undefined) registeredResource.enabled = updates.enabled;
this.sendResourceListChanged();
void this.sendResourceListChanged();
}
};
this._registeredResources[uri] = registeredResource;
Expand Down Expand Up @@ -679,7 +680,7 @@ export class McpServer {
if (updates.metadata !== undefined) registeredResourceTemplate.metadata = updates.metadata;
if (updates.callback !== undefined) registeredResourceTemplate.readCallback = updates.callback;
if (updates.enabled !== undefined) registeredResourceTemplate.enabled = updates.enabled;
this.sendResourceListChanged();
void this.sendResourceListChanged();
}
};
this._registeredResourceTemplates[name] = registeredResourceTemplate;
Expand Down Expand Up @@ -738,7 +739,7 @@ export class McpServer {
}

if (updates.enabled !== undefined) registeredPrompt.enabled = updates.enabled;
this.sendPromptListChanged();
void this.sendPromptListChanged();
}
};
this._registeredPrompts[name] = registeredPrompt;
Expand Down Expand Up @@ -821,13 +822,13 @@ export class McpServer {
if (updates.annotations !== undefined) registeredTool.annotations = updates.annotations;
if (updates._meta !== undefined) registeredTool._meta = updates._meta;
if (updates.enabled !== undefined) registeredTool.enabled = updates.enabled;
this.sendToolListChanged();
void this.sendToolListChanged();
}
};
this._registeredTools[name] = registeredTool;

this.setToolRequestHandlers();
this.sendToolListChanged();
void this.sendToolListChanged();

return registeredTool;
}
Expand Down Expand Up @@ -939,7 +940,7 @@ export class McpServer {
);

this.setPromptRequestHandlers();
this.sendPromptListChanged();
void this.sendPromptListChanged();

return registeredPrompt;
}
Expand Down Expand Up @@ -973,28 +974,31 @@ export class McpServer {
/**
* Sends a resource list changed event to the client, if connected.
*/
sendResourceListChanged() {
if (this.isConnected()) {
this.server.sendResourceListChanged();
sendResourceListChanged(options?: NotificationOptions) {
if (!this.isConnected()) {
return;
}
return this.server.sendResourceListChanged(options);
}

/**
* Sends a tool list changed event to the client, if connected.
*/
sendToolListChanged() {
if (this.isConnected()) {
this.server.sendToolListChanged();
sendToolListChanged(options?: NotificationOptions) {
if (!this.isConnected()) {
return;
}
return this.server.sendToolListChanged(options);
}

/**
* Sends a prompt list changed event to the client, if connected.
*/
sendPromptListChanged() {
if (this.isConnected()) {
this.server.sendPromptListChanged();
sendPromptListChanged(options?: NotificationOptions) {
if (!this.isConnected()) {
return;
}
return this.server.sendPromptListChanged(options);
}
}

Expand Down
19 changes: 11 additions & 8 deletions packages/server/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,17 +660,20 @@ export class Server extends Protocol<ServerContext> {
});
}

async sendResourceListChanged() {
return this.notification({
method: 'notifications/resources/list_changed'
});
async sendResourceListChanged(options?: NotificationOptions) {
return this.notification(
{
method: 'notifications/resources/list_changed'
},
options
);
}

async sendToolListChanged() {
return this.notification({ method: 'notifications/tools/list_changed' });
async sendToolListChanged(options?: NotificationOptions) {
return this.notification({ method: 'notifications/tools/list_changed' }, options);
}

async sendPromptListChanged() {
return this.notification({ method: 'notifications/prompts/list_changed' });
async sendPromptListChanged(options?: NotificationOptions) {
return this.notification({ method: 'notifications/prompts/list_changed' }, options);
}
}
46 changes: 46 additions & 0 deletions packages/server/test/server/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ function parseSSEData(text: string): unknown {
return JSON.parse(dataLine.slice(5).trim());
}

function parseSSEEvents(text: string): unknown[] {
return text
.split('\n\n')
.filter(event => event.includes('data:'))
.map(event => parseSSEData(event));
}

function expectErrorResponse(data: unknown, expectedCode: number, expectedMessagePattern: RegExp): void {
expect(data).toMatchObject({
jsonrpc: '2.0',
Expand Down Expand Up @@ -472,6 +479,45 @@ describe('Zod v4', () => {

expect(response.status).toBe(200);
});

it('should send list changed notifications inline on the POST SSE stream when relatedRequestId is provided', async () => {
mcpServer.registerTool('refresh-tools', { description: 'Refresh tool list' }, async ctx => {
await mcpServer.sendToolListChanged({ relatedRequestId: ctx.mcpReq.id });
return { content: [{ type: 'text', text: 'refreshed' }] };
});

const initRequest = createRequest('POST', TEST_MESSAGES.initialize);
await transport.handleRequest(initRequest);

const toolCallMessage: JSONRPCMessage = {
jsonrpc: '2.0',
method: 'tools/call',
params: {
name: 'refresh-tools'
},
id: 'call-1'
};

const request = createRequest('POST', toolCallMessage);
const response = await transport.handleRequest(request);

expect(response.status).toBe(200);

const events = parseSSEEvents(await response.text());

expect(events).toMatchObject([
{
method: 'notifications/tools/list_changed'
},
{
jsonrpc: '2.0',
id: 'call-1',
result: {
content: [{ type: 'text', text: 'refreshed' }]
}
}
]);
});
});

describe('HTTPServerTransport - JSON Response Mode', () => {
Expand Down
53 changes: 53 additions & 0 deletions test/integration/test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,59 @@ test('should forward notification options when using elicitation completion noti
);
});

test('should forward notification options for list changed notifications', async () => {
const server = new Server(
{
name: 'test server',
version: '1.0'
},
{
capabilities: {
tools: {},
resources: {},
prompts: {}
}
}
);

const client = new Client({
name: 'test client',
version: '1.0'
});

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);

const notificationSpy = vi.spyOn(server, 'notification');

await server.sendToolListChanged({ relatedRequestId: 11 });
await server.sendResourceListChanged({ relatedRequestId: 12 });
await server.sendPromptListChanged({ relatedRequestId: 13 });

expect(notificationSpy).toHaveBeenNthCalledWith(
1,
{
method: 'notifications/tools/list_changed'
},
expect.objectContaining({ relatedRequestId: 11 })
);
expect(notificationSpy).toHaveBeenNthCalledWith(
2,
{
method: 'notifications/resources/list_changed'
},
expect.objectContaining({ relatedRequestId: 12 })
);
expect(notificationSpy).toHaveBeenNthCalledWith(
3,
{
method: 'notifications/prompts/list_changed'
},
expect.objectContaining({ relatedRequestId: 13 })
);
});

test('should create notifier that emits elicitation completion notification', async () => {
const server = new Server(
{
Expand Down
37 changes: 37 additions & 0 deletions test/integration/test/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,43 @@ describe('Zod v4', () => {
]);
});

test('should forward notification options for list changed helpers', async () => {
const mcpServer = new McpServer(
{
name: 'test server',
version: '1.0'
},
{
capabilities: {
tools: {},
resources: {},
prompts: {}
}
}
);

const client = new Client({
name: 'test client',
version: '1.0'
});

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);

const sendToolListChangedSpy = vi.spyOn(mcpServer.server, 'sendToolListChanged');
const sendResourceListChangedSpy = vi.spyOn(mcpServer.server, 'sendResourceListChanged');
const sendPromptListChangedSpy = vi.spyOn(mcpServer.server, 'sendPromptListChanged');

await expect(mcpServer.sendToolListChanged({ relatedRequestId: 21 })).resolves.toBeUndefined();
await expect(mcpServer.sendResourceListChanged({ relatedRequestId: 22 })).resolves.toBeUndefined();
await expect(mcpServer.sendPromptListChanged({ relatedRequestId: 23 })).resolves.toBeUndefined();

expect(sendToolListChangedSpy).toHaveBeenCalledWith(expect.objectContaining({ relatedRequestId: 21 }));
expect(sendResourceListChangedSpy).toHaveBeenCalledWith(expect.objectContaining({ relatedRequestId: 22 }));
expect(sendPromptListChangedSpy).toHaveBeenCalledWith(expect.objectContaining({ relatedRequestId: 23 }));
});

/***
* Test: ctx.mcpReq.log convenience method
*/
Expand Down
Loading