MCP 实战 08:查询超时怎么办?给工具加上有限重试

MCP 实战 09:同一个工具,怎么让不同的人只能查自己的项目?

工具能查到数据了,接下来还要问一句:

谁在查?他能看哪些数据?

比如 Alice 负责网页项目,Bob 负责移动项目。不能因为两个人用了同一个工具,就把所有记录都返回。

这篇用本地配置演示权限过滤,不需要账号系统。

一、先把规则说清楚

我们给两个人分配不同范围:

使用者 允许查询
Alice web 项目中属于自己的记录
Bob mobile 项目中属于自己的记录

工具只接受项目参数。

身份由启动配置提供,不能让模型在调用工具时自己填。

否则,模型传一句"我是管理员",程序就当真了,权限也就失去意义。

二、准备项目

新建文件夹 mcp-permissions:

text 复制代码
mcp-permissions/
├── run.py
├── check.py
├── make-config.py
└── fixtures/
    └── issues.json

在这个文件夹打开终端。

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/issues.json,复制:

json 复制代码
[
  {
    "id": "WEB-1",
    "owner": "alice",
    "project": "web",
    "title": "演示网页问题"
  },
  {
    "id": "MOB-1",
    "owner": "bob",
    "project": "mobile",
    "title": "演示移动问题"
  },
  {
    "id": "WEB-PRIVATE",
    "owner": "bob",
    "project": "web",
    "title": "另一个人的记录"
  }
]

第三条是故意放进去的。

它也属于 web 项目,但归 Bob 所有。Alice 查询网页项目时,不应该看到这条记录。

四、在工具里检查身份和范围

打开 run.py,复制:

python 复制代码
import json
import os
from pathlib import Path
from typing import Any, Literal

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

BASE = Path(__file__).resolve().parent

# 身份来自本地启动配置,不来自工具参数。
OWNER = os.environ.get("DEMO_OWNER", "")

SCOPES = {
    "alice": {"web"},
    "bob": {"mobile"},
}

server = MCPServer("local-scope-demo")


def query(project):
    if OWNER not in SCOPES:
        raise ToolError("untrusted or missing launcher identity")

    if project not in SCOPES[OWNER]:
        raise ToolError("project outside configured scope")

    rows = json.loads(
        (BASE / "fixtures/issues.json").read_text("utf-8")
    )

    return {
        "owner": OWNER,
        "project": project,
        "rows": [
            row for row in rows
            if row["owner"] == OWNER
            and row["project"] == project
        ],
    }


@server.tool()
def query_issues(
    project: Literal["web", "mobile"],
) -> dict[str, Any]:
    """只查询当前进程身份有权访问、且归其所有的演示记录。"""
    return query(project)


if __name__ == "__main__":
    server.run()

查询分三步:

  1. 身份不认识,拒绝。
  2. 项目不在允许范围,拒绝。
  3. 返回记录前,再按归属过滤。

这里没有 owner 工具参数,模型只能选择查询哪个项目。

五、检查"能查"和"不能查"

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

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

from mcp import Client, StdioServerParameters

BASE = Path(__file__).resolve().parent
checks = []


def check(name, condition):
    assert condition, name
    checks.append(name)


def config(owner):
    return StdioServerParameters(
        command=sys.executable,
        args=[str(BASE / "run.py")],
        cwd=str(BASE),
        env={
            **os.environ,
            "PYTHONUTF8": "1",
            "DEMO_OWNER": owner,
        },
    )


async def main():
    results = {}

    for owner, allowed, denied, expected in [
        ("alice", "web", "mobile", "WEB-1"),
        ("bob", "mobile", "web", "MOB-1"),
    ]:
        async with Client(config(owner)) as client:
            tool = (await client.list_tools()).tools[0]

            check(
                owner + "_schema_has_no_identity",
                set(tool.input_schema["properties"]) == {"project"},
            )

            result = await client.call_tool(
                "query_issues", {"project": allowed}
            )
            data = result.structured_content

            check(
                owner + "_allowed",
                not result.is_error
                and [row["id"] for row in data["rows"]] == [expected],
            )
            results[owner] = data

            result = await client.call_tool(
                "query_issues", {"project": denied}
            )
            check(owner + "_denied", result.is_error)

    for owner in ["", "mallory"]:
        async with Client(config(owner)) as client:
            result = await client.call_tool(
                "query_issues", {"project": "web"}
            )
            check(
                "missing_identity" if not owner else "unknown_identity",
                result.is_error,
            )

    async with Client(config("alice")) as a, Client(config("bob")) as b:
        ra, rb = await asyncio.gather(
            a.call_tool("query_issues", {"project": "web"}),
            b.call_tool("query_issues", {"project": "mobile"}),
        )
        check(
            "separate_process_isolation",
            ra.structured_content["owner"] == "alice"
            and rb.structured_content["owner"] == "bob",
        )

    report = {
        "checks_passed": len(checks),
        "checks": checks,
        "all_passed": True,
        "transport": "real stdio, separately configured processes",
        "source": "fictional local fixture",
    }

    (BASE / "verification.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        "utf-8",
    )
    (BASE / "demo-result.json").write_text(
        json.dumps({
            "checks_passed": len(checks),
            "results": results,
            "denied": "alice→mobile / bob→web",
            "note": "本地可信配置身份演示",
        }, ensure_ascii=False, indent=2) + "\n",
        "utf-8",
    )

    print(json.dumps(report, ensure_ascii=False))


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

Mac 执行:

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

Windows 执行:

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

正常结果包含:

json 复制代码
{
  "checks_passed": 9,
  "all_passed": true
}

检查过程中会故意触发四次拒绝,所以出现权限错误是正常的。

打开运行后生成的 demo-result.json,可以看到:

  • Alice 只拿到 WEB-1。
  • Bob 只拿到 MOB-1。
  • WEB-PRIVATE 没有混进 Alice 的结果。

六、生成两份客户端配置

打开 make-config.py,复制:

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

BASE = Path(__file__).resolve().parent

for owner in ["alice", "bob"]:
    config = {
        "mcpServers": {
            "local-" + owner: {
                "command": sys.executable,
                "args": [str(BASE / "run.py")],
                "env": {
                    "PYTHONUTF8": "1",
                    "DEMO_OWNER": owner,
                },
            }
        }
    }

    target = BASE / ("client-" + owner + ".json")
    target.write_text(
        json.dumps(config, ensure_ascii=False, indent=2) + "\n",
        "utf-8",
    )
    print(target.name)

Mac 执行:

bash 复制代码
.venv/bin/python make-config.py

Windows 执行:

powershell 复制代码
.\.venv\Scripts\python.exe make-config.py

会生成两份配置:

text 复制代码
client-alice.json
client-bob.json

按照第 04 篇的方法,从对应配置中取出 command、args、env,手动填写客户端。

先只给当前 Agent 启用 Alice 的服务,发送:

text 复制代码
请调用 query_issues,project 填 web。
列出工具返回的问题编号和标题。

应该只看到 WEB-1。

再让它查询 mobile,应该被拒绝。不要同时启用 Bob 的服务,否则模型可能改用 Bob 的工具完成查询,影响观察结果。

七、这套演示的边界在哪?

这里的"可信身份",意思是:启动配置由你或管理员控制,模型只能调用工具,不能修改配置。

它没有实现登录、OAuth 或远程用户认证。每个服务进程只有一个固定身份,不能拿一个全局变量同时代表所有用户。

如果使用者能够任意修改环境变量、代码或数据文件,就能改变演示规则。因此,这篇适合学习本地权限过滤,不能直接当成多人共享服务的安全隔离方案。

相关推荐
nujnewnehc7 小时前
给记事软件加上 mcp
agent·mcp
用户2762318412212 小时前
做抖音解析时,我差点把别人的视频数据写进去
mcp
jerrywus12 小时前
普通人怎么用 Codex:从表格、文件和小工具开始
chatgpt·agent·mcp
骑着蜗牛撵大象32715 小时前
Spring Boot 4.1 + Spring AI 2.0:RAG 供文、图谱供关系、MCP 供工具的 Java Agent 平台
java·spring boot·spring·微服务·知识图谱·rag·mcp
情 九17 小时前
MCP 实战 07:让 AI 查询 SQLite,只开放一个查询入口
mcp
骑着蜗牛撵大象3271 天前
2026 后端 AI 工程化:Spring AI 管调用、MCP 管工具、Agent 内嵌管自治
java·人工智能·后端·spring·ai·可观测性·mcp
Danny转行做跨境1 天前
亚马逊选品接 MCP 到 Claude Code 的四个坑
跨境电商·mcp·sorftime
只睡四小时2 天前
MCP 弃用 SSE?Node 分帧漏 81.7% 事件
node.js·webassembly·实时通信·mcp·streamable http
跨境Jacky2 天前
选品数据采集,MCP比爬虫稳在哪?亚马逊到1688实测
跨境电商·mcp·sorftime