Create your own battery

This guide shows MCP server authors how to create and publish a battery for their server.

Define tool contracts in TOML, using argument matching where appropriate. Add an annotator when determining a contract requires additional logic or information from the provider.

For example, a push to a public GitHub repository requires data that may be shared publicly. A push to a private repository requires data that may be shared with that repository's readers. Determining the repository's visibility and readers may require a GitHub API lookup.

Ask your coding agent

Copy this prompt into your coding agent:

Read https://openappa.com/write-a-battery and follow the guide.

Inspect the MCP server in this repository.
Create a battery for it.
Add its documentation and catalog card.
Run every available check.
Do not guess when the server code does not answer a question.
Tell me what you created and what you verified.
Tell me what I still need to review.

What happens next#

  1. Read. Your agent reads the server code and docs. It lists every tool, its inputs and results, and anything it can change.
  2. Build. It creates the battery, tests, documentation page, and catalog card. It records the exact server version and links each rule to the source that supports it.
  3. Review. You open those links, check the rules, resolve any unanswered questions, and submit the pull request.
What should I review?

Open the links next to each rule and check:

  • The battery includes every tool in the server version it names.
  • It identifies every tool that sends data or changes something.
  • Each action sends data only to the people or services you expect.
  • Only the right people can see each result.
  • Data from sources you have not checked is suspicious, not trusted.
  • The battery asks a person before every action that needs approval.
  • The agent lists every question it could not answer from the server code or docs.

Add the battery files#

Create a folder under marketplace/batteries/:

marketplace/batteries/
`-- your-server/
    |-- README.md
    |-- appa-package.toml
    |-- appa.toml
    |-- annotator.py       # optional
    `-- test_annotator.py  # when an annotator is included

appa-package.toml is the package manifest: the battery's name and description, the policy file, the hosts it is composed with, and the helper scripts its bindings name. Two optional fields in [battery] speak to the person who sets the battery up:

benefit = "APPA asks the Slack API who can read each channel. With this, APPA lets channel text flow only to people who can already read it."
setup = [
  "Create a Slack app at https://api.slack.com/apps.",
  "In OAuth & Permissions, add the User Token Scopes `users:read` and `channels:read`.",
  "Click Install to Workspace and copy the User OAuth Token. It starts with `xoxp-`.",
]
  • benefit is one line. Say what the helpers ask the provider, and what APPA does better with the answer. A battery without helpers says what its rules give. Do not claim more than the helpers and rules do.
  • setup is a list of steps, one line each, in imperative mood. Say which kind of token to make, where to make it, which scopes it needs, the prefix it starts with, and any other variable to set. Name the scopes the helpers' API calls need, not more. Do not describe a CLI sign-in here: declare it as a CLI alternative (see below), and appa ui shows it as a separate choice below the token field.

A step may contain code spans in backticks and bare https:// URLs. No other Markdown is rendered. appa battery install prints the benefit and the numbered steps after the battery is included, beside every token_env variable the policy's bindings name, and the token page saves the token under that variable. Thus a step does not tell the person to set the credential variable. A blank or multi-line benefit or step makes the manifest invalid.

Run bash scripts/appa-marketplace.sh to generate the catalog entry and content digest, then commit marketplace/marketplace.toml with the package. CI checks that the generated catalog is current.

appa.toml contains the tool contracts. An annotator determines contracts that static rules cannot express. An audience source supplies the members of the provider's collections: the viewer, the full membership, groups, and per-resource readers such as one channel's members. The battery binds it under [externals.audience.<provider>] with command, token_env, and the it serves. Contracts in the battery may then name those collections with selector placeholders, such as @slack:channel/$channel_id.

The battery README.md must name the server version and list the covered tools. It must also explain each contract, script, test, and known limit.

Declare in the battery's README.md what a root config adds around it: the authority its rules require, the mapping onto its source, and the credential variable. examples/README.md shows the install steps a root follows.

The test suite composes every battery into each host it declares to make sure the shipped batteries work together.

Declare local setup checks#

The existing appa-package.toml also describes the prerequisites that the readiness check tests. Credential names remain derived from the policy's token_env bindings.

[battery.readiness]
command = ["python3", "check.py"]
required_executables = ["python3"]

[[battery.readiness.cli_alternatives]]
executable = "gh"
credential = "APPA_PROVIDER_GITHUB_TOKEN"
login_hint = "gh auth login -s user:email"

List check.py in battery.helpers. The command must name python3 and one packaged helper, and required_executables must then include python3. Omit command to declare executable prerequisites without a provider check. A CLI alternative is optional when its credential is supplied; a CLI needed even with a token belongs in required_executables too. login_hint is the one command that signs the person in with every scope the helpers need.

The readiness helper receives only this battery's declared APPA credential variables. It must perform read-only checks, reuse the battery's authentication logic, and write one JSON object to stdout:

{"status":"ready","authentication":"token","reason":"verified"}

status is ready, needs_configuration, or unavailable. authentication is token, cli, or none. Reasons are verified, missing_configuration, missing_credential, cli_not_authenticated, missing_executable, invalid_credential, insufficient_access, provider_unavailable, check_failed, or check_timed_out. Only ready may use verified, and ready must use it.

Do not print credentials, identity information, freeform messages, or provider response bodies. APPA rejects unknown JSON fields and translates reason codes into UI text. It discards stderr, limits stdout to 4 KiB, and stops checks after 15 seconds. Checks must not install software or start an interactive login. login_hint is shown to the user and never executed automatically.

A successful check establishes only the access it actually tested. For example, a viewer check cannot prove access to every repository. Without a provider check, APPA decides readiness itself: the battery is ready when its required executables exist and every token_env variable has a value. APPA reports this as ready with reason configured; the token is set but not tested. A battery that declares nothing is ready.

Test the battery#

Unit tests#

For every annotator and audience source, write tests that need no credential: the consult envelope, the template check against declaration.templates, argument and selector handling, and every refusal path. An audience source must refuse a declaration that lists templates it does not serve, before it reads its token; test that refusal. Check the service against the real provider with a replay trace and a real token, as described below.

For example, to test a Python battery:

python3 -m unittest discover -s marketplace/batteries/your-server -p 'test_*.py'

Then check that the battery composes with the shipped ones:

cargo test -p appa --test marketplace

This test detects invalid battery config and conflicts with other batteries. CI also runs Python tests under marketplace/batteries/*/test_*.py.

Integration test#

Use appa replay validation to test the battery end to end. Replay proposes tool calls and checks the decisions from OpenAPPA. It does not run the tools.

In this example, the marketplace GitHub battery uses a github.repository-visibility annotator for reads. The annotator reads the repository owner and name from the call, asks the GitHub API whether the repository is private, and returns a complete contract for the call:

  • A public repository gives the result a audience.
  • A private repository limits the audience to the repository's collaborators, the collection @github:repo/<owner>/<repo>/collaborators that the battery's audience source resolves.
  • Trust follows who wrote the result. The battery's github context provider reports the authors of a pull request or issue; the result keeps the session's trust only when all of them are the repository's own people, and is otherwise.

The battery declares the annotator with a selector placeholder in its mandate, so each call's consult admits exactly the repository that call names; owner and repo become required string arguments of every tool that uses it:

# marketplace/batteries/github/appa.toml
[policy]
version = 2

[[policy.annotator]]
name = "github.repository-visibility"
ranks = ["suspicious"]
audiences = ["@github:repo/$owner/$repo/collaborators"]
marks = []

[[policy.tool]]
name = "mcp/github/get_file_contents"
annotator = "github.repository-visibility"

[externals.annotators."github.repository-visibility"]
command = ["python3", "repository-visibility.py"]
token_env = "APPA_PROVIDER_GITHUB_TOKEN"

[externals.audience.github]
command = ["python3", "audience-source.py"]
token_env = "APPA_PROVIDER_GITHUB_TOKEN"
selectors = [
  { template = "viewer", feeds = "self" },
  { template = "repo/<owner>/<repo>/collaborators" },
]

The battery implementation is in marketplace/batteries/github and the live replay in examples/live-replays/github.

The replay's appa.toml includes the battery and adds two tools that are not part of it. These tools make the trust and audience changes observable.

include = ["../../../marketplace/batteries/github/appa.toml"]

[policy]
version = 2
trust_chain = ["suspicious", "trusted"]

# This tool tests the trust change from the GitHub result.
[[policy.tool]]
name = "mcp/shell/run_command"
requires = { trust = "trusted" }
delta = {}

# This tool tests who can receive the GitHub result.
[[policy.tool]]
name = "mcp/mail/send"
requires = { trust = "suspicious", audience = { contains = ["$to"] } }
delta = {}

[externals]
timeout_ms = 30000
max_body_bytes = 1048576

The github-battery.appa trace uses one public repository and one private repository that the GitHub token can read:

# Public repository content is suspicious, but it can remain public.
mcp/github/get_file_contents {
  owner: "your-org"
  repo: "your-public-repo"
  path: "README.md"
}
expect allow

# Suspicious content cannot enter a tool that requires trusted input.
mcp/shell/run_command {
  command: "deploy"
}
expect deny

# Public repository content can go to a public destination.
mcp/mail/send {
  to: "public"
}
expect allow

# Private repository content narrows the audience.
mcp/github/get_file_contents {
  owner: "your-org"
  repo: "your-private-repo"
  path: "README.md"
}
expect allow

# Private repository content cannot go to a public destination.
mcp/mail/send {
  to: "public"
}
expect deny

# The repository's collaborators can receive the content.
mcp/mail/send {
  to: "@github:repo/your-org/your-private-repo/collaborators"
}
expect allow

Run the replay with a GitHub token that can read both repositories and list the private one's collaborators:

APPA_PROVIDER_GITHUB_TOKEN=... appa replay \
  --config examples/live-replays/github/appa.toml \
  examples/live-replays/github/github-battery.appa

Replay calls the configured annotator for each GitHub tool call. The annotator can call the GitHub API. The GitHub MCP tool and the other tools do not run.

Open the pull request#

Open a pull request against archestra-ai/OpenAPPA main. Include:

  • the new battery folder;
  • its README.md, scripts, and tests;
  • a battery documentation page under website/content/docs/;
  • a card in website/components/BatteryCatalog.tsx; and
  • the server repository, exact version or commit, covered tools, and test results in the pull request description.

Verify the pull request#

The pull request is ready for review when:

  1. Repository CI passes.
  2. Every contract uses the canonical tool id (mcp/<server>/<tool>) and the exact arguments from that server version.
  3. Every script passes its tests and refuses invalid input.
  4. The test that loads all batteries passes.
  5. The catalog card opens the battery documentation page.