Skip to content

Bug Report: Initialize Request Cancellation Violation #998

@younaman

Description

@younaman

Summary

The TypeScript SDK violates the MCP specification by allowing cancellation of initialize requests, which is explicitly forbidden.

Specification Violation

According to the MCP specification in src/types.ts line 183:

/**
 * A client MUST NOT attempt to cancel its `initialize` request.
 */

Current Implementation Issue

Location

File: src/shared/protocol.ts
Function: Protocol.request() method
Lines: 582-601 (cancel function)

Architecture Context

  • Protocol is a base class inherited by both Client and Server
  • The cancel function exists in the base class, so both Client and Server have this functionality
  • The specification constraint applies specifically to Client behavior

Problem

The cancel function unconditionally sends notifications/cancelled for all requests, including initialize requests, regardless of whether it's called from Client or Server:

const cancel = (reason: unknown) => {
  this._responseHandlers.delete(messageId);
  this._progressHandlers.delete(messageId);
  this._cleanupTimeout(messageId);

  this._transport
    ?.send({
      jsonrpc: "2.0",
      method: "notifications/cancelled",
      params: {
        requestId: messageId,
        reason: String(reason),
      },
    }, { relatedRequestId, resumptionToken, onresumptiontoken })
    .catch((error) =>
      this._onerror(new Error(`Failed to send cancellation: ${error}`)),
    );

  reject(reason);
};

Trigger Points

The cancel function is called in two scenarios:

  1. Manual cancellation via AbortSignal (lines 620-622):

    options?.signal?.addEventListener("abort", () => {
      cancel(options?.signal?.reason);
    });
  2. Automatic timeout cancellation (lines 625-629):

    const timeoutHandler = () => cancel(new McpError(
      ErrorCode.RequestTimeout,
      "Request timed out",
      { timeout }
    ));

Request Flow Analysis

  1. Client sends initialize request to Server via Protocol.request()
  2. If Client cancels (via AbortSignal or timeout), the cancel function is called
  3. cancel function sends notifications/cancelled to Server with the Client's messageId
  4. This violates the specification that Client must not attempt to cancel initialize requests

Impact

  • Specification Compliance: Violates explicit MCP requirement that Client must not cancel initialize requests
  • Protocol Correctness: Client sends notifications/cancelled to Server for initialize requests, which should not happen
  • Server Behavior: Servers may receive unexpected cancellation notifications for initialize requests from clients
  • Architecture Issue: The problem exists in the shared Protocol base class, but the constraint only applies to Client behavior

Proposed Fix

Add a check in the cancel function to prevent Client from sending cancellation notifications for initialize requests:

const cancel = (reason: unknown) => {
  this._responseHandlers.delete(messageId);
  this._progressHandlers.delete(messageId);
  this._cleanupTimeout(messageId);

  // Don't send cancellation notification if this is a Client trying to cancel initialize
  const isClientCancellingInitialize = 
    this instanceof Client && request.method === "initialize";
    
  if (!isClientCancellingInitialize) {
    this._transport
      ?.send({
        jsonrpc: "2.0",
        method: "notifications/cancelled",
        params: {
          requestId: messageId,
          reason: String(reason),
        },
      }, { relatedRequestId, resumptionToken, onresumptiontoken })
      .catch((error) =>
        this._onerror(new Error(`Failed to send cancellation: ${error}`)),
      );
  }

  reject(reason);
};

Alternative Approach

Alternatively, prevent AbortSignal from being passed to initialize requests in the Client.connect() method:

// In Client.connect(), strip signal from options for initialize
const initializeOptions = options ? { ...options, signal: undefined } : undefined;
const result = await this.request(
  {
    method: "initialize",
    params: {
      protocolVersion: LATEST_PROTOCOL_VERSION,
      capabilities: this._capabilities,
      clientInfo: this._clientInfo,
    },
  },
  InitializeResultSchema,
  initializeOptions
);

Testing

The fix should be tested with:

  1. Client cancelling initialize requests (should not send notifications/cancelled to Server)
  2. Client timeout of initialize requests (should not send notifications/cancelled to Server)
  3. Server cancelling any requests (should continue to send notifications/cancelled normally)
  4. Client cancelling non-initialize requests (should continue to send notifications/cancelled normally)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions