All files / src/middleware mcp-auth-routes.ts

100% Statements 11/11
100% Branches 13/13
100% Functions 3/3
100% Lines 10/10

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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              48x   48x 192x             48x 88x 87x         65x 22x                   48x 63x    
/**
 * Routes of the MCP transports that require OAuth: Streamable HTTP (`/`, `/mcp`) and the
 * SSE transport (`/sse`, `/messages`), which runs tools too.
 */
 
import type { Express, Request, RequestHandler } from 'express';
 
export const MCP_TRANSPORT_PATHS = ['/', '/mcp', '/sse', '/messages'];
// Match Express's default case-insensitive, optional-trailing-slash route behavior.
const transportPath = new RegExp(
  `^(?:${MCP_TRANSPORT_PATHS.map((path) => path + '/?').join('|')})$`,
  'i',
);
 
/** Root streaming GETs belong to MCP, even when the public dashboard is enabled.
 * https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server
 */
export function isMcpTransportRequest(req: Request, dashboardEnabled: boolean): boolean {
  if (!transportPath.test(req.path)) return false;
  if (
    (req.path !== '/' && req.path !== '//') ||
    (req.method !== 'GET' && req.method !== 'HEAD') ||
    !dashboardEnabled
  )
    return true;
  return (
    Boolean(req.headers['mcp-session-id']) ||
    /(?:^|,)\s*text\/event-stream\s*(?:;|,|$)/i.test(req.headers.accept ?? '')
  );
}
 
/**
 * Run `authenticate` before every MCP transport route. Exact routes, not a mount: a mount
 * strips its path from `req.path`, from which the check derives the protected resource.
 */
export function authenticateMcpTransports(app: Express, authenticate: RequestHandler): void {
  app.all(MCP_TRANSPORT_PATHS, authenticate);
}