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.
Summary
MCPConnectionSession.connect(signal?: AbortSignal)accepts cancellation, but the session returnedby
SDKMCPConnectionSessionFactory.create()ignores the signal. Both an already-aborted signal andan abort during initialization still result in a successful connection.
Version
@codesoul-co/hypha-mcp:1.0.1ac80a8f7d1fbc8136b3bd85d94c48cf6e18dedf5@modelcontextprotocol/sdk:1.30.0Expected / actual
connectThe public interface declares
connect(signal?: AbortSignal), while the SDK-backed implementationdeclares
async connect()and callsthis.client.connect(this.transport)without request options.The pinned MCP SDK accepts request options, including the signal, in its second
connectargument.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 asrepro.cjsand runnode repro.cjs.Only the fixture profile below allows loopback HTTP; no production security settings are changed.
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.