Skip to Content

Solution: Build a Server Step by Step

Build a Real MCP Server

The CSV Data Reader — from hello world to practical tools

The hello world greet tool from the previous lesson proves your setup works. But let us build something actually useful — an MCP server that lets Claude read, search, and analyze data from CSV/JSON files. This is a common business need: you have spreadsheets with customer data, sales reports, or inventory lists, and you want Claude to work with them.

The Plan

search_data Tool

Search records by any field. Claude can ask for customers in Prague or orders above a certain amount.

get_stats Tool

Get summary statistics — count, averages, min, max. Claude can give you quick data insights.

data://schema Resource

Expose the data structure so Claude knows what fields exist before querying.

TypeScript Implementation

data-reader/src/index.ts (excerpt)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFileSync } from "fs";

const server = new McpServer({
  name: "data-reader",
  version: "1.0.0",
});

const DATA_PATH = process.env.DATA_PATH || "./data.json";
let records: Record<string, any>[] = [];
try {
  records = JSON.parse(readFileSync(DATA_PATH, "utf-8"));
} catch (e) {
  console.error(`Could not load ${DATA_PATH}`);
}

server.tool(
  "search_data",
  "Search records by field name and value",
  {
    field: z.string().describe("Field name to search"),
    value: z.string().describe("Value to search for"),
    limit: z.number().optional().describe("Max results"),
  },
  async ({ field, value, limit = 10 }) => ({
    content: [{
      type: "text",
      text: JSON.stringify(
        records.filter(r =>
          String(r[field] || "").toLowerCase()
            .includes(value.toLowerCase())
        ).slice(0, limit), null, 2),
    }],
  })
);

Python Equivalent

data-reader/server.py
import json, os
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("data-reader")

DATA_PATH = os.environ.get("DATA_PATH", "./data.json")
try:
    with open(DATA_PATH) as f:
        records = json.load(f)
except Exception:
    records = []

@mcp.tool()
def search_data(field: str, value: str,
                limit: int = 10) -> str:
    """Search records by field name and value."""
    results = [
        r for r in records
        if value.lower() in str(r.get(field, "")).lower()
    ][:limit]
    return json.dumps(results, indent=2,
                      ensure_ascii=False)

@mcp.tool()
def get_stats(numeric_field: str = "") -> str:
    """Get summary statistics about the data."""
    stats = {"total_records": len(records),
             "fields": list(records[0].keys())
                       if records else []}
    if numeric_field and records:
        values = [float(r[numeric_field])
                  for r in records
                  if numeric_field in r]
        if values:
            stats.update({"sum": sum(values),
                          "avg": sum(values)/len(values),
                          "min": min(values),
                          "max": max(values)})
    return json.dumps(stats, indent=2)

Understanding the Zod Schema

Why Schemas Matter

In TypeScript, Zod defines the input schema for each tool. It tells Claude what parameters the tool accepts, provides descriptions that help Claude decide how to use the tool, and validates input automatically. In Python, FastMCP achieves the same through type hints and docstrings.

Connecting to Claude Code

The key step — telling Claude Code about your server. Create or edit .mcp.json in your project root:

.mcp.json
{
  "mcpServers": {
    "data-reader": {
      "command": "node",
      "args": ["dist/index.js"],
      "env": {
        "DATA_PATH": "./customers.json"
      }
    }
  }
}

When Claude Code starts, it reads this file, launches your server process, and makes its tools available. You can then ask Claude: "Search my customer data for anyone from Prague" — and Claude will call your search_data tool automatically.

Testing with the Inspector

1

See All Tools

The inspector shows all registered tools and their schemas in a web UI.

2

Call Manually

Test tools with custom parameters to verify they work before connecting to Claude.

3

View Resources

Browse resources and check their contents are formatted correctly.

4

Check for Errors

Catch any issues in your server before they cause confusion in a conversation.

Adding to an Existing Plugin

Plugin Structure
my-plugin/
├── plugin.json
├── skills/
│   └── my-skill/
│       └── SKILL.md
├── commands/
│   └── my-command.md
├── .mcp.json              # Your MCP server config
└── mcp-servers/
    └── data-reader/       # Your MCP server code
        ├── src/index.ts
        ├── package.json
        └── tsconfig.json

Seamless Integration

When users install your plugin, the .mcp.json automatically configures Claude to start your MCP server. The skill can reference MCP tools in its instructions, creating a seamless experience.

Key Takeaway

You now have a working MCP server with real tools that Claude can use. The pattern is always the same: define a tool with a name, description, and schema — implement the handler — register it with the server. In the next section, we will connect to real business systems.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.