Skip to main content
AllDevToolsHub
2026-08-01
Last reviewed: Aug 2026
AI
Est Read: 10_MIN

How to Build a Custom MCP Server in TypeScript

How to Build a Custom MCP Server in TypeScript
Processing_Node: 01

#1Building a custom MCP server in TypeScript

What we tested: We counted tokens across multiple model families (GPT-4o, Claude, Gemini) using our LLM token counter. Prompt caching behavior was tested with repeated inputs of varying length.

A custom MCP server is worth building when you want an AI client to reach your own tools, files, or internal APIs through one controlled interface.

The goal is to expose a small, validated capability layer that behaves like any other production boundary.

#2What problem a custom MCP server solves

Without MCP, teams often wire every AI client to every internal system separately:

  • one adapter for an editor
  • another for a desktop assistant
  • another for a hosted agent
  • another for internal automation

That gets expensive fast. A custom MCP server gives you one standard layer that can expose:

  • tools for actions
  • resources for read-only context
  • prompts for reusable instructions

MCP keeps showing up in modern developer tooling and agent workflows because it reduces bespoke glue code and makes your capability layer easier to reuse across clients.

If you want the bigger protocol background first, read What is MCP (Model Context Protocol)? Complete 2026 Guide. If you are comparing design options, MCP vs Function Calling vs APIs is the better starting point.

#2What you should build first

Most teams should not start by exposing every internal service.

A better first MCP server usually does one of these jobs:

  • validate or transform structured data
  • wrap a small internal API
  • expose a read-only operational resource
  • provide a narrow workflow helper for engineering teams

Good first examples:

  • "fetch deployment status"
  • "read service health summary"
  • "validate OpenAPI document"
  • "look up customer environment metadata"

Bad first examples:

  • "give the model unrestricted database access"
  • "let the agent mutate production config"
  • "proxy arbitrary shell execution"

The safest first server is narrow, testable, and easy to audit.

#2MCP server building blocks

At a high level, an MCP server usually exposes three primitive types:

PrimitiveBest use
ToolPerform an action or computation
ResourceReturn read-only context
PromptReusable task template for a client

For many real servers, you can start with only tools and resources. Prompts are useful, but they are not required to make the server valuable.

The more important distinction is this:

  • tools should be explicit and bounded
  • resources should be stable and readable
  • both should be documented clearly enough that the model can choose them correctly

That last point matters more than people expect. Weak descriptions and vague schemas cause tool misuse even when the protocol wiring is technically correct.

#2Project setup

Start with a small TypeScript project:

bash
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node

Use a strict tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"]
}

Why strict mode matters:

  • tool inputs are untrusted
  • server responses should be predictable
  • a loose type boundary becomes production ambiguity quickly

#2A minimal custom MCP server in TypeScript

Below is a practical example. It exposes:

  1. one read-only resource
  2. one small tool with Zod validation
  3. stdio transport for local client integration
typescript
import { z } from "zod";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
  {
    name: "team-ops-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
      resources: {},
    },
  }
);

const deploymentInput = z.object({
  service: z.string().min(1),
  environment: z.enum(["dev", "staging", "prod"]).default("dev"),
});

server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: [
      {
        uri: "internal://runbooks/deployments",
        name: "Deployment Runbook",
        mimeType: "text/markdown",
        description: "Read-only deployment guidance for supported services.",
      },
    ],
  };
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  if (request.params.uri !== "internal://runbooks/deployments") {
    throw new Error(`Unknown resource: ${request.params.uri}`);
  }

  return {
    contents: [
      {
        uri: request.params.uri,
        mimeType: "text/markdown",
        text: [
          "# Deployment Runbook",
          "",
          "- Verify CI passed",
          "- Check migration requirements",
          "- Confirm rollback path",
        ].join("\n"),
      },
    ],
  };
});

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "get_deployment_status",
        description:
          "Return the current deployment status for a named service and environment.",
        inputSchema: {
          type: "object",
          properties: {
            service: {
              type: "string",
              description: "Service name, for example billing-api",
            },
            environment: {
              type: "string",
              enum: ["dev", "staging", "prod"],
              description: "Target environment",
            },
          },
          required: ["service"],
        },
      },
    ],
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: rawArgs } = request.params;

  if (name !== "get_deployment_status") {
    throw new Error(`Unknown tool: ${name}`);
  }

  const args = deploymentInput.parse(rawArgs ?? {});

  const result = {
    service: args.service,
    environment: args.environment,
    status: "healthy",
    version: "2026.08.19",
    lastDeploymentAt: "2026-08-19T09:30:00Z",
  };

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

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP server connected over stdio");
}

main().catch((error) => {
  console.error("Fatal MCP server error:", error);
  process.exit(1);
});

This is intentionally small. It demonstrates the structure without pretending that a production server is just one file.

#2Why this example is a better starting point

The original instinct many teams have is to expose a powerful tool immediately. That is usually the wrong move.

This example is better because:

  • the tool is narrow
  • the resource is read-only
  • input validation is explicit
  • logs stay on stderr
  • the output is structured enough for the client to interpret

In other words, it treats the MCP layer as an engineering boundary, not a marketing demo.

#2Registering the server with an MCP client

For local development, stdio is usually the simplest transport. Many desktop tools and editors can launch your server as a child process and communicate with it directly.

A typical config shape looks like this:

json
{
  "mcpServers": {
    "team-ops": {
      "command": "npx",
      "args": ["-y", "tsx", "/absolute/path/to/src/index.ts"]
    }
  }
}

Exact configuration details depend on the client, but the core pattern is consistent:

  1. register a server name
  2. provide the command to launch it
  3. pass the correct entrypoint
  4. restart or reload the client

After that, confirm the client can:

  • discover the tool list
  • read the exposed resource
  • call the tool successfully with valid arguments
  • surface a clear error on invalid input

#2Choosing between stdio and remote transports

For many internal workflows:

  • use stdio when the server runs locally with a desktop client or editor
  • use a remote transport when the server must run as a shared service

The right choice depends on deployment model, authentication needs, and client support. The main operational question is not "which transport sounds modern?" It is "where should this trust boundary live?"

If a tool needs access to sensitive internal systems, local stdio may be easier to reason about for a single developer workflow. If multiple clients need shared access, a remote service may be appropriate - but only after you define authentication, authorization, logging, and rate limits.

#2Production patterns that matter more than the tutorial demo

This is the section that usually determines whether your MCP server is useful six weeks later.

#31. Validate every input

Do not trust tool arguments because they came from a protocol client. The model may send malformed, partial, or semantically wrong values.

Use schema validation at the handler boundary. Zod is a good fit because it keeps:

  • runtime validation
  • default handling
  • parsing errors
  • TypeScript inference

all in one place.

#32. Keep tool descriptions concrete

Bad description:

  • "Gets deployment info"

Better description:

  • "Return the current deployment status for a named service and environment."

Good tool descriptions reduce tool-selection errors. That matters because many agent failures are not transport failures. They are tool-choice failures.

#33. Return structured content

If your tool produces an object, return a stable JSON-shaped payload in text form instead of a vague prose paragraph.

That makes it easier for both humans and models to:

  • inspect results
  • compare outputs
  • diagnose failures
  • chain the result into other steps

#34. Separate read-only resources from action tools

If something is reference material, expose it as a resource instead of turning it into a tool call. This makes the server easier to reason about and reduces accidental side effects.

#35. Log carefully

For local stdio servers, stdout is typically reserved for the protocol stream. Use stderr for operational logs and error messages.

That single rule avoids one of the most common beginner mistakes.

#36. Design for least privilege

If your tool only needs to read deployment status, do not give it permission to modify rollout config.

Least privilege matters even more with AI-facing systems because:

  • models can misunderstand instructions
  • users can ask for overly broad actions
  • tool misuse can become a security issue quickly

#2How to test a custom MCP server

Do not stop after "the client can see the tool."

At minimum, test:

  1. valid tool invocation
  2. invalid arguments
  3. unknown tool name
  4. missing resource
  5. stable output shape
  6. expected error behavior

If the server is important, add workflow testing too. MCP Server Testing 2026 Developer Guide goes deeper on test strategies, while Production-Grade AI Tool Calling: Schema Design, Error Handling, and Retry Loops is useful for thinking about schema reliability and tool failure loops.

Useful companion tools on AllDevToolsHub:

#2Common mistakes when building MCP servers

#3Making the first server too broad

If version one exposes ten tools, three auth modes, and multiple data sources, you will learn less than if version one exposes one good tool with strong validation.

#3Treating internal APIs as self-documenting

They are not. MCP helps with interoperability, but poor tool naming and poor argument design still make the server hard for models to use correctly.

#3Skipping authorization design

A server can be protocol-correct and still be operationally unsafe. If you plan to expose sensitive services, define:

  • who can call the server
  • what each tool can access
  • how calls are audited
  • which environments are allowed

#3Returning success-shaped noise

Do not hide errors behind vague text like "request completed." Return clear failures when the action did not complete correctly.

#3Forgetting observability

You should know:

  • which tools are called most often
  • what validation errors are common
  • which clients trigger failures
  • whether the issue is model behavior or server behavior

Without this, MCP adoption becomes hard to debug at scale.

#2When a custom MCP server is worth it

Build one when at least one of these is true:

  • multiple AI clients need the same capability layer
  • your current tool wiring is fragmented
  • you need a standard way to expose internal tools safely
  • you want a reusable boundary between models and systems

Do not build one just because the protocol is popular. If a simple internal API is enough, keep the system simple. MCP is valuable when it reduces integration cost and clarifies the tool layer - not when it adds ceremony.

#2Frequently Asked Questions (FAQ)

#3What is a custom MCP server?

A custom MCP server is a server that exposes your own tools, resources, and sometimes prompts through the Model Context Protocol so supported AI clients can discover and use them through a standard interface.

#3Why build an MCP server in TypeScript?

TypeScript is a strong fit because many AI tooling stacks already use Node.js, and TypeScript makes it easier to define strict schemas, validate inputs, and keep server behavior predictable.

#3What should my first MCP tool do?

Pick a narrow, low-risk capability such as validation, status lookup, or read-only retrieval. Avoid starting with broad write access to sensitive systems.

#3What is the difference between an MCP tool and an MCP resource?

An MCP tool performs an action or computation. An MCP resource returns read-only context that a client can inspect without taking action.

#3Should I use stdio or a remote transport?

Use stdio for many local editor and desktop workflows. Use a remote transport when the server needs to be shared as a service and you are prepared to manage authentication, authorization, and observability.

#3How do I make an MCP server safer?

Validate every input, keep tool scopes narrow, use least privilege, log clearly, test invalid cases, and avoid exposing powerful actions before you have review and audit controls.

#2Conclusion

The best custom MCP server is usually not the most ambitious one. It is the one that exposes a clear capability boundary that both humans and AI clients can use reliably.

Start with one useful tool, one stable resource, and one strict validation layer. Once that works, add observability, access controls, and focused tests before you expand the surface area. That path creates a server that is genuinely useful in production instead of just impressive in a demo.

Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.

#2Sources / Further reading

#2Try These Tools

Quick Summary

>- Learn how to build a practical MCP server in TypeScript with tools, resources, input validation, and safer production patterns. Includes a minimal server example, client wiring, testing advice, and common mistakes to avoid.

RJRahul JalavadiyaFounder & Lead Engineer
Published 2026-08-01Last reviewed 2026-08-23

Tools Mentioned in This Article

Tools, tactics, and toughened-up tips, once a week

New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.

Found an error or have feedback?

We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.

Last reviewed: 2026-08-23
Security Memo
AT

Rahul Jalavadiya

Engineering Protocol V1

Specializing in local-first architecture and Zero-Trust developer workflows. No data leaves the machine.