From f314168f229dbfd2e333fc35c03629fa8bbb9cc5 Mon Sep 17 00:00:00 2001 From: hai <1003394729@qq.com> Date: Thu, 12 Feb 2026 18:10:14 +0800 Subject: [PATCH 01/10] Add MCP to Friday --- package-lock.json | 66 +++--- packages/app/friday/args.py | 20 ++ packages/app/friday/main.py | 8 + packages/app/friday/mcp_manager/__init__.py | 8 + packages/app/friday/mcp_manager/manager.py | 177 +++++++++++++++ .../components/MCP/DeleteConfirmDialog.tsx | 53 +++++ .../components/MCP/MCPServerCardHeader.tsx | 72 ++++++ .../src/components/MCP/MCPServerForm.tsx | 206 ++++++++++++++++++ .../src/context/FridaySettingRoomContext.tsx | 4 + packages/client/src/context/MCPContext.tsx | 171 +++++++++++++++ packages/client/src/i18n/en.json | 38 +++- packages/client/src/i18n/zh.json | 44 +++- .../ChatPage/MCPManagementSheet.tsx | 184 ++++++++++++++++ .../src/pages/FridayPage/ChatPage/index.tsx | 56 ++++- .../client/src/pages/FridayPage/index.tsx | 16 +- packages/shared/src/config/friday.ts | 17 ++ 16 files changed, 1091 insertions(+), 49 deletions(-) create mode 100644 packages/app/friday/mcp_manager/__init__.py create mode 100644 packages/app/friday/mcp_manager/manager.py create mode 100644 packages/client/src/components/MCP/DeleteConfirmDialog.tsx create mode 100644 packages/client/src/components/MCP/MCPServerCardHeader.tsx create mode 100644 packages/client/src/components/MCP/MCPServerForm.tsx create mode 100644 packages/client/src/context/MCPContext.tsx create mode 100644 packages/client/src/pages/FridayPage/ChatPage/MCPManagementSheet.tsx diff --git a/package-lock.json b/package-lock.json index 2af3c5ee..ba87cdbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -279,6 +279,7 @@ "integrity": "sha512-hfpCIukPuwkrlwsYfJEWdU5R5bduBHEq2uuPcqmgPgNq5MSjmiNIzRuzxGZZgiBKcre6gZT00DR7G1AFn//wiQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.3", "@algolia/requester-browser-xhr": "5.46.3", @@ -436,6 +437,7 @@ "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", "license": "MIT", + "peer": true, "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", @@ -515,6 +517,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -932,7 +935,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -950,7 +952,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -968,7 +969,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -986,7 +986,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -1004,7 +1003,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -1022,7 +1020,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -1040,7 +1037,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1058,7 +1054,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1076,7 +1071,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1094,7 +1088,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1112,7 +1105,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1130,7 +1122,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1148,7 +1139,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1166,7 +1156,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1184,7 +1173,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1202,7 +1190,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1220,7 +1207,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1238,7 +1224,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1256,7 +1241,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1274,7 +1258,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1292,7 +1275,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1310,7 +1292,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -1328,7 +1309,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -1346,7 +1326,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -1364,7 +1343,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -1382,7 +1360,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -3189,6 +3166,7 @@ "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.24.13.tgz", "integrity": "sha512-ONTr14s7LFIjx2VRFLuOpagL76sM/HPy6/OhdBfq6UukINmTIs6+aFN0GgcR0aXQHFDXQ7f/fel0o/SO05Htdg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "lodash": "^4.17.21", "lodash-es": "^4.17.21", @@ -3208,6 +3186,7 @@ "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.13.tgz", "integrity": "sha512-rNF8tDxIwTtXzz5O/U23QU73nlhgQNYJ+Sv5BAwQOIyhIE2Z3S5tUiSVMwZHt0julkv/Ryfwi+qsD4FiE5rOuw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "json-schema-merge-allof": "^0.8.1", "jsonpointer": "^5.0.1", @@ -3893,6 +3872,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -4249,6 +4229,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.17.tgz", "integrity": "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.17" }, @@ -4268,6 +4249,7 @@ "https://trpc.io/sponsor" ], "license": "MIT", + "peer": true, "peerDependencies": { "@trpc/server": "11.8.1", "typescript": ">=5.7.2" @@ -4298,6 +4280,7 @@ "https://trpc.io/sponsor" ], "license": "MIT", + "peer": true, "peerDependencies": { "typescript": ">=5.7.2" } @@ -4653,6 +4636,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4663,6 +4647,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4773,6 +4758,7 @@ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -5330,6 +5316,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5418,6 +5405,7 @@ "integrity": "sha512-n/NdPglzmkcNYZfIT3Fo8pnDR/lKiK1kZ1Yaa315UoLyHymADhWw15+bzN5gBxrCA8KyeNu0JJD6mLtTov43lQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.12.3", "@algolia/client-abtesting": "5.46.3", @@ -5479,6 +5467,7 @@ "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz", "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==", "license": "MIT", + "peer": true, "dependencies": { "@ant-design/colors": "^7.2.1", "@ant-design/cssinjs": "^1.23.0", @@ -6036,6 +6025,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6862,7 +6852,8 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -7558,6 +7549,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8107,6 +8099,7 @@ "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -8804,6 +8797,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -9708,7 +9702,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9730,7 +9723,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9752,7 +9744,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9774,7 +9765,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9796,7 +9786,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9818,7 +9807,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9840,7 +9828,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9862,7 +9849,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9884,7 +9870,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9906,7 +9891,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9928,7 +9912,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11707,6 +11690,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12593,6 +12577,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13178,6 +13163,7 @@ "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -14750,6 +14736,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15167,6 +15154,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -15871,6 +15859,7 @@ "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", @@ -16245,6 +16234,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/packages/app/friday/args.py b/packages/app/friday/args.py index b36affcc..383c51a3 100644 --- a/packages/app/friday/args.py +++ b/packages/app/friday/args.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import json from argparse import ArgumentParser, Namespace +from typing import List, Dict, Any def json_type(value: str) -> dict: @@ -16,6 +17,19 @@ def json_type(value: str) -> dict: raise ValueError(f"Invalid JSON string: {e}") +def json_list_type(value: str) -> List[Dict[str, Any]]: + """Parse a JSON string into a list of dictionaries.""" + if not value or value == "": + return [] + try: + result = json.loads(value) + if not isinstance(result, list): + raise ValueError("JSON must be an array/list") + return result + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON string: {e}") + + def get_args() -> Namespace: """Get the command line arguments for the script.""" parser = ArgumentParser(description="Arguments for friday") @@ -61,5 +75,11 @@ def get_args() -> Namespace: default={}, help="A JSON string representing a dictionary of keyword arguments to pass to the LLM generate method.", ) + parser.add_argument( + "--mcpServers", + type=json_list_type, + default=[], + help="A JSON string representing a list of MCP server configurations.", + ) args = parser.parse_args() return args diff --git a/packages/app/friday/main.py b/packages/app/friday/main.py index 817006a6..e025470c 100644 --- a/packages/app/friday/main.py +++ b/packages/app/friday/main.py @@ -35,6 +35,7 @@ from utils.connect import StudioConnect from utils.constants import FRIDAY_SESSION_ID +from mcp_manager import connect_mcp_servers, close_mcp_connections async def main(): args = get_args() @@ -88,6 +89,10 @@ async def main(): view_agentscope_faq, group_name="agentscope_tools" ) + # Get MCP servers configuration and connect + mcp_servers = args.mcpServers if hasattr(args, 'mcpServers') else [] + local_mcp_clients = await connect_mcp_servers(mcp_servers, toolkit) + # get model from args model = get_model(args.llmProvider, args.modelName, args.apiKey, args.clientKwargs, args.generateKwargs) formatter = get_formatter(args.llmProvider) @@ -164,6 +169,9 @@ async def main(): session_id=FRIDAY_SESSION_ID, friday=agent ) + + # Close local MCP connections + await close_mcp_connections(local_mcp_clients) if __name__ == '__main__': asyncio.run(main()) diff --git a/packages/app/friday/mcp_manager/__init__.py b/packages/app/friday/mcp_manager/__init__.py new file mode 100644 index 00000000..8de5f5a1 --- /dev/null +++ b/packages/app/friday/mcp_manager/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +"""MCP module for Friday.""" +from .manager import connect_mcp_servers, close_mcp_connections + +__all__ = [ + 'connect_mcp_servers', + 'close_mcp_connections', +] diff --git a/packages/app/friday/mcp_manager/manager.py b/packages/app/friday/mcp_manager/manager.py new file mode 100644 index 00000000..f37ee67d --- /dev/null +++ b/packages/app/friday/mcp_manager/manager.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +"""MCP (Model Context Protocol) connection manager for Friday.""" +from typing import List, Dict, Any +import json5 + +from agentscope.mcp import HttpStatelessClient, StdIOStatefulClient +from agentscope.tool import Toolkit + + +async def connect_mcp_servers( + mcp_servers: List[Dict[str, Any]], + toolkit: Toolkit +) -> List[StdIOStatefulClient]: + """ + Connect to MCP servers and register them with the toolkit. + + Args: + mcp_servers: List of MCP server configurations + toolkit: AgentScope toolkit to register MCP clients + + Returns: + List of connected local MCP clients (for cleanup) + """ + local_mcp_clients = [] + + print(f"[Friday] Loaded {len(mcp_servers)} MCP server(s) from configuration") + + # Log and process MCP server details + for idx, server in enumerate(mcp_servers, 1): + server_type = server.get('type', 'local') + server_name = server.get('name', f'Server {idx}') + is_enabled = server.get('enabled', True) + + status = "✓ Enabled" if is_enabled else "✗ Disabled" + print(f"[Friday] MCP Server {idx}: {server_name} (Type: {server_type}) [{status}]") + + # Skip disabled servers + if not is_enabled: + print(f" - Skipped (disabled)") + continue + + if server_type == 'local': + # Handle local MCP servers + clients = await _connect_local_server(server, toolkit) + local_mcp_clients.extend(clients) + + elif server_type == 'remote': + # Handle remote MCP servers + await _connect_remote_server(server, toolkit) + + return local_mcp_clients + + +async def _connect_local_server( + server: Dict[str, Any], + toolkit: Toolkit +) -> List[StdIOStatefulClient]: + """ + Connect to a local MCP server. + + Args: + server: Server configuration + toolkit: AgentScope toolkit + + Returns: + List of connected clients + """ + clients = [] + + # Parse JSON config for local MCP servers + config_str = server.get('config', '') + if not config_str: + print(f" - Error: No configuration provided") + return clients + + try: + # Parse JSON configuration + config = json5.loads(config_str) + mcp_servers_config = config.get('mcpServers', {}) + + if not mcp_servers_config: + print(f" - Error: No 'mcpServers' field in configuration") + return clients + + # Register each service in the mcpServers object + for service_name, service_config in mcp_servers_config.items(): + command = service_config.get('command', '') + args_list = service_config.get('args', []) + env_vars = service_config.get('env', {}) + + if not command: + print(f" - Error: Service '{service_name}' missing 'command' field") + continue + + print(f" - Registering service: {service_name}") + print(f" Command: {command}") + print(f" Args: {args_list}") + if env_vars: + print(f" Environment variables: {len(env_vars)} vars") + + try: + # Create StdIOStatefulClient for local MCP service + client = StdIOStatefulClient( + name=service_name, + command=command, + args=args_list, + env=env_vars if env_vars else None + ) + + await client.connect() + # Register the MCP client with toolkit + await toolkit.register_mcp_client(client) + # Add to list for later cleanup + clients.append(client) + print(f" ✓ Successfully registered {service_name}") + + except Exception as e: + print(f" ✗ Error registering {service_name}: {e}") + + except Exception as e: + print(f" - Error parsing configuration: {e}") + + return clients + + +async def _connect_remote_server( + server: Dict[str, Any], + toolkit: Toolkit +) -> None: + """ + Connect to a remote MCP server. + + Args: + server: Server configuration + toolkit: AgentScope toolkit + """ + url = server.get('url', '') + transport = server.get('transportType', 'streamable_http') + api_key = server.get('apiKey', '') + + print(f" - URL: {url}") + print(f" - Transport: {transport}") + print(f" - API Key: {api_key}") + + try: + stateless_client = HttpStatelessClient( + name=server.get('name', 'MCP Client'), + transport=transport, + url=url, + headers={"Authorization": f"Bearer {api_key}"} + ) + + await toolkit.register_mcp_client(stateless_client) + print(f" ✓ Successfully registered remote server") + + except Exception as e: + print(f" - Error: {e}") + + +async def close_mcp_connections( + local_mcp_clients: List[StdIOStatefulClient] +) -> None: + """ + Close local MCP connections in LIFO order (last connected, first closed). + + Args: + local_mcp_clients: List of local MCP clients to close + """ + print(f"[Friday] Closing {len(local_mcp_clients)} local MCP connection(s)...") + + while local_mcp_clients: + client = local_mcp_clients.pop() # LIFO: pop from end + try: + await client.close() + print(f" ✓ Closed connection: {client.name}") + except Exception as e: + print(f" ✗ Error closing {client.name}: {e}") diff --git a/packages/client/src/components/MCP/DeleteConfirmDialog.tsx b/packages/client/src/components/MCP/DeleteConfirmDialog.tsx new file mode 100644 index 00000000..1a50512f --- /dev/null +++ b/packages/client/src/components/MCP/DeleteConfirmDialog.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; + +interface DeleteConfirmDialogProps { + isOpen: boolean; + serverName: string; + onConfirm: () => void; + onCancel: () => void; +} + +export const DeleteConfirmDialog: React.FC = ({ + isOpen, + serverName, + onConfirm, + onCancel, +}) => { + const { t } = useTranslation(); + + if (!isOpen) return null; + + return ( +
+
+
+

+ {t('mcp.delete-confirm-title')} +

+

+ {t('mcp.delete-confirm-message')} + + {' "'} + {serverName} + {'"'} + +

+
+ + +
+
+
+
+ ); +}; diff --git a/packages/client/src/components/MCP/MCPServerCardHeader.tsx b/packages/client/src/components/MCP/MCPServerCardHeader.tsx new file mode 100644 index 00000000..df4a7a37 --- /dev/null +++ b/packages/client/src/components/MCP/MCPServerCardHeader.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Trash2Icon, ChevronDownIcon } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { useMCP } from '@/context/MCPContext'; +import { MCPServer } from '@shared/config/friday'; + +interface MCPServerCardHeaderProps { + server: MCPServer; + index: number; + isOpen: boolean; + onToggle: () => void; + onDelete: () => void; +} + +export const MCPServerCardHeader: React.FC = ({ + server, + index, + isOpen, + onToggle, + onDelete, +}) => { + const { t } = useTranslation(); + const { updateAndSaveEnabled } = useMCP(); + + const handleEnabledChange = (checked: boolean) => { + // 立即更新并保存开关状态 + updateAndSaveEnabled(index, checked); + }; + + return ( +
+ + +
+ +
+
+ ); +}; diff --git a/packages/client/src/components/MCP/MCPServerForm.tsx b/packages/client/src/components/MCP/MCPServerForm.tsx new file mode 100644 index 00000000..355a606d --- /dev/null +++ b/packages/client/src/components/MCP/MCPServerForm.tsx @@ -0,0 +1,206 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Button } from '@/components/ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useMCP } from '@/context/MCPContext'; +import { MCPServer } from '@shared/config/friday'; + +interface MCPServerFormProps { + server: MCPServer; + index: number; + onSave: () => void; + onCancel: () => void; +} + +export const MCPServerForm: React.FC = ({ + server, + index, + onSave, + onCancel, +}) => { + const { t } = useTranslation(); + const { updateServer, validateServer } = useMCP(); + + const handleSave = () => { + const validation = validateServer(server); + if (!validation.valid && validation.message) { + alert(t(validation.message) + ' ' + t('mcp.required')); + return; + } + onSave(); + }; + + return ( +
+ {/* 服务器名称 */} +
+ + + updateServer(index, 'name', e.target.value) + } + className="w-full" + /> +
+ + {/* 类型选择 */} +
+ +
+ + +
+
+ + {/* 本地服务配置 */} + {server.type === 'local' ? ( +
+ +