DEV Community

Cover image for Building MCP Server - The Hidden Protocol Behind Smart AI Collaboration
hiruthicSha
hiruthicSha

Posted on • Originally published at hiruthicsha.Medium

Building MCP Server - The Hidden Protocol Behind Smart AI Collaboration

Back in the 1960s, when computers were rare and applications even rarer, the term API quietly entered the scene. It wasn't about the web or microservices back then; it was about getting one piece of software to talk to another within the same machine.

Fast forward to the 2000s, when the internet exploded into the hands of everyday developers. New frameworks, operating systems, and applications were popping up faster than anyone could keep track of. It was an incredible time; every week brought something new to try, build, or break.

But that rapid innovation came with a cost: incompatibility. Everyone built their own thing in their own way. There was no single language or standard for systems to communicate. If your shopping site wanted to talk to another vendor, you had to build a custom connector. One partner? One connector. A hundred partners? A hundred connectors. It was chaos.

That’s where APIs changed everything; they gave systems a common protocol to collaborate without needing to know each other’s internal wiring.

And that's exactly what the Model Context Protocol (MCP) aims to do, but for AI systems. Just as APIs allowed applications to exchange data and perform tasks together, MCP defines a unified language that lets AI models, tools, and environments interoperate seamlessly.

If you are new to MCP, please follow these before continuing:

Building the MCP Server

Project Setup

In my earlier store, where we talked about how to build an MCP Client where we used filesystem-mcp-server. This time, since we are building our own server, we'll call it "Wannabe FS MCP Server".

mkdir wannabe-fs-mcp-server 
cd wannabe-fs-mcp-server
npm init -y
npm install express body-parser cors @modelcontextprotocol/sdk
npm install --save-dev typescript ts-node @types/node @types/express @types/cors
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

Our project structure should look like:

mcp-server/
├─ src/
│  ├─ server.ts
│  └─ fs-service.ts
├─ package.json
├─ tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Core Logic

Let's create the obvious logic, listing files and reading the file fs-service.ts.

import fs from 'fs';
import path from 'path';

const ROOT_DIR = path.resolve('./');

export function listDirectory(relPath: string = ''): string[] {
  const fullPath = path.resolve(ROOT_DIR, relPath);
  if (!fullPath.startsWith(ROOT_DIR)) throw new Error('Access denied');
  return fs.readdirSync(fullPath);
}

export function readFile(relPath: string): string {
  const fullPath = path.resolve(ROOT_DIR, relPath);
  if (!fullPath.startsWith(ROOT_DIR)) throw new Error('Access denied');
  return fs.readFileSync(fullPath, 'utf-8');
}
Enter fullscreen mode Exit fullscreen mode

This service logic is responsible for reading a file or listing a directory under a directory.
Let's build the MCP part.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "fs";
import path from "path";
Enter fullscreen mode Exit fullscreen mode

Now, let's be a good developer and follow proper security practices, at least one. We'll allow the server access to the current directory.

const ROOT_DIR = path.resolve("./");

function safeResolve(relPath: string) {
  const fullPath = path.resolve(ROOT_DIR, relPath);
  if (!fullPath.startsWith(ROOT_DIR)) throw new Error("Access denied");
  return fullPath;
}
Enter fullscreen mode Exit fullscreen mode

Now, let's create the handlers that the MCP can register as tools.

function listDirectory(relPath: string = ""): string[] {
  const fullPath = safeResolve(relPath);
  return fs.readdirSync(fullPath);
}

function readFile(relPath: string): string {
  const fullPath = safeResolve(relPath);
  return fs.readFileSync(fullPath, "utf-8");
}
Enter fullscreen mode Exit fullscreen mode

Create a new instance of the MCP Server and register the tools.

const server = new McpServer({
  name: "file-server",
  version: "1.0.0",
  capabilities: { tools: {}, resources: {} },
});

server.registerTool("list_dir", {
  description: "List files in a directory"
}, async (args: any) => {
  try {
    const relPath = args?.path || "";
    const files = listDirectory(relPath);
    return { content: [{ type: "text", text: files.join("\n") }] };
  } catch (err: any) {
    return { content: [{ type: "text", text: `Error: ${err.message}` }] };
  }
});

// Read file tool
server.registerTool("read_file", {
  description: "Read the contents of a file"
}, async (args: any) => {
  try {
    if (!args?.path) {
      return { content: [{ type: "text", text: "Error: path parameter is required" }] };
    }
    const content = readFile(args.path);
    return { content: [{ type: "text", text: content }] };
  } catch (err: any) {
    return { content: [{ type: "text", text: `Error: ${err.message}` }] };
  }
});
Enter fullscreen mode Exit fullscreen mode

The above process is just telling the server instance to expose certain tools, which are bound to our handlers. This way, an MCP Client can discover these tools when the server is connected and provide context to the LLM. A little bit of driver like below and that's it.

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Wannabe File System MCP Server running on stdio");
Enter fullscreen mode Exit fullscreen mode

And that’s it, our humble, wannabe filesystem MCP server, "ready" to tackle production-level chaos (just kidding, don’t actually deploy it💀).

You can build the project, grab the path to the transpiled script.js, and paste it into the MCP Client from the previous post to give it a spin.


This is a very simple introduction to building an MCP Server, and there are a lot of other things like:

  • Different transport types
  • Authentication and access control
  • Error handling and retries
  • Logging and observability
  • Scaling and concurrency management
  • Message schema versioning and backward compatibility
  • Security and input validation

Each of these aspects adds robustness and makes the server truly ready for real-world workloads.

If you are interested, I have created such a simple MCP server that runs multiple LLMs at the same time and can also summarize by querying multiple LLMs. Read more: MMMCP — An MCP Server for Multi-Model Prompts.

Stay Curious. Adios 👋

Cover image generated with Canva AI

Top comments (0)