MCP服务介绍与实现

MCP(Model Context Protocol,模型上下文协议)是Anthropic公司于2024年11月提出的开放标准协议,旨在解决AI系统与数据源、工具及物理设备间的互联难题,被称为​"AI领域的USB-C接口"​。

MCP可以帮助我们在LLM之上构建智能体和复杂的工作流,通过MCP可以提供给LLM可用的工具的列表以及能力信息,并且提供了标准化的接口。

MCP采用了客户端-服务器端的架构设计,一个Host可以连接多个MCP Server,MCP的架构如下:

在上图中

  • MCP Hosts: 如 Claude Desktop、IDE 或 AI 工具,希望通过 MCP 访问数据的程序
  • MCP Clients: 维护与服务器一对一连接的协议客户端
  • MCP Servers: 轻量级程序,通过标准的 Model Context Protocol 提供特定能力
  • 本地数据源: MCP 服务器可安全访问的计算机文件、数据库和服务
  • 远程服务: MCP 服务器可连接的互联网上的外部系统(如通过 APIs)

​MCP服务器

下面开始动手实践,编写一个简单的MCP服务器,提供查询城市天气预报的能力。按照官网的介绍,可以采用uv来创建虚拟环境和python项目,运行以下命令

pip install uv
uv init mcp-server-demo
uv add "mcp[cli]" httpx openai python-dotenv

另外还需要安装node,我是Ubuntu 18的环境,在安装最新版本的Node提示GLIBC版本不是2.28,因此还需要升级GLIBC,运行以下命令来添加软件源和安装libc6

deb http://security.debian.org/debian-security buster/updates main
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 112695A0E562B32A 54404762BBB6E853
sudo apt install libc6-dev libc6

为了提供天气预报的能力,我使用了高德天气提供的API,需要在高德地图的官网上基础 API 文档-开发指南-Web服务 API | 高德地图API进行注册,拿到API key。高德天气API需要传入地区的adcode来进行天气查询,例如广州市的编码是440100,这个信息可以在相关下载-Web服务 API | 高德地图API进行下载。

在mcp-server-demo目录下新建一个server.py文件,这个mcp server可以提供查询天气预报的工具能力,代码如下:

# server.py
import httpx
import json
from mcp.server.fastmcp import FastMCP

# Create an MCP server
mcp = FastMCP("Weather")

API_BASE = "https://restapi.amap.com/v3/weather/weatherInfo"

city_code_mapping = {}

async def make_weather_request(city: str) -> dict[str, any] | None:
    params = {
        "key": "XXXXX",
        "city": city,
        "extensions": "all"
    }

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(API_BASE, params=params, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception:
            return None

@mcp.tool()
async def get_forecast(city: str) -> str:
    """获取城市的天气预报

    Args:
        city: 城市名称
    """
    try:
        city_code = city_code_mapping[city]
    except KeyError:
        return "无法获取该城市的天气预报"

    data = await make_weather_request(city_code)

    if not data:
        return "无法获取天气预报"
    
    forecast_data= [city + '的天气预报为:']
    for item in data['forecasts'][0]['casts']:
        forecast_data.append(f"日期: {item['date']}, 白天天气: {item['dayweather']}, 夜晚天气: {item['nightweather']}, 白天气温: {item['daytemp']}, 夜晚气温: {item['nighttemp']}")

    return "\n".join(forecast_data)
    
if __name__ == "__main__":
    with open("city_code.json", "r", encoding="utf-8") as f:
        city_code_mapping = json.load(f)
    # Initialize and run the server
    mcp.run(transport='stdio')

 在以上代码中,@mcp.tool注解表示这个函数是一个MCP工具,MCP server会把这个注解的函数的描述暴露出去,这样子LLM就可以了解工具接受到的参数以及用途。

另外这个mcp server是以stdio方式运行,即在本地以进程通讯的方式,通过标准输入输出方式实现server与client的通讯。另外一种运行方式是sse,基于HTTP协议,通过Server-Sent-Events提供单向实时推送。当前先以stdio方式来进行测试。

MCP客户端

接着我们可以构建一个MCP客户端,在这个客户端里面我将调用火山引擎提供的DeepSeek V3大模型,通过提供MCP工具的信息给大模型,对用户的提问进行分析看是否要调用工具来完成回复。注意在大模型选择上要选择支持工具调用的大模型,例如DeepSeek V3, Qwen QwQ之类的,DeepSeek R1不是原生支持工具调用的,因为在训练时没有引入工具调用相关的结构化数据。新建一个名为client.py的文件,以下是代码:

import asyncio
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 dotenv import load_dotenv
import os

import json

load_dotenv()  # load environment variables from .env

class MCPClient:
    def __init__(self):
        # Initialize session and client objects
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()
        self.openai = OpenAI(api_key = os.getenv("DEEPSEEK_KEY"), base_url = "https://ark.cn-beijing.volces.com/api/v3")
        self.model = "deepseek-v3-250324"

    async def connect_to_server(self, server_script_path: str):
        """Connect to an MCP server
        
        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]

        response = self.openai.chat.completions.create(
            model = self.model,  # your model endpoint ID
            messages = messages,
            tools = available_tools
        )

        content = response.choices[0]
        print(content)
        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")

            # 将结果存入消息历史
            messages.append(content.message.model_dump())
            messages.append({
                "role": "tool",
                "content": result.content[0].text,
                "tool_call_id": tool_call.id,
            })

            # 将结果返回给大模型生成最终响应
            response = self.openai.chat.completions.create(
                model = self.model,
                messages = messages,
                temperature=0.6
            )
            return response.choices[0].message.content

        return response.choices[0].message.content


    async def chat_loop(self):
        """Run an interactive chat loop"""
        print("\nMCP Client Started!")
        print("Type your queries or 'quit' to exit.")
        
        while True:
            try:
                query = input("\nQuery: ").strip()
                
                if query.lower() == 'quit':
                    break
                    
                response = await self.process_query(query)
                print("\n" + 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)
        
    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())

在以上的代码中,在connect_to_server函数中, 启动mcp服务器脚本,获取服务器提供的工具列表,包含工具的描述和参数信息。在chat_loop函数中,当与大模型对话时,提供工具列表信息给大模型,这样大模型就可以根据用户的问题选择是否使用工具来提供辅助信息,如果需要调用工具,通过mcpclientsession进行工具调用,然后把结果附加到历史对话消息中,大模型就会根据工具提供的信息来进行回复。

运行效果

通过以下命令启动

python client.py server.py

然后在Query中输入问题,例如我询问广州市未来的天气预报

     从以上截图可以看到,大模型根据用户的问题判断需要调用预测天气的工具,从打印出的消息finish_reason='tool_calls', tool_calls=[ChatCompletionMessageToolCall(id='call_k18tfi38hscct2m7p9cnqsus', function=Function(arguments='{"city":"广州市"}', name='get_forecast'), type='function')])),可以看到大模型正确的判断了为了回复用户的问题,需要调用get_forecast这个mcp工具,并传入参数city=广州市

    在之后的Info中也可以看到成功调用了高德天气的API并返回了结果。最后大模型根据工具返回的结果正确的给出了未来几天的天气预报信息。

    <think>好的,我现在需要回答用户关于MCP服务器的介绍和部署教程的问题。首先,我需要回顾用户提供的引用内容,确保信息准确且符合要求。 根据引用[2],MCP服务器是轻量级服务,连接数据源或工具,如数据库、文件系统、第三方API等。每个服务器专注于特定资源或功能,使用JSON-RPC或gRPC协议。这解释了MCP服务器的基本架构和作用。 引用[3]提到MCP服务器如何Istio集成,将第三方注册表的服务转换为ServiceEntry和WorkloadEntry资源,通过MCP协议提供给Pilot。这说明MCP服务器在服务网格中的应用场景,特别是在Istio中的角色。 引用[4]给出了一个具体例子,用户需要编写MCP服务器集成SCP命令,实现文件上传功能。这展示了如何实际部署和配置MCP服务器,结合特定工具完成自动化任务。 接下来,整理MCP服务器的核心功能:协议转换、资源连接、标准化接口。部署步骤需要包括环境准备、编写服务器逻辑、协议配置、服务注册和启动测试。同时,需要结合用户提供的例子,比如SCP文件上传,来说明具体实现。 要注意用户可能的需求,比如如何开始搭建、如何集成现有工具,以及可能的优化方法。需要确保步骤清晰,引用正确,并生成相关问题帮助用户深入理解。</think>### MCP 服务介绍 MCP(Managed Configuration Protocol)服务器是一种轻量级中间服务,主要用于连接客户端工具外部资源(如数据库、API、文件系统等),其核心功能是通过标准化协议实现资源交互和自动化操作[^2]。以下是其关键特性: 1. **协议转换** 将不同数据源的通信协议统一为JSON-RPC或gRPC等标准格式,例如将本地文件操作转换为SCP协议传输[^4]; 2. **资源连接** 每个MCP服务器专注于特定资源类型(如示例中的SCP文件传输服务),通过模块化设计支持快速扩展; 3. **标准化接口** 提供声明式API接口,例如通过关键词“上传微信目录”触发自动化流程[^4]。 ### MCP 服务器部署教程(以文件传输为例) #### 步骤1:环境准备 安装必要依赖(Python示例): ```bash pip install json-rpc scp ``` 这会包含基础通信协议库和SCP客户端工具[^1]。 #### 步骤2:编写服务器逻辑 ```python from jsonrpcserver import method, serve import paramiko @method def upload_wechat_dir(): transport = paramiko.Transport(("47.93.xx.xx", 22)) transport.connect(username="root", password="") sftp = paramiko.SFTPClient.from_transport(transport) sftp.put_r("/Users/syw/project/wechatAr", "/opt/ll") return {"status": "success"} ``` 此代码实现了通过SFTP协议上传文件夹的RPC方法[^4]。 #### 步骤3:配置协议层 创建协议配置文件`mcp_config.yaml`: ```yaml protocol: json-rpc endpoints: - name: file_transfer trigger_keyword: "上传微信目录" method: upload_wechat_dir resource_type: scp ``` #### 步骤4:服务注册启动 ```bash python3 -m jsonrpcserver serve mcp_config.yaml --port 8080 ``` 此时服务器将监听8080端口等待MCP主机的连接请求。 #### 步骤5:客户端测试 通过curl验证服务: ```bash curl -X POST http://localhost:8080 -d '{"jsonrpc":"2.0","method":"upload_wechat_dir","id":1}' ``` ### 典型应用场景 1. **服务网格集成** 将第三方注册中心的服务信息转换为Istio可识别的ServiceEntry资源[^3]; 2. **自动化运维** 通过关键词触发服务器维护任务(如日志清理、备份等); 3. **跨平台数据同步** 实现不同云平台间的文件自动同步。
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包

    打赏作者

    gzroy

    你的鼓励将是我创作的最大动力

    ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
    扫码支付:¥1
    获取中
    扫码支付

    您的余额不足,请更换扫码支付或充值

    打赏作者

    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

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

    余额充值