Add MCP support to any REST API - no code changes required.
We've spent decades building REST APIs. Adding MCP support shouldn't mean
reimplementing your business logic. mcp-proxy
exposes your existing REST endpoints as MCP tools automatically, with zero changes to your API.
mcp-proxy
loads a YAML config describing one or more upstream REST APIs, dynamically registers MCP tools for them, and translates each tool call into an HTTP request against the real upstream API and back.
flowchart LR
A[MCP Client] -- MCP tool call --> B[mcp-proxy]
B -- HTTP request --> C[REST API]
C -- HTTP response --> B
B -- MCP tool result --> A
mcp-proxy
can be deployed in a sidecar pattern in Kubernetes or infront of any API service that doesnt provide a native MCP server.
Say for example, you already have an REST API server at https://api.gold-api.com but you want to be able to query it via MCP.
To achieve this, create a config.yaml
endpoints:
- name: gold-api
upstream:
base_url: "https://api.gold-api.com"
timeout: 10s
auth:
type: "none"
tools:
- name: getPrice
description: "Fetch gold price"
http:
method: GET
path: "/price/XAU"
Run the server.
mcp-proxy --config config.yaml
What happens here is a new MCP tool called getPrice
is registered on the server. When this MCP tool is called the server makes a HTTP GET
request to https://api.gold-api.com/price/XAU
and returns the result to the MCP Client.
Verify its working by making MCP tool call using curl
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "gold-api_getPrice",
"arguments": {}
}
}'
Response:
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"currency\":\"USD\",\"price\":4409.299805}"}],"structuredContent":{"currency":"USD","price":4409.299805}}}
Congrats! We have now adapted our API to be MCP ready with no code changes to the API!
go install github.com/crhuber/mcp-proxy/cmd/mcp-proxy@latest
Or build from source:
go build -o mcp-proxy ./cmd/mcp-proxy
Or Use kelp
kelp add crhuber/mcp-proxy --install
mcp-proxy --config config.yaml
| Flag | Env var | Default | Description |
|---|---|---|---|
--config |
|||
MCP_PROXY_CONFIG |
|||
| (required) | |||
| Path to the proxy config YAML file | |||
--listen |
|||
MCP_PROXY_LISTEN_ADDR |
|||
:8080 |
|||
| Address to listen on | |||
--auth-mode |
|||
MCP_PROXY_AUTH_MODE |
|||
none |
|||
none |
bearer — whether the proxy's own /mcp endpoint requires a bearer token |
||
--log-level |
|||
MCP_PROXY_LOG_LEVEL |
|||
info |
|||
debug |
info |
warn |
error |
--shutdown-grace |
|||
MCP_PROXY_SHUTDOWN_GRACE |
|||
15s |
|||
| How long to wait for in-flight requests to finish on shutdown | |||
--disable-localhost-protection |
|||
MCP_PROXY_DISABLE_LOCALHOST_PROTECTION |
|||
false |
|||
| Disable the MCP SDK's DNS-rebinding Host header check |
When --auth-mode=bearer
, set MCP_PROXY_BEARER_TOKEN
in the environment
(it is intentionally not a flag so it never shows up in --help
output or process argv).
The MCP SDK auto-enables DNS-rebinding protection: if the TCP connection it
accepted came in on a loopback address, it requires the request's Host
header to also look like loopback (localhost
, 127.0.0.0/8
, ::1
), and
otherwise rejects it with 403 Forbidden: invalid Host header
. This guards local companion services against a browser being tricked into hitting them, and doesn't apply to mcp-proxy's deployment model.
If a sidecar connects to mcp-proxy over 127.0.0.1
but forwards the
original external Host
header (e.g. mcp.domain.com
) — the accepted
connection's local address is loopback regardless of what address mcp-proxy
itself binds to, so changing --listen
/MCP_PROXY_LISTEN_ADDR
alone won't
fix it. Set --disable-localhost-protection
(or
MCP_PROXY_DISABLE_LOCALHOST_PROTECTION=1
) in that topology.
The config file describes one or more upstream endpoints and the tools that should be generated for each. See config.yaml.example for a full annotated example, including path/query parameters, request body templating, and response field selection.
To connect to an upstream REST API, you need to define your upstream base_url
, timeout
, and auth
type.
endpoints:
- name: gold-api
upstream:
base_url: "https://api.gold-api.com"
timeout: 10s
auth:
type: "none"
The base_url should be defined without a trailing slash /
.
Auth types supported are:
bearer
auth:
type: bearer # bearer | header | query | none
env: ORDERS_API_KEY # secret ALWAYS comes from this env var, never literal in file
header
auth:
type: header
header: X-API-Key
type: "none"
env: ORDERS_API_KEY # secret ALWAYS comes from this env var, never literal in file
none
auth:
type: "none"
For any key defined in env
it must be set in environment variables before starting the server. In the preceeding examples ORDERS_API_KEY
must be set
Tools are the MCP tools to create on the MCP server. Each tool has name
, description
, and parameters
tools:
- name: createInvoice
description: "Create a invoice for a customer."
parameters:
type: object
required: [customerId]
properties:
customerId:
type: string
description: "Customer to bill."
Whenever a toolcall is made to the attached tool, a coresponding http request is made to the upstream.
The http
object controls what how the HTTP request upstream is made. The usual parameters for a HTTP request including method
, path
and body
tools:
- name: getPrice
description: "Fetch gold price"
http:
method: GET
path: "/price/XAU"
A JSON request body can also be sent on any HTTP method by adding the body
http:
method: POST
path: "/v1/invoices"
body: # literal JSON template of exactly what to POST
customerId: "123"
Suppose your tool has properties customerId
and currency
but you want to reference those when you make a http POST method upstream. You can reference them using {parameter}
syntax.
tools:
- name: createInvoice
description: "Create a new draft invoice for a customer."
parameters:
type: object
required: [customerId]
properties:
customerId:
type: string
description: "Customer to bill."
currency:
type: string
description: "ISO-4217 currency code."
default: "USD"
http:
method: POST
path: "/v1/invoices"
body: # literal JSON template of exactly what to POST
customerId: "{customerId}" # "{name}" -> substituted with that parameter's runtime value
currency: "{currency}"
Sometimes you want to want to use parameters as query strings on the upstream request. To do this use in
on the properties.
For example the following config would result in a HTTP GET
to /v1/invoices?customerId={customerId}¤cy={currency}
properties:
customerId:
type: string
in: query
description: "Customer to bill."
currency:
type: string
in: query
description: "ISO-4217 currency code."
http:
method: POST
path: "/v1/invoices"
This also works in paths
http:
method: POST
path: "/v1/invoices/{customerId}"
Suppose our API call to https://api.gold-api.com/price/XAU
returns JSON response
{
"currency": "USD",
"currencySymbol": "$",
"exchangeRate": 1.0,
"name": "Gold",
"price": 4394.299805,
"symbol": "XAU",
"updatedAt": "2026-08-17T12:23:02Z",
"updatedAtReadable": "a few seconds ago"
}
In many MCP cases it is preferred to use structured but compact text over raw JSON dumps. For something like this, a lightly formatted markdown table or bullet list is often easier for the model to reason over than raw JSON and cheaper in tokens.
To achieve this we can use the http.response.select
object to JQ
style select only the fields we need
http:
method: GET
path: "/price/XAU"
response:
select:
currency: "{currency}"
price: "{price}"
Here we use "{currency}"
variables to return fields from the JSON response which will return
currency: USD
price: 4394.299805
A tool call result in MCP follows this structure:
{
"content": [
{
"type": "text",
"text": "
currency: USD
price: 4394.299805
"
}
],
"isError": false
}
Other available response formatting functions:
{path.to.field}
— plain field extraction{arrayField[].path}
— map a sub-path over every element of an array{{literal}}
— escape hatch for a literal string that looks like a path (e.g. {{id}} → the string "{id}"){truncate(path, maxLen)}
— truncate a string (or every string in an array-valued path) to maxLen
mcp-proxy
can be protected with an API key and setting MCP_PROXY_AUTH_MODE
and MCP_PROXY_BEARER_TOKEN
export MCP_PROXY_AUTH_MODE=bearer
openssl rand -base64 32
export MCP_PROXY_BEARER_TOKEN=****
To connect to mcp-proxy
you will need to pass header
Authorization: Bearer ***
go build ./...
go vet ./...
go test ./...