# Permissions and Tools

Each MCP tool checks three things before touching data:

1. The bearer token is a valid, unexpired agent key.
2. The key includes the tool's required permission.
3. The key owner's current org role still allows the matching action.

Workspace scope is enforced after permission checks. Cross-organization and out-of-scope workspaces return ordinary tool errors such as no such workspace, never cross-tenant data.

## Workspace arguments

Most tools are workspace-scoped.

- Workspace-bound key: omit workspace, or pass the same workspace id.
- Organization-wide key: pass workspace for every workspace-scoped tool.
- Restricted member owner: the key can only reach the owner's assigned workspaces.

## Field reference conventions

Fields are sent inside the JSON-RPC params.arguments object of a tools/call. The per-tool tables below use these conventions.

- Type is the JSON type the field must be: string, number, boolean, or string[] (an array of strings).
- Required yes means the call fails without the field. Required no means the field is optional; the Notes column gives its default or the effect of omitting it. On update tools, any field you omit keeps its current value.
- The shared workspace field applies to every workspace-scoped tool (required for organization-wide keys, omitted for workspace-bound keys, per Workspace arguments above) and is not repeated in the field tables. Tools that ignore or do not accept workspace are called out.
- Fields the handler does not recognize are ignored.

## Permission groups

| Group | Permission | Enables |
| --- | --- | --- |
| Content | search | search_docs, semantic_search |
| Content | article.read | list_articles, get_article, article/sidebar/changelog resources |
| Content | article.create | create_article |
| Content | article.update | update_article content and metadata changes |
| Content | article.publish | status changes through update_article |
| Content | article.propose | propose_article_edit, list_proposals |
| Landing | landing.read | get_landing, autofill_landing, landing.json resource |
| Landing | landing.propose | propose_landing_edit, list_proposals |
| Landing | landing.update | set_landing |
| Navigation | section.create | create_section |
| Navigation | article.place | place_article |
| Changelog | changelog.read | list_changelog |
| Changelog | changelog.create | create_changelog |
| Changelog | changelog.update | update_changelog fields other than publishing |
| Changelog | changelog.publish | publish or unpublish changelog entries |
| Changelog | changelog.delete | delete_changelog |
| Workspaces | workspace.read | list_workspaces |
| Workspaces | workspace.create | create_workspace |
| Workspaces | workspace.update | update_workspace |
| Workspaces | workspace.delete | delete_workspace |
| OpenAPI | openapi.read | get_openapi_status, list_openapi_pages |
| OpenAPI | openapi.attach | attach_openapi_spec |
| OpenAPI | openapi.sync | sync_openapi |
| OpenAPI | openapi.detach | detach_openapi_spec, detach_openapi_page |
| OpenAPI | openapi.layout | create_openapi_section, place_openapi_page |

## Content tools

| Tool | Permission | Result |
| --- | --- | --- |
| search_docs | search | Matching article titles and paths (JSON) |
| semantic_search | search | { semantic, results } ranked by meaning (JSON) |
| list_articles | article.read | Article metadata rows for the sidebar (JSON) |
| get_article | article.read | Article Markdown text |
| create_article | article.create | { slug } of the created draft |
| update_article | article.update | { slug } of the updated article |
| propose_article_edit | article.propose | { id, status } of the pending proposal |
| list_proposals | article.propose | Edit proposals and their review status (JSON) |
| create_section | section.create | { key, label } of the new section |
| place_article | article.place | { slug, section } |

Fields, in addition to the shared workspace field. list_articles takes no other fields.

| Tool | Field | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| search_docs | query | string | yes | Full-text search text. |
| semantic_search | query | string | yes | Natural-language search text (ranked by embedding similarity). |
| semantic_search | limit | number | no | Max results. Defaults to 10, capped at 50. |
| get_article | slug | string | yes | Slug of the article to fetch. |
| create_article | title | string | yes | Article title. |
| create_article | section | string | no | Section name. Defaults to Guides. |
| create_article | markdown | string | no | Body in Markdown. Defaults to empty. |
| update_article | slug | string | yes | Slug of the article to update. |
| update_article | title | string | no | New title. |
| update_article | section | string | no | New section. |
| update_article | markdown | string | no | New Markdown body. |
| update_article | status | string | no | One of DRAFT, IN REVIEW, BETA, STABLE, DEPRECATED. Requires article.publish. |
| propose_article_edit | slug | string | no | Article to edit. Omit to propose a NEW article (then title is required). |
| propose_article_edit | title | string | no | Proposed title. Defaults to the existing article's title when editing. |
| propose_article_edit | markdown | string | yes | Proposed Markdown body. |
| propose_article_edit | note | string | no | Note to the reviewer. |
| list_proposals | status | string | no | pending (default), approved, rejected, or all. |
| create_section | label | string | yes | Section display name. |
| place_article | slug | string | yes | Slug of the article to move. |
| place_article | section | string | yes | Target section key, or none to unlist. Appended to the end of the section. |

Notes:

- create_article always creates a draft.
- update_article only changes the fields you pass.
- Changing status requires article.publish in addition to article.update.
- OpenAPI-generated pages cannot be edited with update_article until they are detached.
- Encrypted article content is not exposed over MCP. The tool returns an explanatory message.
- semantic_search ranks by embedding similarity when the deployment has an embeddings provider configured, and transparently falls back to full-text search otherwise. Check the semantic flag in the result to know which ran.
- propose_article_edit needs only article.propose (a read-tier capability), so a read-only key can suggest changes safely. Proposals are NOT applied until an editor approves them; approval never publishes (new articles land as drafts, edited articles keep their status). Use this instead of update_article when your key cannot (or should not) write directly.
- list_proposals is shared across proposal types: a key with either article.propose or landing.propose can call it, and the result includes both article and landing proposals for the workspace (landing rows carry objectType: "landing").

## Landing tools

| Tool | Permission | Result |
| --- | --- | --- |
| get_landing | landing.read | The current landing document (JSON) |
| autofill_landing | landing.read | A generated landing draft built from workspace data (JSON, NOT saved) |
| set_landing | landing.update | The saved landing document (JSON) |
| propose_landing_edit | landing.propose | { id, status } of the pending proposal |

Fields, in addition to the shared workspace field.

| Tool | Field | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| set_landing | landing | string | yes | The landing document as JSON text: layout, hero, popularSearches, categories, popular, footerColumns. |
| propose_landing_edit | landing | string | yes | Proposed landing document as JSON text, same shape as set_landing. |
| propose_landing_edit | note | string | no | Note to the reviewer. |

Notes:

- The landing document is passed as a JSON string in the landing field (the same convention attach_openapi_spec uses for spec), not as nested arguments. get_landing's output is valid input to set_landing and propose_landing_edit as-is.
- autofill_landing generates a draft from the workspace's nav groups, article views, and search history. It does not persist; review the result, then pass it to set_landing or propose_landing_edit.
- set_landing replaces the whole document and validates it (layout, image origins, category/popular links, footer URLs) before saving. A successful call records a landing.updated audit entry and fans out to the workspace's subscribed webhooks.
- propose_landing_edit needs only landing.propose (a read-tier capability), so a read-only key can suggest a landing change safely. The proposal is validated up front and again at approval time, and is NOT applied until an editor approves it in the same proposals queue used for article edits.

## Changelog tools

| Tool | Permission | Result |
| --- | --- | --- |
| list_changelog | changelog.read | Up to 50 changelog entries (JSON) |
| create_changelog | changelog.create | The created entry (JSON) |
| update_changelog | changelog.update | The updated entry (JSON) |
| delete_changelog | changelog.delete | { deleted: id } |

Fields, in addition to the shared workspace field.

| Tool | Field | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| list_changelog | drafts | boolean | no | Include drafts. Defaults to false; honored only when the key has changelog.update. |
| create_changelog | title | string | yes | Entry title. Max 200 characters. |
| create_changelog | markdown | string | no | Entry body in Markdown. Max 20000 characters. |
| create_changelog | tags | string[] | no | Any of new, improved, fixed, deprecated, security. Unknown tags are rejected; duplicates are dropped. |
| create_changelog | status | string | no | draft or published. Defaults to draft; published requires changelog.publish. |
| create_changelog | published_at | string | no | RFC3339 timestamp. |
| update_changelog | id | number | yes | Id of the entry to update. Positive integer. |
| update_changelog | title | string | no | New title. Max 200 characters; must not be empty if sent. |
| update_changelog | markdown | string | no | New Markdown body. Max 20000 characters. |
| update_changelog | tags | string[] | no | Replaces the entry's tags. Same values as create_changelog. |
| update_changelog | status | string | no | draft or published. Changing it requires changelog.publish. |
| update_changelog | published_at | string | no | RFC3339 timestamp. |
| delete_changelog | id | number | yes | Id of the entry to delete. Positive integer. |

Notes:

- list_changelog returns at most 50 entries.
- drafts=true on list_changelog is honored only when the key can update changelog entries.
- Publishing at create or update time requires changelog.publish.
- tags must be drawn from new, improved, fixed, deprecated, security.
- published_at must be an RFC3339 timestamp, for example 2026-01-31T09:00:00Z.

## Workspace tools

| Tool | Permission | Result |
| --- | --- | --- |
| list_workspaces | workspace.read | Workspaces visible to the key (JSON) |
| create_workspace | workspace.create | The created workspace (JSON) |
| update_workspace | workspace.update | The updated workspace (JSON) |
| delete_workspace | workspace.delete | { deleted: id } |

Fields. list_workspaces takes no arguments and ignores workspace. delete_workspace takes only the shared workspace field. create_workspace does not accept workspace; it creates a new workspace in the key's organization.

| Tool | Field | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| create_workspace | name | string | yes | Workspace name. |
| create_workspace | color | string | no | Accent color as #rrggbb. Defaults to #6b7280. |
| create_workspace | access_mode | string | no | Anonymous access: public (default), internal (sign-in gate), or private (hidden). |
| create_workspace | llms_txt | boolean | no | Include public pages in llms.txt. Defaults to true. |
| update_workspace | name | string | no | New name. Must not be empty if sent. |
| update_workspace | color | string | no | Accent color as #rrggbb. |
| update_workspace | access_mode | string | no | Anonymous access: public, internal (sign-in gate), or private (hidden). |
| update_workspace | llms_txt | boolean | no | Include public pages in llms.txt. |

Notes:

- color must be a #rrggbb hex color.
- create_workspace defaults color to #6b7280, access_mode to public, and llms_txt to true.
- Single-workspace keys cannot create or delete workspaces.
- The organization must keep at least one workspace.

## OpenAPI tools

| Tool | Permission | Result |
| --- | --- | --- |
| get_openapi_status | openapi.read | Attached spec status (JSON) |
| attach_openapi_spec | openapi.attach | Sync status after generation (JSON) |
| sync_openapi | openapi.sync | Sync status after regeneration (JSON) |
| detach_openapi_spec | openapi.detach | { detached: true } |
| detach_openapi_page | openapi.detach | The detached article (JSON) |
| list_openapi_pages | openapi.read | { sections, pages } (JSON) |
| create_openapi_section | openapi.layout | { key, label } of the new section |
| place_openapi_page | openapi.layout | The moved article (JSON) |

Fields, in addition to the shared workspace field. get_openapi_status, sync_openapi, detach_openapi_spec, and list_openapi_pages take no other fields.

| Tool | Field | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| attach_openapi_spec | spec | string | one of spec or url | Raw OpenAPI 3.x JSON or YAML. Max 2 MiB. |
| attach_openapi_spec | url | string | one of spec or url | HTTPS URL to an OpenAPI JSON/YAML document. |
| detach_openapi_page | slug | string | yes | Slug of the generated page to detach. |
| create_openapi_section | label | string | yes | Section display name. |
| place_openapi_page | slug | string | yes | Slug of the generated page to move. |
| place_openapi_page | section | string | yes | Target section key, or none to unlist. |
| place_openapi_page | position | number | no | Zero-based position within the section. Non-negative integer. Omit to append. |

Notes:

- attach_openapi_spec requires exactly one of spec or url, never both.
- Raw specs are limited to 2 MiB.
- URL-backed specs are refetched by sync_openapi.
- detach_openapi_spec leaves generated pages as editable articles.
- detach_openapi_page detaches one generated page so it can be edited manually.
- place_openapi_page accepts section=none to unlist a generated page.

## Resources

Structural context is browsable as MCP resources, so a client can load a whole workspace's shape in one read instead of many tool calls. Each enforces the same permission and workspace scope as the equivalent tool.

| URI | Permission | Contents |
| --- | --- | --- |
| contextowl://workspaces.json | workspace.read | The organization's workspaces (static index; appears in resources/list) |
| contextowl://{workspace}/sidebar.json | article.read | Nav groups and article placement (JSON) |
| contextowl://{workspace}/changelog.json | changelog.read | Published changelog entries (JSON) |
| contextowl://{workspace}/landing.json | landing.read | Workspace landing page (JSON) |
| contextowl://{workspace}/{slug}.md | article.read | One article's Markdown |

Use resources when a client wants to attach a page or the workspace structure as context instead of calling a tool. Resource subscriptions and list-changed notifications are not offered: the deployed transport is stateless, so clients re-read on demand.

## Prompts

The server publishes guided, multi-step workflow templates (discoverable with prompts/list, expanded with prompts/get). Each is inert text that names the concrete tools to call, so a client can chain a task without guessing the API. Prompts perform no action and carry no permission.

| Prompt | Arguments | Purpose |
| --- | --- | --- |
| onboard_openapi | workspace, spec_url | Attach an OpenAPI spec and organize the generated pages. |
| draft_changelog | workspace, summary | Draft a changelog entry from a summary, left as a draft for review. |
| audit_stale_docs | workspace | Review docs for staleness and propose fixes (via propose_article_edit) without publishing. |
