The problem#
Wrapping an existing REST API as an MCP server means writing protocol code, hosting it somewhere, and then discovering that the model picks the wrong tool because the descriptions were copied from an OpenAPI summary that was written for humans.
The approach#
You give MCPForge a spec by URL or upload. It validates the spec, turns each operation into an MCP tool, and hosts the result behind a gateway so there is nothing to install locally.
- Spec ingestion: the spec is fetched with SSRF checks, validated, de-duplicated by SHA-256 hash, stored in S3, and analysed into tool definitions.
- AI description engine: an LLM rewrites each tool description using the spec details, the sibling tools and few-shot examples. A separate regex-based scorer then rates the result on functionality, accuracy, completeness and context, with no second LLM call.
- Security scanner: eight deterministic rules (DELETE without auth, credential-like fields in responses, prompt-injection text in descriptions, and so on) run before a server is deployed.
- Playground and analytics: a WebSocket playground to try tools before deploying, and per-call analytics for servers that are live.
Architecture#
The monorepo has two apps. apps/web is a Next.js 15 dashboard, and apps/api is a single FastAPI service that serves the REST API, the MCP gateway and the WebSocket playground together, so one process on a free tier covers all three. Celery workers read from Redis on separate queues for AI enhancement, security scans and analytics.
Build progress is streamed to the dashboard over SSE: Celery tasks publish to Redis pub/sub and the API fans events out to connected browsers. Clients such as Claude Desktop reach a deployed server through the gateway, which accepts either SSE or StreamableHTTP transport.
Hover or tap any part to trace what it talks to. 18 parts, 22 connections. Swipe sideways to see it all.
- 1A spec is uploaded or fetched by URL, validated, hashed, stored in S3 and analysed into tool definitions in Postgres.
- 2Starting a build enqueues a Celery task on the ai queue through Redis and returns a job id immediately.
- 3The worker rewrites each description with the LLM, scores it, saves it, and publishes progress through Redis pub/sub, which the API streams to the dashboard as SSE.
- 4The security scanner runs its eight rules against the tool set on its own queue before the server goes live.
- 5An MCP client connects to the gateway with a JWT or API key; the gateway checks plan rate limits and reads the server config from the Redis cache.
- 6Each tool call becomes an HTTP request, passes the SSRF guard and goes to the customer's API.
- 7Secondary path: the gateway fires a Celery event after every call, and the analytics worker writes it to a daily-partitioned table that beat aggregates hourly.
- 8Secondary path: Stripe subscription webhooks hit the API, are signature-checked and deduplicated, then sync the user's plan.
Calling the upstream API safely#
The gateway does not run any generated code. A tool call is a stored config (method, path, parameters) that a dispatcher turns into an HTTP request, after decrypting the stored credential and building the auth header. Before sending, every request URL goes through an SSRF guard that resolves the hostname and rejects any address in a private or loopback range.
for _, _type, _proto, _canonname, sockaddr in addrinfo:
ip = ipaddress.ip_address(sockaddr[0])
for blocked_net in _ALL_BLOCKED:
if ip in blocked_net:
raise SSRFBlockedError(
f"URL resolves to blocked IP: {sockaddr[0]}",
)The same dispatcher backs both the gateway and the playground. Tool annotations (readOnlyHint, destructiveHint, idempotentHint) are derived from the HTTP method when tools are generated.
What I learned#
Most of the work was around the edges of the MCP protocol: auth, SSRF, rate limits and analytics. The part that looks like the product, turning a spec into tools, was the smaller piece.