How I Turned a REST API into an MCP Server
Let’s say you walk into a fast food restaurant. You shout, “One burger!” and they toss it to you in under 30 seconds. Quick, dirty, and done. That’s your typical REST API: fast, unstructured, and a little unpredictable.
But what if you wanted to open a high-end place — where every dish has a printed menu, the ingredients are verified, and the chef follows a consistent recipe? That’s what Modular Contract Protocols (MCPs) are like. They don’t just serve data; they serve it with structure, clarity, and a smile.
In this post, I’ll show you exactly how I wrapped the public Hacker News API in an MCP — turning a chaotic data stream into a clean, LLM-friendly microservice.
🧠 Wait... What’s an MCP Again?
Let me break it down:
MCP stands for Modular Contract Protocol. It’s a way of writing API logic where every endpoint:
- Has a clear input and output contract
- Is modular and self-contained
- Can be called by humans, machines, or LLMs
- Plays nicely in a function-calling world (like OpenAI or Claude)
In simple terms: it's like giving every API endpoint its own resume. It knows who it is, what it accepts, and what it gives back.
🔥 Why Wrap an Existing API?
I wanted to showcase how powerful MCPs are by not building everything from scratch. Instead, I took the Hacker News API — a basic REST interface with zero documentation, no validation, and no structure — and made it:
- 🌱 Type-safe with Zod
- 🤖 Callable by LLM agents
- 📜 OpenAPI-exportable via
zod-openapi
- 🌍 Deployable & reusable
You get all the benefits of modern, clean backend architecture — without rebuilding the wheel.
🚧 Step 1: Picking the API
I went with Hacker News API. Why?
- It’s public and requires no authentication
- It returns JSON (yay!)
- It’s a little raw — making it perfect for a glow-up
Hacker News gives you endpoints like:
/topstories.json
→ returns an array of story IDs/item/{id}.json
→ returns the details for a story or comment
But there’s no validation. No input schema. No docs. Just vibes.
🧱 Step 2: MCP Project Structure
I set up the project with this simple folder structure:
src/
├── contracts/ // Zod + OpenAPI metadata
├── resolvers/ // Business logic
├── handlers/ // Express routes
├── utils/ // Hacker News client
├── docs/openapi.ts // OpenAPI spec generator (zod-openapi)
├── setup/zod-openapi-init.ts // Shared zod setup with OpenAPI support
└── server.ts // Main entry point
Think of it like building with LEGO blocks — every piece does one thing, and snaps into place without duct tape.
🧪 Step 3: Creating the First Contract
Let’s start with the endpoint to list top stories.
✅ Zod Contract
export const listTopStoriesOutput = z.array(z.number()).openapi({ description: "Array of Hacker News story IDs",
});
It’s like saying: “Hey, this endpoint gives back an array of numbers. No more, no less.”
⚙️ Step 4: Writing the Resolver
The resolver is the actual brain. It connects to Hacker News, fetches the data, and validates it.
import { fetchTopStoryIds } from "../utils/hnClient";
import { listTopStoriesOutput } from "../contracts/listTopStories.contract"; export const listTopStoriesHandler = async (req, res) => { try { const storyIds = await fetchTopStoryIds(); res.json(listTopStoriesOutput.parse(storyIds)); } catch (e) { res.status(500).json({ error: "Something went wrong!" });
}
};
getStory
Endpoint
📖 Step 5: Adding the This one lets you fetch details about any story by ID.
✅ Input Contract
export const getStoryInput = z .object({ id: z .string() .regex(/^[0-9]+$/) .openapi({ description: "Story ID" }), }) .openapi({ title: "GetStoryInput" });
✅ Output Contract
export const getStoryOutput = z .object({ id: z.number().openapi({ description: "ID of the story" }), title: z.string().openapi({ description: "Title of the story" }), by: z.string().openapi({ description: "Author" }), score: z.number().openapi({ description: "Score or points of the story" }), url: z.string().optional().openapi({ description: "URL (if any)" }), time: z.number().openapi({ description: "Unix timestamp" }), type: z.string().openapi({ description: "Item type (story/comment)" }), }) .openapi({ title: "GetStoryOutput" });
🚀 Step 6: Hosting + OpenAPI + LLM Ready
After writing the resolvers, I hosted the whole thing here:
🔗 https://mcp-news-server.onrender.com
Test it:
/api/listTopStories
/api/getStory/8863
/openapi.json
And yes, it works with LangChain, Claude, OpenAI, or any custom LLM runner!
🎁 What You Can Do Next
- Wrap any REST API you love in an MCP
- Add contracts, deploy, and share with LLMs
- Use
zod-openapi
to create swagger-compatible specs - Register it in an agent-aware toolchain or build your own GPT plugin
💬 TL;DR
Modular Contract Protocols give your API structure and meaning. By wrapping a basic REST API like Hacker News with Zod contracts and generating OpenAPI with zod-openapi
, you can:
- Build more robust backend tools
- Make them compatible with LLMs
- Reduce guesswork and increase composability
Let’s stop building brittle REST services — and start building smart, structured, machine-readable APIs.
💡 Try it yourself — and let me know what you wrap next!