Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 28 additions & 38 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions packages/app/friday/args.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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")
Expand Down Expand Up @@ -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
8 changes: 8 additions & 0 deletions packages/app/friday/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
8 changes: 8 additions & 0 deletions packages/app/friday/mcp_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -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',
]
177 changes: 177 additions & 0 deletions packages/app/friday/mcp_manager/manager.py
Original file line number Diff line number Diff line change
@@ -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}")
53 changes: 53 additions & 0 deletions packages/client/src/components/MCP/DeleteConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<DeleteConfirmDialogProps> = ({
isOpen,
serverName,
onConfirm,
onCancel,
}) => {
const { t } = useTranslation();

if (!isOpen) return null;

return (
<div className="absolute inset-0 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl border border-gray-200 w-full max-w-[400px] mx-4">
<div className="p-6">
<h2 className="text-base font-semibold mb-2">
{t('mcp.delete-confirm-title')}
</h2>
<p className="text-sm text-muted-foreground mb-4">
{t('mcp.delete-confirm-message')}
<span className="font-medium text-foreground">
{' "'}
{serverName}
{'"'}
</span>
</p>
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={onCancel}>
{t('button.cancel')}
</Button>
<Button
variant="destructive"
size="sm"
onClick={onConfirm}
>
{t('button.delete')}
</Button>
</div>
</div>
</div>
</div>
);
};
72 changes: 72 additions & 0 deletions packages/client/src/components/MCP/MCPServerCardHeader.tsx
Original file line number Diff line number Diff line change
@@ -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<MCPServerCardHeaderProps> = ({
server,
index,
isOpen,
onToggle,
onDelete,
}) => {
const { t } = useTranslation();
const { updateAndSaveEnabled } = useMCP();

const handleEnabledChange = (checked: boolean) => {
// 立即更新并保存开关状态
updateAndSaveEnabled(index, checked);
};

return (
<div className="flex items-center justify-between pr-2">
<button
type="button"
className="flex items-center gap-3 flex-1 py-4 px-4 text-left hover:no-underline focus:outline-none"
onClick={onToggle}
>
<Switch
checked={server.enabled !== false}
onCheckedChange={handleEnabledChange}
onClick={(e) => e.stopPropagation()}
className="data-[state=checked]:bg-green-500"
/>
<span className="text-sm font-medium text-gray-800 flex-1">
{server.name || `${t('mcp.server')} ${index + 1}`}
</span>
</button>
<Button
variant="ghost"
size="icon-sm"
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
className="text-red-500 hover:text-red-600 hover:bg-red-50"
>
<Trash2Icon className="h-4 w-4" />
</Button>
<div
className="flex items-center justify-center w-8 h-8 cursor-pointer"
onClick={onToggle}
>
<ChevronDownIcon
className={`h-5 w-5 text-gray-500 transition-transform duration-200 ${
isOpen ? 'rotate-180' : ''
}`}
/>
</div>
</div>
);
};
Loading
Loading