情 九头像
MCP 实战 06:把 HTTP 接口接成工具,让 AI 帮你查数据封面图

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

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

思路很直接:

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

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

一、准备三个文件

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

mcp-http/
├── run.py
├── check.py
└── fixtures/
    └── mock_api.py

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

Mac:

python3 -m venv .venv
.venv/bin/python -m pip install mcp==2.3.0 pydantic==2.14.1

Windows 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,复制下面的代码:

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

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

http://127.0.0.1:8765/issues

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

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

三、把接口包装成 MCP 工具

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

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:

.venv/bin/python fixtures/mock_api.py

Windows:

.\.venv\Scripts\python.exe fixtures/mock_api.py

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

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

打开 check.py,复制:

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 执行:

.venv/bin/python check.py

Windows 执行:

.\.venv\Scripts\python.exe check.py

正常结果应该包含:

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

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

五、让 AI 调用它

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

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

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

请调用 list_api_issues。

project 填 demo-project,status 填 open。

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

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

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

六、故意让它失败一次

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

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

这时应该看到:

upstream_unreachable

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

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

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

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

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

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_44674766/article/details/169549000

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--