{"slug": "show-hn-mcp-proxy-add-mcp-support-to-any-rest-api-no-code-changes-required", "title": "Show HN: MCP-Proxy Add MCP support to any REST API – no code changes required", "summary": "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.", "body_md": "Add MCP support to any REST API - no code changes required.\n\nWe've spent decades building REST APIs. Adding MCP support shouldn't mean\nreimplementing your business logic. `mcp-proxy`\n\nexposes your existing REST\nendpoints as MCP tools automatically, with zero changes to your API.\n\n`mcp-proxy`\n\nloads a YAML config describing one or more upstream REST APIs, dynamically registers MCP tools for them, and translates each\ntool call into an HTTP request against the real upstream API and back.\n\n``` php\nflowchart LR\n    A[MCP Client] -- MCP tool call --> B[mcp-proxy]\n    B -- HTTP request --> C[REST API]\n    C -- HTTP response --> B\n    B -- MCP tool result --> A\n```\n\n`mcp-proxy`\n\ncan be deployed in a sidecar pattern in Kubernetes or infront of any API service that doesnt provide a native MCP server.\n\nSay 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.\n\nTo achieve this, create a `config.yaml`\n\n```\nendpoints:\n  - name: gold-api\n    upstream:\n      base_url: \"https://api.gold-api.com\"\n      timeout: 10s\n      auth:\n        type: \"none\"\n    tools:\n      - name: getPrice\n        description: \"Fetch gold price\"\n        http:\n          method: GET\n          path: \"/price/XAU\"\n```\n\nRun the server.\n\n```\nmcp-proxy --config config.yaml\n```\n\nWhat happens here is a new MCP tool called `getPrice`\n\nis registered on the server. When this MCP tool is called the server makes a `HTTP GET`\n\nrequest to `https://api.gold-api.com/price/XAU`\n\nand returns the result to the MCP Client.\n\nVerify its working by making MCP tool call using `curl`\n\n```\ncurl -X POST http://localhost:8080/mcp \\\n        -H \"Content-Type: application/json\" \\\n        -H \"Accept: application/json, text/event-stream\" \\\n        -d '{\n      \"jsonrpc\": \"2.0\",\n      \"id\": 1,\n      \"method\": \"tools/call\",\n      \"params\": {\n        \"name\": \"gold-api_getPrice\",\n        \"arguments\": {}\n      }\n    }'\n```\n\nResponse:\n\n```\nevent: message\ndata: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"{\\\"currency\\\":\\\"USD\\\",\\\"price\\\":4409.299805}\"}],\"structuredContent\":{\"currency\":\"USD\",\"price\":4409.299805}}}\n```\n\nCongrats! We have now adapted our API to be MCP ready with no code changes to the API!\n\n```\ngo install github.com/crhuber/mcp-proxy/cmd/mcp-proxy@latest\n```\n\nOr build from source:\n\n```\ngo build -o mcp-proxy ./cmd/mcp-proxy\n```\n\nOr Use [kelp](https://github.com/crhuber/kelp)\n\n```\nkelp add crhuber/mcp-proxy --install\nmcp-proxy --config config.yaml\n```\n\n| Flag | Env var | Default | Description |\n|---|---|---|---|\n`--config` |\n`MCP_PROXY_CONFIG` |\n(required) |\nPath to the proxy config YAML file |\n`--listen` |\n`MCP_PROXY_LISTEN_ADDR` |\n`:8080` |\nAddress to listen on |\n`--auth-mode` |\n`MCP_PROXY_AUTH_MODE` |\n`none` |\n`none` | `bearer` — whether the proxy's own `/mcp` endpoint requires a bearer token |\n`--log-level` |\n`MCP_PROXY_LOG_LEVEL` |\n`info` |\n`debug` | `info` | `warn` | `error` |\n`--shutdown-grace` |\n`MCP_PROXY_SHUTDOWN_GRACE` |\n`15s` |\nHow long to wait for in-flight requests to finish on shutdown |\n`--disable-localhost-protection` |\n`MCP_PROXY_DISABLE_LOCALHOST_PROTECTION` |\n`false` |\nDisable the MCP SDK's DNS-rebinding Host header check |\n\nWhen `--auth-mode=bearer`\n\n, set `MCP_PROXY_BEARER_TOKEN`\n\nin the environment\n(it is intentionally not a flag so it never shows up in `--help`\n\noutput or\nprocess argv).\n\nThe MCP SDK auto-enables DNS-rebinding protection: if the TCP connection it\naccepted came in on a loopback address, it requires the request's `Host`\n\nheader to also look like loopback (`localhost`\n\n, `127.0.0.0/8`\n\n, `::1`\n\n), and\notherwise rejects it with `403 Forbidden: invalid Host header`\n\n. This guards\nlocal companion services against a browser being tricked into hitting them,\nand doesn't apply to mcp-proxy's deployment model.\n\nIf a sidecar connects to mcp-proxy over `127.0.0.1`\n\nbut forwards the\noriginal external `Host`\n\nheader (e.g. `mcp.domain.com`\n\n) — the accepted\nconnection's local address is loopback regardless of what address mcp-proxy\nitself binds to, so changing `--listen`\n\n/`MCP_PROXY_LISTEN_ADDR`\n\nalone won't\nfix it. Set `--disable-localhost-protection`\n\n(or\n`MCP_PROXY_DISABLE_LOCALHOST_PROTECTION=1`\n\n) in that topology.\n\nThe config file describes one or more upstream endpoints and the tools that\nshould be generated for each. See [ config.yaml.example](/crhuber/mcp-proxy/blob/main/docs/config.yaml.example)\nfor a full annotated example, including path/query parameters, request body\ntemplating, and response field selection.\n\nTo connect to an upstream REST API, you need to define your upstream `base_url`\n\n, `timeout`\n\n, and `auth`\n\ntype.\n\n```\nendpoints:\n  - name: gold-api\n    upstream:\n      base_url: \"https://api.gold-api.com\"\n      timeout: 10s\n      auth:\n        type: \"none\"\n```\n\nThe base_url should be defined *without* a trailing slash `/`\n\n.\n\nAuth types supported are:\n\n`bearer`\n\n```\n      auth:\n        type: bearer                      # bearer | header | query | none\n        env: ORDERS_API_KEY               # secret ALWAYS comes from this env var, never literal in file\n```\n\n`header`\n\n```\n      auth:\n        type: header\n        header: X-API-Key\n        type: \"none\"\n        env: ORDERS_API_KEY               # secret ALWAYS comes from this env var, never literal in file\n```\n\n`none`\n\n```\n      auth:\n        type: \"none\"\n```\n\nFor any key defined in `env`\n\nit must be set in environment variables before starting the server. In the preceeding examples `ORDERS_API_KEY`\n\nmust be set\n\nTools are the MCP tools to create on the MCP server. Each tool has `name`\n\n, `description`\n\n, and `parameters`\n\n```\n    tools:\n      - name: createInvoice\n        description: \"Create a invoice for a customer.\"\n        parameters:\n          type: object\n          required: [customerId]\n          properties:\n            customerId:\n              type: string\n              description: \"Customer to bill.\"\n```\n\nWhenever a toolcall is made to the attached tool, a coresponding http request is made to the upstream.\n\nThe `http`\n\nobject controls what how the HTTP request upstream is made. The usual parameters for a HTTP request including `method`\n\n, `path`\n\nand `body`\n\n```\n    tools:\n      - name: getPrice\n        description: \"Fetch gold price\"\n        http:\n          method: GET\n          path: \"/price/XAU\"\n```\n\nA JSON request body can also be sent on any HTTP method by adding the `body`\n\n```\n        http:\n          method: POST\n          path: \"/v1/invoices\"\n          body: # literal JSON template of exactly what to POST\n            customerId: \"123\"\n```\n\nSuppose your tool has properties `customerId`\n\nand `currency`\n\nbut you want to reference those when you make a http POST method upstream. You can reference them using `{parameter}`\n\nsyntax.\n\n```\n    tools:\n      - name: createInvoice\n        description: \"Create a new draft invoice for a customer.\"\n        parameters:\n          type: object\n          required: [customerId]\n          properties:\n            customerId:\n              type: string\n              description: \"Customer to bill.\"\n            currency:\n              type: string\n              description: \"ISO-4217 currency code.\"\n              default: \"USD\"\n        http:\n          method: POST\n          path: \"/v1/invoices\"\n          body:                            # literal JSON template of exactly what to POST\n            customerId: \"{customerId}\"      # \"{name}\" -> substituted with that parameter's runtime value\n            currency: \"{currency}\"\n```\n\nSometimes you want to want to use parameters as query strings on the upstream request. To do this use `in`\n\non the properties.\nFor example the following config would result in a `HTTP GET`\n\nto `/v1/invoices?customerId={customerId}¤cy={currency}`\n\n```\n          properties:\n            customerId:\n              type: string\n              in: query\n              description: \"Customer to bill.\"\n            currency:\n              type: string\n              in: query\n              description: \"ISO-4217 currency code.\"\n        http:\n          method: POST\n          path: \"/v1/invoices\"\n```\n\nThis also works in paths\n\n```\n        http:\n          method: POST\n          path: \"/v1/invoices/{customerId}\"\n```\n\nSuppose our API call to `https://api.gold-api.com/price/XAU`\n\nreturns JSON response\n\n```\n{\n    \"currency\": \"USD\",\n    \"currencySymbol\": \"$\",\n    \"exchangeRate\": 1.0,\n    \"name\": \"Gold\",\n    \"price\": 4394.299805,\n    \"symbol\": \"XAU\",\n    \"updatedAt\": \"2026-08-17T12:23:02Z\",\n    \"updatedAtReadable\": \"a few seconds ago\"\n}\n```\n\nIn 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.\n\nTo achieve this we can use the `http.response.select`\n\nobject to `JQ`\n\nstyle select only the fields we need\n\n```\nhttp:\n  method: GET\n  path: \"/price/XAU\"\n  response:\n    select:\n      currency: \"{currency}\"\n      price: \"{price}\"\n```\n\nHere we use `\"{currency}\"`\n\nvariables to return fields from the JSON response which will return\n\n```\ncurrency: USD\nprice: 4394.299805\n```\n\nA tool call result in MCP follows this structure:\n\n```\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"\n        currency: USD\n        price: 4394.299805\n      \"\n    }\n  ],\n  \"isError\": false\n}\n```\n\nOther available response formatting functions:\n\n`{path.to.field}`\n\n— plain field extraction`{arrayField[].path}`\n\n— map a sub-path over every element of an array`{{literal}}`\n\n— escape hatch for a literal string that looks like a path (e.g. {{id}} → the string \"{id}\")`{truncate(path, maxLen)}`\n\n— truncate a string (or every string in an array-valued path) to maxLen\n\n`mcp-proxy`\n\ncan be protected with an API key and setting `MCP_PROXY_AUTH_MODE`\n\nand `MCP_PROXY_BEARER_TOKEN`\n\n```\nexport MCP_PROXY_AUTH_MODE=bearer\nopenssl rand -base64 32\nexport MCP_PROXY_BEARER_TOKEN=****\n```\n\nTo connect to `mcp-proxy`\n\nyou will need to pass header\n\n```\nAuthorization: Bearer ***\ngo build ./...\ngo vet ./...\ngo test ./...\n```\n\n", "url": "https://wpnews.pro/news/show-hn-mcp-proxy-add-mcp-support-to-any-rest-api-no-code-changes-required", "canonical_source": "https://github.com/crhuber/mcp-proxy", "published_at": "2026-08-18 10:36:18+00:00", "updated_at": "2026-08-18 11:11:33.809537+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-infrastructure"], "entities": ["mcp-proxy", "crhuber", "kelp", "MCP", "REST API", "Kubernetes"], "alternates": {"html": "https://wpnews.pro/news/show-hn-mcp-proxy-add-mcp-support-to-any-rest-api-no-code-changes-required", "markdown": "https://wpnews.pro/news/show-hn-mcp-proxy-add-mcp-support-to-any-rest-api-no-code-changes-required.md", "text": "https://wpnews.pro/news/show-hn-mcp-proxy-add-mcp-support-to-any-rest-api-no-code-changes-required.txt", "jsonld": "https://wpnews.pro/news/show-hn-mcp-proxy-add-mcp-support-to-any-rest-api-no-code-changes-required.jsonld"}}