diff --git a/README.md b/README.md index 83bf5f1..502a011 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,19 @@ A framework for testing MCP (Model Context Protocol) client implementations agai ## Quick Start +### Testing Clients + ```bash npm install npm run start -- --command "tsx examples/clients/typescript/test1.ts" --scenario initialize ``` +### Testing Servers + +```bash +npm run test:server -- --server-url http://localhost:3000/mcp --all +``` + ## Overview The conformance test framework validates MCP client implementations by: diff --git a/SERVER_REQUIREMENTS.md b/SERVER_REQUIREMENTS.md new file mode 100644 index 0000000..2e32517 --- /dev/null +++ b/SERVER_REQUIREMENTS.md @@ -0,0 +1,933 @@ +# MCP Server Conformance Requirements + +This document specifies the requirements for building an MCP "Everything Server" for conformance testing. SDK maintainers should implement a server meeting these requirements to enable automated conformance testing. + +## Purpose + +The Everything Server is a reference implementation that: + +- Demonstrates all MCP server features in a single, testable server +- Uses standardized naming conventions for tools, resources, and prompts +- Enables automated conformance testing across different SDK implementations +- Serves as a working example for SDK users + +## Protocol Version + + + +**Target MCP Specification**: `2025-06-18` + +## Transport + +**Required Transport**: Streamable HTTP (for initial conformance testing) + +## Specification Compliance + +This document specifies requirements based on the MCP specification. All features and behaviors described are mandated by the MCP specification itself and enable automated conformance testing. + +--- + +## Server Information + +### Server Identity + +Your server MUST provide: + +```typescript +{ + name: "mcp-conformance-test-server", // Or your SDK-specific name + version: "1.0.0" +} +``` + +### Capabilities Declaration + +Your server MUST declare these capabilities during initialization: + +```typescript +{ + capabilities: { + tools: { + listChanged: true + }, + resources: { + subscribe: true, + listChanged: true + }, + prompts: { + listChanged: true + }, + logging: {}, + completions: {} + } +} +``` + +**Note**: All capabilities listed are required for conformance testing. The MCP specification also supports optional `experimental` capabilities for non-standard features, but these are not required for conformance. + +--- + +## 1. Lifecycle Requirements + +### 1.1. Initialize Handshake + +**Endpoint**: `initialize` + +**Requirements**: + +- Accept `initialize` request with client info and capabilities +- Return server info, protocol version, and capabilities +- Protocol version MUST be `"2025-06-18"` + +**Example Response**: + +```json +{ + "protocolVersion": "2025-06-18", + "serverInfo": { + "name": "mcp-conformance-test-server", + "version": "1.0.0" + }, + "capabilities": { + /* as above */ + } +} +``` + +### 1.2. Initialized Notification + +**Notification**: `initialized` + +**Requirements**: + +- Accept `initialized` notification from client after handshake +- No response required (it's a notification) +- Server should be ready for requests after receiving this + +--- + +## 2. Tools Requirements + +### 2.1. List Tools + +**Endpoint**: `tools/list` + +**Requirements**: + +- Return array of all available tools +- Each tool MUST have: + - `name` (string) + - `description` (string) + - `inputSchema` (valid JSON Schema object) + +### 2.2. Call Tool + +**Endpoint**: `tools/call` + +**Requirements**: + +- Accept tool name and arguments +- Execute tool and return result +- Result MUST have `content` array +- Support `_meta.progressToken` for progress reporting (if provided) + +### 2.3. Required Tools + +Implement these tools with exact names: + +#### `test_simple_text` + +**Arguments**: None + +**Returns**: Text content + +```typescript +{ + content: [{ type: 'text', text: 'This is a simple text response for testing.' }]; +} +``` + +#### `test_image_content` + +**Arguments**: None + +**Returns**: Image content with base64 data + +```typescript +{ + content: [ + { + type: 'image', + data: '', + mimeType: 'image/png' + } + ]; +} +``` + +**Implementation Note**: Use a minimal test image (e.g., 1x1 red pixel PNG) + +#### `test_audio_content` + +**Arguments**: None + +**Returns**: Audio content with base64 data + +```typescript +{ + content: [ + { + type: 'audio', + data: '', + mimeType: 'audio/wav' + } + ]; +} +``` + +**Implementation Note**: Use a minimal test audio file + +#### `test_embedded_resource` + +**Arguments**: None + +**Returns**: Embedded resource content + +```typescript +{ + content: [ + { + type: 'resource', + resource: { + uri: 'test://embedded-resource', + mimeType: 'text/plain', + text: 'This is an embedded resource content.' + } + } + ]; +} +``` + +#### `test_multiple_content_types` + +**Arguments**: None + +**Returns**: Multiple content items (text + image + resource) + +```typescript +{ + 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}' + } + } + ]; +} +``` + +#### `test_tool_with_logging` + +**Arguments**: None + +**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 + +#### `test_tool_with_progress` + +**Arguments**: None + +**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**: + +```typescript +{ + method: "notifications/progress", + params: { + progressToken: , + progress: 50, + total: 100 + } +} +``` + +#### `test_error_handling` + +**Arguments**: None + +**Behavior**: Always throw an error + +**Returns**: JSON-RPC error + +```json +{ + "error": { + "code": -32000, + "message": "This tool intentionally returns an error for testing" + } +} +``` + +#### `test_sampling` + +**Arguments**: + +- `prompt` (string, required) - The prompt to send to the LLM + +**Behavior**: Request LLM sampling from the client using `sampling/createMessage` + +**Sampling Request**: + +```typescript +{ + "method": "sampling/createMessage", + "params": { + "messages": [ + { + "role": "user", + "content": { "type": "text", "text": "" } + } + ], + "maxTokens": 100 + } +} +``` + +**Returns**: Text content with the LLM's response + +```typescript +{ + "content": [ + { + "type": "text", + "text": "LLM response: " + } + ] +} +``` + +**Implementation Note**: If the client doesn't support sampling (no `sampling` capability), return an error. + +#### `test_elicitation` + +**Arguments**: + +- `message` (string, required) - The message to show the user + +**Behavior**: Request user input from the client using `elicitation/create` + +**Elicitation Request**: + +```typescript +{ + "method": "elicitation/create", + "params": { + "message": "", + "requestedSchema": { + "type": "object", + "properties": { + "response": { + "type": "string", + "description": "User's response" + } + }, + "required": ["response"] + } + } +} +``` + +**Returns**: Text content with the user's response + +```typescript +{ + "content": [ + { + "type": "text", + "text": "User response: " + } + ] +} +``` + +**Implementation Note**: If the client doesn't support elicitation (no `elicitation` capability), return an error. + +--- + +## 3. Resources Requirements + +### 3.1. List Resources + +**Endpoint**: `resources/list` + +**Requirements**: + +- Return array of all available **direct resources** (not templates) +- Support optional cursor-based pagination (see Section 6.1) +- Each resource MUST have: + - `uri` (string) + - `name` (string) + - `description` (string) + - `mimeType` (string, optional) + +**Note**: Resource templates are listed via separate `resources/templates/list` endpoint (see 3.1a) + +### 3.1a. List Resource Templates + +**Endpoint**: `resources/templates/list` + +**Requirements**: + +- Return array of all available **resource templates** +- Support optional cursor-based pagination (see Section 6.1) +- Each template MUST have: + - `uriTemplate` (string) - RFC 6570 URI template + - `name` (string) + - `description` (string) + - `mimeType` (string, optional) + +### 3.2. Read Resource + +**Endpoint**: `resources/read` + +**Requirements**: + +- Accept resource URI +- Return resource contents +- Response MUST have `contents` array +- Each content item MUST have `uri`, `mimeType`, and either `text` or `blob` + +### 3.3. Required Resources + +Implement these resources with exact URIs: + +#### `test://static-text` + +**Type**: Static text resource + +**Metadata** (for `resources/list`): + +```typescript +{ + uri: "test://static-text", + name: "Static Text Resource", + description: "A static text resource for testing", + mimeType: "text/plain" +} +``` + +**Content** (for `resources/read`): + +```typescript +{ + contents: [ + { + uri: 'test://static-text', + mimeType: 'text/plain', + text: 'This is the content of the static text resource.' + } + ]; +} +``` + +#### `test://static-binary` + +**Type**: Static binary resource (image) + +**Metadata**: + +```typescript +{ + uri: "test://static-binary", + name: "Static Binary Resource", + description: "A static binary resource (image) for testing", + mimeType: "image/png" +} +``` + +**Content**: + +```typescript +{ + contents: [ + { + uri: 'test://static-binary', + mimeType: 'image/png', + blob: '' + } + ]; +} +``` + +#### `test://template/{id}/data` + +**Type**: Resource template with parameter + +**Metadata** (for `resources/list`): + +```typescript +{ + uriTemplate: "test://template/{id}/data", + name: "Resource Template", + description: "A resource template with parameter substitution", + mimeType: "application/json" +} +``` + +**Behavior**: When client requests `test://template/123/data`, substitute `{id}` with `123` + +**Content** (for `resources/read` with `uri: "test://template/123/data"`): + +```typescript +{ + contents: [ + { + uri: 'test://template/123/data', + mimeType: 'application/json', + text: '{"id":"123","templateTest":true,"data":"Data for ID: 123"}' + } + ]; +} +``` + +**Implementation Note**: Use RFC 6570 URI template syntax + +#### `test://watched-resource` + +**Type**: Subscribable resource + +**Metadata**: + +```typescript +{ + uri: "test://watched-resource", + name: "Watched Resource", + description: "A resource that can be subscribed to", + mimeType: "text/plain" +} +``` + +**Content**: + +```typescript +{ + contents: [ + { + uri: 'test://watched-resource', + mimeType: 'text/plain', + text: 'Watched resource content' + } + ]; +} +``` + +### 3.4. Resource Subscription + +**Endpoint**: `resources/subscribe` + +**Requirements**: + +- Accept subscription request with URI +- Track subscribed URIs +- Send `notifications/resources/updated` when subscribed resources change +- Return empty object `{}` + +**Example Request**: + +```json +{ + "method": "resources/subscribe", + "params": { + "uri": "test://watched-resource" + } +} +``` + +**Endpoint**: `resources/unsubscribe` + +**Requirements**: + +- Accept unsubscribe request with URI +- Remove URI from subscriptions +- Stop sending update notifications for that URI +- Return empty object `{}` + +--- + +## 4. Prompts Requirements + +### 4.1. List Prompts + +**Endpoint**: `prompts/list` + +**Requirements**: + +- Return array of all available prompts +- Each prompt MUST have: + - `name` (string) + - `description` (string) + - `arguments` (array, optional) - list of required arguments + +### 4.2. Get Prompt + +**Endpoint**: `prompts/get` + +**Requirements**: + +- Accept prompt name and arguments +- Return prompt messages +- Response MUST have `messages` array +- Each message MUST have `role` and `content` + +### 4.3. Required Prompts + +Implement these prompts with exact names: + +#### `test_simple_prompt` + +**Arguments**: None + +**Returns**: + +```typescript +{ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: 'This is a simple prompt for testing.' + } + } + ]; +} +``` + +#### `test_prompt_with_arguments` + +**Arguments**: + +- `arg1` (string, required) - First test argument +- `arg2` (string, required) - Second test argument + +**Metadata** (for `prompts/list`): + +```typescript +{ + name: "test_prompt_with_arguments", + description: "A prompt with required arguments", + arguments: [ + { name: "arg1", description: "First test argument", required: true }, + { name: "arg2", description: "Second test argument", required: true } + ] +} +``` + +**Returns** (with args `{arg1: "hello", arg2: "world"}`): + +```typescript +{ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: "Prompt with arguments: arg1='hello', arg2='world'" + } + } + ]; +} +``` + +#### `test_prompt_with_embedded_resource` + +**Arguments**: + +- `resourceUri` (string, required) - URI of the resource to embed + +**Returns**: + +```typescript +{ + messages: [ + { + role: 'user', + content: { + type: 'resource', + resource: { + uri: '', + mimeType: 'text/plain', + text: 'Embedded resource content for testing.' + } + } + }, + { + role: 'user', + content: { + type: 'text', + text: 'Please process the embedded resource above.' + } + } + ]; +} +``` + +#### `test_prompt_with_image` + +**Arguments**: None + +**Returns**: + +```typescript +{ + messages: [ + { + role: 'user', + content: { + type: 'image', + data: '', + mimeType: 'image/png' + } + }, + { + role: 'user', + content: { + type: 'text', + text: 'Please analyze the image above.' + } + } + ]; +} +``` + +--- + +## 5. Logging Requirements + +### 5.1. Set Log Level + +**Endpoint**: `logging/setLevel` + +**Requirements**: + +- Accept log level setting +- Filter subsequent log notifications based on level +- Return empty object `{}` + +**Log Levels** (in order of severity): + +- `debug` +- `info` +- `notice` +- `warning` +- `error` +- `critical` +- `alert` +- `emergency` + +### 5.2. Log Notifications + +**Notification**: `notifications/message` + +**Requirements**: + +- Send log notifications during operations +- Each log MUST have: + - `level` (string) - one of the log levels above + - `data` (any) - log message or structured data + - `logger` (string, optional) - logger name + +**Example**: + +```typescript +{ + method: "notifications/message", + params: { + level: "info", + logger: "conformance-test-server", + data: "Tool execution started" + } +} +``` + +**Implementation Note**: When no client is connected, log notifications may fail. Handle this gracefully by catching promise rejections: + +```typescript +mcpServer.server + .notification({ + method: 'notifications/message', + params: { level, logger, data } + }) + .catch(() => { + // Ignore error if no client is connected + }); +``` + +--- + +## 6. Pagination (Utility Feature) + +### 6.1. Cursor-Based Pagination + +**Purpose**: Allow servers to return large result sets in manageable chunks. + +**Applies To**: + +- `tools/list` +- `resources/list` +- `resources/templates/list` +- `prompts/list` + +**Requirements**: + +**Request Format**: + +```typescript +{ + "method": "tools/list", // or resources/list, prompts/list, etc. + "params": { + "cursor": "optional-opaque-token" // Optional: for subsequent pages + } +} +``` + +**Response Format**: + +```typescript +{ + "tools": [ /* array of tools */ ], // or resources, prompts, etc. + "nextCursor": "optional-opaque-token" // Optional: indicates more results available +} +``` + +**Implementation Requirements**: + +- If `cursor` parameter is provided, return results starting after that position +- If more results are available, include `nextCursor` in response +- Cursor tokens MUST be opaque strings (format is server-defined) +- Page size is determined by server (clients MUST NOT assume fixed page size) +- For conformance testing, pagination is optional (all items can be returned in single response) + +**Example**: + +```typescript +// First request (no cursor) +Request: { "method": "tools/list" } +Response: { + "tools": [/* first 10 tools */], + "nextCursor": "page2token" +} + +// Second request (with cursor) +Request: { "method": "tools/list", "params": { "cursor": "page2token" } } +Response: { + "tools": [/* next 10 tools */], + "nextCursor": "page3token" +} + +// Last page (no nextCursor) +Request: { "method": "tools/list", "params": { "cursor": "page3token" } } +Response: { + "tools": [/* remaining tools */] + // No nextCursor = end of results +} +``` + +--- + +## 7. Completion Requirements + +### 6.1. Complete Request + +**Endpoint**: `completion/complete` + +**Requirements**: + +- Accept completion requests for prompt or resource template arguments +- Provide contextual suggestions based on partial input +- Return array of completion values ranked by relevance + +**Request Format**: + +```typescript +{ + "method": "completion/complete", + "params": { + "ref": { + "type": "ref/prompt", // or "ref/resource" + "name": "test_prompt_with_arguments" // or uri for resources + }, + "argument": { + "name": "arg1", + "value": "par" // partial value to complete + } + } +} +``` + +**Response Format**: + +```typescript +{ + "completion": { + "values": ["paris", "park", "party"], // Up to 100 items + "total": 150, // Optional: total available matches + "hasMore": true // Optional: indicates more results exist + } +} +``` + +**Implementation Note**: For conformance testing, completion support can be minimal or return empty arrays. The capability just needs to be declared and the endpoint must respond correctly. + +--- + +## 8. Testing Your Server + +### 8.1. Starting Your Server + +```bash +# Example for TypeScript +cd examples/servers/typescript +npm install +npm start +``` + +Server should output: + +``` +MCP Conformance Test Server running on http://localhost:3000 + - MCP endpoint: http://localhost:3000/mcp +``` + +### 8.2. Running Conformance Tests + +```bash +# Single test +npm run test:server -- --server-url http://localhost:3000/mcp --scenario server-initialize + +# All tests +npm run test:server -- --server-url http://localhost:3000/mcp --all +``` diff --git a/examples/servers/typescript/README.md b/examples/servers/typescript/README.md new file mode 100644 index 0000000..ba5963f --- /dev/null +++ b/examples/servers/typescript/README.md @@ -0,0 +1,173 @@ +# MCP Conformance Test Server + +A reference implementation of an MCP server that implements all features required for conformance testing. + +## Features + +This server implements: + +### Tools + +- `test_simple_text` - Returns simple text content +- `test_image_content` - Returns image content (base64 PNG) +- `test_audio_content` - Returns audio content (base64 WAV) +- `test_embedded_resource` - Returns embedded resource +- `test_multiple_content_types` - Returns mixed content types +- `test_tool_with_logging` - Emits log messages during execution +- `test_tool_with_progress` - Reports progress notifications +- `test_error_handling` - Returns error response +- `test_sampling` - Requests LLM completion from client +- `test_elicitation` - Requests user input from client +- `test_dynamic_tool` - Dynamically added/removed tool + +### Resources + +- `test://static-text` - Static text resource +- `test://static-binary` - Static binary resource (image) +- `test://template/{id}/data` - Resource template with parameter +- `test://watched-resource` - Subscribable resource with updates + +### Prompts + +- `test_simple_prompt` - Simple prompt without arguments +- `test_prompt_with_arguments(arg1, arg2)` - Parameterized prompt +- `test_prompt_with_embedded_resource(resourceUri)` - Prompt with embedded resource +- `test_prompt_with_image` - Prompt with image content + +### Other Capabilities + +- Logging at all levels (debug, info, notice, warning, error, critical, alert, emergency) +- Completion support for prompt and resource arguments +- List changed notifications for tools, resources, and prompts +- Resource subscription and update notifications + +## Installation + +```bash +npm install +``` + +## Running the Server + +```bash +npm start +``` + +The server will start on `http://localhost:3000` (or the port specified in `PORT` environment variable). + +## Endpoints + +### MCP Endpoint + +- `POST /mcp` - Main MCP protocol endpoint + +### Health Check + +- `GET /health` - Server health check + +## Automatic Behaviors + +The server automatically demonstrates dynamic capabilities: + +- **Dynamic Tool** - `test_dynamic_tool` is automatically added 2 seconds after server starts +- **Dynamic Resource** - `test://dynamic-resource` is automatically added 2 seconds after server starts +- **Dynamic Prompt** - `test_dynamic_prompt` is automatically added 2 seconds after server starts +- **Resource Updates** - `test://watched-resource` automatically updates every 3 seconds with new content + +These behaviors allow testing of MCP notifications without requiring manual triggers. + +## Example Usage + +### Starting the Server + +```bash +npm start +``` + +### Testing with MCP Inspector + +```bash +npx @modelcontextprotocol/inspector http://localhost:3000/mcp +``` + +### Testing with curl + +#### Initialize + +```bash +curl -X POST http://localhost:3000/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-06-18", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } + }' +``` + +#### List Tools + +```bash +curl -X POST http://localhost:3000/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list" + }' +``` + +#### Call Tool + +```bash +curl -X POST http://localhost:3000/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "test_simple_text", + "arguments": {} + } + }' +``` + +## Conformance Testing + +This server implements the MCP Server Conformance Requirements specified in `../../../SERVER_REQUIREMENTS.md`. All tools, resources, and prompts use standardized naming conventions for consistent testing across SDK implementations. + +To run conformance tests against this server: + +```bash +cd ../../../ +npm run test:server -- --server-url http://localhost:3000/mcp --all +``` + +## Implementation Notes + +- All tool, resource, and prompt names follow the standardized naming conventions (`test_*` for tools/prompts, `test://` for resources) +- Names are descriptive of the feature being tested (e.g., `test_image_content`, `test_tool_with_progress`) +- The server uses the TypeScript MCP SDK (`@modelcontextprotocol/sdk`) high-level API +- Uses `registerTool()`, `registerResource()`, and `registerPrompt()` methods +- Transport is Streamable HTTP (Express) for web-based testing compatibility +- Promise rejections from notifications are caught and handled gracefully + +## For SDK Implementers + +If you're implementing MCP in another language/SDK: + +1. **Read the Requirements**: See `../../../SERVER_REQUIREMENTS.md` for complete specifications +2. **Use This as Reference**: This TypeScript implementation demonstrates all required features +3. **Follow Naming Conventions**: Use exact tool/resource/prompt names specified in requirements +4. **Implement Automatic Behaviors**: Dynamic registration after 2s, resource updates every 3s +5. **Handle Notifications Carefully**: Catch/ignore errors when no client is connected + +**Goal**: All SDK example servers provide the same interface, enabling a single test suite to verify conformance across all implementations. diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts new file mode 100644 index 0000000..6fff9f4 --- /dev/null +++ b/examples/servers/typescript/everything-server.ts @@ -0,0 +1,748 @@ +#!/usr/bin/env node + +/** + * MCP Everything Server - Conformance Test Server + * + * Server implementing all MCP features for conformance testing based on Conformnace Server Specification. + * Should be using registerTool(), registerResource(), and registerPrompt(). + * we use tool() instead of registerTool() as there is a bug with logging in registerTool(). + */ + +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { z } from 'zod'; +import express from 'express'; +import cors from 'cors'; +import { randomUUID } from 'crypto'; + +// Server state +const resourceSubscriptions = new Set(); +const watchedResourceContent = 'Watched resource content'; + +// Session management +const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; +const servers: { [sessionId: string]: McpServer } = {}; + +// Sample base64 encoded 1x1 red PNG pixel for testing +const TEST_IMAGE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=='; + +// Sample base64 encoded minimal WAV file for testing +const TEST_AUDIO_BASE64 = 'UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA='; + +// Function to create a new MCP server instance (one per session) +function createMcpServer() { + const mcpServer = new McpServer( + { + name: 'mcp-conformance-test-server', + version: '1.0.0' + }, + { + capabilities: { + tools: { + listChanged: true + }, + resources: { + subscribe: true, + listChanged: true + }, + prompts: { + listChanged: true + }, + logging: {}, + completions: {} + } + } + ); + + // Helper to send log messages using the underlying server + function sendLog( + level: 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency', + message: string, + data?: any + ) { + mcpServer.server + .notification({ + method: 'notifications/message', + params: { + level, + logger: 'conformance-test-server', + data: data || message + } + }) + .catch(() => { + // Ignore error if no client is connected + }); + } + + // ===== TOOLS ===== + + // Simple text tool + mcpServer.tool('test_simple_text', 'Tests simple text content response', {}, async () => { + return { + content: [{ type: 'text', text: 'This is a simple text response for testing.' }] + }; + }); + + // Image content tool + mcpServer.registerTool( + 'test_image_content', + { + description: 'Tests image content response' + }, + async () => { + return { + content: [{ type: 'image', data: TEST_IMAGE_BASE64, mimeType: 'image/png' }] + }; + } + ); + + // Audio content tool + mcpServer.registerTool( + 'test_audio_content', + { + description: 'Tests audio content response' + }, + async () => { + return { + content: [{ type: 'audio', data: TEST_AUDIO_BASE64, mimeType: 'audio/wav' }] + }; + } + ); + + // Embedded resource tool + mcpServer.registerTool( + 'test_embedded_resource', + { + description: 'Tests embedded resource content response' + }, + async () => { + return { + content: [ + { + type: 'resource', + resource: { + uri: 'test://embedded-resource', + mimeType: 'text/plain', + text: 'This is an embedded resource content.' + } + } + ] + }; + } + ); + + // Multiple content types tool + mcpServer.registerTool( + 'test_multiple_content_types', + { + description: 'Tests response with multiple content types (text, image, resource)' + }, + async () => { + return { + content: [ + { type: 'text', text: 'Multiple content types test:' }, + { type: 'image', data: TEST_IMAGE_BASE64, mimeType: 'image/png' }, + { + type: 'resource', + resource: { + uri: 'test://mixed-content-resource', + mimeType: 'application/json', + text: JSON.stringify({ test: 'data', value: 123 }) + } + } + ] + }; + } + ); + + // Tool with logging - registerTool with empty inputSchema to get (args, extra) signature + mcpServer.registerTool( + 'test_tool_with_logging', + { + description: 'Tests tool that emits log messages during execution', + inputSchema: {} // Empty schema so callback gets (args, extra) instead of just (extra) + }, + async (_args, { sendNotification }) => { + await sendNotification({ + method: 'notifications/message', + params: { + level: 'info', + data: 'Tool execution started' + } + }); + await new Promise(resolve => setTimeout(resolve, 50)); + + await sendNotification({ + method: 'notifications/message', + params: { + level: 'info', + data: 'Tool processing data' + } + }); + await new Promise(resolve => setTimeout(resolve, 50)); + + await sendNotification({ + method: 'notifications/message', + params: { + level: 'info', + data: 'Tool execution completed' + } + }); + return { + content: [{ type: 'text', text: 'Tool with logging executed successfully' }] + }; + } + ); + + // Tool with progress - registerTool with empty inputSchema to get (args, extra) signature + mcpServer.registerTool( + 'test_tool_with_progress', + { + description: 'Tests tool that reports progress notifications', + inputSchema: {} // Empty schema so callback gets (args, extra) instead of just (extra) + }, + async (_args, { sendNotification, _meta }) => { + const progressToken = _meta?.progressToken ?? 0; + console.log('???? Progress token:', progressToken); + await sendNotification({ + method: 'notifications/progress', + params: { + progressToken, + progress: 0, + total: 100, + message: `Completed step ${0} of ${100}` + } + }); + await new Promise(resolve => setTimeout(resolve, 50)); + + await sendNotification({ + method: 'notifications/progress', + params: { + progressToken, + progress: 50, + total: 100, + message: `Completed step ${50} of ${100}` + } + }); + await new Promise(resolve => setTimeout(resolve, 50)); + + await sendNotification({ + method: 'notifications/progress', + params: { + progressToken, + progress: 100, + total: 100, + message: `Completed step ${100} of ${100}` + } + }); + + return { + content: [{ type: 'text', text: String(progressToken) }] + }; + } + ); + + // Error handling tool + mcpServer.registerTool( + 'test_error_handling', + { + description: 'Tests error response handling' + }, + async () => { + throw new Error('This tool intentionally returns an error for testing'); + } + ); + + // Sampling tool - requests LLM completion from client + mcpServer.registerTool( + 'test_sampling', + { + description: 'Tests server-initiated sampling (LLM completion request)', + inputSchema: { + prompt: z.string().describe('The prompt to send to the LLM') + } + }, + async (args: { prompt: string }) => { + try { + // Request sampling from client + const result = await mcpServer.server.request( + { + method: 'sampling/createMessage', + params: { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: args.prompt + } + } + ], + maxTokens: 100 + } + }, + z.object({ method: z.literal('sampling/createMessage') }).passthrough() as any + ); + + const samplingResult = result as any; + const modelResponse = samplingResult.content?.text || samplingResult.message?.content?.text || 'No response'; + + return { + content: [ + { + type: 'text', + text: `LLM response: ${modelResponse}` + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Sampling not supported or error: ${error.message}` + } + ] + }; + } + } + ); + + // Elicitation tool - requests user input from client + mcpServer.registerTool( + 'test_elicitation', + { + description: 'Tests server-initiated elicitation (user input request)', + inputSchema: { + message: z.string().describe('The message to show the user') + } + }, + async (args: { message: string }) => { + try { + // Request user input from client + const result = await mcpServer.server.request( + { + method: 'elicitation/create', + params: { + message: args.message, + requestedSchema: { + type: 'object', + properties: { + response: { + type: 'string', + description: "User's response" + } + }, + required: ['response'] + } + } + }, + z.object({ method: z.literal('elicitation/create') }).passthrough() as any + ); + + const elicitResult = result as any; + return { + content: [ + { + type: 'text', + text: `User response: action=${elicitResult.action}, content=${JSON.stringify(elicitResult.content || {})}` + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Elicitation not supported or error: ${error.message}` + } + ] + }; + } + } + ); + + // Dynamic tool (registered later via timer) + + // ===== RESOURCES ===== + + // Static text resource + mcpServer.registerResource( + 'static-text', + 'test://static-text', + { + title: 'Static Text Resource', + description: 'A static text resource for testing', + mimeType: 'text/plain' + }, + async () => { + return { + contents: [ + { + uri: 'test://static-text', + mimeType: 'text/plain', + text: 'This is the content of the static text resource.' + } + ] + }; + } + ); + + // Static binary resource + mcpServer.registerResource( + 'static-binary', + 'test://static-binary', + { + title: 'Static Binary Resource', + description: 'A static binary resource (image) for testing', + mimeType: 'image/png' + }, + async () => { + return { + contents: [ + { + uri: 'test://static-binary', + mimeType: 'image/png', + blob: TEST_IMAGE_BASE64 + } + ] + }; + } + ); + + // Resource template + mcpServer.registerResource( + 'template', + new ResourceTemplate('test://template/{id}/data', { + list: undefined + }), + { + title: 'Resource Template', + description: 'A resource template with parameter substitution', + mimeType: 'application/json' + }, + async (uri, variables) => { + const id = variables.id; + return { + contents: [ + { + uri: uri.toString(), + mimeType: 'application/json', + text: JSON.stringify({ + id, + templateTest: true, + data: `Data for ID: ${id}` + }) + } + ] + }; + } + ); + + // Watched resource + mcpServer.registerResource( + 'watched-resource', + 'test://watched-resource', + { + title: 'Watched Resource', + description: 'A resource that auto-updates every 3 seconds', + mimeType: 'text/plain' + }, + async () => { + return { + contents: [ + { + uri: 'test://watched-resource', + mimeType: 'text/plain', + text: watchedResourceContent + } + ] + }; + } + ); + + // Subscribe/Unsubscribe handlers + mcpServer.server.setRequestHandler(z.object({ method: z.literal('resources/subscribe') }).passthrough(), async (request: any) => { + const uri = request.params.uri; + resourceSubscriptions.add(uri); + sendLog('info', `Subscribed to resource: ${uri}`); + return {}; + }); + + mcpServer.server.setRequestHandler(z.object({ method: z.literal('resources/unsubscribe') }).passthrough(), async (request: any) => { + const uri = request.params.uri; + resourceSubscriptions.delete(uri); + sendLog('info', `Unsubscribed from resource: ${uri}`); + return {}; + }); + + // ===== PROMPTS ===== + + // Simple prompt + mcpServer.registerPrompt( + 'test_simple_prompt', + { + title: 'Simple Test Prompt', + description: 'A simple prompt without arguments' + }, + async () => { + return { + messages: [ + { + role: 'user', + content: { type: 'text', text: 'This is a simple prompt for testing.' } + } + ] + }; + } + ); + + // Prompt with arguments + mcpServer.registerPrompt( + 'test_prompt_with_arguments', + { + title: 'Prompt With Arguments', + description: 'A prompt with required arguments', + argsSchema: { + arg1: z.string().describe('First test argument'), + arg2: z.string().describe('Second test argument') + } + }, + async args => { + const { arg1, arg2 } = args; + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Prompt with arguments: arg1='${arg1}', arg2='${arg2}'` + } + } + ] + }; + } + ); + + // Prompt with embedded resource + mcpServer.registerPrompt( + 'test_prompt_with_embedded_resource', + { + title: 'Prompt With Embedded Resource', + description: 'A prompt that includes an embedded resource', + argsSchema: { + resourceUri: z.string().describe('URI of the resource to embed') + } + }, + async args => { + const uri = args.resourceUri; + return { + messages: [ + { + role: 'user', + content: { + type: 'resource', + resource: { + uri, + mimeType: 'text/plain', + text: 'Embedded resource content for testing.' + } + } + }, + { + role: 'user', + content: { type: 'text', text: 'Please process the embedded resource above.' } + } + ] + }; + } + ); + + // Prompt with image + mcpServer.registerPrompt( + 'test_prompt_with_image', + { + title: 'Prompt With Image', + description: 'A prompt that includes image content' + }, + async () => { + return { + messages: [ + { + role: 'user', + content: { type: 'image', data: TEST_IMAGE_BASE64, mimeType: 'image/png' } + }, + { + role: 'user', + content: { type: 'text', text: 'Please analyze the image above.' } + } + ] + }; + } + ); + + // ===== LOGGING ===== + + mcpServer.server.setRequestHandler(z.object({ method: z.literal('logging/setLevel') }).passthrough(), async (request: any) => { + const level = request.params.level; + sendLog('info', `Log level set to: ${level}`); + return {}; + }); + + // ===== COMPLETION ===== + + mcpServer.server.setRequestHandler(z.object({ method: z.literal('completion/complete') }).passthrough(), async (_request: any) => { + // Basic completion support - returns empty array for conformance + // Real implementations would provide contextual suggestions + return { + completion: { + values: [], + total: 0, + hasMore: false + } + }; + }); + + return mcpServer; +} + +// Helper to check if request is an initialize request +function isInitializeRequest(body: any): boolean { + return body?.method === 'initialize'; +} + +// ===== EXPRESS APP ===== + +const app = express(); +app.use(express.json()); + +// Configure CORS to expose Mcp-Session-Id header for browser-based clients +app.use( + cors({ + origin: '*', // Allow all origins + exposedHeaders: ['Mcp-Session-Id'], + allowedHeaders: ['Content-Type', 'mcp-session-id', 'last-event-id'] + }) +); + +// Handle POST requests - stateful mode +app.post('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + try { + let transport: StreamableHTTPServerTransport; + + if (sessionId && transports[sessionId]) { + // Reuse existing transport for established sessions + transport = transports[sessionId]; + } else if (!sessionId && isInitializeRequest(req.body)) { + // Create new transport for initialization requests + const mcpServer = createMcpServer(); + + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: newSessionId => { + transports[newSessionId] = transport; + servers[newSessionId] = mcpServer; + console.log(`Session initialized with ID: ${newSessionId}`); + } + }); + + transport.onclose = () => { + const sid = transport.sessionId; + if (sid && transports[sid]) { + delete transports[sid]; + if (servers[sid]) { + servers[sid].close(); + delete servers[sid]; + } + console.log(`Session ${sid} closed`); + } + }; + + await mcpServer.connect(transport); + await transport.handleRequest(req, res, req.body); + return; + } else { + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Invalid or missing session ID' + }, + id: null + }); + return; + } + + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error' + }, + id: null + }); + } + } +}); + +// Handle GET requests - SSE streams for sessions +app.get('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + const lastEventId = req.headers['last-event-id'] as string | undefined; + if (lastEventId) { + console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); + } else { + console.log(`Establishing SSE stream for session ${sessionId}`); + } + + try { + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + } catch (error) { + console.error('Error handling SSE stream:', error); + if (!res.headersSent) { + res.status(500).send('Error establishing SSE stream'); + } + } +}); + +// Handle DELETE requests - session termination +app.delete('/mcp', async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + + console.log(`Received session termination request for session ${sessionId}`); + + try { + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + } catch (error) { + console.error('Error handling termination:', error); + if (!res.headersSent) { + res.status(500).send('Error processing session termination'); + } + } +}); + +// Start server +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`MCP Conformance Test Server running on http://localhost:${PORT}`); + console.log(` - MCP endpoint: http://localhost:${PORT}/mcp`); +}); diff --git a/examples/servers/typescript/package-lock.json b/examples/servers/typescript/package-lock.json new file mode 100644 index 0000000..14b3c85 --- /dev/null +++ b/examples/servers/typescript/package-lock.json @@ -0,0 +1,1679 @@ +{ + "name": "mcp-conformance-test-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-conformance-test-server", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.20.1", + "@types/cors": "^2.8.19", + "cors": "^2.8.5", + "express": "^5.1.0" + }, + "devDependencies": { + "tsx": "^4.7.0", + "typescript": "^5.5.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.20.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz", + "integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.8.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@types/node/-/node-24.8.0.tgz", + "integrity": "sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-generator-function": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/async-generator-function/-/async-generator-function-1.0.0.tgz", + "integrity": "sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.11", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-intrinsic/-/get-intrinsic-1.3.1.tgz", + "integrity": "sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "async-generator-function": "^1.0.0", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.12.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-tsconfig/-/get-tsconfig-4.12.0.tgz", + "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/wrappy/1.0.2/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/examples/servers/typescript/package.json b/examples/servers/typescript/package.json new file mode 100644 index 0000000..592d455 --- /dev/null +++ b/examples/servers/typescript/package.json @@ -0,0 +1,27 @@ +{ + "name": "mcp-conformance-test-server", + "version": "1.0.0", + "description": "Reference implementation of MCP server for conformance testing", + "type": "module", + "main": "everything-server.ts", + "scripts": { + "start": "tsx everything-server.ts", + "dev": "tsx watch everything-server.ts" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "conformance", + "testing" + ], + "dependencies": { + "@modelcontextprotocol/sdk": "^1.20.1", + "@types/cors": "^2.8.19", + "cors": "^2.8.5", + "express": "^5.1.0" + }, + "devDependencies": { + "tsx": "^4.7.0", + "typescript": "^5.5.4" + } +} diff --git a/examples/servers/typescript/tsconfig.json b/examples/servers/typescript/tsconfig.json new file mode 100644 index 0000000..5363227 --- /dev/null +++ b/examples/servers/typescript/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": ["*.ts"] +} diff --git a/package-lock.json b/package-lock.json index 8fec0b0..63b2b97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "devDependencies": { "@eslint/js": "^9.8.0", "@jest/globals": "^29.7.0", + "@types/express": "^5.0.3", "@types/jest": "^29.5.8", "eslint": "^9.8.0", "eslint-config-prettier": "^10.1.8", @@ -58,7 +59,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1787,6 +1787,27 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1794,6 +1815,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1804,6 +1850,13 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1849,6 +1902,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.10.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", @@ -1859,6 +1919,53 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -2412,9 +2519,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.23", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz", - "integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==", + "version": "2.8.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.24.tgz", + "integrity": "sha512-uUhTRDPXamakPyghwrUcjaGvvBqGrWvBHReoiULMIpOJVM9IYzQh83Xk2Onx5HlGI2o10NNCzcs9TG/S3TkwrQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 21f0938..5e28b4e 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "type": "module", "scripts": { "start": "tsx src/runner.ts", + "test:server": "tsx src/server-runner.ts", "test": "jest", "lint": "eslint src/ && prettier --check .", "lint:fix": "eslint src/ --fix && prettier --write .", @@ -12,6 +13,7 @@ "devDependencies": { "@eslint/js": "^9.8.0", "@jest/globals": "^29.7.0", + "@types/express": "^5.0.3", "@types/jest": "^29.5.8", "eslint": "^9.8.0", "eslint-config-prettier": "^10.1.8", diff --git a/src/checks/index.ts b/src/checks/index.ts index b262c17..cb807fe 100644 --- a/src/checks/index.ts +++ b/src/checks/index.ts @@ -1,6 +1,4 @@ -// Namespaced exports for client and server checks +// Namespaced exports for client checks import * as client from './client.js'; -import * as server from './server.js'; export const clientChecks = client; -export const serverChecks = server; diff --git a/src/checks/server.ts b/src/checks/server.ts index 894448b..e69de29 100644 --- a/src/checks/server.ts +++ b/src/checks/server.ts @@ -1,37 +0,0 @@ -import { ConformanceCheck, CheckStatus } from '../types.js'; - -export function createServerInitializationCheck(initializeResponse: any, expectedSpecVersion: string = '2025-06-18'): ConformanceCheck { - const result = initializeResponse?.result; - const protocolVersion = result?.protocolVersion; - const serverInfo = result?.serverInfo; - const capabilities = result?.capabilities; - - const errors: string[] = []; - if (!initializeResponse?.jsonrpc) errors.push('Missing jsonrpc field'); - if (!initializeResponse?.id) errors.push('Missing id field'); - if (!result) errors.push('Missing result field'); - if (!protocolVersion) errors.push('Missing protocolVersion in result'); - if (protocolVersion !== expectedSpecVersion) - errors.push(`Protocol version mismatch: expected ${expectedSpecVersion}, got ${protocolVersion}`); - if (!serverInfo) errors.push('Missing serverInfo in result'); - if (!serverInfo?.name) errors.push('Missing server name in serverInfo'); - if (!serverInfo?.version) errors.push('Missing server version in serverInfo'); - if (capabilities === undefined) errors.push('Missing capabilities in result'); - - const status: CheckStatus = errors.length === 0 ? 'SUCCESS' : 'FAILURE'; - - return { - id: 'mcp-server-initialization', - name: 'MCPServerInitialization', - description: 'Validates that MCP server properly responds to initialize request', - status, - timestamp: new Date().toISOString(), - specReferences: [{ id: 'MCP-Lifecycle', url: 'https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle' }], - details: { - expectedSpecVersion, - response: initializeResponse - }, - errorMessage: errors.length > 0 ? errors.join('; ') : undefined, - logs: errors.length > 0 ? errors : undefined - }; -} diff --git a/src/runner.ts b/src/runner.ts index 0ee592e..ff1d929 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,4 +1,3 @@ import { runConformanceTest } from './runner/index.js'; export { runConformanceTest }; -export type { ScenarioConfig } from './runner/index.js'; diff --git a/src/scenarios/client/tools_call.ts b/src/scenarios/client/tools_call.ts index 606ce88..0310a26 100644 --- a/src/scenarios/client/tools_call.ts +++ b/src/scenarios/client/tools_call.ts @@ -2,7 +2,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import type { Scenario, ConformanceCheck } from '../../types.js'; -import express from 'express'; +// @ts-expect-error - express has incomplete type definitions +import express, { Request, Response, NextFunction } from 'express'; import { ScenarioUrls } from '../../types.js'; function createServer(checks: ConformanceCheck[]): express.Application { @@ -83,7 +84,7 @@ function createServer(checks: ConformanceCheck[]): express.Application { const app = express(); app.use(express.json()); - app.use((req, res, next) => { + app.use((req: Request, res: Response, next: NextFunction) => { // Log incoming requests for debugging // console.log(`Incoming request: ${req.method} ${req.url}`); checks.push({ @@ -105,13 +106,13 @@ function createServer(checks: ConformanceCheck[]): express.Application { status: 'INFO', timestamp: new Date().toISOString(), details: { - // TODO: this isn't working - body: JSON.stringify(res.body) + // TODO: Response body not available in express middleware + statusCode: res.statusCode } }); }); - app.post('/mcp', async (req, res) => { + app.post('/mcp', async (req: Request, res: Response) => { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); @@ -125,6 +126,7 @@ function createServer(checks: ConformanceCheck[]): express.Application { export class ToolsCallScenario implements Scenario { name = 'tools_call'; + description = 'Tests calling tools with various parameter types'; private app: express.Application | null = null; private httpServer: any = null; private checks: ConformanceCheck[] = []; diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 0d9baec..fd69fc2 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -1,14 +1,84 @@ import { Scenario, ClientScenario } from '../types.js'; import { InitializeScenario } from './client/initialize.js'; import { ToolsCallScenario } from './client/tools_call.js'; -import { ServerInitializeClientScenario } from './server/server_initialize.js'; + +// Import all new server test scenarios +import { ServerInitializeScenario } from './server/lifecycle.js'; + +import { LoggingSetLevelScenario, CompletionCompleteScenario } from './server/utils.js'; + +import { + ToolsListScenario, + ToolsCallSimpleTextScenario, + ToolsCallImageScenario, + ToolsCallMultipleContentTypesScenario, + ToolsCallWithLoggingScenario, + ToolsCallErrorScenario, + ToolsCallWithProgressScenario, + ToolsCallSamplingScenario, + ToolsCallElicitationScenario, + ToolsCallAudioScenario, + ToolsCallEmbeddedResourceScenario +} from './server/tools.js'; + +import { + ResourcesListScenario, + ResourcesReadTextScenario, + ResourcesReadBinaryScenario, + ResourcesTemplateReadScenario, + ResourcesSubscribeScenario, + ResourcesUnsubscribeScenario +} from './server/resources.js'; + +import { + PromptsListScenario, + PromptsGetSimpleScenario, + PromptsGetWithArgsScenario, + PromptsGetEmbeddedResourceScenario, + PromptsGetWithImageScenario +} from './server/prompts.js'; export const scenarios = new Map([ ['initialize', new InitializeScenario()], ['tools-call', new ToolsCallScenario()] ]); -export const clientScenarios = new Map([['initialize', new ServerInitializeClientScenario()]]); +export const clientScenarios = new Map([ + // Lifecycle scenarios + ['server-initialize', new ServerInitializeScenario()], + + // Utilities scenarios + ['logging-set-level', new LoggingSetLevelScenario()], + ['completion-complete', new CompletionCompleteScenario()], + + // Tools scenarios + ['tools-list', new ToolsListScenario()], + ['tools-call-simple-text', new ToolsCallSimpleTextScenario()], + ['tools-call-image', new ToolsCallImageScenario()], + ['tools-call-audio', new ToolsCallAudioScenario()], + ['tools-call-embedded-resource', new ToolsCallEmbeddedResourceScenario()], + ['tools-call-mixed-content', new ToolsCallMultipleContentTypesScenario()], + ['tools-call-with-logging', new ToolsCallWithLoggingScenario()], + ['tools-call-error', new ToolsCallErrorScenario()], + ['tools-call-with-progress', new ToolsCallWithProgressScenario()], + ['tools-call-sampling', new ToolsCallSamplingScenario()], + ['tools-call-elicitation', new ToolsCallElicitationScenario()], + + // Resources scenarios + ['resources-list', new ResourcesListScenario()], + ['resources-read-text', new ResourcesReadTextScenario()], + ['resources-read-binary', new ResourcesReadBinaryScenario()], + ['resources-templates-read', new ResourcesTemplateReadScenario()], + ['resources-subscribe', new ResourcesSubscribeScenario()], + ['resources-unsubscribe', new ResourcesUnsubscribeScenario()], + + // Prompts scenarios + ['prompts-list', new PromptsListScenario()], + ['prompts-get-simple', new PromptsGetSimpleScenario()], + ['prompts-get-with-args', new PromptsGetWithArgsScenario()], + ['prompts-get-embedded-resource', new PromptsGetEmbeddedResourceScenario()], + ['prompts-get-with-image', new PromptsGetWithImageScenario()] +]); export function registerScenario(name: string, scenario: Scenario): void { scenarios.set(name, scenario); diff --git a/src/scenarios/server/client-helper.ts b/src/scenarios/server/client-helper.ts new file mode 100644 index 0000000..27fed86 --- /dev/null +++ b/src/scenarios/server/client-helper.ts @@ -0,0 +1,83 @@ +/** + * Helper utilities for creating MCP clients to test servers + */ + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { LoggingMessageNotificationSchema, ProgressNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; + +export interface MCPClientConnection { + client: Client; + close: () => Promise; +} + +/** + * Create and connect an MCP client to a server + */ +export async function connectToServer(serverUrl: string): Promise { + const client = new Client( + { + name: 'conformance-test-client', + version: '1.0.0' + }, + { + capabilities: { + // Client capabilities + sampling: {}, + elicitation: {} + } + } + ); + + const transport = new StreamableHTTPClientTransport(new URL(serverUrl)); + + await client.connect(transport); + + return { + client, + close: async () => { + await client.close(); + } + }; +} + +/** + * Helper to collect notifications (logging and progress) + */ +export class NotificationCollector { + private loggingNotifications: any[] = []; + private progressNotifications: any[] = []; + + constructor(client: Client) { + // Set up notification handler for logging messages + client.setNotificationHandler(LoggingMessageNotificationSchema, notification => { + this.loggingNotifications.push(notification); + }); + + // Set up notification handler for progress notifications + client.setNotificationHandler(ProgressNotificationSchema, notification => { + this.progressNotifications.push(notification); + }); + } + + /** + * Get all collected logging notifications + */ + getLoggingNotifications(): any[] { + return this.loggingNotifications; + } + + /** + * Get all collected progress notifications + */ + getProgressNotifications(): any[] { + return this.progressNotifications; + } + + /** + * Get all notifications (for backward compatibility) + */ + getNotifications(): any[] { + return this.loggingNotifications; + } +} diff --git a/src/scenarios/server/lifecycle.ts b/src/scenarios/server/lifecycle.ts new file mode 100644 index 0000000..f525c09 --- /dev/null +++ b/src/scenarios/server/lifecycle.ts @@ -0,0 +1,58 @@ +/** + * Lifecycle test scenarios for MCP servers + */ + +import { ClientScenario, ConformanceCheck } from '../../types.js'; +import { connectToServer } from './client-helper.js'; + +export class ServerInitializeScenario implements ClientScenario { + name = 'server-initialize'; + description = 'Test basic server initialization handshake'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + // The connection process already does initialization + // Check that we have a connected client + checks.push({ + id: 'server-initialize', + name: 'ServerInitialize', + description: 'Server responds to initialize request with valid structure', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'MCP-Initialize', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#initialization' + } + ], + details: { + serverUrl, + connected: true + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'server-initialize', + name: 'ServerInitialize', + description: 'Server responds to initialize request with valid structure', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed to initialize: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Initialize', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#initialization' + } + ] + }); + } + + return checks; + } +} diff --git a/src/scenarios/server/prompts.ts b/src/scenarios/server/prompts.ts new file mode 100644 index 0000000..563933b --- /dev/null +++ b/src/scenarios/server/prompts.ts @@ -0,0 +1,350 @@ +/** + * Prompts test scenarios for MCP servers + */ + +import { ClientScenario, ConformanceCheck } from '../../types.js'; +import { connectToServer } from './client-helper.js'; + +export class PromptsListScenario implements ClientScenario { + name = 'prompts-list'; + description = 'Test listing available prompts'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.listPrompts(); + + // Validate response structure + const errors: string[] = []; + if (!result.prompts) { + errors.push('Missing prompts array'); + } else { + if (!Array.isArray(result.prompts)) { + errors.push('prompts is not an array'); + } + + result.prompts.forEach((prompt, index) => { + if (!prompt.name) errors.push(`Prompt ${index}: missing name`); + if (!prompt.description) errors.push(`Prompt ${index}: missing description`); + }); + } + + checks.push({ + id: 'prompts-list', + name: 'PromptsList', + description: 'Server lists available prompts with valid structure', + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: [ + { + id: 'MCP-Prompts-List', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#listing-prompts' + } + ], + details: { + promptCount: result.prompts?.length || 0, + prompts: result.prompts?.map(p => p.name) + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'prompts-list', + name: 'PromptsList', + description: 'Server lists available prompts with valid structure', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Prompts-List', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#listing-prompts' + } + ] + }); + } + + return checks; + } +} + +export class PromptsGetSimpleScenario implements ClientScenario { + name = 'prompts-get-simple'; + description = 'Test getting a simple prompt without arguments'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.getPrompt({ + name: 'test_simple_prompt' + }); + + // Validate response + const errors: string[] = []; + if (!result.messages) errors.push('Missing messages array'); + if (!Array.isArray(result.messages)) errors.push('messages is not an array'); + if (result.messages.length === 0) errors.push('messages array is empty'); + + result.messages.forEach((message: any, index: number) => { + if (!message.role) errors.push(`Message ${index}: missing role`); + if (!message.content) errors.push(`Message ${index}: missing content`); + }); + + checks.push({ + id: 'prompts-get-simple', + name: 'PromptsGetSimple', + description: 'Get simple prompt successfully', + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: [ + { + id: 'MCP-Prompts-Get', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#getting-prompts' + } + ], + details: { + messageCount: result.messages?.length || 0 + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'prompts-get-simple', + name: 'PromptsGetSimple', + description: 'Get simple prompt successfully', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Prompts-Get', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#getting-prompts' + } + ] + }); + } + + return checks; + } +} + +export class PromptsGetWithArgsScenario implements ClientScenario { + name = 'prompts-get-with-args'; + description = 'Test parameterized prompt'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.getPrompt({ + name: 'test_prompt_with_arguments', + arguments: { + arg1: 'testValue1', + arg2: 'testValue2' + } + }); + + // Validate response + const errors: string[] = []; + if (!result.messages) errors.push('Missing messages array'); + if (result.messages.length === 0) errors.push('messages array is empty'); + + // Check if arguments were substituted + const messageText = JSON.stringify(result.messages); + if (!messageText.includes('testValue1')) { + errors.push('arg1 not substituted in prompt'); + } + if (!messageText.includes('testValue2')) { + errors.push('arg2 not substituted in prompt'); + } + + checks.push({ + id: 'prompts-get-with-args', + name: 'PromptsGetWithArgs', + description: 'Get parameterized prompt with argument substitution', + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: [ + { + id: 'MCP-Prompts-Get', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#getting-prompts' + } + ], + details: { + messageCount: result.messages?.length || 0, + messages: result.messages + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'prompts-get-with-args', + name: 'PromptsGetWithArgs', + description: 'Get parameterized prompt with argument substitution', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Prompts-Get', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#getting-prompts' + } + ] + }); + } + + return checks; + } +} + +export class PromptsGetEmbeddedResourceScenario implements ClientScenario { + name = 'prompts-get-embedded-resource'; + description = 'Test prompt with embedded resource content'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.getPrompt({ + name: 'test_prompt_with_embedded_resource', + arguments: { + resourceUri: 'test://example-resource' + } + }); + + // Validate response + const errors: string[] = []; + if (!result.messages) errors.push('Missing messages array'); + + // Look for resource content + const hasResourceContent = result.messages.some( + (msg: any) => msg.content?.type === 'resource' || msg.content?.resource !== undefined + ); + + if (!hasResourceContent) { + errors.push('No embedded resource found in prompt'); + } + + checks.push({ + id: 'prompts-get-embedded-resource', + name: 'PromptsGetEmbeddedResource', + description: 'Get prompt with embedded resource', + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: [ + { + id: 'MCP-Prompts-Embedded-Resources', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#embedded-resources' + } + ], + details: { + messageCount: result.messages?.length || 0, + messages: result.messages + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'prompts-get-embedded-resource', + name: 'PromptsGetEmbeddedResource', + description: 'Get prompt with embedded resource', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Prompts-Embedded-Resources', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#embedded-resources' + } + ] + }); + } + + return checks; + } +} + +export class PromptsGetWithImageScenario implements ClientScenario { + name = 'prompts-get-with-image'; + description = 'Test prompt with image content'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.getPrompt({ + name: 'test_prompt_with_image' + }); + + // Validate response + const errors: string[] = []; + if (!result.messages) errors.push('Missing messages array'); + + // Look for image content + const hasImageContent = result.messages.some( + (msg: any) => msg.content?.type === 'image' && msg.content?.data && msg.content?.mimeType + ); + + if (!hasImageContent) { + errors.push('No image content found in prompt'); + } + + checks.push({ + id: 'prompts-get-with-image', + name: 'PromptsGetWithImage', + description: 'Get prompt with image content', + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: [ + { + id: 'MCP-Prompts-Image', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#image-content' + } + ], + details: { + messageCount: result.messages?.length || 0 + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'prompts-get-with-image', + name: 'PromptsGetWithImage', + description: 'Get prompt with image content', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Prompts-Image', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/prompts#image-content' + } + ] + }); + } + + return checks; + } +} diff --git a/src/scenarios/server/resources.ts b/src/scenarios/server/resources.ts new file mode 100644 index 0000000..f475e55 --- /dev/null +++ b/src/scenarios/server/resources.ts @@ -0,0 +1,389 @@ +/** + * Resources test scenarios for MCP servers + */ + +import { ClientScenario, ConformanceCheck } from '../../types.js'; +import { connectToServer } from './client-helper.js'; + +export class ResourcesListScenario implements ClientScenario { + name = 'resources-list'; + description = 'Test listing available resources'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.listResources(); + + // Validate response structure + const errors: string[] = []; + if (!result.resources) { + errors.push('Missing resources array'); + } else { + if (!Array.isArray(result.resources)) { + errors.push('resources is not an array'); + } + + result.resources.forEach((resource, index) => { + if (!resource.uri) errors.push(`Resource ${index}: missing uri`); + if (!resource.name) errors.push(`Resource ${index}: missing name`); + }); + } + + checks.push({ + id: 'resources-list', + name: 'ResourcesList', + description: 'Server lists available resources with valid structure', + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: [ + { + id: 'MCP-Resources-List', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#listing-resources' + } + ], + details: { + resourceCount: result.resources?.length || 0, + resources: result.resources?.map(r => r.uri) + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'resources-list', + name: 'ResourcesList', + description: 'Server lists available resources with valid structure', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Resources-List', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#listing-resources' + } + ] + }); + } + + return checks; + } +} + +export class ResourcesReadTextScenario implements ClientScenario { + name = 'resources-read-text'; + description = 'Test reading text resource'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.readResource({ + uri: 'test://static-text' + }); + + // Validate response + const errors: string[] = []; + if (!result.contents) errors.push('Missing contents array'); + if (!Array.isArray(result.contents)) errors.push('contents is not an array'); + if (result.contents.length === 0) errors.push('contents array is empty'); + + const content = result.contents[0]; + if (content) { + if (!content.uri) errors.push('Content missing uri'); + if (!content.mimeType) errors.push('Content missing mimeType'); + if (!content.text) errors.push('Content missing text field'); + } + + checks.push({ + id: 'resources-read-text', + name: 'ResourcesReadText', + description: 'Read text resource successfully', + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: [ + { + id: 'MCP-Resources-Read', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#reading-resources' + } + ], + details: { + uri: content?.uri, + mimeType: content?.mimeType, + hasText: !!content?.text + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'resources-read-text', + name: 'ResourcesReadText', + description: 'Read text resource successfully', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Resources-Read', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#reading-resources' + } + ] + }); + } + + return checks; + } +} + +export class ResourcesReadBinaryScenario implements ClientScenario { + name = 'resources-read-binary'; + description = 'Test reading binary resource'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.readResource({ + uri: 'test://static-binary' + }); + + // Validate response + const errors: string[] = []; + if (!result.contents) errors.push('Missing contents array'); + if (result.contents.length === 0) errors.push('contents array is empty'); + + const content = result.contents[0]; + if (content) { + if (!content.uri) errors.push('Content missing uri'); + if (!content.mimeType) errors.push('Content missing mimeType'); + if (!content.blob) errors.push('Content missing blob field'); + } + + checks.push({ + id: 'resources-read-binary', + name: 'ResourcesReadBinary', + description: 'Read binary resource successfully', + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: [ + { + id: 'MCP-Resources-Read', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#reading-resources' + } + ], + details: { + uri: content?.uri, + mimeType: content?.mimeType, + hasBlob: !!content?.blob + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'resources-read-binary', + name: 'ResourcesReadBinary', + description: 'Read binary resource successfully', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Resources-Read', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#reading-resources' + } + ] + }); + } + + return checks; + } +} + +export class ResourcesTemplateReadScenario implements ClientScenario { + name = 'resources-templates-read'; + description = 'Test reading resource from template'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.readResource({ + uri: 'test://template/123/data' + }); + + // Validate response + const errors: string[] = []; + if (!result.contents) errors.push('Missing contents array'); + if (result.contents.length === 0) errors.push('contents array is empty'); + + const content = result.contents[0]; + if (content) { + if (!content.uri) errors.push('Content missing uri'); + if (!content.text && !content.blob) errors.push('Content missing text or blob'); + + // Check if parameter was substituted + const text = content.text || (content.blob ? '[binary]' : ''); + if (typeof text === 'string' && !text.includes('123')) { + errors.push('Parameter substitution not reflected in content'); + } + } + + checks.push({ + id: 'resources-templates-read', + name: 'ResourcesTemplateRead', + description: 'Read resource from template with parameter substitution', + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: [ + { + id: 'MCP-Resources-Templates', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-templates' + } + ], + details: { + uri: content?.uri, + content: content?.text || content?.blob + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'resources-templates-read', + name: 'ResourcesTemplateRead', + description: 'Read resource from template with parameter substitution', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Resources-Templates', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-templates' + } + ] + }); + } + + return checks; + } +} + +export class ResourcesSubscribeScenario implements ClientScenario { + name = 'resources-subscribe'; + description = 'Test subscribing to resource updates'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + await connection.client.subscribeResource({ + uri: 'test://watched-resource' + }); + + checks.push({ + id: 'resources-subscribe', + name: 'ResourcesSubscribe', + description: 'Subscribe to resource successfully', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'MCP-Resources-Subscribe', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-subscriptions' + } + ] + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'resources-subscribe', + name: 'ResourcesSubscribe', + description: 'Subscribe to resource successfully', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Resources-Subscribe', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-subscriptions' + } + ] + }); + } + + return checks; + } +} + +export class ResourcesUnsubscribeScenario implements ClientScenario { + name = 'resources-unsubscribe'; + description = 'Test unsubscribing from resource'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + // First subscribe + await connection.client.subscribeResource({ + uri: 'test://watched-resource' + }); + + // Then unsubscribe + await connection.client.unsubscribeResource({ + uri: 'test://watched-resource' + }); + + checks.push({ + id: 'resources-unsubscribe', + name: 'ResourcesUnsubscribe', + description: 'Unsubscribe from resource successfully', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'MCP-Resources-Subscribe', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-subscriptions' + } + ] + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'resources-unsubscribe', + name: 'ResourcesUnsubscribe', + description: 'Unsubscribe from resource successfully', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Resources-Subscribe', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-subscriptions' + } + ] + }); + } + + return checks; + } +} diff --git a/src/scenarios/server/tools.ts b/src/scenarios/server/tools.ts new file mode 100644 index 0000000..21a2302 --- /dev/null +++ b/src/scenarios/server/tools.ts @@ -0,0 +1,807 @@ +/** + * Tools test scenarios for MCP servers + */ + +import { ClientScenario, ConformanceCheck } from '../../types.js'; +import { connectToServer, NotificationCollector } from './client-helper.js'; +import { CallToolResultSchema, CreateMessageRequestSchema, ElicitRequestSchema, Progress } from '@modelcontextprotocol/sdk/types.js'; + +export class ToolsListScenario implements ClientScenario { + name = 'tools-list'; + description = 'Test listing available tools'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.listTools(); + + // 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) + } + }); + + await connection.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'; + description = 'Test calling a tool that returns simple text'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.callTool({ + name: 'test_simple_text', + 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 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 connection.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'; + description = 'Test calling a tool that returns image content'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.callTool({ + 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 connection.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'; + description = 'Test tool returning multiple content types'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.callTool({ + 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 connection.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'; + description = 'Test tool that sends log messages during execution'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + const notifications = new NotificationCollector(connection.client); + + 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'; + description = 'Test tool error reporting'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result: any = await connection.client.callTool({ + 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 connection.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'; + description = 'Test tool that reports progress notifications'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + const progressUpdates: Array = []; + // TODO: investigate why await connection.client.callTool didn't work for progress. + const result = await connection.client.request( + { + method: 'tools/call', + params: { + name: 'test_tool_with_progress', + arguments: {}, + _meta: { + progressToken: 'progress-test-1' + } + } + }, + CallToolResultSchema, + { + onprogress: progress => { + progressUpdates.push(progress); + } + } + ); + + 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.map((n: Progress) => n), + result + } + }); + + await connection.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'; + description = 'Test tool that requests LLM sampling from client'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + 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'; + description = 'Test tool that requests user input (elicitation) from client'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + 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'; + description = 'Test calling a tool that returns audio content'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.callTool({ + 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 connection.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'; + description = 'Test calling a tool that returns embedded resource content'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + const result = await connection.client.callTool({ + 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 connection.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; + } +} diff --git a/src/scenarios/server/utils.ts b/src/scenarios/server/utils.ts new file mode 100644 index 0000000..511d4ac --- /dev/null +++ b/src/scenarios/server/utils.ts @@ -0,0 +1,136 @@ +/** + * Utilities test scenarios for MCP servers + */ + +import { ClientScenario, ConformanceCheck } from '../../types.js'; +import { connectToServer } from './client-helper.js'; + +export class LoggingSetLevelScenario implements ClientScenario { + name = 'logging-set-level'; + description = 'Test setting logging level'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + // Send logging/setLevel request + const result = await connection.client.setLoggingLevel('info'); + + // Validate response (should return empty object {}) + const errors: string[] = []; + if (result && Object.keys(result).length > 0) { + errors.push('Expected empty object {} response'); + } + + checks.push({ + id: 'logging-set-level', + name: 'LoggingSetLevel', + description: 'Server accepts logging level setting', + 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: { + result + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'logging-set-level', + name: 'LoggingSetLevel', + description: 'Server accepts logging level setting', + 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 CompletionCompleteScenario implements ClientScenario { + name = 'completion-complete'; + description = 'Test completion endpoint'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServer(serverUrl); + + // Send completion/complete request + const result = await connection.client.complete({ + ref: { + type: 'ref/prompt', + name: 'test_prompt_with_arguments' + }, + argument: { + name: 'arg1', + value: 'test' + } + }); + + // Validate response structure + const errors: string[] = []; + if (!result.completion) { + errors.push('Missing completion field'); + } else { + if (!result.completion.values) errors.push('Missing values array in completion'); + if (!Array.isArray(result.completion.values)) errors.push('completion.values is not an array'); + } + + checks.push({ + id: 'completion-complete', + name: 'CompletionComplete', + description: 'Server responds to completion requests', + status: errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: [ + { + id: 'MCP-Completion', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/completion' + } + ], + details: { + result + } + }); + + await connection.close(); + } catch (error) { + checks.push({ + id: 'completion-complete', + name: 'CompletionComplete', + description: 'Server responds to completion requests', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'MCP-Completion', + url: 'https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/completion' + } + ] + }); + } + + return checks; + } +} diff --git a/src/server-runner.ts b/src/server-runner.ts index 2ca5c19..bfd983d 100644 --- a/src/server-runner.ts +++ b/src/server-runner.ts @@ -48,6 +48,7 @@ async function main(): Promise { const args = process.argv.slice(2); let serverUrl: string | null = null; let scenario: string | null = null; + let runAll = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--server-url' && i + 1 < args.length) { @@ -56,40 +57,99 @@ async function main(): Promise { } else if (args[i] === '--scenario' && i + 1 < args.length) { scenario = args[i + 1]; i++; + } else if (args[i] === '--all') { + runAll = true; } } - if (!serverUrl || !scenario) { - console.error('Usage: server-runner --server-url --scenario '); - console.error('Example: server-runner --server-url http://localhost:3000 --scenario initialize'); + if (!serverUrl) { + console.error('Usage: server-runner --server-url [--scenario | --all]'); + console.error('Example: server-runner --server-url http://localhost:3000/mcp --scenario server-initialize'); + console.error('Example: server-runner --server-url http://localhost:3000/mcp --all'); + process.exit(1); + } + + if (!scenario && !runAll) { + console.error('Must specify either --scenario or --all'); process.exit(1); } try { - const result = await runServerConformanceTest(serverUrl, scenario); - - const denominator = result.checks.filter(c => c.status === 'SUCCESS' || c.status == 'FAILURE').length; - const passed = result.checks.filter(c => c.status === 'SUCCESS').length; - const failed = result.checks.filter(c => c.status === 'FAILURE').length; - - console.log(`Checks:\n${JSON.stringify(result.checks, null, 2)}`); - - console.log(`\nTest Results:`); - console.log(`Passed: ${passed}/${denominator}, ${failed} failed`); - - if (failed > 0) { - console.log('\nFailed Checks:'); - result.checks - .filter(c => c.status === 'FAILURE') - .forEach(c => { - console.log(` - ${c.name}: ${c.description}`); - if (c.errorMessage) { - console.log(` Error: ${c.errorMessage}`); - } - }); + const allResults: { scenario: string; checks: ConformanceCheck[] }[] = []; + + if (runAll) { + // Get all server scenarios + const { listClientScenarios } = await import('./scenarios/index.js'); + const scenarios = listClientScenarios(); + + console.log(`Running ${scenarios.length} scenarios against ${serverUrl}\n`); + + for (const scenarioName of scenarios) { + console.log(`\n=== Running scenario: ${scenarioName} ===`); + try { + const result = await runServerConformanceTest(serverUrl, scenarioName); + allResults.push({ scenario: scenarioName, checks: result.checks }); + } catch (error) { + console.error(`Failed to run scenario ${scenarioName}:`, error); + allResults.push({ + scenario: scenarioName, + checks: [ + { + id: scenarioName, + name: scenarioName, + description: `Failed to run scenario`, + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: error instanceof Error ? error.message : String(error) + } + ] + }); + } + } + + // Summary + console.log('\n\n=== SUMMARY ==='); + let totalPassed = 0; + let totalFailed = 0; + + for (const result of allResults) { + const passed = result.checks.filter(c => c.status === 'SUCCESS').length; + const failed = result.checks.filter(c => c.status === 'FAILURE').length; + totalPassed += passed; + totalFailed += failed; + + const status = failed === 0 ? '✓' : '✗'; + console.log(`${status} ${result.scenario}: ${passed} passed, ${failed} failed`); + } + + console.log(`\nTotal: ${totalPassed} passed, ${totalFailed} failed`); + process.exit(totalFailed > 0 ? 1 : 0); + } else if (scenario) { + const result = await runServerConformanceTest(serverUrl, scenario); + + const denominator = result.checks.filter(c => c.status === 'SUCCESS' || c.status == 'FAILURE').length; + const passed = result.checks.filter(c => c.status === 'SUCCESS').length; + const failed = result.checks.filter(c => c.status === 'FAILURE').length; + + console.log(`Checks:\n${JSON.stringify(result.checks, null, 2)}`); + + console.log(`\nTest Results:`); + console.log(`Passed: ${passed}/${denominator}, ${failed} failed`); + + if (failed > 0) { + console.log('\nFailed Checks:'); + result.checks + .filter(c => c.status === 'FAILURE') + .forEach(c => { + console.log(` - ${c.name}: ${c.description}`); + if (c.errorMessage) { + console.log(` Error: ${c.errorMessage}`); + } + }); + } + + process.exit(failed > 0 ? 1 : 0); } - - process.exit(failed > 0 ? 1 : 0); } catch (error) { console.error('Server test runner error:', error); process.exit(1);