Build Your First MCP Server in Under an Hour: A Developer’s Hands-On Guide

Build Your First MCP Server in Under an Hour: A Developer’s Hands-On Guide

The Model Context Protocol (MCP) is an open standard that lets AI assistants like Claude talk directly to your tools, files, and APIs — without custom prompt hacks or brittle middleware. If you’ve ever wanted to give an AI real access to your codebase, internal services, or local data, MCP is the clean, sanctioned way to do it. This tutorial skips the theory (check out our [MCP explainer](#) for that) and gets you to a working server fast.

Prerequisites and Setup

Before writing a single line, make sure you have the following:

  • Node.js 18+ (or Python 3.10+ if you prefer the Python SDK)
  • Claude Desktop installed and signed in — this will be your MCP host for testing
  • Basic familiarity with async JavaScript or Python
  • A use case in mind — for this guide, we’ll expose a local project directory as a Resource and a simple file-search tool

Install the official MCP SDK:

“`bash
npm install @modelcontextprotocol/sdk
“`

Choosing a Transport

MCP supports multiple transports. For local development, stdio is the right choice — your server runs as a subprocess, Claude Desktop launches it, and they talk over standard input/output. No ports, no auth headers, no networking headaches. You’ll switch to SSE or HTTP transport only if you need a remotely hosted server.

Writing the Server

Create a file called `server.mjs`. Here’s the complete, annotated implementation:

“`javascript
import { McpServer } from “@modelcontextprotocol/sdk/server/mcp.js”;
import { StdioServerTransport } from “@modelcontextprotocol/sdk/server/stdio.js”;
import { readdir, readFile } from “fs/promises”;
import { join } from “path”;
import { z } from “zod”;

// 1. Initialize the server with a name and version
const server = new McpServer({
name: “local-files”,
version: “1.0.0”,
});

const PROJECT_DIR = process.env.PROJECT_DIR ?? process.cwd();

// 2. Register a Resource — something the AI can READ
// Resources are identified by a URI and return structured content
server.resource(
“project-listing”,
“file://project/listing”,
async (uri) => {
const entries = await readdir(PROJECT_DIR, { withFileTypes: true });
const lines = entries.map(
(e) => `${e.isDirectory() ? “[DIR] ” : “[FILE]”} ${e.name}`
);
return {
contents: [{ uri: uri.href, text: lines.join(“\n”), mimeType: “text/plain” }],
};
}
);

// 3. Register a Tool — something the AI can CALL with arguments
// Tools use Zod schemas for input validation (the SDK enforces this)
server.tool(
“read-file”,
“Read the contents of a file in the project directory”,
{ filename: z.string().describe(“Relative filename to read”) },
async ({ filename }) => {
const safePath = join(PROJECT_DIR, filename);
// Basic path-traversal guard
if (!safePath.startsWith(PROJECT_DIR)) {
return { content: [{ type: “text”, text: “Access denied.” }], isError: true };
}
const text = await readFile(safePath, “utf-8”);
return { content: [{ type: “text”, text }] };
}
);

// 4. Connect the transport and start listening
const transport = new StdioServerTransport();
await server.connect(transport);
“`

That’s under 50 lines for a fully functional MCP server with both a Resource and a Tool. A few things worth noting:

  • Resources are passive — they represent data the model can inspect (think: files, database snapshots, config). They’re identified by URIs and return content.
  • Tools are active — they accept validated arguments, execute logic, and return a result the model can reason about.
  • The Zod schema on the tool isn’t optional boilerplate; the SDK uses it to generate the JSON Schema that the AI host uses to understand what arguments to pass.
  • Error handling is explicit: return `isError: true` in the content object and the host will surface it cleanly rather than silently failing.

Connecting to Claude Desktop

Claude Desktop looks for MCP server configurations in a JSON file. Open (or create) the config at:

  • macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
  • Windows: `%APPDATA%\Claude\claude_desktop_config.json`

Add your server under the `mcpServers` key:

“`json
{
“mcpServers”: {
“local-files”: {
“command”: “node”,
“args”: [“/absolute/path/to/server.mjs”],
“env”: {
“PROJECT_DIR”: “/absolute/path/to/your/project”
}
}
}
}
“`

Save the file, then fully quit and relaunch Claude Desktop. The app spawns your server process on startup — a partial restart won’t pick up config changes.

Testing the Connection

Once Claude Desktop restarts, look for the hammer icon (🔨) in the chat input area. Click it — you should see `read-file` listed as an available tool. If you don’t:

1. Check Claude Desktop’s MCP logs at `~/Library/Logs/Claude/mcp-server-local-files.log` (macOS) for startup errors.
2. Verify the `command` and `args` paths are absolute, not relative.
3. Run `node /path/to/server.mjs` directly in your terminal — if it errors, you’ve found the problem.

Once connected, try asking Claude: “What files are in my project?” or “Read the contents of README.md.” Watch it invoke your tool in real time — the model will show a tool-use block before returning its answer.

Next Steps and Ecosystem Pointers

You’ve got a working MCP server. Here’s where to go next:

  • Official SDK docs: [modelcontextprotocol.io](https://modelcontextprotocol.io) — covers Prompts (the third primitive), sampling, and advanced transport options.
  • Community servers: The [MCP servers repository on GitHub](https://github.com/modelcontextprotocol/servers) has production-ready servers for PostgreSQL, GitHub, Brave Search, Slack, and dozens more — great for reading real patterns.
  • Ideas to extend this example:

– Add a `write-file` tool (with careful sandboxing)
– Expose a REST API endpoint as a Tool by wrapping `fetch` calls
– Add a Prompt primitive to give Claude a pre-built template for code review
– Package the server as an npm binary so teammates can install it with one command

The surface area of MCP is intentionally small — Resources, Tools, and Prompts cover a surprisingly wide range of integrations. Once you’ve internalized the pattern, connecting a new data source or capability is a matter of registering one more handler. The hard part isn’t the protocol; it’s deciding what to expose first.

Leave a Reply

Your email address will not be published. Required fields are marked *