-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
ElicitResultSchema violates MCP Specs
ElicitResultSchema
validation fails when content: null
is used for cancel/decline responses, even though this is a reasonable developer pattern and the MCP spec allows flexibility. The MCP spec says content is "typically omitted" (not forbidden) for cancel/decline actions, but the SDK rejects content: null
.
To Reproduce
Steps to reproduce the behavior:
- Install the SDK:
npm install @modelcontextprotocol/sdk@1.17.5
- Run this code:
import { ElicitResultSchema } from "@modelcontextprotocol/sdk/types.js";
ElicitResultSchema.parse({ action: "cancel", content: null });
// Error: Expected object, received null
Expected behavior
The schema should accept content: null
for cancel/decline responses. According to the MCP Elicitation Specification:
Decline (
action: "decline"
): User explicitly declined the request
- The content field is typically omitted
Cancel (
action: "cancel"
): User dismissed without making an explicit choice
- The content field is typically omitted
The spec uses "typically omitted" (not "must be omitted"), allowing flexibility for different developer patterns like content: null
.
Evidence that the current schema is inconsistent:
- This works:
ElicitResultSchema.parse({ action: "cancel" })
(omitting content) - This works:
ElicitResultSchema.parse({ action: "cancel", content: {} })
(empty object) - This fails:
ElicitResultSchema.parse({ action: "cancel", content: null })
(explicit null)
Logs
Error: [
{
"code": "invalid_type",
"expected": "object",
"received": "null",
"path": ["content"],
"message": "Expected object, received null"
}
]
Additional context
This breaks real elicitation workflows where clients use content: null
as a sensible default:
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
return { action: "cancel", content: null }; // Fails validation
});
Full reproduction with server/client setup:
Server code:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{ name: "test-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// Tool that uses elicitInput - this is where the validation failure occurs
server.setRequestHandler({ method: "tools/call" }, async (request) => {
if (request.params.name === "test-tool") {
const result = await server.elicitInput({
message: "Choose an option",
requestedSchema: { type: "object", properties: { choice: { type: "string" } } }
});
// The validation error happens when processing result from client
return { content: [{ type: "text", text: `Action: ${result.action}` }] };
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
Client code:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";
const client = new Client(
{ name: "test-client", version: "1.0.0" },
{ capabilities: { elicitation: {} } }
);
// This handler causes the validation error
client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
return {
action: "cancel",
content: null // This breaks SDK validation
};
});
const transport = new StdioClientTransport({
command: "node", args: ["server.js"]
});
await client.connect(transport);
Proposed fix:
// only validate content for accept actions
z.union([
// Accept: content required
ResultSchema.extend({
action: z.literal("accept"),
content: z.record(z.string(), z.unknown())
}),
// Cancel/decline: content optional and flexible
ResultSchema.extend({
action: z.enum(["decline", "cancel"]),
content: z.optional(z.any())
})
])
Environment: @modelcontextprotocol/sdk@1.17.5
, Node v22.17.0, macOS Darwin 24.6.0