Skip to content

Commit d96c427

Browse files
authored
fix: notifications (#145)
* use mcp request handler extra to send notifications * use mcp handler extra to send notifications, this is needed so the streamable post sse notifications are sent correctly * add tests
1 parent efa9e9c commit d96c427

File tree

5 files changed

+38
-7
lines changed

5 files changed

+38
-7
lines changed

src/actor/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function createExpressApp(
130130
// New initialization request - use JSON response mode
131131
transport = new StreamableHTTPServerTransport({
132132
sessionIdGenerator: () => randomUUID(),
133-
enableJsonResponse: true, // Enable JSON response mode
133+
enableJsonResponse: false, // Use SSE response mode
134134
});
135135
// Load MCP server tools
136136
await loadToolsAndActors(mcpServer, req.url, process.env.APIFY_TOKEN as string);

src/mcp/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,10 @@ export class ActorsMcpServer {
344344
/**
345345
* Handles the request to call a tool.
346346
* @param {object} request - The request object containing tool name and arguments.
347+
* @param {object} extra - Extra data given to the request handler, such as sendNotification function.
347348
* @throws {McpError} - based on the McpServer class code from the typescript MCP SDK
348349
*/
349-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
350+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
350351
// eslint-disable-next-line prefer-const
351352
let { name, arguments: args } = request.params;
352353
const apifyToken = (request.params.apifyToken || process.env.APIFY_TOKEN) as string;
@@ -414,6 +415,7 @@ export class ActorsMcpServer {
414415
const internalTool = tool.tool as HelperTool;
415416
const res = await internalTool.call({
416417
args,
418+
extra,
417419
apifyMcpServer: this,
418420
mcpServer: this.server,
419421
apifyToken,

src/tools/helpers.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const addTool: ToolEntry = {
7878
ajvValidate: ajv.compile(zodToJsonSchema(addToolArgsSchema)),
7979
// TODO: I don't like that we are passing apifyMcpServer and mcpServer to the tool
8080
call: async (toolArgs) => {
81-
const { apifyMcpServer, mcpServer, apifyToken, args } = toolArgs;
81+
const { apifyMcpServer, apifyToken, args, extra: { sendNotification } } = toolArgs;
8282
const parsed = addToolArgsSchema.parse(args);
8383
if (apifyMcpServer.listAllToolNames().includes(parsed.actorName)) {
8484
return {
@@ -90,7 +90,7 @@ export const addTool: ToolEntry = {
9090
}
9191
const tools = await getActorsAsTools([parsed.actorName], apifyToken);
9292
const toolsAdded = apifyMcpServer.upsertTools(tools, true);
93-
await mcpServer.notification({ method: 'notifications/tools/list_changed' });
93+
await sendNotification({ method: 'notifications/tools/list_changed' });
9494

9595
return {
9696
content: [{
@@ -121,13 +121,13 @@ export const removeTool: ToolEntry = {
121121
ajvValidate: ajv.compile(zodToJsonSchema(removeToolArgsSchema)),
122122
// TODO: I don't like that we are passing apifyMcpServer and mcpServer to the tool
123123
call: async (toolArgs) => {
124-
const { apifyMcpServer, mcpServer, args } = toolArgs;
124+
const { apifyMcpServer, args, extra: { sendNotification } } = toolArgs;
125125
const parsed = removeToolArgsSchema.parse(args);
126126
// Check if tool exists before attempting removal
127127
if (!apifyMcpServer.tools.has(parsed.toolName)) {
128128
// Send notification so client can update its tool list
129129
// just in case the client tool list is out of sync
130-
await mcpServer.notification({ method: 'notifications/tools/list_changed' });
130+
await sendNotification({ method: 'notifications/tools/list_changed' });
131131
return {
132132
content: [{
133133
type: 'text',
@@ -136,7 +136,7 @@ export const removeTool: ToolEntry = {
136136
};
137137
}
138138
const removedTools = apifyMcpServer.removeToolsByName([parsed.toolName], true);
139-
await mcpServer.notification({ method: 'notifications/tools/list_changed' });
139+
await sendNotification({ method: 'notifications/tools/list_changed' });
140140
return { content: [{ type: 'text', text: `Tools removed: ${removedTools.join(', ')}` }] };
141141
},
142142
} as InternalTool,

src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2+
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
3+
import type { Notification, Request } from '@modelcontextprotocol/sdk/types.js';
24
import type { ValidateFunction } from 'ajv';
35
import type { ActorDefaultRunOptions, ActorDefinition } from 'apify-client';
46

@@ -80,6 +82,13 @@ export interface ActorTool extends ToolBase {
8082
export type InternalToolArgs = {
8183
/** Arguments passed to the tool */
8284
args: Record<string, unknown>;
85+
/** Extra data given to request handlers.
86+
*
87+
* Can be used to send notifications from the server to the client.
88+
*
89+
* For more details see: https://github.com/modelcontextprotocol/typescript-sdk/blob/f822c1255edcf98c4e73b9bf17a9dd1b03f86716/src/shared/protocol.ts#L102
90+
*/
91+
extra: RequestHandlerExtra<Request, Notification>;
8392
/** Reference to the Apify MCP server instance */
8493
apifyMcpServer: ActorsMcpServer;
8594
/** Reference to the MCP server instance */

tests/integration/suite.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
2+
import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
23
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
34

45
import { defaults, HelperTools } from '../../src/const.js';
@@ -329,5 +330,24 @@ export function createIntegrationTestsSuite(
329330
expect(notificationCount).toBe(1);
330331
await client.close();
331332
});
333+
334+
it('should notify client about tool list changed', async () => {
335+
const client = await createClientFn({ enableAddingActors: true });
336+
337+
// This flag is set to true when a 'notifications/tools/list_changed' notification is received,
338+
// indicating that the tool list has been updated dynamically.
339+
let hasReceivedNotification = false;
340+
client.setNotificationHandler(ToolListChangedNotificationSchema, async (notification) => {
341+
if (notification.method === 'notifications/tools/list_changed') {
342+
hasReceivedNotification = true;
343+
}
344+
});
345+
// Add Actor dynamically
346+
await client.callTool({ name: HelperTools.ACTOR_ADD, arguments: { actorName: ACTOR_PYTHON_EXAMPLE } });
347+
348+
expect(hasReceivedNotification).toBe(true);
349+
350+
await client.close();
351+
});
332352
});
333353
}

0 commit comments

Comments
 (0)