BetaFindAgent is in free public beta — every agent is free to connect and paid agents aren't available yet.
A declarative agent — kind: mcp-tool — describes tools, binds each to a fixed HTTP action, and
lets the runtime attach credentials per destination host. No code ships, so there is no
remote-code-execution surface to review. That model covers a surprising share of the REST APIs
people actually want to wrap.
It does not cover all of them. Some APIs have properties that a declaration structurally cannot express — not "would be awkward to express", but cannot. When you hit one, the answer is not a cleverer manifest. The answer is a code bundle.
This post is the diagnostic. Seven live disqualifiers, two we've since removed, and a one-minute test for each.
Before reading the list, run this against the API docs you're staring at.
flowchart TD
Start["Read the auth section of the API docs"] --> Q1{"Where does the<br/>credential go?"}
Q1 -->|"Request body"| D1["❌ Disqualifier 1"]
Q1 -->|"Query string"| D2["❌ Disqualifier 2"]
Q1 -->|"A header"| Q2{"How many secrets<br/>in one request?"}
Q2 -->|"Two or more"| D3["❌ Disqualifier 3"]
Q2 -->|"One"| Q3{"Is the header value<br/>ready to send?"}
Q3 -->|"Needs a token exchange first"| D4["❌ Disqualifier 4"]
Q3 -->|"Needs a computed signature"| D5["❌ Disqualifier 5"]
Q3 -->|"Send as-is"| Q4{"What comes back?"}
Q4 -->|"Binary, or a redirect to follow"| D6["❌ Disqualifier 6"]
Q4 -->|"JSON"| Q5{"Is the data behind<br/>many pages?"}
Q5 -->|"Cursor-chasing, unbounded"| D7["❌ Disqualifier 7"]
Q5 -->|"One page, or a bounded few"| OK["✅ Ships as a declarative doer"]Anything landing on a red box goes to the GitHub submit flow as a code bundle. Everything else is a manifest you can hand-write today.
{ "api_key": "sk_live_…", "monitors": "1", "format": "json" }Plenty of older APIs — uptime monitors, SMS gateways, a lot of self-serve SaaS from the 2010s — take the key as a form or JSON body field.
Why it's structural. The runtime attaches credentials to the request, by matching the
destination host, and it attaches them as headers. It does not template secrets into a
body_template. If it did, the secret would have to be a literal in the manifest, and a literal key
in body_template is exactly what the static scan rejects. There is no version of this that passes.
Diagnose in ten seconds: search the API docs for your key parameter name. If it appears inside a JSON or form example rather than in a header example, stop.
GET https://api.vendor.com/v1/reports?apikey=sk_live_…&range=30d
Same root cause, worse blast radius: query strings land in server logs, proxy logs, browser history
and Referer headers. Putting the key in action.url would mean writing the literal into the
manifest — a secret in a URL, rejected by the scan.
Note the exception that isn't one. A non-secret identifier in the query string is fine — a property ID, an account slug, a region. Only the secret is the problem.
Authorization: Bearer <access-token>
X-Account-Signature: <account-secret>
An action resolves exactly one auth_ref. Two secrets in a single call means two slots, and no way
to attach both.
This is the one people argue with, usually by suggesting the second secret is "not really
secret". If it isn't secret, put it in headers as a literal and you're fine — you're now back to
one credential. If it is secret, you're disqualified.
POST /oauth/token (client_id + client_secret) → access_token, expires_in: 3600
GET /v1/data (Authorization: Bearer <access_token>)
Client-credentials OAuth, AWS STS, any "exchange your long-lived key for a short-lived token" pattern. The declared action is a single HTTP call, and there is no place to express do this first, keep the result, watch its expiry, refresh on 401. That's a state machine, and a state machine is code.
Not a disqualifier: an API that takes a long-lived personal access token directly. Only the exchange step disqualifies.
signature = HMAC-SHA256(secret, timestamp + method + path + body)
AWS SigV4, most crypto exchange APIs, some payment providers. The header value depends on the request's own contents, computed at call time with a hashing function. A declared action has no compute step — it binds parameters into a template and sends it.
Anything with the words canonical request, string to sign, or signature version in the docs is this case.
Two variants, same conclusion:
302 to a signed, short-lived download URL on a different
host. The runtime deliberately does not follow redirects blindly, because doing so is how
credentials get re-sent to hosts that never passed the audience check. That protection is exactly
what makes this flow undeliverable declaratively.Not a disqualifier: a JSON response that contains a URL. Returning a link the user clicks is fine. Fetching it is not.
GET /v1/orders?cursor=abc → { data: [...100], next_cursor: "def" }
GET /v1/orders?cursor=def → { data: [...100], next_cursor: "ghi" }
… 400 more times
One action is one request. Chasing a cursor means a loop, a stopping condition, and accumulation across calls — code, again.
The nuance worth knowing: shallow pagination is fine. If the useful answer lives in the first
page or two, declare per_page as an input and let the model request the next page as a second tool
call. It's when correctness requires exhausting the pages — totals, reconciliations, full exports
— that you're disqualified. A summarising agent that reads the top 50 records is honest and useful;
one that claims a total from page one is wrong.
Both of these used to be on the list. They aren't limitations of the declarative model — they were gaps in ours, and we closed them.
Some APIs don't use Authorization at all: X-API-Key, apikey, X-Auth-Token. Originally that
was a disqualifier, because the runtime only knew how to build an Authorization header.
Now auth_scheme: "header" with a companion header_name field sends the bare key in any header
you name:
{
"ref": "vendor_key",
"label": "Vendor API key",
"type": "secret",
"auth_scheme": "header",
"header_name": "X-API-Key",
"allowed_hosts": ["api.vendor.com"],
"required": true
}The full scheme table, for reference:
auth_scheme |
Runtime sends | Buyer supplies |
|---|---|---|
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 |
basic is the one that catches people: the runtime prepends only Basic , so the buyer has to
paste base64 themselves. Say so in the slot's description.
An agent for a self-hosted tool — GitLab CE, a private Grafana, an on-prem ticketing system — has no
host at authoring time. Every buyer runs it somewhere different. Since allowed_hosts is mandatory
and an action URL is fixed, that used to be undeliverable.
Now the host is bound at install time: the buyer supplies their own instance host, the action URL and the credential's audience are pinned to it, and the audience-binding guarantee holds unchanged. You author the agent once; each installation is scoped to exactly one instance.
It isn't a lesser tier. It's a different — and in some ways heavier — path:
| Declarative doer | Code bundle | |
|---|---|---|
| Who writes the manifest | You | Generated from your repo |
| What's inspected | The declaration | The declaration and a static scan of the code |
| Where it runs | Shared runtime | Isolated, ephemeral per-run sandbox |
| Buyer's machine | Local install possible | Never — connect-only, always hosted |
| Network | Credential bound per host | Default-deny egress allowlist, auto-detected from the repo |
| Auth complexity | One header, no compute | Anything you can write |
The import reads your DXT manifest.json, MCP config, .env.example keys, package.json and
README, and derives the tools, credential slots and egress allowlist. You never write a
findagent.json by hand — and you shouldn't, since it's regenerated on every version bump.
So the honest framing: a disqualifier means this API needs computation between requests. Code bundles exist for exactly that, and the sandbox is the reason it's safe to allow.
| # | Trait | Tell in the docs | Status |
|---|---|---|---|
| 1 | Credential in the request body | Key appears in a JSON/form example | ❌ Live |
| 2 | Credential in the query string | ?api_key= in the endpoint examples |
❌ Live |
| 3 | Two secrets in one request | Two headers, both from your account page | ❌ Live |
| 4 | Token exchange required | A /oauth/token or /sts step before the call |
❌ Live |
| 5 | Signed requests | "canonical request", "string to sign", HMAC | ❌ Live |
| 6 | Binary or redirect response | Content-Type: application/pdf, 302 to a CDN |
❌ Live |
| 7 | Unbounded pagination | next_cursor and correctness needs all pages |
❌ Live |
| 8 | Custom auth header | X-API-Key instead of Authorization |
✅ Fixed — auth_scheme: header |
| 9 | Self-hosted, unknown host | "your instance URL" in the docs | ✅ Fixed — install-time host binding |
What is a structural disqualifier? A property of the target API that a declarative manifest cannot express at all — a secret in the request body, a computed signature, a token exchange. It's a property of the API rather than a flaw in your manifest, so rewriting the manifest never resolves it.
Why can't a manifest put an API key in the request body?
The runtime attaches credentials to requests as headers, matched to the destination host. Templating
a secret into a body would require writing the literal value into the manifest, and a literal
secret in body_template is rejected by the static scan.
Can a declarative agent handle OAuth? It can use a long-lived token directly. It cannot perform a client-credentials exchange, because that requires making one request, storing the result, tracking its expiry and refreshing on failure — a state machine, which is code.
Is pagination always a disqualifier? No. Declare a page or cursor parameter as tool input and let the model request additional pages as separate tool calls. It's only a disqualifier when correctness requires exhausting every page, such as computing a total or running a full export.
Are custom auth headers still a problem?
No. auth_scheme: "header" with a header_name field sends the bare key in any header you name,
so X-API-Key and similar schemes are fully supported.
How do I ship an agent for a self-hosted tool? Author it once and let the buyer bind their instance host at install time. The action URL and the credential's audience are pinned to that host, so each installation is scoped to exactly one instance.
Is a code bundle worse than a declarative agent? Different, not worse. It's generated from your repo rather than hand-written, it runs in an isolated ephemeral sandbox instead of the shared runtime, it's connect-only rather than locally installable, and its network access is a default-deny allowlist derived from the repo.
Related: The MCP Agent Manifest Cookbook · Audience-bound credentials · From GitHub repo to hosted code agent
A manifest describes an agent; it never ships code. Here is the full anatomy, a working end-to-end example, and the checks the review pipeline runs before your listing goes live.
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.