模型上下文协议(MCP)使得AI代理能够连接到工具和资源,然而目前PydanticAI框架并不原生支持这一协议。在这里,我将展示如何克服这一限制,以便你能够构建强大的AI代理!(剧透:过程并不那么美观。)
什么是MCP?
模型上下文协议(MCP)是由Anthropic提出的一项倡议,旨在定义一种标准化方式,使AI代理能够连接到外部上下文,如提供信息的资源或执行操作的工具。总体思路是设立一个MCP服务器,提供一组工具或资源,以及一个MCP客户端,该客户端可以连接到一个或多个服务器,以访问它们的功能并将其提供给AI代理。
这旨在消除提供上下文或工具的服务与需要访问这些服务的AI框架或模型之间的耦合。
PydanticAI
PydanticAI是一个Python代理框架,旨在减轻使用生成式AI构建生产级应用程序的痛苦。
与LangChain等笨重的替代方案相比,该框架在开发AI代理时通常更加直观和灵活。
然而,它目前还不原生支持MCP,因此创建能够利用通过该协议交付的所有酷炫功能的AI代理并不那么直接。我决定借此机会深入探索MCP,并找出如何“从零开始”将其集成!
准备MCP服务器
我想专注于将PydanticAI代理与现有的MCP服务器实现连接起来,而不是创建自己的服务器(因为这正是协议的目的!)。幸运的是,有一个存储库提供了基本MCP服务器的参考实现,这些服务器可以访问Slack、Google Drive、Github以及各种数据库等服务。为了简单起见,我决定使用文件系统服务器,以帮助创建一个能够协助软件开发的代理,该代理能够读取和编辑文件。
它作为一个简单的Node包/工具实现,可以在本地运行,并通过标准输入输出(STDIO)进行通信。
npx -y @modelcontextprotocol/server-filesystem "/path/to/working/dir""/path/to/working/dir"
经过一些测试后,我很快发现文件搜索和目录树工具存在一些问题,并且在代码仓库中使用起来并不实际,因为它们会被node_modules或Python虚拟环境中的大量内容,或其他通常会被Git或集成开发环境(IDE)忽略的文件所淹没。因此,我复制了一份服务器实现,并添加了一些自定义增强功能,使相关工具能够遵循任何.gitignore规则,从而使它们的输出更加可用。(我还提交了一个拉取请求来修复search_files工具的行为。)
为PydanticAI代理提供工具
下一个挑战是如何将MCP服务器提供的功能注册为PydanticAI代理的工具。使用MCP Python SDK启动一个MCP服务器,用客户端连接到它,并查看它提供了哪些工具,过程如下:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
# Define server configuration
server_params = StdioServerParameters(
command="npx",
args=[
"tsx",
"server/index.ts",
"path/to/working/directory",
],
)
# Start server and connect client
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Initialize the connection
await session.initialize()
# Get details of available tools
tools_result = await session.list_tools()
tools = tools_result.tools
在这里,tools是一个工具Pydantic模型的列表,它们以大多数大型语言模型(LLM)API所要求的工具规范格式,定义了每个可用工具的名称、描述和输入模式。然而,不幸的是,PydanticAI目前不支持直接提供函数工具的输入模式;它只能通过检查函数签名来构建输入模式。
因此,为了绕过这一限制,我需要构建一种能力,将MCP工具规范转换为动态函数定义,仅仅是为了让PydanticAI能够再次将其转换回JSON工具规范 。大致上,它的工作原理是这样的:
async def get_tools(session: ClientSession) -> list[Tool[AgentDeps]]:
"""
Get all tools from the MCP session and convert them to Pydantic AI tools.
"""
tools_result = await session.list_tools()
return [pydantic_tool_from_mcp_tool(session, tool) for tool in tools_result.tools]
def pydantic_tool_from_mcp_tool(session: ClientSession, tool: MCPTool) -> Tool[AgentDeps]:
"""
Convert a MCP tool to a Pydantic AI tool.
"""
tool_function = create_function_from_schema(session=session, name=tool.name, schema=tool.inputSchema)
return Tool(name=tool.name, description=tool.description, function=tool_function, takes_ctx=True)
对于create_function_from_schema()的实现,关键部分是,我们需要为每个工具定义一个函数,该函数向MCP服务器发出实际请求,服务器将执行实际操作,然后返回响应。这看起来像这样:
def create_function_from_schema(session: ClientSession, name: str, schema: Dict[str, Any]) -> types.FunctionType:create_function_from_schema(session: ClientSession, name: str, schema: Dict[str, Any]) -> types.FunctionType:
"""
Create a function with a signature based on a JSON schema. This is necessary because PydanticAI does not yet
support providing a tool JSON schema directly.
"""
# Create parameter list from tool schema
parameters = convert_schema_to_params(schema)
# Create the signature
sig = inspect.Signature(parameters=parameters)
# Create function body
async def function_body(ctx: RunContext[AgentDeps], **kwargs) -> str:
# Call the MCP tool with provided arguments
result = await session.call_tool(name, arguments=kwargs)
# Assume response is always TextContent
if isinstance(result.content[0], TextContent):
return result.content[0].text
else:
raise ValueError("Expected TextContent, got ", type(result.content[0]))
# Create the function with the correct signature
dynamic_function = types.FunctionType(
function_body.__code__,
function_body.__globals__,
name=name,
argdefs=function_body.__defaults__,
closure=function_body.__closure__,
)
# Add signature and annotations
dynamic_function.__signature__ = sig # type: ignore
dynamic_function.__annotations__ = {param.name: param.annotation for param in parameters}
return dynamic_function
整合所有内容
现在我们已经有了一组标准的PydanticAI工具,它们可以直接提供给代理,像平常一样使用,例如:
from pydantic_ai import Agent
from rich.console import Console
from rich.prompt import Prompt
...
def run():
# Configure Model and Agent dependencies
...
# Initialise & connect MCP server, construct tools
...
agent = Agent(
model=model,
deps_type=type(deps),
system_prompt=SYSTEM_PROMPT,
tools=tools,
)
message_history: list[ModelMessage] = []
while True:
prompt = Prompt.ask("[cyan]>[/cyan] ").strip()
if not prompt:
continue
# Handle special commands
if prompt.lower() in EXIT_COMMANDS:
break
# Process normal input through the agent
result = await agent.run(prompt, deps=deps, message_history=message_history)
response = result.data
console.print(f"[bold green]Agent:[/bold green] {response}")
message_history = result.all_messages()
自己尝试一下使用文件系统代理,看看它能做什么!以下是一个示例:
Welcome to MCP Demo CLI! Type /quit to exit.exit.
[DEBUG] Starting MCP server with working directory: /Users/finn.andersen/projects/mcp_demo
[DEBUG] Secure MCP Filesystem Server running on stdio
[DEBUG] Allowed directories: [ '/Users/finn.andersen/projects/mcp_demo' ]
> : Create a file that contains a directory tree structure of the project in a hierarchical format which shows depth of each item using indentation, using a similar format as the result of the "tree" tool. Exclude hidden files and folders
[DEBUG] Calling tool directory_tree with arguments: {'path': '.'}
[DEBUG] Calling tool write_file with arguments: {'path': 'directory_tree.txt', 'content': '...'}
Agent: The directory tree has been successfully written to a file named `directory_tree.txt`. If you need anything
else, feel free to ask!
> :
它成功创建了一个名为directory_tree.txt的文件,内容如下:
Makefile
README.md
client
__init__.py
mcp_agent
__init__.py
agent.py
cli.py
deps.py
llm.py
run.py
tools.py
util
__init__.py
schema_to_params.py
pyproject.toml
requirements-dev.txt
requirements.txt
server
Dockerfile
README.md
index.ts
package-lock.jsonlock.json
package.json
test.ts
tsconfig.json
uv.lock
总结
我希望这有助于大家理解MCP的工作原理,以及如何利用它将AI代理与新功能集成。快去用那些免费可用的MCP服务器构建一些很酷的东西吧!