MCPcopy Create free account
hub / github.com/Mnexa-AI/e2a / buildApp

Function buildApp

mcp/src/http-server.ts:71–164  ·  view source on GitHub ↗
(opts: HttpServerOptions)

Source from the content-addressed store, hash-verified

69 * OAuth refresh) ever ends a connection.
70 */
71export function buildApp(opts: HttpServerOptions): BuiltApp {
72 const cache =
73 opts.resolveCache ??
74 new ResolveCache({
75 ttlMs: opts.resolveCacheTtlMs ?? 60_000,
76 maxEntries: opts.resolveCacheMaxEntries ?? 500,
77 });
78
79 const app = express();
80 app.use(express.json({ limit: "1mb" }));
81 // CORS open for v0.2; revisit when we have a real allowlist of MCP hosts.
82 app.use(cors({ origin: "*", exposedHeaders: ["Mcp-Session-Id"] }));
83
84 app.get("/healthz", (_req, res) => {
85 res.json({ ok: true });
86 });
87
88 // DNS rebinding protection. The SDK deprecated its in-transport allowlist
89 // in favor of external middleware; we enforce it here. 421 Misdirected
90 // Request is the spec-recommended status for "this server is not what
91 // you asked for." Strip port before comparing so the same allowlist
92 // entry works both for prod (`api.e2a.dev`) and tests on random ports
93 // (`127.0.0.1:54321`).
94 const allowedHosts = new Set(opts.allowedHosts.map((h) => h.toLowerCase()));
95 app.use("/mcp", (req, res, next) => {
96 const host = req.headers.host;
97 if (!host) {
98 res.status(421).end();
99 return;
100 }
101 const bare = host.split(":")[0]!.toLowerCase();
102 if (!allowedHosts.has(bare)) {
103 res.status(421).end();
104 return;
105 }
106 next();
107 });
108
109 // Spec-mandated discovery for hosts probing where the auth server lives.
110 // Served unconditionally (even pre-OAuth) so clients don't get a confusing
111 // 404 between v0.2 and v0.3. Validates Host against the allowlist to
112 // avoid reflecting attacker-controlled hosts back in the `resource` URL.
113 // Compute the public-facing URL of this MCP server. Three cases:
114 // 1. publicUrl set explicitly: trust it verbatim (local-dev http,
115 // or any deployment behind a fronting proxy that knows its own
116 // external URL better than we do).
117 // 2. unset + Host header present + Host in allowlist: synthesize
118 // `https://{Host}`. This is the prod-default Caddy-fronted path.
119 // 3. unset + Host missing/disallowed: caller wrapped function
120 // rejects with 421 before reaching here.
121 const resolveResourceUrl = (req: Request): string | null => {
122 if (opts.publicUrl) {
123 return opts.publicUrl.replace(/\/+$/, "");
124 }
125 const host = req.headers.host;
126 if (!host) return null;
127 const bare = host.split(":")[0]!.toLowerCase();
128 if (!allowedHosts.has(bare)) return null;

Callers 1

startHttpServerFunction · 0.85

Calls 6

resolveResourceUrlFunction · 0.85
handleClientRequestFunction · 0.85
hasMethod · 0.80
postMethod · 0.65
getMethod · 0.45
deleteMethod · 0.45

Tested by

no test coverage detected