-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathhttp.ts
More file actions
96 lines (85 loc) · 4.02 KB
/
Copy pathhttp.ts
File metadata and controls
96 lines (85 loc) · 4.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { createServer as createHttpServer } from 'node:http';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createServer } from './server.js';
import { getCredentials, runWithCredentials } from './utils/client.js';
import { logger } from './utils/logger.js';
import { verifyS2sHeader, S2S_HEADER } from './s2s-verify.js';
const S2S_SECRET = process.env.CONDUIT_S2S_SECRET || '';
function startHttpServer(): void {
const port = parseInt(process.env.MCP_HTTP_PORT || '8080', 10);
const host = process.env.MCP_HTTP_HOST || '0.0.0.0';
const isGatewayMode = process.env.AUTH_MODE === 'gateway';
const httpServer = createHttpServer(async (req, res) => {
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
if (url.pathname === '/health') {
// /health is container LIVENESS, not credential-readiness. In gateway
// mode, credentials arrive per-request via X-Huntress-* headers, not
// at startup — so checking `getCredentials()` here would always 503
// the container and the WYRE vendor-monitor would false-red Huntress
// permanently. Liveness is "the server is accepting traffic"; the
// credentials state is reported as informational only.
// Standalone (non-gateway) mode also returns 200 — if the operator
// ran the server with no creds, that's their config problem and the
// first tool call will surface it clearly; conflating that with
// liveness causes more harm than the 503 prevents.
const creds = getCredentials();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
transport: 'http',
mode: isGatewayMode ? 'gateway' : 'standalone',
credentials: { configured: !!creds },
timestamp: new Date().toISOString(),
}));
return;
}
if (url.pathname !== '/mcp') {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found', endpoints: ['/mcp', '/health'] }));
return;
}
if (S2S_SECRET && !verifyS2sHeader(req.headers[S2S_HEADER] as string | undefined, S2S_SECRET)) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
error: 'Missing or invalid X-Gateway-S2S header: this endpoint only accepts requests signed by the gateway.',
})
);
return;
}
const apiKey = isGatewayMode ? (req.headers['x-huntress-api-key'] as string | undefined) : undefined;
const apiSecret = isGatewayMode ? (req.headers['x-huntress-api-secret'] as string | undefined) : undefined;
const handle = async () => {
// SECURITY-CRITICAL invariant: this transport MUST stay stateless
// (sessionIdGenerator: undefined + enableJsonResponse: true). Per-request
// tenant credentials are carried in an AsyncLocalStorage context opened by
// runWithCredentials() below. A stateless request->single-response flow
// keeps the tool call inside that context. Switching to a stateful/SSE
// transport (sessionIdGenerator set, persistent stream) would let a
// long-lived connection serve later messages under a stale/foreign
// credential context — re-review tenant isolation before changing this.
const server = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});
res.on('close', () => { transport.close(); server.close(); });
await server.connect(transport);
await transport.handleRequest(req, res);
};
if (apiKey && apiSecret) {
await runWithCredentials({ apiKey, apiSecret }, handle);
} else {
await handle();
}
});
httpServer.listen(port, host, () => {
logger.info(`HTTP streaming server listening on ${host}:${port}`);
});
}
const transport = process.env.MCP_TRANSPORT;
if (transport === 'http') {
startHttpServer();
} else {
import('./index.js');
}