<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Udara Bibile</title>
    <description>The latest articles on Forem by Udara Bibile (@udarabibile).</description>
    <link>https://forem.com/udarabibile</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1932637%2Fb3d7e985-140c-4ab6-98b7-9f0084caf7d8.jpg</url>
      <title>Forem: Udara Bibile</title>
      <link>https://forem.com/udarabibile</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/udarabibile"/>
    <language>en</language>
    <item>
      <title>Integrating MCP Tools into Express with Minimal Changes</title>
      <dc:creator>Udara Bibile</dc:creator>
      <pubDate>Thu, 28 Aug 2025 09:14:06 +0000</pubDate>
      <link>https://forem.com/udarabibile/integrating-mcp-tools-into-express-with-minimal-changes-28e6</link>
      <guid>https://forem.com/udarabibile/integrating-mcp-tools-into-express-with-minimal-changes-28e6</guid>
      <description>&lt;p&gt;This guide walks through setting up an &lt;strong&gt;Express HTTP server&lt;/strong&gt; and extending it with &lt;strong&gt;MCP tools&lt;/strong&gt;, using &lt;code&gt;Zod&lt;/code&gt; for schema validation. By the end, you’ll have a server that supports both REST-style routes and MCP-compatible tools with shared logic.&lt;/p&gt;

&lt;p&gt;Let’s create new project with Nodejs v23+ and install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install express zod @modelcontextprotocol/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create &lt;code&gt;index.ts&lt;/code&gt; and it can be run via &lt;code&gt;node index.ts&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Basic Express HTTP Route
&lt;/h2&gt;

&lt;p&gt;This route accepts a JSON body with &lt;code&gt;name&lt;/code&gt;, validates it with Zod, and replies with a greeting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import express from "express";
import { z } from "zod";

const app = express();
app.use(express.json());

app.post('/greet-user', async (req, res) =&amp;gt; {
    try {
        const { name } = z.object({
            name: z.string().describe("Name of the user"),
        }).parse(req.body);
        res.json({ text: `Hello, ${name}!` });
    } catch (error) {
        if (error instanceof z.ZodError) {
            return res.status(400).json({ error: error.errors });
        }
        res.status(500).json({ error: 'Internal server error' });
    }
});

const PORT = 4100;
app.listen(PORT, () =&amp;gt; {
    console.log(`Express HTTP server connected and running on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be verified via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -X POST http://localhost:4100/greet-user \
  -H "Content-Type: application/json" \
  -d '{"name": "Tommy Shelby"}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Basic MCP Tool Handler
&lt;/h2&gt;

&lt;p&gt;Next, let’s expose similar functionality, but as an MCP tool available over HTTP.&lt;/p&gt;

&lt;p&gt;We set up an MCP server and register a tool named &lt;code&gt;greet-user&lt;/code&gt;, with the same Zod validation and response format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import express from "express";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const server = new McpServer({
  name: "Greet Server",
  version: "1.0.0",
  capabilities: { tools: {} }
});

server.tool(
  'greet-user',
  'Greet user by name',
  {
    name: z.string().describe("Name of the user"),
  },
  async ({ name }) =&amp;gt; {
    return {
      content: [{ type: "text", text: `Hello, ${name}!` }]
    };
  }
)

const app = express();
app.use(express.json());

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined,
});

app.post('/mcp', async (req, res) =&amp;gt; {
  await transport.handleRequest(req, res, req.body);
});

const PORT = 4100;
server.connect(transport)
  .then(() =&amp;gt; {
    app.listen(PORT, () =&amp;gt; {
      console.log(`MCP HTTP server connected and running on port ${PORT}`);
    });
  })
  .catch(error =&amp;gt; {
    console.error("Failed to connect MCP server to transport:", error);
    process.exit(1);
  });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be verified via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -X POST http://localhost:4100/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{   "jsonrpc": "2.0",                  
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "greet-user",
    "arguments": { "name": "Tommy Shelby" },
    "_meta": { "progressToken": 0 }
  }
}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Unifying Express Routes &amp;amp; MCP Tools
&lt;/h2&gt;

&lt;p&gt;Notice both the Express route and the MCP tool share common elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Validation schema (Zod)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handler function&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only difference is output format — Express returns &lt;code&gt;JSON&lt;/code&gt;, MCP requires the &lt;code&gt;{ type: "text"; text: string }[]&lt;/code&gt; structure.&lt;/p&gt;

&lt;p&gt;To avoid duplication, we create wrapper functions that adapt a schema + handler into either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;an Express request handler (&lt;code&gt;createExpressHandler&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;an MCP tool function (&lt;code&gt;createMcpTool&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;As a practical example, let’s build a mini book management service.&lt;br&gt;
We’ll store books in a local file (&lt;code&gt;books.json&lt;/code&gt;) and expose two operations: &lt;code&gt;createBook&lt;/code&gt; and &lt;code&gt;getBookById&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bookService.ts&lt;/code&gt; → define read/write logic using Node’s &lt;code&gt;fs/promises&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import fs from "fs/promises";
import path from "path";

export interface IBook { id: string; title: string; author: string; year?: number; }

const DATA_FILE = path.join(process.cwd(), "books.json");

export async function readBooks(): Promise&amp;lt;IBook[]&amp;gt; {
  try {
    const data = await fs.readFile(DATA_FILE, "utf8");
    return JSON.parse(data) as IBook[];
  } catch (e) {
    if (e.code === "ENOENT") return [];
    throw e;
  }
}

export async function writeBooks(books: IBook[]) {
  await fs.writeFile(DATA_FILE, JSON.stringify(books, null, 2), "utf8");
}

export async function getAllBooks() {
  return await readBooks();
}

export async function getBookById(id: string) {
  const books = await readBooks();
  const book = books.find(b =&amp;gt; b.id === id);
  if (!book) throw new Error(`Book with ID ${id} not found.`);
  return book;
}

export async function createBook(data: Omit&amp;lt;IBook, "id"&amp;gt;) {
  const books = await readBooks();
  const id = books.length ? String(Math.max(...books.map(b =&amp;gt; Number(b.id))) + 1) : "1";
  const newBook = { id, ...data };
  books.push(newBook);
  await writeBooks(books);
  return newBook;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wrappers.ts&lt;/code&gt; → create the helper wrappers for Express and MCP, both powered by &lt;code&gt;Zod&lt;/code&gt; validation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from "zod";

export function createMcpTool&amp;lt;TInput&amp;gt;(
  schema: z.ZodType&amp;lt;TInput&amp;gt;,
  handler: (input: TInput) =&amp;gt; Promise&amp;lt;any&amp;gt;
): (input: TInput) =&amp;gt; Promise&amp;lt;{ content: { type: "text"; text: string }[] }&amp;gt; {
  return async (input: TInput) =&amp;gt; {
    const parseResult = schema.safeParse(input);
    if (!parseResult.success) {
      return {
        content: [{ type: "text", text: `Validation failed: ${JSON.stringify(parseResult.error.errors)}` }]
      };
    }
    try {
      const result = await handler(parseResult.data);
      return {
        content: [{ type: "text", text: JSON.stringify(result) }]
      };
    } catch (err: any) {
      return {
        content: [{ type: "text", text: `Error: ${err.message}` }]
      };
    }
  };
}

export function createExpressHandler&amp;lt;TInput&amp;gt;(
  schema: z.ZodType&amp;lt;TInput&amp;gt;,
  handler: (input: TInput) =&amp;gt; Promise&amp;lt;any&amp;gt;
) {
  return async (req, res) =&amp;gt; {
    const parseResult = schema.safeParse(req.body || req.params);
    if (!parseResult.success) {
      return res.status(400).json({ error: parseResult.error.errors });
    }
    try {
      const result = await handler(parseResult.data);
      res.json(result);
    } catch (err: any) {
      const isNotFound = err.message.includes("not found");
      res.status(isNotFound ? 404 : 500).json({ error: err.message });
    }
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;index.ts&lt;/code&gt; → This starts an Express server that registers multiple routes, and serves MCP tools over HTTP through the &lt;code&gt;/mcp&lt;/code&gt; endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import express from "express";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createExpressHandler, createMcpTool } from "./wrappers";
import { getBookById, createBook } from "./bookService";

const expressServer = express();
expressServer.use(express.json());

// Schema and Handler for Get Book by ID
const getBookByIdSchema = z.object({
  id: z.string().describe("Book ID"),
});
const getBookByIdFunc = async ({ id }) =&amp;gt; {
  const book = await getBookById(id);
  if (!book) throw new Error(`Book with ID ${id} not found.`);
  return book;
}
// Schema and Handler for Create Book
const createBookSchema = z.object({
  title: z.string(),
  author: z.string(),
  year: z.number().min(0).optional(),
});
const createBookFunc = async ({ title, author, year }: { title: string; author: string; year?: number }) =&amp;gt; {
  const book = await createBook({ title, author, year });
  if (!book) throw new Error("Failed to create book.");
  return book;
}

// Express Route
expressServer.get(
  "/books/:id",
  createExpressHandler(getBookByIdSchema, getBookByIdFunc)
);

expressServer.post(
  "/books",
  createExpressHandler(createBookSchema, createBookFunc)
);

// MCP Tool
const mcpServer = new McpServer({
  name: "Books CRUD MCP Server",
  version: "1.0.0",
  capabilities: { tools: {} }
});

mcpServer.tool(
  "get-book-by-id",
  "Get book by ID",
  getBookByIdSchema.shape,
  createMcpTool(getBookByIdSchema, getBookByIdFunc)
);

mcpServer.tool(
  "create-book",
  "Create a new book",
  createBookSchema.shape,
  createMcpTool(createBookSchema, createBookFunc)
);

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined,
});

expressServer.post('/mcp', async (req, res) =&amp;gt; {
  await transport.handleRequest(req, res, req.body);
});

const PORT = 4100;
mcpServer.connect(transport)
  .then(() =&amp;gt; {
    expressServer.listen(PORT, () =&amp;gt; {
      console.log(`MCP HTTP mcpServer connected and running on port ${PORT}`);
    });
  })
  .catch(error =&amp;gt; {
    console.error("Failed to connect MCP mcpServer to transport:", error);
    process.exit(1);
  });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, &lt;code&gt;index.ts&lt;/code&gt; ties everything together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create Zod schemas for &lt;code&gt;getBookById&lt;/code&gt; and &lt;code&gt;createBook&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Define handler functions for &lt;code&gt;getBookById&lt;/code&gt; and &lt;code&gt;createBook&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Register Express routes for HTTP calls via &lt;code&gt;createExpressHandler&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Register MCP tools for MCP calls via &lt;code&gt;createMcpTool&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wrapper functions would be reusing the same schemas &amp;amp; handlers&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s test Express routes and MCP tools by running the server.&lt;/p&gt;

&lt;p&gt;Lets install &lt;code&gt;tsx&lt;/code&gt; via &lt;code&gt;npm install --save-dev tsx&lt;/code&gt; and update &lt;code&gt;package.json&lt;/code&gt; to use: &lt;code&gt;"dev": "tsx index.ts"&lt;/code&gt; and run the server using &lt;code&gt;npm run dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let’s verify both Express routes and MCP tools works fine using curls&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Create Book via Express Route

curl -X POST http://localhost:4100/books \
  -H "Content-Type: application/json" \
  -d '{"title": "The Brothers Karamazov", "author": "Fyodor Dostoevsky", "year": 1880}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Create Book via MCP Tool

curl -X POST http://localhost:4100/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{   "jsonrpc": "2.0",                  
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "create-book",
    "arguments": { "title": "War and Peace", "author": "Leo Tolstoy", "year": 1867 },
    "_meta": { "progressToken": 0 }
  }
}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get Book via Express Route

curl -X GET http://localhost:4100/books/2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get Book via MCP Tool

curl -X POST http://localhost:4100/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{   "jsonrpc": "2.0",                  
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get-book-by-id",
    "arguments": { "id": "1" },
    "_meta": { "progressToken": 0 }
  }
}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MCP tools can also be verified via &lt;code&gt;@modelcontextprotocol/inspector&lt;/code&gt; by&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx @modelcontextprotocol/inspector npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xf54z3omw22rra7577e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xf54z3omw22rra7577e.png" alt="modelcontextprotocol inspector" width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;This approach minimizes code duplication by extracting validation schemas and core logic from Express routes and reusing them seamlessly in MCP tools. It enables rapid exposure of MCP functionality for existing Express servers without rewriting business logic. With this structure, you achieve a maintainable, efficient setup where you build your application logic once and expose it everywhere — supporting both traditional REST APIs and modern MCP tools with ease and consistency.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxtgm4ykmt3xoo3x5liv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxtgm4ykmt3xoo3x5liv.png" alt="Core Business Logic reused" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/udarabibile/mcp-using-javascript/tree/main/mcp-tools-express-routes" rel="noopener noreferrer"&gt;https://github.com/udarabibile/mcp-using-javascript/tree/main/mcp-tools-express-routes&lt;/a&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>express</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
