#MCP#TypeScript

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
Author @admin·Published July 26, 2026·Updated July 26, 2026

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

bash
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:

text
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

ts
// 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):

ts
// 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:

bash
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):

jsonc
{
  "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):

ts
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/sdk upgrades 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