This commit is contained in:
Jerome 2025-06-13 16:04:25 +01:00
commit 3207959af0
No known key found for this signature in database
3 changed files with 296 additions and 236 deletions

View file

@ -349,9 +349,9 @@ const App = () => {
const headers: HeadersInit = {}; const headers: HeadersInit = {};
const proxyAuthToken = getMCPProxyAuthToken(config); const proxyAuthToken = getMCPProxyAuthToken(config);
if (proxyAuthToken) { if (proxyAuthToken) {
headers['Authorization'] = `Bearer ${proxyAuthToken}`; headers["Authorization"] = `Bearer ${proxyAuthToken}`;
} }
fetch(`${getMCPProxyAddress(config)}/config`, { headers }) fetch(`${getMCPProxyAddress(config)}/config`, { headers })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {

View file

@ -246,7 +246,7 @@ export function useConnection({
const proxyAuthToken = getMCPProxyAuthToken(config); const proxyAuthToken = getMCPProxyAuthToken(config);
const headers: HeadersInit = {}; const headers: HeadersInit = {};
if (proxyAuthToken) { if (proxyAuthToken) {
headers['Authorization'] = `Bearer ${proxyAuthToken}`; headers["Authorization"] = `Bearer ${proxyAuthToken}`;
} }
const proxyHealthResponse = await fetch(proxyHealthUrl, { headers }); const proxyHealthResponse = await fetch(proxyHealthUrl, { headers });
const proxyHealth = await proxyHealthResponse.json(); const proxyHealth = await proxyHealthResponse.json();
@ -269,7 +269,7 @@ export function useConnection({
const isProxyAuthError = (error: unknown): boolean => { const isProxyAuthError = (error: unknown): boolean => {
return ( return (
error instanceof Error && error instanceof Error &&
error.message.includes("Authentication required. Use the session token") error.message.includes("Authentication required. Use the session token")
); );
}; };
@ -335,7 +335,7 @@ export function useConnection({
const proxyAuthToken = getMCPProxyAuthToken(config); const proxyAuthToken = getMCPProxyAuthToken(config);
const proxyHeaders: HeadersInit = {}; const proxyHeaders: HeadersInit = {};
if (proxyAuthToken) { if (proxyAuthToken) {
proxyHeaders['Authorization'] = `Bearer ${proxyAuthToken}`; proxyHeaders["Authorization"] = `Bearer ${proxyAuthToken}`;
} }
// Create appropriate transport // Create appropriate transport
@ -356,7 +356,11 @@ export function useConnection({
fetch: ( fetch: (
url: string | URL | globalThis.Request, url: string | URL | globalThis.Request,
init: RequestInit | undefined, init: RequestInit | undefined,
) => fetch(url, { ...init, headers: { ...headers, ...proxyHeaders } }), ) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
}, },
requestInit: { requestInit: {
headers: { ...headers, ...proxyHeaders }, headers: { ...headers, ...proxyHeaders },
@ -372,7 +376,11 @@ export function useConnection({
fetch: ( fetch: (
url: string | URL | globalThis.Request, url: string | URL | globalThis.Request,
init: RequestInit | undefined, init: RequestInit | undefined,
) => fetch(url, { ...init, headers: { ...headers, ...proxyHeaders } }), ) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
}, },
requestInit: { requestInit: {
headers: { ...headers, ...proxyHeaders }, headers: { ...headers, ...proxyHeaders },
@ -388,7 +396,11 @@ export function useConnection({
fetch: ( fetch: (
url: string | URL | globalThis.Request, url: string | URL | globalThis.Request,
init: RequestInit | undefined, init: RequestInit | undefined,
) => fetch(url, { ...init, headers: { ...headers, ...proxyHeaders } }), ) =>
fetch(url, {
...init,
headers: { ...headers, ...proxyHeaders },
}),
}, },
requestInit: { requestInit: {
headers: { ...headers, ...proxyHeaders }, headers: { ...headers, ...proxyHeaders },
@ -471,7 +483,8 @@ export function useConnection({
if (isProxyAuthError(error)) { if (isProxyAuthError(error)) {
toast({ toast({
title: "Proxy Authentication Required", title: "Proxy Authentication Required",
description: "Please enter the session token from the proxy server console in the Configuration settings.", description:
"Please enter the session token from the proxy server console in the Configuration settings.",
variant: "destructive", variant: "destructive",
}); });
setConnectionStatus("error"); setConnectionStatus("error");

View file

@ -89,46 +89,57 @@ app.use((req, res, next) => {
const webAppTransports: Map<string, Transport> = new Map<string, Transport>(); // Web app transports by web app sessionId const webAppTransports: Map<string, Transport> = new Map<string, Transport>(); // Web app transports by web app sessionId
const serverTransports: Map<string, Transport> = new Map<string, Transport>(); // Server Transports by web app sessionId const serverTransports: Map<string, Transport> = new Map<string, Transport>(); // Server Transports by web app sessionId
const sessionToken = randomBytes(32).toString('hex'); const sessionToken = randomBytes(32).toString("hex");
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH; const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
// Origin validation middleware to prevent DNS rebinding attacks // Origin validation middleware to prevent DNS rebinding attacks
const originValidationMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => { const originValidationMiddleware = (
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
const origin = req.headers.origin; const origin = req.headers.origin;
// Default origins based on CLIENT_PORT or use environment variable // Default origins based on CLIENT_PORT or use environment variable
const clientPort = process.env.CLIENT_PORT || '6274'; const clientPort = process.env.CLIENT_PORT || "6274";
const defaultOrigins = [ const defaultOrigins = [
`http://localhost:${clientPort}`, `http://localhost:${clientPort}`,
`http://127.0.0.1:${clientPort}` `http://127.0.0.1:${clientPort}`,
]; ];
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || defaultOrigins; const allowedOrigins =
process.env.ALLOWED_ORIGINS?.split(",") || defaultOrigins;
if (origin && !allowedOrigins.includes(origin)) { if (origin && !allowedOrigins.includes(origin)) {
console.error(`Invalid origin: ${origin}`); console.error(`Invalid origin: ${origin}`);
res.status(403).json({ res.status(403).json({
error: 'Forbidden - invalid origin', error: "Forbidden - invalid origin",
message: 'Request blocked to prevent DNS rebinding attacks. Configure allowed origins via environment variable.' message:
"Request blocked to prevent DNS rebinding attacks. Configure allowed origins via environment variable.",
}); });
return; return;
} }
next(); next();
}; };
const authMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => { const authMiddleware = (
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
if (authDisabled) { if (authDisabled) {
return next(); return next();
} }
const sendUnauthorized = () => { const sendUnauthorized = () => {
res.status(401).json({ res.status(401).json({
error: "Unauthorized", error: "Unauthorized",
message: "Authentication required. Use the session token shown in the console when starting the server." message:
"Authentication required. Use the session token shown in the console when starting the server.",
}); });
}; };
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) { if (!authHeader || !authHeader.startsWith("Bearer ")) {
sendUnauthorized(); sendUnauthorized();
return; return;
} }
@ -151,7 +162,7 @@ const authMiddleware = (req: express.Request, res: express.Response, next: expre
sendUnauthorized(); sendUnauthorized();
return; return;
} }
next(); next();
}; };
@ -216,38 +227,152 @@ const createTransport = async (req: express.Request): Promise<Transport> => {
} }
}; };
app.get("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => { app.get(
const sessionId = req.headers["mcp-session-id"] as string; "/mcp",
console.log(`Received GET message for sessionId ${sessionId}`); originValidationMiddleware,
try { authMiddleware,
const transport = webAppTransports.get( async (req, res) => {
sessionId, const sessionId = req.headers["mcp-session-id"] as string;
) as StreamableHTTPServerTransport; console.log(`Received GET message for sessionId ${sessionId}`);
if (!transport) {
res.status(404).end("Session not found");
return;
} else {
await transport.handleRequest(req, res);
}
} catch (error) {
console.error("Error in /mcp route:", error);
res.status(500).json(error);
}
});
app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let serverTransport: Transport | undefined;
if (!sessionId) {
try { try {
console.log("New StreamableHttp connection request"); const transport = webAppTransports.get(
sessionId,
) as StreamableHTTPServerTransport;
if (!transport) {
res.status(404).end("Session not found");
return;
} else {
await transport.handleRequest(req, res);
}
} catch (error) {
console.error("Error in /mcp route:", error);
res.status(500).json(error);
}
},
);
app.post(
"/mcp",
originValidationMiddleware,
authMiddleware,
async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let serverTransport: Transport | undefined;
if (!sessionId) {
try {
console.log("New StreamableHttp connection request");
try {
serverTransport = await createTransport(req);
} catch (error) {
if (error instanceof SseError && error.code === 401) {
console.error(
"Received 401 Unauthorized from MCP server:",
error.message,
);
res.status(401).json(error);
return;
}
throw error;
}
console.log("Created StreamableHttp server transport");
const webAppTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: randomUUID,
onsessioninitialized: (sessionId) => {
webAppTransports.set(sessionId, webAppTransport);
serverTransports.set(sessionId, serverTransport!);
console.log("Client <-> Proxy sessionId: " + sessionId);
},
});
console.log("Created StreamableHttp client transport");
await webAppTransport.start();
mcpProxy({
transportToClient: webAppTransport,
transportToServer: serverTransport,
});
await (webAppTransport as StreamableHTTPServerTransport).handleRequest(
req,
res,
req.body,
);
} catch (error) {
console.error("Error in /mcp POST route:", error);
res.status(500).json(error);
}
} else {
console.log(`Received POST message for sessionId ${sessionId}`);
try {
const transport = webAppTransports.get(
sessionId,
) as StreamableHTTPServerTransport;
if (!transport) {
res.status(404).end("Transport not found for sessionId " + sessionId);
} else {
await (transport as StreamableHTTPServerTransport).handleRequest(
req,
res,
);
}
} catch (error) {
console.error("Error in /mcp route:", error);
res.status(500).json(error);
}
}
},
);
app.delete(
"/mcp",
originValidationMiddleware,
authMiddleware,
async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
console.log(`Received DELETE message for sessionId ${sessionId}`);
let serverTransport: Transport | undefined;
if (sessionId) {
try {
serverTransport = serverTransports.get(
sessionId,
) as StreamableHTTPClientTransport;
if (!serverTransport) {
res.status(404).end("Transport not found for sessionId " + sessionId);
} else {
await (
serverTransport as StreamableHTTPClientTransport
).terminateSession();
webAppTransports.delete(sessionId);
serverTransports.delete(sessionId);
console.log(`Transports removed for sessionId ${sessionId}`);
}
res.status(200).end();
} catch (error) {
console.error("Error in /mcp route:", error);
res.status(500).json(error);
}
}
},
);
app.get(
"/stdio",
originValidationMiddleware,
authMiddleware,
async (req, res) => {
try {
console.log("New STDIO connection request");
let serverTransport: Transport | undefined;
try { try {
serverTransport = await createTransport(req); serverTransport = await createTransport(req);
console.log("Created server transport");
} catch (error) { } catch (error) {
if (error instanceof SseError && error.code === 401) { if (error instanceof SseError && error.code === 401) {
console.error( console.error(
"Received 401 Unauthorized from MCP server:", "Received 401 Unauthorized from MCP server. Authentication failure.",
error.message,
); );
res.status(401).json(error); res.status(401).json(error);
return; return;
@ -256,211 +381,127 @@ app.post("/mcp", originValidationMiddleware, authMiddleware, async (req, res) =>
throw error; throw error;
} }
console.log("Created StreamableHttp server transport");
const webAppTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: randomUUID,
onsessioninitialized: (sessionId) => {
webAppTransports.set(sessionId, webAppTransport);
serverTransports.set(sessionId, serverTransport!);
console.log("Client <-> Proxy sessionId: " + sessionId);
},
});
console.log("Created StreamableHttp client transport");
await webAppTransport.start();
mcpProxy({
transportToClient: webAppTransport,
transportToServer: serverTransport,
});
await (webAppTransport as StreamableHTTPServerTransport).handleRequest(
req,
res,
req.body,
);
} catch (error) {
console.error("Error in /mcp POST route:", error);
res.status(500).json(error);
}
} else {
console.log(`Received POST message for sessionId ${sessionId}`);
try {
const transport = webAppTransports.get(
sessionId,
) as StreamableHTTPServerTransport;
if (!transport) {
res.status(404).end("Transport not found for sessionId " + sessionId);
} else {
await (transport as StreamableHTTPServerTransport).handleRequest(
req,
res,
);
}
} catch (error) {
console.error("Error in /mcp route:", error);
res.status(500).json(error);
}
}
});
app.delete("/mcp", originValidationMiddleware, authMiddleware, async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
console.log(`Received DELETE message for sessionId ${sessionId}`);
let serverTransport: Transport | undefined;
if (sessionId) {
try {
serverTransport = serverTransports.get(
sessionId,
) as StreamableHTTPClientTransport;
if (!serverTransport) {
res.status(404).end("Transport not found for sessionId " + sessionId);
} else {
await (
serverTransport as StreamableHTTPClientTransport
).terminateSession();
webAppTransports.delete(sessionId);
serverTransports.delete(sessionId);
console.log(`Transports removed for sessionId ${sessionId}`);
}
res.status(200).end();
} catch (error) {
console.error("Error in /mcp route:", error);
res.status(500).json(error);
}
}
});
app.get("/stdio", originValidationMiddleware, authMiddleware, async (req, res) => {
try {
console.log("New STDIO connection request");
let serverTransport: Transport | undefined;
try {
serverTransport = await createTransport(req);
console.log("Created server transport");
} catch (error) {
if (error instanceof SseError && error.code === 401) {
console.error(
"Received 401 Unauthorized from MCP server. Authentication failure.",
);
res.status(401).json(error);
return;
}
throw error;
}
const webAppTransport = new SSEServerTransport("/message", res);
console.log("Created client transport");
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
serverTransports.set(webAppTransport.sessionId, serverTransport);
await webAppTransport.start();
(serverTransport as StdioClientTransport).stderr!.on("data", (chunk) => {
if (chunk.toString().includes("MODULE_NOT_FOUND")) {
webAppTransport.send({
jsonrpc: "2.0",
method: "notifications/stderr",
params: {
content: "Command not found, transports removed",
},
});
webAppTransport.close();
serverTransport.close();
webAppTransports.delete(webAppTransport.sessionId);
serverTransports.delete(webAppTransport.sessionId);
console.error("Command not found, transports removed");
} else {
webAppTransport.send({
jsonrpc: "2.0",
method: "notifications/stderr",
params: {
content: chunk.toString(),
},
});
}
});
mcpProxy({
transportToClient: webAppTransport,
transportToServer: serverTransport,
});
} catch (error) {
console.error("Error in /stdio route:", error);
res.status(500).json(error);
}
});
app.get("/sse", originValidationMiddleware, authMiddleware, async (req, res) => {
try {
console.log(
"New SSE connection request. NOTE: The sse transport is deprecated and has been replaced by StreamableHttp",
);
let serverTransport: Transport | undefined;
try {
serverTransport = await createTransport(req);
} catch (error) {
if (error instanceof SseError && error.code === 401) {
console.error(
"Received 401 Unauthorized from MCP server. Authentication failure.",
);
res.status(401).json(error);
return;
} else if (error instanceof SseError && error.code === 404) {
console.error(
"Received 404 not found from MCP server. Does the MCP server support SSE?",
);
res.status(404).json(error);
return;
} else if (JSON.stringify(error).includes("ECONNREFUSED")) {
console.error("Connection refused. Is the MCP server running?");
res.status(500).json(error);
} else {
throw error;
}
}
if (serverTransport) {
const webAppTransport = new SSEServerTransport("/message", res); const webAppTransport = new SSEServerTransport("/message", res);
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
console.log("Created client transport"); console.log("Created client transport");
serverTransports.set(webAppTransport.sessionId, serverTransport!);
console.log("Created server transport"); webAppTransports.set(webAppTransport.sessionId, webAppTransport);
serverTransports.set(webAppTransport.sessionId, serverTransport);
await webAppTransport.start(); await webAppTransport.start();
(serverTransport as StdioClientTransport).stderr!.on("data", (chunk) => {
if (chunk.toString().includes("MODULE_NOT_FOUND")) {
webAppTransport.send({
jsonrpc: "2.0",
method: "notifications/stderr",
params: {
content: "Command not found, transports removed",
},
});
webAppTransport.close();
serverTransport.close();
webAppTransports.delete(webAppTransport.sessionId);
serverTransports.delete(webAppTransport.sessionId);
console.error("Command not found, transports removed");
} else {
webAppTransport.send({
jsonrpc: "2.0",
method: "notifications/stderr",
params: {
content: chunk.toString(),
},
});
}
});
mcpProxy({ mcpProxy({
transportToClient: webAppTransport, transportToClient: webAppTransport,
transportToServer: serverTransport, transportToServer: serverTransport,
}); });
} catch (error) {
console.error("Error in /stdio route:", error);
res.status(500).json(error);
} }
} catch (error) { },
console.error("Error in /sse route:", error); );
res.status(500).json(error);
}
});
app.post("/message", originValidationMiddleware, authMiddleware, async (req, res) => { app.get(
try { "/sse",
const sessionId = req.query.sessionId; originValidationMiddleware,
console.log(`Received POST message for sessionId ${sessionId}`); authMiddleware,
async (req, res) => {
try {
console.log(
"New SSE connection request. NOTE: The sse transport is deprecated and has been replaced by StreamableHttp",
);
let serverTransport: Transport | undefined;
try {
serverTransport = await createTransport(req);
} catch (error) {
if (error instanceof SseError && error.code === 401) {
console.error(
"Received 401 Unauthorized from MCP server. Authentication failure.",
);
res.status(401).json(error);
return;
} else if (error instanceof SseError && error.code === 404) {
console.error(
"Received 404 not found from MCP server. Does the MCP server support SSE?",
);
res.status(404).json(error);
return;
} else if (JSON.stringify(error).includes("ECONNREFUSED")) {
console.error("Connection refused. Is the MCP server running?");
res.status(500).json(error);
} else {
throw error;
}
}
const transport = webAppTransports.get( if (serverTransport) {
sessionId as string, const webAppTransport = new SSEServerTransport("/message", res);
) as SSEServerTransport; webAppTransports.set(webAppTransport.sessionId, webAppTransport);
if (!transport) { console.log("Created client transport");
res.status(404).end("Session not found"); serverTransports.set(webAppTransport.sessionId, serverTransport!);
return; console.log("Created server transport");
await webAppTransport.start();
mcpProxy({
transportToClient: webAppTransport,
transportToServer: serverTransport,
});
}
} catch (error) {
console.error("Error in /sse route:", error);
res.status(500).json(error);
} }
await transport.handlePostMessage(req, res); },
} catch (error) { );
console.error("Error in /message route:", error);
res.status(500).json(error); app.post(
} "/message",
}); originValidationMiddleware,
authMiddleware,
async (req, res) => {
try {
const sessionId = req.query.sessionId;
console.log(`Received POST message for sessionId ${sessionId}`);
const transport = webAppTransports.get(
sessionId as string,
) as SSEServerTransport;
if (!transport) {
res.status(404).end("Session not found");
return;
}
await transport.handlePostMessage(req, res);
} catch (error) {
console.error("Error in /message route:", error);
res.status(500).json(error);
}
},
);
app.get("/health", (req, res) => { app.get("/health", (req, res) => {
res.json({ res.json({
@ -481,22 +522,28 @@ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
} }
}); });
const PORT = parseInt(process.env.PORT || '6277', 10); const PORT = parseInt(process.env.PORT || "6277", 10);
const HOST = process.env.HOST || '127.0.0.1'; const HOST = process.env.HOST || "127.0.0.1";
const server = app.listen(PORT, HOST); const server = app.listen(PORT, HOST);
server.on("listening", () => { server.on("listening", () => {
console.log(`⚙️ Proxy server listening on ${HOST}:${PORT}`); console.log(`⚙️ Proxy server listening on ${HOST}:${PORT}`);
if (!authDisabled) { if (!authDisabled) {
console.log(`🔑 Session token: ${sessionToken}`); console.log(`🔑 Session token: ${sessionToken}`);
console.log(`Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth`); console.log(
`Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth`,
);
// Display clickable URL with pre-filled token // Display clickable URL with pre-filled token
const clientPort = process.env.CLIENT_PORT || '6274'; const clientPort = process.env.CLIENT_PORT || "6274";
const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`; const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
console.log(`\n🔗 Open inspector with token pre-filled:\n ${clientUrl}\n (Auto-open is disabled when authentication is enabled)\n`); console.log(
`\n🔗 Open inspector with token pre-filled:\n ${clientUrl}\n (Auto-open is disabled when authentication is enabled)\n`,
);
} else { } else {
console.log(`⚠️ WARNING: Authentication is disabled. This is not recommended.`); console.log(
`⚠️ WARNING: Authentication is disabled. This is not recommended.`,
);
} }
}); });
server.on("error", (err) => { server.on("error", (err) => {