/** * Tools test scenarios for MCP servers */ import { ClientScenario, ConformanceCheck, DRAFT_PROTOCOL_VERSION, specVersionAtLeast, type SpecVersion } from '../../types'; import type { RunContext } from '../../connection'; import type { ListToolsResult, CallToolResult } from '../../spec-types/2025-06-18'; import { connectToServer, NotificationCollector } from '../../connection/sdk-client'; import { CreateMessageRequestSchema, ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js'; /** * Tool name rules per the 2025-11-25 spec prose (#tool-names): 1–128 chars of * [A-Za-z0-9_.-]. (The SEP-986 markdown still shows the older 64-char / `/` * rules; the published spec is authoritative.) */ const TOOL_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/; const TOOL_NAME_MAX_LENGTH = 128; const TOOLS_NAME_FORMAT_SPEC_REFS = [ { id: 'MCP-Tool-Names', url: 'https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names' }, { id: 'MCP-Tool-Names-Draft', url: 'https://modelcontextprotocol.io/specification/draft/server/tools#tool-names' }, // Background only; the dated spec above is the rule source. { id: 'SEP-986-History', url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986' }, { id: 'SEP-986-Spec-Integration', url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1603' } ]; /** The Tool Names rules first appear in the 2025-11-25 revision. */ export function toolNameFormatCheckApplies(specVersion: SpecVersion): boolean { return specVersionAtLeast(specVersion, '2025-11-25'); } export function validateToolNameFormat(name: string): string | null { if (name.length < 1 || name.length > TOOL_NAME_MAX_LENGTH) { return `length ${name.length} is outside 1-${TOOL_NAME_MAX_LENGTH}`; } if (!TOOL_NAME_PATTERN.test(name)) { return 'contains characters outside [A-Za-z0-9_.-]'; } return null; } export function buildToolsNameFormatCheck( tools: ReadonlyArray<{ name?: unknown }> | undefined ): ConformanceCheck { const timestamp = new Date().toISOString(); const baseCheck = { id: 'tools-name-format', name: 'ToolsNameFormat', description: 'Tool names SHOULD be 1-128 characters and match ^[A-Za-z0-9_.-]+$', specReferences: TOOLS_NAME_FORMAT_SPEC_REFS, timestamp }; if (!Array.isArray(tools) || tools.length === 0) { return { ...baseCheck, status: 'INFO', errorMessage: 'No tools advertised; nothing to validate', details: { toolCount: 0 } }; } const toolResults: Record = {}; const violations: string[] = []; tools.forEach((tool, index) => { const name = typeof tool.name === 'string' ? tool.name : ''; const key = name || ``; const reason = typeof tool.name === 'string' ? validateToolNameFormat(tool.name) : 'name is not a string'; if (reason) { toolResults[key] = `invalid: ${reason}`; violations.push(`${key}: ${reason}`); } else { toolResults[key] = 'valid'; } }); return { ...baseCheck, status: violations.length === 0 ? 'SUCCESS' : 'WARNING', errorMessage: violations.length > 0 ? `${violations.length} tool name(s) violate spec Tool Names SHOULD rules: ${violations.join('; ')}` : undefined, details: { toolCount: tools.length, results: toolResults } }; } export class ToolsListScenario implements ClientScenario { name = 'tools-list'; readonly source = { introducedIn: '2025-06-18' } as const; description = `Test listing available tools. **Server Implementation Requirements:** **Endpoint**: \`tools/list\` **Requirements**: - Return array of all available tools - Each tool MUST have: - \`name\` (string) - \`description\` (string) - \`inputSchema\` (valid JSON Schema object) - From 2025-11-25 onward, advertised \`name\` values SHOULD follow the spec Tool Names rules (1–128 chars, \`[A-Za-z0-9_.-]\` only) — see \`tools-name-format\` check`; async run(ctx: RunContext): Promise { const checks: ConformanceCheck[] = []; try { const conn = await ctx.connect(); const result = await conn.request('tools/list'); // Validate response structure const errors: string[] = []; if (!result.tools) { errors.push('Missing tools array'); } else { if (!Array.isArray(result.tools)) { errors.push('tools is not an array'); } result.tools.forEach((tool, index) => { if (!tool.name) errors.push(`Tool ${index}: missing name`); if (!tool.description) errors.push(`Tool ${index}: missing description`); if (!tool.inputSchema) errors.push(`Tool ${index}: missing inputSchema`); }); } checks.push({ id: 'tools-list', name: 'ToolsList', description: 'Server lists available tools with valid structure', status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: [ { id: 'MCP-Tools-List', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#listing-tools' } ], details: { toolCount: result.tools?.length || 0, tools: result.tools?.map((t) => t.name) } }); if (toolNameFormatCheckApplies(ctx.specVersion)) { checks.push(buildToolsNameFormatCheck(result.tools)); } await conn.close(); } catch (error) { checks.push({ id: 'tools-list', name: 'ToolsList', description: 'Server lists available tools with valid structure', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Tools-List', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#listing-tools' } ] }); } return checks; } } export class ToolsCallSimpleTextScenario implements ClientScenario { name = 'tools-call-simple-text'; readonly source = { introducedIn: '2025-06-18' } as const; description = `Test calling a tool that returns simple text. **Server Implementation Requirements:** Implement tool \`test_simple_text\` with no arguments that returns: \`\`\`json { "content": [ { "type": "text", "text": "This is a simple text response for testing." } ] } \`\`\``; async run(ctx: RunContext): Promise { const checks: ConformanceCheck[] = []; try { const conn = await ctx.connect(); const result = await conn.request('tools/call', { name: 'test_simple_text' /* omit arguments as it is not required in the schema */ }); // Validate response const errors: string[] = []; const content = (result as any).content; if (!content) errors.push('Missing content array'); if (!Array.isArray(content)) errors.push('content is not an array'); if (content && content.length === 0) errors.push('content array is empty'); const textContent = content && content.find((c: any) => c.type === 'text'); if (!textContent) errors.push('No text content found'); if (textContent && !textContent.text) errors.push('Text content missing text field'); checks.push({ id: 'tools-call-simple-text', name: 'ToolsCallSimpleText', description: 'Tool returns simple text content', status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: [ { id: 'MCP-Tools-Call', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools' } ], details: { result } }); await conn.close(); } catch (error) { checks.push({ id: 'tools-call-simple-text', name: 'ToolsCallSimpleText', description: 'Tool returns simple text content', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Tools-Call', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools' } ] }); } return checks; } } export class ToolsCallImageScenario implements ClientScenario { name = 'tools-call-image'; readonly source = { introducedIn: '2025-06-18' } as const; description = `Test calling a tool that returns image content. **Server Implementation Requirements:** Implement tool \`test_image_content\` with no arguments that returns: \`\`\`json { "content": [ { "type": "image", "data": "", "mimeType": "image/png" } ] } \`\`\` **Implementation Note**: Use a minimal test image (e.g., 1x1 red pixel PNG)`; async run(ctx: RunContext): Promise { const checks: ConformanceCheck[] = []; try { const conn = await ctx.connect(); const result = await conn.request('tools/call', { name: 'test_image_content', arguments: {} }); // Validate response const errors: string[] = []; const content = (result as any).content; if (!content) errors.push('Missing content array'); const imageContent = content && content.find((c: any) => c.type === 'image'); if (!imageContent) errors.push('No image content found'); if (imageContent && !imageContent.data) errors.push('Image content missing data field'); if (imageContent && !imageContent.mimeType) errors.push('Image content missing mimeType'); checks.push({ id: 'tools-call-image', name: 'ToolsCallImage', description: 'Tool returns image content', status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: [ { id: 'MCP-Tools-Call', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools' } ], details: { mimeType: imageContent?.mimeType, hasData: !!imageContent?.data } }); await conn.close(); } catch (error) { checks.push({ id: 'tools-call-image', name: 'ToolsCallImage', description: 'Tool returns image content', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Tools-Call', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools' } ] }); } return checks; } } export class ToolsCallMultipleContentTypesScenario implements ClientScenario { name = 'tools-call-mixed-content'; readonly source = { introducedIn: '2025-06-18' } as const; description = `Test tool returning multiple content types. **Server Implementation Requirements:** Implement tool \`test_multiple_content_types\` with no arguments that returns: \`\`\`json { "content": [ { "type": "text", "text": "Multiple content types test:" }, { "type": "image", "data": "", "mimeType": "image/png" }, { "type": "resource", "resource": { "uri": "test://mixed-content-resource", "mimeType": "application/json", "text": "{"test":"data","value":123}" } } ] } \`\`\``; async run(ctx: RunContext): Promise { const checks: ConformanceCheck[] = []; try { const conn = await ctx.connect(); const result = await conn.request('tools/call', { name: 'test_multiple_content_types', arguments: {} }); // Validate response const errors: string[] = []; const content = (result as any).content; if (!content) errors.push('Missing content array'); if (content && content.length < 2) errors.push('Expected multiple content items'); const hasText = content && content.some((c: any) => c.type === 'text'); const hasImage = content && content.some((c: any) => c.type === 'image'); const hasResource = content && content.some((c: any) => c.type === 'resource'); if (!hasText) errors.push('Missing text content'); if (!hasImage) errors.push('Missing image content'); if (!hasResource) errors.push('Missing resource content'); checks.push({ id: 'tools-call-mixed-content', name: 'ToolsCallMixedContent', description: 'Tool returns multiple content types', status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: [ { id: 'MCP-Tools-Call', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools' } ], details: { contentCount: content ? content.length : 0, contentTypes: content ? content.map((c: any) => c.type) : [] } }); await conn.close(); } catch (error) { checks.push({ id: 'tools-call-mixed-content', name: 'ToolsCallMixedContent', description: 'Tool returns multiple content types', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Tools-Call', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools' } ] }); } return checks; } } export class ToolsCallWithLoggingScenario implements ClientScenario { name = 'tools-call-with-logging'; readonly source = { introducedIn: '2025-06-18', removedIn: DRAFT_PROTOCOL_VERSION } as const; description = `Test tool that sends log messages during execution. **Server Implementation Requirements:** Implement tool \`test_tool_with_logging\` with no arguments. **Behavior**: During execution, send 3 log notifications at info level: 1. "Tool execution started" 2. "Tool processing data" (after ~50ms delay) 3. "Tool execution completed" (after another ~50ms delay) **Returns**: Text content confirming execution **Implementation Note**: The delays are important to test that clients can receive multiple log notifications during tool execution`; async run(ctx: RunContext): Promise { const { serverUrl } = ctx; const checks: ConformanceCheck[] = []; try { const connection = await connectToServer(serverUrl, {}, ctx.specVersion); const notifications = new NotificationCollector(connection.client); // Set logging level to debug await connection.client.setLoggingLevel('debug'); await connection.client.callTool({ name: 'test_tool_with_logging', arguments: {} }); // Wait a bit for notifications to arrive await new Promise((resolve) => setTimeout(resolve, 200)); // Check for log notifications from the NotificationCollector const logNotifications = notifications.getNotifications(); const errors: string[] = []; if (logNotifications.length === 0) { errors.push('No log notifications received'); } else if (logNotifications.length < 3) { errors.push( `Expected at least 3 log messages, got ${logNotifications.length}` ); } checks.push({ id: 'tools-call-with-logging', name: 'ToolsCallWithLogging', description: 'Tool sends log messages during execution', status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: [ { id: 'MCP-Logging', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging' } ], details: { logCount: logNotifications.length, logs: logNotifications.map((n: any) => n.params) } }); await connection.close(); } catch (error) { checks.push({ id: 'tools-call-with-logging', name: 'ToolsCallWithLogging', description: 'Tool sends log messages during execution', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Logging', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging' } ] }); } return checks; } } export class ToolsCallErrorScenario implements ClientScenario { name = 'tools-call-error'; readonly source = { introducedIn: '2025-06-18' } as const; description = `Test tool error reporting. **Server Implementation Requirements:** Implement tool \`test_error_handling\` with no arguments. **Behavior**: Always throw an error **Returns**: JSON-RPC response with \`isError: true\` \`\`\`json { "isError": true, "content": [ { "type": "text", "text": "This tool intentionally returns an error for testing" } ] } \`\`\``; async run(ctx: RunContext): Promise { const checks: ConformanceCheck[] = []; try { const conn = await ctx.connect(); const result: any = await conn.request('tools/call', { name: 'test_error_handling', arguments: {} }); // Check if result has isError flag const hasIsError = result.isError === true; const hasContent = result.content && result.content.length > 0; const hasErrorMessage = hasContent && result.content[0].text; checks.push({ id: 'tools-call-error', name: 'ToolsCallError', description: 'Tool returns error correctly', status: hasIsError && hasErrorMessage ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: !hasIsError ? 'Tool did not return isError: true' : !hasErrorMessage ? 'Error result missing error message' : undefined, specReferences: [ { id: 'MCP-Error-Handling', url: 'https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle' } ], details: { result } }); await conn.close(); } catch (error) { checks.push({ id: 'tools-call-error', name: 'ToolsCallError', description: 'Tool returns error correctly', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Error-Handling', url: 'https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle' } ] }); } return checks; } } export class ToolsCallWithProgressScenario implements ClientScenario { name = 'tools-call-with-progress'; readonly source = { introducedIn: '2025-06-18' } as const; description = `Test tool that reports progress notifications. **Server Implementation Requirements:** Implement tool \`test_tool_with_progress\` with no arguments. **Behavior**: If \`_meta.progressToken\` is provided in request: - Send progress notification: \`0/100\` - Wait ~50ms - Send progress notification: \`50/100\` - Wait ~50ms - Send progress notification: \`100/100\` If no progress token provided, just execute with delays. **Returns**: Text content confirming execution **Progress Notification Format**: \`\`\`json { "method": "notifications/progress", "params": { "progressToken": "", "progress": 50, "total": 100 } } \`\`\``; async run(ctx: RunContext): Promise { const checks: ConformanceCheck[] = []; try { const conn = await ctx.connect(); const result = await conn.request('tools/call', { name: 'test_tool_with_progress', arguments: {}, _meta: { progressToken: 'progress-test-1' } }); const progressUpdates = conn.notifications .filter((n) => n.method === 'notifications/progress') .map( (n) => n.params as { progressToken?: string | number; progress: number; total?: number; } ) // The server must echo the request's progressToken back; ignore // notifications for other tokens. .filter((p) => p.progressToken === 'progress-test-1'); const errors: string[] = []; if (progressUpdates.length === 0) { errors.push('No progress notifications received'); } else if (progressUpdates.length < 3) { errors.push( `Expected at least 3 progress notifications, got ${progressUpdates.length}` ); } if (progressUpdates.length >= 3) { const progress0 = progressUpdates[0].progress; const progress1 = progressUpdates[1].progress; const progress2 = progressUpdates[2].progress; if (!(progress0 <= progress1 && progress1 <= progress2)) { errors.push('Progress values should be increasing'); } } checks.push({ id: 'tools-call-with-progress', name: 'ToolsCallWithProgress', description: 'Tool reports progress notifications', status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: [ { id: 'MCP-Progress', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/progress' } ], details: { progressCount: progressUpdates.length, progressNotifications: progressUpdates, result } }); await conn.close(); } catch (error) { checks.push({ id: 'tools-call-with-progress', name: 'ToolsCallWithProgress', description: 'Tool reports progress notifications', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Progress', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/progress' } ] }); } return checks; } } export class ToolsCallSamplingScenario implements ClientScenario { name = 'tools-call-sampling'; readonly source = { introducedIn: '2025-06-18', removedIn: DRAFT_PROTOCOL_VERSION } as const; description = `Test tool that requests LLM sampling from client. **Server Implementation Requirements:** Implement tool \`test_sampling\` with argument: - \`prompt\` (string, required) - The prompt to send to the LLM **Behavior**: Request LLM sampling from the client using \`sampling/createMessage\` **Sampling Request**: \`\`\`json { "method": "sampling/createMessage", "params": { "messages": [ { "role": "user", "content": { "type": "text", "text": "" } } ], "maxTokens": 100 } } \`\`\` **Returns**: Text content with the LLM's response \`\`\`json { "content": [ { "type": "text", "text": "LLM response: " } ] } \`\`\` **Implementation Note**: If the client doesn't support sampling (no \`sampling\` capability), return an error.`; async run(ctx: RunContext): Promise { const { serverUrl } = ctx; const checks: ConformanceCheck[] = []; try { const connection = await connectToServer(serverUrl, {}, ctx.specVersion); let samplingRequested = false; connection.client.setRequestHandler( CreateMessageRequestSchema, async (_request) => { samplingRequested = true; return { role: 'assistant', content: { type: 'text', text: 'This is a test response from the client' }, model: 'test-model', stopReason: 'endTurn' }; } ); const result = await connection.client.callTool({ name: 'test_sampling', arguments: { prompt: 'Test prompt for sampling' } }); const errors: string[] = []; if (!samplingRequested) { errors.push('Server did not request sampling from client'); } const content = (result as any).content; if (!content || content.length === 0) { errors.push('Tool did not return content'); } checks.push({ id: 'tools-call-sampling', name: 'ToolsCallSampling', description: 'Tool requests LLM sampling from client', status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: [ { id: 'MCP-Sampling', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/sampling' } ], details: { samplingRequested, result } }); await connection.close(); } catch (error) { checks.push({ id: 'tools-call-sampling', name: 'ToolsCallSampling', description: 'Tool requests LLM sampling from client', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Sampling', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/sampling' } ] }); } return checks; } } export class ToolsCallElicitationScenario implements ClientScenario { name = 'tools-call-elicitation'; readonly source = { introducedIn: '2025-06-18', removedIn: DRAFT_PROTOCOL_VERSION } as const; description = `Test tool that requests user input (elicitation) from client. **Server Implementation Requirements:** Implement tool \`test_elicitation\` with argument: - \`message\` (string, required) - The message to show the user **Behavior**: Request user input from the client using \`elicitation/create\` **Elicitation Request**: \`\`\`json { "method": "elicitation/create", "params": { "message": "", "requestedSchema": { "type": "object", "properties": { "username": { "type": "string", "description": "User's response" }, "email": { "type": "string", "description": "User's email address" } }, "required": ["username", "email"] } } } \`\`\` **Returns**: Text content with the user's response \`\`\`json { "content": [ { "type": "text", "text": "User response: " } ] } \`\`\` **Implementation Note**: If the client doesn't support elicitation (no \`elicitation\` capability), return an error.`; async run(ctx: RunContext): Promise { const { serverUrl } = ctx; const checks: ConformanceCheck[] = []; try { const connection = await connectToServer(serverUrl, {}, ctx.specVersion); let elicitationRequested = false; connection.client.setRequestHandler( ElicitRequestSchema, async (_request) => { elicitationRequested = true; return { action: 'accept', content: { username: 'testuser', email: 'test@example.com' } }; } ); const result = await connection.client.callTool({ name: 'test_elicitation', arguments: { message: 'Please provide your information' } }); const errors: string[] = []; if (!elicitationRequested) { errors.push('Server did not request elicitation from client'); } const content = (result as any).content; if (!content || content.length === 0) { errors.push('Tool did not return content'); } checks.push({ id: 'tools-call-elicitation', name: 'ToolsCallElicitation', description: 'Tool requests user input from client', status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: [ { id: 'MCP-Elicitation', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/elicitation' } ], details: { elicitationRequested, result } }); await connection.close(); } catch (error) { checks.push({ id: 'tools-call-elicitation', name: 'ToolsCallElicitation', description: 'Tool requests user input from client', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Elicitation', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/elicitation' } ] }); } return checks; } } export class ToolsCallAudioScenario implements ClientScenario { name = 'tools-call-audio'; readonly source = { introducedIn: '2025-06-18' } as const; description = `Test calling a tool that returns audio content. **Server Implementation Requirements:** Implement tool \`test_audio_content\` with no arguments that returns: \`\`\`json { "content": [ { "type": "audio", "data": "", "mimeType": "audio/wav" } ] } \`\`\` **Implementation Note**: Use a minimal test audio file`; async run(ctx: RunContext): Promise { const checks: ConformanceCheck[] = []; try { const conn = await ctx.connect(); const result = await conn.request('tools/call', { name: 'test_audio_content', arguments: {} }); // Validate response const errors: string[] = []; const content = (result as any).content; if (!content) errors.push('Missing content array'); if (!Array.isArray(content)) errors.push('content is not an array'); if (content && content.length === 0) errors.push('content array is empty'); const audioContent = content && content.find((c: any) => c.type === 'audio'); if (!audioContent) errors.push('No audio content found'); if (audioContent && !audioContent.data) errors.push('Audio content missing data field'); if (audioContent && !audioContent.mimeType) errors.push('Audio content missing mimeType field'); if (audioContent && audioContent.mimeType !== 'audio/wav') errors.push( `Expected mimeType 'audio/wav', got '${audioContent.mimeType}'` ); checks.push({ id: 'tools-call-audio', name: 'ToolsCallAudio', description: 'Tool returns audio content', status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: [ { id: 'MCP-Tools-Call', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools' } ], details: { hasAudioContent: !!audioContent, audioDataLength: audioContent?.data?.length || 0 } }); await conn.close(); } catch (error) { checks.push({ id: 'tools-call-audio', name: 'ToolsCallAudio', description: 'Tool returns audio content', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Tools-Call', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools' } ] }); } return checks; } } export class ToolsCallEmbeddedResourceScenario implements ClientScenario { name = 'tools-call-embedded-resource'; readonly source = { introducedIn: '2025-06-18' } as const; description = `Test calling a tool that returns embedded resource content. **Server Implementation Requirements:** Implement tool \`test_embedded_resource\` with no arguments that returns: \`\`\`json { "content": [ { "type": "resource", "resource": { "uri": "test://embedded-resource", "mimeType": "text/plain", "text": "This is an embedded resource content." } } ] } \`\`\``; async run(ctx: RunContext): Promise { const checks: ConformanceCheck[] = []; try { const conn = await ctx.connect(); const result = await conn.request('tools/call', { name: 'test_embedded_resource', arguments: {} }); // Validate response const errors: string[] = []; const content = (result as any).content; if (!content) errors.push('Missing content array'); if (!Array.isArray(content)) errors.push('content is not an array'); if (content && content.length === 0) errors.push('content array is empty'); const resourceContent = content && content.find((c: any) => c.type === 'resource'); if (!resourceContent) errors.push('No resource content found'); if (resourceContent && !resourceContent.resource) errors.push('Resource content missing resource field'); if (resourceContent?.resource) { if (!resourceContent.resource.uri) errors.push('Resource missing uri field'); if (!resourceContent.resource.mimeType) errors.push('Resource missing mimeType field'); if (!resourceContent.resource.text && !resourceContent.resource.blob) { errors.push('Resource missing both text and blob fields'); } } checks.push({ id: 'tools-call-embedded-resource', name: 'ToolsCallEmbeddedResource', description: 'Tool returns embedded resource content', status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: errors.length > 0 ? errors.join('; ') : undefined, specReferences: [ { id: 'MCP-Tools-Call', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools' } ], details: { hasResourceContent: !!resourceContent, resourceUri: resourceContent?.resource?.uri } }); await conn.close(); } catch (error) { checks.push({ id: 'tools-call-embedded-resource', name: 'ToolsCallEmbeddedResource', description: 'Tool returns embedded resource content', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, specReferences: [ { id: 'MCP-Tools-Call', url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/tools#calling-tools' } ] }); } return checks; } }