Skip to content

VSCode: "Session ID required in mcp-session-id header" #239

Closed

Description

@DaCodeChick

I think the MCP protocol was updated in VSCode. This is what I get when attempting to communicate with it in VSCode. I'm still on 6.0.1:

2025-12-24 15:00:25.772 [info] Starting server ReVa
2025-12-24 15:00:25.772 [info] Connection state: Starting
2025-12-24 15:00:25.773 [info] Starting server from LocalProcess extension host
2025-12-24 15:00:25.773 [info] Connection state: Running
2025-12-24 15:00:25.779 [info] 400 status sending message to http://localhost:8080/mcp/message, will attempt to fall back to legacy SSE
2025-12-24 15:00:25.785 [info] Connection state: Error 400 status connecting to http://localhost:8080/mcp/message as SSE: {"cause":null,"stackTrace":[{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"doGet","fileName":"HttpServletStreamableServerTransportProvider.java","lineNumber":264,"nativeMethod":false,"className":"io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"service","fileName":"HttpServlet.java","lineNumber":633,"nativeMethod":false,"className":"jakarta.servlet.http.HttpServlet"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"service","fileName":"HttpServlet.java","lineNumber":723,"nativeMethod":false,"className":"jakarta.servlet.http.HttpServlet"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"handle","fileName":"ServletHolder.java","lineNumber":764,"nativeMethod":false,"className":"org.eclipse.jetty.servlet.ServletHolder"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"doHandle","fileName":"ServletHandler.java","lineNumber":529,"nativeMethod":false,"className":"org.eclipse.jetty.servlet.ServletHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"nextHandle","fileName":"ScopedHandler.java","lineNumber":221,"nativeMethod":false,"className":"org.eclipse.jetty.server.handler.ScopedHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"doHandle","fileName":"SessionHandler.java","lineNumber":1580,"nativeMethod":false,"className":"org.eclipse.jetty.server.session.SessionHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"nextHandle","fileName":"ScopedHandler.java","lineNumber":221,"nativeMethod":false,"className":"org.eclipse.jetty.server.handler.ScopedHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"doHandle","fileName":"ContextHandler.java","lineNumber":1381,"nativeMethod":false,"className":"org.eclipse.jetty.server.handler.ContextHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"nextScope","fileName":"ScopedHandler.java","lineNumber":176,"nativeMethod":false,"className":"org.eclipse.jetty.server.handler.ScopedHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"doScope","fileName":"ServletHandler.java","lineNumber":484,"nativeMethod":false,"className":"org.eclipse.jetty.servlet.ServletHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"doScope","fileName":"SessionHandler.java","lineNumber":1553,"nativeMethod":false,"className":"org.eclipse.jetty.server.session.SessionHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"nextScope","fileName":"ScopedHandler.java","lineNumber":174,"nativeMethod":false,"className":"org.eclipse.jetty.server.handler.ScopedHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"doScope","fileName":"ContextHandler.java","lineNumber":1303,"nativeMethod":false,"className":"org.eclipse.jetty.server.handler.ContextHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"handle","fileName":"ScopedHandler.java","lineNumber":129,"nativeMethod":false,"className":"org.eclipse.jetty.server.handler.ScopedHandler"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"handle","fileName":"HandlerWrapper.java","lineNumber":122,"nativeMethod":false,"className":"org.eclipse.jetty.server.handler.HandlerWrapper"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"handle","fileName":"Server.java","lineNumber":563,"nativeMethod":false,"className":"org.eclipse.jetty.server.Server"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"dispatch","fileName":"HttpChannel.java","lineNumber":1598,"nativeMethod":false,"className":"org.eclipse.jetty.server.HttpChannel$RequestDispatchable"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"dispatch","fileName":"HttpChannel.java","lineNumber":753,"nativeMethod":false,"className":"org.eclipse.jetty.server.HttpChannel"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"handle","fileName":"HttpChannel.java","lineNumber":501,"nativeMethod":false,"className":"org.eclipse.jetty.server.HttpChannel"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"onFillable","fileName":"HttpConnection.java","lineNumber":287,"nativeMethod":false,"className":"org.eclipse.jetty.server.HttpConnection"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"succeeded","fileName":"AbstractConnection.java","lineNumber":314,"nativeMethod":false,"className":"org.eclipse.jetty.io.AbstractConnection$ReadCallback"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"fillable","fileName":"FillInterest.java","lineNumber":100,"nativeMethod":false,"className":"org.eclipse.jetty.io.FillInterest"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"run","fileName":"SelectableChannelEndPoint.java","lineNumber":53,"nativeMethod":false,"className":"org.eclipse.jetty.io.SelectableChannelEndPoint$1"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"runTask","fileName":"AdaptiveExecutionStrategy.java","lineNumber":421,"nativeMethod":false,"className":"org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"consumeTask","fileName":"AdaptiveExecutionStrategy.java","lineNumber":390,"nativeMethod":false,"className":"org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"tryProduce","fileName":"AdaptiveExecutionStrategy.java","lineNumber":277,"nativeMethod":false,"className":"org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"produce","fileName":"AdaptiveExecutionStrategy.java","lineNumber":193,"nativeMethod":false,"className":"org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"runJob","fileName":"QueuedThreadPool.java","lineNumber":969,"nativeMethod":false,"className":"org.eclipse.jetty.util.thread.QueuedThreadPool"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"doRunJob","fileName":"QueuedThreadPool.java","lineNumber":1194,"nativeMethod":false,"className":"org.eclipse.jetty.util.thread.QueuedThreadPool$Runner"},{"classLoaderName":null,"moduleName":null,"moduleVersion":null,"methodName":"run","fileName":"QueuedThreadPool.java","lineNumber":1149,"nativeMethod":false,"className":"org.eclipse.jetty.util.thread.QueuedThreadPool$Runner"},{"classLoaderName":null,"moduleName":"java.base","moduleVersion":"25.0.1","methodName":"run","fileName":"Thread.java","lineNumber":1474,"nativeMethod":false,"className":"java.lang.Thread"}],"jsonRpcError":null,"message":"Session ID required in mcp-session-id header","suppressed":[],"localizedMessage":"Session ID required in mcp-session-id header"}

Activity

  1. cyberkaida commented on Dec 24, 2025

    @cyberkaida
    Owner

    In your log it says it is trying to fall back to SSE, I am not sure why it is trying this. It is saying it is falling back but missing the header streaming HTTP requires. ReVa is serving Streaming HTTP and VSCode is trying SSE.


    The SSE transport was deprecated.
    https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#backwards-compatibility

    VSCode documentation says "legacy" support is included:
    https://code.visualstudio.com/docs/copilot/customization/mcp-servers#_http-and-serversent-events-sse-servers

  2. self-assigned this
    on Dec 24, 2025
  3. added
    bugSomething isn't working
    Ghidra🐉 - Related to the Ghidra extension
    on Dec 24, 2025
  4. DaCodeChick commented on Dec 25, 2025

    @DaCodeChick
    Author

    This is how it's set up currently in mcp.json:

    "ReVa": {
    	"url": "http://localhost:8080/mcp/message",
    	"type": "http"
    }
    
  5. karliky commented on Jan 4, 2026

    @karliky

    I have the exact same problem, my mpc config:

    {
      "servers": {
        "ReVa Assistant": {
          "type": "http",
          "url": "http://localhost:8080/mcp/message"
        }
      },
      "inputs": []
    }
    
    
  6. karliky commented on Jan 4, 2026

    @karliky

    Took me a lot of time but I finally tracked down what’s actually going on, and it’s not SSE and not a VS Code config problem.

    The root cause is a protocol mismatch between newer VS Code MCP clients and the MCP Java SDK ReVa is using.

    Recent VS Code versions send a newer initialize payload that includes fields under:

    params.capabilities.elicitation.form
    

    The MCP Java SDK currently used by ReVa doesn’t recognize that field and fails JSON deserialization, returning a 400 during initialize with errors like:

    Invalid message format: Unrecognized field "form"
    (class McpSchema$ClientCapabilities$Elicitation)
    

    Once VS Code sees a 400 during Streamable HTTP startup, it automatically falls back to legacy SSE. That fallback then fails because ReVa correctly expects Streamable HTTP + a valid session ID, which is why everyone sees:

    Session ID required in mcp-session-id header
    

    So that error is just a symptom, not the real problem.

    I confirmed this by running a small proxy that strips capabilities.elicitation out of the initialize request before it reaches ReVa. With that one change, everything works immediately, no 400s, no SSE fallback, normal Streamable HTTP session.

    ReVa needs to update its MCP Java SDK (or relax JSON parsing):

    • Upgrade to a newer io.modelcontextprotocol Java SDK that supports the newer MCP spec fields
    • OR mark capability models to ignore unknown fields (@JsonIgnoreProperties(ignoreUnknown = true))

    Either of those fixes this cleanly and keeps ReVa forward-compatible with newer MCP clients.

    I confirmed this by running this script, which acts like a middleware:

    const http = require("http");
    
    const LISTEN_HOST = "127.0.0.1";
    const LISTEN_PORT = 18080;
    
    const TARGET_HOST = "127.0.0.1";
    const TARGET_PORT = 8080;
    const TARGET_PATH = "/mcp/message";
    
    let sessionId = null;
    let initializing = null;
    
    function ts() {
      return new Date().toISOString();
    }
    function log(...args) {
      console.log(ts(), ...args);
    }
    
    function pickHeader(headers, name) {
      const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
      return key ? headers[key] : undefined;
    }
    function setHeader(headers, name, value) {
      for (const k of Object.keys(headers)) {
        if (k.toLowerCase() === name.toLowerCase()) delete headers[k];
      }
      headers[name] = value;
    }
    
    function normalizeAccept(method, headers) {
      if (method === "GET") setHeader(headers, "Accept", "text/event-stream");
      else setHeader(headers, "Accept", "application/json, text/event-stream");
    }
    
    function normalizeOutgoingHeaders(method, incomingHeaders, extraHeaders = {}) {
      const headers = { ...incomingHeaders, ...extraHeaders };
    
      delete headers.host;
      delete headers.connection;
      delete headers["content-length"];
    
      normalizeAccept(method, headers);
    
      if (method !== "GET") {
        if (!pickHeader(headers, "Content-Type")) setHeader(headers, "Content-Type", "application/json");
      }
      return headers;
    }
    
    function readRequestBody(req) {
      return new Promise((resolve) => {
        const chunks = [];
        req.on("data", (c) => chunks.push(c));
        req.on("end", () => resolve(Buffer.concat(chunks)));
      });
    }
    
    function parseJsonSafe(buf) {
      try {
        return JSON.parse(buf.toString("utf8"));
      } catch {
        return null;
      }
    }
    
    /**
     * Sanitize VS Code's initialize payload so older MCP Java SDKs (like ReVa's) don't 400.
     * The observed offending field is: params.capabilities.elicitation.form
     * Safest: remove entire `elicitation` capability block.
     */
    function sanitizeInitialize(bodyBuffer) {
      const j = parseJsonSafe(bodyBuffer);
      if (!j || j.method !== "initialize") return bodyBuffer;
    
      const caps = j?.params?.capabilities;
      if (caps && typeof caps === "object") {
        // Remove unsupported capability block that triggers "Unrecognized field 'form' ... Elicitation"
        if (caps.elicitation) delete caps.elicitation;
    
        // (Optional safety) If VS Code adds other unknown nested fields later, you can strip here too.
        // e.g. delete caps.someFutureCapability;
      }
    
      return Buffer.from(JSON.stringify(j), "utf8");
    }
    
    function requestUpstream({ method, headers, bodyBuffer }) {
      return new Promise((resolve, reject) => {
        const opts = { host: TARGET_HOST, port: TARGET_PORT, path: TARGET_PATH, method, headers };
        const upstreamReq = http.request(opts, (upstreamRes) => resolve({ upstreamRes }));
        upstreamReq.on("error", reject);
        if (bodyBuffer && bodyBuffer.length) upstreamReq.write(bodyBuffer);
        upstreamReq.end();
      });
    }
    
    async function readUpstreamBody(upstreamRes, limitBytes = 256 * 1024) {
      return new Promise((resolve) => {
        const chunks = [];
        let total = 0;
        upstreamRes.on("data", (c) => {
          if (total < limitBytes) {
            const slice = c.slice(0, limitBytes - total);
            chunks.push(slice);
            total += slice.length;
          }
        });
        upstreamRes.on("end", () => resolve(Buffer.concat(chunks)));
      });
    }
    
    async function ensureSession() {
      if (sessionId) return sessionId;
      if (initializing) return initializing;
    
      initializing = (async () => {
        log("[proxy] initializing MCP session…");
    
        const initBody = Buffer.from(
          JSON.stringify({
            jsonrpc: "2.0",
            id: 1,
            method: "initialize",
            params: {
              protocolVersion: "2024-11-05",
              capabilities: {},
              clientInfo: { name: "vscode-proxy", version: "1.0" },
            },
          }),
          "utf8"
        );
    
        const headers = normalizeOutgoingHeaders("POST", {
          "Content-Type": "application/json",
        });
    
        const { upstreamRes } = await requestUpstream({ method: "POST", headers, bodyBuffer: initBody });
        const bodyBuf = await readUpstreamBody(upstreamRes);
    
        const sid = pickHeader(upstreamRes.headers, "mcp-session-id");
        log("[proxy] initialize upstream status =", upstreamRes.statusCode, "sid =", sid || "(none)");
    
        if (!sid) {
          const preview = bodyBuf.toString("utf8").slice(0, 2000);
          throw new Error(`No Mcp-Session-Id from ReVa. status=${upstreamRes.statusCode} body=${preview}`);
        }
    
        sessionId = sid;
        return sessionId;
      })();
    
      try {
        return await initializing;
      } finally {
        initializing = null;
      }
    }
    
    function isInitializeRequest(bodyBuffer) {
      const j = parseJsonSafe(bodyBuffer);
      return !!(j && j.method === "initialize");
    }
    
    async function forwardStreaming(clientReq, clientRes, bodyBuffer, extraHeaders = {}) {
      const headers = normalizeOutgoingHeaders(clientReq.method, clientReq.headers, extraHeaders);
    
      const sidPresent =
        !!pickHeader(headers, "mcp-session-id") || !!pickHeader(headers, "Mcp-Session-Id");
    
      const shortBody =
        bodyBuffer && bodyBuffer.length ? bodyBuffer.toString("utf8").slice(0, 200).replace(/\s+/g, " ") : "";
    
      log(
        `[proxy] IN  ${clientReq.method} ${clientReq.url} sid=${sidPresent} accept="${clientReq.headers.accept || ""}" body="${shortBody}"`
      );
      log(
        `[proxy] ->  ${clientReq.method} ${TARGET_PATH} acceptSent="${pickHeader(headers, "accept") || ""}"`
      );
    
      const { upstreamRes } = await requestUpstream({
        method: clientReq.method,
        headers,
        bodyBuffer,
      });
    
      const upstreamSid = pickHeader(upstreamRes.headers, "mcp-session-id");
      if (upstreamSid) sessionId = upstreamSid;
    
      log(`[proxy] <-  status=${upstreamRes.statusCode}`);
    
      clientRes.writeHead(upstreamRes.statusCode || 500, upstreamRes.headers);
      upstreamRes.pipe(clientRes);
    }
    
    async function forwardInitializeWithRetry(clientReq, clientRes, bodyBuffer) {
      const sanitizedBody = sanitizeInitialize(bodyBuffer);
    
      // 1) Try forwarding sanitized initialize as-is
      const headers1 = normalizeOutgoingHeaders("POST", clientReq.headers);
      log(`[proxy] INIT try#1 -> POST ${TARGET_PATH} acceptSent="${pickHeader(headers1, "accept")}"`);
    
      const { upstreamRes: res1 } = await requestUpstream({
        method: "POST",
        headers: headers1,
        bodyBuffer: sanitizedBody,
      });
    
      const buf1 = await readUpstreamBody(res1);
      const maybeMsg1 = (() => {
        const j = parseJsonSafe(buf1);
        return j && (j.message || j.localizedMessage);
      })();
    
      log(`[proxy] INIT try#1 <- status=${res1.statusCode} msg="${(maybeMsg1 || "").slice(0, 200)}"`);
    
      // If OK, return it to client
      if ((res1.statusCode || 0) < 400) {
        clientRes.writeHead(res1.statusCode || 200, res1.headers);
        clientRes.end(buf1);
        return;
      }
    
      // 2) If still failing, obtain a session and retry initialize WITH Mcp-Session-Id
      const sid = await ensureSession();
    
      const headers2 = normalizeOutgoingHeaders("POST", clientReq.headers, { "Mcp-Session-Id": sid });
      log(`[proxy] INIT try#2 -> POST ${TARGET_PATH} sid=${sid}`);
    
      const { upstreamRes: res2 } = await requestUpstream({
        method: "POST",
        headers: headers2,
        bodyBuffer: sanitizedBody,
      });
    
      const buf2 = await readUpstreamBody(res2);
      const maybeMsg2 = (() => {
        const j = parseJsonSafe(buf2);
        return j && (j.message || j.localizedMessage);
      })();
    
      log(`[proxy] INIT try#2 <- status=${res2.statusCode} msg="${(maybeMsg2 || "").slice(0, 200)}"`);
    
      clientRes.writeHead(res2.statusCode || 500, res2.headers);
      clientRes.end(buf2);
    }
    
    const server = http.createServer(async (req, res) => {
      try {
        const bodyBuffer = await readRequestBody(req);
    
        // Always handle initialize with buffered retry logic (and sanitize)
        if (req.method === "POST" && isInitializeRequest(bodyBuffer)) {
          await forwardInitializeWithRetry(req, res, bodyBuffer);
          return;
        }
    
        // For everything else: ensure session if client didn't send one
        const hasSid = !!pickHeader(req.headers, "mcp-session-id");
        if (!hasSid) {
          const sid = await ensureSession();
          await forwardStreaming(req, res, bodyBuffer, { "Mcp-Session-Id": sid });
          return;
        }
    
        await forwardStreaming(req, res, bodyBuffer);
      } catch (e) {
        log("[proxy] ERROR", String(e));
        res.writeHead(502, { "content-type": "text/plain" });
        res.end(String(e));
      }
    });
    
    server.listen(LISTEN_PORT, LISTEN_HOST, () => {
      log(
        `MCP proxy listening on http://${LISTEN_HOST}:${LISTEN_PORT}/mcp/message -> http://${TARGET_HOST}:${TARGET_PORT}${TARGET_PATH}`
      );
    });
    
    

    Then I connected to it with this copilot mcp config:

    {
      "servers": {
        "ReVa Assistant": {
          "type": "http",
          "url": "http://localhost:18080/mcp/message"
        }
      },
      "inputs": []
    }
    
    

    Run it with node mcp.js

  7. cyberkaida commented on Jan 5, 2026

    @cyberkaida
    Owner

    I wish MCP is versioned better 😢

    I will upgrade Java MCP and and do a release. Thank you so much @karliky 💜

  8. karliky commented on Jan 5, 2026

    @karliky

    Thanks, I stopped using this MCP because it freezes my visual studio code while listing functions from the ghidra process.... but anyways, thanks!

  9. cyberkaida commented on Jan 5, 2026

    @cyberkaida
    Owner

    I have made a new release, v7.1.0. This includes many MCP protocol improvements. Please try this release.

    I have included a new debug option to help with these issues. Please try enabling the Debug mode and the request logging in the ReVa settings (Project window, Edit -> Tool Options), trigger the bug and then create a debug capture (Project window, Tool -> ReVa). This will capture the information I need to debug issues like this.

    If VSCode freezes when listing functions, please create an issue @karliky and I will debug it.

    Image Image
  10. pinned this issue on Jan 5, 2026
  11. cyberkaida commented on Jan 5, 2026

    @cyberkaida
    Owner

    This should be fixed in #232 #243 and release v7.1.0

  12. cyberkaida commented on Jan 7, 2026

    @cyberkaida
    Owner

    I thought they merged the fix. It has not been merged.

  13. added a commit that references this issue on Jan 7, 2026
  14. unpinned this issue on Mar 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Ghidra🐉 - Related to the Ghidra extensionbugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions