MCP Reference Servers: How Anthropic Teaches You to Build for Claude

May 15, 2025

|repo-review

by Florian Narr

MCP Reference Servers: How Anthropic Teaches You to Build for Claude

Seven reference implementations. One TypeScript monorepo. The entire point is to show — not just explain — how to build servers that give LLMs controlled, auditable access to the outside world.

What it does

modelcontextprotocol/servers is the canonical reference for MCP server implementations. It ships seven servers: filesystem, memory, fetch, git, time, everything, and sequentialthinking. These aren't production libraries — the README says so explicitly. They're teaching material for the protocol.

Why I starred it

The MCP spec is abstract until you see it move. These servers are the "how" that the protocol spec leaves out. I wanted to know how path sandboxing actually works when you hand a model access to your disk, how the memory server maintains a knowledge graph without a database, and what the "sequential thinking" server is actually doing under the hood — because the name sounds like vaporware but the implementation is surprisingly principled.

How it works

Every server follows the same shape: instantiate McpServer from @modelcontextprotocol/sdk, register tools with Zod schemas, connect over StdioServerTransport. The protocol message passing is invisible — you just write tool handlers.

The interesting engineering is per-server.

Filesystem server (src/filesystem/index.ts) gets the most attention and it shows. The sandboxing logic in src/filesystem/path-validation.ts is properly paranoid. Every path that comes in goes through isPathWithinAllowedDirectories, which calls path.resolve(path.normalize(...)) to collapse traversal sequences, rejects null bytes, and then does a prefix check that handles Windows drive roots as a special case. Symlinks are resolved at startup — the server stores both the original and the realpath result — specifically to handle macOS's /tmp/private/tmp redirection. That's the kind of detail that only shows up if someone actually ran this on a Mac and got confused about why their paths weren't matching.

// src/filesystem/path-validation.ts
if (absolutePath.includes('\x00')) {
  return false;
}

let normalizedPath: string;
try {
  normalizedPath = path.resolve(path.normalize(absolutePath));
} catch {
  return false;
}

The server also implements dynamic roots: when a client sends roots/list_changed, the server calls server.server.listRoots() and replaces the allowed directories mid-session. This means Claude Desktop can give the server access to a project folder when you open it, rather than configuring directories at startup.

Memory server (src/memory/index.ts) is the most architecturally interesting. It maintains a knowledge graph — entities with typed observations, plus relations between entities — stored as JSONL. No SQLite, no embedding model, no vector store. The KnowledgeGraphManager loads and saves the whole file on every operation, which is fine for the use case but means it won't scale to thousands of nodes. The search is a simple substring match over entity names, types, and observation strings. What's notable is that openNodes explicitly includes relations where either endpoint matches — an earlier version required both endpoints to be in the requested set, which meant you'd query a node and silently drop its connections to nodes you didn't ask for.

// src/memory/index.ts — openNodes
const filteredRelations = graph.relations.filter(r => 
  filteredEntityNames.has(r.from) || filteredEntityNames.has(r.to)
);

Sequential thinking server exposes a single sequentialthinking tool that takes a thought string, a thought number, and a boolean nextThoughtNeeded. The model calls it repeatedly, each call logging a reasoning step. The server tracks branches (branchFromThought, branchId) and revisions (isRevision, revisesThought). There's no output that affects the model's context — the tool just returns a formatted summary of the thought back to the model. The value is in structuring how the model reasons over multi-step problems, not in any computation the server does. Recent commit #3533 fixed a real bug: z.coerce.boolean() coerces "false" to true because Boolean("false") === true. They replaced it with a manual z.preprocess that actually checks the string value.

// src/sequentialthinking/index.ts
const coercedBoolean = z.preprocess((val) => {
  if (typeof val === "string") {
    if (val.toLowerCase() === "false") return false;
  }
  return val;
}, z.boolean());

That's a footgun that catches people who reach for z.coerce without checking what it does with strings.

Using it

# filesystem server: allow access to a single directory
npx -y @modelcontextprotocol/server-filesystem /Users/you/projects

# memory server: set a custom file path
MEMORY_FILE_PATH=/tmp/my-graph.jsonl npx -y @modelcontextprotocol/server-memory

# git server: standard stdio transport, integrates with Claude Desktop
npx -y @modelcontextprotocol/server-git

For Claude Desktop, you wire these up in claude_desktop_config.json:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/you/projects"]
    }
  }
}

The everything server is specifically for testing MCP clients — it implements every resource type (prompts, resources, tools) so you can verify your client handles them all correctly. Not useful in production.

Rough edges

The memory server's JSONL serialization means it rewrites the entire file on every write. For a personal assistant storing a few hundred entities this is fine. For anything heavier it'll be a problem — and the fact that it stores the file next to the installed package (unless you set MEMORY_FILE_PATH) means upgrades can wipe your graph if you're not careful. The README calls this out now after an apparent migration incident.

The README also points clearly at the MCP Registry for community servers — this repo is deliberately not trying to be a catalog. That's the right call for keeping the reference implementations minimal, but it means you'll hit the boundary quickly if you want a PostgreSQL server or a Slack integration.

Tests exist (__tests__/ directories with Vitest) for both the filesystem and memory servers. The path validation has good coverage. The sequential thinking server has no tests, which tracks — it's essentially a state machine that returns formatted strings.

Bottom line

If you're building an MCP server, read src/filesystem/ first. The sandboxing and dynamic roots implementation is the pattern to follow. If you're experimenting with giving Claude memory, the memory server is a usable starting point — just set MEMORY_FILE_PATH to somewhere you control.

modelcontextprotocol/servers on GitHub
modelcontextprotocol/servers