惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 司徒正美
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
大猫的无限游戏
大猫的无限游戏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
爱范儿
爱范儿
V
V2EX
有赞技术团队
有赞技术团队
C
CXSECURITY Database RSS Feed - CXSecurity.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Hacker News
The Hacker News
WordPress大学
WordPress大学
T
Threat Research - Cisco Blogs
Scott Helme
Scott Helme
博客园 - Franky
T
Threatpost
TaoSecurity Blog
TaoSecurity Blog
V
Vulnerabilities – Threatpost
小众软件
小众软件
罗磊的独立博客
量子位
Attack and Defense Labs
Attack and Defense Labs
博客园 - 叶小钗
T
The Exploit Database - CXSecurity.com
Jina AI
Jina AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
L
Lohrmann on Cybersecurity
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
O
OpenAI News
S
Security @ Cisco Blogs
W
WeLiveSecurity
Help Net Security
Help Net Security
腾讯CDC
L
LINUX DO - 最新话题
酷 壳 – CoolShell
酷 壳 – CoolShell
Google Online Security Blog
Google Online Security Blog
SecWiki News
SecWiki News
月光博客
月光博客
Webroot Blog
Webroot Blog
Project Zero
Project Zero
V
Visual Studio Blog
A
Arctic Wolf
The Last Watchdog
The Last Watchdog
博客园 - 聂微东
www.infosecurity-magazine.com
www.infosecurity-magazine.com
J
Java Code Geeks
美团技术团队
S
SegmentFault 最新的问题
N
News and Events Feed by Topic

InfoQ - 促进软件开发领域知识与创新的传播

Meta 收购 Manus 这事儿泡汤了 5.5万 Star 开源项目 Ghostty 被迫出走,GitHub 正在终结一代技术人的乌托邦 Slack 长时运行多智能体系统的上下文管理方案 从 T+1 到分钟级:金城银行基于 Apache Doris 构建高可靠、强一致的实时数据平台 谷歌云推出 Agents CLI,简化 AI 智能体开发全流程 Claude官方击穿高薪、高学历的安全防线!Anthropic点名10大高危职业,但有群人暂时稳了 亚马逊云科技终止 WorkMail 服务,并将 App Runner 转入维护模式 OPPO小布记忆:全模态碎片化内容的理解与智能整理实践|AICon上海 模力工场038周AI应用周榜:工具在消失,工作流在出现 Akamai CEO Tom Leighton:Agent 时代来临,云基础设施正从“中心化”转向“分布式边缘” 日均数百亿入库背后:从“人肉调度”到K8s弹性架构,度小满金融基于OceanBase重构入库架构实践 百度文库网盘发布GenFlow 4.0:月活用户超1亿,要把网盘变成全端AI工作台 Altman 投的 Agent 终端 Warp 开源了!斩获3.5万star 哪些客户需要拒, 敢让龙虾决定吗?_AI&大模型_InfoQ 中文站_InfoQ精选视频 从开发到生产:为什么越来越多的机器学习团队纷纷迁移到 Snowflake | BUILD 2025_AI&大模型_王玮_InfoQ精选视频 探索多智能体工作流:LangGraph Snowflake Cortex AI | BUILD 2025_AI&大模型_王玮_InfoQ精选视频 腾讯云分布式缓存数据库:AI Agent - 从提示词工程到 Harness 工程 | 腾讯云数据库 DBTalk_腾讯_凌敏_InfoQ精选视频 基于 Streamlit 为 CSV 数据构建分析智能体 | BUILD 2025_AI&大模型_王玮_InfoQ精选视频 AI 智能体:告别文档缺漏 | BUILD 2025_AI&大模型_王玮_InfoQ精选视频 构建 AI 驱动的数据管道:深度探讨 Snowflake Openflow 与非结构化数据 | BUILD 2025_AI&大模型_王玮_InfoQ精选视频 云端太贵、本地不够聪明,英特尔押注“端云混合AI”:智能体PC会替人完成工作 不到10%的存储投入,可能拖垮90%的GPU投资!IBM把AI Agent塞进存储系统,算清企业最容易忽略的一笔账 Snowpark 上手实战 | BUILD 2025_大数据_王玮_InfoQ精选视频 ClickHouse + Langfuse,构建 Agent 可观测基石 腾讯云分布式缓存数据库:Cluster Proxy 共享连接架构深度解析 | 腾讯云数据库 DBTalk_腾讯_凌敏_InfoQ精选视频 AI 写代码太烧钱了:Copilot、Claude 一起涨价,不如把程序员请回来? 英特尔发布至强600系列工作站处理器与锐炫Pro B70 GPU,全新AI工作站来了 腾讯云分布式缓存数据库:从 Redis 到 Valkey - 开源社区如何快速创新 | 腾讯云数据库 DBTalk_腾讯_凌敏_InfoQ精选视频 印奇这次要“从0重做”智驾模型!首谈阶跃和千里双公司布局:中国AI商业闭环要靠车跑出来 从Cursor返聘归来,90后华裔女高管带Claude开启日更模式:token成本比工程师工资低多了! 从 Coding 到 Agent:QCon 北京 2026 全景复盘,优秀出品人 & 明星讲师名单揭晓 全链路支撑大模型国产化“Day 0适配”,商汤大装置构建全栈能力底座 凌晨,OpenAI 与亚马逊云科技史上最大联合发布来了 HashiCorp Vault 2.0 发布:引入新身份联邦机制,迈入 IBM 生命周期体系 Yelp 实现超 1,000 个 Cassandra 节点零停机升级 写了 17 年开源代码,我为什么认为 Coding Agents 堆功能是在瞎折腾? 基于 Apache Camel 编排智能体与多模态 AI 管道 面向智能体与人类用户的AI记忆系统:架构设计与核心场景实践|AICon上海 Anthropic 推出 Managed Agents,简化 AI 代理部署流程 阿里HappyHorse开启灰测,720P视频生成低至0.44元/秒 讯飞联合清华团队押注量子AI:不看营收、不设KPI,一群“无人区”科学家,抢夺下代AI算力入口 小米万亿模型全面开源:MIT 协议、1M 上下文,但还是打不过 DeepSeek Cortex Code 入门指南:面向数据工程师的实践路径 | 技术实践 openJiuwen社区首发Team Skills,定义Coordination Engineering新范式 用 Snowflake Cortex Agents 释放结构化数据的最大价值 | 技术实践 Grafana 利用 Kafka 对 Loki 进行了架构重构,并发布了一款命令行工具,旨在将可观测性引入编码代理 ClickHouse重构全文索引:对象存储上跑出高性能 Full-Text Search 可观测性和遥测技术如何提升软件工程实践 Dropbox 与 GitHub 合作,将单体库大小从 87GB 缩减至 20GB Agent 的下一站:基于长期记忆系统 EverOS 的自我演进|AICon上海 同一赛道,四种收费:Agent 控制层(Harness)开始分裂 Cloudflare Sandboxes 正式发布,为 AI 代理提供持久化隔离环境 Agent 的“记忆断片”困局,该怎么破?_AI&大模型_AICon 全球人工智能开发与应用大会_InfoQ精选视频 数据分析师如何快速建立在 AI 时代最值钱的能力:一份可落地的行动路线图 摩尔线程最新财报:研发占比超86%,万卡级大规模智算集群落地 当云区域失效:地缘动荡环境下的高可用重构 Slack 重构通知系统,设置参与度提升 5 倍 智能体工程的隐性技术债务 “我把所有模型都换成了DeepSeek V4”:月账单将降 90%,效果还更好 阿里云智能集团高级技术专家刘少伟已确认出席AICon上海站,并分享如何构建企业 Agent 的自动化行动架构 构建生产就绪的 tRPC API:Apollo Federation 的 TypeScript 替代方案 Anthropic推出面向Claude Code的基于智能体的代码审查功能 北京车展直击:斑马智能甩出车载Agent短剧,比亚迪率先落地,AI让智能座舱又热起来了 Snowflake 作为智能体运行时:从静态管道迈向自主数据系统 | 技术实践 Snowflake 上的本体体系:基于 Cortex Code 能力实现从架构到部署 | 技术实践 Cloudflare 公布 MCP 架构方案,应对企业面临的安全与治理风险 复杂的项目管理怎么做到「AI 友好」?飞书项目用「开放」给出答案 Snowflake Cortex Code 的规范驱动开发:将 SDLC 方法论引入 AI 辅助工作流 | 技术实践 Copilot 不让注册了:从“随便用”到“全面限”,agent 把原有订价模型顶穿了 当互联网用AI卷效率时,这家公司先问了一连串“能不能” Meta 开始记录员工每一次点击:AI 要接管工作,先监控会工作的人 Meta“Token榜”逼疯打工人,一夜烧掉公司几万刀!AI时代Token焦虑越来越离谱 智源FlagOS完成DeepSeek-V4-Flash在八款芯片Day0适配,实现三重技术突破 DeepSeek V4 重磅开源!首次打通华为Ascend,也没丢掉英伟达,百万上下文夺回国产模型话语权 李志飞的“新实验”:当超级个体撞上真实组织 GPT-5.5 登顶时刻,Anthropic 亲口承认 Claude 变笨了!网友群嘲:太敷衍 那些没空写的小需求,龙虾真能做吗?_AI&大模型_InfoQ 中文站_InfoQ精选视频 从 Pandas 到生产:使用任意 IDE 进行可扩展的 ML 数据管道与分布式处理 | BUILD 2025_AI&大模型_王玮_InfoQ精选视频 pnpm 11 候选版本发布,带来 ESM 分发、供应链默认设置以及新的存储格式 银行业PDF表格提取方案重构:基于Java的分层方案 GPT-5.5 赢了 Opus 4.7 和 Mythos?奥特曼晒黄仁勋内部信:英伟达全员用上 Codex! Cloudflare 推出 Think:一款面向 AI 代理的持久化运行时 1850亿美元天价支出、75%代码由AI生成!谷歌正式宣告:全面转向智能体工作流 xAI落后太多,马斯克“开大”重金求购Cursor,100亿美金“分手费”都敢签! Pulumi 新增对 Bun 运行时的全面支持 姚顺雨腾讯模型首秀!不卷参数只做 “听话打工人”,Hy3 preview登场 | 附实测 老板让你“忽悠”投资人,你敢发给龙虾吗?_AI&大模型_InfoQ 中文站_InfoQ精选视频 Gemini CLI 引入子代理机制,实现任务委派与并行代理工作流 清华系团队星工聚将完成数千万天使轮融资,轮式机器人拿下头部制造企业亿级大单 Pretext.js 绕过 DOM 布局重排,实现 120 FPS 的高级交互体验 靠“AI 云”爆红的 Vercel,栽在一个第三方AI工具手里!IPO前夕遭黑,200万美元赎金谈崩? 高能研讨会|端侧 AI 正在重写实时感知效率上限_AI&大模型_王玮_InfoQ精选视频 2050大会看这篇就够了|报名、交通食宿指引大全 Java 近期资讯:OpenJDK JEP、Jakarta EE 12、Spring Framework、Micrometer、Camel、JBang 金融智能的架构编排:基于 Snowflake Cortex Agents 实现结构化与非结构化数据统一分析 | 技术实践 在AK大神爆火的任务里,摸清国产AI真实水平 百灵Ling-2.6-flash 正式发布:高 Token 效率,以 1/10 消耗实现 SOTA 级 Agent 能力 当 PM 懂AI,当技术懂产品:AI 时代产品力的双向进化|PM x AI产品力领航者大会即将开幕 为 AI 智能体设计记忆机制:揭秘 LinkedIn 的认知记忆智能体 获奖名单公布|2026主题征文第一期|分享你最有价值的龙虾场景与核心 Skill_热门活动_InfoQ写作社区官方_InfoQ写作社区
在AWS上为百万企业级B2B平台构建安全的MCP服务器
作者:Shadi Elyafi张卫滨 · 2026-06-15 · via InfoQ - 促进软件开发领域知识与创新的传播

引言

模型上下文协议(Model Context Protocol)让 LLM 客户端接入既有系统变得更加容易,但多数示例仍停留在“Demo 看起来有意思”为止。更难的问题在于,当相同的集成触及真实的业务数据、真实流程和真实运维约束时,会发生什么?

在我们的场景中,我们希望通过 MCP 服务器把一个基于超过 100 万家企业档案构建的 B2B 情报平台暴露给 LLM 客户端。面向用户的想法很简单:用户无需打开门户、输入查询、人工筛选、手动导出,只需提出结构化的请求,例如,“查找德国 50-200 人规模的 SaaS 公司”,就能在 LLM 客户端获得结果。但是,工程方面的问题远远不是这么简单的,如何让这一流程可用,同时避免把 LLM 与生产数据的连接成不安全的通道呢?

这个问题从一开始就决定了其实现方式。我们没有把 MCP 服务器当作现有 API 的便捷包装层,而是将其视为一等接口,并为其单独定义契约、安全假设、测试策略和运维控制。

由于底层平台服务提供的是 100 万家企业的档案,所以,MCP 层从一开始就必须按照可扩展系统来进行设计,而不是轻量级的实验性集成。在这个规模下,清晰的工具边界、可预测的请求处理和可审计性不仅关乎安全,也直接关系到工程师与用户能否理解系统的行为。

架构设计

平台原本已经通过 AWS AppSync 上的GraphQL对外提供数据,这为读取业务对象提供了清晰的后端边界。我们另外构建了一个基于 Go 的 MCP 服务器,把用户请求转换为一组边界明确的工具,而不是把业务逻辑推给 LLM 层。实现上使用了 mcp-go、面向 AppSync 的 GraphQL 客户端,以及覆盖搜索、AI 辅助搜索和集合操作的工具层。

该架构带来了两点收益。首先,AppSync 可以继续作为后端访问的系统事实来源,避免 MCP 层演化为临时拼接的集成面。其次,工具层可以围绕明确的职责进行设计,使系统更易测试,也更易推理。

实践证明,这套架构比协议本身更重要。MCP 只提供了连接模型,真正的生产问题在于每个工具被授予了多大的能力,以及其行为的定义是否足够精确。如果工具契约模糊,MCP 服务器将难以验证、难以观测,也容易被误用。

从实现层面看,MCP 服务器承担的是“契约执行层”而非“透传代理”的角色。用户请求先被规范化为显式的工具调用,再映射到边界受控的 GraphQL 操作,响应结构也保持足够收敛,从而可独立于 LLM 客户端进行校验。这种分层降低了提示词歧义直接泄露到后端行为的风险。

端到端的请求流

当用户通过 LLM 客户端发起请求时,系统按以下层次依次进行处理:

  • LLM 客户端通过 MCP 的 stdio 传输发送工具调用,并携带 JSON 参数(例如,{"query":"SaaS companies in Germany","country":"DE","limit":20})。MCP 服务器通过mcp-go库接收调用,并将其分发给已注册的工具处理器。

  • 参数解析与校验。每个工具先把原始的map[string]any参数通过 JSON 序列化/反序列化解析为类型化的 Go 结构体,再执行校验:必填字段检查、limit 封顶检查(比如,最大值为 100)、输入裁剪与规范化。对写工具会先检查mutationsAllowed布尔值。如果校验失败,立即返回错误,不触达后端。

  • 执行 GraphQL。工具构建变量映射并调用client.Execute(ctx, query, variables, &result)以访问 AppSync。GraphQL 客户端统一处理认证(OIDC Bearer Token、API Key 或 AWS SigV4 签名)和 HTTP 层错误(401/404/429)。

  • 响应整理。GraphQL 响应会先反序列化为内部类型(比如,gqlCompanyModelV2gqlCollection),再映射为扁平、对 AI 友好的公开类型(如CompanySummaryCompanyDetailCollection)。搜索/读取类工具通过toCompanySummary()等函数实现扁平化;写入工具会返回最小化的结果结构(比如,add_to_collection仅返回{collection_id,success})。

  • 序列化与返回。结果被序列化为 JSON,并作为CallToolResult文本内容块返回给 LLM 客户端。

关键点在于校验、执行、响应整理分别由独立的层来进行处理。MCP 服务器既不会把原始用户输入直接传给 GraphQL,也不会把原始 GraphQL 响应直接回传给客户端。

工具清单

实现中定义了 9 个工具,按能力分组如下:

只读工具(6 个)

可变更工具(3 个,均受到--allow-mutations 门控的控制)

在上线时的配置中,实际暴露了上述 9 个工具里的 8 个。create_collection在集成测试暴露了后端 Lambda 的错误后,已经从注册路径中移除。

读写能力不应混在一起

最重要的实现决策之一,就是从一开始就要分离读操作与写操作。许多早期 MCP 示例会把工具的功能做得很宽泛:既能搜索也能更新,还能编排动作。在原型阶段这也许是可以接受的,但一旦接口连接真实的系统,就会产生不必要的歧义。

当模型可触达业务数据时,“查询”和“修改”不能只靠约定区分,而应在工具设计层面硬性分离。在我们的实现中,只读路径会保持只读,所有可变更动作默认都会阻断。

这一策略不仅更安全,也更易于维护。只读工具更易审查、更易测试、更易观测,因为它表达的是单一意图而非多重意图。实践中,严格的契约比在过度灵活的工具上的事后补救策略更可靠。

这一点在百万级数据集上尤为关键。即便是微小的工具行为歧义,也可能放大为结果混乱、查询范围过宽或运维误判。严格保持只读路径为只读,显著降低了早期落地阶段需要“盲信”的行为范围。

读写工具如何分开注册

读写分离要在注册层面强制执行。创建工具注册中心时,会把allowMutations标记传入每个可变更工具中;只读工具不接收这个标记,因为它们不存在写路径:

func NewRegistry(gqlClient graphql.Client, allowMutations bool) *Registry {    return &Registry{        gqlClient:          gqlClient,        // Read-only tools        searchCompanies:    NewSearchCompaniesTool(gqlClient),        getCompany:         NewGetCompanyTool(gqlClient),        getCompaniesBatch:  NewGetCompaniesBatchTool(gqlClient),        aiSearch:           NewAISearchTool(gqlClient),        listCollections:    NewListCollectionsTool(gqlClient),        getCollectionItems: NewGetCollectionItemsTool(gqlClient),        // Mutation tools        createCollection:      NewCreateCollectionTool(gqlClient, allowMutations),        addToCollection:       NewAddToCollectionTool(gqlClient, allowMutations),        requestEmailDiscovery: NewRequestEmailDiscoveryTool(gqlClient, allowMutations),    }}

复制代码

每个写工具都在内部保存该标记,并在Execute入口最先进行检查:

func (t *CreateCollectionTool) Execute(ctx context.Context, params CreateCollectionParams) (*CreateCollectionResult, error) {    if !t.mutationsAllowed {        return nil, fmt.Errorf("mutations are disabled; use --allow-mutations flag to enable write operations")    }    // ... validation and execution follow}

复制代码

这样一来,不管 LLM 客户端主观意图是什么,只要未打开该标记,写工具都会立刻、可预测地失败。

读写工具示例

只读工具示例:search_companies

输入契约:

type SearchCompaniesParams struct {    Query   string `json:"query"`             // required    Country string `json:"country,omitempty"` // ISO 3166-1 alpha-2 or full name    Limit   int    `json:"limit,omitempty"`   // default 10, max 100}

复制代码

输出结构:

{  "companies": [    {      "id": "example.com",      "name": "Example Inc",      "description": "Cloud infrastructure provider...",      "country": "United States",      "countryCode": "US",      "locality": "San Francisco",      "employeeCount": 150,      "employeeRange": "101-250",      "domain": "example.com",      "industryTags": ["Technology", "Cloud Computing"]    }  ],  "total": 42}

复制代码

该工具会校验 query 非空、把 limit 属性封顶到 100、解析国家代码(例如,把DE映射为countries;Germany),并把嵌套的 GraphQL 响应扁平化为CompanySummary记录。

写工具示例:add_to_collection

输入契约:

type AddToCollectionParams struct {    CollectionID string   `json:"collection_id"` // required    CompanyIDs   []string `json:"company_ids"`   // required, non-empty}

复制代码

输出结构:

{  "collection_id": "col-abc-123",  "success": true}

复制代码

该工具先检查mutationsAllowed,再校验参数并执行基于 itemType 执行 GraphQL 变更操作(itemType 为COMPANY)。该操作具备幂等性,也就是向集合重复添加已存在的公司也会成功,不会报错。

默认拒绝变更操作

我们在所有写能力前引入了显式的--allow-mutations标记。这不是完整的授权模型,但它是一个有效的控制点:迫使团队在每个环境中明确决定是否开放写路径。

这之所以重要,是因为 LLM 集成通常从实验起步,随后逐步承载运维方面的期望。一旦系统变得有价值,通常会出现“让它多做一点”的压力。如果初始设计默认就是宽泛地进行访问,那么这种压力往往会演化为通往不安全的捷径。

默认拒绝会改变讨论方式。团队不会再问“为什么不让客户端执行写入操作?”,而是必须问“在允许这类写操作之前,我们还需要哪些证据?”这个问题更工程化,因为它把权限扩张与证据、控制和运维成熟度绑定了起来。

该标记也构建了实验环境与生产环境之间的实用边界:当安全优先级高于便利性时,写能力必须成为有意识的选择,而不是随着功能增强悄然出现的默认行为。

--allow-mutations 如何实现

该标记在Cobra CLI中注册,默认值为 false:

serveCmd.Flags().BoolVar(&allowMutations, "allow-mutations", false,    "Enable write operations (create collections, add items, etc.)")

复制代码

服务启动时,该值会传入工具注册中心:

toolsRegistry := tools.NewRegistry(gqlClient, allowMutations)

复制代码

启动日志会记录该标记的状态和其他配置:

level=INFO auth=oidc mutations=false tools=8 resources=2 prompts=2

复制代码

当前实现没有在该标记后再添加额外的授权判断,它是一个二元值的门控:要么所有写工具可执行,要么全部不可执行。这是初始版本有意采取的选择,因为简单、可见的控制通常优于容易误配的复杂机制。后续可以在使用模式稳定后叠加按工具或按用户的细粒度授权。

在 MCP 客户端配置的.mcp.json中,这个标记通过 CLI 参数传入,便于在审查集成配置时直接可见:

{  "mcpServers": {    "mcp-server": {      "command": "/path/to/mcp-server",      "args": ["serve", "--endpoint", "https://api.example.com/graphql",               "--region", "eu-west-1", "--allow-mutations"]    }  }}

复制代码

工具契约比工具数量更重要

一个关键的经验是,工具数量并非核心,工具形态才是。如果每个工具都有明确的输入模型、受限的输出结构和较小的运维面,那么 9 个功能受限的工具往往比两个通用工具更安全。尤其在面对概率式客户端且同一意图可能被不一致表达时,这一点更为明显。

严格的契约能对冲这种不确定性,让故障更易诊断、结果更易校验、变更更易管理。相反,宽松的工具往往会逐渐演化为后端的遥控器层,并在长期内同时恶化安全性与可维护性。

它还会影响版本演进。严格定义的工具通常可以在稳定契约后平滑演进;宽松的工具则会积累大量的隐式依赖(提示词行为与用户预期),小幅度的改动也可能产生难以预测的回归测试问题。

在这个规模下,输出边界与输入边界同样重要。持续返回一致的结构,有助于评估结果质量、降低意外,并使测试、日志与客户端解释保持可预测性。

在这 9 个工具中,search_companies是很好的参考契约,因为它在单一路径里覆盖了三件事:输入校验、输入规范化(国家代码解析与 limit 封顶设置)以及把嵌套的 GraphQL 响应扁平化为单层记录。

本地验证是生产级的问题

我们在实现中使用 aws-vault 进行认证,并借助MCP Inspector在脱离 LLM 客户端的情况下对工具进行验证。初始看上去这像是开发便利性方面的选择,但实质上是可靠性方面的选择。

如果工程师只能通过最终的客户端验证 MCP 服务器,调试会更慢、接口更不透明。本地检查路径可以直接查看请求/响应的结构、确认工具的行为,并在引入 LLM 前隔离失败点,从而降低端到端测试中的歧义。

良好的本地闭环还能降低“做对的事”的成本。团队更容易保持测试及时更新、契约收敛。相反,本地闭环如果非常痛苦的话,其结果通常不是纪律性变强,而是系统漂移。

面向生产的流程中,本地验证还能确认“失败是否可理解”,避免叠加 LLM 不确定性后故障表达失真。在后端已经有清晰的契约后,MCP 层的职责是维护这些契约而不是削弱它们。

认证流程

MCP 服务器通过平台的身份提供者(identity provider)所签发的 OIDC Bearer Token 向 AppSync 进行认证。每个 Token 都会绑定到已认证用户、短有效期且仅覆盖该用户被授权的操作。AppSync 通过@aws_oidc在 resolver 层执行认证,因此过期或无效 Token 会在 resolver 逻辑之前就被拒绝。

这一点对 MCP 服务器尤为重要,因为 LLM 客户端是代表具体用户而不是通用服务执行操作。如果服务器使用静态 API Key 或共享服务凭证的话,无论请求由谁发起,它都会携带同样的权限。使用 OIDC 后,Token 内会含有用户标识,后端授权、轨迹审计和数据范围都可以与用户直接调用 API 保持一致。MCP 层不会绕过或削弱既有的访问模型,而是继承它。

从运维角度来看,短期 Token 也能降低凭据泄露的爆炸半径。泄露的 API Key 在轮换前会一直有效,而泄露的 OIDC Token 通常在数小时内会自动过期。对于可触达百万企业档案的 MCP 服务器来说,这一差异非常关键。

服务会在启动时记录当前的认证方式,便于运维在避免探查流量的情况下核对配置:

level=INFO auth=oidc mutations=false tools=8

MCP Inspector 验证

MCP Inspector 通过 stdio 连接服务器,让工程师在不经过 LLM 客户端的情况下直接发起工具调用。典型验证会话如下:

成功请求示例

> Tool: search_companies> Args: {"query": "fintech", "country": "GB", "limit": 5}< Result: {"companies": [{"id": "revolut.com", "name": "Revolut", ...}], "total": 127}

复制代码

失败请求示例(变更被阻断)

> Tool: add_to_collection> Args: {"collection_id": "col-123", "company_ids": ["revolut.com"]}< Error: "mutations are disabled; use --allow-mutations flag to enable write operations"

复制代码

第二种场景在开发期的价值尤其明显,它验证了变更门控返回的是明确、有效的错误信息,而非模糊的失败,这对 LLM 客户端向用户解释失败原因时非常重要。

把 MCP 层当作独立接口来进行测试

常见误区是,如果后端 API 可用的话,那么 MCP 层主要就是传输的问题。实际情况并非如此,MCP 层会引入自己的故障模式,比如,脆弱的校验、工具语义不明确、错误处理不佳、工具行为与后端假设不匹配。因此我们把它视为独立的接口,并设计了独立的测试策略。

实现采用 TDD 的方式,单元测试层面使用 Mock GraphQL 客户端测试工具的逻辑,同时通过 MCP Inspector 对真实 AppSync 端点做手工验证。Mock 有助于隔离逻辑和覆盖边界;真实后端验证则确认真实线路能够在实际条件下符合预期。

负向测试同样关键。只验证“合法请求能成功”远远不够,还必须验证“非法输入会明确失败、应该阻断的变更要确实被阻断、工具错误不会诱导混乱或不安全的重试”。这类测试的重点不是抽象意义上的正确性,而是契约纪律。相比“宽容但不可控”的工具,“可预测失败”的工具通常更安全。

分层测试很重要,因为 MCP 服务器不只是做请求的翻译,还在执行安全边界。单元测试会验证工具的局部行为,MCP Inspector 则会验证真实流里 AppSync 边界、认证路径和响应契约是否仍能协同成立。

Mock GraphQL 客户端设置

Mock 客户端基于Testify Mock实现,并复用与真实客户端一致的 graphql.Client 接口:

type MockClient struct {    mock.Mock}func (m *MockClient) Execute(ctx context.Context, query string, variables map[string]any, result any) error {    args := m.Called(ctx, query, variables, result)    return args.Error(0)}

复制代码

在测试中,Mock 可直接向 result 指针注入响应,完全绕过 HTTP:

func TestSearchCompanies_ValidQueryReturnsResults(t *testing.T) {    mockClient := graphql.NewMockClient()    tool := NewSearchCompaniesTool(mockClient)    mockClient.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).        Run(func(args mock.Arguments) {            result := args.Get(3).(*gqlSearchCompaniesResponse)            result.EnhancedSearchCompanies.Companies = []gqlCompanyModelV2{                {ID: "example.com", CompanyModel: gqlCompanyModel{                    Company: gqlCompany{Name: "Example Inc", Domain: "example.com"},                    IndustryTags: []string{"Technology"},                }},            }            result.EnhancedSearchCompanies.TotalResults = 1        }).        Return(nil)    result, err := tool.Execute(context.Background(), SearchCompaniesParams{        Query: "tech companies", Limit: 10,    })    require.NoError(t, err)    assert.Len(t, result.Companies, 1)    assert.Equal(t, "example.com", result.Companies[0].ID)}

复制代码

这种方式还可捕获发送给 GraphQL 的变量,用于验证规范化逻辑是否在请求进入后端前已生效:

var capturedVariables map[string]anymockClient.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).    Run(func(args mock.Arguments) {        capturedVariables = args.Get(2).(map[string]any)        // ... set result    }).Return(nil)// After execution:limit := capturedVariables["limit"].(int)assert.Equal(t, 100, limit, "limit should be capped to 100")

复制代码

该技术首先暴露了国家代码的映射错误,早期版本把US直接传给 GraphQL,而 API 实际要求的是countries;United States这样的格式。由于后端对错误的过滤会返回空集,如果仅断言输出结构,Mock 测试仍会“看起来能够通过”。

TDD 示例:变更门控

变更门控逻辑采用测试先行的方式。在实现前,先要编写测试定义的预期行为:

步骤 1:失败测试

func TestCreateCollection_BlockedWithoutMutationsFlag(t *testing.T) {    mockClient := graphql.NewMockClient()    tool := NewCreateCollectionTool(mockClient, false) // mutations NOT allowed    result, err := tool.Execute(context.Background(), CreateCollectionParams{        Name: "Test Collection",    })    require.Error(t, err)    assert.Nil(t, result)    assert.Contains(t, err.Error(), "mutations")    assert.Contains(t, err.Error(), "--allow-mutations")    mockClient.AssertNotCalled(t, "Execute") // must not reach GraphQL}

复制代码

最初,该测试会失败,因为 Execute 中尚无变更检查。

步骤 2:实现修改

func (t *CreateCollectionTool) Execute(ctx context.Context, params CreateCollectionParams) (*CreateCollectionResult, error) {    if !t.mutationsAllowed {        return nil, fmt.Errorf("mutations are disabled; use --allow-mutations flag to enable write operations")    }    // ... rest of implementation}

复制代码

步骤 3:测试通过

加入守卫后,测试通过:错误信息同时包含mutations--allow-mutations,result 为 nil,更关键的是mockClient.AssertNotCalled(t,"Execute")证明了在触达后端前就已阻断。

失败模式与得失总结

成功之处

读写分离很快产生了收益。当我们把 MCP 服务器接入 LLM 客户端时,只读工具具备了较高的可预测性,不再需要在早期测试中反复担心“模型是否改动了什么内容”。这一类问题在架构层面直接消除了。

Mock 测试中的变量捕获能提前发现规范化的问题。通过断言实际发送给 GraphQL 的变量(而不是只看最终输出),我们识别了国家代码未被正确解析、limit 未在进入后端前设置封顶值等问题。如果仅校验输出结构,这些问题就会被掩盖。

扁平化响应类型让 LLM 的行为更具有可预测性。把嵌套 GraphQL 类型(gqlCompanyModelV2 -> companyModel -> company -> description.text)转换为扁平化的结构(例如,含单一描述字符串的CompanySummary)后,模型解释与呈现结果的波动显著降低。

失败或尚需重新设计的地方

从活跃工具集中移除create_collection是必要的。它在 Mock 单元测试中表现正常,但在通过 MCP Inspector 接入真实 AppSync 时暴露了后端 resolver 中的 Lambda 空指针错误,最终该工具从注册路径中注释掉了。这是一个典型的案例,仅对 MCP 层本身进行测试还不够,真实的后端验证是不可替代的。在我们的 dev-team-a 测试环境中,该错误可稳定复现,因此我们选择直接移除对该工具的注册,而不是将其隐藏在变更标记之后,让这个问题悬而未决。

AI 搜索的限流需要前置面向会话的模式设计。默认 5 次/分钟的阈值会较为保守,但工具从一开始就采用了可配置的AISearchConfig,因为我们预期多轮对话(基于当前会话连续追问)会快速达到这个峰值。提前实现环境级的可配置,避免了首次真实使用就必须修改代码来调整限流。之所以选 5 次/分钟,是因为典型的连续澄清通常在短时间内触发 2-4 次追问,略高于该范围既能支持真实的多轮场景,也能抑制失控的 LLM 循环。

与规模相关的设计约束

面对超过 100 万条的档案时,宽泛的查询从一开始就必须受到限制。以“companies”这类无国家过滤的查询为例,它的结果可能覆盖整个库。即使工具层硬限制返回 100 条(默认 10 条),依然可能出现“范围过宽且几乎无用”的场景,并诱发 LLM 客户端持续追问导致请求范围进一步扩散。基于此,我们在部署前做了两项决策,首先,在search_companies中内置类别过滤体系,让国家和其他约束在到达后端前先收敛范围;其次,为ai_search设置独立的通道与内置限流,使范围更难预测的自然语言查询可以单独节流。例如,早期一个裸请求{query:"companies"}会命中百万级数据并返回近似随机的 10 条结果;而受约束版本会在工具契约层引导 LLM 至少提供位置类过滤后再访问 AppSync。

日志与运维可见性

我们从设计之初就纳入了请求日志,因为 MCP 服务器被视为连接业务数据的真实网关而非实验性的脚手架。该接口存在之后,团队需要看到:哪些工具在被调用、它们在什么条件下被调用、结果如何。如果缺乏这些可见性,就很难判断工具边界是否合理,或者使用模式是否正漂移到不安全的方向。

传统后端团队普遍能够接受日志、追踪和监控的必要性,而 LLM 系统经常因为探索优先的理念而推迟这件事。但是,它的代价通常很高。越早把可观测性放入 MCP 层,越容易理解真实使用的情况,从而收紧契约,并评估何时应扩大能力边界,这也更利于后端、平台与产品团队基于客观证据的协作。

在服务百万企业档案的平台上,日志不仅能够用于事后排查故障,更是大规模场景下纪律的一部分:它有助于区分有价值的严格请求,以及过于宽泛、频繁、成本过高且不宜通过 LLM 接口安全承载的调用模式。

尚需完善的功能

在当前的实现中,我们使用 Go 结构化日志包slog输出到 stderr(stdout 预留给 MCP JSON-RPC 协议)。服务启动时会以结构化字段的方式记录配置:

level=INFO auth=oidc mutations=false tools=8 resources=2 prompts=2

这条启动日志已经足以验证认证方式、变更标记状态和注册工具的数量。例如,看到tools=8而不是tools=9,那么就可以立即确认create_collection未注册。

除启动信息外,当前实现尚未记录每个请求工具的调用和请求级遥测信息。现有提供的是类型化响应错误,它们会通过 MCP 协议返回给 LLM 客户端,包含必要的诊断信息:

  • GraphQL 层错误:GraphQL 客户端传播UnauthorizedErrorNotFoundErrorRateLimitErrorGraphQLError等类型化错误,并包含 HTTP 状态码和错误消息。

  • 限流错误:触发限流时错误消息会带上配置值(比如,maximum 5 requests per 1m0s),调用方可快速定位原因。

  • 变更门控错误:未启用--allow-mutations调用写工具时,错误会同时给出问题和修复动作(mutations are disabled; use --allow-mutations flag to enable write operations)。

这些错误对调试很有帮助,但它们不等于运维遥测。生产部署仍建议补齐针对每条请求的结构化日志,独立记录工具名、延时、输入格式、结果与错误类型。由于当前工具契约已经比较收敛、错误类型也已明确,这项增强会更容易落地。

实践建议

在云系统上构建 MCP 服务器的团队,应该先默认 MCP 层是一等接口:优先设计范围较窄的工具、尽早实现读写分离、克制对“先做灵活性”的冲动。如果一个工具看起来太宽泛,那这样的判断基本就是准确的。

要有意识地利用后端边界。在本实现中,AppSync 与 GraphQL 为 MCP 服务器与系统事实层之间提供了清晰地分隔,使整体设计更容易组织和测试。

把本地验证纳入日常流程中:先利用检查工具与 Mock 客户端验证工具的逻辑,然后在接入真实 LLM 客户端前对真实后端做验证。最后,将日志、审计性与变更控制视为基线要求,而不是后续“再加固”的可选项。

结论

MCP 的价值在于,它降低了 LLM 客户端连接既有关键系统的门槛。因此,生产团队应该更为谨慎。核心挑战不只是“让模型能调 API”,而是“让接口足够收敛、可观测、可测试,并且足够安全以承载真实的业务流程”。

在这个案例中,答案不是某个单一安全功能或提示词的技巧,而是一组被持续贯彻的工程选择:清晰的工具边界、默认的阻断变更、显式认证、本地检查、分层测试与运维可见性。对正在从 MCP Demo 走向生产化系统的团队而言,这些基础仍是最值得优先做好的部分。

查看英文原文:Building a Secure MCP Server on AWS for a Million-Company B2B Platform