Bytebase MCP 服务中 search_api 工具的 Schema Lookup 能力设计与实现解析

Bytebase MCP 服务中 search_api 工具的 Schema Lookup 能力设计与实现解析 Bytebase MCP 服务中 search_api 工具的 Schema Lookup 能力设计与实现解析【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase本篇文章基于仓库 docs/plans/2025-12-15-mcp-schema-lookup.md 这份实现计划文档结合 Bytebase 仓库backend/api/mcp下的真实源码与测试完整还原了 MCPModel Context Protocol服务中search_api工具新增 schema 查找模式Schema Lookup与 protobuf 描述精简Description Truncation的全过程。读者将掌握如何为 MCP 工具扩展输入参数、如何基于 libopenapi 构建 OpenAPI 组件 Schema 索引并支持全名/短名/枚举三种查找方式、如何用简短类型描述替换冗长的 protobuf 文档以及如何在 MCP 客户端中以search_api → call_api的链路精准构造 API 调用。全文所有代码均取自当前仓库实际实现可对照源码逐行验证。背景为什么需要 Schema LookupBytebase 将自身 API 以 MCP 服务器形式暴露给 AI Agent核心工具链是search_api发现端点与call_api执行调用。在引入 schema 查找之前search_api已经支持四种模式无参列出全部服务、service浏览某服务下所有端点、operationId查看某个端点的请求/响应 Schema、service query在服务内搜索。这份实现计划指出两个真实痛点Agent 无法直接查看消息类型Message Type的定义。operationId模式只能看到某个端点请求/响应体中的字段当 Agent 需要构造嵌套对象例如向CreateInstance传入一个bytebase.v1.Instance对象时它并不知道Instance类型本身有哪些字段、哪些必填、字段类型是什么。protobuf 文档过于冗长。OpenAPI 规格由 protobuf 生成后google.protobuf.Timestamp、Duration等内建类型携带大段官方文档如 A Timestamp represents a point in time...这些冗长描述会占用 Agent 宝贵的上下文窗口却几乎不提供可执行信息。因此计划的Goal明确为为search_api增加schema参数用于查找消息类型并精简冗长的 protobuf 描述。技术栈为 Go、libopenapi、MCP SDK。整体架构OpenAPIIndex 与 search_api 的分工在进入具体任务前先看两个核心文件的分工backend/api/mcp/openapi_index.go —— 负责在服务器启动时解析内嵌的 OpenAPI 规格//go:embed gen/openapi.yaml见 backend/api/mcp/gen/openapi.yaml构建OpenAPIIndex结构按 operationId、service、关键词建立索引并提供GetEndpoint、GetServiceEndpoints、Search、GetRequestSchema、GetSchema等方法。backend/api/mcp/tool_search.go —— 负责search_api工具的注册与请求分发handleSearchAPI依据输入参数的优先级OperationIDSchemaService选择不同的格式化输出函数。计划中 Task 1 的落点正是这两处向OpenAPIIndex增加GetSchema方法并让handleSearchAPI增加schema分支。OpenAPIIndex使用 libopenapi当前仓库go.mod中版本为v0.38.7解析 OpenAPI v3 文档通过doc.Model.Components.Schemas访问组件 Schema 表。Task 1为 OpenAPIIndex 增加 GetSchema 方法第一步先写失败测试计划遵循 TDD 流程。在 backend/api/mcp/tool_search_test.go 中添加TestSearchAPISchemaLookup用全名bytebase.v1.Instance查找func TestSearchAPISchemaLookup(t *testing.T) { profile : config.Profile{Mode: common.ReleaseModeDev} s, err : NewServer(nil, profile, test-secret) require.NoError(t, err) // Test schema lookup with full name result, _, err : s.handleSearchAPI(context.Background(), nil, SearchInput{ Schema: bytebase.v1.Instance, }) require.NoError(t, err) require.NotNil(t, result) require.Len(t, result.Content, 1) text : result.Content[0].(*mcpsdk.TextContent).Text require.Contains(t, text, bytebase.v1.Instance) require.Contains(t, text, name:) require.Contains(t, text, engine:) }运行go test -v github.com/bytebase/bytebase/backend/api/mcp -run ^TestSearchAPISchemaLookup$预期失败——因为此时SearchInput还没有Schema字段。仓库中实际实现与计划略有演进当前测试通过newServerWithStore(newTestServerStore(), profile, test-secret, nil)构造服务器见 backend/api/mcp/tool_search_test.go并且断言字段输出带引号name:、engine:与formatProperty输出的 JSON 风格一致。第二步为 SearchInput 增加 Schema 字段在 backend/api/mcp/tool_search.go 中SearchInput的完整定义含注释如下// SearchInput is the input for the search_api tool. type SearchInput struct { // OperationID gets detailed schema for a specific endpoint. // Use this after finding the endpoint you need. OperationID string json:operationId,omitempty // Schema gets the definition of a message type. // Examples: bytebase.v1.Instance, Instance, Engine Schema string json:schema,omitempty // Service filters results to a specific service. // Examples: SQLService, DatabaseService, ProjectService Service string json:service,omitempty }字段说明参数类型作用示例operationIdstring查看指定端点的请求/响应 SchemaDetail 模式SQLService/Queryschemastring查看消息类型定义Schema 模式本计划新增bytebase.v1.Instance、Instance、Engineservicestring浏览某服务下所有端点SQLService、DatabaseService、ProjectServicequerystring自由文本搜索端点计划原始文档包含当前仓库实现由 service 浏览模式覆盖create databaselimitint返回结果条数上限默认 5最大 50计划原始文档含此字段10第三步实现 GetSchema 与 getSchemaByName计划要求在 backend/api/mcp/openapi_index.go 文件末尾追加两个方法。当前仓库中的实际实现位于 openapi_index.go// GetSchema returns the schema properties for a component schema by name. // Supports both full name (bytebase.v1.Instance) and short name (Instance). func (idx *OpenAPIIndex) GetSchema(name string) ([]PropertyInfo, bool) { if idx.doc.Model.Components nil || idx.doc.Model.Components.Schemas nil { return nil, false } // Try exact name first if props : idx.getSchemaByName(name); props ! nil { return props, true } // Try with bytebase.v1. prefix if !strings.HasPrefix(name, bytebase.v1.) { fullName : bytebase.v1. name if props : idx.getSchemaByName(fullName); props ! nil { return props, true } } return nil, false }查找逻辑分两段先按原样精确匹配若失败且名称未带bytebase.v1.前缀则自动补全前缀再匹配一次。这样Instance与bytebase.v1.Instance得到完全相同的结果。getSchemaByName是核心解析函数按以下顺序处理枚举类型若 Schema 携带Enum列表则把全部枚举值合并为单个PropertyInfo{Name: enum, Type: string, Description: 值1, 值2, ...}返回。普通消息遍历schema.Properties为每个属性生成PropertyInfo{Name, Type, Description, Required}。类型推导复用extractPropertyTypeAndDescopenapi_index.go$ref引用类型取其末尾段如#/components/schemas/bytebase.v1.Engine→Engine数组类型展开为array元素类型。排序最后用slices.SortFunc按属性名做字典序排序保证输出稳定、便于 Agent 阅读。PropertyInfo结构定义在 openapi_index.gotype PropertyInfo struct { Name string json:name Type string json:type Description string json:description,omitempty Required bool json:required,omitempty }第四步handleSearchAPI 增加 schema 分支在 backend/api/mcp/tool_search.go 中handleSearchAPI使用switch按优先级分发func (s *Server) handleSearchAPI(_ context.Context, _ *mcp.CallToolRequest, input SearchInput) (*mcp.CallToolResult, any, error) { var text string switch { case input.OperationID ! : // Detail mode: get full schema for a specific endpoint text s.formatEndpointDetail(input.OperationID) case input.Schema ! : // Schema lookup mode: get properties of a message type text s.formatSchemaDetail(input.Schema) case input.Service : // List all services text s.formatServiceList() default: // List all endpoints in a service (no limit) endpoints : s.openAPIIndex.GetServiceEndpoints(input.Service) ... } return mcp.CallToolResult{ Content: []mcp.Content{mcp.TextContent{Text: text}}, }, nil, nil }第五步实现 formatSchemaDetail 输出格式化formatSchemaDetailtool_search.go负责把GetSchema的结果渲染成 Agent 易读的文本func (s *Server) formatSchemaDetail(schemaName string) string { props, ok : s.openAPIIndex.GetSchema(schemaName) if !ok { return fmt.Sprintf(Unknown schema: %s\n\nUse search_api(operationId\...\) to see schemas in request/response bodies., schemaName) } var sb strings.Builder // Normalize name for display displayName : schemaName if !strings.HasPrefix(schemaName, bytebase.v1.) { displayName bytebase.v1. schemaName } fmt.Fprintf(sb, ## %s\n\n, displayName) // Check if its an enum if len(props) 1 props[0].Name enum { sb.WriteString(**Enum values:** ) sb.WriteString(props[0].Description) sb.WriteString(\n) return sb.String() } for _, prop : range props { s.formatProperty(sb, prop) } return sb.String() }关键设计点名称归一化无论调用方传全名还是短名标题统一显示为bytebase.v1.Xxx输出稳定枚举特判单个名为enum的属性直接渲染为**Enum values:** v1, v2, ...未命中提示返回Unknown schema: xxx并引导 Agent 改用operationId模式查看请求/响应体中的 Schema 引用。第六步运行测试go test -v github.com/bytebase/bytebase/backend/api/mcp -run ^TestSearchAPISchemaLookup$预期 PASS。Task 2短名查找、未命中与枚举的回归测试计划继续补充三个测试用例覆盖 Schema 模式的边界情况func TestSearchAPISchemaLookupShortName(t *testing.T) { // Test schema lookup with short name result, _, err : s.handleSearchAPI(context.Background(), nil, SearchInput{ Schema: Instance, }) ... text : result.Content[0].(*mcpsdk.TextContent).Text require.Contains(t, text, bytebase.v1.Instance) require.Contains(t, text, name:) } func TestSearchAPISchemaLookupNotFound(t *testing.T) { // Test schema lookup with unknown name result, _, err : s.handleSearchAPI(context.Background(), nil, SearchInput{ Schema: NonExistentSchema, }) ... text : result.Content[0].(*mcpsdk.TextContent).Text require.Contains(t, text, Unknown schema) } func TestSearchAPISchemaLookupEnum(t *testing.T) { // Test enum schema lookup result, _, err : s.handleSearchAPI(context.Background(), nil, SearchInput{ Schema: Engine, }) ... text : result.Content[0].(*mcpsdk.TextContent).Text require.Contains(t, text, Enum values:) }这三个测试分别验证短名补全路径Instance→bytebase.v1.Instance、未命中路径返回Unknown schema提示、枚举路径Engine返回Enum values:。运行go test -v github.com/bytebase/bytebase/backend/api/mcp -run ^TestSearchAPISchemaLookup全部通过。当前仓库中这些测试均已落地见 tool_search_test.go。Task 3protobuf 类型描述的截断与精简这是本计划第二个核心目标。OpenAPI 规格从 protobuf 生成后内建类型带冗长官方文档需要替换为一行简短说明。第一步失败测试先行func TestSearchAPIProtobufDescriptionTruncation(t *testing.T) { profile : config.Profile{Mode: common.ReleaseModeDev} s, err : NewServer(nil, profile, test-secret) require.NoError(t, err) result, _, err : s.handleSearchAPI(context.Background(), nil, SearchInput{ OperationID: InstanceService/CreateInstance, }) ... // Should NOT contain verbose protobuf documentation require.NotContains(t, text, A Timestamp represents a point in time) require.NotContains(t, text, A Duration represents a signed) // Should contain short description if strings.Contains(text, google.protobuf.Timestamp) { require.Contains(t, text, ISO 8601) } }此时预期失败formatProperty直接把属性描述原样输出冗长文档仍在。第二步typeDescriptions 映射表在 openapi_index.go 中定义全局映射当前仓库已实现// typeDescriptions provides concise descriptions for known types. // These replace verbose protobuf documentation. var typeDescriptions map[string]string{ google.protobuf.Timestamp: ISO 8601 format, e.g. 2024-01-15T01:30:15Z, google.protobuf.Duration: e.g. 3.5s or 1h30m, google.protobuf.FieldMask: e.g. title,engine, google.protobuf.Empty: empty message, google.protobuf.Any: any JSON value, google.protobuf.Struct: JSON object, google.protobuf.Value: any JSON value, } // GetTypeDescription returns a concise description for known types. func GetTypeDescription(typeName string) (string, bool) { desc, ok : typeDescriptions[typeName] return desc, ok }这张表覆盖了 Agent 构造请求体时最常遇到的 7 个 protobuf 内建类型每个都给出可直接用于构造 JSON 的语义如Timestamp直接告诉 Agent 传 ISO 8601 字符串。第三步改造 formatProperty在 tool_search.go 中formatProperty的输出顺序调整为先查短描述映射命中即用否则清洗并截断原描述func (*Server) formatProperty(sb *strings.Builder, prop PropertyInfo) { required : if prop.Required { required (required) } desc : // Check if type has a known short description if shortDesc, ok : GetTypeDescription(prop.Type); ok { desc fmt.Sprintf( // %s, shortDesc) } else if prop.Description ! { // Remove newlines and truncate long descriptions cleanDesc : strings.ReplaceAll(prop.Description, \n, ) cleanDesc strings.ReplaceAll(cleanDesc, \r, ) // Truncate at 100 chars if truncated, ok : common.TruncateString(cleanDesc, 97); ok { cleanDesc truncated ... } desc fmt.Sprintf( // %s, cleanDesc) } sb.WriteString( \) sb.WriteString(prop.Name) sb.WriteString(\: ) sb.WriteString(prop.Type) sb.WriteString(required) sb.WriteString(desc) sb.WriteString(\n) }这里有两个细节值得注意截断函数是 Unicode 安全的common.TruncateStringbackend/common/util.go按 rune 迭代而非按字节截断避免切出半个 UTF-8 字符。截断上限为 97 字符 ...正好 100 字符。输出为 JSON 风格每行形如name: string (required) // 描述配合formatEndpointDetail中的 json 代码块Agent 可以直接把返回内容当作 JSON 骨架使用。第四步验证go test -v github.com/bytebase/bytebase/backend/api/mcp -run ^TestSearchAPIProtobufDescriptionTruncation$预期 PASS再运行go test -v github.com/bytebase/bytebase/backend/api/mcp确认全部测试通过。Task 4更新 search_api 工具描述search_api的Description是 MCP 工具发现机制的一部分——Agent 通过它理解每个参数何时使用。计划要求替换 tool_search.go 中的searchAPIDescription常量const searchAPIDescription Discover Bytebase API endpoints. **Always call before call_api - never guess schemas.** | Mode | Parameters | Result | |------|------------|--------| | List | (none) | All services | | Browse | serviceSQLService | All endpoints in service | | Details | operationIdSQLService/Query | Request/response schema | | Schema | schemaInstance | Message type definition | **Workflow:** search_api() → search_api(service...) → search_api(operationId...) → call_api(...)描述表格把五种模式List / Browse / Search / Filter / Details / Schema与各自参数、返回结果一一对应并明确工作流链路配合 tool_call.go 中call_api的描述Use search_api first to get operationId and schema.形成完整的先发现、后调用闭环。改动后用golangci-lint run --allow-parallel-runners ./backend/api/mcp/...检查。Task 5手动验证清单计划最后给出端到端验证步骤。先构建二进制go build -ldflags -w -s -p16 -o ./bytebase-build/bytebase ./backend/bin/server/main.go启动 Bytebase 后在 MCP 客户端中逐项验证调用预期输出search_api(schemaInstance)显示 Instance 的字段定义名称、类型、必填、描述search_api(schemabytebase.v1.Instance)与上一条完全相同短名归一化search_api(schemaEngine)显示Enum values:及全部枚举值search_api(operationIdInstanceService/CreateInstance)请求/响应 Schema 中的 protobuf 字段使用简短描述如 ISO 8601search_api 与 call_api 的完整协作链路Schema Lookup 能力的最终价值体现在与call_api的配合上。基于 tool_call.go 的源码一个完整的工作流是search_api()—— 列出全部服务确认目标服务如InstanceServicesearch_api(serviceInstanceService)—— 浏览该服务下的端点找到CreateInstancesearch_api(operationIdInstanceService/CreateInstance)—— 查看该端点的请求/响应 Schemasearch_api(schemaInstance)—— 本次计划新增的能力查看Instance消息类型本身的字段定义确定哪些字段必填、字段类型与参考格式如Timestamp用 ISO 8601call_api(operationIdInstanceService/CreateInstance, body{...})—— 按查到的 Schema 精确构造请求体并执行。call_api在 handleCallAPI 中同样通过openAPIIndex.GetEndpoint解析 operationId未知操作会返回unknown operation, use search_api to find valid operations的引导式错误——两个工具共享同一个 OpenAPI 索引保证发现与执行的一致性。实现要点总结维度关键结论核心改动文件backend/api/mcp/openapi_index.go索引与类型描述、backend/api/mcp/tool_search.go参数与格式化、backend/api/mcp/tool_search_test.go测试新增 APIOpenAPIIndex.GetSchema(name)、getSchemaByName(name)、formatSchemaDetail(name)、GetTypeDescription(type)查找策略精确匹配 → 自动补全bytebase.v1.前缀 → 未命中返回Unknown schema引导枚举处理枚举 Schema 折叠为单一enum属性渲染为Enum values:列表描述精简7 个 protobuf 内建类型走短描述映射其余描述去除换行、截断至 100 字符Unicode 安全测试验证go test -v github.com/bytebase/bytebase/backend/api/mcp全量通过从这份实现计划到仓库中的最终代码可以看出Schema Lookup 功能以最小的侵入面一个参数 一个索引方法 一个格式化函数显著增强了search_api工具的可用性使 AI Agent 无需猜测即可精确构造嵌套消息类型是 MCP API 发现链路中不猜 Schema原则的关键落地。文中所有行号对应的实现均可直接在 backend/api/mcp 目录下对照阅读。【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考