新闻详情

新闻详情

首页 / 资讯中心 / 详情

从零手写一个生产级 MCP Server:鉴权、流式传输与状态管理(TaoToken 统一 Key 接入版)

发布时间:2026/11/13 4:10:54来源:尧图网络
从零手写一个生产级 MCP Server:鉴权、流式传输与状态管理(TaoToken 统一 Key 接入版)
1. 为什么本地跑通的 MCP Server 一上线就出事MCP Server 是 Model Context Protocol 的服务端实现简单说就是给 Claude Code、Cursor、各类 Agent 提供工具调用能力的后台服务。它能把数据库查询、文件操作、内部 API 调用包装成标准工具让模型按 JSON-RPC 2.0 协议调用。适合谁适合已经用官方 SDK 写过 Stdio 版 Demo、现在要把它部署到内网给多个 Agent 共用的后端工程师。我见过太多团队卡在同一个地方本地mcp.run()跑得好好的一放进 Kubernetes 就出三类事故。第一类是跨租户越权A 部门的 Agent 查到了 B 部门的表根因是租户信息存在全局变量里异步并发一交错就串了。第二类是长连接雪崩几十个 Agent 同时建 SSE 连接没有心跳保活也没有超时清理active_sessions字典只增不减内存慢慢被僵尸会话吃光。第三类是异常炸进程某个工具执行时抛了未捕获异常整个 MCP 进程直接退出所有在线会话一起断。这三个问题的共同点是它们都不是协议层的问题而是工程层的问题。官方 Demo 用 Stdio 模式一次只服务一个客户端天然没有并发和隔离的烦恼。但生产环境里 MCP Server 是一个独立微服务必须自己处理鉴权、流式传输和状态管理。这篇就按这三个硬核环节从零搭一个能上线的版本接入通道用 TaoToken 的统一 Key省掉自己维护多套上游凭证的麻烦。2. TaoToken 前置准备统一 Key 与接入通道TaoToken 在这里扮演的角色是统一的上游模型接入层。你的 MCP Server 内部如果要调用模型能力比如工具执行后让模型总结结果不需要在代码里硬编码各家厂商的 Key而是通过 TaoToken 的统一 API 通道转发。这样换模型、加配额、做审计都只在一个地方改。先去官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册然后在控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建一个 API Key。这个 Key 就是后面Authorization: Bearer里要带的东西。API 基地址是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 base_url 使用。如果你打算长期跑编码类 Agent建议顺手看一下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它针对高频工具调用场景做了配额优化。只是想先验证模型通不通用模型对话 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 页面直接测就行。Key 的管理入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。拿到 Key 之后先写两个配置文件。第一个是config.toml放服务端参数# config.toml [server] host 0.0.0.0 port 8080 sse_ping_interval 15 # 心跳间隔秒 session_ttl 300 # 会话空闲超时秒 tool_timeout 30 # 单次工具执行硬超时秒 [upstream] base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 从环境变量读不写死在文件里 default_model claude-sonnet-4 [auth] require_tenant_header true jwt_algorithm HS256第二个是settings.json给客户端Claude Code 或你的 Agent 框架用{ mcpServers: { enterprise-mcp: { type: sse, url: http://127.0.0.1:8080/sse, headers: { Authorization: Bearer ${TAOTOKEN_API_KEY}, X-Tenant-ID: team-alpha } } } }注意X-Tenant-ID这个头它是后面多租户隔离的锚点。每个 Agent 实例配自己的租户 ID服务端据此决定它能访问哪些数据。Key 通过环境变量注入不要提交进 Git。3. 可复制配置鉴权中间件与流式响应骨架下面这份代码是完整的可运行骨架基于 Python 3.11 和 FastAPI。核心设计有三个用ContextVar做租户上下文隔离用asyncio.Queue做 SSE 下行通道用asyncio.wait_for做心跳和超时。# enterprise_mcp_server.py import asyncio import json import uuid from contextvars import ContextVar from typing import Any, Dict, List from fastapi import Depends, FastAPI, Header, HTTPException, Request, status from fastapi.responses import JSONResponse, StreamingResponse from pydantic import BaseModel app FastAPI(titleEnterprise MCP Server, version1.0.0) # 关键租户上下文用 ContextVar不用全局变量 current_tenant_id: ContextVar[str] ContextVar(current_tenant_id, defaultdefault) class SessionContext: def __init__(self, session_id: str, tenant_id: str): self.session_id session_id self.tenant_id tenant_id self.queue: asyncio.Queue asyncio.Queue() self.last_active asyncio.get_event_loop().time() active_sessions: Dict[str, SessionContext] {} class ToolDefinition(BaseModel): name: str description: str inputSchema: Dict[str, Any] REGISTERED_TOOLS: Dict[str, Any] {} TOOL_SCHEMAS: List[ToolDefinition] [] def mcp_tool(name: str, description: str, schema: Dict[str, Any]): def decorator(fn): REGISTERED_TOOLS[name] fn TOOL_SCHEMAS.append(ToolDefinition(namename, descriptiondescription, inputSchemaschema)) return fn return decorator mcp_tool( namequery_tenant_metrics, descriptionQuery production metrics for the authenticated tenant safely., schema{ type: object, properties: { metric_name: {type: string, description: e.g. qps, error_rate, latency}, time_range: {type: string, description: e.g. 1h, 24h, 7d}, }, required: [metric_name], }, ) async def query_tenant_metrics(metric_name: str, time_range: str 1h) - str: tenant current_tenant_id.get() return json.dumps({ tenant_id: tenant, metric: metric_name, time_range: time_range, data: {avg_value: 142.5, p99_latency_ms: 23.4, status: healthy}, }) async def auth_middleware( x_tenant_id: str Header(..., aliasX-Tenant-ID), authorization: str Header(..., aliasAuthorization), ) - str: if not authorization.startswith(Bearer ) or not x_tenant_id: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detailUnauthorized: Missing valid Bearer token or Tenant Header, ) current_tenant_id.set(x_tenant_id) return x_tenant_id鉴权中间件做了两件事校验 Bearer 格式把租户 ID 写进ContextVar。这里必须用ContextVar而不是模块级全局变量因为 FastAPI 的异步请求会在同一个事件循环里交错执行全局变量会被后一个请求覆盖导致租户串号。接下来是 SSE 端点和消息端点app.get(/sse) async def sse_endpoint(request: Request, tenant_id: str Depends(auth_middleware)): session_id str(uuid.uuid4()) session_ctx SessionContext(session_idsession_id, tenant_idtenant_id) active_sessions[session_id] session_ctx async def event_generator(): try: yield fevent: endpoint\ndata: /messages?session_id{session_id}\n\n while True: if await request.is_disconnected(): break try: msg await asyncio.wait_for(session_ctx.queue.get(), timeout15.0) yield fevent: message\ndata: {json.dumps(msg)}\n\n except asyncio.TimeoutError: yield : ping\n\n # 心跳防止中间层断连 finally: active_sessions.pop(session_id, None) # 无论怎么退出都清理 return StreamingResponse( event_generator(), media_typetext/event-stream, headers{ Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no, # 关键告诉 NGINX 不要缓冲 }, )X-Accel-Buffering: no这行是踩过坑才加的。NGINX 默认会缓冲响应SSE 事件会被攒在缓冲区里客户端半天收不到一条看起来像连接卡死。加上这个头NGINX 才会逐条透传。消息端点负责接收上行 JSON-RPC 请求把结果塞进对应会话的队列app.post(/messages) async def message_endpoint( request: Request, session_id: str, tenant_id: str Depends(auth_middleware), ): session_ctx active_sessions.get(session_id) if not session_ctx: raise HTTPException(status_code404, detailSession not found or expired) current_tenant_id.set(tenant_id) payload await request.json() method payload.get(method) req_id payload.get(id) if method initialize: await session_ctx.queue.put({ jsonrpc: 2.0, id: req_id, result: { protocolVersion: 2024-11-05, capabilities: {tools: {}}, serverInfo: {name: EnterpriseProductionMCPServer, version: 1.0.0}, }, }) return JSONResponse({status: accepted}) if method tools/list: await session_ctx.queue.put({ jsonrpc: 2.0, id: req_id, result: {tools: [t.model_dump() for t in TOOL_SCHEMAS]}, }) return JSONResponse({status: accepted}) if method tools/call: params payload.get(params, {}) tool_name params.get(name) arguments params.get(arguments, {}) fn REGISTERED_TOOLS.get(tool_name) if fn is None: response {jsonrpc: 2.0, id: req_id, error: {code: -32601, message: fTool {tool_name} not found}} else: try: result await asyncio.wait_for(fn(**arguments), timeout30.0) response {jsonrpc: 2.0, id: req_id, result: {content: [{type: text, text: str(result)}]}} except asyncio.TimeoutError: response {jsonrpc: 2.0, id: req_id, error: {code: -32001, message: Tool execution timeout}} except Exception as e: response {jsonrpc: 2.0, id: req_id, error: {code: -32000, message: fExecution Error: {e}}} await session_ctx.queue.put(response) return JSONResponse({status: accepted}) return JSONResponse({status: ignored})工具执行外面套了asyncio.wait_for30 秒硬超时。没有这层保护一个卡死的 SQL 查询会把整个工作线程池拖垮。异常也被捕获并转成 JSON-RPC 标准错误码进程不会因为单个工具报错而退出。4. 验证请求curl 走通完整链路服务启动后先验证鉴权是否生效。不带任何头直接请求应该返回 401curl -i http://127.0.0.1:8080/sse # HTTP/1.1 422 Unprocessable Entity缺少必填 Header带上正确的头和 Key建立 SSE 连接export TAOTOKEN_API_KEY你的Key curl -N -H Authorization: Bearer ${TAOTOKEN_API_KEY} \ -H X-Tenant-ID: team-alpha \ http://127.0.0.1:8080/sse-N关闭 curl 自己的缓冲你会看到第一行返回event: endpointdata 里带着 session_id。记下这个 ID另开一个终端发上行请求curl -X POST http://127.0.0.1:8080/messages?session_id刚才的ID \ -H Authorization: Bearer ${TAOTOKEN_API_KEY} \ -H X-Tenant-ID: team-alpha \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:tools/list}返回{status:accepted}后切回第一个终端应该能看到 SSE 流里推下来一条event: message内容是工具列表。再发一次tools/callcurl -X POST http://127.0.0.1:8080/messages?session_id刚才的ID \ -H Authorization: Bearer ${TAOTOKEN_API_KEY} \ -H X-Tenant-ID: team-alpha \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:2,method:tools/call,params:{name:query_tenant_metrics,arguments:{metric_name:qps,time_range:1h}}}SSE 流里会推回tenant_id: team-alpha的指标数据。把X-Tenant-ID换成team-beta再建一个新会话返回的tenant_id会跟着变证明租户隔离生效。如果两个会话的数据串了说明ContextVar没用对回去检查是不是写成了全局变量。5. 本篇常见错排查SSE 连上但收不到任何事件。九成是反向代理缓冲。检查响应头有没有X-Accel-Buffering: noNGINX 配置里对应 location 加proxy_buffering off;。另外确认Content-Type是text/event-stream写错成application/json客户端不会按流处理。会话莫名消失报 404 Session not found。看session_ttl和心跳间隔。如果sse_ping_interval大于中间层空闲超时很多网关默认 60 秒连接会被静默掐断但服务端finally已经清理了会话。把心跳设成 15 秒比网关超时短一截。多租户数据串号。最常见的原因是在异步函数里用了模块级全局变量存tenant_id。FastAPI 的请求在同一个事件循环里并发执行全局变量会被覆盖。必须用contextvars.ContextVar它在每个异步任务里有独立副本。工具报错导致整个进程退出。检查tools/call分支有没有把fn(**arguments)包在 try/except 里。任何未捕获异常在异步任务里都可能冒泡到事件循环。同时确认asyncio.wait_for的超时参数生效否则卡死的工具会一直占着协程。401 但头明明带了。检查Authorization的值有没有Bearer前缀加空格Header(..., aliasX-Tenant-ID)的别名大小写要和客户端发的一致。HTTP 头名不区分大小写但 FastAPI 的 alias 匹配是精确的写成x-tenant-id可能匹配不上。连接数上不去新会话建不了。看active_sessions有没有泄漏。正常关闭时finally会 pop但如果客户端异常断网没发关闭信号request.is_disconnected()要等下一次循环才检测到。配合session_ttl做定期扫描清理别只依赖断连检测。6. 下一步把 Key 和通道固定下来这套骨架跑通后鉴权、流式传输、状态管理三个环节就都有了可上线的底子。剩下的是把它接进你真实的工具集数据库连接池按租户路由、工具执行加沙箱、日志接 OpenTelemetry。这些都可以在现有结构上叠加不用推翻重来。接入层建议固定用 TaoToken 的统一 Key别在 MCP Server 代码里散落各家厂商的凭证。Key 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 管理接入规范看 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你要跑的是长期在线的编码 AgentCoding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 的配额模型更适合高频工具调用。想先确认模型通道通不通用模型对话 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 发一条测试请求最快。最后提醒一句config.toml里的api_key_env指向环境变量部署时用 K8s Secret 或容器编排的密钥管理注入别把 Key 写进镜像层。这一步做对后面换 Key、轮换凭证都不用改代码。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

【外设】之大彩串口显示屏 2026/11/13 0:43:49

【外设】之大彩串口显示屏

大彩串口屏初步使用 1 .官网下载 STM32 屏幕 GUI 设计资料 http://www.gz-dc.com/category/typeid/4112 找到 STM32 Keil 工程,移植相关代码因项目而异进行移植,由于项目简单,本人只对用到的指令接口进行修改。 比如:注意事项&…

阅读更多 →
无法下载Windows系统iso文件 2026/11/12 23:59:09

无法下载Windows系统iso文件

当我遇到这个问题的时候,我打开了一个网站: 登录 然后我打算下载的时候: 突然那个官方的连接就可以下载了:

阅读更多 →
【清华代码熊】DeepSeek V4.1 Flash 后训练详解 2026/11/12 9:50:02

【清华代码熊】DeepSeek V4.1 Flash 后训练详解

📌 上期解析了 DeepSeek V4.1 Flash 模型架构改进,本期解析 DeepSeek V4.1 Flash 预训练/后训练技术: 🌟 预训练:45T 文本 多模态混合语料、直接训练 sparse attention(取消 DeepSeek V4 的 dense 冷启动&…

阅读更多 →
Shuffle-R1: Efficient RL framework for Multimodal Large Language Models via Data-centric Dynamic ... 2026/11/12 9:25:38

Shuffle-R1: Efficient RL framework for Multimodal Large Language Models via Data-centric Dynamic ...

文章主要内容和创新点 主要内容 本文聚焦于多模态大语言模型(MLLM)强化学习(RL)训练中的效率问题,提出了一个名为Shuffle-R1的框架。研究发现,当前RL训练存在两个关键缺陷: 优势值坍缩(Advantage Collapsing):批次中大多数优势值集中在零附近,导致有效梯度信号被淹…

阅读更多 →
PRvL: Quantifying the Capabilities and Risks of Large Language Models for PII Redaction 2026/11/12 15:57:44

PRvL: Quantifying the Capabilities and Risks of Large Language Models for PII Redaction

一、文章主要内容总结 本文聚焦于利用大型语言模型(LLMs)实现个人身份信息(PII)脱敏的研究,旨在解决传统脱敏方法(如基于规则的系统、领域特定命名实体识别(NER)模型)泛化能力差、跨格式/跨语境适应性弱的问题。 研究通过全面评估多种LLM架构(包括密集型LLM(D-LLM…

阅读更多 →
LLaVA-RE: Binary Image-Text Relevancy Evaluation with Multimodal Large Language Model 2026/11/13 0:02:27

LLaVA-RE: Binary Image-Text Relevancy Evaluation with Multimodal Large Language Model

文章主要内容和创新点 主要内容 本文聚焦于二进制图像-文本相关性评估任务(判断图像与文本“相关”或“不相关”),针对该任务中文本格式多样、相关性定义随场景变化等挑战,提出了基于多模态大语言模型(MLLM)的解决方案LLaVA-RE。 模型设计:LLaVA-RE基于LLaVA 1.5架构,…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉