Codementor Events

How I Turned a REST API into an MCP Server

Published May 23, 2025

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!

Discover and read more posts from Rajesh Dhiman
get started