Build Your First MCP Server in 15 Minutes
March 3, 2026 · 4 min read
Why Build a Custom MCP Server?
The MCP registry has thousands of pre-built servers for popular services. But your organization has internal tools, proprietary APIs, and custom workflows that no public server covers.
Building a custom MCP server lets you give AI agents controlled access to your internal systems — your deployment pipeline, your monitoring dashboards, your internal knowledge base — through a standardized protocol that works with every major AI tool.
What You Will Build
A simple MCP server that provides two tools:
- get_status — Returns the current status of a service
- list_incidents — Lists recent incidents
This is a pattern you can adapt for any internal API.
Prerequisites
- Node.js 18+
- TypeScript
- Basic familiarity with AI agents and MCP concepts (intro here)
Step 1: Set Up the Project
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init
Step 2: Define Your Server
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-status-server",
version: "1.0.0",
});
// Define the get_status tool
server.tool(
"get_status",
"Get the current status of a service",
{
service: z.string().describe("The service name to check"),
},
async ({ service }) => {
// Replace this with your actual API call
const statuses: Record<string, string> = {
api: "operational",
database: "operational",
worker: "degraded",
};
const status = statuses[service] || "unknown";
return {
content: [
{
type: "text",
text: `Service "${service}" is currently: ${status}`,
},
],
};
}
);
// Define the list_incidents tool
server.tool(
"list_incidents",
"List recent incidents",
{
limit: z.number().optional().default(5).describe("Number of incidents to return"),
},
async ({ limit }) => {
// Replace with your actual incident data source
const incidents = [
{ id: 1, title: "Worker queue backlog", status: "investigating", time: "2 hours ago" },
{ id: 2, title: "API latency spike", status: "resolved", time: "1 day ago" },
{ id: 3, title: "Database failover", status: "resolved", time: "3 days ago" },
];
const result = incidents.slice(0, limit);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
Step 3: Build and Test
Add to your package.json:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Update tsconfig.json to output to dist/:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}
Build it:
npm run build
Step 4: Connect to Claude Desktop
Add your server to ~/.claude/claude_desktop_config.json:
{
"mcpServers": {
"my-status": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}
Restart Claude Desktop. You should see "my-status" in the tools list. Try asking: "What is the status of the worker service?" or "Show me recent incidents."
Step 5: Connect to Your Real Systems
Replace the hardcoded data with actual API calls:
server.tool(
"get_status",
"Get the current status of a service",
{
service: z.string().describe("The service name to check"),
},
async ({ service }) => {
const response = await fetch(
`https://your-internal-api.com/status/${service}`,
{
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
}
);
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
Pass credentials through environment variables in your MCP configuration:
{
"mcpServers": {
"my-status": {
"command": "node",
"args": ["/path/to/dist/index.js"],
"env": {
"API_TOKEN": "your-secret-token"
}
}
}
}
Security Considerations
When building servers for internal use:
- Validate all inputs with Zod schemas. Never trust agent-provided parameters.
- Use read-only access unless write operations are explicitly required.
- Scope credentials to the minimum permissions needed.
- Log tool invocations for audit purposes, especially for write operations.
- Never return sensitive data (API keys, passwords, PII) in tool responses.
What's Next
Once your server works locally, consider:
- Publishing to npm for team-wide access
- Adding more tools for other internal systems
- Submitting to the VaultPlane registry so others can discover it
- Reading the MCP SDK documentation for advanced patterns like resources and prompts