diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index d9f2e45dd..a6bc1c37e 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -865,6 +865,7 @@ class ToolProviderMCPApi(Resource): parser.add_argument( "sse_read_timeout", type=float, required=False, nullable=False, location="json", default=300 ) + parser.add_argument("headers", type=dict, required=False, nullable=True, location="json", default={}) args = parser.parse_args() user = current_user if not is_valid_url(args["server_url"]): @@ -881,6 +882,7 @@ class ToolProviderMCPApi(Resource): server_identifier=args["server_identifier"], timeout=args["timeout"], sse_read_timeout=args["sse_read_timeout"], + headers=args["headers"], ) ) @@ -898,6 +900,7 @@ class ToolProviderMCPApi(Resource): parser.add_argument("server_identifier", type=str, required=True, nullable=False, location="json") parser.add_argument("timeout", type=float, required=False, nullable=True, location="json") parser.add_argument("sse_read_timeout", type=float, required=False, nullable=True, location="json") + parser.add_argument("headers", type=dict, required=False, nullable=True, location="json") args = parser.parse_args() if not is_valid_url(args["server_url"]): if "[__HIDDEN__]" in args["server_url"]: @@ -915,6 +918,7 @@ class ToolProviderMCPApi(Resource): server_identifier=args["server_identifier"], timeout=args.get("timeout"), sse_read_timeout=args.get("sse_read_timeout"), + headers=args.get("headers"), ) return {"result": "success"} @@ -951,6 +955,9 @@ class ToolMCPAuthApi(Resource): authed=False, authorization_code=args["authorization_code"], for_list=True, + headers=provider.decrypted_headers, + timeout=provider.timeout, + sse_read_timeout=provider.sse_read_timeout, ): MCPToolManageService.update_mcp_provider_credentials( mcp_provider=provider, diff --git a/api/core/tools/entities/api_entities.py b/api/core/tools/entities/api_entities.py index 187406fc2..ca3be26ff 100644 --- a/api/core/tools/entities/api_entities.py +++ b/api/core/tools/entities/api_entities.py @@ -43,6 +43,10 @@ class ToolProviderApiEntity(BaseModel): server_url: Optional[str] = Field(default="", description="The server url of the tool") updated_at: int = Field(default_factory=lambda: int(datetime.now().timestamp())) server_identifier: Optional[str] = Field(default="", description="The server identifier of the MCP tool") + timeout: Optional[float] = Field(default=30.0, description="The timeout of the MCP tool") + sse_read_timeout: Optional[float] = Field(default=300.0, description="The SSE read timeout of the MCP tool") + masked_headers: Optional[dict[str, str]] = Field(default=None, description="The masked headers of the MCP tool") + original_headers: Optional[dict[str, str]] = Field(default=None, description="The original headers of the MCP tool") @field_validator("tools", mode="before") @classmethod @@ -65,6 +69,10 @@ class ToolProviderApiEntity(BaseModel): if self.type == ToolProviderType.MCP: optional_fields.update(self.optional_field("updated_at", self.updated_at)) optional_fields.update(self.optional_field("server_identifier", self.server_identifier)) + optional_fields.update(self.optional_field("timeout", self.timeout)) + optional_fields.update(self.optional_field("sse_read_timeout", self.sse_read_timeout)) + optional_fields.update(self.optional_field("masked_headers", self.masked_headers)) + optional_fields.update(self.optional_field("original_headers", self.original_headers)) return { "id": self.id, "author": self.author, diff --git a/api/core/tools/mcp_tool/provider.py b/api/core/tools/mcp_tool/provider.py index dd9d3a137..5f6eb045a 100644 --- a/api/core/tools/mcp_tool/provider.py +++ b/api/core/tools/mcp_tool/provider.py @@ -94,7 +94,7 @@ class MCPToolProviderController(ToolProviderController): provider_id=db_provider.server_identifier or "", tenant_id=db_provider.tenant_id or "", server_url=db_provider.decrypted_server_url, - headers={}, # TODO: get headers from db provider + headers=db_provider.decrypted_headers or {}, timeout=db_provider.timeout, sse_read_timeout=db_provider.sse_read_timeout, ) diff --git a/api/migrations/versions/2025_09_08_1007-c20211f18133_add_headers_to_mcp_provider.py b/api/migrations/versions/2025_09_08_1007-c20211f18133_add_headers_to_mcp_provider.py new file mode 100644 index 000000000..99d47478f --- /dev/null +++ b/api/migrations/versions/2025_09_08_1007-c20211f18133_add_headers_to_mcp_provider.py @@ -0,0 +1,27 @@ +"""add_headers_to_mcp_provider + +Revision ID: c20211f18133 +Revises: 8d289573e1da +Create Date: 2025-08-29 10:07:54.163626 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c20211f18133' +down_revision = 'b95962a3885c' +branch_labels = None +depends_on = None + + +def upgrade(): + # Add encrypted_headers column to tool_mcp_providers table + op.add_column('tool_mcp_providers', sa.Column('encrypted_headers', sa.Text(), nullable=True)) + + +def downgrade(): + # Remove encrypted_headers column from tool_mcp_providers table + op.drop_column('tool_mcp_providers', 'encrypted_headers') diff --git a/api/models/tools.py b/api/models/tools.py index 09c8cd400..96ad76eae 100644 --- a/api/models/tools.py +++ b/api/models/tools.py @@ -280,6 +280,8 @@ class MCPToolProvider(Base): ) timeout: Mapped[float] = mapped_column(sa.Float, nullable=False, server_default=sa.text("30")) sse_read_timeout: Mapped[float] = mapped_column(sa.Float, nullable=False, server_default=sa.text("300")) + # encrypted headers for MCP server requests + encrypted_headers: Mapped[str | None] = mapped_column(sa.Text, nullable=True) def load_user(self) -> Account | None: return db.session.query(Account).where(Account.id == self.user_id).first() @@ -310,6 +312,62 @@ class MCPToolProvider(Base): def decrypted_server_url(self) -> str: return encrypter.decrypt_token(self.tenant_id, self.server_url) + @property + def decrypted_headers(self) -> dict[str, Any]: + """Get decrypted headers for MCP server requests.""" + from core.entities.provider_entities import BasicProviderConfig + from core.helper.provider_cache import NoOpProviderCredentialCache + from core.tools.utils.encryption import create_provider_encrypter + + try: + if not self.encrypted_headers: + return {} + + headers_data = json.loads(self.encrypted_headers) + + # Create dynamic config for all headers as SECRET_INPUT + config = [BasicProviderConfig(type=BasicProviderConfig.Type.SECRET_INPUT, name=key) for key in headers_data] + + encrypter_instance, _ = create_provider_encrypter( + tenant_id=self.tenant_id, + config=config, + cache=NoOpProviderCredentialCache(), + ) + + result = encrypter_instance.decrypt(headers_data) + return result + except Exception: + return {} + + @property + def masked_headers(self) -> dict[str, Any]: + """Get masked headers for frontend display.""" + from core.entities.provider_entities import BasicProviderConfig + from core.helper.provider_cache import NoOpProviderCredentialCache + from core.tools.utils.encryption import create_provider_encrypter + + try: + if not self.encrypted_headers: + return {} + + headers_data = json.loads(self.encrypted_headers) + + # Create dynamic config for all headers as SECRET_INPUT + config = [BasicProviderConfig(type=BasicProviderConfig.Type.SECRET_INPUT, name=key) for key in headers_data] + + encrypter_instance, _ = create_provider_encrypter( + tenant_id=self.tenant_id, + config=config, + cache=NoOpProviderCredentialCache(), + ) + + # First decrypt, then mask + decrypted_headers = encrypter_instance.decrypt(headers_data) + result = encrypter_instance.mask_tool_credentials(decrypted_headers) + return result + except Exception: + return {} + @property def masked_server_url(self) -> str: def mask_url(url: str, mask_char: str = "*") -> str: diff --git a/api/services/tools/mcp_tools_manage_service.py b/api/services/tools/mcp_tools_manage_service.py index b557d2155..7e301c9ba 100644 --- a/api/services/tools/mcp_tools_manage_service.py +++ b/api/services/tools/mcp_tools_manage_service.py @@ -1,7 +1,7 @@ import hashlib import json from datetime import datetime -from typing import Any +from typing import Any, cast from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError @@ -27,6 +27,36 @@ class MCPToolManageService: Service class for managing mcp tools. """ + @staticmethod + def _encrypt_headers(headers: dict[str, str], tenant_id: str) -> dict[str, str]: + """ + Encrypt headers using ProviderConfigEncrypter with all headers as SECRET_INPUT. + + Args: + headers: Dictionary of headers to encrypt + tenant_id: Tenant ID for encryption + + Returns: + Dictionary with all headers encrypted + """ + if not headers: + return {} + + from core.entities.provider_entities import BasicProviderConfig + from core.helper.provider_cache import NoOpProviderCredentialCache + from core.tools.utils.encryption import create_provider_encrypter + + # Create dynamic config for all headers as SECRET_INPUT + config = [BasicProviderConfig(type=BasicProviderConfig.Type.SECRET_INPUT, name=key) for key in headers] + + encrypter_instance, _ = create_provider_encrypter( + tenant_id=tenant_id, + config=config, + cache=NoOpProviderCredentialCache(), + ) + + return cast(dict[str, str], encrypter_instance.encrypt(headers)) + @staticmethod def get_mcp_provider_by_provider_id(provider_id: str, tenant_id: str) -> MCPToolProvider: res = ( @@ -61,6 +91,7 @@ class MCPToolManageService: server_identifier: str, timeout: float, sse_read_timeout: float, + headers: dict[str, str] | None = None, ) -> ToolProviderApiEntity: server_url_hash = hashlib.sha256(server_url.encode()).hexdigest() existing_provider = ( @@ -83,6 +114,12 @@ class MCPToolManageService: if existing_provider.server_identifier == server_identifier: raise ValueError(f"MCP tool {server_identifier} already exists") encrypted_server_url = encrypter.encrypt_token(tenant_id, server_url) + # Encrypt headers + encrypted_headers = None + if headers: + encrypted_headers_dict = MCPToolManageService._encrypt_headers(headers, tenant_id) + encrypted_headers = json.dumps(encrypted_headers_dict) + mcp_tool = MCPToolProvider( tenant_id=tenant_id, name=name, @@ -95,6 +132,7 @@ class MCPToolManageService: server_identifier=server_identifier, timeout=timeout, sse_read_timeout=sse_read_timeout, + encrypted_headers=encrypted_headers, ) db.session.add(mcp_tool) db.session.commit() @@ -118,9 +156,21 @@ class MCPToolManageService: mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id) server_url = mcp_provider.decrypted_server_url authed = mcp_provider.authed + headers = mcp_provider.decrypted_headers + timeout = mcp_provider.timeout + sse_read_timeout = mcp_provider.sse_read_timeout try: - with MCPClient(server_url, provider_id, tenant_id, authed=authed, for_list=True) as mcp_client: + with MCPClient( + server_url, + provider_id, + tenant_id, + authed=authed, + for_list=True, + headers=headers, + timeout=timeout, + sse_read_timeout=sse_read_timeout, + ) as mcp_client: tools = mcp_client.list_tools() except MCPAuthError: raise ValueError("Please auth the tool first") @@ -172,6 +222,7 @@ class MCPToolManageService: server_identifier: str, timeout: float | None = None, sse_read_timeout: float | None = None, + headers: dict[str, str] | None = None, ): mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id) @@ -207,6 +258,13 @@ class MCPToolManageService: mcp_provider.timeout = timeout if sse_read_timeout is not None: mcp_provider.sse_read_timeout = sse_read_timeout + if headers is not None: + # Encrypt headers + if headers: + encrypted_headers_dict = MCPToolManageService._encrypt_headers(headers, tenant_id) + mcp_provider.encrypted_headers = json.dumps(encrypted_headers_dict) + else: + mcp_provider.encrypted_headers = None db.session.commit() except IntegrityError as e: db.session.rollback() @@ -242,6 +300,12 @@ class MCPToolManageService: @classmethod def _re_connect_mcp_provider(cls, server_url: str, provider_id: str, tenant_id: str): + # Get the existing provider to access headers and timeout settings + mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id) + headers = mcp_provider.decrypted_headers + timeout = mcp_provider.timeout + sse_read_timeout = mcp_provider.sse_read_timeout + try: with MCPClient( server_url, @@ -249,6 +313,9 @@ class MCPToolManageService: tenant_id, authed=False, for_list=True, + headers=headers, + timeout=timeout, + sse_read_timeout=sse_read_timeout, ) as mcp_client: tools = mcp_client.list_tools() return { diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index d084b377e..f5fc7f951 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -237,6 +237,10 @@ class ToolTransformService: label=I18nObject(en_US=db_provider.name, zh_Hans=db_provider.name), description=I18nObject(en_US="", zh_Hans=""), server_identifier=db_provider.server_identifier, + timeout=db_provider.timeout, + sse_read_timeout=db_provider.sse_read_timeout, + masked_headers=db_provider.masked_headers, + original_headers=db_provider.decrypted_headers, ) @staticmethod diff --git a/api/tests/test_containers_integration_tests/services/tools/test_mcp_tools_manage_service.py b/api/tests/test_containers_integration_tests/services/tools/test_mcp_tools_manage_service.py index 0fcaf8671..dd22dcbfd 100644 --- a/api/tests/test_containers_integration_tests/services/tools/test_mcp_tools_manage_service.py +++ b/api/tests/test_containers_integration_tests/services/tools/test_mcp_tools_manage_service.py @@ -706,7 +706,14 @@ class TestMCPToolManageService: # Verify mock interactions mock_mcp_client.assert_called_once_with( - "https://example.com/mcp", mcp_provider.id, tenant.id, authed=False, for_list=True + "https://example.com/mcp", + mcp_provider.id, + tenant.id, + authed=False, + for_list=True, + headers={}, + timeout=30.0, + sse_read_timeout=300.0, ) def test_list_mcp_tool_from_remote_server_auth_error( @@ -1181,6 +1188,11 @@ class TestMCPToolManageService: db_session_with_containers, mock_external_service_dependencies ) + # Create MCP provider first + mcp_provider = self._create_test_mcp_provider( + db_session_with_containers, mock_external_service_dependencies, tenant.id, account.id + ) + # Mock MCPClient and its context manager mock_tools = [ type("MockTool", (), {"model_dump": lambda self: {"name": "test_tool_1", "description": "Test tool 1"}})(), @@ -1194,7 +1206,7 @@ class TestMCPToolManageService: # Act: Execute the method under test result = MCPToolManageService._re_connect_mcp_provider( - "https://example.com/mcp", "test_provider_id", tenant.id + "https://example.com/mcp", mcp_provider.id, tenant.id ) # Assert: Verify the expected outcomes @@ -1213,7 +1225,14 @@ class TestMCPToolManageService: # Verify mock interactions mock_mcp_client.assert_called_once_with( - "https://example.com/mcp", "test_provider_id", tenant.id, authed=False, for_list=True + "https://example.com/mcp", + mcp_provider.id, + tenant.id, + authed=False, + for_list=True, + headers={}, + timeout=30.0, + sse_read_timeout=300.0, ) def test_re_connect_mcp_provider_auth_error(self, db_session_with_containers, mock_external_service_dependencies): @@ -1231,6 +1250,11 @@ class TestMCPToolManageService: db_session_with_containers, mock_external_service_dependencies ) + # Create MCP provider first + mcp_provider = self._create_test_mcp_provider( + db_session_with_containers, mock_external_service_dependencies, tenant.id, account.id + ) + # Mock MCPClient to raise authentication error with patch("services.tools.mcp_tools_manage_service.MCPClient") as mock_mcp_client: from core.mcp.error import MCPAuthError @@ -1240,7 +1264,7 @@ class TestMCPToolManageService: # Act: Execute the method under test result = MCPToolManageService._re_connect_mcp_provider( - "https://example.com/mcp", "test_provider_id", tenant.id + "https://example.com/mcp", mcp_provider.id, tenant.id ) # Assert: Verify the expected outcomes @@ -1265,6 +1289,11 @@ class TestMCPToolManageService: db_session_with_containers, mock_external_service_dependencies ) + # Create MCP provider first + mcp_provider = self._create_test_mcp_provider( + db_session_with_containers, mock_external_service_dependencies, tenant.id, account.id + ) + # Mock MCPClient to raise connection error with patch("services.tools.mcp_tools_manage_service.MCPClient") as mock_mcp_client: from core.mcp.error import MCPError @@ -1274,4 +1303,4 @@ class TestMCPToolManageService: # Act & Assert: Verify proper error handling with pytest.raises(ValueError, match="Failed to re-connect MCP server: Connection failed"): - MCPToolManageService._re_connect_mcp_provider("https://example.com/mcp", "test_provider_id", tenant.id) + MCPToolManageService._re_connect_mcp_provider("https://example.com/mcp", mcp_provider.id, tenant.id) diff --git a/web/app/components/tools/mcp/headers-input.tsx b/web/app/components/tools/mcp/headers-input.tsx new file mode 100644 index 000000000..81d62993c --- /dev/null +++ b/web/app/components/tools/mcp/headers-input.tsx @@ -0,0 +1,143 @@ +'use client' +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { RiAddLine, RiDeleteBinLine } from '@remixicon/react' +import Input from '@/app/components/base/input' +import Button from '@/app/components/base/button' +import ActionButton from '@/app/components/base/action-button' +import cn from '@/utils/classnames' + +export type HeaderItem = { + key: string + value: string +} + +type Props = { + headers: Record + onChange: (headers: Record) => void + readonly?: boolean + isMasked?: boolean +} + +const HeadersInput = ({ + headers, + onChange, + readonly = false, + isMasked = false, +}: Props) => { + const { t } = useTranslation() + + const headerItems = Object.entries(headers).map(([key, value]) => ({ key, value })) + + const handleItemChange = useCallback((index: number, field: 'key' | 'value', value: string) => { + const newItems = [...headerItems] + newItems[index] = { ...newItems[index], [field]: value } + + const newHeaders = newItems.reduce((acc, item) => { + if (item.key.trim()) + acc[item.key.trim()] = item.value + return acc + }, {} as Record) + + onChange(newHeaders) + }, [headerItems, onChange]) + + const handleRemoveItem = useCallback((index: number) => { + const newItems = headerItems.filter((_, i) => i !== index) + const newHeaders = newItems.reduce((acc, item) => { + if (item.key.trim()) + acc[item.key.trim()] = item.value + + return acc + }, {} as Record) + onChange(newHeaders) + }, [headerItems, onChange]) + + const handleAddItem = useCallback(() => { + const newHeaders = { ...headers, '': '' } + onChange(newHeaders) + }, [headers, onChange]) + + if (headerItems.length === 0) { + return ( +
+
+ {t('tools.mcp.modal.noHeaders')} +
+ {!readonly && ( + + )} +
+ ) + } + + return ( +
+ {isMasked && ( +
+ {t('tools.mcp.modal.maskedHeadersTip')} +
+ )} +
+
+
{t('tools.mcp.modal.headerKey')}
+
{t('tools.mcp.modal.headerValue')}
+
+ {headerItems.map((item, index) => ( +
+
+ handleItemChange(index, 'key', e.target.value)} + placeholder={t('tools.mcp.modal.headerKeyPlaceholder')} + className='rounded-none border-0' + readOnly={readonly} + /> +
+
+ handleItemChange(index, 'value', e.target.value)} + placeholder={t('tools.mcp.modal.headerValuePlaceholder')} + className='flex-1 rounded-none border-0' + readOnly={readonly} + /> + {!readonly && headerItems.length > 1 && ( + handleRemoveItem(index)} + className='mr-2' + > + + + )} +
+
+ ))} +
+ {!readonly && ( + + )} +
+ ) +} + +export default React.memo(HeadersInput) diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index 2df8349a9..bf395cf1c 100644 --- a/web/app/components/tools/mcp/modal.tsx +++ b/web/app/components/tools/mcp/modal.tsx @@ -9,6 +9,7 @@ import AppIcon from '@/app/components/base/app-icon' import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' +import HeadersInput from './headers-input' import type { AppIconType } from '@/types/app' import type { ToolWithProvider } from '@/app/components/workflow/types' import { noop } from 'lodash-es' @@ -29,6 +30,7 @@ export type DuplicateAppModalProps = { server_identifier: string timeout: number sse_read_timeout: number + headers?: Record }) => void onHide: () => void } @@ -66,12 +68,38 @@ const MCPModal = ({ const [appIcon, setAppIcon] = useState(getIcon(data)) const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '') - const [timeout, setMcpTimeout] = React.useState(30) - const [sseReadTimeout, setSseReadTimeout] = React.useState(300) + const [timeout, setMcpTimeout] = React.useState(data?.timeout || 30) + const [sseReadTimeout, setSseReadTimeout] = React.useState(data?.sse_read_timeout || 300) + const [headers, setHeaders] = React.useState>( + data?.masked_headers || {}, + ) const [isFetchingIcon, setIsFetchingIcon] = useState(false) const appIconRef = useRef(null) const isHovering = useHover(appIconRef) + // Update states when data changes (for edit mode) + React.useEffect(() => { + if (data) { + setUrl(data.server_url || '') + setName(data.name || '') + setServerIdentifier(data.server_identifier || '') + setMcpTimeout(data.timeout || 30) + setSseReadTimeout(data.sse_read_timeout || 300) + setHeaders(data.masked_headers || {}) + setAppIcon(getIcon(data)) + } + else { + // Reset for create mode + setUrl('') + setName('') + setServerIdentifier('') + setMcpTimeout(30) + setSseReadTimeout(300) + setHeaders({}) + setAppIcon(DEFAULT_ICON as AppIconSelection) + } + }, [data]) + const isValidUrl = (string: string) => { try { const urlPattern = /^(https?:\/\/)((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3})|localhost)(\:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?/i @@ -129,6 +157,7 @@ const MCPModal = ({ server_identifier: serverIdentifier.trim(), timeout: timeout || 30, sse_read_timeout: sseReadTimeout || 300, + headers: Object.keys(headers).length > 0 ? headers : undefined, }) if(isCreate) onHide() @@ -231,6 +260,18 @@ const MCPModal = ({ placeholder={t('tools.mcp.modal.timeoutPlaceholder')} /> +
+
+ {t('tools.mcp.modal.headers')} +
+
{t('tools.mcp.modal.headersTip')}
+ 0} + /> +
diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index 01f436ded..5a5c2e040 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -59,6 +59,8 @@ export type Collection = { server_identifier?: string timeout?: number sse_read_timeout?: number + headers?: Record + masked_headers?: Record } export type ToolParameter = { @@ -184,4 +186,5 @@ export type MCPServerDetail = { description: string status: string parameters?: Record + headers?: Record } diff --git a/web/i18n/en-US/tools.ts b/web/i18n/en-US/tools.ts index dfbfb82d8..97c557e62 100644 --- a/web/i18n/en-US/tools.ts +++ b/web/i18n/en-US/tools.ts @@ -187,12 +187,22 @@ const translation = { serverIdentifier: 'Server Identifier', serverIdentifierTip: 'Unique identifier for the MCP server within the workspace. Lowercase letters, numbers, underscores, and hyphens only. Up to 24 characters.', serverIdentifierPlaceholder: 'Unique identifier, e.g., my-mcp-server', - serverIdentifierWarning: 'The server won’t be recognized by existing apps after an ID change', + serverIdentifierWarning: 'The server won\'t be recognized by existing apps after an ID change', + headers: 'Headers', + headersTip: 'Additional HTTP headers to send with MCP server requests', + headerKey: 'Header Name', + headerValue: 'Header Value', + headerKeyPlaceholder: 'e.g., Authorization', + headerValuePlaceholder: 'e.g., Bearer token123', + addHeader: 'Add Header', + noHeaders: 'No custom headers configured', + maskedHeadersTip: 'Header values are masked for security. Changes will update the actual values.', cancel: 'Cancel', save: 'Save', confirm: 'Add & Authorize', timeout: 'Timeout', sseReadTimeout: 'SSE Read Timeout', + timeoutPlaceholder: '30', }, delete: 'Remove MCP Server', deleteConfirmTitle: 'Would you like to remove {{mcp}}?', diff --git a/web/i18n/ja-JP/tools.ts b/web/i18n/ja-JP/tools.ts index f7c005526..95ff8d649 100644 --- a/web/i18n/ja-JP/tools.ts +++ b/web/i18n/ja-JP/tools.ts @@ -37,8 +37,8 @@ const translation = { tip: 'スタジオでワークフローをツールに公開する', }, mcp: { - title: '利用可能なMCPツールはありません', - tip: 'MCPサーバーを追加する', + title: '利用可能な MCP ツールはありません', + tip: 'MCP サーバーを追加する', }, agent: { title: 'Agent strategy は利用できません', @@ -85,13 +85,13 @@ const translation = { apiKeyPlaceholder: 'API キーの HTTP ヘッダー名', apiValuePlaceholder: 'API キーを入力してください', api_key_query: 'クエリパラメータ', - queryParamPlaceholder: 'APIキーのクエリパラメータ名', + queryParamPlaceholder: 'API キーのクエリパラメータ名', api_key_header: 'ヘッダー', }, key: 'キー', value: '値', queryParam: 'クエリパラメータ', - queryParamTooltip: 'APIキーのクエリパラメータとして渡す名前、例えば「https://example.com/test?key=API_KEY」の「key」。', + queryParamTooltip: 'API キーのクエリパラメータとして渡す名前、例えば「https://example.com/test?key=API_KEY」の「key」。', }, authHeaderPrefix: { title: '認証タイプ', @@ -169,32 +169,32 @@ const translation = { noTools: 'ツールが見つかりませんでした', mcp: { create: { - cardTitle: 'MCPサーバー(HTTP)を追加', - cardLink: 'MCPサーバー統合について詳しく知る', + cardTitle: 'MCP サーバー(HTTP)を追加', + cardLink: 'MCP サーバー統合について詳しく知る', }, noConfigured: '未設定', updateTime: '更新日時', toolsCount: '{{count}} 個のツール', noTools: '利用可能なツールはありません', modal: { - title: 'MCPサーバー(HTTP)を追加', - editTitle: 'MCPサーバー(HTTP)を編集', + title: 'MCP サーバー(HTTP)を追加', + editTitle: 'MCP サーバー(HTTP)を編集', name: '名前とアイコン', - namePlaceholder: 'MCPサーバーの名前を入力', + namePlaceholder: 'MCP サーバーの名前を入力', serverUrl: 'サーバーURL', - serverUrlPlaceholder: 'サーバーエンドポイントのURLを入力', + serverUrlPlaceholder: 'サーバーエンドポイントの URL を入力', serverUrlWarning: 'サーバーアドレスを更新すると、このサーバーに依存するアプリケーションに影響を与える可能性があります。', serverIdentifier: 'サーバー識別子', - serverIdentifierTip: 'ワークスペース内でのMCPサーバーのユニーク識別子です。使用可能な文字は小文字、数字、アンダースコア、ハイフンで、最大24文字です。', + serverIdentifierTip: 'ワークスペース内での MCP サーバーのユニーク識別子です。使用可能な文字は小文字、数字、アンダースコア、ハイフンで、最大 24 文字です。', serverIdentifierPlaceholder: 'ユニーク識別子(例:my-mcp-server)', - serverIdentifierWarning: 'IDを変更すると、既存のアプリケーションではサーバーが認識できなくなります。', + serverIdentifierWarning: 'ID を変更すると、既存のアプリケーションではサーバーが認識できなくなります。', cancel: 'キャンセル', save: '保存', confirm: '追加して承認', timeout: 'タイムアウト', sseReadTimeout: 'SSE 読み取りタイムアウト', }, - delete: 'MCPサーバーを削除', + delete: 'MCP サーバーを削除', deleteConfirmTitle: '{{mcp}} を削除しますか?', operation: { edit: '編集', @@ -213,23 +213,23 @@ const translation = { toolUpdateConfirmTitle: 'ツールリストの更新', toolUpdateConfirmContent: 'ツールリストを更新すると、既存のアプリケーションに重大な影響を与える可能性があります。続行しますか?', toolsNum: '{{count}} 個のツールが含まれています', - onlyTool: '1つのツールが含まれています', + onlyTool: '1 つのツールが含まれています', identifier: 'サーバー識別子(クリックしてコピー)', server: { - title: 'MCPサーバー', + title: 'MCP サーバー', url: 'サーバーURL', - reGen: 'サーバーURLを再生成しますか?', + reGen: 'サーバーURL を再生成しますか?', addDescription: '説明を追加', edit: '説明を編集', modal: { - addTitle: 'MCPサーバーを有効化するための説明を追加', + addTitle: 'MCP サーバーを有効化するための説明を追加', editTitle: '説明を編集', description: '説明', - descriptionPlaceholder: 'このツールの機能とLLM(大規模言語モデル)での使用方法を説明してください。', + descriptionPlaceholder: 'このツールの機能と LLM(大規模言語モデル)での使用方法を説明してください。', parameters: 'パラメータ', - parametersTip: '各パラメータの説明を追加して、LLMがその目的と制約を理解できるようにします。', + parametersTip: '各パラメータの説明を追加して、LLM がその目的と制約を理解できるようにします。', parametersPlaceholder: 'パラメータの目的と制約', - confirm: 'MCPサーバーを有効にする', + confirm: 'MCP サーバーを有効にする', }, publishTip: 'アプリが公開されていません。まずアプリを公開してください。', }, diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index 82be1c9bb..9ade1caaa 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -81,7 +81,7 @@ const translation = { type: '鉴权类型', keyTooltip: 'HTTP 头部名称,如果你不知道是什么,可以将其保留为 Authorization 或设置为自定义值', queryParam: '查询参数', - queryParamTooltip: '用于传递 API 密钥查询参数的名称, 如 "https://example.com/test?key=API_KEY" 中的 "key"参数', + queryParamTooltip: '用于传递 API 密钥查询参数的名称,如 "https://example.com/test?key=API_KEY" 中的 "key"参数', types: { none: '无', api_key_header: '请求头', @@ -188,11 +188,21 @@ const translation = { serverIdentifierTip: '工作空间内服务器的唯一标识。支持小写字母、数字、下划线和连字符,最多 24 个字符。', serverIdentifierPlaceholder: '服务器唯一标识,例如 my-mcp-server', serverIdentifierWarning: '更改服务器标识符后,现有应用将无法识别此服务器', + headers: '请求头', + headersTip: '发送到 MCP 服务器的额外 HTTP 请求头', + headerKey: '请求头名称', + headerValue: '请求头值', + headerKeyPlaceholder: '例如:Authorization', + headerValuePlaceholder: '例如:Bearer token123', + addHeader: '添加请求头', + noHeaders: '未配置自定义请求头', + maskedHeadersTip: '为了安全,请求头值已被掩码处理。修改将更新实际值。', cancel: '取消', save: '保存', confirm: '添加并授权', timeout: '超时时间', sseReadTimeout: 'SSE 读取超时时间', + timeoutPlaceholder: '30', }, delete: '删除 MCP 服务', deleteConfirmTitle: '你想要删除 {{mcp}} 吗?', diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts index 4db6039ed..4bd265bf5 100644 --- a/web/service/use-tools.ts +++ b/web/service/use-tools.ts @@ -87,6 +87,7 @@ export const useCreateMCP = () => { icon_background?: string | null timeout?: number sse_read_timeout?: number + headers?: Record }) => { return post('workspaces/current/tool-provider/mcp', { body: { @@ -113,6 +114,7 @@ export const useUpdateMCP = ({ provider_id: string timeout?: number sse_read_timeout?: number + headers?: Record }) => { return put('workspaces/current/tool-provider/mcp', { body: {