MCP案例—客户端和服务端

MCP简介

Model Context Protocol (模型上下文协议),简称MCP,MCP是一种协议,用于LLM与外部拓展资源交互的协议。

想了解具体细节可参考作者本篇文章MCP理论指南

准备

本篇文章将带你通过python创建MCP客户端及服务端,并连接到本地模型

  • 本地模型,我这里使用ollama来管理本地模型,模型应该是有function calling功能的模型,比如千问、deepseek-v3等,本篇教程使用的是符合openai风格的模型
  • 查询天气的网站,这里我们使用OpenWeather,这个需要注册,注册完成后申请一个api-key,测试是免费使用,有次数限制
  • MCP官方推荐使用uv进行包管理,感兴趣可以使用,本篇使用的pip包管理工具
  • .env文件,用于配置一些基础信息
  • 创建client.py、server.py文件,分别当作客户端和服务端,注意.env、client.py、server.py这三个文件应该在同一目录下
  • 启动命令我就先写在这个地方
    # 这个启动命令,也就是启动client的时候把server.py当作参数传入,从而连接到服务端
    python client.py server.py
    

依赖包安装

本篇使用的是pip来管理包,所以安装工具也是pip

  • 安装mcp环境
    pip install mcp
    
  • 安装openai
      pip install openai
    
  • 安装anthropic
    pip install openai
    
  • 安装dotenv
    pip install dotenv
    
  • 安装httpx
    pip install httpx
    

.env文件

env文件主要是配置模型的路径,使用的模型及api-key

BASE_URL=http://127.0.0.1:11434/v1 #模型调用路径
MODEL=qwen2.5:72b #模型名称,这个用于表示你要使用哪个模型
API_KEY=ollama #这里api-key是模型要使用的,如果你使用模型厂商的开放接口,就需要填入相应的api-key,如果是本地模型,就可以随意填写

MCP客户端示例

MCP客户端主要是负责与服务端和模型进行通信的中间层,是比较重要的一层,下面这个示例展示了构建一个聊天,连接MCP服务端以及与模型进行交互的过程

import asyncio
import json
import os
from typing import Optional
from contextlib import AsyncExitStack

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from openai import OpenAI

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv()


class MCPClient:
    def __init__(self):
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()
        self.anthropic = Anthropic()
        self.api_key = os.getenv("API_KEY")  # 读取 OpenAI API Key
        self.base_url = os.getenv("BASE_URL")  # 读取 BASE YRL
        self.model = os.getenv("MODEL")  # 读取 model

        self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)


    async def connect_to_server(self, server_script_path: str):
        """连接MCP服务端

        Args:
            server_script_path: Path to the server script (.py or .js)
        """
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')
        if not (is_python or is_js):
            raise ValueError("Server script must be a .py or .js file")

        command = "python" if is_python else "node"
        server_params = StdioServerParameters(
            command=command,
            args=[server_script_path],
            env=None
        )

        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()

        # List available tools
        response = await self.session.list_tools()
        tools = response.tools
        print("\nConnected to server with tools:", [tool.name for tool in tools])

    async def process_query(self, query: str) -> str:
        """Process a query using Claude and available tools"""
        messages = [
            {
                "role": "user",
                "content": query
            }
        ]

        # 获取可用工具
        response = await self.session.list_tools()
        available_tools = [{
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "input_schema": tool.inputSchema
            }
        } for tool in response.tools]

        print("正在等待模型回答...")
        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            tools=available_tools,
            temperature=0.7,
            max_tokens=1000,
            stream=False
        )
        print(response.choices[0])

        # 处理返回的内容
        content = response.choices[0]
        if content.finish_reason == "tool_calls":
            # 如何是需要使用工具,就解析工具
            tool_call = content.message.tool_calls[0]
            tool_name = tool_call.function.name
            tool_args = json.loads(tool_call.function.arguments)

            # 执行工具
            result = await self.session.call_tool(tool_name, tool_args)
            print(f"\n\n[Calling tool {tool_name} with args {tool_args}]\n\n")
            print("工具执行结果:",result)

            # 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入messages中
            messages.append(content.message.model_dump())
            messages.append({
                "role": "tool",
                "content": result.content[0].text,
                "tool_call_id": tool_call.id,
            })

            # 将上面的结果再返回给大模型用于生产最终的结果
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
            )
            return response.choices[0].message.content

        return content.message.content

    async def chat_loop(self):
        print("\nMCP Client Started!")
        print("Type your queries or 'quit' to exit.")

        while True:
            try:
                query = input("\n我: ").strip()
                if query.lower() == 'quit':
                    break

                response = await self.process_query(query)
                print("\n🤖 qwen" + response)

            except Exception as e:
                print(f"\nError: {str(e)}")

    async def cleanup(self):
        """Clean up resources"""
        await self.exit_stack.aclose()


async def main():
    if len(sys.argv) < 2:
        print("Usage: python client.py <path_to_server_script>")
        sys.exit(1)

    print(sys.argv)
    client = MCPClient()
    try:
        await client.connect_to_server(sys.argv[1])
        await client.chat_loop()
    finally:
        await client.cleanup()


if __name__ == "__main__":
    import sys
    asyncio.run(main())

MCP服务端

MCP服务端,主要是用于定义你的工具、资源等

import json
import httpx
from typing import Any
from mcp.server.fastmcp import FastMCP

# 初始化 MCP 服务器
mcp = FastMCP("WeatherServer")

# OpenWeather API 配置
OPENWEATHER_API_BASE = "https://api.openweathermap.org/data/2.5/weather"
API_KEY = "替换为你自己的apikey"# 请替换为你自己的 OpenWeather API Key
USER_AGENT = "weather-app/1.0"

async def fetch_weather(city: str) -> dict[str, Any] | None:
    """
    从 OpenWeather API 获取天气信息。
    :param city: 城市名称(需使用英文,如 Beijing)
    :return: 天气数据字典;若出错返回包含 error 信息的字典
    """
    params = {
        "q": city,
        "appid": API_KEY,
        "units": "metric",
        "lang": "zh_cn"
    }
    headers = {"User-Agent": USER_AGENT}

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(OPENWEATHER_API_BASE, params=params, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()  # 返回字典类型
        except httpx.HTTPStatusError as e:
            return {"error": f"HTTP 错误: {e.response.status_code}"}
        except Exception as e:
            return {"error": f"请求失败: {str(e)}"}

def format_weather(data: dict[str, Any] | str) -> str:
    """
    将天气数据格式化为易读文本。
    :param data: 天气数据(可以是字典或 JSON 字符串)
    :return: 格式化后的天气信息字符串
    """
    # 如果传入的是字符串,则先转换为字典
    if isinstance(data, str):
        try:
            data = json.loads(data)
        except Exception as e:
            return f"无法解析天气数据: {e}"

    # 如果数据中包含错误信息,直接返回错误提示
    if "error" in data:
        return f"⚠️ {data['error']}"

    # 提取数据时做容错处理
    city = data.get("name", "未知")
    country = data.get("sys", {}).get("country", "未知")
    temp = data.get("main", {}).get("temp", "N/A")
    humidity = data.get("main", {}).get("humidity", "N/A")
    wind_speed = data.get("wind", {}).get("speed", "N/A")
    # weather 可能为空列表,因此用 [0] 前先提供默认字典
    weather_list = data.get("weather", [{}])
    description = weather_list[0].get("description", "未知")

    return (
        f"🌍 {city}, {country}\n"
        f"🌡 温度: {temp}°C\n"
        f"💧 湿度: {humidity}%\n"
        f"🌬 风速: {wind_speed} m/s\n"
        f"🌤 天气: {description}\n"
    )

@mcp.tool()
async def query_weather(city: str) -> str:
    """
    输入指定城市的英文名称,返回今日天气查询结果。
    :param city: 城市名称(需使用英文)
    :return: 格式化后的天气信息
    """
    data = await fetch_weather(city)
    return format_weather(data)

if __name__ == "__main__":
    # 以标准 I/O 方式运行 MCP 服务器
    mcp.run(transport='stdio')

启动服务

启动命令

 python client.py server.py

执行后应该会出现下面页面表示成功
在这里插入图片描述
我们下面问一个问题,测试一下
在这里插入图片描述
在这张图中,问题为北京今天天气怎么样,可以看到,我们把工具及问题传给模型后,模型给我们返回的是要使用工具,并把参数传递给了我们,我们只需要把参数以及工具通过客户端调用服务端,就可以拿到天气api给我们的天气情况,然后把结果及问题一并给模型处理生成最终的回答,这就是MCP调用的一次流程。

<think>好的,我现在需要帮用户找到关于MCP Server如何处理Cursor的案例或示例。用户提到了MCP协议的结构,包括Host、Client、Server,以及本地远程资源。首先,我应该先回顾一下用户提供的引用内容,确保理解MCP的基本架构。 根据引用[^1],MCP Server是用于暴露特殊能力的轻应用,可以访问本地资源或远程服务。Cursor通常与数据库查询中的结果集处理有关,所以可能涉及到MCP Server如何通过游标分批次获取数据。 接下来,我需要考虑用户可能需要的是如何在MCP Server中实现类似数据库游标的功能,或者处理大量数据时如何分页或流式传输。可能的实现方式包括: 1. **分页查询机制**:使用offsetlimit参数,每次请求返回部分数据,并记录当前位置。 2. **服务端游标保持**:在Server保存游标状态,客户端通过token来获取后续数据。 3. **流式传输**:通过HTTP分块传输或WebSocket实时推送数据。 用户可能需要具体的代码示例,比如如何定义MCP协议的消息格式,如何在Server维护游标状态,以及Client如何发起请求接收数据。例如,一个可能的实现是使用类似GraphQL的游标分页,或者REST API中的分页参数。 另外,需要检查是否有现成的案例或文档提到MCP Server处理游标的情况。如果引用中没有直接提到,可能需要基于MCP的结构推测实现方式,并给出示例代码的结构,如定义CursorRequestCursorResponse的消息格式,使用gRPC或WebSocket进行通信。 最后,确保回答符合用户的要求,使用中文,结构清晰,并生成相关问题。同时,注意LaTeX格式的正确使用,如变量用$...$包裹,独立公式用$$...$$,但在这个问题中可能涉及较少数学公式,更多是代码协议说明。</think>根据MCP协议架构特点,处理游标(cursor)通常涉及分页数据获取或持续数据流控制。以下是基于MCP Server的游标实现方案: ### 1. 分页查询模式 ```python # MCP Server实现 cursor_registry = {} class CursorRequest(MCPMessage): query: str page_size: int = 20 cursor_token: Optional[str] = None class CursorResponse(MCPMessage): items: list next_cursor: Optional[str] def handle_cursor_request(req: CursorRequest): if req.cursor_token: query_state = cursor_registry.get(req.cursor_token) results = query_state.fetch_next(req.page_size) else: new_cursor = generate_token() query_state = init_query(req.query) # 初始化数据库查询 results = query_state.fetch_first(req.page_size) cursor_registry[new_cursor] = query_state return CursorResponse( items=results, next_cursor=new_cursor if results else None ) ``` 该模式通过$cursor_token$保持查询状态,适用于数据库分页场景。 ### 2. 流式传输模式 ```javascript // MCP Client连接示例 const cursorStream = new WebSocket('mcp://server/cursor-stream'); cursorStream.onmessage = (event) => { const data = JSON.parse(event.data); if(data.type === 'DATA_CHUNK'){ processChunk(data.payload); } if(data.type === 'CURSOR_UPDATE'){ storeCursor(data.cursor); } }; // 发送初始化请求 cursorStream.send(JSON.stringify({ query: "SELECT * FROM large_dataset", chunk_size: 1000 })); ``` 此模式利用WebSocket实现实时数据流传输,游标状态通过心跳机制维护。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值