AI

MCP Explained: How to Let AI Agents Use Your App (With a Working TypeScript Server)

October 2, 20266 min readBy Saad Minhas

MCP · Model Context Protocol · AI Agents · TypeScript · Claude · Node.js

MCP Explained: How to Let AI Agents Use Your App (With a Working TypeScript Server)

A year ago, if you wanted an AI assistant to work with your product, you built a custom integration for each assistant. One for ChatGPT plugins, another for whatever came next, each with its own format.


MCP, the Model Context Protocol, replaces that with one standard. You build an MCP server for your product once, and any AI app that speaks MCP can use it. Claude, Claude Code, Cursor, VS Code and a growing list of other tools already do.


This post explains what MCP is without the jargon, then walks through building a small working server in TypeScript and connecting it to Claude.


What MCP actually is


MCP is an open protocol, introduced by Anthropic in late 2024, for connecting AI applications to outside tools and data. There are two sides:


  • The client is the AI app: Claude Desktop, Claude Code, Cursor and so on.
  • The server is a small program you write that exposes your product's capabilities.

The client asks the server what it can do, shows those capabilities to the model, and calls them when the model decides they're useful.


A server can expose three kinds of things:


  • Tools are actions the model can take, like "look up an order" or "create an invoice". This is what most people mean when they talk about MCP.
  • Resources are data the app can read, like a file, a document or a database record.
  • Prompts are reusable templates the user can pick, like "summarise this week's support tickets".

Most servers start with tools only. That's what we'll build.


Why it matters if you run a product


Your customers increasingly do their work inside AI tools. If your product has an MCP server, they can say "what's the status of order 1042?" or "add these five leads to the CRM" from inside the assistant they already have open, without switching tabs.


For internal teams the same idea applies. An MCP server over your internal API lets staff query and update company systems in plain language, with the same permissions they already have.


Building a server: order status lookup


We'll build a server with one tool that looks up an order by its number. Swap the fake data for your real database or API later.


Set up the project:


mkdir orders-mcp && cd orders-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

In package.json, add "type": "module" and a build script:


{
  "type": "module",
  "scripts": {
    "build": "tsc"
  }
}

Add a tsconfig.json:


{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Now the server itself, in src/server.ts:


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

// Stand-in for your real database or API
const orders: Record<string, { status: string; items: number; eta: string }> = {
  "ORD-1042": { status: "shipped", items: 3, eta: "2026-10-06" },
  "ORD-1043": { status: "processing", items: 1, eta: "2026-10-09" },
};

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

server.registerTool(
  "get_order_status",
  {
    title: "Get order status",
    description:
      "Look up the current status, item count and delivery estimate " +
      "for a customer order. Use when the user asks where an order is.",
    inputSchema: {
      orderNumber: z.string().describe("Order number, for example ORD-1042"),
    },
  },
  async ({ orderNumber }) => {
    const order = orders[orderNumber.toUpperCase()];

    if (!order) {
      return {
        content: [{ type: "text", text: `No order found with number ${orderNumber}.` }],
        isError: true,
      };
    }

    return {
      content: [
        {
          type: "text",
          text: `Order ${orderNumber}: ${order.status}, ${order.items} items, expected ${order.eta}.`,
        },
      ],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("orders MCP server running");

Build it:


npm run build

The one gotcha that will cost you an hour


Notice the last line uses console.error, not console.log. With the stdio transport, the server talks to the client over standard output. Anything you console.log gets mixed into the protocol messages and breaks the connection, usually with an unhelpful error. Log to stderr, always.


Testing it with the MCP Inspector


Before connecting to a real assistant, test the server with the official inspector. It opens a small web UI where you can see your tools and call them by hand:


npx @modelcontextprotocol/inspector node build/server.js

Click "List tools", pick get_order_status, enter ORD-1042 and run it. If you get the order back, the server works.


Connecting it to Claude


Claude Code takes one command:


claude mcp add orders -- node /absolute/path/to/orders-mcp/build/server.js

Claude Desktop reads a config file. Open Settings, go to Developer, choose Edit Config, and add your server:


{
  "mcpServers": {
    "orders": {
      "command": "node",
      "args": ["/absolute/path/to/orders-mcp/build/server.js"]
    }
  }
}

Restart the app and ask: "Where is order ORD-1042?" The model will see your tool, decide to call it, and answer with the result.


Use absolute paths. Relative paths are the second most common reason a server doesn't show up.


From local to hosted


The server above runs on the user's machine and talks over stdio. That's ideal for developer tools and internal use.


If you want customers to connect to your product from anywhere, you run the server remotely over HTTP instead. The SDK supports this with its Streamable HTTP transport. The tool code stays the same; what changes is that you now need real authentication, because the server is on the internet. MCP uses OAuth for this, so users sign in with their existing account and the server acts with their permissions.


Designing tools the model can actually use


Writing the server is the easy part. Designing good tools is where most of the quality comes from.


The description is a prompt. The model decides when to call a tool based almost entirely on its name and description. Say what it does and when to use it, in plain words.


Build for tasks, not endpoints. Don't mirror your REST API one route at a time. If users usually want "this customer's recent orders with their statuses", make that one tool instead of three that the model has to chain together.


Return less. Send back the fields that answer the question, as short readable text, not a 300-line JSON object. Every extra token is something the model has to read past.


Write clear errors. "No order found with number X" lets the model recover and ask the user to check the number. A stack trace doesn't.


Start read-only. Launch with tools that only read data. Add tools that change things once you trust the setup, and make destructive actions ask for confirmation.


Remember that data can contain instructions. If a tool returns text written by users, like support tickets or emails, that text can include instructions aimed at the model. Treat tool output as data, keep permissions tight, and don't give a server more access than the user it acts for.


Is it worth building one?


If your product has an API and your users already use AI assistants, an MCP server is one of the cheapest ways to show up inside their daily workflow. A useful first version with a handful of read-only tools is a small project, not a quarter-long initiative.


If you'd like help designing or building an MCP server for your product, or connecting AI assistants to your internal systems, tell me what you're working on. And if your assistant also needs to answer questions from documents, read how to build an AI chatbot that answers from your own documents.

Writing

Notes from building with AI tools, React and production systems.

Get In Touch

Connect

Full Stack Software Engineer passionate about building innovative web and mobile applications.

CEO at Appzivo, the software studio I founded.

© 2026 Saad Minhas. All Rights Reserved.