Backends reference
The 27 backend kinds MCPG ships — HTTP, command, NATS, gRPC, GraphQL, Kafka, SQL, OpenAPI, mock, pipeline, plus a 17-kind LLM family — each selected by a nested backend.kind discriminator on an MCP capability.
Adapted from
apps/gateway/docs/backends.md. Every config example uses the real nested binding shape. Validate any full config withcargo run -q -p mcpg --bin mcpg-config-check -- <file>, and see the configuration reference for the full key tree.
A backend maps one MCP capability (a tool, prompt, resource, or resource
template) to one downstream integration. You attach it as a nested backend:
block on the capability, and a kind: discriminator selects the
implementation. The gateway's BackendImpl enum defines 27 kinds:
- 10 general-purpose:
http,command,nats,grpc,graphql,kafka,mock,pipeline,openapi,sql - 17 LLM kinds: managed bindings to hosted and self-hosted model APIs,
with underscore discriminators like
openai_chat,anthropic_chat,gemini_embedding,stability_image,openai_tts— see the LLM backend family below.
Every backend ships as a runtime-loaded cdylib plugin — the mcpg binary
hard-wires no backend. A binding selecting a kind whose plugin is not declared
in plugins[] fails fast at boot.
Where bindings live
Bindings sit under mcp.capabilities.{tools,prompts,resources,resource_templates}[].
There is no top-level bindings: block. A minimal tool binding:
mcp:
capabilities:
tools:
- name: dev.echo
description: Echo a value back for testing.
backend:
kind: mock
response: { ok: true }
Common binding fields
Every binding shares these fields. The implementation lives under backend:;
governance, retry, and quota policy are sibling blocks.
- name: "tool_name" # string — unique MCP capability name (required)
description: "What it does" # string — MCP tool description (required)
title: null # optional — display title
input_schema: null # optional — JSON Schema for argument validation
output_schema: null # optional — JSON Schema for the result
governance: # optional — pre-dispatch policy gate
minimum_trust: verified # unauthenticated | header_asserted | verified
allow_if: '"platform" in $identity.groups' # optional CEL predicate
retry: # optional — see the configuration reference
max_attempts: 3
backend: # required — implementation, discriminated by kind:
kind: http # one of 27 BackendImpl kinds
# …kind-specific fields are flattened siblings of `kind:`
- Governance.
minimum_trustandallow_ifare evaluated by the pre-dispatch policy gate before the backend runs. See identity and policy. - Input validation. When
input_schemais a valid JSON Schema, tool arguments are validated against it before dispatch; invalid arguments produce a JSON-RPC error without invoking the backend. - Per-call CEL. Many backend fields (
url, header values, SQL params) accept CEL templates against the standard variable bag —${$arguments.X},${$context.principal_id},${$tool_name}. Templates compile at config load and evaluate per call. - Per-caller credentials.
cred://<plugin>/<target>URIs in supported fields resolve per request, so each caller's downstream call carries its own credential.
HTTP
Executes an HTTP request to a downstream endpoint (POST or GET).
backend:
kind: http
url: "https://api.example.com/accounts/${$arguments.account_id}" # required; CEL + cred:// supported
method: get # post | get (default: post)
timeout_ms: 5000
max_response_bytes: 1048576
expected_status_codes: [200]
require_json_response: true
headers: # CEL + cred:// supported per value
Authorization: "Bearer ${cred://vault-oauth/api}"
- POST sends tool arguments as the JSON body; GET sends them as query parameters.
- Non-matching status codes produce an error result with retry classification.
- The plugin (
dev.mcpg.backend.http) owns the reqwest client, a per-credential client cache, and a DNS-rebinding guard that refuses loopback / RFC1918 / link-local / ULA destinations after resolution. Setgateway.server.allow_private_backends: true(or per-bindingallow_private_backends: true) to permit private targets in trusted networks.
Command
Runs a local subprocess and captures its output.
backend:
kind: command
command: "/usr/bin/my-tool" # executable path (required, never templated)
args: ["--format", "json"] # CEL templating supported against $arguments / $context
timeout_ms: 10000
max_output_bytes: 1048576
require_json_stdout: true
- Tool arguments are written to stdin as JSON; stdout is captured up to
max_output_bytes. - A non-zero exit code, a timeout, or non-JSON stdout (when
require_json_stdout) produces an error result. - Runs locally — no network or SSRF surface, so the plugin entry needs no
config:block.
NATS
Sends a request via the NATS Core request/reply pattern.
backend:
kind: nats
url: "nats://broker:4222" # all nats bindings must agree
credentials_path: "/etc/mcpg/nats.creds" # optional
subject: "backend.request" # required
timeout_ms: 5000
max_response_bytes: 1048576
- Publishes arguments as JSON to
subject, waits for a reply on an auto-generated inbox, and propagates W3Ctraceparent/tracestatevia NATS headers. - The plugin's connection (
url/credentials_path) is the single source of truth across all NATS bindings, so theplugins[]entry needs noconfig:block. The single entry registers both thenatsbinding and thenats_topicwatch strategy.
plugins:
- id: dev.mcpg.backend.nats
source:
oci: ghcr.io/mcpg-dev/source-code/plugins/backend-nats:<version>-linux-amd64
# or: path: /opt/mcpg/plugins/libmcpg_plugin_backend_nats.so
gRPC
Invokes a gRPC service method using proto-less JSON mapping over HTTP/2.
backend:
kind: grpc
url: "https://grpc.example.com" # endpoint base (required)
service: "example.UserService" # fully-qualified service (required)
method: "GetUser" # method name (required)
timeout_ms: 5000
max_response_bytes: 1048576
headers: {}
- Sends arguments as a JSON body via HTTP POST to
{endpoint}/{service}/{method}; expects a JSON response mapped from protobuf. - Built on the shared
net-coreHTTP core (per-credential client cache, DNS-rebinding guard, per-call CEL /cred://), the same core HTTP and GraphQL use.
GraphQL
Executes a GraphQL query or mutation.
backend:
kind: graphql
url: "https://api.example.com/graphql" # required
operation: "query GetUser($id: ID!) { user(id: $id) { name } }" # required
timeout_ms: 5000
max_response_bytes: 1048576
headers: {}
- Sends
{ "query": "...", "variables": <arguments> }as a JSON POST; tool arguments become GraphQL variables, and the responsedatafield is extracted as output. - A non-200 status or a non-empty
errorsarray produces an error result.
Kafka
Sends a message and waits for a correlated response.
backend:
kind: kafka
bootstrap_servers: "broker:9092" # all kafka bindings must agree
group_id: "mcpg" # all kafka bindings must agree
request_topic: "requests"
response_topic: "responses"
timeout_ms: 10000
max_response_bytes: 1048576
- Generates a correlation ID, subscribes to
response_topic, publishes arguments torequest_topicwith that ID (and forwards W3Ctraceparent), then filters replies by correlation ID. - Connection config (
bootstrap_servers/group_id) is the single source of truth across all Kafka bindings, so theplugins[]entry needs noconfig:block. The single entry registers both thekafkabinding and thekafka_topicwatch strategy.
SQL
Executes a parameterized query (or stored procedure) against PostgreSQL,
MySQL/MariaDB, or SQLite, returning rows shaped by row_mode.
backend:
kind: sql
driver: postgres # postgres | mysql | mariadb | sqlite
url: "postgres://app:${env.ORDERS_DB_PW}@db:5432/orders" # creds via ${env.X}
pool:
max_connections: 10
min_idle: 1
acquire_timeout_ms: 5000
query:
sql: "SELECT id, total FROM orders WHERE tenant = :tenant" # or procedure / sql_file
params: ["tenant"] # named placeholders bind in this order
row_mode: many # single | many | scalar | affected_rows | resource_contents | stream
max_rows: 1000
timeout_ms: 3000
schema:
derive: input # off | input | output | both — opt-in
session_vars: # Postgres only (SET LOCAL via set_config)
app.current_tenant: "${identity.tenant}"
The SQL backend is the deepest of the set. Highlights:
- Three credential surfaces, exactly one per binding. A static password in
the
url(with${env.VAR}expanded at load); per-callercred://in the URL or anysession_varsvalue; or cloud-DB IAM via anauth:block.rds_iamis shipped (Cargo featuresql-rds-iam);azure_ad,gcp_iam, andaurora_failoverparse and validate today but return a clear "not yet implemented" error at boot. Combining two surfaces is a config error caught at startup. - Schema derivation (
schema.derive). Withinputorboth, the plugin introspects prepared-statement parameter types (Postgres today) and merges a JSON Schema fragment intotools/list; operator-suppliedinput_schemafields win at every key. - Streaming (
row_mode: stream). Stateless keyset-cursor pagination — clients pass the previous response'snext_cursoras the_cursorargument. Multi-instance gateways must setstream.signing_key_envto a shared secret so any replica can decode and resume a cursor. - Fire-and-wait (
await). Submit work, then poll a check query and evaluate a CELpredicateuntil it matches or times out — for async provisioning flows. - Circuit breaker, schema-drift retry, driver-level cancel. Per-backend
fail-fast; transparent re-prepare on stale-plan SQLSTATEs; in-flight cancel
via
pg_cancel_backend(Postgres),KILL QUERY(MySQL), orsqlite3_interrupt(SQLite). - Resources and watch strategies. Add the binding under
mcp.capabilities.resources[](staticuri) orresource_templates[](uri_templatelikesqldoc://{slug});row_mode: resource_contentsauto-wraps SELECTeduri/text/mime_typecolumns into the MCPcontentspayload. Awatch.strategyofsql_polling(any engine) orpostgres_listen_notify(Postgres-only, lower overhead) re-emits change events to subscribed MCP sessions.
For transactional multi-statement flows inside a pipeline, see the
sql_tx pipeline step.
OpenAPI
Surfaces one operation of a registered OpenAPI spec as an MCP tool. The
operator names only a source + operation; the plugin parses the spec,
derives the tool's input/output schema, and dispatches an outbound HTTP
request.
backend:
kind: openapi
source: petstore # a source declared in the plugin's own config (required)
operation: getPetById # the OpenAPI operationId to surface (required)
The source is declared in the plugin's own config — the spec, upstream
base_url, per-scheme auth, and limits live there:
plugins:
- id: dev.mcpg.backend.openapi
source:
oci: ghcr.io/mcpg-dev/source-code/plugins/backend-openapi:<version>-linux-amd64
config:
sources:
- name: petstore
spec: "file:///etc/mcpg/specs/petstore.yaml" # file:// URI or { inline: {...} }
base_url: "https://api.petstore.example.com"
auth:
apiKeyAuth: "${cred://vault-api/petstore}" # keyed by the spec's securityScheme name
Two surfacing modes:
- Reference-only. One explicit
backend: { kind: openapi, source, operation }binding per operation you choose to expose. - Bulk auto-expose. Set
expose.tools: trueon the source to fan out every (filtered) operation as a tool; read-by-idGETs become resource templates by default.filter(allow/deny) andtool_prefixscope and namespace the generated tools.
Mock
Returns a configured response with no I/O — for testing and development.
backend:
kind: mock
response: # configured response
message: "hello from mock"
status: "ok"
delay_ms: 0 # simulated latency
error: false # simulate a tool error
error_message: null # error text (error mode)
passthrough: false # treat `response` as a literal CallToolResult
- Default:
responseis JSON-stringified into a text content block plus structured metadata. error: truereturns a simulated error result.passthrough: truesurfacesresponseas a literalCallToolResult(its owncontent/isError/structuredContent) — for image, audio, embedded-resource, or mixed-content shapes. Validated at registration.
The mock plugin has no network surface, so its plugins[] entry needs no
config: block.
Pipeline
Orchestrates multiple steps into a single tool call — data flow between steps, conditional gates, transactional SQL groups, and client interaction (elicitation / sampling / roots).
backend:
kind: pipeline
pipeline_timeout_ms: 30000 # total pipeline timeout
steps:
- id: fetch_order
kind: http
url: "https://orders.internal/v1/orders/${original_args.order_id}"
method: get
- id: check_balance
kind: cel_gate
expression: 'completed_steps.fetch_order.output.balance >= 0'
error_message: "Order balance is negative"
- id: confirm
kind: elicitation
message: "Confirm the charge?"
- id: shape
kind: transform
expression: 'completed_steps.fetch_order.output'
- Steps execute sequentially, sharing a context (
original_args,request_context,completed_steps); the first error aborts the pipeline. - Suspending steps (elicitation, sampling, roots, gather) persist state and resume on the client's response — durable across load-balanced instances.
The pipeline backend has its own full reference: see pipeline steps for all 18 step kinds.
LLM backend family
Beyond the 10 general-purpose kinds, BackendImpl defines a 17-kind LLM
family — managed bindings to hosted and self-hosted model APIs. Each is a
separate backend.kind: (note the underscore discriminators), backed by an
LLM cdylib. Like sql, these bindings forward a raw spec to the plugin, which
owns the per-kind schema (model, generation parameters, streaming, and the API
key via cred://).
backend:
kind: openai_chat
model: gpt-4o-mini
api_key: "${cred://vault-openai/key}"
backend.kind | Surface | Plugin |
|---|---|---|
openai_chat | OpenAI chat completions | backend-llm-openai |
azure_openai_chat | Azure OpenAI chat (per-deployment URL) | backend-llm-openai |
anthropic_chat | Anthropic Messages API | backend-llm-anthropic |
gemini_chat | Google Gemini AI Studio chat | backend-llm-gemini |
compat_chat | Any OpenAI-compatible endpoint (vLLM, LocalAI, Together, Groq, OpenRouter, llama.cpp, Vertex compat) | backend-llm-compat |
openai_embedding | OpenAI embeddings | backend-llm-openai |
azure_openai_embedding | Azure OpenAI embeddings | backend-llm-openai |
gemini_embedding | Gemini embeddings | backend-llm-gemini |
compat_embedding | OpenAI-compatible embeddings | backend-llm-compat |
openai_image | OpenAI image generation (DALL-E) | backend-llm-openai |
azure_openai_image | Azure OpenAI image | backend-llm-openai |
gemini_image | Google Imagen | backend-llm-gemini |
stability_image | Stability AI Stable Image (Core / SD3 / Ultra) | backend-llm-stability |
openai_tts | OpenAI text-to-speech | backend-llm-openai |
azure_openai_tts | Azure OpenAI TTS | backend-llm-openai |
openai_stt | OpenAI speech-to-text (Whisper) | backend-llm-openai |
azure_openai_stt | Azure OpenAI STT | backend-llm-openai |
compat_chat and compat_embedding require an explicit base_url; api_key
is optional for them. Each LLM kind requires its cdylib declared in
plugins[].
Built-in debug tools
When feature_flags.debug_tools_enabled: true, these tools register automatically (gated by
debug.tools.exposure flags). They are all off when feature_flags.debug_tools_enabled: false
(the default).
| Tool | Description |
|---|---|
mcpg.runtime.snapshot | Runtime metadata: service info, uptime, session count |
mcpg.request.echo | Echo request context and arguments |
mcpg.debug.command_probe | Execute a configured command profile |
mcpg.debug.network_probe | HTTP GET to a configured endpoint |
mcpg.debug.network_json_call | HTTP POST JSON to a configured endpoint |
See also
- Configuration reference — the full
AppConfigkey tree, includingplugins[]andmcp.capabilities. - Pipeline steps — all 18 pipeline step kinds.
- Identity and policy — how
governance.minimum_trustandallow_ifare enforced before dispatch.