# Secure TypeScript MCP Server - Hat Trick Report

## Prompt

How do I build a secure production TypeScript MCP server for AI coding agents in 2026? Audit this implementation for current MCP protocol and SDK compatibility, OAuth 2.1 resource-server security, token audience validation, tool authorization, prompt-injection boundaries, tenant isolation, rate limiting, Streamable HTTP and SSE behavior, cancellation, retries, graceful shutdown, and OpenTelemetry. Identify exact defects and modernization opportunities, then provide corrected examples and authoritative sources.

## Verdict

Supplemental research confirms the 2026 v2 options `responseMode: "sse"` and `legacy: "stateless"` are current. The exact compatibility blocker is instead `toNodeHandler(route)`: the adapter expects the handler object returned by `createMcpHandler`. Fix three blockers first: serve RFC 9728 metadata and challenge with the resource-server URL; validate Origin before MCP dispatch; and verify or replace `responseMode: "sse"` plus `legacy: "stateless"` against the pinned 2026-07-28 SDK. Also enforce strict JWT claims, tenant predicates, distributed quotas, ordered shutdown, and initialized OpenTelemetry.

## Security

### Protected Resource Metadata and challenge target are incorrect.

**Severity:** high

No RFC 9728 endpoint is served, and the challenge derives its URL from the authorization-server issuer rather than the MCP protected resource.

**Action:** Serve path-aware metadata for a canonical MCP resource URL and challenge with that metadata URL plus the required scope where appropriate.

**Sources:** [WEB-1](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md), [WEB-2](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/authorization.md), [WEB-4](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/authorization/index.mdx), [WEB-5](https://www.rfc-editor.org/rfc/rfc9728.html)

### Mandatory Origin validation is absent.

**Severity:** high

The MCP route reaches dispatch without checking Origin. Current Streamable HTTP requires rejecting an invalid present Origin with 403 as a DNS-rebinding defense.

**Action:** Validate Origin against an explicit allowlist before authentication and dispatch; define deliberate behavior for clients that omit Origin.

**Sources:** [WEB-3](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/transports/streamable-http.mdx), [WEB-6](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/migration/support-2026-07-28.md)

### Node adapter receives the wrong handler shape

**Severity:** high

**Code**

```typescript
const nodeHandler = toNodeHandler(route);
```

The current adapter expects the object returned by `createMcpHandler`, exposing `fetch` and `close`. `route` is only a function, producing a type defect and possible runtime failure.

**Action:** Pass `mcp` to `toNodeHandler` and place health routing, authentication, and security middleware around that adapter according to the SDK mounting API.

**Sources:** [WEB-13](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md), [WEB-14](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/server/src/index.ts)

### Host validation is also absent

**Severity:** high

**Code**

```typescript
httpServer.listen(config.port, "127.0.0.1");
```

Binding to loopback does not prevent DNS rebinding through an attacker-controlled Host value. The raw Node mounting guide pairs Host and Origin validation before dispatch.

**Action:** Compose `localhostHostValidation()` and `localhostOriginValidation()` for loopback use; use explicit production host/origin allowlists for public deployments.

**Sources:** [WEB-15](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md)

### explain_access lacks tool-specific authorization

**Severity:** high

**Code**

```typescript
"explain_access",
```

Any authenticated token can retrieve the subject, workspace, and complete scope set because this handler has no `requireScope` call.

**Action:** Require a dedicated scope such as `mcp:permissions:read`, minimize the response, or remove the tool.

**Sources:** [WEB-2](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/authorization.md), [WEB-4](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/authorization/index.mdx)

## Performance And Operations

### The in-memory limiter grows without bound.

**Severity:** medium

Idle keys are never removed, each acceptance filters and reallocates an array, and quotas are neither shared nor atomic across replicas.

**Action:** Use an expiring distributed token bucket or counter, plus per-tenant concurrency and cost budgets. Return 429 with `Retry-After`.

**Sources:** [S9](https://nodejs.org/docs/latest/api/)

### OpenTelemetry lifecycle and propagation are incomplete.

**Severity:** medium

No NodeSDK, provider, exporter, HTTP instrumentation, context extraction, metrics, or telemetry shutdown is shown; clearing the active span may discard useful context.

**Action:** Initialize NodeSDK before listening, preserve incoming context, avoid raw identity attributes, and flush or shut down telemetry within the shutdown deadline.

**Sources:** [WEB-10](https://github.com/open-telemetry/opentelemetry-js), [WEB-11](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md), [WEB-12](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-sdk-node/README.md)

### Shutdown ordering can stall on active MCP exchanges

**Severity:** high

**Code**

```typescript
await mcp.close();
```

`mcp.close()` runs only after awaiting `httpServer.close()`. Active MCP exchanges can therefore keep HTTP shutdown open until the deadline. Telemetry is also not explicitly flushed.

**Action:** Make shutdown idempotent; stop admission, close or cancel MCP exchanges, drain bounded work, close HTTP connections, then perform bounded OpenTelemetry `forceFlush()` and `shutdown()` before exit.

**Sources:** [WEB-13](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md), [WEB-10](https://github.com/open-telemetry/opentelemetry-js), [WEB-11](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md), [WEB-19](https://nodejs.org/api/http.html)

### Forced SSE adds avoidable connection pressure

**Severity:** medium

**Code**

```typescript
responseMode: "sse",
```

SSE is always selected even for short, notification-free calls. Modern Streamable HTTP also differs from legacy resumable GET/SSE behavior.

**Action:** Prefer `auto` where negotiation is supported, or JSON for short calls. If SSE is required, configure proxy buffering and idle timeouts and test legacy behavior separately.

**Sources:** [WEB-13](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md), [WEB-3](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/transports/streamable-http.mdx), [WEB-18](https://github.com/modelcontextprotocol/typescript-sdk/issues/2648)

## Modernity

### Cooperative cancellation with AbortSignal

**Status:** modern

Signal composition matches current cancellation practice, including transport-driven cancellation when a response stream closes. The real dependency chain must honor the signal.

**Action:** Propagate the composed signal into every database, HTTP, vector-search, worker, and stream operation; classify timeout separately from disconnect.

**Sources:** [S1](https://dom.spec.whatwg.org/#aborting-ongoing-activities), [WEB-3](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/transports/streamable-http.mdx), [WEB-6](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/migration/support-2026-07-28.md)

### Asynchronous control flow

**Status:** modern

Async handlers suit I/O-bound MCP work, but async execution alone does not bound resource use or drain work during shutdown.

**Action:** Add per-tenant concurrency limits and track active operations for cancellation and draining.

**Sources:** [S5](https://tc39.es/ecma262/), [S9](https://nodejs.org/docs/latest/api/), [WEB-1](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md), [WEB-6](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/migration/support-2026-07-28.md)

### HTTP requests with Fetch

**Status:** acceptable

No direct Fetch client exists, so retry, redirect, SSRF, timeout, and response-size policies cannot be audited.

**Action:** For future outbound clients, enforce HTTPS and host allowlists, propagate cancellation, cap responses, disable unsafe redirects, and retry only classified idempotent failures.

**Sources:** [S2](https://fetch.spec.whatwg.org/), [S9](https://nodejs.org/docs/latest/api/)

### Environment-based configuration

**Status:** acceptable

Startup configuration is externalized, but URL trust, port range, and canonical resource identity are not validated.

**Action:** Validate all configuration once, require HTTPS outside development, and add a canonical `MCP_RESOURCE_URL`.

**Sources:** [S3](https://zod.dev/), [S4](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html), [WEB-2](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/authorization.md)

### Runtime schema validation

**Status:** modern

Zod validates tool arguments, but `payload as Claims` supplies no runtime validation for JWT claims.

**Action:** Strictly parse verified claims and bound serialized output bytes. Remove `requestKey` unless backed by replay storage and clear semantics.

**Sources:** [S3](https://zod.dev/), [WEB-2](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/authorization.md)

### Function decomposition

**Status:** modern

Responsibilities are separated, but policy remains split between HTTP routing and individual tools.

**Action:** Centralize typed authentication and authorization context, with a default-deny capability declaration for every tool.

**Sources:** [S10](https://www.typescriptlang.org/docs/handbook/intro.html), [WEB-1](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md), [WEB-2](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/authorization.md)

### Current MCP v2 handler options

**Status:** modern

`createMcpHandler`, `McpServer`, `responseMode`, `legacy: "stateless"`, pass-through `authInfo`, and handler `close()` remain current v2 concepts as of August 2026.

**Action:** Retain these APIs, but pass the `createMcpHandler` result—not the custom route function—to `toNodeHandler`; choose `legacy: "reject"` if legacy compatibility is unnecessary.

**Sources:** [WEB-13](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md), [WEB-14](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/server/src/index.ts)

## Maintainability

The implementation has clear modules and useful boundary schemas, but production policy and lifecycle behavior are fragmented and several critical dependencies remain stubs or unverified.

**Strengths**

- Tool arguments are bounded with Zod.
- JWT verification constrains issuer, audience, and algorithms.
- Tool work receives a composed cancellation signal.

**Improvements**

- Pin exact MCP package versions and add 2026-07-28 conformance tests.
- Replace JWT type assertions with strict parsing and central policy context.
- Track transports and active operations through an idempotent shutdown coordinator.
- Validate `PORT` as an integer from 1 through 65535; the current `Number(...)` accepts invalid values.
- Use an awaited async startup path so listen errors are observed.
- Make shutdown a shared idempotent promise and clear any deadline timer after successful cleanup.

## Upgrade Plan

### NOW: Repair OAuth resource-server behavior and JWT validation.

1. Add and validate `MCP_RESOURCE_URL`, serving its RFC 9728 metadata document.
2. Point 401 challenges to resource metadata; use 403 insufficient-scope challenges where transport-level step-up is intended.
3. Strictly require string `sub` and `workspace_id`, required `exp`, expected audience and issuer, and deployment-specific token-use claims.
4. Pass expiry through verified SDK authentication context.

**Sources:** [WEB-2](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/authorization.md), [WEB-4](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/authorization/index.mdx), [WEB-5](https://www.rfc-editor.org/rfc/rfc9728.html), [WEB-7](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/authorization/security-considerations.mdx), [S3](https://zod.dev/)

### NOW: Conform and test the current Streamable HTTP transport.

1. Pin exact SDK versions and migrate unverified `responseMode` and `legacy` options to documented 2026-07-28 APIs.
2. Validate Origin before dispatch.
3. Test POST-only JSON/SSE responses, protocol-version metadata, notification handling, disconnect cancellation, and proxy behavior.

**Sources:** [WEB-1](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md), [WEB-3](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/transports/streamable-http.mdx), [WEB-6](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/migration/support-2026-07-28.md)

### NOW: Enforce authorization and isolation below the tool layer.

1. Declare a default-deny scope matrix for every tool and redact `explain_access`.
2. Derive tenant identity only from verified claims and enforce it in database or index predicates and partitions.
3. Add cross-tenant substitution, stale-membership, malformed-claim, and mixed-result tests.

**Sources:** [WEB-2](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/authorization.md), [WEB-4](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/authorization/index.mdx), [WEB-7](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/authorization/security-considerations.mdx)

### NOW: Correct v2 Node mounting

1. Keep the current `createMcpHandler` options.
2. Pass the resulting `mcp` handler object to `toNodeHandler`.
3. Compose Host and Origin guards before dispatch.
4. Add a compile test against the exact pinned SDK versions.

**Sources:** [WEB-13](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md), [WEB-14](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/server/src/index.ts), [WEB-15](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md)

### NEXT: Define safe retry behavior

1. Use bounded exponential backoff with jitter only for transient transport reconnection or explicitly idempotent operations.
2. Do not blindly replay side-effecting `tools/call` requests after ambiguous failures.
3. Do not retry invalid tokens; permit only the SDK-defined credential refresh path on 401.

**Sources:** [WEB-3](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/transports/streamable-http.mdx), [WEB-16](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/client/src/client/streamableHttp.ts), [WEB-17](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx)

## Examples

### Strict verified-claim parsing.

Verify cryptography first, then parse authorization claims at runtime rather than asserting a TypeScript type.

```typescript
const ClaimsSchema = z.object({
  sub: z.string().min(1),
  workspace_id: z.string().min(1),
  exp: z.number().int().positive(),
  scope: z.string().optional(),
}).passthrough();

async function authenticate(token: string) {
  const { payload } = await jwtVerify(token, jwks, {
    issuer: config.issuer,
    audience: config.audience,
    algorithms: ["RS256", "ES256"],
  });
  return ClaimsSchema.parse(payload);
}
```

**Sources:** [S3](https://zod.dev/), [WEB-2](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/authorization.md), [WEB-4](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/authorization/index.mdx)

### Origin gate and resource-server challenge.

This focused route rejects untrusted browser origins and advertises metadata belonging to the protected resource.

```typescript
const allowedOrigins = new Set(["https://agent.example"]);
const resourceMetadata =
  "https://mcp.example/.well-known/oauth-protected-resource/mcp";

function guard(request: Request): Response | undefined {
  const origin = request.headers.get("origin");
  if (origin && !allowedOrigins.has(origin)) {
    return new Response("Forbidden", { status: 403 });
  }
  if (!request.headers.has("authorization")) {
    return new Response("Unauthorized", {
      status: 401,
      headers: { "WWW-Authenticate":
        `Bearer resource_metadata="${resourceMetadata}", scope="code:read"` },
    });
  }
}
```

**Sources:** [WEB-3](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/transports/streamable-http.mdx), [WEB-4](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/authorization/index.mdx), [WEB-5](https://www.rfc-editor.org/rfc/rfc9728.html)

## Submitted Code

```typescript
import { createServer } from "node:http";
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";

import { context, trace, SpanStatusCode } from "@opentelemetry/api";
import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";
import { toNodeHandler } from "@modelcontextprotocol/node";
import { jwtVerify, createRemoteJWKSet, type JWTPayload } from "jose";
import * as z from "zod/v4";

type Claims = JWTPayload & {
  sub: string;
  workspace_id: string;
  scope?: string;
};

type CodeHit = {
  path: string;
  startLine: number;
  endLine: number;
  excerpt: string;
  score: number;
};

const config = {
  issuer: mustEnv("OAUTH_ISSUER"),
  audience: mustEnv("MCP_AUDIENCE"),
  jwksUrl: new URL(mustEnv("OAUTH_JWKS_URL")),
  port: Number(process.env.PORT ?? 8787),
};

const jwks = createRemoteJWKSet(config.jwksUrl);
const tracer = trace.getTracer("secure-code-search-mcp");
const limiter = new SlidingWindowLimiter(60_000, 30);

function mustEnv(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`${name} is required`);
  return value;
}

function scopes(claims: Claims): Set<string> {
  return new Set((claims.scope ?? "").split(/\s+/).filter(Boolean));
}

async function authenticate(request: Request): Promise<Claims> {
  const header = request.headers.get("authorization") ?? "";
  const match = /^Bearer\s+(.+)$/i.exec(header);
  if (!match) throw new HttpError(401, "Bearer token required");

  const { payload } = await jwtVerify(match[1], jwks, {
    issuer: config.issuer,
    audience: config.audience,
    algorithms: ["RS256", "ES256"],
    clockTolerance: 5,
  });
  if (!payload.sub || typeof payload.workspace_id !== "string") {
    throw new HttpError(403, "Token lacks tenant identity");
  }
  return payload as Claims;
}

function requireScope(claims: Claims, required: string): void {
  if (!scopes(claims).has(required)) {
    throw new HttpError(403, `Missing scope: ${required}`);
  }
}

class HttpError extends Error {
  constructor(readonly status: number, message: string) {
    super(message);
  }
}

class SlidingWindowLimiter {
  private readonly requests = new Map<string, number[]>();

  constructor(
    private readonly windowMs: number,
    private readonly maximum: number,
  ) {}

  accept(key: string, now = Date.now()): boolean {
    const floor = now - this.windowMs;
    const recent = (this.requests.get(key) ?? []).filter((time) => time > floor);
    if (recent.length >= this.maximum) return false;
    recent.push(now);
    this.requests.set(key, recent);
    return true;
  }
}

class CodeIndex {
  async search(input: {
    workspaceId: string;
    query: string;
    limit: number;
    signal: AbortSignal;
  }): Promise<CodeHit[]> {
    input.signal.throwIfAborted();
    // Production implementation executes a tenant-scoped hybrid query.
    return [
      {
        path: "src/jobs/claim.ts",
        startLine: 41,
        endLine: 67,
        excerpt: "const job = await claimNextJob(workspaceId);",
        score: 0.91,
      },
    ].filter(() => input.workspaceId.length > 0).slice(0, input.limit);
  }
}

const codeIndex = new CodeIndex();

function stableRequestKey(claims: Claims, query: string): string {
  return createHash("sha256")
    .update(`${claims.workspace_id}\0${claims.sub}\0${query}`)
    .digest("hex");
}

function equalKey(left: string, right: string): boolean {
  const a = Buffer.from(left);
  const b = Buffer.from(right);
  return a.length === b.length && timingSafeEqual(a, b);
}

function buildMcpServer(claims: Claims): McpServer {
  const server = new McpServer({
    name: "secure-code-search",
    version: "2.0.0",
  });

  server.registerTool(
    "search_code",
    {
      title: "Search repository code",
      description:
        "Search code visible to the authenticated workspace. Never accepts a tenant ID from tool input.",
      inputSchema: z.object({
        query: z.string().trim().min(3).max(500),
        limit: z.number().int().min(1).max(20).default(8),
        requestKey: z.string().regex(/^[a-f0-9]{64}$/).optional(),
      }),
    },
    async ({ query, limit, requestKey }, extra) => {
      requireScope(claims, "code:read");
      const expectedKey = stableRequestKey(claims, query);
      if (requestKey && !equalKey(requestKey, expectedKey)) {
        throw new Error("Request key does not match authenticated input");
      }

      const rateKey = `${claims.workspace_id}:${claims.sub}`;
      if (!limiter.accept(rateKey)) throw new Error("Rate limit exceeded");

      return tracer.startActiveSpan("mcp.search_code", async (span) => {
        span.setAttributes({
          "enduser.id": claims.sub,
          "hattrick.workspace_id": claims.workspace_id,
          "mcp.tool.name": "search_code",
          "mcp.request_id": randomUUID(),
        });
        try {
          const deadline = AbortSignal.timeout(8_000);
          const signal = AbortSignal.any([deadline, extra.signal]);
          const hits = await codeIndex.search({
            workspaceId: claims.workspace_id,
            query,
            limit,
            signal,
          });
          span.setAttribute("mcp.result_count", hits.length);
          return {
            structuredContent: { hits },
            content: [{ type: "text", text: JSON.stringify({ hits }) }],
          };
        } catch (error) {
          span.recordException(error as Error);
          span.setStatus({ code: SpanStatusCode.ERROR });
          throw error;
        } finally {
          span.end();
        }
      });
    },
  );

  server.registerTool(
    "explain_access",
    {
      description: "Explain the caller's effective MCP permissions.",
      inputSchema: z.object({}),
    },
    async () => ({
      structuredContent: {
        subject: claims.sub,
        workspaceId: claims.workspace_id,
        scopes: [...scopes(claims)].sort(),
      },
      content: [{ type: "text", text: "Access is derived from the bearer token." }],
    }),
  );
  return server;
}

const mcp = createMcpHandler(({ authInfo }) => {
  if (!authInfo) throw new Error("Authenticated request context missing");
  return buildMcpServer(authInfo.extra?.claims as Claims);
}, {
  responseMode: "sse",
  legacy: "stateless",
});

async function route(request: Request): Promise<Response> {
  const requestId = request.headers.get("x-request-id") ?? randomUUID();
  if (new URL(request.url).pathname === "/healthz") {
    return Response.json({ ok: true, requestId });
  }
  if (new URL(request.url).pathname !== "/mcp") {
    return new Response("Not found", { status: 404 });
  }

  try {
    const claims = await authenticate(request);
    const headers = new Headers(request.headers);
    headers.set("x-auth-subject", claims.sub);
    const authenticated = new Request(request, { headers });
    return await context.with(trace.setSpan(context.active(), undefined), () =>
      mcp.fetch(authenticated, {
        authInfo: {
          token: "verified",
          clientId: claims.sub,
          scopes: [...scopes(claims)],
          extra: { claims },
        },
      })
    );
  } catch (error) {
    const status = error instanceof HttpError ? error.status : 500;
    const message = status === 500 ? "Internal server error" : error.message;
    return Response.json(
      { error: message, requestId },
      {
        status,
        headers: status === 401
          ? { "WWW-Authenticate": `Bearer resource_metadata="${config.issuer}/.well-known/oauth-protected-resource"` }
          : undefined,
      },
    );
  }
}

const nodeHandler = toNodeHandler(route);
const httpServer = createServer(nodeHandler);
httpServer.requestTimeout = 15_000;
httpServer.headersTimeout = 5_000;
httpServer.listen(config.port, "127.0.0.1");

async function shutdown(signal: string): Promise<void> {
  console.info(JSON.stringify({ event: "shutdown", signal }));
  httpServer.closeIdleConnections();
  await Promise.race([
    new Promise<void>((resolve, reject) =>
      httpServer.close((error) => error ? reject(error) : resolve())
    ),
    new Promise<never>((_, reject) =>
      setTimeout(() => reject(new Error("Shutdown deadline exceeded")), 10_000)
    ),
  ]);
  await mcp.close();
}

for (const signal of ["SIGINT", "SIGTERM"] as const) {
  process.once(signal, () => {
    shutdown(signal).then(() => process.exit(0)).catch((error) => {
      console.error(error);
      process.exit(1);
    });
  });
}

```
