MCP 服务器最佳实践:传输、安全与工具设计
MCP 服务器交给真实用户或真实数据之前,需要先定下部署形态、传输和鉴权,再逐条满足 Streamable HTTP 与 OAuth 的 MUST 规则,并确认工具设计让模型能自我纠错。每条做法都对应官方原文的位置、失效模式和恢复步骤;HTTP 规则可以用文中的脚本对线上端点测试。
一、先选部署形态
多数安全决策取决于服务器跑在哪里、谁能访问它。动手写处理函数之前先定下来。
| 你的情况 | 选用 | 鉴权 | 官方依据 |
|---|---|---|---|
| 单用户工具,由宿主在同一台机器上启动 | stdio | 从环境变量读取凭据,不要实现 HTTP 授权流程 | 授权是可选的(OPTIONAL);使用 stdio 的实现不应(SHOULD NOT)遵循 HTTP 授权规范(基础协议:Auth) |
| 本机 HTTP 服务器,机器上的其他进程可能访问到它 | Streamable HTTP | 绑定 127.0.0.1,校验 Origin,要求令牌,或改用受限访问的 IPC | Streamable HTTP “Security & Endpoint”;安全最佳实践“Local MCP Server Compromise” |
| 多用户共享的服务器 | Streamable HTTP | OAuth 2.1 资源服务器:受保护资源元数据、令牌校验、受众绑定 | 授权 |
| 代用户调用其他 API 的服务器 | Streamable HTTP | 用颁发给你自己的令牌;下游使用单独的凭据,绝不转发客户端的令牌 | 授权“Token Handling”:服务器不得(MUST NOT)接受或转发颁发给其他资源的令牌 |
二、有意识地做成无状态
2026-07-28 修订版没有协议层会话。每个请求自带 _meta(协议版本、客户端信息、能力),服务器不得(MUST NOT)依赖同一连接上更早的请求(基础协议:Statelessness)。客户端可以在一条连接上穿插无关的请求,所以连接或进程不等于一段对话。
跨请求的状态,比如购物车、草稿、长任务,要放在显式句柄之后,由模型把句柄当作普通工具参数带回来(工具:Stateful Tools,非规范性指导)。
| 需要记住的东西 | 应该这样做 | 避免 |
|---|---|---|
| 购物车、事务、长任务 | 由创建类工具返回不透明句柄,后续调用把它作为参数传入 | 以连接、进程或 Mcp-Session-Id 头作为状态的键 |
| 谁能使用某个句柄 | 以 <user_id>:<handle> 存储,user 来自已验证的令牌,拒绝其他主体 | 把“持有句柄”当作身份认证 |
| 句柄格式 | 密码学随机、不透明、会过期 | 计数器、时间戳,或编码了内部结构的标识 |
| 过期 | 在创建工具的描述里写明保留规则;句柄过期时返回说明原因的工具执行错误 | 静默失败或笼统的协议错误 |
| 横向扩容 | 把共享状态放在句柄背后的存储里,任何实例都能处理任何请求 | 要求相关请求使用同一连接或进程(SHOULD NOT) |
只支持本修订版的服务器应忽略请求里的 Mcp-Session-Id,既不生成也不回显会话 ID(Streamable HTTP:Earlier Streamable HTTP Revisions)。
三、远程服务器必须通过的规则
每行给出一条做法、背后官方原文的强度、出处,以及第六节哪个探测项会检查它。“指导”表示原文描述了行为但没有 RFC 2119 关键词。
| # | 做法 | 强度 | 官方出处 | 探测项 |
|---|---|---|---|---|
| 1 | 实现 server/discover,并在 supportedVersions 里列出支持的版本 | MUST | Discovery | discover |
| 2 | 对所有入站连接校验 Origin;存在且无效时返回 403 Forbidden | MUST | Streamable HTTP “Security & Endpoint” | origin |
| 3 | 请求缺少 Mcp-Method、Mcp-Name、MCP-Protocol-Version 头,或头格式错误、与请求体不一致:返回 400 与 JSON-RPC 错误 -32020(HeaderMismatch) | MUST | Streamable HTTP “Server Validation” | method-header-missing、method-header-mismatch |
| 4 | 协议版本不受支持:返回 400 与 UnsupportedProtocolVersionError(-32022),并列出支持的版本 | MUST | Streamable HTTP “Protocol Version Header” | unsupported-version、unsupported-version-lists-supported |
| 5 | RPC 方法不存在:返回 404 与 -32601 | MUST | Streamable HTTP “Protocol Version Header” | unknown-method |
| 6 | 拒绝缺少 MCP-Protocol-Version 的请求,除非你有意服务 2025-06-18 之前的客户端 | MUST(针对不支持此类客户端的服务器) | Streamable HTTP “Protocol Version Header” | version-header-missing(WARN) |
| 7 | 对端点的 GET、DELETE 返回 405;忽略 Mcp-Session-Id,不生成会话 ID | SHOULD | Streamable HTTP “Earlier Streamable HTTP Revisions” | get-not-allowed、no-session-id(WARN) |
| 8 | 校验所有工具输入、做访问控制、对调用限流、清洗输出 | MUST | 工具“Security Considerations” | 未探测 |
| 9 | 输入校验失败用 isError: true 的结果返回;未知工具作为协议错误(-32602)返回 | 指导 | 工具“Error Handling” | tool-input-error、unknown-tool(WARN) |
| 10 | 提供受保护资源元数据,authorization_servers 至少含一项,并通过 WWW-Authenticate 的 resource_metadata 参数或 well-known URI 公布 | MUST | Authorization Server Discovery | auth-resource-metadata |
| 11 | 校验访问令牌,只接受以本服务器为受众的令牌,无效或过期的令牌返回 401 | MUST | 授权“Token Handling” | auth-required、auth-invalid-token、auth-audience |
| 12 | 绝不接受或转发颁发给其他资源的令牌 | MUST NOT | 授权“Token Handling”;安全最佳实践“Token Passthrough” | auth-audience(只覆盖“接受”) |
| 13 | 权限范围不足时返回 403,WWW-Authenticate 带 error="insufficient_scope"、scope、resource_metadata,并把该操作需要的全部 scope 放进同一次挑战 | SHOULD | 授权“Scope Challenge Handling” | 未探测 |
| 14 | 最小权限的 scope:初始集合小、不用通配 scope、不在 scopes_supported 里列出全部目录 | 指导 | 安全最佳实践“Scope Minimization” | 未探测 |
| 15 | 状态句柄不可预测,并在服务端绑定到已认证用户 | SHOULD | 安全最佳实践“State Handle Hijacking” | 未探测 |
第 8、13、14、15 行依赖你的业务逻辑,或需要一个权限受限的令牌,通用探测覆盖不了,请自己写测试,场景见第五节。
四、把工具设计成模型能自我纠错
| 规则 | 强度 | 官方出处 |
|---|---|---|
名称:1 到 128 个字符,取自 A-Z a-z 0-9 _ - .,区分大小写,在服务器内唯一 | SHOULD | 工具“Tool Names” |
inputSchema 是合法的 JSON Schema 对象,不能是 null;无参数工具使用 { "type": "object", "additionalProperties": false } | MUST(合法对象);推荐写法 | 工具“Tool” |
声明了 outputSchema,每个结构化结果都必须符合;同时在文本块里返回序列化 JSON 以兼容旧客户端 | MUST;SHOULD | 工具“Output Schema”与“Structured Content” |
tools/list 返回当前请求方可用的工具,不随连接变化,也不受其他请求副作用影响;可以随请求上的授权而不同;顺序保持确定 | 不得随连接变化(MUST NOT);顺序 SHOULD | 工具“Capabilities” |
校验与业务规则失败用 isError: true 的结果返回,消息写清错在哪、合法取值是什么(格式、范围)。客户端应(SHOULD)把执行错误交给模型,而协议错误更难恢复 | 指导 | 工具“Error Handling” |
不要给密钥或个人信息加 x-mcp-header;头的值对网络中间设备可见 | SHOULD NOT | 工具“x-mcp-header” |
| 不要把注解当作安全控制;客户端必须(MUST)把它们视为不可信,除非服务器可信 | MUST(客户端) | 工具“Tool” |
不适合写进某个工具描述的指引,比如工具之间的关系,放进服务器的 instructions。Server Instructions 实战 讲了各修订版里该字段的送达位置与测试方法。
五、失效模式与恢复
| 现象 | 可能原因 | 如何确认 | 处理 |
|---|---|---|---|
经过网关的每个请求都返回 400 与 -32020 | 代理丢弃或改写了 Mcp-Method、Mcp-Name、MCP-Protocol-Version;规范把头缺失或不一致都算校验失败 | 分别对源站和经网关的地址跑探测,比较 method-header-* | 原样转发这三个头,保持请求体与头一致 |
客户端收到 -32022 后反复重试仍失败 | 它没读 data.supported,或你的列表里的版本它不支持 | 看错误体里的 data.supported 与 data.requested | 返回准确的列表;客户端用双方都支持的版本重试(版本管理) |
| 旧版客户端第一个请求就失败 | 服务器只支持新版,而客户端以 initialize 开场。SDK 2.3.0 对缺少 MCP-Protocol-Version 的请求返回 400 与 -32600 | version-header-missing | 接受这一结果,或按对应旧修订版的描述实现兼容 |
浏览器应用收到 403 | Origin 白名单里没有该应用的来源 | 带上应用的 Origin 重发请求 | 添加精确的来源。探测只发送敌意来源,请为每个允许的来源另写测试 |
开启鉴权后每次调用都返回 401 | 令牌是为别的受众签发的,或服务器比对的资源标识与客户端请求的不一致 | 带 --token 运行探测,discover 必须先通过 | 让令牌绑定到服务器的规范 URI,并在服务器上配置同一个 URI |
| 颁发给其他服务的令牌被接受 | 没有任何一层检查受众。SDK 2.3.0 里 AuthSettings.validate_token_resource 默认不设置:SDK 会告警、不检查,并说明 3.0 起默认为 True | auth-audience 失败并显示 got 200(第六节复现) | 设置 validate_token_resource=True,或在你自己的 TokenVerifier 里检查受众 |
| 客户端不停地让用户重新授权 | 服务器每次只挑战一个 scope | 统计同一操作收到的 403 挑战次数 | 把操作需要的所有 scope 放进一次挑战;客户端应限制重试次数 |
| 一个用户能访问另一个用户的购物车 | 句柄可猜测,或没有绑定调用者 | 用第二个用户的令牌回放句柄 | 随机不透明句柄,以 <user_id>:<handle> 为键,user 取自已验证的令牌 |
| 模型反复发出同一个错误调用 | 失败以协议错误或含糊文本返回 | 对比 JSON-RPC 错误与 isError 结果,读消息内容 | 返回 isError: true,指明字段与合法取值 |
| 两条连接得到的工具列表不同 | 列表依赖连接状态或无序存储 | 在两条连接上各调一次 tools/list 并比较 | 只由请求上的授权计算,并排序 |
探测覆盖不到的那几行,建议在自己的 CI 里加入这些场景:权限不足的令牌得到 403,挑战里列出所需的全部 scope;第二个用户的令牌不能使用第一个用户的句柄;过期句柄返回 isError 结果;两条连接对同一令牌得到相同的 tools/list;超出限流的工具调用被拒绝。
六、运行 HTTP 探测脚本
probe_http.py 只用 Python 标准库。它发送第三节中可探测各行对应的请求,每项检查输出一行。FAIL 表示未满足某条 MUST,WARN 表示未遵循某条 SHOULD 或工具页的错误处理指导。退出码 0 表示没有 FAIL,1 表示至少一个 FAIL,2 表示端点无法连接。
python3 probe_http.py http://127.0.0.1:8000/mcp --tool echo --bad-args '{"text":123}'
python3 probe_http.py http://127.0.0.1:8001/mcp --tool echo --bad-args '{"text":123}' \
--token good-token --expect-auth --bad-audience-token other-audience-tokenprobe_http.py
#!/usr/bin/env python3
"""Probe a Streamable HTTP MCP endpoint against the HTTP rules of spec 2026-07-28 (stdlib only).
Usage: probe_http.py URL [--tool NAME --bad-args JSON] [--token T [--expect-auth] [--bad-audience-token T]]
Levels: FAIL = a MUST in the spec is not met, WARN = a SHOULD or the tools page's error guidance is not followed
(version-header-missing is WARN because a server may serve pre-2025-06-18 clients and treat the request as 2025-03-26).
Exit code: 0 no FAIL, 1 at least one FAIL, 2 endpoint unreachable.
"""
import argparse
import http.client
import json
import sys
from urllib.parse import urlsplit
VERSION = "2026-07-28"
EVIL_ORIGIN = "https://evil.example"
counts = {"PASS": 0, "WARN": 0, "FAIL": 0}
def meta(version):
return {
"io.modelcontextprotocol/protocolVersion": version,
"io.modelcontextprotocol/clientInfo": {"name": "probe", "version": "0"},
"io.modelcontextprotocol/clientCapabilities": {},
}
def parse(raw):
"""Return the JSON-RPC response from a JSON body or an SSE stream, else None."""
try:
return json.loads(raw)
except ValueError:
pass
found = None
for line in raw.splitlines():
if line.startswith("data:"):
try:
msg = json.loads(line[5:].strip())
except ValueError:
continue
if isinstance(msg, dict) and ("result" in msg or "error" in msg):
found = msg
return found
def request(url, verb="POST", method=None, params=None, headers=None, version=VERSION):
"""Send one request; a header whose value is None is removed. Returns (status, headers, body)."""
parts = urlsplit(url)
cls = http.client.HTTPSConnection if parts.scheme == "https" else http.client.HTTPConnection
conn = cls(parts.netloc, timeout=10)
hdrs = {"Accept": "application/json, text/event-stream", "MCP-Protocol-Version": version}
payload = None
if verb == "POST":
hdrs.update({"Content-Type": "application/json", "Mcp-Method": method})
if params and "name" in params:
hdrs["Mcp-Name"] = params["name"]
payload = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method,
"params": {**(params or {}), "_meta": meta(version)}})
hdrs.update(headers or {})
conn.request(verb, parts.path or "/", payload, {k: v for k, v in hdrs.items() if v is not None})
resp = conn.getresponse()
raw = resp.read().decode("utf-8", "replace")
conn.close()
return resp.status, resp, parse(raw)
def code(body):
err = body.get("error") if isinstance(body, dict) else None
return err.get("code") if isinstance(err, dict) else None
def show(status, rpc_code):
return "%s/%s" % (status, rpc_code) if rpc_code is not None else str(status)
def report(name, ok, detail, level="FAIL"):
status = "PASS" if ok else level
counts[status] += 1
print("%s %s: %s" % (status, name, detail))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("url")
ap.add_argument("--tool", help="name of an existing tool, enables the tool-error check")
ap.add_argument("--bad-args", default="{}", help="JSON arguments that violate the tool's input schema")
ap.add_argument("--token", help="bearer token that the server accepts")
ap.add_argument("--expect-auth", action="store_true", help="the endpoint is protected: run the auth checks")
ap.add_argument("--bad-audience-token", help="a valid token issued for a different resource")
args = ap.parse_args()
if args.expect_auth and not args.token:
ap.error("--expect-auth needs --token (the other checks must get past authentication)")
auth = {"Authorization": "Bearer " + args.token} if args.token else {}
url = args.url
try:
status, _, body = request(url, method="server/discover", headers=auth)
except (OSError, http.client.HTTPException) as exc:
print("ERROR endpoint unreachable: %s" % exc)
return 2
result = body.get("result") if isinstance(body, dict) else None
ok = status == 200 and isinstance(result, dict) and VERSION in result.get("supportedVersions", [])
report("discover", ok, "HTTP %s, supportedVersions %s" % (status, (result or {}).get("supportedVersions")))
tools = "tools" in (result or {}).get("capabilities", {})
def expect(name, want_status, want_code, **kw):
st, _, b = request(url, **{"headers": auth, **kw})
report(name, st == want_status and (want_code is None or code(b) == want_code),
"want %s, got %s" % (show(want_status, want_code), show(st, code(b))))
return b
expect("origin", 403, None, method="server/discover", headers={**auth, "Origin": EVIL_ORIGIN})
expect("method-header-missing", 400, -32020, method="server/discover", headers={**auth, "Mcp-Method": None})
expect("method-header-mismatch", 400, -32020, method="server/discover", headers={**auth, "Mcp-Method": "tools/list"})
b = expect("unsupported-version", 400, -32022, method="server/discover", version="2099-01-01",
headers={**auth})
supported = ((b or {}).get("error") or {}).get("data", {}).get("supported") if isinstance(b, dict) else None
report("unsupported-version-lists-supported", bool(supported) and VERSION in supported, "data.supported = %s" % supported)
expect("unknown-method", 404, -32601, method="nope/nothing")
st, _, b = request(url, method="server/discover", headers={**auth, "MCP-Protocol-Version": None})
report("version-header-missing", st == 400, "want 400, got %s" % show(st, code(b)), "WARN")
st, _, _ = request(url, verb="GET", headers=auth)
report("get-not-allowed", st == 405, "want 405, got %s" % st, "WARN")
st, resp, _ = request(url, method="server/discover", headers={**auth, "Mcp-Session-Id": "probe-session"})
report("no-session-id", resp.getheader("Mcp-Session-Id") is None, "Mcp-Session-Id in response: %s" % resp.getheader("Mcp-Session-Id"), "WARN")
if args.tool:
st, _, b = request(url, method="tools/call", params={"name": args.tool, "arguments": json.loads(args.bad_args)}, headers=auth)
is_error = isinstance(b, dict) and (b.get("result") or {}).get("isError") is True
report("tool-input-error", st == 200 and is_error, "want 200 with result.isError=true, got %s" % show(st, "isError" if is_error else code(b)), "WARN")
st, _, b = request(url, method="tools/call", params={"name": "no_such_tool", "arguments": {}}, headers=auth)
report("unknown-tool", code(b) == -32602, "want JSON-RPC error -32602, got %s" % show(st, code(b) or "result"), "WARN")
elif tools:
print("SKIP tool checks: pass --tool NAME --bad-args JSON")
if args.expect_auth:
st, resp, b = request(url, method="server/discover")
challenge = resp.getheader("WWW-Authenticate") or ""
report("auth-required", st == 401, "no token: want 401, got %s" % st)
report("auth-invalid-token", request(url, method="server/discover", headers={"Authorization": "Bearer invalid"})[0] == 401,
"garbage token must get 401")
if args.bad_audience_token:
st = request(url, method="server/discover", headers={"Authorization": "Bearer " + args.bad_audience_token})[0]
report("auth-audience", st == 401, "token for another resource: want 401, got %s" % st)
meta_url = challenge.split('resource_metadata="')[1].split('"')[0] if 'resource_metadata="' in challenge else None
origin = "%s://%s" % (urlsplit(url).scheme, urlsplit(url).netloc)
well_known = "/.well-known/oauth-protected-resource"
found = None
for candidate in [meta_url, origin + well_known + urlsplit(url).path, origin + well_known]:
if not candidate:
continue
st, _, doc = request(candidate, verb="GET", headers={})
if st == 200 and isinstance(doc, dict):
found = doc
break
report("auth-resource-metadata", bool(found and found.get("authorization_servers")),
"challenge resource_metadata=%s; document authorization_servers=%s" % (meta_url, (found or {}).get("authorization_servers")))
print("%d pass, %d warn, %d fail" % (counts["PASS"], counts["WARN"], counts["FAIL"]))
return 1 if counts["FAIL"] else 0
if __name__ == "__main__":
try:
sys.exit(main())
except (OSError, http.client.HTTPException) as exc:
print("ERROR connection failed: %s" % exc)
sys.exit(2)两个参照服务器使用官方 Python SDK(pip install mcp==2.3.0):
ref_server.py
"""Official Python SDK (mcp==2.3.0) server on Streamable HTTP, http://127.0.0.1:8000/mcp, no auth."""
from mcp.server.mcpserver import MCPServer
server = MCPServer("ref")
@server.tool()
def echo(text: str) -> str:
"""Echo text."""
return text
if __name__ == "__main__":
server.run("streamable-http")auth_ref_server.py
"""Same server behind bearer-token auth, http://127.0.0.1:8001/mcp.
Tokens: good-token is issued for this server; other-audience-token is valid but issued for another resource.
"""
from mcp.server.auth.provider import AccessToken
from mcp.server.auth.settings import AuthSettings
from mcp.server.mcpserver import MCPServer
RESOURCE = "http://127.0.0.1:8001/mcp"
class Verifier:
async def verify_token(self, token: str) -> AccessToken | None:
if token == "good-token":
return AccessToken(token=token, client_id="probe", scopes=[], resource=RESOURCE)
if token == "other-audience-token":
return AccessToken(token=token, client_id="probe", scopes=[], resource="https://other.example/mcp")
return None
server = MCPServer(
"ref-auth",
token_verifier=Verifier(),
auth=AuthSettings(
issuer_url="https://auth.example.com",
resource_server_url=RESOURCE,
validate_token_resource=True,
),
)
@server.tool()
def echo(text: str) -> str:
"""Echo text."""
return text
if __name__ == "__main__":
server.run("streamable-http", port=8001)在这套环境(Python 3.14、macOS、mcp==2.3.0)上探测脚本的结果:
| 目标 | 结果 |
|---|---|
不带鉴权的 SDK 服务器(ref_server.py) | 11 通过、1 警告、0 失败 |
带 bearer 鉴权的 SDK 服务器(auth_ref_server.py) | 15 通过、1 警告、0 失败 |
同一个鉴权服务器去掉 validate_token_resource(SDK 默认值) | 1 个失败:auth-audience,收到 200 |
| 按规范原文写的桩,不带鉴权与带鉴权 | 12 通过、0 警告、0 失败;16 通过、0 警告、0 失败 |
| 桩每次破坏一条规则,共 11 个变体 | 8 个变体被对应检查判为失败,3 个变体只给出警告,没有一个静默通过 |
鉴权服务器的完整输出:
PASS discover: HTTP 200, supportedVersions ['2026-07-28']
PASS origin: want 403, got 403
PASS method-header-missing: want 400/-32020, got 400/-32020
PASS method-header-mismatch: want 400/-32020, got 400/-32020
PASS unsupported-version: want 400/-32022, got 400/-32022
PASS unsupported-version-lists-supported: data.supported = ['2026-07-28']
PASS unknown-method: want 404/-32601, got 404/-32601
PASS version-header-missing: want 400, got 400/-32600
PASS get-not-allowed: want 405, got 405
PASS no-session-id: Mcp-Session-Id in response: None
PASS tool-input-error: want 200 with result.isError=true, got 200/isError
WARN unknown-tool: want JSON-RPC error -32602, got 200/result
PASS auth-required: no token: want 401, got 401
PASS auth-invalid-token: garbage token must get 401
PASS auth-audience: token for another resource: want 401, got 401
PASS auth-resource-metadata: challenge resource_metadata=http://127.0.0.1:8001/.well-known/oauth-protected-resource/mcp; document authorization_servers=['https://auth.example.com']
15 pass, 1 warn, 0 fail怎么看这个结果:
- 那条警告是发现,不是探测脚本的缺陷。 工具页把“Unknown tool”列在协议错误下,示例错误码是
-32602;SDK 2.3.0 返回的是带isError: true的普通结果。这段文字属于指导而非 MUST,模型对两种返回都能恢复,但请有意识地选定一种行为并写测试。 - 默认受众那一行说明了为什么需要反向测试。 服务器对颁发给
https://other.example/mcp的令牌返回了200。没有崩溃,正常路径也能跑通,只有反向测试才发现得了。 - 通过只证明所列行为在这一套环境里成立。 探测不检查:白名单内的来源能否通过、TLS、限流、scope 挑战、流式(SSE)响应、取消、
x-mcp-header镜像、多轮往返请求、输出清洗和负载。桩与 SDK 通过同样的检查,是对规范的两次独立阅读,但排除不了共同的误读,所以请把第三节的对照表当作评审清单。
七、上线前核对
- 已按第一节选定部署形态并记录理由。
- 没有任何逻辑依赖连接或进程身份;所有句柄不透明、会过期、绑定所有者。
- Origin 校验、头校验与版本错误都通过探测,网关会转发三个镜像头。
- 受保护的服务器:无效、过期、受众错误的令牌都返回
401;元数据文档指明了授权服务器;没有令牌被转发到下游。 - 工具名称、schema 和结果形态符合第四节,校验失败以模型可据以行动的
isError结果返回。 - 第 8、13、14、15 行在你自己的测试集里有对应用例。
- 更换 SDK、网关或授权服务器后重新运行探测。
版本与验证说明
规范事实取自站内发布的 2026-07-28 页面。第六节的实验在本地用官方 Python SDK 2.3.0 完成,文中已标明。
来源
- 2026-07-28 规范中的 Streamable HTTP 传输、授权、Authorization Server Discovery、工具、版本管理与基础协议
- 安全最佳实践、本地 MCP 服务器安全与授权教程
- 站内的 Server Instructions 实战