Strata Documentation
Strata is an AI-empowered document editing platform. Upload Markdown or HTML documents for viewing, commenting, and editing with a section-based document model designed for granular AI editing via MCP (Model Context Protocol).
Quick Start
Connect your AI client to Strata's MCP server to read, edit, search, and manage documents. Most clients handle OAuth automatically — just provide the server URL.
Connection Details
- MCP Server URL
https://api.prod.us-east-2.strata.space/mcp- Authentication
- OAuth 2.1 with Dynamic Client Registration
- Available Tools
app_get_section_content,browse_connector_resources,edit_document,find,get_agent_status,get_document_graph,get_image,get_presence,get_publish_status,invoke_agent,invoke_connector_action,list_connected_tools,manage_comments,manage_suggestions,publish_document,read_document,unpublish_document
Client Setup
Using Claude Code? The Strata plugin is the fastest path: it registers the MCP server and adds the Spaces skills in one command.
Claude Desktop
Add to your claude_desktop_config.json:
{
"mcpServers": {
"strata": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.prod.us-east-2.strata.space/mcp"
]
}
}
}OAuth authentication is handled automatically — you'll be prompted to sign in on first use.
Claude Code
Add the Strata MCP server via the CLI:
claude mcp add strata https://api.prod.us-east-2.strata.space/mcpOAuth authentication is handled automatically via your browser.
Cursor
Add to ~/.cursor/mcp.json or .cursor/mcp.json:
{
"mcpServers": {
"strata": {
"url": "https://api.prod.us-east-2.strata.space/mcp"
}
}
}Cursor handles OAuth automatically when the server returns 401.
VS Code (Copilot)
Add to .vscode/mcp.json in your project:
{
"servers": {
"strata": {
"type": "http",
"url": "https://api.prod.us-east-2.strata.space/mcp"
}
}
}Requires VS Code 1.101+. Uses "servers" (not "mcpServers") and type "http". OAuth with PKCE and Dynamic Client Registration is handled automatically.
Windsurf
Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"strata": {
"serverUrl": "https://api.prod.us-east-2.strata.space/mcp"
}
}
}Windsurf uses serverUrl instead of url. OAuth is handled automatically.
Cline
Open the MCP Servers panel in Cline and add to the config:
{
"mcpServers": {
"strata": {
"url": "https://api.prod.us-east-2.strata.space/mcp",
"type": "streamableHttp"
}
}
}Uses "streamableHttp" (camelCase). When OAuth is required, Cline shows an Authenticate button.
Continue
Add to ~/.continue/config.yaml:
mcpServers:
- name: strata
command: npx
args:
- "-y"
- "mcp-remote"
- "https://api.prod.us-east-2.strata.space/mcp"Continue does not support OAuth natively yet. Use the mcp-remote bridge instead (see below).
Zed
Add to your Zed settings.json:
{
"context_servers": {
"strata": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.prod.us-east-2.strata.space/mcp"
]
}
}
}Zed does not support OAuth natively. Uses mcp-remote as a stdio bridge that handles the OAuth flow in your browser.
Claude.ai and ChatGPT
These chat products render Strata's editor inline as a custom connector. Add the MCP server URL above in the host's connector settings.
Claude.ai
Settings → Connectors → Add custom connector
Available on paid plans. Organization connectors are added by an Owner.
ChatGPT
Settings → Connectors → Create
Requires Developer Mode (Settings → Apps & Connectors → Advanced). Plus, Pro, or Enterprise.
Universal Fallback (mcp-remote)
For any client without native OAuth support, use mcp-remote as a stdio bridge. It handles the full OAuth flow and works with any MCP client:
{
"mcpServers": {
"strata": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.prod.us-east-2.strata.space/mcp"
]
}
}
}Auth state is persisted in ~/.mcp-auth/ so you only authenticate once per server.
Next Steps
Interactive HTML blocks
Code blocks tagged html render as live, sandboxed previews in the Strata editor and in exported PDFs — charts, diagrams, 3D scenes, and small interactive widgets, written as ordinary fenced code.
Write a fenced code block with the html language tag. Its markup, styles, and scripts run automatically in an isolated preview; every edit re-renders from a clean state, and readers can toggle between the preview and the source.
The preview is fully isolated: no cookies or storage, no access to the surrounding page, and no external URLs. WebSocket, beacons, and form posts are removed outright, and fetch reaches nothing except the vetted /sandbox/ assets listed below, which is how the Doom engine loads its own game data. Anything that violates these constraints fails inside its own preview and nowhere else.
Strata design tokens are preloaded: CSS variables such as var(--color-foreground), var(--color-muted-foreground), var(--color-primary), and var(--color-border) match the app theme, and the preview follows the viewer’s light or dark mode automatically. Colors you set yourself are used exactly as written and are never adjusted, so set background and text together — a block that sets only one of the two can end up unreadable in the mode you did not test.
A preview starts about 360 pixels tall and then follows its content: taller content grows the block, shorter content shrinks it. That starting height is also what relative units resolve against, so height: 100%, 100vh, and window.innerHeight all work — which is what three.js scenes and full-bleed charts normally use.
Script errors, unhandled promise rejections, and console.error/console.warn output from inside the preview are collected and shown beneath it, with line numbers where the browser reports them. A preview has no developer tools of its own, so this panel is where a block explains why it did not render.
Charting document data
A block can read data that already lives in the same document, so a chart stays in step with the numbers its readers can see. Three kinds of source can be named. A fenced json, csv, tsv, or yaml block is named on its own fence, as in name=sales. A table is named by an HTML comment line placed immediately above it: <!-- strata:name=headcount -->. The document frontmatter is always available under the reserved name frontmatter, scalar fields only. Those three kinds are the only data a block can bind to. A name starts with a letter, continues with letters, digits, hyphens, or underscores, runs to at most 64 characters, and must be unique within the document.
An html block declares what it consumes on its own fence, as in uses="sales,headcount". A block receives only the sources it names and never the rest of the document, it cannot reach data in another document, and nothing arrives that its reader cannot already see in this one. Editing the block’s code or its declaration reloads the preview from a clean state, while editing the data behind a name it already declared updates the running preview in place. A block declares at most eight names, and any name past the eighth arrives as unavailable with the reason oversize rather than being dropped in silence.
Inside the preview, the declared data is installed as strata.data before your own scripts run. strata.data.get(name) returns one source and strata.data.names lists the names the block declared, in declaration order. A json, csv, tsv, or yaml source arrives unparsed as { kind: 'text', format, text }, so the block parses it with the parser it prefers. A table arrives as { kind: 'table', columns, rows } with every cell as plain text and no numeric or date conversion. Frontmatter arrives as { kind: 'frontmatter', values }. Every delivered value is deeply frozen. When a bound source changes, Strata pushes a fresh snapshot and fires a strata:data event on window whose detail carries the new payload; strata.data.get already returns that snapshot by the time the event fires. The preview never requests data itself, so a block that ignores the event keeps working with what it was first given.
Which kind to reach for depends on who else has to read the numbers. A json or csv fence is compact, diffs cleanly, and keeps a long series out of the way of the prose, but it arrives as raw text and the block owns the parsing; splitting a csv line on commas is the classic mistake, because it tears apart any quoted field that contains one. A named table is the opposite trade: readers get a real table instead of a fenced blob, and the block receives it already separated, with the first row as columns and every row below it in rows. Frontmatter suits single values such as a title or a target rather than a series.
A declared name that cannot be resolved still arrives, as { kind: 'unavailable', reason }, so a block can tell an empty dataset from a missing one. The reason is unknown when no source in the document carries that name, duplicate when two sources claim the same name, removed when the source was deleted while the preview was open, and oversize when the source is larger than the delivery limits allow. Only the affected source is dropped and the others still arrive, so check for the unavailable kind and draw a message rather than assuming the data is there.
Vetted libraries
/sandbox/libs/mermaid.min.js— <script src="/sandbox/libs/mermaid.min.js"></script> then mermaid.initialize({ startOnLoad: false }); mermaid.run()/sandbox/libs/d3.min.js— <script src="/sandbox/libs/d3.min.js"></script> — global `d3`/sandbox/libs/three.module.min.js— <script type="module">import * as THREE from '/sandbox/libs/three.module.min.js'</script>/sandbox/libs/doom.js— <script src="/sandbox/libs/doom.js"></script> is the whole block: it appends its own canvas and boots. Options go on the script tag: data-doom-warp="1,1", data-doom-skill="3", data-doom-manual (call Doom.start() yourself). To place the canvas, supply one with id="canvas". The reader clicks the preview once to give it keyboard focus; arrows move, Ctrl fires, Esc opens the menu. Music is off, sound effects work. First load pulls about 10 MB, then caches. Chocolate Doom compiled to WebAssembly (GPL-2.0-or-later, github.com/cloudflare/doom-wasm) with Freedoom game data (BSD-3-Clause, github.com/freedoom/freedoom). No commercial or shareware WAD is distributed.
No other external script or style URL loads inside the preview. Each library tracks its pinned version and is served from one stable path, so a document renders the same in the editor and in exported PDFs.
Example
<div id="chart"></div>
<script src="/sandbox/libs/d3.min.js"></script>
<script>
const data = [4, 8, 15, 16, 23, 42];
d3.select('#chart')
.selectAll('div')
.data(data)
.join('div')
.style('height', '18px')
.style('margin', '2px 0')
.style('background', 'var(--color-primary, #2c7cb0)')
.style('width', (d) => d * 6 + 'px');
</script>Example: a chart bound to a json fence
```json name=sales
[
{ "quarter": "Q1", "revenue": 42 },
{ "quarter": "Q2", "revenue": 58 },
{ "quarter": "Q3", "revenue": 71 }
]
```
```html uses="sales"
<div id="chart"></div>
<script src="/sandbox/libs/d3.min.js"></script>
<script>
function render() {
const source = strata.data.get('sales');
if (!source || source.kind !== 'text') return;
d3.select('#chart')
.selectAll('div')
.data(JSON.parse(source.text))
.join('div')
.style('height', '18px')
.style('margin', '2px 0')
.style('background', 'var(--color-primary, #2c7cb0)')
.style('width', (d) => d.revenue * 6 + 'px');
}
render();
window.addEventListener('strata:data', render);
</script>
```Example: a chart bound to a csv fence
```csv name=signups
week,signups
"Jan 1, 2026",120
"Jan 8, 2026",148
"Jan 15, 2026",173
"Jan 22, 2026",162
```
```html uses="signups"
<div id="chart"></div>
<script src="/sandbox/libs/d3.min.js"></script>
<script>
function render() {
const chart = d3.select('#chart');
chart.selectAll('*').remove();
const source = strata.data.get('signups');
if (!source || source.kind !== 'text') {
chart.text('No signups data.');
return;
}
// A csv source arrives as raw text. text.split(',') would tear
// "Jan 1, 2026" in half; d3.csvParse honours the quotes.
const rows = d3.csvParse(source.text, (row) => ({
week: row.week,
signups: Number(row.signups),
}));
const scale = d3
.scaleLinear()
.domain([0, Math.max(1, d3.max(rows, (d) => d.signups) || 0)])
.range([0, 100]);
const line = chart.selectAll('div').data(rows).join('div');
line
.style('display', 'flex')
.style('align-items', 'center')
.style('gap', '8px')
.style('margin', '2px 0');
line
.append('span')
.style('flex', '0 0 7rem')
.style('color', 'var(--color-muted-foreground, #6b7280)')
.text((d) => d.week);
line
.append('span')
.style('height', '18px')
.style('background', 'var(--color-primary, #2c7cb0)')
.style('width', (d) => scale(d.signups) + '%');
line.append('span').text((d) => d.signups);
}
render();
// Edit a number in the csv block and this chart follows it.
window.addEventListener('strata:data', render);
</script>
```Example: a chart bound to a document table
<!-- strata:name=headcount -->
| Team | People |
| --- | --- |
| Growth | 12 |
| Platform | 27 |
| Support | 8 |
```html uses="headcount"
<div id="chart"></div>
<script>
function render() {
const chart = document.getElementById('chart');
chart.textContent = '';
const source = strata.data.get('headcount');
if (!source || source.kind !== 'table') {
chart.textContent = 'No headcount table.';
return;
}
// columns is the table's first row; rows is everything under it, and
// every cell is a string, so the numbers are yours to convert.
const team = source.columns.indexOf('Team');
const people = source.columns.indexOf('People');
const counts = source.rows.map((row) => Number(row[people]) || 0);
const widest = Math.max(1, ...counts);
source.rows.forEach((row, index) => {
const line = document.createElement('div');
line.style.display = 'flex';
line.style.alignItems = 'center';
line.style.gap = '8px';
line.style.margin = '2px 0';
const label = document.createElement('span');
label.style.flex = '0 0 7rem';
label.style.color = 'var(--color-muted-foreground, #6b7280)';
label.textContent = row[team];
const bar = document.createElement('span');
bar.style.height = '18px';
bar.style.width = (counts[index] / widest) * 100 + '%';
bar.style.background = 'var(--color-primary, #2c7cb0)';
const value = document.createElement('span');
value.textContent = row[people];
line.append(label, bar, value);
chart.append(line);
});
}
render();
// Type a new number into the table and this chart follows it.
window.addEventListener('strata:data', render);
</script>
```Agent frontmatter
An agent definition is a Markdown document under your /Agents folder. The YAML frontmatter declares the agent's identity, tool allowlist, and orchestrator visibility; the body below the frontmatter is the system prompt the orchestrator hands the user's request to verbatim. Every field is validated server-side on save.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
autoInvokable | boolean | Optional | false | When true, the chat orchestrator may pick this agent on its own if its description matches the user's request. When false (default), the agent runs only on explicit invocation (@mention, MCP invoke_agent, or a connected source). |
description | string | Required | — | One-sentence summary of when to use this agent. Surfaces in the orchestrator's auto-invoke catalog and the @mention picker — be specific about the agent's job. |
enabled | boolean | Optional | true | Master switch. When false, the agent is hidden from every invocation surface even when its frontmatter is otherwise valid. |
model | string | Optional | — | Optional override for the model the agent runs on. Falls back to the platform default when omitted. Must resolve against the platform model registry. |
name | string | Required | — | Kebab-case identifier ([a-z0-9-]), unique within your agents and not colliding with a reserved platform agent name. Drives the @mention token in chat and the agentName argument to MCP invoke_agent. |
pinnedResources | PinnedResourceSpec[] | Optional | — | External resources pinned to this agent as standing knowledge. A compact manifest is injected into every run and the bodies are fetched live with the owner's connector grant. Omit or pass an empty list for no pinned knowledge. |
tools | string[] | Optional | [] | Allowlist of platform tools the agent may call (e.g. read_document, search_space). The Available Tools panel inside the agent banner lists every valid name. Omit or leave empty to grant no tools. |
Example
---
name: meeting-notes-summarizer
description: Summarizes meeting notes into a TL;DR with action items.
tools:
- read_document
- search_space
model: claude-sonnet-4-6
color: emerald
enabled: true
autoInvokable: false
---
You are a meeting-notes summarizer. Given the document body the
orchestrator hands you verbatim, produce a one-paragraph TL;DR and a
bulleted action-item list…
Prompt frontmatter
A prompt template is a Markdown document under your /Prompts folder. The YAML frontmatter declares the prompt's name, description, arguments, and orchestrator visibility; the body below the frontmatter is the prompt template — {{argument}} placeholders are substituted at render time. Argument values are collected by the chat composer's slash sheet or the MCP prompts/get request.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
arguments | PromptArgument[] | Optional | [] | Ordered list of `PromptArgument` entries the prompt body references via `{{name}}` placeholders. Order is preserved across the wire so the slash-picker renders fields in the author's chosen order. Maximum 16 entries. |
autoInvokable | boolean | Optional | false | When true, the chat orchestrator may pick this prompt on its own when the description matches the user's intent. When false (default), the prompt runs only when the user types its slash token or a client invokes MCP prompts/get. |
description | string | Required | — | One-sentence summary of what the prompt does. Surfaces in the slash menu, MCP prompts/list, and (when autoInvokable: true) the orchestrator's tool catalog. |
name | string | Required | — | Human-readable prompt title. The slugified form becomes the /prompt:<slug> token shown in the chat composer's slash menu. |
PromptArgument
Each entry in the `arguments` array above takes the following shape:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
description | string | Required | — | Short human-readable description shown in the slash-picker argument sheet, the MCP `prompts/list` payload, and the `loadUserPrompt` catalog so the user (or the model) knows what to fill in. 1-200 chars. |
name | string | Required | — | Identifier matched verbatim against the body's `{{name}}` placeholders. Matching is case-sensitive — `{{Focus}}` and `{{focus}}` are distinct placeholders. Restricted to ASCII letters, digits, and underscores (1-48 chars) so the same identifier is valid in YAML and the placeholder grammar. |
required | boolean | Optional | false | When true, the slash-picker argument sheet blocks submit until a value is supplied; the `loadUserPrompt` catalog also flags it so the model knows it must ask the user to clarify. When false (default), an omitted argument substitutes the empty string into its placeholders at render time. |
Example
---
name: Summarize document
description: Summarize the active document for a chosen audience.
autoInvokable: false
arguments:
- name: focus
description: What the assistant should focus on.
required: true
- name: audience
description: Target audience for the summary.
required: false
---
Summarize this document for {{audience}}, focusing on {{focus}}. Keep
the summary under 200 words and finish with a short action-items list.
Template frontmatter
A document template is a Markdown document inside a templates folder. The YAML frontmatter declares the template's name, typed variables, and section contract; the body below the frontmatter is the reusable content — {{key}} placeholders are substituted when a document is created from the template, via the gallery, the MCP createFromTemplate action, or an agent's create_document_from_template tool.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
description | string | Required | — | Sentence describing when to use this template. Surfaces in the gallery and in tool catalogs, so be specific about the document shape it produces. |
name | string | Required | — | Kebab-case identifier, unique among the templates in the same folder. Shown in the gallery alongside the document title. |
sections | TemplateSection[] | Optional | [] | Section contract: what a conforming instance must contain. Maximum 64 entries. Empty when the template declares no section structure. |
variables | TemplateVariable[] | Optional | [] | Typed placeholders substituted at instantiation. Order is preserved across the wire so fill-in forms render fields in the author's chosen order. Maximum 16 entries. Empty when the template takes no variables. |
TemplateVariable
Each entry in the `variables` array above takes the following shape:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
key | string | Required | — | Identifier matched verbatim against the body's `{{key}}` placeholders. Matching is case-sensitive. Restricted to ASCII letters, digits, and underscores (1-64 chars) so the same identifier is valid in YAML and the placeholder grammar. |
kind | unknown | Optional | "text" | Input kind. Defaults to free text. |
label | string | Required | — | Human-readable label shown on the fill-in form. 1-80 chars. |
required | boolean | Optional | false | When true, instantiation fails unless a value is supplied. When false (default), an omitted variable substitutes the empty string into its placeholders. |
TemplateSection
Each entry in the `sections` array above takes the following shape:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
fill | unknown | Optional | "required" | Fill discipline for the section. Defaults to required. |
guidance | string | Optional | "" | Instructions for whoever (or whatever) fills the section in. Optional; shown alongside the section in fill-in surfaces. |
title | string | Required | — | Section title. Matches a heading in the template body verbatim. |
Example
---
name: incident-postmortem
description: Standard postmortem with a verbatim escalation matrix.
variables:
- key: incident_id
label: Incident ID
kind: text
required: true
- key: occurred_on
label: Date of incident
kind: date
required: true
sections:
- title: Timeline
guidance: Chronological events from first alert to resolution.
fill: required
- title: Lessons learned
guidance: What we change going forward.
fill: optional
- title: Escalation matrix
fill: verbatim
---
## Timeline
Incident {{incident_id}} on {{occurred_on}}.
## Lessons learned
## Escalation matrix
Page the on-call lead, then the service owner…