From 17408c29aaed0609312faa9627d72e7d506ef9f8 Mon Sep 17 00:00:00 2001 From: Khasbulat Abdullin Date: Tue, 14 Apr 2026 18:55:04 +0300 Subject: [PATCH] feat: add python MCP server generation --- .github/workflows/tests.yml | 19 + .gitignore | 14 +- AGENTS.md | 117 +- README.md | 112 +- cmd/example-python-mcp-server/main.py | 14 + cmd/protoc-gen-mcp/main.go | 63 +- easyp.test.yaml | 8 + easyp.yaml | 8 + examples/1-helloworld/proto/helloworld.pb.go | 234 --- .../{1-helloworld => 1_helloworld}/main.go | 2 +- examples/1_helloworld/main.py | 47 + examples/1_helloworld/proto/__init__.py | 1 + .../proto/helloworld.mcp.go | 2 +- examples/1_helloworld/proto/helloworld.pb.go | 178 ++ .../proto/helloworld.proto | 2 +- examples/1_helloworld/proto/helloworld_mcp.py | 443 +++++ examples/1_helloworld/proto/helloworld_pb2.py | 48 + examples/2-weather-api/proto/weather.pb.go | 378 ----- .../{2-weather-api => 2_weather_api}/main.go | 2 +- examples/2_weather_api/main.py | 68 + examples/2_weather_api/proto/__init__.py | 1 + .../proto/weather.mcp.go | 4 +- examples/2_weather_api/proto/weather.pb.go | 292 ++++ .../proto/weather.proto | 2 +- examples/2_weather_api/proto/weather_mcp.py | 488 ++++++ examples/2_weather_api/proto/weather_pb2.py | 58 + .../3-file-manager/proto/filemanager.pb.go | 380 ----- .../main.go | 4 +- examples/3_file_manager/main.py | 80 + examples/3_file_manager/proto/__init__.py | 1 + .../proto/filemanager.mcp.go | 4 +- .../3_file_manager/proto/filemanager.pb.go | 275 ++++ .../proto/filemanager.proto | 2 +- .../3_file_manager/proto/filemanager_mcp.py | 491 ++++++ .../3_file_manager/proto/filemanager_pb2.py | 56 + examples/4-crm-system/proto/crm.pb.go | 524 ------ .../{4-crm-system => 4_crm_system}/main.go | 2 +- examples/4_crm_system/main.py | 119 ++ examples/4_crm_system/proto/__init__.py | 1 + .../proto/crm.mcp.go | 4 +- examples/4_crm_system/proto/crm.pb.go | 390 +++++ .../proto/crm.proto | 2 +- examples/4_crm_system/proto/crm_mcp.py | 520 ++++++ examples/4_crm_system/proto/crm_pb2.py | 72 + examples/5_python_standalone/.gitignore | 4 + examples/5_python_standalone/Makefile | 31 + examples/5_python_standalone/README.md | 60 + examples/5_python_standalone/easyp.lock | 1 + examples/5_python_standalone/easyp.yaml | 37 + examples/5_python_standalone/mcp/__init__.py | 5 + .../mcp/options/v1/options_pb2.py | 68 + .../5_python_standalone/proto/__init__.py | 1 + .../5_python_standalone/proto/notebook.proto | 158 ++ .../5_python_standalone/proto/notebook_mcp.py | 588 +++++++ .../5_python_standalone/proto/notebook_pb2.py | 88 + examples/5_python_standalone/pyproject.toml | 22 + examples/5_python_standalone/server.py | 91 ++ examples/Makefile | 9 +- examples/README.md | 58 +- examples/easyp.lock | 1 + examples/easyp.yaml | 23 +- examples/mcp/__init__.py | 5 + examples/mcp/options/v1/options_pb2.py | 68 + examples/python_stdio_test.go | 446 +++++ go.mod | 2 + go.sum | 12 + internal/codegen/collect.go | 196 +++ internal/codegen/collect_test.go | 524 ++++++ internal/codegen/generator.go | 372 ++--- internal/codegen/generator_test.go | 554 ++++++- internal/codegen/model.go | 44 + internal/codegen/options.go | 122 ++ internal/codegen/options_test.go | 149 ++ internal/codegen/python_contract_test.go | 845 ++++++++++ internal/codegen/python_mapper_test.go | 804 +++++++++ internal/codegen/python_names.go | 225 +++ internal/codegen/python_request.go | 65 + internal/codegen/python_request_test.go | 99 ++ internal/codegen/python_support.go | 14 + internal/codegen/python_type_collect.go | 586 +++++++ internal/codegen/python_type_collect_test.go | 498 ++++++ internal/codegen/python_type_model.go | 132 ++ internal/codegen/render_go.go | 235 +++ internal/codegen/render_python.go | 386 +++++ internal/codegen/render_python_mappers.go | 541 ++++++ internal/codegen/render_python_runtime.go | 231 +++ internal/codegen/render_python_types.go | 298 ++++ internal/examplemcp/python_server.py | 205 +++ internal/examplemcp/stdio_test.go | 108 +- internal/testproto/example/v1/__init__.py | 1 + internal/testproto/example/v1/example.pb.go | 1444 +++++------------ internal/testproto/example/v1/example_mcp.py | 1167 +++++++++++++ internal/testproto/example/v1/example_pb2.py | 131 ++ mcp/__init__.py | 5 + mcp/options/v1/__init__.py | 1 + mcp/options/v1/options.pb.go | 797 +++------ mcp/options/v1/options_mcp.py | 757 +++++++++ mcp/options/v1/options_pb2.py | 68 + testdata/golden/example_mcp.py.golden | 1167 +++++++++++++ testdata/unsupported/v1/streaming.proto | 13 + 100 files changed, 16666 insertions(+), 3428 deletions(-) create mode 100644 cmd/example-python-mcp-server/main.py delete mode 100644 examples/1-helloworld/proto/helloworld.pb.go rename examples/{1-helloworld => 1_helloworld}/main.go (92%) create mode 100644 examples/1_helloworld/main.py create mode 100644 examples/1_helloworld/proto/__init__.py rename examples/{1-helloworld => 1_helloworld}/proto/helloworld.mcp.go (97%) create mode 100644 examples/1_helloworld/proto/helloworld.pb.go rename examples/{1-helloworld => 1_helloworld}/proto/helloworld.proto (95%) create mode 100644 examples/1_helloworld/proto/helloworld_mcp.py create mode 100644 examples/1_helloworld/proto/helloworld_pb2.py delete mode 100644 examples/2-weather-api/proto/weather.pb.go rename examples/{2-weather-api => 2_weather_api}/main.go (94%) create mode 100644 examples/2_weather_api/main.py create mode 100644 examples/2_weather_api/proto/__init__.py rename examples/{2-weather-api => 2_weather_api}/proto/weather.mcp.go (96%) create mode 100644 examples/2_weather_api/proto/weather.pb.go rename examples/{2-weather-api => 2_weather_api}/proto/weather.proto (97%) create mode 100644 examples/2_weather_api/proto/weather_mcp.py create mode 100644 examples/2_weather_api/proto/weather_pb2.py delete mode 100644 examples/3-file-manager/proto/filemanager.pb.go rename examples/{3-file-manager => 3_file_manager}/main.go (94%) create mode 100644 examples/3_file_manager/main.py create mode 100644 examples/3_file_manager/proto/__init__.py rename examples/{3-file-manager => 3_file_manager}/proto/filemanager.mcp.go (96%) create mode 100644 examples/3_file_manager/proto/filemanager.pb.go rename examples/{3-file-manager => 3_file_manager}/proto/filemanager.proto (97%) create mode 100644 examples/3_file_manager/proto/filemanager_mcp.py create mode 100644 examples/3_file_manager/proto/filemanager_pb2.py delete mode 100644 examples/4-crm-system/proto/crm.pb.go rename examples/{4-crm-system => 4_crm_system}/main.go (97%) create mode 100644 examples/4_crm_system/main.py create mode 100644 examples/4_crm_system/proto/__init__.py rename examples/{4-crm-system => 4_crm_system}/proto/crm.mcp.go (98%) create mode 100644 examples/4_crm_system/proto/crm.pb.go rename examples/{4-crm-system => 4_crm_system}/proto/crm.proto (98%) create mode 100644 examples/4_crm_system/proto/crm_mcp.py create mode 100644 examples/4_crm_system/proto/crm_pb2.py create mode 100644 examples/5_python_standalone/.gitignore create mode 100644 examples/5_python_standalone/Makefile create mode 100644 examples/5_python_standalone/README.md create mode 100644 examples/5_python_standalone/easyp.lock create mode 100644 examples/5_python_standalone/easyp.yaml create mode 100644 examples/5_python_standalone/mcp/__init__.py create mode 100644 examples/5_python_standalone/mcp/options/v1/options_pb2.py create mode 100644 examples/5_python_standalone/proto/__init__.py create mode 100644 examples/5_python_standalone/proto/notebook.proto create mode 100644 examples/5_python_standalone/proto/notebook_mcp.py create mode 100644 examples/5_python_standalone/proto/notebook_pb2.py create mode 100644 examples/5_python_standalone/pyproject.toml create mode 100644 examples/5_python_standalone/server.py create mode 100644 examples/easyp.lock create mode 100644 examples/mcp/__init__.py create mode 100644 examples/mcp/options/v1/options_pb2.py create mode 100644 examples/python_stdio_test.go create mode 100644 internal/codegen/collect.go create mode 100644 internal/codegen/collect_test.go create mode 100644 internal/codegen/model.go create mode 100644 internal/codegen/options.go create mode 100644 internal/codegen/options_test.go create mode 100644 internal/codegen/python_contract_test.go create mode 100644 internal/codegen/python_mapper_test.go create mode 100644 internal/codegen/python_names.go create mode 100644 internal/codegen/python_request.go create mode 100644 internal/codegen/python_request_test.go create mode 100644 internal/codegen/python_support.go create mode 100644 internal/codegen/python_type_collect.go create mode 100644 internal/codegen/python_type_collect_test.go create mode 100644 internal/codegen/python_type_model.go create mode 100644 internal/codegen/render_go.go create mode 100644 internal/codegen/render_python.go create mode 100644 internal/codegen/render_python_mappers.go create mode 100644 internal/codegen/render_python_runtime.go create mode 100644 internal/codegen/render_python_types.go create mode 100644 internal/examplemcp/python_server.py create mode 100644 internal/testproto/example/v1/__init__.py create mode 100644 internal/testproto/example/v1/example_mcp.py create mode 100644 internal/testproto/example/v1/example_pb2.py create mode 100644 mcp/__init__.py create mode 100644 mcp/options/v1/__init__.py create mode 100644 mcp/options/v1/options_mcp.py create mode 100644 mcp/options/v1/options_pb2.py create mode 100644 testdata/golden/example_mcp.py.golden create mode 100644 testdata/unsupported/v1/streaming.proto diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 563e7a9..96bc982 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,9 +20,23 @@ jobs: with: go-version: "1.24" + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install Python runtime dependencies + run: | + python -m pip install --upgrade pip + python -m pip install "mcp>=1.27,<2" "protobuf>=6,<7" "jsonschema>=4,<5" + - name: Install easyp run: go install github.com/easyp-tech/easyp/cmd/easyp@v0.15.2-rc1 + - name: Install protoc plugins + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11 + - name: Validate configs run: | easyp --cfg easyp.yaml validate-config @@ -38,6 +52,11 @@ jobs: easyp --cfg easyp.yaml generate -p mcp -r . easyp --cfg easyp.test.yaml generate -p internal/testproto -r . + - name: Regenerate example artifacts + run: | + cd examples + make generate + - name: Verify generated files are up to date run: git diff --exit-code diff --git a/.gitignore b/.gitignore index 29ab889..c611082 100644 --- a/.gitignore +++ b/.gitignore @@ -27,9 +27,15 @@ go.work.sum # env file .env +# Python runtime artifacts +__pycache__/ +*.py[cod] +*.egg-info/ + # Editor/IDE # .idea/ # .vscode/ +.worktrees/ # Local binaries and release artifacts /example-mcp-server @@ -37,8 +43,8 @@ go.work.sum dist/ # Example server binaries -examples/1-helloworld/helloworld-server -examples/2-weather-api/weather-server -examples/3-file-manager/file-server -examples/4-crm-system/crm-server +examples/1_helloworld/helloworld-server +examples/2_weather_api/weather-server +examples/3_file_manager/file-server +examples/4_crm_system/crm-server .DS_Store diff --git a/AGENTS.md b/AGENTS.md index 6312b1a..5bed9d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,26 +2,42 @@ ## Scope -This repository implements a protobuf-first MCP generator and runtime for Go. -The MVP is intentionally narrow and must stay decision-consistent with the -current architecture unless explicitly revised. +This repository implements a protobuf-first MCP generator and runtime for Go +and Python MCP server bindings. The MVP is intentionally narrow and must stay +decision-consistent with the current architecture unless explicitly revised. ## Stack - Go 1.24+ +- Python 3.10+ for generated-runtime verification and example servers - `easyp v0.15.2-rc1` for repository linting and code generation workflows - `google.golang.org/protobuf` for code generation, reflection, and ProtoJSON +- `google.protobuf` for Python generated modules and ProtoJSON conversion - `github.com/modelcontextprotocol/go-sdk/mcp` as the MCP runtime +- `mcp>=1.27,<2` as the official Python MCP SDK target - `github.com/google/jsonschema-go/jsonschema` for JSON Schema parsing and validation +- `github.com/bufbuild/protocompile` for in-process descriptor compilation in + generator tests, so `go test` does not require an external `protoc` binary ## Layout - `cmd/protoc-gen-mcp`: protoc plugin entrypoint for `--mcp_out` - `cmd/example-mcp-server`: runnable stdio MCP server for manual agent/client checks +- `cmd/example-python-mcp-server`: runnable stdio MCP server for Python SDK parity checks - `mcpruntime`: public runtime helpers used by generated code - `.github/workflows`: GitHub Actions CI and release workflows - `.goreleaser.yaml`: release packaging for the plugin binary +- `examples`: standalone Go/Python integration projects; example directories + use numeric underscore prefixes such as `1_helloworld`, `4_crm_system`, and + `5_python_standalone` +- `examples/easyp.lock`: pinned Easyp dependency lock for standalone examples +- `examples/mcp`: generated Python `mcp.options.*` protobuf modules for + standalone examples; generated from the GitHub dependency declared in + `examples/easyp.yaml`, not from the local repository root +- `examples/5_python_standalone`: Python-only user-style example with its own + `pyproject.toml`, `easyp.yaml`, generated `proto`/`mcp` packages, and stdio + server - `easyp.yaml`: main repository config for shipped protobuf APIs - `easyp.test.yaml`: development and test config for fixture generation - `mcp/options/v1/options.proto`: custom protobuf options for MCP metadata @@ -29,6 +45,17 @@ current architecture unless explicitly revised. - `internal/examplemcp`: reusable example MCP server wiring and stdio smoke test - `internal/schema`: protobuf descriptor to JSON Schema conversion - `internal/testproto`: protobuf fixtures and generated code used in repository tests +- `internal/testproto/example/v1/__init__.py`: generated Python package marker + for the shared test fixture package +- `mcp/__init__.py`: generated Python package bridge that lets `mcp.options.*` + protobuf modules coexist with the official Python MCP SDK package +- `mcp/options/v1/options_mcp.py`: generated Python dataclass/runtime bindings + for the shipped MCP options protobuf API +- `mcp/options/v1/__init__.py`: generated Python package marker for the shipped + options package +- Python generation emits package `__init__.py` files next to generated + `*_mcp.py` modules so generated directories can be imported as Python + packages - `testdata/golden`: golden snapshots for generated `*.mcp.go` files - `testdata/unsupported`: negative fixtures for fail-fast generator coverage @@ -68,18 +95,78 @@ current architecture unless explicitly revised. - Generated code exposes `ToolHandler` - Generated code exposes `RegisterTools(server, impl, opts...) error` +- Generated Python modules expose `ToolHandler` +- Generated Python modules expose `register__tools(server, impl, *, namespace=None)` +- Generated Python modules expose dataclasses, `UNSET`, and explicit `oneof` + wrapper variants from `*_mcp.py`; user handler code should not depend on + `*_pb2.py` - Runtime exposes only the minimal registration options used by generated code - Generated MCP tool names must not contain dots; namespace prefixes and method names are joined with underscores, and any dots in configured segments are normalized to underscores +- Generated tool annotations are forwarded exactly as declared in proto + options; omitted hints stay omitted, so external clients may apply their own + display defaults for missing values ## Current Status - Implemented: - `cmd/protoc-gen-mcp` plugin scaffold and generated `*.mcp.go` bindings + - typed plugin option parsing for `lang=go|python` and + `python_runtime=google.protobuf|betterproto|grpclib` + - single-source custom generator option handling through + `protogen.Options.ParamFunc`, with fail-fast rejection of unknown + `protoc-gen-mcp` params + - Python-mode request preparation synthesizes internal `go_package` metadata + for `.proto` files that omit it, so Python-only users do not need + Go-specific proto options just to run `lang=python` + - generated self-contained Python `*_mcp.py` bindings for + `lang=python,python_runtime=google.protobuf`, including handler protocols, + dataclasses, `UNSET`, explicit `oneof` wrapper variants, schema JSON + constants, shared per-server registry wiring, namespace-aware + `register__tools(...)`, generated protobuf<->dataclass + mappers, and ProtoJSON dict/message conversion via `json_format.ParseDict` + - generated Python `mcp/__init__.py` bridge support file so `mcp.options.*` + protobuf output coexists with the official `mcp` SDK package namespace + - generated Python package `__init__.py` files next to `*_mcp.py` output so + examples can import generated bindings through package imports such as + `from proto import helloworld_mcp` + - checked-in generated Python package markers and Python bindings for shipped + and test protobuf APIs, including `mcp/options/v1/options_mcp.py` and + `internal/testproto/example/v1/__init__.py` + - Python generator-level parity coverage against the shared semantic IR, + including Python golden output, public API-shape assertions, and + fail-fast negative tests for unsupported runtimes, unsupported protobuf + descriptors, and streaming RPCs + - runnable Python stdio example server in `cmd/example-python-mcp-server` + backed by generated dataclass-based `example_mcp.py` bindings and the + official MCP Python SDK + - standalone Python examples in `examples/` implemented against generated + dataclasses from `*_mcp.py`, with checked-in Python example artifacts + regenerated through `examples/Makefile` + - `examples/5_python_standalone` models an external Python-only project with + its own `pyproject.toml`, `easyp.yaml`, generated package `__init__.py` + files, generated local `mcp.options.*` modules, and `server.py` without + `sys.path` bootstrap code + - standalone examples use `examples/easyp.yaml` `deps` plus + `examples/easyp.lock` to resolve `mcp/options/v1/options.proto` from + `github.com/easyp-tech/protoc-gen-mcp` instead of depending on the local + repository root + - end-to-end stdio integration coverage that runs the shared example MCP + contract checks against both the Go server and the Python SDK server, + including `CallToolResult` text/structured parity and output-validation + failure coverage for Python - custom MCP protobuf options in `mcp/options/v1/options.proto` - generated tool metadata includes `ToolAnnotations` (`read_only_hint`, `destructive_hint`, `idempotent_hint`, `open_world_hint`) and `Icon` mappings directly to the Go SDK - - dedicated `examples/` directory featuring 4 standalone integration projects spanning quickstarts to complex CRM mocks + - `read_only_hint` now propagates correctly into both generated Go and + Python tool annotations + - Python generation augments each file's public type module with only the + current-file types actually referenced by other generated files, so + cross-file imports work without forcing unrelated hidden-only types into + the public API surface + - dedicated `examples/` directory featuring 5 standalone integration + projects spanning quickstarts to complex CRM mocks and a pure Python + user-style project - support for `oneof` explicit requiredness through `mcp.options.v1.oneof` options - strict schema generation correctly differentiating zero-values (`0`, `0.0`, `""`) using pointer constraints - runtime registration and JSON Schema validation in `mcpruntime` @@ -122,11 +209,25 @@ current architecture unless explicitly revised. - Verified: - `easyp` lint and generation flows for `mcp` and `internal/testproto` - `go test ./...` - - stdio smoke test via `internal/examplemcp/stdio_test.go` + - stdio smoke tests via `internal/examplemcp/stdio_test.go` + - Python stdio integration coverage for the shared server: + `go test ./internal/examplemcp -run 'TestPythonServerOverStdio|TestPythonServerRejectsInvalidOutputOverStdio' -count=1` + - Python stdio integration coverage for standalone examples: + `go test ./examples -run TestPythonExamplesOverStdio -count=1` + - Python stdio integration coverage for the Python-only standalone example: + `go test ./examples -run TestStandalonePythonExampleOverStdio -count=1` + - `internal/codegen` tests build descriptor requests in-process through + `protocompile`, so `go test ./...` no longer depends on `protoc` being in + `PATH` - client-side schema acceptance checks against advertised `tools/list` `inputSchema` for canonical valid payloads, including recursive objects and explicit `null` on fields that are not schema-required + - end-to-end official MCP Python SDK execution over stdio using generated + Python bindings, including runtime output-validation failure handling - manual Cursor validation of `example_Health` and `example_CreateReport` + - manual Inspector CLI verification of standalone `tools/list` annotations, + including `notebook_SearchNotes` `readOnlyHint=true` and + `openWorldHint=false` - Open: - automate broader client compatibility checks beyond Go SDK + Cursor manual verification @@ -162,10 +263,16 @@ current architecture unless explicitly revised. - Generate shipped protobuf API: `easyp --cfg easyp.yaml generate -p mcp -r .` - Lint test fixtures: `easyp --cfg easyp.test.yaml lint -p internal/testproto -r .` - Generate test fixtures: `easyp --cfg easyp.test.yaml generate -p internal/testproto -r .` +- Generate standalone example artifacts: + - `cd examples && make generate` +- Run Python-only standalone example: + - `cd examples/5_python_standalone && make setup && make run` - Build plugin: `go build ./cmd/protoc-gen-mcp` - Validate GoReleaser config: `goreleaser check` - Build example MCP server binary: - `go build -o example-mcp-server ./cmd/example-mcp-server/main.go` - Run example MCP server: `go -C /abs/path/to/protoc-gen-mcp run ./cmd/example-mcp-server` +- Run Python example MCP server: + - `python /abs/path/to/protoc-gen-mcp/cmd/example-python-mcp-server/main.py` - Run built example MCP server binary: `./example-mcp-server` - Run tests: `go test ./...` diff --git a/README.md b/README.md index 7f34eff..17a3c22 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # protoc-gen-mcp -`protoc-gen-mcp` generates Go MCP tool bindings from protobuf services. +`protoc-gen-mcp` generates Go and Python MCP tool bindings from protobuf services. ## MVP - protobuf is the source of truth -- generator emits typed Go MCP bindings -- runtime uses the official Go MCP SDK +- generator emits typed Go and Python MCP bindings +- Go runtime uses the official Go MCP SDK +- Python runtime targets the official MCP Python SDK with `google.protobuf` +- generated Python handlers use dataclasses and explicit `oneof` wrapper types + from `*_mcp.py`; `*_pb2.py` stays an internal runtime dependency - request and response JSON follows ProtoJSON rules - runtime validation is driven by generated JSON Schema @@ -45,10 +48,11 @@ checks: ```bash go run ./cmd/example-mcp-server +python ./cmd/example-python-mcp-server/main.py ``` It serves the generated tools from `internal/testproto/example/v1` and is used -by the stdio smoke test in `internal/examplemcp/stdio_test.go`. +by the stdio smoke tests in `internal/examplemcp/stdio_test.go`. The example server currently exposes: - `example_CreateReport` @@ -58,11 +62,12 @@ The example server currently exposes: ## Examples -We provide several standalone, runnable examples demonstrating the power of generated MCP tools, protobuf `options`, validation constraints, and integration with the official Go SDK. Check out the [examples/](examples/) directory for: -- [1-helloworld](examples/1-helloworld/) - Minimal Quickstart setup. -- [2-weather-api](examples/2-weather-api/) - Read-only queries, validation limits, Oneofs. -- [3-file-manager](examples/3-file-manager/) - Destructive tools and schema-based string parameter constraints. -- [4-crm-system](examples/4-crm-system/) - A full mock system with FieldMask partial updates, custom icons mapping, schemas nested types, and advanced array filters. +We provide several standalone, runnable examples demonstrating the power of generated MCP tools, protobuf `options`, validation constraints, and integration with the official Go and Python SDKs. Check out the [examples/](examples/) directory for: +- [1_helloworld](examples/1_helloworld/) - Minimal Quickstart setup. +- [2_weather_api](examples/2_weather_api/) - Read-only queries, validation limits, Oneofs. +- [3_file_manager](examples/3_file_manager/) - Destructive tools and schema-based string parameter constraints. +- [4_crm_system](examples/4_crm_system/) - A full mock system with FieldMask partial updates, custom icons mapping, schemas nested types, and advanced array filters. +- [5_python_standalone](examples/5_python_standalone/) - A Python-only user-style project with its own `pyproject.toml`, `easyp.yaml`, generated bindings, and stdio server. ## Agent Skill @@ -82,9 +87,16 @@ policy, ProtoJSON contract, and common patterns. ## Generation With Easyp -The intended workflow is `easyp`, not manual `protoc` invocation. `easyp` -drives both `protoc-gen-go` and `protoc-gen-mcp` with the same repository -config. +The intended workflow is `easyp`, not manual `protoc` invocation. For mixed +Go/Python projects, `easyp` can drive `protoc-gen-go`, the standard Python +protobuf generator, and `protoc-gen-mcp` from one config. + +Before running generation, make sure `protoc-gen-go` is installed and available +in `PATH` if your config uses the standard Go plugin: + +```bash +go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11 +``` Example `easyp.yaml`: @@ -106,10 +118,19 @@ generate: out: . opts: paths: source_relative + - name: python + with_imports: true + out: . - command: ["go", "run", "github.com/easyp-tech/protoc-gen-mcp/cmd/protoc-gen-mcp@latest"] out: . opts: paths: source_relative + - command: ["go", "run", "github.com/easyp-tech/protoc-gen-mcp/cmd/protoc-gen-mcp@latest"] + out: . + opts: + paths: source_relative + lang: python + python_runtime: google.protobuf ``` Typical commands: @@ -120,10 +141,67 @@ easyp --cfg easyp.yaml lint -p mcp -r . easyp --cfg easyp.yaml generate -p mcp -r . ``` -That generates both `*.pb.go` and `*.mcp.go` next to the source `.proto` files. -No special Easyp override is required for `mcp.options.v1`, because the -package declares `go_package` directly in `options.proto`. For reproducible -builds, prefer pinning a specific tag instead of `@latest`. +That generates `*.pb.go`, `*_pb2.py`, `*.mcp.go`, `*_mcp.py`, and package +`__init__.py` files next to the source `.proto` files. The Python target +currently supports only `python_runtime=google.protobuf`. Generated Python +output also emits a small `mcp/__init__.py` bridge so `mcp.options.*` protobuf +modules can coexist with the official `mcp` SDK package in one import tree. No +special Easyp override is required for `mcp.options.v1`, because the package +declares `go_package` directly in `options.proto`. For reproducible builds, +prefer pinning a specific tag instead of `@latest`. + +For Python-only projects, omit the Go plugins and keep only the standard +`python` plugin plus `protoc-gen-mcp` with `lang=python`. User-authored Python +protos do not need a `go_package` option just to satisfy the generator; the +plugin synthesizes internal Go package metadata before building the descriptor +model. See [examples/5_python_standalone](examples/5_python_standalone/) for a +complete Python-only project with its own virtualenv setup. + +When you generate Python handlers, implement against the dataclasses from +`*_mcp.py`, not the raw protobuf classes from `*_pb2.py`. The generated Python +runtime maps MCP JSON -> ProtoJSON -> `pb2` -> dataclasses before calling your +handler, then maps your dataclass response back through protobuf serialization +and output-schema validation. + +For optional fields and `oneof` groups, the handler-facing absence sentinel is +`UNSET`, not `None`. JSON `null` for a schema-optional field is mapped to +`UNSET` on input, and handlers should return `UNSET` when a field should remain +unset in the serialized protobuf output. + +If your Python package imports other `.proto` files that must also resolve as +Python modules at runtime, make sure the standard Python generator emits those +imports too. In `easyp`, that typically means enabling `with_imports: true` on +the Python plugin for that package. + +Generated `ToolAnnotations` are forwarded as declared in protobuf options. The +generator does not synthesize extra hint values that were not set in source +proto. Some MCP clients, including `@modelcontextprotocol/inspector`, display +omitted hints using their own client-side defaults. If you want UI badges to +stay unambiguous, set hints like `destructive_hint: false` explicitly instead +of relying on omission. + +Example Python handler: + +```python +import mcp.server.lowlevel +from weather_mcp import ( + GetCurrentWeatherRequest, + GetCurrentWeatherRequestLocationCityVariant, + GetCurrentWeatherResponse, + register_weather_api_tools, +) + + +class WeatherAPI: + def get_current_weather(self, _ctx, req: GetCurrentWeatherRequest) -> GetCurrentWeatherResponse: + if not isinstance(req.location, GetCurrentWeatherRequestLocationCityVariant): + raise ValueError("city lookup is required") + return GetCurrentWeatherResponse(condition="Sunny", temperature=22.5) + + +server = mcp.server.lowlevel.Server("weather-mcp-server", version="1.0.0") +register_weather_api_tools(server, WeatherAPI()) +``` Generated tool names never contain dots. The runtime joins the optional service namespace and RPC tool name with underscores, so a service namespace @@ -356,6 +434,8 @@ Used via: `option (mcp.options.v1.method) = { ... };` - `description`: Overrides the RPC description inferred from proto comments. - `hidden`: Suppresses tool generation for this RPC method completely. - `annotations`: Provides `ToolAnnotations` hints for agents (like `read_only_hint`, `destructive_hint`, `idempotent_hint`, `open_world_hint`). + Omitted hints are left omitted in generated output; some clients render their + own defaults for missing values. - `icons`: Provides an array of `Icon` metadata for the tool, overriding the service default. - `execution`: Defines `ExecutionOptions` like `task_support`. diff --git a/cmd/example-python-mcp-server/main.py b/cmd/example-python-mcp-server/main.py new file mode 100644 index 0000000..764eed3 --- /dev/null +++ b/cmd/example-python-mcp-server/main.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pathlib import Path +import sys + +_REPO_ROOT = Path(__file__).resolve().parents[2] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from internal.examplemcp.python_server import main + + +if __name__ == "__main__": + main() diff --git a/cmd/protoc-gen-mcp/main.go b/cmd/protoc-gen-mcp/main.go index 50a0e27..e599651 100644 --- a/cmd/protoc-gen-mcp/main.go +++ b/cmd/protoc-gen-mcp/main.go @@ -1,15 +1,72 @@ package main import ( + "fmt" + "io" + "os" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" pluginpb "google.golang.org/protobuf/types/pluginpb" "github.com/easyp-tech/protoc-gen-mcp/internal/codegen" ) func main() { - protogen.Options{}.Run(func(plugin *protogen.Plugin) error { - plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) - return codegen.Generate(plugin) + requestBytes, err := io.ReadAll(os.Stdin) + if err != nil { + writeErrorResponse(fmt.Errorf("read CodeGeneratorRequest: %w", err)) + return + } + + var request pluginpb.CodeGeneratorRequest + if err := proto.Unmarshal(requestBytes, &request); err != nil { + writeErrorResponse(fmt.Errorf("unmarshal CodeGeneratorRequest: %w", err)) + return + } + + opts, err := codegen.ParseOptions(request.GetParameter()) + if err != nil { + writeErrorResponse(err) + return + } + codegen.PrepareRequestForProtogen(&request, opts) + + optionParser := codegen.NewOptionsParser() + + plugin, err := (protogen.Options{ + ParamFunc: optionParser.Set, + }).New(&request) + if err != nil { + writeErrorResponse(err) + return + } + + plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + parsedOpts, err := optionParser.Options() + if err != nil { + plugin.Error(err) + } else if err := codegen.Generate(plugin, parsedOpts); err != nil { + plugin.Error(err) + } + + writeResponse(plugin.Response()) +} + +func writeErrorResponse(err error) { + writeResponse(&pluginpb.CodeGeneratorResponse{ + Error: proto.String(err.Error()), }) } + +func writeResponse(response *pluginpb.CodeGeneratorResponse) { + responseBytes, err := proto.Marshal(response) + if err != nil { + fmt.Fprintf(os.Stderr, "marshal CodeGeneratorResponse: %v\n", err) + os.Exit(1) + } + if _, err := os.Stdout.Write(responseBytes); err != nil { + fmt.Fprintf(os.Stderr, "write CodeGeneratorResponse: %v\n", err) + os.Exit(1) + } +} diff --git a/easyp.test.yaml b/easyp.test.yaml index 71b0f8a..9d27630 100644 --- a/easyp.test.yaml +++ b/easyp.test.yaml @@ -55,7 +55,15 @@ generate: out: . opts: paths: source_relative + - name: python + out: . + - command: ["go", "run", "./cmd/protoc-gen-mcp"] + out: . + opts: + paths: source_relative - command: ["go", "run", "./cmd/protoc-gen-mcp"] out: . opts: paths: source_relative + lang: python + python_runtime: google.protobuf diff --git a/easyp.yaml b/easyp.yaml index 7a85733..0555d5d 100644 --- a/easyp.yaml +++ b/easyp.yaml @@ -55,7 +55,15 @@ generate: out: . opts: paths: source_relative + - name: python + out: . + - command: ["go", "run", "./cmd/protoc-gen-mcp"] + out: . + opts: + paths: source_relative - command: ["go", "run", "./cmd/protoc-gen-mcp"] out: . opts: paths: source_relative + lang: python + python_runtime: google.protobuf diff --git a/examples/1-helloworld/proto/helloworld.pb.go b/examples/1-helloworld/proto/helloworld.pb.go deleted file mode 100644 index 4593dd4..0000000 --- a/examples/1-helloworld/proto/helloworld.pb.go +++ /dev/null @@ -1,234 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.34.2 -// protoc v0.14.1-v0.16.1-bufbuild-protocompile-easyp-modified -// source: examples/1-helloworld/proto/helloworld.proto - -package helloworldv1 - -import ( - _ "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type SayHelloRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` -} - -func (x *SayHelloRequest) Reset() { - *x = SayHelloRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_1_helloworld_proto_helloworld_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *SayHelloRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SayHelloRequest) ProtoMessage() {} - -func (x *SayHelloRequest) ProtoReflect() protoreflect.Message { - mi := &file_examples_1_helloworld_proto_helloworld_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SayHelloRequest.ProtoReflect.Descriptor instead. -func (*SayHelloRequest) Descriptor() ([]byte, []int) { - return file_examples_1_helloworld_proto_helloworld_proto_rawDescGZIP(), []int{0} -} - -func (x *SayHelloRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -type SayHelloResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *SayHelloResponse) Reset() { - *x = SayHelloResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_1_helloworld_proto_helloworld_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *SayHelloResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SayHelloResponse) ProtoMessage() {} - -func (x *SayHelloResponse) ProtoReflect() protoreflect.Message { - mi := &file_examples_1_helloworld_proto_helloworld_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SayHelloResponse.ProtoReflect.Descriptor instead. -func (*SayHelloResponse) Descriptor() ([]byte, []int) { - return file_examples_1_helloworld_proto_helloworld_proto_rawDescGZIP(), []int{1} -} - -func (x *SayHelloResponse) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -var File_examples_1_helloworld_proto_helloworld_proto protoreflect.FileDescriptor - -var file_examples_1_helloworld_proto_helloworld_proto_rawDesc = []byte{ - 0x0a, 0x2c, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x31, 0x2d, 0x68, 0x65, 0x6c, - 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x65, - 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, - 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x31, 0x1a, 0x1c, 0x6d, 0x63, 0x70, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, - 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, - 0x56, 0x0a, 0x0f, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x43, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x2f, 0xda, 0xb7, 0x2c, 0x2b, 0x0a, 0x20, 0x54, 0x68, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, - 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x20, 0x74, - 0x6f, 0x20, 0x67, 0x72, 0x65, 0x65, 0x74, 0x2e, 0x1a, 0x07, 0x0a, 0x05, 0x41, 0x6c, 0x69, 0x63, - 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x10, 0x53, 0x61, 0x79, 0x48, 0x65, - 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xe3, 0x01, 0x0a, 0x0a, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, - 0x72, 0x41, 0x50, 0x49, 0x12, 0x8a, 0x01, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, - 0x6f, 0x12, 0x22, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, - 0x6c, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, - 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0xd2, 0xb7, 0x2c, 0x31, - 0x1a, 0x2f, 0x52, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x20, 0x61, 0x20, 0x66, 0x72, 0x69, 0x65, - 0x6e, 0x64, 0x6c, 0x79, 0x20, 0x67, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x66, 0x6f, - 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x67, 0x69, 0x76, 0x65, 0x6e, 0x20, 0x6e, 0x61, 0x6d, 0x65, - 0x2e, 0x1a, 0x48, 0xca, 0xb7, 0x2c, 0x44, 0x0a, 0x07, 0x67, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, - 0x12, 0x39, 0x41, 0x20, 0x73, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x67, 0x72, 0x65, 0x65, 0x74, - 0x69, 0x6e, 0x67, 0x20, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x64, - 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x20, 0x4d, 0x43, 0x50, 0x20, 0x69, - 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x42, 0x4f, 0x5a, 0x4d, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x61, 0x73, 0x79, 0x70, 0x2d, - 0x74, 0x65, 0x63, 0x68, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, - 0x6d, 0x63, 0x70, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x31, 0x2d, 0x68, - 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, - 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_examples_1_helloworld_proto_helloworld_proto_rawDescOnce sync.Once - file_examples_1_helloworld_proto_helloworld_proto_rawDescData = file_examples_1_helloworld_proto_helloworld_proto_rawDesc -) - -func file_examples_1_helloworld_proto_helloworld_proto_rawDescGZIP() []byte { - file_examples_1_helloworld_proto_helloworld_proto_rawDescOnce.Do(func() { - file_examples_1_helloworld_proto_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_examples_1_helloworld_proto_helloworld_proto_rawDescData) - }) - return file_examples_1_helloworld_proto_helloworld_proto_rawDescData -} - -var file_examples_1_helloworld_proto_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_examples_1_helloworld_proto_helloworld_proto_goTypes = []any{ - (*SayHelloRequest)(nil), // 0: helloworld.api.v1.SayHelloRequest - (*SayHelloResponse)(nil), // 1: helloworld.api.v1.SayHelloResponse -} -var file_examples_1_helloworld_proto_helloworld_proto_depIdxs = []int32{ - 0, // 0: helloworld.api.v1.GreeterAPI.SayHello:input_type -> helloworld.api.v1.SayHelloRequest - 1, // 1: helloworld.api.v1.GreeterAPI.SayHello:output_type -> helloworld.api.v1.SayHelloResponse - 1, // [1:2] is the sub-list for method output_type - 0, // [0:1] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_examples_1_helloworld_proto_helloworld_proto_init() } -func file_examples_1_helloworld_proto_helloworld_proto_init() { - if File_examples_1_helloworld_proto_helloworld_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_examples_1_helloworld_proto_helloworld_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*SayHelloRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_examples_1_helloworld_proto_helloworld_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*SayHelloResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_examples_1_helloworld_proto_helloworld_proto_rawDesc, - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_examples_1_helloworld_proto_helloworld_proto_goTypes, - DependencyIndexes: file_examples_1_helloworld_proto_helloworld_proto_depIdxs, - MessageInfos: file_examples_1_helloworld_proto_helloworld_proto_msgTypes, - }.Build() - File_examples_1_helloworld_proto_helloworld_proto = out.File - file_examples_1_helloworld_proto_helloworld_proto_rawDesc = nil - file_examples_1_helloworld_proto_helloworld_proto_goTypes = nil - file_examples_1_helloworld_proto_helloworld_proto_depIdxs = nil -} diff --git a/examples/1-helloworld/main.go b/examples/1_helloworld/main.go similarity index 92% rename from examples/1-helloworld/main.go rename to examples/1_helloworld/main.go index 83be737..ce781a5 100644 --- a/examples/1-helloworld/main.go +++ b/examples/1_helloworld/main.go @@ -5,8 +5,8 @@ import ( "fmt" "log" + helloworldv1 "github.com/easyp-tech/protoc-gen-mcp/examples/1_helloworld/proto" "github.com/modelcontextprotocol/go-sdk/mcp" - helloworldv1 "github.com/easyp-tech/protoc-gen-mcp/examples/1-helloworld/proto" ) type greeter struct{} diff --git a/examples/1_helloworld/main.py b/examples/1_helloworld/main.py new file mode 100644 index 0000000..956327a --- /dev/null +++ b/examples/1_helloworld/main.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from pathlib import Path +import sys + +_EXAMPLES_ROOT = Path(__file__).resolve().parents[1] +if str(_EXAMPLES_ROOT) not in sys.path: + sys.path.insert(0, str(_EXAMPLES_ROOT)) + +import anyio +import mcp.server.lowlevel +import mcp.server.stdio + +from proto import helloworld_mcp + + +class Greeter: + def say_hello( + self, + _ctx: helloworld_mcp.ToolRequestContext, + req: helloworld_mcp.SayHelloRequest, + ) -> helloworld_mcp.SayHelloResponse: + return helloworld_mcp.SayHelloResponse(message=f"Hello, {req.name}!") + + +def new_server() -> mcp.server.lowlevel.Server: + server = mcp.server.lowlevel.Server("helloworld-mcp-server", version="1.0.0") + helloworld_mcp.register_greeter_api_tools(server, Greeter()) + return server + + +async def run_stdio_server() -> None: + server = new_server() + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options(), + ) + + +def main() -> None: + anyio.run(run_stdio_server) + + +if __name__ == "__main__": + main() diff --git a/examples/1_helloworld/proto/__init__.py b/examples/1_helloworld/proto/__init__.py new file mode 100644 index 0000000..6e8ced9 --- /dev/null +++ b/examples/1_helloworld/proto/__init__.py @@ -0,0 +1 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. diff --git a/examples/1-helloworld/proto/helloworld.mcp.go b/examples/1_helloworld/proto/helloworld.mcp.go similarity index 97% rename from examples/1-helloworld/proto/helloworld.mcp.go rename to examples/1_helloworld/proto/helloworld.mcp.go index a7c9a70..10b0ca4 100644 --- a/examples/1-helloworld/proto/helloworld.mcp.go +++ b/examples/1_helloworld/proto/helloworld.mcp.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-mcp. DO NOT EDIT. -// source: examples/1-helloworld/proto/helloworld.proto +// source: 1_helloworld/proto/helloworld.proto package helloworldv1 diff --git a/examples/1_helloworld/proto/helloworld.pb.go b/examples/1_helloworld/proto/helloworld.pb.go new file mode 100644 index 0000000..a9df5d3 --- /dev/null +++ b/examples/1_helloworld/proto/helloworld.pb.go @@ -0,0 +1,178 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v0.14.1-v0.15.2-bufbuild-protocompile-easyp-rc1 +// source: 1_helloworld/proto/helloworld.proto + +package helloworldv1 + +import ( + _ "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SayHelloRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SayHelloRequest) Reset() { + *x = SayHelloRequest{} + mi := &file__1_helloworld_proto_helloworld_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SayHelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SayHelloRequest) ProtoMessage() {} + +func (x *SayHelloRequest) ProtoReflect() protoreflect.Message { + mi := &file__1_helloworld_proto_helloworld_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SayHelloRequest.ProtoReflect.Descriptor instead. +func (*SayHelloRequest) Descriptor() ([]byte, []int) { + return file__1_helloworld_proto_helloworld_proto_rawDescGZIP(), []int{0} +} + +func (x *SayHelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type SayHelloResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SayHelloResponse) Reset() { + *x = SayHelloResponse{} + mi := &file__1_helloworld_proto_helloworld_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SayHelloResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SayHelloResponse) ProtoMessage() {} + +func (x *SayHelloResponse) ProtoReflect() protoreflect.Message { + mi := &file__1_helloworld_proto_helloworld_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SayHelloResponse.ProtoReflect.Descriptor instead. +func (*SayHelloResponse) Descriptor() ([]byte, []int) { + return file__1_helloworld_proto_helloworld_proto_rawDescGZIP(), []int{1} +} + +func (x *SayHelloResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File__1_helloworld_proto_helloworld_proto protoreflect.FileDescriptor + +const file__1_helloworld_proto_helloworld_proto_rawDesc = "" + + "\n" + + "#1_helloworld/proto/helloworld.proto\x12\x11helloworld.api.v1\x1a\x1cmcp/options/v1/options.proto\"V\n" + + "\x0fSayHelloRequest\x12C\n" + + "\x04name\x18\x01 \x01(\tB/ڷ,+\n" + + " The name of the person to greet.\x1a\a\n" + + "\x05AliceR\x04name\",\n" + + "\x10SayHelloResponse\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage2\xe3\x01\n" + + "\n" + + "GreeterAPI\x12\x8a\x01\n" + + "\bSayHello\x12\".helloworld.api.v1.SayHelloRequest\x1a#.helloworld.api.v1.SayHelloResponse\"5ҷ,1\x1a/Returns a friendly greeting for the given name.\x1aHʷ,D\n" + + "\agreeter\x129A simple greeting service to demonstrate MCP integration.BOZMgithub.com/easyp-tech/protoc-gen-mcp/examples/1_helloworld/proto;helloworldv1b\x06proto3" + +var ( + file__1_helloworld_proto_helloworld_proto_rawDescOnce sync.Once + file__1_helloworld_proto_helloworld_proto_rawDescData []byte +) + +func file__1_helloworld_proto_helloworld_proto_rawDescGZIP() []byte { + file__1_helloworld_proto_helloworld_proto_rawDescOnce.Do(func() { + file__1_helloworld_proto_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file__1_helloworld_proto_helloworld_proto_rawDesc), len(file__1_helloworld_proto_helloworld_proto_rawDesc))) + }) + return file__1_helloworld_proto_helloworld_proto_rawDescData +} + +var file__1_helloworld_proto_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file__1_helloworld_proto_helloworld_proto_goTypes = []any{ + (*SayHelloRequest)(nil), // 0: helloworld.api.v1.SayHelloRequest + (*SayHelloResponse)(nil), // 1: helloworld.api.v1.SayHelloResponse +} +var file__1_helloworld_proto_helloworld_proto_depIdxs = []int32{ + 0, // 0: helloworld.api.v1.GreeterAPI.SayHello:input_type -> helloworld.api.v1.SayHelloRequest + 1, // 1: helloworld.api.v1.GreeterAPI.SayHello:output_type -> helloworld.api.v1.SayHelloResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file__1_helloworld_proto_helloworld_proto_init() } +func file__1_helloworld_proto_helloworld_proto_init() { + if File__1_helloworld_proto_helloworld_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file__1_helloworld_proto_helloworld_proto_rawDesc), len(file__1_helloworld_proto_helloworld_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file__1_helloworld_proto_helloworld_proto_goTypes, + DependencyIndexes: file__1_helloworld_proto_helloworld_proto_depIdxs, + MessageInfos: file__1_helloworld_proto_helloworld_proto_msgTypes, + }.Build() + File__1_helloworld_proto_helloworld_proto = out.File + file__1_helloworld_proto_helloworld_proto_goTypes = nil + file__1_helloworld_proto_helloworld_proto_depIdxs = nil +} diff --git a/examples/1-helloworld/proto/helloworld.proto b/examples/1_helloworld/proto/helloworld.proto similarity index 95% rename from examples/1-helloworld/proto/helloworld.proto rename to examples/1_helloworld/proto/helloworld.proto index 7e40c8c..91a9739 100644 --- a/examples/1-helloworld/proto/helloworld.proto +++ b/examples/1_helloworld/proto/helloworld.proto @@ -4,7 +4,7 @@ package helloworld.api.v1; import "mcp/options/v1/options.proto"; -option go_package = "github.com/easyp-tech/protoc-gen-mcp/examples/1-helloworld/proto;helloworldv1"; +option go_package = "github.com/easyp-tech/protoc-gen-mcp/examples/1_helloworld/proto;helloworldv1"; service GreeterAPI { option (mcp.options.v1.service) = { diff --git a/examples/1_helloworld/proto/helloworld_mcp.py b/examples/1_helloworld/proto/helloworld_mcp.py new file mode 100644 index 0000000..6c1998f --- /dev/null +++ b/examples/1_helloworld/proto/helloworld_mcp.py @@ -0,0 +1,443 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +# source: 1_helloworld/proto/helloworld.proto +from __future__ import annotations + +import enum +import inspect +import json +import weakref +from dataclasses import dataclass, field +from typing import Any, Awaitable, Protocol, TypeAlias + +import jsonschema +import mcp.server.lowlevel +import mcp.server.session +import mcp.shared.exceptions +import mcp.types +from google.protobuf import any_pb2, duration_pb2, empty_pb2, field_mask_pb2, json_format, struct_pb2, timestamp_pb2, wrappers_pb2 +try: + from . import helloworld_pb2 +except ImportError: + import helloworld_pb2 + +ToolRequestContext = mcp.server.session.ServerSession + +class _UnsetType: + __slots__ = () + + def __repr__(self) -> str: + return "UNSET" + +UNSET = _UnsetType() + +JSONValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None +ProtoAny: TypeAlias = dict[str, Any] +Timestamp: TypeAlias = str +Duration: TypeAlias = str +FieldMask: TypeAlias = str +Struct: TypeAlias = dict[str, Any] +Value: TypeAlias = JSONValue +ListValue: TypeAlias = list[Any] +BoolValue: TypeAlias = bool +StringValue: TypeAlias = str +BytesValue: TypeAlias = bytes +Int32Value: TypeAlias = int +UInt32Value: TypeAlias = int +Int64Value: TypeAlias = int +UInt64Value: TypeAlias = int +FloatValue: TypeAlias = float +DoubleValue: TypeAlias = float + +@dataclass(slots=True) +class Empty: + pass + +@dataclass(slots=True) +class SayHelloRequest: + name: str + +@dataclass(slots=True) +class SayHelloResponse: + message: str + +@dataclass(frozen=True) +class _RegisteredTool: + name: str + title: str + description: str + input_schema_json: str + output_schema_json: str + request_type: type[Any] + response_type: type[Any] + from_pb: Any + to_pb: Any + handler: Any + annotations: dict[str, Any] | None + icons: list[dict[str, Any]] | None + +class _ServerToolRegistry: + def __init__(self, server: mcp.server.lowlevel.Server) -> None: + self.server = server + self.tools: dict[str, _RegisteredTool] = {} + self.reserved_tool_names = _get_reserved_tool_names(server) + self.handlers_installed = False + self.previous_list_tools: Any | None = None + self.previous_call_tool: Any | None = None + + def add_tool(self, tool: _RegisteredTool) -> None: + if tool.name in self.reserved_tool_names: + raise ValueError(f"duplicate tool registration: {tool.name}") + self.tools[tool.name] = tool + self.reserved_tool_names.add(tool.name) + + def list_tools(self) -> list[mcp.types.Tool]: + return [_build_tool(tool) for tool in self.tools.values()] + + async def call_tool(self, name: str, arguments: dict[str, Any] | None, context: ToolRequestContext | None = None) -> mcp.types.CallToolResult: + return await _dispatch_call(self, name, arguments or {}, context) + +_SERVER_REGISTRIES: weakref.WeakKeyDictionary[mcp.server.lowlevel.Server, _ServerToolRegistry] = weakref.WeakKeyDictionary() + +def _load_schema(schema_json: str) -> dict[str, Any]: + return json.loads(schema_json) + +def _protojson_to_message(arguments: dict[str, Any], message: Any) -> Any: + json_format.ParseDict(arguments, message) + return message + +def _normalize_tool_segment(segment: str | None) -> str: + if segment is None: + return "" + parts = [part for part in segment.strip().replace(".", "_").split("_") if part] + return "_".join(parts) + +def _normalize_namespace(namespace: str | None, default: str) -> str: + if namespace is None: + namespace = default + return _normalize_tool_segment(namespace) + +def _tool_name(namespace: str, method_name: str) -> str: + namespace = _normalize_tool_segment(namespace) + method_name = _normalize_tool_segment(method_name) + if namespace == '': + return method_name + if method_name == '': + return namespace + return f"{namespace}_{method_name}" + +def _get_registry(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _SERVER_REGISTRIES.get(server) + if registry is None: + registry = _ServerToolRegistry(server) + _SERVER_REGISTRIES[server] = registry + return registry + +_RESERVED_TOOL_NAMES_ATTR = "_protoc_gen_mcp_reserved_tool_names" + +def _get_reserved_tool_names(server: mcp.server.lowlevel.Server) -> set[str]: + reserved = getattr(server, _RESERVED_TOOL_NAMES_ATTR, None) + if reserved is None: + reserved = set() + setattr(server, _RESERVED_TOOL_NAMES_ATTR, reserved) + return reserved + +def _build_list_tools_request(request: Any) -> mcp.types.ListToolsRequest: + if isinstance(request, mcp.types.ListToolsRequest): + return request + return mcp.types.ListToolsRequest() + +def _merge_tools(previous: list[mcp.types.Tool], current: list[mcp.types.Tool]) -> list[mcp.types.Tool]: + merged: list[mcp.types.Tool] = [] + names: set[str] = set() + for tool in previous: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + for tool in current: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + return merged + +def _install_server_handlers(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _get_registry(server) + if registry.handlers_installed: + return registry + registry.previous_list_tools = server.request_handlers.get(mcp.types.ListToolsRequest) + registry.previous_call_tool = server.request_handlers.get(mcp.types.CallToolRequest) + + async def _list_tools(request: Any) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + list_request = _build_list_tools_request(request) + previous_tools: list[mcp.types.Tool] = [] + meta: dict[str, Any] | None = None + if current is not None: + if current.previous_list_tools is not None: + previous_result = await current.previous_list_tools(list_request) + if not isinstance(previous_result.root, mcp.types.ListToolsResult): + return previous_result + if getattr(list_request.params, "cursor", None) is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + if previous_result.root.nextCursor is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + meta = getattr(previous_result.root, "meta", None) + previous_tools = list(previous_result.root.tools) + current_tools = [] if current is None else current.list_tools() + tools = _merge_tools(previous_tools, current_tools) + server._tool_cache.clear() + for tool in tools: + server._tool_cache[tool.name] = tool + return mcp.types.ServerResult(mcp.types.ListToolsResult(tools=tools, _meta=meta)) + + async def _call_tool(request: mcp.types.CallToolRequest) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + if current is None: + return mcp.types.ServerResult(_tool_error_result("tool registry is not installed")) + if request.params.name not in current.tools: + if current.previous_call_tool is not None: + return await current.previous_call_tool(request) + _invalid_params_error(request.params.name, "unknown tool") + if current.previous_call_tool is not None: + await server.request_handlers[mcp.types.ListToolsRequest](mcp.types.ListToolsRequest()) + result = await current.call_tool(request.params.name, request.params.arguments or {}, server.request_context.session) + return mcp.types.ServerResult(result) + + server.request_handlers[mcp.types.ListToolsRequest] = _list_tools + server.request_handlers[mcp.types.CallToolRequest] = _call_tool + + registry.handlers_installed = True + return registry + +def _tool_annotations(raw: dict[str, Any] | None) -> mcp.types.ToolAnnotations | None: + if raw is None: + return None + return mcp.types.ToolAnnotations(**raw) + +def _tool_error_result(message: str) -> mcp.types.CallToolResult: + return mcp.types.CallToolResult( + content=[mcp.types.TextContent(type="text", text=message)], + isError=True, + ) + +def _invalid_params_error(tool_name: str, error: Any) -> mcp.shared.exceptions.McpError: + raise mcp.shared.exceptions.McpError( + mcp.types.ErrorData( + code=mcp.types.INVALID_PARAMS, + message=f"invalid arguments for tool {tool_name!r}: {error}", + ) + ) + +def _build_tool(tool: _RegisteredTool) -> Any: + return mcp.types.Tool( + name=tool.name, + title=tool.title, + description=tool.description, + inputSchema=_load_schema(tool.input_schema_json), + outputSchema=_load_schema(tool.output_schema_json), + annotations=_tool_annotations(tool.annotations), + icons=tool.icons, + ) + +async def _maybe_await(result: Any) -> Any: + if inspect.isawaitable(result): + return await result + return result + +def _message_to_json(message: Any) -> str: + kwargs = {"preserving_proto_field_name": False} + try: + return json_format.MessageToJson(message, always_print_fields_with_no_presence=True, **kwargs) + except TypeError: + return json_format.MessageToJson(message, including_default_value_fields=True, **kwargs) + +async def _dispatch_call(registry: _ServerToolRegistry, name: str, arguments: dict[str, Any], context: ToolRequestContext | None) -> mcp.types.CallToolResult: + tool = registry.tools.get(name) + if tool is None: + _invalid_params_error(name, "unknown tool") + try: + jsonschema.validate(arguments, _load_schema(tool.input_schema_json)) + except jsonschema.ValidationError as error: + _invalid_params_error(name, error) + try: + request_pb = _protojson_to_message(arguments, tool.request_type()) + request_dc = tool.from_pb(request_pb) + except Exception as error: + _invalid_params_error(name, error) + try: + response_dc = await _maybe_await(tool.handler(context, request_dc)) + except Exception as error: + return _tool_error_result(str(error)) + try: + if response_dc is None: + response_pb = tool.response_type() + else: + response_pb = tool.to_pb(response_dc) + payload = json.loads(_message_to_json(response_pb)) + except Exception as error: + raise RuntimeError(f"mcpruntime: marshal output for tool {name!r}: {error}") from error + text = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + content = [mcp.types.TextContent(type="text", text=text)] + try: + jsonschema.validate(payload, _load_schema(tool.output_schema_json)) + except jsonschema.ValidationError as error: + raise RuntimeError(f"mcpruntime: validate output for tool {name!r}: {error}") from error + return mcp.types.CallToolResult(content=content, structuredContent=payload) + +def _enum_from_pb(enum_type: type[enum.IntEnum], value: int) -> enum.IntEnum: + return enum_type(value) + +def _json_to_message(value: Any, message: Any) -> Any: + json_format.Parse(json.dumps(value), message) + return message + +def _from_pb_any(message: any_pb2.Any) -> ProtoAny: + return json.loads(_message_to_json(message)) + +def _to_pb_any(value: ProtoAny) -> any_pb2.Any: + return _json_to_message(value, any_pb2.Any()) + +def _from_pb_timestamp(message: timestamp_pb2.Timestamp) -> Timestamp: + return json.loads(_message_to_json(message)) + +def _to_pb_timestamp(value: Timestamp) -> timestamp_pb2.Timestamp: + return _json_to_message(value, timestamp_pb2.Timestamp()) + +def _from_pb_duration(message: duration_pb2.Duration) -> Duration: + return json.loads(_message_to_json(message)) + +def _to_pb_duration(value: Duration) -> duration_pb2.Duration: + return _json_to_message(value, duration_pb2.Duration()) + +def _from_pb_field_mask(message: field_mask_pb2.FieldMask) -> FieldMask: + return json.loads(_message_to_json(message)) + +def _to_pb_field_mask(value: FieldMask) -> field_mask_pb2.FieldMask: + return _json_to_message(value, field_mask_pb2.FieldMask()) + +def _from_pb_struct(message: struct_pb2.Struct) -> Struct: + return json.loads(_message_to_json(message)) + +def _to_pb_struct(value: Struct) -> struct_pb2.Struct: + return _json_to_message(value, struct_pb2.Struct()) + +def _from_pb_list_value(message: struct_pb2.ListValue) -> ListValue: + return json.loads(_message_to_json(message)) + +def _to_pb_list_value(value: ListValue) -> struct_pb2.ListValue: + return _json_to_message(value, struct_pb2.ListValue()) + +def _from_pb_value(message: struct_pb2.Value) -> Value: + return json.loads(_message_to_json(message)) + +def _to_pb_value(value: Value) -> struct_pb2.Value: + return _json_to_message(value, struct_pb2.Value()) + +def _from_pb_empty(message: empty_pb2.Empty) -> Empty: + return Empty() + +def _to_pb_empty(value: Empty) -> empty_pb2.Empty: + return empty_pb2.Empty() + +def _from_pb_bool_value(message: wrappers_pb2.BoolValue) -> BoolValue: + return message.value + +def _to_pb_bool_value(value: BoolValue) -> wrappers_pb2.BoolValue: + return wrappers_pb2.BoolValue(value=value) + +def _from_pb_string_value(message: wrappers_pb2.StringValue) -> StringValue: + return message.value + +def _to_pb_string_value(value: StringValue) -> wrappers_pb2.StringValue: + return wrappers_pb2.StringValue(value=value) + +def _from_pb_bytes_value(message: wrappers_pb2.BytesValue) -> BytesValue: + return message.value + +def _to_pb_bytes_value(value: BytesValue) -> wrappers_pb2.BytesValue: + return wrappers_pb2.BytesValue(value=value) + +def _from_pb_int32_value(message: wrappers_pb2.Int32Value) -> Int32Value: + return message.value + +def _to_pb_int32_value(value: Int32Value) -> wrappers_pb2.Int32Value: + return wrappers_pb2.Int32Value(value=value) + +def _from_pb_uint32_value(message: wrappers_pb2.UInt32Value) -> UInt32Value: + return message.value + +def _to_pb_uint32_value(value: UInt32Value) -> wrappers_pb2.UInt32Value: + return wrappers_pb2.UInt32Value(value=value) + +def _from_pb_int64_value(message: wrappers_pb2.Int64Value) -> Int64Value: + return message.value + +def _to_pb_int64_value(value: Int64Value) -> wrappers_pb2.Int64Value: + return wrappers_pb2.Int64Value(value=value) + +def _from_pb_uint64_value(message: wrappers_pb2.UInt64Value) -> UInt64Value: + return message.value + +def _to_pb_uint64_value(value: UInt64Value) -> wrappers_pb2.UInt64Value: + return wrappers_pb2.UInt64Value(value=value) + +def _from_pb_float_value(message: wrappers_pb2.FloatValue) -> FloatValue: + return message.value + +def _to_pb_float_value(value: FloatValue) -> wrappers_pb2.FloatValue: + return wrappers_pb2.FloatValue(value=value) + +def _from_pb_double_value(message: wrappers_pb2.DoubleValue) -> DoubleValue: + return message.value + +def _to_pb_double_value(value: DoubleValue) -> wrappers_pb2.DoubleValue: + return wrappers_pb2.DoubleValue(value=value) + +def _from_pb_say_hello_request(message: helloworld_pb2.SayHelloRequest) -> SayHelloRequest: + return SayHelloRequest( + name=message.name, + ) + +def _to_pb_say_hello_request(value: SayHelloRequest) -> helloworld_pb2.SayHelloRequest: + message = helloworld_pb2.SayHelloRequest() + message.name = value.name + return message + +def _from_pb_say_hello_response(message: helloworld_pb2.SayHelloResponse) -> SayHelloResponse: + return SayHelloResponse( + message=message.message, + ) + +def _to_pb_say_hello_response(value: SayHelloResponse) -> helloworld_pb2.SayHelloResponse: + message = helloworld_pb2.SayHelloResponse() + message.message = value.message + return message + +class GreeterAPIToolHandler(Protocol): + def say_hello(self, ctx: ToolRequestContext, req: SayHelloRequest) -> SayHelloResponse | Awaitable[SayHelloResponse]: + ... + +def register_greeter_api_tools(server: mcp.server.lowlevel.Server, impl: GreeterAPIToolHandler, *, namespace: str | None = None) -> None: + if impl is None: + raise ValueError("register_greeter_api_tools: impl is nil") + registry = _install_server_handlers(server) + resolved_namespace = _normalize_namespace(namespace, "greeter") + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "SayHello"), + title="", + description="Returns a friendly greeting for the given name.", + input_schema_json=GREETER_API_SAY_HELLO_INPUT_SCHEMA_JSON, + output_schema_json=GREETER_API_SAY_HELLO_OUTPUT_SCHEMA_JSON, + request_type=helloworld_pb2.SayHelloRequest, + response_type=helloworld_pb2.SayHelloResponse, + from_pb=_from_pb_say_hello_request, + to_pb=_to_pb_say_hello_response, + handler=impl.say_hello, + annotations=None, + icons=None, + )) + +GREETER_API_SAY_HELLO_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"description\":\"The name of the person to greet.\",\"examples\":[\"Alice\"]}},\"examples\":[{\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false}" + +GREETER_API_SAY_HELLO_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\",\"examples\":[\"example\"]}},\"examples\":[{\"message\":\"example\"}],\"required\":[\"message\"],\"additionalProperties\":false}" diff --git a/examples/1_helloworld/proto/helloworld_pb2.py b/examples/1_helloworld/proto/helloworld_pb2.py new file mode 100644 index 0000000..53bb853 --- /dev/null +++ b/examples/1_helloworld/proto/helloworld_pb2.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: 1_helloworld/proto/helloworld.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + '1_helloworld/proto/helloworld.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from mcp.options.v1 import options_pb2 as mcp_dot_options_dot_v1_dot_options__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#1_helloworld/proto/helloworld.proto\x12\x11helloworld.api.v1\x1a\x1cmcp/options/v1/options.proto\"V\n\x0fSayHelloRequest\x12\x43\n\x04name\x18\x01 \x01(\tB/\xda\xb7,+\n The name of the person to greet.\x1a\x07\n\x05\x41liceR\x04name\",\n\x10SayHelloResponse\x12\x18\n\x07message\x18\x01 \x01(\tR\x07message2\xe3\x01\n\nGreeterAPI\x12\x8a\x01\n\x08SayHello\x12\".helloworld.api.v1.SayHelloRequest\x1a#.helloworld.api.v1.SayHelloResponse\"5\xd2\xb7,1\x1a/Returns a friendly greeting for the given name.\x1aH\xca\xb7,D\n\x07greeter\x12\x39\x41 simple greeting service to demonstrate MCP integration.BOZMgithub.com/easyp-tech/protoc-gen-mcp/examples/1_helloworld/proto;helloworldv1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, '1_helloworld.proto.helloworld_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'ZMgithub.com/easyp-tech/protoc-gen-mcp/examples/1_helloworld/proto;helloworldv1' + _globals['_SAYHELLOREQUEST'].fields_by_name['name']._loaded_options = None + _globals['_SAYHELLOREQUEST'].fields_by_name['name']._serialized_options = b'\332\267,+\n The name of the person to greet.\032\007\n\005Alice' + _globals['_GREETERAPI']._loaded_options = None + _globals['_GREETERAPI']._serialized_options = b'\312\267,D\n\007greeter\0229A simple greeting service to demonstrate MCP integration.' + _globals['_GREETERAPI'].methods_by_name['SayHello']._loaded_options = None + _globals['_GREETERAPI'].methods_by_name['SayHello']._serialized_options = b'\322\267,1\032/Returns a friendly greeting for the given name.' + _globals['_SAYHELLOREQUEST']._serialized_start=88 + _globals['_SAYHELLOREQUEST']._serialized_end=174 + _globals['_SAYHELLORESPONSE']._serialized_start=176 + _globals['_SAYHELLORESPONSE']._serialized_end=220 + _globals['_GREETERAPI']._serialized_start=223 + _globals['_GREETERAPI']._serialized_end=450 +# @@protoc_insertion_point(module_scope) diff --git a/examples/2-weather-api/proto/weather.pb.go b/examples/2-weather-api/proto/weather.pb.go deleted file mode 100644 index f49657e..0000000 --- a/examples/2-weather-api/proto/weather.pb.go +++ /dev/null @@ -1,378 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.34.2 -// protoc v0.14.1-v0.16.1-bufbuild-protocompile-easyp-modified -// source: examples/2-weather-api/proto/weather.proto - -package weatherv1 - -import ( - _ "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type GetCurrentWeatherRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to Location: - // - // *GetCurrentWeatherRequest_City - // *GetCurrentWeatherRequest_Coordinates - Location isGetCurrentWeatherRequest_Location `protobuf_oneof:"location"` -} - -func (x *GetCurrentWeatherRequest) Reset() { - *x = GetCurrentWeatherRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_2_weather_api_proto_weather_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GetCurrentWeatherRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetCurrentWeatherRequest) ProtoMessage() {} - -func (x *GetCurrentWeatherRequest) ProtoReflect() protoreflect.Message { - mi := &file_examples_2_weather_api_proto_weather_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetCurrentWeatherRequest.ProtoReflect.Descriptor instead. -func (*GetCurrentWeatherRequest) Descriptor() ([]byte, []int) { - return file_examples_2_weather_api_proto_weather_proto_rawDescGZIP(), []int{0} -} - -func (m *GetCurrentWeatherRequest) GetLocation() isGetCurrentWeatherRequest_Location { - if m != nil { - return m.Location - } - return nil -} - -func (x *GetCurrentWeatherRequest) GetCity() string { - if x, ok := x.GetLocation().(*GetCurrentWeatherRequest_City); ok { - return x.City - } - return "" -} - -func (x *GetCurrentWeatherRequest) GetCoordinates() *Coordinates { - if x, ok := x.GetLocation().(*GetCurrentWeatherRequest_Coordinates); ok { - return x.Coordinates - } - return nil -} - -type isGetCurrentWeatherRequest_Location interface { - isGetCurrentWeatherRequest_Location() -} - -type GetCurrentWeatherRequest_City struct { - City string `protobuf:"bytes,1,opt,name=city,proto3,oneof"` -} - -type GetCurrentWeatherRequest_Coordinates struct { - Coordinates *Coordinates `protobuf:"bytes,2,opt,name=coordinates,proto3,oneof"` -} - -func (*GetCurrentWeatherRequest_City) isGetCurrentWeatherRequest_Location() {} - -func (*GetCurrentWeatherRequest_Coordinates) isGetCurrentWeatherRequest_Location() {} - -type Coordinates struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Latitude float64 `protobuf:"fixed64,1,opt,name=latitude,proto3" json:"latitude,omitempty"` - Longitude float64 `protobuf:"fixed64,2,opt,name=longitude,proto3" json:"longitude,omitempty"` -} - -func (x *Coordinates) Reset() { - *x = Coordinates{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_2_weather_api_proto_weather_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Coordinates) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Coordinates) ProtoMessage() {} - -func (x *Coordinates) ProtoReflect() protoreflect.Message { - mi := &file_examples_2_weather_api_proto_weather_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Coordinates.ProtoReflect.Descriptor instead. -func (*Coordinates) Descriptor() ([]byte, []int) { - return file_examples_2_weather_api_proto_weather_proto_rawDescGZIP(), []int{1} -} - -func (x *Coordinates) GetLatitude() float64 { - if x != nil { - return x.Latitude - } - return 0 -} - -func (x *Coordinates) GetLongitude() float64 { - if x != nil { - return x.Longitude - } - return 0 -} - -type GetCurrentWeatherResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Condition string `protobuf:"bytes,1,opt,name=condition,proto3" json:"condition,omitempty"` - Temperature float64 `protobuf:"fixed64,2,opt,name=temperature,proto3" json:"temperature,omitempty"` -} - -func (x *GetCurrentWeatherResponse) Reset() { - *x = GetCurrentWeatherResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_2_weather_api_proto_weather_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GetCurrentWeatherResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetCurrentWeatherResponse) ProtoMessage() {} - -func (x *GetCurrentWeatherResponse) ProtoReflect() protoreflect.Message { - mi := &file_examples_2_weather_api_proto_weather_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetCurrentWeatherResponse.ProtoReflect.Descriptor instead. -func (*GetCurrentWeatherResponse) Descriptor() ([]byte, []int) { - return file_examples_2_weather_api_proto_weather_proto_rawDescGZIP(), []int{2} -} - -func (x *GetCurrentWeatherResponse) GetCondition() string { - if x != nil { - return x.Condition - } - return "" -} - -func (x *GetCurrentWeatherResponse) GetTemperature() float64 { - if x != nil { - return x.Temperature - } - return 0 -} - -var File_examples_2_weather_api_proto_weather_proto protoreflect.FileDescriptor - -var file_examples_2_weather_api_proto_weather_proto_rawDesc = []byte{ - 0x0a, 0x2a, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x32, 0x2d, 0x77, 0x65, 0x61, - 0x74, 0x68, 0x65, 0x72, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x77, - 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x77, 0x65, - 0x61, 0x74, 0x68, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x6d, 0x63, - 0x70, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9e, 0x02, 0x0a, 0x18, 0x47, - 0x65, 0x74, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x57, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x04, 0x63, 0x69, 0x74, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x3c, 0xda, 0xb7, 0x2c, 0x38, 0x0a, 0x2b, 0x4e, 0x61, 0x6d, - 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x69, 0x74, 0x79, 0x20, 0x28, 0x65, - 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x4c, 0x6f, 0x6e, 0x64, 0x6f, 0x6e, 0x27, 0x2c, 0x20, 0x27, - 0x54, 0x6f, 0x6b, 0x79, 0x6f, 0x27, 0x29, 0x2e, 0x1a, 0x07, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x69, - 0x73, 0x60, 0x02, 0x48, 0x00, 0x52, 0x04, 0x63, 0x69, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0b, 0x63, - 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1b, 0x2e, 0x77, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x48, 0x00, 0x52, - 0x0b, 0x63, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x42, 0x6d, 0x0a, 0x08, - 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x61, 0xfa, 0xb7, 0x2c, 0x5d, 0x0a, 0x59, - 0x54, 0x68, 0x65, 0x20, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x20, - 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x20, 0x4d, 0x75, 0x73, 0x74, 0x20, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x20, 0x65, 0x69, 0x74, 0x68, 0x65, 0x72, 0x20, 0x61, 0x20, 0x63, 0x69, 0x74, - 0x79, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x74, 0x69, 0x74, 0x75, - 0x64, 0x65, 0x2f, 0x6c, 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, 0x20, 0x63, 0x6f, 0x6f, - 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x2e, 0x10, 0x01, 0x22, 0xb8, 0x01, 0x0a, 0x0b, - 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x12, 0x52, 0x0a, 0x08, 0x6c, - 0x61, 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x42, 0x36, 0xda, - 0xb7, 0x2c, 0x32, 0x0a, 0x1c, 0x4c, 0x61, 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x20, 0x69, 0x6e, - 0x20, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x20, 0x64, 0x65, 0x67, 0x72, 0x65, 0x65, 0x73, - 0x2e, 0xa1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x56, 0xc0, 0xa9, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x80, 0x56, 0x40, 0x52, 0x08, 0x6c, 0x61, 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x12, - 0x55, 0x0a, 0x09, 0x6c, 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x01, 0x42, 0x37, 0xda, 0xb7, 0x2c, 0x33, 0x0a, 0x1d, 0x4c, 0x6f, 0x6e, 0x67, 0x69, 0x74, - 0x75, 0x64, 0x65, 0x20, 0x69, 0x6e, 0x20, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x20, 0x64, - 0x65, 0x67, 0x72, 0x65, 0x65, 0x73, 0x2e, 0xa1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x66, - 0xc0, 0xa9, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x66, 0x40, 0x52, 0x09, 0x6c, 0x6f, 0x6e, - 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, 0x22, 0x82, 0x01, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x43, 0x75, - 0x72, 0x72, 0x65, 0x6e, 0x74, 0x57, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x47, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x42, 0x25, 0xda, 0xb7, 0x2c, 0x21, 0x0a, 0x1f, 0x43, - 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x20, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x20, 0x69, 0x6e, 0x20, 0x43, 0x65, 0x6c, 0x73, 0x69, 0x75, 0x73, 0x2e, 0x52, 0x0b, - 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x32, 0xe8, 0x01, 0x0a, 0x0a, - 0x57, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x41, 0x50, 0x49, 0x12, 0xa7, 0x01, 0x0a, 0x11, 0x47, - 0x65, 0x74, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x57, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, - 0x12, 0x28, 0x2e, 0x77, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x57, 0x65, 0x61, 0x74, - 0x68, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x77, 0x65, 0x61, - 0x74, 0x68, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, - 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x57, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0xd2, 0xb7, 0x2c, 0x39, 0x1a, 0x31, 0x47, 0x65, 0x74, - 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x20, 0x77, 0x65, - 0x61, 0x74, 0x68, 0x65, 0x72, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x61, 0x20, 0x73, 0x70, 0x65, 0x63, - 0x69, 0x66, 0x69, 0x63, 0x20, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x04, - 0x08, 0x01, 0x20, 0x01, 0x1a, 0x30, 0xca, 0xb7, 0x2c, 0x2c, 0x0a, 0x07, 0x77, 0x65, 0x61, 0x74, - 0x68, 0x65, 0x72, 0x12, 0x21, 0x41, 0x20, 0x72, 0x65, 0x61, 0x64, 0x2d, 0x6f, 0x6e, 0x6c, 0x79, - 0x20, 0x77, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x20, 0x64, 0x61, 0x74, 0x61, 0x20, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x61, 0x73, 0x79, 0x70, 0x2d, 0x74, 0x65, 0x63, 0x68, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6d, 0x63, 0x70, 0x2f, 0x65, - 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x32, 0x2d, 0x77, 0x65, 0x61, 0x74, 0x68, 0x65, - 0x72, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x77, 0x65, 0x61, 0x74, - 0x68, 0x65, 0x72, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_examples_2_weather_api_proto_weather_proto_rawDescOnce sync.Once - file_examples_2_weather_api_proto_weather_proto_rawDescData = file_examples_2_weather_api_proto_weather_proto_rawDesc -) - -func file_examples_2_weather_api_proto_weather_proto_rawDescGZIP() []byte { - file_examples_2_weather_api_proto_weather_proto_rawDescOnce.Do(func() { - file_examples_2_weather_api_proto_weather_proto_rawDescData = protoimpl.X.CompressGZIP(file_examples_2_weather_api_proto_weather_proto_rawDescData) - }) - return file_examples_2_weather_api_proto_weather_proto_rawDescData -} - -var file_examples_2_weather_api_proto_weather_proto_msgTypes = make([]protoimpl.MessageInfo, 3) -var file_examples_2_weather_api_proto_weather_proto_goTypes = []any{ - (*GetCurrentWeatherRequest)(nil), // 0: weather.api.v1.GetCurrentWeatherRequest - (*Coordinates)(nil), // 1: weather.api.v1.Coordinates - (*GetCurrentWeatherResponse)(nil), // 2: weather.api.v1.GetCurrentWeatherResponse -} -var file_examples_2_weather_api_proto_weather_proto_depIdxs = []int32{ - 1, // 0: weather.api.v1.GetCurrentWeatherRequest.coordinates:type_name -> weather.api.v1.Coordinates - 0, // 1: weather.api.v1.WeatherAPI.GetCurrentWeather:input_type -> weather.api.v1.GetCurrentWeatherRequest - 2, // 2: weather.api.v1.WeatherAPI.GetCurrentWeather:output_type -> weather.api.v1.GetCurrentWeatherResponse - 2, // [2:3] is the sub-list for method output_type - 1, // [1:2] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name -} - -func init() { file_examples_2_weather_api_proto_weather_proto_init() } -func file_examples_2_weather_api_proto_weather_proto_init() { - if File_examples_2_weather_api_proto_weather_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_examples_2_weather_api_proto_weather_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*GetCurrentWeatherRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_examples_2_weather_api_proto_weather_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*Coordinates); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_examples_2_weather_api_proto_weather_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*GetCurrentWeatherResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_examples_2_weather_api_proto_weather_proto_msgTypes[0].OneofWrappers = []any{ - (*GetCurrentWeatherRequest_City)(nil), - (*GetCurrentWeatherRequest_Coordinates)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_examples_2_weather_api_proto_weather_proto_rawDesc, - NumEnums: 0, - NumMessages: 3, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_examples_2_weather_api_proto_weather_proto_goTypes, - DependencyIndexes: file_examples_2_weather_api_proto_weather_proto_depIdxs, - MessageInfos: file_examples_2_weather_api_proto_weather_proto_msgTypes, - }.Build() - File_examples_2_weather_api_proto_weather_proto = out.File - file_examples_2_weather_api_proto_weather_proto_rawDesc = nil - file_examples_2_weather_api_proto_weather_proto_goTypes = nil - file_examples_2_weather_api_proto_weather_proto_depIdxs = nil -} diff --git a/examples/2-weather-api/main.go b/examples/2_weather_api/main.go similarity index 94% rename from examples/2-weather-api/main.go rename to examples/2_weather_api/main.go index b60997e..d2beca3 100644 --- a/examples/2-weather-api/main.go +++ b/examples/2_weather_api/main.go @@ -5,8 +5,8 @@ import ( "fmt" "log" + weatherv1 "github.com/easyp-tech/protoc-gen-mcp/examples/2_weather_api/proto" "github.com/modelcontextprotocol/go-sdk/mcp" - weatherv1 "github.com/easyp-tech/protoc-gen-mcp/examples/2-weather-api/proto" ) type weatherAPI struct{} diff --git a/examples/2_weather_api/main.py b/examples/2_weather_api/main.py new file mode 100644 index 0000000..9e14bb8 --- /dev/null +++ b/examples/2_weather_api/main.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from pathlib import Path +import sys + +_EXAMPLES_ROOT = Path(__file__).resolve().parents[1] +if str(_EXAMPLES_ROOT) not in sys.path: + sys.path.insert(0, str(_EXAMPLES_ROOT)) + +import anyio +import mcp.server.lowlevel +import mcp.server.stdio + +from proto import weather_mcp + + +class WeatherAPI: + def get_current_weather( + self, + _ctx: weather_mcp.ToolRequestContext, + req: weather_mcp.GetCurrentWeatherRequest, + ) -> weather_mcp.GetCurrentWeatherResponse: + if req.location is weather_mcp.UNSET: + raise ValueError("must provide either city or coordinates") + if isinstance(req.location, weather_mcp.GetCurrentWeatherRequestLocationCityVariant): + location_name = req.location.city + elif isinstance(req.location, weather_mcp.GetCurrentWeatherRequestLocationCoordinatesVariant): + location_name = ( + f"Lat: {req.location.coordinates.latitude:f}, " + f"Lon: {req.location.coordinates.longitude:f}" + ) + else: + raise ValueError("must provide either city or coordinates") + + condition = "Sunny" + temperature = 22.5 + if len(location_name) > 5: + condition = "Cloudy" + temperature = 14.0 + + return weather_mcp.GetCurrentWeatherResponse( + condition=condition, + temperature=temperature, + ) + + +def new_server() -> mcp.server.lowlevel.Server: + server = mcp.server.lowlevel.Server("weather-mcp-server", version="1.0.0") + weather_mcp.register_weather_api_tools(server, WeatherAPI()) + return server + + +async def run_stdio_server() -> None: + server = new_server() + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options(), + ) + + +def main() -> None: + anyio.run(run_stdio_server) + + +if __name__ == "__main__": + main() diff --git a/examples/2_weather_api/proto/__init__.py b/examples/2_weather_api/proto/__init__.py new file mode 100644 index 0000000..6e8ced9 --- /dev/null +++ b/examples/2_weather_api/proto/__init__.py @@ -0,0 +1 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. diff --git a/examples/2-weather-api/proto/weather.mcp.go b/examples/2_weather_api/proto/weather.mcp.go similarity index 96% rename from examples/2-weather-api/proto/weather.mcp.go rename to examples/2_weather_api/proto/weather.mcp.go index 140d662..c7a6817 100644 --- a/examples/2-weather-api/proto/weather.mcp.go +++ b/examples/2_weather_api/proto/weather.mcp.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-mcp. DO NOT EDIT. -// source: examples/2-weather-api/proto/weather.proto +// source: 2_weather_api/proto/weather.proto package weatherv1 @@ -28,7 +28,7 @@ func RegisterWeatherAPITools(server *mcp.Server, impl WeatherAPIToolHandler, opt Namespace: "weather", InputSchemaJSON: WeatherAPI_GetCurrentWeather_ToolSpecInputSchemaJSON, OutputSchemaJSON: WeatherAPI_GetCurrentWeather_ToolSpecOutputSchemaJSON, - Annotations: &mcp.ToolAnnotations{OpenWorldHint: proto.Bool(true)}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: proto.Bool(true)}, Icons: nil, NewRequest: func() *GetCurrentWeatherRequest { return &GetCurrentWeatherRequest{} }, NewResponse: func() *GetCurrentWeatherResponse { return &GetCurrentWeatherResponse{} }, diff --git a/examples/2_weather_api/proto/weather.pb.go b/examples/2_weather_api/proto/weather.pb.go new file mode 100644 index 0000000..42e95cc --- /dev/null +++ b/examples/2_weather_api/proto/weather.pb.go @@ -0,0 +1,292 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v0.14.1-v0.15.2-bufbuild-protocompile-easyp-rc1 +// source: 2_weather_api/proto/weather.proto + +package weatherv1 + +import ( + _ "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetCurrentWeatherRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Location: + // + // *GetCurrentWeatherRequest_City + // *GetCurrentWeatherRequest_Coordinates + Location isGetCurrentWeatherRequest_Location `protobuf_oneof:"location"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentWeatherRequest) Reset() { + *x = GetCurrentWeatherRequest{} + mi := &file__2_weather_api_proto_weather_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentWeatherRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentWeatherRequest) ProtoMessage() {} + +func (x *GetCurrentWeatherRequest) ProtoReflect() protoreflect.Message { + mi := &file__2_weather_api_proto_weather_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCurrentWeatherRequest.ProtoReflect.Descriptor instead. +func (*GetCurrentWeatherRequest) Descriptor() ([]byte, []int) { + return file__2_weather_api_proto_weather_proto_rawDescGZIP(), []int{0} +} + +func (x *GetCurrentWeatherRequest) GetLocation() isGetCurrentWeatherRequest_Location { + if x != nil { + return x.Location + } + return nil +} + +func (x *GetCurrentWeatherRequest) GetCity() string { + if x != nil { + if x, ok := x.Location.(*GetCurrentWeatherRequest_City); ok { + return x.City + } + } + return "" +} + +func (x *GetCurrentWeatherRequest) GetCoordinates() *Coordinates { + if x != nil { + if x, ok := x.Location.(*GetCurrentWeatherRequest_Coordinates); ok { + return x.Coordinates + } + } + return nil +} + +type isGetCurrentWeatherRequest_Location interface { + isGetCurrentWeatherRequest_Location() +} + +type GetCurrentWeatherRequest_City struct { + City string `protobuf:"bytes,1,opt,name=city,proto3,oneof"` +} + +type GetCurrentWeatherRequest_Coordinates struct { + Coordinates *Coordinates `protobuf:"bytes,2,opt,name=coordinates,proto3,oneof"` +} + +func (*GetCurrentWeatherRequest_City) isGetCurrentWeatherRequest_Location() {} + +func (*GetCurrentWeatherRequest_Coordinates) isGetCurrentWeatherRequest_Location() {} + +type Coordinates struct { + state protoimpl.MessageState `protogen:"open.v1"` + Latitude float64 `protobuf:"fixed64,1,opt,name=latitude,proto3" json:"latitude,omitempty"` + Longitude float64 `protobuf:"fixed64,2,opt,name=longitude,proto3" json:"longitude,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Coordinates) Reset() { + *x = Coordinates{} + mi := &file__2_weather_api_proto_weather_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Coordinates) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Coordinates) ProtoMessage() {} + +func (x *Coordinates) ProtoReflect() protoreflect.Message { + mi := &file__2_weather_api_proto_weather_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Coordinates.ProtoReflect.Descriptor instead. +func (*Coordinates) Descriptor() ([]byte, []int) { + return file__2_weather_api_proto_weather_proto_rawDescGZIP(), []int{1} +} + +func (x *Coordinates) GetLatitude() float64 { + if x != nil { + return x.Latitude + } + return 0 +} + +func (x *Coordinates) GetLongitude() float64 { + if x != nil { + return x.Longitude + } + return 0 +} + +type GetCurrentWeatherResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Condition string `protobuf:"bytes,1,opt,name=condition,proto3" json:"condition,omitempty"` + Temperature float64 `protobuf:"fixed64,2,opt,name=temperature,proto3" json:"temperature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentWeatherResponse) Reset() { + *x = GetCurrentWeatherResponse{} + mi := &file__2_weather_api_proto_weather_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentWeatherResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentWeatherResponse) ProtoMessage() {} + +func (x *GetCurrentWeatherResponse) ProtoReflect() protoreflect.Message { + mi := &file__2_weather_api_proto_weather_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCurrentWeatherResponse.ProtoReflect.Descriptor instead. +func (*GetCurrentWeatherResponse) Descriptor() ([]byte, []int) { + return file__2_weather_api_proto_weather_proto_rawDescGZIP(), []int{2} +} + +func (x *GetCurrentWeatherResponse) GetCondition() string { + if x != nil { + return x.Condition + } + return "" +} + +func (x *GetCurrentWeatherResponse) GetTemperature() float64 { + if x != nil { + return x.Temperature + } + return 0 +} + +var File__2_weather_api_proto_weather_proto protoreflect.FileDescriptor + +const file__2_weather_api_proto_weather_proto_rawDesc = "" + + "\n" + + "!2_weather_api/proto/weather.proto\x12\x0eweather.api.v1\x1a\x1cmcp/options/v1/options.proto\"\x9e\x02\n" + + "\x18GetCurrentWeatherRequest\x12R\n" + + "\x04city\x18\x01 \x01(\tB<ڷ,8\n" + + "+Name of the city (e.g., 'London', 'Tokyo').\x1a\a\n" + + "\x05Paris`\x02H\x00R\x04city\x12?\n" + + "\vcoordinates\x18\x02 \x01(\v2\x1b.weather.api.v1.CoordinatesH\x00R\vcoordinatesBm\n" + + "\blocation\x12a\xfa\xb7,]\n" + + "YThe location to query. Must provide either a city name or latitude/longitude coordinates.\x10\x01\"\xb8\x01\n" + + "\vCoordinates\x12R\n" + + "\blatitude\x18\x01 \x01(\x01B6ڷ,2\n" + + "\x1cLatitude in decimal degrees.\xa1\x01\x00\x00\x00\x00\x00\x80V\xc0\xa9\x01\x00\x00\x00\x00\x00\x80V@R\blatitude\x12U\n" + + "\tlongitude\x18\x02 \x01(\x01B7ڷ,3\n" + + "\x1dLongitude in decimal degrees.\xa1\x01\x00\x00\x00\x00\x00\x80f\xc0\xa9\x01\x00\x00\x00\x00\x00\x80f@R\tlongitude\"\x82\x01\n" + + "\x19GetCurrentWeatherResponse\x12\x1c\n" + + "\tcondition\x18\x01 \x01(\tR\tcondition\x12G\n" + + "\vtemperature\x18\x02 \x01(\x01B%ڷ,!\n" + + "\x1fCurrent temperature in Celsius.R\vtemperature2\xe8\x01\n" + + "\n" + + "WeatherAPI\x12\xa7\x01\n" + + "\x11GetCurrentWeather\x12(.weather.api.v1.GetCurrentWeatherRequest\x1a).weather.api.v1.GetCurrentWeatherResponse\"=ҷ,9\x1a1Gets the current weather for a specific location.R\x04\b\x01 \x01\x1a0ʷ,,\n" + + "\aweather\x12!A read-only weather data service.BMZKgithub.com/easyp-tech/protoc-gen-mcp/examples/2_weather_api/proto;weatherv1b\x06proto3" + +var ( + file__2_weather_api_proto_weather_proto_rawDescOnce sync.Once + file__2_weather_api_proto_weather_proto_rawDescData []byte +) + +func file__2_weather_api_proto_weather_proto_rawDescGZIP() []byte { + file__2_weather_api_proto_weather_proto_rawDescOnce.Do(func() { + file__2_weather_api_proto_weather_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file__2_weather_api_proto_weather_proto_rawDesc), len(file__2_weather_api_proto_weather_proto_rawDesc))) + }) + return file__2_weather_api_proto_weather_proto_rawDescData +} + +var file__2_weather_api_proto_weather_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file__2_weather_api_proto_weather_proto_goTypes = []any{ + (*GetCurrentWeatherRequest)(nil), // 0: weather.api.v1.GetCurrentWeatherRequest + (*Coordinates)(nil), // 1: weather.api.v1.Coordinates + (*GetCurrentWeatherResponse)(nil), // 2: weather.api.v1.GetCurrentWeatherResponse +} +var file__2_weather_api_proto_weather_proto_depIdxs = []int32{ + 1, // 0: weather.api.v1.GetCurrentWeatherRequest.coordinates:type_name -> weather.api.v1.Coordinates + 0, // 1: weather.api.v1.WeatherAPI.GetCurrentWeather:input_type -> weather.api.v1.GetCurrentWeatherRequest + 2, // 2: weather.api.v1.WeatherAPI.GetCurrentWeather:output_type -> weather.api.v1.GetCurrentWeatherResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file__2_weather_api_proto_weather_proto_init() } +func file__2_weather_api_proto_weather_proto_init() { + if File__2_weather_api_proto_weather_proto != nil { + return + } + file__2_weather_api_proto_weather_proto_msgTypes[0].OneofWrappers = []any{ + (*GetCurrentWeatherRequest_City)(nil), + (*GetCurrentWeatherRequest_Coordinates)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file__2_weather_api_proto_weather_proto_rawDesc), len(file__2_weather_api_proto_weather_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file__2_weather_api_proto_weather_proto_goTypes, + DependencyIndexes: file__2_weather_api_proto_weather_proto_depIdxs, + MessageInfos: file__2_weather_api_proto_weather_proto_msgTypes, + }.Build() + File__2_weather_api_proto_weather_proto = out.File + file__2_weather_api_proto_weather_proto_goTypes = nil + file__2_weather_api_proto_weather_proto_depIdxs = nil +} diff --git a/examples/2-weather-api/proto/weather.proto b/examples/2_weather_api/proto/weather.proto similarity index 97% rename from examples/2-weather-api/proto/weather.proto rename to examples/2_weather_api/proto/weather.proto index ab01f14..bd4945f 100644 --- a/examples/2-weather-api/proto/weather.proto +++ b/examples/2_weather_api/proto/weather.proto @@ -4,7 +4,7 @@ package weather.api.v1; import "mcp/options/v1/options.proto"; -option go_package = "github.com/easyp-tech/protoc-gen-mcp/examples/2-weather-api/proto;weatherv1"; +option go_package = "github.com/easyp-tech/protoc-gen-mcp/examples/2_weather_api/proto;weatherv1"; service WeatherAPI { option (mcp.options.v1.service) = { diff --git a/examples/2_weather_api/proto/weather_mcp.py b/examples/2_weather_api/proto/weather_mcp.py new file mode 100644 index 0000000..0d8676d --- /dev/null +++ b/examples/2_weather_api/proto/weather_mcp.py @@ -0,0 +1,488 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +# source: 2_weather_api/proto/weather.proto +from __future__ import annotations + +import enum +import inspect +import json +import weakref +from dataclasses import dataclass, field +from typing import Any, Awaitable, Protocol, TypeAlias + +import jsonschema +import mcp.server.lowlevel +import mcp.server.session +import mcp.shared.exceptions +import mcp.types +from google.protobuf import any_pb2, duration_pb2, empty_pb2, field_mask_pb2, json_format, struct_pb2, timestamp_pb2, wrappers_pb2 +try: + from . import weather_pb2 +except ImportError: + import weather_pb2 + +ToolRequestContext = mcp.server.session.ServerSession + +class _UnsetType: + __slots__ = () + + def __repr__(self) -> str: + return "UNSET" + +UNSET = _UnsetType() + +JSONValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None +ProtoAny: TypeAlias = dict[str, Any] +Timestamp: TypeAlias = str +Duration: TypeAlias = str +FieldMask: TypeAlias = str +Struct: TypeAlias = dict[str, Any] +Value: TypeAlias = JSONValue +ListValue: TypeAlias = list[Any] +BoolValue: TypeAlias = bool +StringValue: TypeAlias = str +BytesValue: TypeAlias = bytes +Int32Value: TypeAlias = int +UInt32Value: TypeAlias = int +Int64Value: TypeAlias = int +UInt64Value: TypeAlias = int +FloatValue: TypeAlias = float +DoubleValue: TypeAlias = float + +@dataclass(slots=True) +class Empty: + pass + +@dataclass(slots=True) +class Coordinates: + latitude: float + longitude: float + +@dataclass(slots=True) +class GetCurrentWeatherRequestLocationVariant: + pass + +@dataclass(slots=True) +class GetCurrentWeatherRequestLocationCityVariant(GetCurrentWeatherRequestLocationVariant): + city: str + +@dataclass(slots=True) +class GetCurrentWeatherRequestLocationCoordinatesVariant(GetCurrentWeatherRequestLocationVariant): + coordinates: Coordinates + +@dataclass(slots=True) +class GetCurrentWeatherRequest: + location: GetCurrentWeatherRequestLocationVariant | _UnsetType = UNSET + +@dataclass(slots=True) +class GetCurrentWeatherResponse: + condition: str + temperature: float + +@dataclass(frozen=True) +class _RegisteredTool: + name: str + title: str + description: str + input_schema_json: str + output_schema_json: str + request_type: type[Any] + response_type: type[Any] + from_pb: Any + to_pb: Any + handler: Any + annotations: dict[str, Any] | None + icons: list[dict[str, Any]] | None + +class _ServerToolRegistry: + def __init__(self, server: mcp.server.lowlevel.Server) -> None: + self.server = server + self.tools: dict[str, _RegisteredTool] = {} + self.reserved_tool_names = _get_reserved_tool_names(server) + self.handlers_installed = False + self.previous_list_tools: Any | None = None + self.previous_call_tool: Any | None = None + + def add_tool(self, tool: _RegisteredTool) -> None: + if tool.name in self.reserved_tool_names: + raise ValueError(f"duplicate tool registration: {tool.name}") + self.tools[tool.name] = tool + self.reserved_tool_names.add(tool.name) + + def list_tools(self) -> list[mcp.types.Tool]: + return [_build_tool(tool) for tool in self.tools.values()] + + async def call_tool(self, name: str, arguments: dict[str, Any] | None, context: ToolRequestContext | None = None) -> mcp.types.CallToolResult: + return await _dispatch_call(self, name, arguments or {}, context) + +_SERVER_REGISTRIES: weakref.WeakKeyDictionary[mcp.server.lowlevel.Server, _ServerToolRegistry] = weakref.WeakKeyDictionary() + +def _load_schema(schema_json: str) -> dict[str, Any]: + return json.loads(schema_json) + +def _protojson_to_message(arguments: dict[str, Any], message: Any) -> Any: + json_format.ParseDict(arguments, message) + return message + +def _normalize_tool_segment(segment: str | None) -> str: + if segment is None: + return "" + parts = [part for part in segment.strip().replace(".", "_").split("_") if part] + return "_".join(parts) + +def _normalize_namespace(namespace: str | None, default: str) -> str: + if namespace is None: + namespace = default + return _normalize_tool_segment(namespace) + +def _tool_name(namespace: str, method_name: str) -> str: + namespace = _normalize_tool_segment(namespace) + method_name = _normalize_tool_segment(method_name) + if namespace == '': + return method_name + if method_name == '': + return namespace + return f"{namespace}_{method_name}" + +def _get_registry(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _SERVER_REGISTRIES.get(server) + if registry is None: + registry = _ServerToolRegistry(server) + _SERVER_REGISTRIES[server] = registry + return registry + +_RESERVED_TOOL_NAMES_ATTR = "_protoc_gen_mcp_reserved_tool_names" + +def _get_reserved_tool_names(server: mcp.server.lowlevel.Server) -> set[str]: + reserved = getattr(server, _RESERVED_TOOL_NAMES_ATTR, None) + if reserved is None: + reserved = set() + setattr(server, _RESERVED_TOOL_NAMES_ATTR, reserved) + return reserved + +def _build_list_tools_request(request: Any) -> mcp.types.ListToolsRequest: + if isinstance(request, mcp.types.ListToolsRequest): + return request + return mcp.types.ListToolsRequest() + +def _merge_tools(previous: list[mcp.types.Tool], current: list[mcp.types.Tool]) -> list[mcp.types.Tool]: + merged: list[mcp.types.Tool] = [] + names: set[str] = set() + for tool in previous: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + for tool in current: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + return merged + +def _install_server_handlers(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _get_registry(server) + if registry.handlers_installed: + return registry + registry.previous_list_tools = server.request_handlers.get(mcp.types.ListToolsRequest) + registry.previous_call_tool = server.request_handlers.get(mcp.types.CallToolRequest) + + async def _list_tools(request: Any) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + list_request = _build_list_tools_request(request) + previous_tools: list[mcp.types.Tool] = [] + meta: dict[str, Any] | None = None + if current is not None: + if current.previous_list_tools is not None: + previous_result = await current.previous_list_tools(list_request) + if not isinstance(previous_result.root, mcp.types.ListToolsResult): + return previous_result + if getattr(list_request.params, "cursor", None) is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + if previous_result.root.nextCursor is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + meta = getattr(previous_result.root, "meta", None) + previous_tools = list(previous_result.root.tools) + current_tools = [] if current is None else current.list_tools() + tools = _merge_tools(previous_tools, current_tools) + server._tool_cache.clear() + for tool in tools: + server._tool_cache[tool.name] = tool + return mcp.types.ServerResult(mcp.types.ListToolsResult(tools=tools, _meta=meta)) + + async def _call_tool(request: mcp.types.CallToolRequest) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + if current is None: + return mcp.types.ServerResult(_tool_error_result("tool registry is not installed")) + if request.params.name not in current.tools: + if current.previous_call_tool is not None: + return await current.previous_call_tool(request) + _invalid_params_error(request.params.name, "unknown tool") + if current.previous_call_tool is not None: + await server.request_handlers[mcp.types.ListToolsRequest](mcp.types.ListToolsRequest()) + result = await current.call_tool(request.params.name, request.params.arguments or {}, server.request_context.session) + return mcp.types.ServerResult(result) + + server.request_handlers[mcp.types.ListToolsRequest] = _list_tools + server.request_handlers[mcp.types.CallToolRequest] = _call_tool + + registry.handlers_installed = True + return registry + +def _tool_annotations(raw: dict[str, Any] | None) -> mcp.types.ToolAnnotations | None: + if raw is None: + return None + return mcp.types.ToolAnnotations(**raw) + +def _tool_error_result(message: str) -> mcp.types.CallToolResult: + return mcp.types.CallToolResult( + content=[mcp.types.TextContent(type="text", text=message)], + isError=True, + ) + +def _invalid_params_error(tool_name: str, error: Any) -> mcp.shared.exceptions.McpError: + raise mcp.shared.exceptions.McpError( + mcp.types.ErrorData( + code=mcp.types.INVALID_PARAMS, + message=f"invalid arguments for tool {tool_name!r}: {error}", + ) + ) + +def _build_tool(tool: _RegisteredTool) -> Any: + return mcp.types.Tool( + name=tool.name, + title=tool.title, + description=tool.description, + inputSchema=_load_schema(tool.input_schema_json), + outputSchema=_load_schema(tool.output_schema_json), + annotations=_tool_annotations(tool.annotations), + icons=tool.icons, + ) + +async def _maybe_await(result: Any) -> Any: + if inspect.isawaitable(result): + return await result + return result + +def _message_to_json(message: Any) -> str: + kwargs = {"preserving_proto_field_name": False} + try: + return json_format.MessageToJson(message, always_print_fields_with_no_presence=True, **kwargs) + except TypeError: + return json_format.MessageToJson(message, including_default_value_fields=True, **kwargs) + +async def _dispatch_call(registry: _ServerToolRegistry, name: str, arguments: dict[str, Any], context: ToolRequestContext | None) -> mcp.types.CallToolResult: + tool = registry.tools.get(name) + if tool is None: + _invalid_params_error(name, "unknown tool") + try: + jsonschema.validate(arguments, _load_schema(tool.input_schema_json)) + except jsonschema.ValidationError as error: + _invalid_params_error(name, error) + try: + request_pb = _protojson_to_message(arguments, tool.request_type()) + request_dc = tool.from_pb(request_pb) + except Exception as error: + _invalid_params_error(name, error) + try: + response_dc = await _maybe_await(tool.handler(context, request_dc)) + except Exception as error: + return _tool_error_result(str(error)) + try: + if response_dc is None: + response_pb = tool.response_type() + else: + response_pb = tool.to_pb(response_dc) + payload = json.loads(_message_to_json(response_pb)) + except Exception as error: + raise RuntimeError(f"mcpruntime: marshal output for tool {name!r}: {error}") from error + text = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + content = [mcp.types.TextContent(type="text", text=text)] + try: + jsonschema.validate(payload, _load_schema(tool.output_schema_json)) + except jsonschema.ValidationError as error: + raise RuntimeError(f"mcpruntime: validate output for tool {name!r}: {error}") from error + return mcp.types.CallToolResult(content=content, structuredContent=payload) + +def _enum_from_pb(enum_type: type[enum.IntEnum], value: int) -> enum.IntEnum: + return enum_type(value) + +def _json_to_message(value: Any, message: Any) -> Any: + json_format.Parse(json.dumps(value), message) + return message + +def _from_pb_any(message: any_pb2.Any) -> ProtoAny: + return json.loads(_message_to_json(message)) + +def _to_pb_any(value: ProtoAny) -> any_pb2.Any: + return _json_to_message(value, any_pb2.Any()) + +def _from_pb_timestamp(message: timestamp_pb2.Timestamp) -> Timestamp: + return json.loads(_message_to_json(message)) + +def _to_pb_timestamp(value: Timestamp) -> timestamp_pb2.Timestamp: + return _json_to_message(value, timestamp_pb2.Timestamp()) + +def _from_pb_duration(message: duration_pb2.Duration) -> Duration: + return json.loads(_message_to_json(message)) + +def _to_pb_duration(value: Duration) -> duration_pb2.Duration: + return _json_to_message(value, duration_pb2.Duration()) + +def _from_pb_field_mask(message: field_mask_pb2.FieldMask) -> FieldMask: + return json.loads(_message_to_json(message)) + +def _to_pb_field_mask(value: FieldMask) -> field_mask_pb2.FieldMask: + return _json_to_message(value, field_mask_pb2.FieldMask()) + +def _from_pb_struct(message: struct_pb2.Struct) -> Struct: + return json.loads(_message_to_json(message)) + +def _to_pb_struct(value: Struct) -> struct_pb2.Struct: + return _json_to_message(value, struct_pb2.Struct()) + +def _from_pb_list_value(message: struct_pb2.ListValue) -> ListValue: + return json.loads(_message_to_json(message)) + +def _to_pb_list_value(value: ListValue) -> struct_pb2.ListValue: + return _json_to_message(value, struct_pb2.ListValue()) + +def _from_pb_value(message: struct_pb2.Value) -> Value: + return json.loads(_message_to_json(message)) + +def _to_pb_value(value: Value) -> struct_pb2.Value: + return _json_to_message(value, struct_pb2.Value()) + +def _from_pb_empty(message: empty_pb2.Empty) -> Empty: + return Empty() + +def _to_pb_empty(value: Empty) -> empty_pb2.Empty: + return empty_pb2.Empty() + +def _from_pb_bool_value(message: wrappers_pb2.BoolValue) -> BoolValue: + return message.value + +def _to_pb_bool_value(value: BoolValue) -> wrappers_pb2.BoolValue: + return wrappers_pb2.BoolValue(value=value) + +def _from_pb_string_value(message: wrappers_pb2.StringValue) -> StringValue: + return message.value + +def _to_pb_string_value(value: StringValue) -> wrappers_pb2.StringValue: + return wrappers_pb2.StringValue(value=value) + +def _from_pb_bytes_value(message: wrappers_pb2.BytesValue) -> BytesValue: + return message.value + +def _to_pb_bytes_value(value: BytesValue) -> wrappers_pb2.BytesValue: + return wrappers_pb2.BytesValue(value=value) + +def _from_pb_int32_value(message: wrappers_pb2.Int32Value) -> Int32Value: + return message.value + +def _to_pb_int32_value(value: Int32Value) -> wrappers_pb2.Int32Value: + return wrappers_pb2.Int32Value(value=value) + +def _from_pb_uint32_value(message: wrappers_pb2.UInt32Value) -> UInt32Value: + return message.value + +def _to_pb_uint32_value(value: UInt32Value) -> wrappers_pb2.UInt32Value: + return wrappers_pb2.UInt32Value(value=value) + +def _from_pb_int64_value(message: wrappers_pb2.Int64Value) -> Int64Value: + return message.value + +def _to_pb_int64_value(value: Int64Value) -> wrappers_pb2.Int64Value: + return wrappers_pb2.Int64Value(value=value) + +def _from_pb_uint64_value(message: wrappers_pb2.UInt64Value) -> UInt64Value: + return message.value + +def _to_pb_uint64_value(value: UInt64Value) -> wrappers_pb2.UInt64Value: + return wrappers_pb2.UInt64Value(value=value) + +def _from_pb_float_value(message: wrappers_pb2.FloatValue) -> FloatValue: + return message.value + +def _to_pb_float_value(value: FloatValue) -> wrappers_pb2.FloatValue: + return wrappers_pb2.FloatValue(value=value) + +def _from_pb_double_value(message: wrappers_pb2.DoubleValue) -> DoubleValue: + return message.value + +def _to_pb_double_value(value: DoubleValue) -> wrappers_pb2.DoubleValue: + return wrappers_pb2.DoubleValue(value=value) + +def _from_pb_coordinates(message: weather_pb2.Coordinates) -> Coordinates: + return Coordinates( + latitude=message.latitude, + longitude=message.longitude, + ) + +def _to_pb_coordinates(value: Coordinates) -> weather_pb2.Coordinates: + message = weather_pb2.Coordinates() + message.latitude = value.latitude + message.longitude = value.longitude + return message + +def _from_pb_get_current_weather_request(message: weather_pb2.GetCurrentWeatherRequest) -> GetCurrentWeatherRequest: + location = UNSET + location_case = message.WhichOneof("location") + if location_case == "city": + location = GetCurrentWeatherRequestLocationCityVariant(city=message.city) + elif location_case == "coordinates": + location = GetCurrentWeatherRequestLocationCoordinatesVariant(coordinates=_from_pb_coordinates(message.coordinates)) + return GetCurrentWeatherRequest( + location=location, + ) + +def _to_pb_get_current_weather_request(value: GetCurrentWeatherRequest) -> weather_pb2.GetCurrentWeatherRequest: + message = weather_pb2.GetCurrentWeatherRequest() + if value.location is not UNSET: + if isinstance(value.location, GetCurrentWeatherRequestLocationCityVariant): + message.city = value.location.city + elif isinstance(value.location, GetCurrentWeatherRequestLocationCoordinatesVariant): + message.coordinates.CopyFrom(_to_pb_coordinates(value.location.coordinates)) + else: + raise TypeError("unsupported GetCurrentWeatherRequestLocationVariant variant: " + type(value.location).__name__) + + return message + +def _from_pb_get_current_weather_response(message: weather_pb2.GetCurrentWeatherResponse) -> GetCurrentWeatherResponse: + return GetCurrentWeatherResponse( + condition=message.condition, + temperature=message.temperature, + ) + +def _to_pb_get_current_weather_response(value: GetCurrentWeatherResponse) -> weather_pb2.GetCurrentWeatherResponse: + message = weather_pb2.GetCurrentWeatherResponse() + message.condition = value.condition + message.temperature = value.temperature + return message + +class WeatherAPIToolHandler(Protocol): + def get_current_weather(self, ctx: ToolRequestContext, req: GetCurrentWeatherRequest) -> GetCurrentWeatherResponse | Awaitable[GetCurrentWeatherResponse]: + ... + +def register_weather_api_tools(server: mcp.server.lowlevel.Server, impl: WeatherAPIToolHandler, *, namespace: str | None = None) -> None: + if impl is None: + raise ValueError("register_weather_api_tools: impl is nil") + registry = _install_server_handlers(server) + resolved_namespace = _normalize_namespace(namespace, "weather") + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "GetCurrentWeather"), + title="", + description="Gets the current weather for a specific location.", + input_schema_json=WEATHER_API_GET_CURRENT_WEATHER_INPUT_SCHEMA_JSON, + output_schema_json=WEATHER_API_GET_CURRENT_WEATHER_OUTPUT_SCHEMA_JSON, + request_type=weather_pb2.GetCurrentWeatherRequest, + response_type=weather_pb2.GetCurrentWeatherResponse, + from_pb=_from_pb_get_current_weather_request, + to_pb=_to_pb_get_current_weather_response, + handler=impl.get_current_weather, + annotations={"readOnlyHint": True, "openWorldHint": True}, + icons=None, + )) + +WEATHER_API_GET_CURRENT_WEATHER_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"city\":{\"type\":[\"string\",\"null\"],\"description\":\"Name of the city (e.g., 'London', 'Tokyo').\",\"examples\":[\"Paris\"],\"minLength\":2},\"coordinates\":{\"type\":[\"object\",\"null\"],\"properties\":{\"latitude\":{\"description\":\"Latitude in decimal degrees.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]},\"longitude\":{\"description\":\"Longitude in decimal degrees.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]}},\"examples\":[{\"latitude\":1.25,\"longitude\":1.25}],\"required\":[\"latitude\",\"longitude\"],\"additionalProperties\":false}},\"examples\":[{\"city\":\"example\"}],\"additionalProperties\":false,\"allOf\":[{\"oneOf\":[{\"not\":{\"anyOf\":[{\"required\":[\"city\"],\"not\":{\"properties\":{\"city\":{\"type\":\"null\"}},\"required\":[\"city\"]}},{\"required\":[\"coordinates\"],\"not\":{\"properties\":{\"coordinates\":{\"type\":\"null\"}},\"required\":[\"coordinates\"]}}]}},{\"allOf\":[{\"required\":[\"city\"],\"not\":{\"properties\":{\"city\":{\"type\":\"null\"}},\"required\":[\"city\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"coordinates\"],\"not\":{\"properties\":{\"coordinates\":{\"type\":\"null\"}},\"required\":[\"coordinates\"]}}]}}]},{\"allOf\":[{\"required\":[\"coordinates\"],\"not\":{\"properties\":{\"coordinates\":{\"type\":\"null\"}},\"required\":[\"coordinates\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"city\"],\"not\":{\"properties\":{\"city\":{\"type\":\"null\"}},\"required\":[\"city\"]}}]}}]}]}]}" + +WEATHER_API_GET_CURRENT_WEATHER_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"condition\":{\"type\":\"string\",\"examples\":[\"example\"]},\"temperature\":{\"description\":\"Current temperature in Celsius.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]}},\"examples\":[{\"condition\":\"example\",\"temperature\":1.25}],\"required\":[\"condition\",\"temperature\"],\"additionalProperties\":false}" diff --git a/examples/2_weather_api/proto/weather_pb2.py b/examples/2_weather_api/proto/weather_pb2.py new file mode 100644 index 0000000..b968dae --- /dev/null +++ b/examples/2_weather_api/proto/weather_pb2.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: 2_weather_api/proto/weather.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + '2_weather_api/proto/weather.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from mcp.options.v1 import options_pb2 as mcp_dot_options_dot_v1_dot_options__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!2_weather_api/proto/weather.proto\x12\x0eweather.api.v1\x1a\x1cmcp/options/v1/options.proto\"\x9e\x02\n\x18GetCurrentWeatherRequest\x12R\n\x04\x63ity\x18\x01 \x01(\tB<\xda\xb7,8\n+Name of the city (e.g., \'London\', \'Tokyo\').\x1a\x07\n\x05Paris`\x02H\x00R\x04\x63ity\x12?\n\x0b\x63oordinates\x18\x02 \x01(\x0b\x32\x1b.weather.api.v1.CoordinatesH\x00R\x0b\x63oordinatesBm\n\x08location\x12\x61\xfa\xb7,]\nYThe location to query. Must provide either a city name or latitude/longitude coordinates.\x10\x01\"\xb8\x01\n\x0b\x43oordinates\x12R\n\x08latitude\x18\x01 \x01(\x01\x42\x36\xda\xb7,2\n\x1cLatitude in decimal degrees.\xa1\x01\x00\x00\x00\x00\x00\x80V\xc0\xa9\x01\x00\x00\x00\x00\x00\x80V@R\x08latitude\x12U\n\tlongitude\x18\x02 \x01(\x01\x42\x37\xda\xb7,3\n\x1dLongitude in decimal degrees.\xa1\x01\x00\x00\x00\x00\x00\x80\x66\xc0\xa9\x01\x00\x00\x00\x00\x00\x80\x66@R\tlongitude\"\x82\x01\n\x19GetCurrentWeatherResponse\x12\x1c\n\tcondition\x18\x01 \x01(\tR\tcondition\x12G\n\x0btemperature\x18\x02 \x01(\x01\x42%\xda\xb7,!\n\x1f\x43urrent temperature in Celsius.R\x0btemperature2\xe8\x01\n\nWeatherAPI\x12\xa7\x01\n\x11GetCurrentWeather\x12(.weather.api.v1.GetCurrentWeatherRequest\x1a).weather.api.v1.GetCurrentWeatherResponse\"=\xd2\xb7,9\x1a\x31Gets the current weather for a specific location.R\x04\x08\x01 \x01\x1a\x30\xca\xb7,,\n\x07weather\x12!A read-only weather data service.BMZKgithub.com/easyp-tech/protoc-gen-mcp/examples/2_weather_api/proto;weatherv1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, '2_weather_api.proto.weather_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'ZKgithub.com/easyp-tech/protoc-gen-mcp/examples/2_weather_api/proto;weatherv1' + _globals['_GETCURRENTWEATHERREQUEST'].oneofs_by_name['location']._loaded_options = None + _globals['_GETCURRENTWEATHERREQUEST'].oneofs_by_name['location']._serialized_options = b'\372\267,]\nYThe location to query. Must provide either a city name or latitude/longitude coordinates.\020\001' + _globals['_GETCURRENTWEATHERREQUEST'].fields_by_name['city']._loaded_options = None + _globals['_GETCURRENTWEATHERREQUEST'].fields_by_name['city']._serialized_options = b'\332\267,8\n+Name of the city (e.g., \'London\', \'Tokyo\').\032\007\n\005Paris`\002' + _globals['_COORDINATES'].fields_by_name['latitude']._loaded_options = None + _globals['_COORDINATES'].fields_by_name['latitude']._serialized_options = b'\332\267,2\n\034Latitude in decimal degrees.\241\001\000\000\000\000\000\200V\300\251\001\000\000\000\000\000\200V@' + _globals['_COORDINATES'].fields_by_name['longitude']._loaded_options = None + _globals['_COORDINATES'].fields_by_name['longitude']._serialized_options = b'\332\267,3\n\035Longitude in decimal degrees.\241\001\000\000\000\000\000\200f\300\251\001\000\000\000\000\000\200f@' + _globals['_GETCURRENTWEATHERRESPONSE'].fields_by_name['temperature']._loaded_options = None + _globals['_GETCURRENTWEATHERRESPONSE'].fields_by_name['temperature']._serialized_options = b'\332\267,!\n\037Current temperature in Celsius.' + _globals['_WEATHERAPI']._loaded_options = None + _globals['_WEATHERAPI']._serialized_options = b'\312\267,,\n\007weather\022!A read-only weather data service.' + _globals['_WEATHERAPI'].methods_by_name['GetCurrentWeather']._loaded_options = None + _globals['_WEATHERAPI'].methods_by_name['GetCurrentWeather']._serialized_options = b'\322\267,9\0321Gets the current weather for a specific location.R\004\010\001 \001' + _globals['_GETCURRENTWEATHERREQUEST']._serialized_start=84 + _globals['_GETCURRENTWEATHERREQUEST']._serialized_end=370 + _globals['_COORDINATES']._serialized_start=373 + _globals['_COORDINATES']._serialized_end=557 + _globals['_GETCURRENTWEATHERRESPONSE']._serialized_start=560 + _globals['_GETCURRENTWEATHERRESPONSE']._serialized_end=690 + _globals['_WEATHERAPI']._serialized_start=693 + _globals['_WEATHERAPI']._serialized_end=925 +# @@protoc_insertion_point(module_scope) diff --git a/examples/3-file-manager/proto/filemanager.pb.go b/examples/3-file-manager/proto/filemanager.pb.go deleted file mode 100644 index 10474cc..0000000 --- a/examples/3-file-manager/proto/filemanager.pb.go +++ /dev/null @@ -1,380 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.34.2 -// protoc v0.14.1-v0.16.1-bufbuild-protocompile-easyp-modified -// source: examples/3-file-manager/proto/filemanager.proto - -package filemanagerv1 - -import ( - _ "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type ReadFileRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` -} - -func (x *ReadFileRequest) Reset() { - *x = ReadFileRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_3_file_manager_proto_filemanager_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *ReadFileRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ReadFileRequest) ProtoMessage() {} - -func (x *ReadFileRequest) ProtoReflect() protoreflect.Message { - mi := &file_examples_3_file_manager_proto_filemanager_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ReadFileRequest.ProtoReflect.Descriptor instead. -func (*ReadFileRequest) Descriptor() ([]byte, []int) { - return file_examples_3_file_manager_proto_filemanager_proto_rawDescGZIP(), []int{0} -} - -func (x *ReadFileRequest) GetFilename() string { - if x != nil { - return x.Filename - } - return "" -} - -type ReadFileResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` -} - -func (x *ReadFileResponse) Reset() { - *x = ReadFileResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_3_file_manager_proto_filemanager_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *ReadFileResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ReadFileResponse) ProtoMessage() {} - -func (x *ReadFileResponse) ProtoReflect() protoreflect.Message { - mi := &file_examples_3_file_manager_proto_filemanager_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ReadFileResponse.ProtoReflect.Descriptor instead. -func (*ReadFileResponse) Descriptor() ([]byte, []int) { - return file_examples_3_file_manager_proto_filemanager_proto_rawDescGZIP(), []int{1} -} - -func (x *ReadFileResponse) GetContent() string { - if x != nil { - return x.Content - } - return "" -} - -type DeleteFileRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` -} - -func (x *DeleteFileRequest) Reset() { - *x = DeleteFileRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_3_file_manager_proto_filemanager_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DeleteFileRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteFileRequest) ProtoMessage() {} - -func (x *DeleteFileRequest) ProtoReflect() protoreflect.Message { - mi := &file_examples_3_file_manager_proto_filemanager_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DeleteFileRequest.ProtoReflect.Descriptor instead. -func (*DeleteFileRequest) Descriptor() ([]byte, []int) { - return file_examples_3_file_manager_proto_filemanager_proto_rawDescGZIP(), []int{2} -} - -func (x *DeleteFileRequest) GetFilename() string { - if x != nil { - return x.Filename - } - return "" -} - -type DeleteFileResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` -} - -func (x *DeleteFileResponse) Reset() { - *x = DeleteFileResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_3_file_manager_proto_filemanager_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DeleteFileResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteFileResponse) ProtoMessage() {} - -func (x *DeleteFileResponse) ProtoReflect() protoreflect.Message { - mi := &file_examples_3_file_manager_proto_filemanager_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DeleteFileResponse.ProtoReflect.Descriptor instead. -func (*DeleteFileResponse) Descriptor() ([]byte, []int) { - return file_examples_3_file_manager_proto_filemanager_proto_rawDescGZIP(), []int{3} -} - -func (x *DeleteFileResponse) GetSuccess() bool { - if x != nil { - return x.Success - } - return false -} - -var File_examples_3_file_manager_proto_filemanager_proto protoreflect.FileDescriptor - -var file_examples_3_file_manager_proto_filemanager_proto_rawDesc = []byte{ - 0x0a, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x33, 0x2d, 0x66, 0x69, 0x6c, - 0x65, 0x2d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, - 0x66, 0x69, 0x6c, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x12, 0x66, 0x69, 0x6c, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x6d, 0x63, 0x70, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0x90, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x7d, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x61, 0xda, 0xb7, 0x2c, 0x5d, 0x0a, - 0x44, 0x54, 0x68, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, - 0x20, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x65, 0x61, 0x64, 0x2e, 0x20, 0x4d, - 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x20, - 0x70, 0x61, 0x74, 0x68, 0x73, 0x20, 0x6f, 0x72, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x69, 0x65, 0x73, 0x2e, 0x52, 0x13, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, - 0x2d, 0x39, 0x5f, 0x5c, 0x2d, 0x5c, 0x2e, 0x5d, 0x2b, 0x24, 0x60, 0x01, 0x52, 0x08, 0x66, 0x69, - 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x10, 0x52, 0x65, 0x61, 0x64, 0x46, 0x69, - 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x22, 0x94, 0x01, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, - 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x7f, 0x0a, 0x08, 0x66, 0x69, - 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x63, 0xda, 0xb7, - 0x2c, 0x5f, 0x0a, 0x46, 0x54, 0x68, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6f, 0x66, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x64, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x2e, 0x20, 0x4d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x20, 0x70, 0x61, 0x74, 0x68, 0x73, 0x20, 0x6f, 0x72, 0x20, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x2e, 0x52, 0x13, 0x5e, 0x5b, 0x61, 0x2d, - 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5f, 0x5c, 0x2d, 0x5c, 0x2e, 0x5d, 0x2b, 0x24, 0x60, - 0x01, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2e, 0x0a, 0x12, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x32, 0xe0, 0x02, 0x0a, 0x0e, - 0x46, 0x69, 0x6c, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x41, 0x50, 0x49, 0x12, 0x7f, - 0x0a, 0x08, 0x52, 0x65, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x23, 0x2e, 0x66, 0x69, 0x6c, - 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x24, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0xd2, 0xb7, 0x2c, 0x24, 0x1a, 0x1c, 0x52, 0x65, 0x61, - 0x64, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x20, 0x6f, - 0x66, 0x20, 0x61, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x04, 0x08, 0x01, 0x18, 0x01, 0x12, - 0x82, 0x01, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x25, - 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0xd2, - 0xb7, 0x2c, 0x21, 0x1a, 0x1b, 0x50, 0x65, 0x72, 0x6d, 0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, - 0x20, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x2e, - 0x52, 0x02, 0x10, 0x01, 0x1a, 0x48, 0xca, 0xb7, 0x2c, 0x44, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, - 0x73, 0x12, 0x3b, 0x41, 0x20, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x65, - 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x65, 0x6d, 0x70, 0x6f, 0x72, 0x61, 0x72, - 0x79, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x2e, 0x42, 0x52, - 0x5a, 0x50, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x61, 0x73, - 0x79, 0x70, 0x2d, 0x74, 0x65, 0x63, 0x68, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, - 0x65, 0x6e, 0x2d, 0x6d, 0x63, 0x70, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, - 0x33, 0x2d, 0x66, 0x69, 0x6c, 0x65, 0x2d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x66, 0x69, 0x6c, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, - 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_examples_3_file_manager_proto_filemanager_proto_rawDescOnce sync.Once - file_examples_3_file_manager_proto_filemanager_proto_rawDescData = file_examples_3_file_manager_proto_filemanager_proto_rawDesc -) - -func file_examples_3_file_manager_proto_filemanager_proto_rawDescGZIP() []byte { - file_examples_3_file_manager_proto_filemanager_proto_rawDescOnce.Do(func() { - file_examples_3_file_manager_proto_filemanager_proto_rawDescData = protoimpl.X.CompressGZIP(file_examples_3_file_manager_proto_filemanager_proto_rawDescData) - }) - return file_examples_3_file_manager_proto_filemanager_proto_rawDescData -} - -var file_examples_3_file_manager_proto_filemanager_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_examples_3_file_manager_proto_filemanager_proto_goTypes = []any{ - (*ReadFileRequest)(nil), // 0: filemanager.api.v1.ReadFileRequest - (*ReadFileResponse)(nil), // 1: filemanager.api.v1.ReadFileResponse - (*DeleteFileRequest)(nil), // 2: filemanager.api.v1.DeleteFileRequest - (*DeleteFileResponse)(nil), // 3: filemanager.api.v1.DeleteFileResponse -} -var file_examples_3_file_manager_proto_filemanager_proto_depIdxs = []int32{ - 0, // 0: filemanager.api.v1.FileManagerAPI.ReadFile:input_type -> filemanager.api.v1.ReadFileRequest - 2, // 1: filemanager.api.v1.FileManagerAPI.DeleteFile:input_type -> filemanager.api.v1.DeleteFileRequest - 1, // 2: filemanager.api.v1.FileManagerAPI.ReadFile:output_type -> filemanager.api.v1.ReadFileResponse - 3, // 3: filemanager.api.v1.FileManagerAPI.DeleteFile:output_type -> filemanager.api.v1.DeleteFileResponse - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_examples_3_file_manager_proto_filemanager_proto_init() } -func file_examples_3_file_manager_proto_filemanager_proto_init() { - if File_examples_3_file_manager_proto_filemanager_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_examples_3_file_manager_proto_filemanager_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*ReadFileRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_examples_3_file_manager_proto_filemanager_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*ReadFileResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_examples_3_file_manager_proto_filemanager_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*DeleteFileRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_examples_3_file_manager_proto_filemanager_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*DeleteFileResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_examples_3_file_manager_proto_filemanager_proto_rawDesc, - NumEnums: 0, - NumMessages: 4, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_examples_3_file_manager_proto_filemanager_proto_goTypes, - DependencyIndexes: file_examples_3_file_manager_proto_filemanager_proto_depIdxs, - MessageInfos: file_examples_3_file_manager_proto_filemanager_proto_msgTypes, - }.Build() - File_examples_3_file_manager_proto_filemanager_proto = out.File - file_examples_3_file_manager_proto_filemanager_proto_rawDesc = nil - file_examples_3_file_manager_proto_filemanager_proto_goTypes = nil - file_examples_3_file_manager_proto_filemanager_proto_depIdxs = nil -} diff --git a/examples/3-file-manager/main.go b/examples/3_file_manager/main.go similarity index 94% rename from examples/3-file-manager/main.go rename to examples/3_file_manager/main.go index a3fb1f9..3be110f 100644 --- a/examples/3-file-manager/main.go +++ b/examples/3_file_manager/main.go @@ -7,8 +7,8 @@ import ( "os" "path/filepath" + filemanagerv1 "github.com/easyp-tech/protoc-gen-mcp/examples/3_file_manager/proto" "github.com/modelcontextprotocol/go-sdk/mcp" - filemanagerv1 "github.com/easyp-tech/protoc-gen-mcp/examples/3-file-manager/proto" ) type fileManagerAPI struct { @@ -39,7 +39,7 @@ func (s *fileManagerAPI) DeleteFile(ctx context.Context, req *filemanagerv1.Dele func main() { tmpDir := os.TempDir() - + server := mcp.NewServer(&mcp.Implementation{ Name: "filemanager-mcp-server", Version: "1.0.0", diff --git a/examples/3_file_manager/main.py b/examples/3_file_manager/main.py new file mode 100644 index 0000000..c30fece --- /dev/null +++ b/examples/3_file_manager/main.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from pathlib import Path +import sys +import tempfile + +_EXAMPLES_ROOT = Path(__file__).resolve().parents[1] +if str(_EXAMPLES_ROOT) not in sys.path: + sys.path.insert(0, str(_EXAMPLES_ROOT)) + +import anyio +import mcp.server.lowlevel +import mcp.server.stdio + +from proto import filemanager_mcp + + +class FileManagerAPI: + def __init__(self, base_path: Path) -> None: + self.base_path = base_path + self.base_path.mkdir(parents=True, exist_ok=True) + (self.base_path / "example.txt").write_text( + "Hello from the Python file manager example.\n", + encoding="utf-8", + ) + + def read_file( + self, + _ctx: filemanager_mcp.ToolRequestContext, + req: filemanager_mcp.ReadFileRequest, + ) -> filemanager_mcp.ReadFileResponse: + file_path = self._resolve(req.filename) + try: + content = file_path.read_text(encoding="utf-8") + except FileNotFoundError as err: + raise ValueError("file not found") from err + return filemanager_mcp.ReadFileResponse(content=content) + + def delete_file( + self, + _ctx: filemanager_mcp.ToolRequestContext, + req: filemanager_mcp.DeleteFileRequest, + ) -> filemanager_mcp.DeleteFileResponse: + file_path = self._resolve(req.filename) + try: + file_path.unlink() + except FileNotFoundError as err: + raise ValueError("file not found") from err + return filemanager_mcp.DeleteFileResponse(success=True) + + def _resolve(self, filename: str) -> Path: + candidate = Path(filename) + if candidate.name != filename: + raise ValueError("filename must not contain paths or directories") + return self.base_path / candidate + + +def new_server() -> mcp.server.lowlevel.Server: + base_path = Path(tempfile.gettempdir()) / "protoc-gen-mcp-python-example-files" + server = mcp.server.lowlevel.Server("filemanager-mcp-server", version="1.0.0") + filemanager_mcp.register_file_manager_api_tools(server, FileManagerAPI(base_path)) + return server + + +async def run_stdio_server() -> None: + server = new_server() + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options(), + ) + + +def main() -> None: + anyio.run(run_stdio_server) + + +if __name__ == "__main__": + main() diff --git a/examples/3_file_manager/proto/__init__.py b/examples/3_file_manager/proto/__init__.py new file mode 100644 index 0000000..6e8ced9 --- /dev/null +++ b/examples/3_file_manager/proto/__init__.py @@ -0,0 +1 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. diff --git a/examples/3-file-manager/proto/filemanager.mcp.go b/examples/3_file_manager/proto/filemanager.mcp.go similarity index 96% rename from examples/3-file-manager/proto/filemanager.mcp.go rename to examples/3_file_manager/proto/filemanager.mcp.go index 314174d..4dc65c2 100644 --- a/examples/3-file-manager/proto/filemanager.mcp.go +++ b/examples/3_file_manager/proto/filemanager.mcp.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-mcp. DO NOT EDIT. -// source: examples/3-file-manager/proto/filemanager.proto +// source: 3_file_manager/proto/filemanager.proto package filemanagerv1 @@ -29,7 +29,7 @@ func RegisterFileManagerAPITools(server *mcp.Server, impl FileManagerAPIToolHand Namespace: "files", InputSchemaJSON: FileManagerAPI_ReadFile_ToolSpecInputSchemaJSON, OutputSchemaJSON: FileManagerAPI_ReadFile_ToolSpecOutputSchemaJSON, - Annotations: &mcp.ToolAnnotations{IdempotentHint: true}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, IdempotentHint: true}, Icons: nil, NewRequest: func() *ReadFileRequest { return &ReadFileRequest{} }, NewResponse: func() *ReadFileResponse { return &ReadFileResponse{} }, diff --git a/examples/3_file_manager/proto/filemanager.pb.go b/examples/3_file_manager/proto/filemanager.pb.go new file mode 100644 index 0000000..caa79b8 --- /dev/null +++ b/examples/3_file_manager/proto/filemanager.pb.go @@ -0,0 +1,275 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v0.14.1-v0.15.2-bufbuild-protocompile-easyp-rc1 +// source: 3_file_manager/proto/filemanager.proto + +package filemanagerv1 + +import ( + _ "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ReadFileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadFileRequest) Reset() { + *x = ReadFileRequest{} + mi := &file__3_file_manager_proto_filemanager_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadFileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadFileRequest) ProtoMessage() {} + +func (x *ReadFileRequest) ProtoReflect() protoreflect.Message { + mi := &file__3_file_manager_proto_filemanager_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadFileRequest.ProtoReflect.Descriptor instead. +func (*ReadFileRequest) Descriptor() ([]byte, []int) { + return file__3_file_manager_proto_filemanager_proto_rawDescGZIP(), []int{0} +} + +func (x *ReadFileRequest) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +type ReadFileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadFileResponse) Reset() { + *x = ReadFileResponse{} + mi := &file__3_file_manager_proto_filemanager_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadFileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadFileResponse) ProtoMessage() {} + +func (x *ReadFileResponse) ProtoReflect() protoreflect.Message { + mi := &file__3_file_manager_proto_filemanager_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadFileResponse.ProtoReflect.Descriptor instead. +func (*ReadFileResponse) Descriptor() ([]byte, []int) { + return file__3_file_manager_proto_filemanager_proto_rawDescGZIP(), []int{1} +} + +func (x *ReadFileResponse) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type DeleteFileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteFileRequest) Reset() { + *x = DeleteFileRequest{} + mi := &file__3_file_manager_proto_filemanager_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteFileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteFileRequest) ProtoMessage() {} + +func (x *DeleteFileRequest) ProtoReflect() protoreflect.Message { + mi := &file__3_file_manager_proto_filemanager_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteFileRequest.ProtoReflect.Descriptor instead. +func (*DeleteFileRequest) Descriptor() ([]byte, []int) { + return file__3_file_manager_proto_filemanager_proto_rawDescGZIP(), []int{2} +} + +func (x *DeleteFileRequest) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +type DeleteFileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteFileResponse) Reset() { + *x = DeleteFileResponse{} + mi := &file__3_file_manager_proto_filemanager_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteFileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteFileResponse) ProtoMessage() {} + +func (x *DeleteFileResponse) ProtoReflect() protoreflect.Message { + mi := &file__3_file_manager_proto_filemanager_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteFileResponse.ProtoReflect.Descriptor instead. +func (*DeleteFileResponse) Descriptor() ([]byte, []int) { + return file__3_file_manager_proto_filemanager_proto_rawDescGZIP(), []int{3} +} + +func (x *DeleteFileResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +var File__3_file_manager_proto_filemanager_proto protoreflect.FileDescriptor + +const file__3_file_manager_proto_filemanager_proto_rawDesc = "" + + "\n" + + "&3_file_manager/proto/filemanager.proto\x12\x12filemanager.api.v1\x1a\x1cmcp/options/v1/options.proto\"\x90\x01\n" + + "\x0fReadFileRequest\x12}\n" + + "\bfilename\x18\x01 \x01(\tBaڷ,]\n" + + "DThe name of the file to read. Must not contain paths or directories.R\x13^[a-zA-Z0-9_\\-\\.]+$`\x01R\bfilename\",\n" + + "\x10ReadFileResponse\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"\x94\x01\n" + + "\x11DeleteFileRequest\x12\x7f\n" + + "\bfilename\x18\x01 \x01(\tBcڷ,_\n" + + "FThe name of the file to delete. Must not contain paths or directories.R\x13^[a-zA-Z0-9_\\-\\.]+$`\x01R\bfilename\".\n" + + "\x12DeleteFileResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess2\xe0\x02\n" + + "\x0eFileManagerAPI\x12\x7f\n" + + "\bReadFile\x12#.filemanager.api.v1.ReadFileRequest\x1a$.filemanager.api.v1.ReadFileResponse\"(ҷ,$\x1a\x1cReads the content of a file.R\x04\b\x01\x18\x01\x12\x82\x01\n" + + "\n" + + "DeleteFile\x12%.filemanager.api.v1.DeleteFileRequest\x1a&.filemanager.api.v1.DeleteFileResponse\"%ҷ,!\x1a\x1bPermanently deletes a file.R\x02\x10\x01\x1aHʷ,D\n" + + "\x05files\x12;A secure file management service for temporary directories.BRZPgithub.com/easyp-tech/protoc-gen-mcp/examples/3_file_manager/proto;filemanagerv1b\x06proto3" + +var ( + file__3_file_manager_proto_filemanager_proto_rawDescOnce sync.Once + file__3_file_manager_proto_filemanager_proto_rawDescData []byte +) + +func file__3_file_manager_proto_filemanager_proto_rawDescGZIP() []byte { + file__3_file_manager_proto_filemanager_proto_rawDescOnce.Do(func() { + file__3_file_manager_proto_filemanager_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file__3_file_manager_proto_filemanager_proto_rawDesc), len(file__3_file_manager_proto_filemanager_proto_rawDesc))) + }) + return file__3_file_manager_proto_filemanager_proto_rawDescData +} + +var file__3_file_manager_proto_filemanager_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file__3_file_manager_proto_filemanager_proto_goTypes = []any{ + (*ReadFileRequest)(nil), // 0: filemanager.api.v1.ReadFileRequest + (*ReadFileResponse)(nil), // 1: filemanager.api.v1.ReadFileResponse + (*DeleteFileRequest)(nil), // 2: filemanager.api.v1.DeleteFileRequest + (*DeleteFileResponse)(nil), // 3: filemanager.api.v1.DeleteFileResponse +} +var file__3_file_manager_proto_filemanager_proto_depIdxs = []int32{ + 0, // 0: filemanager.api.v1.FileManagerAPI.ReadFile:input_type -> filemanager.api.v1.ReadFileRequest + 2, // 1: filemanager.api.v1.FileManagerAPI.DeleteFile:input_type -> filemanager.api.v1.DeleteFileRequest + 1, // 2: filemanager.api.v1.FileManagerAPI.ReadFile:output_type -> filemanager.api.v1.ReadFileResponse + 3, // 3: filemanager.api.v1.FileManagerAPI.DeleteFile:output_type -> filemanager.api.v1.DeleteFileResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file__3_file_manager_proto_filemanager_proto_init() } +func file__3_file_manager_proto_filemanager_proto_init() { + if File__3_file_manager_proto_filemanager_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file__3_file_manager_proto_filemanager_proto_rawDesc), len(file__3_file_manager_proto_filemanager_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file__3_file_manager_proto_filemanager_proto_goTypes, + DependencyIndexes: file__3_file_manager_proto_filemanager_proto_depIdxs, + MessageInfos: file__3_file_manager_proto_filemanager_proto_msgTypes, + }.Build() + File__3_file_manager_proto_filemanager_proto = out.File + file__3_file_manager_proto_filemanager_proto_goTypes = nil + file__3_file_manager_proto_filemanager_proto_depIdxs = nil +} diff --git a/examples/3-file-manager/proto/filemanager.proto b/examples/3_file_manager/proto/filemanager.proto similarity index 97% rename from examples/3-file-manager/proto/filemanager.proto rename to examples/3_file_manager/proto/filemanager.proto index 628c8ea..bc290e3 100644 --- a/examples/3-file-manager/proto/filemanager.proto +++ b/examples/3_file_manager/proto/filemanager.proto @@ -4,7 +4,7 @@ package filemanager.api.v1; import "mcp/options/v1/options.proto"; -option go_package = "github.com/easyp-tech/protoc-gen-mcp/examples/3-file-manager/proto;filemanagerv1"; +option go_package = "github.com/easyp-tech/protoc-gen-mcp/examples/3_file_manager/proto;filemanagerv1"; service FileManagerAPI { option (mcp.options.v1.service) = { diff --git a/examples/3_file_manager/proto/filemanager_mcp.py b/examples/3_file_manager/proto/filemanager_mcp.py new file mode 100644 index 0000000..0223fe7 --- /dev/null +++ b/examples/3_file_manager/proto/filemanager_mcp.py @@ -0,0 +1,491 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +# source: 3_file_manager/proto/filemanager.proto +from __future__ import annotations + +import enum +import inspect +import json +import weakref +from dataclasses import dataclass, field +from typing import Any, Awaitable, Protocol, TypeAlias + +import jsonschema +import mcp.server.lowlevel +import mcp.server.session +import mcp.shared.exceptions +import mcp.types +from google.protobuf import any_pb2, duration_pb2, empty_pb2, field_mask_pb2, json_format, struct_pb2, timestamp_pb2, wrappers_pb2 +try: + from . import filemanager_pb2 +except ImportError: + import filemanager_pb2 + +ToolRequestContext = mcp.server.session.ServerSession + +class _UnsetType: + __slots__ = () + + def __repr__(self) -> str: + return "UNSET" + +UNSET = _UnsetType() + +JSONValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None +ProtoAny: TypeAlias = dict[str, Any] +Timestamp: TypeAlias = str +Duration: TypeAlias = str +FieldMask: TypeAlias = str +Struct: TypeAlias = dict[str, Any] +Value: TypeAlias = JSONValue +ListValue: TypeAlias = list[Any] +BoolValue: TypeAlias = bool +StringValue: TypeAlias = str +BytesValue: TypeAlias = bytes +Int32Value: TypeAlias = int +UInt32Value: TypeAlias = int +Int64Value: TypeAlias = int +UInt64Value: TypeAlias = int +FloatValue: TypeAlias = float +DoubleValue: TypeAlias = float + +@dataclass(slots=True) +class Empty: + pass + +@dataclass(slots=True) +class ReadFileRequest: + filename: str + +@dataclass(slots=True) +class ReadFileResponse: + content: str + +@dataclass(slots=True) +class DeleteFileRequest: + filename: str + +@dataclass(slots=True) +class DeleteFileResponse: + success: bool + +@dataclass(frozen=True) +class _RegisteredTool: + name: str + title: str + description: str + input_schema_json: str + output_schema_json: str + request_type: type[Any] + response_type: type[Any] + from_pb: Any + to_pb: Any + handler: Any + annotations: dict[str, Any] | None + icons: list[dict[str, Any]] | None + +class _ServerToolRegistry: + def __init__(self, server: mcp.server.lowlevel.Server) -> None: + self.server = server + self.tools: dict[str, _RegisteredTool] = {} + self.reserved_tool_names = _get_reserved_tool_names(server) + self.handlers_installed = False + self.previous_list_tools: Any | None = None + self.previous_call_tool: Any | None = None + + def add_tool(self, tool: _RegisteredTool) -> None: + if tool.name in self.reserved_tool_names: + raise ValueError(f"duplicate tool registration: {tool.name}") + self.tools[tool.name] = tool + self.reserved_tool_names.add(tool.name) + + def list_tools(self) -> list[mcp.types.Tool]: + return [_build_tool(tool) for tool in self.tools.values()] + + async def call_tool(self, name: str, arguments: dict[str, Any] | None, context: ToolRequestContext | None = None) -> mcp.types.CallToolResult: + return await _dispatch_call(self, name, arguments or {}, context) + +_SERVER_REGISTRIES: weakref.WeakKeyDictionary[mcp.server.lowlevel.Server, _ServerToolRegistry] = weakref.WeakKeyDictionary() + +def _load_schema(schema_json: str) -> dict[str, Any]: + return json.loads(schema_json) + +def _protojson_to_message(arguments: dict[str, Any], message: Any) -> Any: + json_format.ParseDict(arguments, message) + return message + +def _normalize_tool_segment(segment: str | None) -> str: + if segment is None: + return "" + parts = [part for part in segment.strip().replace(".", "_").split("_") if part] + return "_".join(parts) + +def _normalize_namespace(namespace: str | None, default: str) -> str: + if namespace is None: + namespace = default + return _normalize_tool_segment(namespace) + +def _tool_name(namespace: str, method_name: str) -> str: + namespace = _normalize_tool_segment(namespace) + method_name = _normalize_tool_segment(method_name) + if namespace == '': + return method_name + if method_name == '': + return namespace + return f"{namespace}_{method_name}" + +def _get_registry(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _SERVER_REGISTRIES.get(server) + if registry is None: + registry = _ServerToolRegistry(server) + _SERVER_REGISTRIES[server] = registry + return registry + +_RESERVED_TOOL_NAMES_ATTR = "_protoc_gen_mcp_reserved_tool_names" + +def _get_reserved_tool_names(server: mcp.server.lowlevel.Server) -> set[str]: + reserved = getattr(server, _RESERVED_TOOL_NAMES_ATTR, None) + if reserved is None: + reserved = set() + setattr(server, _RESERVED_TOOL_NAMES_ATTR, reserved) + return reserved + +def _build_list_tools_request(request: Any) -> mcp.types.ListToolsRequest: + if isinstance(request, mcp.types.ListToolsRequest): + return request + return mcp.types.ListToolsRequest() + +def _merge_tools(previous: list[mcp.types.Tool], current: list[mcp.types.Tool]) -> list[mcp.types.Tool]: + merged: list[mcp.types.Tool] = [] + names: set[str] = set() + for tool in previous: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + for tool in current: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + return merged + +def _install_server_handlers(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _get_registry(server) + if registry.handlers_installed: + return registry + registry.previous_list_tools = server.request_handlers.get(mcp.types.ListToolsRequest) + registry.previous_call_tool = server.request_handlers.get(mcp.types.CallToolRequest) + + async def _list_tools(request: Any) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + list_request = _build_list_tools_request(request) + previous_tools: list[mcp.types.Tool] = [] + meta: dict[str, Any] | None = None + if current is not None: + if current.previous_list_tools is not None: + previous_result = await current.previous_list_tools(list_request) + if not isinstance(previous_result.root, mcp.types.ListToolsResult): + return previous_result + if getattr(list_request.params, "cursor", None) is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + if previous_result.root.nextCursor is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + meta = getattr(previous_result.root, "meta", None) + previous_tools = list(previous_result.root.tools) + current_tools = [] if current is None else current.list_tools() + tools = _merge_tools(previous_tools, current_tools) + server._tool_cache.clear() + for tool in tools: + server._tool_cache[tool.name] = tool + return mcp.types.ServerResult(mcp.types.ListToolsResult(tools=tools, _meta=meta)) + + async def _call_tool(request: mcp.types.CallToolRequest) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + if current is None: + return mcp.types.ServerResult(_tool_error_result("tool registry is not installed")) + if request.params.name not in current.tools: + if current.previous_call_tool is not None: + return await current.previous_call_tool(request) + _invalid_params_error(request.params.name, "unknown tool") + if current.previous_call_tool is not None: + await server.request_handlers[mcp.types.ListToolsRequest](mcp.types.ListToolsRequest()) + result = await current.call_tool(request.params.name, request.params.arguments or {}, server.request_context.session) + return mcp.types.ServerResult(result) + + server.request_handlers[mcp.types.ListToolsRequest] = _list_tools + server.request_handlers[mcp.types.CallToolRequest] = _call_tool + + registry.handlers_installed = True + return registry + +def _tool_annotations(raw: dict[str, Any] | None) -> mcp.types.ToolAnnotations | None: + if raw is None: + return None + return mcp.types.ToolAnnotations(**raw) + +def _tool_error_result(message: str) -> mcp.types.CallToolResult: + return mcp.types.CallToolResult( + content=[mcp.types.TextContent(type="text", text=message)], + isError=True, + ) + +def _invalid_params_error(tool_name: str, error: Any) -> mcp.shared.exceptions.McpError: + raise mcp.shared.exceptions.McpError( + mcp.types.ErrorData( + code=mcp.types.INVALID_PARAMS, + message=f"invalid arguments for tool {tool_name!r}: {error}", + ) + ) + +def _build_tool(tool: _RegisteredTool) -> Any: + return mcp.types.Tool( + name=tool.name, + title=tool.title, + description=tool.description, + inputSchema=_load_schema(tool.input_schema_json), + outputSchema=_load_schema(tool.output_schema_json), + annotations=_tool_annotations(tool.annotations), + icons=tool.icons, + ) + +async def _maybe_await(result: Any) -> Any: + if inspect.isawaitable(result): + return await result + return result + +def _message_to_json(message: Any) -> str: + kwargs = {"preserving_proto_field_name": False} + try: + return json_format.MessageToJson(message, always_print_fields_with_no_presence=True, **kwargs) + except TypeError: + return json_format.MessageToJson(message, including_default_value_fields=True, **kwargs) + +async def _dispatch_call(registry: _ServerToolRegistry, name: str, arguments: dict[str, Any], context: ToolRequestContext | None) -> mcp.types.CallToolResult: + tool = registry.tools.get(name) + if tool is None: + _invalid_params_error(name, "unknown tool") + try: + jsonschema.validate(arguments, _load_schema(tool.input_schema_json)) + except jsonschema.ValidationError as error: + _invalid_params_error(name, error) + try: + request_pb = _protojson_to_message(arguments, tool.request_type()) + request_dc = tool.from_pb(request_pb) + except Exception as error: + _invalid_params_error(name, error) + try: + response_dc = await _maybe_await(tool.handler(context, request_dc)) + except Exception as error: + return _tool_error_result(str(error)) + try: + if response_dc is None: + response_pb = tool.response_type() + else: + response_pb = tool.to_pb(response_dc) + payload = json.loads(_message_to_json(response_pb)) + except Exception as error: + raise RuntimeError(f"mcpruntime: marshal output for tool {name!r}: {error}") from error + text = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + content = [mcp.types.TextContent(type="text", text=text)] + try: + jsonschema.validate(payload, _load_schema(tool.output_schema_json)) + except jsonschema.ValidationError as error: + raise RuntimeError(f"mcpruntime: validate output for tool {name!r}: {error}") from error + return mcp.types.CallToolResult(content=content, structuredContent=payload) + +def _enum_from_pb(enum_type: type[enum.IntEnum], value: int) -> enum.IntEnum: + return enum_type(value) + +def _json_to_message(value: Any, message: Any) -> Any: + json_format.Parse(json.dumps(value), message) + return message + +def _from_pb_any(message: any_pb2.Any) -> ProtoAny: + return json.loads(_message_to_json(message)) + +def _to_pb_any(value: ProtoAny) -> any_pb2.Any: + return _json_to_message(value, any_pb2.Any()) + +def _from_pb_timestamp(message: timestamp_pb2.Timestamp) -> Timestamp: + return json.loads(_message_to_json(message)) + +def _to_pb_timestamp(value: Timestamp) -> timestamp_pb2.Timestamp: + return _json_to_message(value, timestamp_pb2.Timestamp()) + +def _from_pb_duration(message: duration_pb2.Duration) -> Duration: + return json.loads(_message_to_json(message)) + +def _to_pb_duration(value: Duration) -> duration_pb2.Duration: + return _json_to_message(value, duration_pb2.Duration()) + +def _from_pb_field_mask(message: field_mask_pb2.FieldMask) -> FieldMask: + return json.loads(_message_to_json(message)) + +def _to_pb_field_mask(value: FieldMask) -> field_mask_pb2.FieldMask: + return _json_to_message(value, field_mask_pb2.FieldMask()) + +def _from_pb_struct(message: struct_pb2.Struct) -> Struct: + return json.loads(_message_to_json(message)) + +def _to_pb_struct(value: Struct) -> struct_pb2.Struct: + return _json_to_message(value, struct_pb2.Struct()) + +def _from_pb_list_value(message: struct_pb2.ListValue) -> ListValue: + return json.loads(_message_to_json(message)) + +def _to_pb_list_value(value: ListValue) -> struct_pb2.ListValue: + return _json_to_message(value, struct_pb2.ListValue()) + +def _from_pb_value(message: struct_pb2.Value) -> Value: + return json.loads(_message_to_json(message)) + +def _to_pb_value(value: Value) -> struct_pb2.Value: + return _json_to_message(value, struct_pb2.Value()) + +def _from_pb_empty(message: empty_pb2.Empty) -> Empty: + return Empty() + +def _to_pb_empty(value: Empty) -> empty_pb2.Empty: + return empty_pb2.Empty() + +def _from_pb_bool_value(message: wrappers_pb2.BoolValue) -> BoolValue: + return message.value + +def _to_pb_bool_value(value: BoolValue) -> wrappers_pb2.BoolValue: + return wrappers_pb2.BoolValue(value=value) + +def _from_pb_string_value(message: wrappers_pb2.StringValue) -> StringValue: + return message.value + +def _to_pb_string_value(value: StringValue) -> wrappers_pb2.StringValue: + return wrappers_pb2.StringValue(value=value) + +def _from_pb_bytes_value(message: wrappers_pb2.BytesValue) -> BytesValue: + return message.value + +def _to_pb_bytes_value(value: BytesValue) -> wrappers_pb2.BytesValue: + return wrappers_pb2.BytesValue(value=value) + +def _from_pb_int32_value(message: wrappers_pb2.Int32Value) -> Int32Value: + return message.value + +def _to_pb_int32_value(value: Int32Value) -> wrappers_pb2.Int32Value: + return wrappers_pb2.Int32Value(value=value) + +def _from_pb_uint32_value(message: wrappers_pb2.UInt32Value) -> UInt32Value: + return message.value + +def _to_pb_uint32_value(value: UInt32Value) -> wrappers_pb2.UInt32Value: + return wrappers_pb2.UInt32Value(value=value) + +def _from_pb_int64_value(message: wrappers_pb2.Int64Value) -> Int64Value: + return message.value + +def _to_pb_int64_value(value: Int64Value) -> wrappers_pb2.Int64Value: + return wrappers_pb2.Int64Value(value=value) + +def _from_pb_uint64_value(message: wrappers_pb2.UInt64Value) -> UInt64Value: + return message.value + +def _to_pb_uint64_value(value: UInt64Value) -> wrappers_pb2.UInt64Value: + return wrappers_pb2.UInt64Value(value=value) + +def _from_pb_float_value(message: wrappers_pb2.FloatValue) -> FloatValue: + return message.value + +def _to_pb_float_value(value: FloatValue) -> wrappers_pb2.FloatValue: + return wrappers_pb2.FloatValue(value=value) + +def _from_pb_double_value(message: wrappers_pb2.DoubleValue) -> DoubleValue: + return message.value + +def _to_pb_double_value(value: DoubleValue) -> wrappers_pb2.DoubleValue: + return wrappers_pb2.DoubleValue(value=value) + +def _from_pb_read_file_request(message: filemanager_pb2.ReadFileRequest) -> ReadFileRequest: + return ReadFileRequest( + filename=message.filename, + ) + +def _to_pb_read_file_request(value: ReadFileRequest) -> filemanager_pb2.ReadFileRequest: + message = filemanager_pb2.ReadFileRequest() + message.filename = value.filename + return message + +def _from_pb_read_file_response(message: filemanager_pb2.ReadFileResponse) -> ReadFileResponse: + return ReadFileResponse( + content=message.content, + ) + +def _to_pb_read_file_response(value: ReadFileResponse) -> filemanager_pb2.ReadFileResponse: + message = filemanager_pb2.ReadFileResponse() + message.content = value.content + return message + +def _from_pb_delete_file_request(message: filemanager_pb2.DeleteFileRequest) -> DeleteFileRequest: + return DeleteFileRequest( + filename=message.filename, + ) + +def _to_pb_delete_file_request(value: DeleteFileRequest) -> filemanager_pb2.DeleteFileRequest: + message = filemanager_pb2.DeleteFileRequest() + message.filename = value.filename + return message + +def _from_pb_delete_file_response(message: filemanager_pb2.DeleteFileResponse) -> DeleteFileResponse: + return DeleteFileResponse( + success=message.success, + ) + +def _to_pb_delete_file_response(value: DeleteFileResponse) -> filemanager_pb2.DeleteFileResponse: + message = filemanager_pb2.DeleteFileResponse() + message.success = value.success + return message + +class FileManagerAPIToolHandler(Protocol): + def read_file(self, ctx: ToolRequestContext, req: ReadFileRequest) -> ReadFileResponse | Awaitable[ReadFileResponse]: + ... + def delete_file(self, ctx: ToolRequestContext, req: DeleteFileRequest) -> DeleteFileResponse | Awaitable[DeleteFileResponse]: + ... + +def register_file_manager_api_tools(server: mcp.server.lowlevel.Server, impl: FileManagerAPIToolHandler, *, namespace: str | None = None) -> None: + if impl is None: + raise ValueError("register_file_manager_api_tools: impl is nil") + registry = _install_server_handlers(server) + resolved_namespace = _normalize_namespace(namespace, "files") + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "ReadFile"), + title="", + description="Reads the content of a file.", + input_schema_json=FILE_MANAGER_API_READ_FILE_INPUT_SCHEMA_JSON, + output_schema_json=FILE_MANAGER_API_READ_FILE_OUTPUT_SCHEMA_JSON, + request_type=filemanager_pb2.ReadFileRequest, + response_type=filemanager_pb2.ReadFileResponse, + from_pb=_from_pb_read_file_request, + to_pb=_to_pb_read_file_response, + handler=impl.read_file, + annotations={"readOnlyHint": True, "idempotentHint": True}, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "DeleteFile"), + title="", + description="Permanently deletes a file.", + input_schema_json=FILE_MANAGER_API_DELETE_FILE_INPUT_SCHEMA_JSON, + output_schema_json=FILE_MANAGER_API_DELETE_FILE_OUTPUT_SCHEMA_JSON, + request_type=filemanager_pb2.DeleteFileRequest, + response_type=filemanager_pb2.DeleteFileResponse, + from_pb=_from_pb_delete_file_request, + to_pb=_to_pb_delete_file_response, + handler=impl.delete_file, + annotations={"destructiveHint": True}, + icons=None, + )) + +FILE_MANAGER_API_READ_FILE_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"filename\":{\"type\":\"string\",\"description\":\"The name of the file to read. Must not contain paths or directories.\",\"examples\":[\"example\"],\"minLength\":1,\"pattern\":\"^[a-zA-Z0-9_\\\\-\\\\.]+$\"}},\"examples\":[{\"filename\":\"example\"}],\"required\":[\"filename\"],\"additionalProperties\":false}" + +FILE_MANAGER_API_READ_FILE_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"content\":{\"type\":\"string\",\"examples\":[\"example\"]}},\"examples\":[{\"content\":\"example\"}],\"required\":[\"content\"],\"additionalProperties\":false}" + +FILE_MANAGER_API_DELETE_FILE_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"filename\":{\"type\":\"string\",\"description\":\"The name of the file to delete. Must not contain paths or directories.\",\"examples\":[\"example\"],\"minLength\":1,\"pattern\":\"^[a-zA-Z0-9_\\\\-\\\\.]+$\"}},\"examples\":[{\"filename\":\"example\"}],\"required\":[\"filename\"],\"additionalProperties\":false}" + +FILE_MANAGER_API_DELETE_FILE_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"success\":{\"type\":\"boolean\",\"examples\":[true]}},\"examples\":[{\"success\":true}],\"required\":[\"success\"],\"additionalProperties\":false}" diff --git a/examples/3_file_manager/proto/filemanager_pb2.py b/examples/3_file_manager/proto/filemanager_pb2.py new file mode 100644 index 0000000..7681fab --- /dev/null +++ b/examples/3_file_manager/proto/filemanager_pb2.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: 3_file_manager/proto/filemanager.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + '3_file_manager/proto/filemanager.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from mcp.options.v1 import options_pb2 as mcp_dot_options_dot_v1_dot_options__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&3_file_manager/proto/filemanager.proto\x12\x12\x66ilemanager.api.v1\x1a\x1cmcp/options/v1/options.proto\"\x90\x01\n\x0fReadFileRequest\x12}\n\x08\x66ilename\x18\x01 \x01(\tBa\xda\xb7,]\nDThe name of the file to read. Must not contain paths or directories.R\x13^[a-zA-Z0-9_\\-\\.]+$`\x01R\x08\x66ilename\",\n\x10ReadFileResponse\x12\x18\n\x07\x63ontent\x18\x01 \x01(\tR\x07\x63ontent\"\x94\x01\n\x11\x44\x65leteFileRequest\x12\x7f\n\x08\x66ilename\x18\x01 \x01(\tBc\xda\xb7,_\nFThe name of the file to delete. Must not contain paths or directories.R\x13^[a-zA-Z0-9_\\-\\.]+$`\x01R\x08\x66ilename\".\n\x12\x44\x65leteFileResponse\x12\x18\n\x07success\x18\x01 \x01(\x08R\x07success2\xe0\x02\n\x0e\x46ileManagerAPI\x12\x7f\n\x08ReadFile\x12#.filemanager.api.v1.ReadFileRequest\x1a$.filemanager.api.v1.ReadFileResponse\"(\xd2\xb7,$\x1a\x1cReads the content of a file.R\x04\x08\x01\x18\x01\x12\x82\x01\n\nDeleteFile\x12%.filemanager.api.v1.DeleteFileRequest\x1a&.filemanager.api.v1.DeleteFileResponse\"%\xd2\xb7,!\x1a\x1bPermanently deletes a file.R\x02\x10\x01\x1aH\xca\xb7,D\n\x05\x66iles\x12;A secure file management service for temporary directories.BRZPgithub.com/easyp-tech/protoc-gen-mcp/examples/3_file_manager/proto;filemanagerv1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, '3_file_manager.proto.filemanager_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'ZPgithub.com/easyp-tech/protoc-gen-mcp/examples/3_file_manager/proto;filemanagerv1' + _globals['_READFILEREQUEST'].fields_by_name['filename']._loaded_options = None + _globals['_READFILEREQUEST'].fields_by_name['filename']._serialized_options = b'\332\267,]\nDThe name of the file to read. Must not contain paths or directories.R\023^[a-zA-Z0-9_\\-\\.]+$`\001' + _globals['_DELETEFILEREQUEST'].fields_by_name['filename']._loaded_options = None + _globals['_DELETEFILEREQUEST'].fields_by_name['filename']._serialized_options = b'\332\267,_\nFThe name of the file to delete. Must not contain paths or directories.R\023^[a-zA-Z0-9_\\-\\.]+$`\001' + _globals['_FILEMANAGERAPI']._loaded_options = None + _globals['_FILEMANAGERAPI']._serialized_options = b'\312\267,D\n\005files\022;A secure file management service for temporary directories.' + _globals['_FILEMANAGERAPI'].methods_by_name['ReadFile']._loaded_options = None + _globals['_FILEMANAGERAPI'].methods_by_name['ReadFile']._serialized_options = b'\322\267,$\032\034Reads the content of a file.R\004\010\001\030\001' + _globals['_FILEMANAGERAPI'].methods_by_name['DeleteFile']._loaded_options = None + _globals['_FILEMANAGERAPI'].methods_by_name['DeleteFile']._serialized_options = b'\322\267,!\032\033Permanently deletes a file.R\002\020\001' + _globals['_READFILEREQUEST']._serialized_start=93 + _globals['_READFILEREQUEST']._serialized_end=237 + _globals['_READFILERESPONSE']._serialized_start=239 + _globals['_READFILERESPONSE']._serialized_end=283 + _globals['_DELETEFILEREQUEST']._serialized_start=286 + _globals['_DELETEFILEREQUEST']._serialized_end=434 + _globals['_DELETEFILERESPONSE']._serialized_start=436 + _globals['_DELETEFILERESPONSE']._serialized_end=482 + _globals['_FILEMANAGERAPI']._serialized_start=485 + _globals['_FILEMANAGERAPI']._serialized_end=837 +# @@protoc_insertion_point(module_scope) diff --git a/examples/4-crm-system/proto/crm.pb.go b/examples/4-crm-system/proto/crm.pb.go deleted file mode 100644 index f27cebc..0000000 --- a/examples/4-crm-system/proto/crm.pb.go +++ /dev/null @@ -1,524 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.34.2 -// protoc v0.14.1-v0.16.1-bufbuild-protocompile-easyp-modified -// source: examples/4-crm-system/proto/crm.proto - -package crmv1 - -import ( - _ "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type User struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Tags []string `protobuf:"bytes,3,rep,name=tags,proto3" json:"tags,omitempty"` - RegisteredAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=registered_at,json=registeredAt,proto3" json:"registered_at,omitempty"` -} - -func (x *User) Reset() { - *x = User{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_4_crm_system_proto_crm_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *User) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*User) ProtoMessage() {} - -func (x *User) ProtoReflect() protoreflect.Message { - mi := &file_examples_4_crm_system_proto_crm_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use User.ProtoReflect.Descriptor instead. -func (*User) Descriptor() ([]byte, []int) { - return file_examples_4_crm_system_proto_crm_proto_rawDescGZIP(), []int{0} -} - -func (x *User) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *User) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *User) GetTags() []string { - if x != nil { - return x.Tags - } - return nil -} - -func (x *User) GetRegisteredAt() *timestamppb.Timestamp { - if x != nil { - return x.RegisteredAt - } - return nil -} - -type ListUsersRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Limit int32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` - RequiredTags []string `protobuf:"bytes,2,rep,name=required_tags,json=requiredTags,proto3" json:"required_tags,omitempty"` -} - -func (x *ListUsersRequest) Reset() { - *x = ListUsersRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_4_crm_system_proto_crm_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *ListUsersRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListUsersRequest) ProtoMessage() {} - -func (x *ListUsersRequest) ProtoReflect() protoreflect.Message { - mi := &file_examples_4_crm_system_proto_crm_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListUsersRequest.ProtoReflect.Descriptor instead. -func (*ListUsersRequest) Descriptor() ([]byte, []int) { - return file_examples_4_crm_system_proto_crm_proto_rawDescGZIP(), []int{1} -} - -func (x *ListUsersRequest) GetLimit() int32 { - if x != nil { - return x.Limit - } - return 0 -} - -func (x *ListUsersRequest) GetRequiredTags() []string { - if x != nil { - return x.RequiredTags - } - return nil -} - -type ListUsersResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` -} - -func (x *ListUsersResponse) Reset() { - *x = ListUsersResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_4_crm_system_proto_crm_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *ListUsersResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListUsersResponse) ProtoMessage() {} - -func (x *ListUsersResponse) ProtoReflect() protoreflect.Message { - mi := &file_examples_4_crm_system_proto_crm_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead. -func (*ListUsersResponse) Descriptor() ([]byte, []int) { - return file_examples_4_crm_system_proto_crm_proto_rawDescGZIP(), []int{2} -} - -func (x *ListUsersResponse) GetUsers() []*User { - if x != nil { - return x.Users - } - return nil -} - -type UpdateUserRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` - UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` -} - -func (x *UpdateUserRequest) Reset() { - *x = UpdateUserRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_4_crm_system_proto_crm_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *UpdateUserRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateUserRequest) ProtoMessage() {} - -func (x *UpdateUserRequest) ProtoReflect() protoreflect.Message { - mi := &file_examples_4_crm_system_proto_crm_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UpdateUserRequest.ProtoReflect.Descriptor instead. -func (*UpdateUserRequest) Descriptor() ([]byte, []int) { - return file_examples_4_crm_system_proto_crm_proto_rawDescGZIP(), []int{3} -} - -func (x *UpdateUserRequest) GetUser() *User { - if x != nil { - return x.User - } - return nil -} - -func (x *UpdateUserRequest) GetUpdateMask() *fieldmaskpb.FieldMask { - if x != nil { - return x.UpdateMask - } - return nil -} - -type UpdateUserResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` -} - -func (x *UpdateUserResponse) Reset() { - *x = UpdateUserResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_examples_4_crm_system_proto_crm_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *UpdateUserResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateUserResponse) ProtoMessage() {} - -func (x *UpdateUserResponse) ProtoReflect() protoreflect.Message { - mi := &file_examples_4_crm_system_proto_crm_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UpdateUserResponse.ProtoReflect.Descriptor instead. -func (*UpdateUserResponse) Descriptor() ([]byte, []int) { - return file_examples_4_crm_system_proto_crm_proto_rawDescGZIP(), []int{4} -} - -func (x *UpdateUserResponse) GetUser() *User { - if x != nil { - return x.User - } - return nil -} - -var File_examples_4_crm_system_proto_crm_proto protoreflect.FileDescriptor - -var file_examples_4_crm_system_proto_crm_proto_rawDesc = []byte{ - 0x0a, 0x25, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x34, 0x2d, 0x63, 0x72, 0x6d, - 0x2d, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x72, - 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x63, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x6d, 0x63, 0x70, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfb, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x38, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x28, 0xda, 0xb7, 0x2c, 0x24, 0x0a, - 0x1f, 0x55, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x20, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x2e, - 0xc0, 0x02, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xda, 0xb7, 0x2c, 0x02, 0x60, 0x02, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x53, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x09, 0x42, 0x3f, 0xda, 0xb7, 0x2c, 0x3b, 0x0a, 0x1a, 0x54, 0x61, 0x67, 0x73, 0x20, 0x61, 0x73, - 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x75, 0x73, - 0x65, 0x72, 0x2e, 0x1a, 0x17, 0x2a, 0x15, 0x0a, 0x09, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x6d, 0x69, - 0x75, 0x6d, 0x0a, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0xf0, 0x01, 0x01, 0x80, - 0x02, 0x01, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x48, 0x0a, 0x0d, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x07, 0xda, 0xb7, 0x2c, - 0x03, 0xc0, 0x02, 0x01, 0x52, 0x0c, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, - 0x41, 0x74, 0x22, 0xc1, 0x01, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x56, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x40, 0xda, 0xb7, 0x2c, 0x3c, 0x0a, 0x22, 0x4d, 0x61, - 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x20, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x20, 0x6f, 0x66, 0x20, - 0x75, 0x73, 0x65, 0x72, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x2e, - 0x22, 0x02, 0x38, 0x0a, 0xa1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xa9, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0x40, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, - 0x55, 0x0a, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x74, 0x61, 0x67, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x30, 0xda, 0xb7, 0x2c, 0x2c, 0x0a, 0x2a, 0x46, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x20, 0x75, 0x73, 0x65, 0x72, 0x73, 0x20, 0x77, 0x68, 0x6f, 0x20, 0x6d, - 0x75, 0x73, 0x74, 0x20, 0x68, 0x61, 0x76, 0x65, 0x20, 0x41, 0x4c, 0x4c, 0x20, 0x74, 0x68, 0x65, - 0x73, 0x65, 0x20, 0x74, 0x61, 0x67, 0x73, 0x2e, 0x52, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, - 0x65, 0x64, 0x54, 0x61, 0x67, 0x73, 0x22, 0x3b, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, - 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x75, - 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x72, 0x6d, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, - 0x65, 0x72, 0x73, 0x22, 0xee, 0x01, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, - 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x04, 0x75, 0x73, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x72, 0x6d, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x42, 0x2c, 0xda, 0xb7, 0x2c, 0x28, 0x0a, - 0x26, 0x54, 0x68, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x20, 0x6e, 0x65, 0x77, 0x20, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x84, 0x01, - 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x42, - 0x47, 0xda, 0xb7, 0x2c, 0x43, 0x0a, 0x34, 0x54, 0x68, 0x65, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x20, - 0x6f, 0x66, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x6e, 0x61, 0x6d, 0x65, - 0x27, 0x2c, 0x20, 0x27, 0x74, 0x61, 0x67, 0x73, 0x27, 0x29, 0x2e, 0x1a, 0x0b, 0x0a, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x2c, 0x74, 0x61, 0x67, 0x73, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x3a, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, - 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x75, 0x73, - 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x72, 0x6d, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, - 0x32, 0xc3, 0x03, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, 0x73, 0x41, 0x50, 0x49, 0x12, 0x7d, 0x0a, - 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e, 0x63, 0x72, 0x6d, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x72, 0x6d, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x33, 0xd2, 0xb7, 0x2c, 0x2f, 0x1a, 0x29, 0x4c, - 0x69, 0x73, 0x74, 0x20, 0x75, 0x73, 0x65, 0x72, 0x73, 0x20, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x69, - 0x6e, 0x67, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x20, - 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x02, 0x08, 0x01, 0x12, 0xc5, 0x01, 0x0a, - 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1d, 0x2e, 0x63, 0x72, - 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x63, 0x72, 0x6d, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, - 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x78, 0xd2, 0xb7, 0x2c, 0x74, - 0x1a, 0x3e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x61, 0x20, 0x70, 0x61, 0x72, 0x74, - 0x69, 0x61, 0x6c, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x20, - 0x75, 0x73, 0x65, 0x72, 0x20, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x75, 0x73, 0x69, - 0x6e, 0x67, 0x20, 0x61, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x20, 0x6d, 0x61, 0x73, 0x6b, 0x2e, - 0x5a, 0x32, 0x0a, 0x21, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x64, 0x69, 0x74, 0x2d, 0x69, 0x63, 0x6f, - 0x6e, 0x2e, 0x73, 0x76, 0x67, 0x12, 0x0d, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x73, 0x76, 0x67, - 0x2b, 0x78, 0x6d, 0x6c, 0x1a, 0x70, 0xca, 0xb7, 0x2c, 0x6c, 0x0a, 0x03, 0x63, 0x72, 0x6d, 0x12, - 0x36, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x20, 0x43, 0x75, 0x73, 0x74, - 0x6f, 0x6d, 0x65, 0x72, 0x20, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, - 0x70, 0x20, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x28, 0x43, 0x52, - 0x4d, 0x29, 0x20, 0x41, 0x50, 0x49, 0x2e, 0x1a, 0x2d, 0x0a, 0x20, 0x68, 0x74, 0x74, 0x70, 0x73, - 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, - 0x72, 0x6d, 0x2d, 0x69, 0x63, 0x6f, 0x6e, 0x2e, 0x70, 0x6e, 0x67, 0x12, 0x09, 0x69, 0x6d, 0x61, - 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x42, 0x48, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x61, 0x73, 0x79, 0x70, 0x2d, 0x74, 0x65, 0x63, 0x68, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6d, 0x63, 0x70, 0x2f, 0x65, - 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x34, 0x2d, 0x63, 0x72, 0x6d, 0x2d, 0x73, 0x79, - 0x73, 0x74, 0x65, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x63, 0x72, 0x6d, 0x76, 0x31, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_examples_4_crm_system_proto_crm_proto_rawDescOnce sync.Once - file_examples_4_crm_system_proto_crm_proto_rawDescData = file_examples_4_crm_system_proto_crm_proto_rawDesc -) - -func file_examples_4_crm_system_proto_crm_proto_rawDescGZIP() []byte { - file_examples_4_crm_system_proto_crm_proto_rawDescOnce.Do(func() { - file_examples_4_crm_system_proto_crm_proto_rawDescData = protoimpl.X.CompressGZIP(file_examples_4_crm_system_proto_crm_proto_rawDescData) - }) - return file_examples_4_crm_system_proto_crm_proto_rawDescData -} - -var file_examples_4_crm_system_proto_crm_proto_msgTypes = make([]protoimpl.MessageInfo, 5) -var file_examples_4_crm_system_proto_crm_proto_goTypes = []any{ - (*User)(nil), // 0: crm.api.v1.User - (*ListUsersRequest)(nil), // 1: crm.api.v1.ListUsersRequest - (*ListUsersResponse)(nil), // 2: crm.api.v1.ListUsersResponse - (*UpdateUserRequest)(nil), // 3: crm.api.v1.UpdateUserRequest - (*UpdateUserResponse)(nil), // 4: crm.api.v1.UpdateUserResponse - (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp - (*fieldmaskpb.FieldMask)(nil), // 6: google.protobuf.FieldMask -} -var file_examples_4_crm_system_proto_crm_proto_depIdxs = []int32{ - 5, // 0: crm.api.v1.User.registered_at:type_name -> google.protobuf.Timestamp - 0, // 1: crm.api.v1.ListUsersResponse.users:type_name -> crm.api.v1.User - 0, // 2: crm.api.v1.UpdateUserRequest.user:type_name -> crm.api.v1.User - 6, // 3: crm.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask - 0, // 4: crm.api.v1.UpdateUserResponse.user:type_name -> crm.api.v1.User - 1, // 5: crm.api.v1.UsersAPI.ListUsers:input_type -> crm.api.v1.ListUsersRequest - 3, // 6: crm.api.v1.UsersAPI.UpdateUser:input_type -> crm.api.v1.UpdateUserRequest - 2, // 7: crm.api.v1.UsersAPI.ListUsers:output_type -> crm.api.v1.ListUsersResponse - 4, // 8: crm.api.v1.UsersAPI.UpdateUser:output_type -> crm.api.v1.UpdateUserResponse - 7, // [7:9] is the sub-list for method output_type - 5, // [5:7] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name -} - -func init() { file_examples_4_crm_system_proto_crm_proto_init() } -func file_examples_4_crm_system_proto_crm_proto_init() { - if File_examples_4_crm_system_proto_crm_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_examples_4_crm_system_proto_crm_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*User); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_examples_4_crm_system_proto_crm_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*ListUsersRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_examples_4_crm_system_proto_crm_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*ListUsersResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_examples_4_crm_system_proto_crm_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*UpdateUserRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_examples_4_crm_system_proto_crm_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*UpdateUserResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_examples_4_crm_system_proto_crm_proto_rawDesc, - NumEnums: 0, - NumMessages: 5, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_examples_4_crm_system_proto_crm_proto_goTypes, - DependencyIndexes: file_examples_4_crm_system_proto_crm_proto_depIdxs, - MessageInfos: file_examples_4_crm_system_proto_crm_proto_msgTypes, - }.Build() - File_examples_4_crm_system_proto_crm_proto = out.File - file_examples_4_crm_system_proto_crm_proto_rawDesc = nil - file_examples_4_crm_system_proto_crm_proto_goTypes = nil - file_examples_4_crm_system_proto_crm_proto_depIdxs = nil -} diff --git a/examples/4-crm-system/main.go b/examples/4_crm_system/main.go similarity index 97% rename from examples/4-crm-system/main.go rename to examples/4_crm_system/main.go index 62abad8..ce1d597 100644 --- a/examples/4-crm-system/main.go +++ b/examples/4_crm_system/main.go @@ -7,8 +7,8 @@ import ( "sync" "time" + crmv1 "github.com/easyp-tech/protoc-gen-mcp/examples/4_crm_system/proto" "github.com/modelcontextprotocol/go-sdk/mcp" - crmv1 "github.com/easyp-tech/protoc-gen-mcp/examples/4-crm-system/proto" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/examples/4_crm_system/main.py b/examples/4_crm_system/main.py new file mode 100644 index 0000000..fbc215f --- /dev/null +++ b/examples/4_crm_system/main.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from pathlib import Path +import sys +import threading + +_EXAMPLES_ROOT = Path(__file__).resolve().parents[1] +if str(_EXAMPLES_ROOT) not in sys.path: + sys.path.insert(0, str(_EXAMPLES_ROOT)) + +import anyio +import mcp.server.lowlevel +import mcp.server.stdio + +from proto import crm_mcp + + +def _timestamp(value: datetime) -> crm_mcp.Timestamp: + return ( + value.astimezone(timezone.utc) + .replace(microsecond=0) + .isoformat() + .replace("+00:00", "Z") + ) + + +def _clone_user(user: crm_mcp.User) -> crm_mcp.User: + return crm_mcp.User( + id=user.id, + name=user.name, + registered_at=user.registered_at, + tags=list(user.tags), + ) + + +class CRMAPI: + def __init__(self) -> None: + now = datetime.now(timezone.utc) + self._mu = threading.RLock() + self._users = { + "usr_1": crm_mcp.User( + id="usr_1", + name="Alice Smith", + tags=["premium", "enterprise"], + registered_at=_timestamp(now - timedelta(hours=24)), + ), + "usr_2": crm_mcp.User( + id="usr_2", + name="Bob Jones", + tags=["basic"], + registered_at=_timestamp(now - timedelta(hours=2)), + ), + } + + def list_users( + self, + _ctx: crm_mcp.ToolRequestContext, + req: crm_mcp.ListUsersRequest, + ) -> crm_mcp.ListUsersResponse: + with self._mu: + result = [ + _clone_user(user) + for user in self._users.values() + if all(tag in user.tags for tag in req.required_tags) + ] + + limit = req.limit or 10 + return crm_mcp.ListUsersResponse(users=result[:limit]) + + def update_user( + self, + _ctx: crm_mcp.ToolRequestContext, + req: crm_mcp.UpdateUserRequest, + ) -> crm_mcp.UpdateUserResponse: + if req.user.id == "": + raise ValueError("user and user.id must be provided") + + with self._mu: + existing = self._users.get(req.user.id) + if existing is None: + raise ValueError(f'user "{req.user.id}" not found') + + paths = [path for path in req.update_mask.split(",") if path] + if not paths: + existing.name = req.user.name + existing.tags = list(req.user.tags) + else: + for path in paths: + if path == "name": + existing.name = req.user.name + elif path == "tags": + existing.tags = list(req.user.tags) + + return crm_mcp.UpdateUserResponse(user=_clone_user(existing)) + + +def new_server() -> mcp.server.lowlevel.Server: + server = mcp.server.lowlevel.Server("crm-mcp-server", version="1.0.0") + crm_mcp.register_users_api_tools(server, CRMAPI()) + return server + + +async def run_stdio_server() -> None: + server = new_server() + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options(), + ) + + +def main() -> None: + anyio.run(run_stdio_server) + + +if __name__ == "__main__": + main() diff --git a/examples/4_crm_system/proto/__init__.py b/examples/4_crm_system/proto/__init__.py new file mode 100644 index 0000000..6e8ced9 --- /dev/null +++ b/examples/4_crm_system/proto/__init__.py @@ -0,0 +1 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. diff --git a/examples/4-crm-system/proto/crm.mcp.go b/examples/4_crm_system/proto/crm.mcp.go similarity index 98% rename from examples/4-crm-system/proto/crm.mcp.go rename to examples/4_crm_system/proto/crm.mcp.go index 0ce8f16..906b22c 100644 --- a/examples/4-crm-system/proto/crm.mcp.go +++ b/examples/4_crm_system/proto/crm.mcp.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-mcp. DO NOT EDIT. -// source: examples/4-crm-system/proto/crm.proto +// source: 4_crm_system/proto/crm.proto package crmv1 @@ -28,7 +28,7 @@ func RegisterUsersAPITools(server *mcp.Server, impl UsersAPIToolHandler, opts .. Namespace: "crm", InputSchemaJSON: UsersAPI_ListUsers_ToolSpecInputSchemaJSON, OutputSchemaJSON: UsersAPI_ListUsers_ToolSpecOutputSchemaJSON, - Annotations: &mcp.ToolAnnotations{}, + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true}, Icons: []mcp.Icon{mcp.Icon{Source: "https://example.com/crm-icon.png", MIMEType: "image/png", Sizes: nil, Theme: ""}}, NewRequest: func() *ListUsersRequest { return &ListUsersRequest{} }, NewResponse: func() *ListUsersResponse { return &ListUsersResponse{} }, diff --git a/examples/4_crm_system/proto/crm.pb.go b/examples/4_crm_system/proto/crm.pb.go new file mode 100644 index 0000000..f830b52 --- /dev/null +++ b/examples/4_crm_system/proto/crm.pb.go @@ -0,0 +1,390 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v0.14.1-v0.15.2-bufbuild-protocompile-easyp-rc1 +// source: 4_crm_system/proto/crm.proto + +package crmv1 + +import ( + _ "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Tags []string `protobuf:"bytes,3,rep,name=tags,proto3" json:"tags,omitempty"` + RegisteredAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=registered_at,json=registeredAt,proto3" json:"registered_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file__4_crm_system_proto_crm_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file__4_crm_system_proto_crm_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file__4_crm_system_proto_crm_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *User) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *User) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *User) GetRegisteredAt() *timestamppb.Timestamp { + if x != nil { + return x.RegisteredAt + } + return nil +} + +type ListUsersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit int32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + RequiredTags []string `protobuf:"bytes,2,rep,name=required_tags,json=requiredTags,proto3" json:"required_tags,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUsersRequest) Reset() { + *x = ListUsersRequest{} + mi := &file__4_crm_system_proto_crm_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUsersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUsersRequest) ProtoMessage() {} + +func (x *ListUsersRequest) ProtoReflect() protoreflect.Message { + mi := &file__4_crm_system_proto_crm_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUsersRequest.ProtoReflect.Descriptor instead. +func (*ListUsersRequest) Descriptor() ([]byte, []int) { + return file__4_crm_system_proto_crm_proto_rawDescGZIP(), []int{1} +} + +func (x *ListUsersRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ListUsersRequest) GetRequiredTags() []string { + if x != nil { + return x.RequiredTags + } + return nil +} + +type ListUsersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUsersResponse) Reset() { + *x = ListUsersResponse{} + mi := &file__4_crm_system_proto_crm_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUsersResponse) ProtoMessage() {} + +func (x *ListUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file__4_crm_system_proto_crm_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead. +func (*ListUsersResponse) Descriptor() ([]byte, []int) { + return file__4_crm_system_proto_crm_proto_rawDescGZIP(), []int{2} +} + +func (x *ListUsersResponse) GetUsers() []*User { + if x != nil { + return x.Users + } + return nil +} + +type UpdateUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateUserRequest) Reset() { + *x = UpdateUserRequest{} + mi := &file__4_crm_system_proto_crm_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserRequest) ProtoMessage() {} + +func (x *UpdateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file__4_crm_system_proto_crm_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateUserRequest.ProtoReflect.Descriptor instead. +func (*UpdateUserRequest) Descriptor() ([]byte, []int) { + return file__4_crm_system_proto_crm_proto_rawDescGZIP(), []int{3} +} + +func (x *UpdateUserRequest) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +func (x *UpdateUserRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +type UpdateUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateUserResponse) Reset() { + *x = UpdateUserResponse{} + mi := &file__4_crm_system_proto_crm_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserResponse) ProtoMessage() {} + +func (x *UpdateUserResponse) ProtoReflect() protoreflect.Message { + mi := &file__4_crm_system_proto_crm_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateUserResponse.ProtoReflect.Descriptor instead. +func (*UpdateUserResponse) Descriptor() ([]byte, []int) { + return file__4_crm_system_proto_crm_proto_rawDescGZIP(), []int{4} +} + +func (x *UpdateUserResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +var File__4_crm_system_proto_crm_proto protoreflect.FileDescriptor + +const file__4_crm_system_proto_crm_proto_rawDesc = "" + + "\n" + + "\x1c4_crm_system/proto/crm.proto\x12\n" + + "crm.api.v1\x1a\x1cmcp/options/v1/options.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfb\x01\n" + + "\x04User\x128\n" + + "\x02id\x18\x01 \x01(\tB(ڷ,$\n" + + "\x1fUnique identifier for the user.\xc0\x02\x01R\x02id\x12\x1a\n" + + "\x04name\x18\x02 \x01(\tB\x06ڷ,\x02`\x02R\x04name\x12S\n" + + "\x04tags\x18\x03 \x03(\tB?ڷ,;\n" + + "\x1aTags assigned to the user.\x1a\x17*\x15\n" + + "\t\n" + + "\apremium\n" + + "\b\n" + + "\x06active\xf0\x01\x01\x80\x02\x01R\x04tags\x12H\n" + + "\rregistered_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampB\aڷ,\x03\xc0\x02\x01R\fregisteredAt\"\xc1\x01\n" + + "\x10ListUsersRequest\x12V\n" + + "\x05limit\x18\x01 \x01(\x05B@ڷ,<\n" + + "\"Maximum number of users to return.\"\x028\n" + + "\xa1\x01\x00\x00\x00\x00\x00\x00\xf0?\xa9\x01\x00\x00\x00\x00\x00\x00Y@R\x05limit\x12U\n" + + "\rrequired_tags\x18\x02 \x03(\tB0ڷ,,\n" + + "*Filter users who must have ALL these tags.R\frequiredTags\";\n" + + "\x11ListUsersResponse\x12&\n" + + "\x05users\x18\x01 \x03(\v2\x10.crm.api.v1.UserR\x05users\"\xee\x01\n" + + "\x11UpdateUserRequest\x12R\n" + + "\x04user\x18\x01 \x01(\v2\x10.crm.api.v1.UserB,ڷ,(\n" + + "&The user object containing new values.R\x04user\x12\x84\x01\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskBGڷ,C\n" + + "4The list of fields to update (e.g., 'name', 'tags').\x1a\v\n" + + "\tname,tagsR\n" + + "updateMask\":\n" + + "\x12UpdateUserResponse\x12$\n" + + "\x04user\x18\x01 \x01(\v2\x10.crm.api.v1.UserR\x04user2\xc3\x03\n" + + "\bUsersAPI\x12}\n" + + "\tListUsers\x12\x1c.crm.api.v1.ListUsersRequest\x1a\x1d.crm.api.v1.ListUsersResponse\"3ҷ,/\x1a)List users matching the selected filters.R\x02\b\x01\x12\xc5\x01\n" + + "\n" + + "UpdateUser\x12\x1d.crm.api.v1.UpdateUserRequest\x1a\x1e.crm.api.v1.UpdateUserResponse\"xҷ,t\x1a>Perform a partial update of a user profile using a field mask.Z2\n" + + "!https://example.com/edit-icon.svg\x12\rimage/svg+xml\x1apʷ,l\n" + + "\x03crm\x126Enterprise Customer Relationship Management (CRM) API.\x1a-\n" + + " https://example.com/crm-icon.png\x12\timage/pngBHZFgithub.com/easyp-tech/protoc-gen-mcp/examples/4_crm_system/proto;crmv1b\x06proto3" + +var ( + file__4_crm_system_proto_crm_proto_rawDescOnce sync.Once + file__4_crm_system_proto_crm_proto_rawDescData []byte +) + +func file__4_crm_system_proto_crm_proto_rawDescGZIP() []byte { + file__4_crm_system_proto_crm_proto_rawDescOnce.Do(func() { + file__4_crm_system_proto_crm_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file__4_crm_system_proto_crm_proto_rawDesc), len(file__4_crm_system_proto_crm_proto_rawDesc))) + }) + return file__4_crm_system_proto_crm_proto_rawDescData +} + +var file__4_crm_system_proto_crm_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file__4_crm_system_proto_crm_proto_goTypes = []any{ + (*User)(nil), // 0: crm.api.v1.User + (*ListUsersRequest)(nil), // 1: crm.api.v1.ListUsersRequest + (*ListUsersResponse)(nil), // 2: crm.api.v1.ListUsersResponse + (*UpdateUserRequest)(nil), // 3: crm.api.v1.UpdateUserRequest + (*UpdateUserResponse)(nil), // 4: crm.api.v1.UpdateUserResponse + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*fieldmaskpb.FieldMask)(nil), // 6: google.protobuf.FieldMask +} +var file__4_crm_system_proto_crm_proto_depIdxs = []int32{ + 5, // 0: crm.api.v1.User.registered_at:type_name -> google.protobuf.Timestamp + 0, // 1: crm.api.v1.ListUsersResponse.users:type_name -> crm.api.v1.User + 0, // 2: crm.api.v1.UpdateUserRequest.user:type_name -> crm.api.v1.User + 6, // 3: crm.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask + 0, // 4: crm.api.v1.UpdateUserResponse.user:type_name -> crm.api.v1.User + 1, // 5: crm.api.v1.UsersAPI.ListUsers:input_type -> crm.api.v1.ListUsersRequest + 3, // 6: crm.api.v1.UsersAPI.UpdateUser:input_type -> crm.api.v1.UpdateUserRequest + 2, // 7: crm.api.v1.UsersAPI.ListUsers:output_type -> crm.api.v1.ListUsersResponse + 4, // 8: crm.api.v1.UsersAPI.UpdateUser:output_type -> crm.api.v1.UpdateUserResponse + 7, // [7:9] is the sub-list for method output_type + 5, // [5:7] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file__4_crm_system_proto_crm_proto_init() } +func file__4_crm_system_proto_crm_proto_init() { + if File__4_crm_system_proto_crm_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file__4_crm_system_proto_crm_proto_rawDesc), len(file__4_crm_system_proto_crm_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file__4_crm_system_proto_crm_proto_goTypes, + DependencyIndexes: file__4_crm_system_proto_crm_proto_depIdxs, + MessageInfos: file__4_crm_system_proto_crm_proto_msgTypes, + }.Build() + File__4_crm_system_proto_crm_proto = out.File + file__4_crm_system_proto_crm_proto_goTypes = nil + file__4_crm_system_proto_crm_proto_depIdxs = nil +} diff --git a/examples/4-crm-system/proto/crm.proto b/examples/4_crm_system/proto/crm.proto similarity index 98% rename from examples/4-crm-system/proto/crm.proto rename to examples/4_crm_system/proto/crm.proto index 68341a4..d033150 100644 --- a/examples/4-crm-system/proto/crm.proto +++ b/examples/4_crm_system/proto/crm.proto @@ -6,7 +6,7 @@ import "mcp/options/v1/options.proto"; import "google/protobuf/field_mask.proto"; import "google/protobuf/timestamp.proto"; -option go_package = "github.com/easyp-tech/protoc-gen-mcp/examples/4-crm-system/proto;crmv1"; +option go_package = "github.com/easyp-tech/protoc-gen-mcp/examples/4_crm_system/proto;crmv1"; service UsersAPI { option (mcp.options.v1.service) = { diff --git a/examples/4_crm_system/proto/crm_mcp.py b/examples/4_crm_system/proto/crm_mcp.py new file mode 100644 index 0000000..caffc0d --- /dev/null +++ b/examples/4_crm_system/proto/crm_mcp.py @@ -0,0 +1,520 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +# source: 4_crm_system/proto/crm.proto +from __future__ import annotations + +import enum +import inspect +import json +import weakref +from dataclasses import dataclass, field +from typing import Any, Awaitable, Protocol, TypeAlias + +import jsonschema +import mcp.server.lowlevel +import mcp.server.session +import mcp.shared.exceptions +import mcp.types +from google.protobuf import any_pb2, duration_pb2, empty_pb2, field_mask_pb2, json_format, struct_pb2, timestamp_pb2, wrappers_pb2 +try: + from . import crm_pb2 +except ImportError: + import crm_pb2 + +ToolRequestContext = mcp.server.session.ServerSession + +class _UnsetType: + __slots__ = () + + def __repr__(self) -> str: + return "UNSET" + +UNSET = _UnsetType() + +JSONValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None +ProtoAny: TypeAlias = dict[str, Any] +Timestamp: TypeAlias = str +Duration: TypeAlias = str +FieldMask: TypeAlias = str +Struct: TypeAlias = dict[str, Any] +Value: TypeAlias = JSONValue +ListValue: TypeAlias = list[Any] +BoolValue: TypeAlias = bool +StringValue: TypeAlias = str +BytesValue: TypeAlias = bytes +Int32Value: TypeAlias = int +UInt32Value: TypeAlias = int +Int64Value: TypeAlias = int +UInt64Value: TypeAlias = int +FloatValue: TypeAlias = float +DoubleValue: TypeAlias = float + +@dataclass(slots=True) +class Empty: + pass + +@dataclass(slots=True) +class ListUsersRequest: + limit: int + required_tags: list[str] = field(default_factory=list) + +@dataclass(slots=True) +class User: + id: str + name: str + registered_at: Timestamp + tags: list[str] = field(default_factory=list) + +@dataclass(slots=True) +class ListUsersResponse: + users: list[User] = field(default_factory=list) + +@dataclass(slots=True) +class UpdateUserRequest: + user: User + update_mask: FieldMask + +@dataclass(slots=True) +class UpdateUserResponse: + user: User + +@dataclass(frozen=True) +class _RegisteredTool: + name: str + title: str + description: str + input_schema_json: str + output_schema_json: str + request_type: type[Any] + response_type: type[Any] + from_pb: Any + to_pb: Any + handler: Any + annotations: dict[str, Any] | None + icons: list[dict[str, Any]] | None + +class _ServerToolRegistry: + def __init__(self, server: mcp.server.lowlevel.Server) -> None: + self.server = server + self.tools: dict[str, _RegisteredTool] = {} + self.reserved_tool_names = _get_reserved_tool_names(server) + self.handlers_installed = False + self.previous_list_tools: Any | None = None + self.previous_call_tool: Any | None = None + + def add_tool(self, tool: _RegisteredTool) -> None: + if tool.name in self.reserved_tool_names: + raise ValueError(f"duplicate tool registration: {tool.name}") + self.tools[tool.name] = tool + self.reserved_tool_names.add(tool.name) + + def list_tools(self) -> list[mcp.types.Tool]: + return [_build_tool(tool) for tool in self.tools.values()] + + async def call_tool(self, name: str, arguments: dict[str, Any] | None, context: ToolRequestContext | None = None) -> mcp.types.CallToolResult: + return await _dispatch_call(self, name, arguments or {}, context) + +_SERVER_REGISTRIES: weakref.WeakKeyDictionary[mcp.server.lowlevel.Server, _ServerToolRegistry] = weakref.WeakKeyDictionary() + +def _load_schema(schema_json: str) -> dict[str, Any]: + return json.loads(schema_json) + +def _protojson_to_message(arguments: dict[str, Any], message: Any) -> Any: + json_format.ParseDict(arguments, message) + return message + +def _normalize_tool_segment(segment: str | None) -> str: + if segment is None: + return "" + parts = [part for part in segment.strip().replace(".", "_").split("_") if part] + return "_".join(parts) + +def _normalize_namespace(namespace: str | None, default: str) -> str: + if namespace is None: + namespace = default + return _normalize_tool_segment(namespace) + +def _tool_name(namespace: str, method_name: str) -> str: + namespace = _normalize_tool_segment(namespace) + method_name = _normalize_tool_segment(method_name) + if namespace == '': + return method_name + if method_name == '': + return namespace + return f"{namespace}_{method_name}" + +def _get_registry(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _SERVER_REGISTRIES.get(server) + if registry is None: + registry = _ServerToolRegistry(server) + _SERVER_REGISTRIES[server] = registry + return registry + +_RESERVED_TOOL_NAMES_ATTR = "_protoc_gen_mcp_reserved_tool_names" + +def _get_reserved_tool_names(server: mcp.server.lowlevel.Server) -> set[str]: + reserved = getattr(server, _RESERVED_TOOL_NAMES_ATTR, None) + if reserved is None: + reserved = set() + setattr(server, _RESERVED_TOOL_NAMES_ATTR, reserved) + return reserved + +def _build_list_tools_request(request: Any) -> mcp.types.ListToolsRequest: + if isinstance(request, mcp.types.ListToolsRequest): + return request + return mcp.types.ListToolsRequest() + +def _merge_tools(previous: list[mcp.types.Tool], current: list[mcp.types.Tool]) -> list[mcp.types.Tool]: + merged: list[mcp.types.Tool] = [] + names: set[str] = set() + for tool in previous: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + for tool in current: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + return merged + +def _install_server_handlers(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _get_registry(server) + if registry.handlers_installed: + return registry + registry.previous_list_tools = server.request_handlers.get(mcp.types.ListToolsRequest) + registry.previous_call_tool = server.request_handlers.get(mcp.types.CallToolRequest) + + async def _list_tools(request: Any) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + list_request = _build_list_tools_request(request) + previous_tools: list[mcp.types.Tool] = [] + meta: dict[str, Any] | None = None + if current is not None: + if current.previous_list_tools is not None: + previous_result = await current.previous_list_tools(list_request) + if not isinstance(previous_result.root, mcp.types.ListToolsResult): + return previous_result + if getattr(list_request.params, "cursor", None) is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + if previous_result.root.nextCursor is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + meta = getattr(previous_result.root, "meta", None) + previous_tools = list(previous_result.root.tools) + current_tools = [] if current is None else current.list_tools() + tools = _merge_tools(previous_tools, current_tools) + server._tool_cache.clear() + for tool in tools: + server._tool_cache[tool.name] = tool + return mcp.types.ServerResult(mcp.types.ListToolsResult(tools=tools, _meta=meta)) + + async def _call_tool(request: mcp.types.CallToolRequest) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + if current is None: + return mcp.types.ServerResult(_tool_error_result("tool registry is not installed")) + if request.params.name not in current.tools: + if current.previous_call_tool is not None: + return await current.previous_call_tool(request) + _invalid_params_error(request.params.name, "unknown tool") + if current.previous_call_tool is not None: + await server.request_handlers[mcp.types.ListToolsRequest](mcp.types.ListToolsRequest()) + result = await current.call_tool(request.params.name, request.params.arguments or {}, server.request_context.session) + return mcp.types.ServerResult(result) + + server.request_handlers[mcp.types.ListToolsRequest] = _list_tools + server.request_handlers[mcp.types.CallToolRequest] = _call_tool + + registry.handlers_installed = True + return registry + +def _tool_annotations(raw: dict[str, Any] | None) -> mcp.types.ToolAnnotations | None: + if raw is None: + return None + return mcp.types.ToolAnnotations(**raw) + +def _tool_error_result(message: str) -> mcp.types.CallToolResult: + return mcp.types.CallToolResult( + content=[mcp.types.TextContent(type="text", text=message)], + isError=True, + ) + +def _invalid_params_error(tool_name: str, error: Any) -> mcp.shared.exceptions.McpError: + raise mcp.shared.exceptions.McpError( + mcp.types.ErrorData( + code=mcp.types.INVALID_PARAMS, + message=f"invalid arguments for tool {tool_name!r}: {error}", + ) + ) + +def _build_tool(tool: _RegisteredTool) -> Any: + return mcp.types.Tool( + name=tool.name, + title=tool.title, + description=tool.description, + inputSchema=_load_schema(tool.input_schema_json), + outputSchema=_load_schema(tool.output_schema_json), + annotations=_tool_annotations(tool.annotations), + icons=tool.icons, + ) + +async def _maybe_await(result: Any) -> Any: + if inspect.isawaitable(result): + return await result + return result + +def _message_to_json(message: Any) -> str: + kwargs = {"preserving_proto_field_name": False} + try: + return json_format.MessageToJson(message, always_print_fields_with_no_presence=True, **kwargs) + except TypeError: + return json_format.MessageToJson(message, including_default_value_fields=True, **kwargs) + +async def _dispatch_call(registry: _ServerToolRegistry, name: str, arguments: dict[str, Any], context: ToolRequestContext | None) -> mcp.types.CallToolResult: + tool = registry.tools.get(name) + if tool is None: + _invalid_params_error(name, "unknown tool") + try: + jsonschema.validate(arguments, _load_schema(tool.input_schema_json)) + except jsonschema.ValidationError as error: + _invalid_params_error(name, error) + try: + request_pb = _protojson_to_message(arguments, tool.request_type()) + request_dc = tool.from_pb(request_pb) + except Exception as error: + _invalid_params_error(name, error) + try: + response_dc = await _maybe_await(tool.handler(context, request_dc)) + except Exception as error: + return _tool_error_result(str(error)) + try: + if response_dc is None: + response_pb = tool.response_type() + else: + response_pb = tool.to_pb(response_dc) + payload = json.loads(_message_to_json(response_pb)) + except Exception as error: + raise RuntimeError(f"mcpruntime: marshal output for tool {name!r}: {error}") from error + text = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + content = [mcp.types.TextContent(type="text", text=text)] + try: + jsonschema.validate(payload, _load_schema(tool.output_schema_json)) + except jsonschema.ValidationError as error: + raise RuntimeError(f"mcpruntime: validate output for tool {name!r}: {error}") from error + return mcp.types.CallToolResult(content=content, structuredContent=payload) + +def _enum_from_pb(enum_type: type[enum.IntEnum], value: int) -> enum.IntEnum: + return enum_type(value) + +def _json_to_message(value: Any, message: Any) -> Any: + json_format.Parse(json.dumps(value), message) + return message + +def _from_pb_any(message: any_pb2.Any) -> ProtoAny: + return json.loads(_message_to_json(message)) + +def _to_pb_any(value: ProtoAny) -> any_pb2.Any: + return _json_to_message(value, any_pb2.Any()) + +def _from_pb_timestamp(message: timestamp_pb2.Timestamp) -> Timestamp: + return json.loads(_message_to_json(message)) + +def _to_pb_timestamp(value: Timestamp) -> timestamp_pb2.Timestamp: + return _json_to_message(value, timestamp_pb2.Timestamp()) + +def _from_pb_duration(message: duration_pb2.Duration) -> Duration: + return json.loads(_message_to_json(message)) + +def _to_pb_duration(value: Duration) -> duration_pb2.Duration: + return _json_to_message(value, duration_pb2.Duration()) + +def _from_pb_field_mask(message: field_mask_pb2.FieldMask) -> FieldMask: + return json.loads(_message_to_json(message)) + +def _to_pb_field_mask(value: FieldMask) -> field_mask_pb2.FieldMask: + return _json_to_message(value, field_mask_pb2.FieldMask()) + +def _from_pb_struct(message: struct_pb2.Struct) -> Struct: + return json.loads(_message_to_json(message)) + +def _to_pb_struct(value: Struct) -> struct_pb2.Struct: + return _json_to_message(value, struct_pb2.Struct()) + +def _from_pb_list_value(message: struct_pb2.ListValue) -> ListValue: + return json.loads(_message_to_json(message)) + +def _to_pb_list_value(value: ListValue) -> struct_pb2.ListValue: + return _json_to_message(value, struct_pb2.ListValue()) + +def _from_pb_value(message: struct_pb2.Value) -> Value: + return json.loads(_message_to_json(message)) + +def _to_pb_value(value: Value) -> struct_pb2.Value: + return _json_to_message(value, struct_pb2.Value()) + +def _from_pb_empty(message: empty_pb2.Empty) -> Empty: + return Empty() + +def _to_pb_empty(value: Empty) -> empty_pb2.Empty: + return empty_pb2.Empty() + +def _from_pb_bool_value(message: wrappers_pb2.BoolValue) -> BoolValue: + return message.value + +def _to_pb_bool_value(value: BoolValue) -> wrappers_pb2.BoolValue: + return wrappers_pb2.BoolValue(value=value) + +def _from_pb_string_value(message: wrappers_pb2.StringValue) -> StringValue: + return message.value + +def _to_pb_string_value(value: StringValue) -> wrappers_pb2.StringValue: + return wrappers_pb2.StringValue(value=value) + +def _from_pb_bytes_value(message: wrappers_pb2.BytesValue) -> BytesValue: + return message.value + +def _to_pb_bytes_value(value: BytesValue) -> wrappers_pb2.BytesValue: + return wrappers_pb2.BytesValue(value=value) + +def _from_pb_int32_value(message: wrappers_pb2.Int32Value) -> Int32Value: + return message.value + +def _to_pb_int32_value(value: Int32Value) -> wrappers_pb2.Int32Value: + return wrappers_pb2.Int32Value(value=value) + +def _from_pb_uint32_value(message: wrappers_pb2.UInt32Value) -> UInt32Value: + return message.value + +def _to_pb_uint32_value(value: UInt32Value) -> wrappers_pb2.UInt32Value: + return wrappers_pb2.UInt32Value(value=value) + +def _from_pb_int64_value(message: wrappers_pb2.Int64Value) -> Int64Value: + return message.value + +def _to_pb_int64_value(value: Int64Value) -> wrappers_pb2.Int64Value: + return wrappers_pb2.Int64Value(value=value) + +def _from_pb_uint64_value(message: wrappers_pb2.UInt64Value) -> UInt64Value: + return message.value + +def _to_pb_uint64_value(value: UInt64Value) -> wrappers_pb2.UInt64Value: + return wrappers_pb2.UInt64Value(value=value) + +def _from_pb_float_value(message: wrappers_pb2.FloatValue) -> FloatValue: + return message.value + +def _to_pb_float_value(value: FloatValue) -> wrappers_pb2.FloatValue: + return wrappers_pb2.FloatValue(value=value) + +def _from_pb_double_value(message: wrappers_pb2.DoubleValue) -> DoubleValue: + return message.value + +def _to_pb_double_value(value: DoubleValue) -> wrappers_pb2.DoubleValue: + return wrappers_pb2.DoubleValue(value=value) + +def _from_pb_list_users_request(message: crm_pb2.ListUsersRequest) -> ListUsersRequest: + return ListUsersRequest( + limit=message.limit, + required_tags=list(message.required_tags), + ) + +def _to_pb_list_users_request(value: ListUsersRequest) -> crm_pb2.ListUsersRequest: + message = crm_pb2.ListUsersRequest() + message.limit = value.limit + message.required_tags.extend(value.required_tags) + return message + +def _from_pb_user(message: crm_pb2.User) -> User: + return User( + id=message.id, + name=message.name, + tags=list(message.tags), + registered_at=_from_pb_timestamp(message.registered_at), + ) + +def _to_pb_user(value: User) -> crm_pb2.User: + message = crm_pb2.User() + message.id = value.id + message.name = value.name + message.tags.extend(value.tags) + message.registered_at.CopyFrom(_to_pb_timestamp(value.registered_at)) + return message + +def _from_pb_list_users_response(message: crm_pb2.ListUsersResponse) -> ListUsersResponse: + return ListUsersResponse( + users=[_from_pb_user(item) for item in message.users], + ) + +def _to_pb_list_users_response(value: ListUsersResponse) -> crm_pb2.ListUsersResponse: + message = crm_pb2.ListUsersResponse() + message.users.extend(_to_pb_user(item) for item in value.users) + return message + +def _from_pb_update_user_request(message: crm_pb2.UpdateUserRequest) -> UpdateUserRequest: + return UpdateUserRequest( + user=_from_pb_user(message.user), + update_mask=_from_pb_field_mask(message.update_mask), + ) + +def _to_pb_update_user_request(value: UpdateUserRequest) -> crm_pb2.UpdateUserRequest: + message = crm_pb2.UpdateUserRequest() + message.user.CopyFrom(_to_pb_user(value.user)) + message.update_mask.CopyFrom(_to_pb_field_mask(value.update_mask)) + return message + +def _from_pb_update_user_response(message: crm_pb2.UpdateUserResponse) -> UpdateUserResponse: + return UpdateUserResponse( + user=_from_pb_user(message.user), + ) + +def _to_pb_update_user_response(value: UpdateUserResponse) -> crm_pb2.UpdateUserResponse: + message = crm_pb2.UpdateUserResponse() + message.user.CopyFrom(_to_pb_user(value.user)) + return message + +class UsersAPIToolHandler(Protocol): + def list_users(self, ctx: ToolRequestContext, req: ListUsersRequest) -> ListUsersResponse | Awaitable[ListUsersResponse]: + ... + def update_user(self, ctx: ToolRequestContext, req: UpdateUserRequest) -> UpdateUserResponse | Awaitable[UpdateUserResponse]: + ... + +def register_users_api_tools(server: mcp.server.lowlevel.Server, impl: UsersAPIToolHandler, *, namespace: str | None = None) -> None: + if impl is None: + raise ValueError("register_users_api_tools: impl is nil") + registry = _install_server_handlers(server) + resolved_namespace = _normalize_namespace(namespace, "crm") + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "ListUsers"), + title="", + description="List users matching the selected filters.", + input_schema_json=USERS_API_LIST_USERS_INPUT_SCHEMA_JSON, + output_schema_json=USERS_API_LIST_USERS_OUTPUT_SCHEMA_JSON, + request_type=crm_pb2.ListUsersRequest, + response_type=crm_pb2.ListUsersResponse, + from_pb=_from_pb_list_users_request, + to_pb=_to_pb_list_users_response, + handler=impl.list_users, + annotations={"readOnlyHint": True}, + icons=[{"src": "https://example.com/crm-icon.png", "mimeType": "image/png", "sizes": [], "theme": ""}], + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "UpdateUser"), + title="", + description="Perform a partial update of a user profile using a field mask.", + input_schema_json=USERS_API_UPDATE_USER_INPUT_SCHEMA_JSON, + output_schema_json=USERS_API_UPDATE_USER_OUTPUT_SCHEMA_JSON, + request_type=crm_pb2.UpdateUserRequest, + response_type=crm_pb2.UpdateUserResponse, + from_pb=_from_pb_update_user_request, + to_pb=_to_pb_update_user_response, + handler=impl.update_user, + annotations=None, + icons=[{"src": "https://example.com/edit-icon.svg", "mimeType": "image/svg+xml", "sizes": [], "theme": ""}], + )) + +USERS_API_LIST_USERS_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"limit\":{\"type\":\"integer\",\"description\":\"Maximum number of users to return.\",\"examples\":[-1],\"minimum\":1,\"maximum\":100},\"requiredTags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"Filter users who must have ALL these tags.\",\"examples\":[\"example\"]},\"description\":\"Filter users who must have ALL these tags.\",\"examples\":[[\"example\"]]}},\"examples\":[{\"limit\":-1,\"requiredTags\":[\"example\"]}],\"required\":[\"limit\"],\"additionalProperties\":false}" + +USERS_API_LIST_USERS_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"users\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"description\":\"Unique identifier for the user.\",\"readOnly\":true,\"examples\":[\"example\"]},\"name\":{\"type\":\"string\",\"examples\":[\"example\"],\"minLength\":2},\"registeredAt\":{\"type\":\"string\",\"readOnly\":true,\"examples\":[\"2026-03-09T10:11:12Z\"],\"format\":\"date-time\"},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"Tags assigned to the user.\",\"examples\":[\"example\"]},\"description\":\"Tags assigned to the user.\",\"examples\":[[\"premium\",\"active\"]],\"minItems\":1,\"uniqueItems\":true}},\"examples\":[{\"id\":\"example\",\"name\":\"example\",\"registeredAt\":\"2026-03-09T10:11:12Z\",\"tags\":[\"example\"]}],\"required\":[\"id\",\"name\",\"registeredAt\"],\"additionalProperties\":false},\"examples\":[[{\"id\":\"example\",\"name\":\"example\",\"registeredAt\":\"2026-03-09T10:11:12Z\",\"tags\":[\"example\"]}]]}},\"examples\":[{\"users\":[{\"id\":\"example\",\"name\":\"example\",\"registeredAt\":\"2026-03-09T10:11:12Z\",\"tags\":[\"example\"]}]}],\"additionalProperties\":false}" + +USERS_API_UPDATE_USER_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"updateMask\":{\"type\":\"string\",\"description\":\"The list of fields to update (e.g., 'name', 'tags').\",\"examples\":[\"name,tags\"]},\"user\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"description\":\"Unique identifier for the user.\",\"readOnly\":true,\"examples\":[\"example\"]},\"name\":{\"type\":\"string\",\"examples\":[\"example\"],\"minLength\":2},\"registeredAt\":{\"type\":\"string\",\"readOnly\":true,\"examples\":[\"2026-03-09T10:11:12Z\"],\"format\":\"date-time\"},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"Tags assigned to the user.\",\"examples\":[\"example\"]},\"description\":\"Tags assigned to the user.\",\"examples\":[[\"premium\",\"active\"]],\"minItems\":1,\"uniqueItems\":true}},\"description\":\"The user object containing new values.\",\"examples\":[{\"id\":\"example\",\"name\":\"example\",\"registeredAt\":\"2026-03-09T10:11:12Z\",\"tags\":[\"example\"]}],\"required\":[\"id\",\"name\",\"registeredAt\"],\"additionalProperties\":false}},\"examples\":[{\"updateMask\":\"fieldName,otherField\",\"user\":{\"id\":\"example\",\"name\":\"example\",\"registeredAt\":\"2026-03-09T10:11:12Z\",\"tags\":[\"example\"]}}],\"required\":[\"user\",\"updateMask\"],\"additionalProperties\":false}" + +USERS_API_UPDATE_USER_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"user\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"description\":\"Unique identifier for the user.\",\"readOnly\":true,\"examples\":[\"example\"]},\"name\":{\"type\":\"string\",\"examples\":[\"example\"],\"minLength\":2},\"registeredAt\":{\"type\":\"string\",\"readOnly\":true,\"examples\":[\"2026-03-09T10:11:12Z\"],\"format\":\"date-time\"},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"Tags assigned to the user.\",\"examples\":[\"example\"]},\"description\":\"Tags assigned to the user.\",\"examples\":[[\"premium\",\"active\"]],\"minItems\":1,\"uniqueItems\":true}},\"examples\":[{\"id\":\"example\",\"name\":\"example\",\"registeredAt\":\"2026-03-09T10:11:12Z\",\"tags\":[\"example\"]}],\"required\":[\"id\",\"name\",\"registeredAt\"],\"additionalProperties\":false}},\"examples\":[{\"user\":{\"id\":\"example\",\"name\":\"example\",\"registeredAt\":\"2026-03-09T10:11:12Z\",\"tags\":[\"example\"]}}],\"required\":[\"user\"],\"additionalProperties\":false}" diff --git a/examples/4_crm_system/proto/crm_pb2.py b/examples/4_crm_system/proto/crm_pb2.py new file mode 100644 index 0000000..6d5f2bf --- /dev/null +++ b/examples/4_crm_system/proto/crm_pb2.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: 4_crm_system/proto/crm.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + '4_crm_system/proto/crm.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from mcp.options.v1 import options_pb2 as mcp_dot_options_dot_v1_dot_options__pb2 +from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x34_crm_system/proto/crm.proto\x12\ncrm.api.v1\x1a\x1cmcp/options/v1/options.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfb\x01\n\x04User\x12\x38\n\x02id\x18\x01 \x01(\tB(\xda\xb7,$\n\x1fUnique identifier for the user.\xc0\x02\x01R\x02id\x12\x1a\n\x04name\x18\x02 \x01(\tB\x06\xda\xb7,\x02`\x02R\x04name\x12S\n\x04tags\x18\x03 \x03(\tB?\xda\xb7,;\n\x1aTags assigned to the user.\x1a\x17*\x15\n\t\n\x07premium\n\x08\n\x06\x61\x63tive\xf0\x01\x01\x80\x02\x01R\x04tags\x12H\n\rregistered_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x07\xda\xb7,\x03\xc0\x02\x01R\x0cregisteredAt\"\xc1\x01\n\x10ListUsersRequest\x12V\n\x05limit\x18\x01 \x01(\x05\x42@\xda\xb7,<\n\"Maximum number of users to return.\"\x02\x38\n\xa1\x01\x00\x00\x00\x00\x00\x00\xf0?\xa9\x01\x00\x00\x00\x00\x00\x00Y@R\x05limit\x12U\n\rrequired_tags\x18\x02 \x03(\tB0\xda\xb7,,\n*Filter users who must have ALL these tags.R\x0crequiredTags\";\n\x11ListUsersResponse\x12&\n\x05users\x18\x01 \x03(\x0b\x32\x10.crm.api.v1.UserR\x05users\"\xee\x01\n\x11UpdateUserRequest\x12R\n\x04user\x18\x01 \x01(\x0b\x32\x10.crm.api.v1.UserB,\xda\xb7,(\n&The user object containing new values.R\x04user\x12\x84\x01\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskBG\xda\xb7,C\n4The list of fields to update (e.g., \'name\', \'tags\').\x1a\x0b\n\tname,tagsR\nupdateMask\":\n\x12UpdateUserResponse\x12$\n\x04user\x18\x01 \x01(\x0b\x32\x10.crm.api.v1.UserR\x04user2\xc3\x03\n\x08UsersAPI\x12}\n\tListUsers\x12\x1c.crm.api.v1.ListUsersRequest\x1a\x1d.crm.api.v1.ListUsersResponse\"3\xd2\xb7,/\x1a)List users matching the selected filters.R\x02\x08\x01\x12\xc5\x01\n\nUpdateUser\x12\x1d.crm.api.v1.UpdateUserRequest\x1a\x1e.crm.api.v1.UpdateUserResponse\"x\xd2\xb7,t\x1a>Perform a partial update of a user profile using a field mask.Z2\n!https://example.com/edit-icon.svg\x12\rimage/svg+xml\x1ap\xca\xb7,l\n\x03\x63rm\x12\x36\x45nterprise Customer Relationship Management (CRM) API.\x1a-\n https://example.com/crm-icon.png\x12\timage/pngBHZFgithub.com/easyp-tech/protoc-gen-mcp/examples/4_crm_system/proto;crmv1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, '4_crm_system.proto.crm_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'ZFgithub.com/easyp-tech/protoc-gen-mcp/examples/4_crm_system/proto;crmv1' + _globals['_USER'].fields_by_name['id']._loaded_options = None + _globals['_USER'].fields_by_name['id']._serialized_options = b'\332\267,$\n\037Unique identifier for the user.\300\002\001' + _globals['_USER'].fields_by_name['name']._loaded_options = None + _globals['_USER'].fields_by_name['name']._serialized_options = b'\332\267,\002`\002' + _globals['_USER'].fields_by_name['tags']._loaded_options = None + _globals['_USER'].fields_by_name['tags']._serialized_options = b'\332\267,;\n\032Tags assigned to the user.\032\027*\025\n\t\n\007premium\n\010\n\006active\360\001\001\200\002\001' + _globals['_USER'].fields_by_name['registered_at']._loaded_options = None + _globals['_USER'].fields_by_name['registered_at']._serialized_options = b'\332\267,\003\300\002\001' + _globals['_LISTUSERSREQUEST'].fields_by_name['limit']._loaded_options = None + _globals['_LISTUSERSREQUEST'].fields_by_name['limit']._serialized_options = b'\332\267,<\n\"Maximum number of users to return.\"\0028\n\241\001\000\000\000\000\000\000\360?\251\001\000\000\000\000\000\000Y@' + _globals['_LISTUSERSREQUEST'].fields_by_name['required_tags']._loaded_options = None + _globals['_LISTUSERSREQUEST'].fields_by_name['required_tags']._serialized_options = b'\332\267,,\n*Filter users who must have ALL these tags.' + _globals['_UPDATEUSERREQUEST'].fields_by_name['user']._loaded_options = None + _globals['_UPDATEUSERREQUEST'].fields_by_name['user']._serialized_options = b'\332\267,(\n&The user object containing new values.' + _globals['_UPDATEUSERREQUEST'].fields_by_name['update_mask']._loaded_options = None + _globals['_UPDATEUSERREQUEST'].fields_by_name['update_mask']._serialized_options = b'\332\267,C\n4The list of fields to update (e.g., \'name\', \'tags\').\032\013\n\tname,tags' + _globals['_USERSAPI']._loaded_options = None + _globals['_USERSAPI']._serialized_options = b'\312\267,l\n\003crm\0226Enterprise Customer Relationship Management (CRM) API.\032-\n https://example.com/crm-icon.png\022\timage/png' + _globals['_USERSAPI'].methods_by_name['ListUsers']._loaded_options = None + _globals['_USERSAPI'].methods_by_name['ListUsers']._serialized_options = b'\322\267,/\032)List users matching the selected filters.R\002\010\001' + _globals['_USERSAPI'].methods_by_name['UpdateUser']._loaded_options = None + _globals['_USERSAPI'].methods_by_name['UpdateUser']._serialized_options = b'\322\267,t\032>Perform a partial update of a user profile using a field mask.Z2\n!https://example.com/edit-icon.svg\022\rimage/svg+xml' + _globals['_USER']._serialized_start=142 + _globals['_USER']._serialized_end=393 + _globals['_LISTUSERSREQUEST']._serialized_start=396 + _globals['_LISTUSERSREQUEST']._serialized_end=589 + _globals['_LISTUSERSRESPONSE']._serialized_start=591 + _globals['_LISTUSERSRESPONSE']._serialized_end=650 + _globals['_UPDATEUSERREQUEST']._serialized_start=653 + _globals['_UPDATEUSERREQUEST']._serialized_end=891 + _globals['_UPDATEUSERRESPONSE']._serialized_start=893 + _globals['_UPDATEUSERRESPONSE']._serialized_end=951 + _globals['_USERSAPI']._serialized_start=954 + _globals['_USERSAPI']._serialized_end=1405 +# @@protoc_insertion_point(module_scope) diff --git a/examples/5_python_standalone/.gitignore b/examples/5_python_standalone/.gitignore new file mode 100644 index 0000000..f03a7a0 --- /dev/null +++ b/examples/5_python_standalone/.gitignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +*.pyc +*.egg-info/ diff --git a/examples/5_python_standalone/Makefile b/examples/5_python_standalone/Makefile new file mode 100644 index 0000000..1974825 --- /dev/null +++ b/examples/5_python_standalone/Makefile @@ -0,0 +1,31 @@ +.PHONY: setup generate lint run clean + +PYTHON ?= python3 +VENV ?= .venv +VENV_PYTHON := $(VENV)/bin/python +VENV_STAMP := $(VENV)/.installed + +$(VENV_PYTHON): + $(PYTHON) -m venv $(VENV) + +$(VENV_STAMP): pyproject.toml | $(VENV_PYTHON) + $(VENV_PYTHON) -m pip install --upgrade pip + $(VENV_PYTHON) -m pip install -e . + touch $(VENV_STAMP) + +setup: $(VENV_STAMP) + +generate: + easyp --cfg easyp.yaml mod download + easyp --cfg easyp.yaml generate -p . -r . + rm -rf google + +lint: + easyp --cfg easyp.yaml mod download + easyp --cfg easyp.yaml lint -p . -r . + +run: setup + $(VENV_PYTHON) server.py + +clean: + rm -rf .venv google diff --git a/examples/5_python_standalone/README.md b/examples/5_python_standalone/README.md new file mode 100644 index 0000000..1639c8f --- /dev/null +++ b/examples/5_python_standalone/README.md @@ -0,0 +1,60 @@ +# Standalone Python MCP Server + +This example is structured like a user-owned Python project rather than a +repository fixture. It has its own `easyp.yaml`, `pyproject.toml`, virtualenv +target, protobuf contract, generated Python bindings, and stdio server. + +The generated code is imported as a normal local package: + +```python +from proto import notebook_mcp +``` + +There is no `sys.path` bootstrap in `server.py`. Run commands from this +directory so Python resolves the local `proto/` package and the generated +`mcp/` namespace bridge. + +## Generate + +```bash +make generate +``` + +`easyp` downloads `mcp/options/v1/options.proto` through the `deps` section and +generates: + +- `proto/notebook_pb2.py` +- `proto/notebook_mcp.py` +- `proto/__init__.py` +- `mcp/__init__.py` +- `mcp/options/v1/options_pb2.py` + +The checked-in `easyp.yaml` runs the local plugin from this repository so the +example always exercises the current source tree. In an external project, +replace that command with a released version such as: + +```yaml +command: ["go", "run", "github.com/easyp-tech/protoc-gen-mcp/cmd/protoc-gen-mcp@latest"] +``` + +## Run + +```bash +make setup +make run +``` + +The server exposes: + +- `notebook_CreateNote` +- `notebook_SearchNotes` +- `notebook_Health` + +Handlers implement generated dataclasses from `proto/notebook_mcp.py`. Raw +`*_pb2.py` classes stay internal to the generated runtime. + +If you inspect this server in `@modelcontextprotocol/inspector`, note that the +server forwards tool annotations exactly as declared in `proto/notebook.proto`. +Inspector may render omitted hints like `destructiveHint` using its own +defaults, so set those hints explicitly in the proto if you want the UI badges +to be unambiguous. diff --git a/examples/5_python_standalone/easyp.lock b/examples/5_python_standalone/easyp.lock new file mode 100644 index 0000000..13dc44f --- /dev/null +++ b/examples/5_python_standalone/easyp.lock @@ -0,0 +1 @@ +github.com/easyp-tech/protoc-gen-mcp v0.0.0-20260330005019-b9d1262e553d532eaff774a334fce9454b25bbec h1:BLaM59sORcHls9Dv7kVVUCfN1axznUGf8ANAZKMXdPA= diff --git a/examples/5_python_standalone/easyp.yaml b/examples/5_python_standalone/easyp.yaml new file mode 100644 index 0000000..4b7320d --- /dev/null +++ b/examples/5_python_standalone/easyp.yaml @@ -0,0 +1,37 @@ +deps: + - github.com/easyp-tech/protoc-gen-mcp + +lint: + use: + - PACKAGE_DEFINED + - PACKAGE_LOWER_SNAKE_CASE + - PACKAGE_VERSION_SUFFIX + - FILE_LOWER_SNAKE_CASE + - MESSAGE_PASCAL_CASE + - FIELD_LOWER_SNAKE_CASE + - RPC_PASCAL_CASE + - SERVICE_PASCAL_CASE + - SERVICE_SUFFIX + - RPC_REQUEST_RESPONSE_UNIQUE + - RPC_REQUEST_STANDARD_NAME + - RPC_RESPONSE_STANDARD_NAME + - RPC_NO_CLIENT_STREAMING + - RPC_NO_SERVER_STREAMING + + service_suffix: API + +generate: + inputs: + - directory: + path: proto + root: "." + plugins: + - name: python + with_imports: true + out: . + - command: ["go", "run", "../../cmd/protoc-gen-mcp"] + out: . + opts: + paths: source_relative + lang: python + python_runtime: google.protobuf diff --git a/examples/5_python_standalone/mcp/__init__.py b/examples/5_python_standalone/mcp/__init__.py new file mode 100644 index 0000000..813691e --- /dev/null +++ b/examples/5_python_standalone/mcp/__init__.py @@ -0,0 +1,5 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +"""Bridge generated mcp.options.* modules with the official MCP SDK package.""" +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) diff --git a/examples/5_python_standalone/mcp/options/v1/options_pb2.py b/examples/5_python_standalone/mcp/options/v1/options_pb2.py new file mode 100644 index 0000000..fe8a59f --- /dev/null +++ b/examples/5_python_standalone/mcp/options/v1/options_pb2.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: mcp/options/v1/options.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'mcp/options/v1/options.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cmcp/options/v1/options.proto\x12\x0emcp.options.v1\x1a google/protobuf/descriptor.proto\"|\n\x0eServiceOptions\x12\x1c\n\tnamespace\x18\x01 \x01(\tR\tnamespace\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12*\n\x05icons\x18\x03 \x03(\x0b\x32\x14.mcp.options.v1.IconR\x05icons\"\xa2\x02\n\rMethodOptions\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n\x05title\x18\x02 \x01(\tR\x05title\x12 \n\x0b\x64\x65scription\x18\x03 \x01(\tR\x0b\x64\x65scription\x12\x16\n\x06hidden\x18\x04 \x01(\x08R\x06hidden\x12\x41\n\x0b\x61nnotations\x18\n \x01(\x0b\x32\x1f.mcp.options.v1.ToolAnnotationsR\x0b\x61nnotations\x12*\n\x05icons\x18\x0b \x03(\x0b\x32\x14.mcp.options.v1.IconR\x05icons\x12>\n\texecution\x18\x0c \x01(\x0b\x32 .mcp.options.v1.ExecutionOptionsR\texecution\"\x81\x06\n\x0c\x46ieldOptions\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x38\n\x08\x65xamples\x18\x03 \x03(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x08\x65xamples\x12\x41\n\rdefault_value\x18\x04 \x01(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x0c\x64\x65\x66\x61ultValue\x12\x18\n\x07pattern\x18\n \x01(\tR\x07pattern\x12\x16\n\x06\x66ormat\x18\x0b \x01(\tR\x06\x66ormat\x12\"\n\nmin_length\x18\x0c \x01(\rH\x00R\tminLength\x88\x01\x01\x12\"\n\nmax_length\x18\r \x01(\rH\x01R\tmaxLength\x88\x01\x01\x12\x1d\n\x07minimum\x18\x14 \x01(\x01H\x02R\x07minimum\x88\x01\x01\x12\x1d\n\x07maximum\x18\x15 \x01(\x01H\x03R\x07maximum\x88\x01\x01\x12\x30\n\x11\x65xclusive_minimum\x18\x16 \x01(\x01H\x04R\x10\x65xclusiveMinimum\x88\x01\x01\x12\x30\n\x11\x65xclusive_maximum\x18\x17 \x01(\x01H\x05R\x10\x65xclusiveMaximum\x88\x01\x01\x12$\n\x0bmultiple_of\x18\x18 \x01(\x01H\x06R\nmultipleOf\x88\x01\x01\x12 \n\tmin_items\x18\x1e \x01(\rH\x07R\x08minItems\x88\x01\x01\x12 \n\tmax_items\x18\x1f \x01(\rH\x08R\x08maxItems\x88\x01\x01\x12!\n\x0cunique_items\x18 \x01(\x08R\x0buniqueItems\x12\x1b\n\tread_only\x18( \x01(\x08R\x08readOnlyB\r\n\x0b_min_lengthB\r\n\x0b_max_lengthB\n\n\x08_minimumB\n\n\x08_maximumB\x14\n\x12_exclusive_minimumB\x14\n\x12_exclusive_maximumB\x0e\n\x0c_multiple_ofB\x0c\n\n_min_itemsB\x0c\n\n_max_items\"\xce\x02\n\x0c\x45xampleValue\x12#\n\x0cstring_value\x18\x01 \x01(\tH\x00R\x0bstringValue\x12#\n\x0cnumber_value\x18\x02 \x01(\x01H\x00R\x0bnumberValue\x12\x1f\n\nbool_value\x18\x03 \x01(\x08H\x00R\tboolValue\x12\x42\n\x0cobject_value\x18\x04 \x01(\x0b\x32\x1d.mcp.options.v1.ExampleObjectH\x00R\x0bobjectValue\x12?\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32\x1c.mcp.options.v1.ExampleArrayH\x00R\narrayValue\x12\x1f\n\nnull_value\x18\x06 \x01(\x08H\x00R\tnullValue\x12%\n\rinteger_value\x18\x07 \x01(\x03H\x00R\x0cintegerValueB\x06\n\x04kind\"\xbb\x01\n\rExampleObject\x12M\n\nproperties\x18\x01 \x03(\x0b\x32-.mcp.options.v1.ExampleObject.PropertiesEntryR\nproperties\x1a[\n\x0fPropertiesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x05value:\x02\x38\x01\"B\n\x0c\x45xampleArray\x12\x32\n\x05items\x18\x01 \x03(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x05items\"\xfc\x01\n\x0fToolAnnotations\x12$\n\x0eread_only_hint\x18\x01 \x01(\x08R\x0creadOnlyHint\x12.\n\x10\x64\x65structive_hint\x18\x02 \x01(\x08H\x00R\x0f\x64\x65structiveHint\x88\x01\x01\x12\'\n\x0fidempotent_hint\x18\x03 \x01(\x08R\x0eidempotentHint\x12+\n\x0fopen_world_hint\x18\x04 \x01(\x08H\x01R\ropenWorldHint\x88\x01\x01\x12\x14\n\x05title\x18\x05 \x01(\tR\x05titleB\x13\n\x11_destructive_hintB\x12\n\x10_open_world_hint\"a\n\x04Icon\x12\x10\n\x03src\x18\x01 \x01(\tR\x03src\x12\x1b\n\tmime_type\x18\x02 \x01(\tR\x08mimeType\x12\x14\n\x05sizes\x18\x03 \x03(\tR\x05sizes\x12\x14\n\x05theme\x18\x04 \x01(\tR\x05theme\"R\n\x10\x45xecutionOptions\x12>\n\x0ctask_support\x18\x01 \x01(\x0e\x32\x1b.mcp.options.v1.TaskSupportR\x0btaskSupport\"L\n\x0cOneofOptions\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08required\x18\x02 \x01(\x08R\x08required\"\x83\x01\n\x0eMessageOptions\x12\x14\n\x05title\x18\x01 \x01(\tR\x05title\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x39\n\x08\x65xamples\x18\x03 \x03(\x0b\x32\x1d.mcp.options.v1.ExampleObjectR\x08\x65xamples\"E\n\x0b\x45numOptions\x12\x14\n\x05title\x18\x01 \x01(\tR\x05title\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\"L\n\x10\x45numValueOptions\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x16\n\x06hidden\x18\x02 \x01(\x08R\x06hidden*Z\n\x0bTaskSupport\x12\x15\n\x11TASK_SUPPORT_NONE\x10\x00\x12\x19\n\x15TASK_SUPPORT_OPTIONAL\x10\x01\x12\x19\n\x15TASK_SUPPORT_REQUIRED\x10\x02:[\n\x07service\x12\x1f.google.protobuf.ServiceOptions\x18\xf9\xc6\x05 \x01(\x0b\x32\x1e.mcp.options.v1.ServiceOptionsR\x07service:W\n\x06method\x12\x1e.google.protobuf.MethodOptions\x18\xfa\xc6\x05 \x01(\x0b\x32\x1d.mcp.options.v1.MethodOptionsR\x06method:S\n\x05\x66ield\x12\x1d.google.protobuf.FieldOptions\x18\xfb\xc6\x05 \x01(\x0b\x32\x1c.mcp.options.v1.FieldOptionsR\x05\x66ield:[\n\x07message\x12\x1f.google.protobuf.MessageOptions\x18\xfc\xc6\x05 \x01(\x0b\x32\x1e.mcp.options.v1.MessageOptionsR\x07message:O\n\x04\x65num\x12\x1c.google.protobuf.EnumOptions\x18\xfd\xc6\x05 \x01(\x0b\x32\x1b.mcp.options.v1.EnumOptionsR\x04\x65num:d\n\nenum_value\x12!.google.protobuf.EnumValueOptions\x18\xfe\xc6\x05 \x01(\x0b\x32 .mcp.options.v1.EnumValueOptionsR\tenumValue:S\n\x05oneof\x12\x1d.google.protobuf.OneofOptions\x18\xff\xc6\x05 \x01(\x0b\x32\x1c.mcp.options.v1.OneofOptionsR\x05oneofB?Z=github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1;optionsv1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcp.options.v1.options_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1;optionsv1' + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._loaded_options = None + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._serialized_options = b'8\001' + _globals['_TASKSUPPORT']._serialized_start=2667 + _globals['_TASKSUPPORT']._serialized_end=2757 + _globals['_SERVICEOPTIONS']._serialized_start=82 + _globals['_SERVICEOPTIONS']._serialized_end=206 + _globals['_METHODOPTIONS']._serialized_start=209 + _globals['_METHODOPTIONS']._serialized_end=499 + _globals['_FIELDOPTIONS']._serialized_start=502 + _globals['_FIELDOPTIONS']._serialized_end=1271 + _globals['_EXAMPLEVALUE']._serialized_start=1274 + _globals['_EXAMPLEVALUE']._serialized_end=1608 + _globals['_EXAMPLEOBJECT']._serialized_start=1611 + _globals['_EXAMPLEOBJECT']._serialized_end=1798 + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._serialized_start=1707 + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._serialized_end=1798 + _globals['_EXAMPLEARRAY']._serialized_start=1800 + _globals['_EXAMPLEARRAY']._serialized_end=1866 + _globals['_TOOLANNOTATIONS']._serialized_start=1869 + _globals['_TOOLANNOTATIONS']._serialized_end=2121 + _globals['_ICON']._serialized_start=2123 + _globals['_ICON']._serialized_end=2220 + _globals['_EXECUTIONOPTIONS']._serialized_start=2222 + _globals['_EXECUTIONOPTIONS']._serialized_end=2304 + _globals['_ONEOFOPTIONS']._serialized_start=2306 + _globals['_ONEOFOPTIONS']._serialized_end=2382 + _globals['_MESSAGEOPTIONS']._serialized_start=2385 + _globals['_MESSAGEOPTIONS']._serialized_end=2516 + _globals['_ENUMOPTIONS']._serialized_start=2518 + _globals['_ENUMOPTIONS']._serialized_end=2587 + _globals['_ENUMVALUEOPTIONS']._serialized_start=2589 + _globals['_ENUMVALUEOPTIONS']._serialized_end=2665 +# @@protoc_insertion_point(module_scope) diff --git a/examples/5_python_standalone/proto/__init__.py b/examples/5_python_standalone/proto/__init__.py new file mode 100644 index 0000000..6e8ced9 --- /dev/null +++ b/examples/5_python_standalone/proto/__init__.py @@ -0,0 +1 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. diff --git a/examples/5_python_standalone/proto/notebook.proto b/examples/5_python_standalone/proto/notebook.proto new file mode 100644 index 0000000..b7f4437 --- /dev/null +++ b/examples/5_python_standalone/proto/notebook.proto @@ -0,0 +1,158 @@ +syntax = "proto3"; + +package notebook.v1; + +import "google/protobuf/timestamp.proto"; +import "mcp/options/v1/options.proto"; + +service NotebookAPI { + option (mcp.options.v1.service) = { + namespace: "notebook" + description: "A small in-memory notebook service for generated Python MCP bindings." + }; + + rpc CreateNote(CreateNoteRequest) returns (CreateNoteResponse) { + option (mcp.options.v1.method) = { + title: "Create note" + description: "Create a note in the local in-memory notebook." + annotations: { + destructive_hint: false + idempotent_hint: false + open_world_hint: false + } + }; + } + + rpc SearchNotes(SearchNotesRequest) returns (SearchNotesResponse) { + option (mcp.options.v1.method) = { + title: "Search notes" + description: "Search notes by text and tags." + annotations: { + read_only_hint: true + open_world_hint: false + } + }; + } + + rpc Health(HealthRequest) returns (HealthResponse) { + option (mcp.options.v1.method) = { + title: "Notebook health" + description: "Verify that the notebook MCP server is alive." + annotations: { + read_only_hint: true + open_world_hint: false + } + }; + } +} + +message Note { + string id = 1 [(mcp.options.v1.field) = { + read_only: true + description: "Server-assigned note identifier." + }]; + + string title = 2 [(mcp.options.v1.field) = { + description: "Short human-readable note title." + min_length: 1 + max_length: 80 + examples: [{ string_value: "Ship Python support" }] + }]; + + string body = 3 [(mcp.options.v1.field) = { + description: "Note body in plain text." + min_length: 1 + max_length: 2000 + examples: [{ string_value: "Verify that generated Python MCP bindings are pleasant to use." }] + }]; + + repeated string tags = 4 [(mcp.options.v1.field) = { + description: "Optional tags used for filtering." + unique_items: true + examples: [ + { array_value: { items: [{ string_value: "python" }, { string_value: "mcp" }] } } + ] + }]; + + optional string due_date = 5 [(mcp.options.v1.field) = { + description: "Optional due date in ISO 8601 date format." + format: "date" + examples: [{ string_value: "2026-04-30" }] + }]; + + google.protobuf.Timestamp created_at = 6 [(mcp.options.v1.field) = { + read_only: true + description: "Server-assigned creation timestamp." + }]; +} + +message CreateNoteRequest { + string title = 1 [(mcp.options.v1.field) = { + description: "Short human-readable note title." + min_length: 1 + max_length: 80 + examples: [{ string_value: "Ship Python support" }] + }]; + + string body = 2 [(mcp.options.v1.field) = { + description: "Note body in plain text." + min_length: 1 + max_length: 2000 + examples: [{ string_value: "Verify that generated Python MCP bindings are pleasant to use." }] + }]; + + repeated string tags = 3 [(mcp.options.v1.field) = { + description: "Optional tags used for filtering." + unique_items: true + examples: [ + { array_value: { items: [{ string_value: "python" }, { string_value: "mcp" }] } } + ] + }]; + + optional string due_date = 4 [(mcp.options.v1.field) = { + description: "Optional due date in ISO 8601 date format." + format: "date" + examples: [{ string_value: "2026-04-30" }] + }]; +} + +message CreateNoteResponse { + Note note = 1; +} + +message SearchNotesRequest { + optional string query = 1 [(mcp.options.v1.field) = { + description: "Optional case-insensitive text query matched against title and body." + min_length: 1 + examples: [{ string_value: "python" }] + }]; + + repeated string tags = 2 [(mcp.options.v1.field) = { + description: "Optional tags; a note must have every requested tag." + unique_items: true + examples: [ + { array_value: { items: [{ string_value: "mcp" }] } } + ] + }]; + + optional int32 limit = 3 [(mcp.options.v1.field) = { + description: "Maximum number of notes to return." + minimum: 1.0 + maximum: 50.0 + default_value: { integer_value: 10 } + }]; +} + +message SearchNotesResponse { + repeated Note notes = 1; +} + +message HealthRequest {} + +message HealthResponse { + bool ok = 1; + int32 note_count = 2 [(mcp.options.v1.field) = { + read_only: true + description: "Current number of notes in memory." + }]; +} diff --git a/examples/5_python_standalone/proto/notebook_mcp.py b/examples/5_python_standalone/proto/notebook_mcp.py new file mode 100644 index 0000000..2a4f8ab --- /dev/null +++ b/examples/5_python_standalone/proto/notebook_mcp.py @@ -0,0 +1,588 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +# source: proto/notebook.proto +from __future__ import annotations + +import enum +import inspect +import json +import weakref +from dataclasses import dataclass, field +from typing import Any, Awaitable, Protocol, TypeAlias + +import jsonschema +import mcp.server.lowlevel +import mcp.server.session +import mcp.shared.exceptions +import mcp.types +from google.protobuf import any_pb2, duration_pb2, empty_pb2, field_mask_pb2, json_format, struct_pb2, timestamp_pb2, wrappers_pb2 +try: + from . import notebook_pb2 +except ImportError: + import notebook_pb2 + +ToolRequestContext = mcp.server.session.ServerSession + +class _UnsetType: + __slots__ = () + + def __repr__(self) -> str: + return "UNSET" + +UNSET = _UnsetType() + +JSONValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None +ProtoAny: TypeAlias = dict[str, Any] +Timestamp: TypeAlias = str +Duration: TypeAlias = str +FieldMask: TypeAlias = str +Struct: TypeAlias = dict[str, Any] +Value: TypeAlias = JSONValue +ListValue: TypeAlias = list[Any] +BoolValue: TypeAlias = bool +StringValue: TypeAlias = str +BytesValue: TypeAlias = bytes +Int32Value: TypeAlias = int +UInt32Value: TypeAlias = int +Int64Value: TypeAlias = int +UInt64Value: TypeAlias = int +FloatValue: TypeAlias = float +DoubleValue: TypeAlias = float + +@dataclass(slots=True) +class Empty: + pass + +@dataclass(slots=True) +class CreateNoteRequest: + title: str + body: str + tags: list[str] = field(default_factory=list) + due_date: str | _UnsetType = UNSET + +@dataclass(slots=True) +class Note: + id: str + title: str + body: str + created_at: Timestamp + tags: list[str] = field(default_factory=list) + due_date: str | _UnsetType = UNSET + +@dataclass(slots=True) +class CreateNoteResponse: + note: Note + +@dataclass(slots=True) +class SearchNotesRequest: + query: str | _UnsetType = UNSET + tags: list[str] = field(default_factory=list) + limit: int | _UnsetType = UNSET + +@dataclass(slots=True) +class SearchNotesResponse: + notes: list[Note] = field(default_factory=list) + +@dataclass(slots=True) +class HealthRequest: + pass + +@dataclass(slots=True) +class HealthResponse: + ok: bool + note_count: int + +@dataclass(frozen=True) +class _RegisteredTool: + name: str + title: str + description: str + input_schema_json: str + output_schema_json: str + request_type: type[Any] + response_type: type[Any] + from_pb: Any + to_pb: Any + handler: Any + annotations: dict[str, Any] | None + icons: list[dict[str, Any]] | None + +class _ServerToolRegistry: + def __init__(self, server: mcp.server.lowlevel.Server) -> None: + self.server = server + self.tools: dict[str, _RegisteredTool] = {} + self.reserved_tool_names = _get_reserved_tool_names(server) + self.handlers_installed = False + self.previous_list_tools: Any | None = None + self.previous_call_tool: Any | None = None + + def add_tool(self, tool: _RegisteredTool) -> None: + if tool.name in self.reserved_tool_names: + raise ValueError(f"duplicate tool registration: {tool.name}") + self.tools[tool.name] = tool + self.reserved_tool_names.add(tool.name) + + def list_tools(self) -> list[mcp.types.Tool]: + return [_build_tool(tool) for tool in self.tools.values()] + + async def call_tool(self, name: str, arguments: dict[str, Any] | None, context: ToolRequestContext | None = None) -> mcp.types.CallToolResult: + return await _dispatch_call(self, name, arguments or {}, context) + +_SERVER_REGISTRIES: weakref.WeakKeyDictionary[mcp.server.lowlevel.Server, _ServerToolRegistry] = weakref.WeakKeyDictionary() + +def _load_schema(schema_json: str) -> dict[str, Any]: + return json.loads(schema_json) + +def _protojson_to_message(arguments: dict[str, Any], message: Any) -> Any: + json_format.ParseDict(arguments, message) + return message + +def _normalize_tool_segment(segment: str | None) -> str: + if segment is None: + return "" + parts = [part for part in segment.strip().replace(".", "_").split("_") if part] + return "_".join(parts) + +def _normalize_namespace(namespace: str | None, default: str) -> str: + if namespace is None: + namespace = default + return _normalize_tool_segment(namespace) + +def _tool_name(namespace: str, method_name: str) -> str: + namespace = _normalize_tool_segment(namespace) + method_name = _normalize_tool_segment(method_name) + if namespace == '': + return method_name + if method_name == '': + return namespace + return f"{namespace}_{method_name}" + +def _get_registry(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _SERVER_REGISTRIES.get(server) + if registry is None: + registry = _ServerToolRegistry(server) + _SERVER_REGISTRIES[server] = registry + return registry + +_RESERVED_TOOL_NAMES_ATTR = "_protoc_gen_mcp_reserved_tool_names" + +def _get_reserved_tool_names(server: mcp.server.lowlevel.Server) -> set[str]: + reserved = getattr(server, _RESERVED_TOOL_NAMES_ATTR, None) + if reserved is None: + reserved = set() + setattr(server, _RESERVED_TOOL_NAMES_ATTR, reserved) + return reserved + +def _build_list_tools_request(request: Any) -> mcp.types.ListToolsRequest: + if isinstance(request, mcp.types.ListToolsRequest): + return request + return mcp.types.ListToolsRequest() + +def _merge_tools(previous: list[mcp.types.Tool], current: list[mcp.types.Tool]) -> list[mcp.types.Tool]: + merged: list[mcp.types.Tool] = [] + names: set[str] = set() + for tool in previous: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + for tool in current: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + return merged + +def _install_server_handlers(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _get_registry(server) + if registry.handlers_installed: + return registry + registry.previous_list_tools = server.request_handlers.get(mcp.types.ListToolsRequest) + registry.previous_call_tool = server.request_handlers.get(mcp.types.CallToolRequest) + + async def _list_tools(request: Any) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + list_request = _build_list_tools_request(request) + previous_tools: list[mcp.types.Tool] = [] + meta: dict[str, Any] | None = None + if current is not None: + if current.previous_list_tools is not None: + previous_result = await current.previous_list_tools(list_request) + if not isinstance(previous_result.root, mcp.types.ListToolsResult): + return previous_result + if getattr(list_request.params, "cursor", None) is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + if previous_result.root.nextCursor is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + meta = getattr(previous_result.root, "meta", None) + previous_tools = list(previous_result.root.tools) + current_tools = [] if current is None else current.list_tools() + tools = _merge_tools(previous_tools, current_tools) + server._tool_cache.clear() + for tool in tools: + server._tool_cache[tool.name] = tool + return mcp.types.ServerResult(mcp.types.ListToolsResult(tools=tools, _meta=meta)) + + async def _call_tool(request: mcp.types.CallToolRequest) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + if current is None: + return mcp.types.ServerResult(_tool_error_result("tool registry is not installed")) + if request.params.name not in current.tools: + if current.previous_call_tool is not None: + return await current.previous_call_tool(request) + _invalid_params_error(request.params.name, "unknown tool") + if current.previous_call_tool is not None: + await server.request_handlers[mcp.types.ListToolsRequest](mcp.types.ListToolsRequest()) + result = await current.call_tool(request.params.name, request.params.arguments or {}, server.request_context.session) + return mcp.types.ServerResult(result) + + server.request_handlers[mcp.types.ListToolsRequest] = _list_tools + server.request_handlers[mcp.types.CallToolRequest] = _call_tool + + registry.handlers_installed = True + return registry + +def _tool_annotations(raw: dict[str, Any] | None) -> mcp.types.ToolAnnotations | None: + if raw is None: + return None + return mcp.types.ToolAnnotations(**raw) + +def _tool_error_result(message: str) -> mcp.types.CallToolResult: + return mcp.types.CallToolResult( + content=[mcp.types.TextContent(type="text", text=message)], + isError=True, + ) + +def _invalid_params_error(tool_name: str, error: Any) -> mcp.shared.exceptions.McpError: + raise mcp.shared.exceptions.McpError( + mcp.types.ErrorData( + code=mcp.types.INVALID_PARAMS, + message=f"invalid arguments for tool {tool_name!r}: {error}", + ) + ) + +def _build_tool(tool: _RegisteredTool) -> Any: + return mcp.types.Tool( + name=tool.name, + title=tool.title, + description=tool.description, + inputSchema=_load_schema(tool.input_schema_json), + outputSchema=_load_schema(tool.output_schema_json), + annotations=_tool_annotations(tool.annotations), + icons=tool.icons, + ) + +async def _maybe_await(result: Any) -> Any: + if inspect.isawaitable(result): + return await result + return result + +def _message_to_json(message: Any) -> str: + kwargs = {"preserving_proto_field_name": False} + try: + return json_format.MessageToJson(message, always_print_fields_with_no_presence=True, **kwargs) + except TypeError: + return json_format.MessageToJson(message, including_default_value_fields=True, **kwargs) + +async def _dispatch_call(registry: _ServerToolRegistry, name: str, arguments: dict[str, Any], context: ToolRequestContext | None) -> mcp.types.CallToolResult: + tool = registry.tools.get(name) + if tool is None: + _invalid_params_error(name, "unknown tool") + try: + jsonschema.validate(arguments, _load_schema(tool.input_schema_json)) + except jsonschema.ValidationError as error: + _invalid_params_error(name, error) + try: + request_pb = _protojson_to_message(arguments, tool.request_type()) + request_dc = tool.from_pb(request_pb) + except Exception as error: + _invalid_params_error(name, error) + try: + response_dc = await _maybe_await(tool.handler(context, request_dc)) + except Exception as error: + return _tool_error_result(str(error)) + try: + if response_dc is None: + response_pb = tool.response_type() + else: + response_pb = tool.to_pb(response_dc) + payload = json.loads(_message_to_json(response_pb)) + except Exception as error: + raise RuntimeError(f"mcpruntime: marshal output for tool {name!r}: {error}") from error + text = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + content = [mcp.types.TextContent(type="text", text=text)] + try: + jsonschema.validate(payload, _load_schema(tool.output_schema_json)) + except jsonschema.ValidationError as error: + raise RuntimeError(f"mcpruntime: validate output for tool {name!r}: {error}") from error + return mcp.types.CallToolResult(content=content, structuredContent=payload) + +def _enum_from_pb(enum_type: type[enum.IntEnum], value: int) -> enum.IntEnum: + return enum_type(value) + +def _json_to_message(value: Any, message: Any) -> Any: + json_format.Parse(json.dumps(value), message) + return message + +def _from_pb_any(message: any_pb2.Any) -> ProtoAny: + return json.loads(_message_to_json(message)) + +def _to_pb_any(value: ProtoAny) -> any_pb2.Any: + return _json_to_message(value, any_pb2.Any()) + +def _from_pb_timestamp(message: timestamp_pb2.Timestamp) -> Timestamp: + return json.loads(_message_to_json(message)) + +def _to_pb_timestamp(value: Timestamp) -> timestamp_pb2.Timestamp: + return _json_to_message(value, timestamp_pb2.Timestamp()) + +def _from_pb_duration(message: duration_pb2.Duration) -> Duration: + return json.loads(_message_to_json(message)) + +def _to_pb_duration(value: Duration) -> duration_pb2.Duration: + return _json_to_message(value, duration_pb2.Duration()) + +def _from_pb_field_mask(message: field_mask_pb2.FieldMask) -> FieldMask: + return json.loads(_message_to_json(message)) + +def _to_pb_field_mask(value: FieldMask) -> field_mask_pb2.FieldMask: + return _json_to_message(value, field_mask_pb2.FieldMask()) + +def _from_pb_struct(message: struct_pb2.Struct) -> Struct: + return json.loads(_message_to_json(message)) + +def _to_pb_struct(value: Struct) -> struct_pb2.Struct: + return _json_to_message(value, struct_pb2.Struct()) + +def _from_pb_list_value(message: struct_pb2.ListValue) -> ListValue: + return json.loads(_message_to_json(message)) + +def _to_pb_list_value(value: ListValue) -> struct_pb2.ListValue: + return _json_to_message(value, struct_pb2.ListValue()) + +def _from_pb_value(message: struct_pb2.Value) -> Value: + return json.loads(_message_to_json(message)) + +def _to_pb_value(value: Value) -> struct_pb2.Value: + return _json_to_message(value, struct_pb2.Value()) + +def _from_pb_empty(message: empty_pb2.Empty) -> Empty: + return Empty() + +def _to_pb_empty(value: Empty) -> empty_pb2.Empty: + return empty_pb2.Empty() + +def _from_pb_bool_value(message: wrappers_pb2.BoolValue) -> BoolValue: + return message.value + +def _to_pb_bool_value(value: BoolValue) -> wrappers_pb2.BoolValue: + return wrappers_pb2.BoolValue(value=value) + +def _from_pb_string_value(message: wrappers_pb2.StringValue) -> StringValue: + return message.value + +def _to_pb_string_value(value: StringValue) -> wrappers_pb2.StringValue: + return wrappers_pb2.StringValue(value=value) + +def _from_pb_bytes_value(message: wrappers_pb2.BytesValue) -> BytesValue: + return message.value + +def _to_pb_bytes_value(value: BytesValue) -> wrappers_pb2.BytesValue: + return wrappers_pb2.BytesValue(value=value) + +def _from_pb_int32_value(message: wrappers_pb2.Int32Value) -> Int32Value: + return message.value + +def _to_pb_int32_value(value: Int32Value) -> wrappers_pb2.Int32Value: + return wrappers_pb2.Int32Value(value=value) + +def _from_pb_uint32_value(message: wrappers_pb2.UInt32Value) -> UInt32Value: + return message.value + +def _to_pb_uint32_value(value: UInt32Value) -> wrappers_pb2.UInt32Value: + return wrappers_pb2.UInt32Value(value=value) + +def _from_pb_int64_value(message: wrappers_pb2.Int64Value) -> Int64Value: + return message.value + +def _to_pb_int64_value(value: Int64Value) -> wrappers_pb2.Int64Value: + return wrappers_pb2.Int64Value(value=value) + +def _from_pb_uint64_value(message: wrappers_pb2.UInt64Value) -> UInt64Value: + return message.value + +def _to_pb_uint64_value(value: UInt64Value) -> wrappers_pb2.UInt64Value: + return wrappers_pb2.UInt64Value(value=value) + +def _from_pb_float_value(message: wrappers_pb2.FloatValue) -> FloatValue: + return message.value + +def _to_pb_float_value(value: FloatValue) -> wrappers_pb2.FloatValue: + return wrappers_pb2.FloatValue(value=value) + +def _from_pb_double_value(message: wrappers_pb2.DoubleValue) -> DoubleValue: + return message.value + +def _to_pb_double_value(value: DoubleValue) -> wrappers_pb2.DoubleValue: + return wrappers_pb2.DoubleValue(value=value) + +def _from_pb_create_note_request(message: notebook_pb2.CreateNoteRequest) -> CreateNoteRequest: + return CreateNoteRequest( + title=message.title, + body=message.body, + tags=list(message.tags), + due_date=message.due_date if message.HasField("due_date") else UNSET, + ) + +def _to_pb_create_note_request(value: CreateNoteRequest) -> notebook_pb2.CreateNoteRequest: + message = notebook_pb2.CreateNoteRequest() + message.title = value.title + message.body = value.body + message.tags.extend(value.tags) + if value.due_date is not UNSET: + message.due_date = value.due_date + return message + +def _from_pb_note(message: notebook_pb2.Note) -> Note: + return Note( + id=message.id, + title=message.title, + body=message.body, + tags=list(message.tags), + due_date=message.due_date if message.HasField("due_date") else UNSET, + created_at=_from_pb_timestamp(message.created_at), + ) + +def _to_pb_note(value: Note) -> notebook_pb2.Note: + message = notebook_pb2.Note() + message.id = value.id + message.title = value.title + message.body = value.body + message.tags.extend(value.tags) + if value.due_date is not UNSET: + message.due_date = value.due_date + message.created_at.CopyFrom(_to_pb_timestamp(value.created_at)) + return message + +def _from_pb_create_note_response(message: notebook_pb2.CreateNoteResponse) -> CreateNoteResponse: + return CreateNoteResponse( + note=_from_pb_note(message.note), + ) + +def _to_pb_create_note_response(value: CreateNoteResponse) -> notebook_pb2.CreateNoteResponse: + message = notebook_pb2.CreateNoteResponse() + message.note.CopyFrom(_to_pb_note(value.note)) + return message + +def _from_pb_search_notes_request(message: notebook_pb2.SearchNotesRequest) -> SearchNotesRequest: + return SearchNotesRequest( + query=message.query if message.HasField("query") else UNSET, + tags=list(message.tags), + limit=message.limit if message.HasField("limit") else UNSET, + ) + +def _to_pb_search_notes_request(value: SearchNotesRequest) -> notebook_pb2.SearchNotesRequest: + message = notebook_pb2.SearchNotesRequest() + if value.query is not UNSET: + message.query = value.query + message.tags.extend(value.tags) + if value.limit is not UNSET: + message.limit = value.limit + return message + +def _from_pb_search_notes_response(message: notebook_pb2.SearchNotesResponse) -> SearchNotesResponse: + return SearchNotesResponse( + notes=[_from_pb_note(item) for item in message.notes], + ) + +def _to_pb_search_notes_response(value: SearchNotesResponse) -> notebook_pb2.SearchNotesResponse: + message = notebook_pb2.SearchNotesResponse() + message.notes.extend(_to_pb_note(item) for item in value.notes) + return message + +def _from_pb_health_request(message: notebook_pb2.HealthRequest) -> HealthRequest: + return HealthRequest( + ) + +def _to_pb_health_request(value: HealthRequest) -> notebook_pb2.HealthRequest: + message = notebook_pb2.HealthRequest() + return message + +def _from_pb_health_response(message: notebook_pb2.HealthResponse) -> HealthResponse: + return HealthResponse( + ok=message.ok, + note_count=message.note_count, + ) + +def _to_pb_health_response(value: HealthResponse) -> notebook_pb2.HealthResponse: + message = notebook_pb2.HealthResponse() + message.ok = value.ok + message.note_count = value.note_count + return message + +class NotebookAPIToolHandler(Protocol): + def create_note(self, ctx: ToolRequestContext, req: CreateNoteRequest) -> CreateNoteResponse | Awaitable[CreateNoteResponse]: + ... + def search_notes(self, ctx: ToolRequestContext, req: SearchNotesRequest) -> SearchNotesResponse | Awaitable[SearchNotesResponse]: + ... + def health(self, ctx: ToolRequestContext, req: HealthRequest) -> HealthResponse | Awaitable[HealthResponse]: + ... + +def register_notebook_api_tools(server: mcp.server.lowlevel.Server, impl: NotebookAPIToolHandler, *, namespace: str | None = None) -> None: + if impl is None: + raise ValueError("register_notebook_api_tools: impl is nil") + registry = _install_server_handlers(server) + resolved_namespace = _normalize_namespace(namespace, "notebook") + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "CreateNote"), + title="Create note", + description="Create a note in the local in-memory notebook.", + input_schema_json=NOTEBOOK_API_CREATE_NOTE_INPUT_SCHEMA_JSON, + output_schema_json=NOTEBOOK_API_CREATE_NOTE_OUTPUT_SCHEMA_JSON, + request_type=notebook_pb2.CreateNoteRequest, + response_type=notebook_pb2.CreateNoteResponse, + from_pb=_from_pb_create_note_request, + to_pb=_to_pb_create_note_response, + handler=impl.create_note, + annotations={"destructiveHint": False, "openWorldHint": False}, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "SearchNotes"), + title="Search notes", + description="Search notes by text and tags.", + input_schema_json=NOTEBOOK_API_SEARCH_NOTES_INPUT_SCHEMA_JSON, + output_schema_json=NOTEBOOK_API_SEARCH_NOTES_OUTPUT_SCHEMA_JSON, + request_type=notebook_pb2.SearchNotesRequest, + response_type=notebook_pb2.SearchNotesResponse, + from_pb=_from_pb_search_notes_request, + to_pb=_to_pb_search_notes_response, + handler=impl.search_notes, + annotations={"readOnlyHint": True, "openWorldHint": False}, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "Health"), + title="Notebook health", + description="Verify that the notebook MCP server is alive.", + input_schema_json=NOTEBOOK_API_HEALTH_INPUT_SCHEMA_JSON, + output_schema_json=NOTEBOOK_API_HEALTH_OUTPUT_SCHEMA_JSON, + request_type=notebook_pb2.HealthRequest, + response_type=notebook_pb2.HealthResponse, + from_pb=_from_pb_health_request, + to_pb=_to_pb_health_response, + handler=impl.health, + annotations={"readOnlyHint": True, "openWorldHint": False}, + icons=None, + )) + +NOTEBOOK_API_CREATE_NOTE_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"body\":{\"type\":\"string\",\"description\":\"Note body in plain text.\",\"examples\":[\"Verify that generated Python MCP bindings are pleasant to use.\"],\"minLength\":1,\"maxLength\":2000},\"dueDate\":{\"type\":[\"string\",\"null\"],\"description\":\"Optional due date in ISO 8601 date format.\",\"examples\":[\"2026-04-30\"],\"format\":\"date\"},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"Optional tags used for filtering.\",\"examples\":[\"example\"]},\"description\":\"Optional tags used for filtering.\",\"examples\":[[\"python\",\"mcp\"]],\"uniqueItems\":true},\"title\":{\"type\":\"string\",\"description\":\"Short human-readable note title.\",\"examples\":[\"Ship Python support\"],\"minLength\":1,\"maxLength\":80}},\"examples\":[{\"body\":\"example\",\"dueDate\":\"example\",\"tags\":[\"example\"],\"title\":\"example\"}],\"required\":[\"title\",\"body\"],\"additionalProperties\":false}" + +NOTEBOOK_API_CREATE_NOTE_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"note\":{\"type\":\"object\",\"properties\":{\"body\":{\"type\":\"string\",\"description\":\"Note body in plain text.\",\"examples\":[\"Verify that generated Python MCP bindings are pleasant to use.\"],\"minLength\":1,\"maxLength\":2000},\"createdAt\":{\"type\":\"string\",\"description\":\"Server-assigned creation timestamp.\",\"readOnly\":true,\"examples\":[\"2026-03-09T10:11:12Z\"],\"format\":\"date-time\"},\"dueDate\":{\"type\":[\"string\",\"null\"],\"description\":\"Optional due date in ISO 8601 date format.\",\"examples\":[\"2026-04-30\"],\"format\":\"date\"},\"id\":{\"type\":\"string\",\"description\":\"Server-assigned note identifier.\",\"readOnly\":true,\"examples\":[\"example\"]},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"Optional tags used for filtering.\",\"examples\":[\"example\"]},\"description\":\"Optional tags used for filtering.\",\"examples\":[[\"python\",\"mcp\"]],\"uniqueItems\":true},\"title\":{\"type\":\"string\",\"description\":\"Short human-readable note title.\",\"examples\":[\"Ship Python support\"],\"minLength\":1,\"maxLength\":80}},\"examples\":[{\"body\":\"example\",\"createdAt\":\"2026-03-09T10:11:12Z\",\"dueDate\":\"example\",\"id\":\"example\",\"tags\":[\"example\"],\"title\":\"example\"}],\"required\":[\"id\",\"title\",\"body\",\"createdAt\"],\"additionalProperties\":false}},\"examples\":[{\"note\":{\"body\":\"example\",\"createdAt\":\"2026-03-09T10:11:12Z\",\"dueDate\":\"example\",\"id\":\"example\",\"tags\":[\"example\"],\"title\":\"example\"}}],\"required\":[\"note\"],\"additionalProperties\":false}" + +NOTEBOOK_API_SEARCH_NOTES_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"limit\":{\"type\":[\"integer\",\"null\"],\"description\":\"Maximum number of notes to return.\",\"examples\":[-1],\"minimum\":1,\"maximum\":50},\"query\":{\"type\":[\"string\",\"null\"],\"description\":\"Optional case-insensitive text query matched against title and body.\",\"examples\":[\"python\"],\"minLength\":1},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"Optional tags; a note must have every requested tag.\",\"examples\":[\"example\"]},\"description\":\"Optional tags; a note must have every requested tag.\",\"examples\":[[\"mcp\"]],\"uniqueItems\":true}},\"examples\":[{\"limit\":-1,\"query\":\"example\",\"tags\":[\"example\"]}],\"additionalProperties\":false}" + +NOTEBOOK_API_SEARCH_NOTES_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"notes\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"object\",\"properties\":{\"body\":{\"type\":\"string\",\"description\":\"Note body in plain text.\",\"examples\":[\"Verify that generated Python MCP bindings are pleasant to use.\"],\"minLength\":1,\"maxLength\":2000},\"createdAt\":{\"type\":\"string\",\"description\":\"Server-assigned creation timestamp.\",\"readOnly\":true,\"examples\":[\"2026-03-09T10:11:12Z\"],\"format\":\"date-time\"},\"dueDate\":{\"type\":[\"string\",\"null\"],\"description\":\"Optional due date in ISO 8601 date format.\",\"examples\":[\"2026-04-30\"],\"format\":\"date\"},\"id\":{\"type\":\"string\",\"description\":\"Server-assigned note identifier.\",\"readOnly\":true,\"examples\":[\"example\"]},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"Optional tags used for filtering.\",\"examples\":[\"example\"]},\"description\":\"Optional tags used for filtering.\",\"examples\":[[\"python\",\"mcp\"]],\"uniqueItems\":true},\"title\":{\"type\":\"string\",\"description\":\"Short human-readable note title.\",\"examples\":[\"Ship Python support\"],\"minLength\":1,\"maxLength\":80}},\"examples\":[{\"body\":\"example\",\"createdAt\":\"2026-03-09T10:11:12Z\",\"dueDate\":\"example\",\"id\":\"example\",\"tags\":[\"example\"],\"title\":\"example\"}],\"required\":[\"id\",\"title\",\"body\",\"createdAt\"],\"additionalProperties\":false},\"examples\":[[{\"body\":\"example\",\"createdAt\":\"2026-03-09T10:11:12Z\",\"dueDate\":\"example\",\"id\":\"example\",\"tags\":[\"example\"],\"title\":\"example\"}]]}},\"examples\":[{\"notes\":[{\"body\":\"example\",\"createdAt\":\"2026-03-09T10:11:12Z\",\"dueDate\":\"example\",\"id\":\"example\",\"tags\":[\"example\"],\"title\":\"example\"}]}],\"additionalProperties\":false}" + +NOTEBOOK_API_HEALTH_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"additionalProperties\":false}" + +NOTEBOOK_API_HEALTH_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"noteCount\":{\"type\":\"integer\",\"description\":\"Current number of notes in memory.\",\"readOnly\":true,\"examples\":[-1]},\"ok\":{\"type\":\"boolean\",\"examples\":[true]}},\"examples\":[{\"noteCount\":-1,\"ok\":true}],\"required\":[\"ok\",\"noteCount\"],\"additionalProperties\":false}" diff --git a/examples/5_python_standalone/proto/notebook_pb2.py b/examples/5_python_standalone/proto/notebook_pb2.py new file mode 100644 index 0000000..f2219a6 --- /dev/null +++ b/examples/5_python_standalone/proto/notebook_pb2.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: proto/notebook.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'proto/notebook.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from mcp.options.v1 import options_pb2 as mcp_dot_options_dot_v1_dot_options__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14proto/notebook.proto\x12\x0bnotebook.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cmcp/options/v1/options.proto\"\xc6\x04\n\x04Note\x12\x39\n\x02id\x18\x01 \x01(\tB)\xda\xb7,%\n Server-assigned note identifier.\xc0\x02\x01R\x02id\x12W\n\x05title\x18\x02 \x01(\tBA\xda\xb7,=\n Short human-readable note title.\x1a\x15\n\x13Ship Python support`\x01hPR\x05title\x12y\n\x04\x62ody\x18\x03 \x01(\tBe\xda\xb7,a\n\x18Note body in plain text.\x1a@\n>Verify that generated Python MCP bindings are pleasant to use.`\x01h\xd0\x0fR\x04\x62ody\x12S\n\x04tags\x18\x04 \x03(\tB?\xda\xb7,;\n!Optional tags used for filtering.\x1a\x13*\x11\n\x08\n\x06python\n\x05\n\x03mcp\x80\x02\x01R\x04tags\x12\x64\n\x08\x64ue_date\x18\x05 \x01(\tBD\xda\xb7,@\n*Optional due date in ISO 8601 date format.\x1a\x0c\n\n2026-04-30Z\x04\x64\x61teH\x00R\x07\x64ueDate\x88\x01\x01\x12g\n\ncreated_at\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB,\xda\xb7,(\n#Server-assigned creation timestamp.\xc0\x02\x01R\tcreatedAtB\x0b\n\t_due_date\"\xaf\x03\n\x11\x43reateNoteRequest\x12W\n\x05title\x18\x01 \x01(\tBA\xda\xb7,=\n Short human-readable note title.\x1a\x15\n\x13Ship Python support`\x01hPR\x05title\x12y\n\x04\x62ody\x18\x02 \x01(\tBe\xda\xb7,a\n\x18Note body in plain text.\x1a@\n>Verify that generated Python MCP bindings are pleasant to use.`\x01h\xd0\x0fR\x04\x62ody\x12S\n\x04tags\x18\x03 \x03(\tB?\xda\xb7,;\n!Optional tags used for filtering.\x1a\x13*\x11\n\x08\n\x06python\n\x05\n\x03mcp\x80\x02\x01R\x04tags\x12\x64\n\x08\x64ue_date\x18\x04 \x01(\tBD\xda\xb7,@\n*Optional due date in ISO 8601 date format.\x1a\x0c\n\n2026-04-30Z\x04\x64\x61teH\x00R\x07\x64ueDate\x88\x01\x01\x42\x0b\n\t_due_date\";\n\x12\x43reateNoteResponse\x12%\n\x04note\x18\x01 \x01(\x0b\x32\x11.notebook.v1.NoteR\x04note\"\xd6\x02\n\x12SearchNotesRequest\x12q\n\x05query\x18\x01 \x01(\tBV\xda\xb7,R\nDOptional case-insensitive text query matched against title and body.\x1a\x08\n\x06python`\x01H\x00R\x05query\x88\x01\x01\x12\\\n\x04tags\x18\x02 \x03(\tBH\xda\xb7,D\n4Optional tags; a note must have every requested tag.\x1a\t*\x07\n\x05\n\x03mcp\x80\x02\x01R\x04tags\x12[\n\x05limit\x18\x03 \x01(\x05\x42@\xda\xb7,<\n\"Maximum number of notes to return.\"\x02\x38\n\xa1\x01\x00\x00\x00\x00\x00\x00\xf0?\xa9\x01\x00\x00\x00\x00\x00\x00I@H\x01R\x05limit\x88\x01\x01\x42\x08\n\x06_queryB\x08\n\x06_limit\">\n\x13SearchNotesResponse\x12\'\n\x05notes\x18\x01 \x03(\x0b\x32\x11.notebook.v1.NoteR\x05notes\"\x0f\n\rHealthRequest\"l\n\x0eHealthResponse\x12\x0e\n\x02ok\x18\x01 \x01(\x08R\x02ok\x12J\n\nnote_count\x18\x02 \x01(\x05\x42+\xda\xb7,\'\n\"Current number of notes in memory.\xc0\x02\x01R\tnoteCount2\x9a\x04\n\x0bNotebookAPI\x12\x96\x01\n\nCreateNote\x12\x1e.notebook.v1.CreateNoteRequest\x1a\x1f.notebook.v1.CreateNoteResponse\"G\xd2\xb7,C\x12\x0b\x43reate note\x1a.Create a note in the local in-memory notebook.R\x04\x10\x00 \x00\x12\x8a\x01\n\x0bSearchNotes\x12\x1f.notebook.v1.SearchNotesRequest\x1a .notebook.v1.SearchNotesResponse\"8\xd2\xb7,4\x12\x0cSearch notes\x1a\x1eSearch notes by text and tags.R\x04\x08\x01 \x00\x12\x8d\x01\n\x06Health\x12\x1a.notebook.v1.HealthRequest\x1a\x1b.notebook.v1.HealthResponse\"J\xd2\xb7,F\x12\x0fNotebook health\x1a-Verify that the notebook MCP server is alive.R\x04\x08\x01 \x00\x1aU\xca\xb7,Q\n\x08notebook\x12\x45\x41 small in-memory notebook service for generated Python MCP bindings.b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'proto.notebook_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_NOTE'].fields_by_name['id']._loaded_options = None + _globals['_NOTE'].fields_by_name['id']._serialized_options = b'\332\267,%\n Server-assigned note identifier.\300\002\001' + _globals['_NOTE'].fields_by_name['title']._loaded_options = None + _globals['_NOTE'].fields_by_name['title']._serialized_options = b'\332\267,=\n Short human-readable note title.\032\025\n\023Ship Python support`\001hP' + _globals['_NOTE'].fields_by_name['body']._loaded_options = None + _globals['_NOTE'].fields_by_name['body']._serialized_options = b'\332\267,a\n\030Note body in plain text.\032@\n>Verify that generated Python MCP bindings are pleasant to use.`\001h\320\017' + _globals['_NOTE'].fields_by_name['tags']._loaded_options = None + _globals['_NOTE'].fields_by_name['tags']._serialized_options = b'\332\267,;\n!Optional tags used for filtering.\032\023*\021\n\010\n\006python\n\005\n\003mcp\200\002\001' + _globals['_NOTE'].fields_by_name['due_date']._loaded_options = None + _globals['_NOTE'].fields_by_name['due_date']._serialized_options = b'\332\267,@\n*Optional due date in ISO 8601 date format.\032\014\n\n2026-04-30Z\004date' + _globals['_NOTE'].fields_by_name['created_at']._loaded_options = None + _globals['_NOTE'].fields_by_name['created_at']._serialized_options = b'\332\267,(\n#Server-assigned creation timestamp.\300\002\001' + _globals['_CREATENOTEREQUEST'].fields_by_name['title']._loaded_options = None + _globals['_CREATENOTEREQUEST'].fields_by_name['title']._serialized_options = b'\332\267,=\n Short human-readable note title.\032\025\n\023Ship Python support`\001hP' + _globals['_CREATENOTEREQUEST'].fields_by_name['body']._loaded_options = None + _globals['_CREATENOTEREQUEST'].fields_by_name['body']._serialized_options = b'\332\267,a\n\030Note body in plain text.\032@\n>Verify that generated Python MCP bindings are pleasant to use.`\001h\320\017' + _globals['_CREATENOTEREQUEST'].fields_by_name['tags']._loaded_options = None + _globals['_CREATENOTEREQUEST'].fields_by_name['tags']._serialized_options = b'\332\267,;\n!Optional tags used for filtering.\032\023*\021\n\010\n\006python\n\005\n\003mcp\200\002\001' + _globals['_CREATENOTEREQUEST'].fields_by_name['due_date']._loaded_options = None + _globals['_CREATENOTEREQUEST'].fields_by_name['due_date']._serialized_options = b'\332\267,@\n*Optional due date in ISO 8601 date format.\032\014\n\n2026-04-30Z\004date' + _globals['_SEARCHNOTESREQUEST'].fields_by_name['query']._loaded_options = None + _globals['_SEARCHNOTESREQUEST'].fields_by_name['query']._serialized_options = b'\332\267,R\nDOptional case-insensitive text query matched against title and body.\032\010\n\006python`\001' + _globals['_SEARCHNOTESREQUEST'].fields_by_name['tags']._loaded_options = None + _globals['_SEARCHNOTESREQUEST'].fields_by_name['tags']._serialized_options = b'\332\267,D\n4Optional tags; a note must have every requested tag.\032\t*\007\n\005\n\003mcp\200\002\001' + _globals['_SEARCHNOTESREQUEST'].fields_by_name['limit']._loaded_options = None + _globals['_SEARCHNOTESREQUEST'].fields_by_name['limit']._serialized_options = b'\332\267,<\n\"Maximum number of notes to return.\"\0028\n\241\001\000\000\000\000\000\000\360?\251\001\000\000\000\000\000\000I@' + _globals['_HEALTHRESPONSE'].fields_by_name['note_count']._loaded_options = None + _globals['_HEALTHRESPONSE'].fields_by_name['note_count']._serialized_options = b'\332\267,\'\n\"Current number of notes in memory.\300\002\001' + _globals['_NOTEBOOKAPI']._loaded_options = None + _globals['_NOTEBOOKAPI']._serialized_options = b'\312\267,Q\n\010notebook\022EA small in-memory notebook service for generated Python MCP bindings.' + _globals['_NOTEBOOKAPI'].methods_by_name['CreateNote']._loaded_options = None + _globals['_NOTEBOOKAPI'].methods_by_name['CreateNote']._serialized_options = b'\322\267,C\022\013Create note\032.Create a note in the local in-memory notebook.R\004\020\000 \000' + _globals['_NOTEBOOKAPI'].methods_by_name['SearchNotes']._loaded_options = None + _globals['_NOTEBOOKAPI'].methods_by_name['SearchNotes']._serialized_options = b'\322\267,4\022\014Search notes\032\036Search notes by text and tags.R\004\010\001 \000' + _globals['_NOTEBOOKAPI'].methods_by_name['Health']._loaded_options = None + _globals['_NOTEBOOKAPI'].methods_by_name['Health']._serialized_options = b'\322\267,F\022\017Notebook health\032-Verify that the notebook MCP server is alive.R\004\010\001 \000' + _globals['_NOTE']._serialized_start=101 + _globals['_NOTE']._serialized_end=683 + _globals['_CREATENOTEREQUEST']._serialized_start=686 + _globals['_CREATENOTEREQUEST']._serialized_end=1117 + _globals['_CREATENOTERESPONSE']._serialized_start=1119 + _globals['_CREATENOTERESPONSE']._serialized_end=1178 + _globals['_SEARCHNOTESREQUEST']._serialized_start=1181 + _globals['_SEARCHNOTESREQUEST']._serialized_end=1523 + _globals['_SEARCHNOTESRESPONSE']._serialized_start=1525 + _globals['_SEARCHNOTESRESPONSE']._serialized_end=1587 + _globals['_HEALTHREQUEST']._serialized_start=1589 + _globals['_HEALTHREQUEST']._serialized_end=1604 + _globals['_HEALTHRESPONSE']._serialized_start=1606 + _globals['_HEALTHRESPONSE']._serialized_end=1714 + _globals['_NOTEBOOKAPI']._serialized_start=1717 + _globals['_NOTEBOOKAPI']._serialized_end=2255 +# @@protoc_insertion_point(module_scope) diff --git a/examples/5_python_standalone/pyproject.toml b/examples/5_python_standalone/pyproject.toml new file mode 100644 index 0000000..052e53d --- /dev/null +++ b/examples/5_python_standalone/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "protoc-gen-mcp-python-standalone-example" +version = "0.1.0" +description = "Standalone Python MCP server generated from protobuf with protoc-gen-mcp." +requires-python = ">=3.10" +dependencies = [ + "anyio>=4,<5", + "jsonschema>=4,<5", + "mcp>=1.27,<2", + "protobuf>=6,<7", +] + +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +py-modules = [] + +[tool.pyright] +pythonVersion = "3.10" +typeCheckingMode = "basic" diff --git a/examples/5_python_standalone/server.py b/examples/5_python_standalone/server.py new file mode 100644 index 0000000..910bbf2 --- /dev/null +++ b/examples/5_python_standalone/server.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +import anyio +import mcp.server.lowlevel +import mcp.server.stdio + +from proto import notebook_mcp + + +def _now_protojson() -> str: + return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + + +class Notebook: + def __init__(self) -> None: + self._next_id = 1 + self._notes: dict[str, notebook_mcp.Note] = {} + + def create_note( + self, + _ctx: notebook_mcp.ToolRequestContext, + req: notebook_mcp.CreateNoteRequest, + ) -> notebook_mcp.CreateNoteResponse: + note_id = f"note-{self._next_id}" + self._next_id += 1 + + note = notebook_mcp.Note( + id=note_id, + title=req.title, + body=req.body, + tags=list(req.tags), + due_date=req.due_date, + created_at=_now_protojson(), + ) + self._notes[note.id] = note + return notebook_mcp.CreateNoteResponse(note=note) + + def search_notes( + self, + _ctx: notebook_mcp.ToolRequestContext, + req: notebook_mcp.SearchNotesRequest, + ) -> notebook_mcp.SearchNotesResponse: + query = "" if req.query is notebook_mcp.UNSET else req.query.casefold() + required_tags = set(req.tags) + limit = 10 if req.limit is notebook_mcp.UNSET else req.limit + + notes: list[notebook_mcp.Note] = [] + for note in self._notes.values(): + haystack = f"{note.title}\n{note.body}".casefold() + if query and query not in haystack: + continue + if required_tags and not required_tags.issubset(set(note.tags)): + continue + notes.append(note) + if len(notes) >= limit: + break + + return notebook_mcp.SearchNotesResponse(notes=notes) + + def health( + self, + _ctx: notebook_mcp.ToolRequestContext, + _req: notebook_mcp.HealthRequest, + ) -> notebook_mcp.HealthResponse: + return notebook_mcp.HealthResponse(ok=True, note_count=len(self._notes)) + + +def new_server() -> mcp.server.lowlevel.Server: + server = mcp.server.lowlevel.Server("standalone-python-notebook", version="0.1.0") + notebook_mcp.register_notebook_api_tools(server, Notebook()) + return server + + +async def run_stdio_server() -> None: + server = new_server() + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options(), + ) + + +def main() -> None: + anyio.run(run_stdio_server) + + +if __name__ == "__main__": + main() diff --git a/examples/Makefile b/examples/Makefile index 79d53c0..f3117ea 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -1,7 +1,12 @@ .PHONY: generate lint generate: - easyp --cfg easyp.yaml generate -p . -r ../ + easyp --cfg easyp.yaml mod download + easyp --cfg easyp.yaml generate -p . -r . + rm -rf google + $(MAKE) -C 5_python_standalone generate lint: - easyp --cfg easyp.yaml lint -p . -r ../ + easyp --cfg easyp.yaml mod download + easyp --cfg easyp.yaml lint -p . -r . + $(MAKE) -C 5_python_standalone lint diff --git a/examples/README.md b/examples/README.md index 984a9a4..ca839c8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,41 +1,79 @@ # protoc-gen-mcp Examples -This directory contains standalone, runnable examples of how to build and expose Model Context Protocol (MCP) tools using Protocol Buffers and the Go SDK. +This directory contains standalone, runnable examples of how to build and expose Model Context Protocol (MCP) tools using Protocol Buffers with both the Go SDK and the official Python SDK. ## Prerequisites - **easyp**: Ensure you have [easyp](https://github.com/easyp-tech/easyp) installed for code generation. ## Running the Examples -1. Generate the Go code from the `.proto` definitions. Run this from the root of the repository or from the examples directory: +1. Generate the Go and Python code from the `.proto` definitions. Run this from the root of the repository or from the examples directory: ```bash cd examples make generate ``` -2. Run any of the servers (e.g. `1-helloworld`): + For `1_helloworld` through `4_crm_system`, the generated Python handler surface lives in `proto/*_mcp.py`, with + generated `proto/__init__.py` files so the example servers can import + bindings through `from proto import ...`. The example Python servers + implement those dataclasses directly; `*_pb2.py` stays internal to the + generated runtime. The `mcp/options/v1/options.proto` import is resolved + through the `deps` entry in `examples/easyp.yaml` and pinned by + `examples/easyp.lock`, so the examples do not depend on the local + repository-root options package during generation. + `5_python_standalone` has its own `easyp.yaml`, `easyp.lock`, and + `pyproject.toml` to model a user-owned Python project. +2. Run any of the Go servers (e.g. `1_helloworld`): ```bash - cd examples/1-helloworld + cd examples/1_helloworld go run main.go ``` +3. Run the matching Python server: + ```bash + cd examples/1_helloworld + python main.py + ``` +4. Run the Python-only standalone project: + ```bash + cd examples/5_python_standalone + make setup + make run + ``` + +When you inspect tools in clients like `@modelcontextprotocol/inspector`, +remember that omitted tool-annotation hints are often rendered with +client-side defaults. If you need deterministic badges in UI, set hints such +as `destructive_hint: false` explicitly in the proto instead of relying on +omission. ## Included Examples -### [1-helloworld](./1-helloworld) +### [1_helloworld](./1_helloworld) A minimal Quickstart example. It contains a single RPC method that takes a name and returns a greeting. Demonstrates the simplest way to configure `easyp.yaml` and initialize `mcp.Server` with the generated bindings. -### [2-weather-api](./2-weather-api) (Validation & Safe Read-Only Queries) +### [2_weather_api](./2_weather_api) (Validation & Safe Read-Only Queries) A mock weather service demonstrating input safety limits and schema hints. - **Validation**: Strict limits on parameters (e.g. `minimum`, `maximum` degrees). -- **Oneof**: Allows lookup either by `city` or by `coordinates`. +- **Oneof**: Allows lookup either by `city` or by `coordinates`, using explicit + generated Python wrapper variants in the handler API. - **Annotations**: Sets `read_only_hint: true` and `open_world_hint: true` so the LLM knows it is harmless to aggressively scan locations without altering state. -### [3-file-manager](./3-file-manager) (Destructive Operations) +### [3_file_manager](./3_file_manager) (Destructive Operations) A straightforward demo for operating on a temporary directory. - **Security Check**: Strings validated by `pattern: "^[a-zA-Z0-9_\\-\\.]+$"` to guarantee LLMs don't trigger path injections. - **Destructive Operations**: Explicitly flags the `DeleteFile` tool with `destructive_hint: true` so MCP hosts demand user confirmation before dropping files. -### [4-crm-system](./4-crm-system) (Enterprise Kitchen Sink) +### [4_crm_system](./4_crm_system) (Enterprise Kitchen Sink) A full-featured (mocked) customer relationship management service covering complex configurations. - **Icons**: Distinct tool and service icons encoded as JSON. -- **Well-Known Types (WKTs)**: `google.protobuf.Timestamp` emitted into schemas and correctly initialized, and `google.protobuf.FieldMask` for targeted partial updates. +- **Well-Known Types (WKTs)**: `google.protobuf.Timestamp` and `google.protobuf.FieldMask` + surface in Python handlers as their ProtoJSON forms (`RFC 3339` strings and + field-mask strings), while the generated runtime keeps protobuf conversion + internal. - **Advanced Constraints**: Ensures slice entries hold unique, non-empty values (`min_items: 1`, `unique_items: true`). - **Rich Output**: Complex responses carrying arrays of message objects back to the LLM context. + +### [5_python_standalone](./5_python_standalone) (Python-Only User Project) +A pure Python MCP server example with its own `pyproject.toml`, `easyp.yaml`, +protobuf contract, generated bindings, and stdio server. +- **Independent environment**: `make setup` creates a local `.venv` and installs the official Python MCP SDK plus protobuf/jsonschema dependencies. +- **No import hacks**: `server.py` imports generated code with `from proto import notebook_mcp` and does not edit `sys.path`. +- **Python-only proto**: the user-authored proto does not need `go_package`; `protoc-gen-mcp` synthesizes internal metadata for Python generation. diff --git a/examples/easyp.lock b/examples/easyp.lock new file mode 100644 index 0000000..13dc44f --- /dev/null +++ b/examples/easyp.lock @@ -0,0 +1 @@ +github.com/easyp-tech/protoc-gen-mcp v0.0.0-20260330005019-b9d1262e553d532eaff774a334fce9454b25bbec h1:BLaM59sORcHls9Dv7kVVUCfN1axznUGf8ANAZKMXdPA= diff --git a/examples/easyp.yaml b/examples/easyp.yaml index 4367b99..cc76547 100644 --- a/examples/easyp.yaml +++ b/examples/easyp.yaml @@ -1,3 +1,6 @@ +deps: + - github.com/easyp-tech/protoc-gen-mcp + lint: use: - DIRECTORY_SAME_PACKAGE @@ -39,14 +42,32 @@ lint: generate: inputs: - directory: - path: examples + path: "1_helloworld/proto" + root: "." + - directory: + path: "2_weather_api/proto" + root: "." + - directory: + path: "3_file_manager/proto" + root: "." + - directory: + path: "4_crm_system/proto" root: "." plugins: - name: go out: . opts: paths: source_relative + - name: python + with_imports: true + out: . + - command: ["go", "run", "../cmd/protoc-gen-mcp"] + out: . + opts: + paths: source_relative - command: ["go", "run", "../cmd/protoc-gen-mcp"] out: . opts: paths: source_relative + lang: python + python_runtime: google.protobuf diff --git a/examples/mcp/__init__.py b/examples/mcp/__init__.py new file mode 100644 index 0000000..813691e --- /dev/null +++ b/examples/mcp/__init__.py @@ -0,0 +1,5 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +"""Bridge generated mcp.options.* modules with the official MCP SDK package.""" +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) diff --git a/examples/mcp/options/v1/options_pb2.py b/examples/mcp/options/v1/options_pb2.py new file mode 100644 index 0000000..fe8a59f --- /dev/null +++ b/examples/mcp/options/v1/options_pb2.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: mcp/options/v1/options.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'mcp/options/v1/options.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cmcp/options/v1/options.proto\x12\x0emcp.options.v1\x1a google/protobuf/descriptor.proto\"|\n\x0eServiceOptions\x12\x1c\n\tnamespace\x18\x01 \x01(\tR\tnamespace\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12*\n\x05icons\x18\x03 \x03(\x0b\x32\x14.mcp.options.v1.IconR\x05icons\"\xa2\x02\n\rMethodOptions\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n\x05title\x18\x02 \x01(\tR\x05title\x12 \n\x0b\x64\x65scription\x18\x03 \x01(\tR\x0b\x64\x65scription\x12\x16\n\x06hidden\x18\x04 \x01(\x08R\x06hidden\x12\x41\n\x0b\x61nnotations\x18\n \x01(\x0b\x32\x1f.mcp.options.v1.ToolAnnotationsR\x0b\x61nnotations\x12*\n\x05icons\x18\x0b \x03(\x0b\x32\x14.mcp.options.v1.IconR\x05icons\x12>\n\texecution\x18\x0c \x01(\x0b\x32 .mcp.options.v1.ExecutionOptionsR\texecution\"\x81\x06\n\x0c\x46ieldOptions\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x38\n\x08\x65xamples\x18\x03 \x03(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x08\x65xamples\x12\x41\n\rdefault_value\x18\x04 \x01(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x0c\x64\x65\x66\x61ultValue\x12\x18\n\x07pattern\x18\n \x01(\tR\x07pattern\x12\x16\n\x06\x66ormat\x18\x0b \x01(\tR\x06\x66ormat\x12\"\n\nmin_length\x18\x0c \x01(\rH\x00R\tminLength\x88\x01\x01\x12\"\n\nmax_length\x18\r \x01(\rH\x01R\tmaxLength\x88\x01\x01\x12\x1d\n\x07minimum\x18\x14 \x01(\x01H\x02R\x07minimum\x88\x01\x01\x12\x1d\n\x07maximum\x18\x15 \x01(\x01H\x03R\x07maximum\x88\x01\x01\x12\x30\n\x11\x65xclusive_minimum\x18\x16 \x01(\x01H\x04R\x10\x65xclusiveMinimum\x88\x01\x01\x12\x30\n\x11\x65xclusive_maximum\x18\x17 \x01(\x01H\x05R\x10\x65xclusiveMaximum\x88\x01\x01\x12$\n\x0bmultiple_of\x18\x18 \x01(\x01H\x06R\nmultipleOf\x88\x01\x01\x12 \n\tmin_items\x18\x1e \x01(\rH\x07R\x08minItems\x88\x01\x01\x12 \n\tmax_items\x18\x1f \x01(\rH\x08R\x08maxItems\x88\x01\x01\x12!\n\x0cunique_items\x18 \x01(\x08R\x0buniqueItems\x12\x1b\n\tread_only\x18( \x01(\x08R\x08readOnlyB\r\n\x0b_min_lengthB\r\n\x0b_max_lengthB\n\n\x08_minimumB\n\n\x08_maximumB\x14\n\x12_exclusive_minimumB\x14\n\x12_exclusive_maximumB\x0e\n\x0c_multiple_ofB\x0c\n\n_min_itemsB\x0c\n\n_max_items\"\xce\x02\n\x0c\x45xampleValue\x12#\n\x0cstring_value\x18\x01 \x01(\tH\x00R\x0bstringValue\x12#\n\x0cnumber_value\x18\x02 \x01(\x01H\x00R\x0bnumberValue\x12\x1f\n\nbool_value\x18\x03 \x01(\x08H\x00R\tboolValue\x12\x42\n\x0cobject_value\x18\x04 \x01(\x0b\x32\x1d.mcp.options.v1.ExampleObjectH\x00R\x0bobjectValue\x12?\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32\x1c.mcp.options.v1.ExampleArrayH\x00R\narrayValue\x12\x1f\n\nnull_value\x18\x06 \x01(\x08H\x00R\tnullValue\x12%\n\rinteger_value\x18\x07 \x01(\x03H\x00R\x0cintegerValueB\x06\n\x04kind\"\xbb\x01\n\rExampleObject\x12M\n\nproperties\x18\x01 \x03(\x0b\x32-.mcp.options.v1.ExampleObject.PropertiesEntryR\nproperties\x1a[\n\x0fPropertiesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x05value:\x02\x38\x01\"B\n\x0c\x45xampleArray\x12\x32\n\x05items\x18\x01 \x03(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x05items\"\xfc\x01\n\x0fToolAnnotations\x12$\n\x0eread_only_hint\x18\x01 \x01(\x08R\x0creadOnlyHint\x12.\n\x10\x64\x65structive_hint\x18\x02 \x01(\x08H\x00R\x0f\x64\x65structiveHint\x88\x01\x01\x12\'\n\x0fidempotent_hint\x18\x03 \x01(\x08R\x0eidempotentHint\x12+\n\x0fopen_world_hint\x18\x04 \x01(\x08H\x01R\ropenWorldHint\x88\x01\x01\x12\x14\n\x05title\x18\x05 \x01(\tR\x05titleB\x13\n\x11_destructive_hintB\x12\n\x10_open_world_hint\"a\n\x04Icon\x12\x10\n\x03src\x18\x01 \x01(\tR\x03src\x12\x1b\n\tmime_type\x18\x02 \x01(\tR\x08mimeType\x12\x14\n\x05sizes\x18\x03 \x03(\tR\x05sizes\x12\x14\n\x05theme\x18\x04 \x01(\tR\x05theme\"R\n\x10\x45xecutionOptions\x12>\n\x0ctask_support\x18\x01 \x01(\x0e\x32\x1b.mcp.options.v1.TaskSupportR\x0btaskSupport\"L\n\x0cOneofOptions\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08required\x18\x02 \x01(\x08R\x08required\"\x83\x01\n\x0eMessageOptions\x12\x14\n\x05title\x18\x01 \x01(\tR\x05title\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x39\n\x08\x65xamples\x18\x03 \x03(\x0b\x32\x1d.mcp.options.v1.ExampleObjectR\x08\x65xamples\"E\n\x0b\x45numOptions\x12\x14\n\x05title\x18\x01 \x01(\tR\x05title\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\"L\n\x10\x45numValueOptions\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x16\n\x06hidden\x18\x02 \x01(\x08R\x06hidden*Z\n\x0bTaskSupport\x12\x15\n\x11TASK_SUPPORT_NONE\x10\x00\x12\x19\n\x15TASK_SUPPORT_OPTIONAL\x10\x01\x12\x19\n\x15TASK_SUPPORT_REQUIRED\x10\x02:[\n\x07service\x12\x1f.google.protobuf.ServiceOptions\x18\xf9\xc6\x05 \x01(\x0b\x32\x1e.mcp.options.v1.ServiceOptionsR\x07service:W\n\x06method\x12\x1e.google.protobuf.MethodOptions\x18\xfa\xc6\x05 \x01(\x0b\x32\x1d.mcp.options.v1.MethodOptionsR\x06method:S\n\x05\x66ield\x12\x1d.google.protobuf.FieldOptions\x18\xfb\xc6\x05 \x01(\x0b\x32\x1c.mcp.options.v1.FieldOptionsR\x05\x66ield:[\n\x07message\x12\x1f.google.protobuf.MessageOptions\x18\xfc\xc6\x05 \x01(\x0b\x32\x1e.mcp.options.v1.MessageOptionsR\x07message:O\n\x04\x65num\x12\x1c.google.protobuf.EnumOptions\x18\xfd\xc6\x05 \x01(\x0b\x32\x1b.mcp.options.v1.EnumOptionsR\x04\x65num:d\n\nenum_value\x12!.google.protobuf.EnumValueOptions\x18\xfe\xc6\x05 \x01(\x0b\x32 .mcp.options.v1.EnumValueOptionsR\tenumValue:S\n\x05oneof\x12\x1d.google.protobuf.OneofOptions\x18\xff\xc6\x05 \x01(\x0b\x32\x1c.mcp.options.v1.OneofOptionsR\x05oneofB?Z=github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1;optionsv1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcp.options.v1.options_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1;optionsv1' + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._loaded_options = None + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._serialized_options = b'8\001' + _globals['_TASKSUPPORT']._serialized_start=2667 + _globals['_TASKSUPPORT']._serialized_end=2757 + _globals['_SERVICEOPTIONS']._serialized_start=82 + _globals['_SERVICEOPTIONS']._serialized_end=206 + _globals['_METHODOPTIONS']._serialized_start=209 + _globals['_METHODOPTIONS']._serialized_end=499 + _globals['_FIELDOPTIONS']._serialized_start=502 + _globals['_FIELDOPTIONS']._serialized_end=1271 + _globals['_EXAMPLEVALUE']._serialized_start=1274 + _globals['_EXAMPLEVALUE']._serialized_end=1608 + _globals['_EXAMPLEOBJECT']._serialized_start=1611 + _globals['_EXAMPLEOBJECT']._serialized_end=1798 + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._serialized_start=1707 + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._serialized_end=1798 + _globals['_EXAMPLEARRAY']._serialized_start=1800 + _globals['_EXAMPLEARRAY']._serialized_end=1866 + _globals['_TOOLANNOTATIONS']._serialized_start=1869 + _globals['_TOOLANNOTATIONS']._serialized_end=2121 + _globals['_ICON']._serialized_start=2123 + _globals['_ICON']._serialized_end=2220 + _globals['_EXECUTIONOPTIONS']._serialized_start=2222 + _globals['_EXECUTIONOPTIONS']._serialized_end=2304 + _globals['_ONEOFOPTIONS']._serialized_start=2306 + _globals['_ONEOFOPTIONS']._serialized_end=2382 + _globals['_MESSAGEOPTIONS']._serialized_start=2385 + _globals['_MESSAGEOPTIONS']._serialized_end=2516 + _globals['_ENUMOPTIONS']._serialized_start=2518 + _globals['_ENUMOPTIONS']._serialized_end=2587 + _globals['_ENUMVALUEOPTIONS']._serialized_start=2589 + _globals['_ENUMVALUEOPTIONS']._serialized_end=2665 +# @@protoc_insertion_point(module_scope) diff --git a/examples/python_stdio_test.go b/examples/python_stdio_test.go new file mode 100644 index 0000000..0882fff --- /dev/null +++ b/examples/python_stdio_test.go @@ -0,0 +1,446 @@ +package examples_test + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "reflect" + "runtime" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func TestPythonExamplesOverStdio(t *testing.T) { + root := repoRoot(t) + examples := []struct { + name string + path string + run func(t *testing.T, session *mcp.ClientSession) + }{ + { + name: "helloworld", + path: filepath.Join(root, "examples/1_helloworld/main.py"), + run: runHelloWorldExample, + }, + { + name: "weather", + path: filepath.Join(root, "examples/2_weather_api/main.py"), + run: runWeatherExample, + }, + { + name: "file-manager", + path: filepath.Join(root, "examples/3_file_manager/main.py"), + run: runFileManagerExample, + }, + { + name: "crm", + path: filepath.Join(root, "examples/4_crm_system/main.py"), + run: runCRMExample, + }, + } + + for _, tc := range examples { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + client := mcp.NewClient(&mcp.Implementation{ + Name: "protoc-gen-mcp-python-examples-test-client", + Version: "v0.0.1", + }, nil) + + session, err := client.Connect(ctx, &mcp.CommandTransport{ + Command: pythonExampleCommand(t, root, tc.path), + }, nil) + if err != nil { + t.Fatalf("client.Connect() over stdio failed: %v", err) + } + defer session.Close() + + tc.run(t, session) + }) + } +} + +func TestStandalonePythonExampleOverStdio(t *testing.T) { + root := repoRoot(t) + projectDir := filepath.Join(root, "examples/5_python_standalone") + ctx := context.Background() + client := mcp.NewClient(&mcp.Implementation{ + Name: "protoc-gen-mcp-standalone-python-example-test-client", + Version: "v0.0.1", + }, nil) + + session, err := client.Connect(ctx, &mcp.CommandTransport{ + Command: standalonePythonExampleCommand(t, projectDir), + }, nil) + if err != nil { + t.Fatalf("client.Connect() over stdio failed: %v", err) + } + defer session.Close() + + runStandaloneNotebookExample(t, session) +} + +func runHelloWorldExample(t *testing.T, session *mcp.ClientSession) { + t.Helper() + + ctx := context.Background() + tools, err := session.ListTools(ctx, nil) + if err != nil { + t.Fatalf("ListTools() failed: %v", err) + } + findTool(t, tools.Tools, "greeter_SayHello") + + result, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "greeter_SayHello", + Arguments: map[string]any{ + "name": "Alice", + }, + }) + if err != nil { + t.Fatalf("CallTool(SayHello) failed: %v", err) + } + if result.IsError { + t.Fatalf("SayHello returned tool error: %+v", result) + } + assertTextStructuredContentMatch(t, "greeter_SayHello", result) + structured := decodeMap(t, result.StructuredContent) + if got := structured["message"]; got != "Hello, Alice!" { + t.Fatalf("message = %v, want Hello, Alice!", got) + } +} + +func runWeatherExample(t *testing.T, session *mcp.ClientSession) { + t.Helper() + + ctx := context.Background() + tools, err := session.ListTools(ctx, nil) + if err != nil { + t.Fatalf("ListTools() failed: %v", err) + } + findTool(t, tools.Tools, "weather_GetCurrentWeather") + + result, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "weather_GetCurrentWeather", + Arguments: map[string]any{ + "city": "London", + }, + }) + if err != nil { + t.Fatalf("CallTool(GetCurrentWeather) failed: %v", err) + } + if result.IsError { + t.Fatalf("GetCurrentWeather returned tool error: %+v", result) + } + assertTextStructuredContentMatch(t, "weather_GetCurrentWeather", result) + structured := decodeMap(t, result.StructuredContent) + if got := structured["condition"]; got != "Cloudy" { + t.Fatalf("condition = %v, want Cloudy", got) + } + if got := structured["temperature"]; got != 14.0 { + t.Fatalf("temperature = %v, want 14", got) + } +} + +func runFileManagerExample(t *testing.T, session *mcp.ClientSession) { + t.Helper() + + ctx := context.Background() + tools, err := session.ListTools(ctx, nil) + if err != nil { + t.Fatalf("ListTools() failed: %v", err) + } + findTool(t, tools.Tools, "files_ReadFile") + findTool(t, tools.Tools, "files_DeleteFile") + + readResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "files_ReadFile", + Arguments: map[string]any{ + "filename": "example.txt", + }, + }) + if err != nil { + t.Fatalf("CallTool(ReadFile) failed: %v", err) + } + if readResult.IsError { + t.Fatalf("ReadFile returned tool error: %+v", readResult) + } + assertTextStructuredContentMatch(t, "files_ReadFile", readResult) + readStructured := decodeMap(t, readResult.StructuredContent) + if got := readStructured["content"]; got != "Hello from the Python file manager example.\n" { + t.Fatalf("content = %v, want seeded example file content", got) + } + + deleteResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "files_DeleteFile", + Arguments: map[string]any{ + "filename": "example.txt", + }, + }) + if err != nil { + t.Fatalf("CallTool(DeleteFile) failed: %v", err) + } + if deleteResult.IsError { + t.Fatalf("DeleteFile returned tool error: %+v", deleteResult) + } + assertTextStructuredContentMatch(t, "files_DeleteFile", deleteResult) + deleteStructured := decodeMap(t, deleteResult.StructuredContent) + if got := deleteStructured["success"]; got != true { + t.Fatalf("success = %v, want true", got) + } +} + +func runCRMExample(t *testing.T, session *mcp.ClientSession) { + t.Helper() + + ctx := context.Background() + tools, err := session.ListTools(ctx, nil) + if err != nil { + t.Fatalf("ListTools() failed: %v", err) + } + findTool(t, tools.Tools, "crm_ListUsers") + findTool(t, tools.Tools, "crm_UpdateUser") + + listResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "crm_ListUsers", + Arguments: map[string]any{ + "limit": 2, + "requiredTags": []any{"premium"}, + }, + }) + if err != nil { + t.Fatalf("CallTool(ListUsers) failed: %v", err) + } + if listResult.IsError { + t.Fatalf("ListUsers returned tool error: %+v", listResult) + } + assertTextStructuredContentMatch(t, "crm_ListUsers", listResult) + listStructured := decodeMap(t, listResult.StructuredContent) + users, ok := listStructured["users"].([]any) + if !ok || len(users) != 1 { + t.Fatalf("users = %T %v, want single filtered user", listStructured["users"], listStructured["users"]) + } + user, ok := users[0].(map[string]any) + if !ok { + t.Fatalf("user[0] has type %T, want map[string]any", users[0]) + } + + updateResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "crm_UpdateUser", + Arguments: map[string]any{ + "user": map[string]any{ + "id": user["id"], + "name": "Alice Updated", + "registeredAt": user["registeredAt"], + "tags": []any{"premium", "updated"}, + }, + "updateMask": "name,tags", + }, + }) + if err != nil { + t.Fatalf("CallTool(UpdateUser) failed: %v", err) + } + if updateResult.IsError { + t.Fatalf("UpdateUser returned tool error: %+v", updateResult) + } + assertTextStructuredContentMatch(t, "crm_UpdateUser", updateResult) + updateStructured := decodeMap(t, updateResult.StructuredContent) + updatedUser, ok := updateStructured["user"].(map[string]any) + if !ok { + t.Fatalf("updated user has type %T, want map[string]any", updateStructured["user"]) + } + if got := updatedUser["name"]; got != "Alice Updated" { + t.Fatalf("updated user.name = %v, want Alice Updated", got) + } +} + +func runStandaloneNotebookExample(t *testing.T, session *mcp.ClientSession) { + t.Helper() + + ctx := context.Background() + tools, err := session.ListTools(ctx, nil) + if err != nil { + t.Fatalf("ListTools() failed: %v", err) + } + findTool(t, tools.Tools, "notebook_CreateNote") + findTool(t, tools.Tools, "notebook_SearchNotes") + + createResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "notebook_CreateNote", + Arguments: map[string]any{ + "title": "Ship Python support", + "body": "Verify the generated Python MCP bindings are pleasant to use.", + "tags": []any{"python", "mcp"}, + "dueDate": "2026-04-30", + }, + }) + if err != nil { + t.Fatalf("CallTool(CreateNote) failed: %v", err) + } + if createResult.IsError { + t.Fatalf("CreateNote returned tool error: %+v", createResult) + } + assertTextStructuredContentMatch(t, "notebook_CreateNote", createResult) + createStructured := decodeMap(t, createResult.StructuredContent) + note, ok := createStructured["note"].(map[string]any) + if !ok { + t.Fatalf("created note has type %T, want map[string]any", createStructured["note"]) + } + if got := note["title"]; got != "Ship Python support" { + t.Fatalf("created note.title = %v, want Ship Python support", got) + } + + searchResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "notebook_SearchNotes", + Arguments: map[string]any{ + "query": "python", + "tags": []any{"mcp"}, + "limit": 5, + }, + }) + if err != nil { + t.Fatalf("CallTool(SearchNotes) failed: %v", err) + } + if searchResult.IsError { + t.Fatalf("SearchNotes returned tool error: %+v", searchResult) + } + assertTextStructuredContentMatch(t, "notebook_SearchNotes", searchResult) + searchStructured := decodeMap(t, searchResult.StructuredContent) + notes, ok := searchStructured["notes"].([]any) + if !ok || len(notes) != 1 { + t.Fatalf("notes = %T %v, want one matching note", searchStructured["notes"], searchStructured["notes"]) + } +} + +func repoRoot(t *testing.T) string { + t.Helper() + + _, filename, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("resolve current test file path") + } + + return filepath.Clean(filepath.Join(filepath.Dir(filename), "..")) +} + +func pythonExampleCommand(t *testing.T, root, script string) *exec.Cmd { + t.Helper() + + python := pythonCommand(t) + probe := exec.Command(python, "-c", "import anyio, google.protobuf, mcp") + probe.Dir = root + if output, err := probe.CombinedOutput(); err != nil { + t.Fatalf("python runtime dependencies are not available: %v\n%s", err, output) + } + + cmd := exec.Command(python, script) + cmd.Dir = root + cmd.Env = append(os.Environ(), + "PYTHONPATH=", + "PYTHONUNBUFFERED=1", + ) + return cmd +} + +func standalonePythonExampleCommand(t *testing.T, projectDir string) *exec.Cmd { + t.Helper() + + python := standalonePythonCommand(t, projectDir) + probe := exec.Command(python, "-c", "import anyio, google.protobuf, jsonschema, mcp") + probe.Dir = projectDir + if output, err := probe.CombinedOutput(); err != nil { + t.Fatalf("python runtime dependencies are not available: %v\n%s", err, output) + } + + cmd := exec.Command(python, "server.py") + cmd.Dir = projectDir + cmd.Env = append(os.Environ(), + "PYTHONPATH=", + "PYTHONUNBUFFERED=1", + ) + return cmd +} + +func standalonePythonCommand(t *testing.T, projectDir string) string { + t.Helper() + + candidates := []string{ + filepath.Join(projectDir, ".venv", "bin", "python"), + filepath.Join(projectDir, ".venv", "Scripts", "python.exe"), + } + for _, candidate := range candidates { + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + + return pythonCommand(t) +} + +func pythonCommand(t *testing.T) string { + t.Helper() + + if path, err := exec.LookPath("python3"); err == nil { + return path + } + if path, err := exec.LookPath("python"); err == nil { + return path + } + + t.Fatal("python3/python not found in PATH") + return "" +} + +func decodeMap(t *testing.T, value any) map[string]any { + t.Helper() + + raw, err := json.Marshal(value) + if err != nil { + t.Fatalf("marshal JSON value: %v", err) + } + + var decoded map[string]any + if err := json.Unmarshal(raw, &decoded); err != nil { + t.Fatalf("unmarshal JSON value: %v", err) + } + + return decoded +} + +func assertTextStructuredContentMatch(t *testing.T, toolName string, result *mcp.CallToolResult) { + t.Helper() + + if len(result.Content) != 1 { + t.Fatalf("%s returned %d content items, want 1", toolName, len(result.Content)) + } + + textContent, ok := result.Content[0].(*mcp.TextContent) + if !ok { + t.Fatalf("%s content[0] has type %T, want *mcp.TextContent", toolName, result.Content[0]) + } + + var fromText map[string]any + if err := json.Unmarshal([]byte(textContent.Text), &fromText); err != nil { + t.Fatalf("decode text content for %s: %v", toolName, err) + } + + fromStructured := decodeMap(t, result.StructuredContent) + if !reflect.DeepEqual(fromText, fromStructured) { + t.Fatalf("%s text content %v does not match structured content %v", toolName, fromText, fromStructured) + } +} + +func findTool(t *testing.T, tools []*mcp.Tool, toolName string) *mcp.Tool { + t.Helper() + + for _, tool := range tools { + if tool != nil && tool.Name == toolName { + return tool + } + } + + t.Fatalf("tool %q not found in tools/list", toolName) + return nil +} diff --git a/go.mod b/go.mod index adb258f..b470c0c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/easyp-tech/protoc-gen-mcp go 1.26 require ( + github.com/bufbuild/protocompile v0.14.1 github.com/google/jsonschema-go v0.4.2 github.com/modelcontextprotocol/go-sdk v1.4.1 google.golang.org/protobuf v1.36.11 @@ -14,5 +15,6 @@ require ( github.com/segmentio/encoding v0.5.4 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.42.0 // indirect ) diff --git a/go.sum b/go.sum index 7079555..35d54b4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -6,19 +10,27 @@ github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbc github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/internal/codegen/collect.go b/internal/codegen/collect.go new file mode 100644 index 0000000..95d308e --- /dev/null +++ b/internal/codegen/collect.go @@ -0,0 +1,196 @@ +package codegen + +import ( + "fmt" + + "github.com/easyp-tech/protoc-gen-mcp/internal/schema" + "github.com/google/jsonschema-go/jsonschema" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// CollectFileModel normalizes a protobuf file into the shared semantic IR used +// by language-specific renderers. +func CollectFileModel(file *protogen.File, opts Options) (FileModel, error) { + if file.Desc.Syntax() != protoreflect.Proto3 { + return FileModel{}, fmt.Errorf("only proto3 files are supported in MVP: %s", file.Desc.Path()) + } + + model := FileModel{ + ProtoPath: file.Desc.Path(), + GeneratedFilenamePrefix: file.GeneratedFilenamePrefix, + Options: opts, + Services: make([]ServiceModel, 0, len(file.Services)), + } + var pythonGraphMethods []*protogen.Method + + for _, service := range file.Services { + serviceMetadata, err := loadServiceMetadata(service) + if err != nil { + return FileModel{}, err + } + + serviceModel := ServiceModel{ + ProtoFullName: string(service.Desc.FullName()), + ProtoName: string(service.Desc.Name()), + Namespace: serviceMetadata.Namespace, + Description: serviceMetadata.Description, + Icons: serviceMetadata.Icons, + Methods: make([]MethodModel, 0, len(service.Methods)), + } + + for _, method := range service.Methods { + methodMetadata, include, err := selectToolMethod(method) + if err != nil { + return FileModel{}, err + } + if !include { + continue + } + + inputSchema, err := generateSchema(method.Input) + if err != nil { + return FileModel{}, fmt.Errorf("input schema for %s: %w", method.Desc.FullName(), err) + } + if len(methodMetadata.Examples) > 0 { + inputSchema.Examples = make([]any, 0, len(methodMetadata.Examples)) + for _, example := range methodMetadata.Examples { + inputSchema.Examples = append(inputSchema.Examples, example) + } + } + + if methodMetadata.Deprecated { + inputSchema.Deprecated = true + } + + outputSchema, err := generateSchema(method.Output) + if err != nil { + return FileModel{}, fmt.Errorf("output schema for %s: %w", method.Desc.FullName(), err) + } + + inputSchemaJSON, err := schema.MarshalJSON(inputSchema) + if err != nil { + return FileModel{}, fmt.Errorf("marshal input schema for %s: %w", method.Desc.FullName(), err) + } + outputSchemaJSON, err := schema.MarshalJSON(outputSchema) + if err != nil { + return FileModel{}, fmt.Errorf("marshal output schema for %s: %w", method.Desc.FullName(), err) + } + + methodModel := MethodModel{ + ProtoFullName: string(method.Desc.FullName()), + ProtoName: string(method.Desc.Name()), + Name: methodMetadata.Name, + Title: methodMetadata.Title, + Description: methodMetadata.Description, + Examples: methodMetadata.Examples, + Deprecated: methodMetadata.Deprecated, + Input: newTypeRef(method.Input), + Output: newTypeRef(method.Output), + InputSchemaJSON: inputSchemaJSON, + OutputSchemaJSON: outputSchemaJSON, + Annotations: methodMetadata.Annotations, + Icons: methodMetadata.Icons, + } + if len(methodModel.Icons) == 0 { + methodModel.Icons = serviceModel.Icons + } + + serviceModel.Methods = append(serviceModel.Methods, methodModel) + pythonGraphMethods = append(pythonGraphMethods, method) + } + + if len(serviceModel.Methods) > 0 { + model.Services = append(model.Services, serviceModel) + } + } + + if opts.Language == LanguagePython { + includeCurrentFileTypes := len(pythonGraphMethods) == 0 + pythonTypes, err := collectPythonTypeGraphFromMethods(file, opts.PythonRuntime, pythonGraphMethods, includeCurrentFileTypes) + if err != nil { + return FileModel{}, err + } + model.PythonTypes = &pythonTypes + } + + return model, nil +} + +func selectToolMethod(method *protogen.Method) (methodMetadata, bool, error) { + if method.Desc.IsStreamingClient() || method.Desc.IsStreamingServer() { + return methodMetadata{}, false, fmt.Errorf("streaming RPC is not supported: %s", method.Desc.FullName()) + } + + metadata, err := loadMethodMetadata(method) + if err != nil { + return methodMetadata{}, false, err + } + if metadata.Hidden || metadata.Disabled { + return metadata, false, nil + } + + return metadata, true, nil +} + +func generateSchema(message *protogen.Message) (*jsonschema.Schema, error) { + var fieldErr error + + generatedSchema, err := schema.GenerateMessageSchema(message, schema.Options{ + MessageMetadata: func(current *protogen.Message) schema.Metadata { + metadata, err := loadMessageMetadata(current) + if err != nil && fieldErr == nil { + fieldErr = err + } + commentMetadata := parseCommentBlock(current.Comments.Leading) + return schema.Metadata{ + Title: metadata.Title, + Description: metadata.Description, + Examples: commentMetadata.Examples, + TypedExamples: metadata.TypedExamples, + } + }, + FieldMetadata: func(field *protogen.Field) schema.FieldMetadata { + metadata, err := loadFieldMetadata(field) + if err != nil && fieldErr == nil { + fieldErr = err + } + return metadata.FieldMetadata + }, + EnumMetadata: func(enum *protogen.Enum) schema.EnumMetadata { + metadata, err := loadEnumMetadata(enum) + if err != nil && fieldErr == nil { + fieldErr = err + } + return schema.EnumMetadata{ + Title: metadata.Title, + Description: metadata.Description, + } + }, + EnumValueMetadata: func(enumValue *protogen.EnumValue) schema.EnumValueMetadata { + metadata, err := loadEnumValueMetadata(enumValue) + if err != nil && fieldErr == nil { + fieldErr = err + } + return schema.EnumValueMetadata{ + Description: metadata.Description, + Hidden: metadata.Hidden, + } + }, + }) + if err != nil { + return nil, err + } + if fieldErr != nil { + return nil, fieldErr + } + + return generatedSchema, nil +} + +func newTypeRef(message *protogen.Message) TypeRef { + return TypeRef{ + ProtoFullName: string(message.Desc.FullName()), + ProtoDisplayName: string(message.Desc.Name()), + } +} diff --git a/internal/codegen/collect_test.go b/internal/codegen/collect_test.go new file mode 100644 index 0000000..280d864 --- /dev/null +++ b/internal/codegen/collect_test.go @@ -0,0 +1,524 @@ +package codegen + +import ( + "context" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + "github.com/bufbuild/protocompile" + "github.com/bufbuild/protocompile/protoutil" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/pluginpb" +) + +func TestCollectFileModel_ExampleAPI(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{Language: LanguageGo}) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + + if len(model.Services) != 1 { + t.Fatalf("service count = %d, want 1", len(model.Services)) + } + + service := model.Services[0] + if got := model.ProtoPath; got != "internal/testproto/example/v1/example.proto" { + t.Fatalf("ProtoPath = %q, want %q", got, "internal/testproto/example/v1/example.proto") + } + if got := model.GeneratedFilenamePrefix; got != "internal/testproto/example/v1/example" { + t.Fatalf("GeneratedFilenamePrefix = %q, want %q", got, "internal/testproto/example/v1/example") + } + if model.Options.Language != LanguageGo { + t.Fatalf("Options.Language = %q, want %q", model.Options.Language, LanguageGo) + } + if got := service.Namespace; got != "example" { + t.Fatalf("Namespace = %q, want %q", got, "example") + } + if got := service.ProtoName; got != "ExampleAPI" { + t.Fatalf("ProtoName = %q, want %q", got, "ExampleAPI") + } + if got := service.ProtoFullName; got != "internal.testproto.example.v1.ExampleAPI" { + t.Fatalf("ProtoFullName = %q, want %q", got, "internal.testproto.example.v1.ExampleAPI") + } + if len(service.Methods) != 5 { + t.Fatalf("method count = %d, want 5", len(service.Methods)) + } + + method := service.Methods[0] + if got := method.ProtoName; got != "CreateReport" { + t.Fatalf("ProtoName = %q, want %q", got, "CreateReport") + } + if got := method.ProtoFullName; got != "internal.testproto.example.v1.ExampleAPI.CreateReport" { + t.Fatalf("ProtoFullName = %q, want %q", got, "internal.testproto.example.v1.ExampleAPI.CreateReport") + } + if got := method.Name; got != "CreateReport" { + t.Fatalf("Method.Name = %q, want %q", got, "CreateReport") + } + if got := method.Title; got != "Create report" { + t.Fatalf("Method.Title = %q, want %q", got, "Create report") + } + if got := method.Description; got != "Create a report for a city." { + t.Fatalf("Method.Description = %q, want %q", got, "Create a report for a city.") + } + if method.InputSchemaJSON == "" { + t.Fatal("Method.InputSchemaJSON is empty") + } + if method.OutputSchemaJSON == "" { + t.Fatal("Method.OutputSchemaJSON is empty") + } + if got := method.Input.ProtoDisplayName; got != "CreateReportRequest" { + t.Fatalf("Input.ProtoDisplayName = %q, want %q", got, "CreateReportRequest") + } + if got := method.Input.ProtoFullName; got != "internal.testproto.example.v1.CreateReportRequest" { + t.Fatalf("Input.ProtoFullName = %q, want %q", got, "internal.testproto.example.v1.CreateReportRequest") + } + if got := method.Output.ProtoDisplayName; got != "CreateReportResponse" { + t.Fatalf("Output.ProtoDisplayName = %q, want %q", got, "CreateReportResponse") + } + if got := method.Output.ProtoFullName; got != "internal.testproto.example.v1.CreateReportResponse" { + t.Fatalf("Output.ProtoFullName = %q, want %q", got, "internal.testproto.example.v1.CreateReportResponse") + } + + hidden := service.Methods[len(service.Methods)-1] + if !hidden.Deprecated { + t.Fatal("HiddenThing should propagate deprecated metadata") + } + if !strings.Contains(hidden.InputSchemaJSON, `"deprecated":true`) { + t.Fatalf("HiddenThing input schema should be marked deprecated: %s", hidden.InputSchemaJSON) + } +} + +func TestCollectFileModel_StructuralIRDoesNotContainRawProtogenDescriptors(t *testing.T) { + disallowed := map[string]struct{}{ + "google.golang.org/protobuf/compiler/protogen.Enum": {}, + "google.golang.org/protobuf/compiler/protogen.Field": {}, + "google.golang.org/protobuf/compiler/protogen.File": {}, + "google.golang.org/protobuf/compiler/protogen.Message": {}, + "google.golang.org/protobuf/compiler/protogen.Method": {}, + "google.golang.org/protobuf/compiler/protogen.Service": {}, + } + + assertIRTypeHasNoDisallowedFields(t, reflect.TypeOf(FileModel{}), disallowed) +} + +func TestCollectFileModel_StructuralIRIsLanguageNeutral(t *testing.T) { + assertIRTypeHasNoFieldNames(t, reflect.TypeOf(FileModel{}), map[string]struct{}{ + "GoPackageName": {}, + "GoImportPath": {}, + "GoName": {}, + "GoIdent": {}, + "GoQualifiedIdent": {}, + }) +} + +func newExampleProtogenPlugin(t *testing.T) *protogen.Plugin { + t.Helper() + + return newCompiledProtogenPlugin(t, []string{"internal/testproto/example/v1/example.proto"}, &protocompile.SourceResolver{ + ImportPaths: []string{repoRoot(t)}, + }) +} + +func TestCollectFileModel_ProtoSyntaxGate(t *testing.T) { + t.Run("accepts proto3", func(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/accept.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/accept;acceptv1";`, + `message PingRequest {}`, + `message PingResponse {}`, + `service AcceptService {`, + ` rpc Ping(PingRequest) returns (PingResponse);`, + `}`, + "", + }, "\n"), + }, "test/v1/accept.proto") + file := plugin.FilesByPath["test/v1/accept.proto"] + if file == nil { + t.Fatal("accept proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{Language: LanguageGo}) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if len(model.Services) != 1 { + t.Fatalf("service count = %d, want 1", len(model.Services)) + } + }) + + t.Run("rejects proto2", func(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/reject.proto": strings.Join([]string{ + `syntax = "proto2";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/reject;rejectv1";`, + `message PingRequest {}`, + `message PingResponse {}`, + `service RejectService {`, + ` rpc Ping(PingRequest) returns (PingResponse);`, + `}`, + "", + }, "\n"), + }, "test/v1/reject.proto") + file := plugin.FilesByPath["test/v1/reject.proto"] + if file == nil { + t.Fatal("reject proto file not found in plugin") + } + + _, err := CollectFileModel(file, Options{Language: LanguageGo}) + if err == nil { + t.Fatal("CollectFileModel unexpectedly succeeded for proto2 file") + } + if !strings.Contains(err.Error(), "only proto3 files are supported in MVP") { + t.Fatalf("CollectFileModel error = %v, want proto3 rejection", err) + } + }) +} + +func TestCollectFileModel_RejectsStreamingRPC(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/streaming.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/streaming;streamingv1";`, + `message StreamRequest {}`, + `message StreamResponse {}`, + `service StreamService {`, + ` rpc Watch(StreamRequest) returns (stream StreamResponse);`, + `}`, + "", + }, "\n"), + }, "test/v1/streaming.proto") + file := plugin.FilesByPath["test/v1/streaming.proto"] + if file == nil { + t.Fatal("streaming proto file not found in plugin") + } + + _, err := CollectFileModel(file, Options{Language: LanguageGo}) + if err == nil { + t.Fatal("CollectFileModel unexpectedly succeeded for streaming RPC") + } + if !strings.Contains(err.Error(), "streaming RPC is not supported") { + t.Fatalf("CollectFileModel error = %v, want streaming rejection", err) + } +} + +func TestCollectFileModel_FiltersHiddenMethodsAndFallsBackToServiceIcons(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/metadata.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/metadata;metadatav1";`, + `import "mcp/options/v1/options.proto";`, + `message Request {}`, + `message Response {}`, + `service MetadataService {`, + ` option (mcp.options.v1.service) = {`, + ` namespace: "metadata"`, + ` icons: [{`, + ` src: "https://example.com/service.svg"`, + ` mime_type: "image/svg+xml"`, + ` sizes: "64x64"`, + ` theme: "light"`, + ` }]`, + ` };`, + ``, + ` rpc Visible(Request) returns (Response) {`, + ` option (mcp.options.v1.method) = { title: "Visible tool" };`, + ` }`, + ``, + ` rpc Hidden(Request) returns (Response) {`, + ` option (mcp.options.v1.method) = { hidden: true };`, + ` }`, + ``, + ` rpc Deprecated(Request) returns (Response) {`, + ` option deprecated = true;`, + ` }`, + ``, + ` rpc OverrideIcon(Request) returns (Response) {`, + ` option (mcp.options.v1.method) = {`, + ` icons: [{`, + ` src: "https://example.com/method.png"`, + ` mime_type: "image/png"`, + ` sizes: "32x32"`, + ` theme: "dark"`, + ` }]`, + ` };`, + ` }`, + `}`, + "", + }, "\n"), + }, "test/v1/metadata.proto") + file := plugin.FilesByPath["test/v1/metadata.proto"] + if file == nil { + t.Fatal("metadata proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{Language: LanguageGo}) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if len(model.Services) != 1 { + t.Fatalf("service count = %d, want 1", len(model.Services)) + } + + service := model.Services[0] + if len(service.Methods) != 3 { + t.Fatalf("method count = %d, want 3 after hidden filtering", len(service.Methods)) + } + + methods := make(map[string]MethodModel, len(service.Methods)) + for _, method := range service.Methods { + methods[method.Name] = method + } + if _, ok := methods["Hidden"]; ok { + t.Fatal("hidden method should not be collected") + } + + visible, ok := methods["Visible"] + if !ok { + t.Fatal("visible method not collected") + } + if len(visible.Icons) != 1 { + t.Fatalf("visible method icon count = %d, want 1", len(visible.Icons)) + } + if got := visible.Icons[0].GetSrc(); got != "https://example.com/service.svg" { + t.Fatalf("visible method icon src = %q, want service fallback", got) + } + + deprecated, ok := methods["Deprecated"] + if !ok { + t.Fatal("deprecated method not collected") + } + if !deprecated.Deprecated { + t.Fatal("deprecated method should propagate deprecated metadata") + } + if !strings.Contains(deprecated.InputSchemaJSON, `"deprecated":true`) { + t.Fatalf("deprecated input schema should be marked deprecated: %s", deprecated.InputSchemaJSON) + } + if len(deprecated.Icons) != 1 || deprecated.Icons[0].GetSrc() != "https://example.com/service.svg" { + t.Fatalf("deprecated method should inherit service icon, got %+v", deprecated.Icons) + } + + override, ok := methods["OverrideIcon"] + if !ok { + t.Fatal("override icon method not collected") + } + if len(override.Icons) != 1 { + t.Fatalf("override method icon count = %d, want 1", len(override.Icons)) + } + if got := override.Icons[0].GetSrc(); got != "https://example.com/method.png" { + t.Fatalf("override method icon src = %q, want method-specific icon", got) + } +} + +func sourceFileDescriptors(files []*descriptorpb.FileDescriptorProto, paths ...string) []*descriptorpb.FileDescriptorProto { + wanted := make(map[string]bool, len(paths)) + for _, path := range paths { + wanted[path] = true + } + + var descriptors []*descriptorpb.FileDescriptorProto + for _, file := range files { + if wanted[file.GetName()] { + descriptors = append(descriptors, file) + } + } + return descriptors +} + +func repoRoot(t *testing.T) string { + t.Helper() + + _, filename, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("resolve current test file path") + } + + return filepath.Clean(filepath.Join(filepath.Dir(filename), "..", "..")) +} + +func newTempProtogenPlugin(t *testing.T, files map[string]string, filesToGenerate ...string) *protogen.Plugin { + t.Helper() + + if len(filesToGenerate) == 0 { + t.Fatal("newTempProtogenPlugin requires at least one file to generate") + } + + return newCompiledProtogenPlugin(t, filesToGenerate, protocompile.CompositeResolver{ + &protocompile.SourceResolver{Accessor: protocompile.SourceAccessorFromMap(files)}, + &protocompile.SourceResolver{ImportPaths: []string{repoRoot(t)}}, + }) +} + +func newCompiledProtogenPlugin(t *testing.T, filesToGenerate []string, resolver protocompile.Resolver) *protogen.Plugin { + t.Helper() + + compiler := protocompile.Compiler{ + Resolver: protocompile.WithStandardImports(resolver), + SourceInfoMode: protocompile.SourceInfoStandard, + } + + compiled, err := compiler.Compile(context.Background(), filesToGenerate...) + if err != nil { + t.Fatalf("compile proto descriptors for %v: %v", filesToGenerate, err) + } + + compiledDescriptors := make([]protoreflect.FileDescriptor, 0, len(compiled)) + for _, file := range compiled { + compiledDescriptors = append(compiledDescriptors, file) + } + descriptorProtos := normalizeDescriptorProtos(t, collectDescriptorProtos(compiledDescriptors)) + + request := &pluginpb.CodeGeneratorRequest{ + FileToGenerate: append([]string(nil), filesToGenerate...), + Parameter: proto.String("paths=source_relative"), + ProtoFile: descriptorProtos, + SourceFileDescriptors: sourceFileDescriptors(descriptorProtos, filesToGenerate...), + CompilerVersion: &pluginpb.Version{}, + } + + plugin, err := protogen.Options{}.New(request) + if err != nil { + t.Fatalf("protogen.Options.New for %v: %v", filesToGenerate, err) + } + + return plugin +} + +func collectDescriptorProtos(files []protoreflect.FileDescriptor) []*descriptorpb.FileDescriptorProto { + visited := make(map[string]bool) + descriptors := make([]*descriptorpb.FileDescriptorProto, 0, len(files)) + + var walk func(protoreflect.FileDescriptor) + walk = func(file protoreflect.FileDescriptor) { + if visited[file.Path()] { + return + } + visited[file.Path()] = true + + imports := file.Imports() + for i := 0; i < imports.Len(); i++ { + walk(imports.Get(i).FileDescriptor) + } + + descriptors = append(descriptors, protoutil.ProtoFromFileDescriptor(file)) + } + + for _, file := range files { + walk(file) + } + + return descriptors +} + +func normalizeDescriptorProtos(t *testing.T, files []*descriptorpb.FileDescriptorProto) []*descriptorpb.FileDescriptorProto { + t.Helper() + + normalized := make([]*descriptorpb.FileDescriptorProto, 0, len(files)) + for _, file := range files { + data, err := proto.Marshal(file) + if err != nil { + t.Fatalf("marshal descriptor proto %q: %v", file.GetName(), err) + } + + var clone descriptorpb.FileDescriptorProto + if err := proto.Unmarshal(data, &clone); err != nil { + t.Fatalf("unmarshal descriptor proto %q: %v", file.GetName(), err) + } + normalized = append(normalized, &clone) + } + + return normalized +} + +func assertIRTypeHasNoDisallowedFields(t *testing.T, typ reflect.Type, disallowed map[string]struct{}) { + t.Helper() + + visited := map[reflect.Type]bool{} + var walk func(reflect.Type) + walk = func(current reflect.Type) { + for current.Kind() == reflect.Pointer || current.Kind() == reflect.Slice || current.Kind() == reflect.Array { + current = current.Elem() + } + switch current.Kind() { + case reflect.Map: + walk(current.Key()) + walk(current.Elem()) + return + case reflect.Struct: + default: + return + } + + if visited[current] { + return + } + visited[current] = true + + for i := 0; i < current.NumField(); i++ { + field := current.Field(i) + fieldType := field.Type + baseType := fieldType + for baseType.Kind() == reflect.Pointer || baseType.Kind() == reflect.Slice || baseType.Kind() == reflect.Array { + baseType = baseType.Elem() + } + if _, ok := disallowed[baseType.PkgPath()+"."+baseType.Name()]; ok { + t.Fatalf("%s.%s uses disallowed IR field type %s", current.Name(), field.Name, fieldType) + } + walk(fieldType) + } + } + + walk(typ) +} + +func assertIRTypeHasNoFieldNames(t *testing.T, typ reflect.Type, disallowed map[string]struct{}) { + t.Helper() + + visited := map[reflect.Type]bool{} + var walk func(reflect.Type) + walk = func(current reflect.Type) { + for current.Kind() == reflect.Pointer || current.Kind() == reflect.Slice || current.Kind() == reflect.Array { + current = current.Elem() + } + switch current.Kind() { + case reflect.Map: + walk(current.Key()) + walk(current.Elem()) + return + case reflect.Struct: + default: + return + } + + if visited[current] { + return + } + visited[current] = true + + for i := 0; i < current.NumField(); i++ { + field := current.Field(i) + if _, ok := disallowed[field.Name]; ok { + t.Fatalf("%s.%s is Go-specific and should not be part of the shared IR", current.Name(), field.Name) + } + walk(field.Type) + } + } + + walk(typ) +} diff --git a/internal/codegen/generator.go b/internal/codegen/generator.go index 3ad9a7c..884b0b2 100644 --- a/internal/codegen/generator.go +++ b/internal/codegen/generator.go @@ -2,327 +2,149 @@ package codegen import ( "fmt" - "strings" + "path" - "github.com/easyp-tech/protoc-gen-mcp/internal/schema" - mcpoptionsv1 "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" - "github.com/google/jsonschema-go/jsonschema" "google.golang.org/protobuf/compiler/protogen" - "google.golang.org/protobuf/reflect/protoreflect" ) -type methodSpec struct { - Method *protogen.Method - Name string - Title string - Description string - Examples []string - Deprecated bool - InputSchemaJSON string - OutputSchemaJSON string - Annotations *mcpoptionsv1.ToolAnnotations - Icons []*mcpoptionsv1.Icon -} - -type serviceSpec struct { - Service *protogen.Service - Namespace string - Description string - Icons []*mcpoptionsv1.Icon - Methods []methodSpec -} - // Generate emits MCP bindings for protobuf services in generated files. -func Generate(plugin *protogen.Plugin) error { - for _, file := range plugin.Files { - if !file.Generate { - continue - } - - if file.Desc.Syntax() != protoreflect.Proto3 { - return fmt.Errorf("only proto3 files are supported in MVP: %s", file.Desc.Path()) +func Generate(plugin *protogen.Plugin, opts Options) error { + switch opts.Language { + case LanguageGo: + case LanguagePython: + if err := emitPythonSupportFiles(plugin); err != nil { + return err } + default: + return fmt.Errorf("unsupported lang %q", opts.Language) + } - serviceSpecs, err := collectFileSpecs(file) + pythonPackageInitFiles := map[string]bool{} + if opts.Language == LanguagePython { + models, orderedFiles, err := collectPythonModels(plugin, opts) if err != nil { return err } - if len(serviceSpecs) == 0 { - continue - } - - if err := generateFile(plugin, file, serviceSpecs); err != nil { - return err + for _, file := range orderedFiles { + model := models[file.Desc.Path()] + if !pythonModelRequiresOutput(model) { + continue + } + emitPythonPackageInitFile(plugin, pythonPackageInitFiles, pythonOutputPath(file)) + if err := renderPythonFile(plugin, model); err != nil { + return err + } } + return nil } - return nil -} - -func collectFileSpecs(file *protogen.File) ([]serviceSpec, error) { - serviceSpecs := make([]serviceSpec, 0, len(file.Services)) - - for _, service := range file.Services { - serviceMetadata, err := loadServiceMetadata(service) - if err != nil { - return nil, err + for _, file := range plugin.Files { + if !file.Generate { + continue } - spec := serviceSpec{ - Service: service, - Namespace: serviceMetadata.Namespace, - Description: serviceMetadata.Description, - Icons: serviceMetadata.Icons, + model, err := CollectFileModel(file, opts) + if err != nil { + return err } - for _, method := range service.Methods { - if method.Desc.IsStreamingClient() || method.Desc.IsStreamingServer() { - return nil, fmt.Errorf("streaming RPC is not supported: %s", method.Desc.FullName()) - } - - methodMetadata, err := loadMethodMetadata(method) - if err != nil { - return nil, err - } - if methodMetadata.Hidden || methodMetadata.Disabled { + switch opts.Language { + case LanguageGo: + if len(model.Services) == 0 { continue } - - inputSchema, err := generateSchema(method.Input) - if err != nil { - return nil, fmt.Errorf("input schema for %s: %w", method.Desc.FullName(), err) + if err := renderGoFile(plugin, model); err != nil { + return err } - if len(methodMetadata.Examples) > 0 { - inputSchema.Examples = make([]any, 0, len(methodMetadata.Examples)) - for _, example := range methodMetadata.Examples { - inputSchema.Examples = append(inputSchema.Examples, example) - } + case LanguagePython: + if !pythonModelRequiresOutput(model) { + continue } - - if methodMetadata.Deprecated { - inputSchema.Deprecated = true + emitPythonPackageInitFile(plugin, pythonPackageInitFiles, pythonOutputPath(file)) + if err := renderPythonFile(plugin, model); err != nil { + return err } + } + } - outputSchema, err := generateSchema(method.Output) - if err != nil { - return nil, fmt.Errorf("output schema for %s: %w", method.Desc.FullName(), err) - } + return nil +} - inputSchemaJSON, err := schema.MarshalJSON(inputSchema) - if err != nil { - return nil, fmt.Errorf("marshal input schema for %s: %w", method.Desc.FullName(), err) - } - outputSchemaJSON, err := schema.MarshalJSON(outputSchema) - if err != nil { - return nil, fmt.Errorf("marshal output schema for %s: %w", method.Desc.FullName(), err) - } +func collectPythonModels(plugin *protogen.Plugin, opts Options) (map[string]FileModel, []*protogen.File, error) { + models := make(map[string]FileModel) + orderedFiles := make([]*protogen.File, 0, len(plugin.Files)) - spec.Methods = append(spec.Methods, methodSpec{ - Method: method, - Name: methodMetadata.Name, - Title: methodMetadata.Title, - Description: methodMetadata.Description, - Examples: methodMetadata.Examples, - Deprecated: methodMetadata.Deprecated, - InputSchemaJSON: inputSchemaJSON, - OutputSchemaJSON: outputSchemaJSON, - Annotations: methodMetadata.Annotations, - Icons: methodMetadata.Icons, - }) + for _, file := range plugin.Files { + if !file.Generate { + continue } - if len(spec.Methods) > 0 { - serviceSpecs = append(serviceSpecs, spec) + model, err := CollectFileModel(file, opts) + if err != nil { + return nil, nil, err } + models[file.Desc.Path()] = model + orderedFiles = append(orderedFiles, file) } - for i := range serviceSpecs { - for j := range serviceSpecs[i].Methods { - if len(serviceSpecs[i].Methods[j].Icons) == 0 { - serviceSpecs[i].Methods[j].Icons = serviceSpecs[i].Icons - } + extraRefs := make(map[string]map[string]struct{}) + for _, file := range orderedFiles { + graph := models[file.Desc.Path()].PythonTypes + if graph == nil { + continue } - } - - return serviceSpecs, nil -} - -func generateSchema(message *protogen.Message) (*jsonschema.Schema, error) { - var fieldErr error - - generatedSchema, err := schema.GenerateMessageSchema(message, schema.Options{ - MessageMetadata: func(current *protogen.Message) schema.Metadata { - metadata, err := loadMessageMetadata(current) - if err != nil && fieldErr == nil { - fieldErr = err - } - commentMetadata := parseCommentBlock(current.Comments.Leading) - return schema.Metadata{ - Title: metadata.Title, - Description: metadata.Description, - Examples: commentMetadata.Examples, - TypedExamples: metadata.TypedExamples, - } - }, - FieldMetadata: func(field *protogen.Field) schema.FieldMetadata { - metadata, err := loadFieldMetadata(field) - if err != nil && fieldErr == nil { - fieldErr = err - } - return metadata.FieldMetadata - }, - EnumMetadata: func(enum *protogen.Enum) schema.EnumMetadata { - metadata, err := loadEnumMetadata(enum) - if err != nil && fieldErr == nil { - fieldErr = err - } - return schema.EnumMetadata{ - Title: metadata.Title, - Description: metadata.Description, - } - }, - EnumValueMetadata: func(enumValue *protogen.EnumValue) schema.EnumValueMetadata { - metadata, err := loadEnumValueMetadata(enumValue) - if err != nil && fieldErr == nil { - fieldErr = err + for _, typ := range graph.Types { + if typ.Owner.IsCurrentFile { + continue } - return schema.EnumValueMetadata{ - Description: metadata.Description, - Hidden: metadata.Hidden, + if extraRefs[typ.Owner.ProtoPath] == nil { + extraRefs[typ.Owner.ProtoPath] = make(map[string]struct{}) } - }, - }) - if err != nil { - return nil, err - } - if fieldErr != nil { - return nil, fieldErr - } - - return generatedSchema, nil -} - -func generateFile(plugin *protogen.Plugin, file *protogen.File, services []serviceSpec) error { - filename := file.GeneratedFilenamePrefix + ".mcp.go" - generated := plugin.NewGeneratedFile(filename, file.GoImportPath) - - generated.P("// Code generated by protoc-gen-mcp. DO NOT EDIT.") - generated.P("// source: ", file.Desc.Path()) - generated.P() - generated.P("package ", file.GoPackageName) - generated.P() - - contextIdent := generated.QualifiedGoIdent(protogen.GoImportPath("context").Ident("Context")) - errorsIdent := generated.QualifiedGoIdent(protogen.GoImportPath("errors").Ident("New")) - mcpServerIdent := generated.QualifiedGoIdent(protogen.GoImportPath("github.com/modelcontextprotocol/go-sdk/mcp").Ident("Server")) - mcpruntimeImport := protogen.GoImportPath("github.com/easyp-tech/protoc-gen-mcp/mcpruntime") - registerOptionIdent := generated.QualifiedGoIdent(mcpruntimeImport.Ident("RegisterOption")) - registerToolIdent := generated.QualifiedGoIdent(mcpruntimeImport.Ident("RegisterProtoTool")) - toolSpecIdent := generated.QualifiedGoIdent(mcpruntimeImport.Ident("ToolSpec")) - - for _, service := range services { - interfaceName := service.Service.GoName + "ToolHandler" - generated.P("// ", interfaceName, " defines the business logic required by generated MCP tools.") - generated.P("type ", interfaceName, " interface {") - for _, method := range service.Methods { - inputType := generated.QualifiedGoIdent(method.Method.Input.GoIdent) - outputType := generated.QualifiedGoIdent(method.Method.Output.GoIdent) - generated.P(method.Method.GoName, "(ctx ", contextIdent, ", req *", inputType, ") (*", outputType, ", error)") + extraRefs[typ.Owner.ProtoPath][typ.ProtoFullName] = struct{}{} } - generated.P("}") - generated.P() + } - registerName := "Register" + service.Service.GoName + "Tools" - generated.P("// ", registerName, " registers generated MCP tools for ", service.Service.GoName, ".") - generated.P("func ", registerName, "(server *", mcpServerIdent, ", impl ", interfaceName, ", opts ...", registerOptionIdent, ") error {") - generated.P("if impl == nil {") - generated.P("return ", errorsIdent, "(\"", registerName, ": impl is nil\")") - generated.P("}") - for _, method := range service.Methods { - specName := service.Service.GoName + "_" + method.Method.GoName + "_ToolSpec" - generated.P("if err := ", registerToolIdent, "(server, ", toolSpecIdent, "[*", generated.QualifiedGoIdent(method.Method.Input.GoIdent), ", *", generated.QualifiedGoIdent(method.Method.Output.GoIdent), "]{") - generated.P("Name: ", quote(method.Name), ",") - generated.P("Title: ", quote(method.Title), ",") - generated.P("Description: ", quote(method.Description), ",") - generated.P("Namespace: ", quote(service.Namespace), ",") - generated.P("InputSchemaJSON: ", specName, "InputSchemaJSON,") - generated.P("OutputSchemaJSON: ", specName, "OutputSchemaJSON,") - generated.P("Annotations: ", stringifyAnnotations(generated, method.Annotations), ",") - generated.P("Icons: ", stringifyIcons(generated, method.Icons), ",") - generated.P("NewRequest: func() *", generated.QualifiedGoIdent(method.Method.Input.GoIdent), " { return &", generated.QualifiedGoIdent(method.Method.Input.GoIdent), "{} },") - generated.P("NewResponse: func() *", generated.QualifiedGoIdent(method.Method.Output.GoIdent), " { return &", generated.QualifiedGoIdent(method.Method.Output.GoIdent), "{} },") - generated.P("Handler: impl.", method.Method.GoName, ",") - generated.P("}, opts...); err != nil {") - generated.P("return err") - generated.P("}") + for _, file := range orderedFiles { + refs := extraRefs[file.Desc.Path()] + if len(refs) == 0 { + continue } - generated.P("return nil") - generated.P("}") - generated.P() - - for _, method := range service.Methods { - specName := service.Service.GoName + "_" + method.Method.GoName + "_ToolSpec" - generated.P("const ", specName, "InputSchemaJSON = ", quote(method.InputSchemaJSON)) - generated.P() - generated.P("const ", specName, "OutputSchemaJSON = ", quote(method.OutputSchemaJSON)) - generated.P() + model := models[file.Desc.Path()] + if err := augmentPythonModelWithCurrentTypeRefs(file, &model, refs); err != nil { + return nil, nil, err } + models[file.Desc.Path()] = model } - return nil -} - -func quote(value string) string { - return fmt.Sprintf("%q", value) + return models, orderedFiles, nil } -func stringifyAnnotations(generated *protogen.GeneratedFile, ann *mcpoptionsv1.ToolAnnotations) string { - if ann == nil { - return "nil" +func pythonModelRequiresOutput(model FileModel) bool { + if len(model.Services) > 0 { + return true } - mcpAnnIdent := generated.QualifiedGoIdent(protogen.GoImportPath("github.com/modelcontextprotocol/go-sdk/mcp").Ident("ToolAnnotations")) - - var fields []string - if ann.DestructiveHint != nil { - protoBoolIdent := generated.QualifiedGoIdent(protogen.GoImportPath("google.golang.org/protobuf/proto").Ident("Bool")) - fields = append(fields, fmt.Sprintf("DestructiveHint: %s(%t),", protoBoolIdent, *ann.DestructiveHint)) - } - if ann.IdempotentHint != false { - fields = append(fields, fmt.Sprintf("IdempotentHint: %t,", ann.IdempotentHint)) - } - if ann.OpenWorldHint != nil { - protoBoolIdent := generated.QualifiedGoIdent(protogen.GoImportPath("google.golang.org/protobuf/proto").Ident("Bool")) - fields = append(fields, fmt.Sprintf("OpenWorldHint: %s(%t),", protoBoolIdent, *ann.OpenWorldHint)) + if model.PythonTypes == nil { + return false } - if ann.Title != "" { - protoStringIdent := generated.QualifiedGoIdent(protogen.GoImportPath("google.golang.org/protobuf/proto").Ident("String")) - fields = append(fields, fmt.Sprintf("Title: %s(%q),", protoStringIdent, ann.Title)) - } - - if len(fields) == 0 { - return "&" + mcpAnnIdent + "{}" + for _, typ := range model.PythonTypes.Types { + if typ.Owner.IsCurrentFile { + return true + } } - return "&" + mcpAnnIdent + "{" + strings.Join(fields, " ") + "}" + return false } -func stringifyIcons(generated *protogen.GeneratedFile, icons []*mcpoptionsv1.Icon) string { - if len(icons) == 0 { - return "nil" +func emitPythonPackageInitFile(plugin *protogen.Plugin, emitted map[string]bool, outputPath string) { + dir := path.Dir(outputPath) + if dir == "." || dir == "/" { + return } - mcpIconIdent := generated.QualifiedGoIdent(protogen.GoImportPath("github.com/modelcontextprotocol/go-sdk/mcp").Ident("Icon")) - - var items []string - for _, icon := range icons { - sizesStr := "nil" - if len(icon.GetSizes()) > 0 { - var quotedSizes []string - for _, s := range icon.GetSizes() { - quotedSizes = append(quotedSizes, fmt.Sprintf("%q", s)) - } - sizesStr = "[]string{" + strings.Join(quotedSizes, ", ") + "}" - } - items = append(items, fmt.Sprintf("%s{Source: %q, MIMEType: %q, Sizes: %s, Theme: %q},", - mcpIconIdent, icon.GetSrc(), icon.GetMimeType(), sizesStr, icon.GetTheme())) + filename := path.Join(dir, "__init__.py") + if emitted[filename] { + return } - return "[]" + mcpIconIdent + "{" + strings.Join(items, " ") + "}" + emitted[filename] = true + + generated := plugin.NewGeneratedFile(filename, "") + generated.P("# Code generated by protoc-gen-mcp. DO NOT EDIT.") } diff --git a/internal/codegen/generator_test.go b/internal/codegen/generator_test.go index 4581c82..b915456 100644 --- a/internal/codegen/generator_test.go +++ b/internal/codegen/generator_test.go @@ -1,53 +1,284 @@ -package codegen_test +package codegen import ( "bytes" + "fmt" "os" "os/exec" "path/filepath" - "runtime" "strings" "testing" + + "google.golang.org/protobuf/compiler/protogen" ) -func TestEasypGenerateExampleGolden(t *testing.T) { - root := repoRoot(t) +func TestGenerateExampleGolden(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + if err := Generate(plugin, Options{Language: LanguageGo}); err != nil { + t.Fatalf("Generate: %v", err) + } - output := runEasyp(t, root, filepath.Join(root, "easyp.test.yaml"), "generate", "-p", "internal/testproto", "-r", ".") - if strings.TrimSpace(output) == "" { - t.Fatal("easyp generate returned empty output") + got := generatedFileContent(t, plugin, "internal/testproto/example/v1/example.mcp.go") + want := readExampleGolden(t, repoRoot(t)) + if !bytes.Equal(got, want) { + t.Fatalf("fresh Go renderer output differs from golden:\n%s", diffBytes(want, got)) } +} - gotPath := filepath.Join(root, "internal/testproto/example/v1/example.mcp.go") - wantPath := filepath.Join(root, "testdata/golden/example.mcp.go.golden") +func TestGeneratePythonExampleGolden(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + if err := Generate(plugin, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }); err != nil { + t.Fatalf("Generate: %v", err) + } - got, err := os.ReadFile(gotPath) - if err != nil { - t.Fatalf("read generated file %q: %v", gotPath, err) + got := generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py") + want := readExamplePythonGolden(t, repoRoot(t)) + if !bytes.Equal(got, want) { + t.Fatalf("fresh Python renderer output differs from golden:\n%s", diffBytes(want, got)) } +} - want, err := os.ReadFile(wantPath) - if err != nil { - t.Fatalf("read golden file %q: %v", wantPath, err) +func TestGenerate_PythonEmitsMCPNamespaceBridge(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + if err := Generate(plugin, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }); err != nil { + t.Fatalf("Generate: %v", err) } - if !bytes.Equal(got, want) { - t.Fatalf("generated file %s does not match golden snapshot %s", gotPath, wantPath) + generated := string(generatedFileContent(t, plugin, "mcp/__init__.py")) + wantSnippets := []string{ + `"""Bridge generated mcp.options.* modules with the official MCP SDK package."""`, + "from pkgutil import extend_path", + "__path__ = extend_path(__path__, __name__)", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated namespace bridge missing snippet %q\n%s", snippet, generated) + } } } -func TestGeneratedSchemasUseInterpretedStringLiterals(t *testing.T) { - root := repoRoot(t) +func TestGenerate_PythonEmitsPackageInitFiles(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/service.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/service;servicev1";`, + `message PingRequest {}`, + `message PingResponse {}`, + `service PingAPI {`, + ` rpc Ping(PingRequest) returns (PingResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/service.proto") + + if err := Generate(plugin, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }); err != nil { + t.Fatalf("Generate: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "test/v1/__init__.py")) + if !strings.Contains(generated, "# Code generated by protoc-gen-mcp. DO NOT EDIT.") { + t.Fatalf("package __init__.py missing generated marker\n%s", generated) + } +} + +func TestGenerate_PythonEmitsCrossFilePublicTypeModules(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/shared.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/shared;sharedv1";`, + `message SharedRequest {}`, + `message SharedResponse {}`, + ``, + }, "\n"), + "test/v1/service.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/service;servicev1";`, + `import "test/v1/shared.proto";`, + `service CrossFileAPI {`, + ` rpc UseShared(SharedRequest) returns (SharedResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/shared.proto", "test/v1/service.proto") + + if err := Generate(plugin, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }); err != nil { + t.Fatalf("Generate: %v", err) + } + + serviceGenerated := string(generatedFileContent(t, plugin, "test/v1/service_mcp.py")) + if !strings.Contains(serviceGenerated, "from test.v1 import shared_mcp as test_v1_shared_mcp") { + t.Fatalf("service module must import generated shared public module\n%s", serviceGenerated) + } + + sharedGenerated := string(generatedFileContent(t, plugin, "test/v1/shared_mcp.py")) + for _, snippet := range []string{ + "class SharedRequest:", + "class SharedResponse:", + } { + if !strings.Contains(sharedGenerated, snippet) { + t.Fatalf("shared module missing public dataclass snippet %q\n%s", snippet, sharedGenerated) + } + } +} + +func TestGenerate_PythonEmitsPublicTypesForHiddenOnlyServiceImports(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/shared.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/shared;sharedv1";`, + `import "mcp/options/v1/options.proto";`, + `message SharedRequest {`, + ` string label = 1;`, + `}`, + `message SharedResponse {`, + ` string label = 1;`, + `}`, + `service SharedAPI {`, + ` rpc HiddenEcho(SharedRequest) returns (SharedResponse) {`, + ` option (mcp.options.v1.method) = { hidden: true };`, + ` }`, + `}`, + ``, + }, "\n"), + "test/v1/service.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/service;servicev1";`, + `import "test/v1/shared.proto";`, + `message UseSharedRequest {`, + ` SharedRequest request = 1;`, + `}`, + `message UseSharedResponse {`, + ` SharedResponse response = 1;`, + `}`, + `service VisibleAPI {`, + ` rpc UseShared(UseSharedRequest) returns (UseSharedResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/shared.proto", "test/v1/service.proto") + + if err := Generate(plugin, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }); err != nil { + t.Fatalf("Generate: %v", err) + } + + sharedGenerated := string(generatedFileContent(t, plugin, "test/v1/shared_mcp.py")) + for _, snippet := range []string{ + "class SharedRequest:", + "class SharedResponse:", + } { + if !strings.Contains(sharedGenerated, snippet) { + t.Fatalf("shared hidden-only module missing snippet %q\n%s", snippet, sharedGenerated) + } + } + + serviceGenerated := string(generatedFileContent(t, plugin, "test/v1/service_mcp.py")) + if !strings.Contains(serviceGenerated, "from test.v1 import shared_mcp as") { + t.Fatalf("service module must import hidden-only shared public module\n%s", serviceGenerated) + } +} + +func TestGenerate_PythonEmitsCurrentFilePublicTypesUnusedByLocalServices(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/shared.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/shared;sharedv1";`, + `message SharedRequest {}`, + `message SharedResponse {}`, + `message ExportedDetails {`, + ` string label = 1;`, + `}`, + `service SharedAPI {`, + ` rpc Echo(SharedRequest) returns (SharedResponse);`, + `}`, + ``, + }, "\n"), + "test/v1/service.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/service;servicev1";`, + `import "test/v1/shared.proto";`, + `message UseSharedRequest {`, + ` ExportedDetails details = 1;`, + `}`, + `message UseSharedResponse {`, + ` ExportedDetails details = 1;`, + `}`, + `service VisibleAPI {`, + ` rpc UseShared(UseSharedRequest) returns (UseSharedResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/shared.proto", "test/v1/service.proto") + + if err := Generate(plugin, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }); err != nil { + t.Fatalf("Generate: %v", err) + } + + sharedGenerated := string(generatedFileContent(t, plugin, "test/v1/shared_mcp.py")) + if !strings.Contains(sharedGenerated, "class ExportedDetails:") { + t.Fatalf("shared module missing unrelated exported public type\n%s", sharedGenerated) + } - runEasyp(t, root, filepath.Join(root, "easyp.test.yaml"), "generate", "-p", "internal/testproto", "-r", ".") + serviceGenerated := string(generatedFileContent(t, plugin, "test/v1/service_mcp.py")) + for _, snippet := range []string{ + "from test.v1 import shared_mcp as", + ".ExportedDetails", + } { + if !strings.Contains(serviceGenerated, snippet) { + t.Fatalf("service module missing cross-file public type snippet %q\n%s", snippet, serviceGenerated) + } + } +} - gotPath := filepath.Join(root, "internal/testproto/example/v1/example.mcp.go") - got, err := os.ReadFile(gotPath) +func TestGoRendererMatchesExistingGolden(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + model, err := CollectFileModel(file, Options{Language: LanguageGo}) if err != nil { - t.Fatalf("read generated file %q: %v", gotPath, err) + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderGoFile(plugin, model); err != nil { + t.Fatalf("renderGoFile: %v", err) + } + + got := generatedFileContent(t, plugin, "internal/testproto/example/v1/example.mcp.go") + want := readExampleGolden(t, repoRoot(t)) + if !bytes.Equal(got, want) { + t.Fatalf("fresh Go renderer output differs from golden:\n%s", diffBytes(want, got)) + } +} + +func TestGeneratedSchemasUseInterpretedStringLiterals(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + if err := Generate(plugin, Options{Language: LanguageGo}); err != nil { + t.Fatalf("Generate: %v", err) } - generated := string(got) + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example.mcp.go")) if strings.Contains(generated, "InputSchemaJSON = `") || strings.Contains(generated, "OutputSchemaJSON = `") { t.Fatalf("generated schema constants must not use raw string literals:\n%s", generated) } @@ -56,48 +287,214 @@ func TestGeneratedSchemasUseInterpretedStringLiterals(t *testing.T) { } } +func TestGenerate_GoEmitsReadOnlyHintAnnotation(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/annotations.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `import "mcp/options/v1/options.proto";`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/annotations;annotationsv1";`, + `message AnnotatedRequest {}`, + `message AnnotatedResponse {}`, + `service AnnotatedAPI {`, + ` rpc ReadThing(AnnotatedRequest) returns (AnnotatedResponse) {`, + ` option (mcp.options.v1.method) = {`, + ` annotations: {`, + ` read_only_hint: true`, + ` idempotent_hint: true`, + ` open_world_hint: true`, + ` }`, + ` };`, + ` }`, + `}`, + ``, + }, "\n"), + }, "test/v1/annotations.proto") + + if err := Generate(plugin, Options{Language: LanguageGo}); err != nil { + t.Fatalf("Generate: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "test/v1/annotations.mcp.go")) + for _, snippet := range []string{ + "ReadOnlyHint: true,", + "IdempotentHint: true,", + "OpenWorldHint: proto.Bool(true)", + } { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated go missing annotation snippet %q\n%s", snippet, generated) + } + } +} + func TestEasypGenerateUnsupportedFails(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/unsupported.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/unsupported;unsupportedv1";`, + `import "google/protobuf/type.proto";`, + `message BuildUnsupportedRequest {`, + ` google.protobuf.Type payload = 1;`, + `}`, + `message BuildUnsupportedResponse {}`, + `service UnsupportedAPI {`, + ` rpc BuildUnsupported(BuildUnsupportedRequest) returns (BuildUnsupportedResponse);`, + `}`, + "", + }, "\n"), + }, "test/v1/unsupported.proto") + + err := Generate(plugin, Options{Language: LanguageGo}) + if err == nil { + t.Fatal("Generate unexpectedly succeeded") + } + + if !strings.Contains(err.Error(), `well-known type "google.protobuf.Type" is not supported`) { + t.Fatalf("unexpected generator failure: %v", err) + } +} + +func TestGenerate_PythonUnsupportedDescriptorFails(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/unsupported.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/unsupported;unsupportedv1";`, + `import "google/protobuf/type.proto";`, + `message BuildUnsupportedRequest {`, + ` google.protobuf.Type payload = 1;`, + `}`, + `message BuildUnsupportedResponse {}`, + `service UnsupportedAPI {`, + ` rpc BuildUnsupported(BuildUnsupportedRequest) returns (BuildUnsupportedResponse);`, + `}`, + "", + }, "\n"), + }, "test/v1/unsupported.proto") + + err := Generate(plugin, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err == nil { + t.Fatal("Generate unexpectedly succeeded") + } + + if !strings.Contains(err.Error(), `well-known type "google.protobuf.Type" is not supported`) { + t.Fatalf("unexpected generator failure: %v", err) + } +} + +func TestGenerate_PythonUnsupportedRuntimeBetterprotoFails(t *testing.T) { root := repoRoot(t) - configPath := filepath.Join(t.TempDir(), "easyp.unsupported.yaml") - config := strings.Join([]string{ + output, err := runEasypGenerateExpectFailure(t, root, strings.Join([]string{ "generate:", " inputs:", " - directory:", - " path: testdata/unsupported", + " path: internal/testproto", " root: \".\"", " plugins:", " - command: [\"go\", \"run\", \"./cmd/protoc-gen-mcp\"]", " out: .", " opts:", " paths: source_relative", + " lang: python", + " python_runtime: betterproto", "", - }, "\n") - if err := os.WriteFile(configPath, []byte(config), 0o600); err != nil { - t.Fatalf("write temp config: %v", err) + }, "\n"), "internal/testproto") + if err == nil { + t.Fatal("easyp generate unexpectedly succeeded") } - cmd := exec.Command("easyp", "--cfg", configPath, "generate", "-p", "testdata/unsupported", "-r", ".") - cmd.Dir = root - output, err := cmd.CombinedOutput() + if !strings.Contains(output, `unsupported python_runtime "betterproto"`) { + t.Fatalf("unexpected easyp failure output:\n%s", output) + } +} + +func TestGenerate_PythonUnsupportedRuntimeGrpclibFails(t *testing.T) { + root := repoRoot(t) + + output, err := runEasypGenerateExpectFailure(t, root, strings.Join([]string{ + "generate:", + " inputs:", + " - directory:", + " path: internal/testproto", + " root: \".\"", + " plugins:", + " - command: [\"go\", \"run\", \"./cmd/protoc-gen-mcp\"]", + " out: .", + " opts:", + " paths: source_relative", + " lang: python", + " python_runtime: grpclib", + "", + }, "\n"), "internal/testproto") if err == nil { - t.Fatalf("easyp generate unexpectedly succeeded:\n%s", output) + t.Fatal("easyp generate unexpectedly succeeded") } - if !strings.Contains(string(output), `well-known type "google.protobuf.Type" is not supported`) { + if !strings.Contains(output, `unsupported python_runtime "grpclib"`) { t.Fatalf("unexpected easyp failure output:\n%s", output) } } -func repoRoot(t *testing.T) string { - t.Helper() +func TestGenerate_PythonStreamingRPCFails(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/streaming.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/streaming;streamingv1";`, + `message StreamingRequest {}`, + `message StreamingResponse {}`, + `service StreamingAPI {`, + ` rpc Watch(StreamingRequest) returns (stream StreamingResponse);`, + `}`, + "", + }, "\n"), + }, "test/v1/streaming.proto") + + err := Generate(plugin, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err == nil { + t.Fatal("Generate unexpectedly succeeded") + } + + if !strings.Contains(err.Error(), `streaming RPC is not supported`) { + t.Fatalf("unexpected generator failure: %v", err) + } +} - _, filename, _, ok := runtime.Caller(0) - if !ok { - t.Fatal("resolve current test file path") +func TestGenerate_RejectsUnknownCustomParamAndAllowsBuiltInParams(t *testing.T) { + root := repoRoot(t) + + output, err := runEasypGenerateExpectFailure(t, root, strings.Join([]string{ + "generate:", + " inputs:", + " - directory:", + " path: internal/testproto", + " root: \".\"", + " plugins:", + " - command: [\"go\", \"run\", \"./cmd/protoc-gen-mcp\"]", + " out: .", + " opts:", + " paths: source_relative", + " Minternal/testproto/example/v1/example.proto: example.com/project/internal/testproto/example/v1;examplev1", + " apilevelMinternal/testproto/example/v1/example.proto: API_OPEN", + " lang: python", + " python_runtme: google.protobuf", + "", + }, "\n"), "internal/testproto") + if err == nil { + t.Fatal("easyp generate unexpectedly succeeded") } - return filepath.Clean(filepath.Join(filepath.Dir(filename), "..", "..")) + if !strings.Contains(output, `unknown protoc-gen-mcp option "python_runtme"`) { + t.Fatalf("unexpected easyp failure output:\n%s", output) + } } func runEasyp(t *testing.T, dir string, config string, args ...string) string { @@ -118,3 +515,78 @@ func runEasyp(t *testing.T, dir string, config string, args ...string) string { return string(output) } + +func runEasypGenerateExpectFailure(t *testing.T, dir string, config string, pkg string) (string, error) { + t.Helper() + + if _, err := exec.LookPath("easyp"); err != nil { + t.Fatalf("easyp not found in PATH: %v", err) + } + + configPath := filepath.Join(t.TempDir(), "easyp.generate.yaml") + if err := os.WriteFile(configPath, []byte(config), 0o600); err != nil { + t.Fatalf("write temp config: %v", err) + } + + cmd := exec.Command("easyp", "--cfg", configPath, "generate", "-p", pkg, "-r", ".") + cmd.Dir = dir + output, err := cmd.CombinedOutput() + return string(output), err +} + +func readExampleGolden(t *testing.T, root string) []byte { + t.Helper() + + wantPath := filepath.Join(root, "testdata/golden/example.mcp.go.golden") + want, err := os.ReadFile(wantPath) + if err != nil { + t.Fatalf("read golden file %q: %v", wantPath, err) + } + return want +} + +func readExamplePythonGolden(t *testing.T, root string) []byte { + t.Helper() + + wantPath := filepath.Join(root, "testdata/golden/example_mcp.py.golden") + want, err := os.ReadFile(wantPath) + if err != nil { + t.Fatalf("read golden file %q: %v", wantPath, err) + } + return want +} + +func generatedFileContent(t *testing.T, plugin *protogen.Plugin, path string) []byte { + t.Helper() + + for _, file := range plugin.Response().GetFile() { + if file.GetName() == path { + return []byte(file.GetContent()) + } + } + + t.Fatalf("generated file %q not found in plugin response", path) + return nil +} + +func diffBytes(want []byte, got []byte) string { + return fmt.Sprintf("--- want\n+++ got\n%s", firstDiffLine(string(want), string(got))) +} + +func firstDiffLine(want string, got string) string { + wantLines := strings.Split(want, "\n") + gotLines := strings.Split(got, "\n") + limit := len(wantLines) + if len(gotLines) < limit { + limit = len(gotLines) + } + for i := 0; i < limit; i++ { + if wantLines[i] != gotLines[i] { + return fmt.Sprintf("@@ line %d @@\n- %s\n+ %s\n", i+1, wantLines[i], gotLines[i]) + } + } + if len(wantLines) != len(gotLines) { + return fmt.Sprintf("@@ line count @@\n- %d lines\n+ %d lines\n", len(wantLines), len(gotLines)) + } + return "(contents differ, but no differing line was isolated)" +} diff --git a/internal/codegen/model.go b/internal/codegen/model.go new file mode 100644 index 0000000..5db1dc4 --- /dev/null +++ b/internal/codegen/model.go @@ -0,0 +1,44 @@ +package codegen + +import ( + mcpoptionsv1 "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" +) + +// FileModel is the shared semantic IR entrypoint for language-specific renderers. +type FileModel struct { + ProtoPath string + GeneratedFilenamePrefix string + Options Options + PythonTypes *PythonTypeGraph + Services []ServiceModel +} + +type ServiceModel struct { + ProtoFullName string + ProtoName string + Namespace string + Description string + Icons []*mcpoptionsv1.Icon + Methods []MethodModel +} + +type MethodModel struct { + ProtoFullName string + ProtoName string + Name string + Title string + Description string + Examples []string + Deprecated bool + Input TypeRef + Output TypeRef + InputSchemaJSON string + OutputSchemaJSON string + Annotations *mcpoptionsv1.ToolAnnotations + Icons []*mcpoptionsv1.Icon +} + +type TypeRef struct { + ProtoFullName string + ProtoDisplayName string +} diff --git a/internal/codegen/options.go b/internal/codegen/options.go new file mode 100644 index 0000000..5670cf7 --- /dev/null +++ b/internal/codegen/options.go @@ -0,0 +1,122 @@ +package codegen + +import ( + "fmt" + "strings" +) + +type Language string + +const ( + LanguageGo Language = "go" + LanguagePython Language = "python" +) + +type PythonRuntime string + +const ( + PythonRuntimeGoogleProtobuf PythonRuntime = "google.protobuf" + PythonRuntimeBetterproto PythonRuntime = "betterproto" + PythonRuntimeGrpclib PythonRuntime = "grpclib" +) + +type Options struct { + Language Language + PythonRuntime PythonRuntime +} + +type OptionsParser struct { + opts Options + sawPythonRuntime bool +} + +func NewOptionsParser() *OptionsParser { + return &OptionsParser{ + opts: Options{ + Language: LanguageGo, + }, + } +} + +func (p *OptionsParser) Set(name, value string) error { + if isProtogenManagedParam(name) { + return nil + } + + switch name { + case "lang": + p.opts.Language = Language(value) + case "python_runtime": + p.opts.PythonRuntime = PythonRuntime(value) + p.sawPythonRuntime = true + default: + return fmt.Errorf("unknown protoc-gen-mcp option %q", name) + } + + return nil +} + +func (p *OptionsParser) Options() (Options, error) { + switch p.opts.Language { + case LanguageGo: + if p.sawPythonRuntime { + return Options{}, fmt.Errorf("python_runtime is only supported when lang=python") + } + case LanguagePython: + if !p.sawPythonRuntime { + p.opts.PythonRuntime = PythonRuntimeGoogleProtobuf + } + switch p.opts.PythonRuntime { + case PythonRuntimeGoogleProtobuf: + default: + return Options{}, fmt.Errorf("unsupported python_runtime %q", p.opts.PythonRuntime) + } + default: + return Options{}, fmt.Errorf("unsupported lang %q", p.opts.Language) + } + + return p.opts, nil +} + +// ParseOptions exists for direct parser tests and any non-protogen callers that +// need the same option semantics as the plugin entrypoint. +func ParseOptions(raw string) (Options, error) { + parser := NewOptionsParser() + + for _, param := range strings.Split(raw, ",") { + param = strings.TrimSpace(param) + if param == "" { + continue + } + + name, value, hasValue := strings.Cut(param, "=") + if !hasValue { + value = "" + } + + if isProtogenManagedParam(name) { + continue + } + + if err := parser.Set(name, value); err != nil { + return Options{}, err + } + } + + return parser.Options() +} + +func isProtogenManagedParam(name string) bool { + switch { + case name == "": + return true + case name == "module", name == "paths", name == "annotate_code", name == "default_api_level": + return true + case strings.HasPrefix(name, "M"): + return true + case strings.HasPrefix(name, "apilevelM"): + return true + default: + return false + } +} diff --git a/internal/codegen/options_test.go b/internal/codegen/options_test.go new file mode 100644 index 0000000..2f2ceb3 --- /dev/null +++ b/internal/codegen/options_test.go @@ -0,0 +1,149 @@ +package codegen + +import "testing" + +func TestParseOptions_DefaultLangGo(t *testing.T) { + opts, err := ParseOptions("") + if err != nil { + t.Fatalf("ParseOptions returned error: %v", err) + } + + if opts.Language != LanguageGo { + t.Fatalf("Language = %q, want %q", opts.Language, LanguageGo) + } + if opts.PythonRuntime != "" { + t.Fatalf("PythonRuntime = %q, want empty", opts.PythonRuntime) + } +} + +func TestParseOptions_PythonDefaultsRuntime(t *testing.T) { + opts, err := ParseOptions("lang=python") + if err != nil { + t.Fatalf("ParseOptions returned error: %v", err) + } + + if opts.Language != LanguagePython { + t.Fatalf("Language = %q, want %q", opts.Language, LanguagePython) + } + if opts.PythonRuntime != PythonRuntimeGoogleProtobuf { + t.Fatalf("PythonRuntime = %q, want %q", opts.PythonRuntime, PythonRuntimeGoogleProtobuf) + } +} + +func TestParseOptions_PythonExplicitGoogleProtobufRuntime(t *testing.T) { + opts, err := ParseOptions("lang=python,python_runtime=google.protobuf") + if err != nil { + t.Fatalf("ParseOptions returned error: %v", err) + } + + if opts.Language != LanguagePython { + t.Fatalf("Language = %q, want %q", opts.Language, LanguagePython) + } + if opts.PythonRuntime != PythonRuntimeGoogleProtobuf { + t.Fatalf("PythonRuntime = %q, want %q", opts.PythonRuntime, PythonRuntimeGoogleProtobuf) + } +} + +func TestParseOptions_PythonRuntimeConstants(t *testing.T) { + if PythonRuntimeGoogleProtobuf != "google.protobuf" { + t.Fatalf("PythonRuntimeGoogleProtobuf = %q, want %q", PythonRuntimeGoogleProtobuf, "google.protobuf") + } + if PythonRuntimeBetterproto != "betterproto" { + t.Fatalf("PythonRuntimeBetterproto = %q, want %q", PythonRuntimeBetterproto, "betterproto") + } + if PythonRuntimeGrpclib != "grpclib" { + t.Fatalf("PythonRuntimeGrpclib = %q, want %q", PythonRuntimeGrpclib, "grpclib") + } +} + +func TestParseOptions_PythonRuntimeRejectedForGo(t *testing.T) { + _, err := ParseOptions("lang=go,python_runtime=google.protobuf") + if err == nil { + t.Fatal("ParseOptions succeeded, want error") + } +} + +func TestParseOptions_RejectsUnsupportedPythonRuntime(t *testing.T) { + tests := []struct { + name string + runtime PythonRuntime + }{ + { + name: "betterproto", + runtime: PythonRuntimeBetterproto, + }, + { + name: "grpclib", + runtime: PythonRuntimeGrpclib, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := ParseOptions("lang=python,python_runtime=" + string(tt.runtime)) + if err == nil { + t.Fatal("ParseOptions succeeded, want error") + } + }) + } +} + +func TestParseOptions_IgnoresNonMCPParams(t *testing.T) { + opts, err := ParseOptions("paths=source_relative,module=example.com/project,Mfoo.proto=example.com/project/foo,apilevelMbar.proto=API_OPEN,lang=python") + if err != nil { + t.Fatalf("ParseOptions returned error: %v", err) + } + + if opts.Language != LanguagePython { + t.Fatalf("Language = %q, want %q", opts.Language, LanguagePython) + } + if opts.PythonRuntime != PythonRuntimeGoogleProtobuf { + t.Fatalf("PythonRuntime = %q, want %q", opts.PythonRuntime, PythonRuntimeGoogleProtobuf) + } +} + +func TestOptionsParserSet_IgnoresProtogenManagedParams(t *testing.T) { + parser := NewOptionsParser() + + params := []struct { + name string + value string + }{ + {name: "paths", value: "source_relative"}, + {name: "module", value: "example.com/project"}, + {name: "Mfoo.proto", value: "example.com/project/foo"}, + {name: "apilevelMbar.proto", value: "API_OPEN"}, + } + + for _, param := range params { + if err := parser.Set(param.name, param.value); err != nil { + t.Fatalf("Set(%q, %q) returned error: %v", param.name, param.value, err) + } + } + + opts, err := parser.Options() + if err != nil { + t.Fatalf("Options returned error: %v", err) + } + + if opts.Language != LanguageGo { + t.Fatalf("Language = %q, want %q", opts.Language, LanguageGo) + } + if opts.PythonRuntime != "" { + t.Fatalf("PythonRuntime = %q, want empty", opts.PythonRuntime) + } +} + +func TestParseOptions_RejectsUnknownLang(t *testing.T) { + _, err := ParseOptions("lang=ruby") + if err == nil { + t.Fatal("ParseOptions succeeded, want error") + } +} + +func TestParseOptions_RejectsUnknownCustomParam(t *testing.T) { + _, err := ParseOptions("lang=python,python_runtme=google.protobuf") + if err == nil { + t.Fatal("ParseOptions succeeded, want error") + } +} diff --git a/internal/codegen/python_contract_test.go b/internal/codegen/python_contract_test.go new file mode 100644 index 0000000..9977c75 --- /dev/null +++ b/internal/codegen/python_contract_test.go @@ -0,0 +1,845 @@ +package codegen + +import ( + "strings" + "testing" +) + +func TestPythonRenderer_EmitsDataclassPublicAPI(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) + wantSnippets := []string{ + "class _UnsetType:", + "UNSET = _UnsetType()", + "ToolRequestContext = mcp.server.session.ServerSession", + "class ReportStatus(enum.IntEnum):", + "@dataclass(slots=True)", + "class ReportDetails:", + "class CreateReportRequest:", + "class CreateReportResponse:", + "class ExampleAPIToolHandler(Protocol):", + "def create_report(self, ctx: ToolRequestContext, req: CreateReportRequest) -> CreateReportResponse | Awaitable[CreateReportResponse]:", + "def ping(self, ctx: ToolRequestContext, req: PingRequest) -> PingResponse | Awaitable[PingResponse]:", + "def describe_advanced_shapes(self, ctx: ToolRequestContext, req: DescribeAdvancedShapesRequest) -> DescribeAdvancedShapesResponse | Awaitable[DescribeAdvancedShapesResponse]:", + "def describe_scalar_shapes(self, ctx: ToolRequestContext, req: DescribeScalarShapesRequest) -> DescribeScalarShapesResponse | Awaitable[DescribeScalarShapesResponse]:", + "def hidden_thing(self, ctx: ToolRequestContext, req: HiddenThingRequest) -> HiddenThingResponse | Awaitable[HiddenThingResponse]:", + "EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON =", + "EXAMPLE_API_HEALTH_OUTPUT_SCHEMA_JSON =", + "def register_example_api_tools(server: mcp.server.lowlevel.Server, impl: ExampleAPIToolHandler, *, namespace: str | None = None) -> None:", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing snippet %q\n%s", snippet, generated) + } + } + notWantSnippets := []string{ + " REPORT_STATUS_NONE = 0", + "def create_report(self, ctx: ToolRequestContext, req: example_pb2.CreateReportRequest)", + "def create_report(self, ctx: ToolRequestContext, req: CreateReportRequest) -> example_pb2.CreateReportResponse | Awaitable[example_pb2.CreateReportResponse]:", + "def ping(self, ctx: ToolRequestContext, req: example_pb2.PingRequest)", + "def ping(self, ctx: ToolRequestContext, req: PingRequest) -> example_pb2.PingResponse | Awaitable[example_pb2.PingResponse]:", + "def describe_advanced_shapes(self, ctx: ToolRequestContext, req: example_pb2.DescribeAdvancedShapesRequest)", + "def describe_advanced_shapes(self, ctx: ToolRequestContext, req: DescribeAdvancedShapesRequest) -> example_pb2.DescribeAdvancedShapesResponse | Awaitable[example_pb2.DescribeAdvancedShapesResponse]:", + "def describe_scalar_shapes(self, ctx: ToolRequestContext, req: example_pb2.DescribeScalarShapesRequest)", + "def describe_scalar_shapes(self, ctx: ToolRequestContext, req: DescribeScalarShapesRequest) -> example_pb2.DescribeScalarShapesResponse | Awaitable[example_pb2.DescribeScalarShapesResponse]:", + "def hidden_thing(self, ctx: ToolRequestContext, req: example_pb2.HiddenThingRequest)", + "def hidden_thing(self, ctx: ToolRequestContext, req: HiddenThingRequest) -> example_pb2.HiddenThingResponse | Awaitable[example_pb2.HiddenThingResponse]:", + } + for _, snippet := range notWantSnippets { + if strings.Contains(generated, snippet) { + t.Fatalf("generated python must not retain pb2 public API snippet %q\n%s", snippet, generated) + } + } +} + +func TestPythonAndGoRenderersShareContractModel(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + goModel, err := CollectFileModel(file, Options{Language: LanguageGo}) + if err != nil { + t.Fatalf("CollectFileModel go: %v", err) + } + pythonModel, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel python: %v", err) + } + + if len(goModel.Services) != len(pythonModel.Services) { + t.Fatalf("service count mismatch: go=%d python=%d", len(goModel.Services), len(pythonModel.Services)) + } + + for i := range goModel.Services { + goService := goModel.Services[i] + pythonService := pythonModel.Services[i] + + if goService.Namespace != pythonService.Namespace { + t.Fatalf("service %d namespace mismatch: go=%q python=%q", i, goService.Namespace, pythonService.Namespace) + } + if len(goService.Methods) != len(pythonService.Methods) { + t.Fatalf("service %q method count mismatch: go=%d python=%d", goService.ProtoName, len(goService.Methods), len(pythonService.Methods)) + } + + for j := range goService.Methods { + goMethod := goService.Methods[j] + pythonMethod := pythonService.Methods[j] + if goMethod.Name != pythonMethod.Name { + t.Fatalf("method %d name mismatch: go=%q python=%q", j, goMethod.Name, pythonMethod.Name) + } + if goMethod.Title != pythonMethod.Title { + t.Fatalf("method %q title mismatch: go=%q python=%q", goMethod.Name, goMethod.Title, pythonMethod.Title) + } + if goMethod.Description != pythonMethod.Description { + t.Fatalf("method %q description mismatch: go=%q python=%q", goMethod.Name, goMethod.Description, pythonMethod.Description) + } + if goMethod.InputSchemaJSON != pythonMethod.InputSchemaJSON { + t.Fatalf("method %q input schema mismatch", goMethod.Name) + } + if goMethod.OutputSchemaJSON != pythonMethod.OutputSchemaJSON { + t.Fatalf("method %q output schema mismatch", goMethod.Name) + } + if goMethod.Deprecated != pythonMethod.Deprecated { + t.Fatalf("method %q deprecated mismatch: go=%t python=%t", goMethod.Name, goMethod.Deprecated, pythonMethod.Deprecated) + } + } + } +} + +func TestPythonRenderer_ImportsCrossFilePublicTypes(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/shared.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/shared;sharedv1";`, + `message SharedRequest {}`, + `message SharedResponse {}`, + ``, + }, "\n"), + "test/v1/service.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/service;servicev1";`, + `import "test/v1/shared.proto";`, + `service CrossFileAPI {`, + ` rpc UseShared(SharedRequest) returns (SharedResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/service.proto") + + file := plugin.FilesByPath["test/v1/service.proto"] + if file == nil { + t.Fatal("service proto file not found in plugin") + } + sharedFile := plugin.FilesByPath["test/v1/shared.proto"] + if sharedFile == nil { + t.Fatal("shared proto file not found in plugin") + } + sharedAlias := pythonPublicModuleAliasForProtoPath(sharedFile.Desc.Path(), false) + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "test/v1/service_mcp.py")) + wantSnippets := []string{ + "from test.v1 import shared_mcp as " + sharedAlias, + "req: " + sharedAlias + ".SharedRequest", + ") -> " + sharedAlias + ".SharedResponse", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing cross-file import snippet %q\n%s", snippet, generated) + } + } +} + +func TestPythonRenderer_ImportsCrossFileProtobufModulesForMapperHelpers(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/shared.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/shared;sharedv1";`, + `message SharedDetails {`, + ` string label = 1;`, + `}`, + ``, + }, "\n"), + "test/v1/service.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/service;servicev1";`, + `import "test/v1/shared.proto";`, + `message LocalRequest {`, + ` SharedDetails details = 1;`, + `}`, + `message LocalResponse {`, + ` SharedDetails details = 1;`, + `}`, + `service CrossFileMapperAPI {`, + ` rpc UseShared(LocalRequest) returns (LocalResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/service.proto") + + file := plugin.FilesByPath["test/v1/service.proto"] + if file == nil { + t.Fatal("service proto file not found in plugin") + } + sharedFile := plugin.FilesByPath["test/v1/shared.proto"] + if sharedFile == nil { + t.Fatal("shared proto file not found in plugin") + } + sharedPublicAlias := pythonPublicModuleAliasForProtoPath(sharedFile.Desc.Path(), false) + sharedPBAlias := pythonModuleAlias(sharedFile, false) + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "test/v1/service_mcp.py")) + wantSnippets := []string{ + "from test.v1 import shared_pb2 as " + sharedPBAlias, + "def _from_pb_" + sharedPublicAlias + "_shared_details(message: " + sharedPBAlias + ".SharedDetails) ->", + "def _to_pb_" + sharedPublicAlias + "_shared_details(value:", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing cross-file protobuf mapper import snippet %q\n%s", snippet, generated) + } + } +} + +func TestValidatePythonMapperHelperNames_FailsOnCurrentAndImportedCollision(t *testing.T) { + err := validatePythonMapperHelperNames([]PythonType{ + { + Kind: PythonTypeKindMessage, + ProtoFullName: "test.v1.SharedDetails", + PublicName: "SharedDetails", + Owner: PythonTypeOwner{ + IsCurrentFile: true, + }, + }, + { + Kind: PythonTypeKindMessage, + ProtoFullName: "test.v1.Details", + PublicName: "Details", + Owner: PythonTypeOwner{ + PublicModule: PythonModuleRef{ModuleAlias: "shared"}, + }, + }, + }) + if err == nil { + t.Fatal("validatePythonMapperHelperNames unexpectedly succeeded") + } + if !strings.Contains(err.Error(), "python mapper helper name collision") { + t.Fatalf("unexpected helper collision error: %v", err) + } +} + +func TestPythonRenderer_FailsOnMapperHelperNameCollision(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/service.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/service;servicev1";`, + `message Any {`, + ` string label = 1;`, + `}`, + `message CollisionRequest {`, + ` Any payload = 1;`, + `}`, + `message CollisionResponse {}`, + `service CollisionAPI {`, + ` rpc UseCollision(CollisionRequest) returns (CollisionResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/service.proto") + + file := plugin.FilesByPath["test/v1/service.proto"] + if file == nil { + t.Fatal("service proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + + err = renderPythonFile(plugin, model) + if err == nil { + t.Fatal("renderPythonFile unexpectedly succeeded") + } + if !strings.Contains(err.Error(), "python mapper helper name collision") { + t.Fatalf("unexpected render error: %v", err) + } +} + +func TestPythonRenderer_DoesNotImportWellKnownTypePublicModules(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) + notWantSnippets := []string{ + "from google.protobuf import any_mcp", + "from google.protobuf import duration_mcp", + "from google.protobuf import empty_mcp", + "from google.protobuf import field_mask_mcp", + "from google.protobuf import struct_mcp", + "from google.protobuf import timestamp_mcp", + "from google.protobuf import wrappers_mcp", + } + for _, snippet := range notWantSnippets { + if strings.Contains(generated, snippet) { + t.Fatalf("generated python must not import nonexistent WKT public module %q\n%s", snippet, generated) + } + } +} + +func TestPythonRenderer_EmitsExplicitOneofWrappers(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) + wantSnippets := []string{ + "class DescribeAdvancedShapesRequestSelectorVariant:", + "class DescribeAdvancedShapesRequestSelectorCityAliasVariant(DescribeAdvancedShapesRequestSelectorVariant):", + "class DescribeAdvancedShapesRequestSelectorCityIdVariant(DescribeAdvancedShapesRequestSelectorVariant):", + "class DescribeAdvancedShapesRequestSelectorCityDetailsVariant(DescribeAdvancedShapesRequestSelectorVariant):", + "selector: DescribeAdvancedShapesRequestSelectorVariant | _UnsetType = UNSET", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing explicit oneof wrapper snippet %q\n%s", snippet, generated) + } + } +} + +func TestPythonRenderer_AlignsDataclassRequirednessWithSchemaRequiredFields(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) + wantSnippets := []string{ + "class CreateReportRequest:", + " city: str", + " count: int", + " details: ReportDetails", + " units: str | _UnsetType = UNSET", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing requiredness snippet %q\n%s", snippet, generated) + } + } + if strings.Contains(generated, " details: ReportDetails | _UnsetType = UNSET") { + t.Fatalf("generated python must keep schema-required dataclass fields required\n%s", generated) + } +} + +func TestPythonRenderer_UsesEnumAnnotationsWithoutNumericFallback(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/enums.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/enums;enumsv1";`, + `enum JobStatus {`, + ` JOB_STATUS_UNSPECIFIED = 0;`, + ` JOB_STATUS_OK = 1;`, + ` JOB_STATUS_FAILED = 2;`, + `}`, + `message EnumRequest {`, + ` JobStatus status = 1;`, + ` optional JobStatus optional_status = 2;`, + ` repeated JobStatus statuses = 3;`, + ` map status_by_name = 4;`, + `}`, + `message EnumResponse {}`, + `service EnumAPI {`, + ` rpc UseEnum(EnumRequest) returns (EnumResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/enums.proto") + + file := plugin.FilesByPath["test/v1/enums.proto"] + if file == nil { + t.Fatal("enums proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "test/v1/enums_mcp.py")) + wantSnippets := []string{ + " status: JobStatus", + " optional_status: JobStatus | _UnsetType = UNSET", + " statuses: list[JobStatus] = field(default_factory=list)", + " status_by_name: dict[str, JobStatus] = field(default_factory=dict)", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python must keep enum annotations aligned with the MCP boundary contract: missing %q\n%s", snippet, generated) + } + } + notWantSnippets := []string{ + " status: JobStatus | int", + " optional_status: JobStatus | int | _UnsetType = UNSET", + " statuses: list[JobStatus | int] = field(default_factory=list)", + " status_by_name: dict[str, JobStatus | int] = field(default_factory=dict)", + } + for _, snippet := range notWantSnippets { + if strings.Contains(generated, snippet) { + t.Fatalf("generated python must not advertise numeric enum fallback in public annotations: found %q\n%s", snippet, generated) + } + } +} + +func TestPythonRenderer_KeepsOptionalMessageAndWKTFieldsUnsettable(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) + wantSnippets := []string{ + " child: RecursiveNode | _UnsetType = UNSET", + " observed_at: Timestamp | _UnsetType = UNSET", + " ttl: Duration | _UnsetType = UNSET", + " payload: Struct | _UnsetType = UNSET", + " detail_any: ProtoAny | _UnsetType = UNSET", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python must keep optional message/WKT fields unsettable: missing %q\n%s", snippet, generated) + } + } +} + +func TestPythonRenderer_UsesSafeCurrentModuleImport(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "examples/1-helloworld/proto/hello.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/hello;hellov1";`, + `message HelloRequest {}`, + `message HelloResponse {}`, + `service HelloAPI {`, + ` rpc SayHello(HelloRequest) returns (HelloResponse);`, + `}`, + ``, + }, "\n"), + }, "examples/1-helloworld/proto/hello.proto") + + file := plugin.FilesByPath["examples/1-helloworld/proto/hello.proto"] + if file == nil { + t.Fatal("hello proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "examples/1_helloworld/proto/hello_mcp.py")) + wantSnippets := []string{ + "try:\n from . import hello_pb2", + "except ImportError:\n import hello_pb2", + "@dataclass(slots=True)\nclass HelloRequest:", + "@dataclass(slots=True)\nclass HelloResponse:", + "def say_hello(self, ctx: ToolRequestContext, req: HelloRequest) -> HelloResponse | Awaitable[HelloResponse]:", + "request_type=hello_pb2.HelloRequest", + "response_type=hello_pb2.HelloResponse", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing safe current-module import snippet %q\n%s", snippet, generated) + } + } +} + +func TestPythonRenderer_EmitsPythonBoolLiteralsInAnnotations(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/annotations.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `import "mcp/options/v1/options.proto";`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/annotations;annotationsv1";`, + `message AnnotatedRequest {}`, + `message AnnotatedResponse {}`, + `service AnnotatedAPI {`, + ` rpc ReadThing(AnnotatedRequest) returns (AnnotatedResponse) {`, + ` option (mcp.options.v1.method) = {`, + ` annotations: {`, + ` read_only_hint: true`, + ` idempotent_hint: true`, + ` open_world_hint: true`, + ` }`, + ` };`, + ` }`, + `}`, + ``, + }, "\n"), + }, "test/v1/annotations.proto") + + file := plugin.FilesByPath["test/v1/annotations.proto"] + if file == nil { + t.Fatal("annotations proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "test/v1/annotations_mcp.py")) + if !strings.Contains(generated, `annotations={"readOnlyHint": True, "idempotentHint": True, "openWorldHint": True}`) { + t.Fatalf("generated python must emit Python bool literals for annotations\n%s", generated) + } + if strings.Contains(generated, `annotations={"readOnlyHint": true`) || strings.Contains(generated, `annotations={"idempotentHint": true`) { + t.Fatalf("generated python must not emit JSON bool literals for annotations\n%s", generated) + } +} + +func TestPythonModuleAlias_AvoidsImportedModuleCollisions(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "a/b_c.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/aliasone;aliasonev1";`, + `message AliasOne {}`, + ``, + }, "\n"), + "a_b/c.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/aliastwo;aliastwov1";`, + `message AliasTwo {}`, + ``, + }, "\n"), + "test/v1/service.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/aliassvc;aliassvcv1";`, + `import "a/b_c.proto";`, + `import "a_b/c.proto";`, + `message AliasRequest {`, + ` test.v1.AliasOne one = 1;`, + ` test.v1.AliasTwo two = 2;`, + `}`, + `message AliasResponse {}`, + `service AliasAPI {`, + ` rpc UseAlias(AliasRequest) returns (AliasResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/service.proto") + + first := plugin.FilesByPath["a/b_c.proto"] + if first == nil { + t.Fatal("first proto file not found in plugin") + } + second := plugin.FilesByPath["a_b/c.proto"] + if second == nil { + t.Fatal("second proto file not found in plugin") + } + + firstAlias := pythonModuleAlias(first, false) + secondAlias := pythonModuleAlias(second, false) + if firstAlias == secondAlias { + t.Fatalf("imported module aliases collided: %q", firstAlias) + } +} + +func TestPythonRenderer_ComposesExistingLowLevelHandlers(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) + wantSnippets := []string{ + "self.previous_list_tools: Any | None = None", + "self.previous_call_tool: Any | None = None", + "registry.previous_list_tools = server.request_handlers.get(mcp.types.ListToolsRequest)", + "registry.previous_call_tool = server.request_handlers.get(mcp.types.CallToolRequest)", + "list_request = _build_list_tools_request(request)", + "previous_result = await current.previous_list_tools(list_request)", + "meta: dict[str, Any] | None = None", + "if current.previous_list_tools is not None:", + "if getattr(list_request.params, \"cursor\", None) is not None:", + "if previous_result.root.nextCursor is not None:", + "raise ValueError(\"cannot compose protoc-gen-mcp tools with paginated tools/list handlers\")", + "meta = getattr(previous_result.root, \"meta\", None)", + "tools = _merge_tools(previous_tools, current_tools)", + "mcp.types.ListToolsResult(tools=tools, _meta=meta)", + "if current.previous_call_tool is not None:", + "await server.request_handlers[mcp.types.ListToolsRequest](mcp.types.ListToolsRequest())", + "return await current.previous_call_tool(request)", + "_invalid_params_error(request.params.name, \"unknown tool\")", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing handler-composition snippet %q\n%s", snippet, generated) + } + } +} + +func TestPythonRenderer_ReservesToolNamesServerWide(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) + wantSnippets := []string{ + "_RESERVED_TOOL_NAMES_ATTR = \"_protoc_gen_mcp_reserved_tool_names\"", + "self.reserved_tool_names = _get_reserved_tool_names(server)", + "def _get_reserved_tool_names(server: mcp.server.lowlevel.Server) -> set[str]:", + "reserved = getattr(server, _RESERVED_TOOL_NAMES_ATTR, None)", + "setattr(server, _RESERVED_TOOL_NAMES_ATTR, reserved)", + "if tool.name in self.reserved_tool_names:", + "self.reserved_tool_names.add(tool.name)", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing server-wide reservation snippet %q\n%s", snippet, generated) + } + } +} + +func TestPythonRenderer_DetectsMergedDuplicateToolNames(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) + wantSnippets := []string{ + "def _merge_tools(previous: list[mcp.types.Tool], current: list[mcp.types.Tool]) -> list[mcp.types.Tool]:", + "if tool.name in names:", + "raise ValueError(f\"duplicate tool registration: {tool.name}\")", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing merged-duplicate detection snippet %q\n%s", snippet, generated) + } + } +} + +func TestPythonRenderer_WrapsOutputValidationFailures(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) + wantSnippets := []string{ + "except jsonschema.ValidationError as error:", + "raise RuntimeError(f\"mcpruntime: validate output for tool {name!r}: {error}\") from error", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing output-validation wrapper snippet %q\n%s", snippet, generated) + } + } +} + +func TestPythonRuntime_DispatchesThroughDataclassMappers(t *testing.T) { + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + generated := string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) + wantSnippets := []string{ + "request_pb = _protojson_to_message(arguments, tool.request_type())", + "request_dc = tool.from_pb(request_pb)", + "response_dc = await _maybe_await(tool.handler(context, request_dc))", + "response_pb = tool.to_pb(response_dc)", + "from_pb=_from_pb_create_report_request,", + "to_pb=_to_pb_create_report_response,", + "payload = json.loads(_message_to_json(response_pb))", + "return mcp.types.CallToolResult(content=content, structuredContent=payload)", + } + for _, snippet := range wantSnippets { + if !strings.Contains(generated, snippet) { + t.Fatalf("generated python missing dataclass dispatch snippet %q\n%s", snippet, generated) + } + } +} diff --git a/internal/codegen/python_mapper_test.go b/internal/codegen/python_mapper_test.go new file mode 100644 index 0000000..cc101e6 --- /dev/null +++ b/internal/codegen/python_mapper_test.go @@ -0,0 +1,804 @@ +package codegen + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +func renderExamplePythonForMapperTests(t *testing.T) string { + t.Helper() + + plugin := newExampleProtogenPlugin(t) + file := plugin.FilesByPath["internal/testproto/example/v1/example.proto"] + if file == nil { + t.Fatal("example proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if err := renderPythonFile(plugin, model); err != nil { + t.Fatalf("renderPythonFile: %v", err) + } + + return string(generatedFileContent(t, plugin, "internal/testproto/example/v1/example_mcp.py")) +} + +func repoRootForPythonMapperTests(t *testing.T) string { + t.Helper() + + _, filename, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + return filepath.Clean(filepath.Join(filepath.Dir(filename), "..", "..")) +} + +func prepareExamplePythonRuntime(t *testing.T) (tempRoot string, repoRoot string) { + t.Helper() + + repoRoot = repoRootForPythonMapperTests(t) + tempRoot = t.TempDir() + + packageDir := filepath.Join(tempRoot, "internal", "testproto", "example", "v1") + if err := os.MkdirAll(packageDir, 0o755); err != nil { + t.Fatalf("MkdirAll(%q): %v", packageDir, err) + } + + for _, rel := range []string{ + "internal/__init__.py", + "internal/testproto/__init__.py", + "internal/testproto/example/__init__.py", + "internal/testproto/example/v1/__init__.py", + } { + path := filepath.Join(tempRoot, filepath.FromSlash(rel)) + if err := os.WriteFile(path, nil, 0o644); err != nil { + t.Fatalf("WriteFile(%q): %v", path, err) + } + } + + generatedPath := filepath.Join(packageDir, "example_mcp.py") + if err := os.WriteFile(generatedPath, []byte(renderExamplePythonForMapperTests(t)), 0o644); err != nil { + t.Fatalf("WriteFile(%q): %v", generatedPath, err) + } + + examplePB2Path := filepath.Join(repoRoot, "internal", "testproto", "example", "v1", "example_pb2.py") + examplePB2Content, err := os.ReadFile(examplePB2Path) + if err != nil { + t.Fatalf("ReadFile(%q): %v", examplePB2Path, err) + } + if err := os.WriteFile(filepath.Join(packageDir, "example_pb2.py"), examplePB2Content, 0o644); err != nil { + t.Fatalf("WriteFile(example_pb2.py): %v", err) + } + + return tempRoot, repoRoot +} + +func runExamplePythonScript(t *testing.T, script string) { + t.Helper() + + python, err := exec.LookPath("python3") + if err != nil { + t.Skipf("python3 not available: %v", err) + } + + tempRoot, repoRoot := prepareExamplePythonRuntime(t) + cmd := exec.Command(python, "-c", fmt.Sprintf( + "from internal.testproto.example.v1 import example_mcp as module\n"+ + "from internal.testproto.example.v1 import example_pb2\n%s", + script, + )) + cmd.Dir = tempRoot + cmd.Env = append(os.Environ(), "PYTHONPATH="+tempRoot+string(os.PathListSeparator)+repoRoot) + + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("python runtime failed: %v\n%s", err, output) + } +} + +func TestPythonMapper_RoundTripsOptionalAndUnset(t *testing.T) { + runExamplePythonScript(t, ` +request = module.CreateReportRequest( + city="Paris", + count=2, + details=module.ReportDetails(label="today"), + units=module.UNSET, + labels=["daily"], +) +request_pb = module._to_pb_create_report_request(request) +assert not request_pb.HasField("units") +assert list(request_pb.labels) == ["daily"] +assert module._from_pb_create_report_request(request_pb) == request + +scalar = module.DescribeScalarShapesRequest( + bool_flag=True, + text_value="hello", + bytes_value=b"abc", + int32_value=-1, + sint32_value=-2, + sfixed32_value=-3, + uint32_value=7, + fixed32_value=8, + int64_value=-9, + sint64_value=-10, + sfixed64_value=-11, + uint64_value=12, + fixed64_value=13, + float_value=1.25, + double_value=2.5, + status=module.ReportStatus.REPORT_STATUS_OK, + details=module.ReportDetails(label="shape"), + samples=[1, 2, 3], + optional_text_value=module.UNSET, + optional_status=module.ReportStatus.REPORT_STATUS_FAILED, +) +scalar_pb = module._to_pb_describe_scalar_shapes_request(scalar) +assert not scalar_pb.HasField("optional_text_value") +assert scalar_pb.HasField("optional_status") +assert scalar_pb.optional_status == int(module.ReportStatus.REPORT_STATUS_FAILED) +scalar_roundtrip = module._from_pb_describe_scalar_shapes_request(scalar_pb) +assert scalar_roundtrip.optional_text_value is module.UNSET +assert scalar_roundtrip == scalar +`) +} + +func TestPythonMapper_RoundTripsExplicitOneofVariants(t *testing.T) { + runExamplePythonScript(t, ` +cases = [ + ( + module.DescribeAdvancedShapesRequest( + selector=module.DescribeAdvancedShapesRequestSelectorCityAliasVariant(city_alias="msk"), + ), + "city_alias", + ), + ( + module.DescribeAdvancedShapesRequest( + selector=module.DescribeAdvancedShapesRequestSelectorCityIdVariant(city_id=1234567890123), + ), + "city_id", + ), + ( + module.DescribeAdvancedShapesRequest( + selector=module.DescribeAdvancedShapesRequestSelectorCityDetailsVariant( + city_details=module.ReportDetails(label="nested"), + ), + ), + "city_details", + ), +] +for request, field_name in cases: + request_pb = module._to_pb_describe_advanced_shapes_request(request) + assert request_pb.WhichOneof("selector") == field_name + assert module._from_pb_describe_advanced_shapes_request(request_pb) == request +`) +} + +func TestPythonMapper_RejectsOneofBaseVariant(t *testing.T) { + runExamplePythonScript(t, ` +request = module.DescribeAdvancedShapesRequest( + selector=module.DescribeAdvancedShapesRequestSelectorVariant(), +) +try: + module._to_pb_describe_advanced_shapes_request(request) +except TypeError as exc: + assert "unsupported DescribeAdvancedShapesRequestSelectorVariant variant" in str(exc) +else: + raise AssertionError("expected TypeError for base oneof wrapper") +`) +} + +func TestPythonMapper_RoundTripsRecursiveMessages(t *testing.T) { + runExamplePythonScript(t, ` +request = module.DescribeAdvancedShapesRequest( + tree=module.RecursiveNode( + name="root", + child=module.RecursiveNode(name="branch"), + children=[ + module.RecursiveNode( + name="child", + children=[module.RecursiveNode(name="leaf")], + ) + ], + ), +) +request_pb = module._to_pb_describe_advanced_shapes_request(request) +assert request_pb.HasField("tree") +assert request_pb.tree.child.name == "branch" +assert request_pb.tree.children[0].children[0].name == "leaf" +assert module._from_pb_describe_advanced_shapes_request(request_pb) == request +`) +} + +func TestPythonMapper_RoundTripsAnyAndValueFamilies(t *testing.T) { + runExamplePythonScript(t, ` +request = module.DescribeAdvancedShapesRequest( + payload={"service": "example", "nested": {"ok": True}}, + items=[1, "two", {"three": 3}, None], + dynamic={"kind": ["alpha", False, None]}, + detail_any={ + "@type": "type.googleapis.com/internal.testproto.example.v1.ReportDetails", + "label": "from-any", + }, + duration_any={ + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "3600s", + }, +) +request_pb = module._to_pb_describe_advanced_shapes_request(request) +assert request_pb.HasField("payload") +assert request_pb.HasField("items") +assert request_pb.HasField("dynamic") +assert request_pb.HasField("detail_any") +assert request_pb.HasField("duration_any") +assert module._from_pb_describe_advanced_shapes_request(request_pb) == request +`) +} + +func TestPythonMapper_RoundTripsSupportedWKTs(t *testing.T) { + runExamplePythonScript(t, ` +request = module.DescribeAdvancedShapesRequest( + observed_at="2026-04-10T12:34:56Z", + ttl="3600s", + note="memo", + total=42, + enabled=True, + ratio=3.5, + mask="fooBar,bazQux", + blob=b"\x00hi", + small_total=-7, + uint_total=9, + huge_total=1234567890123, + weight=1.25, + raw_ratio=2.75, +) +request_pb = module._to_pb_describe_advanced_shapes_request(request) +assert request_pb.HasField("observed_at") +assert request_pb.HasField("ttl") +assert request_pb.HasField("note") +assert request_pb.HasField("total") +assert request_pb.HasField("enabled") +assert request_pb.HasField("ratio") +assert request_pb.HasField("mask") +assert request_pb.HasField("blob") +assert request_pb.HasField("small_total") +assert request_pb.HasField("uint_total") +assert request_pb.HasField("huge_total") +assert request_pb.HasField("weight") +assert request_pb.HasField("raw_ratio") +assert request_pb.uint_total.value == 9 +assert request_pb.huge_total.value == 1234567890123 +assert module._from_pb_describe_advanced_shapes_request(request_pb) == request + +ping = module.PingResponse(ack=module.Empty()) +ping_pb = module._to_pb_ping_response(ping) +assert ping_pb.HasField("ack") +assert module._from_pb_ping_response(ping_pb) == ping +`) +} + +func TestPythonMapper_RejectsUnknownEnumNumbers(t *testing.T) { + runExamplePythonScript(t, ` +request_pb = example_pb2.DescribeScalarShapesRequest( + bool_flag=True, + text_value="hello", + bytes_value=b"abc", + int32_value=-1, + sint32_value=-2, + sfixed32_value=-3, + uint32_value=7, + fixed32_value=8, + int64_value=-9, + sint64_value=-10, + sfixed64_value=-11, + uint64_value=12, + fixed64_value=13, + float_value=1.25, + double_value=2.5, + status=123, + details=example_pb2.ReportDetails(label="shape"), + optional_status=456, +) + +try: + module._from_pb_describe_scalar_shapes_request(request_pb) +except ValueError as exc: + assert "123" in str(exc) +else: + raise AssertionError("expected ValueError for unknown enum number") +`) +} + +func TestPythonRuntime_AcceptsExplicitNullForNonRequiredFields(t *testing.T) { + runExamplePythonScript(t, ` +import asyncio + +class DummyServer: + pass + +registry = module._ServerToolRegistry(DummyServer()) + +async def handler(ctx, req): + assert req.city == "Paris" + assert req.count == 2 + assert req.details == module.ReportDetails(label="today") + assert req.units is module.UNSET + assert req.labels == ["daily"] + return module.CreateReportResponse( + report_id="report-1", + total_count=2, + status=module.ReportStatus.REPORT_STATUS_OK, + details=req.details, + warnings=[], + ) + +registry.add_tool(module._RegisteredTool( + name="example_CreateReport", + title="Create report", + description="Create a report for a city.", + input_schema_json=module.EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON, + output_schema_json=module.EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.CreateReportRequest, + response_type=example_pb2.CreateReportResponse, + from_pb=module._from_pb_create_report_request, + to_pb=module._to_pb_create_report_response, + handler=handler, + annotations=None, + icons=None, +)) + +result = asyncio.run(module._dispatch_call( + registry, + "example_CreateReport", + { + "city": "Paris", + "count": 2, + "details": {"label": "today"}, + "units": None, + "labels": ["daily"], + }, + None, +)) + +assert result.isError in (None, False) +assert result.structuredContent["reportId"] == "report-1" +assert result.structuredContent["totalCount"] == "2" +assert result.structuredContent["status"] == "REPORT_STATUS_OK" +assert result.structuredContent["details"] == {"label": "today"} +assert result.structuredContent["warnings"] == [] +assert result.content[0].text +`) +} + +func TestPythonRuntime_InvalidSchemaPayloadRaisesInvalidParams(t *testing.T) { + runExamplePythonScript(t, ` +import asyncio +import mcp.shared.exceptions +import mcp.types + +class DummyServer: + pass + +registry = module._ServerToolRegistry(DummyServer()) + +def handler(_ctx, _req): + raise AssertionError("handler must not run for schema-invalid input") + +registry.add_tool(module._RegisteredTool( + name="example_CreateReport", + title="Create report", + description="Create a report for a city.", + input_schema_json=module.EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON, + output_schema_json=module.EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.CreateReportRequest, + response_type=example_pb2.CreateReportResponse, + from_pb=module._from_pb_create_report_request, + to_pb=module._to_pb_create_report_response, + handler=handler, + annotations=None, + icons=None, +)) + +async def main(): + try: + await module._dispatch_call( + registry, + "example_CreateReport", + { + "city": "Paris", + "count": "two", + "details": {"label": "today"}, + }, + None, + ) + except mcp.shared.exceptions.McpError as exc: + assert exc.error.code == mcp.types.INVALID_PARAMS + assert "example_CreateReport" in exc.error.message + assert "invalid arguments for tool" in exc.error.message + else: + raise AssertionError("expected McpError for schema validation failure") + +asyncio.run(main()) +`) +} + +func TestPythonRuntime_InvalidProtoJSONPayloadRaisesInvalidParams(t *testing.T) { + runExamplePythonScript(t, ` +import asyncio +import mcp.shared.exceptions +import mcp.types + +class DummyServer: + pass + +registry = module._ServerToolRegistry(DummyServer()) + +def handler(_ctx, _req): + raise AssertionError("handler must not run for protojson-invalid input") + +registry.add_tool(module._RegisteredTool( + name="example_DescribeScalarShapes", + title="Describe scalar shapes", + description="Describe scalar protobuf kinds.", + input_schema_json=module.EXAMPLE_API_DESCRIBE_SCALAR_SHAPES_INPUT_SCHEMA_JSON, + output_schema_json=module.EXAMPLE_API_DESCRIBE_SCALAR_SHAPES_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.DescribeScalarShapesRequest, + response_type=example_pb2.DescribeScalarShapesResponse, + from_pb=module._from_pb_describe_scalar_shapes_request, + to_pb=module._to_pb_describe_scalar_shapes_response, + handler=handler, + annotations=None, + icons=None, +)) + +invalid_arguments = { + "boolFlag": True, + "textValue": "hello", + "bytesValue": "YWJj", + "int32Value": -1, + "sint32Value": -2, + "sfixed32Value": -3, + "uint32Value": 7, + "fixed32Value": 8, + "int64Value": "-9", + "sint64Value": "-10", + "sfixed64Value": "-11", + "uint64Value": "12", + "fixed64Value": "not-an-int", + "floatValue": 1.25, + "doubleValue": 2.5, + "status": "REPORT_STATUS_OK", + "details": {"label": "shape"}, +} + +async def main(): + try: + await module._dispatch_call( + registry, + "example_DescribeScalarShapes", + invalid_arguments, + None, + ) + except mcp.shared.exceptions.McpError as exc: + assert exc.error.code == mcp.types.INVALID_PARAMS + assert "example_DescribeScalarShapes" in exc.error.message + assert "invalid arguments for tool" in exc.error.message + else: + raise AssertionError("expected McpError for protojson parse failure") + +asyncio.run(main()) +`) +} + +func TestPythonRuntime_DispatchesSyncHandlers(t *testing.T) { + runExamplePythonScript(t, ` +import asyncio +import json + +class DummyServer: + pass + +registry = module._ServerToolRegistry(DummyServer()) + +def handler(_ctx, req): + assert req.city == "Paris" + assert req.count == 2 + return module.CreateReportResponse( + report_id="report-1", + total_count=req.count, + status=module.ReportStatus.REPORT_STATUS_OK, + details=req.details, + warnings=["cached"], + ) + +registry.add_tool(module._RegisteredTool( + name="example_CreateReport", + title="Create report", + description="Create a report for a city.", + input_schema_json=module.EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON, + output_schema_json=module.EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.CreateReportRequest, + response_type=example_pb2.CreateReportResponse, + from_pb=module._from_pb_create_report_request, + to_pb=module._to_pb_create_report_response, + handler=handler, + annotations=None, + icons=None, +)) + +result = asyncio.run(module._dispatch_call( + registry, + "example_CreateReport", + { + "city": "Paris", + "count": 2, + "details": {"label": "today"}, + }, + None, +)) + +assert result.isError in (None, False) +assert result.structuredContent["reportId"] == "report-1" +assert result.structuredContent["totalCount"] == "2" +assert result.structuredContent["status"] == "REPORT_STATUS_OK" +assert result.structuredContent["warnings"] == ["cached"] +assert json.loads(result.content[0].text) == result.structuredContent +`) +} + +func TestPythonRuntime_RejectsUnknownEnumOutputAgainstSchema(t *testing.T) { + runExamplePythonScript(t, ` +import asyncio + +class DummyServer: + pass + +registry = module._ServerToolRegistry(DummyServer()) + +def handler(_ctx, req): + return module.CreateReportResponse( + report_id="report-1", + total_count=req.count, + status=123, + details=req.details, + warnings=[], + ) + +registry.add_tool(module._RegisteredTool( + name="example_CreateReport", + title="Create report", + description="Create a report for a city.", + input_schema_json=module.EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON, + output_schema_json=module.EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.CreateReportRequest, + response_type=example_pb2.CreateReportResponse, + from_pb=module._from_pb_create_report_request, + to_pb=module._to_pb_create_report_response, + handler=handler, + annotations=None, + icons=None, +)) + +try: + asyncio.run(module._dispatch_call( + registry, + "example_CreateReport", + { + "city": "Paris", + "count": 2, + "details": {"label": "today"}, + }, + None, + )) +except RuntimeError as exc: + assert "mcpruntime: validate output for tool 'example_CreateReport'" in str(exc) +else: + raise AssertionError("expected RuntimeError for unknown enum output") +`) +} + +func TestPythonRuntime_HandlerBusinessErrorsReturnToolErrorResult(t *testing.T) { + runExamplePythonScript(t, ` +import asyncio + +class DummyServer: + pass + +registry = module._ServerToolRegistry(DummyServer()) + +def handler(_ctx, _req): + raise RuntimeError("boom") + +registry.add_tool(module._RegisteredTool( + name="example_CreateReport", + title="Create report", + description="Create a report for a city.", + input_schema_json=module.EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON, + output_schema_json=module.EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.CreateReportRequest, + response_type=example_pb2.CreateReportResponse, + from_pb=module._from_pb_create_report_request, + to_pb=module._to_pb_create_report_response, + handler=handler, + annotations=None, + icons=None, +)) + +result = asyncio.run(module._dispatch_call( + registry, + "example_CreateReport", + { + "city": "Paris", + "count": 2, + "details": {"label": "today"}, + }, + None, +)) + +assert result.isError is True +assert len(result.content) == 1 +assert result.content[0].text == "boom" +`) +} + +func TestPythonRuntime_WrapsOutputMarshalFailures(t *testing.T) { + runExamplePythonScript(t, ` +import asyncio + +class DummyServer: + pass + +registry = module._ServerToolRegistry(DummyServer()) + +async def handler(_ctx, _req): + return object() + +registry.add_tool(module._RegisteredTool( + name="example_CreateReport", + title="Create report", + description="Create a report for a city.", + input_schema_json=module.EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON, + output_schema_json=module.EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.CreateReportRequest, + response_type=example_pb2.CreateReportResponse, + from_pb=module._from_pb_create_report_request, + to_pb=module._to_pb_create_report_response, + handler=handler, + annotations=None, + icons=None, +)) + +try: + asyncio.run(module._dispatch_call( + registry, + "example_CreateReport", + { + "city": "Paris", + "count": 2, + "details": {"label": "today"}, + }, + None, + )) +except RuntimeError as exc: + assert "mcpruntime: marshal output for tool 'example_CreateReport'" in str(exc) +else: + raise AssertionError("expected RuntimeError for output marshal failure") +`) +} + +func TestPythonRuntime_UnknownToolRaisesInvalidParams(t *testing.T) { + runExamplePythonScript(t, ` +import asyncio +import mcp.server.lowlevel +import mcp.shared.exceptions +import mcp.types + +class Handler: + def create_report(self, _ctx, req): + return module.CreateReportResponse( + report_id="report-1", + total_count=req.count, + status=module.ReportStatus.REPORT_STATUS_OK, + details=req.details, + warnings=[], + ) + + def ping(self, _ctx, _req): + return module.PingResponse(ack=module.Empty()) + + def describe_advanced_shapes(self, _ctx, req): + return module.DescribeAdvancedShapesResponse( + labels=req.labels, + quantities=req.quantities, + toggles=req.toggles, + limits=req.limits, + observed_at=req.observed_at, + ttl=req.ttl, + payload=req.payload, + items=req.items, + dynamic=req.dynamic, + note=req.note, + total=req.total, + enabled=req.enabled, + ratio=req.ratio, + mask=req.mask, + blob=req.blob, + small_total=req.small_total, + uint_total=req.uint_total, + huge_total=req.huge_total, + weight=req.weight, + raw_ratio=req.raw_ratio, + tree=req.tree, + detail_any=req.detail_any, + duration_any=req.duration_any, + selector=req.selector, + ) + + def describe_scalar_shapes(self, _ctx, req): + return module.DescribeScalarShapesResponse( + bool_flag=req.bool_flag, + text_value=req.text_value, + bytes_value=req.bytes_value, + int32_value=req.int32_value, + sint32_value=req.sint32_value, + sfixed32_value=req.sfixed32_value, + uint32_value=req.uint32_value, + fixed32_value=req.fixed32_value, + int64_value=req.int64_value, + sint64_value=req.sint64_value, + sfixed64_value=req.sfixed64_value, + uint64_value=req.uint64_value, + fixed64_value=req.fixed64_value, + float_value=req.float_value, + double_value=req.double_value, + status=req.status, + details=req.details, + samples=req.samples, + optional_bool_flag=req.optional_bool_flag, + optional_text_value=req.optional_text_value, + optional_bytes_value=req.optional_bytes_value, + optional_int32_value=req.optional_int32_value, + optional_sint32_value=req.optional_sint32_value, + optional_sfixed32_value=req.optional_sfixed32_value, + optional_uint32_value=req.optional_uint32_value, + optional_fixed32_value=req.optional_fixed32_value, + optional_int64_value=req.optional_int64_value, + optional_sint64_value=req.optional_sint64_value, + optional_sfixed64_value=req.optional_sfixed64_value, + optional_uint64_value=req.optional_uint64_value, + optional_fixed64_value=req.optional_fixed64_value, + optional_float_value=req.optional_float_value, + optional_double_value=req.optional_double_value, + optional_status=req.optional_status, + ) + + def hidden_thing(self, _ctx, _req): + return module.HiddenThingResponse() + +server = mcp.server.lowlevel.Server("mapper-test-server", version="1.0.0") +module.register_example_api_tools(server, Handler()) + +request = mcp.types.CallToolRequest( + params=mcp.types.CallToolRequestParams(name="missing_tool", arguments={}), +) + +async def main(): + try: + await server.request_handlers[mcp.types.CallToolRequest](request) + except mcp.shared.exceptions.McpError as exc: + assert exc.error.code == mcp.types.INVALID_PARAMS + assert "unknown tool" in exc.error.message + else: + raise AssertionError("expected McpError for unknown tool") + +asyncio.run(main()) +`) +} diff --git a/internal/codegen/python_names.go b/internal/codegen/python_names.go new file mode 100644 index 0000000..464c13b --- /dev/null +++ b/internal/codegen/python_names.go @@ -0,0 +1,225 @@ +package codegen + +import ( + "fmt" + "hash/crc32" + "path" + "strings" + "unicode" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/reflect/protoreflect" +) + +func pythonModulePath(file *protogen.File) string { + return pythonModulePathForProtoPath(file.Desc.Path()) +} + +func pythonOutputPath(file *protogen.File) string { + return pythonGeneratedFilenamePrefix(file) + "_mcp.py" +} + +func pythonPublicModulePathForProtoPath(protoPath string) string { + return strings.ReplaceAll(pythonGeneratedFilenamePrefixForProtoPath(protoPath), "/", ".") + "_mcp" +} + +func pythonPublicBasenameModuleForProtoPath(protoPath string) string { + return path.Base(pythonGeneratedFilenamePrefixForProtoPath(protoPath)) + "_mcp" +} + +func pythonPublicModuleAliasForProtoPath(protoPath string, isCurrent bool) string { + if isCurrent { + return pythonPublicBasenameModuleForProtoPath(protoPath) + } + + modulePath := strings.ReplaceAll(pythonPublicModulePathForProtoPath(protoPath), ".", "_") + return fmt.Sprintf("%s_%08x", modulePath, crc32.ChecksumIEEE([]byte(protoPath))) +} + +func pythonMethodName(method string) string { + return toSnakeCase(method) +} + +func pythonSchemaConst(service, method string) string { + return toUpperSnakeCase(service) + "_" + toUpperSnakeCase(method) +} + +func pythonRegisterName(service string) string { + return "register_" + toSnakeCase(service) + "_tools" +} + +func pythonProtocolName(service string) string { + return service + "ToolHandler" +} + +func pythonBasenameModule(file *protogen.File) string { + return pythonBasenameModuleForProtoPath(file.Desc.Path()) +} + +func pythonModuleAlias(file *protogen.File, isCurrent bool) string { + return pythonModuleAliasForProtoPath(file.Desc.Path(), isCurrent) +} + +func pythonModuleAliasForProtoPath(protoPath string, isCurrent bool) string { + if isCurrent { + return pythonBasenameModuleForProtoPath(protoPath) + } + + modulePath := strings.ReplaceAll(pythonModulePathForProtoPath(protoPath), ".", "_") + return fmt.Sprintf("%s_%08x", modulePath, crc32.ChecksumIEEE([]byte(protoPath))) +} + +func pythonImportParts(file *protogen.File) (string, string) { + modulePath := pythonModulePath(file) + lastDot := strings.LastIndex(modulePath, ".") + if lastDot == -1 { + return "", modulePath + } + return modulePath[:lastDot], modulePath[lastDot+1:] +} + +func pythonGeneratedFilenamePrefix(file *protogen.File) string { + return pythonGeneratedFilenamePrefixForProtoPath(file.Desc.Path()) +} + +func pythonGeneratedFilenamePrefixForProtoPath(protoPath string) string { + prefix := strings.TrimSuffix(protoPath, path.Ext(protoPath)) + parts := strings.Split(prefix, "/") + for i, part := range parts { + parts[i] = strings.ReplaceAll(part, "-", "_") + } + return strings.Join(parts, "/") +} + +func pythonModulePathForProtoPath(protoPath string) string { + return strings.ReplaceAll(pythonGeneratedFilenamePrefixForProtoPath(protoPath), "/", ".") + "_pb2" +} + +func pythonBasenameModuleForProtoPath(protoPath string) string { + return path.Base(pythonGeneratedFilenamePrefixForProtoPath(protoPath)) + "_pb2" +} + +func pythonPublicTypeName(message *protogen.Message) string { + return pythonPublicIdentifier(descriptorTypePath(message.Desc.FullName(), message.Desc.ParentFile().Package())) +} + +func pythonPublicEnumName(enum *protogen.Enum) string { + return pythonPublicIdentifier(descriptorTypePath(enum.Desc.FullName(), enum.Desc.ParentFile().Package())) +} + +func pythonPublicIdentifier(parts []string) string { + var b strings.Builder + for _, part := range parts { + b.WriteString(pythonExportedIdentifier(part)) + } + return b.String() +} + +func pythonOneofWrapperName(typeName, oneofName string) string { + return typeName + pythonExportedIdentifier(oneofName) + "Variant" +} + +func pythonOneofVariantWrapperName(typeName, oneofName, fieldName string) string { + return typeName + pythonExportedIdentifier(oneofName) + pythonExportedIdentifier(fieldName) + "Variant" +} + +func pythonExportedIdentifier(value string) string { + if value == "" { + return "" + } + + segments := strings.FieldsFunc(value, func(r rune) bool { + return r == '_' || r == '.' || r == '-' || r == ' ' || r == '/' + }) + var b strings.Builder + for _, segment := range segments { + if segment == "" { + continue + } + runes := []rune(segment) + for i, r := range runes { + if i == 0 { + b.WriteRune(unicode.ToUpper(r)) + continue + } + if i > 0 && unicode.IsUpper(r) && i+1 < len(runes) && unicode.IsLower(runes[i+1]) && unicode.IsUpper(runes[i-1]) { + b.WriteRune(r) + continue + } + b.WriteRune(r) + } + } + return b.String() +} + +func descriptorTypePath(fullName, pkg protoreflect.FullName) []string { + name := string(fullName) + packagePrefix := string(pkg) + if packagePrefix != "" { + prefix := packagePrefix + "." + name = strings.TrimPrefix(name, prefix) + } + if name == "" { + return nil + } + return strings.Split(name, ".") +} + +func pythonCheckNameCollision(registry map[string]string, category, name, owner string) error { + if existing, ok := registry[name]; ok && existing != owner { + return fmt.Errorf("python %s collision for %q between %s and %s", category, name, existing, owner) + } + registry[name] = owner + return nil +} + +func pythonCheckOwnedNameCollision(scoped map[string]map[string]string, scope, category, name, owner string) error { + registry := scoped[scope] + if registry == nil { + registry = map[string]string{} + scoped[scope] = registry + } + return pythonCheckNameCollision(registry, category, name, owner) +} + +func toSnakeCase(value string) string { + if value == "" { + return "" + } + + runes := []rune(value) + var b strings.Builder + for i, r := range runes { + if unicode.IsUpper(r) { + if i > 0 && shouldInsertUnderscore(runes, i) { + b.WriteByte('_') + } + b.WriteRune(unicode.ToLower(r)) + continue + } + if r == '.' || r == '-' || r == ' ' { + b.WriteByte('_') + continue + } + b.WriteRune(r) + } + return b.String() +} + +func toUpperSnakeCase(value string) string { + return strings.ToUpper(toSnakeCase(value)) +} + +func shouldInsertUnderscore(runes []rune, i int) bool { + prev := runes[i-1] + if prev == '_' || prev == '.' || prev == '-' || prev == ' ' { + return false + } + if unicode.IsLower(prev) || unicode.IsDigit(prev) { + return true + } + if i+1 < len(runes) && unicode.IsLower(runes[i+1]) { + return true + } + return false +} diff --git a/internal/codegen/python_request.go b/internal/codegen/python_request.go new file mode 100644 index 0000000..ffc133b --- /dev/null +++ b/internal/codegen/python_request.go @@ -0,0 +1,65 @@ +package codegen + +import ( + "path" + "strings" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/pluginpb" +) + +const pythonSyntheticGoPackageName = "mcppython" + +// PrepareRequestForProtogen adjusts the raw protoc request before protogen +// validates Go-specific metadata. Python generation does not use Go import +// paths, but protogen still requires them to build its descriptor model. +func PrepareRequestForProtogen(req *pluginpb.CodeGeneratorRequest, opts Options) { + if req == nil || opts.Language != LanguagePython { + return + } + + for _, file := range req.ProtoFile { + if file.GetOptions().GetGoPackage() != "" { + continue + } + if file.Options == nil { + file.Options = &descriptorpb.FileOptions{} + } + file.Options.GoPackage = proto.String(syntheticPythonGoPackage(file)) + } +} + +func syntheticPythonGoPackage(file *descriptorpb.FileDescriptorProto) string { + name := strings.TrimSuffix(file.GetName(), ".proto") + name = strings.Trim(path.Clean(name), "/.") + if name == "" { + name = "unknown" + } + + return "protoc-gen-mcp.local/python/" + sanitizeSyntheticGoImportPath(name) + ";" + pythonSyntheticGoPackageName +} + +func sanitizeSyntheticGoImportPath(value string) string { + var builder strings.Builder + for _, char := range value { + switch { + case char >= 'a' && char <= 'z': + builder.WriteRune(char) + case char >= 'A' && char <= 'Z': + builder.WriteRune(char) + case char >= '0' && char <= '9': + builder.WriteRune(char) + case char == '/', char == '.', char == '_', char == '-': + builder.WriteRune(char) + default: + builder.WriteByte('_') + } + } + + sanitized := strings.Trim(builder.String(), "/.") + if sanitized == "" { + return "unknown" + } + return sanitized +} diff --git a/internal/codegen/python_request_test.go b/internal/codegen/python_request_test.go new file mode 100644 index 0000000..fbcd649 --- /dev/null +++ b/internal/codegen/python_request_test.go @@ -0,0 +1,99 @@ +package codegen + +import ( + "testing" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/pluginpb" +) + +func TestPrepareRequestForProtogen_SynthesizesGoPackageForPython(t *testing.T) { + req := &pluginpb.CodeGeneratorRequest{ + ProtoFile: []*descriptorpb.FileDescriptorProto{ + { + Name: proto.String("proto/notebook.proto"), + Package: proto.String("notebook.v1"), + Options: &descriptorpb.FileOptions{}, + }, + }, + } + + PrepareRequestForProtogen(req, Options{Language: LanguagePython}) + + got := req.ProtoFile[0].GetOptions().GetGoPackage() + if got == "" { + t.Fatal("GoPackage was not synthesized") + } + if got == "notebook.v1" { + t.Fatalf("GoPackage = %q, want an import path with explicit package suffix", got) + } +} + +func TestPrepareRequestForProtogen_DoesNotSynthesizeGoPackageForGo(t *testing.T) { + req := &pluginpb.CodeGeneratorRequest{ + ProtoFile: []*descriptorpb.FileDescriptorProto{ + { + Name: proto.String("proto/notebook.proto"), + Package: proto.String("notebook.v1"), + Options: &descriptorpb.FileOptions{}, + }, + }, + } + + PrepareRequestForProtogen(req, Options{Language: LanguageGo}) + + if got := req.ProtoFile[0].GetOptions().GetGoPackage(); got != "" { + t.Fatalf("GoPackage = %q, want empty for Go mode", got) + } +} + +func TestPrepareRequestForProtogen_PreservesExplicitGoPackage(t *testing.T) { + req := &pluginpb.CodeGeneratorRequest{ + ProtoFile: []*descriptorpb.FileDescriptorProto{ + { + Name: proto.String("proto/notebook.proto"), + Package: proto.String("notebook.v1"), + Options: &descriptorpb.FileOptions{ + GoPackage: proto.String("example.com/acme/notebook;notebookv1"), + }, + }, + }, + } + + PrepareRequestForProtogen(req, Options{Language: LanguagePython}) + + if got := req.ProtoFile[0].GetOptions().GetGoPackage(); got != "example.com/acme/notebook;notebookv1" { + t.Fatalf("GoPackage = %q, want explicit value preserved", got) + } +} + +func TestPrepareRequestForProtogen_AllowsPythonProtogenNewWithoutGoPackage(t *testing.T) { + req := &pluginpb.CodeGeneratorRequest{ + Parameter: proto.String("paths=source_relative,lang=python"), + FileToGenerate: []string{"proto/notebook.proto"}, + ProtoFile: []*descriptorpb.FileDescriptorProto{ + { + Name: proto.String("proto/notebook.proto"), + Package: proto.String("notebook.v1"), + Syntax: proto.String("proto3"), + Options: &descriptorpb.FileOptions{}, + }, + }, + } + opts, err := ParseOptions(req.GetParameter()) + if err != nil { + t.Fatalf("ParseOptions: %v", err) + } + PrepareRequestForProtogen(req, opts) + + parser := NewOptionsParser() + plugin, err := (protogen.Options{ParamFunc: parser.Set}).New(req) + if err != nil { + t.Fatalf("protogen.Options.New() failed after Python request preparation: %v", err) + } + if got := plugin.FilesByPath["proto/notebook.proto"].GoImportPath; got == "" { + t.Fatal("GoImportPath is empty after request preparation") + } +} diff --git a/internal/codegen/python_support.go b/internal/codegen/python_support.go new file mode 100644 index 0000000..3fc2d60 --- /dev/null +++ b/internal/codegen/python_support.go @@ -0,0 +1,14 @@ +package codegen + +import "google.golang.org/protobuf/compiler/protogen" + +func emitPythonSupportFiles(plugin *protogen.Plugin) error { + generated := plugin.NewGeneratedFile("mcp/__init__.py", "") + generated.P("# Code generated by protoc-gen-mcp. DO NOT EDIT.") + generated.P(`"""Bridge generated mcp.options.* modules with the official MCP SDK package."""`) + generated.P("from pkgutil import extend_path") + generated.P() + generated.P("__path__ = extend_path(__path__, __name__)") + + return nil +} diff --git a/internal/codegen/python_type_collect.go b/internal/codegen/python_type_collect.go new file mode 100644 index 0000000..bcb19ae --- /dev/null +++ b/internal/codegen/python_type_collect.go @@ -0,0 +1,586 @@ +package codegen + +import ( + "fmt" + "sort" + "strings" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/reflect/protoreflect" +) + +func CollectPythonTypeGraph(file *protogen.File, runtime PythonRuntime) (PythonTypeGraph, error) { + methods, err := collectPythonGraphMethods(file) + if err != nil { + return PythonTypeGraph{}, err + } + return collectPythonTypeGraphFromMethods(file, runtime, methods, false) +} + +func collectPythonTypeGraphFromMethods(file *protogen.File, runtime PythonRuntime, methods []*protogen.Method, includeCurrentFileTypes bool) (PythonTypeGraph, error) { + collector := pythonTypeCollector{ + rootProtoPath: file.Desc.Path(), + graph: PythonTypeGraph{ + Runtime: runtime, + CurrentFile: newPythonTypeOwner(file.Desc.Path(), file.Desc.Path()), + }, + ownedPublicNames: map[string]map[string]string{}, + importAliases: map[string]string{}, + seenMessages: map[protoreflect.FullName]bool{}, + seenEnums: map[protoreflect.FullName]bool{}, + } + + for _, method := range methods { + if err := collector.collectMessage(method.Input); err != nil { + return PythonTypeGraph{}, err + } + if err := collector.collectMessage(method.Output); err != nil { + return PythonTypeGraph{}, err + } + } + if includeCurrentFileTypes { + if err := collector.collectCurrentFileTypes(file); err != nil { + return PythonTypeGraph{}, err + } + } + + return collector.graph, nil +} + +func collectPythonTypeGraphFromRefs(file *protogen.File, runtime PythonRuntime, refs map[string]struct{}) (PythonTypeGraph, error) { + collector := pythonTypeCollector{ + rootProtoPath: file.Desc.Path(), + graph: PythonTypeGraph{ + Runtime: runtime, + CurrentFile: newPythonTypeOwner(file.Desc.Path(), file.Desc.Path()), + }, + ownedPublicNames: map[string]map[string]string{}, + importAliases: map[string]string{}, + seenMessages: map[protoreflect.FullName]bool{}, + seenEnums: map[protoreflect.FullName]bool{}, + } + + messageIndex := map[string]*protogen.Message{} + enumIndex := map[string]*protogen.Enum{} + indexPythonFileTypes(file.Messages, file.Enums, messageIndex, enumIndex) + + orderedRefs := make([]string, 0, len(refs)) + for ref := range refs { + orderedRefs = append(orderedRefs, ref) + } + sort.Strings(orderedRefs) + + for _, ref := range orderedRefs { + switch { + case messageIndex[ref] != nil: + if err := collector.collectMessage(messageIndex[ref]); err != nil { + return PythonTypeGraph{}, err + } + case enumIndex[ref] != nil: + if err := collector.collectEnum(enumIndex[ref]); err != nil { + return PythonTypeGraph{}, err + } + default: + return PythonTypeGraph{}, fmt.Errorf("python type %q not found in file %q", ref, file.Desc.Path()) + } + } + + return collector.graph, nil +} + +func augmentPythonModelWithCurrentTypeRefs(file *protogen.File, model *FileModel, refs map[string]struct{}) error { + if model == nil || len(refs) == 0 { + return nil + } + + graph := model.PythonTypes + if graph == nil { + collected, err := collectPythonTypeGraphFromRefs(file, model.Options.PythonRuntime, refs) + if err != nil { + return err + } + model.PythonTypes = &collected + return nil + } + + extra, err := collectPythonTypeGraphFromRefs(file, model.Options.PythonRuntime, refs) + if err != nil { + return err + } + return mergePythonTypeGraphs(graph, extra) +} + +func mergePythonTypeGraphs(base *PythonTypeGraph, extra PythonTypeGraph) error { + if base == nil { + return nil + } + + importsByProtoPath := make(map[string]struct{}, len(base.Imports)) + for _, item := range base.Imports { + importsByProtoPath[item.ProtoPath] = struct{}{} + } + for _, item := range extra.Imports { + if _, exists := importsByProtoPath[item.ProtoPath]; exists { + continue + } + base.Imports = append(base.Imports, item) + importsByProtoPath[item.ProtoPath] = struct{}{} + } + + typesByFullName := make(map[string]struct{}, len(base.Types)) + currentPublicNames := make(map[string]string) + for _, typ := range base.Types { + typesByFullName[typ.ProtoFullName] = struct{}{} + if typ.Owner.IsCurrentFile { + currentPublicNames[typ.PublicName] = typ.ProtoFullName + } + } + for _, typ := range extra.Types { + if existing, ok := currentPublicNames[typ.PublicName]; ok && typ.Owner.IsCurrentFile && existing != typ.ProtoFullName { + return fmt.Errorf("python public type name collision for %q between %s and %s", typ.PublicName, existing, typ.ProtoFullName) + } + if _, exists := typesByFullName[typ.ProtoFullName]; exists { + continue + } + base.Types = append(base.Types, typ) + typesByFullName[typ.ProtoFullName] = struct{}{} + if typ.Owner.IsCurrentFile { + currentPublicNames[typ.PublicName] = typ.ProtoFullName + } + } + + return nil +} + +func collectPythonGraphMethods(file *protogen.File) ([]*protogen.Method, error) { + var methods []*protogen.Method + for _, service := range file.Services { + for _, method := range service.Methods { + _, include, err := selectToolMethod(method) + if err != nil { + return nil, err + } + if !include { + continue + } + methods = append(methods, method) + } + } + return methods, nil +} + +func indexPythonFileTypes(messages []*protogen.Message, enums []*protogen.Enum, messageIndex map[string]*protogen.Message, enumIndex map[string]*protogen.Enum) { + for _, enum := range enums { + enumIndex[string(enum.Desc.FullName())] = enum + } + for _, message := range messages { + messageIndex[string(message.Desc.FullName())] = message + indexPythonFileTypes(message.Messages, message.Enums, messageIndex, enumIndex) + } +} + +type pythonTypeCollector struct { + rootProtoPath string + graph PythonTypeGraph + ownedPublicNames map[string]map[string]string + importAliases map[string]string + seenMessages map[protoreflect.FullName]bool + seenEnums map[protoreflect.FullName]bool +} + +func (c *pythonTypeCollector) collectCurrentFileTypes(file *protogen.File) error { + for _, message := range file.Messages { + if err := c.collectMessageTree(message); err != nil { + return err + } + } + for _, enum := range file.Enums { + if err := c.collectEnum(enum); err != nil { + return err + } + } + return nil +} + +func (c *pythonTypeCollector) collectMessageTree(message *protogen.Message) error { + if err := c.collectMessage(message); err != nil { + return err + } + for _, nested := range message.Messages { + if err := c.collectMessageTree(nested); err != nil { + return err + } + } + for _, enum := range message.Enums { + if err := c.collectEnum(enum); err != nil { + return err + } + } + return nil +} + +func (c *pythonTypeCollector) collectMessage(message *protogen.Message) error { + if message == nil || message.Desc.IsMapEntry() { + return nil + } + + if wkt, ok, err := classifyPythonWellKnownType(message.Desc.FullName()); ok { + if err != nil { + return err + } + if wkt != PythonWellKnownTypeNone { + return nil + } + } + + if c.seenMessages[message.Desc.FullName()] { + return nil + } + c.seenMessages[message.Desc.FullName()] = true + + owner, err := c.ownerForProtoPath(message.Desc.ParentFile().Path()) + if err != nil { + return err + } + + publicName := pythonPublicTypeName(message) + ownerKey := string(message.Desc.FullName()) + if err := c.checkOwnedPublicName(owner, "public type name", publicName, ownerKey); err != nil { + return err + } + + model := PythonType{ + Kind: PythonTypeKindMessage, + ProtoFullName: string(message.Desc.FullName()), + ProtoName: string(message.Desc.Name()), + PublicName: publicName, + Owner: owner, + NestingPath: descriptorTypePath(message.Desc.FullName(), message.Desc.ParentFile().Package()), + } + + oneofWrappers := make(map[protoreflect.Name]string, len(message.Oneofs)) + for _, oneof := range message.Oneofs { + if oneof.Desc.IsSynthetic() { + continue + } + wrapperName := pythonOneofWrapperName(publicName, string(oneof.Desc.Name())) + if err := c.checkOwnedPublicName(owner, "oneof wrapper name", wrapperName, ownerKey+"."+string(oneof.Desc.Name())); err != nil { + return err + } + oneofWrappers[oneof.Desc.Name()] = wrapperName + } + + for _, field := range message.Fields { + fieldModel, err := c.collectField(field, oneofWrappers, publicName) + if err != nil { + return err + } + model.Fields = append(model.Fields, fieldModel) + } + + for _, oneof := range message.Oneofs { + if oneof.Desc.IsSynthetic() { + continue + } + wrapperName := oneofWrappers[oneof.Desc.Name()] + oneofModel := PythonOneof{ + ProtoName: string(oneof.Desc.Name()), + WrapperName: wrapperName, + Variants: make([]PythonOneofVariant, 0, len(oneof.Fields)), + } + for _, field := range oneof.Fields { + ref, err := c.typeRefForField(field) + if err != nil { + return err + } + variantWrapper := pythonOneofVariantWrapperName(publicName, string(oneof.Desc.Name()), string(field.Desc.Name())) + if err := c.checkOwnedPublicName(owner, "oneof wrapper name", variantWrapper, ownerKey+"."+string(oneof.Desc.Name())+"."+string(field.Desc.Name())); err != nil { + return err + } + oneofModel.Variants = append(oneofModel.Variants, PythonOneofVariant{ + ProtoName: string(field.Desc.Name()), + FieldNumber: int(field.Desc.Number()), + WrapperName: variantWrapper, + Type: ref, + HasPresence: field.Desc.HasPresence(), + }) + } + model.Oneofs = append(model.Oneofs, oneofModel) + } + + c.graph.Types = append(c.graph.Types, model) + return nil +} + +func (c *pythonTypeCollector) collectEnum(enum *protogen.Enum) error { + if enum == nil { + return nil + } + if c.seenEnums[enum.Desc.FullName()] { + return nil + } + c.seenEnums[enum.Desc.FullName()] = true + + owner, err := c.ownerForProtoPath(enum.Desc.ParentFile().Path()) + if err != nil { + return err + } + + publicName := pythonPublicEnumName(enum) + if err := c.checkOwnedPublicName(owner, "public type name", publicName, string(enum.Desc.FullName())); err != nil { + return err + } + + model := PythonType{ + Kind: PythonTypeKindEnum, + ProtoFullName: string(enum.Desc.FullName()), + ProtoName: string(enum.Desc.Name()), + PublicName: publicName, + Owner: owner, + NestingPath: descriptorTypePath(enum.Desc.FullName(), enum.Desc.ParentFile().Package()), + } + for _, value := range enum.Values { + metadata, err := loadEnumValueMetadata(value) + if err != nil { + return err + } + model.EnumValues = append(model.EnumValues, PythonEnumValue{ + ProtoName: string(value.Desc.Name()), + Number: int32(value.Desc.Number()), + Hidden: metadata.Hidden, + }) + } + + c.graph.Types = append(c.graph.Types, model) + return nil +} + +func (c *pythonTypeCollector) collectField(field *protogen.Field, oneofWrappers map[protoreflect.Name]string, parentPublicName string) (PythonField, error) { + typeRef, err := c.typeRefForField(field) + if err != nil { + return PythonField{}, err + } + + model := PythonField{ + ProtoName: string(field.Desc.Name()), + JSONName: field.Desc.JSONName(), + Number: int(field.Desc.Number()), + Type: typeRef, + IsRepeated: field.Desc.IsList() && !field.Desc.IsMap(), + IsMap: field.Desc.IsMap(), + HasPresence: field.Desc.HasPresence(), + IsSchemaRequired: !field.Desc.IsList() && + !field.Desc.IsMap() && + (field.Oneof == nil || field.Oneof.Desc.IsSynthetic()) && + !field.Desc.HasOptionalKeyword(), + } + + if field.Oneof != nil && !field.Oneof.Desc.IsSynthetic() { + model.OneofProtoName = string(field.Oneof.Desc.Name()) + model.OneofWrapperName = oneofWrappers[field.Oneof.Desc.Name()] + model.VariantWrapperName = pythonOneofVariantWrapperName(parentPublicName, string(field.Oneof.Desc.Name()), string(field.Desc.Name())) + } + + if field.Desc.IsMap() && field.Message != nil && len(field.Message.Fields) == 2 { + model.MapKeyScalar = pythonScalarForKind(field.Message.Fields[0].Desc.Kind()) + valueRef, err := c.typeRefForField(field.Message.Fields[1]) + if err != nil { + return PythonField{}, err + } + model.MapValue = &valueRef + } + + return model, nil +} + +func (c *pythonTypeCollector) typeRefForField(field *protogen.Field) (PythonTypeRef, error) { + switch field.Desc.Kind() { + case protoreflect.BoolKind: + return PythonTypeRef{Scalar: PythonScalarBool}, nil + case protoreflect.StringKind: + return PythonTypeRef{Scalar: PythonScalarString}, nil + case protoreflect.BytesKind: + return PythonTypeRef{Scalar: PythonScalarBytes}, nil + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + return PythonTypeRef{Scalar: PythonScalarInt32}, nil + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + return PythonTypeRef{Scalar: PythonScalarUInt32}, nil + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + return PythonTypeRef{Scalar: PythonScalarInt64}, nil + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + return PythonTypeRef{Scalar: PythonScalarUInt64}, nil + case protoreflect.FloatKind: + return PythonTypeRef{Scalar: PythonScalarFloat}, nil + case protoreflect.DoubleKind: + return PythonTypeRef{Scalar: PythonScalarDouble}, nil + case protoreflect.EnumKind: + if err := c.collectEnum(field.Enum); err != nil { + return PythonTypeRef{}, err + } + owner, err := c.ownerForProtoPath(field.Enum.Desc.ParentFile().Path()) + if err != nil { + return PythonTypeRef{}, err + } + return PythonTypeRef{ + ProtoFullName: string(field.Enum.Desc.FullName()), + ProtoName: string(field.Enum.Desc.Name()), + PublicName: pythonPublicEnumName(field.Enum), + Owner: owner, + IsEnum: true, + }, nil + case protoreflect.MessageKind: + if field.Message == nil { + return PythonTypeRef{}, nil + } + wkt, ok, err := classifyPythonWellKnownType(field.Message.Desc.FullName()) + if err != nil { + return PythonTypeRef{}, err + } + owner, ownerErr := c.ownerForProtoPath(field.Message.Desc.ParentFile().Path()) + if ownerErr != nil { + return PythonTypeRef{}, ownerErr + } + if ok { + return PythonTypeRef{ + ProtoFullName: string(field.Message.Desc.FullName()), + ProtoName: string(field.Message.Desc.Name()), + Owner: owner, + WellKnownType: wkt, + IsMessage: true, + }, nil + } + if err := c.collectMessage(field.Message); err != nil { + return PythonTypeRef{}, err + } + return PythonTypeRef{ + ProtoFullName: string(field.Message.Desc.FullName()), + ProtoName: string(field.Message.Desc.Name()), + PublicName: pythonPublicTypeName(field.Message), + Owner: owner, + IsMessage: true, + }, nil + default: + return PythonTypeRef{}, nil + } +} + +func (c *pythonTypeCollector) ownerForProtoPath(protoPath string) (PythonTypeOwner, error) { + owner := newPythonTypeOwner(protoPath, c.rootProtoPath) + if owner.IsCurrentFile { + return owner, nil + } + + if err := pythonCheckNameCollision(c.importAliases, "import alias", owner.PublicModule.ModuleAlias, owner.ProtoPath+":public"); err != nil { + return PythonTypeOwner{}, err + } + if err := pythonCheckNameCollision(c.importAliases, "import alias", owner.ProtobufModule.ModuleAlias, owner.ProtoPath+":protobuf"); err != nil { + return PythonTypeOwner{}, err + } + + for _, existing := range c.graph.Imports { + if existing.ProtoPath == owner.ProtoPath { + return owner, nil + } + } + c.graph.Imports = append(c.graph.Imports, PythonImport{ + ProtoPath: owner.ProtoPath, + PublicModule: owner.PublicModule, + ProtobufModule: owner.ProtobufModule, + }) + return owner, nil +} + +func (c *pythonTypeCollector) checkOwnedPublicName(owner PythonTypeOwner, category, name, ownerKey string) error { + return pythonCheckOwnedNameCollision(c.ownedPublicNames, owner.PublicModule.ModulePath, category, name, ownerKey) +} + +func newPythonTypeOwner(protoPath, currentProtoPath string) PythonTypeOwner { + isCurrent := protoPath == currentProtoPath + return PythonTypeOwner{ + ProtoPath: protoPath, + IsCurrentFile: isCurrent, + PublicModule: PythonModuleRef{ + ModulePath: pythonPublicModulePathForProtoPath(protoPath), + ModuleAlias: pythonPublicModuleAliasForProtoPath(protoPath, isCurrent), + ModuleBasename: pythonPublicBasenameModuleForProtoPath(protoPath), + }, + ProtobufModule: PythonModuleRef{ + ModulePath: pythonModulePathForProtoPath(protoPath), + ModuleAlias: pythonModuleAliasForProtoPath(protoPath, isCurrent), + ModuleBasename: pythonBasenameModuleForProtoPath(protoPath), + }, + } +} + +func classifyPythonWellKnownType(fullName protoreflect.FullName) (PythonWellKnownType, bool, error) { + switch fullName { + case "google.protobuf.Any": + return PythonWellKnownTypeAny, true, nil + case "google.protobuf.Empty": + return PythonWellKnownTypeEmpty, true, nil + case "google.protobuf.Timestamp": + return PythonWellKnownTypeTimestamp, true, nil + case "google.protobuf.Duration": + return PythonWellKnownTypeDuration, true, nil + case "google.protobuf.FieldMask": + return PythonWellKnownTypeFieldMask, true, nil + case "google.protobuf.Struct": + return PythonWellKnownTypeStruct, true, nil + case "google.protobuf.Value": + return PythonWellKnownTypeValue, true, nil + case "google.protobuf.ListValue": + return PythonWellKnownTypeListValue, true, nil + case "google.protobuf.BoolValue": + return PythonWellKnownTypeBoolValue, true, nil + case "google.protobuf.StringValue": + return PythonWellKnownTypeStringValue, true, nil + case "google.protobuf.BytesValue": + return PythonWellKnownTypeBytesValue, true, nil + case "google.protobuf.Int32Value": + return PythonWellKnownTypeInt32Value, true, nil + case "google.protobuf.UInt32Value": + return PythonWellKnownTypeUInt32Value, true, nil + case "google.protobuf.Int64Value": + return PythonWellKnownTypeInt64Value, true, nil + case "google.protobuf.UInt64Value": + return PythonWellKnownTypeUInt64Value, true, nil + case "google.protobuf.FloatValue": + return PythonWellKnownTypeFloatValue, true, nil + case "google.protobuf.DoubleValue": + return PythonWellKnownTypeDoubleValue, true, nil + default: + if isGoogleWellKnownType(fullName) { + return PythonWellKnownTypeNone, true, fmt.Errorf("well-known type %q is not supported", fullName) + } + return PythonWellKnownTypeNone, false, nil + } +} + +func isGoogleWellKnownType(fullName protoreflect.FullName) bool { + return strings.HasPrefix(string(fullName), "google.protobuf.") +} + +func pythonScalarForKind(kind protoreflect.Kind) PythonScalar { + switch kind { + case protoreflect.BoolKind: + return PythonScalarBool + case protoreflect.StringKind: + return PythonScalarString + case protoreflect.BytesKind: + return PythonScalarBytes + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + return PythonScalarInt32 + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + return PythonScalarUInt32 + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + return PythonScalarInt64 + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + return PythonScalarUInt64 + case protoreflect.FloatKind: + return PythonScalarFloat + case protoreflect.DoubleKind: + return PythonScalarDouble + default: + return PythonScalarUnknown + } +} diff --git a/internal/codegen/python_type_collect_test.go b/internal/codegen/python_type_collect_test.go new file mode 100644 index 0000000..8395cf0 --- /dev/null +++ b/internal/codegen/python_type_collect_test.go @@ -0,0 +1,498 @@ +package codegen + +import ( + "strings" + "testing" +) + +func TestCollectPythonTypeGraph_CrossFileOwnership(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/shared.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/shared;sharedv1";`, + `message SharedLeaf {`, + ` string value = 1;`, + `}`, + `message SharedRequest {`, + ` SharedLeaf leaf = 1;`, + `}`, + `message SharedResponse {}`, + ``, + }, "\n"), + "test/v1/service.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/service;servicev1";`, + `import "test/v1/shared.proto";`, + `message LocalEnvelope {`, + ` SharedLeaf leaf = 1;`, + `}`, + `service CrossFileAPI {`, + ` rpc UseShared(SharedRequest) returns (LocalEnvelope);`, + `}`, + ``, + }, "\n"), + }, "test/v1/service.proto") + + file := plugin.FilesByPath["test/v1/service.proto"] + if file == nil { + t.Fatal("service proto file not found in plugin") + } + sharedFile := plugin.FilesByPath["test/v1/shared.proto"] + if sharedFile == nil { + t.Fatal("shared proto file not found in plugin") + } + + graph, err := CollectPythonTypeGraph(file, PythonRuntimeGoogleProtobuf) + if err != nil { + t.Fatalf("CollectPythonTypeGraph: %v", err) + } + + if got, want := graph.CurrentFile.ProtoPath, "test/v1/service.proto"; got != want { + t.Fatalf("CurrentFile.ProtoPath = %q, want %q", got, want) + } + if got, want := graph.CurrentFile.PublicModule.ModulePath, "test.v1.service_mcp"; got != want { + t.Fatalf("CurrentFile.PublicModule.ModulePath = %q, want %q", got, want) + } + if got, want := graph.CurrentFile.ProtobufModule.ModulePath, "test.v1.service_pb2"; got != want { + t.Fatalf("CurrentFile.ProtobufModule.ModulePath = %q, want %q", got, want) + } + + localType := findPythonTypeByProtoName(t, graph, "test.v1.LocalEnvelope") + if !localType.Owner.IsCurrentFile { + t.Fatal("LocalEnvelope should be owned by the current file") + } + if got, want := localType.Owner.PublicModule.ModuleAlias, pythonPublicModuleAliasForProtoPath(file.Desc.Path(), true); got != want { + t.Fatalf("LocalEnvelope public owner alias = %q, want %q", got, want) + } + if got, want := localType.Owner.ProtobufModule.ModuleAlias, pythonModuleAliasForProtoPath(file.Desc.Path(), true); got != want { + t.Fatalf("LocalEnvelope protobuf owner alias = %q, want %q", got, want) + } + + sharedType := findPythonTypeByProtoName(t, graph, "test.v1.SharedRequest") + if sharedType.Owner.IsCurrentFile { + t.Fatal("SharedRequest should be marked as cross-file") + } + if got, want := sharedType.Owner.ProtoPath, "test/v1/shared.proto"; got != want { + t.Fatalf("SharedRequest owner proto path = %q, want %q", got, want) + } + if got, want := sharedType.Owner.PublicModule.ModuleAlias, pythonPublicModuleAliasForProtoPath(sharedFile.Desc.Path(), false); got != want { + t.Fatalf("SharedRequest public owner alias = %q, want %q", got, want) + } + if got, want := sharedType.Owner.ProtobufModule.ModuleAlias, pythonModuleAlias(sharedFile, false); got != want { + t.Fatalf("SharedRequest protobuf owner alias = %q, want %q", got, want) + } + + field := findPythonFieldByProtoName(t, localType, "leaf") + if got, want := field.Type.ProtoFullName, "test.v1.SharedLeaf"; got != want { + t.Fatalf("field type proto full name = %q, want %q", got, want) + } + if field.Type.Owner.IsCurrentFile { + t.Fatal("cross-file field type should not be marked current-file owned") + } + if got, want := field.Type.Owner.PublicModule.ModulePath, "test.v1.shared_mcp"; got != want { + t.Fatalf("field type public owner module path = %q, want %q", got, want) + } + if got, want := field.Type.Owner.ProtobufModule.ModulePath, "test.v1.shared_pb2"; got != want { + t.Fatalf("field type protobuf owner module path = %q, want %q", got, want) + } + if len(graph.Imports) != 1 { + t.Fatalf("import count = %d, want 1", len(graph.Imports)) + } + if got, want := graph.Imports[0].PublicModule.ModuleAlias, pythonPublicModuleAliasForProtoPath(sharedFile.Desc.Path(), false); got != want { + t.Fatalf("import public alias = %q, want %q", got, want) + } + if got, want := graph.Imports[0].ProtobufModule.ModuleAlias, pythonModuleAlias(sharedFile, false); got != want { + t.Fatalf("import protobuf alias = %q, want %q", got, want) + } +} + +func TestCollectPythonTypeGraph_FailsOnNameCollision(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/collision.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/collision;collisionv1";`, + `message OuterInner {}`, + `message Outer {`, + ` message Inner {}`, + ` Inner inner = 1;`, + `}`, + `message CollisionResponse {}`, + `service CollisionAPI {`, + ` rpc Check(Outer) returns (OuterInner);`, + `}`, + ``, + }, "\n"), + }, "test/v1/collision.proto") + + file := plugin.FilesByPath["test/v1/collision.proto"] + if file == nil { + t.Fatal("collision proto file not found in plugin") + } + + _, err := CollectPythonTypeGraph(file, PythonRuntimeGoogleProtobuf) + if err == nil { + t.Fatal("CollectPythonTypeGraph unexpectedly succeeded") + } + if !strings.Contains(err.Error(), `python public type name collision for "OuterInner"`) { + t.Fatalf("CollectPythonTypeGraph error = %v, want collision diagnostic", err) + } +} + +func TestCollectPythonTypeGraph_TracksOneofVariantsAndPresence(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/oneof.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/oneof;oneofv1";`, + `message VariantMessage {`, + ` string note = 1;`, + `}`, + `message ShapeRequest {`, + ` optional string nickname = 1;`, + ` oneof selector {`, + ` string city_alias = 2;`, + ` int64 city_id = 3;`, + ` VariantMessage city_details = 4;`, + ` }`, + `}`, + `message ShapeResponse {}`, + `service ShapeAPI {`, + ` rpc Describe(ShapeRequest) returns (ShapeResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/oneof.proto") + + file := plugin.FilesByPath["test/v1/oneof.proto"] + if file == nil { + t.Fatal("oneof proto file not found in plugin") + } + + graph, err := CollectPythonTypeGraph(file, PythonRuntimeGoogleProtobuf) + if err != nil { + t.Fatalf("CollectPythonTypeGraph: %v", err) + } + + shape := findPythonTypeByProtoName(t, graph, "test.v1.ShapeRequest") + nickname := findPythonFieldByProtoName(t, shape, "nickname") + if !nickname.HasPresence { + t.Fatal("optional field should track presence") + } + if nickname.OneofProtoName != "" { + t.Fatalf("optional field should not belong to oneof, got %q", nickname.OneofProtoName) + } + + selector := findPythonOneofByProtoName(t, shape, "selector") + if got, want := selector.WrapperName, "ShapeRequestSelectorVariant"; got != want { + t.Fatalf("selector wrapper name = %q, want %q", got, want) + } + if len(selector.Variants) != 3 { + t.Fatalf("selector variant count = %d, want 3", len(selector.Variants)) + } + + cityID := findPythonVariantByProtoName(t, selector, "city_id") + if !cityID.HasPresence { + t.Fatal("oneof variant should track presence") + } + if got, want := cityID.WrapperName, "ShapeRequestSelectorCityIdVariant"; got != want { + t.Fatalf("city_id wrapper name = %q, want %q", got, want) + } + if got, want := cityID.Type.Scalar, PythonScalarInt64; got != want { + t.Fatalf("city_id scalar = %q, want %q", got, want) + } + + cityDetails := findPythonVariantByProtoName(t, selector, "city_details") + if got, want := cityDetails.Type.ProtoFullName, "test.v1.VariantMessage"; got != want { + t.Fatalf("city_details type = %q, want %q", got, want) + } +} + +func TestCollectPythonTypeGraph_ClassifiesSupportedWKTs(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/wkts.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/wkts;wktsv1";`, + `import "google/protobuf/any.proto";`, + `import "google/protobuf/duration.proto";`, + `import "google/protobuf/timestamp.proto";`, + `import "google/protobuf/wrappers.proto";`, + `message WKTRequest {`, + ` google.protobuf.Timestamp observed_at = 1;`, + ` google.protobuf.Duration ttl = 2;`, + ` google.protobuf.Any detail = 3;`, + ` google.protobuf.StringValue note = 4;`, + `}`, + `message WKTResponse {}`, + `service WKTAPI {`, + ` rpc Describe(WKTRequest) returns (WKTResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/wkts.proto") + + file := plugin.FilesByPath["test/v1/wkts.proto"] + if file == nil { + t.Fatal("wkt proto file not found in plugin") + } + + graph, err := CollectPythonTypeGraph(file, PythonRuntimeGoogleProtobuf) + if err != nil { + t.Fatalf("CollectPythonTypeGraph: %v", err) + } + + req := findPythonTypeByProtoName(t, graph, "test.v1.WKTRequest") + if got, want := findPythonFieldByProtoName(t, req, "observed_at").Type.WellKnownType, PythonWellKnownTypeTimestamp; got != want { + t.Fatalf("observed_at WKT = %q, want %q", got, want) + } + if got, want := findPythonFieldByProtoName(t, req, "ttl").Type.WellKnownType, PythonWellKnownTypeDuration; got != want { + t.Fatalf("ttl WKT = %q, want %q", got, want) + } + if got, want := findPythonFieldByProtoName(t, req, "detail").Type.WellKnownType, PythonWellKnownTypeAny; got != want { + t.Fatalf("detail WKT = %q, want %q", got, want) + } + if got, want := findPythonFieldByProtoName(t, req, "note").Type.WellKnownType, PythonWellKnownTypeStringValue; got != want { + t.Fatalf("note WKT = %q, want %q", got, want) + } +} + +func TestCollectPythonTypeGraph_AllowsSameFlattenedPublicNameAcrossDifferentOwners(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "left/v1/types.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package left.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/left;leftv1";`, + `message Outer {`, + ` message Inner {`, + ` string value = 1;`, + ` }`, + `}`, + ``, + }, "\n"), + "right/v1/types.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package right.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/right;rightv1";`, + `message Outer {`, + ` message Inner {`, + ` string value = 1;`, + ` }`, + `}`, + ``, + }, "\n"), + "test/v1/service.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/service;servicev1";`, + `import "left/v1/types.proto";`, + `import "right/v1/types.proto";`, + `message UseBothRequest {`, + ` left.v1.Outer.Inner left_value = 1;`, + ` right.v1.Outer.Inner right_value = 2;`, + `}`, + `message UseBothResponse {}`, + `service SharedNamesAPI {`, + ` rpc UseBoth(UseBothRequest) returns (UseBothResponse);`, + `}`, + ``, + }, "\n"), + }, "test/v1/service.proto") + + file := plugin.FilesByPath["test/v1/service.proto"] + if file == nil { + t.Fatal("service proto file not found in plugin") + } + + graph, err := CollectPythonTypeGraph(file, PythonRuntimeGoogleProtobuf) + if err != nil { + t.Fatalf("CollectPythonTypeGraph: %v", err) + } + + leftInner := findPythonTypeByProtoName(t, graph, "left.v1.Outer.Inner") + rightInner := findPythonTypeByProtoName(t, graph, "right.v1.Outer.Inner") + if got, want := leftInner.PublicName, "OuterInner"; got != want { + t.Fatalf("left public name = %q, want %q", got, want) + } + if got, want := rightInner.PublicName, "OuterInner"; got != want { + t.Fatalf("right public name = %q, want %q", got, want) + } + if leftInner.Owner.PublicModule.ModulePath == rightInner.Owner.PublicModule.ModulePath { + t.Fatalf("owner public modules should differ: left=%q right=%q", leftInner.Owner.PublicModule.ModulePath, rightInner.Owner.PublicModule.ModulePath) + } +} + +func TestCollectFileModel_PythonGraphIgnoresHiddenMethodsWithInvalidTypes(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/hidden.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/hidden;hiddenv1";`, + `import "google/protobuf/type.proto";`, + `import "mcp/options/v1/options.proto";`, + `message VisibleRequest {}`, + `message VisibleResponse {}`, + `message OuterInner {}`, + `message Outer {`, + ` message Inner {}`, + ` Inner inner = 1;`, + `}`, + `message UnsupportedRequest {`, + ` google.protobuf.Type payload = 1;`, + `}`, + `service HiddenAPI {`, + ` rpc Visible(VisibleRequest) returns (VisibleResponse);`, + ` rpc HiddenCollision(Outer) returns (OuterInner) {`, + ` option (mcp.options.v1.method) = { hidden: true };`, + ` }`, + ` rpc HiddenUnsupported(UnsupportedRequest) returns (VisibleResponse) {`, + ` option (mcp.options.v1.method) = { hidden: true };`, + ` }`, + `}`, + ``, + }, "\n"), + }, "test/v1/hidden.proto") + + file := plugin.FilesByPath["test/v1/hidden.proto"] + if file == nil { + t.Fatal("hidden proto file not found in plugin") + } + + model, err := CollectFileModel(file, Options{ + Language: LanguagePython, + PythonRuntime: PythonRuntimeGoogleProtobuf, + }) + if err != nil { + t.Fatalf("CollectFileModel: %v", err) + } + if model.PythonTypes == nil { + t.Fatal("PythonTypes should be populated for python target") + } + + if len(model.Services) != 1 || len(model.Services[0].Methods) != 1 { + t.Fatalf("visible method count = %d, want 1", len(model.Services[0].Methods)) + } + if got, want := model.Services[0].Methods[0].Name, "Visible"; got != want { + t.Fatalf("visible method name = %q, want %q", got, want) + } + + findPythonTypeByProtoName(t, *model.PythonTypes, "test.v1.VisibleRequest") + findPythonTypeByProtoName(t, *model.PythonTypes, "test.v1.VisibleResponse") + assertPythonTypeAbsent(t, *model.PythonTypes, "test.v1.Outer") + assertPythonTypeAbsent(t, *model.PythonTypes, "test.v1.Outer.Inner") + assertPythonTypeAbsent(t, *model.PythonTypes, "test.v1.OuterInner") + assertPythonTypeAbsent(t, *model.PythonTypes, "test.v1.UnsupportedRequest") +} + +func TestCollectPythonTypeGraph_IgnoresHiddenMethodsWithInvalidTypes(t *testing.T) { + plugin := newTempProtogenPlugin(t, map[string]string{ + "test/v1/hidden.proto": strings.Join([]string{ + `syntax = "proto3";`, + `package test.v1;`, + `option go_package = "github.com/easyp-tech/protoc-gen-mcp/internal/codegen/testdata/hidden;hiddenv1";`, + `import "google/protobuf/type.proto";`, + `import "mcp/options/v1/options.proto";`, + `message VisibleRequest {}`, + `message VisibleResponse {}`, + `message OuterInner {}`, + `message Outer {`, + ` message Inner {}`, + ` Inner inner = 1;`, + `}`, + `message UnsupportedRequest {`, + ` google.protobuf.Type payload = 1;`, + `}`, + `service HiddenAPI {`, + ` rpc Visible(VisibleRequest) returns (VisibleResponse);`, + ` rpc HiddenCollision(Outer) returns (OuterInner) {`, + ` option (mcp.options.v1.method) = { hidden: true };`, + ` }`, + ` rpc HiddenUnsupported(UnsupportedRequest) returns (VisibleResponse) {`, + ` option (mcp.options.v1.method) = { hidden: true };`, + ` }`, + `}`, + ``, + }, "\n"), + }, "test/v1/hidden.proto") + + file := plugin.FilesByPath["test/v1/hidden.proto"] + if file == nil { + t.Fatal("hidden proto file not found in plugin") + } + + graph, err := CollectPythonTypeGraph(file, PythonRuntimeGoogleProtobuf) + if err != nil { + t.Fatalf("CollectPythonTypeGraph: %v", err) + } + + findPythonTypeByProtoName(t, graph, "test.v1.VisibleRequest") + findPythonTypeByProtoName(t, graph, "test.v1.VisibleResponse") + assertPythonTypeAbsent(t, graph, "test.v1.Outer") + assertPythonTypeAbsent(t, graph, "test.v1.Outer.Inner") + assertPythonTypeAbsent(t, graph, "test.v1.OuterInner") + assertPythonTypeAbsent(t, graph, "test.v1.UnsupportedRequest") +} + +func findPythonTypeByProtoName(t *testing.T, graph PythonTypeGraph, protoFullName string) PythonType { + t.Helper() + + for _, typ := range graph.Types { + if typ.ProtoFullName == protoFullName { + return typ + } + } + + t.Fatalf("python type %q not found", protoFullName) + return PythonType{} +} + +func findPythonFieldByProtoName(t *testing.T, typ PythonType, protoName string) PythonField { + t.Helper() + + for _, field := range typ.Fields { + if field.ProtoName == protoName { + return field + } + } + + t.Fatalf("python field %q not found in %q", protoName, typ.ProtoFullName) + return PythonField{} +} + +func findPythonOneofByProtoName(t *testing.T, typ PythonType, protoName string) PythonOneof { + t.Helper() + + for _, oneof := range typ.Oneofs { + if oneof.ProtoName == protoName { + return oneof + } + } + + t.Fatalf("python oneof %q not found in %q", protoName, typ.ProtoFullName) + return PythonOneof{} +} + +func findPythonVariantByProtoName(t *testing.T, oneof PythonOneof, protoName string) PythonOneofVariant { + t.Helper() + + for _, variant := range oneof.Variants { + if variant.ProtoName == protoName { + return variant + } + } + + t.Fatalf("python oneof variant %q not found in %q", protoName, oneof.ProtoName) + return PythonOneofVariant{} +} + +func assertPythonTypeAbsent(t *testing.T, graph PythonTypeGraph, protoFullName string) { + t.Helper() + + for _, typ := range graph.Types { + if typ.ProtoFullName == protoFullName { + t.Fatalf("python type %q should be absent", protoFullName) + } + } +} diff --git a/internal/codegen/python_type_model.go b/internal/codegen/python_type_model.go new file mode 100644 index 0000000..4daf376 --- /dev/null +++ b/internal/codegen/python_type_model.go @@ -0,0 +1,132 @@ +package codegen + +type PythonTypeGraph struct { + Runtime PythonRuntime + CurrentFile PythonTypeOwner + Imports []PythonImport + Types []PythonType +} + +type PythonModuleRef struct { + ModulePath string + ModuleAlias string + ModuleBasename string +} + +type PythonImport struct { + ProtoPath string + PublicModule PythonModuleRef + ProtobufModule PythonModuleRef +} + +type PythonTypeOwner struct { + ProtoPath string + IsCurrentFile bool + PublicModule PythonModuleRef + ProtobufModule PythonModuleRef +} + +type PythonTypeKind string + +const ( + PythonTypeKindMessage PythonTypeKind = "message" + PythonTypeKindEnum PythonTypeKind = "enum" +) + +type PythonType struct { + Kind PythonTypeKind + ProtoFullName string + ProtoName string + PublicName string + Owner PythonTypeOwner + NestingPath []string + Fields []PythonField + Oneofs []PythonOneof + EnumValues []PythonEnumValue + WellKnownType PythonWellKnownType +} + +type PythonEnumValue struct { + ProtoName string + Number int32 + Hidden bool +} + +type PythonField struct { + ProtoName string + JSONName string + Number int + Type PythonTypeRef + IsRepeated bool + IsMap bool + MapKeyScalar PythonScalar + MapValue *PythonTypeRef + HasPresence bool + IsSchemaRequired bool + OneofProtoName string + OneofWrapperName string + VariantWrapperName string +} + +type PythonOneof struct { + ProtoName string + WrapperName string + Variants []PythonOneofVariant +} + +type PythonOneofVariant struct { + ProtoName string + FieldNumber int + WrapperName string + Type PythonTypeRef + HasPresence bool +} + +type PythonTypeRef struct { + ProtoFullName string + ProtoName string + PublicName string + Owner PythonTypeOwner + Scalar PythonScalar + WellKnownType PythonWellKnownType + IsEnum bool + IsMessage bool +} + +type PythonScalar string + +const ( + PythonScalarUnknown PythonScalar = "" + PythonScalarBool PythonScalar = "bool" + PythonScalarBytes PythonScalar = "bytes" + PythonScalarString PythonScalar = "string" + PythonScalarInt32 PythonScalar = "int32" + PythonScalarUInt32 PythonScalar = "uint32" + PythonScalarInt64 PythonScalar = "int64" + PythonScalarUInt64 PythonScalar = "uint64" + PythonScalarFloat PythonScalar = "float" + PythonScalarDouble PythonScalar = "double" +) + +type PythonWellKnownType string + +const ( + PythonWellKnownTypeNone PythonWellKnownType = "" + PythonWellKnownTypeAny PythonWellKnownType = "Any" + PythonWellKnownTypeEmpty PythonWellKnownType = "Empty" + PythonWellKnownTypeTimestamp PythonWellKnownType = "Timestamp" + PythonWellKnownTypeDuration PythonWellKnownType = "Duration" + PythonWellKnownTypeFieldMask PythonWellKnownType = "FieldMask" + PythonWellKnownTypeStruct PythonWellKnownType = "Struct" + PythonWellKnownTypeValue PythonWellKnownType = "Value" + PythonWellKnownTypeListValue PythonWellKnownType = "ListValue" + PythonWellKnownTypeBoolValue PythonWellKnownType = "BoolValue" + PythonWellKnownTypeStringValue PythonWellKnownType = "StringValue" + PythonWellKnownTypeBytesValue PythonWellKnownType = "BytesValue" + PythonWellKnownTypeInt32Value PythonWellKnownType = "Int32Value" + PythonWellKnownTypeUInt32Value PythonWellKnownType = "UInt32Value" + PythonWellKnownTypeInt64Value PythonWellKnownType = "Int64Value" + PythonWellKnownTypeUInt64Value PythonWellKnownType = "UInt64Value" + PythonWellKnownTypeFloatValue PythonWellKnownType = "FloatValue" + PythonWellKnownTypeDoubleValue PythonWellKnownType = "DoubleValue" +) diff --git a/internal/codegen/render_go.go b/internal/codegen/render_go.go new file mode 100644 index 0000000..0a2c5f6 --- /dev/null +++ b/internal/codegen/render_go.go @@ -0,0 +1,235 @@ +package codegen + +import ( + "fmt" + "strings" + + mcpoptionsv1 "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" + "google.golang.org/protobuf/compiler/protogen" +) + +func renderGoFile(plugin *protogen.Plugin, model FileModel) error { + goInfo, err := newGoRenderInfo(plugin, model) + if err != nil { + return err + } + + filename := model.GeneratedFilenamePrefix + ".mcp.go" + generated := plugin.NewGeneratedFile(filename, goInfo.file.GoImportPath) + + generated.P("// Code generated by protoc-gen-mcp. DO NOT EDIT.") + generated.P("// source: ", model.ProtoPath) + generated.P() + generated.P("package ", goInfo.file.GoPackageName) + generated.P() + + contextIdent := generated.QualifiedGoIdent(protogen.GoImportPath("context").Ident("Context")) + errorsIdent := generated.QualifiedGoIdent(protogen.GoImportPath("errors").Ident("New")) + mcpServerIdent := generated.QualifiedGoIdent(protogen.GoImportPath("github.com/modelcontextprotocol/go-sdk/mcp").Ident("Server")) + mcpruntimeImport := protogen.GoImportPath("github.com/easyp-tech/protoc-gen-mcp/mcpruntime") + registerOptionIdent := generated.QualifiedGoIdent(mcpruntimeImport.Ident("RegisterOption")) + registerToolIdent := generated.QualifiedGoIdent(mcpruntimeImport.Ident("RegisterProtoTool")) + toolSpecIdent := generated.QualifiedGoIdent(mcpruntimeImport.Ident("ToolSpec")) + + for _, service := range model.Services { + serviceGoName, err := goInfo.serviceGoName(service) + if err != nil { + return err + } + interfaceName := serviceGoName + "ToolHandler" + generated.P("// ", interfaceName, " defines the business logic required by generated MCP tools.") + generated.P("type ", interfaceName, " interface {") + for _, method := range service.Methods { + methodGoName, err := goInfo.methodGoName(method) + if err != nil { + return err + } + inputType, err := qualifyTypeRef(generated, goInfo, method.Input) + if err != nil { + return err + } + outputType, err := qualifyTypeRef(generated, goInfo, method.Output) + if err != nil { + return err + } + generated.P(methodGoName, "(ctx ", contextIdent, ", req *", inputType, ") (*", outputType, ", error)") + } + generated.P("}") + generated.P() + + registerName := "Register" + serviceGoName + "Tools" + generated.P("// ", registerName, " registers generated MCP tools for ", serviceGoName, ".") + generated.P("func ", registerName, "(server *", mcpServerIdent, ", impl ", interfaceName, ", opts ...", registerOptionIdent, ") error {") + generated.P("if impl == nil {") + generated.P("return ", errorsIdent, "(\"", registerName, ": impl is nil\")") + generated.P("}") + for _, method := range service.Methods { + methodGoName, err := goInfo.methodGoName(method) + if err != nil { + return err + } + specName := serviceGoName + "_" + methodGoName + "_ToolSpec" + inputType, err := qualifyTypeRef(generated, goInfo, method.Input) + if err != nil { + return err + } + outputType, err := qualifyTypeRef(generated, goInfo, method.Output) + if err != nil { + return err + } + generated.P("if err := ", registerToolIdent, "(server, ", toolSpecIdent, "[*", inputType, ", *", outputType, "]{") + generated.P("Name: ", quote(method.Name), ",") + generated.P("Title: ", quote(method.Title), ",") + generated.P("Description: ", quote(method.Description), ",") + generated.P("Namespace: ", quote(service.Namespace), ",") + generated.P("InputSchemaJSON: ", specName, "InputSchemaJSON,") + generated.P("OutputSchemaJSON: ", specName, "OutputSchemaJSON,") + generated.P("Annotations: ", stringifyAnnotations(generated, method.Annotations), ",") + generated.P("Icons: ", stringifyIcons(generated, method.Icons), ",") + generated.P("NewRequest: func() *", inputType, " { return &", inputType, "{} },") + generated.P("NewResponse: func() *", outputType, " { return &", outputType, "{} },") + generated.P("Handler: impl.", methodGoName, ",") + generated.P("}, opts...); err != nil {") + generated.P("return err") + generated.P("}") + } + generated.P("return nil") + generated.P("}") + generated.P() + + for _, method := range service.Methods { + methodGoName, err := goInfo.methodGoName(method) + if err != nil { + return err + } + specName := serviceGoName + "_" + methodGoName + "_ToolSpec" + generated.P("const ", specName, "InputSchemaJSON = ", quote(method.InputSchemaJSON)) + generated.P() + generated.P("const ", specName, "OutputSchemaJSON = ", quote(method.OutputSchemaJSON)) + generated.P() + } + } + + return nil +} + +type goRenderInfo struct { + file *protogen.File + messages map[string]*protogen.Message + methods map[string]*protogen.Method + services map[string]*protogen.Service +} + +func newGoRenderInfo(plugin *protogen.Plugin, model FileModel) (goRenderInfo, error) { + file := plugin.FilesByPath[model.ProtoPath] + if file == nil { + return goRenderInfo{}, fmt.Errorf("generated file %q not found in plugin", model.ProtoPath) + } + + info := goRenderInfo{ + file: file, + messages: map[string]*protogen.Message{}, + methods: map[string]*protogen.Method{}, + services: map[string]*protogen.Service{}, + } + for _, current := range plugin.Files { + indexGoMessages(info.messages, current.Messages) + for _, service := range current.Services { + info.services[string(service.Desc.FullName())] = service + for _, method := range service.Methods { + info.methods[string(method.Desc.FullName())] = method + } + } + } + + return info, nil +} + +func indexGoMessages(index map[string]*protogen.Message, messages []*protogen.Message) { + for _, message := range messages { + index[string(message.Desc.FullName())] = message + indexGoMessages(index, message.Messages) + } +} + +func (g goRenderInfo) serviceGoName(service ServiceModel) (string, error) { + resolved := g.services[service.ProtoFullName] + if resolved == nil { + return "", fmt.Errorf("service %q not found during Go render", service.ProtoFullName) + } + return resolved.GoName, nil +} + +func (g goRenderInfo) methodGoName(method MethodModel) (string, error) { + resolved := g.methods[method.ProtoFullName] + if resolved == nil { + return "", fmt.Errorf("method %q not found during Go render", method.ProtoFullName) + } + return resolved.GoName, nil +} + +func qualifyTypeRef(generated *protogen.GeneratedFile, goInfo goRenderInfo, ref TypeRef) (string, error) { + message := goInfo.messages[ref.ProtoFullName] + if message == nil { + return "", fmt.Errorf("message %q not found during Go render", ref.ProtoFullName) + } + return generated.QualifiedGoIdent(message.GoIdent), nil +} + +func quote(value string) string { + return fmt.Sprintf("%q", value) +} + +func stringifyAnnotations(generated *protogen.GeneratedFile, ann *mcpoptionsv1.ToolAnnotations) string { + if ann == nil { + return "nil" + } + mcpAnnIdent := generated.QualifiedGoIdent(protogen.GoImportPath("github.com/modelcontextprotocol/go-sdk/mcp").Ident("ToolAnnotations")) + + var fields []string + if ann.ReadOnlyHint { + fields = append(fields, fmt.Sprintf("ReadOnlyHint: %t,", ann.ReadOnlyHint)) + } + if ann.DestructiveHint != nil { + protoBoolIdent := generated.QualifiedGoIdent(protogen.GoImportPath("google.golang.org/protobuf/proto").Ident("Bool")) + fields = append(fields, fmt.Sprintf("DestructiveHint: %s(%t),", protoBoolIdent, *ann.DestructiveHint)) + } + if ann.IdempotentHint != false { + fields = append(fields, fmt.Sprintf("IdempotentHint: %t,", ann.IdempotentHint)) + } + if ann.OpenWorldHint != nil { + protoBoolIdent := generated.QualifiedGoIdent(protogen.GoImportPath("google.golang.org/protobuf/proto").Ident("Bool")) + fields = append(fields, fmt.Sprintf("OpenWorldHint: %s(%t),", protoBoolIdent, *ann.OpenWorldHint)) + } + if ann.Title != "" { + protoStringIdent := generated.QualifiedGoIdent(protogen.GoImportPath("google.golang.org/protobuf/proto").Ident("String")) + fields = append(fields, fmt.Sprintf("Title: %s(%q),", protoStringIdent, ann.Title)) + } + + if len(fields) == 0 { + return "&" + mcpAnnIdent + "{}" + } + return "&" + mcpAnnIdent + "{" + strings.Join(fields, " ") + "}" +} + +func stringifyIcons(generated *protogen.GeneratedFile, icons []*mcpoptionsv1.Icon) string { + if len(icons) == 0 { + return "nil" + } + mcpIconIdent := generated.QualifiedGoIdent(protogen.GoImportPath("github.com/modelcontextprotocol/go-sdk/mcp").Ident("Icon")) + + var items []string + for _, icon := range icons { + sizesStr := "nil" + if len(icon.GetSizes()) > 0 { + var quotedSizes []string + for _, s := range icon.GetSizes() { + quotedSizes = append(quotedSizes, fmt.Sprintf("%q", s)) + } + sizesStr = "[]string{" + strings.Join(quotedSizes, ", ") + "}" + } + items = append(items, fmt.Sprintf("%s{Source: %q, MIMEType: %q, Sizes: %s, Theme: %q},", + mcpIconIdent, icon.GetSrc(), icon.GetMimeType(), sizesStr, icon.GetTheme())) + } + return "[]" + mcpIconIdent + "{" + strings.Join(items, " ") + "}" +} diff --git a/internal/codegen/render_python.go b/internal/codegen/render_python.go new file mode 100644 index 0000000..323dc68 --- /dev/null +++ b/internal/codegen/render_python.go @@ -0,0 +1,386 @@ +package codegen + +import ( + "fmt" + "sort" + "strings" + + mcpoptionsv1 "github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1" + "google.golang.org/protobuf/compiler/protogen" +) + +func renderPythonFile(plugin *protogen.Plugin, model FileModel) error { + info, err := newPythonRenderInfo(plugin, model) + if err != nil { + return err + } + + filename := pythonOutputPath(info.file) + generated := plugin.NewGeneratedFile(filename, "") + + generated.P("# Code generated by protoc-gen-mcp. DO NOT EDIT.") + generated.P("# source: ", model.ProtoPath) + generated.P("from __future__ import annotations") + generated.P() + generated.P("import enum") + generated.P("import inspect") + generated.P("import json") + generated.P("import weakref") + generated.P("from dataclasses import dataclass, field") + generated.P("from typing import Any, Awaitable, Protocol, TypeAlias") + generated.P() + generated.P("import jsonschema") + generated.P("import mcp.server.lowlevel") + generated.P("import mcp.server.session") + generated.P("import mcp.shared.exceptions") + generated.P("import mcp.types") + generated.P("from google.protobuf import any_pb2, duration_pb2, empty_pb2, field_mask_pb2, json_format, struct_pb2, timestamp_pb2, wrappers_pb2") + for _, moduleRef := range info.publicImportsForModel(model) { + if moduleRef.Package == "" { + if moduleRef.Alias == moduleRef.Module { + generated.P("import ", moduleRef.Module) + } else { + generated.P("import ", moduleRef.Module, " as ", moduleRef.Alias) + } + continue + } + if moduleRef.Alias == moduleRef.Module { + generated.P("from ", moduleRef.Package, " import ", moduleRef.Module) + } else { + generated.P("from ", moduleRef.Package, " import ", moduleRef.Module, " as ", moduleRef.Alias) + } + } + for _, moduleRef := range info.protobufImportsForModel(model) { + if moduleRef.IsCurrent { + generated.P("try:") + generated.P(" from . import ", moduleRef.Module) + generated.P("except ImportError:") + if moduleRef.Alias == moduleRef.Module { + generated.P(" import ", moduleRef.Module) + } else { + generated.P(" import ", moduleRef.Module, " as ", moduleRef.Alias) + } + continue + } + if moduleRef.Package == "" { + if moduleRef.Alias == moduleRef.Module { + generated.P("import ", moduleRef.Module) + } else { + generated.P("import ", moduleRef.Module, " as ", moduleRef.Alias) + } + continue + } + if moduleRef.Alias == moduleRef.Module { + generated.P("from ", moduleRef.Package, " import ", moduleRef.Module) + } else { + generated.P("from ", moduleRef.Package, " import ", moduleRef.Module, " as ", moduleRef.Alias) + } + } + generated.P() + generated.P("ToolRequestContext = mcp.server.session.ServerSession") + generated.P() + if err := renderPythonPublicTypes(generated, info, model); err != nil { + return err + } + renderPythonRuntime(generated) + if err := renderPythonMappers(generated, info, model); err != nil { + return err + } + + for serviceIdx, service := range model.Services { + generated.P("class ", pythonProtocolName(service.ProtoName), "(Protocol):") + for _, method := range service.Methods { + inputType, err := info.pythonPublicMethodTypeRef(method.Input) + if err != nil { + return err + } + outputType, err := info.pythonPublicMethodTypeRef(method.Output) + if err != nil { + return err + } + generated.P(" def ", pythonMethodName(method.ProtoName), "(self, ctx: ToolRequestContext, req: ", inputType, ") -> ", outputType, " | Awaitable[", outputType, "]:") + generated.P(" ...") + } + generated.P() + + registerName := pythonRegisterName(service.ProtoName) + protocolName := pythonProtocolName(service.ProtoName) + generated.P("def ", registerName, "(server: mcp.server.lowlevel.Server, impl: ", protocolName, ", *, namespace: str | None = None) -> None:") + generated.P(" if impl is None:") + generated.P(" raise ValueError(\"", registerName, ": impl is nil\")") + generated.P(" registry = _install_server_handlers(server)") + generated.P(" resolved_namespace = _normalize_namespace(namespace, ", quote(service.Namespace), ")") + for _, method := range service.Methods { + inputType, err := info.pythonProtobufTypeRef(method.Input) + if err != nil { + return err + } + outputType, err := info.pythonProtobufTypeRef(method.Output) + if err != nil { + return err + } + inputMapper, err := info.pythonMapperHelperForMethodType("from_pb", method.Input) + if err != nil { + return err + } + outputMapper, err := info.pythonMapperHelperForMethodType("to_pb", method.Output) + if err != nil { + return err + } + schemaName := pythonSchemaConst(service.ProtoName, method.Name) + generated.P(" registry.add_tool(_RegisteredTool(") + generated.P(" name=_tool_name(resolved_namespace, ", quote(method.Name), "),") + generated.P(" title=", quote(method.Title), ",") + generated.P(" description=", quote(method.Description), ",") + generated.P(" input_schema_json=", schemaName, "_INPUT_SCHEMA_JSON,") + generated.P(" output_schema_json=", schemaName, "_OUTPUT_SCHEMA_JSON,") + generated.P(" request_type=", inputType, ",") + generated.P(" response_type=", outputType, ",") + generated.P(" from_pb=", inputMapper, ",") + generated.P(" to_pb=", outputMapper, ",") + generated.P(" handler=impl.", pythonMethodName(method.ProtoName), ",") + generated.P(" annotations=", pythonAnnotations(method.Annotations), ",") + generated.P(" icons=", pythonIcons(method.Icons), ",") + generated.P(" ))") + } + generated.P() + + for methodIdx, method := range service.Methods { + schemaName := pythonSchemaConst(service.ProtoName, method.Name) + generated.P(schemaName, "_INPUT_SCHEMA_JSON = ", quote(method.InputSchemaJSON)) + generated.P() + generated.P(schemaName, "_OUTPUT_SCHEMA_JSON = ", quote(method.OutputSchemaJSON)) + if methodIdx < len(service.Methods)-1 || serviceIdx < len(model.Services)-1 { + generated.P() + } + } + } + + return nil +} + +type pythonRenderInfo struct { + file *protogen.File + messages map[string]*protogen.Message + publicTypes map[string]PythonType + modules map[string]pythonModuleRef +} + +type pythonModuleRef struct { + Package string + Module string + Alias string + IsCurrent bool +} + +func newPythonRenderInfo(plugin *protogen.Plugin, model FileModel) (pythonRenderInfo, error) { + file := plugin.FilesByPath[model.ProtoPath] + if file == nil { + return pythonRenderInfo{}, fmt.Errorf("generated file %q not found in plugin", model.ProtoPath) + } + + info := pythonRenderInfo{ + file: file, + messages: map[string]*protogen.Message{}, + publicTypes: map[string]PythonType{}, + modules: map[string]pythonModuleRef{}, + } + for _, current := range plugin.Files { + pkg, module := pythonImportParts(current) + info.modules[current.Desc.Path()] = pythonModuleRef{ + Package: pkg, + Module: module, + Alias: pythonModuleAlias(current, current.Desc.Path() == file.Desc.Path()), + IsCurrent: current.Desc.Path() == file.Desc.Path(), + } + indexPythonMessages(info.messages, current.Messages) + } + if model.PythonTypes != nil { + for _, typ := range model.PythonTypes.Types { + info.publicTypes[typ.ProtoFullName] = typ + } + } + + return info, nil +} + +func indexPythonMessages(index map[string]*protogen.Message, messages []*protogen.Message) { + for _, message := range messages { + index[string(message.Desc.FullName())] = message + indexPythonMessages(index, message.Messages) + } +} + +func (p pythonRenderInfo) pythonProtobufTypeRef(ref TypeRef) (string, error) { + message := p.messages[ref.ProtoFullName] + if message == nil { + return "", fmt.Errorf("message %q not found during Python render", ref.ProtoFullName) + } + + messageFilePath := message.Desc.ParentFile().Path() + moduleRef, ok := p.modules[messageFilePath] + if !ok { + return "", fmt.Errorf("module for message %q not found during Python render", ref.ProtoFullName) + } + return moduleRef.Alias + "." + pythonMessageRef(message), nil +} + +func (p pythonRenderInfo) pythonPublicMethodTypeRef(ref TypeRef) (string, error) { + typ, ok := p.publicTypes[ref.ProtoFullName] + if !ok { + return "", fmt.Errorf("public python type %q not found during render", ref.ProtoFullName) + } + return p.pythonPublicTypeRef(typ.typeRef()), nil +} + +func (p pythonRenderInfo) pythonMapperHelperForMethodType(prefix string, ref TypeRef) (string, error) { + typ, ok := p.publicTypes[ref.ProtoFullName] + if !ok { + return "", fmt.Errorf("public python type %q not found during mapper lookup", ref.ProtoFullName) + } + return pythonMapperHelperName(prefix, typ), nil +} + +func (p pythonRenderInfo) protobufImportsForModel(model FileModel) []pythonModuleRef { + refs := map[string]pythonModuleRef{} + if model.PythonTypes != nil { + for _, typ := range model.PythonTypes.Types { + if typ.Kind != PythonTypeKindMessage || typ.WellKnownType != PythonWellKnownTypeNone { + continue + } + refs[typ.Owner.ProtoPath] = p.modules[typ.Owner.ProtoPath] + } + } + for _, service := range model.Services { + for _, method := range service.Methods { + if message := p.messages[method.Input.ProtoFullName]; message != nil { + messageFilePath := message.Desc.ParentFile().Path() + refs[messageFilePath] = p.modules[messageFilePath] + } + if message := p.messages[method.Output.ProtoFullName]; message != nil { + messageFilePath := message.Desc.ParentFile().Path() + refs[messageFilePath] = p.modules[messageFilePath] + } + } + } + + ordered := make([]pythonModuleRef, 0, len(refs)) + for _, ref := range refs { + ordered = append(ordered, ref) + } + sort.Slice(ordered, func(i, j int) bool { + left := ordered[i].Package + "." + ordered[i].Module + right := ordered[j].Package + "." + ordered[j].Module + return left < right + }) + return ordered +} + +func (p pythonRenderInfo) publicImportsForModel(model FileModel) []pythonModuleRef { + if model.PythonTypes == nil { + return nil + } + + referencedProtoPaths := p.publicImportProtoPaths(*model.PythonTypes) + ordered := make([]pythonModuleRef, 0, len(model.PythonTypes.Imports)) + for _, ref := range model.PythonTypes.Imports { + if _, ok := referencedProtoPaths[ref.ProtoPath]; !ok { + continue + } + modulePath := ref.PublicModule.ModulePath + lastDot := strings.LastIndex(modulePath, ".") + moduleRef := pythonModuleRef{ + Alias: ref.PublicModule.ModuleAlias, + Module: ref.PublicModule.ModuleBasename, + } + if lastDot != -1 { + moduleRef.Package = modulePath[:lastDot] + } + ordered = append(ordered, moduleRef) + } + sort.Slice(ordered, func(i, j int) bool { + left := ordered[i].Package + "." + ordered[i].Module + right := ordered[j].Package + "." + ordered[j].Module + return left < right + }) + return ordered +} + +func (p pythonRenderInfo) publicImportProtoPaths(graph PythonTypeGraph) map[string]struct{} { + refs := make(map[string]struct{}) + for _, typ := range graph.Types { + if typ.Owner.IsCurrentFile || typ.WellKnownType != PythonWellKnownTypeNone { + continue + } + refs[typ.Owner.ProtoPath] = struct{}{} + } + return refs +} + +func pythonMessageRef(message *protogen.Message) string { + fullName := string(message.Desc.FullName()) + pkg := string(message.Desc.ParentFile().Package()) + if pkg != "" { + fullName = strings.TrimPrefix(fullName, pkg+".") + } + return fullName +} + +func pythonAnnotations(ann *mcpoptionsv1.ToolAnnotations) string { + if ann == nil { + return "None" + } + + fields := make([]string, 0, 4) + if ann.ReadOnlyHint { + fields = append(fields, fmt.Sprintf("%q: %s", "readOnlyHint", pythonBool(ann.ReadOnlyHint))) + } + if ann.DestructiveHint != nil { + fields = append(fields, fmt.Sprintf("%q: %s", "destructiveHint", pythonBool(*ann.DestructiveHint))) + } + if ann.IdempotentHint { + fields = append(fields, fmt.Sprintf("%q: %s", "idempotentHint", pythonBool(ann.IdempotentHint))) + } + if ann.OpenWorldHint != nil { + fields = append(fields, fmt.Sprintf("%q: %s", "openWorldHint", pythonBool(*ann.OpenWorldHint))) + } + if ann.Title != "" { + fields = append(fields, fmt.Sprintf("%q: %q", "title", ann.Title)) + } + if len(fields) == 0 { + return "{}" + } + return "{" + strings.Join(fields, ", ") + "}" +} + +func pythonBool(value bool) string { + if value { + return "True" + } + return "False" +} + +func pythonIcons(icons []*mcpoptionsv1.Icon) string { + if len(icons) == 0 { + return "None" + } + + items := make([]string, 0, len(icons)) + for _, icon := range icons { + sizes := "[]" + if len(icon.GetSizes()) > 0 { + quoted := make([]string, 0, len(icon.GetSizes())) + for _, size := range icon.GetSizes() { + quoted = append(quoted, fmt.Sprintf("%q", size)) + } + sizes = "[" + strings.Join(quoted, ", ") + "]" + } + items = append(items, fmt.Sprintf("{%q: %q, %q: %q, %q: %s, %q: %q}", + "src", icon.GetSrc(), + "mimeType", icon.GetMimeType(), + "sizes", sizes, + "theme", icon.GetTheme())) + } + + return "[" + strings.Join(items, ", ") + "]" +} diff --git a/internal/codegen/render_python_mappers.go b/internal/codegen/render_python_mappers.go new file mode 100644 index 0000000..b8f17d2 --- /dev/null +++ b/internal/codegen/render_python_mappers.go @@ -0,0 +1,541 @@ +package codegen + +import ( + "fmt" + + "google.golang.org/protobuf/compiler/protogen" +) + +func renderPythonMappers(generated *protogen.GeneratedFile, info pythonRenderInfo, model FileModel) error { + if model.PythonTypes == nil { + return nil + } + if err := validatePythonMapperHelperNames(model.PythonTypes.Types); err != nil { + return err + } + + generated.P("def _enum_from_pb(enum_type: type[enum.IntEnum], value: int) -> enum.IntEnum:") + generated.P(" return enum_type(value)") + generated.P() + + generated.P("def _json_to_message(value: Any, message: Any) -> Any:") + generated.P(" json_format.Parse(json.dumps(value), message)") + generated.P(" return message") + generated.P() + + renderPythonWellKnownTypeMappers(generated) + + for _, typ := range model.PythonTypes.Types { + if typ.Kind != PythonTypeKindMessage { + continue + } + if err := renderPythonMessageFromPBMapper(generated, info, typ); err != nil { + return err + } + if err := renderPythonMessageToPBMapper(generated, info, typ); err != nil { + return err + } + } + + return nil +} + +func renderPythonWellKnownTypeMappers(generated *protogen.GeneratedFile) { + generated.P("def _from_pb_any(message: any_pb2.Any) -> ProtoAny:") + generated.P(" return json.loads(_message_to_json(message))") + generated.P() + generated.P("def _to_pb_any(value: ProtoAny) -> any_pb2.Any:") + generated.P(" return _json_to_message(value, any_pb2.Any())") + generated.P() + generated.P("def _from_pb_timestamp(message: timestamp_pb2.Timestamp) -> Timestamp:") + generated.P(" return json.loads(_message_to_json(message))") + generated.P() + generated.P("def _to_pb_timestamp(value: Timestamp) -> timestamp_pb2.Timestamp:") + generated.P(" return _json_to_message(value, timestamp_pb2.Timestamp())") + generated.P() + generated.P("def _from_pb_duration(message: duration_pb2.Duration) -> Duration:") + generated.P(" return json.loads(_message_to_json(message))") + generated.P() + generated.P("def _to_pb_duration(value: Duration) -> duration_pb2.Duration:") + generated.P(" return _json_to_message(value, duration_pb2.Duration())") + generated.P() + generated.P("def _from_pb_field_mask(message: field_mask_pb2.FieldMask) -> FieldMask:") + generated.P(" return json.loads(_message_to_json(message))") + generated.P() + generated.P("def _to_pb_field_mask(value: FieldMask) -> field_mask_pb2.FieldMask:") + generated.P(" return _json_to_message(value, field_mask_pb2.FieldMask())") + generated.P() + generated.P("def _from_pb_struct(message: struct_pb2.Struct) -> Struct:") + generated.P(" return json.loads(_message_to_json(message))") + generated.P() + generated.P("def _to_pb_struct(value: Struct) -> struct_pb2.Struct:") + generated.P(" return _json_to_message(value, struct_pb2.Struct())") + generated.P() + generated.P("def _from_pb_list_value(message: struct_pb2.ListValue) -> ListValue:") + generated.P(" return json.loads(_message_to_json(message))") + generated.P() + generated.P("def _to_pb_list_value(value: ListValue) -> struct_pb2.ListValue:") + generated.P(" return _json_to_message(value, struct_pb2.ListValue())") + generated.P() + generated.P("def _from_pb_value(message: struct_pb2.Value) -> Value:") + generated.P(" return json.loads(_message_to_json(message))") + generated.P() + generated.P("def _to_pb_value(value: Value) -> struct_pb2.Value:") + generated.P(" return _json_to_message(value, struct_pb2.Value())") + generated.P() + generated.P("def _from_pb_empty(message: empty_pb2.Empty) -> Empty:") + generated.P(" return Empty()") + generated.P() + generated.P("def _to_pb_empty(value: Empty) -> empty_pb2.Empty:") + generated.P(" return empty_pb2.Empty()") + generated.P() + generated.P("def _from_pb_bool_value(message: wrappers_pb2.BoolValue) -> BoolValue:") + generated.P(" return message.value") + generated.P() + generated.P("def _to_pb_bool_value(value: BoolValue) -> wrappers_pb2.BoolValue:") + generated.P(" return wrappers_pb2.BoolValue(value=value)") + generated.P() + generated.P("def _from_pb_string_value(message: wrappers_pb2.StringValue) -> StringValue:") + generated.P(" return message.value") + generated.P() + generated.P("def _to_pb_string_value(value: StringValue) -> wrappers_pb2.StringValue:") + generated.P(" return wrappers_pb2.StringValue(value=value)") + generated.P() + generated.P("def _from_pb_bytes_value(message: wrappers_pb2.BytesValue) -> BytesValue:") + generated.P(" return message.value") + generated.P() + generated.P("def _to_pb_bytes_value(value: BytesValue) -> wrappers_pb2.BytesValue:") + generated.P(" return wrappers_pb2.BytesValue(value=value)") + generated.P() + generated.P("def _from_pb_int32_value(message: wrappers_pb2.Int32Value) -> Int32Value:") + generated.P(" return message.value") + generated.P() + generated.P("def _to_pb_int32_value(value: Int32Value) -> wrappers_pb2.Int32Value:") + generated.P(" return wrappers_pb2.Int32Value(value=value)") + generated.P() + generated.P("def _from_pb_uint32_value(message: wrappers_pb2.UInt32Value) -> UInt32Value:") + generated.P(" return message.value") + generated.P() + generated.P("def _to_pb_uint32_value(value: UInt32Value) -> wrappers_pb2.UInt32Value:") + generated.P(" return wrappers_pb2.UInt32Value(value=value)") + generated.P() + generated.P("def _from_pb_int64_value(message: wrappers_pb2.Int64Value) -> Int64Value:") + generated.P(" return message.value") + generated.P() + generated.P("def _to_pb_int64_value(value: Int64Value) -> wrappers_pb2.Int64Value:") + generated.P(" return wrappers_pb2.Int64Value(value=value)") + generated.P() + generated.P("def _from_pb_uint64_value(message: wrappers_pb2.UInt64Value) -> UInt64Value:") + generated.P(" return message.value") + generated.P() + generated.P("def _to_pb_uint64_value(value: UInt64Value) -> wrappers_pb2.UInt64Value:") + generated.P(" return wrappers_pb2.UInt64Value(value=value)") + generated.P() + generated.P("def _from_pb_float_value(message: wrappers_pb2.FloatValue) -> FloatValue:") + generated.P(" return message.value") + generated.P() + generated.P("def _to_pb_float_value(value: FloatValue) -> wrappers_pb2.FloatValue:") + generated.P(" return wrappers_pb2.FloatValue(value=value)") + generated.P() + generated.P("def _from_pb_double_value(message: wrappers_pb2.DoubleValue) -> DoubleValue:") + generated.P(" return message.value") + generated.P() + generated.P("def _to_pb_double_value(value: DoubleValue) -> wrappers_pb2.DoubleValue:") + generated.P(" return wrappers_pb2.DoubleValue(value=value)") + generated.P() +} + +func renderPythonMessageFromPBMapper(generated *protogen.GeneratedFile, info pythonRenderInfo, typ PythonType) error { + pbType, err := info.pythonProtobufTypeRefForPublicRef(typ.typeRef()) + if err != nil { + return err + } + dcType := info.pythonPublicTypeRef(typ.typeRef()) + generated.P("def ", pythonMapperHelperName("from_pb", typ), "(message: ", pbType, ") -> ", dcType, ":") + + for _, oneof := range typ.Oneofs { + generated.P(" ", oneof.ProtoName, " = UNSET") + generated.P(" ", oneof.ProtoName, "_case = message.WhichOneof(", quote(oneof.ProtoName), ")") + for idx, variant := range oneof.Variants { + keyword := "if" + if idx > 0 { + keyword = "elif" + } + valueExpr, err := info.pythonFromPBExpr(variant.Type, "message."+variant.ProtoName) + if err != nil { + return err + } + generated.P(" ", keyword, " ", oneof.ProtoName, "_case == ", quote(variant.ProtoName), ":") + generated.P(" ", oneof.ProtoName, " = ", variant.WrapperName, "(", variant.ProtoName, "=", valueExpr, ")") + } + } + + generated.P(" return ", dcType, "(") + for _, field := range typ.Fields { + if field.OneofProtoName != "" { + continue + } + valueExpr, err := info.pythonFromPBFieldExpr(field) + if err != nil { + return err + } + generated.P(" ", field.ProtoName, "=", valueExpr, ",") + } + for _, oneof := range typ.Oneofs { + generated.P(" ", oneof.ProtoName, "=", oneof.ProtoName, ",") + } + generated.P(" )") + generated.P() + return nil +} + +func renderPythonMessageToPBMapper(generated *protogen.GeneratedFile, info pythonRenderInfo, typ PythonType) error { + pbType, err := info.pythonProtobufTypeRefForPublicRef(typ.typeRef()) + if err != nil { + return err + } + dcType := info.pythonPublicTypeRef(typ.typeRef()) + generated.P("def ", pythonMapperHelperName("to_pb", typ), "(value: ", dcType, ") -> ", pbType, ":") + generated.P(" message = ", pbType, "()") + for _, field := range typ.Fields { + if field.OneofProtoName != "" { + continue + } + if err := renderPythonToPBFieldAssignment(generated, info, field, "value."+field.ProtoName, "message."+field.ProtoName, 1); err != nil { + return err + } + } + for _, oneof := range typ.Oneofs { + generated.P(" if value.", oneof.ProtoName, " is not UNSET:") + for idx, variant := range oneof.Variants { + keyword := "if" + if idx > 0 { + keyword = "elif" + } + generated.P(" ", keyword, " isinstance(value.", oneof.ProtoName, ", ", variant.WrapperName, "):") + if err := renderPythonToPBSingularAssignment(generated, info, variant.Type, "value."+oneof.ProtoName+"."+variant.ProtoName, "message."+variant.ProtoName, 3); err != nil { + return err + } + } + generated.P(" else:") + generated.P(" raise TypeError(\"unsupported ", oneof.WrapperName, " variant: \" + type(value.", oneof.ProtoName, ").__name__)") + generated.P() + } + generated.P(" return message") + generated.P() + return nil +} + +func renderPythonToPBFieldAssignment(generated *protogen.GeneratedFile, info pythonRenderInfo, field PythonField, sourceExpr, targetExpr string, indent int) error { + switch { + case field.IsRepeated: + return renderPythonToPBRepeatedAssignment(generated, info, field.Type, sourceExpr, targetExpr, indent) + case field.IsMap: + if field.MapValue == nil { + return nil + } + return renderPythonToPBMapAssignment(generated, info, field, sourceExpr, targetExpr, indent) + case pythonFieldUsesUnset(field): + generated.P(pythonIndent(indent), "if ", sourceExpr, " is not UNSET:") + return renderPythonToPBSingularAssignment(generated, info, field.Type, sourceExpr, targetExpr, indent+1) + default: + return renderPythonToPBSingularAssignment(generated, info, field.Type, sourceExpr, targetExpr, indent) + } +} + +func renderPythonToPBRepeatedAssignment(generated *protogen.GeneratedFile, info pythonRenderInfo, ref PythonTypeRef, sourceExpr, targetExpr string, indent int) error { + prefix := pythonIndent(indent) + switch { + case ref.Scalar != PythonScalarUnknown: + generated.P(prefix, targetExpr, ".extend(", sourceExpr, ")") + case ref.IsEnum: + generated.P(prefix, targetExpr, ".extend(int(item) for item in ", sourceExpr, ")") + default: + itemExpr, err := info.pythonToPBExpr(ref, "item") + if err != nil { + return err + } + generated.P(prefix, targetExpr, ".extend(", itemExpr, " for item in ", sourceExpr, ")") + } + return nil +} + +func renderPythonToPBMapAssignment(generated *protogen.GeneratedFile, info pythonRenderInfo, field PythonField, sourceExpr, targetExpr string, indent int) error { + prefix := pythonIndent(indent) + valueRef := *field.MapValue + switch { + case valueRef.Scalar != PythonScalarUnknown: + generated.P(prefix, targetExpr, ".update(", sourceExpr, ")") + case valueRef.IsEnum: + generated.P(prefix, targetExpr, ".update({key: int(item) for key, item in ", sourceExpr, ".items()})") + default: + itemExpr, err := info.pythonToPBExpr(valueRef, "item") + if err != nil { + return err + } + generated.P(prefix, "for key, item in ", sourceExpr, ".items():") + generated.P(prefix, " ", targetExpr, "[key].CopyFrom(", itemExpr, ")") + } + return nil +} + +func renderPythonToPBSingularAssignment(generated *protogen.GeneratedFile, info pythonRenderInfo, ref PythonTypeRef, sourceExpr, targetExpr string, indent int) error { + prefix := pythonIndent(indent) + valueExpr, err := info.pythonToPBExpr(ref, sourceExpr) + if err != nil { + return err + } + switch { + case ref.Scalar != PythonScalarUnknown || ref.IsEnum: + generated.P(prefix, targetExpr, " = ", valueExpr) + default: + generated.P(prefix, targetExpr, ".CopyFrom(", valueExpr, ")") + } + return nil +} + +func (p pythonRenderInfo) pythonFromPBFieldExpr(field PythonField) (string, error) { + switch { + case field.IsRepeated: + if field.Type.Scalar != PythonScalarUnknown { + return "list(message." + field.ProtoName + ")", nil + } + itemExpr, err := p.pythonFromPBExpr(field.Type, "item") + if err != nil { + return "", err + } + return "[" + itemExpr + " for item in message." + field.ProtoName + "]", nil + case field.IsMap: + if field.MapValue == nil { + return "dict(message." + field.ProtoName + ")", nil + } + if field.MapValue.Scalar != PythonScalarUnknown { + return "dict(message." + field.ProtoName + ")", nil + } + valueExpr, err := p.pythonFromPBExpr(*field.MapValue, "item") + if err != nil { + return "", err + } + return "{key: " + valueExpr + " for key, item in message." + field.ProtoName + ".items()}", nil + default: + baseExpr, err := p.pythonFromPBExpr(field.Type, "message."+field.ProtoName) + if err != nil { + return "", err + } + if pythonFieldUsesUnset(field) { + return baseExpr + " if message.HasField(" + quote(field.ProtoName) + ") else UNSET", nil + } + return baseExpr, nil + } +} + +func (p pythonRenderInfo) pythonFromPBExpr(ref PythonTypeRef, expr string) (string, error) { + switch { + case ref.Scalar != PythonScalarUnknown: + return expr, nil + case ref.IsEnum: + return "_enum_from_pb(" + p.pythonPublicEnumTypeRef(ref) + ", " + expr + ")", nil + case ref.WellKnownType != PythonWellKnownTypeNone: + return pythonWellKnownMapperFunc("from_pb", ref.WellKnownType) + "(" + expr + ")", nil + case ref.IsMessage: + fn, err := p.pythonMapperHelperForRef("from_pb", ref) + if err != nil { + return "", err + } + return fn + "(" + expr + ")", nil + default: + return expr, nil + } +} + +func (p pythonRenderInfo) pythonToPBExpr(ref PythonTypeRef, expr string) (string, error) { + switch { + case ref.Scalar != PythonScalarUnknown: + return expr, nil + case ref.IsEnum: + return "int(" + expr + ")", nil + case ref.WellKnownType != PythonWellKnownTypeNone: + return pythonWellKnownMapperFunc("to_pb", ref.WellKnownType) + "(" + expr + ")", nil + case ref.IsMessage: + fn, err := p.pythonMapperHelperForRef("to_pb", ref) + if err != nil { + return "", err + } + return fn + "(" + expr + ")", nil + default: + return expr, nil + } +} + +func (p pythonRenderInfo) pythonMapperHelperForRef(prefix string, ref PythonTypeRef) (string, error) { + if ref.WellKnownType != PythonWellKnownTypeNone { + return pythonWellKnownMapperFunc(prefix, ref.WellKnownType), nil + } + typ, ok := p.publicTypes[ref.ProtoFullName] + if !ok { + return "", fmt.Errorf("public python type %q not found during mapper render", ref.ProtoFullName) + } + return pythonMapperHelperName(prefix, typ), nil +} + +func (p pythonRenderInfo) pythonProtobufTypeRefForPublicRef(ref PythonTypeRef) (string, error) { + if ref.WellKnownType != PythonWellKnownTypeNone { + return pythonWellKnownProtobufType(ref.WellKnownType), nil + } + return p.pythonProtobufTypeRef(TypeRef{ProtoFullName: ref.ProtoFullName}) +} + +func pythonMapperHelperName(prefix string, typ PythonType) string { + base := toSnakeCase(typ.PublicName) + if typ.Owner.IsCurrentFile { + return "_" + prefix + "_" + base + } + return "_" + prefix + "_" + toSnakeCase(typ.Owner.PublicModule.ModuleAlias) + "_" + base +} + +func validatePythonMapperHelperNames(types []PythonType) error { + owners := map[string]string{} + for _, name := range pythonWellKnownHelperNames() { + owners[name] = "built-in well-known type helper" + } + for _, typ := range types { + if typ.Kind != PythonTypeKindMessage { + continue + } + for _, prefix := range []string{"from_pb", "to_pb"} { + helper := pythonMapperHelperName(prefix, typ) + owner := fmt.Sprintf("%s mapper for %s", prefix, typ.ProtoFullName) + if existing, exists := owners[helper]; exists { + return fmt.Errorf("python mapper helper name collision for %q between %s and %s", helper, existing, owner) + } + owners[helper] = owner + } + } + return nil +} + +func pythonWellKnownHelperNames() []string { + names := make([]string, 0, 2*len(pythonSupportedWellKnownTypes())) + for _, kind := range pythonSupportedWellKnownTypes() { + names = append(names, pythonWellKnownMapperFunc("from_pb", kind)) + names = append(names, pythonWellKnownMapperFunc("to_pb", kind)) + } + return names +} + +func pythonSupportedWellKnownTypes() []PythonWellKnownType { + return []PythonWellKnownType{ + PythonWellKnownTypeAny, + PythonWellKnownTypeEmpty, + PythonWellKnownTypeTimestamp, + PythonWellKnownTypeDuration, + PythonWellKnownTypeFieldMask, + PythonWellKnownTypeStruct, + PythonWellKnownTypeValue, + PythonWellKnownTypeListValue, + PythonWellKnownTypeBoolValue, + PythonWellKnownTypeStringValue, + PythonWellKnownTypeBytesValue, + PythonWellKnownTypeInt32Value, + PythonWellKnownTypeUInt32Value, + PythonWellKnownTypeInt64Value, + PythonWellKnownTypeUInt64Value, + PythonWellKnownTypeFloatValue, + PythonWellKnownTypeDoubleValue, + } +} + +func pythonWellKnownMapperFunc(prefix string, kind PythonWellKnownType) string { + var suffix string + switch kind { + case PythonWellKnownTypeAny: + suffix = "any" + case PythonWellKnownTypeEmpty: + suffix = "empty" + case PythonWellKnownTypeTimestamp: + suffix = "timestamp" + case PythonWellKnownTypeDuration: + suffix = "duration" + case PythonWellKnownTypeFieldMask: + suffix = "field_mask" + case PythonWellKnownTypeStruct: + suffix = "struct" + case PythonWellKnownTypeValue: + suffix = "value" + case PythonWellKnownTypeListValue: + suffix = "list_value" + case PythonWellKnownTypeBoolValue: + suffix = "bool_value" + case PythonWellKnownTypeStringValue: + suffix = "string_value" + case PythonWellKnownTypeBytesValue: + suffix = "bytes_value" + case PythonWellKnownTypeInt32Value: + suffix = "int32_value" + case PythonWellKnownTypeUInt32Value: + suffix = "uint32_value" + case PythonWellKnownTypeInt64Value: + suffix = "int64_value" + case PythonWellKnownTypeUInt64Value: + suffix = "uint64_value" + case PythonWellKnownTypeFloatValue: + suffix = "float_value" + case PythonWellKnownTypeDoubleValue: + suffix = "double_value" + default: + suffix = toSnakeCase(string(kind)) + } + return "_" + prefix + "_" + suffix +} + +func pythonWellKnownProtobufType(kind PythonWellKnownType) string { + switch kind { + case PythonWellKnownTypeAny: + return "any_pb2.Any" + case PythonWellKnownTypeEmpty: + return "empty_pb2.Empty" + case PythonWellKnownTypeTimestamp: + return "timestamp_pb2.Timestamp" + case PythonWellKnownTypeDuration: + return "duration_pb2.Duration" + case PythonWellKnownTypeFieldMask: + return "field_mask_pb2.FieldMask" + case PythonWellKnownTypeStruct: + return "struct_pb2.Struct" + case PythonWellKnownTypeValue: + return "struct_pb2.Value" + case PythonWellKnownTypeListValue: + return "struct_pb2.ListValue" + case PythonWellKnownTypeBoolValue: + return "wrappers_pb2.BoolValue" + case PythonWellKnownTypeStringValue: + return "wrappers_pb2.StringValue" + case PythonWellKnownTypeBytesValue: + return "wrappers_pb2.BytesValue" + case PythonWellKnownTypeInt32Value: + return "wrappers_pb2.Int32Value" + case PythonWellKnownTypeUInt32Value: + return "wrappers_pb2.UInt32Value" + case PythonWellKnownTypeInt64Value: + return "wrappers_pb2.Int64Value" + case PythonWellKnownTypeUInt64Value: + return "wrappers_pb2.UInt64Value" + case PythonWellKnownTypeFloatValue: + return "wrappers_pb2.FloatValue" + case PythonWellKnownTypeDoubleValue: + return "wrappers_pb2.DoubleValue" + default: + return "Any" + } +} + +func pythonIndent(level int) string { + if level <= 0 { + return "" + } + indent := "" + for range level { + indent += " " + } + return indent +} diff --git a/internal/codegen/render_python_runtime.go b/internal/codegen/render_python_runtime.go new file mode 100644 index 0000000..fc13a79 --- /dev/null +++ b/internal/codegen/render_python_runtime.go @@ -0,0 +1,231 @@ +package codegen + +import "google.golang.org/protobuf/compiler/protogen" + +func renderPythonRuntime(generated *protogen.GeneratedFile) { + generated.P("@dataclass(frozen=True)") + generated.P("class _RegisteredTool:") + generated.P(" name: str") + generated.P(" title: str") + generated.P(" description: str") + generated.P(" input_schema_json: str") + generated.P(" output_schema_json: str") + generated.P(" request_type: type[Any]") + generated.P(" response_type: type[Any]") + generated.P(" from_pb: Any") + generated.P(" to_pb: Any") + generated.P(" handler: Any") + generated.P(" annotations: dict[str, Any] | None") + generated.P(" icons: list[dict[str, Any]] | None") + generated.P() + generated.P("class _ServerToolRegistry:") + generated.P(" def __init__(self, server: mcp.server.lowlevel.Server) -> None:") + generated.P(" self.server = server") + generated.P(" self.tools: dict[str, _RegisteredTool] = {}") + generated.P(" self.reserved_tool_names = _get_reserved_tool_names(server)") + generated.P(" self.handlers_installed = False") + generated.P(" self.previous_list_tools: Any | None = None") + generated.P(" self.previous_call_tool: Any | None = None") + generated.P() + generated.P(" def add_tool(self, tool: _RegisteredTool) -> None:") + generated.P(" if tool.name in self.reserved_tool_names:") + generated.P(" raise ValueError(f\"duplicate tool registration: {tool.name}\")") + generated.P(" self.tools[tool.name] = tool") + generated.P(" self.reserved_tool_names.add(tool.name)") + generated.P() + generated.P(" def list_tools(self) -> list[mcp.types.Tool]:") + generated.P(" return [_build_tool(tool) for tool in self.tools.values()]") + generated.P() + generated.P(" async def call_tool(self, name: str, arguments: dict[str, Any] | None, context: ToolRequestContext | None = None) -> mcp.types.CallToolResult:") + generated.P(" return await _dispatch_call(self, name, arguments or {}, context)") + generated.P() + generated.P("_SERVER_REGISTRIES: weakref.WeakKeyDictionary[mcp.server.lowlevel.Server, _ServerToolRegistry] = weakref.WeakKeyDictionary()") + generated.P() + generated.P("def _load_schema(schema_json: str) -> dict[str, Any]:") + generated.P(" return json.loads(schema_json)") + generated.P() + generated.P("def _protojson_to_message(arguments: dict[str, Any], message: Any) -> Any:") + generated.P(" json_format.ParseDict(arguments, message)") + generated.P(" return message") + generated.P() + generated.P("def _normalize_tool_segment(segment: str | None) -> str:") + generated.P(" if segment is None:") + generated.P(" return \"\"") + generated.P(" parts = [part for part in segment.strip().replace(\".\", \"_\").split(\"_\") if part]") + generated.P(" return \"_\".join(parts)") + generated.P() + generated.P("def _normalize_namespace(namespace: str | None, default: str) -> str:") + generated.P(" if namespace is None:") + generated.P(" namespace = default") + generated.P(" return _normalize_tool_segment(namespace)") + generated.P() + generated.P("def _tool_name(namespace: str, method_name: str) -> str:") + generated.P(" namespace = _normalize_tool_segment(namespace)") + generated.P(" method_name = _normalize_tool_segment(method_name)") + generated.P(" if namespace == '':") + generated.P(" return method_name") + generated.P(" if method_name == '':") + generated.P(" return namespace") + generated.P(" return f\"{namespace}_{method_name}\"") + generated.P() + generated.P("def _get_registry(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry:") + generated.P(" registry = _SERVER_REGISTRIES.get(server)") + generated.P(" if registry is None:") + generated.P(" registry = _ServerToolRegistry(server)") + generated.P(" _SERVER_REGISTRIES[server] = registry") + generated.P(" return registry") + generated.P() + generated.P("_RESERVED_TOOL_NAMES_ATTR = \"_protoc_gen_mcp_reserved_tool_names\"") + generated.P() + generated.P("def _get_reserved_tool_names(server: mcp.server.lowlevel.Server) -> set[str]:") + generated.P(" reserved = getattr(server, _RESERVED_TOOL_NAMES_ATTR, None)") + generated.P(" if reserved is None:") + generated.P(" reserved = set()") + generated.P(" setattr(server, _RESERVED_TOOL_NAMES_ATTR, reserved)") + generated.P(" return reserved") + generated.P() + generated.P("def _build_list_tools_request(request: Any) -> mcp.types.ListToolsRequest:") + generated.P(" if isinstance(request, mcp.types.ListToolsRequest):") + generated.P(" return request") + generated.P(" return mcp.types.ListToolsRequest()") + generated.P() + generated.P("def _merge_tools(previous: list[mcp.types.Tool], current: list[mcp.types.Tool]) -> list[mcp.types.Tool]:") + generated.P(" merged: list[mcp.types.Tool] = []") + generated.P(" names: set[str] = set()") + generated.P(" for tool in previous:") + generated.P(" if tool.name in names:") + generated.P(" raise ValueError(f\"duplicate tool registration: {tool.name}\")") + generated.P(" names.add(tool.name)") + generated.P(" merged.append(tool)") + generated.P(" for tool in current:") + generated.P(" if tool.name in names:") + generated.P(" raise ValueError(f\"duplicate tool registration: {tool.name}\")") + generated.P(" names.add(tool.name)") + generated.P(" merged.append(tool)") + generated.P(" return merged") + generated.P() + generated.P("def _install_server_handlers(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry:") + generated.P(" registry = _get_registry(server)") + generated.P(" if registry.handlers_installed:") + generated.P(" return registry") + generated.P(" registry.previous_list_tools = server.request_handlers.get(mcp.types.ListToolsRequest)") + generated.P(" registry.previous_call_tool = server.request_handlers.get(mcp.types.CallToolRequest)") + generated.P() + generated.P(" async def _list_tools(request: Any) -> mcp.types.ServerResult:") + generated.P(" current = _SERVER_REGISTRIES.get(server)") + generated.P(" list_request = _build_list_tools_request(request)") + generated.P(" previous_tools: list[mcp.types.Tool] = []") + generated.P(" meta: dict[str, Any] | None = None") + generated.P(" if current is not None:") + generated.P(" if current.previous_list_tools is not None:") + generated.P(" previous_result = await current.previous_list_tools(list_request)") + generated.P(" if not isinstance(previous_result.root, mcp.types.ListToolsResult):") + generated.P(" return previous_result") + generated.P(" if getattr(list_request.params, \"cursor\", None) is not None:") + generated.P(" raise ValueError(\"cannot compose protoc-gen-mcp tools with paginated tools/list handlers\")") + generated.P(" if previous_result.root.nextCursor is not None:") + generated.P(" raise ValueError(\"cannot compose protoc-gen-mcp tools with paginated tools/list handlers\")") + generated.P(" meta = getattr(previous_result.root, \"meta\", None)") + generated.P(" previous_tools = list(previous_result.root.tools)") + generated.P(" current_tools = [] if current is None else current.list_tools()") + generated.P(" tools = _merge_tools(previous_tools, current_tools)") + generated.P(" server._tool_cache.clear()") + generated.P(" for tool in tools:") + generated.P(" server._tool_cache[tool.name] = tool") + generated.P(" return mcp.types.ServerResult(mcp.types.ListToolsResult(tools=tools, _meta=meta))") + generated.P() + generated.P(" async def _call_tool(request: mcp.types.CallToolRequest) -> mcp.types.ServerResult:") + generated.P(" current = _SERVER_REGISTRIES.get(server)") + generated.P(" if current is None:") + generated.P(" return mcp.types.ServerResult(_tool_error_result(\"tool registry is not installed\"))") + generated.P(" if request.params.name not in current.tools:") + generated.P(" if current.previous_call_tool is not None:") + generated.P(" return await current.previous_call_tool(request)") + generated.P(" _invalid_params_error(request.params.name, \"unknown tool\")") + generated.P(" if current.previous_call_tool is not None:") + generated.P(" await server.request_handlers[mcp.types.ListToolsRequest](mcp.types.ListToolsRequest())") + generated.P(" result = await current.call_tool(request.params.name, request.params.arguments or {}, server.request_context.session)") + generated.P(" return mcp.types.ServerResult(result)") + generated.P() + generated.P(" server.request_handlers[mcp.types.ListToolsRequest] = _list_tools") + generated.P(" server.request_handlers[mcp.types.CallToolRequest] = _call_tool") + generated.P() + generated.P(" registry.handlers_installed = True") + generated.P(" return registry") + generated.P() + generated.P("def _tool_annotations(raw: dict[str, Any] | None) -> mcp.types.ToolAnnotations | None:") + generated.P(" if raw is None:") + generated.P(" return None") + generated.P(" return mcp.types.ToolAnnotations(**raw)") + generated.P() + generated.P("def _tool_error_result(message: str) -> mcp.types.CallToolResult:") + generated.P(" return mcp.types.CallToolResult(") + generated.P(" content=[mcp.types.TextContent(type=\"text\", text=message)],") + generated.P(" isError=True,") + generated.P(" )") + generated.P() + generated.P("def _invalid_params_error(tool_name: str, error: Any) -> mcp.shared.exceptions.McpError:") + generated.P(" raise mcp.shared.exceptions.McpError(") + generated.P(" mcp.types.ErrorData(") + generated.P(" code=mcp.types.INVALID_PARAMS,") + generated.P(" message=f\"invalid arguments for tool {tool_name!r}: {error}\",") + generated.P(" )") + generated.P(" )") + generated.P() + generated.P("def _build_tool(tool: _RegisteredTool) -> Any:") + generated.P(" return mcp.types.Tool(") + generated.P(" name=tool.name,") + generated.P(" title=tool.title,") + generated.P(" description=tool.description,") + generated.P(" inputSchema=_load_schema(tool.input_schema_json),") + generated.P(" outputSchema=_load_schema(tool.output_schema_json),") + generated.P(" annotations=_tool_annotations(tool.annotations),") + generated.P(" icons=tool.icons,") + generated.P(" )") + generated.P() + generated.P("async def _maybe_await(result: Any) -> Any:") + generated.P(" if inspect.isawaitable(result):") + generated.P(" return await result") + generated.P(" return result") + generated.P() + generated.P("def _message_to_json(message: Any) -> str:") + generated.P(" kwargs = {\"preserving_proto_field_name\": False}") + generated.P(" try:") + generated.P(" return json_format.MessageToJson(message, always_print_fields_with_no_presence=True, **kwargs)") + generated.P(" except TypeError:") + generated.P(" return json_format.MessageToJson(message, including_default_value_fields=True, **kwargs)") + generated.P() + generated.P("async def _dispatch_call(registry: _ServerToolRegistry, name: str, arguments: dict[str, Any], context: ToolRequestContext | None) -> mcp.types.CallToolResult:") + generated.P(" tool = registry.tools.get(name)") + generated.P(" if tool is None:") + generated.P(" _invalid_params_error(name, \"unknown tool\")") + generated.P(" try:") + generated.P(" jsonschema.validate(arguments, _load_schema(tool.input_schema_json))") + generated.P(" except jsonschema.ValidationError as error:") + generated.P(" _invalid_params_error(name, error)") + generated.P(" try:") + generated.P(" request_pb = _protojson_to_message(arguments, tool.request_type())") + generated.P(" request_dc = tool.from_pb(request_pb)") + generated.P(" except Exception as error:") + generated.P(" _invalid_params_error(name, error)") + generated.P(" try:") + generated.P(" response_dc = await _maybe_await(tool.handler(context, request_dc))") + generated.P(" except Exception as error:") + generated.P(" return _tool_error_result(str(error))") + generated.P(" try:") + generated.P(" if response_dc is None:") + generated.P(" response_pb = tool.response_type()") + generated.P(" else:") + generated.P(" response_pb = tool.to_pb(response_dc)") + generated.P(" payload = json.loads(_message_to_json(response_pb))") + generated.P(" except Exception as error:") + generated.P(" raise RuntimeError(f\"mcpruntime: marshal output for tool {name!r}: {error}\") from error") + generated.P(" text = json.dumps(payload, separators=(\",\", \":\"), ensure_ascii=False)") + generated.P(" content = [mcp.types.TextContent(type=\"text\", text=text)]") + generated.P(" try:") + generated.P(" jsonschema.validate(payload, _load_schema(tool.output_schema_json))") + generated.P(" except jsonschema.ValidationError as error:") + generated.P(" raise RuntimeError(f\"mcpruntime: validate output for tool {name!r}: {error}\") from error") + generated.P(" return mcp.types.CallToolResult(content=content, structuredContent=payload)") + generated.P() +} diff --git a/internal/codegen/render_python_types.go b/internal/codegen/render_python_types.go new file mode 100644 index 0000000..ac4d9d9 --- /dev/null +++ b/internal/codegen/render_python_types.go @@ -0,0 +1,298 @@ +package codegen + +import ( + "fmt" + + "google.golang.org/protobuf/compiler/protogen" +) + +func renderPythonPublicTypes(generated *protogen.GeneratedFile, info pythonRenderInfo, model FileModel) error { + graph := model.PythonTypes + if graph == nil { + return nil + } + + generated.P("class _UnsetType:") + generated.P(" __slots__ = ()") + generated.P() + generated.P(" def __repr__(self) -> str:") + generated.P(" return \"UNSET\"") + generated.P() + generated.P("UNSET = _UnsetType()") + generated.P() + generated.P("JSONValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None") + generated.P("ProtoAny: TypeAlias = dict[str, Any]") + generated.P("Timestamp: TypeAlias = str") + generated.P("Duration: TypeAlias = str") + generated.P("FieldMask: TypeAlias = str") + generated.P("Struct: TypeAlias = dict[str, Any]") + generated.P("Value: TypeAlias = JSONValue") + generated.P("ListValue: TypeAlias = list[Any]") + generated.P("BoolValue: TypeAlias = bool") + generated.P("StringValue: TypeAlias = str") + generated.P("BytesValue: TypeAlias = bytes") + generated.P("Int32Value: TypeAlias = int") + generated.P("UInt32Value: TypeAlias = int") + generated.P("Int64Value: TypeAlias = int") + generated.P("UInt64Value: TypeAlias = int") + generated.P("FloatValue: TypeAlias = float") + generated.P("DoubleValue: TypeAlias = float") + generated.P() + generated.P("@dataclass(slots=True)") + generated.P("class Empty:") + generated.P(" pass") + generated.P() + + for _, typ := range graph.Types { + if !typ.Owner.IsCurrentFile { + continue + } + switch typ.Kind { + case PythonTypeKindEnum: + renderPythonPublicEnum(generated, typ) + case PythonTypeKindMessage: + if err := renderPythonPublicMessage(generated, info, typ); err != nil { + return err + } + } + } + + return nil +} + +func renderPythonPublicEnum(generated *protogen.GeneratedFile, typ PythonType) { + generated.P("class ", typ.PublicName, "(enum.IntEnum):") + rendered := 0 + for _, value := range typ.EnumValues { + if value.Hidden { + continue + } + generated.P(" ", value.ProtoName, " = ", value.Number) + rendered++ + } + if rendered == 0 { + generated.P(" pass") + } + generated.P() +} + +func renderPythonPublicMessage(generated *protogen.GeneratedFile, info pythonRenderInfo, typ PythonType) error { + for _, oneof := range typ.Oneofs { + generated.P("@dataclass(slots=True)") + generated.P("class ", oneof.WrapperName, ":") + generated.P(" pass") + generated.P() + for _, variant := range oneof.Variants { + valueType := info.pythonPublicTypeRef(variant.Type) + generated.P("@dataclass(slots=True)") + generated.P("class ", variant.WrapperName, "(", oneof.WrapperName, "):") + generated.P(" ", variant.ProtoName, ": ", valueType) + generated.P() + } + } + + renderedFields, err := renderablePythonFields(info, typ) + if err != nil { + return err + } + + generated.P("@dataclass(slots=True)") + generated.P("class ", typ.PublicName, ":") + if len(renderedFields) == 0 { + generated.P(" pass") + generated.P() + return nil + } + for _, field := range renderedFields { + switch { + case field.defaultFactory != "": + generated.P(" ", field.name, ": ", field.typ, " = field(default_factory=", field.defaultFactory, ")") + case field.hasDefault: + generated.P(" ", field.name, ": ", field.typ, " = ", field.defaultValue) + default: + generated.P(" ", field.name, ": ", field.typ) + } + } + generated.P() + return nil +} + +type renderedPythonField struct { + name string + typ string + hasDefault bool + defaultValue string + defaultFactory string +} + +func renderablePythonFields(info pythonRenderInfo, typ PythonType) ([]renderedPythonField, error) { + required := make([]renderedPythonField, 0, len(typ.Fields)) + optional := make([]renderedPythonField, 0, len(typ.Fields)+len(typ.Oneofs)) + + for _, field := range typ.Fields { + if field.OneofProtoName != "" { + continue + } + + fieldType, err := info.pythonFieldTypeAnnotation(field) + if err != nil { + return nil, err + } + rendered := renderedPythonField{ + name: field.ProtoName, + typ: fieldType, + } + switch { + case field.IsRepeated: + rendered.hasDefault = true + rendered.defaultFactory = "list" + optional = append(optional, rendered) + case field.IsMap: + rendered.hasDefault = true + rendered.defaultFactory = "dict" + optional = append(optional, rendered) + case pythonFieldUsesUnset(field): + rendered.typ += " | _UnsetType" + rendered.hasDefault = true + rendered.defaultValue = "UNSET" + optional = append(optional, rendered) + default: + required = append(required, rendered) + } + } + + for _, oneof := range typ.Oneofs { + optional = append(optional, renderedPythonField{ + name: oneof.ProtoName, + typ: oneof.WrapperName + " | _UnsetType", + hasDefault: true, + defaultValue: "UNSET", + }) + } + + return append(required, optional...), nil +} + +func pythonFieldUsesUnset(field PythonField) bool { + if field.OneofProtoName != "" || field.IsRepeated || field.IsMap { + return false + } + if field.IsSchemaRequired { + return false + } + return field.HasPresence +} + +func (p pythonRenderInfo) pythonFieldTypeAnnotation(field PythonField) (string, error) { + switch { + case field.IsRepeated: + return "list[" + p.pythonPublicTypeRef(field.Type) + "]", nil + case field.IsMap: + if field.MapValue == nil { + return "dict[str, Any]", nil + } + return "dict[" + pythonScalarAnnotation(field.MapKeyScalar) + ", " + p.pythonPublicTypeRef(*field.MapValue) + "]", nil + default: + return p.pythonPublicTypeRef(field.Type), nil + } +} + +func (p pythonRenderInfo) pythonPublicTypeRef(ref PythonTypeRef) string { + if ref.IsEnum { + return p.pythonPublicEnumTypeRef(ref) + } + switch { + case ref.Scalar != PythonScalarUnknown: + return pythonScalarAnnotation(ref.Scalar) + case ref.WellKnownType != PythonWellKnownTypeNone: + return pythonWellKnownTypeAnnotation(ref.WellKnownType) + case ref.Owner.IsCurrentFile: + return ref.PublicName + default: + return ref.Owner.PublicModule.ModuleAlias + "." + ref.PublicName + } +} + +func (p pythonRenderInfo) pythonPublicEnumTypeRef(ref PythonTypeRef) string { + if ref.Owner.IsCurrentFile { + return ref.PublicName + } + return ref.Owner.PublicModule.ModuleAlias + "." + ref.PublicName +} + +func (t PythonType) typeRef() PythonTypeRef { + return PythonTypeRef{ + ProtoFullName: t.ProtoFullName, + ProtoName: t.ProtoName, + PublicName: t.PublicName, + Owner: t.Owner, + WellKnownType: t.WellKnownType, + IsEnum: t.Kind == PythonTypeKindEnum, + IsMessage: t.Kind == PythonTypeKindMessage, + } +} + +func pythonScalarAnnotation(scalar PythonScalar) string { + switch scalar { + case PythonScalarBool: + return "bool" + case PythonScalarBytes: + return "bytes" + case PythonScalarString: + return "str" + case PythonScalarInt32, PythonScalarUInt32, PythonScalarInt64, PythonScalarUInt64: + return "int" + case PythonScalarFloat, PythonScalarDouble: + return "float" + default: + return "Any" + } +} + +func pythonWellKnownTypeAnnotation(kind PythonWellKnownType) string { + switch kind { + case PythonWellKnownTypeAny: + return "ProtoAny" + case PythonWellKnownTypeEmpty: + return "Empty" + case PythonWellKnownTypeTimestamp: + return "Timestamp" + case PythonWellKnownTypeDuration: + return "Duration" + case PythonWellKnownTypeFieldMask: + return "FieldMask" + case PythonWellKnownTypeStruct: + return "Struct" + case PythonWellKnownTypeValue: + return "Value" + case PythonWellKnownTypeListValue: + return "ListValue" + case PythonWellKnownTypeBoolValue: + return "BoolValue" + case PythonWellKnownTypeStringValue: + return "StringValue" + case PythonWellKnownTypeBytesValue: + return "BytesValue" + case PythonWellKnownTypeInt32Value: + return "Int32Value" + case PythonWellKnownTypeUInt32Value: + return "UInt32Value" + case PythonWellKnownTypeInt64Value: + return "Int64Value" + case PythonWellKnownTypeUInt64Value: + return "UInt64Value" + case PythonWellKnownTypeFloatValue: + return "FloatValue" + case PythonWellKnownTypeDoubleValue: + return "DoubleValue" + default: + return "Any" + } +} + +func assertPythonTypesAvailable(model FileModel) error { + if model.PythonTypes == nil { + return fmt.Errorf("python type graph is required for python public type rendering") + } + return nil +} diff --git a/internal/examplemcp/python_server.py b/internal/examplemcp/python_server.py new file mode 100644 index 0000000..7e93135 --- /dev/null +++ b/internal/examplemcp/python_server.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +import sys + +import anyio +import mcp.server.lowlevel +import mcp.server.stdio + +_REPO_ROOT = Path(__file__).resolve().parents[2] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from internal.testproto.example.v1 import example_mcp + +_INVALID_OUTPUT_ENV = "PROTOC_GEN_MCP_PYTHON_INVALID_OUTPUT" + + +def _copy_report_details(value: example_mcp.ReportDetails) -> example_mcp.ReportDetails: + return example_mcp.ReportDetails(label=value.label) + + +def _copy_recursive_node( + value: example_mcp.RecursiveNode | example_mcp._UnsetType, +) -> example_mcp.RecursiveNode | example_mcp._UnsetType: + if value is example_mcp.UNSET: + return example_mcp.UNSET + return example_mcp.RecursiveNode( + name=value.name, + child=_copy_recursive_node(value.child), + children=[_copy_recursive_node(child) for child in value.children], + ) + + +def _copy_advanced_selector( + value: example_mcp.DescribeAdvancedShapesRequestSelectorVariant | example_mcp._UnsetType, +) -> example_mcp.DescribeAdvancedShapesResponseSelectorVariant | example_mcp._UnsetType: + if value is example_mcp.UNSET: + return example_mcp.UNSET + if isinstance(value, example_mcp.DescribeAdvancedShapesRequestSelectorCityAliasVariant): + return example_mcp.DescribeAdvancedShapesResponseSelectorCityAliasVariant( + city_alias=value.city_alias, + ) + if isinstance(value, example_mcp.DescribeAdvancedShapesRequestSelectorCityIdVariant): + return example_mcp.DescribeAdvancedShapesResponseSelectorCityIdVariant( + city_id=value.city_id, + ) + if isinstance(value, example_mcp.DescribeAdvancedShapesRequestSelectorCityDetailsVariant): + return example_mcp.DescribeAdvancedShapesResponseSelectorCityDetailsVariant( + city_details=_copy_report_details(value.city_details), + ) + raise TypeError(f"unsupported selector variant: {type(value).__name__}") + + +class Handler: + def create_report( + self, + _ctx: example_mcp.ToolRequestContext, + req: example_mcp.CreateReportRequest, + ) -> example_mcp.CreateReportResponse: + return example_mcp.CreateReportResponse( + report_id="report-1", + total_count=42, + status=example_mcp.ReportStatus.REPORT_STATUS_OK, + details=_copy_report_details(req.details), + warnings=["none"], + ) + + def ping( + self, + _ctx: example_mcp.ToolRequestContext, + _req: example_mcp.PingRequest, + ) -> example_mcp.PingResponse: + return example_mcp.PingResponse(ack=example_mcp.Empty()) + + def describe_advanced_shapes( + self, + _ctx: example_mcp.ToolRequestContext, + req: example_mcp.DescribeAdvancedShapesRequest, + ) -> example_mcp.DescribeAdvancedShapesResponse: + return example_mcp.DescribeAdvancedShapesResponse( + labels=dict(req.labels), + quantities=dict(req.quantities), + toggles=dict(req.toggles), + limits=dict(req.limits), + observed_at=req.observed_at, + ttl=req.ttl, + payload=req.payload, + items=list(req.items) if req.items is not example_mcp.UNSET else example_mcp.UNSET, + dynamic=req.dynamic, + note=req.note, + total=req.total, + enabled=req.enabled, + ratio=req.ratio, + mask=req.mask, + blob=req.blob, + small_total=req.small_total, + uint_total=req.uint_total, + huge_total=req.huge_total, + weight=req.weight, + raw_ratio=req.raw_ratio, + tree=_copy_recursive_node(req.tree), + detail_any=dict(req.detail_any) if req.detail_any is not example_mcp.UNSET else example_mcp.UNSET, + duration_any=dict(req.duration_any) if req.duration_any is not example_mcp.UNSET else example_mcp.UNSET, + selector=_copy_advanced_selector(req.selector), + ) + + def hidden_thing( + self, + _ctx: example_mcp.ToolRequestContext, + _req: example_mcp.HiddenThingRequest, + ) -> example_mcp.HiddenThingResponse: + return example_mcp.HiddenThingResponse() + + def describe_scalar_shapes( + self, + _ctx: example_mcp.ToolRequestContext, + req: example_mcp.DescribeScalarShapesRequest, + ) -> example_mcp.DescribeScalarShapesResponse: + return example_mcp.DescribeScalarShapesResponse( + bool_flag=req.bool_flag, + text_value=req.text_value, + bytes_value=req.bytes_value, + int32_value=req.int32_value, + sint32_value=req.sint32_value, + sfixed32_value=req.sfixed32_value, + uint32_value=req.uint32_value, + fixed32_value=req.fixed32_value, + int64_value=req.int64_value, + sint64_value=req.sint64_value, + sfixed64_value=req.sfixed64_value, + uint64_value=req.uint64_value, + fixed64_value=req.fixed64_value, + float_value=req.float_value, + double_value=req.double_value, + status=req.status, + details=_copy_report_details(req.details), + samples=list(req.samples), + optional_bool_flag=req.optional_bool_flag, + optional_text_value=req.optional_text_value, + optional_bytes_value=req.optional_bytes_value, + optional_int32_value=req.optional_int32_value, + optional_sint32_value=req.optional_sint32_value, + optional_sfixed32_value=req.optional_sfixed32_value, + optional_uint32_value=req.optional_uint32_value, + optional_fixed32_value=req.optional_fixed32_value, + optional_int64_value=req.optional_int64_value, + optional_sint64_value=req.optional_sint64_value, + optional_sfixed64_value=req.optional_sfixed64_value, + optional_uint64_value=req.optional_uint64_value, + optional_fixed64_value=req.optional_fixed64_value, + optional_float_value=req.optional_float_value, + optional_double_value=req.optional_double_value, + optional_status=req.optional_status, + ) + + +def _apply_invalid_output_mode() -> None: + mode = os.getenv(_INVALID_OUTPUT_ENV) + if mode in ("", None): + return + if mode != "create_report": + raise ValueError(f"unsupported {_INVALID_OUTPUT_ENV} value: {mode}") + + example_mcp.EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON = json.dumps( + { + "type": "object", + "properties": { + "reportId": {"type": "integer"}, + }, + "required": ["reportId"], + "additionalProperties": False, + }, + separators=(",", ":"), + ) + + +def new_server() -> mcp.server.lowlevel.Server: + server = mcp.server.lowlevel.Server( + "protoc-gen-mcp-python-example-server", + version="v0.0.1", + ) + _apply_invalid_output_mode() + example_mcp.register_example_api_tools(server, Handler()) + return server + + +async def run_stdio_server() -> None: + server = new_server() + async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options(), + ) + + +def main() -> None: + anyio.run(run_stdio_server) + + +if __name__ == "__main__": + main() diff --git a/internal/examplemcp/stdio_test.go b/internal/examplemcp/stdio_test.go index effefb0..701ba3f 100644 --- a/internal/examplemcp/stdio_test.go +++ b/internal/examplemcp/stdio_test.go @@ -3,10 +3,13 @@ package examplemcp_test import ( "context" "encoding/json" + "os" "os/exec" "path/filepath" + "reflect" "runtime" "slices" + "strings" "testing" "github.com/easyp-tech/protoc-gen-mcp/internal/examplemcp" @@ -20,11 +23,53 @@ func TestServerOverStdio(t *testing.T) { } root := repoRoot(t) - ctx := context.Background() - cmd := exec.Command("go", "run", filepath.Join(root, "cmd/example-mcp-server")) cmd.Dir = root + runServerOverStdioContract(t, cmd) +} + +func TestPythonServerOverStdio(t *testing.T) { + root := repoRoot(t) + runServerOverStdioContract(t, pythonExampleServerCommand(t, root)) +} + +func TestPythonServerRejectsInvalidOutputOverStdio(t *testing.T) { + root := repoRoot(t) + cmd := pythonExampleServerCommand(t, root) + cmd.Env = append(cmd.Env, "PROTOC_GEN_MCP_PYTHON_INVALID_OUTPUT=create_report") + ctx := context.Background() + client := mcp.NewClient(&mcp.Implementation{ + Name: "protoc-gen-mcp-python-invalid-output-test-client", + Version: "v0.0.1", + }, nil) + + session, err := client.Connect(ctx, &mcp.CommandTransport{Command: cmd}, nil) + if err != nil { + t.Fatalf("client.Connect() over stdio failed: %v", err) + } + defer session.Close() + + _, err = session.CallTool(ctx, &mcp.CallToolParams{ + Name: "example_CreateReport", + Arguments: map[string]any{ + "city": "Paris", + "count": 2, + "details": map[string]any{"label": "today"}, + }, + }) + if err == nil { + t.Fatal("CallTool(CreateReport) unexpectedly succeeded with invalid output schema") + } + if !strings.Contains(err.Error(), "mcpruntime: validate output for tool") || !strings.Contains(err.Error(), "example_CreateReport") { + t.Fatalf("CallTool(CreateReport) error = %v, want output validation failure", err) + } +} + +func runServerOverStdioContract(t *testing.T, cmd *exec.Cmd) { + t.Helper() + + ctx := context.Background() client := mcp.NewClient(&mcp.Implementation{ Name: "protoc-gen-mcp-stdio-test-client", Version: "v0.0.1", @@ -141,6 +186,7 @@ func TestServerOverStdio(t *testing.T) { t.Fatalf("CreateReport returned tool error over stdio: %+v", result) } + assertTextStructuredContentMatch(t, "example_CreateReport", result) structured := decodeMap(t, result.StructuredContent) if got := structured["totalCount"]; got != "42" { t.Fatalf("totalCount = %v, want ProtoJSON string \"42\"", got) @@ -184,6 +230,7 @@ func TestServerOverStdio(t *testing.T) { t.Fatalf("DescribeAdvancedShapes returned tool error over stdio: %+v", advancedResult) } + assertTextStructuredContentMatch(t, "example_DescribeAdvancedShapes", advancedResult) advancedStructured := decodeMap(t, advancedResult.StructuredContent) if got := advancedStructured["ratio"]; got != "NaN" { t.Fatalf("ratio = %v, want NaN", got) @@ -233,6 +280,7 @@ func TestServerOverStdio(t *testing.T) { t.Fatalf("DescribeScalarShapes returned tool error over stdio: %+v", scalarResult) } + assertTextStructuredContentMatch(t, "example_DescribeScalarShapes", scalarResult) scalarStructured := decodeMap(t, scalarResult.StructuredContent) if got := scalarStructured["int64Value"]; got != "-4567890123" { t.Fatalf("int64Value = %v, want -4567890123", got) @@ -256,6 +304,39 @@ func repoRoot(t *testing.T) string { return filepath.Clean(filepath.Join(filepath.Dir(filename), "..", "..")) } +func pythonExampleServerCommand(t *testing.T, root string) *exec.Cmd { + t.Helper() + + python := pythonCommand(t) + probe := exec.Command(python, "-c", "import anyio, google.protobuf, jsonschema, mcp") + probe.Dir = root + if output, err := probe.CombinedOutput(); err != nil { + t.Fatalf("python runtime dependencies are not available: %v\n%s", err, output) + } + + cmd := exec.Command(python, filepath.Join(root, "cmd/example-python-mcp-server/main.py")) + cmd.Dir = root + cmd.Env = append(os.Environ(), + "PYTHONPATH="+root, + "PYTHONUNBUFFERED=1", + ) + return cmd +} + +func pythonCommand(t *testing.T) string { + t.Helper() + + if path, err := exec.LookPath("python3"); err == nil { + return path + } + if path, err := exec.LookPath("python"); err == nil { + return path + } + + t.Fatal("python3/python not found in PATH") + return "" +} + func decodeMap(t *testing.T, value any) map[string]any { t.Helper() @@ -301,6 +382,29 @@ func validateToolInputSchema(t *testing.T, tools []*mcp.Tool, toolName string, a } } +func assertTextStructuredContentMatch(t *testing.T, toolName string, result *mcp.CallToolResult) { + t.Helper() + + if len(result.Content) != 1 { + t.Fatalf("%s returned %d content items, want 1", toolName, len(result.Content)) + } + + textContent, ok := result.Content[0].(*mcp.TextContent) + if !ok { + t.Fatalf("%s content[0] has type %T, want *mcp.TextContent", toolName, result.Content[0]) + } + + var fromText map[string]any + if err := json.Unmarshal([]byte(textContent.Text), &fromText); err != nil { + t.Fatalf("decode text content for %s: %v", toolName, err) + } + + fromStructured := decodeMap(t, result.StructuredContent) + if !reflect.DeepEqual(fromText, fromStructured) { + t.Fatalf("%s text content %v does not match structured content %v", toolName, fromText, fromStructured) + } +} + func findTool(t *testing.T, tools []*mcp.Tool, toolName string) *mcp.Tool { t.Helper() diff --git a/internal/testproto/example/v1/__init__.py b/internal/testproto/example/v1/__init__.py new file mode 100644 index 0000000..6e8ced9 --- /dev/null +++ b/internal/testproto/example/v1/__init__.py @@ -0,0 +1 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. diff --git a/internal/testproto/example/v1/example.pb.go b/internal/testproto/example/v1/example.pb.go index 7ccd66f..9cc3faa 100644 --- a/internal/testproto/example/v1/example.pb.go +++ b/internal/testproto/example/v1/example.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v0.14.1-v0.16.1-bufbuild-protocompile-easyp-modified +// protoc-gen-go v1.36.11 +// protoc v0.14.1-v0.15.2-bufbuild-protocompile-easyp-rc1 // source: internal/testproto/example/v1/example.proto package examplev1 @@ -19,6 +19,7 @@ import ( wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -83,10 +84,7 @@ func (ReportStatus) EnumDescriptor() ([]byte, []int) { // CreateReportRequest describes a report generation request. type CreateReportRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // city is the city name. City string `protobuf:"bytes,1,opt,name=city,proto3" json:"city,omitempty"` // count is the number of requested items. @@ -96,16 +94,16 @@ type CreateReportRequest struct { // units overrides the default units. Units *string `protobuf:"bytes,4,opt,name=units,proto3,oneof" json:"units,omitempty"` // labels adds additional labels. - Labels []string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty"` + Labels []string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateReportRequest) Reset() { *x = CreateReportRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateReportRequest) String() string { @@ -116,7 +114,7 @@ func (*CreateReportRequest) ProtoMessage() {} func (x *CreateReportRequest) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -168,10 +166,7 @@ func (x *CreateReportRequest) GetLabels() []string { // CreateReportResponse describes a report generation result. type CreateReportResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // report_id is the generated report identifier. ReportId string `protobuf:"bytes,1,opt,name=report_id,json=reportId,proto3" json:"report_id,omitempty"` // total_count is returned as a ProtoJSON string. @@ -181,16 +176,16 @@ type CreateReportResponse struct { // details echoes the nested options. Details *ReportDetails `protobuf:"bytes,4,opt,name=details,proto3" json:"details,omitempty"` // warnings contains optional warning messages. - Warnings []string `protobuf:"bytes,5,rep,name=warnings,proto3" json:"warnings,omitempty"` + Warnings []string `protobuf:"bytes,5,rep,name=warnings,proto3" json:"warnings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateReportResponse) Reset() { *x = CreateReportResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateReportResponse) String() string { @@ -201,7 +196,7 @@ func (*CreateReportResponse) ProtoMessage() {} func (x *CreateReportResponse) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -253,21 +248,18 @@ func (x *CreateReportResponse) GetWarnings() []string { // HiddenThingRequest is used by the hidden RPC. type HiddenThingRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // name is the hidden request payload. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HiddenThingRequest) Reset() { *x = HiddenThingRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *HiddenThingRequest) String() string { @@ -278,7 +270,7 @@ func (*HiddenThingRequest) ProtoMessage() {} func (x *HiddenThingRequest) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -302,18 +294,16 @@ func (x *HiddenThingRequest) GetName() string { // HiddenThingResponse is used by the hidden RPC. type HiddenThingResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *HiddenThingResponse) Reset() { *x = HiddenThingResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *HiddenThingResponse) String() string { @@ -324,7 +314,7 @@ func (*HiddenThingResponse) ProtoMessage() {} func (x *HiddenThingResponse) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -341,18 +331,16 @@ func (*HiddenThingResponse) Descriptor() ([]byte, []int) { // PingRequest is used by the health-check RPC. type PingRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PingRequest) Reset() { *x = PingRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PingRequest) String() string { @@ -363,7 +351,7 @@ func (*PingRequest) ProtoMessage() {} func (x *PingRequest) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -380,21 +368,18 @@ func (*PingRequest) Descriptor() ([]byte, []int) { // PingResponse is used by the health-check RPC. type PingResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // ack confirms the health-check call. - Ack *emptypb.Empty `protobuf:"bytes,1,opt,name=ack,proto3" json:"ack,omitempty"` + Ack *emptypb.Empty `protobuf:"bytes,1,opt,name=ack,proto3" json:"ack,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PingResponse) Reset() { *x = PingResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PingResponse) String() string { @@ -405,7 +390,7 @@ func (*PingResponse) ProtoMessage() {} func (x *PingResponse) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -429,18 +414,15 @@ func (x *PingResponse) GetAck() *emptypb.Empty { // DescribeAdvancedShapesRequest exercises supported advanced protobuf shapes. type DescribeAdvancedShapesRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // labels stores arbitrary string metadata. - Labels map[string]string `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Labels map[string]string `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // quantities demonstrates numeric map keys encoded as JSON object keys. - Quantities map[int32]string `protobuf:"bytes,2,rep,name=quantities,proto3" json:"quantities,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Quantities map[int32]string `protobuf:"bytes,2,rep,name=quantities,proto3" json:"quantities,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // toggles demonstrates bool map keys encoded as JSON object keys. - Toggles map[bool]string `protobuf:"bytes,3,rep,name=toggles,proto3" json:"toggles,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Toggles map[bool]string `protobuf:"bytes,3,rep,name=toggles,proto3" json:"toggles,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // limits demonstrates unsigned numeric map keys encoded as JSON object keys. - Limits map[uint64]string `protobuf:"bytes,14,rep,name=limits,proto3" json:"limits,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Limits map[uint64]string `protobuf:"bytes,14,rep,name=limits,proto3" json:"limits,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // observed_at is represented as an RFC 3339 timestamp string. ObservedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=observed_at,json=observedAt,proto3,oneof" json:"observed_at,omitempty"` // ttl is represented as a protobuf duration string. @@ -483,21 +465,21 @@ type DescribeAdvancedShapesRequest struct { DurationAny *anypb.Any `protobuf:"bytes,26,opt,name=duration_any,json=durationAny,proto3,oneof" json:"duration_any,omitempty"` // selector exercises top-level oneof support. // - // Types that are assignable to Selector: + // Types that are valid to be assigned to Selector: // // *DescribeAdvancedShapesRequest_CityAlias // *DescribeAdvancedShapesRequest_CityId // *DescribeAdvancedShapesRequest_CityDetails - Selector isDescribeAdvancedShapesRequest_Selector `protobuf_oneof:"selector"` + Selector isDescribeAdvancedShapesRequest_Selector `protobuf_oneof:"selector"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DescribeAdvancedShapesRequest) Reset() { *x = DescribeAdvancedShapesRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DescribeAdvancedShapesRequest) String() string { @@ -508,7 +490,7 @@ func (*DescribeAdvancedShapesRequest) ProtoMessage() {} func (x *DescribeAdvancedShapesRequest) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -684,30 +666,36 @@ func (x *DescribeAdvancedShapesRequest) GetDurationAny() *anypb.Any { return nil } -func (m *DescribeAdvancedShapesRequest) GetSelector() isDescribeAdvancedShapesRequest_Selector { - if m != nil { - return m.Selector +func (x *DescribeAdvancedShapesRequest) GetSelector() isDescribeAdvancedShapesRequest_Selector { + if x != nil { + return x.Selector } return nil } func (x *DescribeAdvancedShapesRequest) GetCityAlias() string { - if x, ok := x.GetSelector().(*DescribeAdvancedShapesRequest_CityAlias); ok { - return x.CityAlias + if x != nil { + if x, ok := x.Selector.(*DescribeAdvancedShapesRequest_CityAlias); ok { + return x.CityAlias + } } return "" } func (x *DescribeAdvancedShapesRequest) GetCityId() int64 { - if x, ok := x.GetSelector().(*DescribeAdvancedShapesRequest_CityId); ok { - return x.CityId + if x != nil { + if x, ok := x.Selector.(*DescribeAdvancedShapesRequest_CityId); ok { + return x.CityId + } } return 0 } func (x *DescribeAdvancedShapesRequest) GetCityDetails() *ReportDetails { - if x, ok := x.GetSelector().(*DescribeAdvancedShapesRequest_CityDetails); ok { - return x.CityDetails + if x != nil { + if x, ok := x.Selector.(*DescribeAdvancedShapesRequest_CityDetails); ok { + return x.CityDetails + } } return nil } @@ -739,18 +727,15 @@ func (*DescribeAdvancedShapesRequest_CityDetails) isDescribeAdvancedShapesReques // DescribeAdvancedShapesResponse echoes supported advanced protobuf shapes. type DescribeAdvancedShapesResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // labels stores arbitrary string metadata. - Labels map[string]string `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Labels map[string]string `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // quantities demonstrates numeric map keys encoded as JSON object keys. - Quantities map[int32]string `protobuf:"bytes,2,rep,name=quantities,proto3" json:"quantities,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Quantities map[int32]string `protobuf:"bytes,2,rep,name=quantities,proto3" json:"quantities,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // toggles demonstrates bool map keys encoded as JSON object keys. - Toggles map[bool]string `protobuf:"bytes,3,rep,name=toggles,proto3" json:"toggles,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Toggles map[bool]string `protobuf:"bytes,3,rep,name=toggles,proto3" json:"toggles,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // limits demonstrates unsigned numeric map keys encoded as JSON object keys. - Limits map[uint64]string `protobuf:"bytes,14,rep,name=limits,proto3" json:"limits,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Limits map[uint64]string `protobuf:"bytes,14,rep,name=limits,proto3" json:"limits,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // observed_at is represented as an RFC 3339 timestamp string. ObservedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=observed_at,json=observedAt,proto3,oneof" json:"observed_at,omitempty"` // ttl is represented as a protobuf duration string. @@ -793,21 +778,21 @@ type DescribeAdvancedShapesResponse struct { DurationAny *anypb.Any `protobuf:"bytes,26,opt,name=duration_any,json=durationAny,proto3,oneof" json:"duration_any,omitempty"` // selector exercises top-level oneof support. // - // Types that are assignable to Selector: + // Types that are valid to be assigned to Selector: // // *DescribeAdvancedShapesResponse_CityAlias // *DescribeAdvancedShapesResponse_CityId // *DescribeAdvancedShapesResponse_CityDetails - Selector isDescribeAdvancedShapesResponse_Selector `protobuf_oneof:"selector"` + Selector isDescribeAdvancedShapesResponse_Selector `protobuf_oneof:"selector"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DescribeAdvancedShapesResponse) Reset() { *x = DescribeAdvancedShapesResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DescribeAdvancedShapesResponse) String() string { @@ -818,7 +803,7 @@ func (*DescribeAdvancedShapesResponse) ProtoMessage() {} func (x *DescribeAdvancedShapesResponse) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -994,30 +979,36 @@ func (x *DescribeAdvancedShapesResponse) GetDurationAny() *anypb.Any { return nil } -func (m *DescribeAdvancedShapesResponse) GetSelector() isDescribeAdvancedShapesResponse_Selector { - if m != nil { - return m.Selector +func (x *DescribeAdvancedShapesResponse) GetSelector() isDescribeAdvancedShapesResponse_Selector { + if x != nil { + return x.Selector } return nil } func (x *DescribeAdvancedShapesResponse) GetCityAlias() string { - if x, ok := x.GetSelector().(*DescribeAdvancedShapesResponse_CityAlias); ok { - return x.CityAlias + if x != nil { + if x, ok := x.Selector.(*DescribeAdvancedShapesResponse_CityAlias); ok { + return x.CityAlias + } } return "" } func (x *DescribeAdvancedShapesResponse) GetCityId() int64 { - if x, ok := x.GetSelector().(*DescribeAdvancedShapesResponse_CityId); ok { - return x.CityId + if x != nil { + if x, ok := x.Selector.(*DescribeAdvancedShapesResponse_CityId); ok { + return x.CityId + } } return 0 } func (x *DescribeAdvancedShapesResponse) GetCityDetails() *ReportDetails { - if x, ok := x.GetSelector().(*DescribeAdvancedShapesResponse_CityDetails); ok { - return x.CityDetails + if x != nil { + if x, ok := x.Selector.(*DescribeAdvancedShapesResponse_CityDetails); ok { + return x.CityDetails + } } return nil } @@ -1049,10 +1040,7 @@ func (*DescribeAdvancedShapesResponse_CityDetails) isDescribeAdvancedShapesRespo // DescribeScalarShapesRequest exercises plain protobuf scalar kinds. type DescribeScalarShapesRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // bool_flag exercises the plain bool kind. BoolFlag bool `protobuf:"varint,1,opt,name=bool_flag,json=boolFlag,proto3" json:"bool_flag,omitempty"` // text_value exercises the plain string kind. @@ -1121,15 +1109,15 @@ type DescribeScalarShapesRequest struct { OptionalDoubleValue *float64 `protobuf:"fixed64,33,opt,name=optional_double_value,json=optionalDoubleValue,proto3,oneof" json:"optional_double_value,omitempty"` // optional_status exercises proto3 optional enum. OptionalStatus *ReportStatus `protobuf:"varint,34,opt,name=optional_status,json=optionalStatus,proto3,enum=internal.testproto.example.v1.ReportStatus,oneof" json:"optional_status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DescribeScalarShapesRequest) Reset() { *x = DescribeScalarShapesRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DescribeScalarShapesRequest) String() string { @@ -1140,7 +1128,7 @@ func (*DescribeScalarShapesRequest) ProtoMessage() {} func (x *DescribeScalarShapesRequest) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1395,10 +1383,7 @@ func (x *DescribeScalarShapesRequest) GetOptionalStatus() ReportStatus { // DescribeScalarShapesResponse echoes plain protobuf scalar kinds. type DescribeScalarShapesResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // bool_flag echoes the plain bool kind. BoolFlag bool `protobuf:"varint,1,opt,name=bool_flag,json=boolFlag,proto3" json:"bool_flag,omitempty"` // text_value echoes the plain string kind. @@ -1467,15 +1452,15 @@ type DescribeScalarShapesResponse struct { OptionalDoubleValue *float64 `protobuf:"fixed64,33,opt,name=optional_double_value,json=optionalDoubleValue,proto3,oneof" json:"optional_double_value,omitempty"` // optional_status echoes proto3 optional enum. OptionalStatus *ReportStatus `protobuf:"varint,34,opt,name=optional_status,json=optionalStatus,proto3,enum=internal.testproto.example.v1.ReportStatus,oneof" json:"optional_status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DescribeScalarShapesResponse) Reset() { *x = DescribeScalarShapesResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DescribeScalarShapesResponse) String() string { @@ -1486,7 +1471,7 @@ func (*DescribeScalarShapesResponse) ProtoMessage() {} func (x *DescribeScalarShapesResponse) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1741,21 +1726,18 @@ func (x *DescribeScalarShapesResponse) GetOptionalStatus() ReportStatus { // ReportDetails contains nested report options. type ReportDetails struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // label is an arbitrary label. - Label string `protobuf:"bytes,1,opt,name=label,proto3" json:"label,omitempty"` + Label string `protobuf:"bytes,1,opt,name=label,proto3" json:"label,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReportDetails) Reset() { *x = ReportDetails{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReportDetails) String() string { @@ -1766,7 +1748,7 @@ func (*ReportDetails) ProtoMessage() {} func (x *ReportDetails) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1790,25 +1772,22 @@ func (x *ReportDetails) GetLabel() string { // RecursiveNode exercises recursive message schemas. type RecursiveNode struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // name identifies the current node. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // child points at the next recursive node. Child *RecursiveNode `protobuf:"bytes,2,opt,name=child,proto3,oneof" json:"child,omitempty"` // children holds repeated recursive nodes. - Children []*RecursiveNode `protobuf:"bytes,3,rep,name=children,proto3" json:"children,omitempty"` + Children []*RecursiveNode `protobuf:"bytes,3,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RecursiveNode) Reset() { *x = RecursiveNode{} - if protoimpl.UnsafeEnabled { - mi := &file_internal_testproto_example_v1_example_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_internal_testproto_example_v1_example_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RecursiveNode) String() string { @@ -1819,7 +1798,7 @@ func (*RecursiveNode) ProtoMessage() {} func (x *RecursiveNode) ProtoReflect() protoreflect.Message { mi := &file_internal_testproto_example_v1_example_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1857,704 +1836,330 @@ func (x *RecursiveNode) GetChildren() []*RecursiveNode { var File_internal_testproto_example_v1_example_proto protoreflect.FileDescriptor -var file_internal_testproto_example_v1_example_proto_rawDesc = []byte{ - 0x0a, 0x2b, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, - 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1d, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x6d, 0x63, - 0x70, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0xa9, 0x02, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x43, 0x0a, 0x04, 0x63, - 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2f, 0xda, 0xb7, 0x2c, 0x2b, 0x0a, - 0x0a, 0x43, 0x69, 0x74, 0x79, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x2e, 0x1a, 0x07, 0x0a, 0x05, 0x50, - 0x61, 0x72, 0x69, 0x73, 0x1a, 0x08, 0x0a, 0x06, 0x4c, 0x6f, 0x6e, 0x64, 0x6f, 0x6e, 0x52, 0x06, - 0x5e, 0x5b, 0x41, 0x2d, 0x5a, 0x5d, 0x60, 0x01, 0x68, 0x64, 0x52, 0x04, 0x63, 0x69, 0x74, 0x79, - 0x12, 0x39, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, - 0x23, 0xda, 0xb7, 0x2c, 0x1f, 0x22, 0x09, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x40, - 0xa1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xa9, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x40, 0x8f, 0x40, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x46, 0x0a, 0x07, 0x64, - 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, - 0x69, 0x6c, 0x73, 0x12, 0x19, 0x0a, 0x05, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x88, 0x01, 0x01, 0x12, 0x25, - 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x42, 0x0d, - 0xda, 0xb7, 0x2c, 0x09, 0xf0, 0x01, 0x01, 0xf8, 0x01, 0x32, 0x80, 0x02, 0x01, 0x52, 0x06, 0x6c, - 0x61, 0x62, 0x65, 0x6c, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x22, - 0xfd, 0x01, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x07, 0x64, - 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, - 0x69, 0x6c, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x22, - 0x28, 0x0a, 0x12, 0x48, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x15, 0x0a, 0x13, 0x48, 0x69, 0x64, - 0x64, 0x65, 0x6e, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x0d, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0x38, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x28, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x22, 0xf1, 0x10, 0x0a, 0x1d, 0x44, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x64, 0x53, 0x68, - 0x61, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x60, 0x0a, 0x06, 0x6c, - 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x48, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x62, 0x65, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x64, 0x53, 0x68, 0x61, 0x70, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x6c, 0x0a, - 0x0a, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x4c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, - 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, - 0x65, 0x64, 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, - 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x0a, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x63, 0x0a, 0x07, 0x74, - 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x62, 0x65, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x64, 0x53, 0x68, 0x61, - 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x6f, 0x67, 0x67, 0x6c, - 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x74, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x73, - 0x12, 0x60, 0x0a, 0x06, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x48, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, - 0x64, 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, - 0x69, 0x6d, 0x69, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x73, 0x12, 0x40, 0x0a, 0x0b, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x61, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x48, 0x01, 0x52, 0x0a, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x41, - 0x74, 0x88, 0x01, 0x01, 0x12, 0x30, 0x0a, 0x03, 0x74, 0x74, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x02, 0x52, 0x03, - 0x74, 0x74, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x36, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x48, 0x03, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x88, 0x01, 0x01, 0x12, 0x35, - 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x04, 0x52, 0x05, 0x69, 0x74, 0x65, - 0x6d, 0x73, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x07, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x05, - 0x52, 0x07, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x04, - 0x6e, 0x6f, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x06, 0x52, 0x04, 0x6e, 0x6f, 0x74, 0x65, - 0x88, 0x01, 0x01, 0x12, 0x36, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, - 0x07, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x07, 0x65, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, - 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x05, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x18, - 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x48, 0x09, 0x52, 0x05, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x88, 0x01, 0x01, 0x12, - 0x33, 0x0a, 0x04, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x48, 0x0a, 0x52, 0x04, 0x6d, 0x61, 0x73, - 0x6b, 0x88, 0x01, 0x01, 0x12, 0x34, 0x0a, 0x04, 0x62, 0x6c, 0x6f, 0x62, 0x18, 0x0f, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, - 0x0b, 0x52, 0x04, 0x62, 0x6c, 0x6f, 0x62, 0x88, 0x01, 0x01, 0x12, 0x41, 0x0a, 0x0b, 0x73, 0x6d, - 0x61, 0x6c, 0x6c, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x0c, 0x52, 0x0a, - 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, - 0x0a, 0x75, 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x11, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, - 0x0d, 0x52, 0x09, 0x75, 0x69, 0x6e, 0x74, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, - 0x40, 0x0a, 0x0a, 0x68, 0x75, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x12, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x48, 0x0e, 0x52, 0x09, 0x68, 0x75, 0x67, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x88, 0x01, - 0x01, 0x12, 0x38, 0x0a, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x0f, - 0x52, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x72, - 0x61, 0x77, 0x5f, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x18, 0x14, 0x20, 0x01, 0x28, 0x01, 0x48, 0x10, - 0x52, 0x08, 0x72, 0x61, 0x77, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, - 0x04, 0x74, 0x72, 0x65, 0x65, 0x18, 0x18, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x75, - 0x72, 0x73, 0x69, 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x48, 0x11, 0x52, 0x04, 0x74, 0x72, 0x65, - 0x65, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x0a, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x5f, 0x61, - 0x6e, 0x79, 0x18, 0x19, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x48, 0x12, - 0x52, 0x09, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x41, 0x6e, 0x79, 0x88, 0x01, 0x01, 0x12, 0x3c, - 0x0a, 0x0c, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x6e, 0x79, 0x18, 0x1a, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x48, 0x13, 0x52, 0x0b, 0x64, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6e, 0x79, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x0a, - 0x63, 0x69, 0x74, 0x79, 0x5f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x00, 0x52, 0x09, 0x63, 0x69, 0x74, 0x79, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x19, 0x0a, - 0x07, 0x63, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x16, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, - 0x52, 0x06, 0x63, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x51, 0x0a, 0x0c, 0x63, 0x69, 0x74, 0x79, - 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x48, 0x00, 0x52, 0x0b, - 0x63, 0x69, 0x74, 0x79, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, - 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3d, 0x0a, 0x0f, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, - 0x74, 0x69, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0a, 0x0a, 0x08, - 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x6f, 0x62, 0x73, - 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x74, 0x74, 0x6c, - 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x08, 0x0a, 0x06, - 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x64, 0x79, 0x6e, 0x61, 0x6d, - 0x69, 0x63, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6e, 0x6f, 0x74, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, - 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x42, 0x07, 0x0a, 0x05, 0x5f, - 0x6d, 0x61, 0x73, 0x6b, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x42, 0x0e, 0x0a, - 0x0c, 0x5f, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x0d, 0x0a, - 0x0b, 0x5f, 0x75, 0x69, 0x6e, 0x74, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x0d, 0x0a, 0x0b, - 0x5f, 0x68, 0x75, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x09, 0x0a, 0x07, 0x5f, - 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x72, 0x61, 0x77, 0x5f, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x74, 0x72, 0x65, 0x65, 0x42, 0x0d, 0x0a, - 0x0b, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x5f, 0x61, 0x6e, 0x79, 0x42, 0x0f, 0x0a, 0x0d, - 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x6e, 0x79, 0x22, 0xf6, 0x10, - 0x0a, 0x1e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, - 0x65, 0x64, 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x61, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x49, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, - 0x64, 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, - 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, - 0x65, 0x6c, 0x73, 0x12, 0x6d, 0x0a, 0x0a, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4d, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, - 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x64, 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x69, - 0x65, 0x73, 0x12, 0x64, 0x0a, 0x07, 0x74, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x4a, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, - 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x41, 0x64, 0x76, 0x61, - 0x6e, 0x63, 0x65, 0x64, 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x2e, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x07, 0x74, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x73, 0x12, 0x61, 0x0a, 0x06, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, - 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, - 0x65, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x64, 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x40, 0x0a, 0x0b, 0x6f, - 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x01, 0x52, 0x0a, - 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x30, 0x0a, - 0x03, 0x74, 0x74, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x02, 0x52, 0x03, 0x74, 0x74, 0x6c, 0x88, 0x01, 0x01, 0x12, - 0x36, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x03, 0x52, 0x07, 0x70, 0x61, 0x79, - 0x6c, 0x6f, 0x61, 0x64, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x48, 0x04, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x88, 0x01, 0x01, 0x12, 0x35, - 0x0a, 0x07, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x05, 0x52, 0x07, 0x64, 0x79, 0x6e, 0x61, 0x6d, - 0x69, 0x63, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x48, 0x06, 0x52, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x88, 0x01, 0x01, 0x12, 0x36, 0x0a, 0x05, - 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, - 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x07, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x48, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, - 0x37, 0x0a, 0x05, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x09, 0x52, 0x05, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x04, 0x6d, 0x61, 0x73, 0x6b, - 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, - 0x73, 0x6b, 0x48, 0x0a, 0x52, 0x04, 0x6d, 0x61, 0x73, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x34, 0x0a, - 0x04, 0x62, 0x6c, 0x6f, 0x62, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79, - 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x0b, 0x52, 0x04, 0x62, 0x6c, 0x6f, 0x62, - 0x88, 0x01, 0x01, 0x12, 0x41, 0x0a, 0x0b, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x5f, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x0c, 0x52, 0x0a, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x54, 0x6f, - 0x74, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x0a, 0x75, 0x69, 0x6e, 0x74, 0x5f, 0x74, - 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, - 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x0d, 0x52, 0x09, 0x75, 0x69, 0x6e, 0x74, - 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x0a, 0x68, 0x75, 0x67, 0x65, - 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, - 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x0e, 0x52, 0x09, 0x68, 0x75, - 0x67, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x06, 0x77, 0x65, - 0x69, 0x67, 0x68, 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x6c, 0x6f, - 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x0f, 0x52, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, - 0x74, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x72, 0x61, 0x77, 0x5f, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x18, 0x14, 0x20, 0x01, 0x28, 0x01, 0x48, 0x10, 0x52, 0x08, 0x72, 0x61, 0x77, 0x52, 0x61, - 0x74, 0x69, 0x6f, 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, 0x04, 0x74, 0x72, 0x65, 0x65, 0x18, 0x18, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x4e, 0x6f, - 0x64, 0x65, 0x48, 0x11, 0x52, 0x04, 0x74, 0x72, 0x65, 0x65, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, - 0x0a, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x5f, 0x61, 0x6e, 0x79, 0x18, 0x19, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x48, 0x12, 0x52, 0x09, 0x64, 0x65, 0x74, 0x61, 0x69, - 0x6c, 0x41, 0x6e, 0x79, 0x88, 0x01, 0x01, 0x12, 0x3c, 0x0a, 0x0c, 0x64, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x6e, 0x79, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x41, 0x6e, 0x79, 0x48, 0x13, 0x52, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, - 0x6e, 0x79, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x0a, 0x63, 0x69, 0x74, 0x79, 0x5f, 0x61, 0x6c, - 0x69, 0x61, 0x73, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x63, 0x69, 0x74, - 0x79, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x19, 0x0a, 0x07, 0x63, 0x69, 0x74, 0x79, 0x5f, 0x69, - 0x64, 0x18, 0x16, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x06, 0x63, 0x69, 0x74, 0x79, 0x49, - 0x64, 0x12, 0x51, 0x0a, 0x0c, 0x63, 0x69, 0x74, 0x79, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, - 0x73, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x44, 0x65, - 0x74, 0x61, 0x69, 0x6c, 0x73, 0x48, 0x00, 0x52, 0x0b, 0x63, 0x69, 0x74, 0x79, 0x44, 0x65, 0x74, - 0x61, 0x69, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, - 0x3d, 0x0a, 0x0f, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, - 0x0a, 0x0c, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x69, - 0x6d, 0x69, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x5f, 0x61, - 0x74, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x74, 0x74, 0x6c, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x61, - 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, - 0x0a, 0x0a, 0x08, 0x5f, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x42, 0x07, 0x0a, 0x05, 0x5f, - 0x6e, 0x6f, 0x74, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x0a, - 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x42, 0x07, 0x0a, - 0x05, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x73, 0x6d, 0x61, 0x6c, 0x6c, - 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x75, 0x69, 0x6e, 0x74, 0x5f, - 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x68, 0x75, 0x67, 0x65, 0x5f, 0x74, - 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x42, - 0x0c, 0x0a, 0x0a, 0x5f, 0x72, 0x61, 0x77, 0x5f, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x42, 0x07, 0x0a, - 0x05, 0x5f, 0x74, 0x72, 0x65, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, - 0x6c, 0x5f, 0x61, 0x6e, 0x79, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x61, 0x6e, 0x79, 0x22, 0x8e, 0x10, 0x0a, 0x1b, 0x44, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x62, 0x65, 0x53, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x66, - 0x6c, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x62, 0x6f, 0x6f, 0x6c, 0x46, - 0x6c, 0x61, 0x67, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x65, 0x78, 0x74, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x11, 0x52, 0x0b, 0x73, 0x69, 0x6e, 0x74, - 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x66, 0x69, 0x78, 0x65, - 0x64, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0f, 0x52, - 0x0d, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, - 0x0a, 0x0c, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x07, 0x52, 0x0c, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, - 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x69, 0x6e, 0x74, - 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x69, 0x6e, 0x74, 0x36, - 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x12, 0x52, 0x0b, 0x73, - 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x66, - 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x10, 0x52, 0x0d, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x06, 0x52, 0x0c, 0x66, 0x69, 0x78, - 0x65, 0x64, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x6c, 0x6f, - 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x02, 0x52, 0x0a, - 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x6f, - 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x01, - 0x52, 0x0b, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x43, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x46, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x11, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, - 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, - 0x73, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x18, 0x12, 0x20, 0x03, 0x28, 0x05, 0x52, 0x07, 0x73, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x73, 0x12, 0x31, 0x0a, 0x12, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x5f, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x18, 0x13, 0x20, 0x01, 0x28, 0x08, - 0x48, 0x00, 0x52, 0x10, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x42, 0x6f, 0x6f, 0x6c, - 0x46, 0x6c, 0x61, 0x67, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x13, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x61, 0x6c, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x14, - 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x11, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x54, 0x65, 0x78, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x14, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x02, 0x52, 0x12, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, - 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x16, 0x20, 0x01, 0x28, - 0x05, 0x48, 0x03, 0x52, 0x12, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x6e, 0x74, - 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x17, 0x20, 0x01, 0x28, 0x11, 0x48, 0x04, 0x52, 0x13, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a, 0x17, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, - 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x18, - 0x20, 0x01, 0x28, 0x0f, 0x48, 0x05, 0x52, 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x53, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, - 0x12, 0x37, 0x0a, 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x69, 0x6e, - 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x19, 0x20, 0x01, 0x28, 0x0d, 0x48, - 0x06, 0x52, 0x13, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x55, 0x69, 0x6e, 0x74, 0x33, - 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x16, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x07, 0x48, 0x07, 0x52, 0x14, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x5f, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x1b, 0x20, 0x01, - 0x28, 0x03, 0x48, 0x08, 0x52, 0x12, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, 0x6e, - 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x12, 0x48, 0x09, 0x52, 0x13, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a, 0x17, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x5f, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x1d, 0x20, 0x01, 0x28, 0x10, 0x48, 0x0a, 0x52, 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x53, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, - 0x01, 0x12, 0x37, 0x0a, 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x69, - 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x04, - 0x48, 0x0b, 0x52, 0x13, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x55, 0x69, 0x6e, 0x74, - 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x16, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x06, 0x48, 0x0c, 0x52, 0x14, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x5f, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x20, 0x20, - 0x01, 0x28, 0x02, 0x48, 0x0d, 0x52, 0x12, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x46, - 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x21, 0x20, 0x01, 0x28, 0x01, 0x48, 0x0e, 0x52, 0x13, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x59, 0x0a, 0x0f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x22, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x0f, 0x52, 0x0e, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x88, 0x01, 0x01, - 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x62, 0x6f, - 0x6f, 0x6c, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x6f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, - 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, - 0x65, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, - 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x1a, 0x0a, 0x18, 0x5f, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, - 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x42, 0x19, 0x0a, 0x17, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, - 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x17, 0x0a, 0x15, - 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, - 0x1a, 0x0a, 0x18, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x66, 0x69, - 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x5f, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x19, 0x0a, 0x17, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x6c, - 0x6f, 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x8f, 0x10, 0x0a, 0x1c, 0x44, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x62, 0x65, 0x53, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x6f, 0x6f, 0x6c, - 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x62, 0x6f, 0x6f, - 0x6c, 0x46, 0x6c, 0x61, 0x67, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x65, 0x78, 0x74, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x69, 0x6e, 0x74, 0x33, - 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x69, 0x6e, 0x74, 0x33, 0x32, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x11, 0x52, 0x0b, 0x73, 0x69, - 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x66, 0x69, - 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x0f, 0x52, 0x0d, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x07, 0x52, 0x0c, 0x66, 0x69, 0x78, 0x65, - 0x64, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x74, 0x36, - 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x69, - 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x69, 0x6e, - 0x74, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x12, 0x52, - 0x0b, 0x73, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, 0x0e, - 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x10, 0x52, 0x0d, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x75, 0x69, 0x6e, 0x74, 0x36, - 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, - 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x06, 0x52, 0x0c, 0x66, - 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x66, - 0x6c, 0x6f, 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x02, - 0x52, 0x0a, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, - 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0f, 0x20, 0x01, - 0x28, 0x01, 0x52, 0x0b, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, - 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x2b, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, - 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, - 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x44, 0x65, 0x74, 0x61, - 0x69, 0x6c, 0x73, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x18, 0x0a, 0x07, - 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x18, 0x12, 0x20, 0x03, 0x28, 0x05, 0x52, 0x07, 0x73, - 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x31, 0x0a, 0x12, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x5f, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x18, 0x13, 0x20, 0x01, - 0x28, 0x08, 0x48, 0x00, 0x52, 0x10, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x42, 0x6f, - 0x6f, 0x6c, 0x46, 0x6c, 0x61, 0x67, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x13, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x11, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x35, - 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x02, 0x52, 0x12, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x5f, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x16, 0x20, - 0x01, 0x28, 0x05, 0x48, 0x03, 0x52, 0x12, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x49, - 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x17, 0x20, 0x01, 0x28, 0x11, 0x48, 0x04, 0x52, 0x13, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a, 0x17, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x5f, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x18, 0x20, 0x01, 0x28, 0x0f, 0x48, 0x05, 0x52, 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x53, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, - 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, - 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x19, 0x20, 0x01, 0x28, - 0x0d, 0x48, 0x06, 0x52, 0x13, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x55, 0x69, 0x6e, - 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x16, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x07, 0x48, 0x07, 0x52, 0x14, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x5f, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x1b, - 0x20, 0x01, 0x28, 0x03, 0x48, 0x08, 0x52, 0x12, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, - 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x6e, 0x74, 0x36, 0x34, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x12, 0x48, 0x09, 0x52, 0x13, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a, 0x17, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x5f, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x10, 0x48, 0x0a, 0x52, 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x61, 0x6c, 0x53, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, - 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x1e, 0x20, 0x01, - 0x28, 0x04, 0x48, 0x0b, 0x52, 0x13, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x55, 0x69, - 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x16, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x06, 0x48, 0x0c, 0x52, 0x14, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x20, 0x20, 0x01, 0x28, 0x02, 0x48, 0x0d, 0x52, 0x12, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x37, - 0x0a, 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x6f, 0x75, 0x62, 0x6c, - 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x21, 0x20, 0x01, 0x28, 0x01, 0x48, 0x0e, 0x52, - 0x13, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x59, 0x0a, 0x0f, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x22, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x2b, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x0f, 0x52, - 0x0e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x88, - 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, - 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x62, - 0x79, 0x74, 0x65, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x5f, 0x73, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x1a, 0x0a, - 0x18, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x66, 0x69, 0x78, 0x65, - 0x64, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x42, 0x19, 0x0a, 0x17, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, - 0x5f, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x17, - 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x6e, 0x74, 0x36, - 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x42, 0x1a, 0x0a, 0x18, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, - 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x18, 0x0a, - 0x16, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x69, 0x6e, 0x74, 0x36, - 0x34, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x19, 0x0a, 0x17, 0x5f, 0x6f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, - 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x5f, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x61, 0x0a, 0x0d, 0x52, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, - 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, - 0x3a, 0x3a, 0xe2, 0xb7, 0x2c, 0x36, 0x0a, 0x0e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x44, - 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x24, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x20, 0x72, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x20, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x22, 0xc0, 0x01, 0x0a, - 0x0d, 0x52, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x47, 0x0a, 0x05, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x2c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, - 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x48, - 0x00, 0x52, 0x05, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x88, 0x01, 0x01, 0x12, 0x48, 0x0a, 0x08, 0x63, - 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x63, 0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x08, 0x63, 0x68, 0x69, - 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x2a, - 0xd8, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x1e, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, - 0x53, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x1a, 0x06, 0xf2, 0xb7, 0x2c, 0x02, 0x10, 0x01, - 0x12, 0x3a, 0x0a, 0x10, 0x52, 0x45, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, - 0x53, 0x5f, 0x4f, 0x4b, 0x10, 0x01, 0x1a, 0x24, 0xf2, 0xb7, 0x2c, 0x20, 0x0a, 0x1e, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x20, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x20, 0x73, - 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x6c, 0x79, 0x2e, 0x12, 0x39, 0x0a, 0x14, - 0x52, 0x45, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, - 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x1a, 0x1f, 0xf2, 0xb7, 0x2c, 0x1b, 0x0a, 0x19, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x20, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, - 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x2e, 0x1a, 0x31, 0xea, 0xb7, 0x2c, 0x2d, 0x0a, 0x0d, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x43, 0x75, - 0x72, 0x72, 0x65, 0x6e, 0x74, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, - 0x68, 0x65, 0x20, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x32, 0xfc, 0x06, 0x0a, 0x0a, 0x45, - 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x41, 0x50, 0x49, 0x12, 0xa9, 0x01, 0x0a, 0x0c, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x30, 0xd2, 0xb7, 0x2c, 0x2c, 0x12, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x20, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x1a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x20, 0x61, 0x20, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x61, 0x20, - 0x63, 0x69, 0x74, 0x79, 0x2e, 0x12, 0x7b, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x2a, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, - 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, - 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0xd2, 0xb7, 0x2c, 0x16, 0x0a, 0x06, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x12, 0x0c, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x20, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x12, 0xe3, 0x01, 0x0a, 0x16, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x41, - 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x64, 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, 0x12, 0x3c, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x64, 0x53, 0x68, - 0x61, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3d, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x62, 0x65, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x64, 0x53, 0x68, 0x61, 0x70, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4c, 0xd2, 0xb7, 0x2c, 0x48, - 0x12, 0x18, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x20, 0x61, 0x64, 0x76, 0x61, 0x6e, - 0x63, 0x65, 0x64, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x73, 0x1a, 0x2c, 0x45, 0x78, 0x65, 0x72, - 0x63, 0x69, 0x73, 0x65, 0x20, 0x6d, 0x61, 0x70, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x77, 0x65, - 0x6c, 0x6c, 0x2d, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x20, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x20, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x12, 0xd4, 0x01, 0x0a, 0x14, 0x44, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x53, 0x68, 0x61, 0x70, 0x65, - 0x73, 0x12, 0x3a, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, - 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x63, 0x61, 0x6c, 0x61, 0x72, - 0x53, 0x68, 0x61, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x53, 0x68, 0x61, 0x70, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x43, 0xd2, 0xb7, 0x2c, 0x3f, - 0x12, 0x16, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x20, 0x73, 0x63, 0x61, 0x6c, 0x61, - 0x72, 0x20, 0x73, 0x68, 0x61, 0x70, 0x65, 0x73, 0x1a, 0x25, 0x45, 0x78, 0x65, 0x72, 0x63, 0x69, - 0x73, 0x65, 0x20, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x20, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x20, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x20, 0x6b, 0x69, 0x6e, 0x64, 0x73, 0x2e, 0x12, - 0x79, 0x0a, 0x0b, 0x48, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x12, 0x31, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x48, - 0x69, 0x64, 0x64, 0x65, 0x6e, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x32, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x74, 0x65, 0x73, - 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x48, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x54, 0x68, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x1a, 0x0d, 0xca, 0xb7, 0x2c, 0x09, - 0x0a, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x42, 0x4e, 0x5a, 0x4c, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x61, 0x73, 0x79, 0x70, 0x2d, 0x74, 0x65, - 0x63, 0x68, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6d, 0x63, - 0x70, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, - 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, -} +const file_internal_testproto_example_v1_example_proto_rawDesc = "" + + "\n" + + "+internal/testproto/example/v1/example.proto\x12\x1dinternal.testproto.example.v1\x1a\x1cmcp/options/v1/options.proto\x1a\x19google/protobuf/any.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\xa9\x02\n" + + "\x13CreateReportRequest\x12C\n" + + "\x04city\x18\x01 \x01(\tB/ڷ,+\n" + + "\n" + + "City name.\x1a\a\n" + + "\x05Paris\x1a\b\n" + + "\x06LondonR\x06^[A-Z]`\x01hdR\x04city\x129\n" + + "\x05count\x18\x02 \x01(\x05B#ڷ,\x1f\"\t\x11\x00\x00\x00\x00\x00\x00$@\xa1\x01\x00\x00\x00\x00\x00\x00\xf0?\xa9\x01\x00\x00\x00\x00\x00@\x8f@R\x05count\x12F\n" + + "\adetails\x18\x03 \x01(\v2,.internal.testproto.example.v1.ReportDetailsR\adetails\x12\x19\n" + + "\x05units\x18\x04 \x01(\tH\x00R\x05units\x88\x01\x01\x12%\n" + + "\x06labels\x18\x05 \x03(\tB\rڷ,\t\xf0\x01\x01\xf8\x012\x80\x02\x01R\x06labelsB\b\n" + + "\x06_units\"\xfd\x01\n" + + "\x14CreateReportResponse\x12\x1b\n" + + "\treport_id\x18\x01 \x01(\tR\breportId\x12\x1f\n" + + "\vtotal_count\x18\x02 \x01(\x03R\n" + + "totalCount\x12C\n" + + "\x06status\x18\x03 \x01(\x0e2+.internal.testproto.example.v1.ReportStatusR\x06status\x12F\n" + + "\adetails\x18\x04 \x01(\v2,.internal.testproto.example.v1.ReportDetailsR\adetails\x12\x1a\n" + + "\bwarnings\x18\x05 \x03(\tR\bwarnings\"(\n" + + "\x12HiddenThingRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x15\n" + + "\x13HiddenThingResponse\"\r\n" + + "\vPingRequest\"8\n" + + "\fPingResponse\x12(\n" + + "\x03ack\x18\x01 \x01(\v2\x16.google.protobuf.EmptyR\x03ack\"\xf1\x10\n" + + "\x1dDescribeAdvancedShapesRequest\x12`\n" + + "\x06labels\x18\x01 \x03(\v2H.internal.testproto.example.v1.DescribeAdvancedShapesRequest.LabelsEntryR\x06labels\x12l\n" + + "\n" + + "quantities\x18\x02 \x03(\v2L.internal.testproto.example.v1.DescribeAdvancedShapesRequest.QuantitiesEntryR\n" + + "quantities\x12c\n" + + "\atoggles\x18\x03 \x03(\v2I.internal.testproto.example.v1.DescribeAdvancedShapesRequest.TogglesEntryR\atoggles\x12`\n" + + "\x06limits\x18\x0e \x03(\v2H.internal.testproto.example.v1.DescribeAdvancedShapesRequest.LimitsEntryR\x06limits\x12@\n" + + "\vobserved_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\n" + + "observedAt\x88\x01\x01\x120\n" + + "\x03ttl\x18\x05 \x01(\v2\x19.google.protobuf.DurationH\x02R\x03ttl\x88\x01\x01\x126\n" + + "\apayload\x18\x06 \x01(\v2\x17.google.protobuf.StructH\x03R\apayload\x88\x01\x01\x125\n" + + "\x05items\x18\a \x01(\v2\x1a.google.protobuf.ListValueH\x04R\x05items\x88\x01\x01\x125\n" + + "\adynamic\x18\b \x01(\v2\x16.google.protobuf.ValueH\x05R\adynamic\x88\x01\x01\x125\n" + + "\x04note\x18\t \x01(\v2\x1c.google.protobuf.StringValueH\x06R\x04note\x88\x01\x01\x126\n" + + "\x05total\x18\n" + + " \x01(\v2\x1b.google.protobuf.Int64ValueH\aR\x05total\x88\x01\x01\x129\n" + + "\aenabled\x18\v \x01(\v2\x1a.google.protobuf.BoolValueH\bR\aenabled\x88\x01\x01\x127\n" + + "\x05ratio\x18\f \x01(\v2\x1c.google.protobuf.DoubleValueH\tR\x05ratio\x88\x01\x01\x123\n" + + "\x04mask\x18\r \x01(\v2\x1a.google.protobuf.FieldMaskH\n" + + "R\x04mask\x88\x01\x01\x124\n" + + "\x04blob\x18\x0f \x01(\v2\x1b.google.protobuf.BytesValueH\vR\x04blob\x88\x01\x01\x12A\n" + + "\vsmall_total\x18\x10 \x01(\v2\x1b.google.protobuf.Int32ValueH\fR\n" + + "smallTotal\x88\x01\x01\x12@\n" + + "\n" + + "uint_total\x18\x11 \x01(\v2\x1c.google.protobuf.UInt32ValueH\rR\tuintTotal\x88\x01\x01\x12@\n" + + "\n" + + "huge_total\x18\x12 \x01(\v2\x1c.google.protobuf.UInt64ValueH\x0eR\thugeTotal\x88\x01\x01\x128\n" + + "\x06weight\x18\x13 \x01(\v2\x1b.google.protobuf.FloatValueH\x0fR\x06weight\x88\x01\x01\x12 \n" + + "\traw_ratio\x18\x14 \x01(\x01H\x10R\brawRatio\x88\x01\x01\x12E\n" + + "\x04tree\x18\x18 \x01(\v2,.internal.testproto.example.v1.RecursiveNodeH\x11R\x04tree\x88\x01\x01\x128\n" + + "\n" + + "detail_any\x18\x19 \x01(\v2\x14.google.protobuf.AnyH\x12R\tdetailAny\x88\x01\x01\x12<\n" + + "\fduration_any\x18\x1a \x01(\v2\x14.google.protobuf.AnyH\x13R\vdurationAny\x88\x01\x01\x12\x1f\n" + + "\n" + + "city_alias\x18\x15 \x01(\tH\x00R\tcityAlias\x12\x19\n" + + "\acity_id\x18\x16 \x01(\x03H\x00R\x06cityId\x12Q\n" + + "\fcity_details\x18\x17 \x01(\v2,.internal.testproto.example.v1.ReportDetailsH\x00R\vcityDetails\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a=\n" + + "\x0fQuantitiesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\x05R\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" + + "\fTogglesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\bR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a9\n" + + "\vLimitsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\x04R\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\n" + + "\n" + + "\bselectorB\x0e\n" + + "\f_observed_atB\x06\n" + + "\x04_ttlB\n" + + "\n" + + "\b_payloadB\b\n" + + "\x06_itemsB\n" + + "\n" + + "\b_dynamicB\a\n" + + "\x05_noteB\b\n" + + "\x06_totalB\n" + + "\n" + + "\b_enabledB\b\n" + + "\x06_ratioB\a\n" + + "\x05_maskB\a\n" + + "\x05_blobB\x0e\n" + + "\f_small_totalB\r\n" + + "\v_uint_totalB\r\n" + + "\v_huge_totalB\t\n" + + "\a_weightB\f\n" + + "\n" + + "_raw_ratioB\a\n" + + "\x05_treeB\r\n" + + "\v_detail_anyB\x0f\n" + + "\r_duration_any\"\xf6\x10\n" + + "\x1eDescribeAdvancedShapesResponse\x12a\n" + + "\x06labels\x18\x01 \x03(\v2I.internal.testproto.example.v1.DescribeAdvancedShapesResponse.LabelsEntryR\x06labels\x12m\n" + + "\n" + + "quantities\x18\x02 \x03(\v2M.internal.testproto.example.v1.DescribeAdvancedShapesResponse.QuantitiesEntryR\n" + + "quantities\x12d\n" + + "\atoggles\x18\x03 \x03(\v2J.internal.testproto.example.v1.DescribeAdvancedShapesResponse.TogglesEntryR\atoggles\x12a\n" + + "\x06limits\x18\x0e \x03(\v2I.internal.testproto.example.v1.DescribeAdvancedShapesResponse.LimitsEntryR\x06limits\x12@\n" + + "\vobserved_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\n" + + "observedAt\x88\x01\x01\x120\n" + + "\x03ttl\x18\x05 \x01(\v2\x19.google.protobuf.DurationH\x02R\x03ttl\x88\x01\x01\x126\n" + + "\apayload\x18\x06 \x01(\v2\x17.google.protobuf.StructH\x03R\apayload\x88\x01\x01\x125\n" + + "\x05items\x18\a \x01(\v2\x1a.google.protobuf.ListValueH\x04R\x05items\x88\x01\x01\x125\n" + + "\adynamic\x18\b \x01(\v2\x16.google.protobuf.ValueH\x05R\adynamic\x88\x01\x01\x125\n" + + "\x04note\x18\t \x01(\v2\x1c.google.protobuf.StringValueH\x06R\x04note\x88\x01\x01\x126\n" + + "\x05total\x18\n" + + " \x01(\v2\x1b.google.protobuf.Int64ValueH\aR\x05total\x88\x01\x01\x129\n" + + "\aenabled\x18\v \x01(\v2\x1a.google.protobuf.BoolValueH\bR\aenabled\x88\x01\x01\x127\n" + + "\x05ratio\x18\f \x01(\v2\x1c.google.protobuf.DoubleValueH\tR\x05ratio\x88\x01\x01\x123\n" + + "\x04mask\x18\r \x01(\v2\x1a.google.protobuf.FieldMaskH\n" + + "R\x04mask\x88\x01\x01\x124\n" + + "\x04blob\x18\x0f \x01(\v2\x1b.google.protobuf.BytesValueH\vR\x04blob\x88\x01\x01\x12A\n" + + "\vsmall_total\x18\x10 \x01(\v2\x1b.google.protobuf.Int32ValueH\fR\n" + + "smallTotal\x88\x01\x01\x12@\n" + + "\n" + + "uint_total\x18\x11 \x01(\v2\x1c.google.protobuf.UInt32ValueH\rR\tuintTotal\x88\x01\x01\x12@\n" + + "\n" + + "huge_total\x18\x12 \x01(\v2\x1c.google.protobuf.UInt64ValueH\x0eR\thugeTotal\x88\x01\x01\x128\n" + + "\x06weight\x18\x13 \x01(\v2\x1b.google.protobuf.FloatValueH\x0fR\x06weight\x88\x01\x01\x12 \n" + + "\traw_ratio\x18\x14 \x01(\x01H\x10R\brawRatio\x88\x01\x01\x12E\n" + + "\x04tree\x18\x18 \x01(\v2,.internal.testproto.example.v1.RecursiveNodeH\x11R\x04tree\x88\x01\x01\x128\n" + + "\n" + + "detail_any\x18\x19 \x01(\v2\x14.google.protobuf.AnyH\x12R\tdetailAny\x88\x01\x01\x12<\n" + + "\fduration_any\x18\x1a \x01(\v2\x14.google.protobuf.AnyH\x13R\vdurationAny\x88\x01\x01\x12\x1f\n" + + "\n" + + "city_alias\x18\x15 \x01(\tH\x00R\tcityAlias\x12\x19\n" + + "\acity_id\x18\x16 \x01(\x03H\x00R\x06cityId\x12Q\n" + + "\fcity_details\x18\x17 \x01(\v2,.internal.testproto.example.v1.ReportDetailsH\x00R\vcityDetails\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a=\n" + + "\x0fQuantitiesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\x05R\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" + + "\fTogglesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\bR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a9\n" + + "\vLimitsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\x04R\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\n" + + "\n" + + "\bselectorB\x0e\n" + + "\f_observed_atB\x06\n" + + "\x04_ttlB\n" + + "\n" + + "\b_payloadB\b\n" + + "\x06_itemsB\n" + + "\n" + + "\b_dynamicB\a\n" + + "\x05_noteB\b\n" + + "\x06_totalB\n" + + "\n" + + "\b_enabledB\b\n" + + "\x06_ratioB\a\n" + + "\x05_maskB\a\n" + + "\x05_blobB\x0e\n" + + "\f_small_totalB\r\n" + + "\v_uint_totalB\r\n" + + "\v_huge_totalB\t\n" + + "\a_weightB\f\n" + + "\n" + + "_raw_ratioB\a\n" + + "\x05_treeB\r\n" + + "\v_detail_anyB\x0f\n" + + "\r_duration_any\"\x8e\x10\n" + + "\x1bDescribeScalarShapesRequest\x12\x1b\n" + + "\tbool_flag\x18\x01 \x01(\bR\bboolFlag\x12\x1d\n" + + "\n" + + "text_value\x18\x02 \x01(\tR\ttextValue\x12\x1f\n" + + "\vbytes_value\x18\x03 \x01(\fR\n" + + "bytesValue\x12\x1f\n" + + "\vint32_value\x18\x04 \x01(\x05R\n" + + "int32Value\x12!\n" + + "\fsint32_value\x18\x05 \x01(\x11R\vsint32Value\x12%\n" + + "\x0esfixed32_value\x18\x06 \x01(\x0fR\rsfixed32Value\x12!\n" + + "\fuint32_value\x18\a \x01(\rR\vuint32Value\x12#\n" + + "\rfixed32_value\x18\b \x01(\aR\ffixed32Value\x12\x1f\n" + + "\vint64_value\x18\t \x01(\x03R\n" + + "int64Value\x12!\n" + + "\fsint64_value\x18\n" + + " \x01(\x12R\vsint64Value\x12%\n" + + "\x0esfixed64_value\x18\v \x01(\x10R\rsfixed64Value\x12!\n" + + "\fuint64_value\x18\f \x01(\x04R\vuint64Value\x12#\n" + + "\rfixed64_value\x18\r \x01(\x06R\ffixed64Value\x12\x1f\n" + + "\vfloat_value\x18\x0e \x01(\x02R\n" + + "floatValue\x12!\n" + + "\fdouble_value\x18\x0f \x01(\x01R\vdoubleValue\x12C\n" + + "\x06status\x18\x10 \x01(\x0e2+.internal.testproto.example.v1.ReportStatusR\x06status\x12F\n" + + "\adetails\x18\x11 \x01(\v2,.internal.testproto.example.v1.ReportDetailsR\adetails\x12\x18\n" + + "\asamples\x18\x12 \x03(\x05R\asamples\x121\n" + + "\x12optional_bool_flag\x18\x13 \x01(\bH\x00R\x10optionalBoolFlag\x88\x01\x01\x123\n" + + "\x13optional_text_value\x18\x14 \x01(\tH\x01R\x11optionalTextValue\x88\x01\x01\x125\n" + + "\x14optional_bytes_value\x18\x15 \x01(\fH\x02R\x12optionalBytesValue\x88\x01\x01\x125\n" + + "\x14optional_int32_value\x18\x16 \x01(\x05H\x03R\x12optionalInt32Value\x88\x01\x01\x127\n" + + "\x15optional_sint32_value\x18\x17 \x01(\x11H\x04R\x13optionalSint32Value\x88\x01\x01\x12;\n" + + "\x17optional_sfixed32_value\x18\x18 \x01(\x0fH\x05R\x15optionalSfixed32Value\x88\x01\x01\x127\n" + + "\x15optional_uint32_value\x18\x19 \x01(\rH\x06R\x13optionalUint32Value\x88\x01\x01\x129\n" + + "\x16optional_fixed32_value\x18\x1a \x01(\aH\aR\x14optionalFixed32Value\x88\x01\x01\x125\n" + + "\x14optional_int64_value\x18\x1b \x01(\x03H\bR\x12optionalInt64Value\x88\x01\x01\x127\n" + + "\x15optional_sint64_value\x18\x1c \x01(\x12H\tR\x13optionalSint64Value\x88\x01\x01\x12;\n" + + "\x17optional_sfixed64_value\x18\x1d \x01(\x10H\n" + + "R\x15optionalSfixed64Value\x88\x01\x01\x127\n" + + "\x15optional_uint64_value\x18\x1e \x01(\x04H\vR\x13optionalUint64Value\x88\x01\x01\x129\n" + + "\x16optional_fixed64_value\x18\x1f \x01(\x06H\fR\x14optionalFixed64Value\x88\x01\x01\x125\n" + + "\x14optional_float_value\x18 \x01(\x02H\rR\x12optionalFloatValue\x88\x01\x01\x127\n" + + "\x15optional_double_value\x18! \x01(\x01H\x0eR\x13optionalDoubleValue\x88\x01\x01\x12Y\n" + + "\x0foptional_status\x18\" \x01(\x0e2+.internal.testproto.example.v1.ReportStatusH\x0fR\x0eoptionalStatus\x88\x01\x01B\x15\n" + + "\x13_optional_bool_flagB\x16\n" + + "\x14_optional_text_valueB\x17\n" + + "\x15_optional_bytes_valueB\x17\n" + + "\x15_optional_int32_valueB\x18\n" + + "\x16_optional_sint32_valueB\x1a\n" + + "\x18_optional_sfixed32_valueB\x18\n" + + "\x16_optional_uint32_valueB\x19\n" + + "\x17_optional_fixed32_valueB\x17\n" + + "\x15_optional_int64_valueB\x18\n" + + "\x16_optional_sint64_valueB\x1a\n" + + "\x18_optional_sfixed64_valueB\x18\n" + + "\x16_optional_uint64_valueB\x19\n" + + "\x17_optional_fixed64_valueB\x17\n" + + "\x15_optional_float_valueB\x18\n" + + "\x16_optional_double_valueB\x12\n" + + "\x10_optional_status\"\x8f\x10\n" + + "\x1cDescribeScalarShapesResponse\x12\x1b\n" + + "\tbool_flag\x18\x01 \x01(\bR\bboolFlag\x12\x1d\n" + + "\n" + + "text_value\x18\x02 \x01(\tR\ttextValue\x12\x1f\n" + + "\vbytes_value\x18\x03 \x01(\fR\n" + + "bytesValue\x12\x1f\n" + + "\vint32_value\x18\x04 \x01(\x05R\n" + + "int32Value\x12!\n" + + "\fsint32_value\x18\x05 \x01(\x11R\vsint32Value\x12%\n" + + "\x0esfixed32_value\x18\x06 \x01(\x0fR\rsfixed32Value\x12!\n" + + "\fuint32_value\x18\a \x01(\rR\vuint32Value\x12#\n" + + "\rfixed32_value\x18\b \x01(\aR\ffixed32Value\x12\x1f\n" + + "\vint64_value\x18\t \x01(\x03R\n" + + "int64Value\x12!\n" + + "\fsint64_value\x18\n" + + " \x01(\x12R\vsint64Value\x12%\n" + + "\x0esfixed64_value\x18\v \x01(\x10R\rsfixed64Value\x12!\n" + + "\fuint64_value\x18\f \x01(\x04R\vuint64Value\x12#\n" + + "\rfixed64_value\x18\r \x01(\x06R\ffixed64Value\x12\x1f\n" + + "\vfloat_value\x18\x0e \x01(\x02R\n" + + "floatValue\x12!\n" + + "\fdouble_value\x18\x0f \x01(\x01R\vdoubleValue\x12C\n" + + "\x06status\x18\x10 \x01(\x0e2+.internal.testproto.example.v1.ReportStatusR\x06status\x12F\n" + + "\adetails\x18\x11 \x01(\v2,.internal.testproto.example.v1.ReportDetailsR\adetails\x12\x18\n" + + "\asamples\x18\x12 \x03(\x05R\asamples\x121\n" + + "\x12optional_bool_flag\x18\x13 \x01(\bH\x00R\x10optionalBoolFlag\x88\x01\x01\x123\n" + + "\x13optional_text_value\x18\x14 \x01(\tH\x01R\x11optionalTextValue\x88\x01\x01\x125\n" + + "\x14optional_bytes_value\x18\x15 \x01(\fH\x02R\x12optionalBytesValue\x88\x01\x01\x125\n" + + "\x14optional_int32_value\x18\x16 \x01(\x05H\x03R\x12optionalInt32Value\x88\x01\x01\x127\n" + + "\x15optional_sint32_value\x18\x17 \x01(\x11H\x04R\x13optionalSint32Value\x88\x01\x01\x12;\n" + + "\x17optional_sfixed32_value\x18\x18 \x01(\x0fH\x05R\x15optionalSfixed32Value\x88\x01\x01\x127\n" + + "\x15optional_uint32_value\x18\x19 \x01(\rH\x06R\x13optionalUint32Value\x88\x01\x01\x129\n" + + "\x16optional_fixed32_value\x18\x1a \x01(\aH\aR\x14optionalFixed32Value\x88\x01\x01\x125\n" + + "\x14optional_int64_value\x18\x1b \x01(\x03H\bR\x12optionalInt64Value\x88\x01\x01\x127\n" + + "\x15optional_sint64_value\x18\x1c \x01(\x12H\tR\x13optionalSint64Value\x88\x01\x01\x12;\n" + + "\x17optional_sfixed64_value\x18\x1d \x01(\x10H\n" + + "R\x15optionalSfixed64Value\x88\x01\x01\x127\n" + + "\x15optional_uint64_value\x18\x1e \x01(\x04H\vR\x13optionalUint64Value\x88\x01\x01\x129\n" + + "\x16optional_fixed64_value\x18\x1f \x01(\x06H\fR\x14optionalFixed64Value\x88\x01\x01\x125\n" + + "\x14optional_float_value\x18 \x01(\x02H\rR\x12optionalFloatValue\x88\x01\x01\x127\n" + + "\x15optional_double_value\x18! \x01(\x01H\x0eR\x13optionalDoubleValue\x88\x01\x01\x12Y\n" + + "\x0foptional_status\x18\" \x01(\x0e2+.internal.testproto.example.v1.ReportStatusH\x0fR\x0eoptionalStatus\x88\x01\x01B\x15\n" + + "\x13_optional_bool_flagB\x16\n" + + "\x14_optional_text_valueB\x17\n" + + "\x15_optional_bytes_valueB\x17\n" + + "\x15_optional_int32_valueB\x18\n" + + "\x16_optional_sint32_valueB\x1a\n" + + "\x18_optional_sfixed32_valueB\x18\n" + + "\x16_optional_uint32_valueB\x19\n" + + "\x17_optional_fixed32_valueB\x17\n" + + "\x15_optional_int64_valueB\x18\n" + + "\x16_optional_sint64_valueB\x1a\n" + + "\x18_optional_sfixed64_valueB\x18\n" + + "\x16_optional_uint64_valueB\x19\n" + + "\x17_optional_fixed64_valueB\x17\n" + + "\x15_optional_float_valueB\x18\n" + + "\x16_optional_double_valueB\x12\n" + + "\x10_optional_status\"a\n" + + "\rReportDetails\x12\x14\n" + + "\x05label\x18\x01 \x01(\tR\x05label::\xe2\xb7,6\n" + + "\x0eReport Details\x12$Nested report configuration options.\"\xc0\x01\n" + + "\rRecursiveNode\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12G\n" + + "\x05child\x18\x02 \x01(\v2,.internal.testproto.example.v1.RecursiveNodeH\x00R\x05child\x88\x01\x01\x12H\n" + + "\bchildren\x18\x03 \x03(\v2,.internal.testproto.example.v1.RecursiveNodeR\bchildrenB\b\n" + + "\x06_child*\xd8\x01\n" + + "\fReportStatus\x12\x1e\n" + + "\x12REPORT_STATUS_NONE\x10\x00\x1a\x06\xf2\xb7,\x02\x10\x01\x12:\n" + + "\x10REPORT_STATUS_OK\x10\x01\x1a$\xf2\xb7, \n" + + "\x1eReport completed successfully.\x129\n" + + "\x14REPORT_STATUS_FAILED\x10\x02\x1a\x1f\xf2\xb7,\x1b\n" + + "\x19Report generation failed.\x1a1\xea\xb7,-\n" + + "\rReport Status\x12\x1cCurrent state of the report.2\xfc\x06\n" + + "\n" + + "ExampleAPI\x12\xa9\x01\n" + + "\fCreateReport\x122.internal.testproto.example.v1.CreateReportRequest\x1a3.internal.testproto.example.v1.CreateReportResponse\"0ҷ,,\x12\rCreate report\x1a\x1bCreate a report for a city.\x12{\n" + + "\x04Ping\x12*.internal.testproto.example.v1.PingRequest\x1a+.internal.testproto.example.v1.PingResponse\"\x1aҷ,\x16\n" + + "\x06Health\x12\fHealth check\x12\xe3\x01\n" + + "\x16DescribeAdvancedShapes\x12<.internal.testproto.example.v1.DescribeAdvancedShapesRequest\x1a=.internal.testproto.example.v1.DescribeAdvancedShapesResponse\"Lҷ,H\x12\x18Describe advanced shapes\x1a,Exercise maps and well-known protobuf types.\x12\xd4\x01\n" + + "\x14DescribeScalarShapes\x12:.internal.testproto.example.v1.DescribeScalarShapesRequest\x1a;.internal.testproto.example.v1.DescribeScalarShapesResponse\"Cҷ,?\x12\x16Describe scalar shapes\x1a%Exercise plain protobuf scalar kinds.\x12y\n" + + "\vHiddenThing\x121.internal.testproto.example.v1.HiddenThingRequest\x1a2.internal.testproto.example.v1.HiddenThingResponse\"\x03\x88\x02\x01\x1a\rʷ,\t\n" + + "\aexampleBNZLgithub.com/easyp-tech/protoc-gen-mcp/internal/testproto/example/v1;examplev1b\x06proto3" var ( file_internal_testproto_example_v1_example_proto_rawDescOnce sync.Once - file_internal_testproto_example_v1_example_proto_rawDescData = file_internal_testproto_example_v1_example_proto_rawDesc + file_internal_testproto_example_v1_example_proto_rawDescData []byte ) func file_internal_testproto_example_v1_example_proto_rawDescGZIP() []byte { file_internal_testproto_example_v1_example_proto_rawDescOnce.Do(func() { - file_internal_testproto_example_v1_example_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_testproto_example_v1_example_proto_rawDescData) + file_internal_testproto_example_v1_example_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_internal_testproto_example_v1_example_proto_rawDesc), len(file_internal_testproto_example_v1_example_proto_rawDesc))) }) return file_internal_testproto_example_v1_example_proto_rawDescData } @@ -2682,152 +2287,6 @@ func file_internal_testproto_example_v1_example_proto_init() { if File_internal_testproto_example_v1_example_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_internal_testproto_example_v1_example_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*CreateReportRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*CreateReportResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*HiddenThingRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*HiddenThingResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*PingRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[5].Exporter = func(v any, i int) any { - switch v := v.(*PingResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[6].Exporter = func(v any, i int) any { - switch v := v.(*DescribeAdvancedShapesRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[7].Exporter = func(v any, i int) any { - switch v := v.(*DescribeAdvancedShapesResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[8].Exporter = func(v any, i int) any { - switch v := v.(*DescribeScalarShapesRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[9].Exporter = func(v any, i int) any { - switch v := v.(*DescribeScalarShapesResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[10].Exporter = func(v any, i int) any { - switch v := v.(*ReportDetails); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_internal_testproto_example_v1_example_proto_msgTypes[11].Exporter = func(v any, i int) any { - switch v := v.(*RecursiveNode); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } file_internal_testproto_example_v1_example_proto_msgTypes[0].OneofWrappers = []any{} file_internal_testproto_example_v1_example_proto_msgTypes[6].OneofWrappers = []any{ (*DescribeAdvancedShapesRequest_CityAlias)(nil), @@ -2846,7 +2305,7 @@ func file_internal_testproto_example_v1_example_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_internal_testproto_example_v1_example_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_testproto_example_v1_example_proto_rawDesc), len(file_internal_testproto_example_v1_example_proto_rawDesc)), NumEnums: 1, NumMessages: 20, NumExtensions: 0, @@ -2858,7 +2317,6 @@ func file_internal_testproto_example_v1_example_proto_init() { MessageInfos: file_internal_testproto_example_v1_example_proto_msgTypes, }.Build() File_internal_testproto_example_v1_example_proto = out.File - file_internal_testproto_example_v1_example_proto_rawDesc = nil file_internal_testproto_example_v1_example_proto_goTypes = nil file_internal_testproto_example_v1_example_proto_depIdxs = nil } diff --git a/internal/testproto/example/v1/example_mcp.py b/internal/testproto/example/v1/example_mcp.py new file mode 100644 index 0000000..2e03300 --- /dev/null +++ b/internal/testproto/example/v1/example_mcp.py @@ -0,0 +1,1167 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +# source: internal/testproto/example/v1/example.proto +from __future__ import annotations + +import enum +import inspect +import json +import weakref +from dataclasses import dataclass, field +from typing import Any, Awaitable, Protocol, TypeAlias + +import jsonschema +import mcp.server.lowlevel +import mcp.server.session +import mcp.shared.exceptions +import mcp.types +from google.protobuf import any_pb2, duration_pb2, empty_pb2, field_mask_pb2, json_format, struct_pb2, timestamp_pb2, wrappers_pb2 +try: + from . import example_pb2 +except ImportError: + import example_pb2 + +ToolRequestContext = mcp.server.session.ServerSession + +class _UnsetType: + __slots__ = () + + def __repr__(self) -> str: + return "UNSET" + +UNSET = _UnsetType() + +JSONValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None +ProtoAny: TypeAlias = dict[str, Any] +Timestamp: TypeAlias = str +Duration: TypeAlias = str +FieldMask: TypeAlias = str +Struct: TypeAlias = dict[str, Any] +Value: TypeAlias = JSONValue +ListValue: TypeAlias = list[Any] +BoolValue: TypeAlias = bool +StringValue: TypeAlias = str +BytesValue: TypeAlias = bytes +Int32Value: TypeAlias = int +UInt32Value: TypeAlias = int +Int64Value: TypeAlias = int +UInt64Value: TypeAlias = int +FloatValue: TypeAlias = float +DoubleValue: TypeAlias = float + +@dataclass(slots=True) +class Empty: + pass + +@dataclass(slots=True) +class ReportDetails: + label: str + +@dataclass(slots=True) +class CreateReportRequest: + city: str + count: int + details: ReportDetails + units: str | _UnsetType = UNSET + labels: list[str] = field(default_factory=list) + +class ReportStatus(enum.IntEnum): + REPORT_STATUS_OK = 1 + REPORT_STATUS_FAILED = 2 + +@dataclass(slots=True) +class CreateReportResponse: + report_id: str + total_count: int + status: ReportStatus + details: ReportDetails + warnings: list[str] = field(default_factory=list) + +@dataclass(slots=True) +class PingRequest: + pass + +@dataclass(slots=True) +class PingResponse: + ack: Empty + +@dataclass(slots=True) +class RecursiveNode: + name: str + child: RecursiveNode | _UnsetType = UNSET + children: list[RecursiveNode] = field(default_factory=list) + +@dataclass(slots=True) +class DescribeAdvancedShapesRequestSelectorVariant: + pass + +@dataclass(slots=True) +class DescribeAdvancedShapesRequestSelectorCityAliasVariant(DescribeAdvancedShapesRequestSelectorVariant): + city_alias: str + +@dataclass(slots=True) +class DescribeAdvancedShapesRequestSelectorCityIdVariant(DescribeAdvancedShapesRequestSelectorVariant): + city_id: int + +@dataclass(slots=True) +class DescribeAdvancedShapesRequestSelectorCityDetailsVariant(DescribeAdvancedShapesRequestSelectorVariant): + city_details: ReportDetails + +@dataclass(slots=True) +class DescribeAdvancedShapesRequest: + labels: dict[str, str] = field(default_factory=dict) + quantities: dict[int, str] = field(default_factory=dict) + toggles: dict[bool, str] = field(default_factory=dict) + limits: dict[int, str] = field(default_factory=dict) + observed_at: Timestamp | _UnsetType = UNSET + ttl: Duration | _UnsetType = UNSET + payload: Struct | _UnsetType = UNSET + items: ListValue | _UnsetType = UNSET + dynamic: Value | _UnsetType = UNSET + note: StringValue | _UnsetType = UNSET + total: Int64Value | _UnsetType = UNSET + enabled: BoolValue | _UnsetType = UNSET + ratio: DoubleValue | _UnsetType = UNSET + mask: FieldMask | _UnsetType = UNSET + blob: BytesValue | _UnsetType = UNSET + small_total: Int32Value | _UnsetType = UNSET + uint_total: UInt32Value | _UnsetType = UNSET + huge_total: UInt64Value | _UnsetType = UNSET + weight: FloatValue | _UnsetType = UNSET + raw_ratio: float | _UnsetType = UNSET + tree: RecursiveNode | _UnsetType = UNSET + detail_any: ProtoAny | _UnsetType = UNSET + duration_any: ProtoAny | _UnsetType = UNSET + selector: DescribeAdvancedShapesRequestSelectorVariant | _UnsetType = UNSET + +@dataclass(slots=True) +class DescribeAdvancedShapesResponseSelectorVariant: + pass + +@dataclass(slots=True) +class DescribeAdvancedShapesResponseSelectorCityAliasVariant(DescribeAdvancedShapesResponseSelectorVariant): + city_alias: str + +@dataclass(slots=True) +class DescribeAdvancedShapesResponseSelectorCityIdVariant(DescribeAdvancedShapesResponseSelectorVariant): + city_id: int + +@dataclass(slots=True) +class DescribeAdvancedShapesResponseSelectorCityDetailsVariant(DescribeAdvancedShapesResponseSelectorVariant): + city_details: ReportDetails + +@dataclass(slots=True) +class DescribeAdvancedShapesResponse: + labels: dict[str, str] = field(default_factory=dict) + quantities: dict[int, str] = field(default_factory=dict) + toggles: dict[bool, str] = field(default_factory=dict) + limits: dict[int, str] = field(default_factory=dict) + observed_at: Timestamp | _UnsetType = UNSET + ttl: Duration | _UnsetType = UNSET + payload: Struct | _UnsetType = UNSET + items: ListValue | _UnsetType = UNSET + dynamic: Value | _UnsetType = UNSET + note: StringValue | _UnsetType = UNSET + total: Int64Value | _UnsetType = UNSET + enabled: BoolValue | _UnsetType = UNSET + ratio: DoubleValue | _UnsetType = UNSET + mask: FieldMask | _UnsetType = UNSET + blob: BytesValue | _UnsetType = UNSET + small_total: Int32Value | _UnsetType = UNSET + uint_total: UInt32Value | _UnsetType = UNSET + huge_total: UInt64Value | _UnsetType = UNSET + weight: FloatValue | _UnsetType = UNSET + raw_ratio: float | _UnsetType = UNSET + tree: RecursiveNode | _UnsetType = UNSET + detail_any: ProtoAny | _UnsetType = UNSET + duration_any: ProtoAny | _UnsetType = UNSET + selector: DescribeAdvancedShapesResponseSelectorVariant | _UnsetType = UNSET + +@dataclass(slots=True) +class DescribeScalarShapesRequest: + bool_flag: bool + text_value: str + bytes_value: bytes + int32_value: int + sint32_value: int + sfixed32_value: int + uint32_value: int + fixed32_value: int + int64_value: int + sint64_value: int + sfixed64_value: int + uint64_value: int + fixed64_value: int + float_value: float + double_value: float + status: ReportStatus + details: ReportDetails + samples: list[int] = field(default_factory=list) + optional_bool_flag: bool | _UnsetType = UNSET + optional_text_value: str | _UnsetType = UNSET + optional_bytes_value: bytes | _UnsetType = UNSET + optional_int32_value: int | _UnsetType = UNSET + optional_sint32_value: int | _UnsetType = UNSET + optional_sfixed32_value: int | _UnsetType = UNSET + optional_uint32_value: int | _UnsetType = UNSET + optional_fixed32_value: int | _UnsetType = UNSET + optional_int64_value: int | _UnsetType = UNSET + optional_sint64_value: int | _UnsetType = UNSET + optional_sfixed64_value: int | _UnsetType = UNSET + optional_uint64_value: int | _UnsetType = UNSET + optional_fixed64_value: int | _UnsetType = UNSET + optional_float_value: float | _UnsetType = UNSET + optional_double_value: float | _UnsetType = UNSET + optional_status: ReportStatus | _UnsetType = UNSET + +@dataclass(slots=True) +class DescribeScalarShapesResponse: + bool_flag: bool + text_value: str + bytes_value: bytes + int32_value: int + sint32_value: int + sfixed32_value: int + uint32_value: int + fixed32_value: int + int64_value: int + sint64_value: int + sfixed64_value: int + uint64_value: int + fixed64_value: int + float_value: float + double_value: float + status: ReportStatus + details: ReportDetails + samples: list[int] = field(default_factory=list) + optional_bool_flag: bool | _UnsetType = UNSET + optional_text_value: str | _UnsetType = UNSET + optional_bytes_value: bytes | _UnsetType = UNSET + optional_int32_value: int | _UnsetType = UNSET + optional_sint32_value: int | _UnsetType = UNSET + optional_sfixed32_value: int | _UnsetType = UNSET + optional_uint32_value: int | _UnsetType = UNSET + optional_fixed32_value: int | _UnsetType = UNSET + optional_int64_value: int | _UnsetType = UNSET + optional_sint64_value: int | _UnsetType = UNSET + optional_sfixed64_value: int | _UnsetType = UNSET + optional_uint64_value: int | _UnsetType = UNSET + optional_fixed64_value: int | _UnsetType = UNSET + optional_float_value: float | _UnsetType = UNSET + optional_double_value: float | _UnsetType = UNSET + optional_status: ReportStatus | _UnsetType = UNSET + +@dataclass(slots=True) +class HiddenThingRequest: + name: str + +@dataclass(slots=True) +class HiddenThingResponse: + pass + +@dataclass(frozen=True) +class _RegisteredTool: + name: str + title: str + description: str + input_schema_json: str + output_schema_json: str + request_type: type[Any] + response_type: type[Any] + from_pb: Any + to_pb: Any + handler: Any + annotations: dict[str, Any] | None + icons: list[dict[str, Any]] | None + +class _ServerToolRegistry: + def __init__(self, server: mcp.server.lowlevel.Server) -> None: + self.server = server + self.tools: dict[str, _RegisteredTool] = {} + self.reserved_tool_names = _get_reserved_tool_names(server) + self.handlers_installed = False + self.previous_list_tools: Any | None = None + self.previous_call_tool: Any | None = None + + def add_tool(self, tool: _RegisteredTool) -> None: + if tool.name in self.reserved_tool_names: + raise ValueError(f"duplicate tool registration: {tool.name}") + self.tools[tool.name] = tool + self.reserved_tool_names.add(tool.name) + + def list_tools(self) -> list[mcp.types.Tool]: + return [_build_tool(tool) for tool in self.tools.values()] + + async def call_tool(self, name: str, arguments: dict[str, Any] | None, context: ToolRequestContext | None = None) -> mcp.types.CallToolResult: + return await _dispatch_call(self, name, arguments or {}, context) + +_SERVER_REGISTRIES: weakref.WeakKeyDictionary[mcp.server.lowlevel.Server, _ServerToolRegistry] = weakref.WeakKeyDictionary() + +def _load_schema(schema_json: str) -> dict[str, Any]: + return json.loads(schema_json) + +def _protojson_to_message(arguments: dict[str, Any], message: Any) -> Any: + json_format.ParseDict(arguments, message) + return message + +def _normalize_tool_segment(segment: str | None) -> str: + if segment is None: + return "" + parts = [part for part in segment.strip().replace(".", "_").split("_") if part] + return "_".join(parts) + +def _normalize_namespace(namespace: str | None, default: str) -> str: + if namespace is None: + namespace = default + return _normalize_tool_segment(namespace) + +def _tool_name(namespace: str, method_name: str) -> str: + namespace = _normalize_tool_segment(namespace) + method_name = _normalize_tool_segment(method_name) + if namespace == '': + return method_name + if method_name == '': + return namespace + return f"{namespace}_{method_name}" + +def _get_registry(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _SERVER_REGISTRIES.get(server) + if registry is None: + registry = _ServerToolRegistry(server) + _SERVER_REGISTRIES[server] = registry + return registry + +_RESERVED_TOOL_NAMES_ATTR = "_protoc_gen_mcp_reserved_tool_names" + +def _get_reserved_tool_names(server: mcp.server.lowlevel.Server) -> set[str]: + reserved = getattr(server, _RESERVED_TOOL_NAMES_ATTR, None) + if reserved is None: + reserved = set() + setattr(server, _RESERVED_TOOL_NAMES_ATTR, reserved) + return reserved + +def _build_list_tools_request(request: Any) -> mcp.types.ListToolsRequest: + if isinstance(request, mcp.types.ListToolsRequest): + return request + return mcp.types.ListToolsRequest() + +def _merge_tools(previous: list[mcp.types.Tool], current: list[mcp.types.Tool]) -> list[mcp.types.Tool]: + merged: list[mcp.types.Tool] = [] + names: set[str] = set() + for tool in previous: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + for tool in current: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + return merged + +def _install_server_handlers(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _get_registry(server) + if registry.handlers_installed: + return registry + registry.previous_list_tools = server.request_handlers.get(mcp.types.ListToolsRequest) + registry.previous_call_tool = server.request_handlers.get(mcp.types.CallToolRequest) + + async def _list_tools(request: Any) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + list_request = _build_list_tools_request(request) + previous_tools: list[mcp.types.Tool] = [] + meta: dict[str, Any] | None = None + if current is not None: + if current.previous_list_tools is not None: + previous_result = await current.previous_list_tools(list_request) + if not isinstance(previous_result.root, mcp.types.ListToolsResult): + return previous_result + if getattr(list_request.params, "cursor", None) is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + if previous_result.root.nextCursor is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + meta = getattr(previous_result.root, "meta", None) + previous_tools = list(previous_result.root.tools) + current_tools = [] if current is None else current.list_tools() + tools = _merge_tools(previous_tools, current_tools) + server._tool_cache.clear() + for tool in tools: + server._tool_cache[tool.name] = tool + return mcp.types.ServerResult(mcp.types.ListToolsResult(tools=tools, _meta=meta)) + + async def _call_tool(request: mcp.types.CallToolRequest) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + if current is None: + return mcp.types.ServerResult(_tool_error_result("tool registry is not installed")) + if request.params.name not in current.tools: + if current.previous_call_tool is not None: + return await current.previous_call_tool(request) + _invalid_params_error(request.params.name, "unknown tool") + if current.previous_call_tool is not None: + await server.request_handlers[mcp.types.ListToolsRequest](mcp.types.ListToolsRequest()) + result = await current.call_tool(request.params.name, request.params.arguments or {}, server.request_context.session) + return mcp.types.ServerResult(result) + + server.request_handlers[mcp.types.ListToolsRequest] = _list_tools + server.request_handlers[mcp.types.CallToolRequest] = _call_tool + + registry.handlers_installed = True + return registry + +def _tool_annotations(raw: dict[str, Any] | None) -> mcp.types.ToolAnnotations | None: + if raw is None: + return None + return mcp.types.ToolAnnotations(**raw) + +def _tool_error_result(message: str) -> mcp.types.CallToolResult: + return mcp.types.CallToolResult( + content=[mcp.types.TextContent(type="text", text=message)], + isError=True, + ) + +def _invalid_params_error(tool_name: str, error: Any) -> mcp.shared.exceptions.McpError: + raise mcp.shared.exceptions.McpError( + mcp.types.ErrorData( + code=mcp.types.INVALID_PARAMS, + message=f"invalid arguments for tool {tool_name!r}: {error}", + ) + ) + +def _build_tool(tool: _RegisteredTool) -> Any: + return mcp.types.Tool( + name=tool.name, + title=tool.title, + description=tool.description, + inputSchema=_load_schema(tool.input_schema_json), + outputSchema=_load_schema(tool.output_schema_json), + annotations=_tool_annotations(tool.annotations), + icons=tool.icons, + ) + +async def _maybe_await(result: Any) -> Any: + if inspect.isawaitable(result): + return await result + return result + +def _message_to_json(message: Any) -> str: + kwargs = {"preserving_proto_field_name": False} + try: + return json_format.MessageToJson(message, always_print_fields_with_no_presence=True, **kwargs) + except TypeError: + return json_format.MessageToJson(message, including_default_value_fields=True, **kwargs) + +async def _dispatch_call(registry: _ServerToolRegistry, name: str, arguments: dict[str, Any], context: ToolRequestContext | None) -> mcp.types.CallToolResult: + tool = registry.tools.get(name) + if tool is None: + _invalid_params_error(name, "unknown tool") + try: + jsonschema.validate(arguments, _load_schema(tool.input_schema_json)) + except jsonschema.ValidationError as error: + _invalid_params_error(name, error) + try: + request_pb = _protojson_to_message(arguments, tool.request_type()) + request_dc = tool.from_pb(request_pb) + except Exception as error: + _invalid_params_error(name, error) + try: + response_dc = await _maybe_await(tool.handler(context, request_dc)) + except Exception as error: + return _tool_error_result(str(error)) + try: + if response_dc is None: + response_pb = tool.response_type() + else: + response_pb = tool.to_pb(response_dc) + payload = json.loads(_message_to_json(response_pb)) + except Exception as error: + raise RuntimeError(f"mcpruntime: marshal output for tool {name!r}: {error}") from error + text = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + content = [mcp.types.TextContent(type="text", text=text)] + try: + jsonschema.validate(payload, _load_schema(tool.output_schema_json)) + except jsonschema.ValidationError as error: + raise RuntimeError(f"mcpruntime: validate output for tool {name!r}: {error}") from error + return mcp.types.CallToolResult(content=content, structuredContent=payload) + +def _enum_from_pb(enum_type: type[enum.IntEnum], value: int) -> enum.IntEnum: + return enum_type(value) + +def _json_to_message(value: Any, message: Any) -> Any: + json_format.Parse(json.dumps(value), message) + return message + +def _from_pb_any(message: any_pb2.Any) -> ProtoAny: + return json.loads(_message_to_json(message)) + +def _to_pb_any(value: ProtoAny) -> any_pb2.Any: + return _json_to_message(value, any_pb2.Any()) + +def _from_pb_timestamp(message: timestamp_pb2.Timestamp) -> Timestamp: + return json.loads(_message_to_json(message)) + +def _to_pb_timestamp(value: Timestamp) -> timestamp_pb2.Timestamp: + return _json_to_message(value, timestamp_pb2.Timestamp()) + +def _from_pb_duration(message: duration_pb2.Duration) -> Duration: + return json.loads(_message_to_json(message)) + +def _to_pb_duration(value: Duration) -> duration_pb2.Duration: + return _json_to_message(value, duration_pb2.Duration()) + +def _from_pb_field_mask(message: field_mask_pb2.FieldMask) -> FieldMask: + return json.loads(_message_to_json(message)) + +def _to_pb_field_mask(value: FieldMask) -> field_mask_pb2.FieldMask: + return _json_to_message(value, field_mask_pb2.FieldMask()) + +def _from_pb_struct(message: struct_pb2.Struct) -> Struct: + return json.loads(_message_to_json(message)) + +def _to_pb_struct(value: Struct) -> struct_pb2.Struct: + return _json_to_message(value, struct_pb2.Struct()) + +def _from_pb_list_value(message: struct_pb2.ListValue) -> ListValue: + return json.loads(_message_to_json(message)) + +def _to_pb_list_value(value: ListValue) -> struct_pb2.ListValue: + return _json_to_message(value, struct_pb2.ListValue()) + +def _from_pb_value(message: struct_pb2.Value) -> Value: + return json.loads(_message_to_json(message)) + +def _to_pb_value(value: Value) -> struct_pb2.Value: + return _json_to_message(value, struct_pb2.Value()) + +def _from_pb_empty(message: empty_pb2.Empty) -> Empty: + return Empty() + +def _to_pb_empty(value: Empty) -> empty_pb2.Empty: + return empty_pb2.Empty() + +def _from_pb_bool_value(message: wrappers_pb2.BoolValue) -> BoolValue: + return message.value + +def _to_pb_bool_value(value: BoolValue) -> wrappers_pb2.BoolValue: + return wrappers_pb2.BoolValue(value=value) + +def _from_pb_string_value(message: wrappers_pb2.StringValue) -> StringValue: + return message.value + +def _to_pb_string_value(value: StringValue) -> wrappers_pb2.StringValue: + return wrappers_pb2.StringValue(value=value) + +def _from_pb_bytes_value(message: wrappers_pb2.BytesValue) -> BytesValue: + return message.value + +def _to_pb_bytes_value(value: BytesValue) -> wrappers_pb2.BytesValue: + return wrappers_pb2.BytesValue(value=value) + +def _from_pb_int32_value(message: wrappers_pb2.Int32Value) -> Int32Value: + return message.value + +def _to_pb_int32_value(value: Int32Value) -> wrappers_pb2.Int32Value: + return wrappers_pb2.Int32Value(value=value) + +def _from_pb_uint32_value(message: wrappers_pb2.UInt32Value) -> UInt32Value: + return message.value + +def _to_pb_uint32_value(value: UInt32Value) -> wrappers_pb2.UInt32Value: + return wrappers_pb2.UInt32Value(value=value) + +def _from_pb_int64_value(message: wrappers_pb2.Int64Value) -> Int64Value: + return message.value + +def _to_pb_int64_value(value: Int64Value) -> wrappers_pb2.Int64Value: + return wrappers_pb2.Int64Value(value=value) + +def _from_pb_uint64_value(message: wrappers_pb2.UInt64Value) -> UInt64Value: + return message.value + +def _to_pb_uint64_value(value: UInt64Value) -> wrappers_pb2.UInt64Value: + return wrappers_pb2.UInt64Value(value=value) + +def _from_pb_float_value(message: wrappers_pb2.FloatValue) -> FloatValue: + return message.value + +def _to_pb_float_value(value: FloatValue) -> wrappers_pb2.FloatValue: + return wrappers_pb2.FloatValue(value=value) + +def _from_pb_double_value(message: wrappers_pb2.DoubleValue) -> DoubleValue: + return message.value + +def _to_pb_double_value(value: DoubleValue) -> wrappers_pb2.DoubleValue: + return wrappers_pb2.DoubleValue(value=value) + +def _from_pb_report_details(message: example_pb2.ReportDetails) -> ReportDetails: + return ReportDetails( + label=message.label, + ) + +def _to_pb_report_details(value: ReportDetails) -> example_pb2.ReportDetails: + message = example_pb2.ReportDetails() + message.label = value.label + return message + +def _from_pb_create_report_request(message: example_pb2.CreateReportRequest) -> CreateReportRequest: + return CreateReportRequest( + city=message.city, + count=message.count, + details=_from_pb_report_details(message.details), + units=message.units if message.HasField("units") else UNSET, + labels=list(message.labels), + ) + +def _to_pb_create_report_request(value: CreateReportRequest) -> example_pb2.CreateReportRequest: + message = example_pb2.CreateReportRequest() + message.city = value.city + message.count = value.count + message.details.CopyFrom(_to_pb_report_details(value.details)) + if value.units is not UNSET: + message.units = value.units + message.labels.extend(value.labels) + return message + +def _from_pb_create_report_response(message: example_pb2.CreateReportResponse) -> CreateReportResponse: + return CreateReportResponse( + report_id=message.report_id, + total_count=message.total_count, + status=_enum_from_pb(ReportStatus, message.status), + details=_from_pb_report_details(message.details), + warnings=list(message.warnings), + ) + +def _to_pb_create_report_response(value: CreateReportResponse) -> example_pb2.CreateReportResponse: + message = example_pb2.CreateReportResponse() + message.report_id = value.report_id + message.total_count = value.total_count + message.status = int(value.status) + message.details.CopyFrom(_to_pb_report_details(value.details)) + message.warnings.extend(value.warnings) + return message + +def _from_pb_ping_request(message: example_pb2.PingRequest) -> PingRequest: + return PingRequest( + ) + +def _to_pb_ping_request(value: PingRequest) -> example_pb2.PingRequest: + message = example_pb2.PingRequest() + return message + +def _from_pb_ping_response(message: example_pb2.PingResponse) -> PingResponse: + return PingResponse( + ack=_from_pb_empty(message.ack), + ) + +def _to_pb_ping_response(value: PingResponse) -> example_pb2.PingResponse: + message = example_pb2.PingResponse() + message.ack.CopyFrom(_to_pb_empty(value.ack)) + return message + +def _from_pb_recursive_node(message: example_pb2.RecursiveNode) -> RecursiveNode: + return RecursiveNode( + name=message.name, + child=_from_pb_recursive_node(message.child) if message.HasField("child") else UNSET, + children=[_from_pb_recursive_node(item) for item in message.children], + ) + +def _to_pb_recursive_node(value: RecursiveNode) -> example_pb2.RecursiveNode: + message = example_pb2.RecursiveNode() + message.name = value.name + if value.child is not UNSET: + message.child.CopyFrom(_to_pb_recursive_node(value.child)) + message.children.extend(_to_pb_recursive_node(item) for item in value.children) + return message + +def _from_pb_describe_advanced_shapes_request(message: example_pb2.DescribeAdvancedShapesRequest) -> DescribeAdvancedShapesRequest: + selector = UNSET + selector_case = message.WhichOneof("selector") + if selector_case == "city_alias": + selector = DescribeAdvancedShapesRequestSelectorCityAliasVariant(city_alias=message.city_alias) + elif selector_case == "city_id": + selector = DescribeAdvancedShapesRequestSelectorCityIdVariant(city_id=message.city_id) + elif selector_case == "city_details": + selector = DescribeAdvancedShapesRequestSelectorCityDetailsVariant(city_details=_from_pb_report_details(message.city_details)) + return DescribeAdvancedShapesRequest( + labels=dict(message.labels), + quantities=dict(message.quantities), + toggles=dict(message.toggles), + limits=dict(message.limits), + observed_at=_from_pb_timestamp(message.observed_at) if message.HasField("observed_at") else UNSET, + ttl=_from_pb_duration(message.ttl) if message.HasField("ttl") else UNSET, + payload=_from_pb_struct(message.payload) if message.HasField("payload") else UNSET, + items=_from_pb_list_value(message.items) if message.HasField("items") else UNSET, + dynamic=_from_pb_value(message.dynamic) if message.HasField("dynamic") else UNSET, + note=_from_pb_string_value(message.note) if message.HasField("note") else UNSET, + total=_from_pb_int64_value(message.total) if message.HasField("total") else UNSET, + enabled=_from_pb_bool_value(message.enabled) if message.HasField("enabled") else UNSET, + ratio=_from_pb_double_value(message.ratio) if message.HasField("ratio") else UNSET, + mask=_from_pb_field_mask(message.mask) if message.HasField("mask") else UNSET, + blob=_from_pb_bytes_value(message.blob) if message.HasField("blob") else UNSET, + small_total=_from_pb_int32_value(message.small_total) if message.HasField("small_total") else UNSET, + uint_total=_from_pb_uint32_value(message.uint_total) if message.HasField("uint_total") else UNSET, + huge_total=_from_pb_uint64_value(message.huge_total) if message.HasField("huge_total") else UNSET, + weight=_from_pb_float_value(message.weight) if message.HasField("weight") else UNSET, + raw_ratio=message.raw_ratio if message.HasField("raw_ratio") else UNSET, + tree=_from_pb_recursive_node(message.tree) if message.HasField("tree") else UNSET, + detail_any=_from_pb_any(message.detail_any) if message.HasField("detail_any") else UNSET, + duration_any=_from_pb_any(message.duration_any) if message.HasField("duration_any") else UNSET, + selector=selector, + ) + +def _to_pb_describe_advanced_shapes_request(value: DescribeAdvancedShapesRequest) -> example_pb2.DescribeAdvancedShapesRequest: + message = example_pb2.DescribeAdvancedShapesRequest() + message.labels.update(value.labels) + message.quantities.update(value.quantities) + message.toggles.update(value.toggles) + message.limits.update(value.limits) + if value.observed_at is not UNSET: + message.observed_at.CopyFrom(_to_pb_timestamp(value.observed_at)) + if value.ttl is not UNSET: + message.ttl.CopyFrom(_to_pb_duration(value.ttl)) + if value.payload is not UNSET: + message.payload.CopyFrom(_to_pb_struct(value.payload)) + if value.items is not UNSET: + message.items.CopyFrom(_to_pb_list_value(value.items)) + if value.dynamic is not UNSET: + message.dynamic.CopyFrom(_to_pb_value(value.dynamic)) + if value.note is not UNSET: + message.note.CopyFrom(_to_pb_string_value(value.note)) + if value.total is not UNSET: + message.total.CopyFrom(_to_pb_int64_value(value.total)) + if value.enabled is not UNSET: + message.enabled.CopyFrom(_to_pb_bool_value(value.enabled)) + if value.ratio is not UNSET: + message.ratio.CopyFrom(_to_pb_double_value(value.ratio)) + if value.mask is not UNSET: + message.mask.CopyFrom(_to_pb_field_mask(value.mask)) + if value.blob is not UNSET: + message.blob.CopyFrom(_to_pb_bytes_value(value.blob)) + if value.small_total is not UNSET: + message.small_total.CopyFrom(_to_pb_int32_value(value.small_total)) + if value.uint_total is not UNSET: + message.uint_total.CopyFrom(_to_pb_uint32_value(value.uint_total)) + if value.huge_total is not UNSET: + message.huge_total.CopyFrom(_to_pb_uint64_value(value.huge_total)) + if value.weight is not UNSET: + message.weight.CopyFrom(_to_pb_float_value(value.weight)) + if value.raw_ratio is not UNSET: + message.raw_ratio = value.raw_ratio + if value.tree is not UNSET: + message.tree.CopyFrom(_to_pb_recursive_node(value.tree)) + if value.detail_any is not UNSET: + message.detail_any.CopyFrom(_to_pb_any(value.detail_any)) + if value.duration_any is not UNSET: + message.duration_any.CopyFrom(_to_pb_any(value.duration_any)) + if value.selector is not UNSET: + if isinstance(value.selector, DescribeAdvancedShapesRequestSelectorCityAliasVariant): + message.city_alias = value.selector.city_alias + elif isinstance(value.selector, DescribeAdvancedShapesRequestSelectorCityIdVariant): + message.city_id = value.selector.city_id + elif isinstance(value.selector, DescribeAdvancedShapesRequestSelectorCityDetailsVariant): + message.city_details.CopyFrom(_to_pb_report_details(value.selector.city_details)) + else: + raise TypeError("unsupported DescribeAdvancedShapesRequestSelectorVariant variant: " + type(value.selector).__name__) + + return message + +def _from_pb_describe_advanced_shapes_response(message: example_pb2.DescribeAdvancedShapesResponse) -> DescribeAdvancedShapesResponse: + selector = UNSET + selector_case = message.WhichOneof("selector") + if selector_case == "city_alias": + selector = DescribeAdvancedShapesResponseSelectorCityAliasVariant(city_alias=message.city_alias) + elif selector_case == "city_id": + selector = DescribeAdvancedShapesResponseSelectorCityIdVariant(city_id=message.city_id) + elif selector_case == "city_details": + selector = DescribeAdvancedShapesResponseSelectorCityDetailsVariant(city_details=_from_pb_report_details(message.city_details)) + return DescribeAdvancedShapesResponse( + labels=dict(message.labels), + quantities=dict(message.quantities), + toggles=dict(message.toggles), + limits=dict(message.limits), + observed_at=_from_pb_timestamp(message.observed_at) if message.HasField("observed_at") else UNSET, + ttl=_from_pb_duration(message.ttl) if message.HasField("ttl") else UNSET, + payload=_from_pb_struct(message.payload) if message.HasField("payload") else UNSET, + items=_from_pb_list_value(message.items) if message.HasField("items") else UNSET, + dynamic=_from_pb_value(message.dynamic) if message.HasField("dynamic") else UNSET, + note=_from_pb_string_value(message.note) if message.HasField("note") else UNSET, + total=_from_pb_int64_value(message.total) if message.HasField("total") else UNSET, + enabled=_from_pb_bool_value(message.enabled) if message.HasField("enabled") else UNSET, + ratio=_from_pb_double_value(message.ratio) if message.HasField("ratio") else UNSET, + mask=_from_pb_field_mask(message.mask) if message.HasField("mask") else UNSET, + blob=_from_pb_bytes_value(message.blob) if message.HasField("blob") else UNSET, + small_total=_from_pb_int32_value(message.small_total) if message.HasField("small_total") else UNSET, + uint_total=_from_pb_uint32_value(message.uint_total) if message.HasField("uint_total") else UNSET, + huge_total=_from_pb_uint64_value(message.huge_total) if message.HasField("huge_total") else UNSET, + weight=_from_pb_float_value(message.weight) if message.HasField("weight") else UNSET, + raw_ratio=message.raw_ratio if message.HasField("raw_ratio") else UNSET, + tree=_from_pb_recursive_node(message.tree) if message.HasField("tree") else UNSET, + detail_any=_from_pb_any(message.detail_any) if message.HasField("detail_any") else UNSET, + duration_any=_from_pb_any(message.duration_any) if message.HasField("duration_any") else UNSET, + selector=selector, + ) + +def _to_pb_describe_advanced_shapes_response(value: DescribeAdvancedShapesResponse) -> example_pb2.DescribeAdvancedShapesResponse: + message = example_pb2.DescribeAdvancedShapesResponse() + message.labels.update(value.labels) + message.quantities.update(value.quantities) + message.toggles.update(value.toggles) + message.limits.update(value.limits) + if value.observed_at is not UNSET: + message.observed_at.CopyFrom(_to_pb_timestamp(value.observed_at)) + if value.ttl is not UNSET: + message.ttl.CopyFrom(_to_pb_duration(value.ttl)) + if value.payload is not UNSET: + message.payload.CopyFrom(_to_pb_struct(value.payload)) + if value.items is not UNSET: + message.items.CopyFrom(_to_pb_list_value(value.items)) + if value.dynamic is not UNSET: + message.dynamic.CopyFrom(_to_pb_value(value.dynamic)) + if value.note is not UNSET: + message.note.CopyFrom(_to_pb_string_value(value.note)) + if value.total is not UNSET: + message.total.CopyFrom(_to_pb_int64_value(value.total)) + if value.enabled is not UNSET: + message.enabled.CopyFrom(_to_pb_bool_value(value.enabled)) + if value.ratio is not UNSET: + message.ratio.CopyFrom(_to_pb_double_value(value.ratio)) + if value.mask is not UNSET: + message.mask.CopyFrom(_to_pb_field_mask(value.mask)) + if value.blob is not UNSET: + message.blob.CopyFrom(_to_pb_bytes_value(value.blob)) + if value.small_total is not UNSET: + message.small_total.CopyFrom(_to_pb_int32_value(value.small_total)) + if value.uint_total is not UNSET: + message.uint_total.CopyFrom(_to_pb_uint32_value(value.uint_total)) + if value.huge_total is not UNSET: + message.huge_total.CopyFrom(_to_pb_uint64_value(value.huge_total)) + if value.weight is not UNSET: + message.weight.CopyFrom(_to_pb_float_value(value.weight)) + if value.raw_ratio is not UNSET: + message.raw_ratio = value.raw_ratio + if value.tree is not UNSET: + message.tree.CopyFrom(_to_pb_recursive_node(value.tree)) + if value.detail_any is not UNSET: + message.detail_any.CopyFrom(_to_pb_any(value.detail_any)) + if value.duration_any is not UNSET: + message.duration_any.CopyFrom(_to_pb_any(value.duration_any)) + if value.selector is not UNSET: + if isinstance(value.selector, DescribeAdvancedShapesResponseSelectorCityAliasVariant): + message.city_alias = value.selector.city_alias + elif isinstance(value.selector, DescribeAdvancedShapesResponseSelectorCityIdVariant): + message.city_id = value.selector.city_id + elif isinstance(value.selector, DescribeAdvancedShapesResponseSelectorCityDetailsVariant): + message.city_details.CopyFrom(_to_pb_report_details(value.selector.city_details)) + else: + raise TypeError("unsupported DescribeAdvancedShapesResponseSelectorVariant variant: " + type(value.selector).__name__) + + return message + +def _from_pb_describe_scalar_shapes_request(message: example_pb2.DescribeScalarShapesRequest) -> DescribeScalarShapesRequest: + return DescribeScalarShapesRequest( + bool_flag=message.bool_flag, + text_value=message.text_value, + bytes_value=message.bytes_value, + int32_value=message.int32_value, + sint32_value=message.sint32_value, + sfixed32_value=message.sfixed32_value, + uint32_value=message.uint32_value, + fixed32_value=message.fixed32_value, + int64_value=message.int64_value, + sint64_value=message.sint64_value, + sfixed64_value=message.sfixed64_value, + uint64_value=message.uint64_value, + fixed64_value=message.fixed64_value, + float_value=message.float_value, + double_value=message.double_value, + status=_enum_from_pb(ReportStatus, message.status), + details=_from_pb_report_details(message.details), + samples=list(message.samples), + optional_bool_flag=message.optional_bool_flag if message.HasField("optional_bool_flag") else UNSET, + optional_text_value=message.optional_text_value if message.HasField("optional_text_value") else UNSET, + optional_bytes_value=message.optional_bytes_value if message.HasField("optional_bytes_value") else UNSET, + optional_int32_value=message.optional_int32_value if message.HasField("optional_int32_value") else UNSET, + optional_sint32_value=message.optional_sint32_value if message.HasField("optional_sint32_value") else UNSET, + optional_sfixed32_value=message.optional_sfixed32_value if message.HasField("optional_sfixed32_value") else UNSET, + optional_uint32_value=message.optional_uint32_value if message.HasField("optional_uint32_value") else UNSET, + optional_fixed32_value=message.optional_fixed32_value if message.HasField("optional_fixed32_value") else UNSET, + optional_int64_value=message.optional_int64_value if message.HasField("optional_int64_value") else UNSET, + optional_sint64_value=message.optional_sint64_value if message.HasField("optional_sint64_value") else UNSET, + optional_sfixed64_value=message.optional_sfixed64_value if message.HasField("optional_sfixed64_value") else UNSET, + optional_uint64_value=message.optional_uint64_value if message.HasField("optional_uint64_value") else UNSET, + optional_fixed64_value=message.optional_fixed64_value if message.HasField("optional_fixed64_value") else UNSET, + optional_float_value=message.optional_float_value if message.HasField("optional_float_value") else UNSET, + optional_double_value=message.optional_double_value if message.HasField("optional_double_value") else UNSET, + optional_status=_enum_from_pb(ReportStatus, message.optional_status) if message.HasField("optional_status") else UNSET, + ) + +def _to_pb_describe_scalar_shapes_request(value: DescribeScalarShapesRequest) -> example_pb2.DescribeScalarShapesRequest: + message = example_pb2.DescribeScalarShapesRequest() + message.bool_flag = value.bool_flag + message.text_value = value.text_value + message.bytes_value = value.bytes_value + message.int32_value = value.int32_value + message.sint32_value = value.sint32_value + message.sfixed32_value = value.sfixed32_value + message.uint32_value = value.uint32_value + message.fixed32_value = value.fixed32_value + message.int64_value = value.int64_value + message.sint64_value = value.sint64_value + message.sfixed64_value = value.sfixed64_value + message.uint64_value = value.uint64_value + message.fixed64_value = value.fixed64_value + message.float_value = value.float_value + message.double_value = value.double_value + message.status = int(value.status) + message.details.CopyFrom(_to_pb_report_details(value.details)) + message.samples.extend(value.samples) + if value.optional_bool_flag is not UNSET: + message.optional_bool_flag = value.optional_bool_flag + if value.optional_text_value is not UNSET: + message.optional_text_value = value.optional_text_value + if value.optional_bytes_value is not UNSET: + message.optional_bytes_value = value.optional_bytes_value + if value.optional_int32_value is not UNSET: + message.optional_int32_value = value.optional_int32_value + if value.optional_sint32_value is not UNSET: + message.optional_sint32_value = value.optional_sint32_value + if value.optional_sfixed32_value is not UNSET: + message.optional_sfixed32_value = value.optional_sfixed32_value + if value.optional_uint32_value is not UNSET: + message.optional_uint32_value = value.optional_uint32_value + if value.optional_fixed32_value is not UNSET: + message.optional_fixed32_value = value.optional_fixed32_value + if value.optional_int64_value is not UNSET: + message.optional_int64_value = value.optional_int64_value + if value.optional_sint64_value is not UNSET: + message.optional_sint64_value = value.optional_sint64_value + if value.optional_sfixed64_value is not UNSET: + message.optional_sfixed64_value = value.optional_sfixed64_value + if value.optional_uint64_value is not UNSET: + message.optional_uint64_value = value.optional_uint64_value + if value.optional_fixed64_value is not UNSET: + message.optional_fixed64_value = value.optional_fixed64_value + if value.optional_float_value is not UNSET: + message.optional_float_value = value.optional_float_value + if value.optional_double_value is not UNSET: + message.optional_double_value = value.optional_double_value + if value.optional_status is not UNSET: + message.optional_status = int(value.optional_status) + return message + +def _from_pb_describe_scalar_shapes_response(message: example_pb2.DescribeScalarShapesResponse) -> DescribeScalarShapesResponse: + return DescribeScalarShapesResponse( + bool_flag=message.bool_flag, + text_value=message.text_value, + bytes_value=message.bytes_value, + int32_value=message.int32_value, + sint32_value=message.sint32_value, + sfixed32_value=message.sfixed32_value, + uint32_value=message.uint32_value, + fixed32_value=message.fixed32_value, + int64_value=message.int64_value, + sint64_value=message.sint64_value, + sfixed64_value=message.sfixed64_value, + uint64_value=message.uint64_value, + fixed64_value=message.fixed64_value, + float_value=message.float_value, + double_value=message.double_value, + status=_enum_from_pb(ReportStatus, message.status), + details=_from_pb_report_details(message.details), + samples=list(message.samples), + optional_bool_flag=message.optional_bool_flag if message.HasField("optional_bool_flag") else UNSET, + optional_text_value=message.optional_text_value if message.HasField("optional_text_value") else UNSET, + optional_bytes_value=message.optional_bytes_value if message.HasField("optional_bytes_value") else UNSET, + optional_int32_value=message.optional_int32_value if message.HasField("optional_int32_value") else UNSET, + optional_sint32_value=message.optional_sint32_value if message.HasField("optional_sint32_value") else UNSET, + optional_sfixed32_value=message.optional_sfixed32_value if message.HasField("optional_sfixed32_value") else UNSET, + optional_uint32_value=message.optional_uint32_value if message.HasField("optional_uint32_value") else UNSET, + optional_fixed32_value=message.optional_fixed32_value if message.HasField("optional_fixed32_value") else UNSET, + optional_int64_value=message.optional_int64_value if message.HasField("optional_int64_value") else UNSET, + optional_sint64_value=message.optional_sint64_value if message.HasField("optional_sint64_value") else UNSET, + optional_sfixed64_value=message.optional_sfixed64_value if message.HasField("optional_sfixed64_value") else UNSET, + optional_uint64_value=message.optional_uint64_value if message.HasField("optional_uint64_value") else UNSET, + optional_fixed64_value=message.optional_fixed64_value if message.HasField("optional_fixed64_value") else UNSET, + optional_float_value=message.optional_float_value if message.HasField("optional_float_value") else UNSET, + optional_double_value=message.optional_double_value if message.HasField("optional_double_value") else UNSET, + optional_status=_enum_from_pb(ReportStatus, message.optional_status) if message.HasField("optional_status") else UNSET, + ) + +def _to_pb_describe_scalar_shapes_response(value: DescribeScalarShapesResponse) -> example_pb2.DescribeScalarShapesResponse: + message = example_pb2.DescribeScalarShapesResponse() + message.bool_flag = value.bool_flag + message.text_value = value.text_value + message.bytes_value = value.bytes_value + message.int32_value = value.int32_value + message.sint32_value = value.sint32_value + message.sfixed32_value = value.sfixed32_value + message.uint32_value = value.uint32_value + message.fixed32_value = value.fixed32_value + message.int64_value = value.int64_value + message.sint64_value = value.sint64_value + message.sfixed64_value = value.sfixed64_value + message.uint64_value = value.uint64_value + message.fixed64_value = value.fixed64_value + message.float_value = value.float_value + message.double_value = value.double_value + message.status = int(value.status) + message.details.CopyFrom(_to_pb_report_details(value.details)) + message.samples.extend(value.samples) + if value.optional_bool_flag is not UNSET: + message.optional_bool_flag = value.optional_bool_flag + if value.optional_text_value is not UNSET: + message.optional_text_value = value.optional_text_value + if value.optional_bytes_value is not UNSET: + message.optional_bytes_value = value.optional_bytes_value + if value.optional_int32_value is not UNSET: + message.optional_int32_value = value.optional_int32_value + if value.optional_sint32_value is not UNSET: + message.optional_sint32_value = value.optional_sint32_value + if value.optional_sfixed32_value is not UNSET: + message.optional_sfixed32_value = value.optional_sfixed32_value + if value.optional_uint32_value is not UNSET: + message.optional_uint32_value = value.optional_uint32_value + if value.optional_fixed32_value is not UNSET: + message.optional_fixed32_value = value.optional_fixed32_value + if value.optional_int64_value is not UNSET: + message.optional_int64_value = value.optional_int64_value + if value.optional_sint64_value is not UNSET: + message.optional_sint64_value = value.optional_sint64_value + if value.optional_sfixed64_value is not UNSET: + message.optional_sfixed64_value = value.optional_sfixed64_value + if value.optional_uint64_value is not UNSET: + message.optional_uint64_value = value.optional_uint64_value + if value.optional_fixed64_value is not UNSET: + message.optional_fixed64_value = value.optional_fixed64_value + if value.optional_float_value is not UNSET: + message.optional_float_value = value.optional_float_value + if value.optional_double_value is not UNSET: + message.optional_double_value = value.optional_double_value + if value.optional_status is not UNSET: + message.optional_status = int(value.optional_status) + return message + +def _from_pb_hidden_thing_request(message: example_pb2.HiddenThingRequest) -> HiddenThingRequest: + return HiddenThingRequest( + name=message.name, + ) + +def _to_pb_hidden_thing_request(value: HiddenThingRequest) -> example_pb2.HiddenThingRequest: + message = example_pb2.HiddenThingRequest() + message.name = value.name + return message + +def _from_pb_hidden_thing_response(message: example_pb2.HiddenThingResponse) -> HiddenThingResponse: + return HiddenThingResponse( + ) + +def _to_pb_hidden_thing_response(value: HiddenThingResponse) -> example_pb2.HiddenThingResponse: + message = example_pb2.HiddenThingResponse() + return message + +class ExampleAPIToolHandler(Protocol): + def create_report(self, ctx: ToolRequestContext, req: CreateReportRequest) -> CreateReportResponse | Awaitable[CreateReportResponse]: + ... + def ping(self, ctx: ToolRequestContext, req: PingRequest) -> PingResponse | Awaitable[PingResponse]: + ... + def describe_advanced_shapes(self, ctx: ToolRequestContext, req: DescribeAdvancedShapesRequest) -> DescribeAdvancedShapesResponse | Awaitable[DescribeAdvancedShapesResponse]: + ... + def describe_scalar_shapes(self, ctx: ToolRequestContext, req: DescribeScalarShapesRequest) -> DescribeScalarShapesResponse | Awaitable[DescribeScalarShapesResponse]: + ... + def hidden_thing(self, ctx: ToolRequestContext, req: HiddenThingRequest) -> HiddenThingResponse | Awaitable[HiddenThingResponse]: + ... + +def register_example_api_tools(server: mcp.server.lowlevel.Server, impl: ExampleAPIToolHandler, *, namespace: str | None = None) -> None: + if impl is None: + raise ValueError("register_example_api_tools: impl is nil") + registry = _install_server_handlers(server) + resolved_namespace = _normalize_namespace(namespace, "example") + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "CreateReport"), + title="Create report", + description="Create a report for a city.", + input_schema_json=EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON, + output_schema_json=EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.CreateReportRequest, + response_type=example_pb2.CreateReportResponse, + from_pb=_from_pb_create_report_request, + to_pb=_to_pb_create_report_response, + handler=impl.create_report, + annotations=None, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "Health"), + title="Health check", + description="Ping returns an empty response.", + input_schema_json=EXAMPLE_API_HEALTH_INPUT_SCHEMA_JSON, + output_schema_json=EXAMPLE_API_HEALTH_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.PingRequest, + response_type=example_pb2.PingResponse, + from_pb=_from_pb_ping_request, + to_pb=_to_pb_ping_response, + handler=impl.ping, + annotations=None, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "DescribeAdvancedShapes"), + title="Describe advanced shapes", + description="Exercise maps and well-known protobuf types.", + input_schema_json=EXAMPLE_API_DESCRIBE_ADVANCED_SHAPES_INPUT_SCHEMA_JSON, + output_schema_json=EXAMPLE_API_DESCRIBE_ADVANCED_SHAPES_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.DescribeAdvancedShapesRequest, + response_type=example_pb2.DescribeAdvancedShapesResponse, + from_pb=_from_pb_describe_advanced_shapes_request, + to_pb=_to_pb_describe_advanced_shapes_response, + handler=impl.describe_advanced_shapes, + annotations=None, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "DescribeScalarShapes"), + title="Describe scalar shapes", + description="Exercise plain protobuf scalar kinds.", + input_schema_json=EXAMPLE_API_DESCRIBE_SCALAR_SHAPES_INPUT_SCHEMA_JSON, + output_schema_json=EXAMPLE_API_DESCRIBE_SCALAR_SHAPES_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.DescribeScalarShapesRequest, + response_type=example_pb2.DescribeScalarShapesResponse, + from_pb=_from_pb_describe_scalar_shapes_request, + to_pb=_to_pb_describe_scalar_shapes_response, + handler=impl.describe_scalar_shapes, + annotations=None, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "HiddenThing"), + title="", + description="HiddenThing is intentionally hidden from generated tools.\nNote: the `hidden` method option was removed in this iteration.", + input_schema_json=EXAMPLE_API_HIDDEN_THING_INPUT_SCHEMA_JSON, + output_schema_json=EXAMPLE_API_HIDDEN_THING_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.HiddenThingRequest, + response_type=example_pb2.HiddenThingResponse, + from_pb=_from_pb_hidden_thing_request, + to_pb=_to_pb_hidden_thing_response, + handler=impl.hidden_thing, + annotations=None, + icons=None, + )) + +EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"description\":\"City name.\",\"examples\":[\"Paris\",\"London\"],\"minLength\":1,\"maxLength\":100,\"pattern\":\"^[A-Z]\"},\"count\":{\"type\":\"integer\",\"description\":\"count is the number of requested items.\",\"default\":10,\"examples\":[-1],\"minimum\":1,\"maximum\":1000},\"details\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"details contains nested report options.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"labels\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"labels adds additional labels.\",\"examples\":[\"example\"]},\"description\":\"labels adds additional labels.\",\"examples\":[[\"example\"]],\"minItems\":1,\"maxItems\":50,\"uniqueItems\":true},\"units\":{\"type\":[\"string\",\"null\"],\"description\":\"units overrides the default units.\",\"examples\":[\"example\"]}},\"description\":\"CreateReportRequest describes a report generation request.\",\"examples\":[\"{\\\"city\\\":\\\"Paris\\\",\\\"count\\\":2,\\\"details\\\":{\\\"label\\\":\\\"today\\\"}}\"],\"required\":[\"city\",\"count\",\"details\"],\"additionalProperties\":false}" + +EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"details\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"details echoes the nested options.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"reportId\":{\"type\":\"string\",\"description\":\"report_id is the generated report identifier.\",\"examples\":[\"example\"]},\"status\":{\"type\":\"string\",\"title\":\"Report Status\",\"description\":\"status is the final report status.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"enum\":[\"REPORT_STATUS_OK\",\"REPORT_STATUS_FAILED\"]},\"totalCount\":{\"type\":\"string\",\"description\":\"total_count is returned as a ProtoJSON string.\",\"examples\":[\"-1\"]},\"warnings\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"warnings contains optional warning messages.\",\"examples\":[\"example\"]},\"description\":\"warnings contains optional warning messages.\",\"examples\":[[\"example\"]]}},\"description\":\"CreateReportResponse describes a report generation result.\",\"examples\":[{\"details\":{\"label\":\"example\"},\"reportId\":\"example\",\"status\":\"REPORT_STATUS_OK\",\"totalCount\":\"-1\",\"warnings\":[\"example\"]}],\"required\":[\"reportId\",\"totalCount\",\"status\",\"details\"],\"additionalProperties\":false}" + +EXAMPLE_API_HEALTH_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"description\":\"PingRequest is used by the health-check RPC.\",\"additionalProperties\":false}" + +EXAMPLE_API_HEALTH_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"ack\":{\"type\":\"object\",\"description\":\"ack confirms the health-check call.\",\"examples\":[{}],\"additionalProperties\":false}},\"description\":\"PingResponse is used by the health-check RPC.\",\"examples\":[{\"ack\":{}}],\"required\":[\"ack\"],\"additionalProperties\":false}" + +EXAMPLE_API_DESCRIBE_ADVANCED_SHAPES_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"blob\":{\"type\":[\"string\",\"null\"],\"description\":\"blob exercises BytesValue wrapper support.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"cityAlias\":{\"type\":[\"string\",\"null\"],\"description\":\"city_alias exercises string oneof members.\",\"examples\":[\"example\"]},\"cityDetails\":{\"type\":[\"object\",\"null\"],\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"city_details exercises message oneof members.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"cityId\":{\"type\":[\"string\",\"null\"],\"description\":\"city_id exercises int64 oneof members with ProtoJSON string encoding.\",\"examples\":[\"-1\"]},\"detailAny\":{\"type\":[\"object\",\"null\"],\"properties\":{\"@type\":{\"type\":\"string\",\"description\":\"Type URL of the embedded protobuf message, usually in the form type.googleapis.com/\\u003cfull.message.name\\u003e.\"}},\"description\":\"detail_any exercises Any with a regular embedded message payload.\\n\\nProtoJSON representation of google.protobuf.Any. Provide @type with the embedded message type URL and include the embedded message JSON fields alongside it. For well-known types that use a custom JSON form, send @type plus a value field containing that custom representation.\",\"examples\":[{\"@type\":\"type.googleapis.com/internal.testproto.example.v1.ReportDetails\",\"label\":\"from-any\"}],\"required\":[\"@type\"],\"additionalProperties\":true},\"durationAny\":{\"type\":[\"object\",\"null\"],\"properties\":{\"@type\":{\"type\":\"string\",\"description\":\"Type URL of the embedded protobuf message, usually in the form type.googleapis.com/\\u003cfull.message.name\\u003e.\"}},\"description\":\"duration_any exercises Any with a well-known embedded payload.\\n\\nProtoJSON representation of google.protobuf.Any. Provide @type with the embedded message type URL and include the embedded message JSON fields alongside it. For well-known types that use a custom JSON form, send @type plus a value field containing that custom representation.\",\"examples\":[{\"@type\":\"type.googleapis.com/google.protobuf.Duration\",\"value\":\"3600s\"}],\"required\":[\"@type\"],\"additionalProperties\":true},\"dynamic\":{\"description\":\"dynamic accepts arbitrary JSON values.\",\"examples\":[{\"kind\":\"demo\"}]},\"enabled\":{\"type\":[\"boolean\",\"null\"],\"description\":\"enabled exercises BoolValue wrapper support.\",\"examples\":[true]},\"hugeTotal\":{\"type\":[\"string\",\"null\"],\"description\":\"huge_total exercises UInt64Value wrapper support.\",\"examples\":[\"1\"]},\"items\":{\"type\":[\"array\",\"null\"],\"items\":true,\"description\":\"items accepts arbitrary JSON arrays.\",\"examples\":[[\"item\",2,true]]},\"labels\":{\"type\":[\"object\",\"null\"],\"description\":\"labels stores arbitrary string metadata.\",\"examples\":[{\"key\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\"}},\"limits\":{\"type\":[\"object\",\"null\"],\"description\":\"limits demonstrates unsigned numeric map keys encoded as JSON object keys.\",\"examples\":[{\"1\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"pattern\":\"^(0|[1-9][0-9]*)$\"}},\"mask\":{\"type\":[\"string\",\"null\"],\"description\":\"mask exercises FieldMask string support.\",\"examples\":[\"fieldName,otherField\"]},\"note\":{\"type\":[\"string\",\"null\"],\"description\":\"note exercises `StringValue` wrapper support.\",\"examples\":[\"example\"]},\"observedAt\":{\"type\":[\"string\",\"null\"],\"description\":\"observed_at is represented as an RFC 3339 timestamp string.\",\"examples\":[\"2026-03-09T10:11:12Z\"],\"format\":\"date-time\"},\"payload\":{\"type\":[\"object\",\"null\"],\"description\":\"payload accepts arbitrary JSON objects.\",\"examples\":[{\"kind\":\"demo\",\"nested\":{\"ok\":true}}],\"additionalProperties\":true},\"quantities\":{\"type\":[\"object\",\"null\"],\"description\":\"quantities demonstrates numeric map keys encoded as JSON object keys.\",\"examples\":[{\"1\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"pattern\":\"^-?(0|[1-9][0-9]*)$\"}},\"ratio\":{\"description\":\"ratio exercises ProtoJSON float special values.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"rawRatio\":{\"description\":\"raw_ratio exercises raw double ProtoJSON float support.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"smallTotal\":{\"type\":[\"integer\",\"null\"],\"description\":\"small_total exercises Int32Value wrapper support.\",\"examples\":[1]},\"toggles\":{\"type\":[\"object\",\"null\"],\"description\":\"toggles demonstrates bool map keys encoded as JSON object keys.\",\"examples\":[{\"true\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"enum\":[\"true\",\"false\"]}},\"total\":{\"type\":[\"string\",\"null\"],\"description\":\"total exercises Int64Value wrapper support.\",\"examples\":[\"1\"]},\"tree\":{\"type\":[\"object\",\"null\"],\"properties\":{\"child\":{\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"anyOf\":[{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},{\"type\":\"null\"}]},\"children\":{\"type\":[\"array\",\"null\"],\"items\":{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"children holds repeated recursive nodes.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},\"description\":\"children holds repeated recursive nodes.\",\"examples\":[[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]]},\"name\":{\"type\":\"string\",\"description\":\"name identifies the current node.\",\"examples\":[\"example\"]}},\"description\":\"tree exercises recursive message schemas.\\n\\nRecursiveNode exercises recursive message schemas.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false},\"ttl\":{\"type\":[\"string\",\"null\"],\"description\":\"ttl is represented as a protobuf duration string.\",\"examples\":[\"3600s\"],\"pattern\":\"^-?[0-9]+(?:\\\\.[0-9]{1,9})?s$\"},\"uintTotal\":{\"type\":[\"integer\",\"null\"],\"description\":\"uint_total exercises UInt32Value wrapper support.\",\"examples\":[1]},\"weight\":{\"description\":\"weight exercises FloatValue wrapper support.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]}},\"$defs\":{\"internal.testproto.example.v1.RecursiveNode\":{\"type\":\"object\",\"properties\":{\"child\":{\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"anyOf\":[{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},{\"type\":\"null\"}]},\"children\":{\"type\":[\"array\",\"null\"],\"items\":{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"children holds repeated recursive nodes.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},\"description\":\"children holds repeated recursive nodes.\",\"examples\":[[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]]},\"name\":{\"type\":\"string\",\"description\":\"name identifies the current node.\",\"examples\":[\"example\"]}},\"description\":\"RecursiveNode exercises recursive message schemas.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false}},\"description\":\"DescribeAdvancedShapesRequest exercises supported advanced protobuf shapes.\",\"examples\":[{\"blob\":\"aGVsbG8=\",\"cityAlias\":\"example\",\"detailAny\":{\"@type\":\"type.googleapis.com/internal.testproto.example.v1.ReportDetails\",\"label\":\"from-any\"},\"durationAny\":{\"@type\":\"type.googleapis.com/google.protobuf.Duration\",\"value\":\"3600s\"},\"dynamic\":{\"kind\":\"demo\"},\"enabled\":true,\"hugeTotal\":\"1\",\"items\":[\"item\",2,true],\"labels\":{\"key\":\"example\"},\"limits\":{\"1\":\"example\"},\"mask\":\"fieldName,otherField\",\"note\":\"example\",\"observedAt\":\"2026-03-09T10:11:12Z\",\"payload\":{\"kind\":\"demo\",\"nested\":{\"ok\":true}},\"quantities\":{\"1\":\"example\"},\"ratio\":1.25,\"rawRatio\":1.25,\"smallTotal\":1,\"toggles\":{\"true\":\"example\"},\"total\":\"1\",\"tree\":{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"},\"ttl\":\"3600s\",\"uintTotal\":1,\"weight\":1.25}],\"additionalProperties\":false,\"allOf\":[{\"oneOf\":[{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}},{\"allOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}}]},{\"allOf\":[{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}}]},{\"allOf\":[{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}}]}}]}]}]}" + +EXAMPLE_API_DESCRIBE_ADVANCED_SHAPES_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"blob\":{\"type\":[\"string\",\"null\"],\"description\":\"blob exercises BytesValue wrapper support.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"cityAlias\":{\"type\":[\"string\",\"null\"],\"description\":\"city_alias exercises string oneof members.\",\"examples\":[\"example\"]},\"cityDetails\":{\"type\":[\"object\",\"null\"],\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"city_details exercises message oneof members.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"cityId\":{\"type\":[\"string\",\"null\"],\"description\":\"city_id exercises int64 oneof members with ProtoJSON string encoding.\",\"examples\":[\"-1\"]},\"detailAny\":{\"type\":[\"object\",\"null\"],\"properties\":{\"@type\":{\"type\":\"string\",\"description\":\"Type URL of the embedded protobuf message, usually in the form type.googleapis.com/\\u003cfull.message.name\\u003e.\"}},\"description\":\"detail_any exercises Any with a regular embedded message payload.\\n\\nProtoJSON representation of google.protobuf.Any. Provide @type with the embedded message type URL and include the embedded message JSON fields alongside it. For well-known types that use a custom JSON form, send @type plus a value field containing that custom representation.\",\"examples\":[{\"@type\":\"type.googleapis.com/internal.testproto.example.v1.ReportDetails\",\"label\":\"from-any\"}],\"required\":[\"@type\"],\"additionalProperties\":true},\"durationAny\":{\"type\":[\"object\",\"null\"],\"properties\":{\"@type\":{\"type\":\"string\",\"description\":\"Type URL of the embedded protobuf message, usually in the form type.googleapis.com/\\u003cfull.message.name\\u003e.\"}},\"description\":\"duration_any exercises Any with a well-known embedded payload.\\n\\nProtoJSON representation of google.protobuf.Any. Provide @type with the embedded message type URL and include the embedded message JSON fields alongside it. For well-known types that use a custom JSON form, send @type plus a value field containing that custom representation.\",\"examples\":[{\"@type\":\"type.googleapis.com/google.protobuf.Duration\",\"value\":\"3600s\"}],\"required\":[\"@type\"],\"additionalProperties\":true},\"dynamic\":{\"description\":\"dynamic accepts arbitrary JSON values.\",\"examples\":[{\"kind\":\"demo\"}]},\"enabled\":{\"type\":[\"boolean\",\"null\"],\"description\":\"enabled exercises BoolValue wrapper support.\",\"examples\":[true]},\"hugeTotal\":{\"type\":[\"string\",\"null\"],\"description\":\"huge_total exercises UInt64Value wrapper support.\",\"examples\":[\"1\"]},\"items\":{\"type\":[\"array\",\"null\"],\"items\":true,\"description\":\"items accepts arbitrary JSON arrays.\",\"examples\":[[\"item\",2,true]]},\"labels\":{\"type\":[\"object\",\"null\"],\"description\":\"labels stores arbitrary string metadata.\",\"examples\":[{\"key\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\"}},\"limits\":{\"type\":[\"object\",\"null\"],\"description\":\"limits demonstrates unsigned numeric map keys encoded as JSON object keys.\",\"examples\":[{\"1\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"pattern\":\"^(0|[1-9][0-9]*)$\"}},\"mask\":{\"type\":[\"string\",\"null\"],\"description\":\"mask exercises FieldMask string support.\",\"examples\":[\"fieldName,otherField\"]},\"note\":{\"type\":[\"string\",\"null\"],\"description\":\"note exercises `StringValue` wrapper support.\",\"examples\":[\"example\"]},\"observedAt\":{\"type\":[\"string\",\"null\"],\"description\":\"observed_at is represented as an RFC 3339 timestamp string.\",\"examples\":[\"2026-03-09T10:11:12Z\"],\"format\":\"date-time\"},\"payload\":{\"type\":[\"object\",\"null\"],\"description\":\"payload accepts arbitrary JSON objects.\",\"examples\":[{\"kind\":\"demo\",\"nested\":{\"ok\":true}}],\"additionalProperties\":true},\"quantities\":{\"type\":[\"object\",\"null\"],\"description\":\"quantities demonstrates numeric map keys encoded as JSON object keys.\",\"examples\":[{\"1\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"pattern\":\"^-?(0|[1-9][0-9]*)$\"}},\"ratio\":{\"description\":\"ratio exercises ProtoJSON float special values.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"rawRatio\":{\"description\":\"raw_ratio exercises raw double ProtoJSON float support.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"smallTotal\":{\"type\":[\"integer\",\"null\"],\"description\":\"small_total exercises Int32Value wrapper support.\",\"examples\":[1]},\"toggles\":{\"type\":[\"object\",\"null\"],\"description\":\"toggles demonstrates bool map keys encoded as JSON object keys.\",\"examples\":[{\"true\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"enum\":[\"true\",\"false\"]}},\"total\":{\"type\":[\"string\",\"null\"],\"description\":\"total exercises Int64Value wrapper support.\",\"examples\":[\"1\"]},\"tree\":{\"type\":[\"object\",\"null\"],\"properties\":{\"child\":{\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"anyOf\":[{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},{\"type\":\"null\"}]},\"children\":{\"type\":[\"array\",\"null\"],\"items\":{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"children holds repeated recursive nodes.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},\"description\":\"children holds repeated recursive nodes.\",\"examples\":[[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]]},\"name\":{\"type\":\"string\",\"description\":\"name identifies the current node.\",\"examples\":[\"example\"]}},\"description\":\"tree exercises recursive message schemas.\\n\\nRecursiveNode exercises recursive message schemas.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false},\"ttl\":{\"type\":[\"string\",\"null\"],\"description\":\"ttl is represented as a protobuf duration string.\",\"examples\":[\"3600s\"],\"pattern\":\"^-?[0-9]+(?:\\\\.[0-9]{1,9})?s$\"},\"uintTotal\":{\"type\":[\"integer\",\"null\"],\"description\":\"uint_total exercises UInt32Value wrapper support.\",\"examples\":[1]},\"weight\":{\"description\":\"weight exercises FloatValue wrapper support.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]}},\"$defs\":{\"internal.testproto.example.v1.RecursiveNode\":{\"type\":\"object\",\"properties\":{\"child\":{\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"anyOf\":[{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},{\"type\":\"null\"}]},\"children\":{\"type\":[\"array\",\"null\"],\"items\":{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"children holds repeated recursive nodes.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},\"description\":\"children holds repeated recursive nodes.\",\"examples\":[[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]]},\"name\":{\"type\":\"string\",\"description\":\"name identifies the current node.\",\"examples\":[\"example\"]}},\"description\":\"RecursiveNode exercises recursive message schemas.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false}},\"description\":\"DescribeAdvancedShapesResponse echoes supported advanced protobuf shapes.\",\"examples\":[{\"blob\":\"aGVsbG8=\",\"cityAlias\":\"example\",\"detailAny\":{\"@type\":\"type.googleapis.com/internal.testproto.example.v1.ReportDetails\",\"label\":\"from-any\"},\"durationAny\":{\"@type\":\"type.googleapis.com/google.protobuf.Duration\",\"value\":\"3600s\"},\"dynamic\":{\"kind\":\"demo\"},\"enabled\":true,\"hugeTotal\":\"1\",\"items\":[\"item\",2,true],\"labels\":{\"key\":\"example\"},\"limits\":{\"1\":\"example\"},\"mask\":\"fieldName,otherField\",\"note\":\"example\",\"observedAt\":\"2026-03-09T10:11:12Z\",\"payload\":{\"kind\":\"demo\",\"nested\":{\"ok\":true}},\"quantities\":{\"1\":\"example\"},\"ratio\":1.25,\"rawRatio\":1.25,\"smallTotal\":1,\"toggles\":{\"true\":\"example\"},\"total\":\"1\",\"tree\":{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"},\"ttl\":\"3600s\",\"uintTotal\":1,\"weight\":1.25}],\"additionalProperties\":false,\"allOf\":[{\"oneOf\":[{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}},{\"allOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}}]},{\"allOf\":[{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}}]},{\"allOf\":[{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}}]}}]}]}]}" + +EXAMPLE_API_DESCRIBE_SCALAR_SHAPES_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"boolFlag\":{\"type\":\"boolean\",\"description\":\"bool_flag exercises the plain bool kind.\",\"examples\":[true]},\"bytesValue\":{\"type\":\"string\",\"description\":\"bytes_value exercises the plain bytes kind.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"details\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"details exercises plain nested message handling.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"doubleValue\":{\"description\":\"double_value exercises the plain double kind.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]},\"fixed32Value\":{\"type\":\"integer\",\"description\":\"fixed32_value exercises the plain fixed32 kind.\",\"examples\":[1]},\"fixed64Value\":{\"type\":\"string\",\"description\":\"fixed64_value exercises the plain fixed64 kind.\",\"examples\":[\"1\"]},\"floatValue\":{\"description\":\"float_value exercises the plain float kind.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]},\"int32Value\":{\"type\":\"integer\",\"description\":\"int32_value exercises the plain int32 kind.\",\"examples\":[-1]},\"int64Value\":{\"type\":\"string\",\"description\":\"int64_value exercises the plain int64 kind.\",\"examples\":[\"-1\"]},\"optionalBoolFlag\":{\"type\":[\"boolean\",\"null\"],\"description\":\"optional_bool_flag exercises proto3 optional bool.\",\"examples\":[true]},\"optionalBytesValue\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_bytes_value exercises proto3 optional bytes.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"optionalDoubleValue\":{\"description\":\"optional_double_value exercises proto3 optional double.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"optionalFixed32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_fixed32_value exercises proto3 optional fixed32.\",\"examples\":[1]},\"optionalFixed64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_fixed64_value exercises proto3 optional fixed64.\",\"examples\":[\"1\"]},\"optionalFloatValue\":{\"description\":\"optional_float_value exercises proto3 optional float.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"optionalInt32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_int32_value exercises proto3 optional int32.\",\"examples\":[-1]},\"optionalInt64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_int64_value exercises proto3 optional int64.\",\"examples\":[\"-1\"]},\"optionalSfixed32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_sfixed32_value exercises proto3 optional sfixed32.\",\"examples\":[-1]},\"optionalSfixed64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_sfixed64_value exercises proto3 optional sfixed64.\",\"examples\":[\"-1\"]},\"optionalSint32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_sint32_value exercises proto3 optional sint32.\",\"examples\":[-1]},\"optionalSint64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_sint64_value exercises proto3 optional sint64.\",\"examples\":[\"-1\"]},\"optionalStatus\":{\"description\":\"optional_status exercises proto3 optional enum.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"anyOf\":[{\"type\":\"string\",\"title\":\"Report Status\",\"description\":\"optional_status exercises proto3 optional enum.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"enum\":[\"REPORT_STATUS_OK\",\"REPORT_STATUS_FAILED\"]},{\"type\":\"null\"}]},\"optionalTextValue\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_text_value exercises proto3 optional string.\",\"examples\":[\"example\"]},\"optionalUint32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_uint32_value exercises proto3 optional uint32.\",\"examples\":[1]},\"optionalUint64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_uint64_value exercises proto3 optional uint64.\",\"examples\":[\"1\"]},\"samples\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"integer\",\"description\":\"samples exercises repeated plain scalar handling.\",\"examples\":[-1]},\"description\":\"samples exercises repeated plain scalar handling.\",\"examples\":[[-1]]},\"sfixed32Value\":{\"type\":\"integer\",\"description\":\"sfixed32_value exercises the plain sfixed32 kind.\",\"examples\":[-1]},\"sfixed64Value\":{\"type\":\"string\",\"description\":\"sfixed64_value exercises the plain sfixed64 kind.\",\"examples\":[\"-1\"]},\"sint32Value\":{\"type\":\"integer\",\"description\":\"sint32_value exercises the plain sint32 kind.\",\"examples\":[-1]},\"sint64Value\":{\"type\":\"string\",\"description\":\"sint64_value exercises the plain sint64 kind.\",\"examples\":[\"-1\"]},\"status\":{\"type\":\"string\",\"title\":\"Report Status\",\"description\":\"status exercises plain enum handling.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"enum\":[\"REPORT_STATUS_OK\",\"REPORT_STATUS_FAILED\"]},\"textValue\":{\"type\":\"string\",\"description\":\"text_value exercises the plain string kind.\",\"examples\":[\"example\"]},\"uint32Value\":{\"type\":\"integer\",\"description\":\"uint32_value exercises the plain uint32 kind.\",\"examples\":[1]},\"uint64Value\":{\"type\":\"string\",\"description\":\"uint64_value exercises the plain uint64 kind.\",\"examples\":[\"1\"]}},\"description\":\"DescribeScalarShapesRequest exercises plain protobuf scalar kinds.\",\"examples\":[{\"boolFlag\":true,\"bytesValue\":\"aGVsbG8=\",\"details\":{\"label\":\"example\"},\"doubleValue\":1.25,\"fixed32Value\":1,\"fixed64Value\":\"1\",\"floatValue\":1.25,\"int32Value\":-1,\"int64Value\":\"-1\",\"optionalBoolFlag\":true,\"optionalBytesValue\":\"aGVsbG8=\",\"optionalDoubleValue\":1.25,\"optionalFixed32Value\":1,\"optionalFixed64Value\":\"1\",\"optionalFloatValue\":1.25,\"optionalInt32Value\":-1,\"optionalInt64Value\":\"-1\",\"optionalSfixed32Value\":-1,\"optionalSfixed64Value\":\"-1\",\"optionalSint32Value\":-1,\"optionalSint64Value\":\"-1\",\"optionalStatus\":\"REPORT_STATUS_OK\",\"optionalTextValue\":\"example\",\"optionalUint32Value\":1,\"optionalUint64Value\":\"1\",\"samples\":[-1],\"sfixed32Value\":-1,\"sfixed64Value\":\"-1\",\"sint32Value\":-1,\"sint64Value\":\"-1\",\"status\":\"REPORT_STATUS_OK\",\"textValue\":\"example\",\"uint32Value\":1,\"uint64Value\":\"1\"}],\"required\":[\"boolFlag\",\"textValue\",\"bytesValue\",\"int32Value\",\"sint32Value\",\"sfixed32Value\",\"uint32Value\",\"fixed32Value\",\"int64Value\",\"sint64Value\",\"sfixed64Value\",\"uint64Value\",\"fixed64Value\",\"floatValue\",\"doubleValue\",\"status\",\"details\"],\"additionalProperties\":false}" + +EXAMPLE_API_DESCRIBE_SCALAR_SHAPES_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"boolFlag\":{\"type\":\"boolean\",\"description\":\"bool_flag echoes the plain bool kind.\",\"examples\":[true]},\"bytesValue\":{\"type\":\"string\",\"description\":\"bytes_value echoes the plain bytes kind.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"details\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"details echoes plain nested message handling.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"doubleValue\":{\"description\":\"double_value echoes the plain double kind.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]},\"fixed32Value\":{\"type\":\"integer\",\"description\":\"fixed32_value echoes the plain fixed32 kind.\",\"examples\":[1]},\"fixed64Value\":{\"type\":\"string\",\"description\":\"fixed64_value echoes the plain fixed64 kind.\",\"examples\":[\"1\"]},\"floatValue\":{\"description\":\"float_value echoes the plain float kind.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]},\"int32Value\":{\"type\":\"integer\",\"description\":\"int32_value echoes the plain int32 kind.\",\"examples\":[-1]},\"int64Value\":{\"type\":\"string\",\"description\":\"int64_value echoes the plain int64 kind.\",\"examples\":[\"-1\"]},\"optionalBoolFlag\":{\"type\":[\"boolean\",\"null\"],\"description\":\"optional_bool_flag echoes proto3 optional bool.\",\"examples\":[true]},\"optionalBytesValue\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_bytes_value echoes proto3 optional bytes.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"optionalDoubleValue\":{\"description\":\"optional_double_value echoes proto3 optional double.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"optionalFixed32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_fixed32_value echoes proto3 optional fixed32.\",\"examples\":[1]},\"optionalFixed64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_fixed64_value echoes proto3 optional fixed64.\",\"examples\":[\"1\"]},\"optionalFloatValue\":{\"description\":\"optional_float_value echoes proto3 optional float.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"optionalInt32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_int32_value echoes proto3 optional int32.\",\"examples\":[-1]},\"optionalInt64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_int64_value echoes proto3 optional int64.\",\"examples\":[\"-1\"]},\"optionalSfixed32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_sfixed32_value echoes proto3 optional sfixed32.\",\"examples\":[-1]},\"optionalSfixed64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_sfixed64_value echoes proto3 optional sfixed64.\",\"examples\":[\"-1\"]},\"optionalSint32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_sint32_value echoes proto3 optional sint32.\",\"examples\":[-1]},\"optionalSint64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_sint64_value echoes proto3 optional sint64.\",\"examples\":[\"-1\"]},\"optionalStatus\":{\"description\":\"optional_status echoes proto3 optional enum.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"anyOf\":[{\"type\":\"string\",\"title\":\"Report Status\",\"description\":\"optional_status echoes proto3 optional enum.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"enum\":[\"REPORT_STATUS_OK\",\"REPORT_STATUS_FAILED\"]},{\"type\":\"null\"}]},\"optionalTextValue\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_text_value echoes proto3 optional string.\",\"examples\":[\"example\"]},\"optionalUint32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_uint32_value echoes proto3 optional uint32.\",\"examples\":[1]},\"optionalUint64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_uint64_value echoes proto3 optional uint64.\",\"examples\":[\"1\"]},\"samples\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"integer\",\"description\":\"samples echoes repeated plain scalar handling.\",\"examples\":[-1]},\"description\":\"samples echoes repeated plain scalar handling.\",\"examples\":[[-1]]},\"sfixed32Value\":{\"type\":\"integer\",\"description\":\"sfixed32_value echoes the plain sfixed32 kind.\",\"examples\":[-1]},\"sfixed64Value\":{\"type\":\"string\",\"description\":\"sfixed64_value echoes the plain sfixed64 kind.\",\"examples\":[\"-1\"]},\"sint32Value\":{\"type\":\"integer\",\"description\":\"sint32_value echoes the plain sint32 kind.\",\"examples\":[-1]},\"sint64Value\":{\"type\":\"string\",\"description\":\"sint64_value echoes the plain sint64 kind.\",\"examples\":[\"-1\"]},\"status\":{\"type\":\"string\",\"title\":\"Report Status\",\"description\":\"status echoes plain enum handling.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"enum\":[\"REPORT_STATUS_OK\",\"REPORT_STATUS_FAILED\"]},\"textValue\":{\"type\":\"string\",\"description\":\"text_value echoes the plain string kind.\",\"examples\":[\"example\"]},\"uint32Value\":{\"type\":\"integer\",\"description\":\"uint32_value echoes the plain uint32 kind.\",\"examples\":[1]},\"uint64Value\":{\"type\":\"string\",\"description\":\"uint64_value echoes the plain uint64 kind.\",\"examples\":[\"1\"]}},\"description\":\"DescribeScalarShapesResponse echoes plain protobuf scalar kinds.\",\"examples\":[{\"boolFlag\":true,\"bytesValue\":\"aGVsbG8=\",\"details\":{\"label\":\"example\"},\"doubleValue\":1.25,\"fixed32Value\":1,\"fixed64Value\":\"1\",\"floatValue\":1.25,\"int32Value\":-1,\"int64Value\":\"-1\",\"optionalBoolFlag\":true,\"optionalBytesValue\":\"aGVsbG8=\",\"optionalDoubleValue\":1.25,\"optionalFixed32Value\":1,\"optionalFixed64Value\":\"1\",\"optionalFloatValue\":1.25,\"optionalInt32Value\":-1,\"optionalInt64Value\":\"-1\",\"optionalSfixed32Value\":-1,\"optionalSfixed64Value\":\"-1\",\"optionalSint32Value\":-1,\"optionalSint64Value\":\"-1\",\"optionalStatus\":\"REPORT_STATUS_OK\",\"optionalTextValue\":\"example\",\"optionalUint32Value\":1,\"optionalUint64Value\":\"1\",\"samples\":[-1],\"sfixed32Value\":-1,\"sfixed64Value\":\"-1\",\"sint32Value\":-1,\"sint64Value\":\"-1\",\"status\":\"REPORT_STATUS_OK\",\"textValue\":\"example\",\"uint32Value\":1,\"uint64Value\":\"1\"}],\"required\":[\"boolFlag\",\"textValue\",\"bytesValue\",\"int32Value\",\"sint32Value\",\"sfixed32Value\",\"uint32Value\",\"fixed32Value\",\"int64Value\",\"sint64Value\",\"sfixed64Value\",\"uint64Value\",\"fixed64Value\",\"floatValue\",\"doubleValue\",\"status\",\"details\"],\"additionalProperties\":false}" + +EXAMPLE_API_HIDDEN_THING_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"description\":\"name is the hidden request payload.\",\"examples\":[\"example\"]}},\"description\":\"HiddenThingRequest is used by the hidden RPC.\",\"deprecated\":true,\"examples\":[{\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false}" + +EXAMPLE_API_HIDDEN_THING_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"description\":\"HiddenThingResponse is used by the hidden RPC.\",\"additionalProperties\":false}" diff --git a/internal/testproto/example/v1/example_pb2.py b/internal/testproto/example/v1/example_pb2.py new file mode 100644 index 0000000..785f4b8 --- /dev/null +++ b/internal/testproto/example/v1/example_pb2.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: internal/testproto/example/v1/example.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'internal/testproto/example/v1/example.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from mcp.options.v1 import options_pb2 as mcp_dot_options_dot_v1_dot_options__pb2 +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 +from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2 +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+internal/testproto/example/v1/example.proto\x12\x1dinternal.testproto.example.v1\x1a\x1cmcp/options/v1/options.proto\x1a\x19google/protobuf/any.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\xa9\x02\n\x13\x43reateReportRequest\x12\x43\n\x04\x63ity\x18\x01 \x01(\tB/\xda\xb7,+\n\nCity name.\x1a\x07\n\x05Paris\x1a\x08\n\x06LondonR\x06^[A-Z]`\x01hdR\x04\x63ity\x12\x39\n\x05\x63ount\x18\x02 \x01(\x05\x42#\xda\xb7,\x1f\"\t\x11\x00\x00\x00\x00\x00\x00$@\xa1\x01\x00\x00\x00\x00\x00\x00\xf0?\xa9\x01\x00\x00\x00\x00\x00@\x8f@R\x05\x63ount\x12\x46\n\x07\x64\x65tails\x18\x03 \x01(\x0b\x32,.internal.testproto.example.v1.ReportDetailsR\x07\x64\x65tails\x12\x19\n\x05units\x18\x04 \x01(\tH\x00R\x05units\x88\x01\x01\x12%\n\x06labels\x18\x05 \x03(\tB\r\xda\xb7,\t\xf0\x01\x01\xf8\x01\x32\x80\x02\x01R\x06labelsB\x08\n\x06_units\"\xfd\x01\n\x14\x43reateReportResponse\x12\x1b\n\treport_id\x18\x01 \x01(\tR\x08reportId\x12\x1f\n\x0btotal_count\x18\x02 \x01(\x03R\ntotalCount\x12\x43\n\x06status\x18\x03 \x01(\x0e\x32+.internal.testproto.example.v1.ReportStatusR\x06status\x12\x46\n\x07\x64\x65tails\x18\x04 \x01(\x0b\x32,.internal.testproto.example.v1.ReportDetailsR\x07\x64\x65tails\x12\x1a\n\x08warnings\x18\x05 \x03(\tR\x08warnings\"(\n\x12HiddenThingRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\x15\n\x13HiddenThingResponse\"\r\n\x0bPingRequest\"8\n\x0cPingResponse\x12(\n\x03\x61\x63k\x18\x01 \x01(\x0b\x32\x16.google.protobuf.EmptyR\x03\x61\x63k\"\xf1\x10\n\x1d\x44\x65scribeAdvancedShapesRequest\x12`\n\x06labels\x18\x01 \x03(\x0b\x32H.internal.testproto.example.v1.DescribeAdvancedShapesRequest.LabelsEntryR\x06labels\x12l\n\nquantities\x18\x02 \x03(\x0b\x32L.internal.testproto.example.v1.DescribeAdvancedShapesRequest.QuantitiesEntryR\nquantities\x12\x63\n\x07toggles\x18\x03 \x03(\x0b\x32I.internal.testproto.example.v1.DescribeAdvancedShapesRequest.TogglesEntryR\x07toggles\x12`\n\x06limits\x18\x0e \x03(\x0b\x32H.internal.testproto.example.v1.DescribeAdvancedShapesRequest.LimitsEntryR\x06limits\x12@\n\x0bobserved_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01R\nobservedAt\x88\x01\x01\x12\x30\n\x03ttl\x18\x05 \x01(\x0b\x32\x19.google.protobuf.DurationH\x02R\x03ttl\x88\x01\x01\x12\x36\n\x07payload\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructH\x03R\x07payload\x88\x01\x01\x12\x35\n\x05items\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.ListValueH\x04R\x05items\x88\x01\x01\x12\x35\n\x07\x64ynamic\x18\x08 \x01(\x0b\x32\x16.google.protobuf.ValueH\x05R\x07\x64ynamic\x88\x01\x01\x12\x35\n\x04note\x18\t \x01(\x0b\x32\x1c.google.protobuf.StringValueH\x06R\x04note\x88\x01\x01\x12\x36\n\x05total\x18\n \x01(\x0b\x32\x1b.google.protobuf.Int64ValueH\x07R\x05total\x88\x01\x01\x12\x39\n\x07\x65nabled\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.BoolValueH\x08R\x07\x65nabled\x88\x01\x01\x12\x37\n\x05ratio\x18\x0c \x01(\x0b\x32\x1c.google.protobuf.DoubleValueH\tR\x05ratio\x88\x01\x01\x12\x33\n\x04mask\x18\r \x01(\x0b\x32\x1a.google.protobuf.FieldMaskH\nR\x04mask\x88\x01\x01\x12\x34\n\x04\x62lob\x18\x0f \x01(\x0b\x32\x1b.google.protobuf.BytesValueH\x0bR\x04\x62lob\x88\x01\x01\x12\x41\n\x0bsmall_total\x18\x10 \x01(\x0b\x32\x1b.google.protobuf.Int32ValueH\x0cR\nsmallTotal\x88\x01\x01\x12@\n\nuint_total\x18\x11 \x01(\x0b\x32\x1c.google.protobuf.UInt32ValueH\rR\tuintTotal\x88\x01\x01\x12@\n\nhuge_total\x18\x12 \x01(\x0b\x32\x1c.google.protobuf.UInt64ValueH\x0eR\thugeTotal\x88\x01\x01\x12\x38\n\x06weight\x18\x13 \x01(\x0b\x32\x1b.google.protobuf.FloatValueH\x0fR\x06weight\x88\x01\x01\x12 \n\traw_ratio\x18\x14 \x01(\x01H\x10R\x08rawRatio\x88\x01\x01\x12\x45\n\x04tree\x18\x18 \x01(\x0b\x32,.internal.testproto.example.v1.RecursiveNodeH\x11R\x04tree\x88\x01\x01\x12\x38\n\ndetail_any\x18\x19 \x01(\x0b\x32\x14.google.protobuf.AnyH\x12R\tdetailAny\x88\x01\x01\x12<\n\x0c\x64uration_any\x18\x1a \x01(\x0b\x32\x14.google.protobuf.AnyH\x13R\x0b\x64urationAny\x88\x01\x01\x12\x1f\n\ncity_alias\x18\x15 \x01(\tH\x00R\tcityAlias\x12\x19\n\x07\x63ity_id\x18\x16 \x01(\x03H\x00R\x06\x63ityId\x12Q\n\x0c\x63ity_details\x18\x17 \x01(\x0b\x32,.internal.testproto.example.v1.ReportDetailsH\x00R\x0b\x63ityDetails\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x1a=\n\x0fQuantitiesEntry\x12\x10\n\x03key\x18\x01 \x01(\x05R\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x1a:\n\x0cTogglesEntry\x12\x10\n\x03key\x18\x01 \x01(\x08R\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x1a\x39\n\x0bLimitsEntry\x12\x10\n\x03key\x18\x01 \x01(\x04R\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\n\n\x08selectorB\x0e\n\x0c_observed_atB\x06\n\x04_ttlB\n\n\x08_payloadB\x08\n\x06_itemsB\n\n\x08_dynamicB\x07\n\x05_noteB\x08\n\x06_totalB\n\n\x08_enabledB\x08\n\x06_ratioB\x07\n\x05_maskB\x07\n\x05_blobB\x0e\n\x0c_small_totalB\r\n\x0b_uint_totalB\r\n\x0b_huge_totalB\t\n\x07_weightB\x0c\n\n_raw_ratioB\x07\n\x05_treeB\r\n\x0b_detail_anyB\x0f\n\r_duration_any\"\xf6\x10\n\x1e\x44\x65scribeAdvancedShapesResponse\x12\x61\n\x06labels\x18\x01 \x03(\x0b\x32I.internal.testproto.example.v1.DescribeAdvancedShapesResponse.LabelsEntryR\x06labels\x12m\n\nquantities\x18\x02 \x03(\x0b\x32M.internal.testproto.example.v1.DescribeAdvancedShapesResponse.QuantitiesEntryR\nquantities\x12\x64\n\x07toggles\x18\x03 \x03(\x0b\x32J.internal.testproto.example.v1.DescribeAdvancedShapesResponse.TogglesEntryR\x07toggles\x12\x61\n\x06limits\x18\x0e \x03(\x0b\x32I.internal.testproto.example.v1.DescribeAdvancedShapesResponse.LimitsEntryR\x06limits\x12@\n\x0bobserved_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01R\nobservedAt\x88\x01\x01\x12\x30\n\x03ttl\x18\x05 \x01(\x0b\x32\x19.google.protobuf.DurationH\x02R\x03ttl\x88\x01\x01\x12\x36\n\x07payload\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructH\x03R\x07payload\x88\x01\x01\x12\x35\n\x05items\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.ListValueH\x04R\x05items\x88\x01\x01\x12\x35\n\x07\x64ynamic\x18\x08 \x01(\x0b\x32\x16.google.protobuf.ValueH\x05R\x07\x64ynamic\x88\x01\x01\x12\x35\n\x04note\x18\t \x01(\x0b\x32\x1c.google.protobuf.StringValueH\x06R\x04note\x88\x01\x01\x12\x36\n\x05total\x18\n \x01(\x0b\x32\x1b.google.protobuf.Int64ValueH\x07R\x05total\x88\x01\x01\x12\x39\n\x07\x65nabled\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.BoolValueH\x08R\x07\x65nabled\x88\x01\x01\x12\x37\n\x05ratio\x18\x0c \x01(\x0b\x32\x1c.google.protobuf.DoubleValueH\tR\x05ratio\x88\x01\x01\x12\x33\n\x04mask\x18\r \x01(\x0b\x32\x1a.google.protobuf.FieldMaskH\nR\x04mask\x88\x01\x01\x12\x34\n\x04\x62lob\x18\x0f \x01(\x0b\x32\x1b.google.protobuf.BytesValueH\x0bR\x04\x62lob\x88\x01\x01\x12\x41\n\x0bsmall_total\x18\x10 \x01(\x0b\x32\x1b.google.protobuf.Int32ValueH\x0cR\nsmallTotal\x88\x01\x01\x12@\n\nuint_total\x18\x11 \x01(\x0b\x32\x1c.google.protobuf.UInt32ValueH\rR\tuintTotal\x88\x01\x01\x12@\n\nhuge_total\x18\x12 \x01(\x0b\x32\x1c.google.protobuf.UInt64ValueH\x0eR\thugeTotal\x88\x01\x01\x12\x38\n\x06weight\x18\x13 \x01(\x0b\x32\x1b.google.protobuf.FloatValueH\x0fR\x06weight\x88\x01\x01\x12 \n\traw_ratio\x18\x14 \x01(\x01H\x10R\x08rawRatio\x88\x01\x01\x12\x45\n\x04tree\x18\x18 \x01(\x0b\x32,.internal.testproto.example.v1.RecursiveNodeH\x11R\x04tree\x88\x01\x01\x12\x38\n\ndetail_any\x18\x19 \x01(\x0b\x32\x14.google.protobuf.AnyH\x12R\tdetailAny\x88\x01\x01\x12<\n\x0c\x64uration_any\x18\x1a \x01(\x0b\x32\x14.google.protobuf.AnyH\x13R\x0b\x64urationAny\x88\x01\x01\x12\x1f\n\ncity_alias\x18\x15 \x01(\tH\x00R\tcityAlias\x12\x19\n\x07\x63ity_id\x18\x16 \x01(\x03H\x00R\x06\x63ityId\x12Q\n\x0c\x63ity_details\x18\x17 \x01(\x0b\x32,.internal.testproto.example.v1.ReportDetailsH\x00R\x0b\x63ityDetails\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x1a=\n\x0fQuantitiesEntry\x12\x10\n\x03key\x18\x01 \x01(\x05R\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x1a:\n\x0cTogglesEntry\x12\x10\n\x03key\x18\x01 \x01(\x08R\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x1a\x39\n\x0bLimitsEntry\x12\x10\n\x03key\x18\x01 \x01(\x04R\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\n\n\x08selectorB\x0e\n\x0c_observed_atB\x06\n\x04_ttlB\n\n\x08_payloadB\x08\n\x06_itemsB\n\n\x08_dynamicB\x07\n\x05_noteB\x08\n\x06_totalB\n\n\x08_enabledB\x08\n\x06_ratioB\x07\n\x05_maskB\x07\n\x05_blobB\x0e\n\x0c_small_totalB\r\n\x0b_uint_totalB\r\n\x0b_huge_totalB\t\n\x07_weightB\x0c\n\n_raw_ratioB\x07\n\x05_treeB\r\n\x0b_detail_anyB\x0f\n\r_duration_any\"\x8e\x10\n\x1b\x44\x65scribeScalarShapesRequest\x12\x1b\n\tbool_flag\x18\x01 \x01(\x08R\x08\x62oolFlag\x12\x1d\n\ntext_value\x18\x02 \x01(\tR\ttextValue\x12\x1f\n\x0b\x62ytes_value\x18\x03 \x01(\x0cR\nbytesValue\x12\x1f\n\x0bint32_value\x18\x04 \x01(\x05R\nint32Value\x12!\n\x0csint32_value\x18\x05 \x01(\x11R\x0bsint32Value\x12%\n\x0esfixed32_value\x18\x06 \x01(\x0fR\rsfixed32Value\x12!\n\x0cuint32_value\x18\x07 \x01(\rR\x0buint32Value\x12#\n\rfixed32_value\x18\x08 \x01(\x07R\x0c\x66ixed32Value\x12\x1f\n\x0bint64_value\x18\t \x01(\x03R\nint64Value\x12!\n\x0csint64_value\x18\n \x01(\x12R\x0bsint64Value\x12%\n\x0esfixed64_value\x18\x0b \x01(\x10R\rsfixed64Value\x12!\n\x0cuint64_value\x18\x0c \x01(\x04R\x0buint64Value\x12#\n\rfixed64_value\x18\r \x01(\x06R\x0c\x66ixed64Value\x12\x1f\n\x0b\x66loat_value\x18\x0e \x01(\x02R\nfloatValue\x12!\n\x0c\x64ouble_value\x18\x0f \x01(\x01R\x0b\x64oubleValue\x12\x43\n\x06status\x18\x10 \x01(\x0e\x32+.internal.testproto.example.v1.ReportStatusR\x06status\x12\x46\n\x07\x64\x65tails\x18\x11 \x01(\x0b\x32,.internal.testproto.example.v1.ReportDetailsR\x07\x64\x65tails\x12\x18\n\x07samples\x18\x12 \x03(\x05R\x07samples\x12\x31\n\x12optional_bool_flag\x18\x13 \x01(\x08H\x00R\x10optionalBoolFlag\x88\x01\x01\x12\x33\n\x13optional_text_value\x18\x14 \x01(\tH\x01R\x11optionalTextValue\x88\x01\x01\x12\x35\n\x14optional_bytes_value\x18\x15 \x01(\x0cH\x02R\x12optionalBytesValue\x88\x01\x01\x12\x35\n\x14optional_int32_value\x18\x16 \x01(\x05H\x03R\x12optionalInt32Value\x88\x01\x01\x12\x37\n\x15optional_sint32_value\x18\x17 \x01(\x11H\x04R\x13optionalSint32Value\x88\x01\x01\x12;\n\x17optional_sfixed32_value\x18\x18 \x01(\x0fH\x05R\x15optionalSfixed32Value\x88\x01\x01\x12\x37\n\x15optional_uint32_value\x18\x19 \x01(\rH\x06R\x13optionalUint32Value\x88\x01\x01\x12\x39\n\x16optional_fixed32_value\x18\x1a \x01(\x07H\x07R\x14optionalFixed32Value\x88\x01\x01\x12\x35\n\x14optional_int64_value\x18\x1b \x01(\x03H\x08R\x12optionalInt64Value\x88\x01\x01\x12\x37\n\x15optional_sint64_value\x18\x1c \x01(\x12H\tR\x13optionalSint64Value\x88\x01\x01\x12;\n\x17optional_sfixed64_value\x18\x1d \x01(\x10H\nR\x15optionalSfixed64Value\x88\x01\x01\x12\x37\n\x15optional_uint64_value\x18\x1e \x01(\x04H\x0bR\x13optionalUint64Value\x88\x01\x01\x12\x39\n\x16optional_fixed64_value\x18\x1f \x01(\x06H\x0cR\x14optionalFixed64Value\x88\x01\x01\x12\x35\n\x14optional_float_value\x18 \x01(\x02H\rR\x12optionalFloatValue\x88\x01\x01\x12\x37\n\x15optional_double_value\x18! \x01(\x01H\x0eR\x13optionalDoubleValue\x88\x01\x01\x12Y\n\x0foptional_status\x18\" \x01(\x0e\x32+.internal.testproto.example.v1.ReportStatusH\x0fR\x0eoptionalStatus\x88\x01\x01\x42\x15\n\x13_optional_bool_flagB\x16\n\x14_optional_text_valueB\x17\n\x15_optional_bytes_valueB\x17\n\x15_optional_int32_valueB\x18\n\x16_optional_sint32_valueB\x1a\n\x18_optional_sfixed32_valueB\x18\n\x16_optional_uint32_valueB\x19\n\x17_optional_fixed32_valueB\x17\n\x15_optional_int64_valueB\x18\n\x16_optional_sint64_valueB\x1a\n\x18_optional_sfixed64_valueB\x18\n\x16_optional_uint64_valueB\x19\n\x17_optional_fixed64_valueB\x17\n\x15_optional_float_valueB\x18\n\x16_optional_double_valueB\x12\n\x10_optional_status\"\x8f\x10\n\x1c\x44\x65scribeScalarShapesResponse\x12\x1b\n\tbool_flag\x18\x01 \x01(\x08R\x08\x62oolFlag\x12\x1d\n\ntext_value\x18\x02 \x01(\tR\ttextValue\x12\x1f\n\x0b\x62ytes_value\x18\x03 \x01(\x0cR\nbytesValue\x12\x1f\n\x0bint32_value\x18\x04 \x01(\x05R\nint32Value\x12!\n\x0csint32_value\x18\x05 \x01(\x11R\x0bsint32Value\x12%\n\x0esfixed32_value\x18\x06 \x01(\x0fR\rsfixed32Value\x12!\n\x0cuint32_value\x18\x07 \x01(\rR\x0buint32Value\x12#\n\rfixed32_value\x18\x08 \x01(\x07R\x0c\x66ixed32Value\x12\x1f\n\x0bint64_value\x18\t \x01(\x03R\nint64Value\x12!\n\x0csint64_value\x18\n \x01(\x12R\x0bsint64Value\x12%\n\x0esfixed64_value\x18\x0b \x01(\x10R\rsfixed64Value\x12!\n\x0cuint64_value\x18\x0c \x01(\x04R\x0buint64Value\x12#\n\rfixed64_value\x18\r \x01(\x06R\x0c\x66ixed64Value\x12\x1f\n\x0b\x66loat_value\x18\x0e \x01(\x02R\nfloatValue\x12!\n\x0c\x64ouble_value\x18\x0f \x01(\x01R\x0b\x64oubleValue\x12\x43\n\x06status\x18\x10 \x01(\x0e\x32+.internal.testproto.example.v1.ReportStatusR\x06status\x12\x46\n\x07\x64\x65tails\x18\x11 \x01(\x0b\x32,.internal.testproto.example.v1.ReportDetailsR\x07\x64\x65tails\x12\x18\n\x07samples\x18\x12 \x03(\x05R\x07samples\x12\x31\n\x12optional_bool_flag\x18\x13 \x01(\x08H\x00R\x10optionalBoolFlag\x88\x01\x01\x12\x33\n\x13optional_text_value\x18\x14 \x01(\tH\x01R\x11optionalTextValue\x88\x01\x01\x12\x35\n\x14optional_bytes_value\x18\x15 \x01(\x0cH\x02R\x12optionalBytesValue\x88\x01\x01\x12\x35\n\x14optional_int32_value\x18\x16 \x01(\x05H\x03R\x12optionalInt32Value\x88\x01\x01\x12\x37\n\x15optional_sint32_value\x18\x17 \x01(\x11H\x04R\x13optionalSint32Value\x88\x01\x01\x12;\n\x17optional_sfixed32_value\x18\x18 \x01(\x0fH\x05R\x15optionalSfixed32Value\x88\x01\x01\x12\x37\n\x15optional_uint32_value\x18\x19 \x01(\rH\x06R\x13optionalUint32Value\x88\x01\x01\x12\x39\n\x16optional_fixed32_value\x18\x1a \x01(\x07H\x07R\x14optionalFixed32Value\x88\x01\x01\x12\x35\n\x14optional_int64_value\x18\x1b \x01(\x03H\x08R\x12optionalInt64Value\x88\x01\x01\x12\x37\n\x15optional_sint64_value\x18\x1c \x01(\x12H\tR\x13optionalSint64Value\x88\x01\x01\x12;\n\x17optional_sfixed64_value\x18\x1d \x01(\x10H\nR\x15optionalSfixed64Value\x88\x01\x01\x12\x37\n\x15optional_uint64_value\x18\x1e \x01(\x04H\x0bR\x13optionalUint64Value\x88\x01\x01\x12\x39\n\x16optional_fixed64_value\x18\x1f \x01(\x06H\x0cR\x14optionalFixed64Value\x88\x01\x01\x12\x35\n\x14optional_float_value\x18 \x01(\x02H\rR\x12optionalFloatValue\x88\x01\x01\x12\x37\n\x15optional_double_value\x18! \x01(\x01H\x0eR\x13optionalDoubleValue\x88\x01\x01\x12Y\n\x0foptional_status\x18\" \x01(\x0e\x32+.internal.testproto.example.v1.ReportStatusH\x0fR\x0eoptionalStatus\x88\x01\x01\x42\x15\n\x13_optional_bool_flagB\x16\n\x14_optional_text_valueB\x17\n\x15_optional_bytes_valueB\x17\n\x15_optional_int32_valueB\x18\n\x16_optional_sint32_valueB\x1a\n\x18_optional_sfixed32_valueB\x18\n\x16_optional_uint32_valueB\x19\n\x17_optional_fixed32_valueB\x17\n\x15_optional_int64_valueB\x18\n\x16_optional_sint64_valueB\x1a\n\x18_optional_sfixed64_valueB\x18\n\x16_optional_uint64_valueB\x19\n\x17_optional_fixed64_valueB\x17\n\x15_optional_float_valueB\x18\n\x16_optional_double_valueB\x12\n\x10_optional_status\"a\n\rReportDetails\x12\x14\n\x05label\x18\x01 \x01(\tR\x05label::\xe2\xb7,6\n\x0eReport Details\x12$Nested report configuration options.\"\xc0\x01\n\rRecursiveNode\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12G\n\x05\x63hild\x18\x02 \x01(\x0b\x32,.internal.testproto.example.v1.RecursiveNodeH\x00R\x05\x63hild\x88\x01\x01\x12H\n\x08\x63hildren\x18\x03 \x03(\x0b\x32,.internal.testproto.example.v1.RecursiveNodeR\x08\x63hildrenB\x08\n\x06_child*\xd8\x01\n\x0cReportStatus\x12\x1e\n\x12REPORT_STATUS_NONE\x10\x00\x1a\x06\xf2\xb7,\x02\x10\x01\x12:\n\x10REPORT_STATUS_OK\x10\x01\x1a$\xf2\xb7, \n\x1eReport completed successfully.\x12\x39\n\x14REPORT_STATUS_FAILED\x10\x02\x1a\x1f\xf2\xb7,\x1b\n\x19Report generation failed.\x1a\x31\xea\xb7,-\n\rReport Status\x12\x1c\x43urrent state of the report.2\xfc\x06\n\nExampleAPI\x12\xa9\x01\n\x0c\x43reateReport\x12\x32.internal.testproto.example.v1.CreateReportRequest\x1a\x33.internal.testproto.example.v1.CreateReportResponse\"0\xd2\xb7,,\x12\rCreate report\x1a\x1b\x43reate a report for a city.\x12{\n\x04Ping\x12*.internal.testproto.example.v1.PingRequest\x1a+.internal.testproto.example.v1.PingResponse\"\x1a\xd2\xb7,\x16\n\x06Health\x12\x0cHealth check\x12\xe3\x01\n\x16\x44\x65scribeAdvancedShapes\x12<.internal.testproto.example.v1.DescribeAdvancedShapesRequest\x1a=.internal.testproto.example.v1.DescribeAdvancedShapesResponse\"L\xd2\xb7,H\x12\x18\x44\x65scribe advanced shapes\x1a,Exercise maps and well-known protobuf types.\x12\xd4\x01\n\x14\x44\x65scribeScalarShapes\x12:.internal.testproto.example.v1.DescribeScalarShapesRequest\x1a;.internal.testproto.example.v1.DescribeScalarShapesResponse\"C\xd2\xb7,?\x12\x16\x44\x65scribe scalar shapes\x1a%Exercise plain protobuf scalar kinds.\x12y\n\x0bHiddenThing\x12\x31.internal.testproto.example.v1.HiddenThingRequest\x1a\x32.internal.testproto.example.v1.HiddenThingResponse\"\x03\x88\x02\x01\x1a\r\xca\xb7,\t\n\x07\x65xampleBNZLgithub.com/easyp-tech/protoc-gen-mcp/internal/testproto/example/v1;examplev1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'internal.testproto.example.v1.example_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'ZLgithub.com/easyp-tech/protoc-gen-mcp/internal/testproto/example/v1;examplev1' + _globals['_REPORTSTATUS']._loaded_options = None + _globals['_REPORTSTATUS']._serialized_options = b'\352\267,-\n\rReport Status\022\034Current state of the report.' + _globals['_REPORTSTATUS'].values_by_name["REPORT_STATUS_NONE"]._loaded_options = None + _globals['_REPORTSTATUS'].values_by_name["REPORT_STATUS_NONE"]._serialized_options = b'\362\267,\002\020\001' + _globals['_REPORTSTATUS'].values_by_name["REPORT_STATUS_OK"]._loaded_options = None + _globals['_REPORTSTATUS'].values_by_name["REPORT_STATUS_OK"]._serialized_options = b'\362\267, \n\036Report completed successfully.' + _globals['_REPORTSTATUS'].values_by_name["REPORT_STATUS_FAILED"]._loaded_options = None + _globals['_REPORTSTATUS'].values_by_name["REPORT_STATUS_FAILED"]._serialized_options = b'\362\267,\033\n\031Report generation failed.' + _globals['_CREATEREPORTREQUEST'].fields_by_name['city']._loaded_options = None + _globals['_CREATEREPORTREQUEST'].fields_by_name['city']._serialized_options = b'\332\267,+\n\nCity name.\032\007\n\005Paris\032\010\n\006LondonR\006^[A-Z]`\001hd' + _globals['_CREATEREPORTREQUEST'].fields_by_name['count']._loaded_options = None + _globals['_CREATEREPORTREQUEST'].fields_by_name['count']._serialized_options = b'\332\267,\037\"\t\021\000\000\000\000\000\000$@\241\001\000\000\000\000\000\000\360?\251\001\000\000\000\000\000@\217@' + _globals['_CREATEREPORTREQUEST'].fields_by_name['labels']._loaded_options = None + _globals['_CREATEREPORTREQUEST'].fields_by_name['labels']._serialized_options = b'\332\267,\t\360\001\001\370\0012\200\002\001' + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_LABELSENTRY']._loaded_options = None + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_LABELSENTRY']._serialized_options = b'8\001' + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_QUANTITIESENTRY']._loaded_options = None + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_QUANTITIESENTRY']._serialized_options = b'8\001' + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_TOGGLESENTRY']._loaded_options = None + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_TOGGLESENTRY']._serialized_options = b'8\001' + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_LIMITSENTRY']._loaded_options = None + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_LIMITSENTRY']._serialized_options = b'8\001' + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_LABELSENTRY']._loaded_options = None + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_LABELSENTRY']._serialized_options = b'8\001' + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_QUANTITIESENTRY']._loaded_options = None + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_QUANTITIESENTRY']._serialized_options = b'8\001' + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_TOGGLESENTRY']._loaded_options = None + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_TOGGLESENTRY']._serialized_options = b'8\001' + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_LIMITSENTRY']._loaded_options = None + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_LIMITSENTRY']._serialized_options = b'8\001' + _globals['_REPORTDETAILS']._loaded_options = None + _globals['_REPORTDETAILS']._serialized_options = b'\342\267,6\n\016Report Details\022$Nested report configuration options.' + _globals['_EXAMPLEAPI']._loaded_options = None + _globals['_EXAMPLEAPI']._serialized_options = b'\312\267,\t\n\007example' + _globals['_EXAMPLEAPI'].methods_by_name['CreateReport']._loaded_options = None + _globals['_EXAMPLEAPI'].methods_by_name['CreateReport']._serialized_options = b'\322\267,,\022\rCreate report\032\033Create a report for a city.' + _globals['_EXAMPLEAPI'].methods_by_name['Ping']._loaded_options = None + _globals['_EXAMPLEAPI'].methods_by_name['Ping']._serialized_options = b'\322\267,\026\n\006Health\022\014Health check' + _globals['_EXAMPLEAPI'].methods_by_name['DescribeAdvancedShapes']._loaded_options = None + _globals['_EXAMPLEAPI'].methods_by_name['DescribeAdvancedShapes']._serialized_options = b'\322\267,H\022\030Describe advanced shapes\032,Exercise maps and well-known protobuf types.' + _globals['_EXAMPLEAPI'].methods_by_name['DescribeScalarShapes']._loaded_options = None + _globals['_EXAMPLEAPI'].methods_by_name['DescribeScalarShapes']._serialized_options = b'\322\267,?\022\026Describe scalar shapes\032%Exercise plain protobuf scalar kinds.' + _globals['_EXAMPLEAPI'].methods_by_name['HiddenThing']._loaded_options = None + _globals['_EXAMPLEAPI'].methods_by_name['HiddenThing']._serialized_options = b'\210\002\001' + _globals['_REPORTSTATUS']._serialized_start=9778 + _globals['_REPORTSTATUS']._serialized_end=9994 + _globals['_CREATEREPORTREQUEST']._serialized_start=326 + _globals['_CREATEREPORTREQUEST']._serialized_end=623 + _globals['_CREATEREPORTRESPONSE']._serialized_start=626 + _globals['_CREATEREPORTRESPONSE']._serialized_end=879 + _globals['_HIDDENTHINGREQUEST']._serialized_start=881 + _globals['_HIDDENTHINGREQUEST']._serialized_end=921 + _globals['_HIDDENTHINGRESPONSE']._serialized_start=923 + _globals['_HIDDENTHINGRESPONSE']._serialized_end=944 + _globals['_PINGREQUEST']._serialized_start=946 + _globals['_PINGREQUEST']._serialized_end=959 + _globals['_PINGRESPONSE']._serialized_start=961 + _globals['_PINGRESPONSE']._serialized_end=1017 + _globals['_DESCRIBEADVANCEDSHAPESREQUEST']._serialized_start=1020 + _globals['_DESCRIBEADVANCEDSHAPESREQUEST']._serialized_end=3181 + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_LABELSENTRY']._serialized_start=2701 + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_LABELSENTRY']._serialized_end=2758 + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_QUANTITIESENTRY']._serialized_start=2760 + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_QUANTITIESENTRY']._serialized_end=2821 + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_TOGGLESENTRY']._serialized_start=2823 + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_TOGGLESENTRY']._serialized_end=2881 + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_LIMITSENTRY']._serialized_start=2883 + _globals['_DESCRIBEADVANCEDSHAPESREQUEST_LIMITSENTRY']._serialized_end=2940 + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE']._serialized_start=3184 + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE']._serialized_end=5350 + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_LABELSENTRY']._serialized_start=2701 + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_LABELSENTRY']._serialized_end=2758 + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_QUANTITIESENTRY']._serialized_start=2760 + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_QUANTITIESENTRY']._serialized_end=2821 + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_TOGGLESENTRY']._serialized_start=2823 + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_TOGGLESENTRY']._serialized_end=2881 + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_LIMITSENTRY']._serialized_start=2883 + _globals['_DESCRIBEADVANCEDSHAPESRESPONSE_LIMITSENTRY']._serialized_end=2940 + _globals['_DESCRIBESCALARSHAPESREQUEST']._serialized_start=5353 + _globals['_DESCRIBESCALARSHAPESREQUEST']._serialized_end=7415 + _globals['_DESCRIBESCALARSHAPESRESPONSE']._serialized_start=7418 + _globals['_DESCRIBESCALARSHAPESRESPONSE']._serialized_end=9481 + _globals['_REPORTDETAILS']._serialized_start=9483 + _globals['_REPORTDETAILS']._serialized_end=9580 + _globals['_RECURSIVENODE']._serialized_start=9583 + _globals['_RECURSIVENODE']._serialized_end=9775 + _globals['_EXAMPLEAPI']._serialized_start=9997 + _globals['_EXAMPLEAPI']._serialized_end=10889 +# @@protoc_insertion_point(module_scope) diff --git a/mcp/__init__.py b/mcp/__init__.py new file mode 100644 index 0000000..813691e --- /dev/null +++ b/mcp/__init__.py @@ -0,0 +1,5 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +"""Bridge generated mcp.options.* modules with the official MCP SDK package.""" +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) diff --git a/mcp/options/v1/__init__.py b/mcp/options/v1/__init__.py new file mode 100644 index 0000000..6e8ced9 --- /dev/null +++ b/mcp/options/v1/__init__.py @@ -0,0 +1 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. diff --git a/mcp/options/v1/options.pb.go b/mcp/options/v1/options.pb.go index 7c59148..ae12121 100644 --- a/mcp/options/v1/options.pb.go +++ b/mcp/options/v1/options.pb.go @@ -17,8 +17,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v0.14.1-v0.16.1-bufbuild-protocompile-easyp-modified +// protoc-gen-go v1.36.11 +// protoc v0.14.1-v0.15.2-bufbuild-protocompile-easyp-rc1 // source: mcp/options/v1/options.proto package optionsv1 @@ -29,6 +29,7 @@ import ( descriptorpb "google.golang.org/protobuf/types/descriptorpb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -101,10 +102,7 @@ func (TaskSupport) EnumDescriptor() ([]byte, []int) { // control the namespace prefix, description, and default icons for all tools // generated for the service's unary RPC methods. type ServiceOptions struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // namespace is prepended to every generated MCP tool name for this service, // joined with an underscore. Dots and repeated underscores in the value are // normalized to a single underscore. @@ -134,16 +132,16 @@ type ServiceOptions struct { // Individual method-level icons override these defaults. // // See: MCP spec 2025-11-25, Icon data type. - Icons []*Icon `protobuf:"bytes,3,rep,name=icons,proto3" json:"icons,omitempty"` + Icons []*Icon `protobuf:"bytes,3,rep,name=icons,proto3" json:"icons,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ServiceOptions) Reset() { *x = ServiceOptions{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ServiceOptions) String() string { @@ -154,7 +152,7 @@ func (*ServiceOptions) ProtoMessage() {} func (x *ServiceOptions) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -203,10 +201,7 @@ func (x *ServiceOptions) GetIcons() []*Icon { // `examples` annotations in FieldOptions. The generator assembles a // composite JSON example by walking the request message's fields. type MethodOptions struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // name overrides the method segment of the generated MCP tool name. // Dots and repeated underscores in the value are normalized to a single // underscore. The final tool name is "{namespace}_{name}". @@ -279,16 +274,16 @@ type MethodOptions struct { // option (mcp.options.v1.method) = { // execution: { task_support: TASK_SUPPORT_OPTIONAL } // }; - Execution *ExecutionOptions `protobuf:"bytes,12,opt,name=execution,proto3" json:"execution,omitempty"` + Execution *ExecutionOptions `protobuf:"bytes,12,opt,name=execution,proto3" json:"execution,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MethodOptions) Reset() { *x = MethodOptions{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *MethodOptions) String() string { @@ -299,7 +294,7 @@ func (*MethodOptions) ProtoMessage() {} func (x *MethodOptions) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -375,10 +370,7 @@ func (x *MethodOptions) GetExecution() *ExecutionOptions { // - A singular field WITH the `optional` keyword is optional. // - `repeated`, `map`, and `oneof` fields are always optional. type FieldOptions struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // description overrides the field description that the generator would // otherwise infer from leading proto comments on the field declaration. // @@ -484,16 +476,16 @@ type FieldOptions struct { // Example: // // string id = 1 [(mcp.options.v1.field) = { read_only: true }]; - ReadOnly bool `protobuf:"varint,40,opt,name=read_only,json=readOnly,proto3" json:"read_only,omitempty"` + ReadOnly bool `protobuf:"varint,40,opt,name=read_only,json=readOnly,proto3" json:"read_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FieldOptions) Reset() { *x = FieldOptions{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FieldOptions) String() string { @@ -504,7 +496,7 @@ func (*FieldOptions) ProtoMessage() {} func (x *FieldOptions) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -643,13 +635,10 @@ func (x *FieldOptions) GetReadOnly() bool { // - Object: { object_value: { properties: { "key": { string_value: "val" } } } } // - Array: { array_value: { items: [{ number_value: 1 }, { number_value: 2 }] } } type ExampleValue struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // kind represents the underlying type of the example value. // - // Types that are assignable to Kind: + // Types that are valid to be assigned to Kind: // // *ExampleValue_StringValue // *ExampleValue_NumberValue @@ -658,16 +647,16 @@ type ExampleValue struct { // *ExampleValue_ArrayValue // *ExampleValue_NullValue // *ExampleValue_IntegerValue - Kind isExampleValue_Kind `protobuf_oneof:"kind"` + Kind isExampleValue_Kind `protobuf_oneof:"kind"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExampleValue) Reset() { *x = ExampleValue{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ExampleValue) String() string { @@ -678,7 +667,7 @@ func (*ExampleValue) ProtoMessage() {} func (x *ExampleValue) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -693,58 +682,72 @@ func (*ExampleValue) Descriptor() ([]byte, []int) { return file_mcp_options_v1_options_proto_rawDescGZIP(), []int{3} } -func (m *ExampleValue) GetKind() isExampleValue_Kind { - if m != nil { - return m.Kind +func (x *ExampleValue) GetKind() isExampleValue_Kind { + if x != nil { + return x.Kind } return nil } func (x *ExampleValue) GetStringValue() string { - if x, ok := x.GetKind().(*ExampleValue_StringValue); ok { - return x.StringValue + if x != nil { + if x, ok := x.Kind.(*ExampleValue_StringValue); ok { + return x.StringValue + } } return "" } func (x *ExampleValue) GetNumberValue() float64 { - if x, ok := x.GetKind().(*ExampleValue_NumberValue); ok { - return x.NumberValue + if x != nil { + if x, ok := x.Kind.(*ExampleValue_NumberValue); ok { + return x.NumberValue + } } return 0 } func (x *ExampleValue) GetBoolValue() bool { - if x, ok := x.GetKind().(*ExampleValue_BoolValue); ok { - return x.BoolValue + if x != nil { + if x, ok := x.Kind.(*ExampleValue_BoolValue); ok { + return x.BoolValue + } } return false } func (x *ExampleValue) GetObjectValue() *ExampleObject { - if x, ok := x.GetKind().(*ExampleValue_ObjectValue); ok { - return x.ObjectValue + if x != nil { + if x, ok := x.Kind.(*ExampleValue_ObjectValue); ok { + return x.ObjectValue + } } return nil } func (x *ExampleValue) GetArrayValue() *ExampleArray { - if x, ok := x.GetKind().(*ExampleValue_ArrayValue); ok { - return x.ArrayValue + if x != nil { + if x, ok := x.Kind.(*ExampleValue_ArrayValue); ok { + return x.ArrayValue + } } return nil } func (x *ExampleValue) GetNullValue() bool { - if x, ok := x.GetKind().(*ExampleValue_NullValue); ok { - return x.NullValue + if x != nil { + if x, ok := x.Kind.(*ExampleValue_NullValue); ok { + return x.NullValue + } } return false } func (x *ExampleValue) GetIntegerValue() int64 { - if x, ok := x.GetKind().(*ExampleValue_IntegerValue); ok { - return x.IntegerValue + if x != nil { + if x, ok := x.Kind.(*ExampleValue_IntegerValue); ok { + return x.IntegerValue + } } return 0 } @@ -815,21 +818,18 @@ func (*ExampleValue_IntegerValue) isExampleValue_Kind() {} // } // } type ExampleObject struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // properties is a map of property names to typed values. - Properties map[string]*ExampleValue `protobuf:"bytes,1,rep,name=properties,proto3" json:"properties,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Properties map[string]*ExampleValue `protobuf:"bytes,1,rep,name=properties,proto3" json:"properties,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExampleObject) Reset() { *x = ExampleObject{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ExampleObject) String() string { @@ -840,7 +840,7 @@ func (*ExampleObject) ProtoMessage() {} func (x *ExampleObject) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -874,21 +874,18 @@ func (x *ExampleObject) GetProperties() map[string]*ExampleValue { // ] // } type ExampleArray struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // items is the list of array elements. - Items []*ExampleValue `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + Items []*ExampleValue `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExampleArray) Reset() { *x = ExampleArray{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ExampleArray) String() string { @@ -899,7 +896,7 @@ func (*ExampleArray) ProtoMessage() {} func (x *ExampleArray) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -935,10 +932,7 @@ func (x *ExampleArray) GetItems() []*ExampleValue { // } // }; type ToolAnnotations struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // read_only_hint: if true, the tool does not modify its environment. // Default: false. // @@ -971,16 +965,16 @@ type ToolAnnotations struct { OpenWorldHint *bool `protobuf:"varint,4,opt,name=open_world_hint,json=openWorldHint,proto3,oneof" json:"open_world_hint,omitempty"` // title provides a human-readable title via annotations. // If MethodOptions.title is set, it takes precedence at the top level. - Title string `protobuf:"bytes,5,opt,name=title,proto3" json:"title,omitempty"` + Title string `protobuf:"bytes,5,opt,name=title,proto3" json:"title,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ToolAnnotations) Reset() { *x = ToolAnnotations{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ToolAnnotations) String() string { @@ -991,7 +985,7 @@ func (*ToolAnnotations) ProtoMessage() {} func (x *ToolAnnotations) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1054,10 +1048,7 @@ func (x *ToolAnnotations) GetTitle() string { // sizes: ["48x48"] // }] type Icon struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // src is a URI pointing to the icon resource (required). // Must be an HTTPS URL or a data: URI with base64-encoded image data. Src string `protobuf:"bytes,1,opt,name=src,proto3" json:"src,omitempty"` @@ -1070,16 +1061,16 @@ type Icon struct { Sizes []string `protobuf:"bytes,3,rep,name=sizes,proto3" json:"sizes,omitempty"` // theme specifies the intended background theme for this icon (optional). // Values: "light" or "dark". - Theme string `protobuf:"bytes,4,opt,name=theme,proto3" json:"theme,omitempty"` + Theme string `protobuf:"bytes,4,opt,name=theme,proto3" json:"theme,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Icon) Reset() { *x = Icon{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Icon) String() string { @@ -1090,7 +1081,7 @@ func (*Icon) ProtoMessage() {} func (x *Icon) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1138,24 +1129,21 @@ func (x *Icon) GetTheme() string { // // See: MCP spec 2025-11-25, execution.taskSupport. type ExecutionOptions struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // task_support indicates whether this tool supports task-augmented execution. // See: MCP spec 2025-11-25, Tool-Level Negotiation. // // Default: TASK_SUPPORT_FORBIDDEN. - TaskSupport TaskSupport `protobuf:"varint,1,opt,name=task_support,json=taskSupport,proto3,enum=mcp.options.v1.TaskSupport" json:"task_support,omitempty"` + TaskSupport TaskSupport `protobuf:"varint,1,opt,name=task_support,json=taskSupport,proto3,enum=mcp.options.v1.TaskSupport" json:"task_support,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ExecutionOptions) Reset() { *x = ExecutionOptions{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ExecutionOptions) String() string { @@ -1166,7 +1154,7 @@ func (*ExecutionOptions) ProtoMessage() {} func (x *ExecutionOptions) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1203,10 +1191,7 @@ func (x *ExecutionOptions) GetTaskSupport() TaskSupport { // string phone = 2; // } type OneofOptions struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // description provides a human-readable description of the oneof group, // surfaced in the JSON Schema or tool documentation. // @@ -1217,16 +1202,16 @@ type OneofOptions struct { // generated MCP schema — the caller may omit all oneof variants. // // Default: false (all variants optional). - Required bool `protobuf:"varint,2,opt,name=required,proto3" json:"required,omitempty"` + Required bool `protobuf:"varint,2,opt,name=required,proto3" json:"required,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *OneofOptions) Reset() { *x = OneofOptions{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *OneofOptions) String() string { @@ -1237,7 +1222,7 @@ func (*OneofOptions) ProtoMessage() {} func (x *OneofOptions) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1271,10 +1256,7 @@ func (x *OneofOptions) GetRequired() bool { // enrich the JSON Schema of nested message objects with a title, description, // and examples. type MessageOptions struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // title sets the human-readable title for this message's JSON Schema object, // surfaced to agents and clients inspecting the tool's input schema. // @@ -1307,16 +1289,16 @@ type MessageOptions struct { // option (mcp.options.v1.message) = { // examples: [{ properties: { "name": { string_value: "Q4 Report" } } }] // }; - Examples []*ExampleObject `protobuf:"bytes,3,rep,name=examples,proto3" json:"examples,omitempty"` + Examples []*ExampleObject `protobuf:"bytes,3,rep,name=examples,proto3" json:"examples,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MessageOptions) Reset() { *x = MessageOptions{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *MessageOptions) String() string { @@ -1327,7 +1309,7 @@ func (*MessageOptions) ProtoMessage() {} func (x *MessageOptions) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1367,10 +1349,7 @@ func (x *MessageOptions) GetExamples() []*ExampleObject { // google.protobuf.EnumOptions. The generator uses these annotations to enrich // the JSON Schema of enum types with a title and description. type EnumOptions struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // title sets the human-readable title for this enum's JSON Schema // representation. // @@ -1392,16 +1371,16 @@ type EnumOptions struct { // option (mcp.options.v1.enum) = { // description: "Output format for generated reports." // }; - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *EnumOptions) Reset() { *x = EnumOptions{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *EnumOptions) String() string { @@ -1412,7 +1391,7 @@ func (*EnumOptions) ProtoMessage() {} func (x *EnumOptions) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1458,10 +1437,7 @@ func (x *EnumOptions) GetDescription() string { // STATUS_ACTIVE = 1; // } type EnumValueOptions struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // description provides a human-readable description for this enum value, // included in the JSON Schema alongside the value. // @@ -1486,16 +1462,16 @@ type EnumValueOptions struct { // STATUS_NONE = 0 [(mcp.options.v1.enum_value) = { // hidden: true // }]; - Hidden bool `protobuf:"varint,2,opt,name=hidden,proto3" json:"hidden,omitempty"` + Hidden bool `protobuf:"varint,2,opt,name=hidden,proto3" json:"hidden,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *EnumValueOptions) Reset() { *x = EnumValueOptions{} - if protoimpl.UnsafeEnabled { - mi := &file_mcp_options_v1_options_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_mcp_options_v1_options_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *EnumValueOptions) String() string { @@ -1506,7 +1482,7 @@ func (*EnumValueOptions) ProtoMessage() {} func (x *EnumValueOptions) ProtoReflect() protoreflect.Message { mi := &file_mcp_options_v1_options_proto_msgTypes[12] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1652,234 +1628,126 @@ var ( var File_mcp_options_v1_options_proto protoreflect.FileDescriptor -var file_mcp_options_v1_options_proto_rawDesc = []byte{ - 0x0a, 0x1c, 0x6d, 0x63, 0x70, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, - 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, - 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x1a, 0x20, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0x7c, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x2a, 0x0a, 0x05, 0x69, 0x63, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x49, 0x63, 0x6f, 0x6e, 0x52, 0x05, 0x69, 0x63, 0x6f, 0x6e, 0x73, 0x22, 0xa2, - 0x02, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, - 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, 0x69, - 0x64, 0x64, 0x65, 0x6e, 0x12, 0x41, 0x0a, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x63, 0x70, 0x2e, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x6f, 0x6f, 0x6c, 0x41, - 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, 0x0a, 0x05, 0x69, 0x63, 0x6f, 0x6e, 0x73, - 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x63, 0x6f, 0x6e, 0x52, 0x05, 0x69, 0x63, - 0x6f, 0x6e, 0x73, 0x12, 0x3e, 0x0a, 0x09, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, - 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x09, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0x81, 0x06, 0x0a, 0x0c, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, - 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, - 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, - 0x12, 0x41, 0x0a, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x12, 0x16, 0x0a, - 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x22, 0x0a, 0x0a, 0x6d, 0x69, 0x6e, 0x5f, 0x6c, 0x65, 0x6e, - 0x67, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x09, 0x6d, 0x69, 0x6e, - 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x0a, 0x6d, 0x61, 0x78, - 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, - 0x09, 0x6d, 0x61, 0x78, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, - 0x07, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x18, 0x14, 0x20, 0x01, 0x28, 0x01, 0x48, 0x02, - 0x52, 0x07, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, - 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x18, 0x15, 0x20, 0x01, 0x28, 0x01, 0x48, 0x03, 0x52, - 0x07, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x88, 0x01, 0x01, 0x12, 0x30, 0x0a, 0x11, 0x65, - 0x78, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x5f, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, - 0x18, 0x16, 0x20, 0x01, 0x28, 0x01, 0x48, 0x04, 0x52, 0x10, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x73, - 0x69, 0x76, 0x65, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x88, 0x01, 0x01, 0x12, 0x30, 0x0a, - 0x11, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x5f, 0x6d, 0x61, 0x78, 0x69, 0x6d, - 0x75, 0x6d, 0x18, 0x17, 0x20, 0x01, 0x28, 0x01, 0x48, 0x05, 0x52, 0x10, 0x65, 0x78, 0x63, 0x6c, - 0x75, 0x73, 0x69, 0x76, 0x65, 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x88, 0x01, 0x01, 0x12, - 0x24, 0x0a, 0x0b, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x5f, 0x6f, 0x66, 0x18, 0x18, - 0x20, 0x01, 0x28, 0x01, 0x48, 0x06, 0x52, 0x0a, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, - 0x4f, 0x66, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x74, 0x65, - 0x6d, 0x73, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x07, 0x52, 0x08, 0x6d, 0x69, 0x6e, 0x49, - 0x74, 0x65, 0x6d, 0x73, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x69, - 0x74, 0x65, 0x6d, 0x73, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x08, 0x52, 0x08, 0x6d, 0x61, - 0x78, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x6e, 0x69, - 0x71, 0x75, 0x65, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x20, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0b, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x1b, 0x0a, 0x09, - 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x28, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x6d, 0x69, - 0x6e, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x6d, 0x61, 0x78, - 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x69, - 0x6d, 0x75, 0x6d, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x42, - 0x14, 0x0a, 0x12, 0x5f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x5f, 0x6d, 0x69, - 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x73, - 0x69, 0x76, 0x65, 0x5f, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, - 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x5f, 0x6f, 0x66, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, - 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x6d, 0x61, - 0x78, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xce, 0x02, 0x0a, 0x0c, 0x45, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x73, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, - 0x52, 0x0b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x23, 0x0a, - 0x0c, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x0b, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x42, 0x0a, 0x0c, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x63, 0x70, 0x2e, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, - 0x6c, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x6f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3f, 0x0a, 0x0b, 0x61, 0x72, 0x72, 0x61, 0x79, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, - 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, - 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x41, 0x72, 0x72, 0x61, 0x79, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x72, - 0x72, 0x61, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x6e, 0x75, 0x6c, 0x6c, - 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, - 0x6e, 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, 0x0d, 0x69, 0x6e, 0x74, - 0x65, 0x67, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, - 0x48, 0x00, 0x52, 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x42, 0x06, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x22, 0xbb, 0x01, 0x0a, 0x0d, 0x45, 0x78, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x4d, 0x0a, 0x0a, 0x70, 0x72, - 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, - 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2e, 0x50, 0x72, - 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x70, - 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x1a, 0x5b, 0x0a, 0x0f, 0x50, 0x72, 0x6f, - 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x32, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, - 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, - 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x42, 0x0a, 0x0c, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, - 0x65, 0x41, 0x72, 0x72, 0x61, 0x79, 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xfc, 0x01, 0x0a, 0x0f, 0x54, - 0x6f, 0x6f, 0x6c, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x24, - 0x0a, 0x0e, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x5f, 0x68, 0x69, 0x6e, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, - 0x48, 0x69, 0x6e, 0x74, 0x12, 0x2e, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x69, 0x76, 0x65, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, - 0x52, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x76, 0x65, 0x48, 0x69, 0x6e, - 0x74, 0x88, 0x01, 0x01, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x64, 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, - 0x6e, 0x74, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, - 0x64, 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x74, 0x48, 0x69, 0x6e, 0x74, 0x12, 0x2b, 0x0a, - 0x0f, 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x68, 0x69, 0x6e, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x0d, 0x6f, 0x70, 0x65, 0x6e, 0x57, 0x6f, - 0x72, 0x6c, 0x64, 0x48, 0x69, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, - 0x74, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, - 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x76, 0x65, - 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x77, - 0x6f, 0x72, 0x6c, 0x64, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x22, 0x61, 0x0a, 0x04, 0x49, 0x63, 0x6f, - 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x72, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x73, 0x72, 0x63, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x7a, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x05, 0x73, 0x69, 0x7a, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x22, 0x52, 0x0a, 0x10, - 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x3e, 0x0a, 0x0c, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x75, 0x70, 0x70, - 0x6f, 0x72, 0x74, 0x52, 0x0b, 0x74, 0x61, 0x73, 0x6b, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, - 0x22, 0x4c, 0x0a, 0x0c, 0x4f, 0x6e, 0x65, 0x6f, 0x66, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x83, - 0x01, 0x0a, 0x0e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x08, 0x65, 0x78, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x63, - 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x08, 0x65, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x73, 0x22, 0x45, 0x0a, 0x0b, 0x45, 0x6e, 0x75, 0x6d, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x4c, 0x0a, 0x10, 0x45, - 0x6e, 0x75, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x06, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x2a, 0x5a, 0x0a, 0x0b, 0x54, 0x61, 0x73, - 0x6b, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x15, 0x0a, 0x11, 0x54, 0x41, 0x53, 0x4b, - 0x5f, 0x53, 0x55, 0x50, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, - 0x19, 0x0a, 0x15, 0x54, 0x41, 0x53, 0x4b, 0x5f, 0x53, 0x55, 0x50, 0x50, 0x4f, 0x52, 0x54, 0x5f, - 0x4f, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x54, 0x41, - 0x53, 0x4b, 0x5f, 0x53, 0x55, 0x50, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x49, - 0x52, 0x45, 0x44, 0x10, 0x02, 0x3a, 0x5b, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x18, 0xf9, 0xc6, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x63, 0x70, 0x2e, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x3a, 0x57, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1e, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xfa, 0xc6, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x3a, 0x53, 0x0a, 0x05, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x12, 0x1d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0xfb, 0xc6, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x63, - 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, - 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, - 0x3a, 0x5b, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xfc, 0xc6, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x3a, 0x4f, 0x0a, - 0x04, 0x65, 0x6e, 0x75, 0x6d, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0xfd, 0xc6, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x63, - 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x75, - 0x6d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x04, 0x65, 0x6e, 0x75, 0x6d, 0x3a, 0x64, - 0x0a, 0x0a, 0x65, 0x6e, 0x75, 0x6d, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6e, 0x75, 0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0xfe, 0xc6, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x09, 0x65, 0x6e, 0x75, 0x6d, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x53, 0x0a, 0x05, 0x6f, 0x6e, 0x65, 0x6f, 0x66, 0x12, 0x1d, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x4f, 0x6e, 0x65, 0x6f, 0x66, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xff, 0xc6, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x63, 0x70, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x65, 0x6f, 0x66, 0x4f, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x05, 0x6f, 0x6e, 0x65, 0x6f, 0x66, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x61, 0x73, 0x79, 0x70, 0x2d, 0x74, 0x65, - 0x63, 0x68, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6d, 0x63, - 0x70, 0x2f, 0x6d, 0x63, 0x70, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, - 0x3b, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, -} +const file_mcp_options_v1_options_proto_rawDesc = "" + + "\n" + + "\x1cmcp/options/v1/options.proto\x12\x0emcp.options.v1\x1a google/protobuf/descriptor.proto\"|\n" + + "\x0eServiceOptions\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12*\n" + + "\x05icons\x18\x03 \x03(\v2\x14.mcp.options.v1.IconR\x05icons\"\xa2\x02\n" + + "\rMethodOptions\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x16\n" + + "\x06hidden\x18\x04 \x01(\bR\x06hidden\x12A\n" + + "\vannotations\x18\n" + + " \x01(\v2\x1f.mcp.options.v1.ToolAnnotationsR\vannotations\x12*\n" + + "\x05icons\x18\v \x03(\v2\x14.mcp.options.v1.IconR\x05icons\x12>\n" + + "\texecution\x18\f \x01(\v2 .mcp.options.v1.ExecutionOptionsR\texecution\"\x81\x06\n" + + "\fFieldOptions\x12 \n" + + "\vdescription\x18\x01 \x01(\tR\vdescription\x128\n" + + "\bexamples\x18\x03 \x03(\v2\x1c.mcp.options.v1.ExampleValueR\bexamples\x12A\n" + + "\rdefault_value\x18\x04 \x01(\v2\x1c.mcp.options.v1.ExampleValueR\fdefaultValue\x12\x18\n" + + "\apattern\x18\n" + + " \x01(\tR\apattern\x12\x16\n" + + "\x06format\x18\v \x01(\tR\x06format\x12\"\n" + + "\n" + + "min_length\x18\f \x01(\rH\x00R\tminLength\x88\x01\x01\x12\"\n" + + "\n" + + "max_length\x18\r \x01(\rH\x01R\tmaxLength\x88\x01\x01\x12\x1d\n" + + "\aminimum\x18\x14 \x01(\x01H\x02R\aminimum\x88\x01\x01\x12\x1d\n" + + "\amaximum\x18\x15 \x01(\x01H\x03R\amaximum\x88\x01\x01\x120\n" + + "\x11exclusive_minimum\x18\x16 \x01(\x01H\x04R\x10exclusiveMinimum\x88\x01\x01\x120\n" + + "\x11exclusive_maximum\x18\x17 \x01(\x01H\x05R\x10exclusiveMaximum\x88\x01\x01\x12$\n" + + "\vmultiple_of\x18\x18 \x01(\x01H\x06R\n" + + "multipleOf\x88\x01\x01\x12 \n" + + "\tmin_items\x18\x1e \x01(\rH\aR\bminItems\x88\x01\x01\x12 \n" + + "\tmax_items\x18\x1f \x01(\rH\bR\bmaxItems\x88\x01\x01\x12!\n" + + "\funique_items\x18 \x01(\bR\vuniqueItems\x12\x1b\n" + + "\tread_only\x18( \x01(\bR\breadOnlyB\r\n" + + "\v_min_lengthB\r\n" + + "\v_max_lengthB\n" + + "\n" + + "\b_minimumB\n" + + "\n" + + "\b_maximumB\x14\n" + + "\x12_exclusive_minimumB\x14\n" + + "\x12_exclusive_maximumB\x0e\n" + + "\f_multiple_ofB\f\n" + + "\n" + + "_min_itemsB\f\n" + + "\n" + + "_max_items\"\xce\x02\n" + + "\fExampleValue\x12#\n" + + "\fstring_value\x18\x01 \x01(\tH\x00R\vstringValue\x12#\n" + + "\fnumber_value\x18\x02 \x01(\x01H\x00R\vnumberValue\x12\x1f\n" + + "\n" + + "bool_value\x18\x03 \x01(\bH\x00R\tboolValue\x12B\n" + + "\fobject_value\x18\x04 \x01(\v2\x1d.mcp.options.v1.ExampleObjectH\x00R\vobjectValue\x12?\n" + + "\varray_value\x18\x05 \x01(\v2\x1c.mcp.options.v1.ExampleArrayH\x00R\n" + + "arrayValue\x12\x1f\n" + + "\n" + + "null_value\x18\x06 \x01(\bH\x00R\tnullValue\x12%\n" + + "\rinteger_value\x18\a \x01(\x03H\x00R\fintegerValueB\x06\n" + + "\x04kind\"\xbb\x01\n" + + "\rExampleObject\x12M\n" + + "\n" + + "properties\x18\x01 \x03(\v2-.mcp.options.v1.ExampleObject.PropertiesEntryR\n" + + "properties\x1a[\n" + + "\x0fPropertiesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x122\n" + + "\x05value\x18\x02 \x01(\v2\x1c.mcp.options.v1.ExampleValueR\x05value:\x028\x01\"B\n" + + "\fExampleArray\x122\n" + + "\x05items\x18\x01 \x03(\v2\x1c.mcp.options.v1.ExampleValueR\x05items\"\xfc\x01\n" + + "\x0fToolAnnotations\x12$\n" + + "\x0eread_only_hint\x18\x01 \x01(\bR\freadOnlyHint\x12.\n" + + "\x10destructive_hint\x18\x02 \x01(\bH\x00R\x0fdestructiveHint\x88\x01\x01\x12'\n" + + "\x0fidempotent_hint\x18\x03 \x01(\bR\x0eidempotentHint\x12+\n" + + "\x0fopen_world_hint\x18\x04 \x01(\bH\x01R\ropenWorldHint\x88\x01\x01\x12\x14\n" + + "\x05title\x18\x05 \x01(\tR\x05titleB\x13\n" + + "\x11_destructive_hintB\x12\n" + + "\x10_open_world_hint\"a\n" + + "\x04Icon\x12\x10\n" + + "\x03src\x18\x01 \x01(\tR\x03src\x12\x1b\n" + + "\tmime_type\x18\x02 \x01(\tR\bmimeType\x12\x14\n" + + "\x05sizes\x18\x03 \x03(\tR\x05sizes\x12\x14\n" + + "\x05theme\x18\x04 \x01(\tR\x05theme\"R\n" + + "\x10ExecutionOptions\x12>\n" + + "\ftask_support\x18\x01 \x01(\x0e2\x1b.mcp.options.v1.TaskSupportR\vtaskSupport\"L\n" + + "\fOneofOptions\x12 \n" + + "\vdescription\x18\x01 \x01(\tR\vdescription\x12\x1a\n" + + "\brequired\x18\x02 \x01(\bR\brequired\"\x83\x01\n" + + "\x0eMessageOptions\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x129\n" + + "\bexamples\x18\x03 \x03(\v2\x1d.mcp.options.v1.ExampleObjectR\bexamples\"E\n" + + "\vEnumOptions\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\"L\n" + + "\x10EnumValueOptions\x12 \n" + + "\vdescription\x18\x01 \x01(\tR\vdescription\x12\x16\n" + + "\x06hidden\x18\x02 \x01(\bR\x06hidden*Z\n" + + "\vTaskSupport\x12\x15\n" + + "\x11TASK_SUPPORT_NONE\x10\x00\x12\x19\n" + + "\x15TASK_SUPPORT_OPTIONAL\x10\x01\x12\x19\n" + + "\x15TASK_SUPPORT_REQUIRED\x10\x02:[\n" + + "\aservice\x12\x1f.google.protobuf.ServiceOptions\x18\xf9\xc6\x05 \x01(\v2\x1e.mcp.options.v1.ServiceOptionsR\aservice:W\n" + + "\x06method\x12\x1e.google.protobuf.MethodOptions\x18\xfa\xc6\x05 \x01(\v2\x1d.mcp.options.v1.MethodOptionsR\x06method:S\n" + + "\x05field\x12\x1d.google.protobuf.FieldOptions\x18\xfb\xc6\x05 \x01(\v2\x1c.mcp.options.v1.FieldOptionsR\x05field:[\n" + + "\amessage\x12\x1f.google.protobuf.MessageOptions\x18\xfc\xc6\x05 \x01(\v2\x1e.mcp.options.v1.MessageOptionsR\amessage:O\n" + + "\x04enum\x12\x1c.google.protobuf.EnumOptions\x18\xfd\xc6\x05 \x01(\v2\x1b.mcp.options.v1.EnumOptionsR\x04enum:d\n" + + "\n" + + "enum_value\x12!.google.protobuf.EnumValueOptions\x18\xfe\xc6\x05 \x01(\v2 .mcp.options.v1.EnumValueOptionsR\tenumValue:S\n" + + "\x05oneof\x12\x1d.google.protobuf.OneofOptions\x18\xff\xc6\x05 \x01(\v2\x1c.mcp.options.v1.OneofOptionsR\x05oneofB?Z=github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1;optionsv1b\x06proto3" var ( file_mcp_options_v1_options_proto_rawDescOnce sync.Once - file_mcp_options_v1_options_proto_rawDescData = file_mcp_options_v1_options_proto_rawDesc + file_mcp_options_v1_options_proto_rawDescData []byte ) func file_mcp_options_v1_options_proto_rawDescGZIP() []byte { file_mcp_options_v1_options_proto_rawDescOnce.Do(func() { - file_mcp_options_v1_options_proto_rawDescData = protoimpl.X.CompressGZIP(file_mcp_options_v1_options_proto_rawDescData) + file_mcp_options_v1_options_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_mcp_options_v1_options_proto_rawDesc), len(file_mcp_options_v1_options_proto_rawDesc))) }) return file_mcp_options_v1_options_proto_rawDescData } @@ -1950,164 +1818,6 @@ func file_mcp_options_v1_options_proto_init() { if File_mcp_options_v1_options_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_mcp_options_v1_options_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*ServiceOptions); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*MethodOptions); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*FieldOptions); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*ExampleValue); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*ExampleObject); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[5].Exporter = func(v any, i int) any { - switch v := v.(*ExampleArray); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[6].Exporter = func(v any, i int) any { - switch v := v.(*ToolAnnotations); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[7].Exporter = func(v any, i int) any { - switch v := v.(*Icon); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[8].Exporter = func(v any, i int) any { - switch v := v.(*ExecutionOptions); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[9].Exporter = func(v any, i int) any { - switch v := v.(*OneofOptions); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[10].Exporter = func(v any, i int) any { - switch v := v.(*MessageOptions); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[11].Exporter = func(v any, i int) any { - switch v := v.(*EnumOptions); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mcp_options_v1_options_proto_msgTypes[12].Exporter = func(v any, i int) any { - switch v := v.(*EnumValueOptions); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } file_mcp_options_v1_options_proto_msgTypes[2].OneofWrappers = []any{} file_mcp_options_v1_options_proto_msgTypes[3].OneofWrappers = []any{ (*ExampleValue_StringValue)(nil), @@ -2123,7 +1833,7 @@ func file_mcp_options_v1_options_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_mcp_options_v1_options_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_mcp_options_v1_options_proto_rawDesc), len(file_mcp_options_v1_options_proto_rawDesc)), NumEnums: 1, NumMessages: 14, NumExtensions: 7, @@ -2136,7 +1846,6 @@ func file_mcp_options_v1_options_proto_init() { ExtensionInfos: file_mcp_options_v1_options_proto_extTypes, }.Build() File_mcp_options_v1_options_proto = out.File - file_mcp_options_v1_options_proto_rawDesc = nil file_mcp_options_v1_options_proto_goTypes = nil file_mcp_options_v1_options_proto_depIdxs = nil } diff --git a/mcp/options/v1/options_mcp.py b/mcp/options/v1/options_mcp.py new file mode 100644 index 0000000..973e3ed --- /dev/null +++ b/mcp/options/v1/options_mcp.py @@ -0,0 +1,757 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +# source: mcp/options/v1/options.proto +from __future__ import annotations + +import enum +import inspect +import json +import weakref +from dataclasses import dataclass, field +from typing import Any, Awaitable, Protocol, TypeAlias + +import jsonschema +import mcp.server.lowlevel +import mcp.server.session +import mcp.shared.exceptions +import mcp.types +from google.protobuf import any_pb2, duration_pb2, empty_pb2, field_mask_pb2, json_format, struct_pb2, timestamp_pb2, wrappers_pb2 +try: + from . import options_pb2 +except ImportError: + import options_pb2 + +ToolRequestContext = mcp.server.session.ServerSession + +class _UnsetType: + __slots__ = () + + def __repr__(self) -> str: + return "UNSET" + +UNSET = _UnsetType() + +JSONValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None +ProtoAny: TypeAlias = dict[str, Any] +Timestamp: TypeAlias = str +Duration: TypeAlias = str +FieldMask: TypeAlias = str +Struct: TypeAlias = dict[str, Any] +Value: TypeAlias = JSONValue +ListValue: TypeAlias = list[Any] +BoolValue: TypeAlias = bool +StringValue: TypeAlias = str +BytesValue: TypeAlias = bytes +Int32Value: TypeAlias = int +UInt32Value: TypeAlias = int +Int64Value: TypeAlias = int +UInt64Value: TypeAlias = int +FloatValue: TypeAlias = float +DoubleValue: TypeAlias = float + +@dataclass(slots=True) +class Empty: + pass + +@dataclass(slots=True) +class Icon: + src: str + mime_type: str + theme: str + sizes: list[str] = field(default_factory=list) + +@dataclass(slots=True) +class ServiceOptions: + namespace: str + description: str + icons: list[Icon] = field(default_factory=list) + +@dataclass(slots=True) +class ToolAnnotations: + read_only_hint: bool + idempotent_hint: bool + title: str + destructive_hint: bool | _UnsetType = UNSET + open_world_hint: bool | _UnsetType = UNSET + +class TaskSupport(enum.IntEnum): + TASK_SUPPORT_NONE = 0 + TASK_SUPPORT_OPTIONAL = 1 + TASK_SUPPORT_REQUIRED = 2 + +@dataclass(slots=True) +class ExecutionOptions: + task_support: TaskSupport + +@dataclass(slots=True) +class MethodOptions: + name: str + title: str + description: str + hidden: bool + annotations: ToolAnnotations + execution: ExecutionOptions + icons: list[Icon] = field(default_factory=list) + +@dataclass(slots=True) +class ExampleObject: + properties: dict[str, ExampleValue] = field(default_factory=dict) + +@dataclass(slots=True) +class ExampleArray: + items: list[ExampleValue] = field(default_factory=list) + +@dataclass(slots=True) +class ExampleValueKindVariant: + pass + +@dataclass(slots=True) +class ExampleValueKindStringValueVariant(ExampleValueKindVariant): + string_value: str + +@dataclass(slots=True) +class ExampleValueKindNumberValueVariant(ExampleValueKindVariant): + number_value: float + +@dataclass(slots=True) +class ExampleValueKindBoolValueVariant(ExampleValueKindVariant): + bool_value: bool + +@dataclass(slots=True) +class ExampleValueKindObjectValueVariant(ExampleValueKindVariant): + object_value: ExampleObject + +@dataclass(slots=True) +class ExampleValueKindArrayValueVariant(ExampleValueKindVariant): + array_value: ExampleArray + +@dataclass(slots=True) +class ExampleValueKindNullValueVariant(ExampleValueKindVariant): + null_value: bool + +@dataclass(slots=True) +class ExampleValueKindIntegerValueVariant(ExampleValueKindVariant): + integer_value: int + +@dataclass(slots=True) +class ExampleValue: + kind: ExampleValueKindVariant | _UnsetType = UNSET + +@dataclass(slots=True) +class FieldOptions: + description: str + default_value: ExampleValue + pattern: str + format: str + unique_items: bool + read_only: bool + examples: list[ExampleValue] = field(default_factory=list) + min_length: int | _UnsetType = UNSET + max_length: int | _UnsetType = UNSET + minimum: float | _UnsetType = UNSET + maximum: float | _UnsetType = UNSET + exclusive_minimum: float | _UnsetType = UNSET + exclusive_maximum: float | _UnsetType = UNSET + multiple_of: float | _UnsetType = UNSET + min_items: int | _UnsetType = UNSET + max_items: int | _UnsetType = UNSET + +@dataclass(slots=True) +class OneofOptions: + description: str + required: bool + +@dataclass(slots=True) +class MessageOptions: + title: str + description: str + examples: list[ExampleObject] = field(default_factory=list) + +@dataclass(slots=True) +class EnumOptions: + title: str + description: str + +@dataclass(slots=True) +class EnumValueOptions: + description: str + hidden: bool + +@dataclass(frozen=True) +class _RegisteredTool: + name: str + title: str + description: str + input_schema_json: str + output_schema_json: str + request_type: type[Any] + response_type: type[Any] + from_pb: Any + to_pb: Any + handler: Any + annotations: dict[str, Any] | None + icons: list[dict[str, Any]] | None + +class _ServerToolRegistry: + def __init__(self, server: mcp.server.lowlevel.Server) -> None: + self.server = server + self.tools: dict[str, _RegisteredTool] = {} + self.reserved_tool_names = _get_reserved_tool_names(server) + self.handlers_installed = False + self.previous_list_tools: Any | None = None + self.previous_call_tool: Any | None = None + + def add_tool(self, tool: _RegisteredTool) -> None: + if tool.name in self.reserved_tool_names: + raise ValueError(f"duplicate tool registration: {tool.name}") + self.tools[tool.name] = tool + self.reserved_tool_names.add(tool.name) + + def list_tools(self) -> list[mcp.types.Tool]: + return [_build_tool(tool) for tool in self.tools.values()] + + async def call_tool(self, name: str, arguments: dict[str, Any] | None, context: ToolRequestContext | None = None) -> mcp.types.CallToolResult: + return await _dispatch_call(self, name, arguments or {}, context) + +_SERVER_REGISTRIES: weakref.WeakKeyDictionary[mcp.server.lowlevel.Server, _ServerToolRegistry] = weakref.WeakKeyDictionary() + +def _load_schema(schema_json: str) -> dict[str, Any]: + return json.loads(schema_json) + +def _protojson_to_message(arguments: dict[str, Any], message: Any) -> Any: + json_format.ParseDict(arguments, message) + return message + +def _normalize_tool_segment(segment: str | None) -> str: + if segment is None: + return "" + parts = [part for part in segment.strip().replace(".", "_").split("_") if part] + return "_".join(parts) + +def _normalize_namespace(namespace: str | None, default: str) -> str: + if namespace is None: + namespace = default + return _normalize_tool_segment(namespace) + +def _tool_name(namespace: str, method_name: str) -> str: + namespace = _normalize_tool_segment(namespace) + method_name = _normalize_tool_segment(method_name) + if namespace == '': + return method_name + if method_name == '': + return namespace + return f"{namespace}_{method_name}" + +def _get_registry(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _SERVER_REGISTRIES.get(server) + if registry is None: + registry = _ServerToolRegistry(server) + _SERVER_REGISTRIES[server] = registry + return registry + +_RESERVED_TOOL_NAMES_ATTR = "_protoc_gen_mcp_reserved_tool_names" + +def _get_reserved_tool_names(server: mcp.server.lowlevel.Server) -> set[str]: + reserved = getattr(server, _RESERVED_TOOL_NAMES_ATTR, None) + if reserved is None: + reserved = set() + setattr(server, _RESERVED_TOOL_NAMES_ATTR, reserved) + return reserved + +def _build_list_tools_request(request: Any) -> mcp.types.ListToolsRequest: + if isinstance(request, mcp.types.ListToolsRequest): + return request + return mcp.types.ListToolsRequest() + +def _merge_tools(previous: list[mcp.types.Tool], current: list[mcp.types.Tool]) -> list[mcp.types.Tool]: + merged: list[mcp.types.Tool] = [] + names: set[str] = set() + for tool in previous: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + for tool in current: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + return merged + +def _install_server_handlers(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _get_registry(server) + if registry.handlers_installed: + return registry + registry.previous_list_tools = server.request_handlers.get(mcp.types.ListToolsRequest) + registry.previous_call_tool = server.request_handlers.get(mcp.types.CallToolRequest) + + async def _list_tools(request: Any) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + list_request = _build_list_tools_request(request) + previous_tools: list[mcp.types.Tool] = [] + meta: dict[str, Any] | None = None + if current is not None: + if current.previous_list_tools is not None: + previous_result = await current.previous_list_tools(list_request) + if not isinstance(previous_result.root, mcp.types.ListToolsResult): + return previous_result + if getattr(list_request.params, "cursor", None) is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + if previous_result.root.nextCursor is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + meta = getattr(previous_result.root, "meta", None) + previous_tools = list(previous_result.root.tools) + current_tools = [] if current is None else current.list_tools() + tools = _merge_tools(previous_tools, current_tools) + server._tool_cache.clear() + for tool in tools: + server._tool_cache[tool.name] = tool + return mcp.types.ServerResult(mcp.types.ListToolsResult(tools=tools, _meta=meta)) + + async def _call_tool(request: mcp.types.CallToolRequest) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + if current is None: + return mcp.types.ServerResult(_tool_error_result("tool registry is not installed")) + if request.params.name not in current.tools: + if current.previous_call_tool is not None: + return await current.previous_call_tool(request) + _invalid_params_error(request.params.name, "unknown tool") + if current.previous_call_tool is not None: + await server.request_handlers[mcp.types.ListToolsRequest](mcp.types.ListToolsRequest()) + result = await current.call_tool(request.params.name, request.params.arguments or {}, server.request_context.session) + return mcp.types.ServerResult(result) + + server.request_handlers[mcp.types.ListToolsRequest] = _list_tools + server.request_handlers[mcp.types.CallToolRequest] = _call_tool + + registry.handlers_installed = True + return registry + +def _tool_annotations(raw: dict[str, Any] | None) -> mcp.types.ToolAnnotations | None: + if raw is None: + return None + return mcp.types.ToolAnnotations(**raw) + +def _tool_error_result(message: str) -> mcp.types.CallToolResult: + return mcp.types.CallToolResult( + content=[mcp.types.TextContent(type="text", text=message)], + isError=True, + ) + +def _invalid_params_error(tool_name: str, error: Any) -> mcp.shared.exceptions.McpError: + raise mcp.shared.exceptions.McpError( + mcp.types.ErrorData( + code=mcp.types.INVALID_PARAMS, + message=f"invalid arguments for tool {tool_name!r}: {error}", + ) + ) + +def _build_tool(tool: _RegisteredTool) -> Any: + return mcp.types.Tool( + name=tool.name, + title=tool.title, + description=tool.description, + inputSchema=_load_schema(tool.input_schema_json), + outputSchema=_load_schema(tool.output_schema_json), + annotations=_tool_annotations(tool.annotations), + icons=tool.icons, + ) + +async def _maybe_await(result: Any) -> Any: + if inspect.isawaitable(result): + return await result + return result + +def _message_to_json(message: Any) -> str: + kwargs = {"preserving_proto_field_name": False} + try: + return json_format.MessageToJson(message, always_print_fields_with_no_presence=True, **kwargs) + except TypeError: + return json_format.MessageToJson(message, including_default_value_fields=True, **kwargs) + +async def _dispatch_call(registry: _ServerToolRegistry, name: str, arguments: dict[str, Any], context: ToolRequestContext | None) -> mcp.types.CallToolResult: + tool = registry.tools.get(name) + if tool is None: + _invalid_params_error(name, "unknown tool") + try: + jsonschema.validate(arguments, _load_schema(tool.input_schema_json)) + except jsonschema.ValidationError as error: + _invalid_params_error(name, error) + try: + request_pb = _protojson_to_message(arguments, tool.request_type()) + request_dc = tool.from_pb(request_pb) + except Exception as error: + _invalid_params_error(name, error) + try: + response_dc = await _maybe_await(tool.handler(context, request_dc)) + except Exception as error: + return _tool_error_result(str(error)) + try: + if response_dc is None: + response_pb = tool.response_type() + else: + response_pb = tool.to_pb(response_dc) + payload = json.loads(_message_to_json(response_pb)) + except Exception as error: + raise RuntimeError(f"mcpruntime: marshal output for tool {name!r}: {error}") from error + text = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + content = [mcp.types.TextContent(type="text", text=text)] + try: + jsonschema.validate(payload, _load_schema(tool.output_schema_json)) + except jsonschema.ValidationError as error: + raise RuntimeError(f"mcpruntime: validate output for tool {name!r}: {error}") from error + return mcp.types.CallToolResult(content=content, structuredContent=payload) + +def _enum_from_pb(enum_type: type[enum.IntEnum], value: int) -> enum.IntEnum: + return enum_type(value) + +def _json_to_message(value: Any, message: Any) -> Any: + json_format.Parse(json.dumps(value), message) + return message + +def _from_pb_any(message: any_pb2.Any) -> ProtoAny: + return json.loads(_message_to_json(message)) + +def _to_pb_any(value: ProtoAny) -> any_pb2.Any: + return _json_to_message(value, any_pb2.Any()) + +def _from_pb_timestamp(message: timestamp_pb2.Timestamp) -> Timestamp: + return json.loads(_message_to_json(message)) + +def _to_pb_timestamp(value: Timestamp) -> timestamp_pb2.Timestamp: + return _json_to_message(value, timestamp_pb2.Timestamp()) + +def _from_pb_duration(message: duration_pb2.Duration) -> Duration: + return json.loads(_message_to_json(message)) + +def _to_pb_duration(value: Duration) -> duration_pb2.Duration: + return _json_to_message(value, duration_pb2.Duration()) + +def _from_pb_field_mask(message: field_mask_pb2.FieldMask) -> FieldMask: + return json.loads(_message_to_json(message)) + +def _to_pb_field_mask(value: FieldMask) -> field_mask_pb2.FieldMask: + return _json_to_message(value, field_mask_pb2.FieldMask()) + +def _from_pb_struct(message: struct_pb2.Struct) -> Struct: + return json.loads(_message_to_json(message)) + +def _to_pb_struct(value: Struct) -> struct_pb2.Struct: + return _json_to_message(value, struct_pb2.Struct()) + +def _from_pb_list_value(message: struct_pb2.ListValue) -> ListValue: + return json.loads(_message_to_json(message)) + +def _to_pb_list_value(value: ListValue) -> struct_pb2.ListValue: + return _json_to_message(value, struct_pb2.ListValue()) + +def _from_pb_value(message: struct_pb2.Value) -> Value: + return json.loads(_message_to_json(message)) + +def _to_pb_value(value: Value) -> struct_pb2.Value: + return _json_to_message(value, struct_pb2.Value()) + +def _from_pb_empty(message: empty_pb2.Empty) -> Empty: + return Empty() + +def _to_pb_empty(value: Empty) -> empty_pb2.Empty: + return empty_pb2.Empty() + +def _from_pb_bool_value(message: wrappers_pb2.BoolValue) -> BoolValue: + return message.value + +def _to_pb_bool_value(value: BoolValue) -> wrappers_pb2.BoolValue: + return wrappers_pb2.BoolValue(value=value) + +def _from_pb_string_value(message: wrappers_pb2.StringValue) -> StringValue: + return message.value + +def _to_pb_string_value(value: StringValue) -> wrappers_pb2.StringValue: + return wrappers_pb2.StringValue(value=value) + +def _from_pb_bytes_value(message: wrappers_pb2.BytesValue) -> BytesValue: + return message.value + +def _to_pb_bytes_value(value: BytesValue) -> wrappers_pb2.BytesValue: + return wrappers_pb2.BytesValue(value=value) + +def _from_pb_int32_value(message: wrappers_pb2.Int32Value) -> Int32Value: + return message.value + +def _to_pb_int32_value(value: Int32Value) -> wrappers_pb2.Int32Value: + return wrappers_pb2.Int32Value(value=value) + +def _from_pb_uint32_value(message: wrappers_pb2.UInt32Value) -> UInt32Value: + return message.value + +def _to_pb_uint32_value(value: UInt32Value) -> wrappers_pb2.UInt32Value: + return wrappers_pb2.UInt32Value(value=value) + +def _from_pb_int64_value(message: wrappers_pb2.Int64Value) -> Int64Value: + return message.value + +def _to_pb_int64_value(value: Int64Value) -> wrappers_pb2.Int64Value: + return wrappers_pb2.Int64Value(value=value) + +def _from_pb_uint64_value(message: wrappers_pb2.UInt64Value) -> UInt64Value: + return message.value + +def _to_pb_uint64_value(value: UInt64Value) -> wrappers_pb2.UInt64Value: + return wrappers_pb2.UInt64Value(value=value) + +def _from_pb_float_value(message: wrappers_pb2.FloatValue) -> FloatValue: + return message.value + +def _to_pb_float_value(value: FloatValue) -> wrappers_pb2.FloatValue: + return wrappers_pb2.FloatValue(value=value) + +def _from_pb_double_value(message: wrappers_pb2.DoubleValue) -> DoubleValue: + return message.value + +def _to_pb_double_value(value: DoubleValue) -> wrappers_pb2.DoubleValue: + return wrappers_pb2.DoubleValue(value=value) + +def _from_pb_icon(message: options_pb2.Icon) -> Icon: + return Icon( + src=message.src, + mime_type=message.mime_type, + sizes=list(message.sizes), + theme=message.theme, + ) + +def _to_pb_icon(value: Icon) -> options_pb2.Icon: + message = options_pb2.Icon() + message.src = value.src + message.mime_type = value.mime_type + message.sizes.extend(value.sizes) + message.theme = value.theme + return message + +def _from_pb_service_options(message: options_pb2.ServiceOptions) -> ServiceOptions: + return ServiceOptions( + namespace=message.namespace, + description=message.description, + icons=[_from_pb_icon(item) for item in message.icons], + ) + +def _to_pb_service_options(value: ServiceOptions) -> options_pb2.ServiceOptions: + message = options_pb2.ServiceOptions() + message.namespace = value.namespace + message.description = value.description + message.icons.extend(_to_pb_icon(item) for item in value.icons) + return message + +def _from_pb_tool_annotations(message: options_pb2.ToolAnnotations) -> ToolAnnotations: + return ToolAnnotations( + read_only_hint=message.read_only_hint, + destructive_hint=message.destructive_hint if message.HasField("destructive_hint") else UNSET, + idempotent_hint=message.idempotent_hint, + open_world_hint=message.open_world_hint if message.HasField("open_world_hint") else UNSET, + title=message.title, + ) + +def _to_pb_tool_annotations(value: ToolAnnotations) -> options_pb2.ToolAnnotations: + message = options_pb2.ToolAnnotations() + message.read_only_hint = value.read_only_hint + if value.destructive_hint is not UNSET: + message.destructive_hint = value.destructive_hint + message.idempotent_hint = value.idempotent_hint + if value.open_world_hint is not UNSET: + message.open_world_hint = value.open_world_hint + message.title = value.title + return message + +def _from_pb_execution_options(message: options_pb2.ExecutionOptions) -> ExecutionOptions: + return ExecutionOptions( + task_support=_enum_from_pb(TaskSupport, message.task_support), + ) + +def _to_pb_execution_options(value: ExecutionOptions) -> options_pb2.ExecutionOptions: + message = options_pb2.ExecutionOptions() + message.task_support = int(value.task_support) + return message + +def _from_pb_method_options(message: options_pb2.MethodOptions) -> MethodOptions: + return MethodOptions( + name=message.name, + title=message.title, + description=message.description, + hidden=message.hidden, + annotations=_from_pb_tool_annotations(message.annotations), + icons=[_from_pb_icon(item) for item in message.icons], + execution=_from_pb_execution_options(message.execution), + ) + +def _to_pb_method_options(value: MethodOptions) -> options_pb2.MethodOptions: + message = options_pb2.MethodOptions() + message.name = value.name + message.title = value.title + message.description = value.description + message.hidden = value.hidden + message.annotations.CopyFrom(_to_pb_tool_annotations(value.annotations)) + message.icons.extend(_to_pb_icon(item) for item in value.icons) + message.execution.CopyFrom(_to_pb_execution_options(value.execution)) + return message + +def _from_pb_example_object(message: options_pb2.ExampleObject) -> ExampleObject: + return ExampleObject( + properties={key: _from_pb_example_value(item) for key, item in message.properties.items()}, + ) + +def _to_pb_example_object(value: ExampleObject) -> options_pb2.ExampleObject: + message = options_pb2.ExampleObject() + for key, item in value.properties.items(): + message.properties[key].CopyFrom(_to_pb_example_value(item)) + return message + +def _from_pb_example_array(message: options_pb2.ExampleArray) -> ExampleArray: + return ExampleArray( + items=[_from_pb_example_value(item) for item in message.items], + ) + +def _to_pb_example_array(value: ExampleArray) -> options_pb2.ExampleArray: + message = options_pb2.ExampleArray() + message.items.extend(_to_pb_example_value(item) for item in value.items) + return message + +def _from_pb_example_value(message: options_pb2.ExampleValue) -> ExampleValue: + kind = UNSET + kind_case = message.WhichOneof("kind") + if kind_case == "string_value": + kind = ExampleValueKindStringValueVariant(string_value=message.string_value) + elif kind_case == "number_value": + kind = ExampleValueKindNumberValueVariant(number_value=message.number_value) + elif kind_case == "bool_value": + kind = ExampleValueKindBoolValueVariant(bool_value=message.bool_value) + elif kind_case == "object_value": + kind = ExampleValueKindObjectValueVariant(object_value=_from_pb_example_object(message.object_value)) + elif kind_case == "array_value": + kind = ExampleValueKindArrayValueVariant(array_value=_from_pb_example_array(message.array_value)) + elif kind_case == "null_value": + kind = ExampleValueKindNullValueVariant(null_value=message.null_value) + elif kind_case == "integer_value": + kind = ExampleValueKindIntegerValueVariant(integer_value=message.integer_value) + return ExampleValue( + kind=kind, + ) + +def _to_pb_example_value(value: ExampleValue) -> options_pb2.ExampleValue: + message = options_pb2.ExampleValue() + if value.kind is not UNSET: + if isinstance(value.kind, ExampleValueKindStringValueVariant): + message.string_value = value.kind.string_value + elif isinstance(value.kind, ExampleValueKindNumberValueVariant): + message.number_value = value.kind.number_value + elif isinstance(value.kind, ExampleValueKindBoolValueVariant): + message.bool_value = value.kind.bool_value + elif isinstance(value.kind, ExampleValueKindObjectValueVariant): + message.object_value.CopyFrom(_to_pb_example_object(value.kind.object_value)) + elif isinstance(value.kind, ExampleValueKindArrayValueVariant): + message.array_value.CopyFrom(_to_pb_example_array(value.kind.array_value)) + elif isinstance(value.kind, ExampleValueKindNullValueVariant): + message.null_value = value.kind.null_value + elif isinstance(value.kind, ExampleValueKindIntegerValueVariant): + message.integer_value = value.kind.integer_value + else: + raise TypeError("unsupported ExampleValueKindVariant variant: " + type(value.kind).__name__) + + return message + +def _from_pb_field_options(message: options_pb2.FieldOptions) -> FieldOptions: + return FieldOptions( + description=message.description, + examples=[_from_pb_example_value(item) for item in message.examples], + default_value=_from_pb_example_value(message.default_value), + pattern=message.pattern, + format=message.format, + min_length=message.min_length if message.HasField("min_length") else UNSET, + max_length=message.max_length if message.HasField("max_length") else UNSET, + minimum=message.minimum if message.HasField("minimum") else UNSET, + maximum=message.maximum if message.HasField("maximum") else UNSET, + exclusive_minimum=message.exclusive_minimum if message.HasField("exclusive_minimum") else UNSET, + exclusive_maximum=message.exclusive_maximum if message.HasField("exclusive_maximum") else UNSET, + multiple_of=message.multiple_of if message.HasField("multiple_of") else UNSET, + min_items=message.min_items if message.HasField("min_items") else UNSET, + max_items=message.max_items if message.HasField("max_items") else UNSET, + unique_items=message.unique_items, + read_only=message.read_only, + ) + +def _to_pb_field_options(value: FieldOptions) -> options_pb2.FieldOptions: + message = options_pb2.FieldOptions() + message.description = value.description + message.examples.extend(_to_pb_example_value(item) for item in value.examples) + message.default_value.CopyFrom(_to_pb_example_value(value.default_value)) + message.pattern = value.pattern + message.format = value.format + if value.min_length is not UNSET: + message.min_length = value.min_length + if value.max_length is not UNSET: + message.max_length = value.max_length + if value.minimum is not UNSET: + message.minimum = value.minimum + if value.maximum is not UNSET: + message.maximum = value.maximum + if value.exclusive_minimum is not UNSET: + message.exclusive_minimum = value.exclusive_minimum + if value.exclusive_maximum is not UNSET: + message.exclusive_maximum = value.exclusive_maximum + if value.multiple_of is not UNSET: + message.multiple_of = value.multiple_of + if value.min_items is not UNSET: + message.min_items = value.min_items + if value.max_items is not UNSET: + message.max_items = value.max_items + message.unique_items = value.unique_items + message.read_only = value.read_only + return message + +def _from_pb_oneof_options(message: options_pb2.OneofOptions) -> OneofOptions: + return OneofOptions( + description=message.description, + required=message.required, + ) + +def _to_pb_oneof_options(value: OneofOptions) -> options_pb2.OneofOptions: + message = options_pb2.OneofOptions() + message.description = value.description + message.required = value.required + return message + +def _from_pb_message_options(message: options_pb2.MessageOptions) -> MessageOptions: + return MessageOptions( + title=message.title, + description=message.description, + examples=[_from_pb_example_object(item) for item in message.examples], + ) + +def _to_pb_message_options(value: MessageOptions) -> options_pb2.MessageOptions: + message = options_pb2.MessageOptions() + message.title = value.title + message.description = value.description + message.examples.extend(_to_pb_example_object(item) for item in value.examples) + return message + +def _from_pb_enum_options(message: options_pb2.EnumOptions) -> EnumOptions: + return EnumOptions( + title=message.title, + description=message.description, + ) + +def _to_pb_enum_options(value: EnumOptions) -> options_pb2.EnumOptions: + message = options_pb2.EnumOptions() + message.title = value.title + message.description = value.description + return message + +def _from_pb_enum_value_options(message: options_pb2.EnumValueOptions) -> EnumValueOptions: + return EnumValueOptions( + description=message.description, + hidden=message.hidden, + ) + +def _to_pb_enum_value_options(value: EnumValueOptions) -> options_pb2.EnumValueOptions: + message = options_pb2.EnumValueOptions() + message.description = value.description + message.hidden = value.hidden + return message + diff --git a/mcp/options/v1/options_pb2.py b/mcp/options/v1/options_pb2.py new file mode 100644 index 0000000..fe8a59f --- /dev/null +++ b/mcp/options/v1/options_pb2.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: mcp/options/v1/options.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'mcp/options/v1/options.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cmcp/options/v1/options.proto\x12\x0emcp.options.v1\x1a google/protobuf/descriptor.proto\"|\n\x0eServiceOptions\x12\x1c\n\tnamespace\x18\x01 \x01(\tR\tnamespace\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12*\n\x05icons\x18\x03 \x03(\x0b\x32\x14.mcp.options.v1.IconR\x05icons\"\xa2\x02\n\rMethodOptions\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n\x05title\x18\x02 \x01(\tR\x05title\x12 \n\x0b\x64\x65scription\x18\x03 \x01(\tR\x0b\x64\x65scription\x12\x16\n\x06hidden\x18\x04 \x01(\x08R\x06hidden\x12\x41\n\x0b\x61nnotations\x18\n \x01(\x0b\x32\x1f.mcp.options.v1.ToolAnnotationsR\x0b\x61nnotations\x12*\n\x05icons\x18\x0b \x03(\x0b\x32\x14.mcp.options.v1.IconR\x05icons\x12>\n\texecution\x18\x0c \x01(\x0b\x32 .mcp.options.v1.ExecutionOptionsR\texecution\"\x81\x06\n\x0c\x46ieldOptions\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x38\n\x08\x65xamples\x18\x03 \x03(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x08\x65xamples\x12\x41\n\rdefault_value\x18\x04 \x01(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x0c\x64\x65\x66\x61ultValue\x12\x18\n\x07pattern\x18\n \x01(\tR\x07pattern\x12\x16\n\x06\x66ormat\x18\x0b \x01(\tR\x06\x66ormat\x12\"\n\nmin_length\x18\x0c \x01(\rH\x00R\tminLength\x88\x01\x01\x12\"\n\nmax_length\x18\r \x01(\rH\x01R\tmaxLength\x88\x01\x01\x12\x1d\n\x07minimum\x18\x14 \x01(\x01H\x02R\x07minimum\x88\x01\x01\x12\x1d\n\x07maximum\x18\x15 \x01(\x01H\x03R\x07maximum\x88\x01\x01\x12\x30\n\x11\x65xclusive_minimum\x18\x16 \x01(\x01H\x04R\x10\x65xclusiveMinimum\x88\x01\x01\x12\x30\n\x11\x65xclusive_maximum\x18\x17 \x01(\x01H\x05R\x10\x65xclusiveMaximum\x88\x01\x01\x12$\n\x0bmultiple_of\x18\x18 \x01(\x01H\x06R\nmultipleOf\x88\x01\x01\x12 \n\tmin_items\x18\x1e \x01(\rH\x07R\x08minItems\x88\x01\x01\x12 \n\tmax_items\x18\x1f \x01(\rH\x08R\x08maxItems\x88\x01\x01\x12!\n\x0cunique_items\x18 \x01(\x08R\x0buniqueItems\x12\x1b\n\tread_only\x18( \x01(\x08R\x08readOnlyB\r\n\x0b_min_lengthB\r\n\x0b_max_lengthB\n\n\x08_minimumB\n\n\x08_maximumB\x14\n\x12_exclusive_minimumB\x14\n\x12_exclusive_maximumB\x0e\n\x0c_multiple_ofB\x0c\n\n_min_itemsB\x0c\n\n_max_items\"\xce\x02\n\x0c\x45xampleValue\x12#\n\x0cstring_value\x18\x01 \x01(\tH\x00R\x0bstringValue\x12#\n\x0cnumber_value\x18\x02 \x01(\x01H\x00R\x0bnumberValue\x12\x1f\n\nbool_value\x18\x03 \x01(\x08H\x00R\tboolValue\x12\x42\n\x0cobject_value\x18\x04 \x01(\x0b\x32\x1d.mcp.options.v1.ExampleObjectH\x00R\x0bobjectValue\x12?\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32\x1c.mcp.options.v1.ExampleArrayH\x00R\narrayValue\x12\x1f\n\nnull_value\x18\x06 \x01(\x08H\x00R\tnullValue\x12%\n\rinteger_value\x18\x07 \x01(\x03H\x00R\x0cintegerValueB\x06\n\x04kind\"\xbb\x01\n\rExampleObject\x12M\n\nproperties\x18\x01 \x03(\x0b\x32-.mcp.options.v1.ExampleObject.PropertiesEntryR\nproperties\x1a[\n\x0fPropertiesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x05value:\x02\x38\x01\"B\n\x0c\x45xampleArray\x12\x32\n\x05items\x18\x01 \x03(\x0b\x32\x1c.mcp.options.v1.ExampleValueR\x05items\"\xfc\x01\n\x0fToolAnnotations\x12$\n\x0eread_only_hint\x18\x01 \x01(\x08R\x0creadOnlyHint\x12.\n\x10\x64\x65structive_hint\x18\x02 \x01(\x08H\x00R\x0f\x64\x65structiveHint\x88\x01\x01\x12\'\n\x0fidempotent_hint\x18\x03 \x01(\x08R\x0eidempotentHint\x12+\n\x0fopen_world_hint\x18\x04 \x01(\x08H\x01R\ropenWorldHint\x88\x01\x01\x12\x14\n\x05title\x18\x05 \x01(\tR\x05titleB\x13\n\x11_destructive_hintB\x12\n\x10_open_world_hint\"a\n\x04Icon\x12\x10\n\x03src\x18\x01 \x01(\tR\x03src\x12\x1b\n\tmime_type\x18\x02 \x01(\tR\x08mimeType\x12\x14\n\x05sizes\x18\x03 \x03(\tR\x05sizes\x12\x14\n\x05theme\x18\x04 \x01(\tR\x05theme\"R\n\x10\x45xecutionOptions\x12>\n\x0ctask_support\x18\x01 \x01(\x0e\x32\x1b.mcp.options.v1.TaskSupportR\x0btaskSupport\"L\n\x0cOneofOptions\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08required\x18\x02 \x01(\x08R\x08required\"\x83\x01\n\x0eMessageOptions\x12\x14\n\x05title\x18\x01 \x01(\tR\x05title\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x39\n\x08\x65xamples\x18\x03 \x03(\x0b\x32\x1d.mcp.options.v1.ExampleObjectR\x08\x65xamples\"E\n\x0b\x45numOptions\x12\x14\n\x05title\x18\x01 \x01(\tR\x05title\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\"L\n\x10\x45numValueOptions\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x16\n\x06hidden\x18\x02 \x01(\x08R\x06hidden*Z\n\x0bTaskSupport\x12\x15\n\x11TASK_SUPPORT_NONE\x10\x00\x12\x19\n\x15TASK_SUPPORT_OPTIONAL\x10\x01\x12\x19\n\x15TASK_SUPPORT_REQUIRED\x10\x02:[\n\x07service\x12\x1f.google.protobuf.ServiceOptions\x18\xf9\xc6\x05 \x01(\x0b\x32\x1e.mcp.options.v1.ServiceOptionsR\x07service:W\n\x06method\x12\x1e.google.protobuf.MethodOptions\x18\xfa\xc6\x05 \x01(\x0b\x32\x1d.mcp.options.v1.MethodOptionsR\x06method:S\n\x05\x66ield\x12\x1d.google.protobuf.FieldOptions\x18\xfb\xc6\x05 \x01(\x0b\x32\x1c.mcp.options.v1.FieldOptionsR\x05\x66ield:[\n\x07message\x12\x1f.google.protobuf.MessageOptions\x18\xfc\xc6\x05 \x01(\x0b\x32\x1e.mcp.options.v1.MessageOptionsR\x07message:O\n\x04\x65num\x12\x1c.google.protobuf.EnumOptions\x18\xfd\xc6\x05 \x01(\x0b\x32\x1b.mcp.options.v1.EnumOptionsR\x04\x65num:d\n\nenum_value\x12!.google.protobuf.EnumValueOptions\x18\xfe\xc6\x05 \x01(\x0b\x32 .mcp.options.v1.EnumValueOptionsR\tenumValue:S\n\x05oneof\x12\x1d.google.protobuf.OneofOptions\x18\xff\xc6\x05 \x01(\x0b\x32\x1c.mcp.options.v1.OneofOptionsR\x05oneofB?Z=github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1;optionsv1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcp.options.v1.options_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/easyp-tech/protoc-gen-mcp/mcp/options/v1;optionsv1' + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._loaded_options = None + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._serialized_options = b'8\001' + _globals['_TASKSUPPORT']._serialized_start=2667 + _globals['_TASKSUPPORT']._serialized_end=2757 + _globals['_SERVICEOPTIONS']._serialized_start=82 + _globals['_SERVICEOPTIONS']._serialized_end=206 + _globals['_METHODOPTIONS']._serialized_start=209 + _globals['_METHODOPTIONS']._serialized_end=499 + _globals['_FIELDOPTIONS']._serialized_start=502 + _globals['_FIELDOPTIONS']._serialized_end=1271 + _globals['_EXAMPLEVALUE']._serialized_start=1274 + _globals['_EXAMPLEVALUE']._serialized_end=1608 + _globals['_EXAMPLEOBJECT']._serialized_start=1611 + _globals['_EXAMPLEOBJECT']._serialized_end=1798 + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._serialized_start=1707 + _globals['_EXAMPLEOBJECT_PROPERTIESENTRY']._serialized_end=1798 + _globals['_EXAMPLEARRAY']._serialized_start=1800 + _globals['_EXAMPLEARRAY']._serialized_end=1866 + _globals['_TOOLANNOTATIONS']._serialized_start=1869 + _globals['_TOOLANNOTATIONS']._serialized_end=2121 + _globals['_ICON']._serialized_start=2123 + _globals['_ICON']._serialized_end=2220 + _globals['_EXECUTIONOPTIONS']._serialized_start=2222 + _globals['_EXECUTIONOPTIONS']._serialized_end=2304 + _globals['_ONEOFOPTIONS']._serialized_start=2306 + _globals['_ONEOFOPTIONS']._serialized_end=2382 + _globals['_MESSAGEOPTIONS']._serialized_start=2385 + _globals['_MESSAGEOPTIONS']._serialized_end=2516 + _globals['_ENUMOPTIONS']._serialized_start=2518 + _globals['_ENUMOPTIONS']._serialized_end=2587 + _globals['_ENUMVALUEOPTIONS']._serialized_start=2589 + _globals['_ENUMVALUEOPTIONS']._serialized_end=2665 +# @@protoc_insertion_point(module_scope) diff --git a/testdata/golden/example_mcp.py.golden b/testdata/golden/example_mcp.py.golden new file mode 100644 index 0000000..2e03300 --- /dev/null +++ b/testdata/golden/example_mcp.py.golden @@ -0,0 +1,1167 @@ +# Code generated by protoc-gen-mcp. DO NOT EDIT. +# source: internal/testproto/example/v1/example.proto +from __future__ import annotations + +import enum +import inspect +import json +import weakref +from dataclasses import dataclass, field +from typing import Any, Awaitable, Protocol, TypeAlias + +import jsonschema +import mcp.server.lowlevel +import mcp.server.session +import mcp.shared.exceptions +import mcp.types +from google.protobuf import any_pb2, duration_pb2, empty_pb2, field_mask_pb2, json_format, struct_pb2, timestamp_pb2, wrappers_pb2 +try: + from . import example_pb2 +except ImportError: + import example_pb2 + +ToolRequestContext = mcp.server.session.ServerSession + +class _UnsetType: + __slots__ = () + + def __repr__(self) -> str: + return "UNSET" + +UNSET = _UnsetType() + +JSONValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None +ProtoAny: TypeAlias = dict[str, Any] +Timestamp: TypeAlias = str +Duration: TypeAlias = str +FieldMask: TypeAlias = str +Struct: TypeAlias = dict[str, Any] +Value: TypeAlias = JSONValue +ListValue: TypeAlias = list[Any] +BoolValue: TypeAlias = bool +StringValue: TypeAlias = str +BytesValue: TypeAlias = bytes +Int32Value: TypeAlias = int +UInt32Value: TypeAlias = int +Int64Value: TypeAlias = int +UInt64Value: TypeAlias = int +FloatValue: TypeAlias = float +DoubleValue: TypeAlias = float + +@dataclass(slots=True) +class Empty: + pass + +@dataclass(slots=True) +class ReportDetails: + label: str + +@dataclass(slots=True) +class CreateReportRequest: + city: str + count: int + details: ReportDetails + units: str | _UnsetType = UNSET + labels: list[str] = field(default_factory=list) + +class ReportStatus(enum.IntEnum): + REPORT_STATUS_OK = 1 + REPORT_STATUS_FAILED = 2 + +@dataclass(slots=True) +class CreateReportResponse: + report_id: str + total_count: int + status: ReportStatus + details: ReportDetails + warnings: list[str] = field(default_factory=list) + +@dataclass(slots=True) +class PingRequest: + pass + +@dataclass(slots=True) +class PingResponse: + ack: Empty + +@dataclass(slots=True) +class RecursiveNode: + name: str + child: RecursiveNode | _UnsetType = UNSET + children: list[RecursiveNode] = field(default_factory=list) + +@dataclass(slots=True) +class DescribeAdvancedShapesRequestSelectorVariant: + pass + +@dataclass(slots=True) +class DescribeAdvancedShapesRequestSelectorCityAliasVariant(DescribeAdvancedShapesRequestSelectorVariant): + city_alias: str + +@dataclass(slots=True) +class DescribeAdvancedShapesRequestSelectorCityIdVariant(DescribeAdvancedShapesRequestSelectorVariant): + city_id: int + +@dataclass(slots=True) +class DescribeAdvancedShapesRequestSelectorCityDetailsVariant(DescribeAdvancedShapesRequestSelectorVariant): + city_details: ReportDetails + +@dataclass(slots=True) +class DescribeAdvancedShapesRequest: + labels: dict[str, str] = field(default_factory=dict) + quantities: dict[int, str] = field(default_factory=dict) + toggles: dict[bool, str] = field(default_factory=dict) + limits: dict[int, str] = field(default_factory=dict) + observed_at: Timestamp | _UnsetType = UNSET + ttl: Duration | _UnsetType = UNSET + payload: Struct | _UnsetType = UNSET + items: ListValue | _UnsetType = UNSET + dynamic: Value | _UnsetType = UNSET + note: StringValue | _UnsetType = UNSET + total: Int64Value | _UnsetType = UNSET + enabled: BoolValue | _UnsetType = UNSET + ratio: DoubleValue | _UnsetType = UNSET + mask: FieldMask | _UnsetType = UNSET + blob: BytesValue | _UnsetType = UNSET + small_total: Int32Value | _UnsetType = UNSET + uint_total: UInt32Value | _UnsetType = UNSET + huge_total: UInt64Value | _UnsetType = UNSET + weight: FloatValue | _UnsetType = UNSET + raw_ratio: float | _UnsetType = UNSET + tree: RecursiveNode | _UnsetType = UNSET + detail_any: ProtoAny | _UnsetType = UNSET + duration_any: ProtoAny | _UnsetType = UNSET + selector: DescribeAdvancedShapesRequestSelectorVariant | _UnsetType = UNSET + +@dataclass(slots=True) +class DescribeAdvancedShapesResponseSelectorVariant: + pass + +@dataclass(slots=True) +class DescribeAdvancedShapesResponseSelectorCityAliasVariant(DescribeAdvancedShapesResponseSelectorVariant): + city_alias: str + +@dataclass(slots=True) +class DescribeAdvancedShapesResponseSelectorCityIdVariant(DescribeAdvancedShapesResponseSelectorVariant): + city_id: int + +@dataclass(slots=True) +class DescribeAdvancedShapesResponseSelectorCityDetailsVariant(DescribeAdvancedShapesResponseSelectorVariant): + city_details: ReportDetails + +@dataclass(slots=True) +class DescribeAdvancedShapesResponse: + labels: dict[str, str] = field(default_factory=dict) + quantities: dict[int, str] = field(default_factory=dict) + toggles: dict[bool, str] = field(default_factory=dict) + limits: dict[int, str] = field(default_factory=dict) + observed_at: Timestamp | _UnsetType = UNSET + ttl: Duration | _UnsetType = UNSET + payload: Struct | _UnsetType = UNSET + items: ListValue | _UnsetType = UNSET + dynamic: Value | _UnsetType = UNSET + note: StringValue | _UnsetType = UNSET + total: Int64Value | _UnsetType = UNSET + enabled: BoolValue | _UnsetType = UNSET + ratio: DoubleValue | _UnsetType = UNSET + mask: FieldMask | _UnsetType = UNSET + blob: BytesValue | _UnsetType = UNSET + small_total: Int32Value | _UnsetType = UNSET + uint_total: UInt32Value | _UnsetType = UNSET + huge_total: UInt64Value | _UnsetType = UNSET + weight: FloatValue | _UnsetType = UNSET + raw_ratio: float | _UnsetType = UNSET + tree: RecursiveNode | _UnsetType = UNSET + detail_any: ProtoAny | _UnsetType = UNSET + duration_any: ProtoAny | _UnsetType = UNSET + selector: DescribeAdvancedShapesResponseSelectorVariant | _UnsetType = UNSET + +@dataclass(slots=True) +class DescribeScalarShapesRequest: + bool_flag: bool + text_value: str + bytes_value: bytes + int32_value: int + sint32_value: int + sfixed32_value: int + uint32_value: int + fixed32_value: int + int64_value: int + sint64_value: int + sfixed64_value: int + uint64_value: int + fixed64_value: int + float_value: float + double_value: float + status: ReportStatus + details: ReportDetails + samples: list[int] = field(default_factory=list) + optional_bool_flag: bool | _UnsetType = UNSET + optional_text_value: str | _UnsetType = UNSET + optional_bytes_value: bytes | _UnsetType = UNSET + optional_int32_value: int | _UnsetType = UNSET + optional_sint32_value: int | _UnsetType = UNSET + optional_sfixed32_value: int | _UnsetType = UNSET + optional_uint32_value: int | _UnsetType = UNSET + optional_fixed32_value: int | _UnsetType = UNSET + optional_int64_value: int | _UnsetType = UNSET + optional_sint64_value: int | _UnsetType = UNSET + optional_sfixed64_value: int | _UnsetType = UNSET + optional_uint64_value: int | _UnsetType = UNSET + optional_fixed64_value: int | _UnsetType = UNSET + optional_float_value: float | _UnsetType = UNSET + optional_double_value: float | _UnsetType = UNSET + optional_status: ReportStatus | _UnsetType = UNSET + +@dataclass(slots=True) +class DescribeScalarShapesResponse: + bool_flag: bool + text_value: str + bytes_value: bytes + int32_value: int + sint32_value: int + sfixed32_value: int + uint32_value: int + fixed32_value: int + int64_value: int + sint64_value: int + sfixed64_value: int + uint64_value: int + fixed64_value: int + float_value: float + double_value: float + status: ReportStatus + details: ReportDetails + samples: list[int] = field(default_factory=list) + optional_bool_flag: bool | _UnsetType = UNSET + optional_text_value: str | _UnsetType = UNSET + optional_bytes_value: bytes | _UnsetType = UNSET + optional_int32_value: int | _UnsetType = UNSET + optional_sint32_value: int | _UnsetType = UNSET + optional_sfixed32_value: int | _UnsetType = UNSET + optional_uint32_value: int | _UnsetType = UNSET + optional_fixed32_value: int | _UnsetType = UNSET + optional_int64_value: int | _UnsetType = UNSET + optional_sint64_value: int | _UnsetType = UNSET + optional_sfixed64_value: int | _UnsetType = UNSET + optional_uint64_value: int | _UnsetType = UNSET + optional_fixed64_value: int | _UnsetType = UNSET + optional_float_value: float | _UnsetType = UNSET + optional_double_value: float | _UnsetType = UNSET + optional_status: ReportStatus | _UnsetType = UNSET + +@dataclass(slots=True) +class HiddenThingRequest: + name: str + +@dataclass(slots=True) +class HiddenThingResponse: + pass + +@dataclass(frozen=True) +class _RegisteredTool: + name: str + title: str + description: str + input_schema_json: str + output_schema_json: str + request_type: type[Any] + response_type: type[Any] + from_pb: Any + to_pb: Any + handler: Any + annotations: dict[str, Any] | None + icons: list[dict[str, Any]] | None + +class _ServerToolRegistry: + def __init__(self, server: mcp.server.lowlevel.Server) -> None: + self.server = server + self.tools: dict[str, _RegisteredTool] = {} + self.reserved_tool_names = _get_reserved_tool_names(server) + self.handlers_installed = False + self.previous_list_tools: Any | None = None + self.previous_call_tool: Any | None = None + + def add_tool(self, tool: _RegisteredTool) -> None: + if tool.name in self.reserved_tool_names: + raise ValueError(f"duplicate tool registration: {tool.name}") + self.tools[tool.name] = tool + self.reserved_tool_names.add(tool.name) + + def list_tools(self) -> list[mcp.types.Tool]: + return [_build_tool(tool) for tool in self.tools.values()] + + async def call_tool(self, name: str, arguments: dict[str, Any] | None, context: ToolRequestContext | None = None) -> mcp.types.CallToolResult: + return await _dispatch_call(self, name, arguments or {}, context) + +_SERVER_REGISTRIES: weakref.WeakKeyDictionary[mcp.server.lowlevel.Server, _ServerToolRegistry] = weakref.WeakKeyDictionary() + +def _load_schema(schema_json: str) -> dict[str, Any]: + return json.loads(schema_json) + +def _protojson_to_message(arguments: dict[str, Any], message: Any) -> Any: + json_format.ParseDict(arguments, message) + return message + +def _normalize_tool_segment(segment: str | None) -> str: + if segment is None: + return "" + parts = [part for part in segment.strip().replace(".", "_").split("_") if part] + return "_".join(parts) + +def _normalize_namespace(namespace: str | None, default: str) -> str: + if namespace is None: + namespace = default + return _normalize_tool_segment(namespace) + +def _tool_name(namespace: str, method_name: str) -> str: + namespace = _normalize_tool_segment(namespace) + method_name = _normalize_tool_segment(method_name) + if namespace == '': + return method_name + if method_name == '': + return namespace + return f"{namespace}_{method_name}" + +def _get_registry(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _SERVER_REGISTRIES.get(server) + if registry is None: + registry = _ServerToolRegistry(server) + _SERVER_REGISTRIES[server] = registry + return registry + +_RESERVED_TOOL_NAMES_ATTR = "_protoc_gen_mcp_reserved_tool_names" + +def _get_reserved_tool_names(server: mcp.server.lowlevel.Server) -> set[str]: + reserved = getattr(server, _RESERVED_TOOL_NAMES_ATTR, None) + if reserved is None: + reserved = set() + setattr(server, _RESERVED_TOOL_NAMES_ATTR, reserved) + return reserved + +def _build_list_tools_request(request: Any) -> mcp.types.ListToolsRequest: + if isinstance(request, mcp.types.ListToolsRequest): + return request + return mcp.types.ListToolsRequest() + +def _merge_tools(previous: list[mcp.types.Tool], current: list[mcp.types.Tool]) -> list[mcp.types.Tool]: + merged: list[mcp.types.Tool] = [] + names: set[str] = set() + for tool in previous: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + for tool in current: + if tool.name in names: + raise ValueError(f"duplicate tool registration: {tool.name}") + names.add(tool.name) + merged.append(tool) + return merged + +def _install_server_handlers(server: mcp.server.lowlevel.Server) -> _ServerToolRegistry: + registry = _get_registry(server) + if registry.handlers_installed: + return registry + registry.previous_list_tools = server.request_handlers.get(mcp.types.ListToolsRequest) + registry.previous_call_tool = server.request_handlers.get(mcp.types.CallToolRequest) + + async def _list_tools(request: Any) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + list_request = _build_list_tools_request(request) + previous_tools: list[mcp.types.Tool] = [] + meta: dict[str, Any] | None = None + if current is not None: + if current.previous_list_tools is not None: + previous_result = await current.previous_list_tools(list_request) + if not isinstance(previous_result.root, mcp.types.ListToolsResult): + return previous_result + if getattr(list_request.params, "cursor", None) is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + if previous_result.root.nextCursor is not None: + raise ValueError("cannot compose protoc-gen-mcp tools with paginated tools/list handlers") + meta = getattr(previous_result.root, "meta", None) + previous_tools = list(previous_result.root.tools) + current_tools = [] if current is None else current.list_tools() + tools = _merge_tools(previous_tools, current_tools) + server._tool_cache.clear() + for tool in tools: + server._tool_cache[tool.name] = tool + return mcp.types.ServerResult(mcp.types.ListToolsResult(tools=tools, _meta=meta)) + + async def _call_tool(request: mcp.types.CallToolRequest) -> mcp.types.ServerResult: + current = _SERVER_REGISTRIES.get(server) + if current is None: + return mcp.types.ServerResult(_tool_error_result("tool registry is not installed")) + if request.params.name not in current.tools: + if current.previous_call_tool is not None: + return await current.previous_call_tool(request) + _invalid_params_error(request.params.name, "unknown tool") + if current.previous_call_tool is not None: + await server.request_handlers[mcp.types.ListToolsRequest](mcp.types.ListToolsRequest()) + result = await current.call_tool(request.params.name, request.params.arguments or {}, server.request_context.session) + return mcp.types.ServerResult(result) + + server.request_handlers[mcp.types.ListToolsRequest] = _list_tools + server.request_handlers[mcp.types.CallToolRequest] = _call_tool + + registry.handlers_installed = True + return registry + +def _tool_annotations(raw: dict[str, Any] | None) -> mcp.types.ToolAnnotations | None: + if raw is None: + return None + return mcp.types.ToolAnnotations(**raw) + +def _tool_error_result(message: str) -> mcp.types.CallToolResult: + return mcp.types.CallToolResult( + content=[mcp.types.TextContent(type="text", text=message)], + isError=True, + ) + +def _invalid_params_error(tool_name: str, error: Any) -> mcp.shared.exceptions.McpError: + raise mcp.shared.exceptions.McpError( + mcp.types.ErrorData( + code=mcp.types.INVALID_PARAMS, + message=f"invalid arguments for tool {tool_name!r}: {error}", + ) + ) + +def _build_tool(tool: _RegisteredTool) -> Any: + return mcp.types.Tool( + name=tool.name, + title=tool.title, + description=tool.description, + inputSchema=_load_schema(tool.input_schema_json), + outputSchema=_load_schema(tool.output_schema_json), + annotations=_tool_annotations(tool.annotations), + icons=tool.icons, + ) + +async def _maybe_await(result: Any) -> Any: + if inspect.isawaitable(result): + return await result + return result + +def _message_to_json(message: Any) -> str: + kwargs = {"preserving_proto_field_name": False} + try: + return json_format.MessageToJson(message, always_print_fields_with_no_presence=True, **kwargs) + except TypeError: + return json_format.MessageToJson(message, including_default_value_fields=True, **kwargs) + +async def _dispatch_call(registry: _ServerToolRegistry, name: str, arguments: dict[str, Any], context: ToolRequestContext | None) -> mcp.types.CallToolResult: + tool = registry.tools.get(name) + if tool is None: + _invalid_params_error(name, "unknown tool") + try: + jsonschema.validate(arguments, _load_schema(tool.input_schema_json)) + except jsonschema.ValidationError as error: + _invalid_params_error(name, error) + try: + request_pb = _protojson_to_message(arguments, tool.request_type()) + request_dc = tool.from_pb(request_pb) + except Exception as error: + _invalid_params_error(name, error) + try: + response_dc = await _maybe_await(tool.handler(context, request_dc)) + except Exception as error: + return _tool_error_result(str(error)) + try: + if response_dc is None: + response_pb = tool.response_type() + else: + response_pb = tool.to_pb(response_dc) + payload = json.loads(_message_to_json(response_pb)) + except Exception as error: + raise RuntimeError(f"mcpruntime: marshal output for tool {name!r}: {error}") from error + text = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + content = [mcp.types.TextContent(type="text", text=text)] + try: + jsonschema.validate(payload, _load_schema(tool.output_schema_json)) + except jsonschema.ValidationError as error: + raise RuntimeError(f"mcpruntime: validate output for tool {name!r}: {error}") from error + return mcp.types.CallToolResult(content=content, structuredContent=payload) + +def _enum_from_pb(enum_type: type[enum.IntEnum], value: int) -> enum.IntEnum: + return enum_type(value) + +def _json_to_message(value: Any, message: Any) -> Any: + json_format.Parse(json.dumps(value), message) + return message + +def _from_pb_any(message: any_pb2.Any) -> ProtoAny: + return json.loads(_message_to_json(message)) + +def _to_pb_any(value: ProtoAny) -> any_pb2.Any: + return _json_to_message(value, any_pb2.Any()) + +def _from_pb_timestamp(message: timestamp_pb2.Timestamp) -> Timestamp: + return json.loads(_message_to_json(message)) + +def _to_pb_timestamp(value: Timestamp) -> timestamp_pb2.Timestamp: + return _json_to_message(value, timestamp_pb2.Timestamp()) + +def _from_pb_duration(message: duration_pb2.Duration) -> Duration: + return json.loads(_message_to_json(message)) + +def _to_pb_duration(value: Duration) -> duration_pb2.Duration: + return _json_to_message(value, duration_pb2.Duration()) + +def _from_pb_field_mask(message: field_mask_pb2.FieldMask) -> FieldMask: + return json.loads(_message_to_json(message)) + +def _to_pb_field_mask(value: FieldMask) -> field_mask_pb2.FieldMask: + return _json_to_message(value, field_mask_pb2.FieldMask()) + +def _from_pb_struct(message: struct_pb2.Struct) -> Struct: + return json.loads(_message_to_json(message)) + +def _to_pb_struct(value: Struct) -> struct_pb2.Struct: + return _json_to_message(value, struct_pb2.Struct()) + +def _from_pb_list_value(message: struct_pb2.ListValue) -> ListValue: + return json.loads(_message_to_json(message)) + +def _to_pb_list_value(value: ListValue) -> struct_pb2.ListValue: + return _json_to_message(value, struct_pb2.ListValue()) + +def _from_pb_value(message: struct_pb2.Value) -> Value: + return json.loads(_message_to_json(message)) + +def _to_pb_value(value: Value) -> struct_pb2.Value: + return _json_to_message(value, struct_pb2.Value()) + +def _from_pb_empty(message: empty_pb2.Empty) -> Empty: + return Empty() + +def _to_pb_empty(value: Empty) -> empty_pb2.Empty: + return empty_pb2.Empty() + +def _from_pb_bool_value(message: wrappers_pb2.BoolValue) -> BoolValue: + return message.value + +def _to_pb_bool_value(value: BoolValue) -> wrappers_pb2.BoolValue: + return wrappers_pb2.BoolValue(value=value) + +def _from_pb_string_value(message: wrappers_pb2.StringValue) -> StringValue: + return message.value + +def _to_pb_string_value(value: StringValue) -> wrappers_pb2.StringValue: + return wrappers_pb2.StringValue(value=value) + +def _from_pb_bytes_value(message: wrappers_pb2.BytesValue) -> BytesValue: + return message.value + +def _to_pb_bytes_value(value: BytesValue) -> wrappers_pb2.BytesValue: + return wrappers_pb2.BytesValue(value=value) + +def _from_pb_int32_value(message: wrappers_pb2.Int32Value) -> Int32Value: + return message.value + +def _to_pb_int32_value(value: Int32Value) -> wrappers_pb2.Int32Value: + return wrappers_pb2.Int32Value(value=value) + +def _from_pb_uint32_value(message: wrappers_pb2.UInt32Value) -> UInt32Value: + return message.value + +def _to_pb_uint32_value(value: UInt32Value) -> wrappers_pb2.UInt32Value: + return wrappers_pb2.UInt32Value(value=value) + +def _from_pb_int64_value(message: wrappers_pb2.Int64Value) -> Int64Value: + return message.value + +def _to_pb_int64_value(value: Int64Value) -> wrappers_pb2.Int64Value: + return wrappers_pb2.Int64Value(value=value) + +def _from_pb_uint64_value(message: wrappers_pb2.UInt64Value) -> UInt64Value: + return message.value + +def _to_pb_uint64_value(value: UInt64Value) -> wrappers_pb2.UInt64Value: + return wrappers_pb2.UInt64Value(value=value) + +def _from_pb_float_value(message: wrappers_pb2.FloatValue) -> FloatValue: + return message.value + +def _to_pb_float_value(value: FloatValue) -> wrappers_pb2.FloatValue: + return wrappers_pb2.FloatValue(value=value) + +def _from_pb_double_value(message: wrappers_pb2.DoubleValue) -> DoubleValue: + return message.value + +def _to_pb_double_value(value: DoubleValue) -> wrappers_pb2.DoubleValue: + return wrappers_pb2.DoubleValue(value=value) + +def _from_pb_report_details(message: example_pb2.ReportDetails) -> ReportDetails: + return ReportDetails( + label=message.label, + ) + +def _to_pb_report_details(value: ReportDetails) -> example_pb2.ReportDetails: + message = example_pb2.ReportDetails() + message.label = value.label + return message + +def _from_pb_create_report_request(message: example_pb2.CreateReportRequest) -> CreateReportRequest: + return CreateReportRequest( + city=message.city, + count=message.count, + details=_from_pb_report_details(message.details), + units=message.units if message.HasField("units") else UNSET, + labels=list(message.labels), + ) + +def _to_pb_create_report_request(value: CreateReportRequest) -> example_pb2.CreateReportRequest: + message = example_pb2.CreateReportRequest() + message.city = value.city + message.count = value.count + message.details.CopyFrom(_to_pb_report_details(value.details)) + if value.units is not UNSET: + message.units = value.units + message.labels.extend(value.labels) + return message + +def _from_pb_create_report_response(message: example_pb2.CreateReportResponse) -> CreateReportResponse: + return CreateReportResponse( + report_id=message.report_id, + total_count=message.total_count, + status=_enum_from_pb(ReportStatus, message.status), + details=_from_pb_report_details(message.details), + warnings=list(message.warnings), + ) + +def _to_pb_create_report_response(value: CreateReportResponse) -> example_pb2.CreateReportResponse: + message = example_pb2.CreateReportResponse() + message.report_id = value.report_id + message.total_count = value.total_count + message.status = int(value.status) + message.details.CopyFrom(_to_pb_report_details(value.details)) + message.warnings.extend(value.warnings) + return message + +def _from_pb_ping_request(message: example_pb2.PingRequest) -> PingRequest: + return PingRequest( + ) + +def _to_pb_ping_request(value: PingRequest) -> example_pb2.PingRequest: + message = example_pb2.PingRequest() + return message + +def _from_pb_ping_response(message: example_pb2.PingResponse) -> PingResponse: + return PingResponse( + ack=_from_pb_empty(message.ack), + ) + +def _to_pb_ping_response(value: PingResponse) -> example_pb2.PingResponse: + message = example_pb2.PingResponse() + message.ack.CopyFrom(_to_pb_empty(value.ack)) + return message + +def _from_pb_recursive_node(message: example_pb2.RecursiveNode) -> RecursiveNode: + return RecursiveNode( + name=message.name, + child=_from_pb_recursive_node(message.child) if message.HasField("child") else UNSET, + children=[_from_pb_recursive_node(item) for item in message.children], + ) + +def _to_pb_recursive_node(value: RecursiveNode) -> example_pb2.RecursiveNode: + message = example_pb2.RecursiveNode() + message.name = value.name + if value.child is not UNSET: + message.child.CopyFrom(_to_pb_recursive_node(value.child)) + message.children.extend(_to_pb_recursive_node(item) for item in value.children) + return message + +def _from_pb_describe_advanced_shapes_request(message: example_pb2.DescribeAdvancedShapesRequest) -> DescribeAdvancedShapesRequest: + selector = UNSET + selector_case = message.WhichOneof("selector") + if selector_case == "city_alias": + selector = DescribeAdvancedShapesRequestSelectorCityAliasVariant(city_alias=message.city_alias) + elif selector_case == "city_id": + selector = DescribeAdvancedShapesRequestSelectorCityIdVariant(city_id=message.city_id) + elif selector_case == "city_details": + selector = DescribeAdvancedShapesRequestSelectorCityDetailsVariant(city_details=_from_pb_report_details(message.city_details)) + return DescribeAdvancedShapesRequest( + labels=dict(message.labels), + quantities=dict(message.quantities), + toggles=dict(message.toggles), + limits=dict(message.limits), + observed_at=_from_pb_timestamp(message.observed_at) if message.HasField("observed_at") else UNSET, + ttl=_from_pb_duration(message.ttl) if message.HasField("ttl") else UNSET, + payload=_from_pb_struct(message.payload) if message.HasField("payload") else UNSET, + items=_from_pb_list_value(message.items) if message.HasField("items") else UNSET, + dynamic=_from_pb_value(message.dynamic) if message.HasField("dynamic") else UNSET, + note=_from_pb_string_value(message.note) if message.HasField("note") else UNSET, + total=_from_pb_int64_value(message.total) if message.HasField("total") else UNSET, + enabled=_from_pb_bool_value(message.enabled) if message.HasField("enabled") else UNSET, + ratio=_from_pb_double_value(message.ratio) if message.HasField("ratio") else UNSET, + mask=_from_pb_field_mask(message.mask) if message.HasField("mask") else UNSET, + blob=_from_pb_bytes_value(message.blob) if message.HasField("blob") else UNSET, + small_total=_from_pb_int32_value(message.small_total) if message.HasField("small_total") else UNSET, + uint_total=_from_pb_uint32_value(message.uint_total) if message.HasField("uint_total") else UNSET, + huge_total=_from_pb_uint64_value(message.huge_total) if message.HasField("huge_total") else UNSET, + weight=_from_pb_float_value(message.weight) if message.HasField("weight") else UNSET, + raw_ratio=message.raw_ratio if message.HasField("raw_ratio") else UNSET, + tree=_from_pb_recursive_node(message.tree) if message.HasField("tree") else UNSET, + detail_any=_from_pb_any(message.detail_any) if message.HasField("detail_any") else UNSET, + duration_any=_from_pb_any(message.duration_any) if message.HasField("duration_any") else UNSET, + selector=selector, + ) + +def _to_pb_describe_advanced_shapes_request(value: DescribeAdvancedShapesRequest) -> example_pb2.DescribeAdvancedShapesRequest: + message = example_pb2.DescribeAdvancedShapesRequest() + message.labels.update(value.labels) + message.quantities.update(value.quantities) + message.toggles.update(value.toggles) + message.limits.update(value.limits) + if value.observed_at is not UNSET: + message.observed_at.CopyFrom(_to_pb_timestamp(value.observed_at)) + if value.ttl is not UNSET: + message.ttl.CopyFrom(_to_pb_duration(value.ttl)) + if value.payload is not UNSET: + message.payload.CopyFrom(_to_pb_struct(value.payload)) + if value.items is not UNSET: + message.items.CopyFrom(_to_pb_list_value(value.items)) + if value.dynamic is not UNSET: + message.dynamic.CopyFrom(_to_pb_value(value.dynamic)) + if value.note is not UNSET: + message.note.CopyFrom(_to_pb_string_value(value.note)) + if value.total is not UNSET: + message.total.CopyFrom(_to_pb_int64_value(value.total)) + if value.enabled is not UNSET: + message.enabled.CopyFrom(_to_pb_bool_value(value.enabled)) + if value.ratio is not UNSET: + message.ratio.CopyFrom(_to_pb_double_value(value.ratio)) + if value.mask is not UNSET: + message.mask.CopyFrom(_to_pb_field_mask(value.mask)) + if value.blob is not UNSET: + message.blob.CopyFrom(_to_pb_bytes_value(value.blob)) + if value.small_total is not UNSET: + message.small_total.CopyFrom(_to_pb_int32_value(value.small_total)) + if value.uint_total is not UNSET: + message.uint_total.CopyFrom(_to_pb_uint32_value(value.uint_total)) + if value.huge_total is not UNSET: + message.huge_total.CopyFrom(_to_pb_uint64_value(value.huge_total)) + if value.weight is not UNSET: + message.weight.CopyFrom(_to_pb_float_value(value.weight)) + if value.raw_ratio is not UNSET: + message.raw_ratio = value.raw_ratio + if value.tree is not UNSET: + message.tree.CopyFrom(_to_pb_recursive_node(value.tree)) + if value.detail_any is not UNSET: + message.detail_any.CopyFrom(_to_pb_any(value.detail_any)) + if value.duration_any is not UNSET: + message.duration_any.CopyFrom(_to_pb_any(value.duration_any)) + if value.selector is not UNSET: + if isinstance(value.selector, DescribeAdvancedShapesRequestSelectorCityAliasVariant): + message.city_alias = value.selector.city_alias + elif isinstance(value.selector, DescribeAdvancedShapesRequestSelectorCityIdVariant): + message.city_id = value.selector.city_id + elif isinstance(value.selector, DescribeAdvancedShapesRequestSelectorCityDetailsVariant): + message.city_details.CopyFrom(_to_pb_report_details(value.selector.city_details)) + else: + raise TypeError("unsupported DescribeAdvancedShapesRequestSelectorVariant variant: " + type(value.selector).__name__) + + return message + +def _from_pb_describe_advanced_shapes_response(message: example_pb2.DescribeAdvancedShapesResponse) -> DescribeAdvancedShapesResponse: + selector = UNSET + selector_case = message.WhichOneof("selector") + if selector_case == "city_alias": + selector = DescribeAdvancedShapesResponseSelectorCityAliasVariant(city_alias=message.city_alias) + elif selector_case == "city_id": + selector = DescribeAdvancedShapesResponseSelectorCityIdVariant(city_id=message.city_id) + elif selector_case == "city_details": + selector = DescribeAdvancedShapesResponseSelectorCityDetailsVariant(city_details=_from_pb_report_details(message.city_details)) + return DescribeAdvancedShapesResponse( + labels=dict(message.labels), + quantities=dict(message.quantities), + toggles=dict(message.toggles), + limits=dict(message.limits), + observed_at=_from_pb_timestamp(message.observed_at) if message.HasField("observed_at") else UNSET, + ttl=_from_pb_duration(message.ttl) if message.HasField("ttl") else UNSET, + payload=_from_pb_struct(message.payload) if message.HasField("payload") else UNSET, + items=_from_pb_list_value(message.items) if message.HasField("items") else UNSET, + dynamic=_from_pb_value(message.dynamic) if message.HasField("dynamic") else UNSET, + note=_from_pb_string_value(message.note) if message.HasField("note") else UNSET, + total=_from_pb_int64_value(message.total) if message.HasField("total") else UNSET, + enabled=_from_pb_bool_value(message.enabled) if message.HasField("enabled") else UNSET, + ratio=_from_pb_double_value(message.ratio) if message.HasField("ratio") else UNSET, + mask=_from_pb_field_mask(message.mask) if message.HasField("mask") else UNSET, + blob=_from_pb_bytes_value(message.blob) if message.HasField("blob") else UNSET, + small_total=_from_pb_int32_value(message.small_total) if message.HasField("small_total") else UNSET, + uint_total=_from_pb_uint32_value(message.uint_total) if message.HasField("uint_total") else UNSET, + huge_total=_from_pb_uint64_value(message.huge_total) if message.HasField("huge_total") else UNSET, + weight=_from_pb_float_value(message.weight) if message.HasField("weight") else UNSET, + raw_ratio=message.raw_ratio if message.HasField("raw_ratio") else UNSET, + tree=_from_pb_recursive_node(message.tree) if message.HasField("tree") else UNSET, + detail_any=_from_pb_any(message.detail_any) if message.HasField("detail_any") else UNSET, + duration_any=_from_pb_any(message.duration_any) if message.HasField("duration_any") else UNSET, + selector=selector, + ) + +def _to_pb_describe_advanced_shapes_response(value: DescribeAdvancedShapesResponse) -> example_pb2.DescribeAdvancedShapesResponse: + message = example_pb2.DescribeAdvancedShapesResponse() + message.labels.update(value.labels) + message.quantities.update(value.quantities) + message.toggles.update(value.toggles) + message.limits.update(value.limits) + if value.observed_at is not UNSET: + message.observed_at.CopyFrom(_to_pb_timestamp(value.observed_at)) + if value.ttl is not UNSET: + message.ttl.CopyFrom(_to_pb_duration(value.ttl)) + if value.payload is not UNSET: + message.payload.CopyFrom(_to_pb_struct(value.payload)) + if value.items is not UNSET: + message.items.CopyFrom(_to_pb_list_value(value.items)) + if value.dynamic is not UNSET: + message.dynamic.CopyFrom(_to_pb_value(value.dynamic)) + if value.note is not UNSET: + message.note.CopyFrom(_to_pb_string_value(value.note)) + if value.total is not UNSET: + message.total.CopyFrom(_to_pb_int64_value(value.total)) + if value.enabled is not UNSET: + message.enabled.CopyFrom(_to_pb_bool_value(value.enabled)) + if value.ratio is not UNSET: + message.ratio.CopyFrom(_to_pb_double_value(value.ratio)) + if value.mask is not UNSET: + message.mask.CopyFrom(_to_pb_field_mask(value.mask)) + if value.blob is not UNSET: + message.blob.CopyFrom(_to_pb_bytes_value(value.blob)) + if value.small_total is not UNSET: + message.small_total.CopyFrom(_to_pb_int32_value(value.small_total)) + if value.uint_total is not UNSET: + message.uint_total.CopyFrom(_to_pb_uint32_value(value.uint_total)) + if value.huge_total is not UNSET: + message.huge_total.CopyFrom(_to_pb_uint64_value(value.huge_total)) + if value.weight is not UNSET: + message.weight.CopyFrom(_to_pb_float_value(value.weight)) + if value.raw_ratio is not UNSET: + message.raw_ratio = value.raw_ratio + if value.tree is not UNSET: + message.tree.CopyFrom(_to_pb_recursive_node(value.tree)) + if value.detail_any is not UNSET: + message.detail_any.CopyFrom(_to_pb_any(value.detail_any)) + if value.duration_any is not UNSET: + message.duration_any.CopyFrom(_to_pb_any(value.duration_any)) + if value.selector is not UNSET: + if isinstance(value.selector, DescribeAdvancedShapesResponseSelectorCityAliasVariant): + message.city_alias = value.selector.city_alias + elif isinstance(value.selector, DescribeAdvancedShapesResponseSelectorCityIdVariant): + message.city_id = value.selector.city_id + elif isinstance(value.selector, DescribeAdvancedShapesResponseSelectorCityDetailsVariant): + message.city_details.CopyFrom(_to_pb_report_details(value.selector.city_details)) + else: + raise TypeError("unsupported DescribeAdvancedShapesResponseSelectorVariant variant: " + type(value.selector).__name__) + + return message + +def _from_pb_describe_scalar_shapes_request(message: example_pb2.DescribeScalarShapesRequest) -> DescribeScalarShapesRequest: + return DescribeScalarShapesRequest( + bool_flag=message.bool_flag, + text_value=message.text_value, + bytes_value=message.bytes_value, + int32_value=message.int32_value, + sint32_value=message.sint32_value, + sfixed32_value=message.sfixed32_value, + uint32_value=message.uint32_value, + fixed32_value=message.fixed32_value, + int64_value=message.int64_value, + sint64_value=message.sint64_value, + sfixed64_value=message.sfixed64_value, + uint64_value=message.uint64_value, + fixed64_value=message.fixed64_value, + float_value=message.float_value, + double_value=message.double_value, + status=_enum_from_pb(ReportStatus, message.status), + details=_from_pb_report_details(message.details), + samples=list(message.samples), + optional_bool_flag=message.optional_bool_flag if message.HasField("optional_bool_flag") else UNSET, + optional_text_value=message.optional_text_value if message.HasField("optional_text_value") else UNSET, + optional_bytes_value=message.optional_bytes_value if message.HasField("optional_bytes_value") else UNSET, + optional_int32_value=message.optional_int32_value if message.HasField("optional_int32_value") else UNSET, + optional_sint32_value=message.optional_sint32_value if message.HasField("optional_sint32_value") else UNSET, + optional_sfixed32_value=message.optional_sfixed32_value if message.HasField("optional_sfixed32_value") else UNSET, + optional_uint32_value=message.optional_uint32_value if message.HasField("optional_uint32_value") else UNSET, + optional_fixed32_value=message.optional_fixed32_value if message.HasField("optional_fixed32_value") else UNSET, + optional_int64_value=message.optional_int64_value if message.HasField("optional_int64_value") else UNSET, + optional_sint64_value=message.optional_sint64_value if message.HasField("optional_sint64_value") else UNSET, + optional_sfixed64_value=message.optional_sfixed64_value if message.HasField("optional_sfixed64_value") else UNSET, + optional_uint64_value=message.optional_uint64_value if message.HasField("optional_uint64_value") else UNSET, + optional_fixed64_value=message.optional_fixed64_value if message.HasField("optional_fixed64_value") else UNSET, + optional_float_value=message.optional_float_value if message.HasField("optional_float_value") else UNSET, + optional_double_value=message.optional_double_value if message.HasField("optional_double_value") else UNSET, + optional_status=_enum_from_pb(ReportStatus, message.optional_status) if message.HasField("optional_status") else UNSET, + ) + +def _to_pb_describe_scalar_shapes_request(value: DescribeScalarShapesRequest) -> example_pb2.DescribeScalarShapesRequest: + message = example_pb2.DescribeScalarShapesRequest() + message.bool_flag = value.bool_flag + message.text_value = value.text_value + message.bytes_value = value.bytes_value + message.int32_value = value.int32_value + message.sint32_value = value.sint32_value + message.sfixed32_value = value.sfixed32_value + message.uint32_value = value.uint32_value + message.fixed32_value = value.fixed32_value + message.int64_value = value.int64_value + message.sint64_value = value.sint64_value + message.sfixed64_value = value.sfixed64_value + message.uint64_value = value.uint64_value + message.fixed64_value = value.fixed64_value + message.float_value = value.float_value + message.double_value = value.double_value + message.status = int(value.status) + message.details.CopyFrom(_to_pb_report_details(value.details)) + message.samples.extend(value.samples) + if value.optional_bool_flag is not UNSET: + message.optional_bool_flag = value.optional_bool_flag + if value.optional_text_value is not UNSET: + message.optional_text_value = value.optional_text_value + if value.optional_bytes_value is not UNSET: + message.optional_bytes_value = value.optional_bytes_value + if value.optional_int32_value is not UNSET: + message.optional_int32_value = value.optional_int32_value + if value.optional_sint32_value is not UNSET: + message.optional_sint32_value = value.optional_sint32_value + if value.optional_sfixed32_value is not UNSET: + message.optional_sfixed32_value = value.optional_sfixed32_value + if value.optional_uint32_value is not UNSET: + message.optional_uint32_value = value.optional_uint32_value + if value.optional_fixed32_value is not UNSET: + message.optional_fixed32_value = value.optional_fixed32_value + if value.optional_int64_value is not UNSET: + message.optional_int64_value = value.optional_int64_value + if value.optional_sint64_value is not UNSET: + message.optional_sint64_value = value.optional_sint64_value + if value.optional_sfixed64_value is not UNSET: + message.optional_sfixed64_value = value.optional_sfixed64_value + if value.optional_uint64_value is not UNSET: + message.optional_uint64_value = value.optional_uint64_value + if value.optional_fixed64_value is not UNSET: + message.optional_fixed64_value = value.optional_fixed64_value + if value.optional_float_value is not UNSET: + message.optional_float_value = value.optional_float_value + if value.optional_double_value is not UNSET: + message.optional_double_value = value.optional_double_value + if value.optional_status is not UNSET: + message.optional_status = int(value.optional_status) + return message + +def _from_pb_describe_scalar_shapes_response(message: example_pb2.DescribeScalarShapesResponse) -> DescribeScalarShapesResponse: + return DescribeScalarShapesResponse( + bool_flag=message.bool_flag, + text_value=message.text_value, + bytes_value=message.bytes_value, + int32_value=message.int32_value, + sint32_value=message.sint32_value, + sfixed32_value=message.sfixed32_value, + uint32_value=message.uint32_value, + fixed32_value=message.fixed32_value, + int64_value=message.int64_value, + sint64_value=message.sint64_value, + sfixed64_value=message.sfixed64_value, + uint64_value=message.uint64_value, + fixed64_value=message.fixed64_value, + float_value=message.float_value, + double_value=message.double_value, + status=_enum_from_pb(ReportStatus, message.status), + details=_from_pb_report_details(message.details), + samples=list(message.samples), + optional_bool_flag=message.optional_bool_flag if message.HasField("optional_bool_flag") else UNSET, + optional_text_value=message.optional_text_value if message.HasField("optional_text_value") else UNSET, + optional_bytes_value=message.optional_bytes_value if message.HasField("optional_bytes_value") else UNSET, + optional_int32_value=message.optional_int32_value if message.HasField("optional_int32_value") else UNSET, + optional_sint32_value=message.optional_sint32_value if message.HasField("optional_sint32_value") else UNSET, + optional_sfixed32_value=message.optional_sfixed32_value if message.HasField("optional_sfixed32_value") else UNSET, + optional_uint32_value=message.optional_uint32_value if message.HasField("optional_uint32_value") else UNSET, + optional_fixed32_value=message.optional_fixed32_value if message.HasField("optional_fixed32_value") else UNSET, + optional_int64_value=message.optional_int64_value if message.HasField("optional_int64_value") else UNSET, + optional_sint64_value=message.optional_sint64_value if message.HasField("optional_sint64_value") else UNSET, + optional_sfixed64_value=message.optional_sfixed64_value if message.HasField("optional_sfixed64_value") else UNSET, + optional_uint64_value=message.optional_uint64_value if message.HasField("optional_uint64_value") else UNSET, + optional_fixed64_value=message.optional_fixed64_value if message.HasField("optional_fixed64_value") else UNSET, + optional_float_value=message.optional_float_value if message.HasField("optional_float_value") else UNSET, + optional_double_value=message.optional_double_value if message.HasField("optional_double_value") else UNSET, + optional_status=_enum_from_pb(ReportStatus, message.optional_status) if message.HasField("optional_status") else UNSET, + ) + +def _to_pb_describe_scalar_shapes_response(value: DescribeScalarShapesResponse) -> example_pb2.DescribeScalarShapesResponse: + message = example_pb2.DescribeScalarShapesResponse() + message.bool_flag = value.bool_flag + message.text_value = value.text_value + message.bytes_value = value.bytes_value + message.int32_value = value.int32_value + message.sint32_value = value.sint32_value + message.sfixed32_value = value.sfixed32_value + message.uint32_value = value.uint32_value + message.fixed32_value = value.fixed32_value + message.int64_value = value.int64_value + message.sint64_value = value.sint64_value + message.sfixed64_value = value.sfixed64_value + message.uint64_value = value.uint64_value + message.fixed64_value = value.fixed64_value + message.float_value = value.float_value + message.double_value = value.double_value + message.status = int(value.status) + message.details.CopyFrom(_to_pb_report_details(value.details)) + message.samples.extend(value.samples) + if value.optional_bool_flag is not UNSET: + message.optional_bool_flag = value.optional_bool_flag + if value.optional_text_value is not UNSET: + message.optional_text_value = value.optional_text_value + if value.optional_bytes_value is not UNSET: + message.optional_bytes_value = value.optional_bytes_value + if value.optional_int32_value is not UNSET: + message.optional_int32_value = value.optional_int32_value + if value.optional_sint32_value is not UNSET: + message.optional_sint32_value = value.optional_sint32_value + if value.optional_sfixed32_value is not UNSET: + message.optional_sfixed32_value = value.optional_sfixed32_value + if value.optional_uint32_value is not UNSET: + message.optional_uint32_value = value.optional_uint32_value + if value.optional_fixed32_value is not UNSET: + message.optional_fixed32_value = value.optional_fixed32_value + if value.optional_int64_value is not UNSET: + message.optional_int64_value = value.optional_int64_value + if value.optional_sint64_value is not UNSET: + message.optional_sint64_value = value.optional_sint64_value + if value.optional_sfixed64_value is not UNSET: + message.optional_sfixed64_value = value.optional_sfixed64_value + if value.optional_uint64_value is not UNSET: + message.optional_uint64_value = value.optional_uint64_value + if value.optional_fixed64_value is not UNSET: + message.optional_fixed64_value = value.optional_fixed64_value + if value.optional_float_value is not UNSET: + message.optional_float_value = value.optional_float_value + if value.optional_double_value is not UNSET: + message.optional_double_value = value.optional_double_value + if value.optional_status is not UNSET: + message.optional_status = int(value.optional_status) + return message + +def _from_pb_hidden_thing_request(message: example_pb2.HiddenThingRequest) -> HiddenThingRequest: + return HiddenThingRequest( + name=message.name, + ) + +def _to_pb_hidden_thing_request(value: HiddenThingRequest) -> example_pb2.HiddenThingRequest: + message = example_pb2.HiddenThingRequest() + message.name = value.name + return message + +def _from_pb_hidden_thing_response(message: example_pb2.HiddenThingResponse) -> HiddenThingResponse: + return HiddenThingResponse( + ) + +def _to_pb_hidden_thing_response(value: HiddenThingResponse) -> example_pb2.HiddenThingResponse: + message = example_pb2.HiddenThingResponse() + return message + +class ExampleAPIToolHandler(Protocol): + def create_report(self, ctx: ToolRequestContext, req: CreateReportRequest) -> CreateReportResponse | Awaitable[CreateReportResponse]: + ... + def ping(self, ctx: ToolRequestContext, req: PingRequest) -> PingResponse | Awaitable[PingResponse]: + ... + def describe_advanced_shapes(self, ctx: ToolRequestContext, req: DescribeAdvancedShapesRequest) -> DescribeAdvancedShapesResponse | Awaitable[DescribeAdvancedShapesResponse]: + ... + def describe_scalar_shapes(self, ctx: ToolRequestContext, req: DescribeScalarShapesRequest) -> DescribeScalarShapesResponse | Awaitable[DescribeScalarShapesResponse]: + ... + def hidden_thing(self, ctx: ToolRequestContext, req: HiddenThingRequest) -> HiddenThingResponse | Awaitable[HiddenThingResponse]: + ... + +def register_example_api_tools(server: mcp.server.lowlevel.Server, impl: ExampleAPIToolHandler, *, namespace: str | None = None) -> None: + if impl is None: + raise ValueError("register_example_api_tools: impl is nil") + registry = _install_server_handlers(server) + resolved_namespace = _normalize_namespace(namespace, "example") + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "CreateReport"), + title="Create report", + description="Create a report for a city.", + input_schema_json=EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON, + output_schema_json=EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.CreateReportRequest, + response_type=example_pb2.CreateReportResponse, + from_pb=_from_pb_create_report_request, + to_pb=_to_pb_create_report_response, + handler=impl.create_report, + annotations=None, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "Health"), + title="Health check", + description="Ping returns an empty response.", + input_schema_json=EXAMPLE_API_HEALTH_INPUT_SCHEMA_JSON, + output_schema_json=EXAMPLE_API_HEALTH_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.PingRequest, + response_type=example_pb2.PingResponse, + from_pb=_from_pb_ping_request, + to_pb=_to_pb_ping_response, + handler=impl.ping, + annotations=None, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "DescribeAdvancedShapes"), + title="Describe advanced shapes", + description="Exercise maps and well-known protobuf types.", + input_schema_json=EXAMPLE_API_DESCRIBE_ADVANCED_SHAPES_INPUT_SCHEMA_JSON, + output_schema_json=EXAMPLE_API_DESCRIBE_ADVANCED_SHAPES_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.DescribeAdvancedShapesRequest, + response_type=example_pb2.DescribeAdvancedShapesResponse, + from_pb=_from_pb_describe_advanced_shapes_request, + to_pb=_to_pb_describe_advanced_shapes_response, + handler=impl.describe_advanced_shapes, + annotations=None, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "DescribeScalarShapes"), + title="Describe scalar shapes", + description="Exercise plain protobuf scalar kinds.", + input_schema_json=EXAMPLE_API_DESCRIBE_SCALAR_SHAPES_INPUT_SCHEMA_JSON, + output_schema_json=EXAMPLE_API_DESCRIBE_SCALAR_SHAPES_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.DescribeScalarShapesRequest, + response_type=example_pb2.DescribeScalarShapesResponse, + from_pb=_from_pb_describe_scalar_shapes_request, + to_pb=_to_pb_describe_scalar_shapes_response, + handler=impl.describe_scalar_shapes, + annotations=None, + icons=None, + )) + registry.add_tool(_RegisteredTool( + name=_tool_name(resolved_namespace, "HiddenThing"), + title="", + description="HiddenThing is intentionally hidden from generated tools.\nNote: the `hidden` method option was removed in this iteration.", + input_schema_json=EXAMPLE_API_HIDDEN_THING_INPUT_SCHEMA_JSON, + output_schema_json=EXAMPLE_API_HIDDEN_THING_OUTPUT_SCHEMA_JSON, + request_type=example_pb2.HiddenThingRequest, + response_type=example_pb2.HiddenThingResponse, + from_pb=_from_pb_hidden_thing_request, + to_pb=_to_pb_hidden_thing_response, + handler=impl.hidden_thing, + annotations=None, + icons=None, + )) + +EXAMPLE_API_CREATE_REPORT_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"description\":\"City name.\",\"examples\":[\"Paris\",\"London\"],\"minLength\":1,\"maxLength\":100,\"pattern\":\"^[A-Z]\"},\"count\":{\"type\":\"integer\",\"description\":\"count is the number of requested items.\",\"default\":10,\"examples\":[-1],\"minimum\":1,\"maximum\":1000},\"details\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"details contains nested report options.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"labels\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"labels adds additional labels.\",\"examples\":[\"example\"]},\"description\":\"labels adds additional labels.\",\"examples\":[[\"example\"]],\"minItems\":1,\"maxItems\":50,\"uniqueItems\":true},\"units\":{\"type\":[\"string\",\"null\"],\"description\":\"units overrides the default units.\",\"examples\":[\"example\"]}},\"description\":\"CreateReportRequest describes a report generation request.\",\"examples\":[\"{\\\"city\\\":\\\"Paris\\\",\\\"count\\\":2,\\\"details\\\":{\\\"label\\\":\\\"today\\\"}}\"],\"required\":[\"city\",\"count\",\"details\"],\"additionalProperties\":false}" + +EXAMPLE_API_CREATE_REPORT_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"details\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"details echoes the nested options.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"reportId\":{\"type\":\"string\",\"description\":\"report_id is the generated report identifier.\",\"examples\":[\"example\"]},\"status\":{\"type\":\"string\",\"title\":\"Report Status\",\"description\":\"status is the final report status.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"enum\":[\"REPORT_STATUS_OK\",\"REPORT_STATUS_FAILED\"]},\"totalCount\":{\"type\":\"string\",\"description\":\"total_count is returned as a ProtoJSON string.\",\"examples\":[\"-1\"]},\"warnings\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"string\",\"description\":\"warnings contains optional warning messages.\",\"examples\":[\"example\"]},\"description\":\"warnings contains optional warning messages.\",\"examples\":[[\"example\"]]}},\"description\":\"CreateReportResponse describes a report generation result.\",\"examples\":[{\"details\":{\"label\":\"example\"},\"reportId\":\"example\",\"status\":\"REPORT_STATUS_OK\",\"totalCount\":\"-1\",\"warnings\":[\"example\"]}],\"required\":[\"reportId\",\"totalCount\",\"status\",\"details\"],\"additionalProperties\":false}" + +EXAMPLE_API_HEALTH_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"description\":\"PingRequest is used by the health-check RPC.\",\"additionalProperties\":false}" + +EXAMPLE_API_HEALTH_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"ack\":{\"type\":\"object\",\"description\":\"ack confirms the health-check call.\",\"examples\":[{}],\"additionalProperties\":false}},\"description\":\"PingResponse is used by the health-check RPC.\",\"examples\":[{\"ack\":{}}],\"required\":[\"ack\"],\"additionalProperties\":false}" + +EXAMPLE_API_DESCRIBE_ADVANCED_SHAPES_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"blob\":{\"type\":[\"string\",\"null\"],\"description\":\"blob exercises BytesValue wrapper support.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"cityAlias\":{\"type\":[\"string\",\"null\"],\"description\":\"city_alias exercises string oneof members.\",\"examples\":[\"example\"]},\"cityDetails\":{\"type\":[\"object\",\"null\"],\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"city_details exercises message oneof members.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"cityId\":{\"type\":[\"string\",\"null\"],\"description\":\"city_id exercises int64 oneof members with ProtoJSON string encoding.\",\"examples\":[\"-1\"]},\"detailAny\":{\"type\":[\"object\",\"null\"],\"properties\":{\"@type\":{\"type\":\"string\",\"description\":\"Type URL of the embedded protobuf message, usually in the form type.googleapis.com/\\u003cfull.message.name\\u003e.\"}},\"description\":\"detail_any exercises Any with a regular embedded message payload.\\n\\nProtoJSON representation of google.protobuf.Any. Provide @type with the embedded message type URL and include the embedded message JSON fields alongside it. For well-known types that use a custom JSON form, send @type plus a value field containing that custom representation.\",\"examples\":[{\"@type\":\"type.googleapis.com/internal.testproto.example.v1.ReportDetails\",\"label\":\"from-any\"}],\"required\":[\"@type\"],\"additionalProperties\":true},\"durationAny\":{\"type\":[\"object\",\"null\"],\"properties\":{\"@type\":{\"type\":\"string\",\"description\":\"Type URL of the embedded protobuf message, usually in the form type.googleapis.com/\\u003cfull.message.name\\u003e.\"}},\"description\":\"duration_any exercises Any with a well-known embedded payload.\\n\\nProtoJSON representation of google.protobuf.Any. Provide @type with the embedded message type URL and include the embedded message JSON fields alongside it. For well-known types that use a custom JSON form, send @type plus a value field containing that custom representation.\",\"examples\":[{\"@type\":\"type.googleapis.com/google.protobuf.Duration\",\"value\":\"3600s\"}],\"required\":[\"@type\"],\"additionalProperties\":true},\"dynamic\":{\"description\":\"dynamic accepts arbitrary JSON values.\",\"examples\":[{\"kind\":\"demo\"}]},\"enabled\":{\"type\":[\"boolean\",\"null\"],\"description\":\"enabled exercises BoolValue wrapper support.\",\"examples\":[true]},\"hugeTotal\":{\"type\":[\"string\",\"null\"],\"description\":\"huge_total exercises UInt64Value wrapper support.\",\"examples\":[\"1\"]},\"items\":{\"type\":[\"array\",\"null\"],\"items\":true,\"description\":\"items accepts arbitrary JSON arrays.\",\"examples\":[[\"item\",2,true]]},\"labels\":{\"type\":[\"object\",\"null\"],\"description\":\"labels stores arbitrary string metadata.\",\"examples\":[{\"key\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\"}},\"limits\":{\"type\":[\"object\",\"null\"],\"description\":\"limits demonstrates unsigned numeric map keys encoded as JSON object keys.\",\"examples\":[{\"1\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"pattern\":\"^(0|[1-9][0-9]*)$\"}},\"mask\":{\"type\":[\"string\",\"null\"],\"description\":\"mask exercises FieldMask string support.\",\"examples\":[\"fieldName,otherField\"]},\"note\":{\"type\":[\"string\",\"null\"],\"description\":\"note exercises `StringValue` wrapper support.\",\"examples\":[\"example\"]},\"observedAt\":{\"type\":[\"string\",\"null\"],\"description\":\"observed_at is represented as an RFC 3339 timestamp string.\",\"examples\":[\"2026-03-09T10:11:12Z\"],\"format\":\"date-time\"},\"payload\":{\"type\":[\"object\",\"null\"],\"description\":\"payload accepts arbitrary JSON objects.\",\"examples\":[{\"kind\":\"demo\",\"nested\":{\"ok\":true}}],\"additionalProperties\":true},\"quantities\":{\"type\":[\"object\",\"null\"],\"description\":\"quantities demonstrates numeric map keys encoded as JSON object keys.\",\"examples\":[{\"1\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"pattern\":\"^-?(0|[1-9][0-9]*)$\"}},\"ratio\":{\"description\":\"ratio exercises ProtoJSON float special values.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"rawRatio\":{\"description\":\"raw_ratio exercises raw double ProtoJSON float support.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"smallTotal\":{\"type\":[\"integer\",\"null\"],\"description\":\"small_total exercises Int32Value wrapper support.\",\"examples\":[1]},\"toggles\":{\"type\":[\"object\",\"null\"],\"description\":\"toggles demonstrates bool map keys encoded as JSON object keys.\",\"examples\":[{\"true\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"enum\":[\"true\",\"false\"]}},\"total\":{\"type\":[\"string\",\"null\"],\"description\":\"total exercises Int64Value wrapper support.\",\"examples\":[\"1\"]},\"tree\":{\"type\":[\"object\",\"null\"],\"properties\":{\"child\":{\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"anyOf\":[{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},{\"type\":\"null\"}]},\"children\":{\"type\":[\"array\",\"null\"],\"items\":{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"children holds repeated recursive nodes.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},\"description\":\"children holds repeated recursive nodes.\",\"examples\":[[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]]},\"name\":{\"type\":\"string\",\"description\":\"name identifies the current node.\",\"examples\":[\"example\"]}},\"description\":\"tree exercises recursive message schemas.\\n\\nRecursiveNode exercises recursive message schemas.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false},\"ttl\":{\"type\":[\"string\",\"null\"],\"description\":\"ttl is represented as a protobuf duration string.\",\"examples\":[\"3600s\"],\"pattern\":\"^-?[0-9]+(?:\\\\.[0-9]{1,9})?s$\"},\"uintTotal\":{\"type\":[\"integer\",\"null\"],\"description\":\"uint_total exercises UInt32Value wrapper support.\",\"examples\":[1]},\"weight\":{\"description\":\"weight exercises FloatValue wrapper support.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]}},\"$defs\":{\"internal.testproto.example.v1.RecursiveNode\":{\"type\":\"object\",\"properties\":{\"child\":{\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"anyOf\":[{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},{\"type\":\"null\"}]},\"children\":{\"type\":[\"array\",\"null\"],\"items\":{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"children holds repeated recursive nodes.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},\"description\":\"children holds repeated recursive nodes.\",\"examples\":[[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]]},\"name\":{\"type\":\"string\",\"description\":\"name identifies the current node.\",\"examples\":[\"example\"]}},\"description\":\"RecursiveNode exercises recursive message schemas.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false}},\"description\":\"DescribeAdvancedShapesRequest exercises supported advanced protobuf shapes.\",\"examples\":[{\"blob\":\"aGVsbG8=\",\"cityAlias\":\"example\",\"detailAny\":{\"@type\":\"type.googleapis.com/internal.testproto.example.v1.ReportDetails\",\"label\":\"from-any\"},\"durationAny\":{\"@type\":\"type.googleapis.com/google.protobuf.Duration\",\"value\":\"3600s\"},\"dynamic\":{\"kind\":\"demo\"},\"enabled\":true,\"hugeTotal\":\"1\",\"items\":[\"item\",2,true],\"labels\":{\"key\":\"example\"},\"limits\":{\"1\":\"example\"},\"mask\":\"fieldName,otherField\",\"note\":\"example\",\"observedAt\":\"2026-03-09T10:11:12Z\",\"payload\":{\"kind\":\"demo\",\"nested\":{\"ok\":true}},\"quantities\":{\"1\":\"example\"},\"ratio\":1.25,\"rawRatio\":1.25,\"smallTotal\":1,\"toggles\":{\"true\":\"example\"},\"total\":\"1\",\"tree\":{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"},\"ttl\":\"3600s\",\"uintTotal\":1,\"weight\":1.25}],\"additionalProperties\":false,\"allOf\":[{\"oneOf\":[{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}},{\"allOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}}]},{\"allOf\":[{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}}]},{\"allOf\":[{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}}]}}]}]}]}" + +EXAMPLE_API_DESCRIBE_ADVANCED_SHAPES_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"blob\":{\"type\":[\"string\",\"null\"],\"description\":\"blob exercises BytesValue wrapper support.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"cityAlias\":{\"type\":[\"string\",\"null\"],\"description\":\"city_alias exercises string oneof members.\",\"examples\":[\"example\"]},\"cityDetails\":{\"type\":[\"object\",\"null\"],\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"city_details exercises message oneof members.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"cityId\":{\"type\":[\"string\",\"null\"],\"description\":\"city_id exercises int64 oneof members with ProtoJSON string encoding.\",\"examples\":[\"-1\"]},\"detailAny\":{\"type\":[\"object\",\"null\"],\"properties\":{\"@type\":{\"type\":\"string\",\"description\":\"Type URL of the embedded protobuf message, usually in the form type.googleapis.com/\\u003cfull.message.name\\u003e.\"}},\"description\":\"detail_any exercises Any with a regular embedded message payload.\\n\\nProtoJSON representation of google.protobuf.Any. Provide @type with the embedded message type URL and include the embedded message JSON fields alongside it. For well-known types that use a custom JSON form, send @type plus a value field containing that custom representation.\",\"examples\":[{\"@type\":\"type.googleapis.com/internal.testproto.example.v1.ReportDetails\",\"label\":\"from-any\"}],\"required\":[\"@type\"],\"additionalProperties\":true},\"durationAny\":{\"type\":[\"object\",\"null\"],\"properties\":{\"@type\":{\"type\":\"string\",\"description\":\"Type URL of the embedded protobuf message, usually in the form type.googleapis.com/\\u003cfull.message.name\\u003e.\"}},\"description\":\"duration_any exercises Any with a well-known embedded payload.\\n\\nProtoJSON representation of google.protobuf.Any. Provide @type with the embedded message type URL and include the embedded message JSON fields alongside it. For well-known types that use a custom JSON form, send @type plus a value field containing that custom representation.\",\"examples\":[{\"@type\":\"type.googleapis.com/google.protobuf.Duration\",\"value\":\"3600s\"}],\"required\":[\"@type\"],\"additionalProperties\":true},\"dynamic\":{\"description\":\"dynamic accepts arbitrary JSON values.\",\"examples\":[{\"kind\":\"demo\"}]},\"enabled\":{\"type\":[\"boolean\",\"null\"],\"description\":\"enabled exercises BoolValue wrapper support.\",\"examples\":[true]},\"hugeTotal\":{\"type\":[\"string\",\"null\"],\"description\":\"huge_total exercises UInt64Value wrapper support.\",\"examples\":[\"1\"]},\"items\":{\"type\":[\"array\",\"null\"],\"items\":true,\"description\":\"items accepts arbitrary JSON arrays.\",\"examples\":[[\"item\",2,true]]},\"labels\":{\"type\":[\"object\",\"null\"],\"description\":\"labels stores arbitrary string metadata.\",\"examples\":[{\"key\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\"}},\"limits\":{\"type\":[\"object\",\"null\"],\"description\":\"limits demonstrates unsigned numeric map keys encoded as JSON object keys.\",\"examples\":[{\"1\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"pattern\":\"^(0|[1-9][0-9]*)$\"}},\"mask\":{\"type\":[\"string\",\"null\"],\"description\":\"mask exercises FieldMask string support.\",\"examples\":[\"fieldName,otherField\"]},\"note\":{\"type\":[\"string\",\"null\"],\"description\":\"note exercises `StringValue` wrapper support.\",\"examples\":[\"example\"]},\"observedAt\":{\"type\":[\"string\",\"null\"],\"description\":\"observed_at is represented as an RFC 3339 timestamp string.\",\"examples\":[\"2026-03-09T10:11:12Z\"],\"format\":\"date-time\"},\"payload\":{\"type\":[\"object\",\"null\"],\"description\":\"payload accepts arbitrary JSON objects.\",\"examples\":[{\"kind\":\"demo\",\"nested\":{\"ok\":true}}],\"additionalProperties\":true},\"quantities\":{\"type\":[\"object\",\"null\"],\"description\":\"quantities demonstrates numeric map keys encoded as JSON object keys.\",\"examples\":[{\"1\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"pattern\":\"^-?(0|[1-9][0-9]*)$\"}},\"ratio\":{\"description\":\"ratio exercises ProtoJSON float special values.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"rawRatio\":{\"description\":\"raw_ratio exercises raw double ProtoJSON float support.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"smallTotal\":{\"type\":[\"integer\",\"null\"],\"description\":\"small_total exercises Int32Value wrapper support.\",\"examples\":[1]},\"toggles\":{\"type\":[\"object\",\"null\"],\"description\":\"toggles demonstrates bool map keys encoded as JSON object keys.\",\"examples\":[{\"true\":\"example\"}],\"additionalProperties\":{\"type\":\"string\"},\"propertyNames\":{\"type\":\"string\",\"enum\":[\"true\",\"false\"]}},\"total\":{\"type\":[\"string\",\"null\"],\"description\":\"total exercises Int64Value wrapper support.\",\"examples\":[\"1\"]},\"tree\":{\"type\":[\"object\",\"null\"],\"properties\":{\"child\":{\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"anyOf\":[{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},{\"type\":\"null\"}]},\"children\":{\"type\":[\"array\",\"null\"],\"items\":{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"children holds repeated recursive nodes.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},\"description\":\"children holds repeated recursive nodes.\",\"examples\":[[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]]},\"name\":{\"type\":\"string\",\"description\":\"name identifies the current node.\",\"examples\":[\"example\"]}},\"description\":\"tree exercises recursive message schemas.\\n\\nRecursiveNode exercises recursive message schemas.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false},\"ttl\":{\"type\":[\"string\",\"null\"],\"description\":\"ttl is represented as a protobuf duration string.\",\"examples\":[\"3600s\"],\"pattern\":\"^-?[0-9]+(?:\\\\.[0-9]{1,9})?s$\"},\"uintTotal\":{\"type\":[\"integer\",\"null\"],\"description\":\"uint_total exercises UInt32Value wrapper support.\",\"examples\":[1]},\"weight\":{\"description\":\"weight exercises FloatValue wrapper support.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]}},\"$defs\":{\"internal.testproto.example.v1.RecursiveNode\":{\"type\":\"object\",\"properties\":{\"child\":{\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"anyOf\":[{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"child points at the next recursive node.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},{\"type\":\"null\"}]},\"children\":{\"type\":[\"array\",\"null\"],\"items\":{\"$ref\":\"#/$defs/internal.testproto.example.v1.RecursiveNode\",\"description\":\"children holds repeated recursive nodes.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]},\"description\":\"children holds repeated recursive nodes.\",\"examples\":[[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}]]},\"name\":{\"type\":\"string\",\"description\":\"name identifies the current node.\",\"examples\":[\"example\"]}},\"description\":\"RecursiveNode exercises recursive message schemas.\",\"examples\":[{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false}},\"description\":\"DescribeAdvancedShapesResponse echoes supported advanced protobuf shapes.\",\"examples\":[{\"blob\":\"aGVsbG8=\",\"cityAlias\":\"example\",\"detailAny\":{\"@type\":\"type.googleapis.com/internal.testproto.example.v1.ReportDetails\",\"label\":\"from-any\"},\"durationAny\":{\"@type\":\"type.googleapis.com/google.protobuf.Duration\",\"value\":\"3600s\"},\"dynamic\":{\"kind\":\"demo\"},\"enabled\":true,\"hugeTotal\":\"1\",\"items\":[\"item\",2,true],\"labels\":{\"key\":\"example\"},\"limits\":{\"1\":\"example\"},\"mask\":\"fieldName,otherField\",\"note\":\"example\",\"observedAt\":\"2026-03-09T10:11:12Z\",\"payload\":{\"kind\":\"demo\",\"nested\":{\"ok\":true}},\"quantities\":{\"1\":\"example\"},\"ratio\":1.25,\"rawRatio\":1.25,\"smallTotal\":1,\"toggles\":{\"true\":\"example\"},\"total\":\"1\",\"tree\":{\"child\":{\"name\":\"example\"},\"children\":[{\"name\":\"example\"}],\"name\":\"example\"},\"ttl\":\"3600s\",\"uintTotal\":1,\"weight\":1.25}],\"additionalProperties\":false,\"allOf\":[{\"oneOf\":[{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}},{\"allOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}}]},{\"allOf\":[{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}}]}}]},{\"allOf\":[{\"required\":[\"cityDetails\"],\"not\":{\"properties\":{\"cityDetails\":{\"type\":\"null\"}},\"required\":[\"cityDetails\"]}},{\"not\":{\"anyOf\":[{\"required\":[\"cityAlias\"],\"not\":{\"properties\":{\"cityAlias\":{\"type\":\"null\"}},\"required\":[\"cityAlias\"]}},{\"required\":[\"cityId\"],\"not\":{\"properties\":{\"cityId\":{\"type\":\"null\"}},\"required\":[\"cityId\"]}}]}}]}]}]}" + +EXAMPLE_API_DESCRIBE_SCALAR_SHAPES_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"boolFlag\":{\"type\":\"boolean\",\"description\":\"bool_flag exercises the plain bool kind.\",\"examples\":[true]},\"bytesValue\":{\"type\":\"string\",\"description\":\"bytes_value exercises the plain bytes kind.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"details\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"details exercises plain nested message handling.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"doubleValue\":{\"description\":\"double_value exercises the plain double kind.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]},\"fixed32Value\":{\"type\":\"integer\",\"description\":\"fixed32_value exercises the plain fixed32 kind.\",\"examples\":[1]},\"fixed64Value\":{\"type\":\"string\",\"description\":\"fixed64_value exercises the plain fixed64 kind.\",\"examples\":[\"1\"]},\"floatValue\":{\"description\":\"float_value exercises the plain float kind.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]},\"int32Value\":{\"type\":\"integer\",\"description\":\"int32_value exercises the plain int32 kind.\",\"examples\":[-1]},\"int64Value\":{\"type\":\"string\",\"description\":\"int64_value exercises the plain int64 kind.\",\"examples\":[\"-1\"]},\"optionalBoolFlag\":{\"type\":[\"boolean\",\"null\"],\"description\":\"optional_bool_flag exercises proto3 optional bool.\",\"examples\":[true]},\"optionalBytesValue\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_bytes_value exercises proto3 optional bytes.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"optionalDoubleValue\":{\"description\":\"optional_double_value exercises proto3 optional double.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"optionalFixed32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_fixed32_value exercises proto3 optional fixed32.\",\"examples\":[1]},\"optionalFixed64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_fixed64_value exercises proto3 optional fixed64.\",\"examples\":[\"1\"]},\"optionalFloatValue\":{\"description\":\"optional_float_value exercises proto3 optional float.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"optionalInt32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_int32_value exercises proto3 optional int32.\",\"examples\":[-1]},\"optionalInt64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_int64_value exercises proto3 optional int64.\",\"examples\":[\"-1\"]},\"optionalSfixed32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_sfixed32_value exercises proto3 optional sfixed32.\",\"examples\":[-1]},\"optionalSfixed64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_sfixed64_value exercises proto3 optional sfixed64.\",\"examples\":[\"-1\"]},\"optionalSint32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_sint32_value exercises proto3 optional sint32.\",\"examples\":[-1]},\"optionalSint64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_sint64_value exercises proto3 optional sint64.\",\"examples\":[\"-1\"]},\"optionalStatus\":{\"description\":\"optional_status exercises proto3 optional enum.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"anyOf\":[{\"type\":\"string\",\"title\":\"Report Status\",\"description\":\"optional_status exercises proto3 optional enum.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"enum\":[\"REPORT_STATUS_OK\",\"REPORT_STATUS_FAILED\"]},{\"type\":\"null\"}]},\"optionalTextValue\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_text_value exercises proto3 optional string.\",\"examples\":[\"example\"]},\"optionalUint32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_uint32_value exercises proto3 optional uint32.\",\"examples\":[1]},\"optionalUint64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_uint64_value exercises proto3 optional uint64.\",\"examples\":[\"1\"]},\"samples\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"integer\",\"description\":\"samples exercises repeated plain scalar handling.\",\"examples\":[-1]},\"description\":\"samples exercises repeated plain scalar handling.\",\"examples\":[[-1]]},\"sfixed32Value\":{\"type\":\"integer\",\"description\":\"sfixed32_value exercises the plain sfixed32 kind.\",\"examples\":[-1]},\"sfixed64Value\":{\"type\":\"string\",\"description\":\"sfixed64_value exercises the plain sfixed64 kind.\",\"examples\":[\"-1\"]},\"sint32Value\":{\"type\":\"integer\",\"description\":\"sint32_value exercises the plain sint32 kind.\",\"examples\":[-1]},\"sint64Value\":{\"type\":\"string\",\"description\":\"sint64_value exercises the plain sint64 kind.\",\"examples\":[\"-1\"]},\"status\":{\"type\":\"string\",\"title\":\"Report Status\",\"description\":\"status exercises plain enum handling.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"enum\":[\"REPORT_STATUS_OK\",\"REPORT_STATUS_FAILED\"]},\"textValue\":{\"type\":\"string\",\"description\":\"text_value exercises the plain string kind.\",\"examples\":[\"example\"]},\"uint32Value\":{\"type\":\"integer\",\"description\":\"uint32_value exercises the plain uint32 kind.\",\"examples\":[1]},\"uint64Value\":{\"type\":\"string\",\"description\":\"uint64_value exercises the plain uint64 kind.\",\"examples\":[\"1\"]}},\"description\":\"DescribeScalarShapesRequest exercises plain protobuf scalar kinds.\",\"examples\":[{\"boolFlag\":true,\"bytesValue\":\"aGVsbG8=\",\"details\":{\"label\":\"example\"},\"doubleValue\":1.25,\"fixed32Value\":1,\"fixed64Value\":\"1\",\"floatValue\":1.25,\"int32Value\":-1,\"int64Value\":\"-1\",\"optionalBoolFlag\":true,\"optionalBytesValue\":\"aGVsbG8=\",\"optionalDoubleValue\":1.25,\"optionalFixed32Value\":1,\"optionalFixed64Value\":\"1\",\"optionalFloatValue\":1.25,\"optionalInt32Value\":-1,\"optionalInt64Value\":\"-1\",\"optionalSfixed32Value\":-1,\"optionalSfixed64Value\":\"-1\",\"optionalSint32Value\":-1,\"optionalSint64Value\":\"-1\",\"optionalStatus\":\"REPORT_STATUS_OK\",\"optionalTextValue\":\"example\",\"optionalUint32Value\":1,\"optionalUint64Value\":\"1\",\"samples\":[-1],\"sfixed32Value\":-1,\"sfixed64Value\":\"-1\",\"sint32Value\":-1,\"sint64Value\":\"-1\",\"status\":\"REPORT_STATUS_OK\",\"textValue\":\"example\",\"uint32Value\":1,\"uint64Value\":\"1\"}],\"required\":[\"boolFlag\",\"textValue\",\"bytesValue\",\"int32Value\",\"sint32Value\",\"sfixed32Value\",\"uint32Value\",\"fixed32Value\",\"int64Value\",\"sint64Value\",\"sfixed64Value\",\"uint64Value\",\"fixed64Value\",\"floatValue\",\"doubleValue\",\"status\",\"details\"],\"additionalProperties\":false}" + +EXAMPLE_API_DESCRIBE_SCALAR_SHAPES_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"boolFlag\":{\"type\":\"boolean\",\"description\":\"bool_flag echoes the plain bool kind.\",\"examples\":[true]},\"bytesValue\":{\"type\":\"string\",\"description\":\"bytes_value echoes the plain bytes kind.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"details\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\",\"description\":\"label is an arbitrary label.\",\"examples\":[\"example\"]}},\"title\":\"Report Details\",\"description\":\"details echoes plain nested message handling.\\n\\nNested report configuration options.\",\"examples\":[{\"label\":\"example\"}],\"required\":[\"label\"],\"additionalProperties\":false},\"doubleValue\":{\"description\":\"double_value echoes the plain double kind.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]},\"fixed32Value\":{\"type\":\"integer\",\"description\":\"fixed32_value echoes the plain fixed32 kind.\",\"examples\":[1]},\"fixed64Value\":{\"type\":\"string\",\"description\":\"fixed64_value echoes the plain fixed64 kind.\",\"examples\":[\"1\"]},\"floatValue\":{\"description\":\"float_value echoes the plain float kind.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]}]},\"int32Value\":{\"type\":\"integer\",\"description\":\"int32_value echoes the plain int32 kind.\",\"examples\":[-1]},\"int64Value\":{\"type\":\"string\",\"description\":\"int64_value echoes the plain int64 kind.\",\"examples\":[\"-1\"]},\"optionalBoolFlag\":{\"type\":[\"boolean\",\"null\"],\"description\":\"optional_bool_flag echoes proto3 optional bool.\",\"examples\":[true]},\"optionalBytesValue\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_bytes_value echoes proto3 optional bytes.\",\"examples\":[\"aGVsbG8=\"],\"contentEncoding\":\"base64\"},\"optionalDoubleValue\":{\"description\":\"optional_double_value echoes proto3 optional double.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"optionalFixed32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_fixed32_value echoes proto3 optional fixed32.\",\"examples\":[1]},\"optionalFixed64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_fixed64_value echoes proto3 optional fixed64.\",\"examples\":[\"1\"]},\"optionalFloatValue\":{\"description\":\"optional_float_value echoes proto3 optional float.\",\"examples\":[1.25,\"NaN\"],\"anyOf\":[{\"type\":\"number\"},{\"type\":\"string\",\"enum\":[\"NaN\",\"Infinity\",\"-Infinity\"]},{\"type\":\"null\"}]},\"optionalInt32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_int32_value echoes proto3 optional int32.\",\"examples\":[-1]},\"optionalInt64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_int64_value echoes proto3 optional int64.\",\"examples\":[\"-1\"]},\"optionalSfixed32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_sfixed32_value echoes proto3 optional sfixed32.\",\"examples\":[-1]},\"optionalSfixed64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_sfixed64_value echoes proto3 optional sfixed64.\",\"examples\":[\"-1\"]},\"optionalSint32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_sint32_value echoes proto3 optional sint32.\",\"examples\":[-1]},\"optionalSint64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_sint64_value echoes proto3 optional sint64.\",\"examples\":[\"-1\"]},\"optionalStatus\":{\"description\":\"optional_status echoes proto3 optional enum.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"anyOf\":[{\"type\":\"string\",\"title\":\"Report Status\",\"description\":\"optional_status echoes proto3 optional enum.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"enum\":[\"REPORT_STATUS_OK\",\"REPORT_STATUS_FAILED\"]},{\"type\":\"null\"}]},\"optionalTextValue\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_text_value echoes proto3 optional string.\",\"examples\":[\"example\"]},\"optionalUint32Value\":{\"type\":[\"integer\",\"null\"],\"description\":\"optional_uint32_value echoes proto3 optional uint32.\",\"examples\":[1]},\"optionalUint64Value\":{\"type\":[\"string\",\"null\"],\"description\":\"optional_uint64_value echoes proto3 optional uint64.\",\"examples\":[\"1\"]},\"samples\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":\"integer\",\"description\":\"samples echoes repeated plain scalar handling.\",\"examples\":[-1]},\"description\":\"samples echoes repeated plain scalar handling.\",\"examples\":[[-1]]},\"sfixed32Value\":{\"type\":\"integer\",\"description\":\"sfixed32_value echoes the plain sfixed32 kind.\",\"examples\":[-1]},\"sfixed64Value\":{\"type\":\"string\",\"description\":\"sfixed64_value echoes the plain sfixed64 kind.\",\"examples\":[\"-1\"]},\"sint32Value\":{\"type\":\"integer\",\"description\":\"sint32_value echoes the plain sint32 kind.\",\"examples\":[-1]},\"sint64Value\":{\"type\":\"string\",\"description\":\"sint64_value echoes the plain sint64 kind.\",\"examples\":[\"-1\"]},\"status\":{\"type\":\"string\",\"title\":\"Report Status\",\"description\":\"status echoes plain enum handling.\\n\\nCurrent state of the report.\\n\\nREPORT_STATUS_OK: Report completed successfully.\\nREPORT_STATUS_FAILED: Report generation failed.\",\"examples\":[\"REPORT_STATUS_OK\"],\"enum\":[\"REPORT_STATUS_OK\",\"REPORT_STATUS_FAILED\"]},\"textValue\":{\"type\":\"string\",\"description\":\"text_value echoes the plain string kind.\",\"examples\":[\"example\"]},\"uint32Value\":{\"type\":\"integer\",\"description\":\"uint32_value echoes the plain uint32 kind.\",\"examples\":[1]},\"uint64Value\":{\"type\":\"string\",\"description\":\"uint64_value echoes the plain uint64 kind.\",\"examples\":[\"1\"]}},\"description\":\"DescribeScalarShapesResponse echoes plain protobuf scalar kinds.\",\"examples\":[{\"boolFlag\":true,\"bytesValue\":\"aGVsbG8=\",\"details\":{\"label\":\"example\"},\"doubleValue\":1.25,\"fixed32Value\":1,\"fixed64Value\":\"1\",\"floatValue\":1.25,\"int32Value\":-1,\"int64Value\":\"-1\",\"optionalBoolFlag\":true,\"optionalBytesValue\":\"aGVsbG8=\",\"optionalDoubleValue\":1.25,\"optionalFixed32Value\":1,\"optionalFixed64Value\":\"1\",\"optionalFloatValue\":1.25,\"optionalInt32Value\":-1,\"optionalInt64Value\":\"-1\",\"optionalSfixed32Value\":-1,\"optionalSfixed64Value\":\"-1\",\"optionalSint32Value\":-1,\"optionalSint64Value\":\"-1\",\"optionalStatus\":\"REPORT_STATUS_OK\",\"optionalTextValue\":\"example\",\"optionalUint32Value\":1,\"optionalUint64Value\":\"1\",\"samples\":[-1],\"sfixed32Value\":-1,\"sfixed64Value\":\"-1\",\"sint32Value\":-1,\"sint64Value\":\"-1\",\"status\":\"REPORT_STATUS_OK\",\"textValue\":\"example\",\"uint32Value\":1,\"uint64Value\":\"1\"}],\"required\":[\"boolFlag\",\"textValue\",\"bytesValue\",\"int32Value\",\"sint32Value\",\"sfixed32Value\",\"uint32Value\",\"fixed32Value\",\"int64Value\",\"sint64Value\",\"sfixed64Value\",\"uint64Value\",\"fixed64Value\",\"floatValue\",\"doubleValue\",\"status\",\"details\"],\"additionalProperties\":false}" + +EXAMPLE_API_HIDDEN_THING_INPUT_SCHEMA_JSON = "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"description\":\"name is the hidden request payload.\",\"examples\":[\"example\"]}},\"description\":\"HiddenThingRequest is used by the hidden RPC.\",\"deprecated\":true,\"examples\":[{\"name\":\"example\"}],\"required\":[\"name\"],\"additionalProperties\":false}" + +EXAMPLE_API_HIDDEN_THING_OUTPUT_SCHEMA_JSON = "{\"type\":\"object\",\"description\":\"HiddenThingResponse is used by the hidden RPC.\",\"additionalProperties\":false}" diff --git a/testdata/unsupported/v1/streaming.proto b/testdata/unsupported/v1/streaming.proto new file mode 100644 index 0000000..f7ef464 --- /dev/null +++ b/testdata/unsupported/v1/streaming.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package testdata.unsupported.v1; + +option go_package = "github.com/easyp-tech/protoc-gen-mcp/testdata/unsupported/v1;unsupportedv1"; + +message StreamingRequest {} + +message StreamingResponse {} + +service StreamingAPI { + rpc Watch(StreamingRequest) returns (stream StreamingResponse); +}