h3-mcp logoh3-mcp

Transport Auth

Bearer tokens, API keys, or your own validator — on every HTTP method.

Auth is off by default because many MCP endpoints sit behind a gateway that already authenticates. Turn it on and every request to the route — POST, GET, and DELETE — must present a credential.

defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  auth: {
    tokens: [process.env.MCP_TOKEN!],
  },
  tools: [/* ... */],
});

With no schemes given, both are accepted: Authorization: Bearer <token> and x-api-key: <token>.

#API Key Only

defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  auth: {
    schemes: ["api-key"],
    header: "x-mcp-token", // default: x-api-key
    tokens: [process.env.MCP_TOKEN!],
  },
});

#Custom Validator

For dynamic credentials — a JWT, a per-tenant key, a lookup — validate yourself. The callback receives the parsed credentials and the event, and returns a boolean:

defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  auth: {
    schemes: ["bearer"],
    validate: async (auth, event) => {
      const claims = await verifyJwt(auth.token);
      if (!claims) return false;
      event.context.tenant = claims.tenant; // pass it down to your handlers
      return true;
    },
  },
});

Stashing the verified identity on event.context is the idiomatic way to get it to your tool handlers — auth runs before any of them.

Enabling auth requires at least one of tokens or validate; a config with neither throws at startup rather than accepting everything.

#Responses

Missing or invalid credentials get 401 with a www-authenticate header. No JSON-RPC body is produced — the request never reached the protocol layer.

Token comparison is length-capped, and tokens are trimmed. Invalid header names in header are rejected at startup.

#What Auth Does Not Do

Important

Auth answers "may this caller talk to this endpoint" — nothing more. It does not scope what they may reach. A valid token still gets every tool you declared and every resource they can name. Per-operation authorization belongs in your handlers, or in dynamic options so a caller only ever sees the tools they may use:

defineMcpHandler((event) => ({
  name: "my-server",
  version: "1.0.0",
  auth: { validate: verifyToken },
  tools: toolsFor(event.context.tenant), // resolved per request, after auth
}));

Also note that on the legacy era a session id is not a credential replacement: send auth on every request, including the ones that carry Mcp-Session-Id.

Read more in Sessions.
h3-mcp logo

h3-mcp  MCP servers, built on H3.