Gradio 代理与 MCP 黑客马拉松

获奖者
Gradio logo
  1. 其他教程
  2. 使用 Gradio 构建 MCP 客户端

将 Gradio 聊天机器人用作 MCP 客户端

本指南将引导您通过 Gradio 实现模型上下文协议 (MCP) 客户端和服务器。您将构建一个 Gradio 聊天机器人,它使用 Anthropic 的 Claude API 响应用户消息,同时作为 MCP 客户端,它还能生成图像(通过连接到 MCP 服务器,该服务器是另一个 Gradio 应用)。

什么是 MCP?

模型上下文协议 (MCP) 规范了应用程序向 LLM 提供上下文的方式。它允许 Claude 与外部工具(如图像生成器、文件系统或 API 等)进行交互。

先决条件

  • Python 3.10+
  • Anthropic API 密钥
  • 对 Python 编程有基本了解

设置

首先,安装所需的包

pip install gradio anthropic mcp

在您的项目目录中创建一个 .env 文件,并添加您的 Anthropic API 密钥

ANTHROPIC_API_KEY=your_api_key_here

第一部分:构建 MCP 服务器

服务器提供 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')

此服务器的作用:

  1. 它创建一个公开 generate_image 工具的 MCP 服务器。
  2. 该工具连接到 HuggingFace Spaces 上托管的 SanaSprint 模型。
  3. 它通过轮询结果来处理图像生成的异步特性。
  4. 当图像准备就绪时,它以结构化 JSON 格式返回 URL。

第二部分:使用 Gradio 构建 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 客户端的作用:

  • 创建用户友好的 Gradio 聊天界面
  • 连接到您指定的 MCP 服务器
  • 处理对话历史和消息格式化
  • 使用工具定义调用 Claude API
  • 处理来自 Claude 的工具使用请求
  • 在聊天中显示图像和其他工具输出
  • 将工具结果发送回 Claude 进行解释

运行应用程序

要运行您的 MCP 应用程序

  • 启动终端窗口并运行 MCP 客户端
    python app.py
  • 打开所示 URL 处的 Gradio 界面(通常为 http://127.0.0.1:7860
  • 在 Gradio 界面中,您会看到一个用于 MCP 服务器路径的字段。它应该默认为 gradio_mcp_server.py
  • 点击“连接”以建立与 MCP 服务器的连接。
  • 您应该看到一条消息,指示服务器连接成功。

示例用法

现在您可以与 Claude 聊天,它将能够根据您的描述生成图像。

尝试以下提示:

  • “你能生成一张日落山景的图片吗?”
  • “创建一张酷酷的虎斑猫图片”
  • “生成一张戴墨镜的熊猫图片”

Claude 将识别这些为图像生成请求,并自动使用您的 MCP 服务器中的 generate_image 工具。

工作原理

以下是聊天会话期间发生的高级流程

  1. 您的提示进入 Gradio 界面
  2. 客户端将您的提示转发给 Claude
  3. Claude 分析提示并决定使用 generate_image 工具
  4. 客户端将工具调用发送到 MCP 服务器
  5. 服务器调用外部图像生成 API
  6. 图像 URL 返回给客户端
  7. 客户端将图像 URL 发送回 Claude
  8. Claude 提供包含生成的图像的回复
  9. Gradio 聊天界面显示 Claude 的回复和图像

后续步骤

现在您已经有了一个可运行的 MCP 系统,以下是一些扩展它的想法:

  • 向您的服务器添加更多工具
  • 改进错误处理
  • 添加带有认证的私有 Huggingface Spaces 以进行安全工具访问
  • 创建连接到您自己的 API 或服务的自定义工具
  • 实现流式响应以改善用户体验

结论

恭喜!您已成功构建了一个 MCP 客户端和服务器,允许 Claude 根据文本提示生成图像。这仅仅是您可以使用 Gradio 和 MCP 实现的开始。本指南使您能够构建复杂的 AI 应用程序,这些应用程序可以使用 Claude 或任何其他强大的 LLM 与几乎任何外部工具或服务进行交互。

阅读我们关于使用Gradio 应用作为 MCP 服务器的其他指南。