DEV Community

MUHAMMAD MUSADIQ
MUHAMMAD MUSADIQ

Posted on

How to Implement Model Context Protocol (MCP) for Custom AI Agent Tooling

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
Enter fullscreen mode Exit fullscreen mode

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"],
        },
      },
    ],
  };
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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 to console.error().
  2. 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.
  3. 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)