Existing MCPWhat is mcp?Model Context Protocol - a standard that lets AI tools connect to external services like databases, issue trackers, or APIs. servers cover many common use cases, but sometimes you need something custom. Maybe your company has an internal APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.. Maybe you want to connect Claude to a service that nobody has built a server for yet. In those cases, you build your own.
This lesson walks you through building a simple MCP server in TypeScript, from setup to testing.
When to build a custom server
Build a custom MCPWhat is mcp?Model Context Protocol - a standard that lets AI tools connect to external services like databases, issue trackers, or APIs. server when:
- Your system has no existing MCP server: Your company's internal APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses., a niche SaaS tool, a custom database
- You need custom business logic: Existing servers expose raw operations, but you want guided workflows
- You will reuse the integration: If you only need it once, a direct API call in your code is simpler
- You want to share it: Building an MCP server means anyone with an MCP-compatible AI tool can use it
Do not build a custom server when a direct API call in a script or function would suffice. MCP adds value through standardization and reusability, if neither matters for your use case, keep it simple.
Project setup
Start by creating a new Node.js project:
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/nodeCreate a tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}The key dependencies:
@modelcontextprotocol/sdk, the official MCPWhat is mcp?Model Context Protocol - a standard that lets AI tools connect to external services like databases, issue trackers, or APIs. SDKWhat is sdk?A pre-built library from a service provider that wraps their API into convenient functions you call in your code instead of writing raw HTTP requests. for TypeScriptzod, schemaWhat is schema?A formal definition of the structure your data must follow - which fields exist, what types they have, and which are required. validation library (used by the SDK for input validation)
A minimal MCPWhat is mcp?Model Context Protocol - a standard that lets AI tools connect to external services like databases, issue trackers, or APIs. server
Create src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "my-weather-server",
version: "1.0.0"
});
// Define a tool
server.tool(
"get_weather",
"Get the current weather for a city. Use this when the user asks about weather conditions, temperature, or forecasts for a specific location.",
{
city: {
type: "string",
description: "The city name, e.g. 'Paris' or 'New York'"
}
},
async ({ city }) => {
// In a real server, you would call a weather API here
const weather = {
city,
temperature: 22,
condition: "Partly cloudy",
humidity: 65
};
return {
content: [
{
type: "text",
text: JSON.stringify(weather, null, 2)
}
]
};
}
);
// Connect using stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);Let's break down each part.
The server instance
const server = new McpServer({
name: "my-weather-server",
version: "1.0.0"
});The McpServer class is the core of your server. The name and version identify your server to clients. Keep the name descriptive, it appears in the AI application's server list.
Defining a tool
server.tool(
"get_weather", // Tool name
"Get the current...", // Description (the AI reads this!)
{ city: { ... } }, // Input schema
async ({ city }) => { } // Handler function
);The four arguments to server.tool():
- Name: A short, descriptive identifier. Use
snake_case. The AI uses this name internally. - Description: A natural-language explanation of what the tool does and when to use it. This is the most important part, the AI reads this description to decide whether to call this tool. Be specific.
- Input schemaWhat is schema?A formal definition of the structure your data must follow - which fields exist, what types they have, and which are required.: A JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. Schema object describing the parameters. Each parameter has a
typeanddescription. - Handler: An async function that receives the validated inputs and returns a result.
The input schema
The input schema uses JSON Schema types:
{
city: {
type: "string",
description: "The city name"
},
units: {
type: "string",
description: "Temperature units: 'celsius' or 'fahrenheit'",
enum: ["celsius", "fahrenheit"]
},
include_forecast: {
type: "boolean",
description: "Whether to include a 5-day forecast"
}
}Common types: string, number, boolean, array, object. Use enum to restrict allowed values. Use description on every parameter, the AI reads these to understand what to pass.
| Schema property | Purpose | Example |
|---|---|---|
type | Data type of the parameter | "string", "number", "boolean" |
description | What the AI reads to understand the parameter | "The city name, e.g. 'Paris'" |
enum | Restrict to specific allowed values | ["celsius", "fahrenheit"] |
default | Fallback if not provided | "celsius" |
Returning results
Tools return a content array with typed blocks:
return {
content: [
{
type: "text",
text: "The weather in Paris is 22°C and partly cloudy."
}
]
};For structured data, return JSON as a string:
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2)
}
]
};Error handling
When something goes wrong, return an error result instead of throwing:
server.tool("get_weather", "...", { city: { type: "string" } },
async ({ city }) => {
try {
const data = await fetchWeatherAPI(city);
return {
content: [{ type: "text", text: JSON.stringify(data) }]
};
} catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: `Failed to fetch weather for "${city}": ${error.message}`
}
]
};
}
}
);The isError: true flag tells the AI that the tool call failed, so it can inform the user or try a different approach.
The transport layer
const transport = new StdioServerTransport();
await server.connect(transport);MCP supports two transport types:
- Stdio: Communication via standard input/output. This is the most common for local servers. The host application starts your server as a child process and communicates over stdin/stdout.
- SSEWhat is sse?Server-Sent Events - a one-way, server-to-client push mechanism built on plain HTTP, simpler than WebSockets for server push. (Server-Sent EventsWhat is server-sent events?A one-way, server-to-client push mechanism built on plain HTTP - simpler than WebSockets when clients never need to send data back.): Communication over HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted.. Used for remote servers that run on a different machine.
For local development, always use stdio. It is simpler and does not require networking.
Writing good tool descriptions
The description is what the AI reads to decide when to use your tool. Bad descriptions lead to tools being ignored or misused.
Bad description:
"Gets weather"Too short. The AI does not know when to use it or what it returns.
Good description:
"Get the current weather for a city. Use this when the user asks about
weather conditions, temperature, humidity, or forecasts for a specific
location. Returns temperature in Celsius, condition description, and
humidity percentage."A good description answers three questions:
- What does this tool do?
- When should the AI use it?
- What does it return?
Testing with MCPWhat is mcp?Model Context Protocol - a standard that lets AI tools connect to external services like databases, issue trackers, or APIs. Inspector
Before connecting your server to Claude, test it with the MCP Inspector, a web-based tool for debugging MCP servers:
npx @modelcontextprotocol/inspector node dist/index.jsThis starts a local web UI where you can:
- See all tools, resources, and prompts your server exposes
- Call tools with custom inputs and see the results
- Debug connection issues and inspect the protocolWhat is protocol?An agreed-upon set of rules for how two systems communicate, defining the format of messages and the expected sequence of exchanges. messages
The Inspector is essential for development. Always test here before configuring your server in Claude Desktop or Claude Code.
Adding your server to Claude
Once your server works in the Inspector, add it to your AI application.
For Claude Desktop, add to claude_desktop_config.json:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}For Claude Code:
claude mcp add weather -- node /absolute/path/to/my-mcp-server/dist/index.jsRestart the application. Your tools should now appear in the available tools list, and the AI will use them when relevant.