Clawctl
Tutorial
10 min

MCP Server Setup for OpenClaw: Build and Connect Custom Tools (2026)

Build MCP servers in TypeScript and Python, connect them to OpenClaw, and let Clawctl handle sandboxing and security. Step-by-step tutorial with real code.

Clawctl Team

Product & Engineering

MCP Server Setup for OpenClaw: Build and Connect Custom Tools (2026)

Anthropic released the Model Context Protocol (MCP) as an open standard in late 2024. It defines how AI models discover and call external tools. One protocol. Any tool. Any model.

For OpenClaw users, MCP servers are the fastest way to extend your agent. Database access, web scraping, GitHub integration, Slack notifications. Build or install an MCP server, point OpenClaw at it, and your agent gains new capabilities without custom integration code.

This tutorial walks through building an MCP server from scratch, connecting it to OpenClaw, and securing it with Clawctl.

What Is MCP and Why It Matters for OpenClaw

MCP stands for Model Context Protocol. It standardizes how AI agents communicate with external tools. Before MCP, every tool integration required custom glue code per vendor.

Without MCPWith MCP
IntegrationCustom code per tool, per modelUniversal protocol for all tools
DiscoveryHard-coded tool definitionsAgent discovers tools at runtime
TransportREST, GraphQL, custom socketsStandardized stdio or Streamable HTTP
ReuseRebuild per LLM vendorBuild once, connect anywhere
ValidationRoll your own schemasJSON Schema built in

OpenClaw natively supports MCP servers. You declare them in your openclaw.yaml config. OpenClaw spawns each server as a child process and routes tool calls through the MCP protocol. Your agent can use tools from multiple servers in a single conversation.

For a broader view of how OpenClaw compares to other agent runtimes, see our comparison of coding agents in 2026.

Prerequisites

You need these installed before starting:

  • Node.js 18+ and npm (for the TypeScript server)
  • Python 3.10+ and pip (for the Python server)
  • An OpenClaw deployment — either self-hosted or via Clawctl

Verify your setup:

node --version     # v18.x or higher
npm --version      # 9.x or higher
python3 --version  # 3.10 or higher

If you need help deploying OpenClaw itself, start with our self-hosted AI coding agent stack guide.

TypeScript MCP Server Tutorial

We will build a database query tool that lets your OpenClaw agent run read-only SQL queries. This is a real use case, not a toy example.

Step 1: Scaffold the Project

mkdir openclaw-db-server
cd openclaw-db-server
npm init -y

Install the MCP SDK and dependencies:

npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Update package.json:

{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Step 2: Define Tools with Zod

Create src/index.ts. The MCP SDK uses Zod schemas to define tool inputs. The SDK converts these to JSON Schema for the protocol.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "db-query-server",
  version: "1.0.0",
});

server.tool(
  "run-query",
  "Run a read-only SQL query against the database",
  {
    sql: z
      .string()
      .min(1)
      .describe("A SELECT query to run against the database"),
  },
  async ({ sql }) => {
    // Enforce read-only queries
    const normalized = sql.trim().toUpperCase();
    if (!normalized.startsWith("SELECT")) {
      return {
        content: [
          { type: "text", text: "Only SELECT queries are allowed." },
        ],
        isError: true,
      };
    }

    try {
      const results = await executeQuery(sql);
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(results, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          { type: "text", text: `Query failed: ${String(error)}` },
        ],
        isError: true,
      };
    }
  }
);

server.tool(
  "list-tables",
  "List all tables in the database",
  {},
  async () => {
    try {
      const tables = await executeQuery(
        "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"
      );
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(tables, null, 2),
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          { type: "text", text: `Failed to list tables: ${String(error)}` },
        ],
        isError: true,
      };
    }
  }
);

The server.tool() method takes four arguments: name, description, Zod schema for inputs, and an async handler. The handler returns content blocks. Setting isError: true tells the model the tool call failed.

Step 3: Implement the Handler

Add the database connection and server startup. Still in src/index.ts:

import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
});

async function executeQuery(sql: string): Promise<Record<string, unknown>[]> {
  const client = await pool.connect();
  try {
    const result = await client.query(sql);
    return result.rows;
  } finally {
    client.release();
  }
}

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("DB query MCP server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

Notice: log to stderr with console.error, not stdout. MCP uses stdout for protocol messages. Logging to stdout corrupts the protocol stream.

Step 4: Test with the Inspector

Build and run the MCP inspector:

npm run build
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" \
  npx @modelcontextprotocol/inspector node dist/index.js

The inspector opens a browser UI at http://localhost:5173. You will see your tools listed: run-query and list-tables. Click a tool, provide input, and run it. Use the inspector every time you change your server. It catches issues before they hit production.

Step 5: Connect to OpenClaw

This is where it gets specific to OpenClaw. Instead of Claude Desktop config, you declare MCP servers in your openclaw.yaml:

agents:
  - id: main
    model: anthropic/claude-sonnet-4-5
    mcp_servers:
      - name: db-query
        command: node
        args:
          - /opt/mcp-servers/db-query/dist/index.js
        env:
          DATABASE_URL: postgresql://user:pass@db:5432/mydb

OpenClaw spawns the MCP server as a child process when the agent starts. The agent discovers available tools through the MCP protocol. When the model decides to call run-query, OpenClaw routes the request to your server over stdio.

No REST endpoints. No webhook URLs. No API gateway. The MCP server runs alongside the agent.

Python MCP Server

The Python SDK uses decorators and type hints. No manual schema definitions.

Install and Build

pip install mcp

Create server.py:

import os
import psycopg2
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("db-query-server")


@mcp.tool()
def run_query(sql: str) -> str:
    """Run a read-only SQL query against the database."""
    normalized = sql.strip().upper()
    if not normalized.startswith("SELECT"):
        return "Error: Only SELECT queries are allowed."

    conn = psycopg2.connect(os.environ["DATABASE_URL"])
    try:
        with conn.cursor() as cur:
            cur.execute(sql)
            columns = [desc[0] for desc in cur.description]
            rows = cur.fetchall()
            results = [dict(zip(columns, row)) for row in rows]
            return str(results)
    finally:
        conn.close()


@mcp.tool()
def list_tables() -> str:
    """List all tables in the database."""
    return run_query(
        "SELECT table_name FROM information_schema.tables "
        "WHERE table_schema = 'public'"
    )


if __name__ == "__main__":
    mcp.run()

FastMCP reads the function signature and docstring to generate the tool schema. The type hint sql: str becomes a required string parameter. The docstring becomes the tool description.

Test and Connect

DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" \
  npx @modelcontextprotocol/inspector python3 server.py

Add to openclaw.yaml:

agents:
  - id: main
    model: anthropic/claude-sonnet-4-5
    mcp_servers:
      - name: db-query
        command: python3
        args:
          - /opt/mcp-servers/db-query/server.py
        env:
          DATABASE_URL: postgresql://user:pass@db:5432/mydb

The protocol is language-agnostic. OpenClaw does not care if the server runs TypeScript or Python.

Connect Multiple MCP Servers to OpenClaw

A single OpenClaw agent can use multiple MCP servers. Each server adds a set of tools. Declare them all in your config:

agents:
  - id: main
    model: anthropic/claude-sonnet-4-5
    mcp_servers:
      - name: database
        command: node
        args:
          - /opt/mcp-servers/db-query/dist/index.js
        env:
          DATABASE_URL: postgresql://user:pass@db:5432/mydb
      - name: github
        command: npx
        args:
          - -y
          - "@modelcontextprotocol/server-github"
        env:
          GITHUB_PERSONAL_ACCESS_TOKEN: ghp_xxxxxxxxxxxx
      - name: filesystem
        command: npx
        args:
          - -y
          - "@modelcontextprotocol/server-filesystem"
          - /workspace

OpenClaw spawns all three servers at startup. The agent sees tools from all of them. It can query the database, create a GitHub issue, and write a file in a single conversation turn.

Real-World MCP Servers

You do not have to build everything. The MCP ecosystem has dozens of production-ready servers:

ServerWhat It DoesInstall
@modelcontextprotocol/server-filesystemRead, write, search filesnpx -y @modelcontextprotocol/server-filesystem /path
@modelcontextprotocol/server-postgresQuery PostgreSQL (read-only)npx -y @modelcontextprotocol/server-postgres
@modelcontextprotocol/server-githubIssues, PRs, repos, branchesnpx -y @modelcontextprotocol/server-github
@modelcontextprotocol/server-slackChannels, messages, searchnpx -y @modelcontextprotocol/server-slack
@modelcontextprotocol/server-puppeteerBrowse, screenshot, scrapenpx -y @modelcontextprotocol/server-puppeteer
mcp-server-sqliteSQLite queriespip install mcp-server-sqlite
mcp-server-fetchFetch URLs as markdownpip install mcp-server-fetch

Browse the full registry at github.com/modelcontextprotocol/servers. Most install with a single command and work with OpenClaw out of the box.

Common Mistakes

These trip up most people building their first MCP server.

1. Wrong Transport

MCP supports stdio and Streamable HTTP (formerly SSE). OpenClaw uses stdio by default. If your server listens on an HTTP port but OpenClaw expects stdio, nothing will connect.

// stdio — use this for OpenClaw
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

// Streamable HTTP — use for remote/web clients
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

Stick with stdio for OpenClaw deployments. It is simpler and avoids network configuration.

2. No Error Handling

An unhandled exception crashes the server process. OpenClaw loses the connection. The agent gets no response. Always return errors through the protocol:

server.tool("my-tool", "Does something", { input: z.string() }, async ({ input }) => {
  try {
    const result = await doWork(input);
    return { content: [{ type: "text", text: result }] };
  } catch (error) {
    return {
      content: [{ type: "text", text: `Error: ${String(error)}` }],
      isError: true,
    };
  }
});

3. Logging to stdout

MCP uses stdout for protocol messages. If your server prints debug output to stdout, it corrupts the protocol stream. Use console.error in TypeScript or print(..., file=sys.stderr) in Python.

4. Vague Tool Names

The model picks tools based on the name and description. A tool called do-thing with description "does stuff" will confuse the model. Be specific:

// Bad — the model cannot figure out when to use this
server.tool("query", "Run a query", ...)

// Good — clear name and description
server.tool("run-sql-select", "Run a read-only SELECT query against PostgreSQL", ...)

Security: MCP Expands the Attack Surface

Every MCP server you add gives your agent more power. More power means more risk.

A filesystem MCP server can read any file the process has access to. A database server can run queries against production data. A shell server can execute arbitrary commands. If the model gets tricked by a prompt injection, those tools become weapons.

This is not theoretical. The Invariant Labs research team demonstrated MCP-specific attacks in early 2025, including tool poisoning and cross-server data exfiltration. When a model calls tools, it trusts the tool descriptions it receives. A malicious MCP server can embed hidden instructions in tool descriptions that override the model's behavior.

The attack surface grows linearly with each server you add. Three MCP servers means three sets of permissions, three processes with host access, three potential entry points.

For a detailed breakdown of agent security risks, read our AI agent security guide.

Clawctl Sandboxes MCP Servers

Clawctl solves the MCP security problem by isolating each server. Your agent keeps its capabilities. The blast radius shrinks.

Network policies per server. Each MCP server gets its own network rules. The database server can reach your PostgreSQL instance. It cannot reach the internet. The GitHub server can reach api.github.com. It cannot reach your database. You define this in your Clawctl config.

Filesystem restrictions. Clawctl mounts only the directories each server needs. The filesystem server gets /workspace. It cannot see /etc/shadow or ~/.ssh. No path traversal. No accidental credential exposure.

Process isolation. Each MCP server runs in its own sandboxed process. A crash in one server does not take down the others. A compromised server cannot access memory from another server or the host agent.

Audit logging. Every tool call is logged with timestamps, parameters, and results. You can trace what the agent did, which tool it called, and what data it accessed. This matters for compliance and incident response.

Without sandboxing, you are trusting every MCP server with full host access. With Clawctl, each server gets only what it needs.

# Clawctl applies these policies automatically
# You just declare your MCP servers in openclaw.yaml
# Clawctl handles the isolation
agents:
  - id: main
    model: anthropic/claude-sonnet-4-5
    mcp_servers:
      - name: database
        command: node
        args:
          - /opt/mcp-servers/db-query/dist/index.js
        env:
          DATABASE_URL: postgresql://user:pass@db:5432/mydb

Clawctl provisions all of this in 60 seconds. No Docker compose files. No iptables rules. No manual audit config.

Get Started

You now know how to build an MCP server, connect it to OpenClaw, and secure it.

The fastest path to production: sign up for Clawctl at $49/month. It provisions OpenClaw with sandboxed MCP support, network policies, and audit logging. Deploy in 60 seconds.

Start building with Clawctl →

Resources

This content is for informational purposes only and does not constitute financial, legal, medical, tax, or other professional advice. Individual results vary. See our Terms of Service for important disclaimers.

Ready to deploy your OpenClaw securely?

Get your OpenClaw running in production with Clawctl's enterprise-grade security.