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 maxLenmcp-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 ./...
9 commits
Go
100.0%
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 maxLenmcp-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 ./...
9 commits
Go
100.0%