feat: mcp custom headers (#4800)

* Add support for custom HTTP headers in MCP servers

Allow users to configure custom HTTP headers for SSE and streamable HTTP
MCP server connections. This enables authentication and other API
requirements.

* Add custom headers i18n strings
This commit is contained in:
LiuVaayne 2025-04-14 17:17:13 +08:00 committed by GitHub
parent c7ef2c5791
commit 1d211ee9f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 76 additions and 20 deletions

View File

@ -7,7 +7,7 @@ import { createInMemoryMCPServer } from '@main/mcpServers/factory'
import { makeSureDirExists } from '@main/utils' import { makeSureDirExists } from '@main/utils'
import { getBinaryName, getBinaryPath } from '@main/utils/process' import { getBinaryName, getBinaryPath } from '@main/utils/process'
import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory' import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
@ -137,12 +137,19 @@ class McpService {
transport = clientTransport transport = clientTransport
} else if (server.baseUrl) { } else if (server.baseUrl) {
if (server.type === 'streamableHttp') { if (server.type === 'streamableHttp') {
transport = new StreamableHTTPClientTransport( const options: StreamableHTTPClientTransportOptions = {
new URL(server.baseUrl!), requestInit: {
{} as StreamableHTTPClientTransportOptions headers: server.headers || {}
) }
}
transport = new StreamableHTTPClientTransport(new URL(server.baseUrl!), options)
} else if (server.type === 'sse') { } else if (server.type === 'sse') {
transport = new SSEClientTransport(new URL(server.baseUrl!)) const options: SSEClientTransportOptions = {
requestInit: {
headers: server.headers || {}
}
}
transport = new SSEClientTransport(new URL(server.baseUrl!), options)
} else { } else {
throw new Error('Invalid server type') throw new Error('Invalid server type')
} }

View File

@ -1072,6 +1072,8 @@
"editServer": "Edit Server", "editServer": "Edit Server",
"env": "Environment Variables", "env": "Environment Variables",
"envTooltip": "Format: KEY=value, one per line", "envTooltip": "Format: KEY=value, one per line",
"headers": "Headers",
"headersTooltip": "Custom headers for HTTP requests",
"findMore": "Find More MCP", "findMore": "Find More MCP",
"searchNpx": "Search MCP", "searchNpx": "Search MCP",
"install": "Install", "install": "Install",

View File

@ -1071,6 +1071,8 @@
"editServer": "サーバーを編集", "editServer": "サーバーを編集",
"env": "環境変数", "env": "環境変数",
"envTooltip": "形式: KEY=value, 1行に1つ", "envTooltip": "形式: KEY=value, 1行に1つ",
"headers": "ヘッダー",
"headersTooltip": "HTTP リクエストのカスタムヘッダー",
"findMore": "MCP を見つける", "findMore": "MCP を見つける",
"searchNpx": "MCP を検索", "searchNpx": "MCP を検索",
"install": "インストール", "install": "インストール",

View File

@ -1071,6 +1071,8 @@
"editServer": "Редактировать сервер", "editServer": "Редактировать сервер",
"env": "Переменные окружения", "env": "Переменные окружения",
"envTooltip": "Формат: KEY=value, по одной на строку", "envTooltip": "Формат: KEY=value, по одной на строку",
"headers": "Заголовки",
"headersTooltip": "Пользовательские заголовки для HTTP-запросов",
"findMore": "Найти больше MCP", "findMore": "Найти больше MCP",
"searchNpx": "Найти MCP", "searchNpx": "Найти MCP",
"install": "Установить", "install": "Установить",

View File

@ -1072,6 +1072,8 @@
"editServer": "编辑服务器", "editServer": "编辑服务器",
"env": "环境变量", "env": "环境变量",
"envTooltip": "格式KEY=value每行一个", "envTooltip": "格式KEY=value每行一个",
"headers": "请求头",
"headersTooltip": "HTTP 请求的自定义请求头",
"findMore": "更多 MCP", "findMore": "更多 MCP",
"searchNpx": "搜索 MCP", "searchNpx": "搜索 MCP",
"install": "安装", "install": "安装",

View File

@ -1071,6 +1071,8 @@
"editServer": "編輯伺服器", "editServer": "編輯伺服器",
"env": "環境變數", "env": "環境變數",
"envTooltip": "格式KEY=value每行一個", "envTooltip": "格式KEY=value每行一個",
"headers": "請求標頭",
"headersTooltip": "HTTP 請求的自定義標頭",
"findMore": "更多 MCP", "findMore": "更多 MCP",
"searchNpx": "搜索 MCP", "searchNpx": "搜索 MCP",
"install": "安裝", "install": "安裝",

View File

@ -27,6 +27,7 @@ interface MCPFormValues {
args?: string args?: string
env?: string env?: string
isActive: boolean isActive: boolean
headers?: string
} }
interface Registry { interface Registry {
@ -101,6 +102,11 @@ const McpSettings: React.FC<Props> = ({ server }) => {
? Object.entries(server.env) ? Object.entries(server.env)
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
.join('\n') .join('\n')
: '',
headers: server.headers
? Object.entries(server.headers)
.map(([key, value]) => `${key}=${value}`)
.join('\n')
: '' : ''
}) })
}, [server, form]) }, [server, form])
@ -218,6 +224,20 @@ const McpSettings: React.FC<Props> = ({ server }) => {
mcpServer.env = env mcpServer.env = env
} }
if (values.headers) {
const headers: Record<string, string> = {}
values.headers.split('\n').forEach((line) => {
if (line.trim()) {
const [key, ...chunks] = line.split(':')
const value = chunks.join(':')
if (key && value) {
headers[key.trim()] = value.trim()
}
}
})
mcpServer.headers = headers
}
try { try {
await window.api.mcp.restartServer(mcpServer) await window.api.mcp.restartServer(mcpServer)
updateMCPServer({ ...mcpServer, isActive: true }) updateMCPServer({ ...mcpServer, isActive: true })
@ -400,22 +420,40 @@ const McpSettings: React.FC<Props> = ({ server }) => {
</Form.Item> </Form.Item>
)} )}
{serverType === 'sse' && ( {serverType === 'sse' && (
<Form.Item <>
name="baseUrl" <Form.Item
label={t('settings.mcp.url')} name="baseUrl"
rules={[{ required: serverType === 'sse', message: '' }]} label={t('settings.mcp.url')}
tooltip={t('settings.mcp.baseUrlTooltip')}> rules={[{ required: serverType === 'sse', message: '' }]}
<Input placeholder="http://localhost:3000/sse" /> tooltip={t('settings.mcp.baseUrlTooltip')}>
</Form.Item> <Input placeholder="http://localhost:3000/sse" />
</Form.Item>
<Form.Item name="headers" label={t('settings.mcp.headers')} tooltip={t('settings.mcp.headersTooltip')}>
<TextArea
rows={3}
placeholder={`Content-Type=application/json\nAuthorization=Bearer token`}
style={{ fontFamily: 'monospace' }}
/>
</Form.Item>
</>
)} )}
{serverType === 'streamableHttp' && ( {serverType === 'streamableHttp' && (
<Form.Item <>
name="baseUrl" <Form.Item
label={t('settings.mcp.url')} name="baseUrl"
rules={[{ required: serverType === 'streamableHttp', message: '' }]} label={t('settings.mcp.url')}
tooltip={t('settings.mcp.baseUrlTooltip')}> rules={[{ required: serverType === 'streamableHttp', message: '' }]}
<Input placeholder="http://localhost:3000/mcp" /> tooltip={t('settings.mcp.baseUrlTooltip')}>
</Form.Item> <Input placeholder="http://localhost:3000/mcp" />
</Form.Item>
<Form.Item name="headers" label={t('settings.mcp.headers')} tooltip={t('settings.mcp.headersTooltip')}>
<TextArea
rows={3}
placeholder={`Content-Type=application/json\nAuthorization=Bearer token`}
style={{ fontFamily: 'monospace' }}
/>
</Form.Item>
</>
)} )}
{serverType === 'stdio' && ( {serverType === 'stdio' && (
<> <>

View File

@ -386,6 +386,7 @@ export interface MCPServer {
env?: Record<string, string> env?: Record<string, string>
isActive: boolean isActive: boolean
disabledTools?: string[] // List of tool names that are disabled for this server disabledTools?: string[] // List of tool names that are disabled for this server
headers?: Record<string, string> // Custom headers to be sent with requests to this server
} }
export interface MCPToolInputSchema { export interface MCPToolInputSchema {