How I Turned a REST API into an MCP Server
π οΈ How I Turned a REST API into an MCP Server (And Why You Should Too)
π Introduction: Turning a Fast Food Joint into Fine Dining
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!" });
}
};
π Step 5: Adding the getStory
Endpoint
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!