> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/cline/cline/llms.txt
> Use this file to discover all available pages before exploring further.

# Building custom MCP servers

> Build a custom MCP server using the @modelcontextprotocol/sdk, register it with Cline, and follow the structured development protocol for planning, implementation, and testing.

MCP servers extend Cline beyond what text prompts can achieve alone. By building your own server, you can give Cline direct access to internal APIs, proprietary data sources, local tools, and any system that has a programmable interface.

This guide walks through the full development lifecycle using the `@modelcontextprotocol/sdk` for TypeScript, Cline's structured development protocol, and a real worked example.

<Tip>
  Once you've built a great MCP server, you can share it with the community by submitting it to the [Cline MCP Marketplace](https://github.com/cline/mcp-marketplace).
</Tip>

## The development protocol

Cline has a built-in protocol for MCP server development enforced through a `.clinerules` file. Place this file at the root of your MCP working directory (`~/Documents/Cline/MCP/`) and Cline will automatically enter a structured development mode when you work in that folder.

The protocol has four phases:

<Steps>
  <Step title="Plan (PLAN MODE)">
    Define the problem, choose the API or service, map out authentication requirements, and design the tool interfaces before writing any code.
  </Step>

  <Step title="Implement (ACT MODE)">
    Bootstrap the project, write the server code using the MCP SDK, add logging, handle errors, and configure the server in your MCP settings.
  </Step>

  <Step title="Test (required before completion)">
    Test every tool with valid inputs and confirm correct output. The protocol blocks completion until all tools pass.
  </Step>

  <Step title="Complete">
    Once all tools are verified, mark the server as complete and optionally submit it to the Marketplace.
  </Step>
</Steps>

### `.clinerules` file

Copy the following into `~/Documents/Cline/MCP/.clinerules`:

````markdown theme={null}
# MCP Server Development Protocol

CRITICAL: DO NOT USE attempt_completion BEFORE TESTING

## Step 1: Planning (PLAN MODE)

- What problem does this tool solve?
- What API/service will it use?
- What are the authentication requirements?
  □ Standard API key
  □ OAuth (requires separate setup script)
  □ Other credentials

## Step 2: Implementation (ACT MODE)

1. Bootstrap

   For TypeScript/Node.js:
   ```bash
   npx @modelcontextprotocol/create-server my-server
   cd my-server
   npm install
   ```

   For Python:
   ```bash
   uv add "mcp[cli]"
   ```

2. Core implementation
   - Use the MCP SDK
   - Add comprehensive logging to stderr
   - Define TypeScript types for all inputs and outputs
   - Handle errors with context
   - Implement rate limiting if needed

3. Configuration — add to cline_mcp_settings.json:
   ```json
   {
     "mcpServers": {
       "my-server": {
         "command": "node",
         "args": ["path/to/build/index.js"],
         "env": { "API_KEY": "your-key" },
         "disabled": false,
         "autoApprove": []
       }
     }
   }
   ```

## Step 3: Testing (BLOCKER)

BEFORE completing, verify:
□ Have I tested EVERY tool?
□ Has the user confirmed success for each test?
□ Have I documented test results?

DO NOT complete until all tools pass.

## Step 4: Completion

Only after ALL tools tested can you mark the task complete.

## Key requirements

- Must use MCP SDK
- Must have comprehensive logging
- Must test each tool individually
- Must handle errors gracefully
- NEVER skip testing before completion
````

## Getting started

### 1. Bootstrap a TypeScript server

The `create-server` scaffolding tool sets up a complete project with the SDK, TypeScript config, and a working `index.ts`:

```bash theme={null}
npx @modelcontextprotocol/create-server my-server
cd my-server
npm install
```

Project structure after scaffolding:

```plaintext theme={null}
my-server/
├── src/
│   └── index.ts        # Main server entry point
├── build/              # Compiled output (created by npm run build)
├── package.json
└── tsconfig.json
```

### 2. Write the server

The core pattern for every MCP server:

```typescript theme={null}
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

// 1. Create the server instance
const server = new Server(
  { name: "my-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

console.error("[Setup] Initializing my-server...");

// 2. Declare available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  console.error("[Setup] Listing tools");
  return {
    tools: [
      {
        name: "get_weather",
        description: "Fetch current weather for a city",
        inputSchema: {
          type: "object",
          properties: {
            city: {
              type: "string",
              description: "City name, e.g. 'San Francisco'",
            },
            units: {
              type: "string",
              enum: ["metric", "imperial"],
              description: "Temperature units (default: metric)",
            },
          },
          required: ["city"],
        },
      },
    ],
  };
});

// 3. Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  console.error(`[Tool] Calling ${name} with`, args);

  try {
    switch (name) {
      case "get_weather": {
        const city = args?.city as string;
        const units = (args?.units as string) ?? "metric";

        if (!city?.trim()) {
          throw new Error("city is required");
        }

        // Call your API here
        const weather = await fetchWeather(city, units);

        return {
          content: [
            {
              type: "text",
              text: `Weather in ${city}: ${weather.temp}° — ${weather.description}`,
            },
          ],
        };
      }

      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    console.error(`[Error] ${name} failed:`, error);
    return {
      content: [{ type: "text", text: `Error: ${String(error)}` }],
      isError: true,
    };
  }
});

// 4. Connect via stdio transport
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("[Setup] Server ready");
}

main().catch((err) => {
  console.error("[Fatal]", err);
  process.exit(1);
});
```

<Note>
  Always log to `console.error`, not `console.log`. The MCP protocol uses stdout for structured messages; anything written to stdout that isn't valid JSON-RPC will break the connection.
</Note>

### 3. Build the server

```bash theme={null}
npm run build
```

This compiles TypeScript to `build/index.js`.

### 4. Register with Cline

Add the server to `cline_mcp_settings.json`:

```json theme={null}
{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["/absolute/path/to/my-server/build/index.js"],
      "env": {
        "WEATHER_API_KEY": "your-api-key-here"
      },
      "disabled": false,
      "autoApprove": []
    }
  }
}
```

Cline picks up the change automatically. You should see the server appear in the MCP Servers panel with a green dot.

## Case study: AlphaAdvantage stock analysis server

To illustrate a complete build, here is a walkthrough of an MCP server that wraps the AlphaAdvantage financial API and exposes tools for stock overviews, technical analysis, fundamental analysis, earnings reports, and news.

### Planning phase

Before writing code, the planning phase established:

* **Problem**: Analysts want stock data, price charts, and earnings history available directly in their AI assistant without switching tools.
* **API**: AlphaAdvantage — standard API key authentication, 5 requests per minute on the free tier.
* **Tools needed**: `get_stock_overview`, `get_technical_analysis`, `get_fundamental_analysis`, `get_earnings_report`, `get_news_sentiment`.
* **Output format**: Clean markdown with tables, trend arrows (↑/↓), and properly formatted financial figures.

### Project structure

```bash theme={null}
npx @modelcontextprotocol/create-server alphaadvantage-mcp
cd alphaadvantage-mcp
npm install axios node-cache
```

```plaintext theme={null}
src/
├── api/
│   └── alphaAdvantageClient.ts   # API client: rate limiting, caching, typed responses
├── formatters/
│   └── markdownFormatter.ts      # Format raw API data into readable markdown
└── index.ts                      # MCP server: tool definitions and handlers
```

### Rate limiting

The free tier allows only 5 API calls per minute. Rate limiting was built into the client:

```typescript theme={null}
private async enforceRateLimit(): Promise<void> {
  if (this.requestsThisMinute >= 5) {
    console.error("[Rate Limit] Limit reached — waiting for next minute...");
    const remainingMs = 60_000 - (Date.now() % 60_000);
    await new Promise((resolve) => setTimeout(resolve, remainingMs + 100));
    this.requestsThisMinute = 0;
  }
  this.requestsThisMinute++;
}
```

### Caching

Cache TTLs matched the staleness tolerance for each data type:

```typescript theme={null}
const CACHE_TTL = {
  STOCK_OVERVIEW: 60 * 60,        // 1 hour
  TECHNICAL_ANALYSIS: 60 * 30,    // 30 minutes
  FUNDAMENTAL_ANALYSIS: 60 * 60 * 24, // 24 hours
  EARNINGS_REPORT: 60 * 60 * 24,  // 24 hours
  NEWS: 60 * 15,                  // 15 minutes
};

// Before every API call:
const cached = this.cache.get<T>(cacheKey);
if (cached) {
  console.error(`[Cache] Hit for ${cacheKey}`);
  return cached;
}

// After a successful API call:
this.cache.set(cacheKey, data, ttl);
```

### Tool definitions

```typescript theme={null}
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "get_stock_overview",
      description: "Current price and company details for a stock symbol",
      inputSchema: {
        type: "object",
        properties: {
          symbol: { type: "string", description: "Ticker, e.g. 'AAPL'" },
          market: { type: "string", description: "Market, e.g. 'US'", default: "US" },
        },
        required: ["symbol"],
      },
    },
    // get_technical_analysis, get_fundamental_analysis,
    // get_earnings_report, get_news_sentiment defined here...
  ],
}));
```

### Configuration

```json theme={null}
{
  "mcpServers": {
    "alphaadvantage-mcp": {
      "command": "node",
      "args": ["/Users/you/Documents/Cline/MCP/alphaadvantage-mcp/build/index.js"],
      "env": {
        "ALPHAVANTAGE_API_KEY": "YOUR_API_KEY"
      },
      "disabled": false,
      "autoApprove": []
    }
  }
}
```

### Test results

Each tool was tested individually before marking the server complete:

<CodeGroup>
  ```markdown get_stock_overview theme={null}
  # AAPL (Apple Inc) — $241.84 ↑+1.91%

  **Sector:** TECHNOLOGY
  **Industry:** ELECTRONIC COMPUTERS
  **Market Cap:** 3.63T
  **P/E Ratio:** 38.26
  ```

  ```markdown get_technical_analysis theme={null}
  # Technical Analysis: AAPL

  ## Daily Price Action

  Current Price: $241.84 (↑$4.54, +1.91%)

  | Date       | Open    | High    | Low     | Close   | Volume |
  |------------|---------|---------|---------|---------|--------|
  | 2025-02-28 | $236.95 | $242.09 | $230.20 | $241.84 | 56.83M |
  ```

  ```markdown get_earnings_report theme={null}
  # Earnings Report: MSFT (Microsoft Corporation)

  **Current EPS:** $12.43

  | Quarter    | EPS Estimate | EPS Actual | Surprise % |
  |------------|--------------|------------|------------|
  | 2024-12-31 | $3.11        | $3.23      | ↑4.01%     |
  ```
</CodeGroup>

## Best practices

### Logging

Consistent log prefixes make debugging fast:

```typescript theme={null}
console.error("[Setup] Initializing server...");
console.error(`[API] GET /overview?symbol=${symbol}`);
console.error(`[Cache] Hit: ${cacheKey}`);
console.error(`[Error] Tool ${name} failed: ${error.message}`);
```

Always include enough context in error logs to diagnose failures without needing to reproduce them.

### Input validation

Validate inputs before making any API calls:

```typescript theme={null}
function validateSymbol(symbol: unknown): asserts symbol is string {
  if (typeof symbol !== "string" || !symbol.trim()) {
    throw new McpError(ErrorCode.InvalidParams, "A valid stock symbol is required");
  }
  if (!/^[A-Za-z0-9.]+$/.test(symbol)) {
    throw new McpError(ErrorCode.InvalidParams, `Invalid symbol: ${symbol}`);
  }
}
```

### Error handling

Return errors as structured responses rather than throwing, so Cline can relay the message to the user:

```typescript theme={null}
try {
  // tool logic
} catch (error) {
  console.error(`[Error] Tool ${name} failed:`, error);

  if (error instanceof McpError) throw error;

  return {
    content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
    isError: true,
  };
}
```

### Exposing resources

Resources let your server share read-only data (files, database records, config) that Cline can reference as context:

```typescript theme={null}
import { ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs/promises";

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: "file:///project/readme.md",
      name: "Project README",
      mimeType: "text/markdown",
    },
  ],
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  if (request.params.uri === "file:///project/readme.md") {
    const text = await fs.readFile("/path/to/readme.md", "utf-8");
    return {
      contents: [{ uri: request.params.uri, mimeType: "text/markdown", text }],
    };
  }
  throw new Error("Resource not found");
});
```

## Common challenges

<AccordionGroup>
  <Accordion title="API authentication">
    * For API keys: pass them as environment variables in your MCP config (`env` field) and read them with `process.env.YOUR_KEY`. Exit with a clear error message if the variable is missing.
    * For OAuth: write a separate script to perform the OAuth flow and store the refresh token, then load it from disk in your server.

    ```typescript theme={null}
    const API_KEY = process.env.MY_API_KEY;
    if (!API_KEY) {
      console.error("[Error] Missing MY_API_KEY environment variable");
      process.exit(1);
    }
    ```
  </Accordion>

  <Accordion title="Rate limiting">
    Design rate limiting into the client from the start. Use a counter + timestamp approach (as shown above) or a token bucket. Add caching to reduce the number of upstream calls. Return a helpful error message when the limit is hit rather than silently failing.
  </Accordion>

  <Accordion title="Slow tool responses">
    If a tool makes multiple sequential API calls, it may exceed the default 60-second timeout. Solutions:

    * Increase the `timeout` value in `cline_mcp_settings.json` for that server.
    * Split complex tools into smaller, single-purpose tools.
    * Cache aggressively to avoid repeated calls.
  </Accordion>

  <Accordion title="Incomplete API coverage">
    APIs don't always expose exactly what you need. Options:

    * Combine multiple endpoints to synthesize the data.
    * Transform and reshape the response to match your tool's output schema.
    * Document limitations clearly in the tool's `description` field so Cline sets accurate user expectations.
  </Accordion>
</AccordionGroup>

## Additional resources

* [MCP protocol documentation](https://github.com/modelcontextprotocol/mcp)
* [TypeScript SDK](https://github.com/modelcontextprotocol/sdk-js)
* [MCP server examples](https://github.com/modelcontextprotocol/servers)
* [Cline MCP Marketplace (submit your server)](https://github.com/cline/mcp-marketplace)
