{
  "name": "cnvs.app",
  "description": "Free, no-signup real-time collaborative whiteboard. Every board at https://cnvs.app/#<id> can be read and mutated by an AI agent alongside human collaborators — strokes, text and images sync live in ~100ms.",
  "documentation": "https://cnvs.app/llms.txt",
  "skills": {
    "cnvs-whiteboard": {
      "description": "Agent Skill that teaches an AI agent how to collaborate on a cnvs.app board — read shared state, add/update/move/delete text/links/strokes/images via REST, subscribe to live edits via MCP, and render the SVG preview when the board has drawings. Use when the user references any cnvs.app board or asks to annotate/edit a shared whiteboard.",
      "canonical": "https://cnvs.app/cnvs-whiteboard/SKILL.md",
      "repository": "https://github.com/lksrz/cnvs-whiteboard-skills",
      "files": {
        "SKILL.md": "https://cnvs.app/cnvs-whiteboard/SKILL.md"
      },
      "pairs_with": "mcp-listen"
    },
    "mcp-listen": {
      "description": "Generic Claude Code / Agent Skill that subscribes to any Streamable-HTTP MCP server's resource updates and emits one JSON line per push on stdout — designed to be wrapped by the Monitor tool so each event becomes an in-chat notification. Not cnvs-specific.",
      "canonical": "https://cnvs.app/mcp-listen/SKILL.md",
      "repository": "https://github.com/lksrz/cnvs-whiteboard-skills",
      "files": {
        "SKILL.md": "https://cnvs.app/mcp-listen/SKILL.md",
        "scripts/listen.mjs": "https://cnvs.app/mcp-listen/scripts/listen.mjs",
        "package.json": "https://cnvs.app/mcp-listen/package.json"
      }
    }
  },
  "httpApi": {
    "openapi": "https://cnvs.app/openapi.json",
    "quotas": "https://cnvs.app/quotas.json",
    "note": "Full HTTP fallback for AI agents that cannot use MCP — covers reads AND mutations (add/update/move/delete texts, strokes, images, links). Same validator + live broadcast as MCP. MCP additionally provides live resource subscriptions and the wait_for_update long-poll. POST /api/boards also accepts a board-import body (content + autolayout + lock + author) to create a pre-filled, optionally locked board in one atomic call — mirrored by the create_board MCP tool; feature-detect via the `imported` field in the response. Live, machine-readable limits manifest at /quotas.json."
  },
  "servers": [
    {
      "name": "cnvs",
      "url": "https://cnvs.app/mcp",
      "transport": "streamable-http",
      "protocolVersion": "2025-06-18",
      "authentication": {
        "default": "none — the board ID is the access credential.",
        "perBoardLock": {
          "modes": ["write", "all"],
          "keyFormat": "8 chars [a-z0-9] (6-char legacy keys also accepted)",
          "channels": {
            "header": "X-Board-Key on the JSON-RPC POST",
            "argument": "`access_key` field inside the tool / resource params"
          },
          "errorCode": -32001,
          "errorData": "{code: 'board_locked', lockMode: 'write'|'all'}",
          "endpoints": {
            "lock": "POST https://cnvs.app/api/boards/<id>/lock",
            "unlock": "POST https://cnvs.app/api/boards/<id>/unlock",
            "verify": "POST https://cnvs.app/api/boards/<id>/verify-key"
          },
          "noRecovery": true
        }
      },
      "notes": "Boards are open by default — the board ID is the access credential. Optionally a board can be PIN-locked (`mode: write` requires the key for mutations only; `mode: all` requires it for everything). On a locked board, every gated tool will respond with JSON-RPC error -32001 `board_locked` until the client supplies the key via the X-Board-Key header or the `access_key` tool argument."
    }
  ],
  "quotasManifest": "https://cnvs.app/quotas.json",
  "quotas": {
    "perRequest": {
      "maxBodyBytes": 5242880,
      "note": "Single request body cap. Enforced up-front via Content-Length AND during the streamed body read, so chunked / header-less clients cannot bypass it. HTTP 413 / JSON-RPC -32000 code:payload_too_large."
    },
    "perBoard": {
      "maxTexts": 500,
      "maxTextContentChars": 100000,
      "maxImages": 50,
      "maxImageBytesTotal": 10485760,
      "maxImageBytesPerItem": 900000,
      "maxStrokes": 2000,
      "maxColumns": 200,
      "maxColumnsPerLane": 20,
      "maxLanes": 10,
      "maxTasks": 1000,
      "maxTaskContentChars": 20000
    },
    "boardImport": {
      "maxItems": 40,
      "maxBatchStatements": 45,
      "countedCollections": ["texts", "lines", "images", "columns", "tasks", "lanes"],
      "counting": "Sum of input array entries exactly as supplied — entries are never merged; strokes aliases lines. Template-seeded columns are not input items.",
      "maxLaneTitles": 10,
      "laneTitleRule": "Each lane title must reference a lane used by a column created in the same request.",
      "infrastructureToken": "X-Import-Token is a server-to-server credential. A valid token selects the infrastructure REST/MCP and board-create rate tiers; it does not bypass import, validation, per-item, per-board, request-body, board-access-lock, security, or Cloudflare platform limits."
    },
    "fieldLimits": {
      "maxAuthorChars": 80,
      "maxThumbnailBytes": 8000,
      "textWidth": { "min": 160, "max": 4096 },
      "columnWidth": { "min": 200, "max": 480, "default": 280 },
      "maxLaneTitleChars": 200,
      "maxColumnTitleChars": 200,
      "maxTaskNameChars": 500,
      "maxAssigneeChars": 200
    },
    "operationLimits": {
      "maxRecolorItems": 500,
      "maxBatchOps": 500,
      "wsContinuation": { "maxFrames": 500, "idleMs": 500 }
    },
    "rateLimits": {
      "standard": {
        "restMcpPerBoard": { "requests": 60, "windowSeconds": 10, "per": "board", "enforcement": "Worker-isolate-local soft ceiling" },
        "boardCreate": { "requests": 5, "windowSeconds": 60, "per": "clientIp", "enforcement": "Cloudflare Rate Limiting binding; permissive and eventually consistent per edge location" }
      },
      "infrastructure": {
        "credential": "valid X-Import-Token (server-to-server only)",
        "restMcpPerBoard": { "requests": 600, "windowSeconds": 10, "per": "board", "enforcement": "Worker-isolate-local soft ceiling" },
        "boardCreate": { "requests": 50, "windowSeconds": 60, "per": "infrastructureToken", "perNote": "Keyed by a one-way digest of the token VALUE (the credential never enters limiter keys or telemetry). There is a single import-token secret, so all trusted callers share this one bucket — it is not a per-integration allowance.", "enforcement": "Cloudflare Rate Limiting binding; permissive and eventually consistent per edge location" }
      },
      "webSocket": { "requests": 60, "windowSeconds": 10, "per": "board", "enforcement": "BoardServer Durable Object, strong", "infrastructureTokenChangesTier": false },
      "security": { "lockUnlockVerify": { "requests": 5, "windowSeconds": 60, "per": "clientIp", "infrastructureTokenChangesTier": false } },
      "tokenPolicy": "X-Import-Token raises only REST/MCP and board-create rate tiers. It never bypasses hard caps or board access locks.",
      "onExceeded": { "rest": "HTTP 429 + Retry-After", "mcp": "-32000 code:rate_limited", "ws": "{type:\"error\", code:\"rate_limited\"} frame" }
    },
    "cors": {
      "allowOrigin": "*",
      "allowMethods": ["GET", "POST", "DELETE", "OPTIONS"],
      "preflightSupported": true
    },
    "onExceeded": "MCP returns JSON-RPC error -32000; REST returns HTTP 413; browser WebSocket receives {type:\"error\", code:\"quota_exceeded\"} which surfaces as a toast."
  },
  "capabilities": {
    "tools": [
      "open_board",
      "create_board",
      "get_board",
      "get_preview",
      "add_text",
      "add_link",
      "add_image",
      "draw_stroke",
      "move",
      "erase",
      "wait_for_update",
      "set_board_mode",
      "create_column",
      "update_column",
      "delete_column",
      "create_task",
      "update_task",
      "move_task",
      "delete_task",
      "list_tasks"
    ],
    "resources": [
      "cnvs://board/{id}/state.json",
      "cnvs://board/{id}/preview.svg",
      "cnvs://board/{id}/tasks.json"
    ],
    "subscribe": true,
    "notifications": {
      "resources/updated": {
        "debounceMs": 3000,
        "transport": "GET /mcp SSE"
      }
    }
  },
  "clientConfig": {
    "claudeDesktop": {
      "mcpServers": {
        "cnvs": {
          "type": "http",
          "url": "https://cnvs.app/mcp"
        }
      }
    },
    "claudeCode": "claude mcp add --transport http cnvs https://cnvs.app/mcp"
  }
}
