Build an MCP Server from Scratch: Plug Your Capabilities into Every AI Tool
The previous article covered the MCP protocol; this one is hands-on. Build a production-ready MCP Server from scratch: pick a scenario, use the official SDK to implement resources and tools, deploy locally via stdio or remotely via HTTP, debug with MCP Inspector, then wire it into Claude Code and Cursor.
Key Points
- Topic Focus: MCP、TypeScript
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
The previous article, "Deep Dive into the MCP Protocol," explained the "why" and "what the protocol looks like." This one is about "how"—we'll build a real MCP Server that Claude Code and Cursor can call directly: a "personal knowledge base search" service that exposes documents as resources and "search/summarize" as tools.
Once you complete this article, you'll have mastered the standard method for plugging any capability into the AI coding ecosystem.
1. Pick a scenario: what makes a good MCP candidate
Not every feature is worth turning into a Server. Criteria for what's worth packaging:
- Called repeatedly (one-shot features don't need this).
- Has state or data (connected to some database, API, or set of files).
- Reused across tools (more than one AI client wants to use it).
We pick "personal knowledge base search": expose a batch of local markdown notes to AI so the model can search("MCP") to search and read("notes/xxx.md") to read the full content. This is a hard requirement for any coding assistant.
2. Initialize the project
mkdir kb-mcp && cd kb-mcp
npm init -y
npm i @modelcontextprotocol/sdk zeta zod
npm i -D typescript @types/node tsx
We recommend pinning the SDK to a specific version (the MCP spec evolves quickly). Directory structure:
kb-mcp/
src/index.ts // Server entry
src/search.ts // Search logic (simple BM25 or grep)
notes/ // The markdown being searched
3. Implement the Server: resources + tools
// src/index.ts
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { listNotes, readNote, searchNotes } from "./search.js";
const server = new McpServer({ name: "kb-mcp", version: "0.1.0" });
// 1) Resources: expose each note as a readable resource
const notesResource = new ResourceTemplate("notes://{slug}", { list: undefined });
server.resource(
"note",
notesResource,
async (uri, { slug }) => ({
contents: [{ uri: uri.href, mimeType: "text/markdown", text: await readNote(slug as string) }],
}),
);
// 2) Tools: search + read entry points
server.tool(
"search_notes",
"Full-text search across local markdown notes. Returns matching slugs and snippets.",
{ query: z.string().describe("keywords to search") },
async ({ query }) => {
const hits = await searchNotes(query);
return {
content: [{ type: "text", text: JSON.stringify(hits, null, 2) }],
};
},
);
server.tool(
"read_note",
"Read the full content of a note by its slug.",
{ slug: z.string() },
async ({ slug }) => ({
content: [{ type: "text", text: await readNote(slug) }],
}),
);
await server.connect(new StdioServerTransport());
The search logic here uses the plainest possible full-text matching (good enough, zero dependencies):
// src/search.ts
import fs from "node:fs/promises";
import path from "node:path";
const NOTES_DIR = path.resolve("notes");
export async function listNotes() {
return (await fs.readdir(NOTES_DIR)).filter((f) => f.endsWith(".md"));
}
export async function readNote(slug: string) {
return fs.readFile(path.join(NOTES_DIR, `${slug}.md`), "utf8");
}
export async function searchNotes(query: string) {
const files = await listNotes();
const q = query.toLowerCase();
const hits = [];
for (const f of files) {
const text = await readNote(f.replace(/\.md$/, ""));
const idx = text.toLowerCase().indexOf(q);
if (idx >= 0) {
hits.push({ slug: f.replace(/\.md$/, ""), snippet: text.slice(Math.max(0, idx - 40), idx + 80) });
}
}
return hits.slice(0, 10);
}
4. Local debugging: MCP Inspector
The official Inspector is a debugging powerhouse—it visualizes the resources/tools list and lets you invoke them manually:
npx @modelcontextprotocol/inspector npx tsx src/index.ts
It spins up a local web interface that lists every tool/resource your Server exposes; one click tests the call result—ten times faster than "configuring it in Claude Code and asking a question to see whether it called it." Use Inspector throughout the development phase, then wire up the client when you're done.
5. Connect to clients
Claude Code (project-level .mcp.json, shared with the repo):
{
"mcpServers": {
"kb": { "command": "npx", "args": ["tsx", "/abs/path/kb-mcp/src/index.ts"] }
}
}
Cursor (Settings → MCP → Add) fill in the same content. After restarting, ask "Do I have any notes about RAG in my notes"—the model will automatically search_notes(query:"RAG") → read_note(slug:"...") and answer.
6. Switch to a remote Server (HTTP)
To be shared by multiple clients or deployed to a server, switch to remote mode (Streamable HTTP):
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer } from "http";
const httpServer = createServer(async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res);
});
httpServer.listen(3001);
The client config changes from command/args to url: "https://kb.example.com/mcp". Remote mode must add authentication—validate Authorization: Bearer <token> at the req entry, otherwise your notes are exposed to anyone who knows the address.
7. Release and version management
- Pin the version:
@modelcontextprotocol/sdkupgrades occasionally change the API; lock the version in production. - Provide a README: clearly write out which tools are exposed and each tool's parameters and purpose—this is both user documentation and a source of material when you write the tool
description. - Semantic logs: log one structured line per tool call (who/what/args/latency) so remote Server issues are easier to investigate.
Summary
The complete loop for building an MCP Server: pick a scenario → implement resources/tools with the SDK → debug with Inspector → connect clients → (optional) switch to remote + add auth → release.
The key to the whole thing is not the amount of code (the core is only a few dozen lines), but the idea of standardizing "your unique data/capabilities" according to the MCP contract. Once packaged, it simultaneously enters the capability pool of Claude Code, Cursor, Cline, and Continue—coverage that previously required N separate integrations. Turning your product/data into an MCP Server is the lowest-cost "be integrated" strategy of the AI coding era.
Related
Related Content
Article
Why Does MCP Matter? Understanding the AI Tool Protocol
Technical trends, engineering practices, and an independent developer perspective on MCP and AI Agents.
Article
Core Architecture of AI Agents: Planning, Memory, Tools, and Execution
Technical trends, engineering practices, and independent developer perspectives around AI Agents and large language models.
Article
SEO Design for a Content Community with Next.js
Technical trends, engineering practices, and an indie developer's perspective on Next.js and TypeScript.
Article
How Should RAG and Agents Work Together?
An independent developer's perspective on technical trends and engineering practices involving RAG and AI agents.