Building tools for AI agents used to feel like reinventing the wheel every week. You wrote custom wrappers, handcrafted JSON schemas, managed token limits, and prayed the model wouldn't hallucinate arguments. Then, Anthropic open-sourced the Model Context Protocol (MCP), giving developers a clean standard for connecting language models to local files, databases, and custom APIs.
In this guide, I will show you how to build and run a custom MCP server from scratch to power your own agent integrations.
At its core, mcp acts like a USB port for LLMs. Instead of hardcoding custom API logic directly into your host application, your MCP server exposes tools, resources, and prompt templates over a standard protocol. The client discovers these tools dynamically.
When a user interacts with the system, the client sends available tool descriptions to the LLM. The LLM decides which tool to execute and returns a structured tool call request. Your MCP server executes the function and returns the result back to the model. This decouples your underlying application logic from your llm deployment strategy.
If you are designing high throughput software pipelines, reading up on modern ai agent development patterns will help you plan your architecture effectively.
Step 1: Set Up Your Project Environment
We will build an MCP server in Node.js using TypeScript and the official MCP SDK.
Create a new directory and initialize the Node package:
mkdir mcp-custom-tools
cd mcp-custom-tools
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
Ensure your tsconfig.json targets a modern Node environment. Setting "target": "ES2022" and "moduleResolution": "NodeNext" works cleanly.
Step 2: Define Your MCP Tools
Create a file named index.ts. We will define a simple tool that simulates looking up a user record and triggering an external api integration.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const server = new Server(
{
name: "user-management-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Define arguments schema using Zod
const GetUserArgsSchema = z.object({
userId: z.string().describe("The unique ID of the user"),
});
// Register tool definitions
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_user_profile",
description: "Fetch user profile details from the internal database by user ID.",
inputSchema: {
type: "object",
properties: {
userId: {
type: "string",
description: "The unique ID of the user",
},
},
required: ["userId"],
},
},
],
};
});
Step 3: Implement Tool Execution Logic
Now, handle the incoming tool calling requests inside the server logic.
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "get_user_profile") {
const { userId } = GetUserArgsSchema.parse(request.params.arguments);
// Mock API call or database query
const userDatabase: Record<string, { name: string; role: string; email: string }> = {
"usr-101": { name: "Sarah Connor", role: "Admin", email: "sarah@example.com" },
"usr-102": { name: "Kyle Reese", role: "Engineer", email: "kyle@example.com" },
};
const user = userDatabase[userId];
if (!user) {
return {
isError: true,
content: [
{
type: "text",
text: `User with ID ${userId} was not found.`,
},
],
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(user, null, 2),
},
],
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server listening on stdio interface");
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});
Step 4: Test Your MCP Server
You can connect your new server directly to Claude Desktop or any client supporting the standard protocol.
Open your local configuration file (for Claude Desktop, this is typically located at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"user-management": {
"command": "npx",
"args": [
"tsx",
"/absolute/path/to/mcp-custom-tools/index.ts"
]
}
}
}
Restart your host client. When you ask the agent "Can you check profile details for usr-101?", the client recognizes the tool definition, formats the JSON-RPC call over standard input/output, and renders the user details in the conversation.
As your tool count grows, combining MCP servers with enterprise workflow automation logic will help you coordinate actions across disparate backends cleanly.
Real Lessons Learned From Building MCP Tools
I ran into a few subtle issues when deploying my first set of custom servers. Keep these tips in mind:
- Never log using standard output. STDIO transport relies strictly on stdout for standard JSON-RPC framing. If you write
console.log()anywhere, you break the parser. Always write logs toconsole.error(). - Keep tool descriptions explicit. Models rely heavily on the description field to determine when to trigger tool calls. Give concrete examples in the text if the model skips your tools.
- Keep individual tool boundaries small. A single tool should perform one specific operation. Do not build mega-tools that try to handle three distinct workflows at once.
Building robust custom infrastructure takes time. If you need dedicated engineers to help build out your agent workflows, Gaper connects companies with vetted software developers specialized in AI engineering.
Top comments (0)