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: trueflag 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");