# go-api Phase 3: OpenAPI Spec Generation + SDK Codegen > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Turn MCP tool definitions into REST endpoints with auto-generated OpenAPI 3.1 specs and multi-language SDK codegen. **Architecture:** Runtime OpenAPI generation (NOT swaggo annotations) — routes are dynamic via RouteGroup, Response[T] generics break swaggo, and MCP tools already carry JSON Schema at runtime. A `ToolBridge` converts tool descriptors into RouteGroup + OpenAPI metadata. A `SpecBuilder` constructs the full OpenAPI 3.1 spec from all registered groups. SDK codegen wraps `openapi-generator-cli`. **Tech Stack:** go-api (Gin), OpenAPI 3.1, openapi-generator-cli, go-ai MCP SDK --- ## Context Phase 1+2 complete (143 tests, 21 With*() options). The "missing link" is: MCP tools are only accessible via JSON-RPC. This phase creates a REST bridge so any HTTP client (or generated SDK) can call the same services. The pipeline: **MCP tool → REST endpoint → OpenAPI spec → SDK codegen**. Current swagger.go serves an empty spec (`paths: {}`). swaggo annotations don't exist anywhere and can't work with dynamic RouteGroups or generics. The solution is runtime spec generation. ## Critical Files - `/Users/snider/Code/go-api/swagger.go` — Refactor from empty hardcoded spec to SpecBuilder - `/Users/snider/Code/go-api/group.go` — Add DescribableGroup interface - `/Users/snider/Code/go-api/api.go` — Pass groups to registerSwagger, Engine struct changes - `/Users/snider/Code/go-api/response.go` — Response[T], Error, Meta structs (read-only reference for envelope schema) - `/Users/snider/Code/go-api/options.go` — WithSwagger already exists, no changes needed - `/Users/snider/Code/go-ai/mcp/mcp.go` — Tool registration with typed Input/Output structs - `/Users/snider/Code/go-ai/mcp/subsystem.go` — Subsystem interface ## Wave 1: OpenAPI Runtime Spec (go-api only) ### Task 1: DescribableGroup interface + RouteDescription **Files:** - Modify: `/Users/snider/Code/go-api/group.go` - Test: `/Users/snider/Code/go-api/group_test.go` (existing — add new tests) Add a `DescribableGroup` interface that RouteGroups can optionally implement to provide OpenAPI metadata. This is the opt-in contract for spec generation. ```go // DescribableGroup extends RouteGroup with OpenAPI metadata. // RouteGroups that implement this will have their endpoints // included in the generated OpenAPI specification. type DescribableGroup interface { RouteGroup Describe() []RouteDescription } // RouteDescription describes a single endpoint for OpenAPI generation. type RouteDescription struct { Method string // HTTP method: GET, POST, PUT, DELETE, PATCH Path string // Path relative to BasePath, e.g. "/generate" Summary string // Short summary Description string // Long description Tags []string // OpenAPI tags for grouping RequestBody map[string]any // JSON Schema for request body (nil for GET) Response map[string]any // JSON Schema for success response data } ``` **Tests (5):** - `TestDescribableGroup_Good_ImplementsRouteGroup` — A struct implementing both interfaces compiles and works - `TestDescribableGroup_Good_DescribeReturnsRoutes` — Verify Describe() returns correct RouteDescription list - `TestDescribableGroup_Good_EmptyDescribe` — Empty Describe() is valid (no endpoints documented) - `TestDescribableGroup_Good_MultipleVerbs` — GET, POST, DELETE on same base path - `TestDescribableGroup_Bad_NilSchemas` — RequestBody and Response can be nil ### Task 2: ToolBridge — tool descriptors to REST endpoints **Files:** - Create: `/Users/snider/Code/go-api/bridge.go` - Create: `/Users/snider/Code/go-api/bridge_test.go` The `ToolBridge` converts tool descriptors into a RouteGroup that also implements DescribableGroup. It creates `POST /{tool_name}` endpoints for each registered tool. ```go // ToolDescriptor describes a tool that can be exposed as a REST endpoint. type ToolDescriptor struct { Name string // Tool name, e.g. "file_read" (becomes POST path) Description string // Human-readable description Group string // OpenAPI tag group, e.g. "files" InputSchema map[string]any // JSON Schema for request body OutputSchema map[string]any // JSON Schema for response data (optional) } // ToolBridge converts tool descriptors into REST endpoints and OpenAPI paths. // It implements both RouteGroup and DescribableGroup. type ToolBridge struct { basePath string tools []boundTool } type boundTool struct { descriptor ToolDescriptor handler gin.HandlerFunc } func NewToolBridge(basePath string) *ToolBridge // Add registers a tool with its HTTP handler. func (b *ToolBridge) Add(desc ToolDescriptor, handler gin.HandlerFunc) // RouteGroup implementation func (b *ToolBridge) Name() string func (b *ToolBridge) BasePath() string func (b *ToolBridge) RegisterRoutes(rg *gin.RouterGroup) // DescribableGroup implementation func (b *ToolBridge) Describe() []RouteDescription // Tools returns registered tool descriptors. func (b *ToolBridge) Tools() []ToolDescriptor ``` **Tests (6):** - `TestToolBridge_Good_RegisterAndServe` — Add 2 tools, verify POST endpoints respond - `TestToolBridge_Good_BasePath` — Custom base path `/api/v1/tools` works - `TestToolBridge_Good_Describe` — Describe() returns correct RouteDescription for each tool - `TestToolBridge_Good_ToolsAccessor` — Tools() returns all registered descriptors - `TestToolBridge_Bad_EmptyBridge` — Bridge with no tools is valid, RegisterRoutes is no-op - `TestToolBridge_Good_IntegrationWithEngine` — Register bridge with Engine, verify routes accessible via Handler() ### Task 3: SpecBuilder — OpenAPI 3.1 spec generation **Files:** - Create: `/Users/snider/Code/go-api/openapi.go` - Create: `/Users/snider/Code/go-api/openapi_test.go` Builds a complete OpenAPI 3.1 JSON document from registered RouteGroups. Groups implementing DescribableGroup contribute their endpoint metadata. All responses are wrapped in the Response[T] envelope. ```go // SpecBuilder constructs an OpenAPI 3.1 spec from registered RouteGroups. type SpecBuilder struct { Title string Description string Version string } // Build generates the complete OpenAPI 3.1 JSON spec. // Groups implementing DescribableGroup contribute endpoint documentation. // Other groups are listed as tags only. func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) ``` Key behaviours: - Always includes `GET /health` (built-in) - For each DescribableGroup, generates path items from Describe() - Wraps all response schemas in the Response[T] envelope (success/data/error/meta) - Uses OpenAPI 3.1 (compatible with JSON Schema 2020-12 from MCP SDK) - Tags derived from RouteGroup.Name() - Output is valid, parseable OpenAPI 3.1 JSON **Tests (6):** - `TestSpecBuilder_Good_EmptyGroups` — Produces valid spec with just /health - `TestSpecBuilder_Good_WithDescribableGroup` — Paths populated from Describe() - `TestSpecBuilder_Good_EnvelopeWrapping` — Response schema wraps data in Response[T] envelope - `TestSpecBuilder_Good_NonDescribableGroup` — Non-describable groups appear as tags only - `TestSpecBuilder_Good_ToolBridgeIntegration` — ToolBridge + SpecBuilder produces correct tool endpoints - `TestSpecBuilder_Bad_InfoFields` — Title, description, version appear in output ### Task 4: Refactor swagger.go to use SpecBuilder **Files:** - Modify: `/Users/snider/Code/go-api/swagger.go` - Modify: `/Users/snider/Code/go-api/api.go` (pass groups to registerSwagger) - Modify: `/Users/snider/Code/go-api/swagger_test.go` (update tests) Replace the hardcoded empty JSON spec with SpecBuilder-generated spec. The swaggerSpec struct now wraps SpecBuilder and caches the result via sync.Once. `registerSwagger` receives the engine's groups. ```go type swaggerSpec struct { builder *SpecBuilder groups []RouteGroup once sync.Once doc string } func (s *swaggerSpec) ReadDoc() string { s.once.Do(func() { data, _ := s.builder.Build(s.groups) s.doc = string(data) }) return s.doc } func registerSwagger(g *gin.Engine, title, description, version string, groups []RouteGroup) ``` In `api.go`, change `build()`: ```go // Mount Swagger UI if enabled. if e.swaggerEnabled { registerSwagger(r, e.swaggerTitle, e.swaggerDesc, e.swaggerVersion, e.groups) } ``` **Tests (5):** - `TestSwagger_Good_SpecNotEmpty` — GET /swagger/doc.json returns spec with /health path - `TestSwagger_Good_WithToolBridge` — Register ToolBridge, verify tool endpoints appear in spec - `TestSwagger_Good_CachesSpec` — Multiple calls return same cached spec - `TestSwagger_Good_InfoFromOptions` — Title/desc/version from WithSwagger() appear in spec - `TestSwagger_Good_ValidOpenAPI` — Output parses as valid JSON with correct openapi version field ### Task 5: SpecExporter — export spec to file/stdout **Files:** - Create: `/Users/snider/Code/go-api/export.go` - Create: `/Users/snider/Code/go-api/export_test.go` A helper that exports the generated spec to a file or io.Writer. Supports JSON and YAML output. This is what `core api spec` will call. ```go // ExportSpec generates the OpenAPI spec and writes it to w. // Format can be "json" or "yaml". func ExportSpec(w io.Writer, format string, builder *SpecBuilder, groups []RouteGroup) error // ExportSpecToFile writes the spec to the given path. func ExportSpecToFile(path, format string, builder *SpecBuilder, groups []RouteGroup) error ``` **Tests (5):** - `TestExportSpec_Good_JSON` — Writes valid JSON to buffer - `TestExportSpec_Good_YAML` — Writes valid YAML to buffer - `TestExportSpec_Bad_InvalidFormat` — Returns error for unknown format - `TestExportSpecToFile_Good_CreatesFile` — Writes file to temp dir - `TestExportSpec_Good_WithToolBridge` — Full pipeline: tools → bridge → spec → export ## Wave 2: MCP-to-REST Bridge (go-ai) ### Task 6: Tool registry in go-ai Service **Files:** - Modify: `/Users/snider/Code/go-ai/mcp/mcp.go` — Add ToolRecord and registry - Create: `/Users/snider/Code/go-ai/mcp/registry.go` — ToolRecord type and helpers - Create: `/Users/snider/Code/go-ai/mcp/registry_test.go` The MCP SDK's `mcp.Server.tools` is unexported. We maintain a parallel registry in `Service` that records tool metadata (name, description, input/output Go types) as tools are registered. This becomes the source of truth for both MCP and REST. ```go // ToolRecord captures metadata about a registered MCP tool. type ToolRecord struct { Name string Description string Group string // Subsystem group name InputSchema map[string]any // JSON Schema from Go struct tags OutputSchema map[string]any Handler any // The original handler func for type assertion } // Tools returns all recorded tool metadata. func (s *Service) Tools() []ToolRecord ``` Modify `registerTools` and subsystem registration to also record tools. Use `jsonschema-go` (already a transitive dep from MCP SDK) to extract schemas from the typed Input/Output parameters. **Tests (5):** - `TestToolRegistry_Good_RecordsTools` — After New(), Tools() returns all registered tools - `TestToolRegistry_Good_IncludesSubsystems` — Subsystem tools included in registry - `TestToolRegistry_Good_SchemaExtraction` — InputSchema contains correct properties from struct tags - `TestToolRegistry_Good_ToolCount` — Count matches expected (10 built-in + subsystem tools) - `TestToolRegistry_Bad_EmptyService` — Service with no subsystems has only built-in tools ### Task 7: BridgeToAPI — MCP tools to go-api ToolBridge **Files:** - Create: `/Users/snider/Code/go-ai/mcp/bridge.go` - Create: `/Users/snider/Code/go-ai/mcp/bridge_test.go` Converts MCP tool records into go-api ToolBridge entries. Each tool becomes a POST endpoint. The handler unmarshals JSON from request body, calls the business logic, and wraps the response in `api.OK()`/`api.Fail()`. ```go // BridgeToAPI populates a go-api ToolBridge from recorded MCP tools. // Each tool becomes a POST endpoint at /{tool_name}. func BridgeToAPI(svc *Service, bridge *api.ToolBridge) error ``` **Tests (5):** - `TestBridgeToAPI_Good_FileRead` — Bridge file_read, POST JSON body, get Response[ReadFileOutput] - `TestBridgeToAPI_Good_AllTools` — All tools from registry appear in bridge - `TestBridgeToAPI_Good_DescribableGroup` — Bridge implements DescribableGroup, Describe() returns correct metadata - `TestBridgeToAPI_Bad_InvalidJSON` — POST malformed JSON returns 400 with error envelope - `TestBridgeToAPI_Good_EndToEnd` — New service → BridgeToAPI → register with Engine → GET /swagger/doc.json shows all tools ## Wave 3: SDK Codegen + CLI ### Task 8: Codegen wrapper **Files:** - Create: `/Users/snider/Code/go-api/codegen.go` - Create: `/Users/snider/Code/go-api/codegen_test.go` Wraps `openapi-generator-cli` for multi-language SDK generation. Checks if the tool is available and provides clear installation instructions if missing. ```go // SDKGenerator wraps openapi-generator-cli for SDK generation. type SDKGenerator struct { SpecPath string OutputDir string PackageName string } // Generate creates an SDK for the given language. // Supported: "go", "typescript-fetch", "python", "java", "csharp" func (g *SDKGenerator) Generate(ctx context.Context, language string) error // Available checks if openapi-generator-cli is installed. func (g *SDKGenerator) Available() bool // SupportedLanguages returns the list of supported SDK languages. func SupportedLanguages() []string ``` **Tests (5):** - `TestSDKGenerator_Good_CommandConstruction` — Verify correct CLI arguments without executing - `TestSDKGenerator_Good_SupportedLanguages` — Returns expected language list - `TestSDKGenerator_Bad_UnsupportedLanguage` — Returns error for "brainfuck" - `TestSDKGenerator_Bad_MissingSpec` — Returns error when spec file doesn't exist - `TestSDKGenerator_Good_OutputDirCreated` — Creates output directory if missing ### Task 9: CLI commands — `core api spec` and `core api sdk` **Files:** - Create: `/Users/snider/Code/host-uk/core/cmd/core-cli/api.go` (or appropriate location in CLI tree) - Create test file alongside New `api` command group in the core CLI: ``` core api spec # Export OpenAPI spec JSON to stdout core api spec --output spec.json # Write to file core api spec --format yaml # YAML format core api sdk --lang go # Generate Go SDK core api sdk --lang typescript-fetch,python --output ./sdk/ core api sdk --spec spec.json --lang go --output ./sdk/go ``` This depends on understanding the CLI registration pattern. The commands create an MCP service, bridge tools, build spec, and either export or generate SDKs. **Tests (4):** - `TestAPISpecCmd_Good_JSON` — Outputs valid JSON to stdout - `TestAPISpecCmd_Good_YAML` — Outputs valid YAML with --format yaml - `TestAPISDKCmd_Bad_NoLang` — Returns error without --lang flag - `TestAPISDKCmd_Good_ValidatesLanguage` — Rejects unsupported language with helpful message ## Verification After all tasks are complete: 1. **Unit tests**: `cd /Users/snider/Code/go-api && go test ./...` — all pass 2. **go-ai tests**: `cd /Users/snider/Code/go-ai && go test ./...` — all pass 3. **Spec roundtrip**: Register a ToolBridge with sample tools, build Engine with WithSwagger, GET `/swagger/doc.json` → verify paths are populated, not empty 4. **Export**: `ExportSpec` to temp file in JSON and YAML, verify both parse correctly 5. **SDK smoke test** (if openapi-generator-cli installed): Generate Go client from exported spec, verify it compiles ## Dependency Sequencing ``` Task 1 (DescribableGroup) ← Task 2 (ToolBridge) ← Task 3 (SpecBuilder) Task 3 ← Task 4 (swagger refactor) Task 3 ← Task 5 (ExportSpec) Task 2 ← Task 6 (Tool registry) ← Task 7 (BridgeToAPI) Task 5 ← Task 8 (Codegen) Task 5 + Task 7 ← Task 9 (CLI commands) ``` Wave 1 (Tasks 1-5) is self-contained in go-api. Wave 2 (Tasks 6-7) bridges go-ai. Wave 3 (Tasks 8-9) adds codegen and CLI.