Skip to content

Add test suite for MCP conformance #6630

Description

@lloydrichards

Building on top of what @IMax153 did in #6610 I want to propose a more complete test suite for MCP spec across the various topics as well as how this will fit with something like #6617 where we might need a full suite to be able to test the same specifications against differently build McpServers.

While looking into this I found the Mcp has its own CLI for this conformance which I'm testing in parallel but gives an amazing list of scenario and specs in yaml which can be fairly quickly converted into vitest descriptions.

Following the Lifecycle.test.ts I would propose the following initial suite:

Full Suite
✓ McpServer (222)
  ✓ 2025-06-18 (222)

    ✓ Lifecycle (10)
      ✓ Lifecycle Phases (9)
        ✓ Initialization (5)
          ✓ MUST requires initialize to be the first request
          ✓ MUST rejects initialized notifications before initialize
          ✓ SCHEMA requires protocolVersion, capabilities, and clientInfo
          ✓ SCHEMA returns server capabilities and implementation information
          ✓ MUST accepts initialized after a successful initialize response
        ✓ Version Negotiation (2)
          ✓ MUST echoes every requested version supported by the server
          ✓ MUST negotiates an unsupported requested version to the latest supported version
        ✓ Capability Negotiation (1)
          ✓ SCHEMA advertises the capabilities provided by the server
      ✓ Operation (1)
        ✓ MUST continues to use the version negotiated during initialization
      ✓ Error Handling (1)
        ✓ SCENARIO handles protocol version mismatch through version negotiation

    ↓ Base Protocol (18)
      ↓ Messages (17)
        ↓ Requests (6)
          ↓ SCHEMA accepts JSON-RPC 2.0 requests with string identifiers
          ↓ SCHEMA accepts JSON-RPC 2.0 requests with numeric identifiers
          ↓ MUST rejects requests with an invalid JSON-RPC version
          ↓ MUST rejects requests without an identifier
          ↓ MUST returns method not found for unknown request methods
          ↓ MUST returns invalid params for request payloads that do not match the method schema
        ↓ Responses (5)
          ↓ MUST returns exactly one result response for a successful request
          ↓ MUST returns exactly one error response for a failed request
          ↓ SCHEMA preserves the request identifier in result responses
          ↓ SCHEMA preserves the request identifier in error responses
          ↓ MUST does not include both result and error in a response
        ↓ Notifications (3)
          ↓ MUST accepts notifications without an identifier
          ↓ MUST does not send a JSON-RPC response for a notification
          ↓ MUST rejects notification messages that include an identifier
        ↓ MUST does not accept JSON-RPC batch messages
        ↓ MUST returns a parse error for malformed JSON
        ↓ MUST returns an invalid request error for malformed JSON-RPC messages
      ↓ General fields (1)
        ↓ SCHEMA preserves additional metadata fields

    ↓ Transports (39)
      ↓ stdio (8)
        ↓ MUST reads UTF-8 JSON-RPC messages from stdin
        ↓ MUST writes UTF-8 JSON-RPC messages to stdout
        ↓ MUST delimits messages with newlines
        ↓ MUST does not emit embedded newlines inside a message
        ↓ MUST does not write non-MCP output to stdout
        ↓ MAY may write diagnostic output to stderr without corrupting the protocol stream
        ↓ SCENARIO processes consecutive messages independently
        ↓ MUST shuts down when the client closes stdin
      ↓ Streamable HTTP (31)
        ↓ Sending Messages to the Server (9)
          ↓ MUST accepts JSON-RPC requests through POST on the MCP endpoint
          ↓ MUST accepts JSON-RPC notifications through POST on the MCP endpoint
          ↓ MUST accepts JSON-RPC responses through POST on the MCP endpoint
          ↓ MUST requires the application/json content type for POST requests
          ↓ MUST requires clients to accept application/json and text/event-stream
          ↓ MUST returns application/json for a single JSON-RPC response
          ↓ MUST returns text/event-stream when streaming multiple messages
          ↓ MUST returns an empty 202 response for accepted notifications and responses
          ↓ MUST rejects unsupported HTTP methods with method not allowed
        ↓ Listening for Messages from the Server (4)
          ↓ MAY allows a client to open an SSE stream with GET
          ↓ MUST returns method not allowed for GET when the server does not offer an SSE stream
          ↓ MUST sends only JSON-RPC requests and notifications on the SSE stream
          ↓ MAY allows a client to close an SSE stream without closing its session
        ↓ Multiple Connections (3)
          ↓ MAY supports multiple simultaneous SSE streams for one session
          ↓ SCENARIO routes each server message to only one connected stream
          ↓ SCENARIO keeps messages from different sessions isolated
        ↓ Resumability and Redelivery (3)
          ↓ MAY assigns an event identifier to resumable SSE events
          ↓ MAY resumes after the last received event identifier
          ↓ MUST does not replay messages from a different stream
        ↓ Session Management (6)
          ↓ MUST returns an MCP session identifier during initialization
          ↓ MUST uses a globally unique and cryptographically secure session identifier
          ↓ MUST requires the session identifier on subsequent HTTP requests
          ↓ MUST rejects an unknown session identifier with not found
          ↓ MUST allows the client to terminate a session with DELETE
          ↓ MUST rejects requests for a terminated session
        ↓ Protocol Version Header (4)
          ↓ MUST requires the negotiated protocol version on subsequent HTTP requests
          ↓ MUST accepts a protocol version supported by the negotiated session
          ↓ MUST rejects an unsupported protocol version with bad request
          ↓ MUST returns the negotiated protocol version on responses
        ↓ Security (2)
          ↓ MUST validates the Origin header to prevent DNS rebinding attacks
          ↓ SHOULD binds local servers to localhost instead of all network interfaces by default

    ↓ Utilities (20)
      ↓ Ping (3)
        ↓ MUST responds to a client ping with an empty result
        ↓ MAY can send a ping to an initialized client
        ↓ SCENARIO keeps ping failures isolated to the unresponsive peer
      ↓ Cancellation (9)
        ↓ MUST accepts a cancellation notification for an in-flight request
        ↓ MUST uses the original request identifier in a cancellation notification
        ↓ MUST does not send a response to a cancellation notification
        ↓ SHOULD stops work on the cancelled request
        ↓ MUST does not return a successful response after cancellation
        ↓ MUST ignores cancellation for an unknown request identifier
        ↓ MUST ignores cancellation for an already completed request
        ↓ SCENARIO does not cancel unrelated concurrent requests
        ↓ MUST does not send cancellation notifications for initialize
      ↓ Progress (8)
        ↓ MUST accepts string progress tokens
        ↓ MUST accepts numeric progress tokens
        ↓ MUST echoes the requested token in progress notifications
        ↓ MUST associates progress notifications with only the originating request
        ↓ MUST does not emit progress when the request omitted a progress token
        ↓ MUST reports non-decreasing progress values
        ↓ SCHEMA preserves the optional total and message
        ↓ MUST does not reuse an active progress token for another request

    ↓ Tools (25)
      ↓ Capabilities (3)
        ↓ MUST advertises the tools capability when tools are registered
        ↓ MUST does not advertise the tools capability when no tools are registered
        ↓ MUST advertises listChanged only when tool list change notifications are supported
      ↓ Listing Tools (5)
        ↓ MUST lists every tool visible to the initialized client
        ↓ MUST returns an empty list when no tools are registered
        ↓ SCHEMA preserves tool names, titles, descriptions, annotations, and metadata
        ↓ MUST returns each tool input schema
        ↓ MUST returns each declared tool output schema
      ↓ Calling Tools (15)
        ↓ MUST calls a registered tool with valid arguments
        ↓ MUST rejects an unknown tool name
        ↓ MUST rejects arguments that do not match the input schema
        ↓ MUST does not invoke a tool handler when argument validation fails
        ↓ SCHEMA returns text content
        ↓ SCHEMA returns image content
        ↓ SCHEMA returns audio content
        ↓ SCHEMA returns resource links
        ↓ SCHEMA returns embedded resources
        ↓ MUST returns multiple content items in order
        ↓ SCHEMA returns structured content
        ↓ SCHEMA validates structured content against the output schema
        ↓ MUST returns tool execution failures with isError
        ↓ MUST keeps tool execution errors distinct from protocol errors
        ↓ SHOULD does not expose defects or internal error details
      ↓ List Changed Notification (2)
        ↓ SHOULD sends a tool list changed notification when the advertised list changes
        ↓ SHOULD does not send tool list changed notifications when listChanged is not advertised

    ↓ Resources (26)
      ↓ Capabilities (4)
        ↓ MUST advertises resources when resources or templates are registered
        ↓ MUST does not advertise resources when no resources or templates are registered
        ↓ MUST advertises subscribe only when resource subscriptions are supported
        ↓ MUST advertises listChanged only when resource list change notifications are supported
      ↓ Listing Resources (3)
        ↓ MUST lists every resource visible to the initialized client
        ↓ MUST returns an empty list when no resources are registered
        ↓ SCHEMA preserves resource URI, name, title, description, MIME type, size, annotations, and metadata
      ↓ Reading Resources (6)
        ↓ MUST reads text resource contents
        ↓ MUST reads binary resource contents as base64
        ↓ SCHEMA preserves the resource URI and MIME type in returned contents
        ↓ MUST returns multiple resource contents in order
        ↓ MUST returns the specified error for an unknown resource URI
        ↓ MUST does not return an empty contents array for a missing resource
      ↓ Resource Templates (5)
        ↓ MUST lists every registered resource template
        ↓ SCHEMA preserves template URI, name, title, description, MIME type and annotations
        ↓ MUST matches a concrete URI against its resource template
        ↓ MUST decodes template parameters before invoking the handler
        ↓ MUST does not invoke the handler when template parameter decoding fails
      ↓ List Changed Notification (2)
        ↓ SHOULD sends a resource list changed notification when the advertised list changes
        ↓ SHOULD does not send resource list changed notifications when listChanged is not advertised
      ↓ Subscriptions (6)
        ↓ MUST subscribes to updates for a resource URI when subscriptions are advertised
        ↓ MUST sends resource updated notifications only for subscribed resources
        ↓ MUST includes the updated resource URI in each notification
        ↓ MUST unsubscribes from updates for a resource URI
        ↓ MUST does not send updates after a resource is unsubscribed
        ↓ MUST rejects resource subscriptions when subscribe is not advertised

    ↓ Prompts (20)
      ↓ Capabilities (3)
        ↓ MUST advertises prompts when prompts are registered
        ↓ MUST does not advertise prompts when no prompts are registered
        ↓ MUST advertises listChanged only when prompt list change notifications are supported
      ↓ Listing Prompts (4)
        ↓ MUST lists every prompt visible to the initialized client
        ↓ MUST returns an empty list when no prompts are registered
        ↓ SCHEMA preserves prompt names, titles, descriptions, arguments, and metadata
        ↓ MUST marks required and optional prompt arguments correctly
      ↓ Getting Prompts (11)
        ↓ MUST gets a registered prompt without arguments
        ↓ MUST gets a registered prompt with valid arguments
        ↓ MUST rejects an unknown prompt name
        ↓ MUST rejects missing required prompt arguments
        ↓ MUST rejects prompt arguments with invalid values
        ↓ MUST does not invoke the prompt handler when argument validation fails
        ↓ SCHEMA preserves the prompt description and message order
        ↓ MUST returns text message content
        ↓ MUST returns image message content
        ↓ MUST returns audio message content
        ↓ MUST returns embedded resource message content
      ↓ List Changed Notification (2)
        ↓ SHOULD sends a prompt list changed notification when the advertised list changes
        ↓ SHOULD does not send prompt list changed notifications when listChanged is not advertised

    ↓ Completion (12)
      ↓ Capabilities (2)
        ↓ MUST advertises completions when argument completion is supported
        ↓ MUST does not advertise completions when argument completion is unsupported
      ↓ Requesting Completions (10)
        ↓ MUST completes a prompt argument
        ↓ MUST completes a resource template argument
        ↓ MUST passes previously resolved argument context to the completion handler
        ↓ MUST rejects an unknown prompt reference
        ↓ MUST rejects an unknown resource template reference
        ↓ MUST rejects an unknown argument name
        ↓ MUST returns completion values in order
        ↓ MUST returns the total number of available completion values
        ↓ MUST reports when additional completion values are available
        ↓ MUST returns at most one hundred completion values

    ↓ Logging (11)
      ↓ Capabilities (2)
        ↓ MUST advertises logging when log notifications are supported
        ↓ MUST does not advertise logging when log notifications are unsupported
      ↓ Setting Log Level (5)
        ↓ MUST accepts every specified log level
        ↓ MUST rejects an unknown log level
        ↓ SHOULD updates the minimum level for subsequent log notifications
        ↓ SHOULD sends notifications at the selected level and higher
        ↓ MUST does not send notifications below the selected level
      ↓ Log Message Notifications (4)
        ↓ SCHEMA preserves the log level, logger name, and data
        ↓ MUST allows arbitrary JSON-compatible log data
        ↓ MUST does not send a response for a log message notification
        ↓ SCENARIO does not corrupt the stdio protocol stream with log output

    ↓ Pagination (11)
      ↓ Pagination Model (7)
        ↓ MUST treats cursors as opaque strings
        ↓ MUST starts from the first page when the request omits a cursor
        ↓ MUST returns a next cursor when another page is available
        ↓ MUST uses the next cursor to retrieve the following page
        ↓ MUST omits the next cursor after the final page
        ↓ SCENARIO does not duplicate or omit items while traversing pages
        ↓ MUST returns an appropriate error for an invalid cursor
      ↓ Paginated Operations (4)
        ↓ MUST paginates tools/list
        ↓ MUST paginates resources/list
        ↓ MUST paginates resources/templates/list
        ↓ MUST paginates prompts/list

    ↓ Roots (11)
      ↓ Capabilities (3)
        ↓ MUST sends roots requests only when the client advertises roots
        ↓ MUST does not send roots requests when the client omits the roots capability
        ↓ MUST observes whether the client advertises roots list changes
      ↓ Listing Roots (5)
        ↓ MUST requests the client's roots
        ↓ MUST accepts roots with file URIs
        ↓ MAY preserves optional root names
        ↓ MAY accepts an empty roots list
        ↓ MUST surfaces client errors returned by roots/list
      ↓ Root List Changes (3)
        ↓ SHOULD accepts roots list changed notifications from capable clients
        ↓ SHOULD refreshes roots after a roots list changed notification
        ↓ MUST does not rely on roots as an authorization boundary

    ↓ Sampling (10)
      ↓ Capabilities (2)
        ↓ MUST sends sampling requests only when the client advertises sampling
        ↓ MUST does not send sampling requests when the client omits the sampling capability
      ↓ Creating Messages (8)
        ↓ MUST sends the requested message history in order
        ↓ SCHEMA preserves the system prompt and model preferences
        ↓ SCHEMA preserves maximum tokens, stop sequences, metadata, and context inclusion
        ↓ MUST accepts text sampling content
        ↓ MUST accepts image sampling content
        ↓ MUST accepts audio sampling content
        ↓ MUST decodes the selected model, role, content, and stop reason
        ↓ MUST surfaces sampling errors returned by the client

    ↓ Elicitation (9)
      ↓ Capabilities (2)
        ↓ MUST sends elicitation requests only when the client advertises elicitation
        ↓ MUST does not send elicitation requests when the client omits the elicitation capability
      ↓ Form Mode (6)
        ↓ MUST sends a message and requested schema
        ↓ SCHEMA limits requested schemas to supported primitive form fields
        ↓ MUST decodes accepted content against the requested schema
        ↓ MUST returns a typed failure when the user declines
        ↓ MUST interrupts the operation when the user cancels
        ↓ MUST rejects accepted content that does not match the requested schema
      ↓ Security (1)
        ↓ MUST does not request sensitive information through form mode

 Test Files  13 (1 implemented | 12 skipped)
      Tests  222 (10 implemented | 212 skipped)

At the moment this is only for 2025-06-18 and the ones added for the Lifecycle are for 2025-11-25 so what we might need to do is move a lot of these into a shared suite which can then be run based on the ProtocolAdaptor:

export const suite = (
  protocol: McpProtocol.ProtocolAdapter
) => {
  const serverLayer = makeServerLayer({
    name: "McpServerTest",
    protocols: [protocol]
  })

  const makeHarness = makeRawHttpHarness(serverLayer)

  describe(`McpServer (${protocol.protocolVersion})`, () => {
    describe("Lifecycle", () => {
      // Shared lifecycle tests
    })

    describe("Base Protocol", () => {
      // Shared JSON-RPC tests
    })
  })
}

McpServerTest.suite(McpProtocol.v2025_06_18)
// McpServerTest.suite(McpProtocol.v2025_11_25)
// McpServerTest.suite(McpProtocol.v2026_07_28)

But this will be easier to split up once the other protocol is implemented (#6157) in a composable way.

For now if anything looks off about the test suite for 2025-06-18 at a high level it would be good to make changes now before writing all 200 tests 😬

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions