Agent-first, self-describing APIs with protobuf reflection

Whichever side you are on the LLM turf wars, one thing is clear. APIs are back, baby! It’s funny how fashion comes and goes, but Unix philosophy tools and wiring APIs together are what moves the industry right now (whether it moves it to the right direction is an entirely different question).

A new bottleneck arises: how to make agents understand and use new APIs. The truth is, not everyone gives their reference pages the same polish and attention that Stripe does. Does your team offer an OpenAPI spec and make sure it’s up-to-date? If that’s a yes, great work, but certainly that’s not the case for most.

One thing I’m really bullish on is Protobuf APIs, and especially Protobuf reflection.

At work, we’ve been using ConnectRPC for a couple of years now and we’ve been really happy with that choice. But the unexpected superpower recently has proved to be how it allows for a self-describing API that agents can browse and understand with a few network calls, without a copy of the schema.

A real-world example.

Here’s a small example from a fresh Claude Code session.

$ claude
❯ Reach out to the API in localhost:8080 using grpcurl and use protobuf reflection.
Give me an overview of the available services and methods I can use to manage pipelines.

⏺ Bash(grpcurl -plaintext localhost:8080 list)
  ⎿  collector.v1.CollectorService
     discovery.v1.DiscoveryService
     instrumentation.v1.InstrumentationService
     … +3 lines (ctrl+o to expand)

⏺ Bash(grpcurl -plaintext localhost:8080 describe pipeline.v1.PipelineService)
  ⎿  pipeline.v1.PipelineService is a service:
     service PipelineService {
       rpc BulkDeletePipelines ( .pipeline.v1.BulkDeletePipelinesRequest ) returns ( .pipeline.v1.BulkDeletePipelinesResponse );
     … +13 lines (ctrl+o to expand)

⏺ Here's an overview of the pipeline.v1.PipelineService available at localhost:8080:

  CRUD Operations

  ┌─────────────────────┬──────────────────────────────────────────────────────────────┐
  │       Method        │                         Description                          │
  ├─────────────────────┼──────────────────────────────────────────────────────────────┤
  │ CreatePipeline      │ Create a single pipeline, returns the created Pipeline       │
  ├─────────────────────┼──────────────────────────────────────────────────────────────┤
  │ GetPipeline         │ Retrieve a pipeline by ID                                    │
  ├─────────────────────┼──────────────────────────────────────────────────────────────┤
  │ GetPipelineID       │ Look up a pipeline's ID (likely by name or other identifier) │
  ├─────────────────────┼──────────────────────────────────────────────────────────────┤
  │ UpdatePipeline      │ Update an existing pipeline                                  │
  ├─────────────────────┼──────────────────────────────────────────────────────────────┤
  │ UpsertPipeline      │ Create or update a pipeline (insert-or-update)               │
  ├─────────────────────┼──────────────────────────────────────────────────────────────┤
  │ DeletePipeline      │ Delete a single pipeline                                     │
  ├─────────────────────┼──────────────────────────────────────────────────────────────┤
  │ BulkDeletePipelines │ Delete multiple pipelines at once                            │
  └─────────────────────┴──────────────────────────────────────────────────────────────┘

[.......]

  You can drill deeper into any method's request/response types with e.g. grpcurl -plaintext localhost:8080 describe
  pipeline.v1.CreatePipelineRequest.

These are the very same endpoints that power our UI. We didn’t have to build anything new.

With a couple sentences as input, Claude was able to dive and understand that API, what its inputs and outputs are, how services have evolved and so on. It can use the same functionality without any browser automation. The agent can focus on what it needs, and avoid burning context from reading lengthy documentation about other features. Furthermore, the truth is objective; specs drift, documentation gets outdated, but what is defined in Protobuf files is what the server receives and sends back. There’s no need for trial-and-error.

As we’re combining new APIs that operate on higher abstraction layers, agents also get these new powers almost for free. For example, we now supports a new pair of APIs for Discovery and Instrumentation. These allow onboarding services for telemetry using a slick and shiny UI in the Instrumentation Hub. I’m proud of that UI jazz, but you know what? You can also tell Claude ‘look here, run discovery and instrument X/Y/Z’ and call it a day.

So what?

I think this is an interesting lightweight alternative to MCPs. No special infrastructure, no MCP server to maintain, no need to write new tool definitions. The agent can discover what’s available on demand. Pair it with the authentication you’re likely already using, and you’re off.

This also works really well with ConnectRPC’s approach of HTTP/JSON compatibility. Once the agents sketch out the endpoints, they don’t even need gRPC tooling, they can build payloads and use curl for all subsequent calls. And if you want to build new skills/tools for your agents, you can do so with a much greater degree of confidence.

Interested? Try it out!

If you want to poke around with an example service, here’s a great starting point.

Let’s start the server

$ git clone https://github.com/grpc/grpc-go
$ cd grpc-go/examples/features/reflection/
$ go run server/main.go
server listening at [::]:50051

Then use grpcurl to poke around

$ grpcurl -plaintext localhost:50051 list

grpc.examples.echo.Echo
grpc.reflection.v1.ServerReflection
grpc.reflection.v1alpha.ServerReflection
helloworld.Greeter

$ grpcurl -plaintext localhost:50051 list helloworld.Greeter

helloworld.Greeter.SayHello

Hmm, looks like this server exposes four services. The Greeter service has a SayHello RPC. Let’s inspect what that looks like.

$ grpcurl -plaintext localhost:50051 describe helloworld.Greeter

helloworld.Greeter is a service:
service Greeter {
  rpc SayHello ( .helloworld.HelloRequest ) returns ( .helloworld.HelloReply );
}

$ grpcurl -plaintext localhost:50051 describe helloworld.HelloRequest

helloworld.HelloRequest is a message:
message HelloRequest {
  string name = 1;
}

This should be enough information to call it, let’s go ahead and do that.

$ grpcurl -plaintext -format text -d 'name: "gRPCurl"' \
  localhost:50051 helloworld.Greeter.SayHello

message: "Hello gRPCurl"

Outro

So that’s all for today. A small brain-dump on something I’m excited about.

What’s your take on agent-first APIs? If your team is working with protos, give this a spin!

Written on March 31, 2026