ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,233 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
"""
This module defines decorators for creating MCP servers.
By using the @mcp_server decorator, you can convert a Python class into an MCP server,
where the class methods will be automatically converted into MCP tools.
"""
import inspect
import functools
import threading
from typing import Type, Dict, Any, Optional, Union, List
# Import FastMCP
from mcp.server import FastMCP
from aworld.core.factory import Factory
from aworld.logs.util import logger
# Save all decorated MCP server classes
class MCPServerRegistry(Factory):
"""Register all MCP server classes"""
def __init__(self, type_name: str = None):
super().__init__(type_name)
self._instance = {}
def register(self, name: str, cls: Type, **kwargs):
"""Register MCP server class"""
self._cls[name] = cls
def get_instance(self, name: str, *args, **kwargs):
"""Get MCP server instance"""
if name not in self._instance:
if name not in self._cls:
raise ValueError(f"MCP server {name} not registered")
self._instance[name] = self._cls[name](*args, **kwargs)
return self._instance[name]
# Create global registry instance
MCPServers = MCPServerRegistry()
def extract_param_desc(method, param_name):
"""Extract parameter description from method docstring"""
if not method.__doc__:
return None
param_docs = [
line.strip() for line in method.__doc__.split('\n')
if line.strip().startswith(f":param {param_name}:")
]
if param_docs:
return param_docs[0].replace(f":param {param_name}:", "").strip()
return None
def mcp_server(name: str = None, **server_config):
"""
Decorator to convert a class into an MCP server
Args:
name: Server name, if None, uses the class name
**server_config: Server configuration parameters
- mode: Server running mode, supports 'stdio' and 'sse' (default: 'sse')
- host: Host address in SSE mode (default: '127.0.0.1')
- port: Port number in SSE mode (default: 8888)
- sse_path: Path in SSE mode (default: '/sse')
- auto_start: Whether to automatically start the server (default: True)
Example:
@mcp_server(
name="simple-calculator",
mode="sse",
host="localhost",
port=8085,
sse_path="/calculator/sse"
)
class Calculator:
'''Server description'''
def __init__(self):
self.data = {}
def get_data(self, key: str) -> str:
'''Get data
:param key: Data key
:return: Data value
'''
return self.data.get(key, "")
"""
# Extract server configuration or use defaults
mode = server_config.get('mode', 'sse')
host = server_config.get('host', '127.0.0.1')
port = server_config.get('port', 8888)
sse_path = server_config.get('sse_path', '/sse')
auto_start = server_config.get('auto_start', True)
def decorator(cls):
server_name = name or cls.__name__
# Use class docstring as server description
server_description = cls.__doc__ or f"{server_name} MCP Server"
# Original initialization method
original_init = cls.__init__
@functools.wraps(original_init)
def new_init(self, *args, **kwargs):
# Call original initialization method
original_init(self, *args, **kwargs)
# Create FastMCP instance, set server name and description
self._mcp = FastMCP(server_name, description=server_description.strip())
# Tool name list for recording
tool_names = []
# Get all methods, filter out built-in and private methods
for method_name, method in inspect.getmembers(self, inspect.ismethod):
if not method_name.startswith('_') and method_name != 'run':
# Get method docstring as tool description
tool_description = method.__doc__ or f"{method_name} tool"
tool_description = tool_description.strip()
# Record tool name
tool_names.append(method_name)
# Create tool and register, using a function generator to ensure each method is correctly bound
def create_tool_wrapper(method_to_call):
# Check if method is async
is_async = inspect.iscoroutinefunction(method_to_call)
if is_async:
@self._mcp.tool(name=method_name, description=tool_description)
@functools.wraps(method_to_call)
async def wrapped_method(*args, **kwargs):
return await method_to_call(*args, **kwargs)
else:
@self._mcp.tool(name=method_name, description=tool_description)
@functools.wraps(method_to_call)
def wrapped_method(*args, **kwargs):
return method_to_call(*args, **kwargs)
return wrapped_method
# Create a dedicated wrapper for each method
create_tool_wrapper(method)
# Print server information
logger.info(f"Creating MCP server: {server_name}")
logger.info(f"Server description: {server_description.strip()}")
if tool_names:
logger.info(f"Registered tools: {', '.join(tool_names)}")
# Save configuration
self._server_config = {
'mode': mode,
'host': host,
'port': port,
'sse_path': sse_path
}
# Auto start server if configured
if auto_start:
# Start server in a new thread to avoid blocking
thread = threading.Thread(
target=self.run,
kwargs=self._server_config,
daemon=True
)
thread.start()
logger.info(f"Server {server_name} started in a background thread")
self._server_thread = thread
# Replace initialization method
cls.__init__ = new_init
# Add method to run server
def run(self, mode: str = mode, host: str = host, port: int = port, sse_path: str = sse_path):
"""
Run MCP server
Args:
mode: Server running mode, supports 'stdio' and 'sse'
host: Host address in SSE mode
port: Port number in SSE mode
sse_path: Path in SSE mode
"""
if not hasattr(self, '_mcp') or self._mcp is None:
raise RuntimeError("MCP server not initialized")
# Run server according to mode
if mode == "stdio":
self._mcp.run(transport="stdio")
elif mode == "sse":
# Configure SSE mode settings
self._mcp.settings.host = host
self._mcp.settings.port = port
self._mcp.settings.sse_path = sse_path
# Print running information
print(f"Running MCP server: {server_name}")
print(f"Description: {server_description.strip()}")
print(f"Address: http://{host}:{port}{sse_path}")
self._mcp.run(transport="sse")
else:
raise ValueError(f"Unsupported mode: {mode}, supported modes are 'stdio' and 'sse'")
cls.run = run
# Add a stop method to gracefully stop the server
def stop(self):
"""Stop the MCP server if it's running"""
if hasattr(self, '_mcp') and self._mcp is not None:
# TODO: Implement proper stopping mechanism based on FastMCP API
logger.info(f"Stopping server {server_name}")
# Currently there might not be a proper way to stop FastMCP server
# This is a placeholder for future implementation
cls.stop = stop
# Register to MCP server registry
MCPServers.register(server_name, cls)
# Return modified class
return cls
return decorator
@@ -0,0 +1,463 @@
from __future__ import annotations
import abc
import asyncio
from datetime import timedelta
import logging
from contextlib import AbstractAsyncContextManager, AsyncExitStack
from pathlib import Path
from typing import Any, Literal
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from mcp import ClientSession, StdioServerParameters, Tool as MCPTool, stdio_client
from mcp.client.sse import sse_client
from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client
from mcp.shared.session import ProgressFnT
from mcp.types import CallToolResult, JSONRPCMessage, InitializeResult
from mcp.shared.message import SessionMessage
from typing_extensions import NotRequired, TypedDict
class MCPServer(abc.ABC):
"""Base class for Model Context Protocol servers."""
@abc.abstractmethod
async def connect(self):
"""Connect to the server. For example, this might mean spawning a subprocess or
opening a network connection. The server is expected to remain connected until
`cleanup()` is called.
"""
pass
@property
@abc.abstractmethod
def name(self) -> str:
"""A readable name for the server."""
pass
@abc.abstractmethod
async def cleanup(self):
"""Cleanup the server. For example, this might mean closing a subprocess or
closing a network connection.
"""
pass
@abc.abstractmethod
async def list_tools(self) -> list[MCPTool]:
"""List the tools available on the server."""
pass
@abc.abstractmethod
async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult:
"""Invoke a tool on the server."""
pass
class _MCPServerWithClientSession(MCPServer, abc.ABC):
"""Base class for MCP servers that use a `ClientSession` to communicate with the server."""
#def __init__(self, cache_tools_list: bool, session_connect_timeout_seconds: int = 120):
def __init__(self, cache_tools_list: bool, client_session_timeout_seconds: float | None):
"""
Args:
cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be
cached and only fetched from the server once. If `False`, the tools list will be
fetched from the server on each call to `list_tools()`. The cache can be invalidated
by calling `invalidate_tools_cache()`. You should set this to `True` if you know the
server will not change its tools list, because it can drastically improve latency
(by avoiding a round-trip to the server every time).
#session_connect_timeout_seconds: session connect timeout seconds
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
"""
self.session: ClientSession | None = None
self.exit_stack: AsyncExitStack = AsyncExitStack()
self._cleanup_lock: asyncio.Lock = asyncio.Lock()
self.cache_tools_list = cache_tools_list
self.server_initialize_result: InitializeResult | None = None
#self.session_connect_timeout_seconds = timedelta(seconds=session_connect_timeout_seconds)
self.client_session_timeout_seconds = client_session_timeout_seconds
# The cache is always dirty at startup, so that we fetch tools at least once
self._cache_dirty = True
self._tools_list: list[MCPTool] | None = None
@abc.abstractmethod
def create_streams(
self,
) -> AbstractAsyncContextManager[
tuple[
MemoryObjectReceiveStream[SessionMessage | Exception],
MemoryObjectSendStream[SessionMessage],
GetSessionIdCallback | None
]
]:
"""Create the streams for the server."""
pass
# def create_streams(
# self,
# ) -> AbstractAsyncContextManager[
# tuple[
# MemoryObjectReceiveStream[JSONRPCMessage | Exception],
# MemoryObjectSendStream[JSONRPCMessage],
# ]
# ]:
# """Create the streams for the server."""
# pass
async def __aenter__(self):
await self.connect()
return self
async def __aexit__(self, exc_type, exc_value, traceback):
await self.cleanup()
def invalidate_tools_cache(self):
"""Invalidate the tools cache."""
self._cache_dirty = True
async def connect(self):
"""Connect to the server."""
try:
transport = await self.exit_stack.enter_async_context(self.create_streams())
# streamablehttp_client returns (read, write, get_session_id)
# sse_client returns (read, write)
read, write, *_ = transport
session = await self.exit_stack.enter_async_context(
ClientSession(
read,
write,
timedelta(seconds=self.client_session_timeout_seconds)
if self.client_session_timeout_seconds
else None,
)
)
server_result = await session.initialize()
self.server_initialize_result = server_result
self.session = session
except Exception as e:
logging.error(f"Error initializing MCP server: {e}")
await self.cleanup()
return
except BaseException as e:
logging.error(f"Error initializing MCP server: {e}")
await self.cleanup()
return
async def list_tools(self) -> list[MCPTool]:
"""List the tools available on the server."""
if not self.session:
raise RuntimeError("Server not initialized. Make sure you call `connect()` first.")
# Return from cache if caching is enabled, we have tools, and the cache is not dirty
if self.cache_tools_list and not self._cache_dirty and self._tools_list:
return self._tools_list
# Reset the cache dirty to False
self._cache_dirty = False
# Fetch the tools from the server
self._tools_list = (await self.session.list_tools()).tools
return self._tools_list
# async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult:
# """Invoke a tool on the server."""
# if not self.session:
# raise RuntimeError("Server not initialized. Make sure you call `connect()` first.")
#
# return await self.session.call_tool(tool_name, arguments)
async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None,read_timeout_seconds: timedelta | None = None,progress_callback: ProgressFnT | None = None) -> CallToolResult:
"""Invoke a tool on the server."""
if not self.session:
raise RuntimeError("Server not initialized. Make sure you call `connect()` first.")
return await self.session.call_tool(name=tool_name, arguments=arguments,read_timeout_seconds=read_timeout_seconds,progress_callback=progress_callback)
async def cleanup(self):
"""Cleanup the server."""
async with self._cleanup_lock:
try:
# Ensure cleanup operations occur in the same task context
session = self.session
self.session = None # Remove reference first
# Wait briefly to ensure any pending operations complete
try:
await asyncio.sleep(0.1)
except asyncio.CancelledError:
# Ignore cancellation exceptions, continue cleaning resources
pass
# Clean up exit_stack, ensuring all resources are properly closed
exit_stack = self.exit_stack
if exit_stack:
try:
await exit_stack.aclose()
except Exception as e:
logging.debug(f"Error closing exit stack during cleanup: {e}")
except Exception as e:
logging.error(f"Error during server cleanup: {e}")
finally:
self.session = None
class MCPServerStdioParams(TypedDict):
"""Mirrors `mcp.client.stdio.StdioServerParameters`, but lets you pass params without another
import.
"""
command: str
"""The executable to run to start the server. For example, `python` or `node`."""
args: NotRequired[list[str]]
"""Command line args to pass to the `command` executable. For example, `['foo.py']` or
`['server.js', '--port', '4242']`."""
env: NotRequired[dict[str, str]]
"""The environment variables to set for the server. ."""
cwd: NotRequired[str | Path]
"""The working directory to use when spawning the process."""
encoding: NotRequired[str]
"""The text encoding used when sending/receiving messages to the server. Defaults to `utf-8`."""
encoding_error_handler: NotRequired[Literal["strict", "ignore", "replace"]]
"""The text encoding error handler. Defaults to `strict`.
See https://docs.python.org/3/library/codecs.html#codec-base-classes for
explanations of possible values.
"""
client_session_timeout_seconds: NotRequired[float]
class MCPServerStdio(_MCPServerWithClientSession):
"""MCP server implementation that uses the stdio transport. See the [spec]
(https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) for
details.
"""
def __init__(
self,
params: MCPServerStdioParams,
cache_tools_list: bool = False,
name: str | None = None,
client_session_timeout_seconds: float | None = 120,
):
"""Create a new MCP server based on the stdio transport.
Args:
params: The params that configure the server. This includes the command to run to
start the server, the args to pass to the command, the environment variables to
set for the server, the working directory to use when spawning the process, and
the text encoding used when sending/receiving messages to the server.
cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be
cached and only fetched from the server once. If `False`, the tools list will be
fetched from the server on each call to `list_tools()`. The cache can be
invalidated by calling `invalidate_tools_cache()`. You should set this to `True`
if you know the server will not change its tools list, because it can drastically
improve latency (by avoiding a round-trip to the server every time).
name: A readable name for the server. If not provided, we'll create one from the
command.
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
"""
# super().__init__(cache_tools_list, int(params.get("env").get("SESSION_REQUEST_CONNECT_TIMEOUT", "60")))
if params and params.get("client_session_timeout_seconds"):
client_session_timeout_seconds = params.get("client_session_timeout_seconds")
super().__init__(cache_tools_list, client_session_timeout_seconds)
self.params = StdioServerParameters(
command=params["command"],
args=params.get("args", []),
env=params.get("env"),
cwd=params.get("cwd"),
encoding=params.get("encoding", "utf-8"),
encoding_error_handler=params.get("encoding_error_handler", "strict"),
)
self._name = name or f"stdio: {self.params.command}"
def create_streams(
self,
) -> AbstractAsyncContextManager[
tuple[
MemoryObjectReceiveStream[SessionMessage | Exception],
MemoryObjectSendStream[SessionMessage],
GetSessionIdCallback | None
]
]:
"""Create the streams for the server."""
return stdio_client(self.params)
@property
def name(self) -> str:
"""A readable name for the server."""
return self._name
class MCPServerSseParams(TypedDict):
"""Mirrors the params in`mcp.client.sse.sse_client`."""
url: str
"""The URL of the server."""
headers: NotRequired[dict[str, str]]
"""The headers to send to the server."""
timeout: NotRequired[float]
"""The timeout for the HTTP request. Defaults to 60 seconds."""
sse_read_timeout: NotRequired[float]
"""The timeout for the SSE connection, in seconds. Defaults to 5 minutes."""
client_session_timeout_seconds: NotRequired[float]
class MCPServerSse(_MCPServerWithClientSession):
"""MCP server implementation that uses the HTTP with SSE transport. See the [spec]
(https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse)
for details.
"""
def __init__(
self,
params: MCPServerSseParams,
cache_tools_list: bool = False,
name: str | None = None,
client_session_timeout_seconds: float | None = 120,
):
"""Create a new MCP server based on the HTTP with SSE transport.
Args:
params: The params that configure the server. This includes the URL of the server,
the headers to send to the server, the timeout for the HTTP request, and the
timeout for the SSE connection.
cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be
cached and only fetched from the server once. If `False`, the tools list will be
fetched from the server on each call to `list_tools()`. The cache can be
invalidated by calling `invalidate_tools_cache()`. You should set this to `True`
if you know the server will not change its tools list, because it can drastically
improve latency (by avoiding a round-trip to the server every time).
name: A readable name for the server. If not provided, we'll create one from the
URL.
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
"""
#super().__init__(cache_tools_list)
if params and params.get("client_session_timeout_seconds"):
client_session_timeout_seconds = params.get("client_session_timeout_seconds")
super().__init__(cache_tools_list, client_session_timeout_seconds)
self.params = params
self._name = name or f"sse: {self.params['url']}"
def create_streams(
self,
) -> AbstractAsyncContextManager[
tuple[
MemoryObjectReceiveStream[SessionMessage | Exception],
MemoryObjectSendStream[SessionMessage],
GetSessionIdCallback | None
]
]:
"""Create the streams for the server."""
return sse_client(
url=self.params["url"],
headers=self.params.get("headers", None),
timeout=self.params.get("timeout", 60),
sse_read_timeout=self.params.get("sse_read_timeout", 60 * 5),
)
@property
def name(self) -> str:
"""A readable name for the server."""
return self._name
class MCPServerStreamableHttpParams(TypedDict):
"""Mirrors the params in`mcp.client.streamable_http.streamablehttp_client`."""
url: str
"""The URL of the server."""
headers: NotRequired[dict[str, str]]
"""The headers to send to the server."""
timeout: NotRequired[timedelta]
"""The timeout for the HTTP request. Defaults to 5 seconds."""
sse_read_timeout: NotRequired[timedelta]
"""The timeout for the SSE connection, in seconds. Defaults to 5 minutes."""
terminate_on_close: NotRequired[bool]
"""Terminate on close"""
client_session_timeout_seconds: NotRequired[float]
class MCPServerStreamableHttp(_MCPServerWithClientSession):
"""MCP server implementation that uses the Streamable HTTP transport. See the [spec]
(https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)
for details.
"""
def __init__(
self,
params: MCPServerStreamableHttpParams,
cache_tools_list: bool = False,
name: str | None = None,
client_session_timeout_seconds: float | None = 120,
):
"""Create a new MCP server based on the Streamable HTTP transport.
Args:
params: The params that configure the server. This includes the URL of the server,
the headers to send to the server, the timeout for the HTTP request, and the
timeout for the Streamable HTTP connection and whether we need to
terminate on close.
cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be
cached and only fetched from the server once. If `False`, the tools list will be
fetched from the server on each call to `list_tools()`. The cache can be
invalidated by calling `invalidate_tools_cache()`. You should set this to `True`
if you know the server will not change its tools list, because it can drastically
improve latency (by avoiding a round-trip to the server every time).
name: A readable name for the server. If not provided, we'll create one from the
URL.
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
"""
if params and params.get("client_session_timeout_seconds"):
client_session_timeout_seconds = params.get("client_session_timeout_seconds")
super().__init__(cache_tools_list, client_session_timeout_seconds)
self.params = params
self._name = name or f"streamable_http: {self.params['url']}"
def create_streams(
self,
) -> AbstractAsyncContextManager[
tuple[
MemoryObjectReceiveStream[SessionMessage | Exception],
MemoryObjectSendStream[SessionMessage],
GetSessionIdCallback | None
]
]:
"""Create the streams for the server."""
return streamablehttp_client(
url=self.params["url"],
headers=self.params.get("headers", None),
timeout=self.params.get("timeout", timedelta(seconds=30)),
sse_read_timeout=self.params.get("sse_read_timeout", timedelta(seconds=60 * 5)),
terminate_on_close=self.params.get("terminate_on_close", True)
)
@property
def name(self) -> str:
"""A readable name for the server."""
return self._name
@@ -0,0 +1,809 @@
import logging
from datetime import timedelta
from typing import List, Dict, Any
import json
import os
from contextlib import AsyncExitStack
import traceback
import requests
from aworld.core.context.base import Context
from mcp.types import TextContent, ImageContent
from aworld.core.common import ActionResult
from aworld.logs.util import logger
from aworld.mcp_client.server import MCPServer, MCPServerSse, MCPServerStdio, MCPServerStreamableHttp
from aworld.tools import get_function_tools
from aworld.utils.common import find_file
MCP_SERVERS_CONFIG = {}
def get_function_tool(sever_name: str) -> List[Dict[str, Any]]:
openai_tools = []
try:
if not sever_name:
return []
tool_server = get_function_tools(sever_name)
if not tool_server:
return []
tools = tool_server.list_tools()
if not tools:
return []
for tool in tools:
required = []
properties = {}
if tool.inputSchema and tool.inputSchema.get("properties"):
required = tool.inputSchema.get("required", [])
_properties = tool.inputSchema["properties"]
for param_name, param_info in _properties.items():
param_type = (
param_info.get("type")
if param_info.get("type") != "str"
and param_info.get("type") is not None
else "string"
)
param_desc = param_info.get("description", "")
if param_type == "array":
# Handle array type parameters
items_info = param_info.get("items", {})
item_type = items_info.get("type", "string")
# Process nested array type parameters
if item_type == "array":
nested_items = items_info.get("items", {})
nested_type = nested_items.get("type", "string")
# If the nested type is an object
if nested_type == "object":
properties[param_name] = {
"description": param_desc,
"type": param_type,
"items": {
"type": item_type,
"items": {
"type": nested_type,
"properties": nested_items.get(
"properties", {}
),
"required": nested_items.get(
"required", []
),
},
},
}
else:
properties[param_name] = {
"description": param_desc,
"type": param_type,
"items": {
"type": item_type,
"items": {"type": nested_type},
},
}
# Process object type cases
elif item_type == "object":
properties[param_name] = {
"description": param_desc,
"type": param_type,
"items": {
"type": item_type,
"properties": items_info.get("properties", {}),
"required": items_info.get("required", []),
},
}
# Process basic type cases
else:
if item_type == "str":
item_type = "string"
properties[param_name] = {
"description": param_desc,
"type": param_type,
"items": {"type": item_type},
}
else:
# Handle non-array type parameters
properties[param_name] = {
"description": param_desc,
"type": param_type,
}
openai_function_schema = {
"name": f"mcp__{sever_name}__{tool.name}",
"description": tool.description,
"parameters": {
"type": "object",
"properties": properties,
"required": required,
},
}
openai_tools.append(
{
"type": "function",
"function": openai_function_schema,
}
)
logging.info(
f"✅ function_tool_server #({sever_name}) connected successtools: {len(tools)}"
)
except Exception as e:
logging.warning(
f"server_name-get_function_tool:{sever_name} translate failed: {e}"
)
return []
finally:
return openai_tools
async def run(mcp_servers: list[MCPServer],black_tool_actions: Dict[str, List[str]] = None) -> List[Dict[str, Any]]:
openai_tools = []
for i, server in enumerate(mcp_servers):
try:
tools = await server.list_tools()
for tool in tools:
balck_server = server.name
if server.name.startswith("mcp__"):
balck_server = server.name[5:] if len(server.name) > 5 else server.name
if (black_tool_actions and
balck_server in black_tool_actions and
black_tool_actions[balck_server] and
tool.name in black_tool_actions[balck_server]):
logging.info(
f"server #{i + 1} ({balck_server}) black_tool_actions: {tool.name}"
)
continue
required = []
properties = {}
if tool.inputSchema and tool.inputSchema.get("properties"):
required = tool.inputSchema.get("required", [])
_properties = tool.inputSchema["properties"]
for param_name, param_info in _properties.items():
param_type = (
param_info.get("type")
if param_info.get("type") != "str"
and param_info.get("type") is not None
else "string"
)
param_desc = param_info.get("description", "")
if param_type == "array":
# Handle array type parameters
items_info = param_info.get("items", {})
item_type = items_info.get("type", "string")
# Process nested array type parameters
if item_type == "array":
nested_items = items_info.get("items", {})
nested_type = nested_items.get("type", "string")
# If the nested type is an object
if nested_type == "object":
properties[param_name] = {
"description": param_desc,
"type": param_type,
"items": {
"type": item_type,
"items": {
"type": nested_type,
"properties": nested_items.get(
"properties", {}
),
"required": nested_items.get(
"required", []
),
},
},
}
else:
properties[param_name] = {
"description": param_desc,
"type": param_type,
"items": {
"type": item_type,
"items": {"type": nested_type},
},
}
# Process object type cases
elif item_type == "object":
properties[param_name] = {
"description": param_desc,
"type": param_type,
"items": {
"type": item_type,
"properties": items_info.get("properties", {}),
"required": items_info.get("required", []),
},
}
# Process basic type cases
else:
if item_type == "str":
item_type = "string"
properties[param_name] = {
"description": param_desc,
"type": param_type,
"items": {"type": item_type},
}
else:
# Handle non-array type parameters
properties[param_name] = {
"description": param_desc,
"type": param_type,
}
openai_function_schema = {
"name": f"{server.name}__{tool.name}",
"description": tool.description,
"parameters": {
"type": "object",
"properties": properties,
"required": required,
},
}
openai_tools.append(
{
"type": "function",
"function": openai_function_schema,
}
)
logging.info(
f"✅ server #{i + 1} ({server.name}) connected successtools: {len(tools)}"
)
except Exception as e:
logging.warning(
f"❌ server #{i + 1} ({server.name}) connect fail: {e}\n"
f"Traceback:\n{traceback.format_exc()}"
)
continue
return openai_tools
async def mcp_tool_desc_transform_v2(
tools: List[str] = None, mcp_config: Dict[str, Any] = None, context: Context = None,
server_instances: Dict[str, Any] = None,
black_tool_actions: Dict[str, List[str]] = None
) -> List[Dict[str, Any]]:
# todo sandbox mcp_config get from registry
if not mcp_config:
return []
config = mcp_config
global MCP_SERVERS_CONFIG
MCP_SERVERS_CONFIG = config
mcp_servers_config = config.get("mcpServers", {})
server_configs = []
openai_tools = []
mcp_openai_tools = []
for server_name, server_config in mcp_servers_config.items():
# Skip disabled servers
if server_config.get("disabled", False):
continue
if tools is None or server_name in tools:
# Handle SSE server
if "function_tool" == server_config.get("type", ""):
try:
tmp_function_tool = get_function_tool(server_name)
openai_tools.extend(tmp_function_tool)
except Exception as e:
logging.warning(f"server_name:{server_name} translate failed: {e}")
elif "api" == server_config.get("type", ""):
api_result = requests.get(server_config["url"] + "/list_tools")
try:
if not api_result or not api_result.text:
continue
# return None
data = json.loads(api_result.text)
if not data or not data.get("tools"):
continue
for item in data.get("tools"):
tmp_function = {
"type": "function",
"function": {
"name": "mcp__" + server_name + "__" + item["name"],
"description": item["description"],
"parameters": {
**item["parameters"],
"properties": {
k: v
for k, v in item["parameters"]
.get("properties", {})
.items()
if "default" not in v
},
},
},
}
openai_tools.append(tmp_function)
except Exception as e:
logging.warning(f"server_name:{server_name} translate failed: {e}")
elif "sse" == server_config.get("type", ""):
server_configs.append(
{
"name": "mcp__" + server_name,
"type": "sse",
"params": {
"url": server_config["url"],
"headers": server_config.get("headers"),
"timeout": server_config.get("timeout"),
"sse_read_timeout": server_config.get("sse_read_timeout"),
"client_session_timeout_seconds": server_config.get("client_session_timeout_seconds")
},
}
)
elif "streamable-http" == server_config.get("type", ""):
server_configs.append(
{
"name": "mcp__" + server_name,
"type": "streamable-http",
"params": {
"url": server_config["url"],
"headers": server_config.get("headers"),
"timeout": server_config.get("timeout"),
"sse_read_timeout": server_config.get("sse_read_timeout"),
"client_session_timeout_seconds": server_config.get("client_session_timeout_seconds")
},
}
)
# Handle stdio server
else:
# elif "stdio" == server_config.get("type", ""):
server_configs.append(
{
"name": "mcp__" + server_name,
"type": "stdio",
"params": {
"command": server_config["command"],
"args": server_config.get("args", []),
"env": server_config.get("env", {}),
"cwd": server_config.get("cwd"),
"encoding": server_config.get("encoding", "utf-8"),
"encoding_error_handler": server_config.get(
"encoding_error_handler", "strict"
),
"client_session_timeout_seconds": server_config.get("client_session_timeout_seconds")
},
}
)
if not server_configs:
return openai_tools
servers = []
for server_config in server_configs:
try:
_mcp_openai_tools = []
async with AsyncExitStack() as stack:
if server_config["type"] == "sse":
params = server_config["params"].copy()
headers = params.get("headers") or {}
if context and context.session_id:
headers["SESSION_ID"] = context.session_id
if context and context.user:
headers["USER_ID"] = context.user
params["headers"] = headers
server = MCPServerSse(
name=server_config["name"], params=params
)
elif server_config["type"] == "streamable-http":
params = server_config["params"].copy()
headers = params.get("headers") or {}
if context and context.session_id:
headers["SESSION_ID"] = context.session_id
if context and context.user:
headers["USER_ID"] = context.user
params["headers"] = headers
if "timeout" in params and not isinstance(params["timeout"], timedelta):
params["timeout"] = timedelta(seconds=float(params["timeout"]))
if "sse_read_timeout" in params and not isinstance(params["sse_read_timeout"], timedelta):
params["sse_read_timeout"] = timedelta(seconds=float(params["sse_read_timeout"]))
server = MCPServerStreamableHttp(
name=server_config["name"], params=params
)
elif server_config["type"] == "stdio":
server = MCPServerStdio(
name=server_config["name"], params=server_config["params"]
)
else:
logging.warning(
f"Unsupported MCP server type: {server_config['type']}"
)
continue
server = await stack.enter_async_context(server)
#servers.append(server)
_mcp_openai_tools = await run([server],black_tool_actions)
if _mcp_openai_tools:
mcp_openai_tools.extend(_mcp_openai_tools)
except BaseException as err:
# single
logging.warning(
f"Failed to get tools for MCP server '{server_config['name']}'.\n"
f"Error: {err}\n"
f"Traceback:\n{traceback.format_exc()}"
)
continue
#async with AsyncExitStack() as stack:
#mcp_openai_tools = await run(servers)
if mcp_openai_tools:
openai_tools.extend(mcp_openai_tools)
return openai_tools
async def mcp_tool_desc_transform(
tools: List[str] = None, mcp_config: Dict[str, Any] = None
) -> List[Dict[str, Any]]:
# todo sandbox mcp_config get from registry
if not mcp_config:
return []
config = mcp_config
global MCP_SERVERS_CONFIG
MCP_SERVERS_CONFIG = config
mcp_servers_config = config.get("mcpServers", {})
server_configs = []
openai_tools = []
mcp_openai_tools = []
for server_name, server_config in mcp_servers_config.items():
# Skip disabled servers
if server_config.get("disabled", False):
continue
if tools is None or server_name in tools:
# Handle SSE server
if "function_tool" == server_config.get("type", ""):
try:
tmp_function_tool = get_function_tool(server_name)
openai_tools.extend(tmp_function_tool)
except Exception as e:
logging.warning(f"server_name:{server_name} translate failed: {e}")
elif "api" == server_config.get("type", ""):
api_result = requests.get(server_config["url"] + "/list_tools")
try:
if not api_result or not api_result.text:
continue
# return None
data = json.loads(api_result.text)
if not data or not data.get("tools"):
continue
for item in data.get("tools"):
tmp_function = {
"type": "function",
"function": {
"name": "mcp__" + server_name + "__" + item["name"],
"description": item["description"],
"parameters": {
**item["parameters"],
"properties": {
k: v
for k, v in item["parameters"]
.get("properties", {})
.items()
if "default" not in v
},
},
},
}
openai_tools.append(tmp_function)
except Exception as e:
logging.warning(f"server_name:{server_name} translate failed: {e}")
elif "sse" == server_config.get("type", ""):
server_configs.append(
{
"name": "mcp__" + server_name,
"type": "sse",
"params": {
"url": server_config["url"],
"headers": server_config.get("headers"),
"timeout": server_config.get("timeout"),
"sse_read_timeout": server_config.get("sse_read_timeout"),
"client_session_timeout_seconds": server_config.get("client_session_timeout_seconds")
},
}
)
elif "streamable-http" == server_config.get("type", ""):
server_configs.append(
{
"name": "mcp__" + server_name,
"type": "streamable-http",
"params": {
"url": server_config["url"],
"headers": server_config.get("headers"),
"timeout": server_config.get("timeout"),
"sse_read_timeout": server_config.get("sse_read_timeout"),
"client_session_timeout_seconds": server_config.get("client_session_timeout_seconds")
},
}
)
# Handle stdio server
else:
# elif "stdio" == server_config.get("type", ""):
server_configs.append(
{
"name": "mcp__" + server_name,
"type": "stdio",
"params": {
"command": server_config["command"],
"args": server_config.get("args", []),
"env": server_config.get("env", {}),
"cwd": server_config.get("cwd"),
"encoding": server_config.get("encoding", "utf-8"),
"encoding_error_handler": server_config.get(
"encoding_error_handler", "strict"
),
"client_session_timeout_seconds": server_config.get("client_session_timeout_seconds")
},
}
)
if not server_configs:
return openai_tools
async with AsyncExitStack() as stack:
servers = []
for server_config in server_configs:
try:
if server_config["type"] == "sse":
server = MCPServerSse(
name=server_config["name"], params=server_config["params"]
)
elif server_config["type"] == "streamable-http":
params = server_config["params"].copy()
if "timeout" in params and not isinstance(params["timeout"], timedelta):
params["timeout"] = timedelta(seconds=float(params["timeout"]))
if "sse_read_timeout" in params and not isinstance(params["sse_read_timeout"], timedelta):
params["sse_read_timeout"] = timedelta(seconds=float(params["sse_read_timeout"]))
server = MCPServerStreamableHttp(
name=server_config["name"], params=params
)
elif server_config["type"] == "stdio":
server = MCPServerStdio(
name=server_config["name"], params=server_config["params"]
)
else:
logging.warning(
f"Unsupported MCP server type: {server_config['type']}"
)
continue
server = await stack.enter_async_context(server)
servers.append(server)
except BaseException as err:
# single
logging.error(
f"Failed to get tools for MCP server '{server_config['name']}'.\n"
f"Error: {err}\n"
f"Traceback:\n{traceback.format_exc()}"
)
mcp_openai_tools = await run(servers)
if mcp_openai_tools:
openai_tools.extend(mcp_openai_tools)
return openai_tools
async def call_function_tool(
server_name: str,
tool_name: str,
parameter: Dict[str, Any] = None,
mcp_config: Dict[str, Any] = None,
) -> ActionResult:
"""Specifically handle API type server calls
Args:
server_name: Server name
tool_name: Tool name
parameter: Parameters
mcp_config: MCP configuration
Returns:
ActionResult: Call result
"""
action_result = ActionResult(
tool_name=server_name, action_name=tool_name, content="", keep=True
)
try:
tool_server = get_function_tools(server_name)
if not tool_server:
return action_result
call_result_raw = tool_server.call_tool(tool_name, parameter)
if call_result_raw and call_result_raw.content:
if isinstance(call_result_raw.content[0], TextContent):
action_result = ActionResult(
tool_name=server_name,
action_name=tool_name,
content=call_result_raw.content[0].text,
keep=True,
metadata=call_result_raw.content[0].model_extra.get("metadata", {}),
)
elif isinstance(call_result_raw.content[0], ImageContent):
action_result = ActionResult(
tool_name=server_name,
action_name=tool_name,
content=f"data:image/jpeg;base64,{call_result_raw.content[0].data}",
keep=True,
metadata=call_result_raw.content[0].model_extra.get("metadata", {}),
)
except Exception as e:
logging.warning(f"call_function_tool ({server_name})({tool_name}) failed: {e}")
action_result = ActionResult(
tool_name=server_name, action_name=tool_name, content="", keep=True
)
return action_result
async def call_api(
server_name: str,
tool_name: str,
parameter: Dict[str, Any] = None,
mcp_config: Dict[str, Any] = None,
) -> ActionResult:
"""Specifically handle API type server calls
Args:
server_name: Server name
tool_name: Tool name
parameter: Parameters
mcp_config: MCP configuration
Returns:
ActionResult: Call result
"""
action_result = ActionResult(
tool_name=server_name, action_name=tool_name, content="", keep=True
)
if not mcp_config or mcp_config.get("mcpServers") is None:
return action_result
mcp_servers = mcp_config.get("mcpServers")
if not mcp_servers.get(server_name):
return action_result
server_config = mcp_servers.get(server_name)
if "api" != server_config.get("type", ""):
logging.warning(
f"Server {server_name} is not API type, should use call_tool instead"
)
return action_result
try:
headers = {"Content-Type": "application/json"}
response = requests.post(
url=server_config["url"] + "/" + tool_name, headers=headers, json=parameter
)
action_result = ActionResult(
tool_name=server_name,
action_name=tool_name,
content=response.text,
keep=True,
)
except Exception as e:
logging.warning(f"call_api ({server_name})({tool_name}) failed: {e}")
action_result = ActionResult(
tool_name=server_name,
action_name=tool_name,
content=f"Error calling API: {str(e)}",
keep=True,
)
return action_result
async def get_server_instance(
server_name: str, mcp_config: Dict[str, Any] = None,
context: Context = None
) -> Any:
"""Get server instance, create a new one if it doesn't exist
Args:
server_name: Server name
mcp_config: MCP configuration
Returns:
Server instance or None (if creation fails)
"""
if not mcp_config or mcp_config.get("mcpServers") is None:
return None
mcp_servers = mcp_config.get("mcpServers")
if not mcp_servers.get(server_name):
return None
server_config = mcp_servers.get(server_name)
try:
# API type servers use special handling, no need for persistent connections
# Note: We've already handled API type in McpServers.call_tool method
# Here we don't return None, but let the caller handle it
if "api" == server_config.get("type", ""):
logging.info(f"API server {server_name} doesn't need persistent connection")
return None
elif "sse" == server_config.get("type", ""):
headers = server_config.get("headers") or {}
if context and context.session_id:
headers["SESSION_ID"] = context.session_id
if context and context.user:
headers["USER_ID"] = context.user
server = MCPServerSse(
name=server_name,
params={
"url": server_config["url"],
"headers": headers,
"timeout": server_config.get("timeout", 5.0),
"sse_read_timeout": server_config.get("sse_read_timeout", 300.0),
"client_session_timeout_seconds": server_config.get("client_session_timeout_seconds", 300.0),
},
)
await server.connect()
logging.info(f"Successfully connected to SSE server: {server_name}")
return server
elif "streamable-http" == server_config.get("type", ""):
headers = server_config.get("headers") or {}
if context and context.session_id:
headers["SESSION_ID"] = context.session_id
if context and context.user:
headers["USER_ID"] = context.user
server = MCPServerStreamableHttp(
name=server_name,
params={
"url": server_config["url"],
"headers": headers,
"timeout": timedelta(seconds=server_config.get("timeout", 120.0)),
"sse_read_timeout": timedelta(seconds=server_config.get("sse_read_timeout", 300.0)),
},
)
await server.connect()
logging.info(f"Successfully connected to STREAMABLE-HTTP server: {server_name}")
return server
else: # stdio type
params = {
"command": server_config["command"],
"args": server_config.get("args", []),
"env": server_config.get("env", {}),
"cwd": server_config.get("cwd"),
"encoding": server_config.get("encoding", "utf-8"),
"encoding_error_handler": server_config.get(
"encoding_error_handler", "strict"
),
"client_session_timeout_seconds": server_config.get("client_session_timeout_seconds", 300.0),
}
server = MCPServerStdio(name=server_name, params=params)
await server.connect()
logging.info(f"Successfully connected to stdio server: {server_name}")
return server
except Exception as e:
logging.warning(f"Failed to create server instance for {server_name}: {e}")
return None
async def cleanup_server(server):
"""Clean up server connection
Args:
server: Server instance
"""
try:
if hasattr(server, "cleanup"):
await server.cleanup()
elif hasattr(server, "close"):
await server.close()
logging.info(
f"Successfully cleaned up server: {getattr(server, 'name', 'unknown')}"
)
except Exception as e:
logging.warning(f"Failed to cleanup server: {e}")