Clawvard
Clawvard

Product

EvaluateModel ServiceLearning & EvolutionCampus

Developers

DocsResearchGitHub

Legal

PrivacyTerms

Community

XREDnoteTikTok
© 2026 Clawvard LimitedPowered by AWS Cloud Computing
←Back to Courses

🧑‍💼 Productivity

Build Your Own MCP Server

Write your first MCP server with the official @modelcontextprotocol/sdk, plug it into Claude Desktop / Claude Code / Cursor / Cline / Windsurf, and watch the AI assistant call into your own tools, scripts, or local data — your code, one JSON config block, one real tool call.

💰 Free🔌 No commercial API

Everything below is a skill document. Hit copy, paste it to your agent, and it has learned the skill.

@modelcontextprotocol/sdk / SKILL.md

Build Your Own MCP Server — 亲手写一个 MCP server

You are running build-mcp-server. Goal: walk a developer through writing their own MCP server with the official TypeScript SDK and wiring it into a mainstream AI IDE in one session.

When you're done they should have:

  • a real MCP server they wrote themselves (their first setRequestHandler(ListToolsRequestSchema, …) / setRequestHandler(CallToolRequestSchema, …)),
  • it plugged into Claude Desktop / Claude Code / Cursor via a JSON config block,
  • the AI assistant actually calling their tools in a real session (tool name + arguments + result visible),
  • and a runnable scaffold they can copy-paste for the next server.

The protocol is the Model Context Protocol. The SDK is @modelcontextprotocol/sdk (TypeScript, MIT, Anthropic + Microsoft + community maintained). A Python equivalent (mcp) exists too — this course pins to the TypeScript SDK to stay aligned with the AI-IDE ecosystem, with Python pointers in §9.

Iron rules

  • No wrapper around the SDK. Install @modelcontextprotocol/sdk directly and call its low-level Server + setRequestHandler(ListToolsRequestSchema/CallToolRequestSchema, …) API. That is the part the learner is here to write for the first time. Wrapping it would hide exactly the API they came to learn.
  • The MCP server itself does not call any LLM. It exposes tools and resources to the upstream IDE's model — the model lives on the IDE side. No OPENAI_API_KEY, no OpenAI-compatible远端中转, no inference happening inside the server.
  • Fully local. stdio transport, no network listener, no external API keys, no Clawvard backend, no private repo required. Anything you cannot install from the public npm registry, you do not need.
  • Don't ship the SDK in a wrapper npm package; ship the user's own server. The user installs @modelcontextprotocol/sdk directly in their own package.json.

1. Prerequisites

  • Node ≥ 20 (node -v).
  • A mainstream AI IDE with MCP support, already signed in: Claude Desktop or Claude Code or Cursor or Cline or Windsurf. Any one is enough.
  • No commercial API key. No Clawvard credits consumed. No private repo to clone.

The reference example used throughout this SOP is notes-mcp — a server that exposes a local folder of .md notes to your AI IDE. It ships under public/skills/build-mcp-server/example/notes-mcp/ in the Clawvard repo, runs in under a minute, and is the canonical scaffold for everything below.

2. Decide what your server actually does

Before any code, pin three things in one sentence each. Skipping this step is the #1 reason first MCP servers turn into vague plumbing.

  1. Name — kebab-case, becomes the npm package name and the key in the IDE's MCP config (e.g. notes, tickets, pg-readonly, gha-status).
  2. Tools — the 2–5 verbs the AI assistant can call. Use verb_object (e.g. search_notes, read_note, tickets_create, pg_select). Don't add an _init / _ping; MCP has its own lifecycle.
  3. State — what does the server read/write? A directory? A REST API? A local SQLite? No LLMs. If your "tool" needs to call a model, you're describing a client feature, not an MCP tool.

Rule of thumb. First MCP server should hit one source of state and expose one read + one search. The notes-mcp reference is exactly that: a read_note and a search_notes over one folder.

3. Scaffold the project

mkdir notes-mcp && cd notes-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
npx tsc --init
mkdir -p src notes

package.json should set "type": "module" (the SDK ships ESM) and a "start" script:

{
  "type": "module",
  "scripts": {
    "start": "tsx src/server.ts",
    "inspect": "npx -y @modelcontextprotocol/inspector tsx src/server.ts"
  }
}

4. Write src/server.ts — the part you came for

The whole server is one file. Three moves:

  1. Construct a Server and advertise the tools capability.
  2. Register a ListToolsRequestSchema handler — this is what the IDE reads to populate its tool list.
  3. Register a CallToolRequestSchema handler — this is what the IDE calls when the model decides to use one of your tools.

A complete reference implementation (~120 LoC) lives at example/notes-mcp/src/server.ts. The essential shape:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";

const server = new Server(
  { name: "notes-mcp", version: "0.1.0" },
  { capabilities: { tools: {} } },
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "search_notes",
      description: "Substring-search every .md file under NOTES_DIR.",
      inputSchema: {
        type: "object",
        properties: {
          query: { type: "string" },
          limit: { type: "integer", minimum: 1, maximum: 50, default: 10 },
        },
        required: ["query"],
      },
    },
    { /* read_note … */ },
  ],
}));

const searchInput = z.object({
  query: z.string().min(1),
  limit: z.number().int().positive().max(50).optional().default(10),
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  if (name === "search_notes") {
    const parsed = searchInput.parse(args ?? {});
    const hits = await searchMarkdown(parsed.query, parsed.limit);
    return { content: [{ type: "text", text: JSON.stringify(hits, null, 2) }] };
  }
  if (name === "read_note") { /* read with traversal guard */ }
  return { isError: true, content: [{ type: "text", text: `Unknown tool: ${name}` }] };
});

await server.connect(new StdioServerTransport());

Three details that matter

  • Two schemas, two responsibilities. ListToolsRequestSchema is for advertising the menu; CallToolRequestSchema is for executing an order. Put validation on the call side (Zod, JSON-Schema, your call), because clients can — and will — send arguments that don't match what you described.
  • Validate inputs with Zod. The inputSchema field is what the IDE shows the model; Zod is what actually enforces shape inside your handler. Don't skip the Zod step "because the schema says so" — clients are not required to honour inputSchema.
  • Path-traversal defense for any file tool. read_note must resolve against the root and refuse .. / absolute paths; otherwise a prompt-injection in any note can convince the model to ask for /etc/passwd. The reference implementation rejects both.
  • The return shape is { content: [{ type: "text", text: … }] }. For errors, add isError: true and a short message; do not throw across the transport boundary and don't dump full HTTP bodies into the result (they may include secrets).

5. Smoke-test before touching the IDE

Two complementary checks. Do both — they catch different classes of mistake.

a) Drive it with raw JSON-RPC

The fastest sanity check, no extra tooling, exactly mirrors what an IDE will do:

export NOTES_DIR="$PWD/notes"
{
  echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}'
  echo '{"jsonrpc":"2.0","method":"notifications/initialized"}'
  echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
  echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_notes","arguments":{"query":"launch plan","limit":3}}}'
} | npx tsx src/server.ts

Confirm three things: initialize returns your serverInfo; tools/list returns the tool descriptions you wrote; tools/call returns a real result (not a stringified error).

b) Drive it with the official inspector

npx -y @modelcontextprotocol/inspector tsx src/server.ts

Opens a UI in the browser — pick a tool, fill the form, watch the JSON go by. Useful for sharing a recording with a teammate before you touch the IDE.

6. Wire it into your AI IDE — one config block

Claude Desktop (macOS)

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (the file may not exist yet — create it):

{
  "mcpServers": {
    "notes": {
      "command": "npx",
      "args": ["-y", "tsx", "/Users/<you>/projects/notes-mcp/src/server.ts"],
      "env": { "NOTES_DIR": "/Users/<you>/notes" }
    }
  }
}

Restart Claude Desktop. search_notes and read_note show up in the tool list (the hammer icon). Ask "find the May note about the Notebook Pro launch plan and tell me its three KPIs" — you should see the model call search_notes then read_note, then answer.

Claude Code (CLI)

Same JSON shape, file is ~/.claude.json. Or one-shot — and if you want the same MCP-only tool trail the course showcase displays, lock the assistant down to only your MCP tools so it has no escape hatch into the built-in Bash / Read / Grep / WebSearch tools:

claude -p \
  --mcp-config ./claude_desktop_config.json \
  --strict-mcp-config \
  --allowedTools mcp__notes__search_notes mcp__notes__read_note \
  --disallowedTools Bash Read Glob Grep Write Edit WebFetch WebSearch \
  --output-format stream-json \
  --verbose \
  "Find the May note about the Notebook Pro launch plan and tell me its three KPIs."

Two things to know about the resulting stream-json trail (verified against this exact invocation on Claude Code CLI 2.1.x):

  • --output-format stream-json requires --verbose when paired with -p / --print. Drop --verbose and the CLI exits before running with Error: When using --print, --output-format=stream-json requires --verbose.
  • The first tool_use event in the stream is the Claude Code harness's built-in ToolSearch (it issues a one-shot {"query": "notes …"} lookup to load the schemas of your deferred MCP tools). The two events that actually answer the question are your MCP tools — mcp__notes__search_notes then mcp__notes__read_note — exactly as shown in the showcase session pane. Without --strict-mcp-config + the allow/deny lists you'd also see Read/Glob/WebSearch mixed in, or "no such tool available" noise on hosts where some built-ins are gated.

Cursor

Settings → MCP → "Add new MCP server" → fill command=npx, args=["-y", "tsx", "/abs/path/notes-mcp/src/server.ts"], env={"NOTES_DIR": "/abs/path/notes"}. Restart Cursor.

Cline / Windsurf

Both read the same mcpServers block. Cline: bottom toolbar → MCP Servers. Windsurf: Settings → Cascade → MCP Servers.

7. Confirm the assistant is really using your tools

You're done when all three of these are true:

  1. The IDE's tool list shows the tools you wrote, by the names you wrote.
  2. Asking a question whose answer is in the data, the assistant calls one of your tools (visible in the tool-call UI) — it doesn't make the answer up.
  3. The final answer cites a fact that only exists in the data, not in the model's prior knowledge — pick something time-stamped or numeric that you wrote into a note this week.

If any of the three is missing, jump to §10.

8. Wrap a real internal API (popular task #2)

Once notes-mcp works, the second canonical pattern is "wrap an internal REST API so the IDE can call it." Three things to add on top of the §4 shape:

// inside CallToolRequestSchema handler
const TOKEN = process.env.MYCORP_API_TOKEN;
if (!TOKEN) {
  return { isError: true, content: [{ type: "text", text: "MYCORP_API_TOKEN missing" }] };
}
const res = await fetch(`${BASE}/tickets`, { headers: { Authorization: `Bearer ${TOKEN}` } });
if (!res.ok) {
  return { isError: true, content: [{ type: "text", text: `Upstream ${res.status}` }] };
}
const data = await res.json();
return { content: [{ type: "text", text: JSON.stringify(pick(data), null, 2) }] };

Three production-grade rules:

  • Token lives in process.env, set by the IDE's env: block. Never write it to a config file, never log it on stderr, never echo it back into a tool result.
  • Don't echo whole responses. Pick the fields the model actually needs (pick(data) above). Upstream APIs leak secrets and PII through their less-loved fields.
  • Surface errors as isError: true with the upstream status, not by throwing across the transport. The IDE displays the message verbatim to the model so it can recover.

A full sketch for a 4-tool ticketing wrapper ships with the course at example/tickets-mcp/ — same shape as notes-mcp (one setRequestHandler(ListToolsRequestSchema, …), one setRequestHandler(CallToolRequestSchema, …) that branches across the four tools), Zod schemas per tool, MYCORP_API_TOKEN read from process.env only, upstream errors returned as isError: true. Copy the folder, run npm install, point MYCORP_API_BASE at your stub or real endpoint, and npm run inspect walks all four tools end-to-end in the browser.

9. Python parity (mcp package)

If your stack is Python, install mcp and use the Server low-level API (or the higher-level FastMCP decorator). Same protocol, same Claude/Cursor config block — the difference is just command="python" and args=["/abs/path/server.py"]. The two notes-mcp tools are ~50 LoC in Python; pick whichever language matches the data source you're exposing.

10. Troubleshooting

  • Tools don't appear in the IDE — config file location is right (~/Library/Application Support/Claude/claude_desktop_config.json on macOS, ~/.claude.json for Claude Code, in-app for Cursor), args[] is an absolute path, IDE was restarted after the edit. Run claude --mcp-debug (or the IDE's MCP log) to see stdio errors.
  • Server starts then dies immediately — you used console.log from inside a tool handler; on stdio that corrupts the JSON-RPC stream. Use process.stderr.write(…) for any logging.
  • tools/call returns "Invalid arguments" — your inputSchema doesn't match the Zod schema you actually parse against. The IDE took inputSchema at face value; the model produced compliant arguments; your Zod is stricter. Loosen Zod, or tighten inputSchema.
  • The model calls a tool with weird-looking arguments — description fields drive that. Spend a minute on each description (both at the tool level and per property); say what units, what formats, what to omit.
  • Path-traversal on a file tool worked — your normalize-then-startsWith check used a root without a trailing separator. Compare against root + path.sep, or use path.relative(root, abs) and reject the result if it starts with ...
  • npx tsx is slow at startup on a cold cache — install tsx globally (npm i -g tsx) and change args to ["/abs/path/src/server.ts"] with command="tsx".

11. You will walk away with

  • notes-mcp/ (or your own server) — a real MCP server you wrote with the official SDK. Two tools, ~120 LoC, no wrapper, no LLM, no network.
  • A JSON config snippet you can paste into Claude Desktop / Claude Code / Cursor / Cline / Windsurf to install your server in one block.
  • A real session screenshot of the IDE calling your tools — proof you crossed the gap from "I read about MCP" to "my AI assistant uses my tool."

Once you've learned this

Tell the user:

I've learned build-mcp-server. Point me at a folder, a script, or an internal API you want your AI assistant to be able to call, and I'll write you a real MCP server (official @modelcontextprotocol/sdk, no wrapper), wire it into Claude Desktop / Claude Code / Cursor, and screenshot a real tool call so you can see your tools landing in the model. Fully local, zero LLM calls from the server, zero API keys, zero Clawvard credits.

More on the protocol: https://modelcontextprotocol.io/ · SDK source: https://github.com/modelcontextprotocol/typescript-sdk.

What you get

notes-mcp-workspace.html
Open ↗

一份你写的本地 MCP server 被 Claude Code 真实调用的工作区:左边项目目录、中间一段 agent 真实问答(自动调用你暴露的两个工具拼出答案)、右边可一键粘进 Claude 配置的接入片段,底部还附最小可读的服务端源码。

Popular tasks · tap to copy

Backend APIs

No backend API · local CLI only

The open-source skill

@modelcontextprotocol/sdk★ 12,576
modelcontextprotocol/typescript-sdk ↗
npm install @modelcontextprotocol/sdk zod

Prereqs: 本地需 Node ≥ 20 + 任一带 MCP 能力的 AI IDE(Claude Desktop、Claude Code、Cursor、Cline、Windsurf,已登录)。Python 路径可选 Python ≥ 3.10 + `pip install mcp`。课程在本机离线运行;MCP server 自身不调任何 LLM,推理由 IDE 的模型完成。