Docs
The package format
A package is one JSON document. It names the site it targets, the tools it registers there, and the site's own HTTP endpoints those tools call. Nothing in it can run arbitrary code in the page: a tool is a declared request, not a script. This page covers every field. When you're ready, paste it into the form.
The shape
Every field below is checked before a package is stored. Validation failures come back as the zod issue list, keyed by the path that failed.
versioninteger ≥ 1 · required
1 when you create a package; on a new version it must be exactly one higher than the current highest, or the API answers 409 with an expectedVersion. That makes a publish built on a stale copy fail loudly instead of silently replacing a version you never read.domainstring, 1–253 · required
www. is stripped. It has to be reachable through urlPatterns. A domain no pattern covers would publish and then never match a page.urlPatternsstring[], 1–20 · required
@match-style patterns: scheme://host/path. Scheme is *, http, or https; host is *, *.acme.com (the apex and any subdomain), or an exact hostname; path starts with / and may contain *. Where more than one package matches a page, the more specific pattern ranks higher.titlestring, 1–200 · required
descriptionstring, 1–5000 · required
toolsToolDescriptor[], 1–30 · required
apiApiBlock · optional
minEngineinteger ≥ 1 · optional
1.pageTypestring, ≤100 · optional
changelogstring, ≤2000 · optional
Tools
A tool is metadata plus a binding. The description and the input schema are the entire interface an agent sees, so they carry the weight. An agent picks a tool by reading them and nothing else.
namestring, ≤30 · required
_ and - after that. Prefix them so they don't collide on a page that has its own tools: notes_search, not search. A tool whose name collides with one the site registered itself is skipped; the rest of the package still registers.descriptionstring, ≤500 · required
inputSchemaJSON Schema object · required
{ type: "object", properties, required? }. properties defaults to {}, so it's effectively optional. At most 20 properties; each takes a type and an optional description (≤1000) and enum, plus nested items/properties/required if you need them. Each property object is loose; unknown keys pass through. Property names are what {{param}} placeholders in the api block refer to.annotationsobject · optional
readOnlyHint, untrustedContentHint. Extra keys are allowed and passed through. Mark writes honestly.execution{ mode: "api", endpoint } · optional
api.endpoints. mode is "api", the only mode there is. A tool with no execution is skipped at registration and never reaches the agent.The api block
This is where the format earns its keep. Instead of shipping a script that pokes at the DOM, you declare the requests the site's own front end already makes, and the extension derives the call. Everything is data, so a reader can audit a package by reading it.
baseUrlhttps:// URL · required
urlPatterns. That is the same-origin rule: a package can only talk to the site it runs on, with the session the user already has. There is no way to declare a request to somewhere else.endpointsRecord<string, ApiEndpoint> · required
authRecord<string, ApiAuthSource> · optional
documentsRecord<string, string> · optional
"@documents/name". Never scanned for placeholders.An endpoint
method / pathrequired
GET, POST, PUT, PATCH, DELETE, and a path resolved against baseUrl.queryRecord<string, string> · optional
{{param}}.body / form / graphqlat most one
body is a JSON template, form is form-encoded fields, graphql is { document, variables }. Declaring two is rejected: the executor would pick one and you'd ship a request you never asked for. A body or graphql.variables leaf that is exactly one {{param}} keeps its type, so a number stays a number.returnsJMESPath expression · optional
errorPathstring[] · optional
["json", "errors"]. A non-empty value there means the call failed, even on a 200. An array, not a dotted string, so a key containing a dot is unambiguous.stripPrefixstring, ≤50 · optional
)]}'. Stripped only when present.persistedQueryboolean · optional
authstring[], ≤10 · optional
An auth source
source.endpointrequired
source.extract / patternexactly one
extract is a locator into the JSON response, e.g. ["data", "modhash"]. pattern is a regex over the raw response text, for tokens served in HTML. Capture group 1 is the token, and no match is a loud failure. Declaring both, or neither, is rejected. Any {{param}} inside pattern is interpolated raw before the regex is compiled, so keep interpolated params identifier-shaped (numeric ids, slugs).sendAs{ in, name } · required
header, form, or query, under name. A form token on an endpoint with no form body is rejected; the request would go out without its credential.ttlSecondsinteger, 1–86400 · optional
Placeholders
{{param}} in a path, query value, form field, body leaf, or GraphQL variable is substituted from the bound tool's input. For a tool-bound endpoint, every placeholder must name a property of that tool's inputSchema, or the package is rejected, which also means a typo can't reach a user as an empty string. The same check covers an auth source's fetch endpoint (path/query only) and its pattern; an endpoint no tool binds to is never scanned at all. A query/form/path placeholder for a param the agent didn't supply interpolates to an empty string, while a body/graphql.variables leaf that is exactly one {{param}} yields undefined and drops out of the JSON entirely. There is no escape hatch for a literal {{ yet.
A complete package
Two tools against a site's own JSON API: one read, one write that needs a CSRF token the site hands out on a different endpoint. This document validates: it is parsed against the schema by the test suite, so it can't drift from the format.
{
"version": 1,
"domain": "acme.com",
"urlPatterns": [
"*://*.acme.com/*"
],
"title": "Example Notes",
"description": "Search and create notes on acme.com as the logged-in user, through the site's own JSON API. Reads need no token; writes attach the CSRF token the session endpoint hands out.",
"minEngine": 1,
"changelog": "Initial version: notes_search, notes_create.",
"api": {
"baseUrl": "https://www.acme.com",
"auth": {
"csrf": {
"source": {
"endpoint": "session",
"extract": [
"data",
"csrfToken"
]
},
"sendAs": {
"in": "header",
"name": "X-CSRF-Token"
},
"ttlSeconds": 300
}
},
"endpoints": {
"session": {
"method": "GET",
"path": "/api/session.json"
},
"searchNotes": {
"method": "GET",
"path": "/api/notes",
"query": {
"q": "{{query}}",
"limit": "{{limit}}"
},
"returns": "notes[].{id: id, title: title, updated_at: updated_at}"
},
"createNote": {
"method": "POST",
"path": "/api/notes",
"body": {
"title": "{{title}}",
"body": "{{body}}"
},
"auth": [
"csrf"
],
"errorPath": [
"errors"
],
"returns": "note.{id: id, url: url}"
}
}
},
"tools": [
{
"name": "notes_search",
"description": "Search the logged-in user's notes on acme.com by keyword. Returns the matching notes' ids, titles, and last-updated timestamps, newest first.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Keywords to search note titles and bodies for"
},
"limit": {
"type": "integer",
"description": "How many notes to return (1-50)"
}
},
"required": [
"query"
]
},
"annotations": {
"readOnlyHint": true
},
"execution": {
"mode": "api",
"endpoint": "searchNotes"
}
},
{
"name": "notes_create",
"description": "Create a new note on acme.com as the logged-in user. Returns the new note's id and URL.",
"inputSchema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Note title"
},
"body": {
"type": "string",
"description": "Note body (plain text)"
}
},
"required": [
"title",
"body"
]
},
"annotations": {
"readOnlyHint": false
},
"execution": {
"mode": "api",
"endpoint": "createNote"
}
}
]
}The three curated packages in the repository are the same thing against real sites, and cover the awkward cases: Reddit extracts a JSON token into a header, Hacker News scrapes its tokens out of HTML with pattern, Google Maps needs stripPrefix and projects positional arrays. Read them.
Publishing
Paste the JSON or POST /api/packages with a Bearer API key. Same schema either way. Nothing reviews it; it goes live when it validates.
Versions are append-only. A new version is a POST to /api/packages/:id/versions with version set to one above the current highest, and only the owner may publish one. urlPatterns, tools, api, minEngine and changelog are version-scoped and travel with the content. Title, description, domain and pageType are package metadata and are edited in place.
Installs pin to a version, so publishing a new one never moves anybody. They update when they choose to. That cuts both ways: a version you published is a version someone may still be running. What you grant when you publish is worth reading once.