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.
Once you’ve built a great MCP server, you can share it with the community by submitting it to the Cline MCP Marketplace.
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.
Copy the following into ~/Documents/Cline/MCP/.clinerules:
# MCP Server Development ProtocolCRITICAL: 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 needed3. 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: CompletionOnly 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
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 instanceconst server = new Server( { name: "my-server", version: "1.0.0" }, { capabilities: { tools: {} } });console.error("[Setup] Initializing my-server...");// 2. Declare available toolsserver.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 callsserver.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 transportasync 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.
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.
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
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}`); }}
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.
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.
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.
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.