feat(mcp): add custom headers functionality to MCP service dialog

Introduce a new feature allowing users to add custom HTTP headers for MCP requests. This includes:

- A user interface for adding and managing custom headers within the MCP service dialog.
- Localization support for custom headers in English, Korean, Russian, and Chinese.
- Updates to the form submission process to include custom headers, ensuring they are serialized correctly.
- Removal of warnings for ignored headers, as all headers are now captured in the custom headers editor.

This enhancement improves the flexibility and usability of the MCP service configuration.
This commit is contained in:
wizardchen
2026-06-22 21:03:42 +08:00
committed by lyingbug
parent 518add2641
commit 94ce8fedb1
5 changed files with 141 additions and 15 deletions
+8 -2
View File
@@ -4388,6 +4388,13 @@ export default {
createFailed: 'Failed to create MCP service',
updateFailed: 'Failed to update MCP service'
},
customHeaders: {
label: 'Custom Headers (optional)',
desc: 'HTTP headers attached to every MCP request, commonly used for enterprise gateway auth, tracing, etc.',
add: 'Add Header',
keyPlaceholder: 'Header name',
valuePlaceholder: 'Header value'
},
codeImport: {
toggle: 'Import from code',
hint: 'Paste a standard mcpServers JSON config to auto-fill the form',
@@ -4404,8 +4411,7 @@ export default {
},
toasts: {
filled: 'Form filled, please review and save',
multipleServers: 'Multiple services detected, imported the first one: {name}',
ignoredHeaders: 'The following headers were not imported: {headers}'
multipleServers: 'Multiple services detected, imported the first one: {name}'
}
}
},
+7 -1
View File
@@ -4384,6 +4384,13 @@ export default {
createFailed: "MCP 서비스 생성 실패",
updateFailed: "MCP 서비스 업데이트 실패",
},
customHeaders: {
label: "사용자 지정 헤더(선택 사항)",
desc: "모든 MCP 요청에 추가되는 HTTP 헤더로, 기업 게이트웨이 인증, 추적 등에 자주 사용됩니다.",
add: "헤더 추가",
keyPlaceholder: "헤더 이름",
valuePlaceholder: "헤더 값",
},
codeImport: {
toggle: "코드에서 가져오기",
hint: "표준 mcpServers JSON 설정을 붙여넣으면 양식이 자동으로 채워집니다",
@@ -4401,7 +4408,6 @@ export default {
toasts: {
filled: "양식이 채워졌습니다. 확인 후 저장해 주세요",
multipleServers: "여러 서비스가 감지되어 첫 번째 항목을 가져왔습니다: {name}",
ignoredHeaders: "다음 헤더는 가져오지 않았습니다: {headers}",
},
},
},
+8 -2
View File
@@ -4037,6 +4037,13 @@ export default {
createFailed: 'Не удалось создать сервис MCP',
updateFailed: 'Не удалось обновить сервис MCP'
},
customHeaders: {
label: 'Пользовательские заголовки (необязательно)',
desc: 'HTTP-заголовки, добавляемые к каждому запросу MCP; обычно используются для аутентификации на корпоративном шлюзе, трассировки и т. п.',
add: 'Добавить заголовок',
keyPlaceholder: 'Имя заголовка',
valuePlaceholder: 'Значение заголовка'
},
codeImport: {
toggle: 'Импорт из кода',
hint: 'Вставьте стандартную JSON-конфигурацию mcpServers для автозаполнения формы',
@@ -4053,8 +4060,7 @@ export default {
},
toasts: {
filled: 'Форма заполнена, проверьте и сохраните',
multipleServers: 'Обнаружено несколько сервисов, импортирован первый: {name}',
ignoredHeaders: 'Следующие заголовки не импортированы: {headers}'
multipleServers: 'Обнаружено несколько сервисов, импортирован первый: {name}'
}
}
},
+7 -1
View File
@@ -4406,6 +4406,13 @@ export default {
revoked: "已撤销授权",
revokeFailed: "撤销失败",
},
customHeaders: {
label: "自定义请求头(可选)",
desc: "附加到每次 MCP 请求的 HTTP 请求头,常用于企业网关鉴权、链路追踪等场景。",
add: "添加请求头",
keyPlaceholder: "Header 名称",
valuePlaceholder: "Header 值",
},
codeImport: {
toggle: "从代码导入",
hint: "粘贴标准 mcpServers JSON 配置,自动填充表单",
@@ -4423,7 +4430,6 @@ export default {
toasts: {
filled: "已填充表单,请检查后保存",
multipleServers: "检测到多个服务,已导入第一个:{name}",
ignoredHeaders: "以下 header 未导入:{headers}",
},
},
},
@@ -150,6 +150,44 @@
<label class="form-label required">{{ t('mcpServiceDialog.serviceUrl') }}</label>
<t-input v-model="formData.url" :placeholder="t('mcpServiceDialog.serviceUrlPlaceholder')" />
</div>
<!-- 自定义请求头:附加到每次 MCP 请求的 HTTP header(与模型管理同款交互) -->
<div class="form-item">
<div class="custom-headers-header">
<label class="form-label" style="margin-bottom: 0">
{{ t('mcpServiceDialog.customHeaders.label') }}
</label>
<t-button variant="text" size="small" theme="primary" @click="addCustomHeader">
<template #icon><t-icon name="add" /></template>
{{ t('mcpServiceDialog.customHeaders.add') }}
</t-button>
</div>
<p class="form-desc custom-headers-desc">{{ t('mcpServiceDialog.customHeaders.desc') }}</p>
<div v-if="formData.headers.length > 0" class="custom-headers-list">
<div v-for="(item, idx) in formData.headers" :key="idx" class="custom-header-row">
<t-input
v-model="item.key"
:placeholder="t('mcpServiceDialog.customHeaders.keyPlaceholder')"
class="custom-header-key"
/>
<t-input
v-model="item.value"
:placeholder="t('mcpServiceDialog.customHeaders.valuePlaceholder')"
class="custom-header-value"
/>
<t-button
variant="text"
shape="square"
size="small"
class="custom-header-remove"
:aria-label="t('common.delete')"
@click="removeCustomHeader(idx)"
>
<t-icon name="close" />
</t-button>
</div>
</div>
</div>
</section>
<!-- Section 3 — 认证配置(无 / API Key / Bearer Token / OAuth) -->
@@ -366,6 +404,10 @@ const formData = ref({
enabled: true,
transport_type: 'sse' as 'sse' | 'http-streamable',
url: '',
// Custom HTTP headers attached to every MCP request — edited as key/value
// rows here, serialised to a Record<string,string> on submit (top-level
// `headers`). Independent of the auth strategy.
headers: [] as { key: string; value: string }[],
auth_config: {
// Authentication strategy: '' (none) | 'api_key' | 'bearer' | 'oauth'.
auth_type: '' as '' | 'api_key' | 'bearer' | 'oauth',
@@ -383,6 +425,14 @@ const formData = ref({
},
})
// ---- Custom headers (key/value editor, same UX as ModelEditorDialog) ----
const addCustomHeader = () => {
formData.value.headers.push({ key: '', value: '' })
}
const removeCustomHeader = (idx: number) => {
formData.value.headers.splice(idx, 1)
}
// ---- Code import (paste standard mcpServers JSON) ----
const codeImportOpen = ref(false)
const codeImportText = ref('')
@@ -411,12 +461,12 @@ function applyServerConfig(name: string, cfg: Record<string, unknown>) {
transport = /\/sse\/?($|\?)/i.test(url) ? 'sse' : 'http-streamable'
}
// Auth: recognise Bearer / API-key headers; other headers can't be held by
// the form, so collect them to warn the user about what was dropped.
// Auth: recognise Bearer / API-key headers; every other header lands in the
// custom-headers editor so nothing is silently dropped.
let authType: '' | 'api_key' | 'bearer' = ''
let token = ''
let apiKey = ''
const ignoredHeaders: string[] = []
const customHeaders: { key: string; value: string }[] = []
const headers = (cfg.headers && typeof cfg.headers === 'object')
? (cfg.headers as Record<string, unknown>)
: {}
@@ -430,7 +480,7 @@ function applyServerConfig(name: string, cfg: Record<string, unknown>) {
authType = 'api_key'
apiKey = strVal.trim()
} else {
ignoredHeaders.push(key)
customHeaders.push({ key, value: strVal })
}
}
@@ -438,16 +488,12 @@ function applyServerConfig(name: string, cfg: Record<string, unknown>) {
formData.value.url = url
formData.value.transport_type = transport
if (typeof cfg.description === 'string') formData.value.description = cfg.description
formData.value.headers = customHeaders
formData.value.auth_config.auth_type = authType
formData.value.auth_config.token = token
formData.value.auth_config.api_key = apiKey
formData.value.auth_config.scopes = []
if (ignoredHeaders.length) {
MessagePlugin.warning(
t('mcpServiceDialog.codeImport.toasts.ignoredHeaders', { headers: ignoredHeaders.join(', ') }) as string,
)
}
return true
}
@@ -778,6 +824,7 @@ const resetForm = () => {
enabled: true,
transport_type: 'sse',
url: '',
headers: [],
auth_config: { auth_type: '', api_key: '', token: '', scopes: [], auth_server_metadata_url: '' },
advanced_config: { timeout: 30, retry_count: 3, retry_delay: 1 },
}
@@ -802,6 +849,9 @@ watch(
enabled: service.enabled ?? true,
transport_type: transportType as 'sse' | 'http-streamable',
url: service.url || '',
headers: service.headers
? Object.entries(service.headers).map(([key, value]) => ({ key, value: String(value) }))
: [],
// Credentials are owned by CredentialResource in edit mode, but reset
// the local state too so a switch to add-mode starts clean.
auth_config: {
@@ -832,6 +882,15 @@ const handleSubmit = async () => {
submitting.value = true
try {
// Custom headers: trim, drop blank rows, collapse to a Record. Always send
// the field (even when empty) so removing the last header persists on edit.
const headersMap: Record<string, string> = {}
for (const item of formData.value.headers) {
const key = (item.key ?? '').trim()
const value = (item.value ?? '').trim()
if (key && value) headersMap[key] = value
}
const data: Partial<MCPService> = {
name: formData.value.name,
description: formData.value.description,
@@ -839,6 +898,7 @@ const handleSubmit = async () => {
transport_type: formData.value.transport_type,
advanced_config: formData.value.advanced_config,
url: formData.value.url || undefined,
headers: headersMap,
}
// Non-secret auth config (strategy + OAuth params) flows through the main
@@ -901,6 +961,48 @@ const handleClose = () => {
margin-bottom: 0;
}
// ---- 自定义请求头(与 ModelEditorDialog 同款 key/value 行) ----
.custom-headers-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.custom-headers-desc {
margin: 4px 0 8px;
}
.custom-headers-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.custom-header-row {
display: flex;
align-items: center;
gap: 8px;
}
.custom-header-key {
flex: 0 0 38%;
}
.custom-header-value {
flex: 1;
min-width: 0;
}
.custom-header-remove {
flex-shrink: 0;
color: var(--td-text-color-placeholder);
&:hover,
&:focus-visible {
color: var(--td-error-color);
}
}
// ---- 从代码导入 ----
.code-import {
&__toggle {