VegaStack Pages docs
Home Open app

Agents

MCP and CLI

Create, edit, review, publish, and template pages from an MCP client or the vpg CLI.

VegaStack Pages exposes review workflows through a Remote MCP server and a Rust CLI. Both surfaces are Notion-style — verb-noun MCP tools with one mega-fetch for reads, noun-first CLI hierarchy (vpg <noun> <verb>).

The CLI hits the same standalone HTTP API the web app uses; it does not require an MCP client. Sessions issued by either flow appear under Settings → My Connections and can be revoked there.

MCP endpoint

The Remote MCP server is mounted at /mcp on the same app and implements the MCP 2025-11-25 Streamable HTTP transport.

https://pages.vegastack.com/mcp                # managed
https://pages.example.com/mcp                  # self-hosted
http://127.0.0.1:4322/mcp                      # local dev

Authentication

The server accepts a bearer token via Authorization: Bearer <token>. Three flows feed the same Sessions table:

  1. Browser OAuth (MCP clients — Claude.ai, ChatGPT, Cursor remote, …). The client discovers the authorization server via /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server, dynamically registers itself via POST /oauth/register (RFC 7591), runs an OAuth 2.1 authorization-code flow with PKCE S256 (RFC 7636), and exchanges the code at /oauth/token. 1-hour access tokens, 60-day rotating refresh tokens. Pasting the endpoint URL is all the user has to do.
  2. vpg login device-code flow. vpg login with no --token starts an RFC 8628 device-code flow against /oauth/device and /oauth/token, opens the verification URL in the browser, and stores the access token in the OS keychain. --no-browser skips the auto-launch (URL still prints — works over SSH).
  3. Manual bearer. Open Settings → My Connections, click Create token, copy the value. Use it as Authorization: Bearer <token> or pass vpg login --token <tok> / VPG_TOKEN. Workspace-scoped server-side.

The /mcp endpoint returns WWW-Authenticate: Bearer realm="VegaStack Pages MCP", resource_metadata="…/.well-known/oauth-protected-resource", error="invalid_token" on 401 so spec-compliant browser clients can self-onboard.

Bearer-authenticated requests are exempt from CSRF (no cookie ambient authority), so the CLI can write against /api/* routes the web app shares.

initialize.instructions

After initialize, the server sends a curated instruction block (≤8 KB) covering the safe-edit workflow, agent-attribution rules, and review-loop pattern. Clients that honor initialize.instructions inject it into the model’s system prompt; clients that don’t can read the same content from vpg://skills/vegastack-pages/SKILL.md.

Workspace scoping

Every workspace-scoped tool call must include workspace_id. The token is workspace-scoped server-side; the explicit id is a guard against cross-workspace mistakes. Call fetch once with include=["workspaces"] and no resource_id (or resource_id: "me") to discover ids.

MCP tool surface

19 tools total. One mega-fetch for reads + verb-noun writes. The reads collapse what used to be ten separate get_*/list_* tools.

CategoryTools
Readsfetch, search, wait_for_review, whoami
Page writecreate_page, update_page, restore_page_version, move_page
Commentscreate_comment, update_thread, delete_thread
Publishapply_publication, delete_publication
Templatescreate_template, update_template, render_template
Attachmentupload_attachment
Workspaceinvite_workspace_member, validate_page_source

fetch — one read tool for every resource

fetch routes by the resource_id prefix: pg_ → page, fld_ → folder, tpl_ → template, thr_ → comment thread, pub_ → publication, wks_ → workspace, "me" → authenticated identity. An unknown prefix is rejected — there is no silent slug fallback.

Omit resource_id and pass include=["workspaces"] to list workspaces the session can access.

include[] enum:

KeyUse onReturns
metadatapage, folder, template, threadCore row (default if include is empty for pages)
sourcepageLive source string (default for pages alongside metadata)
renderedpagePre-rendered HTML
versionspageVersion history
commentspageComment threads (filter with status)
publicationpage, folderActive publication, if any
edit_tokenspagebase_version_id + base_content_hash for update_page
membersworkspaceMember list
propertiestemplateFrontmatter property spec
historypageAudit timeline
review_eventspageReview-event stream tail
workspacesme or omitted resource_idAll workspaces visible to the session
templatesworkspaceTemplate list
treeworkspaceFolder/page tree (use depth to limit)

JSON-RPC shape:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "fetch",
    "arguments": {
      "workspace_id": "wks_123",
      "resource_id": "pg_abc",
      "include": ["source", "edit_tokens", "comments"],
      "status": "open"
    }
  }
}

update_page — 3 mutually-exclusive modes

All three require base_version_id for optimistic concurrency. Re-fetch via fetch with include=["edit_tokens"] whenever the version mismatches.

ModeRequired fieldsOptional
Full sourcesourcecheckpoint, checkpoint_label, allow_noop, base_content_hash
Find / replacefind, replacereplace_all, expected_replacements, checkpoint, checkpoint_label, base_content_hash
Checkpoint onlycheckpoint: true (and no source, find, or replace)checkpoint_label

Fields not relevant to the chosen mode are omitted on the wire (not sent as null) so strict server validators accept the call.

update_thread — one tool, several ops

Pass any combination; the server applies anchor → reply → resolve in that order:

OpField
Replybody
Resolve / reopenstatus: "resolved" | "open" or resolve: true
Move anchoranchor: {…} (same shape as create_comment)
Completion replycomplete: true with body (writes a closing reply, then optionally resolves)
Agent attributionagent_name, agent_model, agent_session_id

apply_publication — page + folder

resource_type: "page" | "folder" selects which subtree gets a publication. Pass publication_id to update an existing one; omit to create. The server rejects calls where publication_id is set but resource_id mismatches the target — pure cutover, no silent drift.

Read example — combined fetch

{
  "name": "fetch",
  "arguments": {
    "workspace_id": "wks_123",
    "resource_id": "pg_abc",
    "include": ["source", "edit_tokens", "comments", "publication"],
    "status": "all"
  }
}

One call returns enough to read, edit, and re-render the page.

Common page loop

const me = await mcp.call("fetch", {
  workspace_id: "wks_demo",
  resource_id: "me",
  include: ["workspaces"],
});
const workspaceId = me.workspaces[0].id;

await mcp.call("create_page", {
  workspace_id: workspaceId,
  title: "Search redesign",
  template_id: "prd",
  properties: { owner: "platform" },
});

await mcp.call("wait_for_review", {
  workspace_id: workspaceId,
  page_id: "pg_123",
  until: "first_response",
  timeout_ms: 600000,
});

const { source, base_version_id, base_content_hash } = await mcp.call("fetch", {
  workspace_id: workspaceId,
  resource_id: "pg_123",
  include: ["edit_tokens"],
});

await mcp.call("update_page", {
  workspace_id: workspaceId,
  page_id: "pg_123",
  base_version_id,
  base_content_hash,
  find: "old sentence",
  replace: "new sentence",
  expected_replacements: 1,
});

await mcp.call("update_thread", {
  workspace_id: workspaceId,
  thread_id: "thr_42",
  body: "Done — see version " + base_version_id,
  resolve: true,
  agent_name: "Claude",
  agent_model: "opus-4.7",
});

CLI

The CLI is a Rust binary distributed through the @vegastack/pages npm package. Two aliases run the same binary:

npm install -g @vegastack/pages
vpg --help            # short alias
vegastack-pages --help

Shape — noun-first, like gh and wrangler

Top-level commands cover the hot path and cross-cutting verbs:

vpg login [--token <t>] [--no-browser]
vpg logout
vpg whoami
vpg use <workspace>
vpg search <query> [--type pages|folders|comments|all] [--limit N]
vpg events [--page X] [--workspace W] [--after-id A] [--limit N]
vpg validate [--page <id> | --file <p> | --stdin] [--type markdown|mdx|html]
vpg deploy [--target cloudflare] [--config vegastack-pages.yaml] [--dry-run] [--managed] [--apply-migrations | --skip-migrations]
vpg doctor
vpg update [--check] [--channel latest|next]
vpg completions <bash|zsh|fish|powershell>

Noun groups for resource CRUD:

vpg pages       create | get | update | move | restore | versions | wait
vpg comments    list | create | reply | resolve | reopen | delete | complete | move-anchor
vpg publish     page | folder | update | revoke
vpg templates   list | get | create | update | render
vpg workspaces  list | tree | export | members | invite
vpg attachments upload
vpg skills      install | update | print | path | doctor

vpg pages get, vpg pages update, vpg pages move, vpg pages restore, vpg pages versions, vpg pages wait, vpg comments *, vpg publish page, vpg publish folder, and vpg attachments upload all accept either a pg_… id or a slug — the CLI resolves the slug server-side via the page-ref endpoint before issuing the actual API call.

Global flags

Every command honours these:

FlagBehavior
--agentNon-interactive mode: JSON envelope to stdout, structured error JSON to stderr, exit codes 1–8, no prompts, no spinners. Streaming commands (events, pages wait) emit NDJSON.
--jsonJSON output, otherwise interactive (confirms etc.).
--yes / -ySkip confirmation prompts. Required under --agent for destructive ops.
--workspace W / VPG_WORKSPACEOverride the active workspace.
--token T / VPG_TOKENOverride the stored token.
--base-url URL / VPG_BASE_URLOverride the API base URL. Stored value is preserved when the flag is omitted.
--quiet / -qSuppress non-error output.
--verbose / -vVerbose diagnostic logging to stderr.

--agent output contract

Success (exit 0, stdout):

{ "data": { "...": "..." }, "meta": { "request_id": "...", "duration_ms": 42 } }

Error (non-zero exit, stderr):

{
  "error": {
    "code": "VPG_NOT_FOUND",
    "message": "Page pg_x not found.",
    "hint": "Run `vpg pages list` to see available pages.",
    "details": { "...": "..." }
  }
}

Streaming (vpg events, vpg pages wait) — NDJSON, one object per line, stdout:

{"type":"event","event":{ "...": "..." }}
{"type":"event","event":{ "...": "..." }}
{"type":"done","matched":{ "...": "..." }}

Exit codes:

CodeMeaning
0Success
1Generic error
2Validation
3Authentication
4Not found
5Permission denied
6Conflict (version)
7Network
8Rate limited

Examples — interactive

vpg login                                                       # browser device-code flow
vpg use wks_123
vpg pages create --title "Plan" --file ./plan.md
vpg pages create --template prd --title "Search redesign" --set owner=platform
vpg pages get plan-abc123 --include source,edit_tokens
vpg pages update pg_123 --base-version-id ver_42 --find "old" --replace "new" --expected-replacements 1
vpg pages restore pg_123 ver_42                                 # confirms y/N
vpg pages wait pg_123 --until first-response --after-id evt_7
vpg comments list pg_123 --status open
vpg comments reply thr_99 --body "Done." --agent-name Claude --agent-model opus-4.7
vpg comments complete thr_99 --body "Fixed." --resolve --agent-name Claude
vpg publish page pg_123 --permission comment --expires-at 2026-12-31T00:00:00Z
vpg publish revoke pub_42                                       # confirms y/N
vpg templates render prd --title "Search redesign" --set owner=platform
vpg workspaces tree
vpg workspaces invite --email teammate@example.com --role editor
vpg attachments upload pg_123 --filename diagram.png --content-type image/png --base64-file ./diagram.b64
vpg search "runbook" --type pages
vpg events --page pg_123

Examples — --agent mode

vpg --agent whoami
vpg --agent pages get pg_123 --include source,edit_tokens
vpg --agent pages update pg_123 --base-version-id ver_42 --source "# New body"
vpg --agent pages wait pg_123 --until first-response --timeout 600 --poll 2
vpg --agent --yes pages restore pg_123 ver_old                  # --yes required for destructive ops
vpg --agent publish page pg_123 --permission comment
vpg --agent comments reply thr_99 --body "Done." --agent-name Claude

--agent writes one compact JSON line on success, structured error JSON to stderr on failure. Streaming commands emit one JSON line per event.

Authentication details

  • Browser device-code (default). vpg login calls /oauth/device, prints the verification URL, opens it, and polls /oauth/token until the user approves. Uses the baked-in well-known client oac_vpg_cli. Sets kind=oauth on the session.
  • Manual bearer. vpg login --token <tok> (or VPG_TOKEN) stores a workspace-scoped token from Settings → My Connections. Sets kind=cli.
  • Storage. Tokens go to the OS keychain when available, otherwise to an owner-only file under ~/.config/vegastack-pages/. vpg logout clears both.

Build from source

pnpm --filter @vegastack/pages build
node cli/vegastack-pages/bin/vpg.js --help
# or, from cli/vegastack-pages:
node scripts/build-native.mjs
cargo run --quiet -- --help

Agent skills

vpg skills install writes the portable SKILL.md bundle to the host agent’s skill directory. Adapters cover Claude, Cursor, Codex, and Gemini layouts:

vpg skills doctor
vpg skills install --agent all --scope user
vpg skills install --agent cursor --scope project
vpg skills update --agent all --scope user
vpg skills path

The bundle is embedded into the Rust binary at build time, so installed npm binaries can run skills install without a source checkout. MCP clients can read the same content as resources under vpg://skills/vegastack-pages/….

Deploy helper

vpg deploy shells out to the repository’s pnpm deploy:cloudflare. Run it from a source checkout. See Self-host on Cloudflare for the full install flow.

Active sessions

Open Settings → My Connections to see your own active tokens. Workspace admins can also open Settings → Connections Log for the workspace-wide list.

Each row shows the recognized vendor (Claude, ChatGPT, Cursor, Windsurf, Continue, Cline, Codex, vpg CLI, …), kind chip (oauth | manual | cli), last-seen timestamp, expiry, and a one-click revoke. Revoked tokens stop working on the next request.

Last updated