The fastest way to understand MCP isn't reading the protocol documentation, but rather building a server yourself and using it within Claude Code or Cursor. This tutorial guides you through implementing a "local file search" server that gives your AI assistant a new capability: searching for keywords within files in a specified directory and returning matching results along with their context snippets. The entire implementation is roughly 100 lines of TypeScript and requires no external services.
We chose file search as our example for two reasons: first, it addresses a real-world need (the frequent request to "have AI search my local machine or intranet resources"); second, it covers all critical aspects of MCP development—tool definition, parameter validation, security boundaries, and result formatting. If you are still unsure whether to adopt MCP, start by reading "MCP vs Function Calling: Which Should You Choose for AI Tool Invocation?".
A minimal viable MCP server: one entry file, one tool, and one stdio transport channel.
Preparation and Project Initialization
Environment requirements: Node.js 18+ and any MCP host (this article demonstrates using Claude Code and Cursor). Initialize the project:
mkdir mcp-file-search && cd mcp-file-search
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init@modelcontextprotocol/sdk is the official TypeScript SDK, which encapsulates JSON-RPC protocol details; zod is used to declare and validate tool parameters. The SDK's interfaces are continuously evolving. This article follows the official documentation at the time of publication. If you encounter issues running the code, first check the API names against the SDK's README.
Core Code: Defining the Server and Search Tool
Create src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "node:fs/promises";
import path from "node:path";
// Root directory allowed for searching, read from the launch argument: this is the server's security boundary
const ROOT = path.resolve(process.argv[2] ?? ".");
const server = new McpServer({ name: "file-search", version: "1.0.0" });
server.registerTool(
"search_files",
{
description:
"Search text file contents by keyword inside a given directory, returning matching file paths and context snippets. " +
"Useful for finding keywords across documents, notes, and code.",
inputSchema: {
query: z.string().min(1).describe("keyword to search for"),
subdir: z.string().default(".").describe("subdirectory relative to the root; defaults to everything"),
maxResults: z.number().int().min(1).max(50).default(10),
},
},
async ({ query, subdir, maxResults }) => {
const base = path.resolve(ROOT, subdir);
// Block directory traversal: the resolved path must still sit inside ROOT
if (!base.startsWith(ROOT)) {
return {
content: [{ type: "text", text: "Error: directory outside the allowed range" }],
isError: true,
};
}
const hits = await searchDir(base, query, maxResults);
return {
content: [{
type: "text",
text: hits.length
? hits.map(h => `${h.file}:${h.line}\n ${h.snippet}`).join("\n\n")
: `No files found containing "${query}"`,
}],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);The search function itself is standard Node.js code—it recursively traverses directories, matches line by line, extracts context, and skips node_modules, binary files, and oversized files. The full implementation takes fewer than 50 lines, so we won’t elaborate here. However, three design points are worth highlighting:
- Descriptions for the model: The
descriptionfield determines when and how a model uses this tool. Clearly stating what it’s suitable for is more important than detailing technical specifics. - Parameter limits: Capping
maxResultsat 50 prevents models from requesting excessively large result sets that could overwhelm the context window. Since all content returned by tools eventually enters the model's context window, restraint is a virtue. - Path safety: A whitelist for
ROOTcombined with traversal checks forms the baseline security measure. Remember: the server’s permissions are the model’s permissions. Setting your root directory to/effectively grants AI access to read your entire hard drive.
Compile and run a trial:
npx tsc && node dist/index.js ~/Documents/notesIf the process silently waits for stdin input, it is functioning correctly—in stdio transport mode, the Host launches this as a child process and communicates via standard input/output streams.
Integrating with Claude Code and Cursor
Claude Code: Register by running a single command in your project:
claude mcp add file-search -- node /absolute/path/dist/index.js ~/Documents/notesAlternatively, declare it in the .mcp.json file at the project root (ideal for team sharing):
{
"mcpServers": {
"file-search": {
"command": "node",
"args": ["/absolute/path/dist/index.js", "/Users/you/Documents/notes"]
}
}
}Cursor: Write the same configuration format into ~/.cursor/mcp.json (global) or your project's .cursor/mcp.json. After restarting, you will see the Server status turn green in the MCP settings.
Once integrated, simply tell the assistant: "Use file-search to find content about the reimbursement process in my notes." You can then observe the model initiating a search_files call, retrieving results, and formulating an answer.
In stdio mode, the Host launches the Server; registration configuration only requires specifying the startup command and arguments.
Debugging: Inspector Is Your First Tool
The MCP official suite provides a powerful debugging utility called Inspector, which connects directly to your Server without routing through any AI application:
npx @modelcontextprotocol/inspector node dist/index.js ~/Documents/notesThis opens a debug interface in your browser where you can view the list of tools declared by the Server, manually input parameters to trigger calls, and inspect raw responses. Verify tool behavior with Inspector first before integrating with a Host; this saves significant time troubleshooting whether an issue lies with the Server or the model. Two other common debugging checkpoints:
- In stdio mode, do not write logs to stdout (this pollutes the protocol channel); route all debug information through
console.error. - If tools do not appear in the Host, it is usually due to a non-absolute path in the configuration or an incompatible Node version. Check the MCP logs within your Host (in Claude Code, run
claude mcp listto check status).
Ensure every tool runs successfully manually in Inspector before attempting model invocation—layered troubleshooting is always fastest.
Common Pitfalls
| Issue | Symptom | Fix |
|---|---|---|
| Logging to stdout | Host reports protocol parsing errors | Route all logs to stderr instead |
| Response payload too large | Context window overflows; responses become slow and degraded | Limit the number of results and snippet length, and provide pagination parameters |
| Vague tool descriptions | Model ignores or misuses tools | Clearly specify applicable scenarios and parameter meanings in the description |
| Root directory scope too broad | Sensitive files can be read | Use a whitelist of directories plus path traversal checks; default to the minimal necessary scope |
| Relative paths configured | Server fails to start | Always use absolute paths in Host configuration |
Target Audience and Next Steps
This guide is for developers who want to integrate internal data or private tools into AI applications like Claude Code or Cursor. Once you have a working implementation, three natural extensions follow: upgrade the search functionality to Embedding-based semantic retrieval (paired with a vector database to create a private RAG Server); add resources capabilities so the Host can read full file contents directly; or switch to Streamable HTTP transport to deploy as a remote server for team-wide use. After deploying remotely, authentication and permission control become essential—refer to the tool permission design in "Why AI Agents Tend to Lose Control".
If you simply want to leverage existing capabilities without writing your own code: official and community servers for file systems, GitHub, databases, and more are already abundant. Search before you build.
Common Questions
Q: Is the Python implementation significantly different? A: The underlying logic is identical. The official Python SDK (FastMCP style) uses decorators to define tools, resulting in a comparable amount of code. Simply use whichever language your team finds most convenient.
Q: How many tools should be included in a single Server? A: Aim for cohesion by responsibility; typically 3–10 tools. Too many tools consume excessive context and can confuse the model's selection process. Split unrelated capabilities into separate Servers, allowing users to mount them as needed.
Q: How do I control which operations require confirmation? A: The confirmation mechanism resides on the Host side: both Claude Code and Cursor will prompt users for approval before executing tool calls (with configurable whitelists). On the Server side, your responsibility is to separate read and write actions into distinct tools; never hide dangerous operations behind innocuous names.
Summary
The minimal composition of an MCP Server boils down to three tasks: declaring identity, registering tools, and handling transport. By rigorously implementing parameter validation and path safety, verifying with the Inspector before connecting to a Host, you equip your AI assistant with another genuinely usable capability. Starting from this skeleton of roughly 100 lines, simply replace the search logic to build any Server you need—whether for database queries, internal APIs, ticketing systems, or beyond.