Contributing an extension
The community index is index.json in this folder. Every Aquamarine
server serves its copy at /extensions/index.json, and the app can point
at any other copy from Settings โ Extensions โ Change index.
New to extensions? Start with Writing extensions, which builds one step by step.
What an extension is
A folder with two files:
manifest.json, described below.- The entry script named by
main(defaultmain.js). It runs in a sandbox with one binding,aquamarine, and nothing else: nofetch, no DOM, no storage. Everything it does goes through the API in API reference.
{
"id": "word-count",
"name": "Word count",
"version": "1.0.0",
"minAppVersion": "0.1.0",
"description": "What it does, in one sentence.",
"author": "You",
"scopes": ["read:notes"],
"main": "main.js"
}
id: lowercase letters, digits and dashes; it is the folder name and must be unique in the index.versionandminAppVersion:major.minor.patch. The app refuses an extension whoseminAppVersionis newer than itself.scopes: only what the extension needs. The person sees this list when enabling it, and the token the extension gets has exactly these scopes. Methods outside them fail with a scope error.
Storing data and adding MCP tools
An extension that keeps its own data declares collections: each has a
JSON Schema that every record must match. tools then turns actions on a
collection into MCP tools, so a client such as Claude can read and write
that data. No extension code runs on the server; the server stores the
records (encrypted at rest), validates them against the schema and answers
the tools itself. See the recipes manifest for a
full example.
{
"collections": {
"recipes": { "description": "One recipe.", "schema": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" } } } }
},
"tools": [
{ "name": "import", "collection": "recipes", "action": "create", "description": "Save a recipe read from a web page." }
]
}
- At most 5 collections and 20 tools. Collection and tool names are
lowercase letters, digits and underscores. A schema is at most 32 KB, its
root is
"type": "object", and$refmay only point inside it (#/...). actionis one oflist,get,create,updateordelete. The MCP tool is named<id>_<name>with dashes in the id turned into underscores, such asrecipes_import, and must not clash with another tool.- The tool's
descriptionis what the MCP client reads to decide how to fill the record, so spell out the format you want there. - In the app, the extension reads and writes its records with
aquamarine.records.*and its own token; it needs no scope for that. It shows them in a tab withaquamarine.views.register. - Uninstalling an extension removes its tools but keeps its records, so installing it again brings them back.
Adding it to the index
- Put the folder next to this file, named after its
id. - Add an entry to
index.jsonwithid,name,description,author,version(the manifest's) andurl(the folder, relative to the index, with a trailing slash). When you release a change, raiseversionin both files; installed copies show Update when the index's is newer. - Run the client tests from
clients/appwithpnpm test; they install every entry of the index and check its manifest. - Open a pull request. Say what the extension does and why it needs each scope.
Trying it locally
Unpack the folder into <workspace folder>/.aquamarine/extensions/<id>/ and press
Rescan folder in Settings โ Extensions. Folder extensions load before the
index and cannot be uninstalled from the app; remove the folder instead.