Skip to main content
The kontext.middleware() method returns an Express router that handles OAuth metadata, bearer auth, and MCP transport. Mount it at the app root.
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Kontext } from "@kontext-dev/js-sdk/server";

const kontext = new Kontext({ clientId: "mcp_your-server" });

function createServer() {
  const server = new McpServer({ name: "my-server", version: "1.0.0" });
  // register tools...
  return server;
}

const app = express();
app.use(kontext.middleware(() => createServer()));
app.listen(3000);

Factory function pattern

Pass a factory function () => McpServer instead of a single instance. Each MCP session requires its own McpServer — the MCP spec mandates a 1:1 relationship between server instances and connections. Sharing a single instance across concurrent sessions will throw.
// Correct -- each session gets its own instance
app.use(kontext.middleware(() => createServer()));

// Also works for local dev with one session
const server = new McpServer({ name: "my-server", version: "1.0.0" });
app.use(kontext.middleware(server));

Options

kontext.middleware(serverOrFactory, {
  mcpPath: "/mcp",               // Transport endpoint path (default: "/mcp")
  resourceServerUrl: "https://...", // Public URL when behind a proxy
  dangerouslyOmitAuth: false,    // Skip auth for local dev (never in production)
  verifier: customVerifier,      // Custom OAuthTokenVerifier
  metadataTransform: (meta) => meta, // Rewrite OAuth metadata URLs
  onSessionInitialized: (id, authInfo, transport) => {},
  onSessionClosed: (id) => {},
  bodyLimit: "1mb",              // Max request body size
});
OptionTypeDefaultDescription
mcpPathstring"/mcp"Path for the MCP transport endpoint
resourceServerUrlstringAuto-detectedPublic URL of your application. Required when behind a reverse proxy.
dangerouslyOmitAuthbooleanfalseSkip OAuth metadata and bearer auth. For local development only.
verifierOAuthTokenVerifierBuilt-in JWKS verifierCustom token verification function
metadataTransform(metadata) => metadataIdentityTransform OAuth metadata before serving
onSessionInitialized(sessionId, authInfo?, transport?) => voidCalled when a new MCP session starts
onSessionClosed(sessionId) => voidCalled when a session ends
bodyLimitstring | number"1mb"Max request body size, passed to express.json()

Registered routes

The middleware registers three route groups:
RouteMethodPurpose
/.well-known/oauth-authorization-serverGETOAuth authorization server metadata (RFC 8414)
/.well-known/oauth-protected-resource{mcpPath}GETProtected resource metadata (RFC 9728). Default: /.well-known/oauth-protected-resource/mcp
/mcp (or custom mcpPath)POST, GET, DELETEStreamable HTTP MCP transport
The .well-known routes must be accessible at the root of your domain. This means you should mount the middleware at the app root, not under a sub-path.

Session lifecycle hooks

Track session creation and teardown with the onSessionInitialized and onSessionClosed callbacks.
app.use(
  kontext.middleware(() => createServer(), {
    onSessionInitialized: (sessionId, authInfo, transport) => {
      console.log(`Session started: ${sessionId}`);
      console.log(`User: ${authInfo?.extra?.sub}`);
    },
    onSessionClosed: (sessionId) => {
      console.log(`Session ended: ${sessionId}`);
    },
  })
);
The authInfo object contains the verified token claims, including token, clientId, scopes, expiresAt, and an extra bag with sub and email.

Custom token verifier

Replace the built-in JWKS verifier with your own. This is useful when your application sits behind a gateway that handles token verification.
import type { OAuthTokenVerifier } from "@kontext-dev/js-sdk/server";

const verifier: OAuthTokenVerifier = {
  async verifyAccessToken(token) {
    const claims = await myGateway.verify(token);
    return {
      token,
      clientId: claims.clientId,
      scopes: claims.scopes,
      expiresAt: claims.exp,
      extra: { sub: claims.sub, email: claims.email },
    };
  },
};

app.use(kontext.middleware(() => createServer(), { verifier }));
When you provide a custom verifier, the middleware skips JWKS-based verification entirely and delegates to your function.

Metadata transform

When your application runs behind a reverse proxy, the OAuth metadata URLs may point to internal addresses. Use metadataTransform to rewrite them before they reach the client.
app.use(
  kontext.middleware(() => createServer(), {
    resourceServerUrl: "https://mcp.example.com/mcp",
    metadataTransform: (metadata) => {
      // Replace internal Hydra URLs with public-facing URLs
      metadata.authorization_endpoint = "https://auth.example.com/oauth2/auth";
      metadata.token_endpoint = "https://auth.example.com/oauth2/token";
      return metadata;
    },
  })
);
The transform runs on every metadata request. The middleware creates a fresh copy of the cached metadata before passing it to your function, so mutations are safe.

Next steps

  • Credentials — Exchange user tokens for integration credentials inside tool handlers.
  • Production — Deployment checklist for running in production.
  • Server Types — Full type reference for MiddlewareOptions and OAuthTokenVerifier.