Blog/How to Build an MCP Server for Cursor: Complete Tutorial

How to Build an MCP Server for Cursor: Complete Tutorial

Want to extend Cursor AI with custom capabilities? The Model Context Protocol (MCP) lets you connect Cursor to any data source or tool you can imagine. This tutorial walks you through building your...

C
CAS Team
December 1, 20249 min read

How to Build an MCP Server for Cursor: Complete Tutorial

Want to extend Cursor AI with custom capabilities? The Model Context Protocol (MCP) lets you connect Cursor to any data source or tool you can imagine. This tutorial walks you through building your first MCP server from scratch using TypeScript.

By the end, you'll have a working MCP server that adds new tools to Cursor's Agent mode—no prior MCP experience required.

We'll create an MCP server that gives Cursor the ability to fetch and analyze any webpage, search the web, and remember context across sessions. These are practical tools you'll actually use daily.

What is the Model Context Protocol?

MCP is an open standard that connects AI assistants like Cursor to external tools and data. Think of it as a plugin system—instead of building custom integrations for each AI application, you build one MCP server that works everywhere MCP is supported.

Key concepts:
  • MCP Servers expose tools, resources, and prompts to AI applications
  • MCP Clients (like Cursor) discover and use these capabilities
  • Transport handles communication between server and client (stdio or HTTP)

---

Prerequisites

Before starting, make sure you have:

  • Node.js 18+ installed
  • Cursor IDE (latest version with MCP support)
  • Basic TypeScript knowledge (we'll explain MCP-specific parts)

MCP tools only work in Cursor's Agent mode, not Ask mode. Make sure you're using Agent mode when testing.

---

Part 1: Setting Up Your MCP Server Project

``bash

mkdir my-cursor-mcp && cd my-cursor-mcp

npm init -y

`

`bash

npm install @modelcontextprotocol/sdk zod

npm install -D typescript @types/node

`

The MCP SDK provides server infrastructure, and Zod handles input validation.

Create tsconfig.json:

`json

{

"compilerOptions": {

"target": "ES2022",

"module": "NodeNext",

"moduleResolution": "NodeNext",

"strict": true,

"esModuleInterop": true,

"skipLibCheck": true,

"outDir": "./dist"

},

"include": ["src/*/"]

}

`

Add these fields to your package.json:

`json

{

"type": "module",

"bin": {

"my-cursor-mcp": "./dist/index.js"

},

"scripts": {

"build": "tsc",

"start": "node dist/index.js"

}

}

`

---

Part 2: Building the Server

Create src/index.ts with the basic MCP server structure:

`typescript

#!/usr/bin/env node

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";

// Create the MCP server

const server = new Server(

{

name: "my-cursor-mcp",

version: "1.0.0",

},

{

capabilities: {

tools: {},

},

}

);

// Define your tools

const TOOLS = [

{

name: "fetch_webpage",

description: "Fetches and extracts text content from a webpage URL",

inputSchema: {

type: "object" as const,

properties: {

url: {

type: "string",

description: "The URL to fetch",

},

},

required: ["url"],

},

},

{

name: "remember",

description: "Stores a piece of information for later recall",

inputSchema: {

type: "object" as const,

properties: {

key: {

type: "string",

description: "A unique key to identify this memory",

},

value: {

type: "string",

description: "The information to remember",

},

},

required: ["key", "value"],

},

},

{

name: "recall",

description: "Retrieves previously stored information",

inputSchema: {

type: "object" as const,

properties: {

key: {

type: "string",

description: "The key to look up",

},

},

required: ["key"],

},

},

];

// Simple in-memory storage for the remember/recall tools

const memory = new Map();

// Handle list_tools requests

server.setRequestHandler(ListToolsRequestSchema, async () => {

return { tools: TOOLS };

});

// Handle tool execution

server.setRequestHandler(CallToolRequestSchema, async (request) => {

const { name, arguments: args } = request.params;

switch (name) {

case "fetch_webpage": {

const url = (args as { url: string }).url;

try {

const response = await fetch(url);

const html = await response.text();

// Basic HTML to text extraction

const text = html

.replace(/(?:(?!<\/script>)<[^<])*<\/script>/gi, "")

.replace(/(?:(?!<\/style>)<[^<])*<\/style>/gi, "")

.replace(/<[^>]+>/g, " ")

.replace(/\s+/g, " ")

.trim()

.slice(0, 5000); // Limit response size

return {

content: [{ type: "text", text }],

};

} catch (error) {

return {

content: [

{ type: "text", text: Error fetching URL: ${error} },

],

isError: true,

};

}

}

case "remember": {

const { key, value } = args as { key: string; value: string };

memory.set(key, value);

return {

content: [{ type: "text", text: Remembered "${key}" }],

};

}

case "recall": {

const { key } = args as { key: string };

const value = memory.get(key);

if (value) {

return {

content: [{ type: "text", text: value }],

};

}

return {

content: [{ type: "text", text: No memory found for "${key}" }],

};

}

default:

return {

content: [{ type: "text", text: Unknown tool: ${name} }],

isError: true,

};

}

});

// Start the server

async function main() {

const transport = new StdioServerTransport();

await server.connect(transport);

console.error("MCP Server running on stdio");

}

main().catch(console.error);

`

Build the Server

`bash

npm run build

`

---

Part 3: Connecting to Cursor

Now let's configure Cursor to use your MCP server.

Go to File → Preferences → Cursor Settings (or press Cmd+, on Mac)

Find the MCP section in the settings sidebar

Click Add new MCP server and enter:

- Name: my-cursor-mcp

- Command: node

- Args: /absolute/path/to/my-cursor-mcp/dist/index.js

Toggle the server to enabled. You should see a green indicator when it's running.

Alternative: Configuration File

You can also configure MCP servers via JSON. Create ~/.cursor/mcp.json:

`json

{

"mcpServers": {

"my-cursor-mcp": {

"command": "node",

"args": ["/absolute/path/to/my-cursor-mcp/dist/index.js"]

}

}

}

`

For project-specific servers, create .cursor/mcp.json in your project root.

Always use absolute paths in your MCP configuration. Relative paths may not resolve correctly when Cursor spawns the server process.

---

Part 4: Testing Your MCP Server

With your server connected, switch to Agent mode in Cursor and test your tools:

Test the Fetch Tool

`

Please fetch the content from https://example.com and summarize it

`

Cursor should use your fetch_webpage tool to retrieve the page content.

Test Memory Tools

`

Remember that the project deadline is December 31st with key "deadline"

`

Then later:

`

What was the deadline I told you to remember?

`

---

Part 5: Adding More Advanced Tools

Let's add a more useful tool—web search using a public API:

`typescript

// Add to your TOOLS array

{

name: "search_web",

description: "Searches the web and returns relevant results",

inputSchema: {

type: "object" as const,

properties: {

query: {

type: "string",

description: "The search query",

},

limit: {

type: "number",

description: "Maximum number of results (default: 5)",

},

},

required: ["query"],

},

}

// Add to your switch statement in the CallToolRequestSchema handler

case "search_web": {

const { query, limit = 5 } = args as { query: string; limit?: number };

try {

// Using DuckDuckGo's instant answer API (no API key required)

const response = await fetch(

https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json

);

const data = await response.json();

const results = [];

if (data.AbstractText) {

results.push(Summary: ${data.AbstractText});

}

if (data.RelatedTopics) {

for (const topic of data.RelatedTopics.slice(0, limit)) {

if (topic.Text) {

results.push(- ${topic.Text});

}

}

}

return {

content: [{

type: "text",

text: results.length > 0

? results.join("\n\n")

: "No results found"

}],

};

} catch (error) {

return {

content: [{ type: "text", text: Search error: ${error} }],

isError: true,

};

}

}

`

---

Part 6: Best Practices

title="Clear Descriptions"

description="Write detailed tool descriptions—Cursor uses them to decide when to invoke each tool."

/>

title="Handle Errors Gracefully"

description="Always return meaningful error messages with isError: true for failed operations."

/>

title="Limit Response Size"

description="Keep responses under 10KB. Large responses consume context and slow down the agent."

/>

title="Use Input Validation"

description="Validate inputs with Zod schemas to catch errors early and provide helpful messages."

/>

Input Validation with Zod

For production servers, add proper input validation:

`typescript

import { z } from "zod";

const FetchWebpageSchema = z.object({

url: z.string().url("Must be a valid URL"),

});

// In your tool handler:

case "fetch_webpage": {

const parsed = FetchWebpageSchema.safeParse(args);

if (!parsed.success) {

return {

content: [{ type: "text", text: Invalid input: ${parsed.error.message} }],

isError: true,

};

}

const { url } = parsed.data;

// ... rest of handler

}

`

---

Debugging Tips

Use console.error() for debug output—it won't interfere with the stdio transport. Check Cursor's developer console for these logs.

Common Issues

| Problem | Solution |

|---------|----------|

| Server won't connect | Verify the path is absolute and the file exists |

| Tools not appearing | Restart Cursor after configuration changes |

| Tools not being used | Make sure you're in Agent mode, not Ask mode |

| Errors in tool execution | Check console.error output in Cursor's developer tools |

---

Publishing Your MCP Server

Once your server works locally, you can share it:

npm Publishing

`bash

npm login

npm publish

`

Users can then install with:

`bash

npm install -g your-mcp-server

`

And configure in Cursor:

`json

{

"mcpServers": {

"your-mcp-server": {

"command": "npx",

"args": ["-y", "your-mcp-server"]

}

}

}

``

---

Next Steps

You've built your first MCP server for Cursor! Here's where to go from here:

title="Add Database Access"

description="Connect Cursor to PostgreSQL, SQLite, or other databases for intelligent query assistance."

href="/blog/supabase-mcp-server-guide"

/>

title="Integrate APIs"

description="Add tools for GitHub, Slack, Notion, or any API your workflow depends on."

href="/blog/best-mcp-servers-claude-code-2025"

/>

title="Try CAS"

description="CAS provides a complete context layer with persistent memory, task tracking, and semantic search."

/>

title="Explore Examples"

description="Check the official MCP servers repo for production-quality examples."

/>

You now have a working MCP server for Cursor. The same server will work with Claude Desktop and any other MCP-compatible client. Build once, use everywhere.

---

Resources

Share: