Every Application — inline or folder-based — has window.charmiq available before your first line of script runs. The platform injects it automatically. You don't import it. You don't initialize it. It's just there.This page covers the full surface of the bridge. appContent and appState always act on your own Application instance. Other Applications cooperate through Application Discovery; authorized Platform APIs, including Commands and MCP, can reach any document content with the User's consent, other Applications' data included.window.charmiq.appContent
Collaborative texts stored in the document, each an app-content. OT-managed: concurrent edits merge, and an app-content's write never lands on text it wasn't measured on. Read and write them through content$() and contents$(); the primitives below them stay for what those do not cover.| Method | Returns | Description |
|---|
content$(selector?) | Observable<AppContent | null> | The app-content (no selector: the Application's first), kept current; null while there is none. Once it is removed, the next app-content the selector names is followed. A failed read errors the stream with its code. |
contents$(selector?) | Observable<AppContent[]> | Every app-content the selector names ([name='x'] or [id='x']; none: every app-content), in the order each first appeared. |
create(text, name?) | Promise<AppContent> | The app-content named name when one exists (its text kept, empty or not), otherwise a new one holding text. |
onChange$(selector?) | Observable<AppContentChange> | Watch for changes. Fires immediately with current state, then on every change from any source. No selector: every app-content. Its deleted notice is a reason to check, not proof. |
get(selector?) | Promise<string> | Get the current text of an app-content. |
set(content, selector?, name?) | Promise<void> | Replace full content. Creates a new app-content if selector doesn't match. |
snapshot(selector?) | Promise<AppContentSnapshot | null> | The app-content as it is now, with its revision; null when none matches. Absent on a page opened before guarded edits. |
edits$(selector?) | Observable<AppContentEdit> | The app-content's actual edits, for a live text editor (the CodeMirror Editor is the worked example): where it stands, then each change with the revision it applies to, naming your own tagged writes. |
applyChanges(changes, selector?, guard?, write?) | Promise<void> | Apply incremental text edits through OT. With a guard ({ id, revision }), applies only while that app-content still has that revision, else rejects with app/content-conflict, writing nothing. A write tag ({ editId }, guarded writes only) comes back as the ack of the edit it makes. |
remove(selector?) | Promise<void> | Delete an app-content. |
type AppContent = {
id: string;
revision: string; // The app-content's exact state: every write is guarded by it
text: string;
lines: readonly string[]; // The text as lines, without the line break that ends each
appendLine(line: string): Promise<void>; // measured again and resent if another writer got there first
removeLine(index: number): Promise<void>;
replaceLine(index: number, line: string): Promise<void>;
setLines(lines: readonly string[]): Promise<void>; // writes only the runs of lines that differ
edit(changes: TextChange[]): Promise<void>; // any range, each measured after the ones before it
remove(): Promise<void>; // the app-content, refused if it changed since
};
// every write but appendLine rejects with code app/content-conflict, writing nothing,
// when the app-content has changed since
type AppContentChange = {
id: string; // Stable ProseMirror node ID
name?: string; // Human-readable name
content: string; // Current text
revision?: string; // The app-content's exact state; absent where edits cannot be guarded
deleted?: boolean; // The app-content may have been removed: a reason to check, not proof
};
type AppContentSnapshot = { id: string; content: string; revision: string; };
type AppContentEdit =
| { kind: 'created'; id: string; revision: string; content: string; }
| { kind: 'changes'; id: string; base: string; revision: string; changes: TextChange[]; ack?: string; } // each change measured after the ones before it
| { kind: 'removed'; id: string; base: string; }
| { kind: 'absent'; id: string; }; // an [id='…'] stream opened on no app-content
type TextChange = {
from: number; // Start offset
to: number; // End offset (same as from for pure inserts)
insert?: string; // Text to insert (absent or empty for pure deletes)
};
See Developer | App-Content and App-State for patterns and the selector reference.window.charmiq.appState
A single JSON blob stored in the document. Last-write-wins.| Method | Returns | Description |
|---|
onChange$() | Observable<any> | Watch for state changes. Fires immediately with current state. |
get() | Promise<any> | Get the current state object. |
set(state) | Promise<void> | Replace the entire state object. |
window.charmiq.oauth
Authenticate with external providers (Google, GitHub, Microsoft, Slack, and 30+ others) using the same OAuth infrastructure that powers Charm integrations.CharmIQ identifies your Application by the Application it loaded, never by anything your code declares — OAuth connections are isolated per Application automatically. An inline Application's connections are scoped to the document that hosts it (the same content in another document authorizes again there); a folder-based Application holds one connection wherever it's embedded.Your Organization administrator must have an Integration configured for the provider before getValidAuth() will work. The integration exists at the Organization level — Applications use it, they don't create it.| Method | Returns | Description |
|---|
getValidAuth(options) | Promise<AuthResult> | Get a valid (non-expired) access token. Opens a consent popup if needed. Refreshes automatically if expired — there is no separate refresh call; call getValidAuth() again. |
listAuth(options) | Promise<AuthDescriptor[]> | List this Application's connected accounts as token-less descriptors, for a multi-account picker. Options are { providerUrl } or { integrationId }. Get a token per account with getValidAuth({ connectionKey }). |
revokeAuth(auth) | Promise<void> | Revoke access. Pass the AuthResult from getValidAuth or a descriptor from listAuth. |
register(config) | Promise<void> | Legacy no-op, kept for backward compatibility. Resolves successfully; any config passed is ignored. New Applications skip it and call getValidAuth() directly. |
// 1. Get a token
const auth = await window.charmiq.oauth.getValidAuth({
providerUrl: 'https://accounts.google.com',
scopes: ['https://www.googleapis.com/auth/analytics.readonly'],
});
// auth.accessToken — ready to use
// 2. Use it
const res = await fetch('https://analyticsdata.googleapis.com/...', {
headers: { Authorization: `Bearer ${auth.accessToken}` },
});
// 3. Revoke when done
await window.charmiq.oauth.revokeAuth(auth);
getValidAuth() options:| Option | Description |
|---|
providerUrl | OAuth provider URL. Platform resolves the matching Integration from your Organization config. |
integrationId | Exact Integration ID. Use when you need to target a specific Integration (dev/testing). |
scopes | Requested scopes. Falls back to the Integration's configured defaultScopes if omitted. |
prompt | 'auto' (default) — uses cached token or auto-selects single account. 'select_account' — always shows the account picker. |
connectionKey | Target one already-connected account (an id from listAuth); skips the picker. Rejected if it isn't your Application's account or doesn't cover scopes. |
Account descriptors: listAuth returns AuthResults without accessToken — enough to render an account (displayIdentifier, integrationId, providerAccountId, …). The same login connected through two Integrations returns two descriptors: equal providerAccountId, different integrationId.Consent: listAuth and getValidAuth share one grant — allowing either grants token access, so listAuth shows a full-access prompt and later getValidAuth calls don't re-prompt. Every call settles: a denied request rejects — handle it, don't wait.Token isolation: Each Application gets its own isolated token scope, keyed on the Application CharmIQ resolved when it loaded. Application A's tokens cannot be read or used by Application B, even if both connect to the same provider with the same account.window.charmiq.mcp
Connect to MCP servers and call their tools. CharmIQ manages credentials, transport and connection lifecycle on behalf of your iframe.| Method | Returns | Description |
|---|
listServers() | Promise<McpServerDescriptor[]> | List all available MCP servers: CharmIQ servers and Organization-defined servers. |
connect(alias, config) | Promise<McpServerInfo> | Connect to an Organization-defined or explicit server. Platform servers don't need connect(). |
disconnect(alias) | Promise<void> | Disconnect a server by its alias. |
listTools(serverAlias) | Promise<McpToolDescriptor[]> | List tools on a connected server. |
callTool(serverAlias, toolName, args?) | Promise<McpToolCallResult> | Call a tool. |
MCP calls must also be declared in the Application's requestedScopes. The runtime alias is not, by itself, the consent scope: CharmIQ servers use mcp.platform.{serverName}, Organization-defined servers use mcp.org.{serverId}, and explicit connections use mcp.adhoc.{connectionName}. For example, calling 'vfs' requires mcp.platform.vfs, not mcp.vfs. See Developer | MCP in Applications → Declaring MCP Scopes for the complete table and wildcard guidance.CharmIQ MCP Servers
CharmIQ's built-in MCP servers are pre-connected. Call their tools directly using the server name as the alias — no connect() call needed.| Server Alias | Purpose |
|---|
'vfs' | Virtual file system — read/write documents, folders, app-content |
'agent' | Run Charms, manage conversations |
'application' | Build Applications into runnable artifacts |
'chat' | Create and manage Chat documents; run completions with Charms |
'collection' | Document collection management |
'context' | Manage a document's Reference Materials — documents, assets, and folders a Charm works against |
'docs' | Browse and read CharmIQ documentation |
'generation' | Generate images, video, audio via AI models |
'git' | Clone, pull, commit, push for git-backed Folders |
'usage' | Query credit balances and usage records |
'echo' | Deterministic testing tool |
// No connect() needed for Platform servers
const tools = await window.charmiq.mcp.listTools('generation');
const result = await window.charmiq.mcp.callTool('generation', 'create_image', {
prompt: 'A mountain at sunrise',
});
console.log(result.content[0].text);
Organization-Defined Servers
Servers your Organization administrator has configured. Discover them via listServers().const servers = await window.charmiq.mcp.listServers();
const orgServers = servers.filter(s => !s.isPlatform);
// → [{ id: 'abc-123', name: 'Company BigQuery', allowedTools: ['execute_sql'] }]
await window.charmiq.mcp.connect('bq', { mcpServerId: orgServers[0].id });
const tools = await window.charmiq.mcp.listTools('bq');
const result = await window.charmiq.mcp.callTool('bq', 'execute_sql', {
query: 'SELECT COUNT(*) FROM `project.dataset.orders`',
});
The first argument to connect() is an alias you choose — a local name you use in subsequent listTools, callTool, and disconnect calls.Explicit Connection (Dev/Testing)
For servers not configured at the Organization level:// OAuth by provider URL
await window.charmiq.mcp.connect('bigquery', {
url: 'https://bigquery.googleapis.com/mcp',
transport: 'proxy', // Routes through CharmIQ cloud function (bypasses CORS)
auth: {
type: 'oauth',
providerUrl: 'https://accounts.google.com',
scopes: ['https://www.googleapis.com/auth/bigquery'],
},
});
// No-auth server (direct browser connection)
await window.charmiq.mcp.connect('local', {
url: 'http://localhost:8080/mcp',
transport: 'direct', // Browser connects directly (requires permissive CORS)
auth: { type: 'none' },
});
Transport options:| Transport | When to Use |
|---|
'direct' | Local servers, LAN servers, or servers with permissive CORS headers |
'proxy' | Remote servers that block browser CORS (e.g. googleapis.com) |
Auth types:| Auth Type | Description |
|---|
{ type: 'none' } | No authentication |
{ type: 'platform' } | CharmIQ platform token — only for a CharmIQ server's own endpoint (any other URL is refused). Prefer calling CharmIQ servers by name |
{ type: 'oauth', providerUrl, scopes? } | OAuth via provider URL — platform resolves Integration from Org config |
{ type: 'oauth', integrationId, scopes, app } | OAuth via exact Integration ID — developer specifies directly |
Tool Call Results
type McpToolCallResult = {
content: Array<{ type: string; text: string }>;
structuredContent?: unknown;
isError?: boolean;
};
const result = await window.charmiq.mcp.callTool('bq', 'execute_sql', { query });
if (result.isError) {
console.error(result.content[0].text);
} else {
const data = JSON.parse(result.content[0].text);
}
Organization Tool Ceilings
When connecting via an Organization-defined server, the allowedTools field in the server descriptor lists the tools your Organization administrator has permitted. listTools() returns only what's allowed. You can never call a tool outside the ceiling regardless of what the MCP server advertises.Application Discovery
Application discovery is how Applications find each other and communicate. See Developer | Application Discovery for the full reference.For the charmiq.command command surface (host- and sibling-callable commands described by the manifest's inputSchema), use charmiq.exportCommands({ ... }) — methods take a single named-args object. See Developer | Application Discovery → The charmiq.command Namespace.At a glance:// Advertise a capability
window.charmiq.advertise('chart', {
setData: (data) => { chart.update(data); },
data$: () => dataSubject.asObservable(),
});
// Discover and call
window.charmiq.discover$('chart').subscribe(providers => {
if (providers[0]) providers[0].setData(myData);
});
The provider declares application.advertise; the consumer declares application.discover and application.call. A refused discovery rejects (discover()) or errors (discover$()) with the reason and code.
Observables
The bridge uses RxJS Observables for reactive data. rxjs is available globally inside every Application — you don't need to import it. Use rxjs.BehaviorSubject, rxjs.switchMap, rxjs.EMPTY, etc. directly.
Next Steps
→ Developer | App-Content and App-State — Patterns for the two storage channels.→ Developer | MCP in Applications — Connecting to MCP servers in depth.→ Developer | Application Discovery — Applications talking to each other.→ Developer | The Build Pipeline — How the manifest, virtual file system, and esbuild work together.