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 →
Related Articles
Best Database Tools in 2026
Best Database Tools in 2026
MCP Server Authentication: OAuth 2.1, API Keys, and Security Best Practices
MCP Server Authentication: OAuth 2.1, API Keys, and Security Best Practices
How to authenticate MCP servers — env vars for local, OAuth 2.1 for remote. Covers PKCE, client-credentials, and the CVE that broke mcp-remote.
Best MCP Servers for Marketing Teams: HubSpot, Salesforce, Ahrefs, and More
Best MCP Servers for Marketing Teams: HubSpot, Salesforce, Ahrefs, and More
MCP servers for marketers — CRM, SEO, email, analytics. Setup guides for HubSpot, Salesforce, Ahrefs, and more.