MCP实战-基于Ollama+qwen2.5以stdio方式实现MCP协议工具调用

0 环境准备

  • ollama已部署推理模型qwen:7b(deepseek目前不支持function calling)
  • 已安装miniconda环境
  • 具备科学上网条件

1 开发环境准备

1.1 创建项目python环境

        通过conda命令创建项目的python开发环境

 conda create -n mcp_demo python=3.10

1.2 在pycharm创建项目

        在pycharm创建mcp_demo项目,并在环境中选择上一步中创建的python环境。

  • 解释器类型:选择自定义环境
  • 环境:选择现有
  • 类型:选择conda
  • 环境:选择上一步创建的环境

1.3 安装项目依赖

 pip install mcp openai python-dotenv 

2 依赖的资源准备

        本项目中需要调用天气接口获取城市的天气信息,因此需要注册天气API网站,并且获取调用key。

2.1 访问openweather

Current weather and forecast - OpenWeatherMap

点击以上连接,使用邮箱注册账号,过程简单,此处不做赘述。

2.2 进入apikey页面

按上图所示点击进入my api keys

2.3 生成apikey

按上图所示,在create key列中输入learn,然后点击generate按钮,就会生成一个名字为learn的key,后续使用就是这个key。如下图:

3 程序逻辑实现

        mcp是c/s架构,客户端和服务端是一一对应的。因此在实现基于MCP协议工具调用时,要分别实现客户端和服务端。

3.1 服务端逻辑实现

3.1.1 新建weather_server.py文件

3.1.2 导入相关依赖包

import json

import httpx
from mcp.server import FastMCP

3.1.3 初始化MCP服务

mcp = FastMCP("WeatherServer")

3.1.4 定义openweather配置

OPENWEATHER_API_KEY = "5082d4XXX163cb"
OPENWEATHER_BASE_URL = "https://api.openweathermap.org/data/2.5/weather"
USER_AGENT = "weather-app/1.0"

3.1.5 定义获取城市天气方法

async def get_weather(city):
    """
    从OpenWeather API 获取天气信息
    :param city: 城市名称(需要试用英文,如 beijing)
    :return: 天气数据字典;若发生错误,返回包含error信息的字典
    """
    params = {
        "q": city,
        "appid": OPENWEATHER_API_KEY,
        "units": "metric",
        "lang": "zh_cn",
    }
    headers = {"User-Agent": USER_AGENT}

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(OPENWEATHER_BASE_URL, params=params, headers=headers, timeout=30)
            response.raise_for_status()
            return response.json()
        except httpx.HTTPStatusError as e:
            return {"error": f"HTTP请求错误:{e}"}
        except Exception as e:
            return {"error": f"发生错误:{e}"}

3.1.6 定义格式话天气数据方法

def format_weather_data(data):
    """
    格式化天气数据
    :param data: 天气数据字典
    :return: 格式化后的字符串;若发生错误,返回包含error信息的字符串
    """

    #  如果传入的是字符串,则先转换成字典
    if isinstance(data, str):
        data = json.loads(data)

    if "error" in data:
        return data["error"]
    weather = data["weather"][0]["description"]
    temperature = data["main"]["temp"]
    city = data["name"]
    country = data["sys"]["country"]
    humidity = data["main"]["humidity"]
    wind = data["wind"]["speed"]

    return f"城市:{city}, {country}\n天气:{weather}\n温度:{temperature}°C\n湿度:{humidity}%\n风速:{wind}m/s"

3.1.7 定义为MCP工具

@mcp.tool()
async def get_weather_tool(city: str):
    """
    获取城市的天气信息
    :param city: 城市名称(需要试用英文,如 beijing)
    :return: 天气数据字典;若发生错误,返回包含error信息的字典
    """
    weather_data = await get_weather(city)
    return format_weather_data(weather_data)

3.1.8 定义main方法

if __name__ == "__main__":
    mcp.run(transport="stdio")

3.2 客户端逻辑实现

3.2.1 新建weather_client文件

3.2.2 导入相关依赖包

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

from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters, stdio_client
from openai import OpenAI

3.2.3 格式化编码环境

sys.stdout.reconfigure(encoding='utf-8')
os.environ['PYTHONIOENCODING'] = 'utf-8'

3.2.4 加载ollama配置

load_dotenv()

3.2.5 定义配置文件

        新建.env文件,并填入ollama相关配置

BASE_URL=http://localhost:11434/v1/
MODEL=qwen2.5:7b
OPENAI_API_KEY=ollama

3.2.6 定义MCPclient类

class MCPClient:
3.2.6.1 定义初始化方法
    def __init__(self):
        """初始化MCP客户端"""
        self.exit_stack = AsyncExitStack()
        self.openai_api_key = os.getenv('OPENAI_API_KEY')
        self.base_url = os.getenv('BASE_URL')
        self.model = os.getenv('MODEL')
        if not self.openai_api_key:
            raise ValueError("❌未找到OpenAI API Key,请在.env文件中设置OPENAI_API_KEY")

        self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url)
        self.session: Optional[ClientSession] = None
3.2.6.2 定义连接至服务器方法
    async def connect_to_server(self, server_script_path):
        """连接到MCP服务器"""
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')
        if not is_python and not is_js:
            raise ValueError("❌不支持的脚本类型,请使用Python或JavaScript脚本")

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

        # 启动 MCP 服务器并建立通信
        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()

        #  列出 MCP服务器上的工具
        response = await self.session.list_tools()
        tools = response.tools
        print("\n已连接到服务器,支持以下工具:", [tool.name for tool in tools])
3.2.6.3 定义查询方法
    async def process_query(self, query):
        """
        使用大模型处理查询并调用可用的 MCP 工具 (Function Calling)
        """
        messages = [{"role": "system", "content": "你是一个智能助手,帮助用户回答问题。"},
        {"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.client.chat.completions.create(
            model=self.model,
            messages=messages,
            tools=available_tools
        )

        content = response.choices[0]
        if content.finish_reason == "tool_calls":
            tool_call = content.message.tool_calls[0]
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)

            # 执行工具
            result = await self.session.call_tool(function_name, function_args)
            print(f"\n\n[Calling tool {function_name} with args {function_args}]\n\n")
            # 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入messages中
            result_content = result.content[0].text
            messages.append(content.message.model_dump())
            messages.append({
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": result_content,
            })

            # 将上面的结果再返回给大模型用于生产最终的结果
            return result_content

        return content.message.content.strip()
3.2.6.4 定义问答方法
    async def chat_loop(self):
        """运行交互式聊天循环"""
        print("✅MCP 客户端已启动!输入 'quit' 退出")

        while True:
            try:
                query = input("输入你的问题:").strip()
                if query.lower() == 'quit':
                    break

                response = await self.process_query(query)

                print(f"openai:{response}")
            except Exception as e:
                print(f"发生错误:{e}")
3.2.6.5 定义连接关闭方法
    async def cleanup(self):
        """清理资源"""
        await self.exit_stack.aclose()

3.2.7 定义主流程方法

async def main():
    if len(sys.argv) < 2:
        print("请提供 MCP 服务器脚本路径作为参数")
        sys.exit(1)
    client = MCPClient()
    try:
        await client.connect_to_server(sys.argv[1])
        await client.chat_loop()
    finally:
        await client.cleanup()

3.2.8 定义main方法

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

4 测试验证

4.1 运行client方法

注意在运行参数里填写server脚本。如果不在同一目录,写路径全名

4.2 输入问题

4.3 查看回答结果

5 完整代码

5.1 服务端完整代码

import json

import httpx
from mcp.server import FastMCP

mcp = FastMCP("WeatherServer")

OPENWEATHER_API_KEY = "5082d4XXXXX51163cb"
OPENWEATHER_BASE_URL = "https://api.openweathermap.org/data/2.5/weather"
USER_AGENT = "weather-app/1.0"

async def get_weather(city):
    """
    从OpenWeather API 获取天气信息
    :param city: 城市名称(需要试用英文,如 beijing)
    :return: 天气数据字典;若发生错误,返回包含error信息的字典
    """
    params = {
        "q": city,
        "appid": OPENWEATHER_API_KEY,
        "units": "metric",
        "lang": "zh_cn",
    }
    headers = {"User-Agent": USER_AGENT}

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(OPENWEATHER_BASE_URL, params=params, headers=headers, timeout=30)
            response.raise_for_status()
            return response.json()
        except httpx.HTTPStatusError as e:
            return {"error": f"HTTP请求错误:{e}"}
        except Exception as e:
            return {"error": f"发生错误:{e}"}

def format_weather_data(data):
    """
    格式化天气数据
    :param data: 天气数据字典
    :return: 格式化后的字符串;若发生错误,返回包含error信息的字符串
    """

    #  如果传入的是字符串,则先转换成字典
    if isinstance(data, str):
        data = json.loads(data)

    if "error" in data:
        return data["error"]
    weather = data["weather"][0]["description"]
    temperature = data["main"]["temp"]
    city = data["name"]
    country = data["sys"]["country"]
    humidity = data["main"]["humidity"]
    wind = data["wind"]["speed"]

    return f"城市:{city}, {country}\n天气:{weather}\n温度:{temperature}°C\n湿度:{humidity}%\n风速:{wind}m/s"


@mcp.tool()
async def get_weather_tool(city: str):
    """
    获取城市的天气信息
    :param city: 城市名称(需要试用英文,如 beijing)
    :return: 天气数据字典;若发生错误,返回包含error信息的字典
    """
    weather_data = await get_weather(city)
    return format_weather_data(weather_data)


if __name__ == "__main__":
    mcp.run(transport="stdio")





5.2 客户端完整代码

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

from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters, stdio_client
from openai import OpenAI

sys.stdout.reconfigure(encoding='utf-8')
os.environ['PYTHONIOENCODING'] = 'utf-8'

load_dotenv()

class MCPClient:
    def __init__(self):
        """初始化MCP客户端"""
        self.exit_stack = AsyncExitStack()
        self.openai_api_key = os.getenv('OPENAI_API_KEY')
        self.base_url = os.getenv('BASE_URL')
        self.model = os.getenv('MODEL')
        if not self.openai_api_key:
            raise ValueError("❌未找到OpenAI API Key,请在.env文件中设置OPENAI_API_KEY")

        self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url)
        self.session: Optional[ClientSession] = None


    async def connect_to_server(self, server_script_path):
        """连接到MCP服务器"""
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')
        if not is_python and not is_js:
            raise ValueError("❌不支持的脚本类型,请使用Python或JavaScript脚本")

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

        # 启动 MCP 服务器并建立通信
        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()

        #  列出 MCP服务器上的工具
        response = await self.session.list_tools()
        tools = response.tools
        print("\n已连接到服务器,支持以下工具:", [tool.name for tool in tools])


    async def process_query(self, query):
        """
        使用大模型处理查询并调用可用的 MCP 工具 (Function Calling)
        """
        messages = [{"role": "system", "content": "你是一个智能助手,帮助用户回答问题。"},
        {"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.client.chat.completions.create(
            model=self.model,
            messages=messages,
            tools=available_tools
        )

        content = response.choices[0]
        if content.finish_reason == "tool_calls":
            tool_call = content.message.tool_calls[0]
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)

            # 执行工具
            result = await self.session.call_tool(function_name, function_args)
            print(f"\n\n[Calling tool {function_name} with args {function_args}]\n\n")
            # 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入messages中
            result_content = result.content[0].text
            messages.append(content.message.model_dump())
            messages.append({
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": result_content,
            })

            # 将上面的结果再返回给大模型用于生产最终的结果
            return result_content

        return content.message.content.strip()



    async def chat_loop(self):
        """运行交互式聊天循环"""
        print("✅MCP 客户端已启动!输入 'quit' 退出")

        while True:
            try:
                query = input("输入你的问题:").strip()
                if query.lower() == 'quit':
                    break

                response = await self.process_query(query)

                print(f"openai:{response}")
            except Exception as e:
                print(f"发生错误:{e}")

    async def cleanup(self):
        """清理资源"""
        await self.exit_stack.aclose()


async def main():
    if len(sys.argv) < 2:
        print("请提供 MCP 服务器脚本路径作为参数")
        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__":
    asyncio.run(main())

附录

项目结构

问题一:报UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc7 in position 0: invalid continuation byte

  • 检查是否代码中是否设置了标准输入输出编码格式

  • 检查项目设置中的编码是不是utf-8

问题二:openai:发生错误:All connection attempts failed

        访问openweather时网络问题,请尝试科学上网。

### MCP 技术概述 MCP 是由 Anthropic 开发的一种用于构建 AI Agent 的技术框架,旨在简化开发者创建和部署人工智能驱动工具的过程[^2]。通过该框架,可以轻松集成多种大模型(LLMs),包括但不限于 DeepSeek 和其他开源模型。 通义千问作为阿里巴巴集团旗下的超大规模语言模型,在功能性和灵活性上具有显著优势。要将通义千问与 MCP 进行集成,可以通过以下方式实现--- ### 集成方案分析 #### 1. 使用 Docker Compose 构建环境 为了快速启动并运行 MCP-Bridge 容器,可采用 `docker-compose` 命令来完成基础环境搭建。具体命令如下所示: ```bash docker compose up --build ``` 此操作会自动拉取所需镜像并初始化容器化服务,从而为后续的开发工作提供稳定的运行平台[^1]。 #### 2. 调整服务器端逻辑以适配通义千问 API 基于 MCP 提供的功能接口定义机制,可通过扩展其内置装饰器函数来自定义业务逻辑。例如,下面展示了一个简单的例子——调用通义千问的服务来进行两数比较处理: ```python from mcp.server.fastmcp import FastMCP import requests mcp = FastMCP("comparisonService") @mcp.tool() def compare_with_qwen(num1, num2): payload = { 'prompt': f'比较 {num1} 和 {num2}, 返回哪个更大或者相等', 'max_tokens': 50, 'temperature': 0.7 } response = requests.post( url="https://your-qwen-api-endpoint.com/v1/completions", headers={"Authorization": "Bearer YOUR_API_KEY"}, json=payload ) result = response.json().get('choices', [{}])[0].get('text', '').strip() return result or "无法获取结果" if __name__ == "__main__": mcp.run(transport='stdio') ``` 上述代码片段展示了如何借助 HTTP 请求向通义千问发送查询请求,并解析返回的结果数据[^3]。 #### 3. 结合资源管理模块增强交互体验 除了核心计算能力外,还可以进一步利用 MCP 中的资源声明特性,增加更多维度上的用户体验优化措施。比如引入问候语生成组件: ```python @mcp.resource("welcomeMessage://{username}") def generate_welcome_message(username): prompt_template = f"欢迎来到我们的应用,请称呼用户为{username}" api_response = call_qwen_api(prompt_template) return api_response ``` 这里假设存在一个辅助方法 `call_qwen_api()` 来封装实际通信细节。 --- ### 总结 综上所述,MCP 不仅能够帮助技术人员迅速建立起强大的 AI 应用程序原型,而且还能灵活对接不同类型的预训练模型,如通义千问这样的高性能解决方案。这使得整个生态体系更加开放多元,同时也促进了行业标准化进程的发展趋势。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值