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!
Related Articles

Explore the key differences between Agent-to-Agent (A2A) communication and Modular Contract Protocols (MCP) β and why you need both to build powerful AI systems.

Unlock the power of tRPC and the T3 Stack for modern web development in 2025. Discover how type safe APIs, modular architecture, and the latest trends like AI integration and Jamstack are transforming how developers build fast, scalable, and maintainable applications.

Your AI-built website might be invisible to Google. Learn the common SEO pitfalls of AI sites and how to fix them with this friendly, step-by-step guide.