Skip to content

Commit ff95400

Browse files
authored
Merge pull request #37 from redpanda-data/jb/bazel
Bazel build with nogo, replace CI with single Bazel job
2 parents d23a3b5 + ae4ba93 commit ff95400

162 files changed

Lines changed: 12748 additions & 25523 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.bazelrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Hermetic builds - no shell env leaking into actions
2+
build --incompatible_strict_action_env
3+
4+
# Only print test output on failure
5+
test --test_output=errors
6+
7+
# Some tests need $HOME (e.g. for config files)
8+
test --test_env=HOME

.bazelversion

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
9.0.1

.github/workflows/test.yml

Lines changed: 9 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -7,105 +7,18 @@ on:
77
branches: [ main ]
88

99
jobs:
10-
test:
11-
runs-on: ubuntu-latest
10+
build:
11+
runs-on: blacksmith-2vcpu-ubuntu-2404
1212

1313
steps:
1414
- uses: actions/checkout@v4
1515

16-
- name: Set up Go
17-
uses: actions/setup-go@v5
16+
- name: Setup Bazel
17+
uses: useblacksmith/setup-bazel@v1
1818
with:
19-
go-version: '1.24'
19+
bazelisk-cache: true
20+
disk-cache: ${{ github.ref }}
21+
repository-cache: true
2022

21-
- name: Set up Buf
22-
uses: bufbuild/buf-setup-action@v1
23-
with:
24-
github_token: ${{ secrets.GITHUB_TOKEN }}
25-
26-
- name: Cache Go modules
27-
uses: actions/cache@v4
28-
with:
29-
path: |
30-
~/.cache/go-build
31-
~/go/pkg/mod
32-
key: ${{ runner.os }}-go-1.24-${{ hashFiles('**/go.sum') }}
33-
restore-keys: |
34-
${{ runner.os }}-go-1.24-
35-
36-
- name: Download dependencies
37-
run: go mod download
38-
39-
- name: Run tests
40-
run: ./taskw test
41-
42-
- name: Check examples build
43-
run: |
44-
cd examples/basic && go build .
45-
cd ../openai-compat && go build .
46-
lint:
47-
runs-on: ubuntu-latest
48-
steps:
49-
- uses: actions/checkout@v4
50-
51-
- name: Set up Go
52-
uses: actions/setup-go@v5
53-
with:
54-
go-version: '1.24'
55-
56-
- name: golangci-lint
57-
uses: golangci/golangci-lint-action@v7
58-
with:
59-
version: v2.1.6
60-
61-
buf-lint:
62-
runs-on: ubuntu-latest
63-
steps:
64-
- uses: actions/checkout@v4
65-
66-
- name: Set up Buf
67-
uses: bufbuild/buf-setup-action@v1
68-
with:
69-
github_token: ${{ secrets.GITHUB_TOKEN }}
70-
71-
- name: Buf lint
72-
run: |
73-
cd examples/basic && buf lint || echo "Buf lint disabled due to package structure"
74-
cd examples/openai-compat && buf lint || echo "Buf lint disabled due to package structure"
75-
cd pkg/generator && buf lint || echo "Buf lint disabled due to package structure"
76-
77-
- name: Buf format check
78-
run: |
79-
cd examples/basic && buf format --diff --exit-code . || echo "Buf format check disabled"
80-
cd examples/openai-compat && buf format --diff --exit-code . || echo "Buf format check disabled"
81-
cd pkg/generator && buf format --diff --exit-code . || echo "Buf format check disabled"
82-
83-
golden-files:
84-
runs-on: ubuntu-latest
85-
steps:
86-
- uses: actions/checkout@v4
87-
88-
- name: Set up Go
89-
uses: actions/setup-go@v5
90-
with:
91-
go-version: '1.24'
92-
93-
- name: Set up Buf
94-
uses: bufbuild/buf-setup-action@v1
95-
with:
96-
github_token: ${{ secrets.GITHUB_TOKEN }}
97-
98-
- name: Install protoc-gen-go
99-
run: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
100-
101-
- name: Check golden files are up to date
102-
run: |
103-
# Generate fresh files using task (includes gofumpt formatting)
104-
./taskw generate-golden
105-
106-
# Check if any files changed
107-
if ! git diff --quiet; then
108-
echo "Golden files are out of date. Please run: task generate-golden"
109-
git diff
110-
exit 1
111-
fi
23+
- name: Build and test
24+
run: bazelisk test //...

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,10 @@ go.work.sum
2424
# env file
2525
.env
2626
/.build
27+
28+
# Bazel
29+
/bazel-*
30+
MODULE.bazel.lock
2731
/examples/basic/basic
2832
/examples/openai-compat/openai-compat
33+
/examples/dynamic/dynamic

BUILD.bazel

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
load("@gazelle//:def.bzl", "gazelle")
2+
load("@rules_go//go:def.bzl", "nogo")
3+
4+
# gazelle:prefix github.com/redpanda-data/protoc-gen-go-mcp
5+
# gazelle:proto disable_global
6+
# gazelle:exclude pkg/testdata/gen/go/google
7+
# gazelle:exclude pkg/testdata/gen/go/buf
8+
# gazelle:exclude pkg/testdata/proto
9+
gazelle(name = "gazelle")
10+
11+
nogo(
12+
name = "nogo",
13+
config = "nogo_config.json",
14+
visibility = ["//visibility:public"],
15+
deps = [
16+
# stdlib analyzers
17+
"@org_golang_x_tools//go/analysis/passes/appends:go_default_library",
18+
"@org_golang_x_tools//go/analysis/passes/asmdecl:go_default_library",
19+
"@org_golang_x_tools//go/analysis/passes/assign:go_default_library",
20+
"@org_golang_x_tools//go/analysis/passes/atomic:go_default_library",
21+
"@org_golang_x_tools//go/analysis/passes/bools:go_default_library",
22+
"@org_golang_x_tools//go/analysis/passes/buildtag:go_default_library",
23+
"@org_golang_x_tools//go/analysis/passes/composite:go_default_library",
24+
"@org_golang_x_tools//go/analysis/passes/copylock:go_default_library",
25+
"@org_golang_x_tools//go/analysis/passes/deepequalerrors:go_default_library",
26+
"@org_golang_x_tools//go/analysis/passes/defers:go_default_library",
27+
"@org_golang_x_tools//go/analysis/passes/directive:go_default_library",
28+
"@org_golang_x_tools//go/analysis/passes/errorsas:go_default_library",
29+
"@org_golang_x_tools//go/analysis/passes/httpresponse:go_default_library",
30+
"@org_golang_x_tools//go/analysis/passes/ifaceassert:go_default_library",
31+
"@org_golang_x_tools//go/analysis/passes/loopclosure:go_default_library",
32+
"@org_golang_x_tools//go/analysis/passes/lostcancel:go_default_library",
33+
"@org_golang_x_tools//go/analysis/passes/nilfunc:go_default_library",
34+
"@org_golang_x_tools//go/analysis/passes/nilness:go_default_library",
35+
"@org_golang_x_tools//go/analysis/passes/printf:go_default_library",
36+
"@org_golang_x_tools//go/analysis/passes/shadow:go_default_library",
37+
"@org_golang_x_tools//go/analysis/passes/shift:go_default_library",
38+
"@org_golang_x_tools//go/analysis/passes/sigchanyzer:go_default_library",
39+
"@org_golang_x_tools//go/analysis/passes/sortslice:go_default_library",
40+
"@org_golang_x_tools//go/analysis/passes/stdmethods:go_default_library",
41+
"@org_golang_x_tools//go/analysis/passes/stringintconv:go_default_library",
42+
"@org_golang_x_tools//go/analysis/passes/structtag:go_default_library",
43+
"@org_golang_x_tools//go/analysis/passes/tests:go_default_library",
44+
"@org_golang_x_tools//go/analysis/passes/timeformat:go_default_library",
45+
"@org_golang_x_tools//go/analysis/passes/unmarshal:go_default_library",
46+
"@org_golang_x_tools//go/analysis/passes/unreachable:go_default_library",
47+
"@org_golang_x_tools//go/analysis/passes/unsafeptr:go_default_library",
48+
"@org_golang_x_tools//go/analysis/passes/unusedresult:go_default_library",
49+
"@org_golang_x_tools//go/analysis/passes/waitgroup:go_default_library",
50+
# staticcheck SA analyzers
51+
"@co_honnef_go_tools//staticcheck/sa1000",
52+
"@co_honnef_go_tools//staticcheck/sa1001",
53+
"@co_honnef_go_tools//staticcheck/sa1002",
54+
"@co_honnef_go_tools//staticcheck/sa1003",
55+
"@co_honnef_go_tools//staticcheck/sa1004",
56+
"@co_honnef_go_tools//staticcheck/sa1005",
57+
"@co_honnef_go_tools//staticcheck/sa1006",
58+
"@co_honnef_go_tools//staticcheck/sa1007",
59+
"@co_honnef_go_tools//staticcheck/sa1008",
60+
"@co_honnef_go_tools//staticcheck/sa1010",
61+
"@co_honnef_go_tools//staticcheck/sa1011",
62+
"@co_honnef_go_tools//staticcheck/sa1012",
63+
"@co_honnef_go_tools//staticcheck/sa1013",
64+
"@co_honnef_go_tools//staticcheck/sa1014",
65+
"@co_honnef_go_tools//staticcheck/sa1015",
66+
"@co_honnef_go_tools//staticcheck/sa1016",
67+
"@co_honnef_go_tools//staticcheck/sa1017",
68+
"@co_honnef_go_tools//staticcheck/sa1018",
69+
"@co_honnef_go_tools//staticcheck/sa1019",
70+
"@co_honnef_go_tools//staticcheck/sa1020",
71+
"@co_honnef_go_tools//staticcheck/sa1021",
72+
"@co_honnef_go_tools//staticcheck/sa1023",
73+
"@co_honnef_go_tools//staticcheck/sa1024",
74+
"@co_honnef_go_tools//staticcheck/sa1025",
75+
"@co_honnef_go_tools//staticcheck/sa1026",
76+
"@co_honnef_go_tools//staticcheck/sa1027",
77+
"@co_honnef_go_tools//staticcheck/sa1028",
78+
"@co_honnef_go_tools//staticcheck/sa1029",
79+
"@co_honnef_go_tools//staticcheck/sa1030",
80+
"@co_honnef_go_tools//staticcheck/sa1031",
81+
"@co_honnef_go_tools//staticcheck/sa1032",
82+
"@co_honnef_go_tools//staticcheck/sa2000",
83+
"@co_honnef_go_tools//staticcheck/sa2001",
84+
"@co_honnef_go_tools//staticcheck/sa2002",
85+
"@co_honnef_go_tools//staticcheck/sa2003",
86+
"@co_honnef_go_tools//staticcheck/sa3000",
87+
"@co_honnef_go_tools//staticcheck/sa3001",
88+
"@co_honnef_go_tools//staticcheck/sa4000",
89+
"@co_honnef_go_tools//staticcheck/sa4001",
90+
"@co_honnef_go_tools//staticcheck/sa4003",
91+
"@co_honnef_go_tools//staticcheck/sa4004",
92+
"@co_honnef_go_tools//staticcheck/sa4005",
93+
"@co_honnef_go_tools//staticcheck/sa4006",
94+
"@co_honnef_go_tools//staticcheck/sa4008",
95+
"@co_honnef_go_tools//staticcheck/sa4009",
96+
"@co_honnef_go_tools//staticcheck/sa4010",
97+
"@co_honnef_go_tools//staticcheck/sa4011",
98+
"@co_honnef_go_tools//staticcheck/sa4012",
99+
"@co_honnef_go_tools//staticcheck/sa4013",
100+
"@co_honnef_go_tools//staticcheck/sa4014",
101+
"@co_honnef_go_tools//staticcheck/sa4015",
102+
"@co_honnef_go_tools//staticcheck/sa4016",
103+
"@co_honnef_go_tools//staticcheck/sa4017",
104+
"@co_honnef_go_tools//staticcheck/sa4018",
105+
"@co_honnef_go_tools//staticcheck/sa4019",
106+
"@co_honnef_go_tools//staticcheck/sa4020",
107+
"@co_honnef_go_tools//staticcheck/sa4021",
108+
"@co_honnef_go_tools//staticcheck/sa4022",
109+
"@co_honnef_go_tools//staticcheck/sa4023",
110+
"@co_honnef_go_tools//staticcheck/sa4024",
111+
"@co_honnef_go_tools//staticcheck/sa4025",
112+
"@co_honnef_go_tools//staticcheck/sa4026",
113+
"@co_honnef_go_tools//staticcheck/sa4027",
114+
"@co_honnef_go_tools//staticcheck/sa4028",
115+
"@co_honnef_go_tools//staticcheck/sa4029",
116+
"@co_honnef_go_tools//staticcheck/sa4030",
117+
"@co_honnef_go_tools//staticcheck/sa4031",
118+
"@co_honnef_go_tools//staticcheck/sa4032",
119+
"@co_honnef_go_tools//staticcheck/sa5000",
120+
"@co_honnef_go_tools//staticcheck/sa5001",
121+
"@co_honnef_go_tools//staticcheck/sa5002",
122+
"@co_honnef_go_tools//staticcheck/sa5003",
123+
"@co_honnef_go_tools//staticcheck/sa5004",
124+
"@co_honnef_go_tools//staticcheck/sa5005",
125+
"@co_honnef_go_tools//staticcheck/sa5007",
126+
"@co_honnef_go_tools//staticcheck/sa5008",
127+
"@co_honnef_go_tools//staticcheck/sa5009",
128+
"@co_honnef_go_tools//staticcheck/sa5010",
129+
"@co_honnef_go_tools//staticcheck/sa5011",
130+
"@co_honnef_go_tools//staticcheck/sa5012",
131+
"@co_honnef_go_tools//staticcheck/sa6000",
132+
"@co_honnef_go_tools//staticcheck/sa6001",
133+
"@co_honnef_go_tools//staticcheck/sa6002",
134+
"@co_honnef_go_tools//staticcheck/sa6003",
135+
"@co_honnef_go_tools//staticcheck/sa6005",
136+
"@co_honnef_go_tools//staticcheck/sa6006",
137+
"@co_honnef_go_tools//staticcheck/sa9001",
138+
"@co_honnef_go_tools//staticcheck/sa9002",
139+
"@co_honnef_go_tools//staticcheck/sa9003",
140+
"@co_honnef_go_tools//staticcheck/sa9004",
141+
"@co_honnef_go_tools//staticcheck/sa9005",
142+
"@co_honnef_go_tools//staticcheck/sa9006",
143+
"@co_honnef_go_tools//staticcheck/sa9007",
144+
"@co_honnef_go_tools//staticcheck/sa9008",
145+
"@co_honnef_go_tools//staticcheck/sa9009",
146+
],
147+
)

CLAUDE.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# protoc-gen-go-mcp
2+
3+
Protoc plugin + runtime library for generating MCP (Model Context Protocol) server handlers from gRPC service definitions.
4+
5+
## Build & Test
6+
7+
```bash
8+
# Go (primary)
9+
./taskw test # Run unit tests with race detector
10+
./taskw test-cover # Run tests with coverage report
11+
./taskw build # Build binary
12+
./taskw generate # Regenerate proto code + descriptor set
13+
./taskw lint # Run golangci-lint
14+
./taskw conformancetest # Run conformance tests against LLM providers (needs API keys)
15+
./taskw integrationtest # Run all integration tests (needs API keys)
16+
17+
# Bazel
18+
just test # bazelisk test //...
19+
just build # bazelisk build //...
20+
just gazelle # Sync BUILD files from go.mod
21+
just generate # Same as taskw generate but from justfile
22+
```
23+
24+
## Architecture
25+
26+
```
27+
cmd/protoc-gen-go-mcp/ Entry point for protoc plugin
28+
pkg/gen/ Core library (THE important package):
29+
schema.go JSON schema generation from protoreflect descriptors
30+
register.go Dynamic MCP tool registration at runtime
31+
pkg/generator/ Protoc plugin: Go template output, delegates to pkg/gen
32+
pkg/runtime/ Runtime helpers: FixOpenAI, error handling, extra properties
33+
pkg/testdata/ Proto files + generated code for testing
34+
conformancetest/ E2E tests against real LLM providers (Gemini, OpenAI, Anthropic)
35+
```
36+
37+
### Two modes of operation
38+
39+
1. **Static (codegen)**: `protoc-gen-go-mcp` generates `*.pb.mcp.go` with pre-computed schemas
40+
2. **Dynamic (runtime)**: `gen.RegisterService()` creates MCP tools from any `protoreflect.ServiceDescriptor` at runtime - no codegen needed. This is the proxy/gateway mode.
41+
42+
## Key APIs
43+
44+
### Static registration (from generated code)
45+
```go
46+
testdatamcp.RegisterTestServiceHandler(mcpServer, myServiceImpl)
47+
testdatamcp.RegisterTestServiceHandlerOpenAI(mcpServer, myServiceImpl)
48+
```
49+
50+
### Dynamic registration (runtime, no codegen)
51+
```go
52+
gen.RegisterService(mcpServer, serviceDescriptor, handler, gen.RegisterServiceOptions{
53+
Provider: runtime.LLMProviderOpenAI,
54+
NewMessage: func(md protoreflect.MessageDescriptor) proto.Message { ... },
55+
})
56+
```
57+
58+
### Schema generation (library use)
59+
```go
60+
schema := gen.MessageSchema(msgDescriptor, gen.SchemaOptions{OpenAICompat: true})
61+
standard, openAI := gen.ToolForMethod(methodDescriptor, "description")
62+
```
63+
64+
## Key Design Decisions
65+
66+
- Two schema modes: standard MCP and OpenAI-compatible (`gen.SchemaOptions`)
67+
- OpenAI mode: maps -> arrays of KV pairs, all fields required, additionalProperties: false
68+
- Well-known types (Struct, Value, ListValue) become JSON strings in OpenAI mode
69+
- Tool names > 64 chars get hash-mangled (Claude desktop limit)
70+
- `pkg/gen` is fully independent of protoc - works with any protoreflect descriptor
71+
- Golden test re-runs generator in-process from compiled descriptors, no shell/buf at test time
72+
73+
## Testing
74+
75+
- Unit tests: `go test ./pkg/...` (with -race, always)
76+
- Conformance tests: `go test -tags=integration ./conformancetest/` (needs API keys)
77+
- Golden test: in-process generator re-run vs checked-in `gen/go/*.pb.mcp.go`
78+
- Edge case protos: `pkg/testdata/proto/testdata/edge_cases.proto`
79+
- Fuzz tests: `pkg/runtime/fix_fuzz_test.go`, `pkg/gen/schema_fuzz_test.go`
80+
81+
## Proto Generation
82+
83+
Uses `buf`. Test protos in `pkg/testdata/proto/`.
84+
After changing protos: `./taskw generate`.
85+
86+
## Development workflow
87+
88+
1. Edit proto or generator code
89+
2. `./taskw generate` (regenerates test proto Go code + descriptor set)
90+
3. `./taskw test` (runs unit tests, including golden comparison)

0 commit comments

Comments
 (0)