All posts
mcptool-calling

MCP Tools: A Practical Guide for Full-Stack Developers

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

SR

Suhail Roushan

August 6, 2026

·
6 min read
·
0 views

MCP Tools let AI models call your APIs directly, turning a chatbot into an actual operator of your software stack. This guide shows you how to wire up Model Context Protocol servers with TypeScript, what pitfalls to dodge, and where the tech actually earns its keep.

MCP Tools are the fastest way to give an LLM hands-on access to your database, file system, or internal services. If you're a full-stack developer building AI features, you've likely hit the wall where a model can talk about actions but can't perform them. MCP (Model Context Protocol) fixes that by defining a standard interface for tools. I've spent the last six months shipping MCP servers in production, and the pattern is solid — but it's not for every use case.

Why MCP Tools Matters (and When to Skip It)

MCP is a network protocol that standardizes how AI clients discover and invoke external tools. Instead of writing bespoke function-calling code for each LLM vendor, you build one MCP server and any compliant client can use it. That's a massive win for team velocity.

Skip MCP Tools when you only need one-off, single-tenant integrations. If you're building a script that calls OpenAI once and you'll never reuse that logic, just use native function calling. MCP adds a transport layer, JSON-RPC overhead, and lifecycle management — that's complexity you don't need for a demo. But the moment you have multiple clients, multiple tools, or an internal tool library, MCP pays for itself.

Getting Started with MCP Tools

You need Node.js 18+, the @modelcontextprotocol/sdk package, and any TypeScript compiler. Here's a minimal server that exposes a single tool to read environment variables:

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: "env-reader",
  version: "1.0.0",
});

server.registerTool(
  "get_env",
  {
    title: "Get Environment Variable",
    description: "Read a value from the process environment",
    inputSchema: z.object({
      key: z.string().describe("Environment variable name"),
    }),
  },
  async ({ key }) => {
    const value = process.env[key] ?? "NOT_SET";
    return {
      content: [{ type: "text", text: `${key}=${value}` }],
    };
  }
);

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

Run it with npx tsx server.ts and any MCP client (Claude Desktop, VS Code extensions) can connect via stdio. That's the whole setup — no HTTP server, no auth boilerplate.

Core MCP Tools Concepts Every Developer Should Know

1. Tool Registration with Zod Schema

Every tool needs a strict input schema. MCP uses Zod for validation, and the schema doubles as the LLM's instruction manual. Be explicit with .describe() — that text goes directly into the model's context window and shapes how it calls the tool.

server.registerTool(
  "query_users",
  {
    inputSchema: z.object({
      minAge: z.number().optional().describe("Filter users by minimum age"),
      role: z.enum(["admin", "user", "moderator"]).optional(),
    }),
  },
  async (args) => {
    // args is fully typed from the Zod schema
    const users = await db.query("SELECT * FROM users WHERE ...");
    return { content: [{ type: "text", text: JSON.stringify(users) }] };
  }
);

2. Resource vs Tool Distinction

A tool performs an action. A resource is read-only data the model can pull in. Don't mix them. If you're exposing a file or a table, use server.registerResource(). If you're mutating state or running a computation, use registerTool(). Models treat them differently — resources get loaded eagerly, tools get called on demand.

3. Streaming Results for Long Operations

Never block the JSON-RPC response on a slow operation. Return a stream handle immediately, then push results as they arrive. The SDK supports this natively:

server.registerTool(
  "run_report",
  {
    inputSchema: z.object({ reportId: z.string() }),
  },
  async (args, extra) => {
    const stream = extra.stream;
    // Start async work
    generateReport(args.reportId).then((data) => {
      stream.send({ type: "text", text: data });
    });
    return { content: [{ type: "text", text: "Report started" }] };
  }
);

Common MCP Tools Mistakes and How to Fix Them

Mistake 1: Over-describing tools. I've seen teams write 500-word descriptions for a simple CRUD endpoint. The model's context window is finite — every word you add is space stolen from actual data. Keep descriptions under 50 words unless the tool has genuinely tricky edge cases.

Mistake 2: Ignoring error responses. The default error format is fine for debugging but terrible for LLM recovery. When a tool fails, return a structured error message that tells the model what went wrong and what to try next. That turns a dead end into a retry loop.

try {
  const result = await riskyOperation();
  return { content: [{ type: "text", text: result }] };
} catch (e) {
  return {
    isError: true,
    content: [{ type: "text", text: `Failed: ${e.message}. Try with retry=true` }],
  };
}

Mistake 3: No rate limiting on tools. An LLM in a loop can hammer your database thousands of times per minute. Wrap every tool with a simple token bucket or debounce. The MCP SDK gives you hooks — use them.

When Should You Use MCP Tools?

Use MCP Tools when you have multiple AI clients (Claude, Copilot, custom apps) that need access to the same backend capabilities. The protocol standardizes the interface, so you write the integration once and every client inherits it. You should also reach for MCP when your tools are complex enough that a model needs structured input schemas — Zod validation catches malformed calls before they hit your business logic.

Skip MCP Tools for single-vendor integrations, internal scripts, or any workflow where the AI calls a function directly in-process. If you're running everything in one Node process and you don't need remote clients, native function calling is simpler and faster.

MCP Tools in Production

First, always run your MCP server behind an HTTP/SSE transport in production, not stdio. Stdio is fine for local dev, but it ties your server lifecycle to the client process. Use StreamableHTTPServerTransport for proper deployment:

import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";

const app = express();
app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
  await server.connect(transport);
  // Handle the stream
});

Second, add observability from day one. Log every tool call, its input, duration, and result. You'll need this to debug model behavior — LLMs make unexpected calls, and you can't reproduce them without logs.

Third, version your tool schemas. Models get trained on schemas, and changing a parameter name breaks everything. Add a version field to each tool registration and honor it in your handler logic.

Your takeaway: Build one MCP server with two tools today — one that reads data, one that mutates state — and wire it to a local LLM client. That hour of hands-on work will teach you more than any blog post about where the protocol shines and where it adds friction.

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