How to Build a Custom MCP Server in TypeScript

#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:
| Primitive | Best use |
|---|---|
| Tool | Perform an action or computation |
| Resource | Return read-only context |
| Prompt | Reusable 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:
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/nodeUse a strict tsconfig.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:
- one read-only resource
- one small tool with Zod validation
- stdio transport for local client integration
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:
{
"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:
- register a server name
- provide the command to launch it
- pass the correct entrypoint
- 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:
- valid tool invocation
- invalid arguments
- unknown tool name
- missing resource
- stable output shape
- 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
- Model Context Protocol - Official documentation
- Anthropic - Building effective agents
- Zod - Documentation
#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.
Tools Mentioned in This Article
JSON to TypeScript
Convert JSON objects into TypeScript interfaces instantly.
ESLint Config Generator
Interactively build and export ESLint configurations.
JS/TS Beautify/Minify
Lightweight JavaScript/TypeScript formatter and minifier.
JSON Schema Generator
Auto-generate a JSON Schema from any JSON object with format detection.
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.