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
10 changes: 10 additions & 0 deletions test/e2e/requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,16 @@ export const REQUIREMENTS: Record<string, Requirement> = {
behavior:
"Log notifications emitted by a tool handler during execution reach the client's logging callback before the tool result returns."
},
'tools:call:omitted-args:all-optional': {
Comment thread
claude[bot] marked this conversation as resolved.
source: 'https://modelcontextprotocol.io/specification/2025-11-25/server/tools#calling-tools',
behavior:
'tools/call that omits the arguments field entirely for a tool whose input schema fields are all optional runs the handler with an empty object and returns its result.'
},
'tools:call:omitted-args:required': {
source: 'https://modelcontextprotocol.io/specification/2025-11-25/server/tools#error-handling',
behavior:
'tools/call that omits the arguments field entirely for a tool with a required argument is answered as a tool execution error (isError: true) whose content names the missing argument, without invoking the handler.'
},
'tools:call:progress': {
source: 'https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress#progress-flow',
behavior: "Progress notifications emitted by a tool handler reach the caller's progress callback before the tool result returns.",
Expand Down
74 changes: 73 additions & 1 deletion test/e2e/scenarios/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ import type {
RequestId,
Tool
} from '@modelcontextprotocol/server';
import { McpServer, ProtocolError, ProtocolErrorCode, Server, UrlElicitationRequiredError } from '@modelcontextprotocol/server';
import {
isJSONRPCRequest,
McpServer,
ProtocolError,
ProtocolErrorCode,
Server,
UrlElicitationRequiredError
} from '@modelcontextprotocol/server';
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv';
import { expect, vi } from 'vitest';
import { z } from 'zod/v4';
Expand Down Expand Up @@ -517,6 +524,71 @@ verifies('tools:call:is-error', async ({ transport }: TestArgs) => {
client.transport!.onmessage = original;
});

verifies('tools:call:omitted-args:all-optional', async ({ transport }: TestArgs) => {
// Shared across factory calls so stateless still observes what the handler received.
const received: unknown[] = [];
const makeServer = () => {
Comment thread
claude[bot] marked this conversation as resolved.
const s = new McpServer({ name: 's', version: '0' });
s.registerTool(
'greet',
{ description: 'Greets by name, or generically.', inputSchema: z.object({ name: z.string().optional() }) },
args => {
received.push(args);
return { content: [{ type: 'text', text: args.name ? `Hello, ${args.name}!` : 'Hello!' }] };
}
);
return s;
};
const client = newClient();
await using _ = await wire(transport, makeServer, client);

// Tap outbound wire messages to prove the request carries no `arguments` key at all (unlike `arguments: {}`).
const outbound: JSONRPCMessage[] = [];
const tx = client.transport!;
const originalSend = tx.send.bind(tx);
tx.send = async (m, opts) => {
outbound.push(m);
return originalSend(m, opts);
};

const result = await client.callTool({ name: 'greet' });

const call = outbound.find(m => isJSONRPCRequest(m) && m.method === 'tools/call');
expect(call).toBeDefined();
if (!call || !isJSONRPCRequest(call)) throw new Error('expected tools/call request');
expect(call.params).not.toHaveProperty('arguments');

expect(result.isError).toBeFalsy();
expect(result.content).toEqual([{ type: 'text', text: 'Hello!' }]);
// The omitted field is validated as an empty object, so the handler sees no keys rather than undefined.
expect(received).toEqual([{}]);

tx.send = originalSend;
});

verifies('tools:call:omitted-args:required', async ({ transport }: TestArgs) => {
// Shared across factory calls so stateless still observes the count.
const handlerCalls = { n: 0 };
const makeServer = () => {
const s = new McpServer({ name: 's', version: '0' });
s.registerTool('summarize', { inputSchema: z.object({ text: z.string() }) }, ({ text }) => {
handlerCalls.n++;
return { content: [{ type: 'text', text: `Summary of: ${text}` }] };
});
return s;
};
const client = newClient();
await using _ = await wire(transport, makeServer, client);

// No `arguments` key at all: an input validation failure is a tool execution error, not a JSON-RPC error.
const result = await client.callTool({ name: 'summarize' });

expect(result.isError).toBe(true);
expect(result.content).toEqual([{ type: 'text', text: expect.stringMatching(/input validation error/i) }]);
expect(result.content).toEqual([{ type: 'text', text: expect.stringContaining('text') }]);
expect(handlerCalls.n).toBe(0);
});

verifies(
'tools:call:unknown-name',
async ({ transport }: TestArgs) => {
Expand Down
Loading