An Envoy external processor that transforms Model Context Protocol (MCP) requests into upstream HTTP API calls based on OpenAPI specifications.
The external processor server communicates only with Envoy over gRPC via a Unix domain socket. Envoy owns all downstream client and upstream service connections, while the processor inspects and can mutate request and response data relayed by Envoy.
go get github.com/ing-bank/envoy-mcp-openapi-processorSee examples/mcp-server for a complete working example including Envoy proxy configured to use the
envoy-mcp-openapi-processor server.
To enable export of logs and traces to an OpenTelemetry collector, use one of the options below. If neither is used, the server runs with a no-op tracer provider and a no-op logger.
package main
import (
"context"
mcp_proc "github.com/ing-bank/envoy-mcp-openapi-processor"
)
func main() {
telemetryConfig := mcp_proc.TelemetryConfig{
OtelEndpoint: "otel-collector:4317",
ServiceName: "mcp-sidecar",
ServiceVersion: "1.0.0",
}
ctx := context.Background()
err := mcp_proc.InitLogger(telemetryConfig)
// ...
tracerShutdown, err := mcp_proc.InitTracer(ctx, telemetryConfig)
// ...
var cfg mcp_proc.Config
// ...
mcp_proc.RunServer(ctx, &cfg)
}Set the global tracer provider by calling otel.SetTracerProvider(myTracerProvider) and the global zap logger by calling zap.ReplaceGlobals(myLogger) before starting the server.
To run all tests, execute the following command:
make testTo check the test coverage, execute the following command:
make coverage-checkTo run the end-to-end suite (spins up Envoy, the processor, and a mock upstream via Docker Compose, then exercises MCP <=> REST translation), execute the following command:
make e2eRequires Docker. On failure, container logs are captured in e2e/last-run.log.
Run server handler fuzz tests one at a time:
make fuzz-requestmake fuzz-responseEach command runs until stopped with Ctrl+C and reports any issues found.
The processor validates the Host/:authority and Origin headers of every
request. By default only loopback hostnames are accepted: localhost, 127.0.0.1 and
::1 (any port); anything else is refused with HTTP 403. This protects unauthenticated
localhost deployments against DNS rebinding attacks where a malicious website's DNS resolves
to 127.0.0.1 so the victim's browser sends same-origin requests to the local server.
A missing Host header is rejected (fail closed). A missing Origin header
is accepted (non-browser clients do not send one), but a present Origin
must be a URL whose hostname is allowed.
Deployments serving other domains configure them via Config.AllowedHosts:
cfg := &mcp_proc.Config{
// ...
AllowedHosts: []string{"mcp.example.com"},
}Matching is case-insensitive and ignores ports. A non-empty list replaces
the localhost default — include "localhost" explicitly if it should remain
allowed.
Each entry is one of:
"*"— match any host. This disables DNS rebinding protection, so use it only on trusted networks."*.example.com"— match any subdomain at any depth (foo.example.com,a.b.example.com) but not the apexexample.com."mcp.example.com"— exact match.
Before deploying to production, we advise to verify if the following controls are in place
- Container runs as non-root user
- Read-only root filesystem where possible
- No privileged mode
- Resource limits (CPU, memory) configured
- seccomp profile applied
- AppArmor/SELinux enabled
- Container image scanned for CVEs
- Image signed and verified
- TLS configured for downstream connections
- TLS configured for upstream connections
- Buffer limits set (≤32KB for edge deployments)
- Connection limits configured
- Stream limits configured (≤100 concurrent for HTTP/2)
- Timeouts configured (connection, stream, request)
- Overload manager enabled
- Admin endpoint restricted to localhost
-
use_remote_address: truefor edge deployments - Path normalization enabled
-
headers_with_underscores_action: REJECT_REQUEST