# MCP

Source: https://gortexa.sheng.page/en/transports/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.

## Annotating methods

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

```protobuf
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.

## Endpoint

- 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)

## tools/list

```bash
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.

## tools/call

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.

## Schema export

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

```bash
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.

## FAQ

### 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?

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.

### Does the MCP endpoint need sessions?

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?

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.
