Skip to content

Commit cad312d

Browse files
authored
Add automated OpenAPI specification generation for hosted documentation (#208)
* Refactor API routes into single source of truth Extract route registration logic into internal/api/routes.go with RegisterRoutes function. This ensures daemon and docs generation use identical route definitions, maintaining 1:1 mapping between code and documentation. Introduces api.APIVersion constant as single source of truth for API version used in OpenAPI spec and URL paths. The RegisterRoutes function extracts this version from the router's OpenAPI spec and returns the path prefix for logging. All parameters require non-nil values with defensive checks using reflection to prevent panics from interface nil checks. Updates internal/daemon/api_server.go to use RegisterRoutes and api.APIVersion constant, removing duplicate route registration code. * Add OpenAPI specification generation tool Introduce tools/docsgen/api/openapi.go for programmatic OpenAPI spec export. The tool uses api.RegisterRoutes with stub implementations of contracts to generate the spec without running the daemon. Output is written to docs/api/openapi.yaml which is excluded from version control via .gitignore, matching the pattern used for CLI docs in docs/commands/. This ensures a 1:1 mapping between the actual API code and the published OpenAPI specification, automatically generated during documentation deployment. * Fix errcheck lint error in nav tool Add error handling for encoder.Close() call in updateMkDocsNav function to satisfy errcheck linter. The encoder must be properly closed to flush any buffered output before writing the final YAML to the file. * Update build configuration for reorganized tools Update Makefile targets to reflect new tools structure where each standalone tool lives in its own directory (cmds/, nav/, api/) rather than sharing tools/docsgen/cli/. Add docs-api target to generate OpenAPI specification. Configure golangci-lint to include build tags for all docsgen tools and validate_registry, ensuring proper linting of build-tagged files that were previously excluded from analysis. * Integrate OpenAPI docs generation into CI pipeline Add OpenAPI spec generation step to deploy-docs workflow between CLI docs generation and navigation updates. This ensures the spec is generated fresh on every docs deployment. Update mkdocs.yaml to include API Reference section with link to the generated OpenAPI specification file. Update GitHub Actions versions across all workflows to latest pinned releases for actions/checkout, actions/setup-go, and astral-sh/setup-uv. * Drop 'make docs-local' and just use 'make docs' * make docs includes building OpenAPI too
1 parent 15a6555 commit cad312d

File tree

15 files changed

+217
-49
lines changed

15 files changed

+217
-49
lines changed

.github/workflows/deploy-docs.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ jobs:
1515

1616
steps:
1717
- name: Checkout repo
18-
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
18+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
1919

2020
- name: Set up Go (from go.mod)
21-
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
21+
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
2222
with:
2323
go-version-file: go.mod
2424

2525
- name: Install the latest version of uv
26-
uses: astral-sh/setup-uv@v6
26+
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0
2727
with:
2828
python-version: 3.12
2929
activate-environment: true
@@ -32,6 +32,9 @@ jobs:
3232
- name: Generate CLI docs
3333
run: make docs-cli
3434

35+
- name: Generate OpenAPI docs
36+
run: make docs-api
37+
3538
- name: Update MkDocs navigation
3639
run: make docs-nav
3740

.github/workflows/golangci-lint.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ jobs:
1717
runs-on: ubuntu-latest
1818
steps:
1919
- name: Checkout
20-
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
20+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
2121

2222
- name: Set up Go (from go.mod)
23-
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
23+
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
2424
with:
2525
go-version-file: go.mod
2626

.github/workflows/release.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
runs-on: ubuntu-latest
3434
steps:
3535
- name: Checkout
36-
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
36+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
3737
with:
3838
fetch-depth: 0
3939

@@ -49,7 +49,7 @@ jobs:
4949
git checkout "$TAG"
5050
5151
- name: Set up Go (from go.mod)
52-
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
52+
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
5353
with:
5454
go-version-file: go.mod
5555

@@ -61,13 +61,13 @@ jobs:
6161
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
6262

6363
- name: Login to DockerHub
64-
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
64+
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
6565
with:
6666
username: ${{ secrets.DOCKERHUB_USERNAME }}
6767
password: ${{ secrets.DOCKERHUB_TOKEN }}
6868

6969
- name: Run GoReleaser
70-
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
70+
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
7171
with:
7272
distribution: goreleaser
7373
version: latest

.github/workflows/tests.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ jobs:
1212
runs-on: ubuntu-latest
1313
steps:
1414
- name: Checkout
15-
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
15+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
1616

1717
- name: Set up Go (from go.mod)
18-
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
18+
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
1919
with:
2020
go-version-file: go.mod
2121

.github/workflows/validate-registry.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ jobs:
2222

2323
steps:
2424
- name: Checkout
25-
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
25+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
2626

27-
- name: Setup Go (from go.mod)
28-
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
27+
- name: Set up Go (from go.mod)
28+
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
2929
with:
3030
go-version-file: go.mod
3131

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ secrets.dev.toml
4646
# Autogenerated docs pages for CLI commands
4747
docs/commands/
4848

49+
# Autogenerated OpenAPI specification
50+
docs/api/
51+
4952
# Any Python virtual environments
5053
.venv/
5154
# Added by goreleaser init:

.golangci.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,10 @@ run:
7575
relative-path-mode: gomod
7676
modules-download-mode: readonly
7777
allow-parallel-runners: true
78-
allow-serial-runners: true
78+
allow-serial-runners: true
79+
# Build tags to include files with custom build constraints.
80+
build-tags:
81+
- docsgen_cli
82+
- docsgen_nav
83+
- docsgen_api
84+
- validate_registry

Makefile

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
.PHONY: build build-dev build-linux build-linux-arm64 clean docs docs-cli docs-local docs-nav install lint local-down local-up test uninstall validate-registry check-licenses check-notice notice
2-
31
MODULE_PATH := github.com/mozilla-ai/mcpd/v2
42

53
# /usr/local/bin is a common default for user-installed binaries
@@ -32,6 +30,7 @@ BUILDFLAGS := -trimpath
3230
# The license types allowed to be imported by the project
3331
ALLOWED_LICENSES := Apache-2.0,MIT,BSD-2-Clause,BSD-3-Clause,ZeroBSD,Unlicense
3432

33+
.PHONY: check-licenses
3534
check-licenses:
3635
@echo "Checking licenses..."
3736
@go install github.com/google/go-licenses/v2@latest
@@ -43,6 +42,7 @@ check-licenses:
4342
exit 1; \
4443
fi
4544

45+
.PHONY: check-notice
4646
check-notice:
4747
@echo "Checking NOTICE..."
4848
@go install github.com/google/go-licenses/v2@latest
@@ -56,82 +56,100 @@ check-notice:
5656
echo "✓ NOTICE is up to date"; \
5757
fi
5858

59+
.PHONY: notice
5960
notice:
6061
@echo "Generating NOTICE..."
6162
@go install github.com/google/go-licenses/v2@latest
6263
@go-licenses report ./... --ignore github.com/mozilla-ai/mcpd/v2 --template build/licenses/notice.tpl > NOTICE
6364
@echo "✓ NOTICE generated"
6465

66+
.PHONY: lint
6567
lint: check-notice
6668
golangci-lint run --fix -v
6769

70+
.PHONY: test
6871
test: lint
6972
go test ./...
7073

74+
.PHONY: validate-registry
7175
validate-registry:
7276
@echo "Validating Mozilla AI registry against schema..."
7377
@go run -tags=validate_registry ./tools/validate/registry.go \
7478
internal/provider/mozilla_ai/data/schema.json \
7579
internal/provider/mozilla_ai/data/registry.json
7680

81+
.PHONY: build
7782
build: lint
7883
@echo "building mcpd (version: $(VERSION), commit: $(COMMIT))..."
7984
@go build $(BUILDFLAGS) -o mcpd -ldflags="$(LDFLAGS)" .
8085

86+
.PHONY: build-linux
8187
build-linux:
8288
@echo "building mcpd for amd64/linux (version: $(VERSION), commit: $(COMMIT))..."
8389
@GOOS=linux GOARCH=amd64 go build $(BUILDFLAGS) -o mcpd -ldflags="$(LDFLAGS)" .
8490

91+
.PHONY: build-linux-arm64
8592
build-linux-arm64:
8693
@echo "building mcpd for arm64/linux (version: $(VERSION), commit: $(COMMIT))..."
8794
@GOOS=linux GOARCH=arm64 go build $(BUILDFLAGS) -o mcpd -ldflags="$(LDFLAGS)" .
8895

8996
# For development builds without optimizations (for debugging)
97+
.PHONY: build-dev
9098
build-dev:
9199
@echo "building mcpd for development (version: $(VERSION), commit: $(COMMIT))..."
92100
@go build -o mcpd -ldflags="-X '$(MODULE_PATH)/internal/cmd.version=$(VERSION)' \
93101
-X '$(MODULE_PATH)/internal/cmd.commit=$(COMMIT)' \
94102
-X '$(MODULE_PATH)/internal/cmd.date=$(DATE)'" .
95103

104+
.PHONY: install
96105
install: build
97106
@# Copy the executable to the install directory
98107
@# Requires sudo if INSTALL_DIR is a system path like /usr/local/bin
99108
@echo "installing mcpd to $(INSTALL_DIR)..."
100109
@cp mcpd $(INSTALL_DIR)/mcpd
101110
@chmod +x $(INSTALL_DIR)/mcpd
102111

112+
.PHONY: clean
103113
clean:
104114
@# Remove the built executable and any temporary files
105115
@echo "cleaning up local build artifacts..."
106116
@rm -f mcpd # The executable itself
107117
@rm -rf $(TARGET_PLATFORM) # Remove any orphaned Docker build directories
108118

119+
.PHONY: uninstall
109120
uninstall:
110121
@# Remove the installed executable from the system
111122
@# Requires sudo if INSTALL_DIR is a system path
112123
@echo "uninstalling mcpd from $(INSTALL_DIR)..."
113124
@rm -f $(INSTALL_DIR)/mcpd
114125

115126
# Runs MkDocs locally
116-
docs: docs-local
117-
118-
# Runs MkDocs locally
119-
docs-local: docs-nav
127+
.PHONY: docs
128+
docs: docs-nav docs-api
120129
@uv venv && \
121130
source .venv/bin/activate && \
122131
uv pip install mkdocs mkdocs-material && \
123132
uv run mkdocs serve
124133

125134
# Generates CLI markdown documentation
135+
.PHONY: docs-cli
126136
docs-cli:
127-
@go run -tags=docsgen_cli ./tools/docsgen/cli/cmds.go
137+
@go run -tags=docsgen_cli ./tools/docsgen/cmds/main.go
128138
@echo "mcpd CLI command documentation generated"
129139

140+
# Generates OpenAPI specification YAML
141+
.PHONY: docs-api
142+
docs-api:
143+
@go run -tags=docsgen_api ./tools/docsgen/api/openapi.go
144+
@echo "OpenAPI specification generated"
145+
130146
## Updates mkdocs.yaml nav to match generated CLI docs
147+
.PHONY: docs-nav
131148
docs-nav: docs-cli
132-
@go run -tags=docsgen_nav ./tools/docsgen/cli/nav.go
149+
@go run -tags=docsgen_nav ./tools/docsgen/nav/main.go
133150
@echo "navigation updated for MkDocs site"
134151

152+
.PHONY: local-up
135153
local-up: build-linux
136154
@echo "organizing binary for docker build"
137155
@mkdir -p $(TARGET_PLATFORM)
@@ -141,6 +159,7 @@ local-up: build-linux
141159
@echo "cleaning up temporary platform directory"
142160
@rm -rf $(TARGET_PLATFORM)
143161

162+
.PHONY: local-down
144163
local-down:
145164
@echo "stopping mcpd container"
146165
@docker compose down

docs/makefile.md

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,18 +137,13 @@ These commands manage the [MkDocs](https://www.mkdocs.org) developer documentati
137137
make docs-nav
138138
```
139139

140-
- **Serve the docs locally using MkDocs + uv**
141-
```bash
142-
make docs-local
143-
```
144-
145-
- **Full pipeline: generate CLI docs, update nav, serve locally**
140+
- **Serve the docs locally using MkDocs + uv: generate CLI docs, update nav, serve locally**
146141
```bash
147142
make docs
148143
```
149144

150145
!!! tip "First time?"
151-
The `docs-local` command will create a virtual environment using `uv`, install MkDocs + Material theme, and start the local server at [http://localhost:8000](http://localhost:8000).
146+
The `docs` command will create a virtual environment using `uv`, install MkDocs + Material theme, and start the local server at [http://localhost:8000/mcpd/](http://localhost:8000/mcpd/).
152147

153148
---
154149

@@ -165,8 +160,7 @@ Here's a complete list of Makefile targets:
165160
| `check-licenses` | Validate all dependency licenses are allowed |
166161
| `check-notice` | Verify NOTICE file is up to date |
167162
| `clean` | Remove compiled binary from working directory |
168-
| `docs` | Alias for `docs-local` (runs everything) |
169-
| `docs-cli` | Generate Markdown CLI reference docs |
163+
| `docs` | Serve docs locally via `mkdocs serve` |
170164
| `docs-local` | Serve docs locally via `mkdocs serve` |
171165
| `docs-nav` | Update CLI doc nav in `mkdocs.yaml` |
172166
| `install` | Install binary to system path |

internal/api/routes.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"reflect"
7+
8+
"github.com/danielgtaylor/huma/v2"
9+
10+
"github.com/mozilla-ai/mcpd/v2/internal/contracts"
11+
)
12+
13+
// APIVersion is the version used in the OpenAPI spec and URL paths.
14+
const APIVersion = "v1"
15+
16+
// RegisterRoutes registers all API routes on the provided Huma router.
17+
// This is the single source of truth for the API route structure.
18+
// Returns the API path prefix (e.g., "/api/v1") under which the routes are created.
19+
func RegisterRoutes(
20+
router huma.API,
21+
healthTracker contracts.MCPHealthMonitor,
22+
clientManager contracts.MCPClientAccessor,
23+
) (string, error) {
24+
if router == nil || reflect.ValueOf(router).IsNil() {
25+
return "", fmt.Errorf("router cannot be nil")
26+
}
27+
if clientManager == nil || reflect.ValueOf(clientManager).IsNil() {
28+
return "", fmt.Errorf("client manager cannot be nil")
29+
}
30+
if healthTracker == nil || reflect.ValueOf(healthTracker).IsNil() {
31+
return "", fmt.Errorf("health tracker cannot be nil")
32+
}
33+
34+
// Extract API version from the router's OpenAPI spec.
35+
apiVersionID := router.OpenAPI().Info.Version
36+
37+
// Safe way to ensure /api/{version}.
38+
apiPathPrefix, err := url.JoinPath("/api", apiVersionID)
39+
if err != nil {
40+
return "", fmt.Errorf("failed to construct API path prefix: %w", err)
41+
}
42+
43+
// Group all routes under the /api/{version} prefix.
44+
versionedGroup := huma.NewGroup(router, apiPathPrefix)
45+
RegisterHealthRoutes(versionedGroup, healthTracker, "/health")
46+
RegisterServerRoutes(versionedGroup, clientManager, "/servers")
47+
48+
return apiPathPrefix, nil
49+
}

0 commit comments

Comments
 (0)