Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
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
68 changes: 29 additions & 39 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"figlet": "^1.7.0",
"find-process": "^1.4.10",
"google-protobuf": "^3.21.4",
"json5": "^2.2.3",
"moment": "^2.30.1",
"opener": "^1.5.2",
"portfinder": "^1.0.36",
Expand Down
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',
]
261 changes: 261 additions & 0 deletions packages/app/friday/mcp_manager/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
# -*- 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')
cwd = service_config.get('cwd')
encoding = service_config.get('encoding', 'utf-8')
encoding_error_handler = service_config.get('encoding_error_handler', 'strict')

if not command:
print(f" - Error: Service '{service_name}' missing 'command' field")
continue

print(f" - Registering service: {service_name}")
print(f" Command: {command}")
if args_list:
print(f" Args: {args_list}")
if env_vars:
print(f" Environment variables: {len(env_vars)} vars")
if cwd:
print(f" Working directory: {cwd}")
if encoding != 'utf-8':
print(f" Encoding: {encoding}")
if encoding_error_handler != 'strict':
print(f" Encoding error handler: {encoding_error_handler}")

try:
# Create StdIOStatefulClient for local MCP service
# 只传递前端提供的参数,其他使用默认值
client_kwargs = {
'name': service_name,
'command': command,
}

# 添加可选参数(只有当前端提供了才添加)
if args_list is not None:
client_kwargs['args'] = args_list
if env_vars is not None:
client_kwargs['env'] = env_vars
if cwd is not None:
client_kwargs['cwd'] = cwd
if encoding != 'utf-8':
client_kwargs['encoding'] = encoding
if encoding_error_handler != 'strict':
client_kwargs['encoding_error_handler'] = encoding_error_handler

client = StdIOStatefulClient(**client_kwargs)

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.
Reads from remoteConfig field which stores mcpServers format JSON.

Args:
server: Server configuration
toolkit: AgentScope toolkit
"""
print(f"\n[Remote MCP] {server.get('name', 'Unknown')}")

# 读取 remoteConfig 字段
remote_config = server.get('remoteConfig', '')
if not remote_config:
print(f" - Error: Remote config is empty")
return

try:
config = json5.loads(remote_config)

# 解析 mcpServers 嵌套格式
if 'mcpServers' not in config:
print(f" - Error: Invalid config format, mcpServers field required")
return

mcp_servers = config['mcpServers']
# 取第一个服务器配置
first_server_key = next(iter(mcp_servers.keys()), None)
if not first_server_key:
print(f" - Error: No server found in mcpServers")
return

server_config = mcp_servers[first_server_key]
url = server_config.get('url', '')
transport_type = server_config.get('type', 'streamablehttp')
# 转换为后端所需的格式:streamablehttp -> streamable_http
transport = 'streamable_http' if transport_type == 'streamablehttp' else transport_type
headers = server_config.get('headers')
timeout = server_config.get('timeout')
sse_read_timeout = server_config.get('sse_read_timeout')
client_kwargs = server_config.get('client_kwargs', {})

if not url:
print(f" - Error: URL is required")
return

print(f" - Server Key: {first_server_key}")
print(f" - URL: {url}")
print(f" - Transport: {transport}")
if headers:
print(f" - Headers: {headers}")
if timeout:
print(f" - Timeout: {timeout}s")
if sse_read_timeout:
print(f" - SSE Read Timeout: {sse_read_timeout}s")
if client_kwargs:
print(f" - Additional client kwargs: {client_kwargs}")

except Exception as e:
print(f" - Error parsing remote config: {e}")
return

try:
# Create HttpStatelessClient for remote MCP service
# 只传递前端提供的参数,其他使用默认值
client_params = {
'name': server.get('name', 'MCP Client'),
'transport': transport,
'url': url,
}

# 添加可选参数(只有当前端提供了才添加)
if headers is not None:
client_params['headers'] = headers
if timeout is not None:
client_params['timeout'] = timeout
if sse_read_timeout is not None:
client_params['sse_read_timeout'] = sse_read_timeout

# 合并额外的 client_kwargs
if client_kwargs:
client_params.update(client_kwargs)

stateless_client = HttpStatelessClient(**client_params)

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}")
Loading
Loading