Guides

Building Custom MCP Servers

A comprehensive guide to building remote MCP servers that connect your private data and APIs to Chipp

|View as Markdown
Hunter HodnettCPTO at Chipp
|19 min read

Building a custom MCP server lets you connect any data source or API to your Chipp-powered AI agents. This guide covers everything from protocol basics to production deployment, with complete code examples for the two most common patterns: REST API adapters and database servers.

What is MCP?

The Model Context Protocol (MCP) is an open standard introduced by Anthropic that defines how AI systems communicate with external tools and data sources. Think of it as a universal adapter layer between AI agents and your existing infrastructure.

MCP Server Architecture

Your Chipp Agent
Your MCP Server
Your Data

Key benefits:

  • Standardized interface: One protocol to connect any data source
  • AI-optimized: Designed for how LLMs discover and use tools
  • Secure: Built-in patterns for authentication and authorization
  • Flexible: Works with REST APIs, databases, files, and more

How MCP Works

MCP uses JSON-RPC 2.0 for communication. There are two core methods your server needs to implement:

tools/list

Returns available tools that the AI agent can call.

RequestPOSTtools/list
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}
Responsetools/list
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "get_user",
        "description": "Fetch a user by their ID",
        "inputSchema": {
          "type": "object",
          "properties": {
            "userId": {
              "type": "string",
              "description": "User ID"
            }
          },
          "required": [
            "userId"
          ]
        }
      }
    ]
  }
}

tools/call

Executes a tool with the provided arguments.

RequestPOSTtools/call
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_user",
    "arguments": {
      "userId": "123"
    }
  }
}
Responsetools/call
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"id\": \"123\", \"name\": \"John Doe\", \"email\": \"mailto:john@example.com\"}"
      }
    ],
    "isError": false
  }
}

Choosing an SDK

The official MCP SDKs handle protocol details so you can focus on your business logic.

For this guide, we'll use the TypeScript SDK as it's the most mature and has the best documentation.

Quick Start: Your First MCP Server

Let's build a minimal MCP server to understand the core concepts.

Project Setup

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk express zod
npm install -D typescript @types/node @types/express ts-node
Project Structure
my-mcp-server
package.json
tsconfig.json
src

Minimal Server Code

// src/server.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import { randomUUID } from "crypto";

const app = express();
app.use(express.json());

// Create MCP server
const server = new McpServer({
  name: "my-first-mcp",
  version: "1.0.0"
});

// Register a simple tool
server.tool(
  "greet",
  "Generate a personalized greeting",
  {
    name: z.string().describe("Person's name")
  },
  async ({ name }) => {
    return {
      content: [{ type: "text", text: `Hello, ${name}! Welcome to MCP.` }]
    };
  }
);

// HTTP transport setup
const transports: Record<string, StreamableHTTPServerTransport> = {};

app.post("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;

  let transport = transports[sessionId];
  if (!transport) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID()
    });
    await server.connect(transport);
  }

  const newSessionId = (transport as any)._sessionId;
  if (newSessionId) transports[newSessionId] = transport;

  await transport.handleRequest(req, res, req.body);
});

app.get("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;
  const transport = transports[sessionId];
  if (transport) await transport.handleRequest(req, res);
  else res.status(404).json({ error: "Session not found" });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`MCP server running at http://localhost:${PORT}/mcp`);
});
61 lines

Run with:

npx ts-node src/server.ts

That's it! You now have a working MCP server. Let's build something more useful.


Pattern 1: REST API Adapter

The most common use case: wrapping your existing REST APIs as MCP tools.

REST API Adapter Pattern

Your Chipp Agent
MCP Server
REST API

When to Use This Pattern

  • You have existing REST/GraphQL APIs
  • You want to expose internal services to AI agents
  • You need to add AI capabilities without rewriting backends

Complete Example: User Management API

This example shows how to wrap a typical REST API with CRUD operations.

// src/rest-adapter-server.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import { randomUUID } from "crypto";

const app = express();
app.use(express.json());

// Configuration from environment
const API_BASE = process.env.API_BASE_URL || "https://api.yourservice.com";
const API_KEY = process.env.API_KEY || "";

// Helper for API calls
async function apiCall(
  endpoint: string,
  options: RequestInit = {}
): Promise<{ ok: boolean; data?: any; error?: string }> {
  try {
    const response = await fetch(`${API_BASE}${endpoint}`, {
      ...options,
      headers: {
        "Authorization": `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
        ...options.headers,
      },
    });

    if (!response.ok) {
      const errorText = await response.text();
      return { ok: false, error: `API error (${response.status}): ${errorText}` };
    }

    const data = await response.json();
    return { ok: true, data };
  } catch (err: any) {
    return { ok: false, error: `Network error: ${err.message}` };
  }
}

// Create MCP server
const server = new McpServer({
  name: "user-management-api",
  version: "1.0.0"
});

// Tool 1: Search Users
server.tool(
  "search_users",
  "Search for users by name, email, or other criteria. Returns matching user profiles.",
  {
    query: z.string().describe("Search query (searches name and email)"),
    limit: z.number().optional().describe("Max results to return (default: 10, max: 100)"),
    offset: z.number().optional().describe("Number of results to skip for pagination")
  },
  async ({ query, limit = 10, offset = 0 }) => {
    const safeLimit = Math.min(limit, 100);
    const result = await apiCall(
      `/users/search?q=${encodeURIComponent(query)}&limit=${safeLimit}&offset=${offset}`
    );

    if (!result.ok) {
      return {
        content: [{ type: "text", text: `Error: ${result.error}` }],
        isError: true
      };
    }

    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          users: result.data.users,
          total: result.data.total,
          hasMore: result.data.total > offset + safeLimit
        }, null, 2)
      }]
    };
  }
);

// Tool 2: Get User by ID
server.tool(
  "get_user",
  "Fetch detailed information about a specific user by their ID.",
  {
    userId: z.string().describe("The unique user identifier")
  },
  async ({ userId }) => {
    const result = await apiCall(`/users/${encodeURIComponent(userId)}`);

    if (!result.ok) {
      return {
        content: [{ type: "text", text: `Error: ${result.error}` }],
        isError: true
      };
    }

    return {
      content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
    };
  }
);

// Tool 3: Create User
server.tool(
  "create_user",
  "Create a new user in the system. Returns the created user with their ID.",
  {
    name: z.string().describe("User's full name"),
    email: z.string().email().describe("User's email address (must be unique)"),
    role: z.enum(["admin", "user", "viewer"]).optional().describe("User role (default: user)"),
    department: z.string().optional().describe("User's department")
  },
  async ({ name, email, role = "user", department }) => {
    const result = await apiCall("/users", {
      method: "POST",
      body: JSON.stringify({ name, email, role, department })
    });

    if (!result.ok) {
      return {
        content: [{ type: "text", text: `Error creating user: ${result.error}` }],
        isError: true
      };
    }

    return {
      content: [{
        type: "text",
        text: `User created successfully:\n${JSON.stringify(result.data, null, 2)}`
      }]
    };
  }
);

// Tool 4: Update User
server.tool(
  "update_user",
  "Update an existing user's information. Only provide fields you want to change.",
  {
    userId: z.string().describe("ID of the user to update"),
    name: z.string().optional().describe("New name"),
    email: z.string().email().optional().describe("New email address"),
    role: z.enum(["admin", "user", "viewer"]).optional().describe("New role"),
    department: z.string().optional().describe("New department"),
    active: z.boolean().optional().describe("Whether the user account is active")
  },
  async ({ userId, ...updates }) => {
    // Filter out undefined values
    const cleanUpdates = Object.fromEntries(
      Object.entries(updates).filter(([_, v]) => v !== undefined)
    );

    if (Object.keys(cleanUpdates).length === 0) {
      return {
        content: [{ type: "text", text: "Error: No fields provided to update" }],
        isError: true
      };
    }

    const result = await apiCall(`/users/${encodeURIComponent(userId)}`, {
      method: "PATCH",
      body: JSON.stringify(cleanUpdates)
    });

    if (!result.ok) {
      return {
        content: [{ type: "text", text: `Error updating user: ${result.error}` }],
        isError: true
      };
    }

    return {
      content: [{
        type: "text",
        text: `User updated successfully:\n${JSON.stringify(result.data, null, 2)}`
      }]
    };
  }
);

// Tool 5: List User Activity
server.tool(
  "list_user_activity",
  "Get recent activity history for a user.",
  {
    userId: z.string().describe("User ID"),
    days: z.number().optional().describe("Number of days of history (default: 7, max: 30)")
  },
  async ({ userId, days = 7 }) => {
    const safeDays = Math.min(days, 30);
    const result = await apiCall(
      `/users/${encodeURIComponent(userId)}/activity?days=${safeDays}`
    );

    if (!result.ok) {
      return {
        content: [{ type: "text", text: `Error: ${result.error}` }],
        isError: true
      };
    }

    return {
      content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
    };
  }
);

// HTTP transport setup (same as before)
const transports: Record<string, StreamableHTTPServerTransport> = {};

app.post("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;
  let transport = transports[sessionId];

  if (!transport) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID()
    });
    await server.connect(transport);
  }

  const newSessionId = (transport as any)._sessionId;
  if (newSessionId) transports[newSessionId] = transport;

  await transport.handleRequest(req, res, req.body);
});

app.get("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;
  const transport = transports[sessionId];
  if (transport) await transport.handleRequest(req, res);
  else res.status(404).json({ error: "Session not found" });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`REST API adapter running at http://localhost:${PORT}/mcp`);
});
241 lines

Authentication Pass-Through

If your REST API requires user-specific authentication, you can pass tokens through:

// Extract auth from MCP request context
server.tool(
  "get_my_profile",
  "Get the current user's profile",
  {},
  async (args, context) => {
    // The client can pass user-specific tokens in the request
    const userToken = context?.meta?.userToken as string;

    const result = await apiCall("/me", {
      headers: userToken ? { "Authorization": `Bearer ${userToken}` } : {}
    });

    // ... handle response
  }
);

Pattern 2: Database Server

Expose database access through MCP for AI-powered queries and data exploration.

Database Server Pattern

Your Chipp Agent
MCP Server
Database
⚠️

Security Warning: Never expose production databases directly to AI agents. Always use read replicas, sanitized copies, or restricted read-only connections.

Complete Example: PostgreSQL Server

// src/database-server.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import { randomUUID } from "crypto";
import { Pool } from "pg";

const app = express();
app.use(express.json());

// Database connection (USE READ REPLICA!)
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10, // Connection pool size
});

// Create MCP server
const server = new McpServer({
  name: "database-explorer",
  version: "1.0.0"
});

// Tool 1: List Database Tables
server.tool(
  "list_tables",
  "List all tables in the database with their row counts.",
  {
    schema: z.string().optional().describe("Schema name (default: public)")
  },
  async ({ schema = "public" }) => {
    try {
      const result = await pool.query(`
        SELECT
          table_name,
          (SELECT COUNT(*) FROM information_schema.columns c
           WHERE c.table_name = t.table_name AND c.table_schema = t.table_schema) as column_count
        FROM information_schema.tables t
        WHERE table_schema = $1 AND table_type = 'BASE TABLE'
        ORDER BY table_name
      `, [schema]);

      return {
        content: [{
          type: "text",
          text: JSON.stringify({
            schema,
            tables: result.rows,
            count: result.rows.length
          }, null, 2)
        }]
      };
    } catch (err: any) {
      return {
        content: [{ type: "text", text: `Database error: ${err.message}` }],
        isError: true
      };
    }
  }
);

// Tool 2: Describe Table Schema
server.tool(
  "describe_table",
  "Get detailed column information for a specific table.",
  {
    tableName: z.string().describe("Name of the table to describe"),
    schema: z.string().optional().describe("Schema name (default: public)")
  },
  async ({ tableName, schema = "public" }) => {
    // Validate table name to prevent SQL injection
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
      return {
        content: [{ type: "text", text: "Error: Invalid table name format" }],
        isError: true
      };
    }

    try {
      const result = await pool.query(`
        SELECT
          column_name,
          data_type,
          character_maximum_length,
          is_nullable,
          column_default,
          (SELECT COUNT(*) FROM information_schema.key_column_usage k
           WHERE k.column_name = c.column_name
           AND k.table_name = c.table_name) > 0 as is_key
        FROM information_schema.columns c
        WHERE table_schema = $1 AND table_name = $2
        ORDER BY ordinal_position
      `, [schema, tableName]);

      if (result.rows.length === 0) {
        return {
          content: [{ type: "text", text: `Table '${tableName}' not found in schema '${schema}'` }],
          isError: true
        };
      }

      return {
        content: [{
          type: "text",
          text: JSON.stringify({
            table: tableName,
            schema,
            columns: result.rows
          }, null, 2)
        }]
      };
    } catch (err: any) {
      return {
        content: [{ type: "text", text: `Database error: ${err.message}` }],
        isError: true
      };
    }
  }
);

// Tool 3: Execute Read-Only Query
server.tool(
  "query_data",
  "Execute a read-only SQL SELECT query. Only SELECT statements are allowed.",
  {
    query: z.string().describe("SQL SELECT query to execute"),
    limit: z.number().optional().describe("Max rows to return (default: 100, max: 1000)")
  },
  async ({ query, limit = 100 }) => {
    const safeLimit = Math.min(limit, 1000);

    // Normalize and validate query
    const normalized = query.trim().toLowerCase();

    // Only allow SELECT queries
    if (!normalized.startsWith("select")) {
      return {
        content: [{ type: "text", text: "Error: Only SELECT queries are allowed" }],
        isError: true
      };
    }

    // Block dangerous patterns
    const forbidden = [
      "insert", "update", "delete", "drop", "alter", "truncate",
      "create", "grant", "revoke", "execute", "exec"
    ];

    for (const word of forbidden) {
      if (normalized.includes(word)) {
        return {
          content: [{ type: "text", text: `Error: Query contains forbidden keyword: ${word}` }],
          isError: true
        };
      }
    }

    try {
      // Force read-only transaction and add limit
      const client = await pool.connect();
      try {
        await client.query("SET TRANSACTION READ ONLY");

        // Remove any existing LIMIT and add our safe limit
        const limitedQuery = query.replace(/\s+limit\s+\d+/gi, "") + ` LIMIT ${safeLimit}`;

        const result = await client.query(limitedQuery);

        return {
          content: [{
            type: "text",
            text: JSON.stringify({
              rowCount: result.rowCount,
              rows: result.rows,
              fields: result.fields.map(f => ({ name: f.name, dataType: f.dataTypeID }))
            }, null, 2)
          }]
        };
      } finally {
        client.release();
      }
    } catch (err: any) {
      return {
        content: [{ type: "text", text: `Query error: ${err.message}` }],
        isError: true
      };
    }
  }
);

// Tool 4: Get Table Sample
server.tool(
  "sample_table",
  "Get a random sample of rows from a table to understand its data.",
  {
    tableName: z.string().describe("Table name"),
    sampleSize: z.number().optional().describe("Number of rows to sample (default: 5, max: 20)")
  },
  async ({ tableName, sampleSize = 5 }) => {
    // Validate table name
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
      return {
        content: [{ type: "text", text: "Error: Invalid table name format" }],
        isError: true
      };
    }

    const safeSampleSize = Math.min(sampleSize, 20);

    try {
      // Use TABLESAMPLE for efficient random sampling on large tables
      const result = await pool.query(`
        SELECT * FROM "${tableName}"
        ORDER BY RANDOM()
        LIMIT $1
      `, [safeSampleSize]);

      return {
        content: [{
          type: "text",
          text: JSON.stringify({
            table: tableName,
            sampleSize: result.rows.length,
            rows: result.rows
          }, null, 2)
        }]
      };
    } catch (err: any) {
      return {
        content: [{ type: "text", text: `Sample error: ${err.message}` }],
        isError: true
      };
    }
  }
);

// Tool 5: Get Table Statistics
server.tool(
  "table_stats",
  "Get statistics about a table: row count, size, and index information.",
  {
    tableName: z.string().describe("Table name")
  },
  async ({ tableName }) => {
    // Validate table name
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
      return {
        content: [{ type: "text", text: "Error: Invalid table name format" }],
        isError: true
      };
    }

    try {
      const [countResult, sizeResult, indexResult] = await Promise.all([
        pool.query(`SELECT COUNT(*) as row_count FROM "${tableName}"`),
        pool.query(`
          SELECT pg_size_pretty(pg_total_relation_size($1)) as total_size,
                 pg_size_pretty(pg_table_size($1)) as table_size,
                 pg_size_pretty(pg_indexes_size($1)) as index_size
        `, [tableName]),
        pool.query(`
          SELECT indexname, indexdef
          FROM pg_indexes
          WHERE tablename = $1
        `, [tableName])
      ]);

      return {
        content: [{
          type: "text",
          text: JSON.stringify({
            table: tableName,
            rowCount: parseInt(countResult.rows[0].row_count),
            size: sizeResult.rows[0],
            indexes: indexResult.rows
          }, null, 2)
        }]
      };
    } catch (err: any) {
      return {
        content: [{ type: "text", text: `Stats error: ${err.message}` }],
        isError: true
      };
    }
  }
);

// HTTP transport setup
const transports: Record<string, StreamableHTTPServerTransport> = {};

app.post("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;
  let transport = transports[sessionId];

  if (!transport) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID()
    });
    await server.connect(transport);
  }

  const newSessionId = (transport as any)._sessionId;
  if (newSessionId) transports[newSessionId] = transport;

  await transport.handleRequest(req, res, req.body);
});

app.get("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;
  const transport = transports[sessionId];
  if (transport) await transport.handleRequest(req, res);
  else res.status(404).json({ error: "Session not found" });
});

// Graceful shutdown
process.on("SIGTERM", async () => {
  console.log("Shutting down...");
  await pool.end();
  process.exit(0);
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Database MCP server running at http://localhost:${PORT}/mcp`);
});
325 lines

Tool Design Best Practices

How you design your tools significantly impacts how well AI agents can use them.

1

Workflow-Focused Tools

Create tools that accomplish complete tasks rather than exposing individual operations. LLMs work better with fewer, more capable tools.

When to use

You have many related API endpoints that are typically used together

Example
analyze_customer instead of get_customer + get_orders + get_support_tickets
2

Progressive Discovery

Provide tools that help the AI understand what's available before diving into specifics. Schema exploration tools are invaluable.

When to use

Working with databases or APIs with many resources

Example
list_tables → describe_table → query_data
3

Descriptive Schemas

Write tool and parameter descriptions as if explaining to a new team member. Include examples, constraints, and common use cases.

When to use

Always - good descriptions are the key to AI understanding

Example
email: User email address (must be unique, used for login)
4

Safe Defaults

Use sensible defaults that prevent accidental damage. Limit result sizes, default to read-only, require confirmation for destructive actions.

When to use

Any tool that could return large datasets or modify data

Example
limit = 100, max = 1000, default role = viewer

Tool Count Guidelines

Tool CountAssessmentRecommendation
3-10IdealEasy for AI to navigate
10-20GoodConsider grouping related tools
20-50ComplexAI may struggle to choose correctly
50+Too manyConsolidate into workflow tools

Adding Authentication

Your MCP server needs to verify that requests come from authorized sources.

Bearer Token Authentication

// Middleware to verify Bearer token
function authenticate(req: express.Request, res: express.Response, next: express.NextFunction) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing or invalid Authorization header" });
  }

  const token = authHeader.substring(7);

  // Verify token (implement your own logic)
  if (!isValidToken(token)) {
    return res.status(401).json({ error: "Invalid token" });
  }

  next();
}

// Apply to MCP endpoints
app.post("/mcp", authenticate, async (req, res) => {
  // ... handler
});

app.get("/mcp", authenticate, async (req, res) => {
  // ... handler
});
26 lines

API Key Authentication

function authenticateApiKey(req: express.Request, res: express.Response, next: express.NextFunction) {
  const apiKey = req.headers["x-api-key"] as string;

  if (!apiKey) {
    return res.status(401).json({ error: "Missing X-API-Key header" });
  }

  // Verify API key
  if (!isValidApiKey(apiKey)) {
    return res.status(401).json({ error: "Invalid API key" });
  }

  next();
}

Deployment Options

Choose a deployment method based on your needs:

Cloud Run Deployment

# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 8080
CMD ["node", "dist/server.js"]
# Deploy to Cloud Run
gcloud run deploy my-mcp-server \
  --source . \
  --region us-central1 \
  --no-allow-unauthenticated \
  --set-env-vars="DATABASE_URL=..."

Development with ngrok

# Start your local server
npm run dev

# In another terminal, expose it
ngrok http 3000

# Use the ngrok URL in Chipp
# https://abc123.ngrok-free.app/mcp

Security Checklist

Before deploying to production, verify these security measures:

Security Checklist

0/8 complete

Connecting to Chipp

Once your MCP server is deployed:

Connecting to Chipp

1
Navigate
App Builder → Pro Actions → Add Pro Action
2
Select
Click 'Custom Remote MCP'
3
Configure
Enter Server URL, Transport (HTTP), Auth Type
4
Test
Click 'Test & List Tools' to discover tools
5
Enable
Select which tools to enable
6
Save
Save your configuration

Configuration Fields

FieldDescriptionExample
Server URLFull URL to your MCP endpointhttps://my-server.run.app/mcp
TransportProtocol typeHTTP (recommended)
Auth TypeHow to authenticateBearer or API Key
Token/KeyYour authentication credentialsk-abc123...

After saving, Chipp will call your server's tools/list method and display available tools. Enable the ones you want your AI agent to use.


Troubleshooting

Common Issues

"Connection refused" error

  • Check your server is running and accessible from the internet
  • Verify HTTPS is configured correctly
  • Check firewall rules allow inbound traffic

"Authentication failed" error

  • Verify your token/API key is correct
  • Check the Authorization header format matches what your server expects
  • Ensure tokens haven't expired

"Tool not found" error

  • Run tools/list manually to verify your tools are registered
  • Check tool names don't contain special characters
  • Verify the tool is enabled in Chipp

Slow response times

  • Add connection pooling for database connections
  • Implement caching for frequently-accessed data
  • Consider moving to a region closer to Chipp's servers

Testing Your Server

Use curl to test your MCP server directly:

# Test tools/list
curl -X POST https://your-server.com/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-token" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

# Test tools/call
curl -X POST https://your-server.com/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-token" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"your_tool","arguments":{}}}'

Resources

Chipp Documentation:

Official MCP Documentation:

Example Repositories:

Deployment Guides: