Build MCP Server: 7 Easy Steps to Your First Custom Tool
Want to build MCP server tools that connect your AI assistant to any REST API? This step-by-step tutorial walks you through creating a fully functional MCP server — from project setup to OAuth2 authentication and deployment. We'll use a real Zoho Projects integration as our example, but the pattern works for any API.
An MCP server is a lightweight TypeScript process that exposes tools to AI assistants like Claude Code and Cursor. By the end of this guide, you'll know how to build an MCP server with authentication, error handling, and business logic that actually works in production.
What You'll Build
We'll build an MCP server that connects to Zoho Projects — a project management tool with time tracking, tasks, and issue management. The finished server lets you log time, list projects, and manage issues directly from your AI assistant.
The same pattern works for any REST API. If you want to build MCP server integrations for Jira, Asana, HubSpot, QuickBooks, or your company's internal tools — follow these exact steps and swap out the API calls.
Prerequisites
Before you build an MCP server, make sure you have:
- Node.js 18+ and npm installed
- TypeScript (we'll configure it in Step 1)
- Claude Code or another MCP-compatible AI client
- API access to the service you want to connect (we use Zoho Projects)
- Basic familiarity with REST APIs and async/await
Step 1: Scaffold the Project
Create a new directory and initialize your project:
mkdir zoho-projects-mcp
cd zoho-projects-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod dotenv
npm install -D typescript @types/node
npx tsc --init
Key dependencies when you build an MCP server:
@modelcontextprotocol/sdk— official SDK that handles protocol communicationzod— schema validation for tool inputsdotenv— environment variable management for credentials
Configure tsconfig.json with "module": "nodenext" and "outDir": "./dist". Add scripts to package.json:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Step 2: Define Your First Tool
Every MCP server exposes tools — functions the AI can call. To build MCP server tools, you need three things: a name, an input schema, and a handler.
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: "zoho-projects",
version: "1.0.0",
});
server.tool(
"list_projects",
"List active projects in Zoho Projects",
{ search_term: z.string().optional() },
async ({ search_term }) => {
const projects = await fetchProjects(search_term);
return {
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Tool naming matters when you build an MCP server. Use clear, action-oriented names: list_projects, log_time, create_issue. Avoid getProjectsList or timeEntryCreate — the AI understands simple verbs better.
Step 3: Set Up OAuth2 Authentication
Most business APIs use OAuth2. To build an MCP server with Zoho, use their "self-client" flow:
- Go to api-console.zoho.eu and create a Self Client
- Generate a grant token with your required scopes (e.g.,
ZohoProjects.portals.READ,ZohoProjects.timesheets.ALL) - Exchange the grant token for access and refresh tokens
Create a .env file (add .env to .gitignore):
ZOHO_CLIENT_ID=your_client_id
ZOHO_CLIENT_SECRET=your_client_secret
ZOHO_REFRESH_TOKEN=your_refresh_token
ZOHO_PORTAL_ID=your_portal_id
Build MCP server auth logic that refreshes tokens automatically:
import "dotenv/config";
let accessToken: string | null = null;
let tokenExpiry = 0;
export async function getAccessToken(): Promise<string> {
if (accessToken && Date.now() < tokenExpiry) return accessToken;
const response = await fetch("https://accounts.zoho.eu/oauth/v2/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: process.env.ZOHO_CLIENT_ID!,
client_secret: process.env.ZOHO_CLIENT_SECRET!,
refresh_token: process.env.ZOHO_REFRESH_TOKEN!,
}),
});
const data = await response.json();
accessToken = data.access_token;
tokenExpiry = Date.now() + data.expires_in * 1000 - 60_000;
return accessToken!;
}
This auto-refresh pattern works for any OAuth2 API. When you build an MCP server for services like HubSpot or Asana, the only change is the token endpoint URL and required parameters.
Step 4: Handle API Quirks Gracefully
Every API has undocumented behaviors that only surface in production. When you build MCP server integrations, robust error handling is essential.
Common quirks I found with Zoho Projects:
Time format mismatch — Zoho accepts 12-hour AM/PM format but returns 24-hour:
function to12Hour(time24: string): string {
const [hours, minutes] = time24.split(":").map(Number);
const period = hours >= 12 ? "PM" : "AM";
const hours12 = hours % 12 || 12;
return `${String(hours12).padStart(2, "0")}:${String(minutes).padStart(2, "0")} ${period}`;
}
Empty responses — Zoho returns HTTP 204 for empty bug lists instead of an empty array:
async function fetchBugs(projectId: string) {
const response = await zohoFetch(`/projects/${projectId}/bugs/`);
if (response.status === 204) return [];
return (await response.json()).bugs;
}
Build an MCP server that handles edge cases silently. The AI and user should never see raw API errors.
Step 5: Add Business Logic
The difference between a useful server and a basic API wrapper is business logic. When you build MCP server tools, think about rules your team follows that the AI can't infer.
Conflict detection example — check for overlapping time entries before logging:
server.tool(
"log_time",
"Log time against a project, task, or issue",
{
project_id: z.string(),
hours: z.string().describe("Time in HH:MM format"),
date: z.string().describe("YYYY-MM-DD"),
start_time: z.string().optional(),
end_time: z.string().optional(),
notes: z.string().optional(),
},
async (input) => {
if (input.start_time && input.end_time) {
const existing = await getTimeLogs(input.date);
const conflict = findOverlap(existing, input.start_time, input.end_time);
if (conflict) {
return {
content: [{
type: "text",
text: `Conflict: overlapping entry "${conflict.notes}" (${conflict.start}–${conflict.end})`,
}],
};
}
}
const result = await createTimeLog(input);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
Other business logic worth adding: default values based on project type, input validation (reject future time slots), and data enrichment (resolve project names from IDs).
Step 6: Test With Claude Code
Build the MCP server and register it with your AI client:
npm run build
claude mcp add zoho-projects node /path/to/dist/index.js
Or configure it in ~/.claude/mcp.json:
{
"mcpServers": {
"zoho-projects": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": {
"ZOHO_CLIENT_ID": "your_client_id",
"ZOHO_CLIENT_SECRET": "your_client_secret",
"ZOHO_REFRESH_TOKEN": "your_refresh_token",
"ZOHO_PORTAL_ID": "your_portal_id"
}
}
}
}
Start a new Claude Code session and test: "List my active projects in Zoho." If the AI returns real data, your server works. Debug issues by checking stderr — the MCP SDK logs errors there.
Step 7: Package and Share
For personal use, local registration is enough. To build MCP server packages others can use:
- Create
.env.example— document required variables without exposing secrets - Write a clear README — installation steps, required API scopes, configuration
- Remove hardcoded values — portal IDs, organization-specific logic
- Add an MIT license — standard for open source MCP servers
- Publish to npm (optional) —
npm publishfor maximum reach
For team use, push to a private repo. Each teammate creates their own .env with their API credentials.
Build MCP Server Tools for Any API
The pattern from this tutorial works for any REST API:
- Pick 3–5 high-value operations — the actions you repeat most often
- Set up authentication — OAuth2, API keys, or basic auth
- Define tools with clear schemas — Zod for input validation, descriptive names
- Handle API quirks — format mismatches, error codes, empty responses
- Add business logic — defaults, validation, conflict detection
- Test and iterate — tool names and descriptions matter more than you think
The best MCP servers are small, focused, and encode real workflow knowledge. Start simple, ship fast, and add tools as you need them.
The complete source code is available on GitHub. Use it as a starting point to build an MCP server for your own tools.
FAQ
How long does it take to build an MCP server from scratch?
A basic MCP server with 3–5 tools takes 2–4 hours for a developer comfortable with TypeScript and REST APIs. OAuth2 setup adds another hour. Most time goes into understanding the target API’s quirks, not the MCP protocol itself.
What programming language can I use to build an MCP server?
TypeScript has the best SDK support and is the most common choice. Python is also well-supported via the official MCP Python SDK. The protocol is language-agnostic — any language that reads and writes JSON over stdio works.
Do I need cloud hosting to run an MCP server?
No. MCP servers run locally on your machine as a child process of your AI client. No cloud hosting, no open ports, no network exposure. Communication happens via stdio — standard input and output streams.
Can I build an MCP server without OAuth2?
Yes. Many APIs use simpler auth — API keys, bearer tokens, or basic auth. OAuth2 is only needed when the target API requires it. API key auth is simpler: store the key in .env and include it in request headers.
What’s the difference between an MCP server and a REST API wrapper?
An MCP server speaks the Model Context Protocol — a standardized way for AI assistants to discover and call tools. A REST wrapper is a generic library. The key difference: MCP servers include tool descriptions that help the AI understand when and how to use each function, making the interaction natural rather than programmatic.
Indie maker and developer. Building productivity tools and writing about systems, automation, and the craft of focused work.
Want a custom Notion template?
Browse my ready-made tools or get in touch for a custom build.