The Claude AI Community Discord is open.Join us

Adding Real Tools

12 min readBuild & Connect Your Server

Three tools that cover the patterns you'll actually use.

Tool 1: Read a local file

import { readFile } from "fs/promises";
import { resolve } from "path";

server.tool(
  "read_file",
  "Read the contents of a file from the local filesystem",
  {
    file_path: z.string().describe("Absolute or relative path to the file"),
  },
  async ({ file_path }) => {
    try {
      const absolutePath = resolve(file_path);
      const content = await readFile(absolutePath, "utf-8");
      return {
        content: [{ type: "text", text: content }],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error reading file: ${error instanceof Error ? error.message : String(error)}`,
          },
        ],
        isError: true,
      };
    }
  }
);

What Claude can now do: "Read my notes.txt file and summarize it."

Two things worth noting here:

  • resolve() converts relative paths to absolute — important for when Claude passes paths
  • The isError: true flag tells Claude something went wrong, so it can handle it gracefully rather than treating an error message as real content

Tool 2: Fetch a URL

server.tool(
  "fetch_url",
  "Fetch the content of a URL and return the response body as text",
  {
    url: z.string().url().describe("The URL to fetch"),
  },
  async ({ url }) => {
    try {
      const response = await fetch(url);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const text = await response.text();
      const trimmed = text.length > 10000 ? text.slice(0, 10000) + "\n...[truncated]" : text;

      return {
        content: [{ type: "text", text: trimmed }],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to fetch URL: ${error instanceof Error ? error.message : String(error)}`,
          },
        ],
        isError: true,
      };
    }
  }
);

What Claude can now do: "What does the homepage of stripe.com say?" — Claude fetches it and tells you.

The 10,000 character trim is intentional. Without it, a single large page can overflow your context window.

Tool 3: List files in a directory

import { readdir } from "fs/promises";

server.tool(
  "list_directory",
  "List all files and folders in a given directory",
  {
    directory_path: z.string().describe("Path to the directory to list"),
  },
  async ({ directory_path }) => {
    try {
      const absolutePath = resolve(directory_path);
      const entries = await readdir(absolutePath, { withFileTypes: true });

      const lines = entries.map((entry) => {
        const type = entry.isDirectory() ? "📁" : "📄";
        return `${type} ${entry.name}`;
      });

      return {
        content: [
          {
            type: "text",
            text: `Contents of ${absolutePath}:\n\n${lines.join("\n")}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error listing directory: ${error instanceof Error ? error.message : String(error)}`,
          },
        ],
        isError: true,
      };
    }
  }
);

What Claude can now do: "What files are in my ~/Documents/projects folder?"

The complete server

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFile, readdir } from "fs/promises";
import { resolve } from "path";
import { z } from "zod";

const server = new McpServer({
  name: "my-first-mcp-server",
  version: "1.0.0",
});

server.tool(
  "say_hello",
  "Returns a greeting for a given name",
  { name: z.string().describe("The name to greet") },
  async ({ name }) => ({
    content: [{ type: "text", text: `Hello, ${name}! MCP is working.` }],
  })
);

server.tool(
  "read_file",
  "Read the contents of a file from the local filesystem",
  { file_path: z.string().describe("Path to the file") },
  async ({ file_path }) => {
    try {
      const content = await readFile(resolve(file_path), "utf-8");
      return { content: [{ type: "text", text: content }] };
    } catch (error) {
      return {
        content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
        isError: true,
      };
    }
  }
);

server.tool(
  "fetch_url",
  "Fetch the content of a URL and return the response body",
  { url: z.string().url().describe("URL to fetch") },
  async ({ url }) => {
    try {
      const res = await fetch(url);
      const text = await res.text();
      const trimmed = text.length > 10000 ? text.slice(0, 10000) + "\n...[truncated]" : text;
      return { content: [{ type: "text", text: trimmed }] };
    } catch (error) {
      return {
        content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
        isError: true,
      };
    }
  }
);

server.tool(
  "list_directory",
  "List all files and folders in a directory",
  { directory_path: z.string().describe("Path to the directory") },
  async ({ directory_path }) => {
    try {
      const entries = await readdir(resolve(directory_path), { withFileTypes: true });
      const lines = entries.map(e => `${e.isDirectory() ? "📁" : "📄"} ${e.name}`);
      return { content: [{ type: "text", text: lines.join("\n") }] };
    } catch (error) {
      return {
        content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
        isError: true,
      };
    }
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running with 4 tools");