How I Turned a REST API into an MCP Server

RDRajesh Dhiman
5 min read

πŸ› οΈ 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!

Share this article