Skip to content

Conversation

@x22x22
Copy link
Contributor

@x22x22 x22x22 commented Jan 15, 2026

PR 说明:MCP SSE/Streamable Headers & Authorization 支持

修改前

  • 网关配置里,MCP 服务类型为 ssestreamable-http 时仅能填写 url,无法配置自定义请求头。
  • Authorization 头没有独立入口,也无法在网关侧注入到下游请求。
  • transport 复用逻辑未考虑 headers 变化,导致更新 headers 后不生效。

修改后

  • MCP 服务新增 headers 配置,前端表单支持自定义 headers map,并提供 Authorization (Bearer) 输入框(最终合并进 headers)。
  • 网关在 SSE 与 Streamable 传输层启动时渲染 headers 模板并注入请求头。
  • headers 变更会触发 transport 重建,确保配置更新立即生效。

截图

image image

单测/校验

  • go build ./cmd/...
  • make test
  • cd web && pnpm run lint

新增单测

  • internal/core/mcpproxy/headers_test.go
  • internal/core/state/state_headers_test.go

单测补充说明

  • internal/common/dto/mcp_test.go 覆盖了 MCPServerConfig.Headers 的 DTO 转换。

Summary by Sourcery

Add configurable HTTP headers, including Authorization bearer support, for MCP SSE and streamable HTTP servers and ensure header changes affect transport reuse.

New Features:

  • Expose MCP server headers configuration in backend DTO, config, and gateway types for SSE and streamable HTTP transports.
  • Add UI controls in the gateway to manage MCP server headers and a dedicated Authorization (Bearer) token field that maps into headers.
  • Support passing rendered header templates into SSE and streamable HTTP transports when starting MCP connections.

Enhancements:

  • Ensure MCP transport reuse now also compares configured headers so that header changes trigger transport recreation.
  • Introduce shared header template rendering logic for MCP proxy transports.

Tests:

  • Extend MCP DTO tests to cover the new headers field on MCP server configs.
  • Add unit tests for MCP proxy header rendering and state transport reuse behavior when headers change.

…nfiguration

- Added new fields for authorization bearer token and headers in the MCP server configuration.
- Implemented functions to manage authorization tokens and headers, including adding, updating, and removing headers.
- Updated UI components to allow users to input and manage authorization tokens and headers.
- Enhanced translation files for English and Chinese to include new UI labels related to authorization.
Copilot AI review requested due to automatic review settings January 15, 2026 01:40
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 15, 2026

Reviewer's Guide

Adds configurable HTTP headers (including a dedicated Authorization/Bearer UX) for MCP SSE and streamable-http servers, wires them end-to-end through config/DTO/frontend, renders header templates at transport startup, and ensures transport reuse is invalidated when headers change.

Sequence diagram for MCP SSE/streamable call with header templating and reuse

sequenceDiagram
    actor User
    participant WebUI as WebUI_MCPServersConfig
    participant GatewayAPI as Gateway_API
    participant StateBuilder as State_BuildStateFromConfig
    participant SSE as SSETransport
    participant StreamHTTP as StreamableTransport
    participant Template as Template_Engine
    participant TransportSSE as transport_NewSSE
    participant TransportHTTP as transport_NewStreamableHTTP

    User->>WebUI: Configure MCP server URL, Authorization token, headers
    WebUI->>GatewayAPI: Save MCPServerConfig (including headers)

    note over GatewayAPI,StateBuilder: On config reload
    GatewayAPI->>StateBuilder: BuildStateFromConfig(cfgs, oldState)
    StateBuilder->>StateBuilder: Compare Type, Name, Args
    StateBuilder->>StateBuilder: headersEqual(oldConfig.Headers, newConfig.Headers)
    alt Args and Headers match
      StateBuilder->>SSE: Reuse existing SSE transport (if type sse)
      StateBuilder->>StreamHTTP: Reuse existing Streamable transport (if type streamable-http)
    else Args or Headers differ
      StateBuilder->>SSE: Create new SSETransport with cfg (including Headers)
      StateBuilder->>StreamHTTP: Create new StreamableTransport with cfg (including Headers)
    end

    note over SSE,TransportSSE: On first tool call when not running
    GatewayAPI->>SSE: CallTool(ctx, params, req)
    SSE->>Template: AssembleTemplateContext(req, args, nil)
    Template-->>SSE: tmplCtx
    SSE->>SSE: Start(ctx, tmplCtx)
    SSE->>Template: renderHeaders(cfg.Headers, tmplCtx)
    Template-->>SSE: renderedHeaders
    SSE->>TransportSSE: NewSSE(cfg.URL, WithHeaders(renderedHeaders))
    TransportSSE-->>SSE: SSE client

    note over StreamHTTP,TransportHTTP: For streamable-http
    GatewayAPI->>StreamHTTP: Start(ctx, tmplCtx)
    StreamHTTP->>Template: renderHeaders(cfg.Headers, tmplCtx)
    Template-->>StreamHTTP: renderedHeaders
    StreamHTTP->>TransportHTTP: NewStreamableHTTP(cfg.URL, WithHTTPHeaders(renderedHeaders))
    TransportHTTP-->>StreamHTTP: HTTP client
Loading

Class diagram for updated MCP server header configuration

classDiagram
    class MCPServerConfig_Config {
      +string Type
      +string Name
      +[]string Args
      +map~string,string~ Env
      +string URL
      +map~string,string~ Headers
      +MCPStartupPolicy Policy
      +bool Preinstalled
    }

    class MCPServerConfig_DTO {
      +string Type
      +string Name
      +[]string Args
      +map~string,string~ Env
      +string URL
      +map~string,string~ Headers
      +string Policy
      +bool Preinstalled
      +FromMCPServerConfigs(cfgs []MCPServerConfig_Config) []MCPServerConfig_DTO
    }

    class MCPServerConfig_TS {
      +string type
      +string name
      +string[] args
      +Record~string,string~ env
      +string url
      +Record~string,string~ headers
      +string policy
      +boolean preinstalled
    }

    MCPServerConfig_Config <--> MCPServerConfig_DTO : conversion
    MCPServerConfig_DTO <--> MCPServerConfig_TS : JSON

    class SSETransport {
      -MCPServerConfig_Config cfg
      +Start(ctx Context, tmplCtx TemplateContext) error
      +CallTool(ctx Context, params CallToolParams, req Request) (*ToolResult, error)
    }

    class StreamableTransport {
      -MCPServerConfig_Config cfg
      +Start(ctx Context, tmplCtx TemplateContext) error
    }

    class HeadersUtil {
      +renderHeaders(headers map~string,string~, tmplCtx TemplateContext) (map~string,string~, error)
      +headersEqual(a map~string,string~, b map~string,string~) bool
    }

    class State {
      +BuildStateFromConfig(ctx Context, cfgs []*MCPConfig, oldState *State, logger Logger) (*State, error)
    }

    class TransportReuseLogic {
      -map~string,string~ oldHeaders
      -map~string,string~ newHeaders
      +shouldReuseTransport(argsMatch bool, oldHeaders map~string,string~, newHeaders map~string,string~) bool
    }

    SSETransport --> HeadersUtil : uses
    StreamableTransport --> HeadersUtil : uses
    State --> HeadersUtil : uses headersEqual
    State --> SSETransport : creates and reuses
    State --> StreamableTransport : creates and reuses
    TransportReuseLogic --> HeadersUtil : uses headersEqual

    class MCPServersConfig_TSX {
      -MCPServerConfig_TS[] mcpServers
      +getAuthorizationToken(headers Record~string,string~) string
      +setAuthorizationToken(serverIndex number, token string) void
      +getEditableHeaderKeys(headers Record~string,string~) string[]
      +updateHeader(serverIndex number, headerIndex number, field string, value string) void
      +addHeader(serverIndex number) void
      +removeHeader(serverIndex number, headerIndex number) void
    }

    MCPServersConfig_TSX --> MCPServerConfig_TS : edits headers
    MCPServersConfig_TSX --> MCPServerConfig_TS : manages Authorization header
Loading

File-Level Changes

Change Details Files
Add headers and Authorization support to MCP server gateway UI and types
  • Extend MCP server default objects and TypeScript interface to include a headers map
  • Implement helper functions to extract/set Bearer token from Authorization headers without duplicating keys
  • Add CRUD UI for arbitrary MCP request headers excluding Authorization, with sensible default header key suggestions
  • Wire Authorization input and headers editor into the MCP server URL configuration form
  • Ensure new MCP server entries start with an empty headers object
web/src/pages/gateway/components/MCPServersConfig.tsx
web/src/types/gateway.ts
web/src/i18n/locales/en/translation.json
web/src/i18n/locales/zh/translation.json
Propagate headers through MCP config and DTO layers
  • Add Headers field to MCP server config in common config model (JSON/YAML) for SSE and streamable-http types
  • Expose Headers on MCPServerConfig DTO and map it from internal config
  • Extend DTO unit tests to assert headers (including Authorization) are preserved in conversions
internal/common/config/mcp.go
internal/common/dto/mcp.go
internal/common/dto/mcp_test.go
Render header templates and attach headers when starting SSE/streamable transports
  • Introduce renderHeaders helper to apply template rendering to configured headers with a given template context
  • Update SSE transport Start to render headers from config and pass them via transport.WithHeaders options to NewSSE
  • Update Streamable HTTP transport Start to render headers and pass them via transport.WithHTTPHeaders options to NewStreamableHTTP
  • Adjust SSE CallTool to always build a template context (from request and tool args) and pass it into Start so that headers/env can be rendered there
internal/core/mcpproxy/headers.go
internal/core/mcpproxy/sse.go
internal/core/mcpproxy/streamable.go
internal/core/mcpproxy/headers_test.go
Ensure MCP transport reuse is sensitive to header changes
  • Introduce headersEqual helper for map[string]string comparison with empty-map equivalence semantics
  • Extend BuildStateFromConfig transport reuse condition to also require headers equality between old and new MCP config, forcing transport recreation when headers change
  • Add unit tests to cover header-based transport recreation behavior
internal/core/state/state.go
internal/core/state/state_headers_test.go

Possibly linked issues

  • Authorization header pass through for external mcp server #174: PR implements header/Authorization config and passes rendered headers into SSE/streamable transports, fulfilling the passthrough requirement
  • #Authorization header pass through for external mcp server: PR introduces header (including Authorization) configuration and injection for MCP SSE/streamable servers, resolving header pass-through.
  • #(no explicit number provided): PR adds configurable Authorization/headers for MCP upstream, enabling separate hidden upstream token as requested in issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In MCPServersConfig, updateHeader and addHeader allow creating empty or duplicate header keys (e.g., renaming a key to an existing one or to an empty string), which can lead to confusing or invalid header maps; consider explicitly preventing empty keys and handling collisions (e.g., disallowing duplicates or auto-renaming).
  • The header editing UI in MCPServersConfig uses the current header key as the React key in the list; when a user renames a header, this can cause odd re-mount behavior and lost cursor state—using a stable identifier (such as the index or a generated id) instead of the header name would make the inputs behave more predictably during edits.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `MCPServersConfig`, `updateHeader` and `addHeader` allow creating empty or duplicate header keys (e.g., renaming a key to an existing one or to an empty string), which can lead to confusing or invalid header maps; consider explicitly preventing empty keys and handling collisions (e.g., disallowing duplicates or auto-renaming).
- The header editing UI in `MCPServersConfig` uses the current header key as the React `key` in the list; when a user renames a header, this can cause odd re-mount behavior and lost cursor state—using a stable identifier (such as the index or a generated id) instead of the header name would make the inputs behave more predictably during edits.

## Individual Comments

### Comment 1
<location> `web/src/pages/gateway/components/MCPServersConfig.tsx:157-158` </location>
<code_context>
+  const getEditableHeaderKeys = (headers?: Record<string, string>) =>
+    Object.keys(headers || {}).filter((key) => !isAuthorizationKey(key));
+
+  const updateHeader = (serverIndex: number, headerIndex: number, field: 'key' | 'value', value: string) => {
+    const updatedServers = [...mcpServers];
+    const server = updatedServers[serverIndex];
+    const headers = { ...(server.headers || {}) };
+    const headerKeys = getEditableHeaderKeys(headers);
+    const key = headerKeys[headerIndex];
+
+    if (!key) {
+      return;
+    }
+
+    if (field === 'key') {
+      if (key !== value) {
+        headers[value] = headers[key];
+        delete headers[key];
+      }
+    } else {
+      headers[key] = value;
+    }
+
+    updatedServers[serverIndex] = {
+      ...server,
+      headers
+    };
+
+    updateConfig({ mcpServers: updatedServers });
+  };
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Consider handling empty or duplicate header keys when editing to avoid unexpected overwrites.

When `field === 'key'`, the code moves the value to `headers[value]` and deletes the old key. This allows:
- Empty strings to become header keys.
- Silent overwrites when `value` matches an existing key.
Consider rejecting empty keys and guarding against collisions (e.g., disallow, confirm, or merge explicitly) to avoid accidental clobbering and surprising behavior.

```suggestion
  const getEditableHeaderKeys = (headers?: Record<string, string>) =>
    Object.keys(headers || {}).filter((key) => !isAuthorizationKey(key));

  const updateHeader = (serverIndex: number, headerIndex: number, field: 'key' | 'value', value: string) => {
    const updatedServers = [...mcpServers];
    const server = updatedServers[serverIndex];
    const headers = { ...(server.headers || {}) };
    const headerKeys = getEditableHeaderKeys(headers);
    const key = headerKeys[headerIndex];

    if (!key) {
      return;
    }

    if (field === 'key') {
      const newKey = value;

      // Disallow empty header keys
      if (!newKey) {
        return;
      }

      // Disallow overwriting an existing header with a different key
      if (newKey !== key && Object.prototype.hasOwnProperty.call(headers, newKey)) {
        return;
      }

      if (key !== newKey) {
        headers[newKey] = headers[key];
        delete headers[key];
      }
    } else {
      headers[key] = value;
    }

    updatedServers[serverIndex] = {
      ...server,
      headers
    };

    updateConfig({ mcpServers: updatedServers });
  };
```
</issue_to_address>

### Comment 2
<location> `web/src/pages/gateway/components/MCPServersConfig.tsx:475-458` </location>
<code_context>
+                      {getEditableHeaderKeys(server.headers).map((key, headerIndex) => (
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Using the header name as the React key can behave unexpectedly when the name is edited.

Because the list items use a renameable value as their React `key`, any rename is treated as unmount + remount, which can disrupt focus or local component state now or in future. Use a stable identifier (e.g., a generated id stored with each header) for the `key`, and keep the header name as regular data instead.

Suggested implementation:

```typescript
                    <div className="flex flex-col gap-2">
                      {getEditableHeaderKeys(server.headers).map(({ id, key }, headerIndex) => (
                        <div key={id} className="flex items-center gap-2">
                          <Input
                            className="flex-1"
                            value={key}
                            onChange={(e) => updateHeader(index, headerIndex, 'key', e.target.value)}
                            placeholder={t('gateway.header_name_placeholder')}
                          />
                          <Input
                            className="flex-1"
                            value={server.headers?.[key] || ""}

```

To fully support this change, you will also need to:
1. Update `getEditableHeaderKeys` to return an array of objects like `{ id: string; key: string }` instead of just header-name strings. The `id` should be a stable identifier stored with each header (e.g., generated when the header is created and persisted in the data structure).
2. Adjust the underlying `server.headers` structure (most likely from a plain key/value map to either:
   - an array of `{ id, key, value }`, or
   - a map of `id -> { key, value }`, with `getEditableHeaderKeys` doing the appropriate projection).
3. Ensure `addHeader` generates and stores a stable `id` for each new header.
4. Ensure `updateHeader` updates the correct header by index or id without relying on the header name as the identity.
These changes keep React keys stable across header renames and prevent unintended unmount/remount behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds support for custom HTTP headers (including Authorization Bearer tokens) in MCP server configurations for SSE and Streamable HTTP transport types. The gateway can now inject authorization and custom headers into downstream MCP requests with support for template rendering.

Changes:

  • Added headers field to MCP server configuration across frontend and backend
  • Implemented frontend UI with separate Authorization Bearer input and custom headers management
  • Enhanced backend to render header templates and pass them to SSE/Streamable transports
  • Updated transport reuse logic to consider header changes when rebuilding connections

Reviewed changes

Copilot reviewed 11 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
web/src/types/gateway.ts Added optional headers field to MCPServerConfig type
web/src/pages/gateway/components/MCPServersConfig.tsx Implemented UI for Authorization Bearer token and custom headers management
web/src/i18n/locales/zh/translation.json Added Chinese translations for new header-related UI elements
web/src/i18n/locales/en/translation.json Added English translations for new header-related UI elements
internal/core/state/state.go Added headersEqual function and updated transport reuse logic to compare headers
internal/core/mcpproxy/streamable.go Added header rendering and injection for Streamable HTTP transport
internal/core/mcpproxy/sse.go Added header rendering and injection for SSE transport, refactored CallTool to support template context
internal/core/mcpproxy/headers.go New utility function to render header templates with template context
internal/common/dto/mcp_test.go Updated test to verify headers field in DTO conversion
internal/common/dto/mcp.go Added Headers field to MCPServerConfig DTO
internal/common/config/mcp.go Added Headers field to MCPServerConfig struct
.gitignore Added Go build cache directories to gitignore

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +172 to +175
const newKey = value;

// Disallow empty header keys
if (!newKey) {
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When renaming a header key, if the new key already exists in the headers object, this code will overwrite the existing value without warning. This could lead to unexpected data loss. Consider checking if value already exists in headers (excluding the current key) and either preventing the rename or warning the user.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines 168 to 184
if !t.IsRunning() {
if err := t.Start(ctx, nil); err != nil {
return nil, err
// Convert arguments to map[string]any
var args map[string]any
if err := json.Unmarshal(params.Arguments, &args); err != nil {
return nil, fmt.Errorf("invalid tool arguments: %w", err)
}
}

// Convert arguments to map[string]any
var args map[string]any
if err := json.Unmarshal(params.Arguments, &args); err != nil {
return nil, fmt.Errorf("invalid tool arguments: %w", err)
}

// Prepare template context for environment variables
tmplCtx, err := template.AssembleTemplateContext(req, args, nil)
if err != nil {
return nil, fmt.Errorf("failed to prepare template context: %w", err)
}

// Process environment variables with templates
renderedClientEnv := make(map[string]string)
for k, v := range t.cfg.Env {
rendered, err := template.RenderTemplate(v, tmplCtx)
// Prepare template context for header templates
tmplCtx, err := template.AssembleTemplateContext(req, args, nil)
if err != nil {
return nil, fmt.Errorf("failed to render env template: %w", err)
return nil, fmt.Errorf("failed to prepare template context: %w", err)
}

if err := t.Start(ctx, tmplCtx); err != nil {
return nil, err
}
renderedClientEnv[k] = rendered
}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactored CallTool method now creates template context only when the transport is not running. However, if the transport is already running (e.g., with PolicyOnStart), any dynamic header templates that depend on the current tool arguments won't be re-evaluated. This means the first request's template values will be reused for all subsequent requests. Consider whether this is the intended behavior or if headers should be re-rendered per request.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback


updatedServers[serverIndex] = {
...server,
headers
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name newKey is reused with different meanings in this function. Initially set to 'Content-Type', it may be reassigned to a common header name, and then potentially to an X-Header-N format. Consider using a more descriptive name like suggestedKey or defaultKey to better convey the variable's purpose.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

@x22x22
Copy link
Contributor Author

x22x22 commented Jan 15, 2026

@iFurySt Hi,请帮忙review和合并本pr,谢谢!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant