SPECMCP ServerAlerting Bridge

Paperclip CEO Model Context Protocol Server & Alerting Bridge

A secure, narrow MCP interface to the private paperclip-ceo-bridge plus an outbound webhook loop that pushes blockers and decisions straight to Poke.

We need to implement a Model Context Protocol (MCP) server that acts as a secure, narrow interface to our private paperclip-ceo-bridge container (running on http://127.0.0.1:3200). This will allow Poke to securely converse with the CEO agent and manage tasks.

We also need to implement an outbound webhook alerting loop so the bridge can push unsolicited alerts (such as blockers or decision points requiring Connor) directly to a Poke ingestion endpoint.

Inbound traffic flows down through the tunnel and MCP server into the locked-down bridge. The bridge fires proactive alerts back up to Poke's webhook independently.

  1. 01You (iMessage / Poke)
  2. 02Poke MCP tool calls
  3. 03Secure tunnelTailscale Funnel or Cloudflare Tunnel
  4. 04paperclip-ceo-mcpPort 3300
  5. 05paperclip-ceo-bridgePort 3200 · localhost only
  6. 06PaperclipExpress / Drizzle / Postgres
Proactive alerts

The local bridge pushes blockers and decisions up to the Poke webhook, out of band from the request flow.

Configure a new service paperclip-ceo-mcp to run alongside paperclip-ceo-bridge. Network them together so the MCP server can access the bridge over the local Docker bridge network, while keeping the bridge itself strictly closed to external traffic.

yaml
version: '3.8'

services:
  paperclip-ceo-bridge:
    image: paperclip-ceo-bridge:latest
    container_name: paperclip-ceo-bridge
    build:
      context: ./paperclip-ceo-bridge
    ports:
      - "127.0.0.1:3200:3200" # Explicitly bound to localhost only
    environment:
      - PORT=3200
      - PAPERCLIP_API_TOKEN=${PAPERCLIP_API_TOKEN}
      - BRIDGE_SECRET=${BRIDGE_SECRET}
      - POKE_WEBHOOK_URL=${POKE_WEBHOOK_URL} # Target for outbound alerts
    restart: unless-stopped

  paperclip-ceo-mcp:
    image: paperclip-ceo-mcp:latest
    container_name: paperclip-ceo-mcp
    build:
      context: ./paperclip-ceo-mcp
    ports:
      - "3300:3300" # Exposed internally on the host; will be funneled via Tailscale
    environment:
      - PORT=3300
      - BRIDGE_URL=http://paperclip-ceo-bridge:3200
      - BRIDGE_SECRET=${BRIDGE_SECRET}
    depends_on:
      - paperclip-ceo-bridge
    restart: unless-stopped

Create a clean TypeScript / Node.js project using @modelcontextprotocol/sdk. It must expose an SSE (Server-Sent Events) transport or standard HTTP POST endpoint matching the MCP spec so Poke can reach it remotely over a web tunnel.

Tool schemas to expose

01send_ceo_message

Args: message: string

Maps to: POST http://paperclip-ceo-bridge:3200/message

02get_ceo_updates

Args: None

Maps to: GET http://paperclip-ceo-bridge:3200/updates

03get_open_decisions

Args: None

Maps to: Retrieves pending issues with decision labels or blockers

04answer_decision

Args: issueId: string, answer: string

Maps to: Posts a comment or resolves a specific decision issue

05get_task_status

Args: issueId: string

Maps to: Checks the specific status of an ongoing worker ticket

TypeScript Express implementation draft (SSE)

typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";

const app = express();
const server = new Server({
  name: "paperclip-ceo-mcp",
  version: "1.0.0"
}, {
  capabilities: {
    tools: {}
  }
});

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "send_ceo_message",
        description: "Send a message or directive to the Paperclip CEO agent to kick off tasks",
        inputSchema: {
          type: "object",
          properties: {
            message: { type: "string" }
          },
          required: ["message"]
        }
      },
      // Register remaining tools...
    ]
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  const bridgeUrl = process.env.BRIDGE_URL || "http://paperclip-ceo-bridge:3200";
  const secret = process.env.BRIDGE_SECRET;

  // Execute calls to paperclip-ceo-bridge with AUTH headers
});

let transport: SSEServerTransport;
app.get("/sse", (req, res) => {
  transport = new SSEServerTransport("/messages", res);
  server.connect(transport);
});

app.post("/messages", (req, res) => {
  transport.handleMessage(req, res);
});

app.listen(process.env.PORT || 3300, () => {
  console.log("MCP Server running on port 3300");
});

When the Paperclip CEO agent writes a new update, flags a blocker, or hits a decision point:

  1. 1The paperclip-ceo-bridge must capture this event.
  2. 2If POKE_WEBHOOK_URL is set, the bridge must fire a POST request with a payload containing:
type

"blocker" | "decision" | "update"

message

The markdown text from the CEO

issueId

The ID of the parent thread / issue

choices

Optional string array for a bounded question, e.g. ["Approve", "Reject", "Snooze"]

Please perform the following actions:

  1. 1Write the Dockerfile and TypeScript code for the new paperclip-ceo-mcp service.
  2. 2Update the existing paperclip-ceo-bridge service to support sending outbound HTTP POST requests to POKE_WEBHOOK_URL.
  3. 3Provide a simple docker-compose.yml that pulls both of these services together on Connor's local docker host.
  4. 4Ensure the system builds successfully and output test curl commands Connor can run to verify health checks on both ports (3200 and 3300).

Suggested health-check commands

bash
# Bridge (localhost only) - run on the docker host
curl -s http://127.0.0.1:3200/health

# MCP server (host-exposed, later funneled via Tailscale)
curl -s http://127.0.0.1:3300/health

# Confirm the MCP can reach the bridge over the internal docker network
docker compose exec paperclip-ceo-mcp \
  curl -s http://paperclip-ceo-bridge:3200/health