From c4b75388d8afdddae2a4c66c91fdfc5a1526f6e2 Mon Sep 17 00:00:00 2001 From: King Star Date: Mon, 22 Jun 2026 01:06:14 +0800 Subject: [PATCH] fix: forward mcp streamable http headers --- .../CopilotLanguageServerSettingsTests.java | 103 ++++++++++++++++++ .../CopilotLanguageServerSettings.java | 67 +++++++++++- 2 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettingsTests.java diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettingsTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettingsTests.java new file mode 100644 index 00000000..e257ac6d --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettingsTests.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.junit.jupiter.api.Test; + +class CopilotLanguageServerSettingsTests { + + private final Gson gson = new Gson(); + + @Test + void testSetMcpServers_copiesTopLevelHeadersToRequestInitHeaders() { + CopilotLanguageServerSettings settings = new CopilotLanguageServerSettings(); + String preference = """ + { + "servers": { + "remote": { + "type": "http", + "url": "https://example.com/mcp", + "headers": { + "X-Username": "myuser", + "X-Api-Token": "my-token" + } + } + } + } + """; + + settings.setMcpServers(preference); + + JsonObject mcpServers = gson.fromJson(settings.getGithubSettings().getCopilotSettings().getMcpServers(), + JsonObject.class); + JsonObject remoteServer = mcpServers.getAsJsonObject("remote"); + assertTrue(remoteServer.has("requestInit")); + JsonObject requestHeaders = remoteServer.getAsJsonObject("requestInit").getAsJsonObject("headers"); + assertEquals("myuser", requestHeaders.get("X-Username").getAsString()); + assertEquals("my-token", requestHeaders.get("X-Api-Token").getAsString()); + assertTrue(remoteServer.has("headers")); + } + + @Test + void testSetMcpServers_copiesTopLevelHeadersWithoutServersWrapper() { + CopilotLanguageServerSettings settings = new CopilotLanguageServerSettings(); + String preference = """ + { + "remote": { + "type": "http", + "url": "https://example.com/mcp", + "headers": { + "X-Api-Token": "my-token" + } + } + } + """; + + settings.setMcpServers(preference); + + JsonObject mcpServers = gson.fromJson(settings.getGithubSettings().getCopilotSettings().getMcpServers(), + JsonObject.class); + JsonObject requestHeaders = mcpServers.getAsJsonObject("remote").getAsJsonObject("requestInit") + .getAsJsonObject("headers"); + assertEquals("my-token", requestHeaders.get("X-Api-Token").getAsString()); + } + + @Test + void testSetMcpServers_preservesExistingRequestInitHeaders() { + CopilotLanguageServerSettings settings = new CopilotLanguageServerSettings(); + String preference = """ + { + "servers": { + "remote": { + "type": "http", + "url": "https://example.com/mcp", + "headers": { + "Authorization": "Bearer top-level-token", + "X-Workspace": "demo" + }, + "requestInit": { + "headers": { + "Authorization": "Bearer request-init-token" + } + } + } + } + } + """; + + settings.setMcpServers(preference); + + JsonObject mcpServers = gson.fromJson(settings.getGithubSettings().getCopilotSettings().getMcpServers(), + JsonObject.class); + JsonObject requestHeaders = mcpServers.getAsJsonObject("remote").getAsJsonObject("requestInit") + .getAsJsonObject("headers"); + assertEquals("Bearer request-init-token", requestHeaders.get("Authorization").getAsString()); + assertEquals("demo", requestHeaders.get("X-Workspace").getAsString()); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettings.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettings.java index f3a30856..a12a294c 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettings.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotLanguageServerSettings.java @@ -9,6 +9,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; @@ -507,14 +509,17 @@ private String parseMcpServers(String mcpServersPreference) { try { Gson gson = new GsonBuilder().disableHtmlEscaping().create(); - Map jsonMap = gson.fromJson(mcpServersPreference, new TypeToken>() { - }.getType()); + JsonObject jsonObject = gson.fromJson(mcpServersPreference, JsonObject.class); - if (jsonMap != null && jsonMap.containsKey("servers")) { - Object serversObj = jsonMap.get("servers"); + if (jsonObject != null && jsonObject.has("servers")) { + JsonElement serversObj = jsonObject.get("servers"); + normalizeMcpServerHeaders(serversObj); return gson.toJson(serversObj); } + if (normalizeMcpServerHeaders(jsonObject)) { + return gson.toJson(jsonObject); + } return mcpServersPreference; } catch (JsonParseException e) { CopilotCore.LOGGER.error("Failed to parse MCP servers JSON", e); @@ -522,6 +527,58 @@ private String parseMcpServers(String mcpServersPreference) { } } + private boolean normalizeMcpServerHeaders(JsonElement serversElement) { + if (serversElement == null || !serversElement.isJsonObject()) { + return false; + } + + boolean changed = false; + JsonObject servers = serversElement.getAsJsonObject(); + for (Map.Entry serverEntry : servers.entrySet()) { + JsonElement serverElement = serverEntry.getValue(); + if (!serverElement.isJsonObject()) { + continue; + } + + JsonObject server = serverElement.getAsJsonObject(); + if (!server.has("headers") || !server.get("headers").isJsonObject()) { + continue; + } + + JsonObject topLevelHeaders = server.getAsJsonObject("headers"); + if (topLevelHeaders.isEmpty()) { + continue; + } + + JsonObject requestInit = getOrCreateObject(server, "requestInit"); + if (requestInit == null) { + continue; + } + JsonObject requestHeaders = getOrCreateObject(requestInit, "headers"); + if (requestHeaders == null) { + continue; + } + for (Map.Entry headerEntry : topLevelHeaders.entrySet()) { + if (!requestHeaders.has(headerEntry.getKey())) { + requestHeaders.add(headerEntry.getKey(), headerEntry.getValue()); + changed = true; + } + } + } + return changed; + } + + private JsonObject getOrCreateObject(JsonObject parent, String memberName) { + JsonElement existing = parent.get(memberName); + if (existing != null) { + return existing.isJsonObject() ? existing.getAsJsonObject() : null; + } + + JsonObject created = new JsonObject(); + parent.add(memberName, created); + return created; + } + @Override public String toString() { ToStringBuilder builder = new ToStringBuilder(this); @@ -555,4 +612,4 @@ public boolean equals(Object obj) { && showEditorCompletions == other.showEditorCompletions; } -} \ No newline at end of file +}