LibreChat/e2e/setup/fake-mcp-dynamic-network-server.js
Danny Avila 1bccc2bc18
📡 fix: Refresh MCP Tools After List-Changed Notifications (#14686)
* fix(mcp): handle dynamic tool list changes

Co-authored-by: Pascal Garber <pascal@artandcode.studio>

* test(mcp): fix CI validation

* fix(mcp): keep dynamic tool catalogs live

* fix(mcp): harden dynamic catalog lifecycle

* test(mcp): use typed startup connection

* test(mcp): isolate dynamic e2e fixtures

* fix(mcp): refresh tools after reconnect

* fix(mcp): close dynamic catalog cache gaps

* test(mcp): update OAuth connection mocks

* fix(mcp): preserve app snapshot ownership

* style(mcp): sort connection imports

* fix(mcp): close review race conditions

* fix(mcp): preserve cache ownership edges

* fix(mcp): harden recovery lifecycle

* fix(mcp): guard tool-less app refresh

* fix(mcp): fence distributed cache races

* fix(mcp): retire stale connection state

* fix(mcp): keep tool snapshots authoritative

* fix(mcp): fence stale app tool publications

* style(mcp): sort repository test imports

* test(mcp): mock empty startup publication

* fix(mcp): preserve app publication generations

* fix(mcp): harden publication recovery races

* fix(mcp): address tool catalogs by runtime config

* fix(mcp): load scoped catalogs for assistant writes

* fix(mcp): harden catalog publication recovery

* fix(mcp): serialize forced connection replacement

* fix(mcp): serialize ordinary creation with replacements

* fix(mcp): harden catalog fallback boundaries

* fix(mcp): close lifecycle fencing gaps

* fix(mcp): preserve catalog authority on failures

* fix(mcp): compensate failed catalog mutations

* fix(mcp): fence catalog refresh ordering

* style(mcp): sort agent loader imports

* fix(mcp): cancel stale connection creation

* fix(mcp): fence catalog coordination

* fix(mcp): close catalog race windows

* fix(mcp): harden cross-pod catalog fencing

* fix(mcp): close catalog lifecycle edges

* style(mcp): sort assistant imports

* fix(mcp): reject stale recovery authority

* fix(mcp): restore static catalog on every startup

* fix(mcp): order app catalog publications

* style(mcp): sort catalog revision imports

* fix(mcp): separate catalog allocation and commit fences

---------

Co-authored-by: Pascal Garber <pascal@artandcode.studio>
2026-08-08 13:50:21 -04:00

125 lines
4.2 KiB
JavaScript

#!/usr/bin/env node
const http = require('node:http');
const { randomUUID } = require('node:crypto');
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { SSEServerTransport } = require('@modelcontextprotocol/sdk/server/sse.js');
const {
StreamableHTTPServerTransport,
} = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
const { watchDynamicTool } = require('./dynamic-mcp-tools');
const PORT = Number.parseInt(process.env.E2E_MCP_DYNAMIC_PORT || '8766', 10);
const HOST = '127.0.0.1';
function createMcpServer(name, transportLabel) {
const server = new McpServer({ name, version: '1.0.0' });
server.registerTool(
'transport_probe',
{
description: `Confirms that the real ${transportLabel} MCP transport is connected.`,
inputSchema: {},
},
async () => ({ content: [{ type: 'text', text: `${transportLabel} connected` }] }),
);
const stopWatching = watchDynamicTool(server);
return { server, stopWatching };
}
/** @type {Map<string, { transport: InstanceType<typeof StreamableHTTPServerTransport>, server: InstanceType<typeof McpServer>, stopWatching: () => void }>} */
const streamableSessions = new Map();
/** @type {Map<string, { transport: InstanceType<typeof SSEServerTransport>, server: InstanceType<typeof McpServer>, stopWatching: () => void }>} */
const sseSessions = new Map();
async function handleStreamableRequest(req, res) {
const sessionId = req.headers['mcp-session-id'];
let session = typeof sessionId === 'string' ? streamableSessions.get(sessionId) : undefined;
if (!session) {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
const mcp = createMcpServer('e2e-streamable', 'Streamable HTTP');
session = { transport, ...mcp };
await mcp.server.connect(transport);
}
await session.transport.handleRequest(req, res);
const connectedSessionId = session.transport.sessionId;
if (connectedSessionId && !streamableSessions.has(connectedSessionId)) {
streamableSessions.set(connectedSessionId, session);
session.transport.onclose = () => {
streamableSessions.delete(connectedSessionId);
session.stopWatching();
};
}
}
async function handleSSEConnect(res) {
const transport = new SSEServerTransport('/messages', res);
const mcp = createMcpServer('e2e-sse', 'SSE');
const session = { transport, ...mcp };
sseSessions.set(transport.sessionId, session);
transport.onclose = () => {
sseSessions.delete(transport.sessionId);
session.stopWatching();
};
await mcp.server.connect(transport);
}
const httpServer = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${req.headers.host}`);
if (req.method === 'GET' && url.pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
return;
}
if (url.pathname === '/mcp') {
await handleStreamableRequest(req, res);
return;
}
if (req.method === 'GET' && url.pathname === '/sse') {
await handleSSEConnect(res);
return;
}
if (req.method === 'POST' && url.pathname === '/messages') {
const sessionId = url.searchParams.get('sessionId') || '';
const session = sseSessions.get(sessionId);
if (!session) {
res.writeHead(404);
res.end();
return;
}
await session.transport.handlePostMessage(req, res);
return;
}
res.writeHead(404);
res.end();
} catch (error) {
console.error('[fake-mcp-dynamic-network-server] request failed', error);
if (!res.headersSent) {
res.writeHead(500);
}
res.end();
}
});
async function shutdown() {
const sessions = [...streamableSessions.values(), ...sseSessions.values()];
streamableSessions.clear();
sseSessions.clear();
await Promise.all(
sessions.map(async ({ server, stopWatching }) => {
stopWatching();
await server.close().catch(() => undefined);
}),
);
httpServer.close(() => process.exit(0));
}
process.once('SIGINT', shutdown);
process.once('SIGTERM', shutdown);
httpServer.listen(PORT, HOST, () => {
console.log(`[e2e] dynamic MCP server listening on http://${HOST}:${PORT}`);
});