拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Memos MCP Server 源码解读:基于 OpenAPI 的进程内 MCP 端点设计与实现

Memos MCP Server 源码解读基于 OpenAPI 的进程内 MCP 端点设计与实现【免费下载链接】memosOpen-source, self-hosted note-taking tool built for quick capture. Markdown-native, lightweight, and fully yours.项目地址: https://gitcode.com/GitHub_Trending/me/memos本文基于 memos 仓库中server/router/mcp包的模块文档与源码完整讲解 memos 如何在单一/mcp端点上提供 OpenAPI 驱动的 Model Context ProtocolMCP服务从 OpenAPI 规范的嵌入式加载、$ref到自包含 JSON Schema 的解析、工具白名单与命名/注解规则到工具调用如何被翻译成进程内的 REST API 请求并复用原有认证体系。读完后你可以掌握“以 OpenAPI 为唯一事实来源”构建 MCP Server 的完整架构思路并能在现有 memos 实例上配置 Streamable HTTP 客户端接入或按文档流程安全地扩展新的 MCP 工具。MCP 端点总体定位memos 的 MCP 服务模块文档见 server/router/mcp/README.md在/mcp路径上提供一个由 OpenAPI 驱动的 MCP 端点基于官方 Go SDKgithub.com/modelcontextprotocol/go-sdk的Streamable HTTP传输对外暴露一套以 memo笔记为核心、精选curated的工具集。其核心设计原则是工具调用在进程内直接执行既有的 REST API。该包不拥有任何自己的 store 或服务层逻辑。每个工具都派生自生成的 OpenAPI 文档proto/gen/openapi.yaml通过proto.OpenAPIYAML()以字节形式嵌入二进制中的一个 operation工具调用会被翻译成对应的/api/v1/...HTTP 请求并在承载公开 API 的同一个 Echo 服务器上执行。这样做带来两个直接收益OpenAPI 始终是工具面tool surface的唯一事实来源工具描述、参数、请求/响应结构不会与 REST API 漂移直接复用 API 现有的认证与授权逻辑MCP 端点不需要维护第二套鉴权体系。与主服务的集成在 server/server.go 中server.NewServer在注册完 API、文件服务与 gRPC-gateway 路由之后调用mcp.NewMCPService并把同一个 Echo 实例传入mcpService, err : mcp.NewMCPService(profile, echoServer) if err ! nil { return nil, errors.Wrap(err, failed to create MCP service) } mcpService.RegisterRoutes(echoServer)该服务只通告tools能力——不暴露 prompts也不暴露 resources。这一约束在NewMCPService中体现得很直接构造时通过sdkmcp.NewServer创建服务实例然后逐个server.AddTool(tool, newMCPToolHandler(...))注册工具见 service.go。启动流程构造期失败优先fail fastNewMCPServiceservice.go在构造期完成全部装配任何不一致都会让服务启动直接失败而不是在运行期暴露解析嵌入式规范。loadMCPServiceOpenAPISpec将proto.OpenAPIYAML()返回的嵌入字节反序列化进openAPISpec结构若paths为空则报错。构建操作注册表。buildOperationRegistryopenapi.go遍历每个 path 与 method按operationId索引所有 operation并记录 method、path、解析后的请求体 schema 与解析后的 200 响应 schema出现重复operationId即为构造错误。挑选白名单工具。buildCuratedToolscatalog.go从注册表中选出curatedOperationIDs列出的 operation并将每一个转换为*sdkmcp.Tool加一个registeredOperation。白名单中存在注册表里没有的 ID、或两个工具解析出重名都会成为构造期错误。注册工具处理器。每个工具通过server.AddTool(tool, newMCPToolHandler(...))绑定到一个统一的 handler 闭包。包装 Streamable HTTP handler。sdkmcp.NewStreamableHTTPHandler以stateless、JSON 响应模式无 SSE、无会话跟踪包装服务器。源码中还有一处值得注意的细节构造 handler 时显式设置了DisableLocalhostProtection: trueservice.go。注释说明了原因memos 常部署在反向代理之后应用本身绑定回环地址而对外Host头是真实域名SDK 的 DNS-rebinding 防护会把这种部署形态误判为攻击并用403 invalid Host header拒绝全部请求。因此项目选择关闭 SDK 内置防护改为依赖 memos 自己的 Origin/Host 白名单下文“Origin 安全”一节来做 CSRF / DNS-rebinding 防护。服务名与版本同样在构造期确定Name固定为memosVersion取profile.Version缺省为devservice.go。请求流程从 /mcp 到进程内 API 调用RegisterRoutesservice.go通过echoServer.Any(/mcp, ...)绑定端点每个请求经过如下管线echoServer.Any(/mcp, func(c *echo.Context) error { request : c.Request() if !isAllowedMCPOrigin(request.Host, request.Header.Get(Origin), s.profile) { return c.NoContent(http.StatusForbidden) } s.handler.ServeHTTP(c.Response(), request) return nil }, middleware.BodyLimit(maxMCPRequestBytes))Origin 检查。isAllowedMCPOriginorigin.go拒绝不合法的跨域浏览器请求返回403判定规则见下文。请求体上限。SDK 读取之前请求体被middleware.BodyLimit(maxMCPRequestBytes)限制在 256 MiB。该常量直接复用 API 的上限const maxMCPRequestBytes int64 apiv1.MaxAPIRequestBytes而 server/router/api/v1/v1.go 中MaxAPIRequestBytes 256 20。之所以对齐是因为每个工具调用最终都要在进程内穿过同一套 API 路由。SDK 分发。Streamable HTTP handler 解析并分发 MCP 消息initialize、tools/list、tools/call等。解码参数。tools/call到达newMCPToolHandlerservice.go它把 JSON 参数解码为map[string]any解码失败即返回工具错误。参数校验。validateToolArgumentsvalidation.go按工具的 input schema 校验参数双层校验机制见“设计说明”。透传凭证。调用方的Authorization头从请求中读出SDK 的*sdkmcp.CallToolRequest上的request.Extra.Header。进程内执行。apiAdapter.executeadapter.go通过buildAPIRequest构造 API 请求——路径参数替换、query 编码、JSON body——然后转发 bearer token用httptest.ResponseRecorder作为响应载体直接在 Echo 服务器上执行该请求。解码与归一化。recorder 中的响应体被解码为 JSON非 2xx 状态转为工具错误newToolErrorResult否则由newStructuredToolResult包装成对象形态的结构化结果。buildAPIRequestadapter.go中还有两个实现细节值得展开嵌套资源名归一化。substitutePathParameters按占位符在路径中出现的顺序逐个解析并用已解析的父段重建前缀trimResourceNamePrefix/resolvedResourceNamePrefix因此像memos/abc123/reactions/reaction456这样的规范资源名只有在其父段与其他参数匹配时才被接受裸 ID 则原样透传每个占位符只从参数表解析一次并缓存避免值中再出现{时被二次展开。必填约束前移。缺少必填路径参数返回missing required path parameter ...缺少必填请求体返回missing required request body body——这两类错误在发出 API 请求之前就被拦截。API 错误信息由apiErrorMessageadapter.go组装为code reason phrase: api message形式例如404 Not Found: ...优先从响应 JSON 的message或error字段取原因。Schema 解析把 OpenAPI$ref变成自包含 JSON SchemaMCP 工具的 input/output schema 必须是自包含的 JSON Schema而 OpenAPI 的 components 大量使用$ref。openapi.go 的解析链resolveSchemaRef→resolveSchemaValue→resolveSchemaMap→addSchemaDef分三层处理顶层内联top-level inlining。操作的请求体 schema 与 200 响应 schema 以inlineRef true解析最外层的$ref被就地展开为完整组件定义resolveComponentSchema。嵌套引用改写为$defs。顶层之下遇到的每个$ref都被改写成本地#/$defs/Name指针被引用的组件则收集进$defs映射addSchemaDef。环安全cycle safety。递归的组件 schema 通过“先给defs[name]塞一个占位、并在递归前标记resolving[name]”来终止自引用——addSchemaDef中若resolving[name]已置位则直接返回保证自引用 schema 不会死循环。在注册表构建阶段openapi.go响应 schema 取 200 响应的application/jsoncontent若 200 响应没有 JSON body则回退到统一的 ok schema{ type: object, properties: { ok: { type: boolean } } }每个工具的 input schema 如何组装catalog.go的inputSchemaForOperationcatalog.go在操作级做二次组装path 与 query 参数升为顶层 properties必填参数进入required列表请求体收敛为单个body属性必填的 body 会让body进入requiredbody schema 内部的$defs被提升到整个 input schema 顶层的$defs按操作放宽资源级约束。requestBodySchemaOverridescatalog.go为 create 与 partial-update 请求体放松资源级 required 字段并从body: *形态的 schema 中移除已由 path 绑定提供的字段。例如MemoService_CreateMemo只要求contentMemoService_UpdateMemo清空 required 列表、剔除name属性、并用minProperties: 1阻止空 body——这样 memo 更新可以省略updateMask让 REST gateway 从请求体中出现的字段自行推断拒绝额外字段。组装出的 schema 一律设置additionalProperties: false把“未知字段”错误前移到校验层。validateOperationOverridescatalog.go还会在构造期校验三张按操作 ID 索引的表requestBodySchemaOverrides、idempotentOperationIDs、destructiveOperationIDs没有引用注册表中不存在的 operation防止 proto RPC 改名后旧键静默失配、导致某个操作悄悄丢失其 schema/注解覆盖。端点、传输与认证端点POST /mcpStreamable HTTP 传输下 SDK 也可能在同一路径使用GET/DELETEechoServer.Any即为此而设。传输Streamable HTTPstatelessJSON 响应无 SSE、无会话。请求大小SDK 分发前请求体上限 256 MiB与 API 上限对齐。认证调用方的Authorization: Bearer token头被原样转发给进程内 API 请求。因此写操作工具需要有效令牌个人访问令牌或 access token公开读取在无令牌时也可能成功——行为与 REST API 完全一致。Origin 安全isAllowedMCPOrigin的判定逻辑origin.goOrigin头缺失时放行桌面端客户端通常不带该头Origin 的 host 与请求Host头一致时放行只比较 host不检查 schemeOrigin 的 scheme 与 host 均匹配配置的profile.InstanceURL时放行其余一律403。这一层用来防御浏览器发起的 DNS-rebinding 攻击。客户端接入配置把任意 Streamable HTTP MCP 客户端指向https://your-instance/mcp并提供个人访问令牌作为 bearer 凭证。示例客户端配置{ mcpServers: { memos: { type: http, url: https://your-instance/mcp, headers: { Authorization: Bearer your-personal-access-token } } } }工具面Tool surface白名单、命名与注解服务器暴露的是一个精选白名单curatedOperationIDscatalog.go以 memo 与 attachment 为中心外加两个只读的“导航”工具memo_view_list_memo_views暴露用户命名的 CEL 过滤器供在memo_list_memos中复用和auth_get_current_user“whoami”让 agent 能解析自己对应的用户——这是唯一被允许的 auth/identity 操作OpenAPI operationMCP 工具MemoService_ListMemosmemo_list_memosMemoService_CreateMemomemo_create_memoMemoService_GetMemomemo_get_memoMemoService_UpdateMemomemo_update_memoMemoService_DeleteMemomemo_delete_memoMemoService_ListMemoCommentsmemo_list_memo_commentsMemoService_CreateMemoCommentmemo_create_memo_commentMemoService_ListMemoAttachmentsmemo_list_memo_attachmentsMemoService_SetMemoAttachmentsmemo_set_memo_attachmentsMemoService_ListMemoReactionsmemo_list_memo_reactionsMemoService_UpsertMemoReactionmemo_upsert_memo_reactionMemoService_DeleteMemoReactionmemo_delete_memo_reactionMemoService_ListMemoRelationsmemo_list_memo_relationsMemoService_SetMemoRelationsmemo_set_memo_relationsAttachmentService_ListAttachmentsattachment_list_attachmentsAttachmentService_CreateAttachmentattachment_create_attachmentAttachmentService_GetAttachmentattachment_get_attachmentAttachmentService_DeleteAttachmentattachment_delete_attachmentMemoViewService_ListMemoViewsmemo_view_list_memo_viewsAuthService_GetCurrentUserauth_get_current_user命名规则toolNameFromOperationIDcatalog.go去掉主题部分的Service后缀主题与方法两段均从 camelCase 转 snake_case用_连接。即MemoService_ListMemos → memo_list_memos。每个工具同时带有由名字推导的 Title每段首字母大写、空格连接以及保留在Meta中的operationId/method/path便于排查工具与实际 API 的对应关系。注解annotationsannotationsForOperationcatalog.go从 HTTP 方法推导基线MethodReadOnlyDestructiveIdempotentGETtruefalsetrueDELETEfalsetruetrue其他POST、PATCH…falsefalsefalse方法启发式判断不了的场合由按操作的覆盖表修正MemoService_SetMemoAttachments与MemoService_SetMemoRelations走 PATCH但语义上是声明式整体替换故同时报告IdempotentHint: true与DestructiveHint: trueMemoService_UpdateMemo也能覆盖既有字段故报告DestructiveHint: true。所有工具的OpenWorldHint均为false。注解只是给客户端的提示client hints不能替代 API 自身的授权。结果形态对象化的 structuredContent每个成功结果都携带对象形态的structuredContentnormalizeStructuredContentresult.goJSON 对象原样返回空响应变为{ ok: true }裸数组变为{ result: [...] }标量变为{ result: value }。这是刻意为之修复了上游 issue #6022——集合类工具返回裸数组会被严格的 MCP 客户端拒绝。信封内部则原样透传 API 的 JSON也就是说 gateway 自身的编码方式也是工具契约的一部分它输出的内容必须能通过由同一份 OpenAPI 规范解析出的 output schema 校验。由于 grpc-gateway 默认 marshaler 会对未设置的消息字段输出null而任何 schema 都未声明 nullableRegisterGateway在 server/router/api/v1/v1.go 中安装了省略null字段的自定义 marshalernewGatewayMarshaler。这修复了上游 issue #6139——motionMedia: null曾导致所有返回 attachment 的工具调用全部校验失败。错误处理失败一律以MCP 工具错误形式返回——CallToolResult置IsError: true并携带文本内容块——而不是 JSON-RPC 协议错误handler 返回(result, nil)。错误结果不携带structuredContentnewToolErrorResultresult.go因为每个工具都声明了只描述成功载荷的outputSchema严格客户端会把structuredContent拿去对照该 schema 校验一个{error: ...}对象会校验失败并掩盖真正的错误信息。失败场景结果参数不是合法 JSON工具错误解码信息参数未通过 schema 校验工具错误校验信息缺少必填 path 参数工具错误missing required path parameter ...缺少必填请求体工具错误missing required request body bodyAPI 返回非 2xx工具错误code reason phrase: api message如404 Not Found: ...由apiErrorMessage组装API 响应体不是可解码 JSON工具错误解码信息核心文件职责文件职责server/router/mcp/service.go构造 MCP 服务器、注册工具、构建 Streamable HTTP handler、绑定/mcp路由server/router/mcp/catalog.go精选 operation 白名单、工具命名、input/output schema 组装、方法派生注解server/router/mcp/adapter.go把工具调用翻译成/api/v1/...请求并在进程内打到 Echo 服务器server/router/mcp/openapi.go解析 OpenAPI 规范、构建操作注册表、把$refschema 解析为自包含 JSON Schemaserver/router/mcp/validation.go按工具的 input schema 校验工具调用参数server/router/mcp/origin.goOrigin头检查防御浏览器 DNS-rebindingserver/router/mcp/result.go把 API 响应归一化为对象形态structuredContent并构造错误结果新增一个工具的正确姿势把对应 OpenAPIoperationId加入 catalog.go 的curatedOperationIDs。若该 operation不在已生成的 OpenAPI 中先补齐 proto/API 面再重新生成cd proto buf generate扩展 catalog_test.go / service_test.go 覆盖新工具。红线是永远不要手改 proto/gen/openapi.yaml 等任何生成产物——修改 proto 定义后重新生成。由于工具面完全派生自这份生成产物proto 变更会经由注册表校验重复 operationId、白名单缺失、覆盖表键失配都会在构造期报错显式暴露出来。测试布局go test ./server/router/mcp/...openapi_test.go — 规范解析、注册表构建、$ref解析catalog_test.go — 工具挑选、命名、schema 与注解构建adapter_test.go — 请求构造与进程内执行adapter.go外加结果归一化与错误整形result.govalidation_test.go — 参数对 input schema 的校验service_test.go — Origin 头检查以及端到端 MCP 协议initialize、tools/list、tools/call确认structuredContent为对象形态。设计说明两个值得注意的机制双层入参校验。validateToolArgumentsvalidation.go先跑一个手写结构检查validateSchemaValue支持$ref本地解析、类型匹配、required、additionalProperties: false能给出诸如argument foo must be object、unknown argument bar这类友好提示再跑google/jsonschema-go完整校验器作为符合 JSON Schema 规范的兜底。前者负责“消息可读”后者负责“语义完备”。嵌入式 vs 文件加载。生产路径从proto.OpenAPIYAML()读规范loadMCPServiceOpenAPISpecservice.go而按文件路径读取的loadOpenAPISpecopenapi.go保留给测试使用二者解析到同一个openAPISpec结构。只做 Tools。当前版本的服务器不通告任何 prompts 或 resources 能力——这是一个边界清晰、以数据读写为核心的 MCP 面而非通用文档/提示词服务器。小结memos 的 MCP 端点展示了一条克制的集成路径不复制 store、不复制鉴权、不维护第二份 API 描述而是让 OpenAPI 生成产物直接派生出工具目录让工具调用以进程内 HTTP 请求的形式穿过既有 API 的全部中间件与授权逻辑。256 MiB 的请求上限、Origin 白名单、对象化structuredContent、省略null字段的 gateway marshaler 这些看似琐碎的细节本质上都在解决同一件事——让“严格客户端 生产部署形态”组合下的 MCP 调用稳定可用。对读者而言这套“OpenAPI 驱动 进程内执行”的架构也值得作为在其他 Echo/REST 项目上构建 MCP Server 时的参考样本。【免费下载链接】memosOpen-source, self-hosted note-taking tool built for quick capture. Markdown-native, lightweight, and fully yours.项目地址: https://gitcode.com/GitHub_Trending/me/memos创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门