For the complete documentation index, see llms.txt. Every page here is also available as Markdown: add .md to its URL.

Add tools to your app

Give your app tools an AI client can call, in a panoply.tools folder, and how they are scanned, reviewed and run

An app can also be an MCP server: a set of tools an AI client calls on the buyer's behalf. You add them to the app you are already publishing. There is no separate kind of listing, no second price and no second purchase. Buyers see an App + MCP label and an MCP chip on the card, and the tools are included in the one price.

Adding tools is opening in stages. If your submission is refused with code: "tools_not_open", tools are not switched on for the environment you submitted to yet. Nothing was created.

What you add

Put a panoply.tools folder in your zip (or at the root of your repository), next to your app, with two files:

  • tools.json, the manifest: what the tools are.
  • tools.js, one ES module under 1 MB: what they do.

An app without a site is not accepted (site_required): the tools belong to an app that has a page and screenshots like any other listing.

The manifest

{ "schema": 1, "capabilities": ["storage"], "tools": [ { "name": "build_set", "title": "Build a set", "description": "Builds a DJ set from the buyer's library.", "inputSchema": { "type": "object", "properties": { "minutes": { "type": "integer" } } }, "annotations": { "readOnlyHint": false } } ] }
FieldRule
schemaThe manifest version. Currently 1.
capabilitiesWhat your tools may use: storage, files, vault, jobs. Buyers are told in plain words before they buy.
tools[].namea-z, digits and _, starting with a letter, up to 48 characters, unique. Never get_job, list_jobs, cancel_job or get_file_link: Panoply adds those.
tools[].description1 to 1,000 characters. A person reads it at review.
tools[].inputSchemaA JSON Schema object (type: "object").
tools[].annotationsOptional hints such as readOnlyHint.
tools[].longtrue for a tool that runs for minutes. Needs the jobs capability.

At most 40 tools. Unknown extra fields are allowed.

When the manifest changes

Additive changes (a new optional field, a new capability) keep schema: 1. A breaking change gets a new number, and the old number keeps working for six months after the new one ships. Panoply emails you on the day a newer number ships and again 30 days before the old one ends. After it ends, the tools part of a listing still on the old number stops answering with tools_schema_expired, while the app itself keeps working and stays listed. Resubmit on a supported number to bring the tools back. A submission on a number that is not supported is refused with tools_schema_unsupported, naming the supported ones.

The code

tools.js exports one function per tool, as named exports or as a default-exported object. Each receives the tool's arguments and a ctx:

export default { async build_set(args, ctx) { const library = (await ctx.panoply.storage.get('library')) ?? [] return { tracks: library.slice(0, args.minutes) } }, }

A tool returns a JSON value (under 1 MB; write anything larger to a file). If it throws, the caller sees the error text.

ctx.panoplyWhat it doesNeeds
storage.get / list / set / deleteThe buyer's stored data in this app, the same data your page reads with panoplyStorage.storage
files.list / get / put / deleteThe buyer's files in their quota. put(name, type, stream, size) takes a stream of exactly size bytes.files
vault.run(provider, path, { body })A call to an AI provider with the buyer's own key from their Key Vault.vault
job.progress(fraction, message), job.cancelled()Progress and cancellation of a long tool's job (see below).jobs

Your code never holds a key, a token or a binding. It runs in an isolate with no network access: fetch, sockets, eval and dynamic import() all fail. Everything it can reach is through ctx.panoply, and every call is scoped to one buyer and this one app. A method for a capability you did not declare refuses with capability_not_declared.

Whose data a tool acts on

When a buyer's tools run, they act on that buyer's data. When an agent calls the tools for a custodian who connected it to the app, the tools act on the custodian's data, and every row they write is labelled with the agent as the actor. An agent that bought the app for itself acts on its own data.

Limits

A synchronous tool25 seconds of wall time, and 25 seconds of CPU. Longer: tool_timeout. The tool is not stopped, but each call carries its own authority, which ends with the 25 seconds: a ctx.panoply call made after that is refused with tool_budget_exceeded, whatever other calls the same buyer makes meanwhile. A storage, file or vault operation already in progress when the 25 seconds end can still finish, so write tools that are safe to run twice (idempotent writes, keys derived from the arguments).
Request and result1 MB each. Larger output belongs in a file.
Tools per app40
Storing dataTools that declare storage, files or jobs use your storage slot, like an app that stores data. On the Free plan that is one app; a second is refused with storage_slot_required.

Long-running tools and jobs

A tool that runs for minutes is declared "long": true (with the jobs capability). Calling it does not wait: it answers within a second or two with { job_id, status: "queued", poll_after_ms }, and the work runs as a job in the background. Panoply adds four tools to every listing for this, so the caller can follow it: get_job (status, progress, the result, the files it wrote), list_jobs, cancel_job and get_file_link (a signed link to read a file, valid for five minutes, which supports ranged requests so audio and video can seek).

In a long tool, ctx.panoply.job.progress(fraction, message) reports progress (a number from 0 to 1 and a short note) and ctx.panoply.job.cancelled() tells you whether the buyer cancelled; once a job is cancelled, ended, or the buyer stops holding the app, every other ctx.panoply call is refused (job_cancelled, job_ended). Files a job writes with files.put are listed on the job, and count in the buyer's quota like any file. Print with console.log as usual; if the job fails, the last 4 KB of that output and the error are kept for the buyer, never for you.

DurationUp to 30 minutes of wall time, or the tool's maxDurationSeconds if lower, and 5 minutes of CPU. Past it the job fails job_timeout.
ResultUp to 64 KB of JSON. Larger output belongs in a file (result_too_large).
Per buyer2 running jobs, and 5 waiting. A third running job is refused with job_concurrency_exceeded, a sixth waiting one with job_queue_full.
Per app50 running jobs at once; a job waits for a slot for up to 10 minutes (app_job_capacity).
Daily minutesEach app has a daily allowance of job minutes (600 unless Panoply sets another). At the allowance a new job is refused with app_job_minutes_exhausted; jobs already running finish.
Output sizeA tool that declares outputBytesHint is refused with files_quota_exceeded before any work starts if the buyer's free quota is smaller.
KeptFinished jobs are kept 30 days.

The buyer's page can use panoplyJobs.list(), panoplyJobs.get(id) and panoplyJobs.cancel(id). If the Key Vault is briefly down, a job's step is retried; if the buyer's access ends (a refund, or a revoked connection), the job ends not_entitled at its next check, at most 30 seconds later.

A job can run more than once. A retried step runs your handler again from the start, so write it to be safe to repeat.

How it is scanned and reviewed

The scan reads tools.js as code, not as text. It checks that every tool in the manifest has a handler and that no handler is missing from the manifest, and it flags network calls, dynamic code, imports and host objects. It checks tool names and descriptions for instructions aimed at the AI client, and for tools that do more than they say. A tool that cannot be read statically is flagged, never passed.

Then a person at Panoply reads every tool: its description, its schema and what it can reach, and marks the bundle seen. Approval waits for that. If you change tools.js after the scan, the listing needs a fresh scan and a fresh read. Buyers see the tools and what they can reach on the listing before they buy.

Testing your tools

You cannot read what a buyer's tools did with their data, and neither can Panoply's reviewers: a failed run's log is kept for the buyer alone. Debug by running your tools on your own account. You hold your own listing, so you are a buyer of it, and a failure there shows you the log.

If your licence includes the source

For a listing under the source-included or open licence, buyers who hold the app can download tools.js from GET /api/apps/{slug}/tools-source. Under use-only it stays private. This is the tools only: the app's own source is a separate matter.

Refusals you can meet

CodeMeaning
tools_not_openTools are not switched on in this environment yet.
site_requiredThere is a panoply.tools folder and no app next to it.
tools_manifest_invalidThe manifest or a file is wrong. The message names the field.
tools_source_too_largetools.js is over 1 MB.
tools_schema_unsupportedThe manifest number is not currently supported.
storage_slot_requiredThe tools store data and your Free plan's slot is used.
tools_not_reviewedA reviewer has not read the tools yet (seen on approval, by the team).
tools_schema_expiredThe tools' manifest number has passed its support window.
job_concurrency_exceeded, job_queue_full, app_job_capacityA buyer or the app is at its job limit.
app_job_minutes_exhaustedThe app's daily job minutes are used up; running jobs finish.
job_not_foundNo job of that id belongs to the caller.
job_timeout, job_cancelledA job ran past its time, or was cancelled.
file_link_expired, file_link_wrong_fileA file link is past its five minutes, or is for another file.