{
  "$schema": "https://modelcontextprotocol.io/schemas/server-card.json",
  "name": "ai.trydock/dock",
  "title": "Dock",
  "description": "The AI workspace for you, your team, and every agent you run. Tables and docs, one live surface.",
  "version": "1.0.1",
  "websiteUrl": "https://trydock.ai",
  "repository": {
    "url": "https://github.com/try-dock-ai/mcp",
    "source": "github"
  },
  "endpoint": "https://trydock.ai/api/mcp",
  "transport": "streamable-http",
  "auth": {
    "type": "oauth2",
    "authorization_endpoint": "https://trydock.ai/oauth/authorize",
    "token_endpoint": "https://trydock.ai/oauth/token",
    "registration_endpoint": "https://trydock.ai/oauth/register",
    "protected_resource_metadata": "https://trydock.ai/.well-known/oauth-protected-resource",
    "authorization_server_metadata": "https://trydock.ai/.well-known/oauth-authorization-server",
    "scopes_supported": [
      "mcp"
    ],
    "bearer_fallback": {
      "description": "Accepts Bearer tokens of the form 'dk_live_<48 hex>' minted at /settings as an alternative to OAuth.",
      "header": "Authorization"
    }
  },
  "capabilities": {
    "tools": {
      "listChanged": false
    }
  },
  "instructions": "You are connected to Dock, a shared cloud workspace for humans and AI agents.\n\n# Mental model\n\nA workspace is a container of one or more surfaces (tabs). Each surface is either a `doc` (TipTap rich-text body) or a `table` (typed rows + columns). A workspace can hold any combination, one or many of either kind — one doc, one table, two docs and a table, etc. There is no rule that every workspace contains both. `mode` on the workspace just picks which tab opens first; it does not restrict what surfaces exist.\n\n# Pick the right surface\n\n- Prose (briefs, summaries, retros, status reports, anything you'd write as paragraphs and headings) → `doc`. Use `update_doc(markdown=...)` for full replacement, `append_doc_section(markdown=...)` for append-only at the end, or `update_doc_section(heading, markdown)` to refresh just one section in the middle. Never hand-build ProseMirror JSON.\n- Records with shared columns (tasks, leads, ingest output, anything tabular) → `table`. Use `create_row` / `update_row`.\n\nThe bias to avoid: shredding prose into single-column rows because rows feel easier. Pick the shape that fits the content.\n\n# Rich content in docs\n\nDoc bodies accept the full friendly format set via markdown. Pick the right syntax for the content shape:\n\n- **Diagrams** → ```mermaid` fenced block. 15 sub-types covered by one library: flowchart, sequence, gantt, ER, state, class, mindmap, timeline, pie, quadrant, sankey, XY-chart, packet, block, journey. Diagram source is text; agents emit it natively.\n- **Math** → `$x$` (inline) or `$$x$$` (block). LaTeX, KaTeX-rendered. Use this instead of typesetting math in code blocks or as plain text.\n- **Callouts** → `> [!NOTE]` / `[!TIP]` / `[!IMPORTANT]` / `[!WARNING]` / `[!CAUTION]`. GFM-standard, identical to GitHub's renderer.\n- **Custom diagrams (escape hatch)** → ```svg` fenced block. Universal fallback for anything Mermaid doesn't model (geometry, decorative figures, custom layouts). Scripts and event handlers are stripped at write time, so write whatever SVG markup you need without worrying about XSS — the gate is applied for you.\n- **Collapsible sections** → `<details><summary>title</summary>\\nbody...\\n</details>`. Native disclosure widget; useful for FAQ-style content or long appendix material.\n- **Cross-references** → `[[slug]]` (whole workspace), `[[slug#tab]]` (specific surface), `[[slug#row-id]]` (specific row), `[[org/slug]]` (org-prefixed), `[[slug|display]]` (custom label). Each cross-ref creates a Backlink row, so the target's \"referenced from\" sidebar widget shows this doc. Targets the reader can't see render as plain text (no info leak). Use cross-refs liberally — they're how you build the workspace knowledge graph.\n- **External embeds** → paste a URL on its own line from a safelisted provider: YouTube, Vimeo, Loom, Figma, CodePen, GitHub gists. The URL becomes a sandboxed iframe. URLs not on the safelist stay as plain links — there's no auto-promotion to iframe outside the safelist.\n\nPer-format caps: max 50 Mermaid diagrams (30 KB source), max 500 math expressions (8 KB source), max 50 SVG blocks (100 KB source post-sanitize), max 200 cross-refs per doc, max 20 embeds per doc. Cap breaches return DocGuardError with the specific dimension. Trim and retry.\n\n**Pre-flight check**: `validate_doc_markdown(markdown=...)` parses your markdown and returns { ok, errors, warnings, parsed } with counts per format type — no writes, no side effects. Use this when iterating on rich-format markdown to catch cap breaches, syntax errors, and unresolved cross-refs BEFORE burning a real write.\n\nTips:\n- Prefer Mermaid over hand-written ASCII art or images for any standard diagram type. Mermaid renders to SVG (zoom-friendly on mobile, indexable for search), round-trips cleanly through `get_doc`, and the agent reading the doc back gets the source verbatim.\n- Use the ```svg` escape hatch ONLY when Mermaid doesn't fit (custom geometry, decorative figures). Always include a `<title>` element inside the SVG for screen readers.\n- Wrap long appendix material or rare-path branches in `<details>` so the doc reads compactly by default but the full content is one click away.\n\n# You can edit anything, anytime\n\nNothing in Dock is write-once. Reach for the right tool whenever state is wrong:\n\n- Rows: `update_row` (partial merge), `delete_row`\n- Doc bodies: `update_doc` (replace whole body), `append_doc_section` (append at end), `update_doc_section(heading, markdown)` (replace one section in the middle without disturbing the rest — heading-keyed, doesn't race with concurrent edits to other sections)\n- Workspace metadata: `update_workspace` (rename, mode, visibility)\n- Tabs: `create_surface`, `update_surface`, `delete_surface` — workspaces are not frozen with their starting tabs, grow them when the work needs another\n- Members: `share_workspace`, `update_workspace_member`, `remove_workspace_member`\n\n# Subscribe to changes, don't poll\n\n`create_webhook` registers an HMAC-signed endpoint that fires on every row, doc, member, and workspace mutation. Use this to wake up on state changes instead of polling on a timer. Manage with `list_webhooks` / `update_webhook` / `rotate_webhook_secret` / `delete_webhook`.\n\n# Self-manage org settings\n\nYou can drive billing and credentials directly. Don't tell users to \"go to settings\" for things you can do yourself:\n\n- Plans: `get_billing`, `upgrade_plan`, `downgrade_plan`, `request_limit_increase`. Plan changes are consent-gated; you pick the surface via `mode`. Default `mode: \"chat\"`: first call returns a `confirm_token` + summary you surface in chat; second call (within 60s, passing the token) flips the plan. Or pass `mode: \"web\"`: first call returns an `approval_url` you print in chat; user clicks, approves in browser, you poll `polling_url` for the result. Use web mode if you're headless or your user prefers a click-to-approve flow.\n- Credentials: `rotate_api_key`, `revoke_api_key` (your own key only).\n- Support: `create_support_ticket` instead of \"email support\" (Dock has no support email).\n\n# Bootstrap a key for sibling agents\n\nIf a teammate agent or a chat-only agent (no MCP, no terminal) needs to onboard onto Dock, point them at the click-to-approve flow at `POST /api/agent-bootstrap/start`: they pass an `agent_name` (and optional `workspace_slug`), surface the returned `approval_url` to the user, and poll `polling_url` every ~3s. When the user clicks Approve in their browser, the next poll returns `{ status: \"approved\", api_key, agent_id, workspace_slug }`. Same security model as `gh auth login` — single-click consent, single-use 5-min token, no terminal required. Docs: `/docs/agent-prompt#bootstrap`.\n\n# Slug form\n\nWorkspace slugs accept two forms: bare (`my-workspace`) or org-prefixed (`my-org/my-workspace`). Both resolve to the same workspace. The dashboard URL shows the org-prefixed form; pass whichever you have.\n\n# Multi-tab workspaces\n\n`list_surfaces(slug)` enumerates the tabs inside a workspace. Pass `surface_slug` on doc/row tools to address a specific tab; omit it to fall through to the workspace's primary surface of that kind.\n\n# Discovery\n\n- `list_workspaces` for everything you can access.\n- `search(q, kind?)` when the user names something fuzzily, faster than listing + filtering.\n- `get_recent_events(slug)` when picking up a workspace after time away.\n\n# When you hit an error\n\nEvery error response carries an `x-request-id`. Include it as context when you `create_support_ticket`. Cap errors include `details.upgrade` (in-plan upgrades) or `details.increase` (past-Scale asks); call the named tool, don't escalate to a human.\n",
  "tools": [
    {
      "name": "list_workspaces",
      "description": "List all workspaces the authenticated principal has access to. Returns workspace name (slug), mode (the default-view preference for the first tab), and creation date. A workspace is a container of one or more surfaces (tabs); each surface is either a `table` (rows + columns) or a `doc` (TipTap body), and a workspace can hold any combination, one or many of either kind. Use `list_surfaces` to see what a given workspace actually contains.",
      "inputSchema": {
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "get_workspace",
      "description": "Get details about a specific workspace by its slug, including columns of its primary table surface, member count, and row count. A workspace contains one or more surfaces (tabs): any combination of `table` (rows + columns) and `doc` (TipTap body) kinds, one or many of either. Use `list_surfaces` to enumerate every tab; fetch /rows or /doc to read or write a specific one.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug, e.g. 'reddit-tracker'. Accepts either the bare slug or the org-prefixed form ('my-org/reddit-tracker') as shown in the dashboard URL."
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "list_rows",
      "description": "List rows in a workspace's table surface. Returns rows with their data (a JSON object of column-name to value), creation time, the principal who created/updated each row, AND the row's `surface_slug` (the sheet it lives on). Empty array if no rows have been added yet. Multi-surface workspaces: pass `surface_slug` to scope to one sheet; omit to return rows from every surface in the workspace (back-compat: pre-multi-surface clients keep working).",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "surface_slug": {
            "type": "string",
            "description": "Optional table surface slug for multi-surface workspaces. Filter rows to one sheet. Omit to return rows from every surface (legacy single-sheet clients see no change). 400 if the slug is a doc surface, archived, or doesn't exist."
          },
          "limit": {
            "type": "number",
            "description": "Max rows to return (default 100, max 1000)"
          },
          "offset": {
            "type": "number",
            "description": "Number of rows to skip (for pagination)"
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "create_row",
      "description": "Append a new row to a workspace's table surface. The data field is a JSON object with column-name keys. Status column accepts: drafted, queued, sealed, active, blocked. Works on any workspace; columns auto-seed on the first row if the table surface is empty. Multi-surface workspaces accept `surface_slug` to target a specific sheet (use `list_surfaces` to enumerate); omit it to fall through to the workspace's primary table surface.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "data": {
            "type": "object",
            "description": "Row data as a JSON object (e.g. {\"title\": \"My post\", \"status\": \"drafted\", \"notes\": \"Initial draft\"})",
            "additionalProperties": true
          },
          "surface_slug": {
            "type": "string",
            "description": "Optional table surface slug for multi-surface workspaces. Omit to write to the workspace's primary table surface. 400 if the slug is a doc surface, archived, or doesn't exist."
          }
        },
        "required": [
          "slug",
          "data"
        ]
      }
    },
    {
      "name": "get_row",
      "description": "Fetch a single row by id without listing the full table. Useful when a cue payload carries a row id and the agent only needs that one record. Returns the same row shape as list_rows.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "rowId": {
            "type": "string",
            "description": "The row id"
          }
        },
        "required": [
          "slug",
          "rowId"
        ]
      }
    },
    {
      "name": "update_row",
      "description": "Update specific fields of an existing row. Only the fields provided in `data` are updated; others are preserved. Setting `surface_slug` to a different sheet than the row currently lives on MOVES the row to that sheet (position recomputes to the new sheet's tail unless `position` is also set). Same surface as current → no-op move.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "rowId": {
            "type": "string",
            "description": "The row ID to update"
          },
          "data": {
            "type": "object",
            "description": "Partial row data with fields to update (e.g. {\"status\": \"sealed\"}). Pass an empty object {} when the call is purely a move (surface_slug change with no field updates).",
            "additionalProperties": true
          },
          "surface_slug": {
            "type": "string",
            "description": "Optional. When set to a different surface than the row currently lives on, moves the row to that surface and emits a `row.moved_surface` event. Same-surface is a no-op. 400 if the slug is a doc surface, archived, or not in this workspace."
          },
          "position": {
            "type": "number",
            "description": "Optional. Override the row's position. When moving across surfaces, omit to land at the new surface's tail; pass a number to land at a specific slot."
          }
        },
        "required": [
          "slug",
          "rowId",
          "data"
        ]
      }
    },
    {
      "name": "delete_row",
      "description": "Permanently delete a row from a workspace. This action cannot be undone.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "rowId": {
            "type": "string",
            "description": "The row ID to delete"
          }
        },
        "required": [
          "slug",
          "rowId"
        ]
      }
    },
    {
      "name": "move_rows",
      "description": "Atomically move N rows from their current sheet(s) to a target sheet inside the same workspace. Use for programmatic data migration: dropping a batch of agent-produced drafts onto the right sheet, reorganizing content across LinkedIn / Twitter / Substack tabs, etc. All-or-nothing: if any rowId doesn't belong to this workspace, the entire batch fails before any write fires. Idempotent: rows already on the target sheet are skipped (returns `skipped` count). Rows land at the destination sheet's tail in the order rowIds was supplied. Emits one `row.moved_surface` event per row that actually moved. Up to 500 rows per call.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "rowIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Row IDs to move (1-500). Order is preserved at the destination: first id lands at the lowest position, last id at the highest.",
            "minItems": 1,
            "maxItems": 500
          },
          "target_surface_slug": {
            "type": "string",
            "description": "Slug of the destination table surface. Use list_surfaces to enumerate. 400 if the slug is a doc surface, archived, or not in this workspace."
          }
        },
        "required": [
          "slug",
          "rowIds",
          "target_surface_slug"
        ]
      }
    },
    {
      "name": "get_doc",
      "description": "Read a workspace's doc (TipTap rich-text) body. Returns three forms of the same content: `content` (TipTap JSON, round-trippable into update_doc for structural edits), `markdown` (CommonMark + GFM, ready to feed to an LLM or render in a non-ProseMirror surface), and `text` (plain text, best for search, summarisation, word-count heuristics). A workspace can hold any combination of doc and table surfaces, one or many of either kind; omit `surface_slug` to read the primary doc surface, or pass it to target a specific doc tab (use `list_surfaces` to enumerate). An unwritten or absent doc returns content={}/markdown=\"\"/text=\"\"; a `surface_slug` that doesn't match any live doc surface 404s.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "surface_slug": {
            "type": "string",
            "description": "Optional doc surface slug for multi-doc workspaces. Omit to read the primary doc surface. Use list_surfaces to see available slugs."
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "get_workspace_schema",
      "description": "Return a table surface's column definitions so an agent knows what keys create_row/update_row will accept. Each column has `key` (the field name in row.data), `label` (human-readable), `type` (text | longtext | url | status | owner | date | number), `position`, and, for status/owner columns, the allowed `options`. Empty array on doc-only workspaces; callers should still be able to write rows (columns auto-seed on first write). Multi-surface workspaces accept `surface_slug` to scope to a specific table sheet (use `list_surfaces` to enumerate); omit to fall through to the workspace's primary table surface.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "surface_slug": {
            "type": "string",
            "description": "Optional. The slug of the specific table surface to read columns from. Omit on single-table workspaces; required on multi-table workspaces if you don't want the primary table surface (lowest position)."
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "add_column",
      "description": "Append a single column to a workspace's table schema. Position is auto-computed as next-after-max so the contiguity invariant holds. Key collision (409) if a column with the same key already exists. Editor role required. Use this for per-column additions; use get_workspace_schema + update_workspace_columns (PUT on /columns) for full schema replacement or reordering. Multi-surface workspaces accept `surface_slug` to target a specific table sheet (use `list_surfaces` to enumerate); omit to fall through to the workspace's primary table surface.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "surface_slug": {
            "type": "string",
            "description": "Optional. The slug of the specific table surface to add the column to. Omit on single-table workspaces; required on multi-table workspaces if you don't want the primary table surface (lowest position)."
          },
          "key": {
            "type": "string",
            "description": "Field name in row.data. Lowercase + underscores recommended; 1-64 chars."
          },
          "label": {
            "type": "string",
            "description": "Human-readable header shown in the sheet."
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "longtext",
              "number",
              "status",
              "person",
              "date",
              "url",
              "checkbox",
              "select"
            ],
            "description": "Column type. See get_workspace_schema for examples."
          },
          "width": {
            "type": "number",
            "description": "Optional. Initial column width in px."
          },
          "description": {
            "type": "string",
            "description": "Optional. Human-readable tooltip shown in the column header."
          },
          "options": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "value": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "color": {
                  "type": "string"
                }
              }
            },
            "description": "Required for `status` + `select` types. The allowed values shown in the dropdown."
          }
        },
        "required": [
          "slug",
          "key",
          "label",
          "type"
        ]
      }
    },
    {
      "name": "list_workspace_members",
      "description": "List principals with explicit access to a workspace. Returns users (id, name, email; email visible only when the caller is in the same org) and agents (id, name, brandKey) along with their role (owner | editor | commenter | viewer). Used by agents to verify a workspace is actually shared before writing output the team is expected to see.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "delete_workspace",
      "description": "Archive a workspace. Soft-delete: rows, doc body, and activity history are preserved, and the workspace can be restored from Settings · Archived. Every member loses access immediately. Idempotent: calling on an already-archived workspace returns its current archivedAt without changing anything. Requires editor role on the agent. Pass `mode: \"web\"` to surface a click-to-approve URL for the human (recommended for any non-trivial workspace); the first call returns { status: 'approval_required', approval_url, polling_url }; print approval_url in chat, user clicks + approves, you poll polling_url for the result. Without `mode: \"web\"` the call executes immediately on the agent's editor role.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "mode": {
            "type": "string",
            "enum": [
              "immediate",
              "web"
            ],
            "description": "Consent surface. 'immediate' (default) executes on the agent's role. 'web' returns an approval_url the user clicks in a browser; recommended for any workspace your user might miss."
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "update_workspace",
      "description": "Rename a workspace, change its slug, switch its default-view mode, or flip its visibility (private | org | unlisted | public). Pass any subset of `name`, `new_slug`, `mode`, `visibility`; fields you omit are left unchanged. Slug renames preserve old URLs via WorkspaceSlugAlias so previously-shared links keep resolving. Visibility flips disconnect every live SSE subscriber so reconnects re-authenticate against the new visibility. Editor role required. Emits `workspace.renamed` and/or `workspace.visibility_changed`. Visibility WIDENING (private → org/unlisted/public, org → unlisted/public, unlisted → public) is consent-gated: pass `consent_mode: \"web\"` to return an approval_url the user clicks; otherwise the call returns `consent_required` and you must re-issue with consent_mode set. Visibility narrowing + non-visibility updates execute immediately on the agent's role.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The current workspace slug"
          },
          "name": {
            "type": "string",
            "description": "New display name. Optional."
          },
          "new_slug": {
            "type": "string",
            "description": "New URL slug (lowercase kebab-case, 3-64 chars). Optional. Must be unique within the org. Old slug stays redirectable via the alias table."
          },
          "mode": {
            "type": "string",
            "enum": [
              "table",
              "doc"
            ],
            "description": "New default-view preference for the workspace's first tab. Optional. Doesn't add or remove surfaces; use `create_surface` / `delete_surface` to change the actual tab set."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "org",
              "unlisted",
              "public"
            ],
            "description": "New visibility. Optional. `private` = explicit members only; `org` = every org member gets virtual editor; `unlisted` = anyone with the URL can view; `public` = listed and viewable to all. Widening transitions are consent-gated; see `consent_mode`."
          },
          "consent_mode": {
            "type": "string",
            "enum": [
              "web"
            ],
            "description": "Required when `visibility` widens audience. Pass 'web' to surface a click-to-approve URL the user opens in their browser; first call returns { status: 'approval_required', approval_url, polling_url }, you print approval_url in chat and poll polling_url for the result."
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "share_workspace",
      "description": "Invite a human (by email) to a workspace at a specified role. If the email already belongs to a Dock user they're added immediately and a notification email is sent; if not, a 7-day invite token is minted that auto-accepts on magic-link sign-in. Editor role required on the workspace. Emits `member.joined` (existing user) or `member.invited` (new user). Use update_workspace_member to change a role afterwards, remove_workspace_member to revoke.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "email": {
            "type": "string",
            "description": "Email address of the human to invite."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "editor",
              "commenter",
              "viewer"
            ],
            "description": "Role to grant. Defaults to `editor`. Owner-tier transitions require an owner caller."
          }
        },
        "required": [
          "slug",
          "email"
        ]
      }
    },
    {
      "name": "update_workspace_member",
      "description": "Change an existing workspace member's role. Editor role required to caller. Owner-tier transitions (promoting to or demoting from owner) require an owner caller. Demoting the sole owner is blocked; promote someone else to owner first. No-op when the role is unchanged. Emits `member.role_changed` with from/to roles.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "member_id": {
            "type": "string",
            "description": "The WorkspaceMember id to update. Get this from list_workspace_members."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "editor",
              "commenter",
              "viewer"
            ],
            "description": "New role."
          }
        },
        "required": [
          "slug",
          "member_id",
          "role"
        ]
      }
    },
    {
      "name": "remove_workspace_member",
      "description": "Remove a workspace member. Editor role required; owner-tier removals require an owner caller. Sole-owner removal is blocked; promote someone else first. Note: if the workspace visibility is `org`, removing an explicit member of the same org leaves them with virtual editor access via the org-membership branch. Consent-gated for agents: the FIRST call returns { status: 'confirmation_required', confirm_token, message, expires_in }. Surface the message to your user and, if they say yes, re-call this tool within 60s with `confirm_token` set to the same token. User callers (cookie session) skip the consent step.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "member_id": {
            "type": "string",
            "description": "The WorkspaceMember id to remove. Get this from list_workspace_members."
          },
          "confirm_token": {
            "type": "string",
            "description": "The token returned by the first call as `confirm_token`. Omit on the first call; include on the second call to execute the removal. Single-use, 60s TTL. Agents only; user callers don't need this."
          }
        },
        "required": [
          "slug",
          "member_id"
        ]
      }
    },
    {
      "name": "update_doc",
      "description": "Replace a workspace's doc body. Takes EITHER TipTap JSON (`content`) OR Markdown (`markdown`): pass markdown when you're producing prose from scratch (CommonMark + GFM is the format every LLM emits natively), pass TipTap JSON when you need structural edits to an existing doc (round-trip from get_doc, mutate, write back). Beyond CommonMark + GFM, the markdown layer recognizes:\n\n- **```mermaid** fenced code → diagram (15 sub-types: flowchart, sequence, gantt, ER, state, class, mindmap, timeline, pie, quadrant, sankey, XY-chart, packet, block, journey)\n- **$x$** inline math, **$$x$$** block math (LaTeX, KaTeX-rendered, scripts/href disabled)\n- **> [!NOTE]** / **[!TIP]** / **[!IMPORTANT]** / **[!WARNING]** / **[!CAUTION]** GFM-style callouts\n- **```svg** fenced code → sanitized SVG embed (the universal escape hatch for custom diagrams; scripts and event handlers stripped at write time)\n- **<details><summary>X</summary>BODY</details>** → collapsible toggle\n- **[[slug]]** / **[[org/slug]]** / **[[slug#tab]]** / **[[slug#row-id]]** / **[[slug|display]]** → cross-references to another workspace, surface, or row. Resolved against your accessible workspace set; targets you can't see render as plain text on the reader's side (no info leak). Every cross-ref creates a Backlink row so the target's 'referenced from' sidebar shows this doc.\n- **[@Label](dock:mention/<kind>/<id>)** → @-mention of a user or agent. `<kind>` is `agent` or `human`; `<id>` is the principal id. Optional query params `?org=<slug>` (agents) or `?email=<addr>` (humans) for renderer hints. Mentioning a human writes a `doc_mention` row to their inbox + sends a deep-link email; mentioning an agent fires the `doc.mention_added` webhook so the agent service can wake up and reply. Re-saving a doc that already mentions someone does NOT re-fire — only newly-added mentions notify (computed from a diff against the previous body). Use this from agent code to ping a teammate when a doc you wrote needs their eyes.\n- A **lone URL on its own line** from a safelisted provider (YouTube, Vimeo, Loom, Figma, CodePen, GitHub gists) → sandboxed iframe embed. Other URLs stay as regular links. Surrounding prose disqualifies the auto-embed.\n\nPer-format caps: max 50 Mermaid diagrams (30 KB source each), max 500 math expressions (8 KB source each), max 50 SVG blocks (100 KB source each post-sanitize), max 200 cross-refs per doc, max 500 @-mentions per doc, max 20 embeds per doc. See /docs/doc-formats for examples. Last-write-wins; no CRDT merge. Emits doc.updated + doc.heading_added + doc.mention_added events as applicable. Requires editor role. Multi-surface workspaces optionally accept `surface_slug` to write to a specific doc tab; omitted writes the primary doc surface. Append-only updates have a dedicated `append_doc_section` tool that doesn't require fetching the body first.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "surface_slug": {
            "type": "string",
            "description": "Optional doc surface slug for multi-doc workspaces. Omit to write the primary doc surface. Use list_surfaces to see available slugs."
          },
          "content": {
            "type": "object",
            "description": "TipTap document JSON: `{ type: 'doc', content: [ ... ] }`. Use this when round-tripping from get_doc to preserve formatting. Mutually exclusive with `markdown` (content wins if both are passed).",
            "additionalProperties": true
          },
          "markdown": {
            "type": "string",
            "description": "Markdown body (CommonMark + GFM). Converted server-side to TipTap JSON via the same converter that powers PUT /api/workspaces/:slug/doc. Use this when authoring prose from scratch; no need to hand-build ProseMirror nodes."
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "validate_doc_markdown",
      "description": "Pre-flight check on markdown BEFORE writing it via update_doc / append_doc_section. Returns { ok, errors, warnings, parsed } with parsed counts per format type (mermaidCount, mathCount, svgCount, calloutCount, crossRefCount, mentionCount, embedCount, detailsCount, headingCount, byteSize, nodeCount, depth) plus structured DocGuardError-equivalent errors (cap breaches) and non-blocking warnings (cross-refs that don't resolve, mention ids that don't resolve, oversize sources, cap-approaching counts). NEVER writes anything; pure parse + analysis. Use when iterating on rich-format markdown to catch problems before burning a write. Cross-ref + mention resolution is gated on caller's accessible workspace set, so unresolved tokens surface in warnings.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "markdown": {
            "type": "string",
            "description": "Markdown body to validate. Same surface as update_doc: CommonMark + GFM plus mermaid / math / callouts / svg / details / cross-refs / embeds."
          }
        },
        "required": [
          "markdown"
        ]
      }
    },
    {
      "name": "update_doc_section",
      "description": "Replace a single section of a workspace's doc body, identified by its heading text. The targeted edit complement to `update_doc` (full replacement) and `append_doc_section` (append-only at the end). Use this when the agent maintains a recurring section (e.g., a 'Status' block in a launch-prep doc, an 'Outcomes' block in a meeting note) and only needs to refresh that one piece. Without it, agents are forced into 'GET → splice → PUT' which costs tokens, costs latency, and races against any concurrent human edit elsewhere in the doc (last-write-wins clobbers). Section semantics: the FIRST heading whose plain text matches `heading` exactly (case-sensitive on trimmed text) is found, and everything from that heading up to the next heading at the same OR shallower level is replaced. So a `## Outcomes` section ends at the next `## …` or `# …`; nested `### …` subsections stay part of the replaced range. Returns 404 when no matching heading exists; strict by design so a misremembered heading fails loudly. `markdown` is the FULL replacement, INCLUDING the heading line: pass it back as-is to keep the heading, change it to rename or rewrite the heading, change the heading level, or omit the heading entirely (collapses the section into the prior one). Empty `markdown` deletes the section. Same markdown surface as update_doc / append_doc_section (CommonMark + GFM + Mermaid + KaTeX + callouts + SVG + details + cross-refs + @-mentions + URL embeds). Identity / attribution / events / doc-guard all flow through the same writeDocBody path as the other doc endpoints, so @-mentions in the new section fire `doc.mention_added` for newly-added mentions just like update_doc does. Requires editor role. Multi-surface workspaces optionally accept `surface_slug` to target a specific doc tab.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace')."
          },
          "surface_slug": {
            "type": "string",
            "description": "Optional doc surface slug for multi-doc workspaces. Omit to target the primary doc surface."
          },
          "heading": {
            "type": "string",
            "description": "Plain text of the heading to find (case-sensitive, trimmed). For `## Outcomes`, pass `Outcomes`. Hash marks and surrounding whitespace are stripped from the comparison automatically by the markdown converter. Use `get_doc` first if you need to enumerate the headings actually present."
          },
          "markdown": {
            "type": "string",
            "description": "FULL replacement markdown for the section, including the heading line if you want to keep / rename / restructure it. Empty string deletes the section."
          }
        },
        "required": [
          "slug",
          "heading",
          "markdown"
        ]
      }
    },
    {
      "name": "append_doc_section",
      "description": "Append a chunk of Markdown to the END of a workspace's doc body. Designed for crons + ingest agents that produce content in timestamped chunks (changelog updates, daily standups, batch summaries). Same markdown surface as update_doc: supports CommonMark, GFM, ```mermaid diagrams, $math$/$$math$$ KaTeX, > [!NOTE]/[!TIP]/[!IMPORTANT]/[!WARNING]/[!CAUTION] callouts, ```svg sanitized embeds, <details><summary>X</summary>...</details> toggles, [[slug]] cross-references, [@Label](dock:mention/<kind>/<id>) @-mentions of users + agents, and lone-URL embeds (YouTube/Vimeo/Loom/Figma/CodePen/gists). Server fetches the current body, splices the new blocks on, and writes the result through the same path as update_doc with the same auth, same events, same byte/depth/node-count guard. Append is non-idempotent by design (every call adds content); the caller is responsible for dedupe. @-mentions inside the appended chunk fire `doc.mention_added` + inbox/email fan-out for newly-added mentions only — appending a chunk that re-mentions someone already mentioned earlier in the doc won't re-fire. Requires editor role. Multi-surface workspaces optionally accept `surface_slug` to append to a specific doc tab.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "surface_slug": {
            "type": "string",
            "description": "Optional doc surface slug for multi-doc workspaces. Omit to append to the primary doc surface."
          },
          "markdown": {
            "type": "string",
            "description": "Markdown chunk to append (CommonMark + GFM). Becomes one or more new blocks at the end of the existing doc."
          }
        },
        "required": [
          "slug",
          "markdown"
        ]
      }
    },
    {
      "name": "create_workspace",
      "description": "Create a new workspace in the caller's org. Works for both user and agent callers; agent-created workspaces attribute to the agent and enroll the agent's owning user as a co-owner so the human sees it in their dashboard. The new workspace is seeded with one primary surface: a `doc` when `initial_markdown` is supplied or `mode='doc'`, otherwise a `table`. Add more tabs later with `create_surface`: a workspace can hold any combination of doc and table surfaces, one or many of either kind, so `mode` here just picks the first tab, not the workspace's structure. Agent-created workspaces default to org-visibility so sibling agents in the same org aren't 403'd. For prose content (briefs, summaries, changelogs) pass `initial_markdown` to seed the doc body in one call; mode auto-resolves to 'doc' and the markdown is converted server-side, no need to hand-build ProseMirror JSON.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The workspace name. Required. Used to derive a slug if you don't pass one."
          },
          "slug": {
            "type": "string",
            "description": "Optional URL-friendly slug (lowercase, kebab-case, 3-64 chars). Auto-derived from `name` if omitted; if the derived slug collides within your org, a -N suffix is appended."
          },
          "mode": {
            "type": "string",
            "enum": [
              "table",
              "doc"
            ],
            "description": "Default-view preference for the first tab. Auto-defaults to 'doc' when initial_markdown is provided, 'table' otherwise. Picks the kind of the seeded primary surface; you can add more tabs of either kind via `create_surface` later. A workspace can hold any combination of doc and table surfaces, one or many of either."
          },
          "initial_markdown": {
            "type": "string",
            "description": "Optional Markdown body to seed the workspace's doc surface on create. CommonMark + GFM (tables, task lists, strikethrough). When provided AND mode is omitted, mode defaults to 'doc'. Skips the empty default-column scaffolding too. Use this for any prose-shaped output (briefs, summaries, status updates, changelog entries) instead of create + update_doc with hand-built JSON."
          }
        },
        "required": [
          "name"
        ]
      }
    },
    {
      "name": "get_recent_events",
      "description": "Get recent activity events for a workspace. Who did what, when. Useful for understanding what's happened since you last looked.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "limit": {
            "type": "number",
            "description": "Max events to return (default 20)"
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "search",
      "description": "Search across everything the caller can already touch: workspace names, row cell values, and doc sections/paragraphs. Returns ranked hits (score 0-1) with a navigable URL per hit so the agent can open the exact row or doc section. Access-gated; never returns hits from workspaces the caller can't open. Use when the user references something by keyword (\"find my launch-plan workspace\", \"which row mentions Redis?\"). Faster than listing workspaces and iterating.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "q": {
            "type": "string",
            "description": "Search query. Case-insensitive substring match."
          },
          "kind": {
            "type": "string",
            "enum": [
              "all",
              "workspace",
              "row",
              "doc-section"
            ],
            "description": "Narrow to one surface. 'all' (default) searches workspace names + row cells + doc sections. 'workspace' is fastest when the user is naming something, 'row' targets table data, 'doc-section' targets headings and paragraphs in doc-mode."
          },
          "limit": {
            "type": "number",
            "description": "Max hits to return (default 20, max 100)."
          },
          "offset": {
            "type": "number",
            "description": "Hits to skip for pagination (default 0)."
          }
        },
        "required": [
          "q"
        ]
      }
    },
    {
      "name": "get_billing",
      "description": "Get the caller's org billing summary: current plan (free, pro, or scale), active counts and caps for every gated resource (agents, members, workspaces, rows per workspace, API calls per month, webhooks per month, messages per month bundle), monthly price in cents, card on file if any, next invoice date. Both humans and agents can call this. Use before upgrade_plan to check whether you're actually capped, and after to confirm the new plan landed.",
      "inputSchema": {
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "upgrade_plan",
      "description": "Move the caller's org to Pro ($19/mo flat, 10 agents, 20 members, 200 workspaces, 5k rows per workspace) or Scale ($49/mo flat, 30 agents, 60 members, 1,000 workspaces, 50k rows per workspace). The bill doesn't change as you add agents. If the org has no card on file, returns a Stripe Checkout URL for the human. If a card exists, a live plan switch (Pro ↔ Scale) is consent-gated. Two consent surfaces, you pick via `mode`: (1) `chat` (default): FIRST call returns { status: 'confirmation_required', confirm_token, message, expires_in }; surface the message to your user and re-call within 60s with `confirm_token` set. (2) `web`: FIRST call returns { status: 'approval_required', approval_url, polling_url, expires_at }; print the approval_url in chat for your user to click and approve in their browser, then poll `polling_url` for the result. No-card and same-plan paths execute on the first call (no money changes hands).",
      "inputSchema": {
        "type": "object",
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "pro",
              "scale"
            ],
            "description": "Target plan. Defaults to 'pro'."
          },
          "mode": {
            "type": "string",
            "enum": [
              "chat",
              "web"
            ],
            "description": "Consent surface. 'chat' (default) uses the in-chat confirm_token round-trip. 'web' returns an approval_url the user clicks in a browser. Use 'web' if you're headless or your user prefers a click-to-approve flow."
          },
          "confirm_token": {
            "type": "string",
            "description": "Chat-mode only. The token returned by the first call as `confirm_token`. Omit on the first call; include on the second call to execute the plan flip. Single-use, 60s TTL, bound to {org, caller, operation, params}."
          }
        }
      }
    },
    {
      "name": "downgrade_plan",
      "description": "Schedule a downgrade to Free at the end of the current billing period. The org keeps its current plan (Pro or Scale) and paid limits until the period ends. No-op when already on Free. Consent-gated. Two consent surfaces, you pick via `mode`: (1) `chat` (default): FIRST call returns { status: 'confirmation_required', confirm_token, message, expires_in }; surface to your user and re-call within 60s with `confirm_token` set. (2) `web`: FIRST call returns { status: 'approval_required', approval_url, polling_url }; print approval_url in chat, user clicks + approves, then poll polling_url for the result.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "chat",
              "web"
            ],
            "description": "Consent surface. 'chat' (default) uses the in-chat confirm_token round-trip. 'web' returns an approval_url the user clicks in a browser."
          },
          "confirm_token": {
            "type": "string",
            "description": "Chat-mode only. The token returned by the first call as `confirm_token`. Omit on the first call; include on the second call to execute the scheduled downgrade. Single-use, 60s TTL."
          }
        }
      }
    },
    {
      "name": "request_limit_increase",
      "description": "Ask Dock to raise a plan limit (agents, workspaces, rows, or other). We record the signal on the admin side; there's no reply loop. Use this when you hit a cap you can't resolve with upgrade_plan (e.g. you're already Pro but need a custom limit).",
      "inputSchema": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "agents",
              "workspaces",
              "rows",
              "other"
            ],
            "description": "Which limit to raise"
          },
          "desiredValue": {
            "type": "number",
            "description": "Optional: the specific limit you'd like"
          },
          "reason": {
            "type": "string",
            "description": "Optional: 1-2 sentences on the use case"
          }
        },
        "required": [
          "kind"
        ]
      }
    },
    {
      "name": "list_surfaces",
      "description": "List the surfaces (tabs) inside a workspace. A workspace can hold any combination of `table` (rows + columns) and `doc` (TipTap body) surfaces, one or many of either kind; this tool tells you exactly what it has. Each surface has its own slug used in surface-scoped tool calls. Order matches the on-screen tab strip. Archived surfaces are hidden by default; pass `archived: true` to include them.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "archived": {
            "type": "boolean",
            "description": "Include archived surfaces too. Default false (live tabs only)."
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "create_surface",
      "description": "Create a new surface (tab) inside a workspace. `kind` picks `table` or `doc`. Optional `slug` (lowercase kebab-case, 3-64 chars); when omitted the server slugifies `name` and appends a numeric suffix on collision. Optional `columns` overrides the default Title/Status/Notes triple for `table` kinds; ignored for `doc`. Editor role required. Emits `surface.created` so live listeners on the workspace stream see the new tab without a refetch.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "kind": {
            "type": "string",
            "enum": [
              "table",
              "doc"
            ],
            "description": "Surface kind. `table` for rows + columns, `doc` for TipTap body."
          },
          "name": {
            "type": "string",
            "description": "Display name shown on the tab. 1-64 chars."
          },
          "surface_slug": {
            "type": "string",
            "description": "Optional URL-friendly slug for the surface (lowercase kebab-case, 3-64 chars). Auto-derived from `name` when omitted."
          },
          "columns": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "Optional initial columns for `table` kind. Same shape as get_workspace_schema returns. Defaults to Title/Status/Notes when omitted."
          }
        },
        "required": [
          "slug",
          "kind",
          "name"
        ]
      }
    },
    {
      "name": "update_surface",
      "description": "Rename, reslug, or reorder a surface inside its workspace. Pass any subset of `name`, `surface_slug`, `position`. Position is 0-based and is normalised across siblings so positions stay contiguous. Editor role required. Emits `surface.updated`.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "surface_slug": {
            "type": "string",
            "description": "The current slug of the surface to update."
          },
          "name": {
            "type": "string",
            "description": "New display name. 1-64 chars."
          },
          "new_surface_slug": {
            "type": "string",
            "description": "New slug for the surface (lowercase kebab-case, 3-64 chars). Must be unique within the workspace."
          },
          "position": {
            "type": "number",
            "description": "0-based index in the tab strip. Other surfaces shift to keep positions contiguous."
          }
        },
        "required": [
          "slug",
          "surface_slug"
        ]
      }
    },
    {
      "name": "delete_surface",
      "description": "Archive a surface (soft-delete). Rows + doc body are preserved for restore. Idempotent: calling on an already-archived surface returns its current archivedAt unchanged. Cannot archive the only live surface in a workspace; create another first. Editor role required. Emits `surface.archived`.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The workspace slug. Accepts either the bare slug ('my-workspace') or the org-prefixed form ('my-org/my-workspace') as shown in the dashboard URL; both resolve to the same workspace."
          },
          "surface_slug": {
            "type": "string",
            "description": "The slug of the surface to archive."
          }
        },
        "required": [
          "slug",
          "surface_slug"
        ]
      }
    },
    {
      "name": "list_api_keys",
      "description": "List API keys. Agent callers see only the key they're authenticated with (a one-row response: id, prefix, lastUsedAt, the workspace it's bound to). User callers (cookie session) see every key for every agent they own. Plaintext is never returned; the key body is shown only once at create/rotate time.",
      "inputSchema": {
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "rotate_api_key",
      "description": "Atomically mint a new API key with the same agent / workspace / scopes / name and revoke the old one. Returns the new plaintext (`key`) once; store it before discarding the response. Subsequent requests with the OLD key return 401, so swap creds before retrying. Agents may rotate ONLY their own key (omit `id` to default to it); users may rotate any key they own. Use this for routine credential hygiene or after a suspected leak.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "API key id to rotate. Omit when called by an agent; defaults to the agent's own current key. Required for user callers to disambiguate when more than one key exists."
          }
        }
      }
    },
    {
      "name": "revoke_api_key",
      "description": "Revoke an API key (soft-delete via `revokedAt`). Subsequent requests with the key return 401. Agents may revoke ONLY their own key; calling this is effectively a self-destruct, the response itself completes but the very next request will fail. Users may revoke any key they own. To swap creds without going dark in the gap, use `rotate_api_key` instead.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "API key id to revoke. Omit when called by an agent; defaults to the agent's own current key."
          }
        }
      }
    },
    {
      "name": "request_revoke_agent_key",
      "description": "Ask the human owner to revoke ANOTHER agent's active API key (sibling agent). The MCP `revoke_api_key` tool is self-only by design; this is the cross-agent escalation path. Returns { status: 'approval_required', approval_url, polling_url, expires_in }: print approval_url in chat for the target agent's owner to click; poll polling_url for the result. Approval gate: the approving user must be the target agent's owner (Agent.ownerUserId match). Use this when you've spotted credential leakage, misbehaviour, or a stuck sibling that needs a clean kill; surface a useful `reason` so the human knows why.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "target_agent_id": {
            "type": "string",
            "description": "The id of the sibling agent whose key should be revoked. Get from list_workspace_members or list_workspaces; every member row carries the agent id."
          },
          "reason": {
            "type": "string",
            "description": "1-2 sentences on why you're asking. Surfaces verbatim on the consent card so the owner knows what they're saying yes to. Capped at 500 chars."
          }
        },
        "required": [
          "target_agent_id"
        ]
      }
    },
    {
      "name": "request_rotate_agent_key",
      "description": "Ask the human owner to rotate ANOTHER agent's active API key (mint a new one + revoke the old). Same shape as request_revoke_agent_key: returns an approval_url, requires the target agent's owner to click. The new key plaintext is INTENTIONALLY not returned to the requesting agent; it's surfaced only to the human owner via Settings → Agents, who hands it to the target agent out of band. Use when you've spotted leakage and the target needs a clean credential without going dark mid-task.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "target_agent_id": {
            "type": "string",
            "description": "The id of the sibling agent whose key should be rotated."
          },
          "reason": {
            "type": "string",
            "description": "1-2 sentences on why. Surfaces on the consent card. Capped at 500 chars."
          }
        },
        "required": [
          "target_agent_id"
        ]
      }
    },
    {
      "name": "list_webhooks",
      "description": "List webhook endpoints registered on an org. Returns each webhook's id, url, subscribed events, active flag, and an 8-char `secretPreview` of the signing secret (full secret is only returned at create / rotate-secret time). Any org member (user or agent) can list. Use to audit what's subscribed before adding or removing endpoints.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "org_slug": {
            "type": "string",
            "description": "Org slug. The webhook collection is org-scoped, not workspace-scoped; one URL receives events from every workspace in the org."
          }
        },
        "required": [
          "org_slug"
        ]
      }
    },
    {
      "name": "create_webhook",
      "description": "Register a new webhook endpoint on an org. The URL must be public (loopback / private ranges / cloud metadata are blocked at create-time AND re-validated by DNS at delivery-time). Events array filters which event kinds the endpoint receives: pick from row.* / comment.* / member.* / workspace.* / doc.*; an empty array means \"none\" so always pass at least one. Returns the signing `secret` exactly once (whsec_… prefixed); store it on the receiver to verify HMAC signatures on incoming requests.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "org_slug": {
            "type": "string",
            "description": "Org slug"
          },
          "url": {
            "type": "string",
            "description": "Public HTTPS URL to POST events to. Loopback (127.0.0.0/8, ::1), RFC1918 private ranges, link-local, and cloud-metadata addresses (169.254.169.254, etc.) are rejected. Max 2048 chars."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Event kinds to subscribe to. Pick from: row.created, row.updated, row.deleted, row.sealed, comment.added, comment.deleted, member.invited, member.joined, member.removed, member.role_changed, workspace.created, workspace.renamed, workspace.columns_updated, workspace.visibility_changed, workspace.archived, doc.created, doc.updated, doc.heading_added, doc.mention_added."
          }
        },
        "required": [
          "org_slug",
          "url",
          "events"
        ]
      }
    },
    {
      "name": "update_webhook",
      "description": "Toggle a webhook's `active` flag on or off. Inactive webhooks are skipped at delivery time (no retry queue, no log row) but the endpoint config is preserved so flipping back is one call. Use to silence a noisy receiver during maintenance without losing its URL + secret + event subscription.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "org_slug": {
            "type": "string",
            "description": "Org slug"
          },
          "webhook_id": {
            "type": "string",
            "description": "Webhook id (from list_webhooks)"
          },
          "active": {
            "type": "boolean",
            "description": "true to enable delivery, false to silence."
          }
        },
        "required": [
          "org_slug",
          "webhook_id",
          "active"
        ]
      }
    },
    {
      "name": "rotate_webhook_secret",
      "description": "Mint a fresh signing secret for a webhook. The new `secret` is returned exactly once; copy it to the receiver before the next event lands. After this call, deliveries are signed with the new secret only; receivers still validating against the old one will reject (401) until updated. Use after a suspected leak or as part of routine rotation hygiene.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "org_slug": {
            "type": "string",
            "description": "Org slug"
          },
          "webhook_id": {
            "type": "string",
            "description": "Webhook id (from list_webhooks)"
          }
        },
        "required": [
          "org_slug",
          "webhook_id"
        ]
      }
    },
    {
      "name": "delete_webhook",
      "description": "Permanently delete a webhook endpoint. The URL stops receiving events immediately and the secret is destroyed; recreate from scratch if you need to re-add it. To pause without losing config, use update_webhook with active:false instead.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "org_slug": {
            "type": "string",
            "description": "Org slug"
          },
          "webhook_id": {
            "type": "string",
            "description": "Webhook id (from list_webhooks)"
          }
        },
        "required": [
          "org_slug",
          "webhook_id"
        ]
      }
    },
    {
      "name": "create_support_ticket",
      "description": "File a support ticket. Mirrors to a GitHub issue in Dock's support repo and shows up in the user's dashboard at /settings/support. Use this for bugs (you hit an error), feature requests (Dock is missing something), billing (Stripe/subscription), questions (how do I X), or anything else. Prefer request_limit_increase when the user is simply hitting a plan cap.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "bug",
              "feature",
              "billing",
              "question",
              "other"
            ],
            "description": "Ticket category."
          },
          "title": {
            "type": "string",
            "description": "Short headline (3-200 chars). Be specific: 'Table view loses focus on cell edit' beats 'broken'."
          },
          "body": {
            "type": "string",
            "description": "Detailed description (5-10000 chars). For bugs: include what you did, what happened, what you expected. For feature requests: the use case."
          },
          "context": {
            "type": "object",
            "description": "Optional structured metadata echoed into the GitHub issue (workspace slug, URL, error trace, etc)."
          },
          "attachmentUrls": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional list of screenshot/attachment URLs to embed in the issue. URLs must be hosted on the Dock blob store; mint them via POST /api/support/upload first. Max 4."
          }
        },
        "required": [
          "kind",
          "title",
          "body"
        ]
      }
    }
  ],
  "contact": {
    "support": "https://trydock.ai/settings/support",
    "status": "https://status.trydock.ai"
  },
  "_meta": {
    "io.modelcontextprotocol.registry/publisher-provided": {
      "namespace": "ai.trydock",
      "slug": "dock"
    }
  }
}