Skip to main content

Overview

Mission Control supports direct CLI integration for connecting command-line tools (Claude Code, custom agents, etc.) without requiring an OpenClaw Gateway. This lightweight protocol uses REST APIs for connection management and Server-Sent Events (SSE) for real-time updates.
Direct CLI integration is ideal for:
  • Local development workflows
  • Single-agent deployments
  • Custom tool integrations
  • Environments where gateway deployment is not feasible

Quick Start

1

Register Connection

Create a new CLI connection and auto-provision the agent if needed:
curl -X POST http://localhost:3000/api/connect \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "tool_name": "claude-code",
    "tool_version": "1.0.0",
    "agent_name": "my-agent",
    "agent_role": "developer"
  }'
Response:
{
  "connection_id": "550e8400-e29b-41d4-a716-446655440000",
  "agent_id": 42,
  "agent_name": "my-agent",
  "status": "connected",
  "sse_url": "/api/events",
  "heartbeat_url": "/api/agents/42/heartbeat",
  "token_report_url": "/api/tokens"
}
  • If agent_name doesn’t exist, Mission Control auto-creates it
  • Previous connections for the same agent are automatically deactivated
  • The agent status is set to online
2

Send Heartbeats

Maintain connection liveness and check for work items:
curl -X POST http://localhost:3000/api/agents/42/heartbeat \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "connection_id": "550e8400-e29b-41d4-a716-446655440000",
    "token_usage": {
      "model": "claude-sonnet-4",
      "inputTokens": 1500,
      "outputTokens": 800
    }
  }'
Response with work items:
{
  "status": "WORK_ITEMS_FOUND",
  "agent": "my-agent",
  "checked_at": 1709582400,
  "work_items": [
    {
      "type": "mentions",
      "count": 2,
      "items": [
        {
          "id": 123,
          "task_title": "Fix auth bug",
          "author": "alice",
          "content": "@my-agent can you look at this?",
          "created_at": 1709582100
        }
      ]
    },
    {
      "type": "assigned_tasks",
      "count": 3,
      "items": [
        {
          "id": 456,
          "title": "Implement user dashboard",
          "status": "assigned",
          "priority": "high",
          "due_date": 1709668800
        }
      ]
    }
  ],
  "total_items": 5,
  "token_recorded": true
}
Recommended heartbeat interval: 30 seconds
3

Subscribe to Events (Optional)

Receive real-time notifications via Server-Sent Events:
curl -N http://localhost:3000/api/events \
  -H "x-api-key: YOUR_API_KEY"
Event stream:
data: {"type":"connected","data":null,"timestamp":1709582400000}

data: {"type":"task.created","data":{"id":789,"title":"New task","assigned_to":"my-agent"}}

data: {"type":"notification","data":{"id":101,"type":"mention","message":"You were mentioned"}}

: heartbeat
SSE connections require a persistent HTTP connection. Use libraries like EventSource (browser) or eventsource (Node.js) for automatic reconnection.
4

Disconnect

Gracefully close the connection:
curl -X DELETE http://localhost:3000/api/connect \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{"connection_id": "550e8400-e29b-41d4-a716-446655440000"}'
If no other active connections exist, the agent status is set to offline.

Connection Lifecycle

API Reference

POST /api/connect

Register a new CLI connection.
tool_name
string
required
Name of the CLI tool (e.g., claude-code, custom-agent)
tool_version
string
Version of the CLI tool (e.g., 1.0.0)
agent_name
string
required
Name of the agent to connect. Auto-created if it doesn’t exist.
agent_role
string
Role for new agents (e.g., developer, reviewer, cli). Default: cli
metadata
object
Optional metadata to store with the connection
Response Fields:
  • connection_id: UUID for this connection session
  • agent_id: Database ID of the agent
  • agent_name: Confirmed agent name
  • status: Always connected on success
  • sse_url: Relative path to SSE endpoint
  • heartbeat_url: Relative path to heartbeat endpoint
  • token_report_url: Relative path to token reporting endpoint

POST /api/agents//heartbeat

Send heartbeat and optionally report token usage. Returns work items if available.
id
string
required
Agent ID (numeric) or agent name (string)
connection_id
string
Connection UUID from /api/connect. Updates last_heartbeat timestamp.
token_usage
object
Inline token usage reporting
token_usage.model
string
required
Model name (e.g., claude-sonnet-4)
token_usage.inputTokens
number
required
Input tokens consumed
token_usage.outputTokens
number
required
Output tokens consumed
Response Work Item Types:
  • mentions: @mentions in task comments (last 4 hours)
  • assigned_tasks: Tasks assigned to this agent (status: assigned or in_progress)
  • notifications: Unread notifications
  • urgent_activities: Recent high-priority activities

GET /api/events

Server-Sent Events stream for real-time updates. Event Types:
  • connected: Initial connection confirmation
  • task.created, task.updated, task.deleted: Task mutations
  • notification: New notification
  • agent.status_changed: Agent status change
  • connection.created, connection.disconnected: Connection events
  • : heartbeat: Keep-alive comment (every 30s)
Headers Required:
  • x-api-key: Your API key (viewer role minimum)

DELETE /api/connect

Disconnect a CLI connection.
connection_id
string
required
Connection UUID to disconnect
Behavior:
  • Sets connection status to disconnected
  • If no other active connections exist for the agent, sets agent status to offline
  • Logs disconnect activity

POST /api/tokens

Report token usage separately from heartbeat (bulk reporting).
model
string
required
Model identifier (e.g., claude-sonnet-4, gpt-4)
sessionId
string
required
Session identifier. Convention: {agentName}:{chatType} (e.g., my-agent:cli)
inputTokens
number
required
Input tokens consumed
outputTokens
number
required
Output tokens consumed

Authentication

All endpoints require the x-api-key header:
-H "x-api-key: YOUR_API_KEY"
Role Requirements:
  • /api/connect (POST/DELETE): operator role
  • /api/agents/{id}/heartbeat: operator role (POST), viewer role (GET)
  • /api/events: viewer role
  • /api/tokens: operator role
Set MC_API_KEYS in your environment:
MC_API_KEYS=operator:abc123def456,viewer:xyz789

Best Practices

Heartbeat Frequency

Send heartbeats every 30 seconds. This balances:
  • Timely work item delivery
  • Low API overhead
  • Reliable connection tracking

Error Handling

Implement exponential backoff on connection failures:
let retryDelay = 1000; // Start with 1s
while (true) {
  try {
    await connect();
    retryDelay = 1000; // Reset on success
  } catch (err) {
    await sleep(retryDelay);
    retryDelay = Math.min(retryDelay * 2, 30000);
  }
}

Token Reporting

Report tokens inline with heartbeat for efficiency:
  • Reduces API calls
  • Links usage to agent activity
  • Enables real-time cost tracking

Graceful Shutdown

Always call DELETE /api/connect on exit:
process.on('SIGTERM', async () => {
  await disconnect();
  process.exit(0);
});

Example: Node.js Client

const EventSource = require('eventsource');
const fetch = require('node-fetch');

class MissionControlClient {
  constructor(baseUrl, apiKey, agentName) {
    this.baseUrl = baseUrl;
    this.apiKey = apiKey;
    this.agentName = agentName;
    this.connectionId = null;
    this.agentId = null;
    this.heartbeatInterval = null;
  }

  async connect(toolName, toolVersion) {
    const res = await fetch(`${this.baseUrl}/api/connect`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': this.apiKey,
      },
      body: JSON.stringify({
        tool_name: toolName,
        tool_version: toolVersion,
        agent_name: this.agentName,
        agent_role: 'developer',
      }),
    });

    if (!res.ok) {
      throw new Error(`Connection failed: ${await res.text()}`);
    }

    const data = await res.json();
    this.connectionId = data.connection_id;
    this.agentId = data.agent_id;

    console.log(`✓ Connected as ${data.agent_name} (${this.connectionId})`);

    // Start heartbeat loop
    this.startHeartbeat();
  }

  startHeartbeat() {
    this.heartbeatInterval = setInterval(async () => {
      try {
        const res = await fetch(`${this.baseUrl}/api/agents/${this.agentId}/heartbeat`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-api-key': this.apiKey,
          },
          body: JSON.stringify({
            connection_id: this.connectionId,
          }),
        });

        const data = await res.json();
        if (data.work_items && data.work_items.length > 0) {
          console.log(`✓ Work items available: ${data.total_items}`);
          this.handleWorkItems(data.work_items);
        }
      } catch (err) {
        console.error('Heartbeat failed:', err.message);
      }
    }, 30000); // 30 seconds
  }

  subscribeToEvents() {
    const es = new EventSource(`${this.baseUrl}/api/events`, {
      headers: { 'x-api-key': this.apiKey },
    });

    es.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        console.log('Event:', data.type, data.data);
      } catch (err) {
        // Ignore heartbeat comments
      }
    };

    es.onerror = (err) => {
      console.error('SSE error:', err);
    };

    return es;
  }

  handleWorkItems(workItems) {
    for (const item of workItems) {
      switch (item.type) {
        case 'mentions':
          console.log(`📢 ${item.count} mentions`);
          break;
        case 'assigned_tasks':
          console.log(`📋 ${item.count} assigned tasks`);
          break;
        case 'notifications':
          console.log(`🔔 ${item.count} notifications`);
          break;
      }
    }
  }

  async reportTokens(model, inputTokens, outputTokens) {
    await fetch(`${this.baseUrl}/api/tokens`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': this.apiKey,
      },
      body: JSON.stringify({
        model,
        sessionId: `${this.agentName}:cli`,
        inputTokens,
        outputTokens,
      }),
    });
  }

  async disconnect() {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
    }

    if (this.connectionId) {
      await fetch(`${this.baseUrl}/api/connect`, {
        method: 'DELETE',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': this.apiKey,
        },
        body: JSON.stringify({ connection_id: this.connectionId }),
      });
      console.log('✓ Disconnected');
    }
  }
}

// Usage
const client = new MissionControlClient(
  'http://localhost:3000',
  'operator:abc123',
  'my-agent'
);

await client.connect('custom-cli', '1.0.0');
const events = client.subscribeToEvents();

process.on('SIGTERM', async () => {
  await client.disconnect();
  process.exit(0);
});

Troubleshooting

Cause: Previous connection with same agent_name is still active.Solution: Each agent can only have one active connection. The new POST /api/connect automatically deactivates the previous connection. Wait a few seconds and retry.
Cause: Agent ID or name not found.Solution:
  • Use the agent_id returned from /api/connect
  • Or use the exact agent_name string
  • Verify workspace_id matches your API key scope
Cause: Proxy or load balancer timeout.Solution:
  • SSE sends : heartbeat\n\n every 30 seconds to prevent timeouts
  • Configure your proxy to allow long-lived connections
  • For nginx: proxy_read_timeout 300s;
Cause: Missing required fields in token_usage object.Solution: Ensure all fields are present:
{
  "token_usage": {
    "model": "claude-sonnet-4",
    "inputTokens": 1500,
    "outputTokens": 800
  }
}
Check response for "token_recorded": true.

OpenClaw Gateway

Full-featured gateway for production deployments

Webhooks

Receive events via HTTP callbacks

Authentication

API key management and roles