Skip to main content
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.
Once you’ve built a great MCP server, you can share it with the community by submitting it to the Cline MCP Marketplace.

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:
1

Plan (PLAN MODE)

Define the problem, choose the API or service, map out authentication requirements, and design the tool interfaces before writing any code.
2

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.
3

Test (required before completion)

Test every tool with valid inputs and confirm correct output. The protocol blocks completion until all tools pass.
4

Complete

Once all tools are verified, mark the server as complete and optionally submit it to the Marketplace.

.clinerules file

Copy the following into ~/Documents/Cline/MCP/.clinerules:
# 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:
npx @modelcontextprotocol/create-server my-server
cd my-server
npm install
Project structure after scaffolding:
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:
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);
});
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.

3. Build the server

npm run build
This compiles TypeScript to build/index.js.

4. Register with Cline

Add the server to cline_mcp_settings.json:
{
  "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

npx @modelcontextprotocol/create-server alphaadvantage-mcp
cd alphaadvantage-mcp
npm install axios node-cache
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:
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:
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

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

{
  "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:
# AAPL (Apple Inc) — $241.84 ↑+1.91%

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

Best practices

Logging

Consistent log prefixes make debugging fast:
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:
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:
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:
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

  • 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.
const API_KEY = process.env.MY_API_KEY;
if (!API_KEY) {
  console.error("[Error] Missing MY_API_KEY environment variable");
  process.exit(1);
}
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.
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.
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.

Additional resources