Manifest reference
Every field of plugin.json for store apps (pluginApiVersion 3), the grants, and every validation code the CLI, the store and the desktop installer can report.
plugin.json sits at the root of your app. It's the part of your app the store reviews and the user consents to: what the app is, where its tools live, which hosts it may reach, and what it may do.
For editor completion and inline errors, point it at the JSON Schema:
{ "$schema": "https://chataway.co/schemas/app-manifest-v3.json" }The schema checks shape. chataway validate --store runs the full store checks — the same code the store and the desktop installer run.
#A complete example
{
"$schema": "https://chataway.co/schemas/app-manifest-v3.json",
"pluginApiVersion": 3,
"id": "com.example.acme-assets",
"name": "Acme Assets",
"version": "1.2.0",
"description": "Upload images and audio from your chats to your Acme library.",
"publisher": "acme",
"categories": ["media", "games"],
"homepage": "https://example.com/chataway",
"support": "mailto:help@example.com",
"branding": {
"icon": "assets/icon.png",
"mark": "assets/mark.svg",
"accent": "#2F7BF6",
"screenshots": ["assets/s1.png", "assets/s2.png"]
},
"remote": {
"mcpUrl": "https://apps.example.com/chataway/mcp",
"domains": ["apps.example.com", "cdn.example.com"]
},
"accounts": [{
"id": "acme", "label": "Acme",
"authorizeUrl": "https://auth.example.com/oauth/authorize",
"tokenUrl": "https://auth.example.com/oauth/token",
"clientId": "chataway-3f9c1a",
"scopes": ["openid", "asset:write"],
"required": true
}],
"grants": ["files:receive", "host:media"],
"permissions": [
"Receive images and audio you choose to upload to Acme",
"Resize images to Acme's decal size before uploading"
],
"contributes": {
"tools": [{
"id": "upload_decals",
"name": "Upload decals",
"description": "Upload images to the user's Acme inventory as decals. Returns the new asset ids.",
"files": {
"images": {
"accept": ["image/png", "image/jpeg"],
"multiple": true,
"maxBytes": 20000000,
"prepare": { "op": "resize", "width": 512, "height": 512, "fit": "contain", "format": "png" }
}
},
"confirm": { "title": "Upload {{count}} image(s) to Acme?", "action": "Upload" },
"account": "acme"
}],
"panels": [{
"id": "main", "title": "Acme", "icon": "branding", "component": "webview",
"config": { "entry": "ui/index.html" },
"views": [{ "id": "assets", "label": "Assets" }]
}],
"settings": []
}
}#Top-level fields
| Field | Type | Required | |
|---|---|---|---|
pluginApiVersion | 3 | Yes | Store apps target 3. (Local dev folders may use 1 or 2.) |
id | string | Yes | Reverse-DNS, lowercase: com.yourcompany.app-name. Letters, digits, . and -; at least two segments; ≤ 64. Permanent once published. |
name | string | Yes | ≤ 40 characters. May not contain Chataway, Otto or claw-dev. |
version | string | Yes | Semver: 1.2.0, 1.3.0-beta.1. Each upload needs a new version. |
description | string | Yes | ≤ 400 characters. One or two sentences, shown in the store and on the install screen. |
permissions | string[] | Yes | Plain-language sentences the user reads before enabling (≤ 200 characters each). Must be non-empty when you ask for any grant. |
contributes | object | Yes | Tools, panels, settings. Use {} for none. |
publisher | string | Yes (store) | Your store publisher slug (≤ 40). |
remote | object | Yes (store) | Where your tools live, and which hosts you reach. → |
branding | object | Yes (store) | Icon (required), mark, accent, screenshots. → |
grants | string[] | No | Capabilities. → |
accounts | object[] | No | OAuth accounts. → |
categories | string[] | Recommended | Up to 3. → |
homepage | string | No | https URL. |
support | string | No | https URL or mailto: address. |
author | string | No | Shown on the detail page. |
platforms | string[] | No | Operating systems your app works on: darwin, win32, linux. Omit for all. Elsewhere it's listed as "Not available on Windows yet" and can't be installed or switched on. |
arch | string[] | No | CPU architectures: x64, arm64. Omit for all. |
requires.providers | string[] | No | Limit which chat providers get your tools: claw-chat, claude-code, codex, grok, cursor, opencode. Omit for all. |
requires.apps | object[] | No | Apps that must be on in the same project: { id, reason }. Store apps: built-ins and store apps only. → |
requires.connectors | object[] | No | Connectors (hosted MCP servers) your app works with: { url, reason, required? }. Fallbacks unless required: true. → |
requires.optional | object[] | No | Companions the user may tick: { id, reason } or { url, reason }. → |
#Not available to store apps
These exist for Chataway's built-in apps and for local apps (which also declare network.domains), and are rejected in store bundles (in dev mode, they're warnings):
| Field | Why | Use instead |
|---|---|---|
server | Store apps don't run code on the Mac | remote.mcpUrl |
contributes.skills, contributes.hooks, contributes.mcp, contributes.adapters | They install files into the user's project | Tools on your MCP server |
contributes.secrets | Apps don't hold API keys | accounts |
activation: "always" | Runs in every project regardless of the switch | — |
Grants network, exec, project:read | Unrestricted access | remote.domains, host services, file hand-off |
#requires
"requires": {
"apps": [{ "id": "browser-use", "reason": "Opens canva.com when the connector isn't connected" }],
"connectors": [{ "url": "https://mcp.canva.com/mcp", "reason": "Fast, reliable Canva edits" }],
"optional": [{ "id": "media", "reason": "Saves exports into your Media library" }]
}| Field | Rules |
|---|---|
apps[].id | An app id. Store apps: a built-in (browser-use, media, chat-artifacts, computer-use) or a published store app (com.acme.app) — never gh-…, npm-…, local.…. Not your own id; no duplicates. |
apps[].reason, connectors[].reason, optional[].reason | Required, ≤ 200 characters — shown on the install sheet. |
apps[].github | Not for store apps. Apps installed from GitHub: owner/repo[@ref] of a GitHub dependency (it gets its own install screen). |
connectors[].url | The connector's https MCP URL (http://localhost in dev mode, with a warning). |
connectors[].required | true = your app waits until the connector is added and on. Default: a fallback. |
optional[] | Exactly one of id or url. |
At most 10 entries per list. Adding a required app or connector in an update needs review and fresh consent. See Dependencies.
#remote
| Field | Type | |
|---|---|---|
mcpUrl | string | Your Streamable HTTP MCP endpoint. Must be https (in dev mode, http://localhost / 127.0.0.1 is allowed with a warning). Its host is always allowed. |
domains | string[] | Every other host your app reaches: your API, your CDN. Hostnames only — no scheme, no path, no port. *.example.com allowed; * and *.com are too broad. At most 20. |
domains is used for:
- your panel's CSP (
connect-src,img-src,media-src), - every request Chataway makes for your app: MCP calls, redirects, and
resource_linkdownloads, - the store listing (users see the list).
Adding a domain in an update needs human review and the user's consent again.
#grants
| Grant | Lets the app | Needed for |
|---|---|---|
storage | Keep files in its own per-project storage. Implicit — you don't need to list it. | — |
files:receive | Receive files the user or agent hands to its tools or panel | Any tool with files; bridge.pickFiles |
files:return | Return files that are kept in its storage and offered to the Media library | Keeping files from tool results |
host:media | Have Chataway run host media operations on files it was handed | prepare; bridge.host.media |
project:write | Ask to save a file into the project folder — always behind a card | Direct saves (panel API not available yet; galleries already offer Save to project) |
Every grant must be explained in permissions. Adding a grant in an update needs human review and the user's consent again.
#branding
| Field | Type | |
|---|---|---|
icon | path | PNG or SVG in the bundle, square, 512×512 (at least 256), ≤ 1 MB. Required for the store. |
mark | path | Single-colour SVG, no scripts or event handlers, ≤ 1 MB. Tinted per theme. |
accent | string | #RRGGBB. Header stripe, card edge. |
screenshots | path[] | Up to 8 PNG/JPG/WebP files, ≤ 3 MB each. |
Paths are relative to the bundle root, with no .. and no leading /. See Branding.
#accounts
| Field | Type | Required | |
|---|---|---|---|
id | string | Yes | Lowercase letters, digits, _; starts with a letter. Unique in the app. |
label | string | Yes | ≤ 40. |
auth | "mcp" | No | The MCP authorization spec: endpoints are discovered from remote.mcpUrl and Chataway registers itself (Dynamic Client Registration) — authorizeUrl, tokenUrl and clientId are then not needed (clientId = fallback for servers without registration). |
authorizeUrl | string | Yes¹ | https (http://localhost allowed in dev mode, with a warning). |
tokenUrl | string | Yes¹ | https. |
revokeUrl | string | No | https. |
clientId | string | Yes¹ | Public client id (≤ 200). Never a secret. |
scopes | string[] | Yes | May be empty. Adding scopes needs review. |
required | boolean | No | Offer Connect when the app is enabled. |
params | object | No | Extra authorize params (string → string). May not set client_secret, redirect_uri, code_challenge or state. |
¹ Not with auth: "mcp".
See Connected accounts.
#contributes.tools[]
| Field | Type | Required | |
|---|---|---|---|
id | string | Yes | The tool's name on your MCP server. Lowercase letters, digits, _; starts with a letter; ≤ 48. Unique. |
name | string | Yes | Human name for cards and the Inspector (≤ 60). |
description | string | Yes | What the agent reads to decide when and how to call the tool (≤ 1024). Overrides your server's description. |
files | object | No | File params, keyed by argument name. Needs files:receive. → |
confirm | object | No | A blocking approval card. → |
account | string | No | An accounts[].id. The call carries that account's token; not connected → a Connect card. |
Write descriptions for the agent: what the tool does, when to use it, what it returns, and anything it must not do. "Upload images to the user's Acme inventory as decals. Returns the new asset ids. Only call it when the user asked to upload."
#files
"files": { "<param>": { "accept": ["image/*"], "multiple": true, "maxBytes": 20000000, "prepare": { "op": "resize", "width": 512 } } }| Field | Type | Default | |
|---|---|---|---|
accept | string[] | any | MIME types or type/*. |
multiple | boolean | false | A list of files. |
maxBytes | number | 20 MiB | Per file, after prepare. 1 to 20,971,520. |
prepare | object | — | A host media request: { "op": …, …params }. Needs host:media. |
Param names follow the tool id rules. See Files & hand-off.
#confirm
| Field | Type | |
|---|---|---|
title | string | Required, ≤ 120. {{count}} = number of files handed over. |
body | string | Markdown under the title. |
action | string | Approve button label. |
See Cards & approvals.
#contributes.panels[]
| Field | Type | Required | |
|---|---|---|---|
id | string | Yes | ≤ 40. |
title | string | Yes | ≤ 40. |
component | "webview" | Yes | The only component for store apps. |
icon | string | No | "branding" for your mark. |
config.entry | path | No | HTML entry in the bundle. Default ui/index.html. Must exist. |
views | { id, label }[] | No | Host-drawn header tabs. |
See Panels & the bridge.
#contributes.settings[] and settingSections[]
See Settings for every field.
#Categories
Up to three of:
media · design · productivity · developer · data · marketing · sales · games · communication · finance · education · other
#Bundle
A .chataway-app file is a zip of:
| Path | |
|---|---|
plugin.json | Required, at the root. |
ui/** | Your panel (HTML, JS, CSS, fonts, images). |
assets/** | Icon, mark, screenshots, other listing art. |
README.md | Becomes the store description. Recommended. |
CHANGELOG.md | Shown with updates. |
LICENSE, LICENSE.md | Optional. |
Anything else at the top level is rejected. No executables (.node, .dylib, .so, .dll, .exe, .sh, .command, .app, .pkg, .dmg, .py, .rb, .jar). Total ≤ 25 MiB. Minified JavaScript is fine; obfuscated JavaScript is flagged. chataway pack builds the bundle for you.
#Validation codes
Every issue has a stable code, a path into the manifest (or bundle) and a message. chataway validate prints them like this:
✖ contributes.tools[0].files.images.prepare: "prepare" needs the "host:media" grant [tools.files.prepare.grant]
⚠ categories: Add at least one category so people can find the app [categories.missing]Error issues block publishing. Warnings don't, but reviewers see them. Rules marked Error (store) · warning (dev) are store-only: chataway dev accepts them so you can experiment locally, and the store rejects them.
This table is generated from the validator the store runs.
| Code | Level | Message |
|---|---|---|
accounts.auth | Error | auth must be "mcp" (or left out for a plain OAuth account) |
accounts.auth.remote | Error | auth "mcp" needs remote.mcpUrl (the endpoint is discovered from it) |
accounts.authorizeUrl | Error | authorizeUrl must be https |
accounts.authorizeUrl.localhost | Warning | authorizeUrl points at localhost — fine for development, the store needs https |
accounts.clientId | Error | clientId is required (public client, PKCE — never a secret) |
accounts.duplicate | Error | Duplicate account id "…" |
accounts.format | Error | accounts must be a list |
accounts.id | Error | account id: lowercase letters, digits, _ |
accounts.item | Error | Each account is an object |
accounts.label | Error | account label is required |
accounts.params | Error | params must map strings to strings |
accounts.params.reserved | Error | params may not set client_secret, redirect_uri, code_challenge or state |
accounts.revokeUrl | Error | revokeUrl must be https |
accounts.revokeUrl.localhost | Warning | revokeUrl points at localhost — fine for development, the store needs https |
accounts.scopes | Error | scopes must be a list of strings |
accounts.tokenUrl | Error | tokenUrl must be https |
accounts.tokenUrl.localhost | Warning | tokenUrl points at localhost — fine for development, the store needs https |
arch.format | Error | arch: a non-empty list without repeats (omit it to mean every CPU) |
arch.unknown | Error | Unknown architecture "…" (use: …) |
branding.accent | Error | accent must be #RRGGBB |
branding.format | Error | branding must be an object |
branding.icon | Error | branding.icon must be a relative path in the bundle |
branding.icon.missing | Error | Store apps need branding.icon (512×512) |
branding.icon.size | Error | branding.icon must be ≤ 1 MB |
branding.icon.small | Error | branding.icon is …px; use 512×512 |
branding.icon.square | Error | branding.icon is …×…; it must be square |
branding.icon.type | Error | branding.icon must be PNG or SVG |
branding.mark | Error | branding.mark must be a relative path in the bundle |
branding.mark.script | Error | branding.mark may not contain scripts or event handlers |
branding.mark.size | Error | branding.mark must be ≤ 1 MB |
branding.mark.svg | Error | branding.mark must be an SVG |
branding.screenshot.size | Error | screenshots must be ≤ 3 MB each |
branding.screenshots | Error | screenshots: up to 8 PNG/JPG/WebP paths in the bundle |
bundle.executable | Error | "…" looks executable; store bundles carry no native code |
bundle.manifest | Error | plugin.json is missing at the bundle root |
bundle.manifest.json | Error | plugin.json is not valid JSON: … |
bundle.missing | Error | … "…" is not in the bundle |
bundle.obfuscated | Warning | "…" looks obfuscated — reviewers may reject it; ship readable (minified is fine) code |
bundle.path | Error | Unsafe path "…" |
bundle.readme | Warning | Add a README.md — it becomes the store description |
bundle.size | Error | Bundle is … MB; the limit is 25 MiB (26.2 MB) |
bundle.unexpected | Error | "…" — store bundles hold plugin.json, ui/, assets/, README.md, CHANGELOG.md, LICENSE |
categories.format | Error | categories: up to 3 |
categories.missing | Warning | Add at least one category so people can find the app |
categories.unknown | Error | Unknown category "…" (use: …) |
contributes.missing | Error | contributes is required (use {} for none) |
description.missing | Error | description is required (≤ 400 characters) |
grants.format | Error | grants must be a list |
grants.level | Error (store) · warning (dev) | "…" is only for built-in apps |
grants.unknown | Error | Unknown grant "…" |
homepage.url | Error | homepage must be an https URL |
id.format | Error | Store app ids are reverse-DNS, lowercase: com.yourcompany.app-name |
id.missing | Error | id is required |
id.reserved | Error | "…" is a built-in app id |
level.activation | Error (store) · warning (dev) | activation 'always' is only for built-in apps |
level.adapters | Error (store) · warning (dev) | contributes.adapters is only for built-in apps |
level.hooks | Error (store) · warning (dev) | contributes.hooks is only for built-in apps |
level.mcp | Error (store) · warning (dev) | contributes.mcp is only for built-in apps |
level.secrets | Error (store) · warning (dev) | Store apps connect accounts (accounts[]) instead of asking for API keys |
level.server | Error (store) · warning (dev) | Store apps cannot ship server code — put your logic behind remote.mcpUrl |
level.skills | Error (store) · warning (dev) | contributes.skills is only for built-in apps |
manifest.api-version | Error | pluginApiVersion must be one of 1, 2, 3 |
manifest.api-version-store | Error | Store apps must target pluginApiVersion 3 |
manifest.not-object | Error | plugin.json must be a JSON object |
media.aspect | Error | aspect like "1:1" or "9:16" |
media.compress | Error | compress needs quality or targetKb |
media.convert | Error | convert needs a format |
media.crop | Error | crop needs width+height (with x/y) or an aspect |
media.dimension | Error | … must be an integer 1..… |
media.fit | Error | fit: cover | contain | fill |
media.format | Error | A media request is an object with an "op" |
media.format.value | Error | unsupported format |
media.gravity | Error | gravity: center | north | south | east | west |
media.offset | Error | … must be an integer 0..16384 |
media.op | Error | op must be one of … |
media.quality | Error | quality must be 1..100 |
media.resize | Error | resize needs width and/or height |
media.targetKb | Error | targetKb must be 10..50000 |
media.time | Error | … must be 0..600 seconds |
media.trim | Error | trim needs start and/or end |
media.trim.range | Error | end must be after start |
name.missing | Error | name is required (≤ 40 characters) |
name.reserved | Error | The name may not contain "Chataway" or other reserved names |
network.domain | Error | "…" is not a hostname (no scheme, no path; "*.example.com" allowed) |
network.domain.broad | Error | "…" is too broad |
network.domains.count | Error | At most 50 domains |
network.format | Error | network must be { domains: string[] } |
network.grant | Warning | network.domains has no effect without the "network" grant |
network.store | Error | Store apps declare remote.domains, not network |
panels.component | Error | Store apps render panels as 'webview' |
panels.entry | Error | config.entry must be a relative path inside the bundle |
panels.format | Error | contributes.panels must be a list |
panels.id | Error | panel id is required |
panels.item | Error | Each panel is an object |
panels.title | Error | panel title is required |
permissions.empty | Error | Explain every grant to the user in permissions[] |
permissions.format | Error | permissions must be a list of plain-language sentences |
platforms.format | Error | platforms: a non-empty list without repeats (omit it to mean every OS) |
platforms.unknown | Error | Unknown platform "…" (use: …) |
publisher.missing | Error | publisher (your store publisher slug) is required |
remote.and-server | Error | An app is either remote or has server code, not both |
remote.domain | Error | "…" is not a hostname (no scheme, no path; "*.example.com" allowed) |
remote.domain.broad | Error | "…" is too broad |
remote.domains | Error | remote.domains must list the hosts the app reaches |
remote.domains.count | Error | At most 20 domains |
remote.localhost | Warning | remote.mcpUrl points at localhost — fine for development, the store needs https |
remote.missing | Error | Store apps need remote.mcpUrl and remote.domains |
remote.url | Error | remote.mcpUrl must be an https URL |
requires.apps.url | Error | Connectors go in requires.connectors ({ url, reason }) |
requires.count | Error | At most … entries in requires.… |
requires.duplicate | Error | "…" is listed twice |
requires.format | Error | requires must be an object |
requires.github | Error | github must be "owner/repo" (optionally "@ref") |
requires.github.store | Error | Store apps can't depend on GitHub code ("…") |
requires.id | Error | id must be an app id (browser-use, com.acme.app…) |
requires.item | Error | Each required app is { id, reason } |
requires.optional.kind | Error | Give either id (an app) or url (a connector) |
requires.reason | Error | Say why in "reason" (≤ 200 characters) — it is shown on the install sheet |
requires.required | Error | required must be true or false |
requires.self | Error | An app cannot require itself |
requires.url | Error | url must be the connector's https MCP URL |
requires.url.localhost | Warning | A localhost connector — fine for development, the store needs https |
settings.format | Error | contributes.settings must be a list |
support.url | Error | support must be an https URL or mailto: |
tools.account | Error | account "…" is not declared in accounts[] |
tools.confirm | Error | confirm needs a title |
tools.description | Error | tool description is required (≤ 1024) — it is what the agent reads |
tools.duplicate | Error | Duplicate tool id "…" |
tools.files | Error | files maps param names to file rules |
tools.files.accept | Error | accept lists MIME types ("image/png", "image/*") |
tools.files.grant | Error | Tools with file params need the "files:receive" grant |
tools.files.maxBytes | Error | maxBytes must be 1..20971520 |
tools.files.param | Error | param names: lowercase letters, digits, _ |
tools.files.prepare.grant | Error | "prepare" needs the "host:media" grant |
tools.files.rule | Error | Each file param is an object |
tools.format | Error | contributes.tools must be a list |
tools.id | Error | tool id: lowercase letters, digits, _ (≤ 48) |
tools.item | Error | Each tool is an object |
tools.name | Error | tool name is required |
version.format | Error | version must be semver (1.2.3) |