Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/actor/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export function createExpressApp(
// New initialization request - use JSON response mode
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: true, // Enable JSON response mode
enableJsonResponse: false, // Use SSE response mode
});
// Load MCP server tools
await loadToolsAndActors(mcpServer, req.url, process.env.APIFY_TOKEN as string);
Expand Down
4 changes: 3 additions & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,10 @@ export class ActorsMcpServer {
/**
* Handles the request to call a tool.
* @param {object} request - The request object containing tool name and arguments.
* @param {object} extra - Extra data given to the request handler, such as sendNotification function.
* @throws {McpError} - based on the McpServer class code from the typescript MCP SDK
*/
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
// eslint-disable-next-line prefer-const
let { name, arguments: args } = request.params;
const apifyToken = (request.params.apifyToken || process.env.APIFY_TOKEN) as string;
Expand Down Expand Up @@ -412,6 +413,7 @@ export class ActorsMcpServer {
const internalTool = tool.tool as HelperTool;
const res = await internalTool.call({
args,
extra,
apifyMcpServer: this,
mcpServer: this.server,
apifyToken,
Expand Down
10 changes: 5 additions & 5 deletions src/tools/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const addTool: ToolEntry = {
ajvValidate: ajv.compile(zodToJsonSchema(addToolArgsSchema)),
// TODO: I don't like that we are passing apifyMcpServer and mcpServer to the tool
call: async (toolArgs) => {
const { apifyMcpServer, mcpServer, apifyToken, args } = toolArgs;
const { apifyMcpServer, apifyToken, args, extra: { sendNotification } } = toolArgs;
const parsed = addToolArgsSchema.parse(args);
if (apifyMcpServer.listAllToolNames().includes(parsed.actorName)) {
return {
Expand All @@ -90,7 +90,7 @@ export const addTool: ToolEntry = {
}
const tools = await getActorsAsTools([parsed.actorName], apifyToken);
const toolsAdded = apifyMcpServer.upsertTools(tools, true);
await mcpServer.notification({ method: 'notifications/tools/list_changed' });
await sendNotification({ method: 'notifications/tools/list_changed' });

return {
content: [{
Expand Down Expand Up @@ -121,13 +121,13 @@ export const removeTool: ToolEntry = {
ajvValidate: ajv.compile(zodToJsonSchema(removeToolArgsSchema)),
// TODO: I don't like that we are passing apifyMcpServer and mcpServer to the tool
call: async (toolArgs) => {
const { apifyMcpServer, mcpServer, args } = toolArgs;
const { apifyMcpServer, args, extra: { sendNotification } } = toolArgs;
const parsed = removeToolArgsSchema.parse(args);
// Check if tool exists before attempting removal
if (!apifyMcpServer.tools.has(parsed.toolName)) {
// Send notification so client can update its tool list
// just in case the client tool list is out of sync
await mcpServer.notification({ method: 'notifications/tools/list_changed' });
await sendNotification({ method: 'notifications/tools/list_changed' });
return {
content: [{
type: 'text',
Expand All @@ -136,7 +136,7 @@ export const removeTool: ToolEntry = {
};
}
const removedTools = apifyMcpServer.removeToolsByName([parsed.toolName], true);
await mcpServer.notification({ method: 'notifications/tools/list_changed' });
await sendNotification({ method: 'notifications/tools/list_changed' });
return { content: [{ type: 'text', text: `Tools removed: ${removedTools.join(', ')}` }] };
},
} as InternalTool,
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
import type { Notification, Request } from '@modelcontextprotocol/sdk/types.js';
import type { ValidateFunction } from 'ajv';
import type { ActorDefaultRunOptions, ActorDefinition } from 'apify-client';

Expand Down Expand Up @@ -80,6 +82,13 @@ export interface ActorTool extends ToolBase {
export type InternalToolArgs = {
/** Arguments passed to the tool */
args: Record<string, unknown>;
/** Extra data given to request handlers.
*
* Can be used to send notifications from the server to the client.
*
* For more details see: https://github.com/modelcontextprotocol/typescript-sdk/blob/f822c1255edcf98c4e73b9bf17a9dd1b03f86716/src/shared/protocol.ts#L102
*/
extra: RequestHandlerExtra<Request, Notification>;
/** Reference to the Apify MCP server instance */
apifyMcpServer: ActorsMcpServer;
/** Reference to the MCP server instance */
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/suite.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';

import { defaults, HelperTools } from '../../src/const.js';
Expand Down Expand Up @@ -329,5 +330,24 @@ export function createIntegrationTestsSuite(
expect(notificationCount).toBe(1);
await client.close();
});

it('should notify client about tool list changed', async () => {
const client = await createClientFn({ enableAddingActors: true });

// This flag is set to true when a 'notifications/tools/list_changed' notification is received,
// indicating that the tool list has been updated dynamically.
let hasReceivedNotification = false;
client.setNotificationHandler(ToolListChangedNotificationSchema, async (notification) => {
if (notification.method === 'notifications/tools/list_changed') {
hasReceivedNotification = true;
}
});
// Add Actor dynamically
await client.callTool({ name: HelperTools.ACTOR_ADD, arguments: { actorName: ACTOR_PYTHON_EXAMPLE } });

expect(hasReceivedNotification).toBe(true);

await client.close();
});
});
}