How to Build an MCP Server from Scratch (TypeScript Tutorial)
Step-by-step tutorial to build a working MCP server in TypeScript. From zero to a published npm package that works with Claude, Cursor, and any MCP client.
We built the Toolradar MCP server in a day. It gives AI agents access to 8,400+ software tools. Here is exactly how we did it, and how you can build one for your own product.
What You'll Build
A working MCP server that:
- Exposes tools callable by Claude, Cursor, Windsurf, or any MCP client
- Runs locally via
npx - Talks to your API over HTTP
- Publishes to npm
Prerequisites
- Node.js 18+
- TypeScript
- An API your server will wrap (if you don't have one, we'll use a mock)
Step 1: Scaffold the Project
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
Set "type": "module" in your package.json.
Step 2: Define Your Tools
Tools are the capabilities your server exposes. Each tool has a name, a description (the AI reads this to decide when to call it), and an input schema.
Create src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
Now add a tool. Say you have a weather API:
server.tool(
"get_weather",
"Get current weather for a city. Returns temperature, conditions, and humidity.",
{
city: z.string().describe("City name, e.g. 'Paris' or 'New York'"),
},
async ({ city }) => {
// Call your API
const res = await fetch(
`https://api.example.com/weather?city=${encodeURIComponent(city)}`
);
const data = await res.json();
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2),
}],
};
}
);
Key decisions:
- The description matters. The AI reads it to decide when to call your tool. "Get current weather for a city" is good. "Weather function" is not, the AI won't know when to use it.
- The schema matters. Use Zod's
.describe()on every parameter. The AI uses these descriptions to construct valid inputs. - Return structured JSON. The AI parses your response. Structured data (JSON) works better than prose.
Step 3: Connect the Transport
MCP servers communicate via stdio (standard input/output). The host application (Claude, Cursor) spawns your server as a subprocess and sends/receives JSON-RPC messages through stdin/stdout.
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error("Server failed:", error);
process.exit(1);
});
That's the entire server. Build it:
npx tsc
Step 4: Create the Bin Entry
Create bin/my-server.js:
#!/usr/bin/env node
import "../dist/index.js";
Add to package.json:
{
"bin": {
"my-mcp-server": "./bin/my-server.js"
},
"files": ["dist", "bin", "README.md"]
}
Step 5: Test Locally
With Claude Desktop, add to claude_desktop_config.json:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"],
"env": {
"API_KEY": "your_key"
}
}
}
}
Restart Claude Desktop. Ask: "What's the weather in Paris?" If your server is connected, Claude will call get_weather automatically.
Step 6: Handle Errors Gracefully
The AI needs to know when something fails. Return isError: true:
server.tool(
"get_weather",
"Get current weather for a city.",
{ city: z.string().describe("City name") },
async ({ city }) => {
try {
const res = await fetch(`https://api.example.com/weather?city=${encodeURIComponent(city)}`, {
signal: AbortSignal.timeout(10000), // 10s timeout
});
if (!res.ok) {
return {
content: [{ type: "text", text: `API error: ${res.status}` }],
isError: true,
};
}
const data = await res.json();
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
} catch (e) {
return {
content: [{ type: "text", text: `Error: ${(e as Error).message}` }],
isError: true,
};
}
}
);
Always add a timeout. If your API hangs, the MCP client hangs, and the user stares at a spinner forever.
Step 7: Publish to npm
npm login
npm publish
Now anyone can use your server:
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["-y", "my-mcp-server"],
"env": { "API_KEY": "..." }
}
}
}
Real-World Example: How We Built Toolradar MCP
Our MCP server wraps the Toolradar REST API with 6 tools. The architecture:
Claude/Cursor → toolradar-mcp (stdio) → toolradar.com/api/v1/* (HTTP)
The MCP server is a thin HTTP client. Each tool maps 1:1 to an API endpoint:
| MCP Tool | REST Endpoint |
|---|---|
search_tools | GET /api/v1/search |
get_tool | GET /api/v1/tools/:slug |
compare_tools | GET /api/v1/compare |
get_alternatives | GET /api/v1/alternatives/:slug |
get_pricing | GET /api/v1/pricing/:slug |
list_categories | GET /api/v1/categories |
The HTTP client is ~80 lines. The tool definitions are ~100 lines. Total server: under 200 lines of TypeScript. The real complexity lives in the API, not the MCP server.
Full source: github.com/Nadeus/toolradar-mcp
Common Pitfalls
1. Vague tool descriptions. The AI uses your description to decide when to call the tool. "Data function" tells it nothing. "Search software tools by keyword, category, and pricing model" tells it exactly when to use it.
2. Missing parameter descriptions. city: z.string() is worse than city: z.string().describe("City name, e.g. 'Paris'"). The AI uses the description to construct valid inputs.
3. No timeout on HTTP calls. If your API is slow, the entire MCP client freezes. Always use AbortSignal.timeout().
4. Giant responses. Don't return 100KB of JSON. The AI's context window is finite. Return the 10 most relevant results, not all 500.
5. No error handling. A thrown exception kills the MCP server process. Catch everything, return isError: true.
6. Forgetting zod as an explicit dependency. The MCP SDK uses zod internally, but relying on transitive dependencies breaks when package managers hoist differently. Always list zod in your dependencies.
Security Checklist
Before publishing, review these. 30+ CVEs were filed against MCP servers in January-February 2026 alone. Common vulnerabilities:
- Shell injection (43% of CVEs). Never pass user-controlled input to
exec()or shell commands. Use parameterized calls. - Path traversal. If your server reads files, validate paths against an allow-list. Don't trust
../handling to the OS. - Env var leakage. Never include
process.envin tool responses. Only return data the user asked for. - No auth on resources. If your server exposes resources (not just tools), ensure they are scoped. A resource listing
/is a security hole.
Read the full guide: MCP Server Security →
Testing
Test your server locally before publishing:
# Run it directly to check for startup errors
npx tsx src/index.ts
# Test with MCP Inspector (Anthropic's debugging tool)
npx @modelcontextprotocol/inspector npx tsx src/index.ts
# Add to Claude Code for a real-world test
claude mcp add my-server -- npx tsx src/index.ts
Verify: (1) all tools appear in the client, (2) tool descriptions are clear enough that the AI calls the right tool, (3) error cases return isError: true instead of crashing.
What's Next
You have a working MCP server. To take it further:
- Add a remote HTTP endpoint, Streamable HTTP (spec 2025-03-26) lets hosted clients use your server without local install. Cloudflare Workers and Vercel both support it.
- Publish to registries, Smithery, mcp.so, Glama for discovery. Smithery alone has 6,000+ servers.
- Create a Desktop Extension (.mcpb), package your server as a one-click installable for Claude Desktop. MCPB spec.
- Create a Claude Code skill, a
.mdfile that tells Claude how to use your tools for specific workflows. Example.
Want to try an MCP server without building one? Install Toolradar MCP in 2 minutes →
Already have an API? The hardest part is writing good tool descriptions. See how we wrote ours →
Deploy your server remotely: How to Deploy a Remote MCP Server →
From working to shipped: what the basic tutorial leaves out
The server above works on your machine. Three more steps separate that from something users install, and they are where we spent most of the effort on our own server (the full account: lessons from building the Toolradar MCP).
Remote beats local for real users. Local stdio servers are perfect for development and a support burden for everyone else: every user installs your runtime, your dependencies, your updates. A remote server updates once, serves everyone, and unlocks the clients that only speak HTTP. The full walkthrough is in deploying a remote MCP server.
Auth is where servers get real. The moment your server touches user-specific data you need OAuth 2.1 or scoped keys, and the protocol's auth story has sharp edges we documented in MCP authentication. Design scopes before the first user, not after the first incident: read-only by default is the pattern the best finance servers ship, and it is right for most domains.
Description quality is retrieval quality. The strings in your tool definitions are what the model reads when deciding which tool to call. Vague descriptions produce wrong tool calls that look like model failures. Write them like documentation for a new colleague, test them by asking an assistant to do the task cold, and iterate on the words before touching the code.
The ecosystem context, if you are deciding whether to bother: 424 servers tracked, 83% vendor-official. The window where an unofficial server represents a major product is closing; the window where YOUR product is expected to have one is open.
From the team behind Toolradar
Show up when someone asks ChatGPT
ChatGPT, Claude, and Perplexity already retrieve Toolradar. We run the content and the 6 newsletters that keep you in those answers.
See how we work- mcp
- tutorial
- typescript
- developer-tools
- how-to
- model-context-protocol

Written by
Louis Corneloup
Related Articles
IDE and code editor statistics 2026
First-party data on the 125 IDEs and code editors Toolradar tracks: 90% free tier, the highest in this wave, and Turbopack at 3.00 from 4,912 reviews against Notepad++ at 4.60.
API tools statistics 2026
First-party data on the 462 API tools Toolradar tracks: 4.4% of the catalog, 74% free tier, and only 71,974 reviews. APIs are used more than they are rated.
Developer tools statistics 2026
First-party data on the 1,746 developer tools Toolradar tracks: 16.4% of the entire catalog, and 81% still give you a free way in. This is the largest unpaid category we publish.