BetaFindAgent is in free public beta — every agent is free to connect and paid agents aren't available yet.
A declarative manifest can describe most REST APIs. It cannot describe an API that wants a secret in the request body, a computed signature, or a token exchange. Here's the full diagnostic.
Four fields — kind, delivery, exec and targets — decide whether an agent installs onto a machine or connects over a hosted URL. Get one wrong and the agent silently won't appear in the client.
Code agents are generated, not authored. The importer reads your DXT manifest, MCP config, .env.example, package.json and README — and everything runs in an ephemeral sandbox behind a default-deny allowlist.
An MCP agent manifest is a declarative JSON document that describes what an agent can do — and nothing that does it. The tools are declared, the actions are bound to a fixed set of verbs, the credentials are bound to an audience, and the guardrails are enforced by the gateway rather than by the agent. The manifest never carries executable code.
That constraint is the entire security model, and it's also why a first submission usually fails
for one of two reasons: a credential slot with no allowed_hosts, or an action pointing at a host
its own credential slot doesn't cover. Both fail closed, before a human ever looks at the listing.
This post is the full anatomy plus one complete, working manifest you can copy — a GitHub release- notes drafter with two read tools, one write tool, one audience-bound credential, and a guardrail block that matches what each tool actually does.
Hand-authoring is the exception, not the default. Route the idea before you write a line of JSON.
flowchart TD
Start["What are you shipping?"] --> Q1{"Does it call<br/>external APIs?"}
Q1 -->|"No — prompt only"| Recipe["kind: static-recipe<br/>delivery: prompt<br/><br/>Author by hand ✅"]
Q1 -->|"Yes"| Q2{"Is the logic already<br/>real code in a repo?"}
Q2 -->|"Yes"| Code["kind: code-bundle<br/><br/>GitHub submit flow GENERATES it.<br/>Never hand-write findagent.json ❌"]
Q2 -->|"No — declarative HTTP"| Doer["kind: mcp-tool (doer)<br/><br/>Author by hand ✅<br/>This post"]
Start --> Q3{"Is it a repo of<br/>skills / rules?"}
Q3 -->|"Yes"| Skills["kind: skills-bundle<br/><br/>Built from your repo<br/>by the submit flow"]
Start --> Q4{"Is it an MCP server<br/>someone already hosts?"}
Q4 -->|"Yes"| Dir["Not a manifest kind —<br/>an MCP directory listing<br/>(Hosted or Local)"]
Start --> Q5{"Is it 2–16 existing agents<br/>working as a team?"}
Q5 -->|"Yes"| Dept["Not a manifest —<br/>a Department<br/>(pipeline · hub · peer-to-peer)"]Three of those five paths are not manifests you write. The most common mistake we see is someone
hand-writing a findagent.json for code they already have in a repo — the importer reads the DXT
manifest.json, MCP config, .env.example keys, package.json and README, and generates the
manifest including the egress allowlist. Hand-writing it means maintaining by hand a document the
platform regenerates on every version bump.
autonomous-agent is a reserved roadmap kind with no distinct runtime behaviour yet. It isn't a
shippable shape and isn't offered as a choice — a submission that names it is coerced to mcp-tool.
Pick a real shape instead.
Nothing in the manifest runs. The shared @findagent/mcp runtime reads the declaration and performs
the work, and the gateway sits in front of it. Understanding where each field takes effect is what
makes the guardrail choices obvious later.
sequenceDiagram
participant C as MCP client<br/>(Claude, Cursor, ChatGPT)
participant G as FindAgent gateway
participant R as @findagent/mcp runtime
participant V as Credential vault
participant API as api.github.com
C->>G: tools/call list_merged_pulls {owner, repo}
Note over G: guardrails.input<br/>max_length · deny_patterns · pii_redaction
G->>G: guardrails.actions[tool]<br/>rate_limit · approval · max_amount
G->>R: forward call
R->>R: bind {owner}/{repo} into action.url
R->>V: resolve auth_ref → github_token
Note over V: allowed_hosts check:<br/>does api.github.com match?
V-->>R: credential attached ONLY on match
R->>API: HTTPS GET (no redirect following)
API-->>R: JSON
R->>G: result
Note over G: guardrails.output<br/>secret_leak_scan (mandatory) · schema
G-->>C: resultTwo things to notice. The credential is attached at request time, per destination host — it is
not handed to the agent. And secret_leak_scan runs on the way out unconditionally; it is not a
setting you configure, and a manifest that tries to disable it fails validation.
{
"name": "GitHub Release Notes Drafter",
"description": "Reads merged pull requests in a repository and drafts a structured release note, then optionally opens it as a draft GitHub release for a human to review and publish.",
"tagline": "Turn merged PRs into a release note you'd actually ship",
"kind": "mcp-tool",
"system_prompt": "You draft release notes from merged pull requests. Group changes into Features, Fixes, and Internal. Write one line per user-visible change in plain language, describing the effect on the user rather than the implementation. Always cite the PR number. Never invent a change that is not present in the pull request data. When asked to create a release, always show the drafted body to the user and wait for explicit confirmation before calling create_release_draft."
}Limits worth memorising: name is 3–80 characters, system_prompt is 50–20,000, description is
optional up to 4,000, tagline is 140, and you may declare at most 40 tools.
category is a loose hint, not an exact slug — the taxonomy has a required discipline axis and
an optional industry axis. Name them explicitly when you know them; a plain "engineering"
resolves safely on its own.
Names are snake_case, verb + object. Descriptions are one sentence. input_schema is optional —
except that every {param} placeholder in an action URL must be defined in it, or the binding
has nothing to bind.
Annotations are not documentation. Clients and runtimes gate behaviour on them:
| Annotation | Means | Set it false when |
|---|---|---|
readOnlyHint |
The tool only reads | The tool writes, creates, or deletes anything |
destructiveHint |
Makes irreversible changes | The change can be undone by the user |
idempotentHint |
The same call can safely repeat | Repeating creates a duplicate |
openWorldHint |
Reaches an external or open-ended resource | Everything stays inside the agent |
Marking a writing tool readOnlyHint: true is the annotation error that most often survives to
review — and it's the one with real consequences, because clients use it to decide what to run
without asking.
Two action types exist and no others: http and prompt-template.
For http: method, url, optional headers and body_template, and an optional auth_ref.
Use https — the runtime restricts outbound calls to secure connections, guards egress against
internal or unexpected addresses, and does not follow redirects blindly.
Never inline a secret into a URL or a header. Point at a slot with auth_ref. This is not
style; a literal key in headers is caught by the static scan and rejected.
There is no agent-to-agent action type. Multi-agent teamwork is not a tool binding — it ships as a Department, which composes existing agents into a topology. Don't reach for an action to call another agent; there isn't one.
Every slot needs a ref, a human label, and — required — allowed_hosts.
{
"ref": "github_token",
"label": "GitHub personal access token",
"description": "A fine-grained PAT with Contents: read and Pull requests: read on the target repository. Add Contents: write only if you want the agent to create draft releases.",
"type": "secret",
"auth_scheme": "bearer",
"env": "GITHUB_TOKEN",
"allowed_hosts": ["api.github.com"],
"required": true
}allowed_hosts matches exact host or subdomain, and the runtime attaches the credential only
when the destination host matches. Keep the list as narrow as the agent allows — ["api.github.com"],
never ["github.com"] and never a wildcard you don't need.
auth_scheme decides what the runtime prepends:
auth_scheme |
Runtime sends | Buyer pastes |
|---|---|---|
bearer |
Authorization: Bearer <value> |
The raw token |
basic |
Authorization: Basic <value> |
A pre-encoded base64 string |
raw |
Authorization: <value> |
The complete header value |
header |
<header_name>: <value> |
The bare key (requires a header_name field) |
ref is literally what a buyer types into findagent secrets set <ref> <value>, so keep it short,
stable and snake_case. type drives the install UI: string, secret (masked), or json (a blob,
e.g. a service-account key). description is buyer-facing help at install time — write it as
instructions, and never put a secret in it.
Guardrails are enforced at the gateway, not in the agent, and may only ever be tightened.
max_length caps payload size, deny_patterns is the prompt-injection deny-list,
pii_redaction strips categories (credit_card, email, phone, iban, national_id, …).secret_leak_scan is mandatory and always runs; an optional schema validates the
result shape.approval (none | human), spend cap max_amount,
rate_limit (e.g. 10/hour), and idempotent (may the gateway auto-retry after a crash).Defaults that should be your reflex: approval: "human" on anything that spends money or carries
destructiveHint; idempotent: false on any tool with side effects; a rate_limit on every tool
that calls an external API.
Nothing elided. This is the artifact.
{
"name": "GitHub Release Notes Drafter",
"description": "Reads merged pull requests in a repository and drafts a structured release note, then optionally opens it as a draft GitHub release for a human to review and publish.",
"tagline": "Turn merged PRs into a release note you'd actually ship",
"system_prompt": "You draft release notes from merged pull requests. Group changes into Features, Fixes, and Internal. Write one line per user-visible change in plain language, describing the effect on the user rather than the implementation. Always cite the PR number. Never invent a change that is not present in the pull request data. When asked to create a release, always show the drafted body to the user and wait for explicit confirmation before calling create_release_draft.",
"kind": "mcp-tool",
"discipline": "engineering-development",
"additional_category_slugs": ["engineering-development/developer-tools"],
"tags": ["github", "release-notes", "changelog", "engineering"],
"llms": ["claude", "chatgpt", "cursor", "vscode"],
"tools": [
{
"name": "list_merged_pulls",
"description": "Lists recently merged pull requests for a repository, newest first.",
"input_schema": {
"type": "object",
"properties": {
"owner": { "type": "string", "description": "Repository owner or organisation." },
"repo": { "type": "string", "description": "Repository name." },
"per_page": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
},
"required": ["owner", "repo"]
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true
},
"action": {
"type": "http",
"method": "GET",
"url": "https://api.github.com/repos/{owner}/{repo}/pulls?state=closed&sort=updated&direction=desc&per_page={per_page}",
"headers": {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28"
},
"auth_ref": "github_token"
}
},
{
"name": "get_pull_request",
"description": "Fetches the title, body, labels and merge state of a single pull request.",
"input_schema": {
"type": "object",
"properties": {
"owner": { "type": "string" },
"repo": { "type": "string" },
"pull_number": { "type": "integer" }
},
"required": ["owner", "repo", "pull_number"]
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true
},
"action": {
"type": "http",
"method": "GET",
"url": "https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}",
"headers": {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28"
},
"auth_ref": "github_token"
}
},
{
"name": "create_release_draft",
"description": "Creates an unpublished draft release on the repository with the supplied tag and notes.",
"input_schema": {
"type": "object",
"properties": {
"owner": { "type": "string" },
"repo": { "type": "string" },
"tag_name": { "type": "string", "description": "Tag for the release, e.g. v1.4.0." },
"release_name": { "type": "string" },
"body": { "type": "string", "description": "Markdown body of the release notes." }
},
"required": ["owner", "repo", "tag_name", "release_name", "body"]
},
"annotations": {
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true
},
"action": {
"type": "http",
"method": "POST",
"url": "https://api.github.com/repos/{owner}/{repo}/releases",
"headers": {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"Content-Type": "application/json"
},
"body_template": "{\"tag_name\":\"{tag_name}\",\"name\":\"{release_name}\",\"body\":\"{body}\",\"draft\":true,\"prerelease\":false}",
"auth_ref": "github_token"
}
}
],
"credential_slots": [
{
"ref": "github_token",
"label": "GitHub personal access token",
"description": "A fine-grained PAT with Contents: read and Pull requests: read on the target repository. Add Contents: write only if you want the agent to create draft releases.",
"type": "secret",
"auth_scheme": "bearer",
"env": "GITHUB_TOKEN",
"allowed_hosts": ["api.github.com"],
"required": true
}
],
"guardrails": {
"input": {
"max_length": 20000,
"pii_redaction": ["email"]
},
"actions": {
"list_merged_pulls": {
"approval": "none",
"rate_limit": "60/hour",
"idempotent": true
},
"get_pull_request": {
"approval": "none",
"rate_limit": "120/hour",
"idempotent": true
},
"create_release_draft": {
"approval": "human",
"rate_limit": "10/hour",
"idempotent": false
}
}
},
"example_prompts": [
"Draft release notes for acme/checkout covering everything merged since v2.3.0.",
"Summarise the last 20 merged PRs in acme/checkout, grouped by feature, fix and internal.",
"What changed in PR 1482 in acme/checkout, and is it user-visible?",
"Draft v2.4.0 notes for acme/checkout and open them as a draft release once I approve the text."
],
"delivery": "mcp.remote",
"exec": "findagent-hosted",
"targets": ["claude-desktop", "claude-code", "chatgpt", "cursor", "vscode", "cli"],
"auth": "oauth-device"
}Read the guardrail block against the annotations and the shape falls out: the two read tools are
approval: "none" and idempotent: true because repeating them is free; create_release_draft
carries approval: "human" and idempotent: false because a retried POST creates a second draft
release. secret_leak_scan doesn't appear anywhere — it's mandatory, always on, and writing it is
how you fail.
targets includes chatgpt, which forces delivery: mcp.remote: ChatGPT has no local-doer path.
A doer that wants ChatGPT is hosted or it is a recipe.
Everything in this table fails closed. Static scan runs first, then human review — "it might slide" is never the right bet.
| Symptom | Cause | Fix |
|---|---|---|
| Slot rejected at validation | allowed_hosts missing |
Bind every slot to an audience. There is no default. |
auth_ref resolves but the scan fails |
The slot's allowed_hosts doesn't cover the host in that action's URL |
Match the audience to the destination host, per action |
| Placeholder never binds; call 404s | {param} in the URL isn't declared in input_schema |
Declare every placeholder as a property |
Validation error on secret_leak_scan |
The field was written, usually as false |
Delete the field |
kind: autonomous-agent silently becomes mcp-tool |
autonomous-agent is a reserved roadmap kind with no runtime of its own |
Pick a real shape — it's coerced to mcp-tool at submission, never shipped as-is |
| Client runs a write tool without asking | readOnlyHint: true on a writing tool |
Annotate what the tool actually does |
| Doer targets ChatGPT but won't connect | delivery: mcp.stdio with chatgpt in targets |
Use mcp.remote, or drop to a recipe |
| Code bundle offered as a local install | mcp.stdio / exec: user-local on a code-bundle |
Code bundles are connect-only, always hosted |
| 401 on every call despite a valid key | auth_scheme mismatch — usually basic with a key that wasn't pre-encoded |
basic prepends only Basic ; the buyer must supply base64 |
| Orphaned slot warning | A slot no action references, or an auth_ref pointing at nothing |
Every slot used, every ref resolvable |
Run this before you submit. It mirrors the gate.
name 3–80 chars; system_prompt 50–20,000 charsexample_prompts showing genuine usetagline ≤140; additional_category_slugs ≤8; tags ≤16; llms from the allowed six{param} in a URL is defined in input_schemaannotations match real behaviour — nothing that writes is marked read-onlyhttpsauth_ref resolves to a slot whose allowed_hosts covers that action's hostallowed_hosts; no orphaned slotsurl, headers, body_template, env or any listing fieldsecret_leak_scan not written at allapproval: "human" on destructive or spending tools; rate_limit on every external callkind / delivery / exec / targets mutually consistentWhatever the manifest already carries prefills the submit wizard's five steps — Basics → Manifest → Pricing → Install → Review. The slug you choose at Basics is permanent, so choose it before you're attached to a name.
What is an MCP agent manifest? A declarative JSON document that describes an agent: its tools, the actions those tools bind to, the credentials it requires, and the guardrails the gateway enforces. It contains no executable code — the shared runtime performs every action from the declaration.
Do I have to write a manifest to publish an agent? Only for prompt-only recipes and declarative doers. Code agents are generated from your GitHub repo, skills bundles are built from your rules repo, a third-party hosted MCP server is a directory listing, and a team of existing agents is a Department.
Why does every credential slot need allowed_hosts? A credential with no declared audience can be attached to any outbound request, which is a cross-host exfiltration risk. The runtime attaches a credential only when the destination host matches the slot's list, so a slot without one has no safe behaviour to fall back to and the static scan rejects it.
Can I turn off the secret leak scan for my own agent?
No. secret_leak_scan runs on every output unconditionally and a manifest that sets it false
fails validation. Don't write the field at all.
What's the difference between llms and targets?
llms is a listing signal — which clients the marketplace shows the agent as supporting. targets
is the runtime client list the agent actually installs into. Related, not the same field, and
unknown llms values are silently dropped.
Why won't my local doer work in ChatGPT?
ChatGPT has no local-doer path. If targets includes chatgpt and the agent has real tools,
delivery must be mcp.remote with exec: findagent-hosted. Otherwise it can only ship as a
prompt recipe.
Related: Audience-bound credentials: the allowed_hosts pattern · Eight reasons your no-code agent has to become a code agent · Connect vs install across MCP clients