Automated fixes: interface{} → any, range-over-int, t.Context(),
wg.Go(), strings.SplitSeq, strings.Builder, slices.Contains,
maps helpers, min/max builtins.
Co-Authored-By: Virgil <virgil@lethean.io>
16 KiB
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.
// 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 worksTestDescribableGroup_Good_DescribeReturnsRoutes— Verify Describe() returns correct RouteDescription listTestDescribableGroup_Good_EmptyDescribe— Empty Describe() is valid (no endpoints documented)TestDescribableGroup_Good_MultipleVerbs— GET, POST, DELETE on same base pathTestDescribableGroup_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.
// 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 respondTestToolBridge_Good_BasePath— Custom base path/api/v1/toolsworksTestToolBridge_Good_Describe— Describe() returns correct RouteDescription for each toolTestToolBridge_Good_ToolsAccessor— Tools() returns all registered descriptorsTestToolBridge_Bad_EmptyBridge— Bridge with no tools is valid, RegisterRoutes is no-opTestToolBridge_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.
// 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 /healthTestSpecBuilder_Good_WithDescribableGroup— Paths populated from Describe()TestSpecBuilder_Good_EnvelopeWrapping— Response schema wraps data in Response[T] envelopeTestSpecBuilder_Good_NonDescribableGroup— Non-describable groups appear as tags onlyTestSpecBuilder_Good_ToolBridgeIntegration— ToolBridge + SpecBuilder produces correct tool endpointsTestSpecBuilder_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.
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():
// 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 pathTestSwagger_Good_WithToolBridge— Register ToolBridge, verify tool endpoints appear in specTestSwagger_Good_CachesSpec— Multiple calls return same cached specTestSwagger_Good_InfoFromOptions— Title/desc/version from WithSwagger() appear in specTestSwagger_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.
// 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 bufferTestExportSpec_Good_YAML— Writes valid YAML to bufferTestExportSpec_Bad_InvalidFormat— Returns error for unknown formatTestExportSpecToFile_Good_CreatesFile— Writes file to temp dirTestExportSpec_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.
// 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 toolsTestToolRegistry_Good_IncludesSubsystems— Subsystem tools included in registryTestToolRegistry_Good_SchemaExtraction— InputSchema contains correct properties from struct tagsTestToolRegistry_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().
// 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 bridgeTestBridgeToAPI_Good_DescribableGroup— Bridge implements DescribableGroup, Describe() returns correct metadataTestBridgeToAPI_Bad_InvalidJSON— POST malformed JSON returns 400 with error envelopeTestBridgeToAPI_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.
// 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 executingTestSDKGenerator_Good_SupportedLanguages— Returns expected language listTestSDKGenerator_Bad_UnsupportedLanguage— Returns error for "brainfuck"TestSDKGenerator_Bad_MissingSpec— Returns error when spec file doesn't existTestSDKGenerator_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 stdoutTestAPISpecCmd_Good_YAML— Outputs valid YAML with --format yamlTestAPISDKCmd_Bad_NoLang— Returns error without --lang flagTestAPISDKCmd_Good_ValidatesLanguage— Rejects unsupported language with helpful message
Verification
After all tasks are complete:
- Unit tests:
cd /Users/snider/Code/go-api && go test ./...— all pass - go-ai tests:
cd /Users/snider/Code/go-ai && go test ./...— all pass - Spec roundtrip: Register a ToolBridge with sample tools, build Engine with WithSwagger, GET
/swagger/doc.json→ verify paths are populated, not empty - Export:
ExportSpecto temp file in JSON and YAML, verify both parse correctly - 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.