@zereight/mcp-gitlab 2.1.4 → 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { getConfig, ENABLE_DYNAMIC_API_URL, GITLAB_AUTH_COOKIE_PATH, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_READ_ONLY_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, } from "./config.js";
2
+ import { getConfig, ENABLE_DYNAMIC_API_URL, GITLAB_AUTH_COOKIE_PATH, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_READ_ONLY_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, } from "./config.js";
3
+ import { loadKeyMaterialFromEnv, looksLikeStatelessSessionId, mintSessionId, openSessionId, } from "./stateless/index.js";
3
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
5
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -37,6 +38,32 @@ import { randomUUID } from "node:crypto";
37
38
  import { pino } from "pino";
38
39
  const logger = pino({
39
40
  level: process.env.LOG_LEVEL || "info",
41
+ // Redact sensitive values that must never appear in logs. Covers both
42
+ // typical auth-context property names and common HTTP header-bag shapes
43
+ // that tool/fetch wrappers may include when logging errors.
44
+ redact: {
45
+ paths: [
46
+ "token",
47
+ "*.token",
48
+ "ctx.token",
49
+ "context.token",
50
+ "authData.token",
51
+ "auth.token",
52
+ "headers.authorization",
53
+ "headers.Authorization",
54
+ 'headers["private-token"]',
55
+ 'headers["Private-Token"]',
56
+ 'headers["job-token"]',
57
+ 'headers["JOB-TOKEN"]',
58
+ 'headers["mcp-session-id"]',
59
+ 'headers["Mcp-Session-Id"]',
60
+ "sessionId",
61
+ "*.sessionId",
62
+ "ctx.sessionId",
63
+ "context.sessionId",
64
+ ],
65
+ censor: "[REDACTED]",
66
+ },
40
67
  transport: {
41
68
  target: "pino-pretty",
42
69
  options: {
@@ -412,6 +439,97 @@ function validateConfiguration() {
412
439
  }
413
440
  let OAUTH_ACCESS_TOKEN = null;
414
441
  let oauthClient = null;
442
+ /**
443
+ * Produce a safe short form of a session id for logs. Legacy UUIDs pass
444
+ * through; stateless sealed sids are truncated to the "v1.sid." prefix
445
+ * plus a handful of bytes of the ciphertext so operators can correlate
446
+ * flows without the log line carrying the sealed bearer token.
447
+ */
448
+ function redactSessionIdForLog(sid) {
449
+ if (!sid)
450
+ return "<none>";
451
+ if (sid.startsWith("v1.sid."))
452
+ return "v1.sid.<redacted>";
453
+ // UUIDs / other formats are low-sensitivity; show first 8 chars.
454
+ return sid.length > 8 ? `${sid.slice(0, 8)}…` : sid;
455
+ }
456
+ /**
457
+ * Detect whether an MCP JSON-RPC request body represents an "initialize"
458
+ * request. Accepts both single-message and batch forms. Returns false on any
459
+ * unexpected shape — callers treat an ambiguous body as non-init, which is
460
+ * the safer default (it means the SDK will fail loudly rather than silently
461
+ * spawning a new session).
462
+ */
463
+ function isInitializationRequestBody(body) {
464
+ if (!body)
465
+ return false;
466
+ const isInitObj = (m) => typeof m === "object" &&
467
+ m !== null &&
468
+ m.method === "initialize";
469
+ if (Array.isArray(body))
470
+ return body.some(isInitObj);
471
+ return isInitObj(body);
472
+ }
473
+ /**
474
+ * Normalize an `Mcp-Session-Id` header value.
475
+ *
476
+ * Node's HTTP types allow any request header to surface as `string[]` when
477
+ * the client sends it more than once. Casting to `string` and calling
478
+ * `.startsWith()` on an array throws `TypeError: startsWith is not a
479
+ * function`, which Express converts to a 500 — silently turning malformed
480
+ * requests into server errors and breaking the 401/404 semantics we
481
+ * carefully distinguish in stateless mode. A duplicated `Mcp-Session-Id` is
482
+ * also ill-formed at the protocol level: there is no well-defined way to
483
+ * pick between two values, so we reject arrays rather than guess.
484
+ *
485
+ * Empty-string is normalized to `undefined` so call sites can use truthy
486
+ * checks and `?? undefined`-style fallbacks uniformly.
487
+ *
488
+ * Exported for unit tests; otherwise used only by the /mcp handlers below.
489
+ */
490
+ export function readMcpSessionIdHeader(req) {
491
+ const raw = req.headers["mcp-session-id"];
492
+ if (typeof raw !== "string")
493
+ return undefined;
494
+ return raw.length > 0 ? raw : undefined;
495
+ }
496
+ /**
497
+ * Loaded once at startup. Null when OAUTH_STATELESS_MODE is disabled.
498
+ * When set, the OAuth provider and (later phases) the Mcp-Session-Id path
499
+ * switch to signed/sealed opaque values instead of per-pod in-memory caches.
500
+ */
501
+ let STATELESS_MATERIAL = null;
502
+ try {
503
+ // Drive enablement from the already-resolved config flag so the CLI flag
504
+ // (--oauth-stateless-mode) is honored. Re-reading env.OAUTH_STATELESS_MODE
505
+ // inside the loader would silently ignore the CLI flag and leave
506
+ // STATELESS_MATERIAL null, falling back to per-pod state.
507
+ STATELESS_MATERIAL = loadKeyMaterialFromEnv(OAUTH_STATELESS_MODE);
508
+ if (OAUTH_STATELESS_MODE && STATELESS_MATERIAL) {
509
+ // Avoid logging anything that could leak key length / entropy details.
510
+ // Keep the message aligned with similar startup banners.
511
+ // eslint-disable-next-line no-console -- startup banner parity
512
+ console.error("[gitlab-mcp] stateless OAuth mode enabled");
513
+ }
514
+ }
515
+ catch (err) {
516
+ // eslint-disable-next-line no-console -- startup failure must be visible
517
+ console.error(`[gitlab-mcp] failed to load stateless secret: ${err.message}`);
518
+ process.exit(1);
519
+ }
520
+ /**
521
+ * True when this request is a candidate for the stateless sid-auth path:
522
+ * stateless mode is enabled, key material loaded, and the client sent an
523
+ * Mcp-Session-Id header. We deliberately key off *presence* (not validity)
524
+ * so malformed / expired / legacy sids still reach handleStatelessMcpRequest
525
+ * and get the intended 404 Session not found — rather than being masked by
526
+ * a 401 from the OAuth bearer middleware.
527
+ */
528
+ export function hasStatelessSessionId(req) {
529
+ return Boolean(OAUTH_STATELESS_MODE &&
530
+ STATELESS_MATERIAL &&
531
+ readMcpSessionIdHeader(req));
532
+ }
415
533
  /**
416
534
  * Ensure the OAuth token is valid before making an API call.
417
535
  * Refreshes the token lazily (only when a tool is actually called).
@@ -7238,6 +7356,12 @@ async function startStreamableHTTPServer() {
7238
7356
  requestsProcessed: 0,
7239
7357
  rejectedByRateLimit: 0,
7240
7358
  rejectedByCapacity: 0,
7359
+ // Stateless-mode counters. Only non-zero when OAUTH_STATELESS_MODE=true.
7360
+ statelessRequests: 0,
7361
+ statelessAuthFromHeader: 0, // fresh Authorization/Private-Token/JOB-TOKEN present
7362
+ statelessAuthFromSealedSid: 0, // auth reconstructed from sealed Mcp-Session-Id
7363
+ statelessAuthFailures: 0, // neither source yielded usable auth
7364
+ statelessSidRotated: 0, // minted a new sid because fresh auth was present
7241
7365
  };
7242
7366
  // Rate limiting per session
7243
7367
  const sessionRequestCounts = {};
@@ -7362,6 +7486,189 @@ async function startStreamableHTTPServer() {
7362
7486
  delete authBySession[sessionId];
7363
7487
  clearAuthTimeout(sessionId);
7364
7488
  };
7489
+ /**
7490
+ * Stateless-mode handler for /mcp POSTs.
7491
+ *
7492
+ * The MCP Streamable HTTP SDK can run in "stateless mode" when
7493
+ * `sessionIdGenerator` is undefined — it creates no session state and
7494
+ * short-circuits session validation. We exploit that by driving the
7495
+ * session identity ourselves via an AEAD-sealed `Mcp-Session-Id`:
7496
+ *
7497
+ * 1. Every request gets a freshly-instantiated transport (SDK-stateless).
7498
+ * 2. The caller's auth is derived from:
7499
+ * - live request headers (preferred; handles OAuth token refresh),
7500
+ * - or the sealed Mcp-Session-Id (for requests that omit headers).
7501
+ * 3. We assign `transport.sessionId = <sealed>` so the SDK emits the
7502
+ * Mcp-Session-Id response header for the client to echo back.
7503
+ * 4. The AsyncLocalStorage auth context is populated from the derived
7504
+ * auth, bypassing authBySession / authTimeouts entirely.
7505
+ *
7506
+ * Rate limiting is explicitly disabled in stateless mode (per-pod counters
7507
+ * would yield a loose global bound — operators can rate-limit upstream).
7508
+ */
7509
+ const handleStatelessMcpRequest = async (req, res, material, sessionTtlSeconds) => {
7510
+ metrics.statelessRequests++;
7511
+ // Step 1: derive the effective auth for this request.
7512
+ // Priority: live headers > sealed sid. This lets clients refresh OAuth
7513
+ // tokens without re-initializing their MCP session.
7514
+ const headerAuth = parseAuthHeaders(req);
7515
+ // In GITLAB_MCP_OAUTH mode, req.auth may be populated by requireBearerAuth.
7516
+ // Use it when headerAuth is null (no Private-Token / JOB-TOKEN / Authorization).
7517
+ let effective = null;
7518
+ let freshAuthPresent = false;
7519
+ if (headerAuth) {
7520
+ effective = {
7521
+ header: headerAuth.header,
7522
+ token: headerAuth.token,
7523
+ apiUrl: headerAuth.apiUrl,
7524
+ };
7525
+ freshAuthPresent = true;
7526
+ }
7527
+ else if (GITLAB_MCP_OAUTH) {
7528
+ const authInfo = req.auth;
7529
+ if (authInfo?.token) {
7530
+ effective = {
7531
+ header: "Authorization",
7532
+ token: authInfo.token,
7533
+ apiUrl: GITLAB_API_URL,
7534
+ };
7535
+ freshAuthPresent = true;
7536
+ }
7537
+ }
7538
+ if (freshAuthPresent)
7539
+ metrics.statelessAuthFromHeader++;
7540
+ // Fall back to the sealed sid when no live headers are present. Track
7541
+ // whether a sid was presented but rejected (expired / tampered / wrong
7542
+ // key / malformed) so we can return 404 below — the MCP Streamable HTTP
7543
+ // contract is that session-bound requests get 404 on a terminated session,
7544
+ // which tells the client to re-initialize. Returning 401 here would
7545
+ // instead trigger the client's auth-failure path and break automatic
7546
+ // recovery after inactivity TTL expiry.
7547
+ const incomingSid = readMcpSessionIdHeader(req);
7548
+ let sidPresentedButInvalid = false;
7549
+ if (!effective && incomingSid) {
7550
+ if (looksLikeStatelessSessionId(incomingSid)) {
7551
+ const opened = openSessionId(material, incomingSid, sessionTtlSeconds);
7552
+ if (opened) {
7553
+ effective = { header: opened.h, token: opened.t, apiUrl: opened.u };
7554
+ metrics.statelessAuthFromSealedSid++;
7555
+ }
7556
+ else {
7557
+ sidPresentedButInvalid = true;
7558
+ }
7559
+ }
7560
+ else {
7561
+ // Non-stateless-shaped sid (e.g. a UUID from a prior stateful run, or
7562
+ // garbage). From the caller's perspective the session is unknown — we
7563
+ // surface that as "session ended" rather than as an auth problem.
7564
+ sidPresentedButInvalid = true;
7565
+ }
7566
+ }
7567
+ if (!effective) {
7568
+ metrics.authFailures++;
7569
+ metrics.statelessAuthFailures++;
7570
+ if (sidPresentedButInvalid) {
7571
+ // Per MCP Streamable HTTP: a 404 on a session-bound request signals
7572
+ // "session ended, re-initialize". The inactivity TTL expiring looks
7573
+ // identical to a session the server no longer recognizes — in both
7574
+ // cases the client should start a fresh initialize handshake.
7575
+ res.status(404).json({
7576
+ error: "Session not found",
7577
+ message: "Stateless mode: Mcp-Session-Id is expired, invalid, or from a " +
7578
+ "different key. Re-initialize to obtain a new session id.",
7579
+ });
7580
+ return;
7581
+ }
7582
+ res.status(401).json({
7583
+ error: "Authentication required",
7584
+ message: "Stateless mode: provide Private-Token, JOB-TOKEN, or Authorization " +
7585
+ "header, or a valid Mcp-Session-Id from a previous response.",
7586
+ });
7587
+ return;
7588
+ }
7589
+ // Step 2: detect whether this is the initialization request. The MCP SDK
7590
+ // only emits an Mcp-Session-Id response header when the transport is in
7591
+ // SDK-stateful mode, which requires a sessionIdGenerator. We use
7592
+ // SDK-stateful mode for init requests (so the client receives the sid)
7593
+ // and SDK-stateless mode for subsequent requests (to avoid the SDK's
7594
+ // per-instance _initialized / sessionId equality checks that would reject
7595
+ // a freshly-constructed transport).
7596
+ const isInit = isInitializationRequestBody(req.body);
7597
+ // Always mint a fresh sid so the embedded iat advances on every request.
7598
+ // This makes OAUTH_STATELESS_SESSION_TTL_SECONDS behave as an inactivity
7599
+ // timeout rather than an absolute-age cap — matching the legacy
7600
+ // setAuthTimeout semantics. Reusing the incoming sid would regress
7601
+ // long-lived sessions that authenticate via sealed-sid replay (typical
7602
+ // REMOTE_AUTHORIZATION flow after init).
7603
+ const freshSid = mintSessionId(material, {
7604
+ header: effective.header,
7605
+ token: effective.token,
7606
+ apiUrl: effective.apiUrl,
7607
+ });
7608
+ if (freshAuthPresent) {
7609
+ metrics.statelessSidRotated++;
7610
+ }
7611
+ logger.debug({
7612
+ sidPrefix: redactSessionIdForLog(freshSid),
7613
+ freshAuthPresent,
7614
+ header: effective.header,
7615
+ }, "stateless /mcp request");
7616
+ // Step 3: build the AsyncLocalStorage context so buildAuthHeaders and
7617
+ // getEffectiveApiUrl read from the derived auth.
7618
+ const ctx = {
7619
+ sessionId: freshSid,
7620
+ header: effective.header,
7621
+ token: effective.token,
7622
+ lastUsed: Date.now(),
7623
+ apiUrl: effective.apiUrl,
7624
+ };
7625
+ // Step 4: create a fresh transport per request.
7626
+ const transport = isInit
7627
+ ? new StreamableHTTPServerTransport({
7628
+ sessionIdGenerator: () => freshSid,
7629
+ })
7630
+ : new StreamableHTTPServerTransport({
7631
+ sessionIdGenerator: undefined, // SDK stateless mode for non-init
7632
+ });
7633
+ // For non-init requests the SDK runs in its internal stateless mode and
7634
+ // does not emit an Mcp-Session-Id response header. We pre-set the
7635
+ // freshly minted sid on the Express response so clients can adopt the
7636
+ // latest sid (and its advanced iat) on every response. Headers passed
7637
+ // to the SDK's writeHead() call are merged with pre-set headers per
7638
+ // Node.js semantics, so this does not clobber SDK-managed values.
7639
+ // Without this, the inactivity-timeout semantics of
7640
+ // OAUTH_STATELESS_SESSION_TTL_SECONDS silently regress to an
7641
+ // absolute-age cap for sid-only auth flows.
7642
+ if (!isInit) {
7643
+ res.setHeader("Mcp-Session-Id", freshSid);
7644
+ }
7645
+ const serverInstance = createServer();
7646
+ await serverInstance.connect(transport);
7647
+ await sessionAuthStore.run(ctx, async () => {
7648
+ try {
7649
+ await transport.handleRequest(req, res, req.body);
7650
+ }
7651
+ catch (err) {
7652
+ logger.error({ err }, "stateless /mcp error");
7653
+ if (!res.headersSent) {
7654
+ res.status(500).json({
7655
+ error: "Internal server error",
7656
+ message: err instanceof Error ? err.message : "Unknown error",
7657
+ });
7658
+ }
7659
+ }
7660
+ finally {
7661
+ // Fresh transport per request — always close to release any stream
7662
+ // resources. The SDK treats close() as idempotent.
7663
+ try {
7664
+ await transport.close();
7665
+ }
7666
+ catch {
7667
+ // ignore
7668
+ }
7669
+ }
7670
+ });
7671
+ };
7365
7672
  // Configure Express middleware
7366
7673
  app.use(express.json());
7367
7674
  // MCP OAuth — mount auth router and prepare bearer-auth middleware
@@ -7374,7 +7681,15 @@ async function startStreamableHTTPServer() {
7374
7681
  const callbackUrl = GITLAB_OAUTH_CALLBACK_PROXY
7375
7682
  ? `${issuerUrl.origin}${issuerUrl.pathname.replace(/\/$/, "")}/callback`
7376
7683
  : undefined;
7377
- const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_READ_ONLY_MODE, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, callbackUrl);
7684
+ const statelessOptions = OAUTH_STATELESS_MODE && STATELESS_MATERIAL
7685
+ ? {
7686
+ material: STATELESS_MATERIAL,
7687
+ clientTtlSeconds: OAUTH_STATELESS_CLIENT_TTL_SECONDS,
7688
+ pendingTtlSeconds: OAUTH_STATELESS_PENDING_TTL_SECONDS,
7689
+ storedTtlSeconds: OAUTH_STATELESS_STORED_TTL_SECONDS,
7690
+ }
7691
+ : null;
7692
+ const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_READ_ONLY_MODE, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, callbackUrl, statelessOptions);
7378
7693
  const scopesSupported = GITLAB_OAUTH_SCOPES ?? ["api", "read_api", "read_user"];
7379
7694
  // When server URL has a path (e.g. behind Kong), the SDK's well-known metadata
7380
7695
  // advertises root-level endpoints. Override to use path-prefixed endpoints.
@@ -7467,14 +7782,40 @@ async function startStreamableHTTPServer() {
7467
7782
  });
7468
7783
  return;
7469
7784
  }
7785
+ // Stateless-mode sid bypass: when the client sends only an
7786
+ // Mcp-Session-Id (no live Authorization), let handleStatelessMcpRequest
7787
+ // open the sealed sid. Without this, requireBearerAuth would 401
7788
+ // before the handler can reconstruct auth from the sid — breaking
7789
+ // sid-only follow-up requests across pods under GITLAB_MCP_OAUTH.
7790
+ //
7791
+ // We still run oauthBearerAuth when an Authorization header IS
7792
+ // present alongside the sid, so a client refreshing its OAuth token
7793
+ // gets the new token validated normally. We also key this off
7794
+ // header *presence* (not validity): malformed / expired / legacy
7795
+ // sids still reach the handler and get the intended 404 Session
7796
+ // not found response rather than being masked by a 401 here.
7797
+ if (hasStatelessSessionId(req) && !req.headers.authorization) {
7798
+ next();
7799
+ return;
7800
+ }
7470
7801
  oauthBearerAuth(req, res, next);
7471
7802
  }
7472
7803
  : (_req, _res, next) => next();
7473
7804
  // Streamable HTTP endpoint - handles both session creation and message handling
7474
7805
  app.post("/mcp", mcpBearerAuth, async (req, res) => {
7475
- const sessionId = req.headers["mcp-session-id"];
7806
+ const sessionId = readMcpSessionIdHeader(req);
7476
7807
  // Track request
7477
7808
  metrics.requestsProcessed++;
7809
+ // Stateless-mode branch: bypass authBySession / streamableTransports
7810
+ // entirely and derive the session auth from either the current request
7811
+ // headers (init) or a sealed Mcp-Session-Id (subsequent requests).
7812
+ // Rate limiting is disabled here because there is no shared counter.
7813
+ if (OAUTH_STATELESS_MODE &&
7814
+ STATELESS_MATERIAL &&
7815
+ (REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH)) {
7816
+ await handleStatelessMcpRequest(req, res, STATELESS_MATERIAL, OAUTH_STATELESS_SESSION_TTL_SECONDS);
7817
+ return;
7818
+ }
7478
7819
  // Rate limiting check for existing sessions
7479
7820
  if ((REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) && sessionId && !checkRateLimit(sessionId)) {
7480
7821
  metrics.rejectedByRateLimit++;
@@ -7697,6 +8038,8 @@ async function startStreamableHTTPServer() {
7697
8038
  sessionTimeoutSeconds: SESSION_TIMEOUT_SECONDS,
7698
8039
  remoteAuthEnabled: REMOTE_AUTHORIZATION,
7699
8040
  mcpOAuthEnabled: GITLAB_MCP_OAUTH,
8041
+ statelessModeEnabled: OAUTH_STATELESS_MODE && STATELESS_MATERIAL !== null,
8042
+ statelessRotationKey: OAUTH_STATELESS_MODE && STATELESS_MATERIAL?.previous != null,
7700
8043
  },
7701
8044
  });
7702
8045
  });
@@ -7711,8 +8054,8 @@ async function startStreamableHTTPServer() {
7711
8054
  });
7712
8055
  });
7713
8056
  // to delete a mcp server session explicitly
7714
- app.delete("/mcp", async (req, res) => {
7715
- const sessionId = req.headers["mcp-session-id"];
8057
+ app.delete("/mcp", mcpBearerAuth, async (req, res) => {
8058
+ const sessionId = readMcpSessionIdHeader(req);
7716
8059
  if (!sessionId) {
7717
8060
  res.status(400).json({ error: "mcp-session-id header is required" });
7718
8061
  return;