Build a File Manager MCP Server

Build an MCP server that lets AI agents create, read, and search files.

Introduction

30 Second Summary

AI assistants can write impressive answers, but they cannot actually do anything on your computer unless you give them a way to act. The standard protocol that makes this possible is called MCP, and every major AI platform now supports it.

In this project, you will build a custom MCP server that gives an AI agent the ability to create, read, list, and search files on your computer.

What You'll Build

A TypeScript MCP server using the official SDK that exposes file management tools to Cursor's AI agent via the stdio transport.

By the end of this project, you'll have:

  • A working MCP server with 4 registered tools that any MCP-compatible AI client can discover and use.
  • A live connection between your server and Cursor's AI agent that lets the agent autonomously call your tools to perform real file system actions.
  • A clear understanding of the MCP architecture (servers, clients, hosts, tools, transports) that transfers to building any AI tool integration.
  • Secret Mission: Add a move-file tool that organizes your sandbox files into subdirectories by extension.

Are there any prerequisites?

This project assumes you have completed "Build an AI Content Repurposer with MongoDB" or have equivalent familiarity with Node.js, TypeScript, and Zod schemas. You should also have Cursor installed on your computer.

Before We Start

Before We Start...

Set Up the MCP Server Project

Every MCP server is a standalone program that communicates with AI clients through a standard protocol. Before you write any tools, you need a TypeScript project with the official MCP SDK installed and a build pipeline that compiles your code into runnable JavaScript.

In this step, you will scaffold the project folder, install the SDK and its dependencies, write a minimal server skeleton that compiles cleanly, and wire it into Cursor so the AI agent can discover your server.

In this step, get ready to:
  • Create the project folder and install the MCP SDK with its dependencies.
  • Write the minimal server skeleton and verify it compiles.
  • Connect the server to Cursor and confirm it appears with a green status.
Create the project and install dependencies

You need a dedicated folder on your Desktop for this project, along with a package.json that configures the project as an ES module and a TypeScript compiler config.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir file-manager-mcp
cd file-manager-mcp
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the file-manager-mcp folder, then click Open.

You should see an empty project folder in Cursor's Explorer sidebar on the left.

  • Open Cursor's integrated terminal by pressing Ctrl+` (backtick key, same shortcut on macOS and Windows).

Now you will create the package.json file that defines your project's configuration and dependencies.

  • In the Explorer sidebar, right-click in the empty space and select New File.
  • Name the file package.json and press Enter.
  • Paste the following content into package.json:
{
  "name": "file-manager-mcp",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc"
  }
}

What does this configuration do?

  • "type": "module" tells Node.js to treat .js files as ES modules, which the MCP SDK requires.
  • The build script runs the TypeScript compiler (tsc) to convert your .ts files into JavaScript that Node.js can execute.
  • Save package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Next, install the MCP SDK and its companion library Zod (used for defining tool input schemas), plus the TypeScript compiler as a dev dependency.

  • Install the project dependencies by running these commands in your terminal:
npm install @modelcontextprotocol/sdk@1.29.0 zod@3.25.0
npm install -D typescript@5.8.3 @types/node@22.15.0
  • Wait for both installs to finish. You should see a node_modules folder appear in the Explorer sidebar.

Seeing permission errors or EACCES?

Make sure you are running the terminal inside the file-manager-mcp folder. Run pwd to check your current directory ends in /file-manager-mcp.

If you see network or registry errors, check your internet connection and try again.

Help me troubleshoot this npm install error

✔️ Awesome, I've got everything!

Great. Double check that package.json now shows both dependencies and devDependencies sections after the install.

ⓧ I'd like to double check the full code

{
  "name": "file-manager-mcp",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.29.0",
    "zod": "3.25.0"
  },
  "devDependencies": {
    "@types/node": "22.15.0",
    "typescript": "5.8.3"
  }
}

Now create the TypeScript compiler configuration that tells tsc how to compile your server code.

  • In the Explorer sidebar, right-click in the empty space and select New File.
  • Name the file tsconfig.json and press Enter.
  • Paste the following content into tsconfig.json:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

What does this configuration do?

  • "target": "ES2022" compiles to modern JavaScript with features like top-level await.
  • "module": "Node16" and "moduleResolution": "Node16" match how Node.js resolves ES module imports.
  • "outDir": "./build" puts the compiled JavaScript in a build folder, keeping source and output separate.
  • "rootDir": "./src" tells TypeScript your source files live in the src folder.
  • Save tsconfig.json.

✔️ Awesome, I've got everything!

Great. Your TypeScript compiler is now configured and ready for the next step.

ⓧ I'd like to double check the full code

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}
Create the MCP server skeleton

With the project scaffolded, you can now write the minimal server code. This skeleton imports the MCP SDK, creates a server instance, and connects it to the stdio transport so it can communicate with AI clients.

  • Create a src folder by right-clicking in the Explorer sidebar and selecting New Folder.
  • Name the folder src and press Enter.
  • Right-click on the src folder and select New File.
  • Name the file index.ts and press Enter.
  • Paste the following code into src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";

// Define the sandbox directory where file operations will be contained
const SANDBOX_DIR = path.resolve(process.cwd(), "sandbox");

// Create the MCP server instance with a name and version
const server = new McpServer({
  name: "file-manager",
  version: "1.0.0",
});

// Main function: set up the sandbox, connect the transport, and start listening
async function main() {
  await fs.mkdir(SANDBOX_DIR, { recursive: true });
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("File Manager MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

What does this code do?

  • McpServer is the main class from the SDK that handles tool registration and protocol messaging.
  • StdioServerTransport connects the server to stdin/stdout. This is how Cursor will send and receive JSON-RPC 2.0 messages to your server.
  • SANDBOX_DIR defines a safe directory where all file operations will happen. This prevents the AI from writing to arbitrary locations on your system.
  • console.error is used instead of console.log because stdout is reserved for the MCP protocol. Diagnostic messages must go to stderr.
  • Save src/index.ts.
  • Build the project by running this command in your terminal:
npm run build
  • You should see no errors in the terminal output. A new build folder appears in the Explorer sidebar containing index.js.

Seeing TypeScript compilation errors?

Check that your tsconfig.json has "rootDir": "./src" and that your file is saved as src/index.ts (inside the src folder, not at the project root).

If you see module resolution errors, confirm "moduleResolution": "Node16" is in your tsconfig.json and that node_modules exists (re-run npm install if needed).

Help me fix this TypeScript compilation error

✔️ Awesome, I've got everything!

Great. Your server skeleton compiles cleanly and is ready to be connected to Cursor.

ⓧ I'd like to double check the full code

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";

const SANDBOX_DIR = path.resolve(process.cwd(), "sandbox");

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

async function main() {
  await fs.mkdir(SANDBOX_DIR, { recursive: true });
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("File Manager MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});
Connect the server to Cursor

Cursor has a built-in MCP client that can connect to servers registered in a configuration file. You will create this config file so Cursor knows how to launch your server.

  • In the Explorer sidebar, right-click in the empty space and select New Folder.
  • Name the folder .cursor and press Enter.
  • Right-click on the .cursor folder and select New File.
  • Name the file mcp.json and press Enter.
  • Paste the following configuration into .cursor/mcp.json:
{
  "mcpServers": {
    "file-manager": {
      "command": "node",
      "args": ["./build/index.js"]
    }
  }
}

What does this configuration do?

This file tells Cursor to launch a process using the node command with ./build/index.js as the argument. Cursor communicates with this process over stdin/stdout using the MCP protocol.

The key "file-manager" is the server name that will appear in Cursor's settings. You can register multiple MCP servers in this file, each with their own entry.

  • Save .cursor/mcp.json.

Now you need to restart Cursor so it picks up the new MCP configuration.

  • Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the Command Palette.
  • Type Reload Window and press Enter.
  • Open Cursor's MCP settings by pressing Ctrl+Shift+J (same on both macOS and Windows).
  • Look for file-manager in the server list. It should show a green indicator confirming it is connected.

Server not showing green?

Make sure you ran npm run build successfully before reloading. Cursor launches node ./build/index.js, so the compiled file must exist.

Confirm your .cursor/mcp.json is inside a .cursor folder at the project root (not in src or any subfolder).

If the server shows a red indicator, click on it to see the error log. Common issues include a missing build/index.js file or a JSON syntax error in the config.

Help me get my MCP server connected

✔️ Awesome, I've got everything!

Great. Your MCP server is live and Cursor can communicate with it.

ⓧ I'd like to double check the full code

{
  "mcpServers": {
    "file-manager": {
      "command": "node",
      "args": ["./build/index.js"]
    }
  }
}

Your MCP server is scaffolded, compiled, and connected to Cursor. Next up, you will register your first tool so the AI agent can actually do something with this server.

Register Your First Tool

Your MCP server is connected to Cursor and running, but it has zero capabilities. If you asked Cursor's AI agent to create a file right now, it wouldn't know how.

MCP tools are the functions your server advertises to AI clients. Each tool has a name, a description the AI reads to understand its purpose, and a typed input schema that defines exactly what parameters the AI must provide. In this step, you'll register your first tool and watch Cursor's agent discover and use it autonomously.

In this step, get ready to:
  • Register a create-file tool with a Zod input schema.
  • Rebuild and test the tool through Cursor's AI agent.
  • Verify the AI created a real file on your disk.
Register the create-file tool

The server.tool() method is how you expose a capability to any MCP client. It accepts a tool name, a human-readable description, a Zod schema defining the expected inputs, and a handler function that runs when the AI calls it.

  • In src/index.ts, find the line that says async function main() {.
  • Copy the following code and paste it directly above that line:
server.tool(
  "create-file",
  "Create a new file with the given content in the sandbox directory",
  {
    filename: z.string().describe("Name of the file to create"),
    content: z.string().describe("Content to write to the file"),
  },
  async ({ filename, content }) => {
    const filePath = path.join(SANDBOX_DIR, filename);
    await fs.mkdir(path.dirname(filePath), { recursive: true });
    await fs.writeFile(filePath, content, "utf-8");
    return {
      content: [
        { type: "text", text: `Created file: ${filename}` },
      ],
    };
  }
);

What does this code do?

  • "create-file" is the tool name that Cursor's agent sees and decides to call.
  • The description tells the AI when this tool is useful. The agent reads it to decide whether to call this tool for a given request.
  • The Zod schema defines two required parameters: filename and content. The SDK validates incoming requests against this schema automatically.
  • The handler writes the file inside SANDBOX_DIR using Node's fs/promises and returns a success message in the MCP text content format.
  • fs.mkdir with recursive: true ensures the parent directory exists before writing. This prevents errors if the sandbox hasn't been created yet.
  • Save src/index.ts.

Why does the sandbox pattern matter?

Constraining file operations to a known sandbox directory prevents the AI from writing to arbitrary locations on your machine. This is a real-world best practice for MCP servers that interact with the file system.

Seeing a red underline on the import?

Make sure the server.tool() call is placed between the const server = new McpServer({...}) block and the async function main() function. It must be at the top level of the file, not nested inside another function.

help me fix a TypeScript error in my MCP server tool registration

Rebuild and test with Cursor's agent

Every time you change your TypeScript source, you need to recompile before the MCP server picks up the changes. The compiled output in the build/ folder is what Cursor actually runs.

  • Rebuild your project by running this command in the terminal:
npm run build
  • Restart the MCP server in Cursor so it loads the new tool. Press Cmd+Shift+J (macOS) or Ctrl+Shift+J (Windows) to open Settings.
  • Click Tools & MCP in the sidebar.
  • Find the file-manager server entry. Click the toggle to disable it, then click it again to re-enable it.
  • Wait for the green status indicator to reappear. This confirms the server restarted with your new tool.

Why toggle off and on?

Toggling the server forces Cursor to stop the old process and start a fresh one from your updated build/index.js. Without this, the agent would still be talking to the old server that has no tools registered.

  • Open a new Agent chat in Cursor by pressing Cmd+L (macOS) or Ctrl+L (Windows).
  • Type this prompt and press Enter:
Create a file called hello.txt with the content 'Hello from MCP!' in my sandbox.
  • Cursor's agent will ask for your approval before calling the create-file tool. Click Approve to allow it.

What should I see?

The agent identifies your create-file tool as the right one for the job, constructs the correct parameters (filename: "hello.txt", content: "Hello from MCP!"), and executes it. You should see a confirmation message like "Created file: hello.txt" in the chat.

Agent not finding your tool?

Make sure you toggled the server off and back on after rebuilding. Start a fresh chat after the toggle. Existing chat sessions may not pick up newly available tools.

help me troubleshoot why Cursor's agent can't find my MCP create-file tool

Verify the file and observe the agent's limits

The AI just took a real action on your file system. Let's confirm the file exists on disk, then see what happens when you ask something the agent doesn't have a tool for yet.

  • Check that the file was created by running this command in your terminal:
cat sandbox/hello.txt
  • You should see Hello from MCP! printed in the terminal. This confirms the AI agent called your tool and your handler wrote a real file to disk.
  • Now ask the agent something it cannot do yet. Type this in the same chat:
What files are in my sandbox?

The agent cannot answer this accurately because it only has the create-file tool available. It has no way to list directory contents. It might try to use its built-in file reading, or it might say it cannot determine the answer. This is the exact limitation you'll solve in the next step by adding more tools.

Why test the limits?

This exercise shows that an MCP agent is only as capable as the tools you give it. The agent cannot invent tools on its own. It can only call what your server explicitly registers and advertises.

✔️ Awesome, I've got everything!

Great. Double check you've saved src/index.ts and that sandbox/hello.txt exists with the correct content.

ⓧ I'd like to double check the full code

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";

const SANDBOX_DIR = path.resolve(process.cwd(), "sandbox");

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

server.tool(
  "create-file",
  "Create a new file with the given content in the sandbox directory",
  {
    filename: z.string().describe("Name of the file to create"),
    content: z.string().describe("Content to write to the file"),
  },
  async ({ filename, content }) => {
    const filePath = path.join(SANDBOX_DIR, filename);
    await fs.mkdir(path.dirname(filePath), { recursive: true });
    await fs.writeFile(filePath, content, "utf-8");
    return {
      content: [
        { type: "text", text: `Created file: ${filename}` },
      ],
    };
  }
);

async function main() {
  await fs.mkdir(SANDBOX_DIR, { recursive: true });
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("File Manager MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

Your MCP server now has its first working tool, and you've seen an AI agent autonomously call it. Next up, you'll give the agent the ability to read and list files so it can answer questions about what's in your sandbox.

Add Read and List Tools

In the last step, you proved your MCP server works. Cursor's agent discovered your create-file tool and used it to write a real file to disk.

Right now, the agent can create files but has no way to see what already exists. In this step, you'll give it the ability to read file contents and list everything in the sandbox directory.

In this step, get ready to:
  • Register a read-file tool that returns file contents as text.
  • Register a list-files tool that returns all filenames in the sandbox.
  • Test multi-step agent behavior where the AI chains multiple tools together.
Register the read-file and list-files tools

Each new capability you want the AI agent to have requires its own tool registration. The agent discovers tools automatically through the MCP protocol, so all you need to do is define them and rebuild.

  • In src/index.ts, find the async function main() near the bottom of the file.
  • Add the following code above it, directly after the closing ); of the create-file tool registration:
server.tool(
  "read-file",
  "Read the contents of a file in the sandbox directory",
  {
    filename: z.string().describe("Name of the file to read"),
  },
  async ({ filename }) => {
    const filePath = path.join(SANDBOX_DIR, filename);
    const content = await fs.readFile(filePath, "utf-8");
    return {
      content: [
        { type: "text", text: content },
      ],
    };
  }
);

What does this code do?

The read-file tool takes a filename parameter and reads that file from the sandbox directory. It returns the file's content as plain text that the agent can process.

The Zod schema validates that the agent always provides a string filename. If the agent sends an invalid input, the SDK rejects the call before your handler runs.

You won't see a change yet. The next chunk adds the list-files tool, and then you'll rebuild and test both together.

  • Still in src/index.ts, add the following code directly below the read-file tool you just added (still above async function main()):
server.tool(
  "list-files",
  "List all files in the sandbox directory",
  {},
  async () => {
    const entries = await fs.readdir(SANDBOX_DIR);
    const fileList = entries.length > 0 ? entries.join("\n") : "No files found.";
    return {
      content: [
        { type: "text", text: fileList },
      ],
    };
  }
);

What does this code do?

The list-files tool takes no parameters (the empty {} schema). It reads all entries in the sandbox directory and returns them as a newline-separated list.

If the sandbox is empty, it returns a friendly "No files found." message instead of an empty string. This helps the agent understand the state of the directory without ambiguity.

  • Save src/index.ts.

Seeing red squiggles in your editor?

Make sure each server.tool() call is placed between the closing ); of the previous tool and the async function main() block. Each tool registration is a standalone statement, not nested inside another.

Check that the list-files tool has an empty {} for its schema (not omitted entirely). The SDK requires a schema object even when there are no parameters.

Help me debug red squiggles in my MCP tool registrations

✔️ Awesome, I've got everything!

Great. Double check you've saved src/index.ts before moving on to the rebuild.

ⓧ I'd like to double check the full code

Here is the full src/index.ts file with all three tools registered:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";

const SANDBOX_DIR = path.resolve(process.cwd(), "sandbox");

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

server.tool(
  "create-file",
  "Create a new file with the given content in the sandbox directory",
  {
    filename: z.string().describe("Name of the file to create"),
    content: z.string().describe("Content to write to the file"),
  },
  async ({ filename, content }) => {
    const filePath = path.join(SANDBOX_DIR, filename);
    await fs.mkdir(path.dirname(filePath), { recursive: true });
    await fs.writeFile(filePath, content, "utf-8");
    return {
      content: [
        { type: "text", text: `Created file: ${filename}` },
      ],
    };
  }
);

server.tool(
  "read-file",
  "Read the contents of a file in the sandbox directory",
  {
    filename: z.string().describe("Name of the file to read"),
  },
  async ({ filename }) => {
    const filePath = path.join(SANDBOX_DIR, filename);
    const content = await fs.readFile(filePath, "utf-8");
    return {
      content: [
        { type: "text", text: content },
      ],
    };
  }
);

server.tool(
  "list-files",
  "List all files in the sandbox directory",
  {},
  async () => {
    const entries = await fs.readdir(SANDBOX_DIR);
    const fileList = entries.length > 0 ? entries.join("\n") : "No files found.";
    return {
      content: [
        { type: "text", text: fileList },
      ],
    };
  }
);

async function main() {
  await fs.mkdir(SANDBOX_DIR, { recursive: true });
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("File Manager MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});
Rebuild and test each tool

Your new tools exist in source code, but the MCP server runs from the compiled JavaScript in the build/ folder. You need to rebuild and restart so Cursor picks up the new tools.

  • Rebuild your project by running this command in your terminal:
npm run build
  • Restart the MCP server in Cursor by opening Settings > Tools & MCP.
  • Toggle the file-manager server off, then toggle it back on.
  • Confirm the green status indicator reappears next to your server.

Why toggle off and on?

Cursor connects to your MCP server as a long-running process via stdio transport. When you rebuild, the old process is still running the previous compiled code. Toggling restarts the process so it loads your updated build/index.js with the new tools.

  • Open Cursor's Agent chat panel.
  • Type this prompt and press Enter:
What files are in my sandbox?
  • The agent should discover and call your list-files tool. Approve the tool call when Cursor asks for permission.

You should see the agent respond with hello.txt (the file you created in the previous step).

  • Now test the read tool by typing this prompt:
Read the contents of hello.txt
  • Approve the tool call. The agent should return "Hello from MCP!" as the file content.

Agent not finding your new tools?

Make sure you ran npm run build after saving your changes. The agent talks to the compiled JavaScript, not your TypeScript source.

Verify the server shows a green indicator in Settings > Tools & MCP. If it shows red, toggle it off and on again. A red indicator means the server process crashed during startup.

Help me troubleshoot why Cursor's agent can't find my new MCP tools

Test a multi-step interaction

The real power of MCP tools shows when an agent chains multiple tools together autonomously. You give it a complex request, and it decides which tools to call and in what order.

  • In Cursor's Agent chat, type this prompt and press Enter:
Create a file called notes.md with some project ideas, then read it back to me and tell me what you wrote.
  • Approve each tool call as the agent requests permission.

Watch how the agent handles this. It should call create-file first to create notes.md, then call read-file to retrieve its contents, and finally summarize what it wrote.

What is AI agent tool chaining?

You gave the agent a single natural-language request. It autonomously decided to break that into three steps: create, read, report. You never told it which tools to use or in what order.

This is the AI agent pattern in action. The agent discovers available tools through MCP's tool discovery protocol, then plans and executes a sequence of calls to fulfill your request.

  • Verify that notes.md now exists in your sandbox/ folder by running:
ls sandbox

You should see both hello.txt and notes.md in the output.

Agent only called one tool?

Some prompts may lead the agent to combine steps differently. If it only used create-file without reading back, try rephrasing: "Create notes.md, then use read-file to show me its contents." Being explicit about using a separate read step helps.

Help me get the agent to chain multiple MCP tool calls in one request

Your agent now has three tools at its disposal: create, read, and list. Next up, you'll add a search tool and push the agent with more complex, open-ended requests.

Add a Search Tool and Test the Full Agent Loop

Your agent can create, read, and list files. But what happens when the sandbox fills up with dozens of files and you need to find a specific one?

In this step, you'll add a search tool that lets the agent filter files by name. More importantly, you'll test what happens when you give the agent open-ended requests that require it to decide which tools to call on its own.

In this step, get ready to:
  • Register a search-files tool with case-insensitive filename matching.
  • Test search-to-read chaining with multiple tool calls.
  • Verify the agent autonomously picks which tools to use for open-ended requests.
Register the search-files tool

The search tool reads all filenames in the sandbox and filters them against a query string. This gives the agent a way to find files without needing to know their exact names.

  • In src/index.ts, find the list-files tool registration block (the one that ends with the closing ); before the async function main() block).
  • Add the following search-files tool registration below it:
server.tool(
  "search-files",
  "Search for files in the sandbox whose names contain the query string",
  {
    query: z.string().describe("Search term to match against filenames"),
  },
  async ({ query }) => {
    // Read all entries from the sandbox directory
    const entries = await fs.readdir(SANDBOX_DIR);
    // Filter entries where the filename includes the query (case-insensitive)
    const matches = entries.filter((entry) =>
      entry.toLowerCase().includes(query.toLowerCase())
    );
    // Format the result message based on whether matches were found
    const result =
      matches.length > 0
        ? `Found ${matches.length} file(s):\n${matches.join("\n")}`
        : `No files matching "${query}" found.`;
    return {
      content: [
        { type: "text", text: result },
      ],
    };
  }
);

What does this code do?

The search-files tool takes a single query parameter and compares it against every filename in the sandbox.

The comparison uses .toLowerCase() on both the query and filenames so that searching for "note" matches Notes.md, NOTE.txt, and my-notes.md. The tool returns a formatted count and list of matching filenames.

  • Save src/index.ts.
  • Rebuild your project by running this command in the terminal:
npm run build
  • The build should complete without errors.

Seeing a TypeScript error?

Check that the new server.tool() block is placed after the closing ); of the list-files tool and before the async function main() line. A missing closing parenthesis or bracket from a previous tool will cascade errors into the new code.

Help me fix a TypeScript compilation error in my MCP server

  • Restart the MCP server in Cursor. Open Settings > Tools & MCP and toggle the file-manager server off, then on again.
  • Confirm the status indicator returns to green.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and that the server shows a green status in Cursor.

ⓧ I'd like to double check the full code

Here is the full src/index.ts file with all four tools registered:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";

const SANDBOX_DIR = path.resolve(process.cwd(), "sandbox");

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

server.tool(
  "create-file",
  "Create a new file with the given content in the sandbox directory",
  {
    filename: z.string().describe("Name of the file to create"),
    content: z.string().describe("Content to write to the file"),
  },
  async ({ filename, content }) => {
    const filePath = path.join(SANDBOX_DIR, filename);
    await fs.mkdir(path.dirname(filePath), { recursive: true });
    await fs.writeFile(filePath, content, "utf-8");
    return {
      content: [
        { type: "text", text: `Created file: ${filename}` },
      ],
    };
  }
);

server.tool(
  "read-file",
  "Read the contents of a file in the sandbox directory",
  {
    filename: z.string().describe("Name of the file to read"),
  },
  async ({ filename }) => {
    const filePath = path.join(SANDBOX_DIR, filename);
    const content = await fs.readFile(filePath, "utf-8");
    return {
      content: [
        { type: "text", text: content },
      ],
    };
  }
);

server.tool(
  "list-files",
  "List all files in the sandbox directory",
  {},
  async () => {
    const entries = await fs.readdir(SANDBOX_DIR);
    const fileList = entries.length > 0 ? entries.join("\n") : "No files found.";
    return {
      content: [
        { type: "text", text: fileList },
      ],
    };
  }
);

server.tool(
  "search-files",
  "Search for files in the sandbox whose names contain the query string",
  {
    query: z.string().describe("Search term to match against filenames"),
  },
  async ({ query }) => {
    const entries = await fs.readdir(SANDBOX_DIR);
    const matches = entries.filter((entry) =>
      entry.toLowerCase().includes(query.toLowerCase())
    );
    const result =
      matches.length > 0
        ? `Found ${matches.length} file(s):\n${matches.join("\n")}`
        : `No files matching "${query}" found.`;
    return {
      content: [
        { type: "text", text: result },
      ],
    };
  }
);

async function main() {
  await fs.mkdir(SANDBOX_DIR, { recursive: true });
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("File Manager MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});
Test search with multi-tool chaining

Now let's give the agent enough files to make searching meaningful. Then you'll ask it a question that forces it to chain search and read together.

  • Open Cursor's Agent chat.
  • Ask the agent to create several test files by typing this prompt:
Create these files in my sandbox: a file called meeting-notes.txt with some meeting agenda items, a file called project-ideas.md with 3 project ideas, a file called todo.txt with a short task list, and a file called lecture-notes.md with notes about APIs.
  • Approve each tool call when Cursor asks for permission. You should see it call create-file four times.

Why does Cursor ask for approval?

By default, Cursor's agent asks for approval before calling any MCP tool. This is a safety measure. Your tools write to the file system, so Cursor lets you confirm each action before it happens.

  • Now test the search-to-read chain by typing this prompt:
Find all files with 'note' in the name and show me their contents.
  • Approve the tool calls as they appear.

Watch the agent's behavior. It should first call search-files with the query "note", then call read-file for each matching file. This is multi-tool chaining in action.

What is multi-tool chaining?

The agent receives your request, decides it needs to search first, reads the search results, then decides it needs to read each matching file. You never told it which tools to call or in what order. The agent figured out the plan from the tool descriptions you registered.

Agent not using search-files?

If the agent uses list-files instead of search-files, that is still valid behavior. The agent may choose whichever approach it considers most efficient. What matters is that it ultimately shows you the file contents.

If you see no tool calls at all, confirm the server is showing a green status in Settings > Tools & MCP. Toggle the server off and on again, then retry your prompt.

Why isn't my agent calling the search-files tool?

Test open-ended autonomous requests

The real power of MCP shows up when you give the agent an open-ended goal and let it decide which tools to call. You are not going to specify which tools to use. The agent figures it out from the descriptions you registered.

  • Type this open-ended prompt into the Agent chat:
Help me organize my thoughts about learning MCP. Create a markdown file summarizing what MCP is, then verify it was created successfully.
  • Approve the tool calls as they appear and observe which tools the agent picks.

The agent should call create-file to write the summary, then call list-files or read-file to confirm it exists. That's 2-3 tool calls from a single natural-language request.

Why does this matter?

You never said "use create-file then use list-files." The agent read your tool descriptions, understood the available capabilities, and built its own plan. This is the core value of MCP. Any MCP-compatible client can discover your tools and use them autonomously.

  • Verify the agent used 3 or more tool calls across both prompts in this substep by scrolling through the chat and counting the tool-call badges Cursor displays.

Agent only using one tool per request?

Make sure you are using the Agent mode in Cursor's chat, not the Ask mode. Only Agent mode supports multi-step tool calling. You can check which mode is active in the dropdown at the top of the chat panel.

Help me get Cursor's agent to chain multiple MCP tool calls

Your MCP server is now a complete toolkit. Four tools, all discoverable, all chainable. You just proved that an AI agent can autonomously plan and execute multi-step file operations using nothing but your tool descriptions. Next up, you'll push this even further in the Secret Mission.

Secret mission

Add a File Organizer Tool

Your sandbox is getting messy. Add a move-file tool that relocates files into organized subdirectories by file extension, then ask Cursor's agent to tidy up your entire sandbox automatically.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The file-manager-mcp project folder on your Desktop (source code, compiled output, and sandbox files).
  • MCP server registration in Cursor (configured via .cursor/mcp.json in the project root).

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your MCP server.

  • Your MCP server stays configured in Cursor and ready to use whenever you open this project.
  • The sandbox folder retains all the files and subdirectories the AI agent created.
  • You can continue asking Cursor's agent to create, read, search, and organize files at any time.

Pause - I'll come back to this later

Stop the MCP server to free up background resources while keeping all your code in place.

  • Open Cursor Settings by pressing Cmd+Shift+J (macOS) or Ctrl+Shift+J (Windows).
  • Navigate to Tools & MCP.
  • Toggle the file-manager server off. The status indicator turns gray.
  • When you want to resume, toggle it back on and the server reconnects automatically.

Delete - I don't want to use this again

Remove all project resources and deregister the MCP server from Cursor.

  • Open Cursor Settings by pressing Cmd+Shift+J (macOS) or Ctrl+Shift+J (Windows).
  • Navigate to Tools & MCP and toggle the file-manager server off.
  • Delete the project folder by running the following command for your operating system.

macOS

rm -rf ~/Desktop/file-manager-mcp

Windows

Remove-Item -Recurse -Force ~\Desktop\file-manager-mcp

This removes all source code, the compiled build folder, the .cursor/mcp.json config, and the sandbox directory with all agent-created files.

Nice Work!

Nice Work!

You just built a fully functional MCP server from scratch and watched an AI agent autonomously discover and use your tools to manage files on your system.

You've learned how to:

  • Built a working MCP server using the official TypeScript SDK and the stdio transport, registering 4 tools (create-file, read-file, list-files, search-files) with typed Zod input schemas that AI agents discover automatically.
  • Connected your server to Cursor via .cursor/mcp.json and watched its AI agent autonomously decide when to call your tools based on natural language requests.
  • Observed multi-step agent behavior where the AI chains multiple tool calls together to fulfill complex requests without you specifying which tools to use.
  • Secret Mission: Added a move-file tool and watched the AI agent independently organize files into typed subdirectories by file extension.

Ready to quiz yourself?