Gradio全解14——使用Gradio构建MCP的客户端与服务器(1)——使用Gradio构建MCP客户端
前言
本系列文章主要介绍WEB界面工具Gradio。Gradio是Hugging Face发布的简易WebUI开发框架,它基于FastAPI和svelte,可以使用机器学习模型、python函数或API开发多功能界面,并可部署人工智能模型,是当前非常热门且易于展示机器学习大语言模型LLM及扩散模型DM的WebUI框架。
本系列文章分为四部分:Gradio介绍、Gradio基础功能实战、Gradio高级功能实战和Gradio与其它应用结合。第一部分Gradio介绍,方便读者对Gradio整体把握,包括三章内容:第一章先介绍Gradio的概念,包括详细技术架构、历史、应用场景、与其他框架Gradio/NiceGui/StreamLit/Dash/PyWebIO的区别,然后详细讲述Gradio的安装与运行,安装包括Linux/Win/Mac三类系统安装,运行包括普通方式和热重载方式;第二章介绍Gradio的4种部署方式,包括本地部署launch()、huggingface托管、FastAPI挂载和Gradio-Lite浏览器集成;第三章介绍Gradio的三种客户端(Client),包括python客户端、javascript客户端和curl客户端。第二部分实战Gradio基础功能,进入本系列文章的核心,包括四章内容:第四章讲解Gradio库的模块架构和环境变量,第五章讲解Gradio高级抽象界面类Interface,第六章讲解Gradio底层区块类Blocks,第七章讲解补充特性Additional Features。第三部分讲解并实战Gradio的高级功能,包括四章内容:第八章讲解融合大模型的多模态聊天机器人组件Chatbot,第九章讲解Gradio Tools工具库的使用及构建方法,第十章讲述讲述数据科学与绘图Data Science And Plots;第十一章讲述流式传输Streaming的多模态应用。第四部分讲解Gradio与其它应用结合,包括三章内容:第十二章讲述由Gradio App创建Discord Bot/Slack Bot/Website Widget;第十三章讲述MCP协议详解;第十四章使用Gradio构建MCP的客户端与服务器。
本系列文章讲解细致,涵盖Gradio及相关框架的大部分组件和功能,代码均可运行并附有大量运行截图,方便读者理解并应用到开发中,Gradio一定会成为每个技术人员实现各种奇思妙想的最称手工具。
本系列文章目录如下:
- 《Gradio全解1——Gradio简介:大模型WebUI框架(上)》
- 《Gradio全解1——Gradio简介:大模型WebUI框架(下)》
- 《Gradio全解2——Gradio的四种部署方式(上)》
- 《Gradio全解2——Gradio的四种部署方式(下)》
- 《Gradio全解3——Gradio三种客户端:python、javascript与curl(一)——python》
- 《Gradio全解3——Gradio三种客户端:python、javascript与curl(二)——javascript》
- 《Gradio全解3——Gradio三种客户端:python、javascript与curl(三)——curl》
- 《Gradio全解4——Gradio库的模块架构和环境变量》
- 《Gradio全解5——Interface:高级抽象界面类(上)》
- 《Gradio全解5——Interface:高级抽象界面类(下)》
- 《Gradio全解6——Blocks:底层区块类(上)》
- 《Gradio全解6——Blocks:底层区块类(下)》
- 《Gradio全解7——Additional Features:补充特性(上)》
- 《Gradio全解7——Additional Features:补充特性(下)》
- 《Gradio全解8——Chatbot:融合大模型的多模态聊天机器人》
- 《Gradio全解9——Gradio Tools:工具库》
- 《Gradio全解10——Data Science And Plots:数据科学与绘图》
- 《Gradio全解11——Streaming:流式传输的多模态应用》
- 《Gradio全解12——由Gradio App创建Discord Bot/Slack Bot/Website Widget》
- 《Gradio全解13——MCP协议详解》
- 《Gradio全解14——使用Gradio构建MCP的客户端与服务器》
第14章 使用Gradio构建MCP的客户端与服务器
本章将使用使用Gradio构建多种形式的MCP客户端与服务器。首先,使用FastMCP构建服务器,使用ChatBot构建MCP客户端。然后,讲解生成MCP服务器的其他方案,包括:利用Gradio参数直接生成MCP服务器、转换现有Space项目以及自定义MCP服务器方案。最后,用一个文档式MCP服务器讲解其它配置。
14.1 使用Gradio构建MCP客户端
本节将基于Gradio实现MCP(Model Context Protocol,模型上下文协议)客户端与服务器。我们将使用Anthropic的大模型接口Claude API构建一个Gradio Chatbot,它既可以回复用户消息,也可以作为MCP客户端,通过连接独立的MCP服务器生成图像。注意:其中MCP服务器集成图像生成工具,也可以作为一个独立的Gradio应用。
14.1.1 构建FastMCP服务器
在本示例中,将通过MCP服务器程序FastMCP,创建一个绑定图像生成工具generate_image的MCP服务器,它将为Claude提供可调用的工具集。工具generate_image中通过Grdio客户端gradio_client,连接托管在Hugging Face Spaces生成图像的Gradio应用SanaSprint。
1. 前置条件
学习本章内容,首先必须具备Python基础编程能力,安装3.10或更高版本的Python。然后申请Anthropic API密钥或替换成其它多模态模型,最后进行环境配置:
- 安装必要的依赖包:
pip install gradio anthropic mcp
- 在项目目录下创建.env文件,并添加Anthropic API密钥:
ANTHROPIC_API_KEY=your_api_key_here
2. 服务器代码
首先,创建文件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的FastMCP类型的服务端;
- 定义generate_image函数,内部客户端连接托管在HuggingFace Spaces的SanaSprint模型;
- 函数中,通过轮询机制处理异步生成的图像;
- 当图像生成完成后,以结构化JSON格式返回图片URL。
最后启动mcp服务器,传输协议为stdio,等待客户端连接。
14.1.2 使用ChatBot构建MCP客户端
现在我们创建一个Gradio聊天界面作为MCP客户端,用于将Claude连接到MCP服务端;然后处理用户消息,直接回复或生成图像后在页面显示。
1. 客户端代码
创建一个名为app.py的文件,首先看一下MCP客户端封装器代码,如下:
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': # use tool
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()
该MCP客户端功能说明如下:
- 创建友好的Gradio聊天界面实现用户交互;
- 连接至指定的MCP服务端;
- 管理历史对话记录与消息格式化;
- 根据定义的工具Anthropic调用Claude API;
- 处理Claude发出的使用工具请求;
- 将工具执行结果返回Claude进行解读,并将回复结果添加到结果消息列表。
下面是定义Gradio界面的代码:
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", scale=4
placeholder="Ask about weather or alerts (e.g., What's the weather in New York?)",
)
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)
在聊天界面展示消息、图像及其他工具输出。以下是聊天会话的核心流程:
- 用户指令prompt通过Gradio界面传入。
- 客户端将指令转发至Claude。
- Claude分析指令后决定调用generate_image工具。
- 客户端向MCP服务端发送工具调用请求。
- 服务端调用外部图像生成SanaSprint API。
- 生成图像的URL被返回至客户端。
- 客户端将图像URL回传Claude。
- Claude生成包含图像URL或相关引用的回复。
- Gradio聊天界面同步显示Claude回复与生成图像。
下面我们将运行整个应用程序,并给出扩展建议。
2. 运行应用程序及扩展
运行应用程序步骤如下:
- 启动终端运行MCP客户端:
python app.py
。 - 在浏览器打开上一步输出的URL以显示Gradio界面(通常为http://127.0.0.1:7860)。
- 在Gradio界面中,可见MCP服务端路径输入框(默认指向gradio_mcp_server.py)。
- 点击"Connect"按钮建立与MCP服务端的连接。
成功连接后将显示服务端连接成功提示。现在我们可与Claude对话,它将根据描述生成图像。尝试输入以下指令:
- “Can you generate an image of a mountain landscape at sunset?”
- “Create an image of a cool tabby cat”
- “Generate a picture of a panda wearing sunglasses”
Claude将自动识别这些图像生成请求,并调用MCP服务器的generate_image工具。现已搭建可运行的MCP系统,后续该如何扩展呢?建议如下:
- 为服务端添加更多工具模块;
- 完善错误处理机制;
- 将应用及其客户端部署在需身份验证的私有Huggingface Spaces,实现安全访问工具;
- 开发连接自有API或服务的定制化服务端工具;
- 采用流式响应提升用户体验。
现在已成功构建由Claude根据文本生成图像的MCP客户端与服务端系统,但这仅是Gradio与MCP联合应用的起点——本指南为用户奠定了开发复杂AI应用的基础,未来可结合Claude或其他大语言模型实现与外部任意工具或服务的交互。为了加深学习,下面进入另一篇指南:将Gradio应用作为MCP服务端使用。