From 94ce8fedb19ad5f4f70ec581033ad034be974598 Mon Sep 17 00:00:00 2001 From: wizardchen Date: Mon, 22 Jun 2026 21:03:42 +0800 Subject: [PATCH] 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. --- frontend/src/i18n/locales/en-US.ts | 10 +- frontend/src/i18n/locales/ko-KR.ts | 8 +- frontend/src/i18n/locales/ru-RU.ts | 10 +- frontend/src/i18n/locales/zh-CN.ts | 8 +- .../settings/components/McpServiceDialog.vue | 120 ++++++++++++++++-- 5 files changed, 141 insertions(+), 15 deletions(-) diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 3981a45e..e96de6bf 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -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}' } } }, diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index 18e7a3e5..d0c9e983 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -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}", }, }, }, diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index f437e502..ffed72a9 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -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}' } } }, diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 774149c9..3bf97676 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -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}", }, }, }, diff --git a/frontend/src/views/settings/components/McpServiceDialog.vue b/frontend/src/views/settings/components/McpServiceDialog.vue index 9ee7ba73..e280da21 100644 --- a/frontend/src/views/settings/components/McpServiceDialog.vue +++ b/frontend/src/views/settings/components/McpServiceDialog.vue @@ -150,6 +150,44 @@ + + +
+
+ + + + {{ t('mcpServiceDialog.customHeaders.add') }} + +
+

{{ t('mcpServiceDialog.customHeaders.desc') }}

+
+
+ + + + + +
+
+
@@ -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 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) { 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) : {} @@ -430,7 +480,7 @@ function applyServerConfig(name: string, cfg: Record) { 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) { 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 = {} + 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 = { 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 {