MCP 实战 06:把 HTTP 接口接成工具,让 AI 帮你查数据

上篇读的是本地文件。如果数据在接口里,怎么交给 AI?

思路很直接:

AI 调用 MCP 工具,工具请求 HTTP 接口,再把结果返回。

这次自己启动一个模拟接口,不需要账号、密钥,也不用连接公司系统。

一、准备三个文件

新建文件夹 mcp-http,按下面的结构创建文件:

text 复制代码
mcp-http/
├── run.py
├── check.py
└── fixtures/
    └── mock_api.py

在这个文件夹打开终端,安装依赖。

Mac:

bash 复制代码
python3 -m venv .venv
.venv/bin/python -m pip install mcp==2.3.0 pydantic==2.14.1

Windows PowerShell:

powershell 复制代码
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install mcp==2.3.0 pydantic==2.14.1

Python 使用 3.11 或更高版本。

二、先准备一个模拟接口

打开 fixtures/mock_api.py,复制下面的代码:

python 复制代码
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
import sys
import time
from urllib.parse import urlsplit, parse_qs


class Handler(BaseHTTPRequestHandler):
    def log_message(self, *args):
        pass

    def do_GET(self):
        mode = os.environ.get("DEMO_MOCK_MODE", "normal")

        if mode == "slow":
            time.sleep(1.2)

        status = (
            503 if mode == "error"
            else 302 if mode == "redirect"
            else 200
        )

        query = parse_qs(urlsplit(self.path).query)
        rows = (
            [] if query.get("status") == ["closed"]
            else [{"id": "HTTP-001", "title": "虚构登录问题"}]
        )

        raw = json.dumps(
            {"issues": rows}, ensure_ascii=False
        ).encode("utf-8")

        if mode == "oversize":
            raw = b"x" * 5000
        if mode == "invalid":
            raw = b"not-json"
        if mode == "shape":
            raw = b'{"issues": [{}]}'

        self.send_response(status)

        if mode == "redirect":
            self.send_header("Location", "http://example.invalid/")

        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(raw)))
        self.end_headers()

        try:
            self.wfile.write(raw)
        except (BrokenPipeError, ConnectionResetError):
            pass


if __name__ == "__main__":
    server = ThreadingHTTPServer(
        ("127.0.0.1", int(os.environ.get("DEMO_API_PORT", "8765"))),
        Handler,
    )

    if len(sys.argv) > 1:
        Path(sys.argv[1]).write_text(str(server.server_port))

    try:
        server.serve_forever()
    finally:
        server.server_close()

它会在本机启动一个接口:

text 复制代码
http://127.0.0.1:8765/issues

查询未关闭问题时,返回一条虚构记录;查询已关闭问题时,返回空列表。

代码里的其他模式用来模拟超时、接口报错等情况,先保持默认即可。

三、把接口包装成 MCP 工具

打开 run.py,复制完整代码:

python 复制代码
from typing import Any, Literal
import json
import os
import socket
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import (
    build_opener,
    ProxyHandler,
    HTTPRedirectHandler,
)

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations

mcp = MCPServer("http-wrapper-demo")

READ_ONLY = ToolAnnotations(
    read_only_hint=True,
    destructive_hint=False,
    idempotent_hint=True,
    open_world_hint=False,
)

PORT = int(os.environ.get("DEMO_API_PORT", "8765"))

if not 1 <= PORT <= 65535:
    raise ValueError("invalid_demo_port")

MAX_BYTES = 4096


class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


OPENER = build_opener(ProxyHandler({}), NoRedirect())


@mcp.tool(annotations=READ_ONLY)
def list_api_issues(
    project: Literal["demo-project"],
    status: Literal["open", "closed"] = "open",
) -> dict[str, Any]:
    """查询本地演示接口,不接受调用方传入网址。"""

    url = f"http://127.0.0.1:{PORT}/issues?" + urlencode({
        "project": project,
        "status": status,
    })

    try:
        with OPENER.open(url, timeout=0.5) as response:
            raw = response.read(MAX_BYTES + 1)

        if len(raw) > MAX_BYTES:
            raise ToolError("upstream_response_too_large")

        data = json.loads(raw.decode("utf-8"))

    except HTTPError as error:
        raise ToolError(f"upstream_http_{error.code}") from None
    except (TimeoutError, socket.timeout):
        raise ToolError("upstream_timeout") from None
    except URLError as error:
        code = (
            "upstream_timeout"
            if isinstance(error.reason, TimeoutError)
            else "upstream_unreachable"
        )
        raise ToolError(code) from None
    except (ValueError, UnicodeError):
        raise ToolError("upstream_invalid_json") from None

    if not isinstance(data, dict) or not isinstance(data.get("issues"), list):
        raise ToolError("upstream_invalid_shape")

    rows = data["issues"]

    if any(
        not isinstance(row, dict)
        or set(row) != {"id", "title"}
        or not all(isinstance(v, str) for v in row.values())
        for row in rows
    ):
        raise ToolError("upstream_invalid_shape")

    return {
        "source": "local-mock-http",
        "count": len(rows),
        "issues": rows,
    }


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

重点看两个地方:

地址固定在代码里。 AI 只能选择项目和状态,不能随便传一个网址。

失败会明确报错。 接口超时、返回错误、数据格式不对,都不会被包装成"没有问题"。

四、启动接口,再检查工具

先在第一个终端启动模拟接口。

Mac:

bash 复制代码
.venv/bin/python fixtures/mock_api.py

Windows:

powershell 复制代码
.\.venv\Scripts\python.exe fixtures/mock_api.py

这个终端保持打开。没有输出是正常现象,接口正在等待请求。

再打开第二个终端,进入同一个项目文件夹。

打开 check.py,复制:

python 复制代码
import asyncio
import json
import sys
from pathlib import Path

from mcp import Client, StdioServerParameters


async def main():
    config = {
        "command": sys.executable,
        "args": [
            str(Path(__file__).with_name("run.py").resolve())
        ],
        "env": {
            "PYTHONUTF8": "1",
            "PYTHONIOENCODING": "utf-8",
        },
    }

    async with Client(StdioServerParameters(**config)) as client:
        result = await client.call_tool("list_api_issues", {
            "project": "demo-project",
            "status": "open",
        })

        if result.is_error:
            for item in result.content:
                print(getattr(item, "text", ""))
            raise SystemExit(1)

        data = result.structured_content
        assert data["source"] == "local-mock-http"
        assert data["count"] == 1
        assert data["issues"][0]["id"] == "HTTP-001"

        print(json.dumps(data, ensure_ascii=False, indent=2))

    print("检查通过。客户端配置:")
    print(json.dumps(config, ensure_ascii=False, indent=2))


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

Mac 执行:

bash 复制代码
.venv/bin/python check.py

Windows 执行:

powershell 复制代码
.\.venv\Scripts\python.exe check.py

正常结果应该包含:

json 复制代码
{
  "source": "local-mock-http",
  "count": 1,
  "issues": [
    {
      "id": "HTTP-001",
      "title": "虚构登录问题"
    }
  ]
}

这就说明:工具确实请求了接口,并拿到了返回数据。

五、让 AI 调用它

按照第 04 篇的方法,在 Cherry Studio 添加本地 stdio 服务。

名称可以填 local-issue-api,其他字段使用 check.py 输出的配置。

给当前 Agent 启用服务,然后发送:

text 复制代码
请调用 list_api_issues。

project 填 demo-project,status 填 open。

根据工具结果,告诉我有几条问题,分别是什么。
如果工具失败,请说明失败,不要回答"没有问题"。

模拟接口所在的第一个终端需要一直保持运行。

注意:这里的 HTTP 是工具访问数据的方式,客户端和 MCP 服务之间仍然使用 stdio。

六、故意让它失败一次

在第一个终端按 Ctrl+C,停止模拟接口。

再在第二个终端运行 check.py。

这时应该看到:

text 复制代码
upstream_unreachable

这表示接口连接失败,不代表问题数量为零。

再启动模拟接口,重新检查,结果应恢复正常。

以后接真实业务接口时,也要保留这个区别:

返回情况 应该怎么理解
请求成功,列表为空 查到了,确实没有匹配记录
请求超时或连接失败 没查成功,暂时不知道结果
返回格式不对 数据异常,需要检查接口

如果启动时提示端口被占用,先检查是不是已经开了一个模拟接口,避免重复启动。

相关推荐
码农小旋风1 小时前
Ray分布式计算引擎介绍
人工智能·架构
火山引擎和TA的超级拍档们1 小时前
产品情报局 08 | AI 内容助手:全网热点驱动,一站式打通内容生产全链路
人工智能
Binovate缤江数智2 小时前
水泵远程开关之后,还需要管什么?数智开关在无人值守泵站中的监测、保护与能耗分析
人工智能·物联网
素男2 小时前
两个月不是六十天
人工智能·agent·self-becoming·ai长期记忆·ai自我介绍
byte轻骑兵2 小时前
【LMP】规范精讲[10]: 语音专线开线记——SCO 固定占座与 eSCO 限时重传
人工智能·人机交互·lmp·蓝牙链路管理·蓝牙外设
龙亘川2 小时前
构建城市公共安全数字底座:亘川智城智慧安防系统全维度解读
java·大数据·开发语言·人工智能·信息可视化
迅易科技2 小时前
Copilot Studio 更新思考:Agent、Workflow、App 如何重新划分企业流程分工
人工智能·微软
Da Da 泓2 小时前
五子棋 Web 端测试用例报告
前端·网络·websocket·网络协议·测试用例·项目