Skip to content

SDK MCP connection session ignores connect AbortSignal #26

Description

@AdamsukS

Summary

MCPConnectionSession.connect(signal?: AbortSignal) accepts cancellation, but the session returned
by SDKMCPConnectionSessionFactory.create() ignores the signal. Both an already-aborted signal and
an abort during initialization still result in a successful connection.

Version

  • @codesoul-co/hypha-mcp: 1.0.1
  • Hypha source/current main checked: ac80a8f7d1fbc8136b3bd85d94c48cf6e18dedf5
  • Transitive @modelcontextprotocol/sdk: 1.30.0
  • Transport: Streamable HTTP, isolated loopback fixture; no external service or credentials required.

Expected / actual

Case Expected Actual
Signal already aborted before connect Reject without sending initialize Sends initialize, then resolves
Abort while initialize response is pending Reject and clean up initialization Resolves after the server responds

The public interface declares connect(signal?: AbortSignal), while the SDK-backed implementation
declares async connect() and calls this.client.connect(this.transport) without request options.
The pinned MCP SDK accepts request options, including the signal, in its second connect argument.
The tool-adapter guide also asks implementations to preserve AbortSignal.

This report is specifically about the SDK connection-session contract. It does not claim that
MCPConnectionManager's initialization timeout or tool-call cancellation is absent.

Minimal reproduction

In a Node project with @codesoul-co/hypha-mcp@1.0.1, save as repro.cjs and run node repro.cjs.
Only the fixture profile below allows loopback HTTP; no production security settings are changed.

const { createServer } = require('node:http');
const { once } = require('node:events');
const { SDKMCPConnectionSessionFactory } = require('@codesoul-co/hypha-mcp');

async function probe(preAborted) {
  let initializeRequests = 0;
  const server = createServer(async (req, res) => {
    if (req.method !== 'POST') { res.writeHead(405).end(); return; }
    const chunks = [];
    for await (const chunk of req) chunks.push(chunk);
    const message = JSON.parse(Buffer.concat(chunks).toString());
    if (message.method !== 'initialize') { res.writeHead(202).end(); return; }
    initializeRequests++;
    setTimeout(() => {
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ jsonrpc: '2.0', id: message.id, result: {
        protocolVersion: '2025-11-25', capabilities: {},
        serverInfo: { name: 'abort-repro', version: '1.0.0' },
      } }));
    }, 150);
  });
  server.listen(0, '127.0.0.1');
  await once(server, 'listening');
  const session = new SDKMCPConnectionSessionFactory().create({
    id: 'abort-repro', mode: 'fixture',
    transport: { type: 'streamable_http', endpoint: `http://127.0.0.1:${server.address().port}/mcp` },
    egressPolicy: { allowedHosts: ['127.0.0.1'], denyPrivateNetworks: false, requireTls: false },
  });
  const controller = new AbortController();
  if (preAborted) controller.abort(new Error('cancelled before connect'));
  const timer = preAborted ? undefined : setTimeout(() => controller.abort(new Error('cancelled during connect')), 20);
  let outcome;
  try { await session.connect(controller.signal); outcome = 'resolved'; }
  catch { outcome = 'rejected'; }
  finally {
    clearTimeout(timer);
    await session.close();
    server.closeAllConnections();
    await new Promise((resolve) => server.close(resolve));
  }
  return { preAborted, aborted: controller.signal.aborted, initializeRequests, outcome };
}
(async () => {
  const results = [await probe(true), await probe(false)];
  console.log(JSON.stringify(results, null, 2));
  if (results.some((r) => r.outcome !== 'rejected')) process.exitCode = 1;
})().catch((error) => { console.error(error); process.exitCode = 1; });

Observed (exit 1):

[
  { "preAborted": true, "aborted": true, "initializeRequests": 1, "outcome": "resolved" },
  { "preAborted": false, "aborted": true, "initializeRequests": 1, "outcome": "resolved" }
]

Suggested acceptance checks: reject an already-aborted signal before initialization, propagate
in-flight cancellation to the MCP SDK, clean up the transport, and preserve successful connection
behavior when no signal is supplied.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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