Nicolas Dabene
Back to blog
12 November 2025 Nicolas Dabene 5 min

Create Your First MCP Tool: readFile

Create Your First MCP Tool: The readFile Tool Explained

IA development Tutorial artificial intelligence
Create Your First MCP Tool: readFile

Create Your First MCP Tool: The readFile Tool Explained

Create Your First MCP Tool: The readFile Tool Explained

You’ve configured your TypeScript environment in the previous article? Perfect! Now, it’s time for the magical moment when theory becomes reality. We’re going to create your very first MCP tool together: a function that will allow an AI to read files on your machine. It’s simple, concrete, and above all: it really works.

Introduction

In my developer career, I’ve always loved those moments when code comes to life. You know, when you launch your application and it does exactly what you imagined? That’s what we’re going to experience together today. After laying the foundations in previous articles, we’re going to build something tangible: an MCP tool that reads files.

Imagine: you ask Claude “Read me the report.txt file”, and it can actually do it thanks to your server. This is no longer theory, it’s your code making this possible. And the best part? Once you master creating one tool, you can create dozens more.

Reminder: What is an MCP Tool?

Before coding, let’s briefly recall what an MCP tool is. It’s essentially a function you expose to the AI with three essential pieces of information:

The tool name: How the AI will call it (for example “readFile”)

The description: What the tool does, so the AI understands when to use it

The parameters: The information the tool needs to function

It’s like creating a function in your code, but with an identity card that the AI can read and understand. Simple, right?

Anatomy of an MCP Tool

Let’s visualize the complete structure of an MCP tool. Here’s the skeleton we’ll fill in:

<span class="c1">// 1. Interface for input parameters</span>
<span class="kr">interface</span> <span class="nx">ToolParams</span> <span class="p">{</span>
  <span class="c1">// Data the AI sends us</span>
<span class="p">}</span>

<span class="c1">// 2. Interface for response</span>
<span class="kr">interface</span> <span class="nx">ToolResponse</span> <span class="p">{</span>
  <span class="nl">success</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span>
  <span class="nl">content</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">error</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// 3. The function that does the work</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">myTool</span><span class="p">(</span><span class="nx">params</span><span class="p">:</span> <span class="nx">ToolParams</span><span class="p">):</span> <span class="nb">Promise</span><span class="o"><</span><span class="nx">ToolResponse</span><span class="o">></span> <span class="p">{</span>
  <span class="c1">// Business logic here</span>
<span class="p">}</span>

<span class="c1">// 4. The tool definition (the "menu")</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">myToolDefinition</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">myTool</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">What my tool does</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">parameters</span><span class="p">:</span> <span class="p">{</span>
    <span class="c1">// Description of expected parameters</span>
  <span class="p">}</span>
<span class="p">};</span>

This four-part structure is your template for creating any MCP tool. Let’s keep it in mind for what follows.

Create the Folder Structure

Let’s start by organizing our code cleanly. In your mcp-server project, create the following structure:

<span class="nb">mkdir</span> <span class="nt">-p</span> src/tools
<span class="nb">mkdir</span> <span class="nt">-p</span> src/types

This organization will help us keep maintainable code. The tools folder will contain our MCP tools, and types our reusable TypeScript definitions.

Define TypeScript Types

Let’s first create our TypeScript interfaces. Create the src/types/mcp.ts file:

<span class="c1">// src/types/mcp.ts</span>

<span class="c1">// Generic type for tool parameters</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">ToolParams</span> <span class="p">{</span>
  <span class="p">[</span><span class="nx">key</span><span class="p">:</span> <span class="kr">string</span><span class="p">]:</span> <span class="kr">any</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Type for standard tool response</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">ToolResponse</span> <span class="p">{</span>
  <span class="nl">success</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span>
  <span class="nl">content</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">error</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">metadata</span><span class="p">?:</span> <span class="p">{</span>
    <span class="p">[</span><span class="na">key</span><span class="p">:</span> <span class="kr">string</span><span class="p">]:</span> <span class="kr">any</span><span class="p">;</span>
  <span class="p">};</span>
<span class="p">}</span>

<span class="c1">// Type for tool definition (the "menu")</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">ToolDefinition</span> <span class="p">{</span>
  <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">description</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">parameters</span><span class="p">:</span> <span class="p">{</span>
    <span class="p">[</span><span class="na">paramName</span><span class="p">:</span> <span class="kr">string</span><span class="p">]:</span> <span class="p">{</span>
      <span class="na">type</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
      <span class="nl">description</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
      <span class="nl">required</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>

<span class="c1">// Specific type for readFile parameters</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">ReadFileParams</span> <span class="kd">extends</span> <span class="nx">ToolParams</span> <span class="p">{</span>
  <span class="nl">file_path</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

These types will help us have auto-completion and avoid stupid errors. TypeScript becomes our best friend for this kind of project.

Create the readFile Tool

Now, the moment you’ve been waiting for: let’s create our tool! Create the src/tools/readFile.ts file:

<span class="c1">// src/tools/readFile.ts</span>
<span class="k">import</span> <span class="nx">fs</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">fs/promises</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">path</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ReadFileParams</span><span class="p">,</span> <span class="nx">ToolResponse</span><span class="p">,</span> <span class="nx">ToolDefinition</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../types/mcp</span><span class="dl">'</span><span class="p">;</span>

<span class="cm">/**
 * Reads the content of a text file
 * @param params - Parameters containing the file path
 * @returns Response with file content or error
 */</span>
<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">readFile</span><span class="p">(</span><span class="nx">params</span><span class="p">:</span> <span class="nx">ReadFileParams</span><span class="p">):</span> <span class="nb">Promise</span><span class="o"><</span><span class="nx">ToolResponse</span><span class="o">></span> <span class="p">{</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="c1">// Step 1: Parameter validation</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">params</span><span class="p">.</span><span class="nx">file_path</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
        <span class="na">error</span><span class="p">:</span> <span class="dl">"</span><span class="s2">The 'file_path' parameter is required</span><span class="dl">"</span>
      <span class="p">};</span>
    <span class="p">}</span>

    <span class="c1">// Step 2: Security - Resolve absolute path</span>
    <span class="c1">// This avoids attempts to access dangerous relative paths</span>
    <span class="kd">const</span> <span class="nx">absolutePath</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">file_path</span><span class="p">);</span>

    <span class="c1">// Step 3: Verify file exists</span>
    <span class="k">try</span> <span class="p">{</span>
      <span class="k">await</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">access</span><span class="p">(</span><span class="nx">absolutePath</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
        <span class="na">error</span><span class="p">:</span> <span class="s2">`File not found: </span><span class="p">${</span><span class="nx">params</span><span class="p">.</span><span class="nx">file_path</span><span class="p">}</span><span class="s2">`</span>
      <span class="p">};</span>
    <span class="p">}</span>

    <span class="c1">// Step 4: Get file information</span>
    <span class="kd">const</span> <span class="nx">stats</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">stat</span><span class="p">(</span><span class="nx">absolutePath</span><span class="p">);</span>

    <span class="c1">// Step 5: Verify it's a file (not a directory)</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">stats</span><span class="p">.</span><span class="nx">isFile</span><span class="p">())</span> <span class="p">{</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
        <span class="na">error</span><span class="p">:</span> <span class="dl">"</span><span class="s2">The specified path is not a file</span><span class="dl">"</span>
      <span class="p">};</span>
    <span class="p">}</span>

    <span class="c1">// Step 6: Limit size (security - avoid reading huge files)</span>
    <span class="kd">const</span> <span class="nx">MAX_FILE_SIZE</span> <span class="o">=</span> <span class="mi">10</span> <span class="o">*</span> <span class="mi">1024</span> <span class="o">*</span> <span class="mi">1024</span><span class="p">;</span> <span class="c1">// 10 MB</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">stats</span><span class="p">.</span><span class="nx">size</span> <span class="o">></span> <span class="nx">MAX_FILE_SIZE</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
        <span class="na">error</span><span class="p">:</span> <span class="s2">`File too large (max </span><span class="p">${</span><span class="nx">MAX_FILE_SIZE</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span><span class="p">}</span><span class="s2"> MB)`</span>
      <span class="p">};</span>
    <span class="p">}</span>

    <span class="c1">// Step 7: Read file content</span>
    <span class="kd">const</span> <span class="nx">content</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span><span class="nx">absolutePath</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf-8</span><span class="dl">'</span><span class="p">);</span>

    <span class="c1">// Step 8: Return success with metadata</span>
    <span class="k">return</span> <span class="p">{</span>
      <span class="na">success</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">content</span><span class="p">:</span> <span class="nx">content</span><span class="p">,</span>
      <span class="na">metadata</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">path</span><span class="p">:</span> <span class="nx">absolutePath</span><span class="p">,</span>
        <span class="na">size</span><span class="p">:</span> <span class="nx">stats</span><span class="p">.</span><span class="nx">size</span><span class="p">,</span>
        <span class="na">lastModified</span><span class="p">:</span> <span class="nx">stats</span><span class="p">.</span><span class="nx">mtime</span>
      <span class="p">}</span>
    <span class="p">};</span>

  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="na">error</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Handling unexpected errors</span>
    <span class="k">return</span> <span class="p">{</span>
      <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
      <span class="na">error</span><span class="p">:</span> <span class="s2">`Read error: </span><span class="p">${</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span>
    <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="cm">/**
 * Tool definition for MCP protocol
 * This is what the AI "sees" when discovering our tools
 */</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">readFileToolDefinition</span><span class="p">:</span> <span class="nx">ToolDefinition</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">readFile</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Reads the content of a text file from the local file system</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">parameters</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">file_path</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Absolute or relative path to the file to read</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">required</span><span class="p">:</span> <span class="kc">true</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">};</span>

Let’s take a moment to understand this code. Each step is numbered and explained:

Validation: We verify the necessary parameter is present. Always validate inputs!

Security: We resolve the absolute path to avoid malicious relative paths like ../../etc/passwd.

Existence check: We ensure the file exists before trying to read it.

Type verification: We confirm it’s a file, not a directory.

Size limit: We avoid loading a 2 GB file into memory by mistake.

Reading: We finally read the content in UTF-8.

Enriched response: We return not only content, but also useful metadata.

Error handling: We cleanly capture any unexpected error.

Create a Tool Manager

Now, let’s create a file that will centralize all our tools. Create src/tools/index.ts:

<span class="c1">// src/tools/index.ts</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ToolDefinition</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../types/mcp</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">readFile</span><span class="p">,</span> <span class="nx">readFileToolDefinition</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./readFile</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// Registry of all our tools</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">tools</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">readFile</span>
<span class="p">};</span>

<span class="c1">// Definitions of all our tools (for the "menu")</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">toolDefinitions</span><span class="p">:</span> <span class="nx">ToolDefinition</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
  <span class="nx">readFileToolDefinition</span>
<span class="p">];</span>

<span class="c1">// Helper function to execute a tool by name</span>
<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">executeTool</span><span class="p">(</span><span class="nx">toolName</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">params</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">tool</span> <span class="o">=</span> <span class="nx">tools</span><span class="p">[</span><span class="nx">toolName</span> <span class="k">as</span> <span class="kr">keyof</span> <span class="k">typeof</span> <span class="nx">tools</span><span class="p">];</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">tool</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
      <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
      <span class="na">error</span><span class="p">:</span> <span class="s2">`Tool '</span><span class="p">${</span><span class="nx">toolName</span><span class="p">}</span><span class="s2">' not found`</span>
    <span class="p">};</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="k">await</span> <span class="nx">tool</span><span class="p">(</span><span class="nx">params</span><span class="p">);</span>
<span class="p">}</span>

This file acts as a central registry. When you create new tools, you’ll simply add them here.

Integrate into Express Server

Let’s now modify our src/index.ts to expose our tools via HTTP routes:

<span class="c1">// src/index.ts</span>
<span class="k">import</span> <span class="nx">express</span><span class="p">,</span> <span class="p">{</span> <span class="nx">Request</span><span class="p">,</span> <span class="nx">Response</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">toolDefinitions</span><span class="p">,</span> <span class="nx">executeTool</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./tools</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">PORT</span> <span class="o">=</span> <span class="mi">3000</span><span class="p">;</span>

<span class="c1">// Middleware to parse JSON</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">express</span><span class="p">.</span><span class="nx">json</span><span class="p">());</span>

<span class="c1">// Test route</span>
<span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">:</span> <span class="nx">Request</span><span class="p">,</span> <span class="nx">res</span><span class="p">:</span> <span class="nx">Response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
  <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">({</span>
    <span class="na">message</span><span class="p">:</span> <span class="dl">'</span><span class="s1">MCP Server operational!</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">version</span><span class="p">:</span> <span class="dl">'</span><span class="s1">1.0.0</span><span class="dl">'</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="c1">// Route to discover available tools (the "menu")</span>
<span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/tools</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">:</span> <span class="nx">Request</span><span class="p">,</span> <span class="nx">res</span><span class="p">:</span> <span class="nx">Response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
  <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">({</span>
    <span class="na">success</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">tools</span><span class="p">:</span> <span class="nx">toolDefinitions</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="c1">// Route to execute a tool</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/tools/:toolName</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">:</span> <span class="nx">Request</span><span class="p">,</span> <span class="nx">res</span><span class="p">:</span> <span class="nx">Response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">toolName</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span>

  <span class="k">try</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">executeTool</span><span class="p">(</span><span class="nx">toolName</span><span class="p">,</span> <span class="nx">params</span><span class="p">);</span>
    <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">result</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="na">error</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">500</span><span class="p">).</span><span class="nx">json</span><span class="p">({</span>
      <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
      <span class="na">error</span><span class="p">:</span> <span class="s2">`Server error: </span><span class="p">${</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span>
    <span class="p">});</span>
  <span class="p">}</span>
<span class="p">});</span>

<span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`✅ MCP Server launched on http://localhost:</span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`📋 Available tools: http://localhost:</span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">/tools`</span><span class="p">);</span>
<span class="p">});</span>

Our server now exposes two important routes:

GET /tools: Lists all available tools (the famous “menu”)

POST /tools/:toolName: Executes a specific tool with provided parameters

Test the Tool

Moment of truth! Let’s test our tool. First, let’s create a test file:

<span class="nb">echo</span> <span class="s2">"This is a test file for MCP!"</span> <span class="o">></span> test.txt

Launch your server:

npm run dev

You should see:

✅ MCP Server launched on http://localhost:3000
📋 Available tools: http://localhost:3000/tools

Test 1: Discover Tools

Open a new terminal and test discovery:

curl http://localhost:3000/tools

Expected response:

<span class="p">{</span><span class="w">
  </span><span class="nl">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"tools"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"readFile"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Reads the content of a text file from the local file system"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Absolute or relative path to the file to read"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"required"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span>

Perfect! The AI can now discover your tool.

Test 2: Use the Tool

Now, let’s use readFile to read our test file:

curl <span class="nt">-X</span> POST http://localhost:3000/tools/readFile <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"file_path": "test.txt"}'</span>

Expected response:

<span class="p">{</span><span class="w">
  </span><span class="nl">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"content"</span><span class="p">:</span><span class="w"> </span><span class="s2">"This is a test file for MCP!</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/absolute/path/to/test.txt"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"size"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="p">,</span><span class="w">
    </span><span class="nl">"lastModified"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2025-11-12T10:30:00.000Z"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span>

It works! Your MCP server can now read files.

Test 3: Error Handling

Let’s test with a non-existent file:

curl <span class="nt">-X</span> POST http://localhost:3000/tools/readFile <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"file_path": "nonexistent_file.txt"}'</span>

Response:

<span class="p">{</span><span class="w">
  </span><span class="nl">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="s2">"File not found: nonexistent_file.txt"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span>

Excellent! Our error handling works correctly.

Improve the Tool

Now that the base works, let’s add some improvements. Let’s modify src/tools/readFile.ts:

<span class="c1">// Adding multiple encoding support</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">ReadFileParams</span> <span class="kd">extends</span> <span class="nx">ToolParams</span> <span class="p">{</span>
  <span class="nl">file_path</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">encoding</span><span class="p">?:</span> <span class="dl">'</span><span class="s1">utf-8</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">ascii</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">base64</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">readFile</span><span class="p">(</span><span class="nx">params</span><span class="p">:</span> <span class="nx">ReadFileParams</span><span class="p">):</span> <span class="nb">Promise</span><span class="o"><</span><span class="nx">ToolResponse</span><span class="o">></span> <span class="p">{</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="c1">// ... existing validation ...</span>

    <span class="c1">// Support for different encodings</span>
    <span class="kd">const</span> <span class="nx">encoding</span> <span class="o">=</span> <span class="nx">params</span><span class="p">.</span><span class="nx">encoding</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">utf-8</span><span class="dl">'</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">content</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span><span class="nx">absolutePath</span><span class="p">,</span> <span class="nx">encoding</span><span class="p">);</span>

    <span class="k">return</span> <span class="p">{</span>
      <span class="na">success</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">content</span><span class="p">:</span> <span class="nx">content</span><span class="p">,</span>
      <span class="na">metadata</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">path</span><span class="p">:</span> <span class="nx">absolutePath</span><span class="p">,</span>
        <span class="na">size</span><span class="p">:</span> <span class="nx">stats</span><span class="p">.</span><span class="nx">size</span><span class="p">,</span>
        <span class="na">encoding</span><span class="p">:</span> <span class="nx">encoding</span><span class="p">,</span>
        <span class="na">lastModified</span><span class="p">:</span> <span class="nx">stats</span><span class="p">.</span><span class="nx">mtime</span>
      <span class="p">}</span>
    <span class="p">};</span>

  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="na">error</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
      <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
      <span class="na">error</span><span class="p">:</span> <span class="s2">`Read error: </span><span class="p">${</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span>
    <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>

And let’s update the definition:

<span class="k">export</span> <span class="kd">const</span> <span class="nx">readFileToolDefinition</span><span class="p">:</span> <span class="nx">ToolDefinition</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">readFile</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Reads the content of a text file from the local file system</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">parameters</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">file_path</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Absolute or relative path to the file to read</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">required</span><span class="p">:</span> <span class="kc">true</span>
    <span class="p">},</span>
    <span class="na">encoding</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">File encoding (utf-8, ascii, base64)</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">required</span><span class="p">:</span> <span class="kc">false</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">};</span>

Create a Second Tool: listFiles

Now that you master creating a tool, let’s create a second one quickly. Create src/tools/listFiles.ts:

<span class="c1">// src/tools/listFiles.ts</span>
<span class="k">import</span> <span class="nx">fs</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">fs/promises</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">path</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ToolParams</span><span class="p">,</span> <span class="nx">ToolResponse</span><span class="p">,</span> <span class="nx">ToolDefinition</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../types/mcp</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">ListFilesParams</span> <span class="kd">extends</span> <span class="nx">ToolParams</span> <span class="p">{</span>
  <span class="nl">directory_path</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">listFiles</span><span class="p">(</span><span class="nx">params</span><span class="p">:</span> <span class="nx">ListFilesParams</span><span class="p">):</span> <span class="nb">Promise</span><span class="o"><</span><span class="nx">ToolResponse</span><span class="o">></span> <span class="p">{</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">params</span><span class="p">.</span><span class="nx">directory_path</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
        <span class="na">error</span><span class="p">:</span> <span class="dl">"</span><span class="s2">The 'directory_path' parameter is required</span><span class="dl">"</span>
      <span class="p">};</span>
    <span class="p">}</span>

    <span class="kd">const</span> <span class="nx">absolutePath</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">directory_path</span><span class="p">);</span>

    <span class="c1">// Verify it's a directory</span>
    <span class="kd">const</span> <span class="nx">stats</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">stat</span><span class="p">(</span><span class="nx">absolutePath</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">stats</span><span class="p">.</span><span class="nx">isDirectory</span><span class="p">())</span> <span class="p">{</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
        <span class="na">error</span><span class="p">:</span> <span class="dl">"</span><span class="s2">The specified path is not a directory</span><span class="dl">"</span>
      <span class="p">};</span>
    <span class="p">}</span>

    <span class="c1">// Read directory content</span>
    <span class="kd">const</span> <span class="nx">files</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">readdir</span><span class="p">(</span><span class="nx">absolutePath</span><span class="p">);</span>

    <span class="c1">// Get details for each file</span>
    <span class="kd">const</span> <span class="nx">filesWithDetails</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span>
      <span class="nx">files</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">file</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">filePath</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">absolutePath</span><span class="p">,</span> <span class="nx">file</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">fileStats</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">stat</span><span class="p">(</span><span class="nx">filePath</span><span class="p">);</span>

        <span class="k">return</span> <span class="p">{</span>
          <span class="na">name</span><span class="p">:</span> <span class="nx">file</span><span class="p">,</span>
          <span class="na">type</span><span class="p">:</span> <span class="nx">fileStats</span><span class="p">.</span><span class="nx">isDirectory</span><span class="p">()</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">directory</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">file</span><span class="dl">'</span><span class="p">,</span>
          <span class="na">size</span><span class="p">:</span> <span class="nx">fileStats</span><span class="p">.</span><span class="nx">size</span><span class="p">,</span>
          <span class="na">lastModified</span><span class="p">:</span> <span class="nx">fileStats</span><span class="p">.</span><span class="nx">mtime</span>
        <span class="p">};</span>
      <span class="p">})</span>
    <span class="p">);</span>

    <span class="k">return</span> <span class="p">{</span>
      <span class="na">success</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">content</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">filesWithDetails</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span>
      <span class="na">metadata</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">path</span><span class="p">:</span> <span class="nx">absolutePath</span><span class="p">,</span>
        <span class="na">count</span><span class="p">:</span> <span class="nx">filesWithDetails</span><span class="p">.</span><span class="nx">length</span>
      <span class="p">}</span>
    <span class="p">};</span>

  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="na">error</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
      <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
      <span class="na">error</span><span class="p">:</span> <span class="s2">`Error reading directory: </span><span class="p">${</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span>
    <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">listFilesToolDefinition</span><span class="p">:</span> <span class="nx">ToolDefinition</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">listFiles</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Lists files and directories in a directory</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">parameters</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">directory_path</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Absolute or relative path to the directory</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">required</span><span class="p">:</span> <span class="kc">true</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">};</span>

Add it in src/tools/index.ts:

<span class="k">import</span> <span class="p">{</span> <span class="nx">listFiles</span><span class="p">,</span> <span class="nx">listFilesToolDefinition</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./listFiles</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">tools</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">readFile</span><span class="p">,</span>
  <span class="nx">listFiles</span>
<span class="p">};</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">toolDefinitions</span><span class="p">:</span> <span class="nx">ToolDefinition</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
  <span class="nx">readFileToolDefinition</span><span class="p">,</span>
  <span class="nx">listFilesToolDefinition</span>
<span class="p">];</span>

Restart your server and test:

curl http://localhost:3000/tools

You’ll now see two available tools!

Best Practices and Security

Now that you know how to create tools, let’s talk security. Here are the golden rules:

Always Validate Inputs

Never trust received parameters. Validate everything: type, format, length, allowed values.

Limit File Access

Create a list of allowed directories:

<span class="kd">const</span> <span class="nx">ALLOWED_DIRECTORIES</span> <span class="o">=</span> <span class="p">[</span>
  <span class="dl">'</span><span class="s1">/home/user/documents</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">/home/user/projects</span><span class="dl">'</span>
<span class="p">];</span>

<span class="kd">function</span> <span class="nx">isPathAllowed</span><span class="p">(</span><span class="nx">filePath</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">boolean</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">absolute</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">filePath</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">ALLOWED_DIRECTORIES</span><span class="p">.</span><span class="nx">some</span><span class="p">(</span><span class="nx">dir</span> <span class="o">=></span> <span class="nx">absolute</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="nx">dir</span><span class="p">));</span>
<span class="p">}</span>

Limit Sizes

Always define file size limits, number of results, recursion depth.

Log Access

Keep a trace of who accesses what:

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`[</span><span class="p">${</span><span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">()}</span><span class="s2">] readFile: </span><span class="p">${</span><span class="nx">params</span><span class="p">.</span><span class="nx">file_path</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>

Conclusion

Congratulations! You just created your first functional MCP tool. You learned to:

  • Structure an MCP tool with TypeScript
  • Handle parameters and responses
  • Validate inputs and handle errors
  • Expose your tools via a REST API
  • Test your tools with curl
  • Create multiple tools and register them

The next step? In the next article, we’ll see how an AI discovers and uses your tools automatically. We’ll implement the complete discovery and execution protocol, then connect your server to Claude Desktop to see the magic operate in real conditions.

Meanwhile, experiment! Create your own tools. How about a tool that searches in files? Or that renames files in bulk? Or that analyzes JSON data? The possibilities are endless.


Article published November 12, 2025 by Nicolas Dabène - PHP & PrestaShop Expert with 15+ years of experience in software architecture and AI integration

Also read:

LinkedIn

Follow my AI and e-commerce analysis

I share practical notes on AI agents, PrestaShop architecture, MCP and automation for e-commerce teams.

Follow on LinkedIn