---
title: "Build Your First AI Tool: The `readFile` MCP Utility Explained"
published: true
tags: ["ai", "typescript", "expressjs"]
canonical_url: https://nicolas-dabene.fr/en/articles/2025/11/12/creer-votre-premier-outil-mcp-l-outil-readfile-explique/
---
Hey there, fellow developers! Nicolas Dabène here, and I'm thrilled to guide you through a truly exciting milestone today. If you’ve been following my previous thoughts on AI and the Model Context Protocol (MCP), or even if you're just diving in, you know the theory can be fascinating. But nothing beats the thrill of seeing that theory manifest as working code. Today, we're going to experience that "Aha!" moment together: building your very first MCP tool, a simple yet powerful function that empowers an AI to read files directly fro...
---
title: "Build Your First AI Tool: The `readFile` MCP Utility Explained"
published: true
tags: ["ai", "typescript", "expressjs"]
canonical_url: https://nicolas-dabene.fr/en/articles/2025/11/12/creer-votre-premier-outil-mcp-l-outil-readfile-explique/
---
Hey there, fellow developers! Nicolas Dabène here, and I'm thrilled to guide you through a truly exciting milestone today. If you’ve been following my previous thoughts on AI and the Model Context Protocol (MCP), or even if you're just diving in, you know the theory can be fascinating. But nothing beats the thrill of seeing that theory manifest as working code. Today, we're going to experience that "Aha!" moment together: building your very first MCP tool, a simple yet powerful function that empowers an AI to read files directly from your machine. It’s practical, concrete, and trust me – it feels like magic when it comes to life.
## Bringing AI to Life with Custom Tools
Throughout my career, I've always cherished those instances where lines of code transform into something tangible and functional. Remember that feeling when your application performs exactly as you envisioned? That's the essence of what we'll achieve today. Having covered the foundational concepts and environment setup, it's time to construct something truly interactive: an MCP utility for reading files.
Imagine giving an AI a prompt: "Please read the `report.txt` file." And it *actually* does it, leveraging your server to access local data. This isn't just a concept anymore; your code will make this capability a reality. The best part? Once you've mastered the creation of one such tool, you'll possess the blueprint to craft countless others, opening up a universe of possibilities for AI interaction.
## Quick Recap: What Exactly is an MCP Tool?
Before we dive into the implementation, let's quickly re-anchor our understanding of an MCP tool. Fundamentally, it's a specialized function that you expose to an AI. This exposure comes with three critical pieces of information, acting like its public API documentation for the AI:
* **The Tool Name**: The unique identifier the AI uses to invoke your tool (e.g., `"readFile"`).
* **The Description**: A clear, concise explanation of what the tool does, enabling the AI to determine when its use is appropriate.
* **The Parameters**: A structured definition of the inputs the tool requires to perform its operation.
Think of it as creating a regular function in your codebase, but with a semantic "ID card" that an intelligent agent can interpret and utilize. Pretty straightforward, right?
## Deconstructing an MCP Tool's Structure
To give you a clearer picture, let's lay out the fundamental anatomy of an MCP tool. This is the structural blueprint we'll follow:
typescript // 1. Interface for input parameters interface ToolParams { // Data the AI sends us }
// 2. Interface for response interface ToolResponse { success: boolean; content?: string; error?: string; }
// 3. The core function that executes the logic async function myTool(params: ToolParams): Promise { // Business logic here }
// 4. The tool’s public definition (its “manifest” for the AI) export const myToolDefinition = { name: “myTool”, description: “What my tool does”, parameters: { // Description of expected parameters } };
This four-part structure serves as your reliable template for building any MCP tool. Keep it in mind as we proceed!
## Setting Up Our Project Structure
Let’s begin by establishing a clean, organized directory structure for our `mcp-server` project. Execute the following commands in your terminal:
bash mkdir -p src/tools mkdir -p src/types
This clear organization enhances maintainability. The `src/tools` directory will house our MCP utilities, while `src/types` will store our reusable TypeScript interface definitions.
## Defining Essential TypeScript Types
Our first code contribution will be to define the necessary TypeScript interfaces. Create a new file at `src/types/mcp.ts`:
typescript // src/types/mcp.ts
// A generic type for all tool parameters, allowing flexible input structures. export interface ToolParams {
}
// The standardized structure for a tool’s response. // Includes success status, optional content, and error details. export interface ToolResponse { success: boolean; content?: string; error?: string; metadata?: { key: string: any; }; }
// The interface defining how a tool is described to the AI. // This is the “menu” item the AI sees. export interface ToolDefinition { name: string; description: string; parameters: { [paramName: string]: { type: string; description: string; required: boolean; }; }; }
// Specific parameters for our ‘readFile’ tool. // It extends ToolParams to inherit flexibility while defining its specific needs. export interface ReadFileParams extends ToolParams { file_path: string; encoding?: ‘utf-8’ | ‘ascii’ | ‘base64’; // Added later in “Improve the Tool” section }
These type definitions are invaluable. They provide auto-completion, catch potential errors early, and make our code much more robust and understandable. TypeScript truly shines in projects like this!
## Crafting Our `readFile` Tool
The moment you've been anticipating is here: let's build our first tool! Create the file `src/tools/readFile.ts`:
typescript // src/tools/readFile.ts import fs from ‘fs/promises’; import path from ‘path’; import { ReadFileParams, ToolResponse, ToolDefinition } from ‘../types/mcp’;
/**
- Reads the content of a specified text file from the local file system.
- Implements crucial security and validation checks.
- @param params - An object containing the ‘file_path’ and optional ‘encoding’.
@returns A Promise resolving to a ToolResponse object with the file content or an error. */ export async function readFile(params: ReadFileParams): Promise { try { // 1. Parameter Validation: Always the first line of defense. // We ensure the ‘file_path’ parameter, critical for this tool, is provided. if (!params.file_path) { return { success: false, error: “The ‘file_path’ parameter is required” }; }
// 2. Security - Resolving Absolute Path: Prevents directory traversal attacks.
// By resolving to an absolute path, we ensure the AI cannot request files
// outside an intended scope using relative paths like ../../etc/passwd.
const absolutePath = path.resolve(params.file_path);
// 3. File Existence Check: Before attempting to read, confirm the file exists.
try {
await fs.access(absolutePath);
} catch {
return {
success: false,
error: File not found: ${params.file_path}
};
}
// 4. Retrieve File Information: Get stats to perform further checks. const stats = await fs.stat(absolutePath);
// 5. Type Verification: Ensure the path points to a file, not a directory. if (!stats.isFile()) { return { success: false, error: “The specified path is not a file” }; }
// 6. Size Limitation (Security): Prevent reading excessively large files into memory.
// This safeguards against resource exhaustion or denial-of-service attempts.
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
if (stats.size > MAX_FILE_SIZE) {
return {
success: false,
error: File too large (max ${MAX_FILE_SIZE / (1024 * 1024)} MB)
};
}
// 7. Read File Content: Finally, read the file using the specified or default encoding. const encoding = params.encoding || ‘utf-8’; const content = await fs.readFile(absolutePath, encoding);
// 8. Return Success with Metadata: Provide not just the content, but also useful contextual info. return { success: true, content: content, metadata: { path: absolutePath, size: stats.size, encoding: encoding, lastModified: stats.mtime } };
} catch (error: any) {
// 9. Robust Error Handling: Catch and report any unexpected issues during the process.
return {
success: false,
error: Read error: ${error.message}
};
}
}
/**
- The MCP tool definition for ‘readFile’.
- This object is the “contract” that the AI uses to understand and call our tool. */ export const readFileToolDefinition: ToolDefinition = { name: “readFile”, description: “Reads the content of a text file from the local file system.”, parameters: { file_path: { type: “string”, description: “Absolute or relative path to the file to read.”, required: true }, encoding: { type: “string”, description: “Optional. Specifies the file encoding (e.g., ‘utf-8’, ‘ascii’, ‘base64’). Defaults to ‘utf-8’.”, required: false } } };
Let's quickly break down the crucial aspects of this code. Each numbered step highlights a vital practice:
* **Validation**: We always start by confirming that all necessary parameters are present and correctly formatted. Never trust input!
* **Security (Path Resolution)**: Crucially, we use `path.resolve()` to ensure that paths like `../../etc/passwd` don't lead to unauthorized access. This locks the AI into a safe, absolute path.
* **Existence & Type Checks**: We verify that the target exists and is indeed a file, not a directory, preventing unexpected errors.
* **Size Limits**: A fundamental security measure to avoid memory exhaustion from accidentally trying to load a massive file.
* **Reading**: The actual file content retrieval, now with flexible encoding options.
* **Enriched Response**: Beyond just the file's content, we provide valuable metadata for the AI's context.
* **Error Handling**: Comprehensive `try-catch` blocks ensure graceful failure reporting for any unforeseen issues.
## Building a Central Tool Manager
To keep our system organized and scalable, let's create a central file to manage all our MCP tools. This will act as a registry. Create `src/tools/index.ts`:
typescript // src/tools/index.ts import { ToolDefinition, ToolResponse } from ‘../types/mcp’; import { readFile, readFileToolDefinition } from ‘./readFile’; // Import other tools here as they are created
// A mapping of tool names to their execution functions. export const tools = { readFile, // Add other tool functions here (e.g., listFiles) };
// An array containing the definitions of all our tools. // This is the “menu” the AI will request to understand available capabilities. export const toolDefinitions: ToolDefinition[] = [ readFileToolDefinition, // Add other tool definitions here (e.g., listFilesToolDefinition) ];
/**
- Executes a tool by its registered name, passing along its parameters.
- Includes basic validation to ensure the tool exists.
- @param toolName - The name of the tool to execute.
- @param params - The parameters to pass to the tool.
- @returns A Promise resolving to the ToolResponse from the executed tool. */ export async function executeTool(toolName: string, params: any): Promise { const tool = tools[toolName as keyof typeof tools];
if (!tool) {
return {
success: false,
error: Tool '${toolName}' not found.
};
}
return await tool(params); }
This `index.ts` file acts as our central hub. As you develop more tools, you'll simply register their function and definition here, making them discoverable and executable.
## Integrating with Our Express Server
Now, let's modify our `src/index.ts` file to expose these new tools through dedicated HTTP endpoints. This is how external AI systems will communicate with our MCP server.
typescript // src/index.ts import express, { Request, Response } from ‘express’; import { toolDefinitions, executeTool } from ‘./tools’; // Our tool manager
const app = express(); const PORT = 3000;
// Middleware to parse incoming JSON payloads in request bodies. app.use(express.json());
// A simple health check or root route. app.get(‘/’, (req: Request, res: Response) => { res.json({ message: ‘MCP Server operational!’, version: ‘1.0.0’, description: ‘Serving AI-powered local system tools.’ }); });
// Endpoint for AI to discover all available tools (the “menu”). app.get(‘/tools’, (req: Request, res: Response) => { res.json({ success: true, tools: toolDefinitions }); });
// Endpoint for AI to execute a specific tool by name. app.post(‘/tools/:toolName’, async (req: Request, res: Response) => { const { toolName } = req.params; const params = req.body;
try {
const result = await executeTool(toolName, params);
res.json(result);
} catch (error: any) {
// Catch any uncaught errors during tool execution.
res.status(500).json({
success: false,
error: Server error during tool execution: ${error.message}
});
}
});
app.listen(PORT, () => {
console.log(✅ MCP Server launched on http://localhost:${PORT});
console.log(📋 Discover tools at: http://localhost:${PORT}/tools);
});
Our Express server now provides two critical API endpoints:
* `GET /tools`: This route allows an AI to fetch a list of all available tools and their definitions, much like browsing a menu.
* `POST /tools/:toolName`: This route enables an AI to trigger a specific tool's execution by its name, passing the necessary parameters in the request body.
## Time to Test Our Creation!
The moment of truth has arrived! Let's verify that our newly minted `readFile` tool works as expected.
First, create a sample text file in your project's root directory:
bash echo “This is a test file for MCP’s readFile tool!” > test.txt
Now, launch your MCP server. If you set up your `package.json` with a `dev` script, you can simply run:
bash npm run dev
You should see output similar to this, confirming your server is running:
✅ MCP Server launched on http://localhost:3000 📋 Discover tools at: http://localhost:3000/tools
### Test 1: Discovering Our Tools
Open a new terminal window and use `curl` to query the `/tools` endpoint:
bash curl http://localhost:3000/tools
The expected response should look like this, listing our `readFile` tool:
json { “success”: true, “tools”: [ { “name”: “readFile”, “description”: “Reads the content of a text file from the local file system.”, “parameters”: { “file_path”: { “type”: “string”, “description”: “Absolute or relative path to the file to read.”, “required”: true }, “encoding”: { “type”: “string”, “description”: “Optional. Specifies the file encoding (e.g., ‘utf-8’, ‘ascii’, ‘base64’). Defaults to ‘utf-8’.”, “required”: false } } } ] }
Excellent! Our server correctly exposes the `readFile` tool's definition to potential AI clients.
### Test 2: Using the `readFile` Tool
Now, let's invoke the `readFile` tool to read the `test.txt` file we created earlier:
bash
curl -X POST http://localhost:3000/tools/readFile
-H “Content-Type: application/json”
-d ‘{“file_path”: “test.txt”}’
You should receive a response similar to this (paths and timestamps will vary):
json { “success”: true, “content”: “This is a test file for MCP’s readFile tool!\n”, “metadata”: { “path”: “/your/project/root/test.txt”, “size”: 42, “encoding”: “utf-8”, “lastModified”: “2025-11-12T10:30:00.000Z” } }
**It works!** Your MCP server has successfully read a local file at the AI's request. This is a huge step!
### Test 3: Validating Error Handling
Robust error handling is paramount. Let's test our tool with a file that doesn't exist:
bash
curl -X POST http://localhost:3000/tools/readFile
-H “Content-Type: application/json”
-d ‘{“file_path”: “nonexistent_file.txt”}’
The expected error response:
json { “success”: false, “error”: “File not found: nonexistent_file.txt” }
Perfect! Our tool correctly identifies and reports missing files.
## Expanding Capabilities: Creating a `listFiles` Tool
Since you've now mastered building a tool, let's quickly add another one to our arsenal: `listFiles`. This will allow the AI to explore directories.
Create `src/tools/listFiles.ts`:
typescript // src/tools/listFiles.ts import fs from ‘fs/promises’; import path from ‘path’; import { ToolParams, ToolResponse, ToolDefinition } from ‘../types/mcp’;
// Parameters for the ‘listFiles’ tool. export interface ListFilesParams extends ToolParams { directory_path: string; }
/**
- Lists the contents (files and subdirectories) of a specified directory.
- Includes checks to ensure the path is a valid directory.
- @param params - An object containing the ‘directory_path’.
@returns A Promise resolving to a ToolResponse with directory contents or an error. */ export async function listFiles(params: ListFilesParams): Promise { try { // Parameter validation if (!params.directory_path) { return { success: false, error: “The ‘directory_path’ parameter is required.” }; }
const absolutePath = path.resolve(params.directory_path);
// Verify that the path points to a directory const stats = await fs.stat(absolutePath); if (!stats.isDirectory()) { return { success: false, error: “The specified path is not a directory.” }; }
// Read the directory contents (names only) const filesAndDirs = await fs.readdir(absolutePath);
// For each item, get detailed statistics const detailedContents = await Promise.all( filesAndDirs.map(async (item) => { const itemPath = path.join(absolutePath, item); const itemStats = await fs.stat(itemPath);
return {
name: item,
type: itemStats.isDirectory() ? 'directory' : 'file',
size: itemStats.size,
lastModified: itemStats.mtime
};
}) );
return { success: true, content: JSON.stringify(detailedContents, null, 2), // Pretty print for readability metadata: { path: absolutePath, count: detailedContents.length } };
} catch (error: any) {
return {
success: false,
error: Error listing directory contents: ${error.message}
};
}
}
/**
- The MCP tool definition for ‘listFiles’.
- This informs the AI about the ‘listFiles’ tool’s purpose and requirements. */ export const listFilesToolDefinition: ToolDefinition = { name: “listFiles”, description: “Lists files and subdirectories within a specified directory.”, parameters: { directory_path: { type: “string”, description: “Absolute or relative path to the directory to list.”, required: true } } };
Now, let's integrate this new tool into our central manager. Update `src/tools/index.ts`:
typescript // src/tools/index.ts import { ToolDefinition, ToolResponse } from ‘../types/mcp’; import { readFile, readFileToolDefinition } from ‘./readFile’; import { listFiles, listFilesToolDefinition } from ‘./listFiles’; // Import our new tool
// Registry of all our tool functions. export const tools = { readFile, listFiles // Add listFiles here };
// Definitions of all our tools (the “menu” for the AI). export const toolDefinitions: ToolDefinition[] = [ readFileToolDefinition, listFilesToolDefinition // Add listFilesToolDefinition here ];
// ... (executeTool function remains the same)
Restart your server (`npm run dev`) and test the discovery endpoint again:
bash curl http://localhost:3000/tools
You'll now observe both `readFile` and `listFiles` proudly listed as available tools!
## Essential Best Practices and Security Considerations
As you build more powerful tools that interact with your system, security becomes paramount. Here are critical best practices to embed in every MCP tool you create:
### Always Sanitize and Validate Inputs
Never implicitly trust any data received from the AI (or any external source). Rigorously validate every parameter: check its type, format, length, and against a list of allowed values. This prevents injection attacks and unexpected behavior.
### Strictly Limit File System Access
Implement a robust whitelist of allowed directories. This strategy ensures your AI tools can only operate within designated, safe areas, preventing them from accessing sensitive system files.
typescript const ALLOWED_DIRECTORIES = [ path.resolve(‘/home/user/documents’), // Example: User’s documents path.resolve(‘/var/www/myproject/data’) // Example: Project-specific data ];
function isPathAllowed(filePath: string): boolean { const absolute = path.resolve(filePath); // Check if the resolved path starts with any of the allowed directories. return ALLOWED_DIRECTORIES.some(dir => absolute.startsWith(dir)); }
// Integrate this into your tools: // if (!isPathAllowed(params.file_path)) { // return { success: false, error: “Access to this path is not permitted.” }; // }
### Enforce Size and Depth Limits
Prevent resource exhaustion. Always define maximum file sizes for reads, maximum number of items for lists, and limit recursion depth for operations that traverse directories. This safeguards your server's stability.
### Implement Comprehensive Logging
Maintain detailed logs of all tool executions, including the tool name, parameters received, and the outcome (success/failure). This is crucial for auditing, debugging, and identifying potential misuse.
typescript
// Example within a tool function:
console.log([${new Date().toISOString()}] Tool Executed: ${toolName}, Params: ${JSON.stringify(params)});
## Conclusion: Your AI is Now Smarter!
Congratulations, trailblazer! You've successfully built and integrated your very first functional MCP tools. You've walked through the entire process, learning to:
* Structure powerful MCP tools with TypeScript.
* Manage input parameters and craft informative responses.
* Implement critical input validation and robust error handling.
* Expose your custom utilities via a clean REST API.
* Thoroughly test your tools using `curl`.
* Organize and register multiple tools within a central manager.
The next exciting chapter in this series will explore how an AI automatically discovers and leverages these tools. We'll set up the full discovery and execution protocol, then connect your server to a platform like Claude Desktop to witness the true magic unfold in real-world scenarios.
In the meantime, don't stop here! The possibilities are truly boundless. How about creating a tool to search for keywords within files? Or one that can manipulate JSON data? Perhaps a `createFile` tool? Get creative, experiment, and share your ideas!
What kind of AI-powered tools are you eager to build first? Have you already experimented with extending AI capabilities? I'd love to hear your thoughts in the comments below!
***
Nicolas Dabène
PrestaShop & Applied AI Expert
#ai #typescript #expressjs