Claude
Skills
Sign in
Back

channels-bootstrap

Included with Lifetime
$97 forever

Production-ready channel server implementations — CI webhook receiver, mobile approval relay, Discord/Telegram bridge, and local fakechat dev profile. Copy-paste starter code with sender allowlists, permission relay, and security hardening.

Cloud & DevOps

What this skill does


# Channels Bootstrap — Starter Implementations

Four production-ready channel server patterns. Each is runnable with Bun or Node.js,
includes sender allowlists, and handles the security concerns that make channels safe to
deploy. Copy the one that fits your use case, drop it in your repo, and register it in
`.mcp.json`.

> **Prerequisites**: Claude Code v2.1.80+, claude.ai login (not API key), Bun or Node.js
> **Security rule**: Every inbound channel is a prompt injection vector — always gate on
> sender identity before forwarding any content to Claude.

---

## Pattern 1: CI Webhook Receiver

**Use case**: React to CI/CD events (GitHub Actions failures, build completions, deploy
status) without polling. Claude gets notified when a build breaks and can investigate,
post a PR comment, or open a fix branch automatically.

### `channels/ci-webhook.ts`

```typescript
#!/usr/bin/env bun
/**
 * CI Webhook Channel — receives POST events from GitHub Actions, GitLab CI,
 * Jenkins, or any webhook-capable CI system and pushes them into Claude Code.
 *
 * Security: webhook HMAC signature verification (GitHub Actions compatible).
 * Setup: set WEBHOOK_SECRET env var matching your CI platform's secret.
 *
 * Register in .mcp.json:
 *   "ci-webhook": { "command": "bun", "args": ["./channels/ci-webhook.ts"],
 *                   "env": { "WEBHOOK_SECRET": "${WEBHOOK_SECRET}" } }
 *
 * Start:
 *   claude --dangerously-load-development-channels server:ci-webhook
 *
 * Test:
 *   curl -X POST http://127.0.0.1:8788/webhook \
 *     -H "Content-Type: application/json" \
 *     -d '{"action":"completed","workflow_run":{"name":"CI","conclusion":"failure","html_url":"https://github.com/org/repo/actions/runs/123"}}'
 */
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { createHmac, timingSafeEqual } from 'crypto'

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET ?? ''
const PORT = parseInt(process.env.CI_WEBHOOK_PORT ?? '8788', 10)

// Map CI event types to human-readable summaries for Claude
function summarizeEvent(event: string, body: Record<string, unknown>): string | null {
  switch (event) {
    case 'workflow_run': {
      const run = body.workflow_run as Record<string, unknown>
      if (!run) return null
      if (run.conclusion === 'failure') {
        return `CI FAILURE: Workflow "${run.name}" failed on branch "${run.head_branch}". ` +
               `Commit: ${String(run.head_sha).slice(0, 7)}. ` +
               `Run: ${run.html_url}`
      }
      if (run.conclusion === 'success') {
        return `CI SUCCESS: Workflow "${run.name}" passed on branch "${run.head_branch}".`
      }
      return null  // ignore in-progress events
    }
    case 'push': {
      const commits = (body.commits as unknown[])?.length ?? 0
      const ref = String(body.ref ?? '').replace('refs/heads/', '')
      const pusher = (body.pusher as Record<string, unknown>)?.name ?? 'unknown'
      return `PUSH: ${pusher} pushed ${commits} commit(s) to ${ref}.`
    }
    case 'pull_request': {
      const pr = body.pull_request as Record<string, unknown>
      const action = String(body.action ?? '')
      if (!['opened', 'ready_for_review', 'closed'].includes(action)) return null
      const state = action === 'closed' && body.merged ? 'merged' : action
      return `PR ${state.toUpperCase()}: "${pr?.title}" → ${pr?.base?.branch ?? 'main'}. ` +
             `URL: ${pr?.html_url}`
    }
    case 'deployment_status': {
      const ds = body.deployment_status as Record<string, unknown>
      const env = (body.deployment as Record<string, unknown>)?.environment ?? 'unknown'
      if (ds?.state === 'failure') {
        return `DEPLOY FAILURE: Deployment to ${env} failed. ` +
               `Log: ${ds?.log_url ?? 'no log'}`
      }
      if (ds?.state === 'success') {
        return `DEPLOY SUCCESS: Deployed to ${env}.`
      }
      return null
    }
    default:
      return null
  }
}

// Verify GitHub webhook signature (HMAC-SHA256)
async function verifySignature(body: string, signature: string | null): Promise<boolean> {
  if (!WEBHOOK_SECRET) return true  // no secret configured — allow all (dev mode)
  if (!signature) return false
  const expected = 'sha256=' + createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex')
  try {
    return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  } catch {
    return false
  }
}

const mcp = new Server(
  { name: 'ci-webhook', version: '1.0.0' },
  {
    capabilities: { experimental: { 'claude/channel': {} } },
    instructions:
      'CI and deployment events arrive as <channel source="ci-webhook" event="..." ...>. ' +
      'For FAILURE events: investigate the failure, check recent commits, and propose a fix. ' +
      'For DEPLOY FAILURE: check logs and determine rollback vs hot-fix. ' +
      'For PR events: summarize changes if asked. ' +
      'Do not act on SUCCESS events unless explicitly asked.',
  },
)

await mcp.connect(new StdioServerTransport())

const server = Bun.serve({
  port: PORT,
  hostname: '127.0.0.1',
  async fetch(req: Request) {
    if (new URL(req.url).pathname !== '/webhook') {
      return new Response('not found', { status: 404 })
    }
    if (req.method !== 'POST') {
      return new Response('method not allowed', { status: 405 })
    }

    const rawBody = await req.text()
    const sig = req.headers.get('x-hub-signature-256')
    if (!(await verifySignature(rawBody, sig))) {
      return new Response('forbidden', { status: 403 })
    }

    const event = req.headers.get('x-github-event') ?? 'unknown'
    let parsed: Record<string, unknown>
    try {
      parsed = JSON.parse(rawBody)
    } catch {
      return new Response('bad request', { status: 400 })
    }

    const summary = summarizeEvent(event, parsed)
    if (summary === null) {
      return new Response('ok (ignored)')  // don't forward irrelevant events
    }

    await mcp.notification({
      method: 'notifications/claude/channel',
      params: {
        content: summary,
        meta: { event, severity: summary.startsWith('CI FAILURE') || summary.startsWith('DEPLOY FAILURE') ? 'high' : 'info' },
      },
    })

    return new Response('ok')
  },
})

console.error(`CI webhook channel listening on http://127.0.0.1:${PORT}/webhook`)
```

### Registration

```json
{
  "mcpServers": {
    "ci-webhook": {
      "command": "bun",
      "args": ["./channels/ci-webhook.ts"],
      "env": {
        "WEBHOOK_SECRET": "${WEBHOOK_SECRET}",
        "CI_WEBHOOK_PORT": "8788"
      }
    }
  }
}
```

### GitHub Actions Setup

Add a webhook in your repository (Settings → Webhooks → Add webhook):
- **Payload URL**: `http://your-tunnel/webhook` (use ngrok or Cloudflare Tunnel to expose local port)
- **Content type**: `application/json`
- **Secret**: same value as `WEBHOOK_SECRET`
- **Events**: Workflow runs, Push, Pull requests, Deployment statuses

```bash
# Quick local tunnel (for dev/testing)
# Requires ngrok: https://ngrok.com
ngrok http 8788

# Cloudflare Tunnel (persistent, no account needed for quick test)
cloudflared tunnel --url http://127.0.0.1:8788
```

### Claude Startup

```bash
# Load channel with development flag (until your channel is in official marketplace)
claude --dangerously-load-development-channels server:ci-webhook
```

---

## Pattern 2: Mobile Approval Relay

**Use case**: Approve or deny Claude's tool calls from your phone when you're away from
your desk. Forwards tool approval prompts to Telegram (or any chat platform), parses
yes/no responses, relays verdicts back to Claude Code.

Requires Claude Code v2.1.81+.

### `channels/mobile-approval.ts`

```typescript
#!/usr/bin/env bun
/**
 * Mobile Approval Relay — forwards Claude Code permission prompts to Telegram
 * and relays yes/no verdicts back. Lets you approve tool calls from your phone.
 *
 * Prerequisites:
 *   - Telegram bot token (BotFather → /newbot)
 *   - Yo

Related in Cloud & DevOps