From ccf2c7f2cb92c62814703101ace227be2d12207a Mon Sep 17 00:00:00 2001
From: Xingyao Wang
Date: Thu, 8 May 2025 00:43:53 +0800
Subject: [PATCH] Add MCP configuration visualization and editing in settings
modal (#8029)
Co-authored-by: openhands
Co-authored-by: Ray Myers
---
.github/workflows/ghcr-build.yml | 4 +-
.../mcp-settings/mcp-config-editor.tsx | 84 ++++
.../mcp-settings/mcp-config-viewer.tsx | 141 ++++++
.../settings/mcp-settings/mcp-json-editor.tsx | 97 ++++
.../settings/mcp-settings/mcp-sse-servers.tsx | 42 ++
.../mcp-settings/mcp-stdio-servers.tsx | 58 +++
.../shared/modals/settings/settings-modal.tsx | 1 +
.../src/hooks/mutation/use-save-settings.ts | 22 +
frontend/src/hooks/query/use-settings.ts | 2 +
frontend/src/i18n/declaration.ts | 29 ++
frontend/src/i18n/translation.json | 435 ++++++++++++++++++
frontend/src/react-app-env.d.ts | 6 +
frontend/src/routes.ts | 1 +
frontend/src/routes/mcp-settings.tsx | 83 ++++
frontend/src/routes/settings.tsx | 1 +
frontend/src/services/observations.ts | 9 +
frontend/src/services/settings.ts | 4 +
frontend/src/state/chat-slice.ts | 25 +
frontend/src/types/action-type.tsx | 3 +
frontend/src/types/core/actions.ts | 12 +-
frontend/src/types/core/base.ts | 4 +-
frontend/src/types/core/observations.ts | 10 +-
frontend/src/types/observation-type.tsx | 3 +
frontend/src/types/settings.ts | 24 +
.../action_execution_client.py | 4 +-
openhands/server/session/session.py | 3 +-
openhands/server/settings.py | 2 +
openhands/storage/data_models/settings.py | 9 +
28 files changed, 1111 insertions(+), 7 deletions(-)
create mode 100644 frontend/src/components/features/settings/mcp-settings/mcp-config-editor.tsx
create mode 100644 frontend/src/components/features/settings/mcp-settings/mcp-config-viewer.tsx
create mode 100644 frontend/src/components/features/settings/mcp-settings/mcp-json-editor.tsx
create mode 100644 frontend/src/components/features/settings/mcp-settings/mcp-sse-servers.tsx
create mode 100644 frontend/src/components/features/settings/mcp-settings/mcp-stdio-servers.tsx
create mode 100644 frontend/src/routes/mcp-settings.tsx
diff --git a/.github/workflows/ghcr-build.yml b/.github/workflows/ghcr-build.yml
index 61671b7fb8..5245ae471a 100644
--- a/.github/workflows/ghcr-build.yml
+++ b/.github/workflows/ghcr-build.yml
@@ -40,7 +40,9 @@ jobs:
# Only build nikolaik on PRs, otherwise build both nikolaik and ubuntu.
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
json=$(jq -n -c '[
- { image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" }
+ { image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
+ { image: "ubuntu:24.04", tag: "ubuntu" }
+
]')
else
json=$(jq -n -c '[
diff --git a/frontend/src/components/features/settings/mcp-settings/mcp-config-editor.tsx b/frontend/src/components/features/settings/mcp-settings/mcp-config-editor.tsx
new file mode 100644
index 0000000000..2015650f52
--- /dev/null
+++ b/frontend/src/components/features/settings/mcp-settings/mcp-config-editor.tsx
@@ -0,0 +1,84 @@
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { MCPConfig } from "#/types/settings";
+import { I18nKey } from "#/i18n/declaration";
+import { MCPSSEServers } from "./mcp-sse-servers";
+import { MCPStdioServers } from "./mcp-stdio-servers";
+import { MCPJsonEditor } from "./mcp-json-editor";
+import { BrandButton } from "../brand-button";
+
+interface MCPConfigEditorProps {
+ mcpConfig?: MCPConfig;
+ onChange: (config: MCPConfig) => void;
+}
+
+export function MCPConfigEditor({ mcpConfig, onChange }: MCPConfigEditorProps) {
+ const { t } = useTranslation();
+ const [isEditing, setIsEditing] = useState(false);
+ const handleConfigChange = (newConfig: MCPConfig) => {
+ onChange(newConfig);
+ setIsEditing(false);
+ };
+
+ const config = mcpConfig || { sse_servers: [], stdio_servers: [] };
+
+ return (
+
+
+
+ {t(I18nKey.SETTINGS$MCP_TITLE)}
+
+
+ {t(I18nKey.SETTINGS$MCP_DESCRIPTION)}
+
+
+
+
+
+ {isEditing ? (
+
+ ) : (
+ <>
+
+
+ {config.sse_servers.length === 0 &&
+ config.stdio_servers.length === 0 && (
+
+ {t(I18nKey.SETTINGS$MCP_NO_SERVERS_CONFIGURED)}
+
+ )}
+ >
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/features/settings/mcp-settings/mcp-config-viewer.tsx b/frontend/src/components/features/settings/mcp-settings/mcp-config-viewer.tsx
new file mode 100644
index 0000000000..011d1151c1
--- /dev/null
+++ b/frontend/src/components/features/settings/mcp-settings/mcp-config-viewer.tsx
@@ -0,0 +1,141 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { MCPConfig, MCPSSEServer, MCPStdioServer } from "#/types/settings";
+import { I18nKey } from "#/i18n/declaration";
+
+interface MCPConfigViewerProps {
+ mcpConfig?: MCPConfig;
+}
+
+interface SSEServerDisplayProps {
+ server: string | MCPSSEServer;
+}
+
+function SSEServerDisplay({ server }: SSEServerDisplayProps) {
+ const { t } = useTranslation();
+
+ if (typeof server === "string") {
+ return (
+
+
+ {t(I18nKey.SETTINGS$MCP_URL)}:{" "}
+ {server}
+
+
+ );
+ }
+
+ return (
+
+
+ {t(I18nKey.SETTINGS$MCP_URL)}:{" "}
+ {server.url}
+
+ {server.api_key && (
+
+
+ {t(I18nKey.SETTINGS$MCP_API_KEY)}:
+ {" "}
+ {server.api_key ? "Set" : t(I18nKey.SETTINGS$MCP_API_KEY_NOT_SET)}
+
+ )}
+
+ );
+}
+
+interface StdioServerDisplayProps {
+ server: MCPStdioServer;
+}
+
+function StdioServerDisplay({ server }: StdioServerDisplayProps) {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t(I18nKey.SETTINGS$MCP_NAME)}:{" "}
+ {server.name}
+
+
+ {t(I18nKey.SETTINGS$MCP_COMMAND)}:{" "}
+ {server.command}
+
+ {server.args && server.args.length > 0 && (
+
+ {t(I18nKey.SETTINGS$MCP_ARGS)}:{" "}
+ {server.args.join(" ")}
+
+ )}
+ {server.env && Object.keys(server.env).length > 0 && (
+
+ {t(I18nKey.SETTINGS$MCP_ENV)}:{" "}
+ {Object.entries(server.env)
+ .map(([key, value]) => `${key}=${value}`)
+ .join(", ")}
+
+ )}
+
+ );
+}
+
+export function MCPConfigViewer({ mcpConfig }: MCPConfigViewerProps) {
+ const { t } = useTranslation();
+
+ if (
+ !mcpConfig ||
+ (mcpConfig.sse_servers.length === 0 && mcpConfig.stdio_servers.length === 0)
+ ) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {mcpConfig.sse_servers.length > 0 && (
+
+
+ {t(I18nKey.SETTINGS$MCP_SSE_SERVERS)}{" "}
+
+ ({mcpConfig.sse_servers.length})
+
+
+ {mcpConfig.sse_servers.map((server, index) => (
+
+ ))}
+
+ )}
+
+ {mcpConfig.stdio_servers.length > 0 && (
+
+
+ {t(I18nKey.SETTINGS$MCP_STDIO_SERVERS)}{" "}
+
+ ({mcpConfig.stdio_servers.length})
+
+
+ {mcpConfig.stdio_servers.map((server, index) => (
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/features/settings/mcp-settings/mcp-json-editor.tsx b/frontend/src/components/features/settings/mcp-settings/mcp-json-editor.tsx
new file mode 100644
index 0000000000..cfc84e545f
--- /dev/null
+++ b/frontend/src/components/features/settings/mcp-settings/mcp-json-editor.tsx
@@ -0,0 +1,97 @@
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { MCPConfig } from "#/types/settings";
+import { I18nKey } from "#/i18n/declaration";
+import { BrandButton } from "../brand-button";
+
+interface MCPJsonEditorProps {
+ mcpConfig?: MCPConfig;
+ onChange: (config: MCPConfig) => void;
+}
+
+export function MCPJsonEditor({ mcpConfig, onChange }: MCPJsonEditorProps) {
+ const { t } = useTranslation();
+ const [configText, setConfigText] = useState(() =>
+ mcpConfig
+ ? JSON.stringify(mcpConfig, null, 2)
+ : t(I18nKey.SETTINGS$MCP_DEFAULT_CONFIG),
+ );
+ const [error, setError] = useState(null);
+
+ const handleTextChange = (e: React.ChangeEvent) => {
+ setConfigText(e.target.value);
+ };
+
+ const handleSave = () => {
+ try {
+ const newConfig = JSON.parse(configText);
+
+ // Validate the structure
+ if (!newConfig.sse_servers || !Array.isArray(newConfig.sse_servers)) {
+ throw new Error(t(I18nKey.SETTINGS$MCP_ERROR_SSE_ARRAY));
+ }
+
+ if (!newConfig.stdio_servers || !Array.isArray(newConfig.stdio_servers)) {
+ throw new Error(t(I18nKey.SETTINGS$MCP_ERROR_STDIO_ARRAY));
+ }
+
+ // Validate SSE servers
+ for (const server of newConfig.sse_servers) {
+ if (
+ typeof server !== "string" &&
+ (!server.url || typeof server.url !== "string")
+ ) {
+ throw new Error(t(I18nKey.SETTINGS$MCP_ERROR_SSE_URL));
+ }
+ }
+
+ // Validate stdio servers
+ for (const server of newConfig.stdio_servers) {
+ if (!server.name || !server.command) {
+ throw new Error(t(I18nKey.SETTINGS$MCP_ERROR_STDIO_PROPS));
+ }
+ }
+
+ onChange(newConfig);
+ setError(null);
+ } catch (e) {
+ setError(
+ e instanceof Error
+ ? e.message
+ : t(I18nKey.SETTINGS$MCP_ERROR_INVALID_JSON),
+ );
+ }
+ };
+
+ return (
+
+
+ {t(I18nKey.SETTINGS$MCP_CONFIG_DESCRIPTION)}
+
+
+ {error && (
+
+ {t(I18nKey.SETTINGS$MCP_CONFIG_ERROR)} {error}
+
+ )}
+
+ {t(I18nKey.SETTINGS$MCP_CONFIG_EXAMPLE)}{" "}
+
+ {
+ '{ "sse_servers": ["https://example-mcp-server.com/sse"], "stdio_servers": [{ "name": "fetch", "command": "uvx", "args": ["mcp-server-fetch"] }] }'
+ }
+
+
+
+
+ {t(I18nKey.SETTINGS$MCP_APPLY_CHANGES)}
+
+
+
+ );
+}
diff --git a/frontend/src/components/features/settings/mcp-settings/mcp-sse-servers.tsx b/frontend/src/components/features/settings/mcp-settings/mcp-sse-servers.tsx
new file mode 100644
index 0000000000..354013f71c
--- /dev/null
+++ b/frontend/src/components/features/settings/mcp-settings/mcp-sse-servers.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { MCPSSEServer } from "#/types/settings";
+import { I18nKey } from "#/i18n/declaration";
+
+interface MCPSSEServersProps {
+ servers: (string | MCPSSEServer)[];
+}
+
+export function MCPSSEServers({ servers }: MCPSSEServersProps) {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t(I18nKey.SETTINGS$MCP_SSE_SERVERS)}{" "}
+ ({servers.length})
+
+ {servers.map((server, index) => (
+
+
+ {t(I18nKey.SETTINGS$MCP_URL)}:{" "}
+ {typeof server === "string" ? server : server.url}
+
+ {typeof server !== "string" && server.api_key && (
+
+
+ {t(I18nKey.SETTINGS$MCP_API_KEY)}:
+ {" "}
+ {server.api_key
+ ? "Configured"
+ : t(I18nKey.SETTINGS$MCP_API_KEY_NOT_SET)}
+
+ )}
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/components/features/settings/mcp-settings/mcp-stdio-servers.tsx b/frontend/src/components/features/settings/mcp-settings/mcp-stdio-servers.tsx
new file mode 100644
index 0000000000..72e0bdec84
--- /dev/null
+++ b/frontend/src/components/features/settings/mcp-settings/mcp-stdio-servers.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { MCPStdioServer } from "#/types/settings";
+import { I18nKey } from "#/i18n/declaration";
+
+interface MCPStdioServersProps {
+ servers: MCPStdioServer[];
+}
+
+export function MCPStdioServers({ servers }: MCPStdioServersProps) {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t(I18nKey.SETTINGS$MCP_STDIO_SERVERS)}{" "}
+ ({servers.length})
+
+ {servers.map((server, index) => (
+
+
+ {t(I18nKey.SETTINGS$MCP_NAME)}:{" "}
+ {server.name}
+
+
+
+ {t(I18nKey.SETTINGS$MCP_COMMAND)}:
+ {" "}
+ {server.command}
+
+ {server.args && server.args.length > 0 && (
+
+
+ {t(I18nKey.SETTINGS$MCP_ARGS)}:
+ {" "}
+ {server.args.join(" ")}
+
+ )}
+ {server.env && Object.keys(server.env).length > 0 && (
+
+
+ {t(I18nKey.SETTINGS$MCP_ENV)}:
+ {" "}
+
+ {Object.entries(server.env)
+ .map(([key, value]) => `${key}=${value}`)
+ .join(", ")}
+
+
+ )}
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/components/shared/modals/settings/settings-modal.tsx b/frontend/src/components/shared/modals/settings/settings-modal.tsx
index a296e22823..4b73aef164 100644
--- a/frontend/src/components/shared/modals/settings/settings-modal.tsx
+++ b/frontend/src/components/shared/modals/settings/settings-modal.tsx
@@ -40,6 +40,7 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
{t(I18nKey.SETTINGS$SEE_ADVANCED_SETTINGS)}
+
{aiConfigOptions.isLoading && (
diff --git a/frontend/src/hooks/mutation/use-save-settings.ts b/frontend/src/hooks/mutation/use-save-settings.ts
index 1338212814..0b8685c252 100644
--- a/frontend/src/hooks/mutation/use-save-settings.ts
+++ b/frontend/src/hooks/mutation/use-save-settings.ts
@@ -1,4 +1,5 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
+import posthog from "posthog-js";
import { DEFAULT_SETTINGS } from "#/services/settings";
import OpenHands from "#/api/open-hands";
import { PostSettings, PostApiSettings } from "#/types/settings";
@@ -20,6 +21,8 @@ const saveSettingsMutationFn = async (settings: Partial
) => {
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: settings.user_consents_to_analytics,
+ provider_tokens_set: settings.PROVIDER_TOKENS_SET,
+ mcp_config: settings.MCP_CONFIG,
enable_proactive_conversation_starters:
settings.ENABLE_PROACTIVE_CONVERSATION_STARTERS,
};
@@ -34,6 +37,25 @@ export const useSaveSettings = () => {
return useMutation({
mutationFn: async (settings: Partial) => {
const newSettings = { ...currentSettings, ...settings };
+
+ // Track MCP configuration changes
+ if (
+ settings.MCP_CONFIG &&
+ currentSettings?.MCP_CONFIG !== settings.MCP_CONFIG
+ ) {
+ const hasMcpConfig = !!settings.MCP_CONFIG;
+ const sseServersCount = settings.MCP_CONFIG?.sse_servers?.length || 0;
+ const stdioServersCount =
+ settings.MCP_CONFIG?.stdio_servers?.length || 0;
+
+ // Track MCP configuration usage
+ posthog.capture("mcp_config_updated", {
+ has_mcp_config: hasMcpConfig,
+ sse_servers_count: sseServersCount,
+ stdio_servers_count: stdioServersCount,
+ });
+ }
+
await saveSettingsMutationFn(newSettings);
},
onSuccess: async () => {
diff --git a/frontend/src/hooks/query/use-settings.ts b/frontend/src/hooks/query/use-settings.ts
index 19f0b18623..2065c08d7f 100644
--- a/frontend/src/hooks/query/use-settings.ts
+++ b/frontend/src/hooks/query/use-settings.ts
@@ -25,6 +25,8 @@ const getSettingsQueryFn = async (): Promise => {
ENABLE_PROACTIVE_CONVERSATION_STARTERS:
apiSettings.enable_proactive_conversation_starters,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
+
+ MCP_CONFIG: apiSettings.mcp_config,
IS_NEW_USER: false,
};
};
diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts
index adc0a87f00..bd636625ce 100644
--- a/frontend/src/i18n/declaration.ts
+++ b/frontend/src/i18n/declaration.ts
@@ -1,5 +1,32 @@
// this file generate by script, don't modify it manually!!!
export enum I18nKey {
+ SETTINGS$MCP_TITLE = "SETTINGS$MCP_TITLE",
+ SETTINGS$MCP_DESCRIPTION = "SETTINGS$MCP_DESCRIPTION",
+ SETTINGS$NAV_MCP = "SETTINGS$NAV_MCP",
+ SETTINGS$MCP_CONFIGURATION = "SETTINGS$MCP_CONFIGURATION",
+ SETTINGS$MCP_EDIT_CONFIGURATION = "SETTINGS$MCP_EDIT_CONFIGURATION",
+ SETTINGS$MCP_CANCEL = "SETTINGS$MCP_CANCEL",
+ SETTINGS$MCP_APPLY_CHANGES = "SETTINGS$MCP_APPLY_CHANGES",
+ SETTINGS$MCP_CONFIG_DESCRIPTION = "SETTINGS$MCP_CONFIG_DESCRIPTION",
+ SETTINGS$MCP_CONFIG_ERROR = "SETTINGS$MCP_CONFIG_ERROR",
+ SETTINGS$MCP_CONFIG_EXAMPLE = "SETTINGS$MCP_CONFIG_EXAMPLE",
+ SETTINGS$MCP_NO_SERVERS_CONFIGURED = "SETTINGS$MCP_NO_SERVERS_CONFIGURED",
+ SETTINGS$MCP_SSE_SERVERS = "SETTINGS$MCP_SSE_SERVERS",
+ SETTINGS$MCP_STDIO_SERVERS = "SETTINGS$MCP_STDIO_SERVERS",
+ SETTINGS$MCP_API_KEY = "SETTINGS$MCP_API_KEY",
+ SETTINGS$MCP_API_KEY_NOT_SET = "SETTINGS$MCP_API_KEY_NOT_SET",
+ SETTINGS$MCP_COMMAND = "SETTINGS$MCP_COMMAND",
+ SETTINGS$MCP_ARGS = "SETTINGS$MCP_ARGS",
+ SETTINGS$MCP_ENV = "SETTINGS$MCP_ENV",
+ SETTINGS$MCP_NAME = "SETTINGS$MCP_NAME",
+ SETTINGS$MCP_URL = "SETTINGS$MCP_URL",
+ SETTINGS$MCP_LEARN_MORE = "SETTINGS$MCP_LEARN_MORE",
+ SETTINGS$MCP_ERROR_SSE_ARRAY = "SETTINGS$MCP_ERROR_SSE_ARRAY",
+ SETTINGS$MCP_ERROR_STDIO_ARRAY = "SETTINGS$MCP_ERROR_STDIO_ARRAY",
+ SETTINGS$MCP_ERROR_SSE_URL = "SETTINGS$MCP_ERROR_SSE_URL",
+ SETTINGS$MCP_ERROR_STDIO_PROPS = "SETTINGS$MCP_ERROR_STDIO_PROPS",
+ SETTINGS$MCP_ERROR_INVALID_JSON = "SETTINGS$MCP_ERROR_INVALID_JSON",
+ SETTINGS$MCP_DEFAULT_CONFIG = "SETTINGS$MCP_DEFAULT_CONFIG",
HOME$CONNECT_PROVIDER_MESSAGE = "HOME$CONNECT_PROVIDER_MESSAGE",
HOME$LETS_START_BUILDING = "HOME$LETS_START_BUILDING",
HOME$OPENHANDS_DESCRIPTION = "HOME$OPENHANDS_DESCRIPTION",
@@ -366,6 +393,7 @@ export enum I18nKey {
FILE_EXPLORER$UPLOAD = "FILE_EXPLORER$UPLOAD",
ACTION_MESSAGE$RUN = "ACTION_MESSAGE$RUN",
ACTION_MESSAGE$RUN_IPYTHON = "ACTION_MESSAGE$RUN_IPYTHON",
+ ACTION_MESSAGE$CALL_TOOL_MCP = "ACTION_MESSAGE$CALL_TOOL_MCP",
ACTION_MESSAGE$READ = "ACTION_MESSAGE$READ",
ACTION_MESSAGE$EDIT = "ACTION_MESSAGE$EDIT",
ACTION_MESSAGE$WRITE = "ACTION_MESSAGE$WRITE",
@@ -379,6 +407,7 @@ export enum I18nKey {
OBSERVATION_MESSAGE$EDIT = "OBSERVATION_MESSAGE$EDIT",
OBSERVATION_MESSAGE$WRITE = "OBSERVATION_MESSAGE$WRITE",
OBSERVATION_MESSAGE$BROWSE = "OBSERVATION_MESSAGE$BROWSE",
+ OBSERVATION_MESSAGE$MCP = "OBSERVATION_MESSAGE$MCP",
OBSERVATION_MESSAGE$RECALL = "OBSERVATION_MESSAGE$RECALL",
EXPANDABLE_MESSAGE$SHOW_DETAILS = "EXPANDABLE_MESSAGE$SHOW_DETAILS",
EXPANDABLE_MESSAGE$HIDE_DETAILS = "EXPANDABLE_MESSAGE$HIDE_DETAILS",
diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json
index da00ea79ca..3fd5d9dd98 100644
--- a/frontend/src/i18n/translation.json
+++ b/frontend/src/i18n/translation.json
@@ -1,4 +1,409 @@
{
+ "SETTINGS$MCP_TITLE": {
+ "en": "Model Context Protocol (MCP)",
+ "ja": "モデルコンテキストプロトコル (MCP)",
+ "zh-CN": "模型上下文协议 (MCP)",
+ "zh-TW": "模型上下文協議 (MCP)",
+ "ko-KR": "모델 컨텍스트 프로토콜 (MCP)",
+ "no": "Modellkontekstprotokoll (MCP)",
+ "it": "Protocollo di Contesto del Modello (MCP)",
+ "pt": "Protocolo de Contexto de Modelo (MCP)",
+ "es": "Protocolo de Contexto de Modelo (MCP)",
+ "ar": "بروتوكول سياق النموذج (MCP)",
+ "fr": "Protocole de Contexte de Modèle (MCP)",
+ "tr": "Model Bağlam Protokolü (MCP)",
+ "de": "Modellkontextprotokoll (MCP)"
+ },
+ "SETTINGS$MCP_DESCRIPTION": {
+ "en": "Configure MCP servers for enhanced model capabilities",
+ "ja": "拡張モデル機能のためのMCPサーバーを設定する",
+ "zh-CN": "配置MCP服务器以增强模型功能",
+ "zh-TW": "配置MCP服務器以增強模型功能",
+ "ko-KR": "향상된 모델 기능을 위한 MCP 서버 구성",
+ "no": "Konfigurer MCP-servere for forbedrede modellfunksjoner",
+ "it": "Configura i server MCP per funzionalità di modello avanzate",
+ "pt": "Configure servidores MCP para capacidades de modelo aprimoradas",
+ "es": "Configure servidores MCP para capacidades mejoradas del modelo",
+ "ar": "قم بتكوين خوادم MCP لتعزيز قدرات النموذج",
+ "fr": "Configurez les serveurs MCP pour des capacités de modèle améliorées",
+ "tr": "Gelişmiş model yetenekleri için MCP sunucularını yapılandırın",
+ "de": "Konfigurieren Sie MCP-Server für erweiterte Modellfunktionen"
+ },
+ "SETTINGS$NAV_MCP": {
+ "en": "MCP",
+ "ja": "MCP",
+ "zh-CN": "MCP",
+ "zh-TW": "MCP",
+ "ko-KR": "MCP",
+ "no": "MCP",
+ "it": "MCP",
+ "pt": "MCP",
+ "es": "MCP",
+ "ar": "MCP",
+ "fr": "MCP",
+ "tr": "MCP",
+ "de": "MCP"
+ },
+ "SETTINGS$MCP_CONFIGURATION": {
+ "en": "MCP Configuration",
+ "ja": "MCP設定",
+ "zh-CN": "MCP配置",
+ "zh-TW": "MCP配置",
+ "ko-KR": "MCP 구성",
+ "no": "MCP-konfigurasjon",
+ "it": "Configurazione MCP",
+ "pt": "Configuração MCP",
+ "es": "Configuración MCP",
+ "ar": "تكوين MCP",
+ "fr": "Configuration MCP",
+ "tr": "MCP Yapılandırması",
+ "de": "MCP-Konfiguration"
+ },
+ "SETTINGS$MCP_EDIT_CONFIGURATION": {
+ "en": "Edit Configuration",
+ "ja": "設定を編集",
+ "zh-CN": "编辑配置",
+ "zh-TW": "編輯配置",
+ "ko-KR": "구성 편집",
+ "no": "Rediger konfigurasjon",
+ "it": "Modifica configurazione",
+ "pt": "Editar configuração",
+ "es": "Editar configuración",
+ "ar": "تعديل التكوين",
+ "fr": "Modifier la configuration",
+ "tr": "Yapılandırmayı Düzenle",
+ "de": "Konfiguration bearbeiten"
+ },
+ "SETTINGS$MCP_CANCEL": {
+ "en": "Cancel",
+ "ja": "キャンセル",
+ "zh-CN": "取消",
+ "zh-TW": "取消",
+ "ko-KR": "취소",
+ "no": "Avbryt",
+ "it": "Annulla",
+ "pt": "Cancelar",
+ "es": "Cancelar",
+ "ar": "إلغاء",
+ "fr": "Annuler",
+ "tr": "İptal",
+ "de": "Abbrechen"
+ },
+ "SETTINGS$MCP_APPLY_CHANGES": {
+ "en": "Apply Changes",
+ "ja": "変更を適用",
+ "zh-CN": "应用更改",
+ "zh-TW": "應用更改",
+ "ko-KR": "변경 사항 적용",
+ "no": "Bruk endringer",
+ "it": "Applica modifiche",
+ "pt": "Aplicar alterações",
+ "es": "Aplicar cambios",
+ "ar": "تطبيق التغييرات",
+ "fr": "Appliquer les modifications",
+ "tr": "Değişiklikleri Uygula",
+ "de": "Änderungen anwenden"
+ },
+ "SETTINGS$MCP_CONFIG_DESCRIPTION": {
+ "en": "Edit the JSON configuration for MCP servers below. The configuration must include both sse_servers and stdio_servers arrays.",
+ "ja": "以下のMCPサーバーのJSON設定を編集してください。設定にはsse_serversとstdio_serversの両方の配列を含める必要があります。",
+ "zh-CN": "在下方编辑MCP服务器的JSON配置。配置必须包含sse_servers和stdio_servers数组。",
+ "zh-TW": "在下方編輯MCP服務器的JSON配置。配置必須包含sse_servers和stdio_servers數組。",
+ "ko-KR": "아래에서 MCP 서버의 JSON 구성을 편집하세요. 구성에는 sse_servers와 stdio_servers 배열이 모두 포함되어야 합니다.",
+ "no": "Rediger JSON-konfigurasjonen for MCP-servere nedenfor. Konfigurasjonen må inkludere både sse_servers og stdio_servers-matriser.",
+ "it": "Modifica la configurazione JSON per i server MCP qui sotto. La configurazione deve includere sia gli array sse_servers che stdio_servers.",
+ "pt": "Edite a configuração JSON para servidores MCP abaixo. A configuração deve incluir os arrays sse_servers e stdio_servers.",
+ "es": "Edite la configuración JSON para los servidores MCP a continuación. La configuración debe incluir tanto los arrays sse_servers como stdio_servers.",
+ "ar": "قم بتحرير تكوين JSON لخوادم MCP أدناه. يجب أن يتضمن التكوين كلاً من مصفوفات sse_servers و stdio_servers.",
+ "fr": "Modifiez la configuration JSON pour les serveurs MCP ci-dessous. La configuration doit inclure à la fois les tableaux sse_servers et stdio_servers.",
+ "tr": "Aşağıdaki MCP sunucuları için JSON yapılandırmasını düzenleyin. Yapılandırma hem sse_servers hem de stdio_servers dizilerini içermelidir.",
+ "de": "Bearbeiten Sie die JSON-Konfiguration für MCP-Server unten. Die Konfiguration muss sowohl sse_servers- als auch stdio_servers-Arrays enthalten."
+ },
+ "SETTINGS$MCP_CONFIG_ERROR": {
+ "en": "Error:",
+ "ja": "エラー:",
+ "zh-CN": "错误:",
+ "zh-TW": "錯誤:",
+ "ko-KR": "오류:",
+ "no": "Feil:",
+ "it": "Errore:",
+ "pt": "Erro:",
+ "es": "Error:",
+ "ar": "خطأ:",
+ "fr": "Erreur :",
+ "tr": "Hata:",
+ "de": "Fehler:"
+ },
+ "SETTINGS$MCP_CONFIG_EXAMPLE": {
+ "en": "Example:",
+ "ja": "例:",
+ "zh-CN": "示例:",
+ "zh-TW": "範例:",
+ "ko-KR": "예시:",
+ "no": "Eksempel:",
+ "it": "Esempio:",
+ "pt": "Exemplo:",
+ "es": "Ejemplo:",
+ "ar": "مثال:",
+ "fr": "Exemple :",
+ "tr": "Örnek:",
+ "de": "Beispiel:"
+ },
+ "SETTINGS$MCP_NO_SERVERS_CONFIGURED": {
+ "en": "No MCP servers are currently configured. Click \"Edit Configuration\" to add servers.",
+ "ja": "現在MCPサーバーが設定されていません。「設定を編集」をクリックしてサーバーを追加してください。",
+ "zh-CN": "当前未配置MCP服务器。点击\"编辑配置\"添加服务器。",
+ "zh-TW": "當前未配置MCP服務器。點擊\"編輯配置\"添加服務器。",
+ "ko-KR": "현재 구성된 MCP 서버가 없습니다. \"구성 편집\"을 클릭하여 서버를 추가하세요.",
+ "no": "Ingen MCP-servere er konfigurert for øyeblikket. Klikk på \"Rediger konfigurasjon\" for å legge til servere.",
+ "it": "Nessun server MCP è attualmente configurato. Fai clic su \"Modifica configurazione\" per aggiungere server.",
+ "pt": "Nenhum servidor MCP está configurado atualmente. Clique em \"Editar configuração\" para adicionar servidores.",
+ "es": "No hay servidores MCP configurados actualmente. Haga clic en \"Editar configuración\" para agregar servidores.",
+ "ar": "لا توجد خوادم MCP مكونة حاليًا. انقر على \"تعديل التكوين\" لإضافة خوادم.",
+ "fr": "Aucun serveur MCP n'est actuellement configuré. Cliquez sur \"Modifier la configuration\" pour ajouter des serveurs.",
+ "tr": "Şu anda yapılandırılmış MCP sunucusu yok. Sunucu eklemek için \"Yapılandırmayı Düzenle\"yi tıklayın.",
+ "de": "Derzeit sind keine MCP-Server konfiguriert. Klicken Sie auf \"Konfiguration bearbeiten\", um Server hinzuzufügen."
+ },
+ "SETTINGS$MCP_SSE_SERVERS": {
+ "en": "SSE Servers",
+ "ja": "SSEサーバー",
+ "zh-CN": "SSE服务器",
+ "zh-TW": "SSE服務器",
+ "ko-KR": "SSE 서버",
+ "no": "SSE-servere",
+ "it": "Server SSE",
+ "pt": "Servidores SSE",
+ "es": "Servidores SSE",
+ "ar": "خوادم SSE",
+ "fr": "Serveurs SSE",
+ "tr": "SSE Sunucuları",
+ "de": "SSE-Server"
+ },
+ "SETTINGS$MCP_STDIO_SERVERS": {
+ "en": "Stdio Servers",
+ "ja": "Stdioサーバー",
+ "zh-CN": "Stdio服务器",
+ "zh-TW": "Stdio服務器",
+ "ko-KR": "Stdio 서버",
+ "no": "Stdio-servere",
+ "it": "Server Stdio",
+ "pt": "Servidores Stdio",
+ "es": "Servidores Stdio",
+ "ar": "خوادم Stdio",
+ "fr": "Serveurs Stdio",
+ "tr": "Stdio Sunucuları",
+ "de": "Stdio-Server"
+ },
+ "SETTINGS$MCP_API_KEY": {
+ "en": "API Key",
+ "ja": "APIキー",
+ "zh-CN": "API密钥",
+ "zh-TW": "API密鑰",
+ "ko-KR": "API 키",
+ "no": "API-nøkkel",
+ "it": "Chiave API",
+ "pt": "Chave API",
+ "es": "Clave API",
+ "ar": "مفتاح API",
+ "fr": "Clé API",
+ "tr": "API Anahtarı",
+ "de": "API-Schlüssel"
+ },
+ "SETTINGS$MCP_API_KEY_NOT_SET": {
+ "en": "Not set",
+ "ja": "未設定",
+ "zh-CN": "未设置",
+ "zh-TW": "未設置",
+ "ko-KR": "설정되지 않음",
+ "no": "Ikke satt",
+ "it": "Non impostato",
+ "pt": "Não definido",
+ "es": "No establecido",
+ "ar": "غير محدد",
+ "fr": "Non défini",
+ "tr": "Ayarlanmadı",
+ "de": "Nicht festgelegt"
+ },
+ "SETTINGS$MCP_COMMAND": {
+ "en": "Command",
+ "ja": "コマンド",
+ "zh-CN": "命令",
+ "zh-TW": "命令",
+ "ko-KR": "명령",
+ "no": "Kommando",
+ "it": "Comando",
+ "pt": "Comando",
+ "es": "Comando",
+ "ar": "أمر",
+ "fr": "Commande",
+ "tr": "Komut",
+ "de": "Befehl"
+ },
+ "SETTINGS$MCP_ARGS": {
+ "en": "Args",
+ "ja": "引数",
+ "zh-CN": "参数",
+ "zh-TW": "參數",
+ "ko-KR": "인수",
+ "no": "Argumenter",
+ "it": "Argomenti",
+ "pt": "Argumentos",
+ "es": "Argumentos",
+ "ar": "وسيطات",
+ "fr": "Arguments",
+ "tr": "Argümanlar",
+ "de": "Argumente"
+ },
+ "SETTINGS$MCP_ENV": {
+ "en": "Env",
+ "ja": "環境変数",
+ "zh-CN": "环境变量",
+ "zh-TW": "環境變數",
+ "ko-KR": "환경",
+ "no": "Miljø",
+ "it": "Ambiente",
+ "pt": "Ambiente",
+ "es": "Entorno",
+ "ar": "بيئة",
+ "fr": "Environnement",
+ "tr": "Ortam",
+ "de": "Umgebung"
+ },
+ "SETTINGS$MCP_NAME": {
+ "en": "Name",
+ "ja": "名前",
+ "zh-CN": "名称",
+ "zh-TW": "名稱",
+ "ko-KR": "이름",
+ "no": "Navn",
+ "it": "Nome",
+ "pt": "Nome",
+ "es": "Nombre",
+ "ar": "اسم",
+ "fr": "Nom",
+ "tr": "İsim",
+ "de": "Name"
+ },
+ "SETTINGS$MCP_URL": {
+ "en": "URL",
+ "ja": "URL",
+ "zh-CN": "URL",
+ "zh-TW": "URL",
+ "ko-KR": "URL",
+ "no": "URL",
+ "it": "URL",
+ "pt": "URL",
+ "es": "URL",
+ "ar": "URL",
+ "fr": "URL",
+ "tr": "URL",
+ "de": "URL"
+ },
+ "SETTINGS$MCP_LEARN_MORE": {
+ "en": "Learn more",
+ "ja": "詳細を見る",
+ "zh-CN": "了解更多",
+ "zh-TW": "了解更多",
+ "ko-KR": "더 알아보기",
+ "no": "Lær mer",
+ "it": "Scopri di più",
+ "pt": "Saiba mais",
+ "es": "Más información",
+ "ar": "تعرف على المزيد",
+ "fr": "En savoir plus",
+ "tr": "Daha fazla bilgi",
+ "de": "Mehr erfahren"
+ },
+ "SETTINGS$MCP_ERROR_SSE_ARRAY": {
+ "en": "sse_servers must be an array",
+ "ja": "sse_serversは配列である必要があります",
+ "zh-CN": "sse_servers必须是一个数组",
+ "zh-TW": "sse_servers必須是一個數組",
+ "ko-KR": "sse_servers는 배열이어야 합니다",
+ "no": "sse_servers må være en matrise",
+ "it": "sse_servers deve essere un array",
+ "pt": "sse_servers deve ser um array",
+ "es": "sse_servers debe ser un array",
+ "ar": "يجب أن يكون sse_servers مصفوفة",
+ "fr": "sse_servers doit être un tableau",
+ "tr": "sse_servers bir dizi olmalıdır",
+ "de": "sse_servers muss ein Array sein"
+ },
+ "SETTINGS$MCP_ERROR_STDIO_ARRAY": {
+ "en": "stdio_servers must be an array",
+ "ja": "stdio_serversは配列である必要があります",
+ "zh-CN": "stdio_servers必须是一个数组",
+ "zh-TW": "stdio_servers必須是一個數組",
+ "ko-KR": "stdio_servers는 배열이어야 합니다",
+ "no": "stdio_servers må være en matrise",
+ "it": "stdio_servers deve essere un array",
+ "pt": "stdio_servers deve ser um array",
+ "es": "stdio_servers debe ser un array",
+ "ar": "يجب أن يكون stdio_servers مصفوفة",
+ "fr": "stdio_servers doit être un tableau",
+ "tr": "stdio_servers bir dizi olmalıdır",
+ "de": "stdio_servers muss ein Array sein"
+ },
+ "SETTINGS$MCP_ERROR_SSE_URL": {
+ "en": "Each SSE server must be a string URL or have a url property",
+ "ja": "各SSEサーバーは文字列URLまたはurlプロパティを持つ必要があります",
+ "zh-CN": "每个SSE服务器必须是字符串URL或具有url属性",
+ "zh-TW": "每個SSE服務器必須是字符串URL或具有url屬性",
+ "ko-KR": "각 SSE 서버는 문자열 URL이거나 url 속성을 가져야 합니다",
+ "no": "Hver SSE-server må være en streng-URL eller ha en url-egenskap",
+ "it": "Ogni server SSE deve essere un URL stringa o avere una proprietà url",
+ "pt": "Cada servidor SSE deve ser uma URL de string ou ter uma propriedade url",
+ "es": "Cada servidor SSE debe ser una URL de cadena o tener una propiedad url",
+ "ar": "يجب أن يكون كل خادم SSE عنوان URL نصيًا أو يحتوي على خاصية url",
+ "fr": "Chaque serveur SSE doit être une URL de chaîne ou avoir une propriété url",
+ "tr": "Her SSE sunucusu bir dize URL'si olmalı veya bir url özelliğine sahip olmalıdır",
+ "de": "Jeder SSE-Server muss eine String-URL sein oder eine URL-Eigenschaft haben"
+ },
+ "SETTINGS$MCP_ERROR_STDIO_PROPS": {
+ "en": "Each stdio server must have name and command properties",
+ "ja": "各stdioサーバーはnameとcommandプロパティを持つ必要があります",
+ "zh-CN": "每个stdio服务器必须具有name和command属性",
+ "zh-TW": "每個stdio服務器必須具有name和command屬性",
+ "ko-KR": "각 stdio 서버는 name 및 command 속성을 가져야 합니다",
+ "no": "Hver stdio-server må ha egenskapene name og command",
+ "it": "Ogni server stdio deve avere le proprietà name e command",
+ "pt": "Cada servidor stdio deve ter propriedades name e command",
+ "es": "Cada servidor stdio debe tener propiedades name y command",
+ "ar": "يجب أن يحتوي كل خادم stdio على خصائص name و command",
+ "fr": "Chaque serveur stdio doit avoir les propriétés name et command",
+ "tr": "Her stdio sunucusu name ve command özelliklerine sahip olmalıdır",
+ "de": "Jeder stdio-Server muss die Eigenschaften name und command haben"
+ },
+ "SETTINGS$MCP_ERROR_INVALID_JSON": {
+ "en": "Invalid JSON",
+ "ja": "無効なJSON",
+ "zh-CN": "无效的JSON",
+ "zh-TW": "無效的JSON",
+ "ko-KR": "잘못된 JSON",
+ "no": "Ugyldig JSON",
+ "it": "JSON non valido",
+ "pt": "JSON inválido",
+ "es": "JSON no válido",
+ "ar": "JSON غير صالح",
+ "fr": "JSON invalide",
+ "tr": "Geçersiz JSON",
+ "de": "Ungültiges JSON"
+ },
+ "SETTINGS$MCP_DEFAULT_CONFIG": {
+ "en": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "ja": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "zh-CN": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "zh-TW": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "ko-KR": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "no": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "it": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "pt": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "es": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "ar": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "fr": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "tr": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}",
+ "de": "{\n \"sse_servers\": [],\n \"stdio_servers\": []\n}"
+ },
"HOME$CONNECT_PROVIDER_MESSAGE": {
"en": "To get started with suggested tasks, please connect your GitHub or GitLab account.",
"ja": "提案されたタスクを始めるには、GitHubまたはGitLabアカウントを接続してください。",
@@ -5220,6 +5625,21 @@
"es": "Ejecutando un comando Python",
"tr": "Python komutu çalıştırılıyor"
},
+ "ACTION_MESSAGE$CALL_TOOL_MCP": {
+ "en": "Calling MCP Tool: {{action.payload.args.name}}",
+ "zh-CN": "调用 MCP 工具: {{action.payload.args.name}}",
+ "zh-TW": "呼叫 MCP 工具: {{action.payload.args.name}}",
+ "ko-KR": "MCP 도구 호출: {{action.payload.args.name}}",
+ "ja": "MCP ツール呼び出し: {{action.payload.args.name}}",
+ "no": "Kaller MCP-verktøy: {{action.payload.args.name}}",
+ "ar": "استدعاء أداة MCP: {{action.payload.args.name}}",
+ "de": "Ruft MCP-Tool auf: {{action.payload.args.name}}",
+ "fr": "Appel de l'outil MCP: {{action.payload.args.name}}",
+ "it": "Chiamata allo strumento MCP: {{action.payload.args.name}}",
+ "pt": "Chamando ferramenta MCP: {{action.payload.args.name}}",
+ "es": "Llamando a la herramienta MCP: {{action.payload.args.name}}",
+ "tr": "MCP Aracı çağrılıyor: {{action.payload.args.name}}"
+ },
"ACTION_MESSAGE$READ": {
"en": "Reading {{action.payload.args.path}}",
"zh-CN": "读取 {{action.payload.args.path}}",
@@ -5415,6 +5835,21 @@
"es": "Navegación completada",
"tr": "Gezinme tamamlandı"
},
+ "OBSERVATION_MESSAGE$MCP": {
+ "en": "MCP Tool Result: {{action.payload.args.name}}",
+ "zh-CN": "MCP 工具结果: {{action.payload.args.name}}",
+ "zh-TW": "MCP 工具結果: {{action.payload.args.name}}",
+ "ko-KR": "MCP 도구 결과: {{action.payload.args.name}}",
+ "ja": "MCP ツール結果: {{action.payload.args.name}}",
+ "no": "MCP verktøyresultat: {{action.payload.args.name}}",
+ "ar": "نتيجة أداة MCP: {{action.payload.args.name}}",
+ "de": "MCP-Tool-Ergebnis: {{action.payload.args.name}}",
+ "fr": "Résultat de l'outil MCP: {{action.payload.args.name}}",
+ "it": "Risultato dello strumento MCP: {{action.payload.args.name}}",
+ "pt": "Resultado da ferramenta MCP: {{action.payload.args.name}}",
+ "es": "Resultado de la herramienta MCP: {{action.payload.args.name}}",
+ "tr": "MCP Aracı Sonucu: {{action.payload.args.name}}"
+ },
"OBSERVATION_MESSAGE$RECALL": {
"en": "Microagent Activated",
"ja": "マイクロエージェントが有効化されました",
diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts
index 6431bc5fc6..c9376e9c0f 100644
--- a/frontend/src/react-app-env.d.ts
+++ b/frontend/src/react-app-env.d.ts
@@ -1 +1,7 @@
///
+
+interface Window {
+ posthog?: {
+ capture: (event: string, properties?: Record) => void;
+ };
+}
diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts
index 036f1d946b..3f43d9690c 100644
--- a/frontend/src/routes.ts
+++ b/frontend/src/routes.ts
@@ -11,6 +11,7 @@ export default [
route("accept-tos", "routes/accept-tos.tsx"),
route("settings", "routes/settings.tsx", [
index("routes/llm-settings.tsx"),
+ route("mcp", "routes/mcp-settings.tsx"),
route("git", "routes/git-settings.tsx"),
route("app", "routes/app-settings.tsx"),
route("billing", "routes/billing.tsx"),
diff --git a/frontend/src/routes/mcp-settings.tsx b/frontend/src/routes/mcp-settings.tsx
new file mode 100644
index 0000000000..d814960237
--- /dev/null
+++ b/frontend/src/routes/mcp-settings.tsx
@@ -0,0 +1,83 @@
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import posthog from "posthog-js";
+import { useSettings } from "#/hooks/query/use-settings";
+import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
+import { MCPConfig } from "#/types/settings";
+import { MCPConfigEditor } from "#/components/features/settings/mcp-settings/mcp-config-editor";
+import { BrandButton } from "#/components/features/settings/brand-button";
+import { I18nKey } from "#/i18n/declaration";
+import {
+ displayErrorToast,
+ displaySuccessToast,
+} from "#/utils/custom-toast-handlers";
+import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
+
+function MCPSettingsScreen() {
+ const { t } = useTranslation();
+ const { data: settings, isLoading } = useSettings();
+ const { mutate: saveSettings, isPending } = useSaveSettings();
+
+ const [mcpConfig, setMcpConfig] = useState(
+ settings?.MCP_CONFIG,
+ );
+ const [isDirty, setIsDirty] = useState(false);
+
+ const handleConfigChange = (config: MCPConfig) => {
+ setMcpConfig(config);
+ setIsDirty(true);
+ };
+
+ const formAction = () => {
+ if (!settings) return;
+
+ saveSettings(
+ { MCP_CONFIG: mcpConfig },
+ {
+ onSuccess: () => {
+ displaySuccessToast(t(I18nKey.SETTINGS$SAVED));
+ posthog.capture("settings_saved", {
+ HAS_MCP_CONFIG: mcpConfig ? "YES" : "NO",
+ MCP_SSE_SERVERS_COUNT: mcpConfig?.sse_servers?.length || 0,
+ MCP_STDIO_SERVERS_COUNT: mcpConfig?.stdio_servers?.length || 0,
+ });
+ setIsDirty(false);
+ },
+ onError: (error) => {
+ const errorMessage = retrieveAxiosErrorMessage(error);
+ displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC));
+ },
+ },
+ );
+ };
+
+ if (isLoading) {
+ return {t(I18nKey.HOME$LOADING)}
;
+ }
+
+ return (
+
+ );
+}
+
+export default MCPSettingsScreen;
diff --git a/frontend/src/routes/settings.tsx b/frontend/src/routes/settings.tsx
index 37bb186189..4b36ecf7a5 100644
--- a/frontend/src/routes/settings.tsx
+++ b/frontend/src/routes/settings.tsx
@@ -23,6 +23,7 @@ function SettingsScreen() {
const ossNavItems = [
{ to: "/settings", text: t("SETTINGS$NAV_LLM") },
+ { to: "/settings/mcp", text: t("SETTINGS$NAV_MCP") },
{ to: "/settings/git", text: t("SETTINGS$NAV_GIT") },
{ to: "/settings/app", text: t("SETTINGS$NAV_APPLICATION") },
];
diff --git a/frontend/src/services/observations.ts b/frontend/src/services/observations.ts
index 0be6112c67..73643599dd 100644
--- a/frontend/src/services/observations.ts
+++ b/frontend/src/services/observations.ts
@@ -53,6 +53,7 @@ export function handleObservationMessage(message: ObservationMessage) {
case ObservationType.NULL:
case ObservationType.RECALL:
case ObservationType.ERROR:
+ case ObservationType.MCP:
break; // We don't display the default message for these observations
default:
store.dispatch(addAssistantMessage(message.message));
@@ -248,6 +249,14 @@ export function handleObservationMessage(message: ObservationMessage) {
}),
);
break;
+ case "mcp":
+ store.dispatch(
+ addAssistantObservation({
+ ...baseObservation,
+ observation: "mcp" as const,
+ }),
+ );
+ break;
default:
// For any unhandled observation types, just ignore them
break;
diff --git a/frontend/src/services/settings.ts b/frontend/src/services/settings.ts
index 5caae6e71e..1004436542 100644
--- a/frontend/src/services/settings.ts
+++ b/frontend/src/services/settings.ts
@@ -17,6 +17,10 @@ export const DEFAULT_SETTINGS: Settings = {
USER_CONSENTS_TO_ANALYTICS: false,
ENABLE_PROACTIVE_CONVERSATION_STARTERS: false,
IS_NEW_USER: true,
+ MCP_CONFIG: {
+ sse_servers: [],
+ stdio_servers: [],
+ },
};
/**
diff --git a/frontend/src/state/chat-slice.ts b/frontend/src/state/chat-slice.ts
index f1403fb7f0..930ec38b24 100644
--- a/frontend/src/state/chat-slice.ts
+++ b/frontend/src/state/chat-slice.ts
@@ -34,6 +34,8 @@ const HANDLED_ACTIONS: OpenHandsEventType[] = [
"recall",
"think",
"system",
+ "call_tool_mcp",
+ "mcp",
];
function getRiskText(risk: ActionSecurityRisk) {
@@ -140,6 +142,16 @@ export const chatSlice = createSlice({
} else if (actionID === "recall") {
// skip recall actions
return;
+ } else if (actionID === "call_tool_mcp") {
+ // Format MCP action with name and arguments
+ const name = action.payload.args.name || "";
+ const args = action.payload.args.arguments || {};
+ text = `**MCP Tool Call:** ${name}\n\n`;
+ // Include thought if available
+ if (action.payload.args.thought) {
+ text += `\n\n**Thought:**\n${action.payload.args.thought}`;
+ }
+ text += `\n\n**Arguments:**\n\`\`\`json\n${JSON.stringify(args, null, 2)}\n\`\`\``;
}
if (actionID === "run" || actionID === "run_ipython") {
if (
@@ -304,6 +316,19 @@ export const chatSlice = createSlice({
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...(truncated)`;
}
causeMessage.content = content;
+ } else if (observationID === "mcp") {
+ // For MCP observations, we want to show the content as formatted output
+ // similar to how run/run_ipython actions are handled
+ let { content } = observation.payload;
+ if (content.length > MAX_CONTENT_LENGTH) {
+ content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
+ }
+ content = `${causeMessage.content}\n\n**Output:**\n\`\`\`\n${content.trim() || "[MCP Tool finished execution with no output]"}\n\`\`\``;
+ causeMessage.content = content; // Observation content includes the action
+ // Set success based on whether there's an error message
+ causeMessage.success = !observation.payload.content
+ .toLowerCase()
+ .includes("error:");
}
},
diff --git a/frontend/src/types/action-type.tsx b/frontend/src/types/action-type.tsx
index f623233ba5..6aa2bbdc2a 100644
--- a/frontend/src/types/action-type.tsx
+++ b/frontend/src/types/action-type.tsx
@@ -41,6 +41,9 @@ enum ActionType {
// Changes the state of the agent, e.g. to paused or running
CHANGE_AGENT_STATE = "change_agent_state",
+
+ // Interact with the MCP server.
+ MCP = "call_tool_mcp",
}
export default ActionType;
diff --git a/frontend/src/types/core/actions.ts b/frontend/src/types/core/actions.ts
index 5010eb3822..0d535b0540 100644
--- a/frontend/src/types/core/actions.ts
+++ b/frontend/src/types/core/actions.ts
@@ -152,6 +152,15 @@ export interface RecallAction extends OpenHandsActionEvent<"recall"> {
};
}
+export interface MCPAction extends OpenHandsActionEvent<"call_tool_mcp"> {
+ source: "agent";
+ args: {
+ name: string;
+ arguments: Record;
+ thought?: string;
+ };
+}
+
export type OpenHandsAction =
| UserMessageAction
| AssistantMessageAction
@@ -167,4 +176,5 @@ export type OpenHandsAction =
| FileEditAction
| FileWriteAction
| RejectAction
- | RecallAction;
+ | RecallAction
+ | MCPAction;
diff --git a/frontend/src/types/core/base.ts b/frontend/src/types/core/base.ts
index cb3b27baa9..b089411664 100644
--- a/frontend/src/types/core/base.ts
+++ b/frontend/src/types/core/base.ts
@@ -14,7 +14,9 @@ export type OpenHandsEventType =
| "think"
| "finish"
| "error"
- | "recall";
+ | "recall"
+ | "mcp"
+ | "call_tool_mcp";
interface OpenHandsBaseEvent {
id: number;
diff --git a/frontend/src/types/core/observations.ts b/frontend/src/types/core/observations.ts
index 7ccea85654..8d9f41bb1f 100644
--- a/frontend/src/types/core/observations.ts
+++ b/frontend/src/types/core/observations.ts
@@ -129,6 +129,13 @@ export interface RecallObservation extends OpenHandsObservationEvent<"recall"> {
};
}
+export interface MCPObservation extends OpenHandsObservationEvent<"mcp"> {
+ source: "agent";
+ extras: {
+ // Add any specific fields for MCP observations
+ };
+}
+
export type OpenHandsObservation =
| AgentStateChangeObservation
| AgentThinkObservation
@@ -141,4 +148,5 @@ export type OpenHandsObservation =
| ReadObservation
| EditObservation
| ErrorObservation
- | RecallObservation;
+ | RecallObservation
+ | MCPObservation;
diff --git a/frontend/src/types/observation-type.tsx b/frontend/src/types/observation-type.tsx
index 341bbb0a58..b2881e3396 100644
--- a/frontend/src/types/observation-type.tsx
+++ b/frontend/src/types/observation-type.tsx
@@ -32,6 +32,9 @@ enum ObservationType {
// An observation that shows agent's context extension
RECALL = "recall",
+ // A MCP tool call observation
+ MCP = "mcp",
+
// An error observation
ERROR = "error",
diff --git a/frontend/src/types/settings.ts b/frontend/src/types/settings.ts
index 09253edcbc..cdc934862b 100644
--- a/frontend/src/types/settings.ts
+++ b/frontend/src/types/settings.ts
@@ -9,6 +9,23 @@ export type ProviderToken = {
token: string;
};
+export type MCPSSEServer = {
+ url: string;
+ api_key?: string;
+};
+
+export type MCPStdioServer = {
+ name: string;
+ command: string;
+ args?: string[];
+ env?: Record;
+};
+
+export type MCPConfig = {
+ sse_servers: (string | MCPSSEServer)[];
+ stdio_servers: MCPStdioServer[];
+};
+
export type Settings = {
LLM_MODEL: string;
LLM_BASE_URL: string;
@@ -24,6 +41,7 @@ export type Settings = {
ENABLE_PROACTIVE_CONVERSATION_STARTERS: boolean;
USER_CONSENTS_TO_ANALYTICS: boolean | null;
IS_NEW_USER?: boolean;
+ MCP_CONFIG?: MCPConfig;
};
export type ApiSettings = {
@@ -41,13 +59,19 @@ export type ApiSettings = {
enable_proactive_conversation_starters: boolean;
user_consents_to_analytics: boolean | null;
provider_tokens_set: Partial>;
+ mcp_config?: {
+ sse_servers: (string | MCPSSEServer)[];
+ stdio_servers: MCPStdioServer[];
+ };
};
export type PostSettings = Settings & {
user_consents_to_analytics: boolean | null;
llm_api_key?: string | null;
+ mcp_config?: MCPConfig;
};
export type PostApiSettings = ApiSettings & {
user_consents_to_analytics: boolean | null;
+ mcp_config?: MCPConfig;
};
diff --git a/openhands/runtime/impl/action_execution/action_execution_client.py b/openhands/runtime/impl/action_execution/action_execution_client.py
index c6282f5073..4e33cd3163 100644
--- a/openhands/runtime/impl/action_execution/action_execution_client.py
+++ b/openhands/runtime/impl/action_execution/action_execution_client.py
@@ -376,8 +376,8 @@ class ActionExecutionClient(Runtime):
)
)
self.log(
- 'debug',
- f'Updated MCP config by adding runtime as another server: {updated_mcp_config}',
+ 'info',
+ f'Updated MCP config: {updated_mcp_config.sse_servers}',
)
return updated_mcp_config
diff --git a/openhands/server/session/session.py b/openhands/server/session/session.py
index 53b9ca5473..58a75032f2 100644
--- a/openhands/server/session/session.py
+++ b/openhands/server/session/session.py
@@ -6,7 +6,7 @@ from logging import LoggerAdapter
import socketio
from openhands.controller.agent import Agent
-from openhands.core.config import AppConfig
+from openhands.core.config import AppConfig, MCPConfig
from openhands.core.config.condenser_config import (
BrowserOutputCondenserConfig,
CondenserPipelineConfig,
@@ -114,6 +114,7 @@ class Session:
or settings.sandbox_runtime_container_image
else self.config.sandbox.runtime_container_image
)
+ self.config.mcp = settings.mcp_config or MCPConfig()
max_iterations = settings.max_iterations or self.config.max_iterations
# This is a shallow copy of the default LLM config, so changes here will
diff --git a/openhands/server/settings.py b/openhands/server/settings.py
index 8cdd453609..2113453356 100644
--- a/openhands/server/settings.py
+++ b/openhands/server/settings.py
@@ -5,6 +5,7 @@ from pydantic import (
SecretStr,
)
+from openhands.core.config.mcp_config import MCPConfig
from openhands.integrations.provider import ProviderToken
from openhands.integrations.service_types import ProviderType
from openhands.storage.data_models.settings import Settings
@@ -15,6 +16,7 @@ class POSTProviderModel(BaseModel):
Settings for POST requests
"""
+ mcp_config: MCPConfig | None = None
provider_tokens: dict[ProviderType, ProviderToken] = {}
diff --git a/openhands/storage/data_models/settings.py b/openhands/storage/data_models/settings.py
index ef40bcb5fc..cbc5b1a96c 100644
--- a/openhands/storage/data_models/settings.py
+++ b/openhands/storage/data_models/settings.py
@@ -11,6 +11,7 @@ from pydantic import (
from pydantic.json import pydantic_encoder
from openhands.core.config.llm_config import LLMConfig
+from openhands.core.config.mcp_config import MCPConfig
from openhands.core.config.utils import load_app_config
from openhands.storage.data_models.user_secrets import UserSecrets
@@ -37,6 +38,7 @@ class Settings(BaseModel):
user_consents_to_analytics: bool | None = None
sandbox_base_container_image: str | None = None
sandbox_runtime_container_image: str | None = None
+ mcp_config: MCPConfig | None = None
model_config = {
'validate_assignment': True,
@@ -105,6 +107,12 @@ class Settings(BaseModel):
# If no api key has been set, we take this to mean that there is no reasonable default
return None
security = app_config.security
+
+ # Get MCP config if available
+ mcp_config = None
+ if hasattr(app_config, 'mcp'):
+ mcp_config = app_config.mcp
+
settings = Settings(
language='en',
agent=app_config.default_agent,
@@ -115,5 +123,6 @@ class Settings(BaseModel):
llm_api_key=llm_config.api_key,
llm_base_url=llm_config.base_url,
remote_runtime_resource_factor=app_config.sandbox.remote_runtime_resource_factor,
+ mcp_config=mcp_config,
)
return settings