-
-
Notifications
You must be signed in to change notification settings - Fork 166
feat(gateway): add support for authorization headers in MCP server co… #298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…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.
Reviewer's GuideAdds 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 reusesequenceDiagram
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
Class diagram for updated MCP server header configurationclassDiagram
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
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this 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,updateHeaderandaddHeaderallow 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
MCPServersConfiguses the current header key as the Reactkeyin 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>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>
There was a problem hiding this 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
headersfield 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.
| const newKey = value; | ||
|
|
||
| // Disallow empty header keys | ||
| if (!newKey) { |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| 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 | ||
| } |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
|
@iFurySt Hi,请帮忙review和合并本pr,谢谢! |
PR 说明:MCP SSE/Streamable Headers & Authorization 支持
修改前
sse或streamable-http时仅能填写url,无法配置自定义请求头。修改后
Authorization (Bearer)输入框(最终合并进 headers)。截图
单测/校验
go build ./cmd/...make testcd web && pnpm run lint新增单测
internal/core/mcpproxy/headers_test.gointernal/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:
Enhancements:
Tests: