Skip to content

MCP Server Template

The MCP (Model Context Protocol) Server template provides a simplified framework for building MCP servers, making it easier to create tools that can be used by AI clients like Cursor.

Technical Stack

  • Node.js - JavaScript runtime
  • TypeScript - Type-safe development
  • Model Context Protocol SDK - Official MCP SDK
  • Fastify - Fast web framework
  • JWT - Authentication support

Quick Start

1. Create Project

bash
# Initialize project
vup init my-mcp-project
cd my-mcp-project

# Add MCP template
vup add my-mcp

2. Install Dependencies

bash
# Install dependencies
pnpm install

3. Start Development

bash
# STDIO mode (local, for Cursor)
cd apps/my-mcp
pnpm dev

# SSE mode (remote)
pnpm dev:remote

Project Structure

apps/my-mcp/
├── src/
│   ├── framework/              # Framework core
│   │   ├── defineTool.ts      # Tool definition helper
│   │   ├── requireAuth.ts     # Authentication handling
│   │   ├── toolRegistry.ts     # Tool registry
│   │   ├── createServer.ts    # Server creation
│   │   ├── types.ts           # Type definitions
│   │   └── index.ts           # Framework entry
│   ├── tools/                 # Tool definitions
│   │   ├── auth.ts            # Authentication tools
│   │   ├── public.ts          # Public tools (simple example)
│   │   ├── demo.ts            # Example tool (document search)
│   │   └── index.ts           # Tool exports
│   └── server.ts              # Server entry
├── public/
│   └── login.html             # Login page
├── data/
│   └── docs.csv               # Example data
├── package.json
└── tsconfig.json

Core Features

Simplified API

Use defineTool() to define tools with a single line of code:

typescript
// src/tools/my-tool.ts
import { defineTool } from '../framework';
import type { ToolContext } from '../framework';

export const my_tool = defineTool({
  name: 'my_tool',
  description: 'My tool description',
  inputSchema: {
    properties: {
      param: { type: 'string', description: 'Parameter description' },
    },
    required: ['param'],
  },
  requiresAuth: true, // Enable authentication with one line
  handler: async (args, context: ToolContext) => {
    // context.userId is automatically injected, no manual check needed
    return {
      content: [{ type: 'text', text: `Result: ${args.param}` }],
    };
  },
});

Automatic Authentication

Enable authentication with requiresAuth: true:

typescript
export const my_tool = defineTool({
  name: 'my_tool',
  requiresAuth: true, // Automatic authentication check
  handler: async (args, context) => {
    // context.userId is guaranteed to exist
  },
});

Tool Registry

Register tools in src/tools/index.ts:

typescript
import { my_tool } from './my-tool';

export const TOOLS = [my_tool];

Framework API

defineTool()

Helper function to define tools, automatically handling authentication, type conversion, etc.

typescript
defineTool({
  name: string;
  description: string;
  inputSchema: {
    properties: Record<string, any>;
    required?: string[];
  };
  requiresAuth?: boolean;  // Whether authentication is required
  handler: (args, context) => Promise<{ content: ... }>;
})

requireAuth()

Manually wrap tool handlers to automatically check authentication.

typescript
import { requireAuth } from '../framework';

const handler = requireAuth(async (args, context) => {
  // context.userId is guaranteed to exist here
});

setAuthConfig()

Set authentication configuration.

typescript
import { setAuthConfig } from '../framework';

setAuthConfig({
  loginUrl: 'http://localhost:9316/login.html',
  checkAuth: (context) => !!context.userId,
});

createMcpServer()

Create MCP server.

typescript
import { createMcpServer, createToolRegistry } from './framework';
import { TOOLS } from './tools';

const registry = createToolRegistry();
TOOLS.forEach((tool) => registry.register(tool));

createMcpServer(
  {
    name: 'mcp-server',
    version: '1.0.0',
    mode: 'stdio', // or 'sse'
    port: 9316,
    auth: {
      loginUrl: 'http://localhost:9316/login.html',
      checkAuth: (context) => !!context.userId,
    },
  },
  registry
);

Development Modes

STDIO Mode (Local)

For direct use by AI clients like Cursor:

bash
pnpm dev

SSE Mode (Remote)

For remote server deployment:

bash
pnpm dev:remote

Access:

  • MCP endpoint: http://localhost:9316/mcp
  • Login page: http://localhost:9316/login.html

Example Tool

search_docs - Document Search Tool

A complete example tool (src/tools/demo.ts) demonstrating how to implement a practical tool.

Features:

  • Read document data from CSV file
  • Support search by title, content, author, category
  • Return formatted search results
  • Limit return count (default 10, max 50)

Data Source:

  • CSV file: data/docs.csv
  • Contains 50 example document records
  • Fields: id, title, content, author, category, created_at

Code Example:

typescript
// src/tools/demo.ts
export const search_docs = defineTool({
  name: 'search_docs',
  description: 'Search document library, support search by title, content, author, category',
  inputSchema: {
    properties: {
      query: { type: 'string', description: 'Search keyword' },
      limit: { type: 'number', description: 'Return limit (default 10)' },
    },
    required: ['query'],
  },
  handler: async (args, context) => {
    // Read data from CSV
    const docs = loadDocs();
    // Search and return results
    // ...
  },
});

Authentication Mechanism

Automatic Authentication

Use requiresAuth: true to automatically handle authentication:

typescript
export const my_tool = defineTool({
  name: 'my_tool',
  requiresAuth: true, // Automatic authentication check
  handler: async (args, context) => {
    // context.userId is guaranteed to exist
  },
});

Manual Authentication

Use requireAuth() to wrap handlers:

typescript
import { requireAuth } from '../framework';

const handler = requireAuth(async (args, context) => {
  // Authentication passed
});

Authentication Flow

  1. When a tool is called, if requiresAuth: true, the framework automatically checks context.userId
  2. If not authenticated, throws UrlElicitationRequiredError
  3. Clients like Cursor recognize error code -32042 and elicitations array
  4. Client automatically opens login page
  5. After user login, client retries tool call with JWT token

Available Scripts

json
{
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "dev:remote": "tsx watch src/server.ts --remote",
    "build": "tsc",
    "start": "node .output/server.js",
    "start:remote": "node .output/server.js --remote"
  }
}

Best Practices

Error Handling

typescript
try {
  // Tool logic
} catch (error) {
  return {
    content: [
      {
        type: 'text',
        text: `Error: ${error.message}`,
      },
    ],
  };
}

Type Safety

Always define proper types for tool arguments:

typescript
inputSchema: {
  properties: {
    query: { type: 'string', description: 'Search keyword' },
    limit: { type: 'number', description: 'Result limit' },
  },
  required: ['query'],
}

Tool Organization

Organize tools by functionality:

src/tools/
├── auth.ts        # Authentication tools
├── database.ts    # Database tools
├── file.ts        # File operations
└── index.ts       # Export all tools