From f6b04167ec6de46ce211e557400a94e8b3894b1e Mon Sep 17 00:00:00 2001 From: pragnyanramtha Date: Sat, 16 May 2026 06:55:27 +0000 Subject: [PATCH 1/2] fix omitted prompt arguments --- .changeset/fix-omitted-prompt-arguments.md | 5 ++ packages/server/src/server/mcp.ts | 2 +- .../test/server/cloudflareWorkers.test.ts | 61 ++++++++++++------- test/integration/test/server/mcp.test.ts | 53 ++++++++++++++++ 4 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 .changeset/fix-omitted-prompt-arguments.md diff --git a/.changeset/fix-omitted-prompt-arguments.md b/.changeset/fix-omitted-prompt-arguments.md new file mode 100644 index 0000000000..7084fb1a4a --- /dev/null +++ b/.changeset/fix-omitted-prompt-arguments.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/server": patch +--- + +Fix prompts/get rejecting omitted arguments when all prompt schema fields are optional. diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index fb45fd5db6..d25cfd0952 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -1339,7 +1339,7 @@ function createPromptHandler( const typedCallback = callback as (args: unknown, ctx: ServerContext) => GetPromptResult | Promise; return async (args, ctx) => { - const parseResult = await validateStandardSchema(argsSchema, args); + const parseResult = await validateStandardSchema(argsSchema, args ?? {}); if (!parseResult.success) { throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid arguments for prompt ${name}: ${parseResult.error}`); } diff --git a/test/integration/test/server/cloudflareWorkers.test.ts b/test/integration/test/server/cloudflareWorkers.test.ts index 9c2d73a40e..dc46d27443 100644 --- a/test/integration/test/server/cloudflareWorkers.test.ts +++ b/test/integration/test/server/cloudflareWorkers.test.ts @@ -8,25 +8,47 @@ import type { ChildProcess } from 'node:child_process'; import { execSync, spawn } from 'node:child_process'; import * as fs from 'node:fs'; +import { createServer } from 'node:net'; import * as os from 'node:os'; import path from 'node:path'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -const PORT = 8787; - interface TestEnv { tempDir: string; + port: number; process: ChildProcess; cleanup: () => Promise; } +async function getAvailablePort(): Promise { + return await new Promise((resolve, reject) => { + const server = createServer(); + server.on('error', reject); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + server.close(() => { + if (address && typeof address === 'object') { + resolve(address.port); + } else { + reject(new Error('Unable to allocate an available port')); + } + }); + }); + }); +} + +async function delay(ms: number): Promise { + await new Promise(resolve => setTimeout(resolve, ms)); +} + describe('Cloudflare Workers compatibility (no nodejs_compat)', () => { let env: TestEnv | null = null; beforeAll(async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cf-worker-test-')); + const port = await getAvailablePort(); // Pack server package const serverPkgPath = path.resolve(__dirname, '../../../../packages/server'); @@ -82,10 +104,10 @@ export default { fs.writeFileSync(path.join(tempDir, 'server.ts'), serverSource); // Install dependencies - execSync('npm install', { cwd: tempDir, stdio: 'pipe', timeout: 60_000 }); + execSync('npm install --prefer-offline --no-audit --no-fund', { cwd: tempDir, stdio: 'pipe', timeout: 180_000 }); // Start wrangler dev server - const proc = spawn('npx', ['wrangler', 'dev', '--local', '--port', String(PORT)], { + const proc = spawn('npx', ['wrangler', 'dev', '--local', '--port', String(port)], { cwd: tempDir, shell: true, stdio: 'pipe' @@ -140,8 +162,8 @@ export default { } }; - env = { tempDir, process: proc, cleanup }; - }, 120_000); + env = { tempDir, port, process: proc, cleanup }; + }, 240_000); afterAll(async () => { await env?.cleanup(); @@ -150,28 +172,25 @@ export default { it('should handle MCP requests', async () => { expect(env).not.toBeNull(); - // Retry connection — wrangler may report "Ready" before it can handle requests - let client!: Client; + // Retry the full round trip — wrangler may report "Ready" before it can + // consistently serve the first few requests. let lastError: unknown; - for (let attempt = 0; attempt < 5; attempt++) { + for (let attempt = 0; attempt < 8; attempt++) { + const client = new Client({ name: 'test-client', version: '1.0.0' }); + const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${env!.port}/`)); try { - client = new Client({ name: 'test-client', version: '1.0.0' }); - const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${PORT}/`)); await client.connect(transport); - lastError = undefined; - break; + const result = await client.callTool({ name: 'greet', arguments: { name: 'World' } }); + expect(result.content).toEqual([{ type: 'text', text: 'Hello, World!' }]); + await client.close(); + return; } catch (error) { lastError = error; - await new Promise(resolve => setTimeout(resolve, 1000)); + await client.close().catch(() => {}); + await delay(1000); } } - if (lastError) { - throw lastError; - } - - const result = await client.callTool({ name: 'greet', arguments: { name: 'World' } }); - expect(result.content).toEqual([{ type: 'text', text: 'Hello, World!' }]); - await client.close(); + throw lastError; }, 30_000); }); diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 92af09744c..7d62dbd6b6 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -4254,6 +4254,59 @@ describe('Zod v4', () => { ]); }); + test('should accept omitted prompt arguments when all schema fields are optional', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.registerPrompt( + 'echo', + { + argsSchema: z.object({ + context: z.string().optional() + }) + }, + ({ context }) => ({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `context: ${context ?? 'none'}` + } + } + ] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.request({ + method: 'prompts/get', + params: { + name: 'echo' + } + }); + + expect(result.messages).toEqual([ + { + role: 'user', + content: { + type: 'text', + text: 'context: none' + } + } + ]); + }); + /*** * Test: Prompt Registration with _meta field */ From 03db793d071b9b4d60c7ccf9b49ed5523f0c429c Mon Sep 17 00:00:00 2001 From: pragnyanramtha Date: Sun, 17 May 2026 10:23:18 +0000 Subject: [PATCH 2/2] test: drop unrelated worker test changes --- .changeset/fix-omitted-prompt-arguments.md | 2 +- .../test/server/cloudflareWorkers.test.ts | 61 +++++++------------ 2 files changed, 22 insertions(+), 41 deletions(-) diff --git a/.changeset/fix-omitted-prompt-arguments.md b/.changeset/fix-omitted-prompt-arguments.md index 7084fb1a4a..5e73e22090 100644 --- a/.changeset/fix-omitted-prompt-arguments.md +++ b/.changeset/fix-omitted-prompt-arguments.md @@ -1,5 +1,5 @@ --- -"@modelcontextprotocol/server": patch +'@modelcontextprotocol/server': patch --- Fix prompts/get rejecting omitted arguments when all prompt schema fields are optional. diff --git a/test/integration/test/server/cloudflareWorkers.test.ts b/test/integration/test/server/cloudflareWorkers.test.ts index dc46d27443..9c2d73a40e 100644 --- a/test/integration/test/server/cloudflareWorkers.test.ts +++ b/test/integration/test/server/cloudflareWorkers.test.ts @@ -8,47 +8,25 @@ import type { ChildProcess } from 'node:child_process'; import { execSync, spawn } from 'node:child_process'; import * as fs from 'node:fs'; -import { createServer } from 'node:net'; import * as os from 'node:os'; import path from 'node:path'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +const PORT = 8787; + interface TestEnv { tempDir: string; - port: number; process: ChildProcess; cleanup: () => Promise; } -async function getAvailablePort(): Promise { - return await new Promise((resolve, reject) => { - const server = createServer(); - server.on('error', reject); - server.listen(0, '127.0.0.1', () => { - const address = server.address(); - server.close(() => { - if (address && typeof address === 'object') { - resolve(address.port); - } else { - reject(new Error('Unable to allocate an available port')); - } - }); - }); - }); -} - -async function delay(ms: number): Promise { - await new Promise(resolve => setTimeout(resolve, ms)); -} - describe('Cloudflare Workers compatibility (no nodejs_compat)', () => { let env: TestEnv | null = null; beforeAll(async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cf-worker-test-')); - const port = await getAvailablePort(); // Pack server package const serverPkgPath = path.resolve(__dirname, '../../../../packages/server'); @@ -104,10 +82,10 @@ export default { fs.writeFileSync(path.join(tempDir, 'server.ts'), serverSource); // Install dependencies - execSync('npm install --prefer-offline --no-audit --no-fund', { cwd: tempDir, stdio: 'pipe', timeout: 180_000 }); + execSync('npm install', { cwd: tempDir, stdio: 'pipe', timeout: 60_000 }); // Start wrangler dev server - const proc = spawn('npx', ['wrangler', 'dev', '--local', '--port', String(port)], { + const proc = spawn('npx', ['wrangler', 'dev', '--local', '--port', String(PORT)], { cwd: tempDir, shell: true, stdio: 'pipe' @@ -162,8 +140,8 @@ export default { } }; - env = { tempDir, port, process: proc, cleanup }; - }, 240_000); + env = { tempDir, process: proc, cleanup }; + }, 120_000); afterAll(async () => { await env?.cleanup(); @@ -172,25 +150,28 @@ export default { it('should handle MCP requests', async () => { expect(env).not.toBeNull(); - // Retry the full round trip — wrangler may report "Ready" before it can - // consistently serve the first few requests. + // Retry connection — wrangler may report "Ready" before it can handle requests + let client!: Client; let lastError: unknown; - for (let attempt = 0; attempt < 8; attempt++) { - const client = new Client({ name: 'test-client', version: '1.0.0' }); - const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${env!.port}/`)); + for (let attempt = 0; attempt < 5; attempt++) { try { + client = new Client({ name: 'test-client', version: '1.0.0' }); + const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${PORT}/`)); await client.connect(transport); - const result = await client.callTool({ name: 'greet', arguments: { name: 'World' } }); - expect(result.content).toEqual([{ type: 'text', text: 'Hello, World!' }]); - await client.close(); - return; + lastError = undefined; + break; } catch (error) { lastError = error; - await client.close().catch(() => {}); - await delay(1000); + await new Promise(resolve => setTimeout(resolve, 1000)); } } + if (lastError) { + throw lastError; + } + + const result = await client.callTool({ name: 'greet', arguments: { name: 'World' } }); + expect(result.content).toEqual([{ type: 'text', text: 'Hello, World!' }]); - throw lastError; + await client.close(); }, 30_000); });