mech.app
Dev Tools

Building Your First MCP Tool: What readFile Reveals About Protocol Design

How Model Context Protocol handles tool registration, file system boundaries, and JSON-RPC transport for agent-to-context communication.

Source: dev.to
Building Your First MCP Tool: What readFile Reveals About Protocol Design

Model Context Protocol (MCP) from Anthropic is a JSON-RPC based standard for connecting AI agents to external tools and data sources. The canonical first tool is readFile, which exposes file system access to an agent. This tutorial reveals how MCP handles capability declaration, security boundaries, and transport layer design.

Protocol Architecture

MCP uses JSON-RPC 2.0 over stdio or HTTP. This choice differs from REST APIs in three ways:

  • Bidirectional messaging: The agent can call tools, and the server can push context updates without polling.
  • Stateful connections: A single transport session handles multiple tool calls, reducing handshake overhead.
  • Schema negotiation: Capabilities are declared once at connection time, not per-request.

The TypeScript SDK abstracts the transport layer. You write tool handlers as async functions. The SDK serializes parameters, routes requests, and manages the connection lifecycle.

Tool Declaration Structure

Every MCP tool requires three pieces of metadata:

ComponentPurposeExample
NameUnique identifier for tool invocationreadFile
DescriptionNatural language explanation for agent reasoning”Read contents of a file from the local filesystem”
Input SchemaJSON Schema defining required and optional parameters{ path: string, encoding?: string }

The agent receives this metadata during the initial capability handshake. It uses the description to decide when to call the tool. It uses the schema to validate parameters before sending the request.

interface ReadFileParams {
  path: string;
  encoding?: string;
}

interface ReadFileResponse {
  content: string;
  size: number;
  mimeType?: string;
}

server.tool({
  name: "readFile",
  description: "Read contents of a file from the local filesystem",
  inputSchema: {
    type: "object",
    properties: {
      path: { type: "string", description: "Absolute or relative file path" },
      encoding: { type: "string", enum: ["utf8", "base64"], default: "utf8" }
    },
    required: ["path"]
  },
  handler: async (params: ReadFileParams): Promise<ReadFileResponse> => {
    const resolvedPath = resolvePath(params.path);
    validatePath(resolvedPath);
    const content = await fs.readFile(resolvedPath, params.encoding || "utf8");
    const stats = await fs.stat(resolvedPath);
    return {
      content,
      size: stats.size,
      mimeType: mime.lookup(resolvedPath) || undefined
    };
  }
});

Security Boundaries

The readFile tool exposes file system access. MCP does not enforce sandboxing at the protocol level. You must implement access control in the tool handler.

Path traversal prevention:

function validatePath(requestedPath: string): void {
  const resolved = path.resolve(requestedPath);
  const allowed = path.resolve(process.env.MCP_ALLOWED_DIR || "./data");
  
  if (!resolved.startsWith(allowed)) {
    throw new Error(`Access denied: ${resolved} outside allowed directory`);
  }
}

File size limits:

const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB

const stats = await fs.stat(resolvedPath);
if (stats.size > MAX_FILE_SIZE) {
  throw new Error(`File too large: ${stats.size} bytes`);
}

Encoding validation:

Binary files should return base64. Text files should return UTF-8. The agent cannot distinguish between encodings without metadata.

Error Handling

MCP uses JSON-RPC error codes. The SDK maps JavaScript exceptions to protocol errors:

  • -32602: Invalid params (schema validation failure)
  • -32603: Internal error (file not found, permission denied)
  • -32000 to -32099: Application-defined errors

The agent receives the error code and message. It can retry with different parameters or escalate to the user.

class FileAccessError extends Error {
  code: number;
  constructor(message: string, code: number = -32603) {
    super(message);
    this.code = code;
  }
}

// In handler:
if (!await fileExists(resolvedPath)) {
  throw new FileAccessError("File not found", -32001);
}

State Management

MCP tools are stateless by design. Each tool call is independent. If you need to maintain state across calls, you have three options:

  1. Client-side state: The agent tracks state in its conversation context.
  2. Server-side cache: Store state in memory or a database, keyed by session ID.
  3. Explicit state parameters: Require the agent to pass state as tool parameters.

The readFile tool is naturally stateless. File system reads do not require session context.

Observability

The TypeScript SDK logs all tool calls to stdout by default. For production deployments, you should instrument handlers with structured logging:

handler: async (params: ReadFileParams): Promise<ReadFileResponse> => {
  const startTime = Date.now();
  logger.info("readFile.start", { path: params.path });
  
  try {
    const result = await performRead(params);
    logger.info("readFile.success", { 
      path: params.path, 
      size: result.size,
      duration: Date.now() - startTime 
    });
    return result;
  } catch (error) {
    logger.error("readFile.error", { 
      path: params.path, 
      error: error.message,
      duration: Date.now() - startTime 
    });
    throw error;
  }
}

Track these metrics:

  • Tool call frequency
  • Parameter distributions (which files are accessed most)
  • Error rates by error code
  • Latency percentiles

Deployment Shape

MCP servers run as separate processes. The agent spawns the server process and communicates over stdio. This has implications for deployment:

  • Process lifecycle: The server starts when the agent needs it and stops when the session ends.
  • Resource isolation: Each server runs in its own process with its own memory and file descriptors.
  • Crash recovery: If the server crashes, the agent can restart it without losing conversation context.

For HTTP transport, the server runs as a long-lived service. The agent connects over HTTP and maintains a persistent connection.

Failure Modes

File descriptor leaks: If you open files without closing them, the server will eventually hit the OS file descriptor limit. Use fs.promises or wrap sync calls in try-finally blocks.

Path traversal: If you do not validate paths, an agent can read arbitrary files. Always resolve paths and check against an allowed directory.

Encoding mismatches: If you return binary data as UTF-8, the agent receives corrupted content. Detect binary files and return base64.

Timeout handling: Large files can take seconds to read. The agent may timeout before the handler completes. Stream large files or return early with a progress indicator.

Technical Verdict

Use MCP when:

  • You need bidirectional communication between agents and tools.
  • You want to expose multiple tools through a single transport connection.
  • You need capability negotiation at connection time.

Avoid MCP when:

  • You only need one-shot tool calls (use REST APIs instead).
  • You need fine-grained access control per tool call (MCP delegates security to tool handlers).
  • You need to support non-JSON-RPC clients (MCP is tightly coupled to the protocol).

The readFile tool is a good starting point. It reveals the core patterns: schema declaration, parameter validation, error handling, and security boundaries. Once you understand these patterns, you can build more complex tools that expose databases, APIs, or compute resources.

Tags

agentic-ai orchestration infrastructure

Primary Source

dev.to