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
11 changes: 9 additions & 2 deletions packages/server/src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1116,7 +1116,9 @@ export type RegisteredTool = {
/**
* Creates an executor that invokes the handler with the appropriate arguments.
* When `inputSchema` is defined, the handler is called with `(args, ctx)`.
* When `inputSchema` is undefined, the handler is called with just `(ctx)`.
* When `inputSchema` is undefined, single-argument callbacks are called with `(ctx)`.
* Two-argument callbacks are called with `({}, ctx)` so handlers that need both
* values receive the context in the correct argument slot.
*/
function createToolExecutor(
inputSchema: StandardSchemaWithJSON | undefined,
Expand All @@ -1127,7 +1129,12 @@ function createToolExecutor(
return async (args, ctx) => callback(args, ctx);
}

// When no inputSchema, call with just ctx (the handler expects (ctx) signature)
if (handler.length >= 2) {
const callback = handler as ToolCallbackInternal;
return async (_args, ctx) => callback({}, ctx);
}

// When no inputSchema, one-argument handlers expect just ctx.
const callback = handler as (ctx: ServerContext) => CallToolResult | Promise<CallToolResult>;
return async (_args, ctx) => callback(ctx);
}
Expand Down
39 changes: 38 additions & 1 deletion test/integration/test/server/mcp.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Client } from '@modelcontextprotocol/client';
import type { Notification, TextContent } from '@modelcontextprotocol/core';
import type { CallToolResult, Notification, ServerContext, TextContent } from '@modelcontextprotocol/core';
import { getDisplayName, InMemoryTransport, ProtocolErrorCode, UriTemplate, UrlElicitationRequiredError } from '@modelcontextprotocol/core';
import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
Expand Down Expand Up @@ -6282,6 +6282,43 @@ describe('Zod v4', () => {
});
});

test('should pass empty args before context for no-schema two-argument tool callbacks', async () => {
const server = new McpServer({
name: 'test',
version: '1.0.0'
});

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

let receivedArgs: unknown;
let receivedRequestId: unknown;

const twoArgumentHandler = ((args: unknown, ctx: ServerContext): CallToolResult => {
receivedArgs = args;
receivedRequestId = ctx.mcpReq.id;
return {
content: [{ type: 'text' as const, text: 'Success' }]
};
}) as unknown as (ctx: ServerContext) => CallToolResult;

server.registerTool('no-schema-two-arg', {}, twoArgumentHandler);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);

const result = await client.callTool({
name: 'no-schema-two-arg'
});

expect(result.content).toEqual([{ type: 'text', text: 'Success' }]);
expect(receivedArgs).toEqual({});
expect(receivedRequestId).toBeDefined();
});

// SEP-2663: `taskSupport: 'required'` enforcement and the automatic-polling wrapper
// depended on the client sending `params.task`. Under the server-directed model the
// tool handler decides to return `{resultType:'task', task}`; there is no per-tool
Expand Down
Loading