Tutorial

How to set up an MCP server for Claude and Cursor (beginner-friendly, 2026)

MCP is everywhere in 2026 but most tutorials assume you already know what a server is. This guide starts from zero, ships a real working server, and walks you through wiring it to Claude, Cursor and Antigravity.

May 18, 202614 min readCruxBit Team

We've written about [[mcp-explained-2026|what MCP is and why it matters]]. This post is the other half — actually building one. By the end you'll have a working MCP server in TypeScript that exposes a tool, a resource and a prompt, and it'll be wired into Claude Desktop, Cursor, and Antigravity. Total time: about an hour, including reading.

Who this is for

Anyone who can run `npm install` and read TypeScript. You don't need to know what a JSON-RPC framing protocol is. By the end you'll have built one without ever having to think about it.

1. Mental model in 30 seconds

MCP — who talks to whom
┌─────────────┐    JSON-RPC over stdio / SSE / WebSocket
│ MCP CLIENT  │◄─────────────────────────────────────────►┌─────────────┐
│ (Claude,    │                                           │ MCP SERVER  │
│  Cursor,    │    list_tools, call_tool,                 │ (yours)     │
│  Antigrav…) │    list_resources, read_resource,         │             │
│             │    list_prompts, get_prompt               │   Tools     │
│             │                                           │   Resources │
│             │                                           │   Prompts   │
└─────────────┘                                           └─────────────┘
       │                                                          │
       │  The CLIENT decides when to call. The SERVER             │
       │  exposes the menu. The LLM inside the client             │
       │  picks from the menu when it needs to.                   │
       └──────────────────────────────────────────────────────────┘

Three things a server can offer: tools (functions the LLM can call), resources (data the client can attach to context), and prompts (templated user-invokable instructions). You don't need all three. Start with tools — that's where 90% of the value lives.

2. Prerequisites

  • Node.js 20+ installed (node -v to check)
  • An MCP client to test against (Claude Desktop is the easiest)
  • A text editor (VS Code, Cursor, whatever)

3. Scaffold the project

bash
mkdir my-first-mcp && cd my-first-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init

Open the generated tsconfig.json and confirm "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext". Set "outDir": "./dist".

Then add "type": "module" to your package.json. MCP servers in 2026 are ESM-first.

4. Write the server

Create src/index.ts. This minimal server exposes one tool that returns the current weather (mocked) and one resource (a static markdown doc).

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

const server = new Server(
  { name: "weather-demo", version: "0.1.0" },
  { capabilities: { tools: {}, resources: {} } }
);

// ── Tools ────────────────────────────────────────────────────────
const GetWeatherInput = z.object({
  city: z.string().describe("City name, e.g. 'Bengaluru'"),
});

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "get_weather",
      description: "Get current weather for a city (mocked).",
      inputSchema: {
        type: "object",
        properties: {
          city: { type: "string", description: "City name" },
        },
        required: ["city"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (req.params.name !== "get_weather") {
    throw new Error(`Unknown tool: ${req.params.name}`);
  }
  const { city } = GetWeatherInput.parse(req.params.arguments);
  // Replace with a real API call when you're ready.
  return {
    content: [{
      type: "text",
      text: `Weather in ${city}: 27°C, partly cloudy. (mocked)`,
    }],
  };
});

// ── Resources ────────────────────────────────────────────────────
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: "weather://docs/about",
      name: "About this server",
      mimeType: "text/markdown",
    },
  ],
}));

server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
  if (req.params.uri === "weather://docs/about") {
    return {
      contents: [{
        uri: req.params.uri,
        mimeType: "text/markdown",
        text: "# Weather Demo\nA tiny MCP server built as a teaching example.",
      }],
    };
  }
  throw new Error(`Unknown resource: ${req.params.uri}`);
});

// ── Start ────────────────────────────────────────────────────────
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("weather-demo MCP server running on stdio");

Why console.error and not console.log?

stdout is the MCP transport channel — writing to it corrupts the protocol. Always log to stderr in MCP servers. This is the #1 "why doesn't my server connect" mistake.

5. Build and smoke-test

bash
# Quick run (no build, useful in dev)
npx tsx src/index.ts

# Production build
npx tsc
node dist/index.js

The process should sit there silently (stdout is reserved for MCP). If it crashes, you'll see the error on stderr. Kill it with Ctrl+C; we'll let the client launch it next.

6. Wire it into Claude Desktop

Find your Claude Desktop config:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json
claude_desktop_config.json
json
{
  "mcpServers": {
    "weather-demo": {
      "command": "node",
      "args": [
        "/absolute/path/to/my-first-mcp/dist/index.js"
      ]
    }
  }
}

Restart Claude Desktop (fully quit, not just close the window). You should see a tools icon (🔌) in the conversation input. Click it — your weather-demo server should be listed with one tool. Try: "What's the weather in Bengaluru using the get_weather tool?"

7. Wire it into Cursor

~/.cursor/mcp.json
json
{
  "mcpServers": {
    "weather-demo": {
      "command": "node",
      "args": ["/absolute/path/to/my-first-mcp/dist/index.js"]
    }
  }
}

Restart Cursor. Confirm in Settings → Features → MCP — green dot = connected. Ask the agent to use your tool.

8. Wire it into Antigravity

Same shape, different file: ~/.antigravity/mcp.json. See our [[how-to-use-google-antigravity-2026|Antigravity guide]] for the full path and options.

9. Debugging the inevitable "it doesn't show up"

  1. 1Use an absolute path — relative paths won't work; the client doesn't know where you launched from
  2. 2Check stderr — run the same command in a terminal first. If it crashes there, it'll crash for the client
  3. 3Confirm the build ranls dist/index.js. The most common bug is editing TS and forgetting to rebuild
  4. 4Fully restart the client — Claude Desktop, Cursor and Antigravity all cache the MCP list; a soft reload won't pick up new servers
  5. 5Use the MCP Inspectornpx @modelcontextprotocol/inspector node dist/index.js launches a UI you can hit your server with directly. Best debugging tool by far

10. From toy to real — what to add next

  • Replace mock with real APIs — swap the weather mock for an OpenWeather or Open-Meteo call
  • Auth — read API keys from process.env and pass them in via the env block in the client config
  • SSE / HTTP transport — for servers that need to run remotely or across a network, switch from StdioServerTransport to the SSE transport. Same handler code
  • Prompts — add ListPromptsRequestSchema and GetPromptRequestSchema to expose reusable templated prompts the user can invoke from the client
  • Pagination on resources — for large catalogs, support cursor in ListResourcesRequestSchema

11. Patterns that will save you pain later

  • One tool, one job. Don't build a Swiss-army do_stuff with a mode parameter. Models pick from a menu — keep the menu legible
  • Tool names like English verbs. create_invoice, not invoiceMgmtAction. The LLM reads names to decide what to call
  • Detailed input schemas with descriptions. Every field needs .describe(). The model uses these to know what to pass
  • Structured errors. Return { isError: true, content: [...] } with a clear message. Don't throw for expected failures — the model can recover from a clear error message; it can't recover from a crashed server
  • Idempotent tools where possible. Models retry; a tool that double-charges on retry is a bad day

12. Publishing your server

If your server is useful beyond your team, publish it. The community catalog at github.com/modelcontextprotocol/servers accepts PRs. Internal-only servers can ship as a private npm package — clients will run them via npx the same way.

TL;DR

  • MCP server = small Node process exposing tools / resources / prompts over JSON-RPC
  • Use the official @modelcontextprotocol/sdk; never write the wire format yourself
  • Log to stderr, not stdout (stdout is the protocol)
  • Use absolute paths in client configs; fully restart the client after editing
  • Debug with the MCP Inspector before debugging through a client
  • One tool, one job, English verb names, descriptive schemas

Building an MCP server for a client product or internal tooling and want a code review or production-hardening pass? Drop us a line — we ship MCP servers for clients almost every week and would be glad to take a look.

#MCP#Tutorial#AI#TypeScript#Claude#Cursor

Have a project?

Building something we've just written about?

Drop us a line. We respond within 24 hours with a candid, no-pressure take on whether we're the right partner.