MCP for gRPC Health Checking protocol
- 4 minutes read - 679 wordsModel Context Protocol (MCP) is “all the rage” these days.
I stumbled upon protoc-gen-go-mcp
and think it’s an elegant application of two technologies: programmatically generating an MCP server from a gRPC protobuf.
I’m considering building an MCP server for Ackal but, thought I’d start with something simple: gRPC Health Checking protocol.
I was surprised to learn as I was doing this that there’s a new List
(Add List
method to gRPC Health service #143) added to grpc.health.v1.Health
. My (Ackal) healthcheck server does not yet implement it (see later).
Curiously, the protocol’s Watch
method is not generated by protoc-gen-go-mcp
and is not available as part of the MCP server’s “tools”. I assume (!?) this is because it’s not a unary RPC (request-response) but a server-streaming RPC (requests-responses) and that this doesn’t fit well with LLMs. Don’t know.
protoc
protoc-gen-go-mcp
uses an (underlying|backing) gRPC protobuf to generate the sources for the MCP server. health.proto
is part of the grpc-proto
repo. The plugin generates MCP-specific code in {$servicename}mcp
sub-folder and references the backing service’s methods.
I hacked this in my repo but you should let it do its thing:
GRPC_PROTO="/path/to/grpc-proto"
MODULE="..." # Your repo
PROTO="grpc/health/v1/health.proto"
protoc \
--proto_path=${GRPC_PROTO} \
--go-mcp_out=${PWD} \
--go-mcp_opt=module=${MODULE} \
--go-mcp_opt=M${PROTO}=${MODULE}/protos \
${GRPC_PROTO}/${PROTO}
The generated file imports {your-repo}/protos
aliased to protos
. This is incorrect and we also want to use the published healthpb
repo "google.golang.org/grpc/health/grpc_health_v1"
.
You can:
HEALTHPB="google.golang.org/grpc/health/grpc_health_v1"
sed \
--in-place \
--expression="s|${MODULE}/protos|${HEALTHPB}|g" \
${PWD}/protos/grpc_health_v1mcp/health.pb.mcp.go
For consistency, you may also want to replace protos
with healthpb
.
MCP
The rest of the code follows the example in the protoc-gen-go-mcp
:
main.go
:
package main
import (
"flag"
"log/slog"
"os"
grpc_health_v1mcp "your/repo/protos/grpc_health_v1mcp"
"github.com/mark3labs/mcp-go/server"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
endpoint := flag.String(
"endpoint.grpc",
"",
"The gRPC endpoint (host:port) of the Healthcheck server.",
)
flag.Parse()
serverOpts := []server.ServerOption{
server.WithLogging(),
}
s := server.NewMCPServer(
"mcp",
"0.0.1",
serverOpts...,
)
srv := Server{
logger: logger,
}
grpc_health_v1mcp.RegisterHealthHandler(s, &srv)
dialOpts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
conn, err := grpc.NewClient(*endpoint, dialOpts...)
if err != nil {
logger.Error("unable to create gRPC client",
"endpoint", *endpoint,
"err", err,
)
}
defer func() {
if err := conn.Close(); err != nil {
logger.Info("unable to close gRPC connection")
}
}()
grpcClient := healthpb.NewHealthClient(conn)
grpc_health_v1mcp.ForwardToHealthClient(s, grpcClient)
logger.Info("Starting MCP server")
if err := server.ServeStdio(s); err != nil {
logger.Error("unable to start stdio server", "err", err)
}
}
And server.go
:
package main
import (
"context"
"log/slog"
"your/repo/protos/grpc_health_v1mcp"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var _ grpc_health_v1mcp.HealthServer = (*Server)(nil)
type Server struct {
logger *slog.Logger
}
func (s *Server) Check(
ctx context.Context,
in *healthpb.HealthCheckRequest,
) (*healthpb.HealthCheckResponse, error) {
s.logger.Info("Check called")
return &healthpb.HealthCheckResponse{},
status.Errorf(
codes.Unimplemented,
"method Check not implemented",
)
}
func (s *Server) List(
ctx context.Context,
in *healthpb.HealthListRequest,
) (*healthpb.HealthListResponse, error) {
s.logger.Info("List called")
return &healthpb.HealthListResponse{},
status.Errorf(
codes.Unimplemented,
"method List not implemented",
)
}
func (s *Server) Watch(
in *healthpb.HealthCheckRequest,
srv healthpb.Health_WatchServer,
) error {
s.logger.Info("Watch called")
return status.Errorf(
codes.Unimplemented,
"method Watch not implemented",
)
}
What’s curious is that, while these Server
methods must exist in order that Server
implements the interface, they are never (!?) called.
NOTE all the methods return “not implemented”.
Test
Run the server and pipe stdout
through jq
to pretty print it:
go run . | jq -r .
Yields:
{
"time":"2025-06-12T00:00:00.000000000-07:00",
"level":"INFO",
"msg":"Starting MCP server"
}
And then to list the MCP server’s methods, we use "method": "tools/list"
NOTE MCP uses
JSON-RPC
andmodelcontextprotocol.io
documents the JSON-RPC messages includingtools/list
andtools/call
as used below.
NOTE The JSON-RPC requests should contain no newlines as these are used by this server’s stdin parsing to delimit the commands.
{
"jsonrpc":"2.0",
"id":1,
"method":"tools/list",
"params":{}
}
Yields:
{
"jsonrpc":"2.0",
"id":1,
"result":{
"tools":[
{
"description":"Check gets the health...",
"inputSchema":{
"properties":{
"service":{
"type":"string"
}
},
"required":[],
"type":"object"
},
"name":"grpc_health_v1_Health_Check"
},{
"description":"List provides a ...",
"inputSchema":{
"properties":{},
"required":[],
"type":"object"
},
"name":"grpc_health_v1_Health_List"
}
]
}
}
NOTE The gRPC fully-qualified method names are mapped from e.g.
grpc.health.v1.Health/Check
togrpc_health_v1_Health_Check
by this MCP implementation.
And to invoke the grpc.health.v1.Health/Check
method:
{
"jsonrpc":"2.0",
"id":2,
"method":"tools/call",
"params":{
"name":"grpc_health_v1_Health_Check",
"arguments":{
"service":"foo"
}
}
}
Yields:
{
"jsonrpc":"2.0",
"id":2,
"result":{
"content":[
{
"type":"text",
"text":"{\"status\":\"SERVING\"}"
}
]
}
}
That’s all!