Show HN: MCP-Proxy Add MCP support to any REST API – no code changes required Mcp-proxy, an open-source tool by crhuber, lets developers add MCP support to any REST API without code changes by loading a YAML config, dynamically registering MCP tools, and translating tool calls into HTTP requests. The tool can be deployed as a sidecar in Kubernetes or in front of any API service lacking a native MCP server, and it includes features like bearer token authentication and DNS-rebinding protection. 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. php 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 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 https://github.com/crhuber/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 /crhuber/mcp-proxy/blob/main/docs/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 ./...