All posts
mcpserver

Build an MCP Server: A Practical Guide for Full-Stack Developers

A practical guide to Build an MCP Server — setup, core concepts, common mistakes, and production tips for full-stack developers.

SR

Suhail Roushan

August 6, 2026

·
5 min read
·
0 views

Build an MCP server to let AI assistants access your tools and data through a single, standardized interface. The Model Context Protocol (MCP) is the missing glue between LLMs and your existing APIs, and this guide shows you exactly how to build one.

Why Build an MCP Server Matters (and When to Skip It)

I've seen teams over-engineer MCP servers for problems that don't need them. If you're just exposing a REST API to ChatGPT, stop — a simple OpenAPI spec gets you 80% of the value with zero infrastructure. MCP shines when you need persistent state, tool composition, or when multiple AI clients (Claude, Cursor, custom agents) need identical access to your backend.

The real payoff: one server, many clients. You write the integration once, and every MCP-compatible client can use it. That's the kind of leverage worth building for.

Getting Started with Build an MCP Server

The official TypeScript SDK makes this straightforward. Here's a minimal server that exposes a database query tool:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "user-db-server",
  version: "1.0.0",
});

server.tool(
  "getUserById",
  { userId: z.string().describe("The user's UUID") },
  async ({ userId }) => {
    const user = await db.query("SELECT * FROM users WHERE id = $1", [userId]);
    return {
      content: [{ type: "text", text: JSON.stringify(user.rows[0]) }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Run it with npx tsx server.ts. That's it — you have a working MCP server. The stdio transport means any MCP client can spawn it as a subprocess and start talking.

Core Build an MCP Server Concepts Every Developer Should Know

Resources: Read-Only Data Access

Resources are for exposing static or semi-static data — configs, schemas, reference docs. They're not for dynamic queries; use tools for that.

server.resource(
  "database-schema",
  "postgres://schema",
  async (uri) => ({
    contents: [{
      uri: uri.href,
      text: `-- Schema for ${process.env.DB_NAME}\n` + await getSchema(),
    }],
  })
);

Prompts: Reusable Instruction Templates

Prompts let you ship curated workflows. If your team always asks the AI to "summarize the latest deployment," encode that as a prompt instead of making them type it out.

server.prompt(
  "deploy-summary",
  { environment: z.string() },
  ({ environment }) => ({
    messages: [{
      role: "user",
      content: `Summarize the last deployment to ${environment}.\nFocus on: changes, rollbacks, and incident reports.`
    }]
  })
);

Tool Composition: Chaining Calls

The killer feature. Tools can call other tools through the MCP protocol, enabling multi-step workflows. Your createOrder tool can internally call validateInventory and chargeCustomer — the client sees them as separate invocations.

server.tool(
  "createOrder",
  { productId: z.string(), qty: z.number() },
  async ({ productId, qty }) => {
    const inventory = await server.callTool({
      name: "checkInventory",
      arguments: { productId }
    });
    // ... rest of order logic
  }
);

Common Build an MCP Server Mistakes and How to Fix Them

Mistake 1: Blocking the Event Loop. Your tools run in the same process. A synchronous fs.readFileSync or a slow database call will freeze the entire server. Wrap everything in async, and if a library is sync-only, offload it to a worker thread.

Mistake 2: Returning Raw Errors. If your tool throws, the client sees a cryptic stack trace. Always catch and return structured errors:

try {
  const result = await riskyOperation();
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (err) {
  return {
    isError: true,
    content: [{ type: "text", text: `Operation failed: ${err.message}` }]
  };
}

Mistake 3: Ignoring Input Validation. The zod schemas aren't optional — they're your security boundary. MCP clients can send anything. If you skip validation, you're one malicious prompt away from SQL injection.

When Should You Use Build an MCP Server?

Use MCP when you have multiple AI clients that need consistent access to your backend, or when your AI workflows require stateful multi-step operations. Skip it if you're building for a single client and a simple REST call suffices. The protocol adds a process boundary and serialization overhead — that's a real cost for high-frequency, low-latency calls.

Build an MCP Server in Production

Authentication at the transport layer. Stdio is fine for local dev, but for remote servers use the SSE or streamable HTTP transport and put an auth proxy in front. MCP doesn't handle auth natively — that's your job.

Version your tools. When you change a tool's input schema, existing clients break. Include a version in the tool name (getUser_v2) or maintain backward-compatible schemas with optional fields.

Log everything. MCP servers are black boxes from the client's perspective. Log every tool invocation with input, output size, and duration. You'll thank yourself when an agent goes rogue and you need to trace what happened. For a production-grade reference implementation, check out the examples on suhailroushan.com — I've documented a few patterns that work well at scale.

Start with one tool that solves a real pain point — a database query or an internal API wrapper — and wire it into Claude Desktop or Cursor today. The protocol is young, but the investment compounds: every tool you add makes your AI workflows more capable.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch