Skip to content

MCP

The MCP (Model Context Protocol) bridge exposes proto methods annotated with gortexa.ai.v1 as tools. An AI agent sends JSON-RPC to /mcp, and the bridge calls the original service over an in-process gRPC loopback — so AI calls pass through the same interceptor chain as everything else, auth and validation included.

Add the gortexa.ai.v1.ai_tool annotation to an rpc:

rpc GetResource(GetResourceRequest) returns (Resource) {
option (google.api.http) = {get: "/v1/resources/{id}"};
option (gortexa.ai.v1.ai_tool) = {
expose: true
name: "get_resource"
description: "Fetch a single resource by id and return its current state."
read_only: true
};
}

read_only and destructive feed the behaviour hints in the tool schema. Methods without expose: true do not appear in the tools list.

  • Path: /mcp (Streamable HTTP)
  • Protocol versions: 2025-03-26 (default) and 2024-11-05
  • Request bodies capped at 1 MiB, read timeout 30 seconds
  • JSON-RPC batches capped at 100 elements; a larger batch gets 413 with a -32000 "batch too large" JSON-RPC error (rejected before the interceptor chain runs)
  • The bridge is stateless: it issues no Mcp-Session-Id and keeps no session state; every request is authorized on its own credentials
  • A rejected Origin and an over-cap batch are each logged at WARN through slog.Default() (with the origin / element count and the client IP) — previously both were answered with a status code and no operator-visible record
  • For browser-based clients, enable server.enable_cors and list the allowed origins in server.cors_origins — that allowlist is the DNS-rebinding guard; a request whose Origin is not listed gets 403 forbidden origin, and a "*" entry is ignored here (list each browser origin explicitly)
Terminal window
curl -XPOST localhost:8080/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Returns the tool schema for every annotated method. The list is memoized (about 142 ns per call), not recomputed on every request.

Arguments to tools/call are serialized into a protobuf message and dispatched to the original service over the loopback; the result is converted back to JSON-RPC. A call that fails auth gets an unauthenticated error envelope, consistent with gRPC and HTTP behaviour. Authorization and X-Request-Id are forwarded to the loopback only when their bytes are valid gRPC metadata (printable ASCII); an invalid value is dropped rather than propagated, so the caller still gets the chain’s real answer (e.g. unauthenticated) instead of an opaque internal, and a fresh request id is minted downstream. An Authorization value longer than 8 KiB is dropped too: a real credential is a few KB at most, and the bridge re-encodes the header onto one loopback call per batch element (up to 100), so an oversized one is an amplification vector rather than a credential.

The same gortexa.ai.v1 annotations can be exported offline in provider-ready formats:

Terminal window
gortexa export --format=mcp
gortexa export --format=openai # OpenAI strict function calling
gortexa export --format=gemini

All three outputs are locked by golden tests: when the proto changes, the schemas change, and unexpected drift fails the tests.

How do I expose a gRPC service as MCP tools for an LLM?

Section titled “How do I expose a gRPC service as MCP tools for an LLM?”

Annotate the proto methods with gortexa.ai.v1, then add the service to mcp.ServiceDescriptors(...) and mount mcp.NewBridge at /mcp. The tool schema is generated from the proto — there is no separate tool definition to hand-write.

Do MCP tool calls go through my auth interceptors?

Section titled “Do MCP tool calls go through my auth interceptors?”

Yes. tools/call is dispatched over an in-process loopback into the same gRPC server, so it traverses recover, request-id, logging, load shedding, rate limiting, the circuit breaker, auth and validation exactly like any other request.

No. The bridge is stateless: it issues no Mcp-Session-Id and keeps none, and every request is authorized on its own credentials.

How do I configure a browser-based MCP client?

Section titled “How do I configure a browser-based MCP client?”

Enable server.enable_cors and list the allowed origins in server.cors_origins. That allowlist doubles as the DNS-rebinding guard — a request from an unlisted origin is rejected with 403 forbidden origin and logged at WARN. A "*" entry, which CORS treats as allow-all, is deliberately ignored by this guard, so list each browser origin explicitly.