Gradio 代理与 MCP 黑客马拉松
获奖者Gradio 代理与 MCP 黑客马拉松
获奖者本指南将引导您通过 Gradio 实现模型上下文协议 (MCP) 客户端和服务器。您将构建一个 Gradio 聊天机器人,它使用 Anthropic 的 Claude API 响应用户消息,同时作为 MCP 客户端,它还能生成图像(通过连接到 MCP 服务器,该服务器是另一个 Gradio 应用)。
模型上下文协议 (MCP) 规范了应用程序向 LLM 提供上下文的方式。它允许 Claude 与外部工具(如图像生成器、文件系统或 API 等)进行交互。
首先,安装所需的包
pip install gradio anthropic mcp
在您的项目目录中创建一个 .env
文件,并添加您的 Anthropic API 密钥
ANTHROPIC_API_KEY=your_api_key_here
服务器提供 Claude 可以使用的工具。在此示例中,我们将创建一个通过 HuggingFace Spaces 生成图像的服务器。
创建一个名为 gradio_mcp_server.py
的文件
from mcp.server.fastmcp import FastMCP
import json
import sys
import io
import time
from gradio_client import Client
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
mcp = FastMCP("huggingface_spaces_image_display")
@mcp.tool()
async def generate_image(prompt: str, width: int = 512, height: int = 512) -> str:
"""Generate an image using SanaSprint model.
Args:
prompt: Text prompt describing the image to generate
width: Image width (default: 512)
height: Image height (default: 512)
"""
client = Client("https://ysharma-sanasprint.hf.space/")
try:
result = client.predict(
prompt,
"0.6B",
0,
True,
width,
height,
4.0,
2,
api_name="/infer"
)
if isinstance(result, list) and len(result) >= 1:
image_data = result[0]
if isinstance(image_data, dict) and "url" in image_data:
return json.dumps({
"type": "image",
"url": image_data["url"],
"message": f"Generated image for prompt: {prompt}"
})
return json.dumps({
"type": "error",
"message": "Failed to generate image"
})
except Exception as e:
return json.dumps({
"type": "error",
"message": f"Error generating image: {str(e)}"
})
if __name__ == "__main__":
mcp.run(transport='stdio')
generate_image
工具的 MCP 服务器。现在让我们创建一个 Gradio 聊天界面作为 MCP 客户端,将 Claude 连接到我们的 MCP 服务器。
创建一个名为 app.py
的文件
import asyncio
import os
import json
from typing import List, Dict, Any, Union
from contextlib import AsyncExitStack
import gradio as gr
from gradio.components.chatbot import ChatMessage
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
class MCPClientWrapper:
def __init__(self):
self.session = None
self.exit_stack = None
self.anthropic = Anthropic()
self.tools = []
def connect(self, server_path: str) -> str:
return loop.run_until_complete(self._connect(server_path))
async def _connect(self, server_path: str) -> str:
if self.exit_stack:
await self.exit_stack.aclose()
self.exit_stack = AsyncExitStack()
is_python = server_path.endswith('.py')
command = "python" if is_python else "node"
server_params = StdioServerParameters(
command=command,
args=[server_path],
env={"PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1"}
)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
await self.session.initialize()
response = await self.session.list_tools()
self.tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
} for tool in response.tools]
tool_names = [tool["name"] for tool in self.tools]
return f"Connected to MCP server. Available tools: {', '.join(tool_names)}"
def process_message(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]) -> tuple:
if not self.session:
return history + [
{"role": "user", "content": message},
{"role": "assistant", "content": "Please connect to an MCP server first."}
], gr.Textbox(value="")
new_messages = loop.run_until_complete(self._process_query(message, history))
return history + [{"role": "user", "content": message}] + new_messages, gr.Textbox(value="")
async def _process_query(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]):
claude_messages = []
for msg in history:
if isinstance(msg, ChatMessage):
role, content = msg.role, msg.content
else:
role, content = msg.get("role"), msg.get("content")
if role in ["user", "assistant", "system"]:
claude_messages.append({"role": role, "content": content})
claude_messages.append({"role": "user", "content": message})
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=claude_messages,
tools=self.tools
)
result_messages = []
for content in response.content:
if content.type == 'text':
result_messages.append({
"role": "assistant",
"content": content.text
})
elif content.type == 'tool_use':
tool_name = content.name
tool_args = content.input
result_messages.append({
"role": "assistant",
"content": f"I'll use the {tool_name} tool to help answer your question.",
"metadata": {
"title": f"Using tool: {tool_name}",
"log": f"Parameters: {json.dumps(tool_args, ensure_ascii=True)}",
"status": "pending",
"id": f"tool_call_{tool_name}"
}
})
result_messages.append({
"role": "assistant",
"content": "```json\n" + json.dumps(tool_args, indent=2, ensure_ascii=True) + "\n```",
"metadata": {
"parent_id": f"tool_call_{tool_name}",
"id": f"params_{tool_name}",
"title": "Tool Parameters"
}
})
result = await self.session.call_tool(tool_name, tool_args)
if result_messages and "metadata" in result_messages[-2]:
result_messages[-2]["metadata"]["status"] = "done"
result_messages.append({
"role": "assistant",
"content": "Here are the results from the tool:",
"metadata": {
"title": f"Tool Result for {tool_name}",
"status": "done",
"id": f"result_{tool_name}"
}
})
result_content = result.content
if isinstance(result_content, list):
result_content = "\n".join(str(item) for item in result_content)
try:
result_json = json.loads(result_content)
if isinstance(result_json, dict) and "type" in result_json:
if result_json["type"] == "image" and "url" in result_json:
result_messages.append({
"role": "assistant",
"content": {"path": result_json["url"], "alt_text": result_json.get("message", "Generated image")},
"metadata": {
"parent_id": f"result_{tool_name}",
"id": f"image_{tool_name}",
"title": "Generated Image"
}
})
else:
result_messages.append({
"role": "assistant",
"content": "```\n" + result_content + "\n```",
"metadata": {
"parent_id": f"result_{tool_name}",
"id": f"raw_result_{tool_name}",
"title": "Raw Output"
}
})
except:
result_messages.append({
"role": "assistant",
"content": "```\n" + result_content + "\n```",
"metadata": {
"parent_id": f"result_{tool_name}",
"id": f"raw_result_{tool_name}",
"title": "Raw Output"
}
})
claude_messages.append({"role": "user", "content": f"Tool result for {tool_name}: {result_content}"})
next_response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=claude_messages,
)
if next_response.content and next_response.content[0].type == 'text':
result_messages.append({
"role": "assistant",
"content": next_response.content[0].text
})
return result_messages
client = MCPClientWrapper()
def gradio_interface():
with gr.Blocks(title="MCP Weather Client") as demo:
gr.Markdown("# MCP Weather Assistant")
gr.Markdown("Connect to your MCP weather server and chat with the assistant")
with gr.Row(equal_height=True):
with gr.Column(scale=4):
server_path = gr.Textbox(
label="Server Script Path",
placeholder="Enter path to server script (e.g., weather.py)",
value="gradio_mcp_server.py"
)
with gr.Column(scale=1):
connect_btn = gr.Button("Connect")
status = gr.Textbox(label="Connection Status", interactive=False)
chatbot = gr.Chatbot(
value=[],
height=500,
type="messages",
show_copy_button=True,
avatar_images=("👤", "🤖")
)
with gr.Row(equal_height=True):
msg = gr.Textbox(
label="Your Question",
placeholder="Ask about weather or alerts (e.g., What's the weather in New York?)",
scale=4
)
clear_btn = gr.Button("Clear Chat", scale=1)
connect_btn.click(client.connect, inputs=server_path, outputs=status)
msg.submit(client.process_message, [msg, chatbot], [chatbot, msg])
clear_btn.click(lambda: [], None, chatbot)
return demo
if __name__ == "__main__":
if not os.getenv("ANTHROPIC_API_KEY"):
print("Warning: ANTHROPIC_API_KEY not found in environment. Please set it in your .env file.")
interface = gradio_interface()
interface.launch(debug=True)
要运行您的 MCP 应用程序
python app.py
gradio_mcp_server.py
。现在您可以与 Claude 聊天,它将能够根据您的描述生成图像。
尝试以下提示:
Claude 将识别这些为图像生成请求,并自动使用您的 MCP 服务器中的 generate_image
工具。
以下是聊天会话期间发生的高级流程
generate_image
工具现在您已经有了一个可运行的 MCP 系统,以下是一些扩展它的想法:
恭喜!您已成功构建了一个 MCP 客户端和服务器,允许 Claude 根据文本提示生成图像。这仅仅是您可以使用 Gradio 和 MCP 实现的开始。本指南使您能够构建复杂的 AI 应用程序,这些应用程序可以使用 Claude 或任何其他强大的 LLM 与几乎任何外部工具或服务进行交互。
阅读我们关于使用Gradio 应用作为 MCP 服务器的其他指南。