top of page
DevCollar Private Limited Logo

How to Build an MCP Server with TypeScript

Writer: Devesh Singh
Devesh Singh
Aug 22
6 min read

Model Context Protocol (MCP) gives AI applications a standardized way to discover and interact with external capabilities.

Instead of writing a custom integration for every AI client, you can build an MCP server that exposes tools, resources, and prompts through a common protocol.

For TypeScript developers, the architecture is familiar:

AI Application
      ↓
MCP Client
      ↓
MCP Transport
      ↓
Your TypeScript MCP Server
      ↓
APIs / Databases / Services

In this guide, we'll build a small MCP server in TypeScript, expose a tool, run it locally over stdio, and look at what needs to change for a production server.

Version note: The official TypeScript SDK has moved to split server/client packages in its current generation. MCP evolves quickly, so check the SDK documentation when starting a new project rather than copying older examples built around deprecated transports or package layouts.

What Does an MCP Server Actually Do?

An MCP server sits between an AI application and a capability you want the AI to access.

Suppose your backend has an internal user API. You could expose a tool called get_user. The MCP server describes the tool, defines its input schema, executes the backend operation when requested, and returns the result in MCP's expected format.

User Request
     ↓
AI Application
     ↓
Discovers get_user
     ↓
MCP Server
     ↓
Validate Input
     ↓
Internal Service
     ↓
Result

The LLM does not need to know your internal implementation. It only needs the capability exposed through the protocol.

MCP server architecture showing clients, tools, resources, prompts, APIs, and data sources

Tools, Resources, and Prompts

Tools

Tools perform operations such as:

get_weather
create_ticket
search_customers
run_report

A tool normally has a name, description, input schema, and handler.

Resources

Resources expose information that a client can read, such as documents, configuration, or application data.

Prompts

Prompts expose reusable prompt templates that clients can discover.

For most backend integrations, tools are the easiest place to start.

Create the TypeScript Project

mkdir typescript-mcp-server
cd typescript-mcp-server
npm init -y

npm install @modelcontextprotocol/server zod
npm install -D typescript tsx @types/node

Create:

typescript-mcp-server/
├── src/
│   └── index.ts
├── package.json
└── tsconfig.json

A basic tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Create the MCP Server

Inside src/index.ts:

import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

function createServer() {
  const server = new McpServer({
    name: "devcollar-example",
    version: "1.0.0",
  });

  return server;
}

void serveStdio(createServer);

At this point, the server exists, but it does not expose anything useful.

Register Your First MCP Tool

Let's add a user lookup tool:

server.registerTool(
  "get_user",
  {
    description: "Get a user by their numeric ID",
    inputSchema: {
      userId: z.number().int().positive(),
    },
  },
  async ({ userId }) => {
    const user = {
      id: userId,
      name: "Alex",
      role: "Developer",
    };

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(user),
        },
      ],
    };
  }
);

The structure is:

get_user
   │
   ├── Description
   ├── Input Schema
   └── Handler

The description helps an AI client understand when the tool is useful. The schema validates arguments, while the handler contains the backend operation.

Why Tool Descriptions Matter

Compare:

"Gets stuff"

with:

"Get a user by their numeric ID"

The second gives the model much better information.

Tool names and descriptions should clearly communicate what the tool does and when it should be used. Keep them concise but unambiguous.

Validate Inputs With Schemas

Never assume arguments are valid because an LLM generated them.

inputSchema: {
  userId: z.number().int().positive(),
}

For a support tool:

inputSchema: {
  title: z.string().min(3),
  description: z.string().min(10),
  priority: z.enum(["low", "medium", "high"]),
}

Treat MCP inputs like inputs to any backend endpoint: validate before touching your systems.

Connect the Tool to a Real Service

A real implementation might call an internal API:

async function getUser(userId: number) {
  const response = await fetch(
    `https://api.example.com/users/${userId}`
  );

  if (!response.ok) {
    throw new Error(`User lookup failed: ${response.status}`);
  }

  return response.json();
}

Then use it from the handler:

async ({ userId }) => {
  const user = await getUser(userId);

  return {
    content: [
      {
        type: "text",
        text: JSON.stringify(user),
      },
    ],
  };
}

Now the architecture is:

AI Client
    ↓
get_user
    ↓
MCP Server
    ↓
Internal API
    ↓
User Data

Run the Server Over stdio

Run:

npx tsx src/index.ts

The process waits for an MCP client to communicate over stdin and stdout.

One important rule:

Do not write normal logs to stdout when using stdio.

stdout is the protocol channel. A console.log() can corrupt JSON-RPC communication.

Use stderr:

console.error("MCP server started");

Test With MCP Inspector

Run:

npx @modelcontextprotocol/inspector npx tsx src/index.ts

The Inspector lets you view exposed capabilities and call tools manually.

For get_user, test:

{
  "userId": 42
}

Testing the server independently helps separate MCP implementation problems from LLM or agent behavior.

stdio vs Streamable HTTP

stdio

Use stdio when an MCP host launches your server as a local process:

MCP Host
   ↓
Starts Process
   ↓
stdin / stdout
   ↓
MCP Server

It is useful for local developer tools and desktop integrations.

Streamable HTTP

For remotely hosted MCP servers, use Streamable HTTP:

AI Application
      ↓
Network
      ↓
HTTP MCP Endpoint
      ↓
MCP Server
      ↓
Backend Services

Older tutorials may show HTTP+SSE. That transport belongs to an older MCP protocol generation; new implementations should use the current Streamable HTTP approach unless older-client compatibility is required.

Structure a Larger MCP Project

Once you have several tools, avoid putting everything in index.ts.

src/
├── index.ts
├── server.ts
├── tools/
│   ├── get-user.ts
│   ├── search-users.ts
│   └── create-ticket.ts
├── services/
│   ├── users.ts
│   └── tickets.ts
└── schemas/
    └── common.ts

Keep MCP handlers thin:

MCP Tool
   ↓
Service Function
   ↓
API / Database

Your service layer should own the actual business logic.

Error Handling

Tools will fail. APIs time out, records disappear, and dependencies become unavailable.

Translate internal failures into useful tool responses without unnecessarily exposing stack traces or infrastructure details.

Internal Error
     ↓
Detailed Server Log
     ↓
Safe Tool Error
     ↓
AI Client

Distinguish where possible between invalid input, not found, permission denied, dependency failure, and unexpected errors.

Authentication and Authorization

An AI requesting an operation does not make that operation authorized.

For tools such as:

get_customer
update_subscription
delete_project

your normal security model still applies:

Identity
   ↓
Authentication
   ↓
Authorization
   ↓
Tool Execution

For sensitive operations, consider explicit confirmation, narrow permissions, audit logs, rate limits, tenant isolation, and downstream validation.

Avoid Overpowered Tools

Compare:

execute_arbitrary_sql

with:

get_customer_orders

The second is narrower and easier to validate, authorize, monitor, and reason about.

Prefer business capabilities such as:

search_products
get_invoice
create_support_ticket
update_order_status

instead of unrestricted infrastructure access.

Production Architecture

A production deployment may look like:

                 AI Clients
                     ↓
              Streamable HTTP
                     ↓
              ┌─────────────┐
              │ MCP Server  │
              └──────┬──────┘
                     │
        ┌────────────┼────────────┐
        ↓            ↓            ↓
    User API     Search API    Ticket API
        ↓            ↓            ↓
   PostgreSQL    Search Index   SaaS Service

The MCP layer should usually remain thin. Its job is to expose capabilities, validate inputs, enforce access rules, call backend services, and return useful results.

What Should You Expose Through MCP?

Do not automatically convert every backend endpoint into a tool.

A capability is a good MCP candidate when:

  • An AI application has a real reason to use it.

  • Its purpose can be described clearly.

  • Inputs can be validated.

  • Permissions can be enforced.

  • Outputs are understandable to the model.

  • Side effects are controlled.

An engineering assistant might expose:

search_documentation
get_service_status
query_deployment
get_recent_errors
create_incident

A focused set of useful capabilities is better than exposing your entire backend.

Common Mistakes

Using Outdated Transport Examples

MCP evolves quickly. Check the current SDK documentation before copying older SSE tutorials.

Logging to stdout With stdio

stdout belongs to MCP protocol traffic. Use stderr for diagnostics.

Weak Tool Descriptions

Ambiguous descriptions make tool selection less reliable.

Putting Business Logic Inside Tool Handlers

Keep handlers thin and delegate to reusable services.

Trusting LLM-Generated Inputs

Always validate arguments.

Exposing Excessive Permissions

Prefer narrow, task-specific capabilities.

Ignoring Observability

Track tool latency, failures, downstream calls, and safe audit information.

A Practical Development Workflow

1. Identify one useful backend capability
              ↓
2. Define a narrow MCP tool
              ↓
3. Add a strict input schema
              ↓
4. Connect existing service logic
              ↓
5. Test with MCP Inspector
              ↓
6. Add authorization + error handling
              ↓
7. Add monitoring
              ↓
8. Integrate with an AI client

Start with one or two tools. You will learn more from watching an AI client use a small, well-designed toolset than from creating dozens of tools before testing.

Conclusion

Building an MCP server with TypeScript is mostly familiar backend engineering wrapped in a standardized protocol.

Create MCP Server
      ↓
Register Tool
      ↓
Define Input Schema
      ↓
Call Backend Service
      ↓
Return MCP Content
      ↓
Connect Through a Transport

For local integrations, stdio is simple and effective. For remote deployments, use Streamable HTTP and apply the authentication, authorization, validation, observability, and error handling expected from any production backend.

The goal is not to expose as many tools as possible.

It is to provide clear, narrow, secure capabilities that an AI application can reliably understand and use.

Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating

Join the DC Club

Join our email list and get access to specials deals exclusive to our subscribers.

Thanks for submitting!

© 2026

 by DevCollar Private Limited

bottom of page