Add MCP configuration visualization and editing in settings modal (#8029)

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
This commit is contained in:
Xingyao Wang 2025-05-08 00:43:53 +08:00 committed by GitHub
parent 3ccc4b34c5
commit ccf2c7f2cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1111 additions and 7 deletions

View File

@ -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 '[

View File

@ -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 (
<div>
<div className="flex flex-col gap-2 mb-6">
<div className="text-sm font-medium">
{t(I18nKey.SETTINGS$MCP_TITLE)}
</div>
<p className="text-xs text-[#A3A3A3]">
{t(I18nKey.SETTINGS$MCP_DESCRIPTION)}
</p>
</div>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center">
<a
href="https://docs.all-hands.dev/modules/usage/mcp"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-400 hover:underline mr-3"
onClick={(e) => e.stopPropagation()}
>
Documentation
</a>
<BrandButton
type="button"
variant="primary"
onClick={() => setIsEditing(!isEditing)}
>
{isEditing
? t(I18nKey.SETTINGS$MCP_CANCEL)
: t(I18nKey.SETTINGS$MCP_EDIT_CONFIGURATION)}
</BrandButton>
</div>
</div>
<div>
{isEditing ? (
<MCPJsonEditor mcpConfig={mcpConfig} onChange={handleConfigChange} />
) : (
<>
<div className="flex flex-col gap-6">
<div>
<MCPSSEServers servers={config.sse_servers} />
</div>
<div>
<MCPStdioServers servers={config.stdio_servers} />
</div>
</div>
{config.sse_servers.length === 0 &&
config.stdio_servers.length === 0 && (
<div className="mt-4 p-2 bg-yellow-50 border border-yellow-200 rounded-md text-sm text-yellow-700">
{t(I18nKey.SETTINGS$MCP_NO_SERVERS_CONFIGURED)}
</div>
)}
</>
)}
</div>
</div>
);
}

View File

@ -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 (
<div className="mb-2 p-2 bg-base-tertiary rounded-md">
<div className="text-sm">
<span className="font-medium">{t(I18nKey.SETTINGS$MCP_URL)}:</span>{" "}
{server}
</div>
</div>
);
}
return (
<div className="mb-2 p-2 bg-base-tertiary rounded-md">
<div className="text-sm">
<span className="font-medium">{t(I18nKey.SETTINGS$MCP_URL)}:</span>{" "}
{server.url}
</div>
{server.api_key && (
<div className="text-sm text-gray-500">
<span className="font-medium">
{t(I18nKey.SETTINGS$MCP_API_KEY)}:
</span>{" "}
{server.api_key ? "Set" : t(I18nKey.SETTINGS$MCP_API_KEY_NOT_SET)}
</div>
)}
</div>
);
}
interface StdioServerDisplayProps {
server: MCPStdioServer;
}
function StdioServerDisplay({ server }: StdioServerDisplayProps) {
const { t } = useTranslation();
return (
<div className="mb-2 p-2 bg-base-tertiary rounded-md">
<div className="text-sm">
<span className="font-medium">{t(I18nKey.SETTINGS$MCP_NAME)}:</span>{" "}
{server.name}
</div>
<div className="text-sm text-gray-500">
<span className="font-medium">{t(I18nKey.SETTINGS$MCP_COMMAND)}:</span>{" "}
{server.command}
</div>
{server.args && server.args.length > 0 && (
<div className="text-sm text-gray-500">
<span className="font-medium">{t(I18nKey.SETTINGS$MCP_ARGS)}:</span>{" "}
{server.args.join(" ")}
</div>
)}
{server.env && Object.keys(server.env).length > 0 && (
<div className="text-sm text-gray-500">
<span className="font-medium">{t(I18nKey.SETTINGS$MCP_ENV)}:</span>{" "}
{Object.entries(server.env)
.map(([key, value]) => `${key}=${value}`)
.join(", ")}
</div>
)}
</div>
);
}
export function MCPConfigViewer({ mcpConfig }: MCPConfigViewerProps) {
const { t } = useTranslation();
if (
!mcpConfig ||
(mcpConfig.sse_servers.length === 0 && mcpConfig.stdio_servers.length === 0)
) {
return null;
}
return (
<div className="mt-4 border border-base-tertiary rounded-md p-3">
<div className="flex justify-between items-center mb-3">
<h3 className="text-sm font-medium">
{t(I18nKey.SETTINGS$MCP_CONFIGURATION)}
</h3>
<a
href="https://docs.all-hands.dev/modules/usage/mcp"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 hover:underline"
onClick={(e) => e.stopPropagation()}
>
{t(I18nKey.SETTINGS$MCP_LEARN_MORE)}
</a>
</div>
<div className="mt-2">
<div className="flex flex-col gap-4">
{mcpConfig.sse_servers.length > 0 && (
<div className="mb-3">
<h4 className="text-sm font-medium mb-1">
{t(I18nKey.SETTINGS$MCP_SSE_SERVERS)}{" "}
<span className="text-gray-500">
({mcpConfig.sse_servers.length})
</span>
</h4>
{mcpConfig.sse_servers.map((server, index) => (
<SSEServerDisplay key={`sse-${index}`} server={server} />
))}
</div>
)}
{mcpConfig.stdio_servers.length > 0 && (
<div>
<h4 className="text-sm font-medium mb-1">
{t(I18nKey.SETTINGS$MCP_STDIO_SERVERS)}{" "}
<span className="text-gray-500">
({mcpConfig.stdio_servers.length})
</span>
</h4>
{mcpConfig.stdio_servers.map((server, index) => (
<StdioServerDisplay key={`stdio-${index}`} server={server} />
))}
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -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<string | null>(null);
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
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 (
<div>
<div className="mb-2 text-sm text-gray-400">
{t(I18nKey.SETTINGS$MCP_CONFIG_DESCRIPTION)}
</div>
<textarea
className="w-full h-64 p-2 text-sm font-mono bg-base-tertiary rounded-md focus:border-blue-500 focus:outline-none"
value={configText}
onChange={handleTextChange}
spellCheck="false"
/>
{error && (
<div className="mt-2 p-2 bg-red-100 border border-red-300 rounded-md text-sm text-red-700">
<strong>{t(I18nKey.SETTINGS$MCP_CONFIG_ERROR)}</strong> {error}
</div>
)}
<div className="mt-2 text-sm text-gray-400">
<strong>{t(I18nKey.SETTINGS$MCP_CONFIG_EXAMPLE)}</strong>{" "}
<code>
{
'{ "sse_servers": ["https://example-mcp-server.com/sse"], "stdio_servers": [{ "name": "fetch", "command": "uvx", "args": ["mcp-server-fetch"] }] }'
}
</code>
</div>
<div className="mt-4 flex justify-end">
<BrandButton type="button" variant="primary" onClick={handleSave}>
{t(I18nKey.SETTINGS$MCP_APPLY_CHANGES)}
</BrandButton>
</div>
</div>
);
}

View File

@ -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 (
<div>
<h4 className="text-sm font-medium mb-2">
{t(I18nKey.SETTINGS$MCP_SSE_SERVERS)}{" "}
<span className="text-gray-500">({servers.length})</span>
</h4>
{servers.map((server, index) => (
<div
key={`sse-${index}`}
className="mb-2 p-2 bg-base-tertiary rounded-md"
>
<div className="text-sm">
<span className="font-medium">{t(I18nKey.SETTINGS$MCP_URL)}:</span>{" "}
{typeof server === "string" ? server : server.url}
</div>
{typeof server !== "string" && server.api_key && (
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">
{t(I18nKey.SETTINGS$MCP_API_KEY)}:
</span>{" "}
{server.api_key
? "Configured"
: t(I18nKey.SETTINGS$MCP_API_KEY_NOT_SET)}
</div>
)}
</div>
))}
</div>
);
}

View File

@ -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 (
<div>
<h4 className="text-sm font-medium mb-2">
{t(I18nKey.SETTINGS$MCP_STDIO_SERVERS)}{" "}
<span className="text-gray-500">({servers.length})</span>
</h4>
{servers.map((server, index) => (
<div
key={`stdio-${index}`}
className="mb-2 p-2 bg-base-tertiary rounded-md"
>
<div className="text-sm">
<span className="font-medium">{t(I18nKey.SETTINGS$MCP_NAME)}:</span>{" "}
{server.name}
</div>
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">
{t(I18nKey.SETTINGS$MCP_COMMAND)}:
</span>{" "}
<code className="font-mono">{server.command}</code>
</div>
{server.args && server.args.length > 0 && (
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">
{t(I18nKey.SETTINGS$MCP_ARGS)}:
</span>{" "}
<code className="font-mono">{server.args.join(" ")}</code>
</div>
)}
{server.env && Object.keys(server.env).length > 0 && (
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">
{t(I18nKey.SETTINGS$MCP_ENV)}:
</span>{" "}
<code className="font-mono">
{Object.entries(server.env)
.map(([key, value]) => `${key}=${value}`)
.join(", ")}
</code>
</div>
)}
</div>
))}
</div>
);
}

View File

@ -40,6 +40,7 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
{t(I18nKey.SETTINGS$SEE_ADVANCED_SETTINGS)}
</Link>
</p>
{aiConfigOptions.isLoading && (
<div className="flex justify-center">
<LoadingSpinner size="small" />

View File

@ -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<PostSettings>) => {
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<PostSettings>) => {
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 () => {

View File

@ -25,6 +25,8 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
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,
};
};

View File

@ -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",

View File

@ -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 <path>{{action.payload.args.path}}</path>",
"zh-CN": "读取 <path>{{action.payload.args.path}}</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": "マイクロエージェントが有効化されました",

View File

@ -1 +1,7 @@
/// <reference types="react-scripts" />
interface Window {
posthog?: {
capture: (event: string, properties?: Record<string, unknown>) => void;
};
}

View File

@ -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"),

View File

@ -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<MCPConfig | undefined>(
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 <div className="p-9">{t(I18nKey.HOME$LOADING)}</div>;
}
return (
<form
data-testid="mcp-settings-screen"
action={formAction}
className="flex flex-col h-full justify-between"
>
<div className="p-9 flex flex-col gap-12">
<MCPConfigEditor mcpConfig={mcpConfig} onChange={handleConfigChange} />
</div>
<div className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
<BrandButton
testId="submit-button"
type="submit"
variant="primary"
isDisabled={!isDirty || isPending}
>
{!isPending && t(I18nKey.SETTINGS$SAVE_CHANGES)}
{isPending && t(I18nKey.SETTINGS$SAVING)}
</BrandButton>
</div>
</form>
);
}
export default MCPSettingsScreen;

View File

@ -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") },
];

View File

@ -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;

View File

@ -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: [],
},
};
/**

View File

@ -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:");
}
},

View File

@ -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;

View File

@ -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<string, unknown>;
thought?: string;
};
}
export type OpenHandsAction =
| UserMessageAction
| AssistantMessageAction
@ -167,4 +176,5 @@ export type OpenHandsAction =
| FileEditAction
| FileWriteAction
| RejectAction
| RecallAction;
| RecallAction
| MCPAction;

View File

@ -14,7 +14,9 @@ export type OpenHandsEventType =
| "think"
| "finish"
| "error"
| "recall";
| "recall"
| "mcp"
| "call_tool_mcp";
interface OpenHandsBaseEvent {
id: number;

View File

@ -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;

View File

@ -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",

View File

@ -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<string, string>;
};
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<Record<Provider, string | null>>;
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;
};

View File

@ -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

View File

@ -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

View File

@ -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] = {}

View File

@ -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