Agents
One HTTP endpoint that gives an AI agent the whole catalogue as tools: search an app, read its record, list the builds we hold, check what a build actually is — checksum, scan verdict, signing key, permissions — and mint a download URL. Authorized with OAuth, and billed to the same credit balance as the REST API.
Endpointhttps://apkcube.com/api/mcp
No key, and nothing to paste. The URL is the whole configuration: the first call comes back with a challenge naming this server's authorization server, your client registers itself, and it opens a browser so you can sign in and approve it. After that it holds a token of its own and refreshes it without asking again.
What you approve is charged to your credits, so the approval screen names the app and says what it will be able to do. You can withdraw it at any time from your account, which cuts the client off on its next call.
claude mcp add --transport http apkcube https://apkcube.com/api/mcp{
"mcpServers": {
"apkcube": {
"type": "http",
"url": "https://apkcube.com/api/mcp"
}
}
}20 tools, each one a call into the same handler its REST twin uses — so a takedown, a price or a response shape can never differ between the two surfaces. Every tool declares an outputSchema and returns both structured content and a text rendering of it.
| Tool | Credits | What it does |
|---|---|---|
| search_apps | 1 | Search the APK catalogue by name, publisher or package id. Start here when you have a phrase and need a package name. Returns 24 results a page out of at most 60 matches, so paging stops at page 3 — narrow the phrase rather than paging for more. Searching an exact package id works as a lookup. |
| get_app | 5 | Full record for one package: description, screenshots, rating, tags, and whether a download would be refused. Does NOT list downloadable files — use list_versions for those. |
| list_versions | 5 | Every APK/XAPK build held for a package, newest first, each with its SHA-256. Call this before get_download_url — the apkId it returns is what that tool takes. An empty list means we hold nothing for this package; it is a complete answer, not an error. A package we will not serve at all is refused here with the same reason a download would give, so you learn it without spending a download. |
| get_download_url | 10 | Mint a short-lived signed URL for one build. Costs 10 credits — the most expensive call here, and the only one that hands over a file. Get the apkId from list_versions first, and check get_version_security before handing anything to a person. The URL expires in about five minutes; fetch it promptly and do not store it. The response carries the file's `sha256` — verify what you downloaded against it, because the signature is on the URL and not on the bytes. |
| list_developer_apps | 1 | Every app we hold from one publisher, most-installed first. The slug comes from `developerSlug` on any app row. |
| list_categories | free | The category taxonomy, for filtering a search. Free — this call costs no credits. |
| list_chart | 1 | A ranked section of the catalogue: what is hot, top-grossing, recently updated, newly released, or carries Play's Editors' Choice badge. An unrecognised section falls back to 'hot' rather than failing. |
| get_version_security | 5 | What one build actually IS: its SHA-256, the anti-malware verdict, the certificates that signed it, and every Android permission it declares with a plain-English gloss. Defaults to the newest build, so you do not need list_versions first. `findings` is the part to read first: it applies our rules to the manifest and native facts on the same response and names what they imply — a debuggable build, a version that disagrees with the listing, cleartext traffic, components any app on the device can start, a packer. Two severities, `warning` and `notice`, and NEITHER is a verdict; an empty `findings` is not an all-clear, because a manifest nobody read produces no findings either. Read this BEFORE recommending an install, and read the nulls carefully — `scanned: false` and `virusTotal: null` mean nobody has looked, which is not the same as clean, and `permissions: null` means the manifest was never read, which is not the same as an app that asks for nothing. |
| list_sdks | 5 | What OTHER companies' code is inside an app: the ad, analytics, attribution, crash-reporting, push and sign-in libraries matched in the build, each with its vendor and where it was recognised. Defaults to the newest build. This is the privacy question, not the safety one — these libraries are ordinary parts of shipping an app, so report them as what the app carries and never as evidence of harm. READ THE BOUNDS BEFORE THE LIST: `analyzed: false` means nobody looked; `codeCoverage` says whether the app's own code was read at all, and in full; a non-empty `protectors` means the build is packed and its real code was never open to inspection. An empty `sdks` is only close to 'none' when `analyzed` is true and `codeCoverage` is `full`. Relay `summary` whole — it states the finding and its bound in one sentence, and dropping the second half is the standard way to get this wrong. |
| get_protection | 5 | Whether this build is wrapped in a commercial protector — DexProtector, Jiagu, Bangcle, Legu, AppSealing, Google's own PairIP — and what that means for everything else you can find out about it. Defaults to the newest build. A PROTECTOR IS NOT A MALWARE SIGNAL: anti-piracy, anti-tamper and anti-cheat are what these are sold for, and saying an app is 'suspicious' because it is packed is wrong about a large part of the catalogue. What it does mean is that the app's real code is not in classes.dex for anyone to read, so an empty SDK list, a short permission list or any other analysis of the same artifact is a floor and not a census — call this BEFORE you report that a build carries no trackers. `analyzed: false` means the archive was never opened, which is not the same as an unpacked build. Relay `summary` whole: it carries the finding and the bound in one sentence. |
| list_trackers | 5 | The privacy question on its own: which advertising, analytics, install-attribution, crash-reporting, push and social sign-in SDKs are in this build, and nothing else. `list_sdks` is the same evidence unfiltered — every recognised library including HTTP clients and UI toolkits — so use that one when you want the full inventory and this one when somebody asked what an app sends about them. A CATEGORY IS NOT AN ACCUSATION: all six are ordinary parts of shipping an app, and `unclassified` counts recognised libraries that are neither (unclassified means we take no position, never that a library is harmless). READ THE BOUNDS BEFORE THE LIST: `analyzed: false` means nobody looked; `codeCoverage` says whether the app's own code was read at all and in full; a non-empty `protectors` means the real code was never readable, and an empty list under one says nothing about the app. Relay `summary` whole — 'no trackers found' without its second clause is the standard way to get this wrong. |
| list_permissions | 5 | Every Android permission this build declares, sensitive ones first, each with a readable label, its capability family and a sentence saying what the app can actually do with it. Defaults to the newest build. This is the same labelling compare_versions diffs, so the two cannot name the same permission differently. THE NULL IS THE PART TO READ: `permissions: null` means the manifest was never read and is NOT a claim that the app asks for nothing — an empty array is that claim. `sensitive` is the count worth quoting; a long list of ordinary permissions is what almost every app looks like. `defines` is separate and often misread: those are permissions this app declares for OTHER apps to hold, not things it is asking for. |
| get_manifest | 5 | What the artifact's own AndroidManifest.xml says beyond the permissions it requests — targetSdk, compileSdk, debuggable, cleartext traffic, backup, sharedUserId, install location, uses-feature, uses-library, and the components it exports without a permission guard — plus the native libraries it ships, and our rules read off the pair as `findings`. Defaults to the newest build. A KEY ABSENT INSIDE `manifest` means the manifest is silent about that attribute, never that it declares the platform default; `manifest: null` means nobody read it at all, and an empty `findings` beside that is not an all-clear. `versionMismatch` is the one to escalate: non-null means the file's own version disagrees with what the listing claims, which is what a swapped or relabelled artifact looks like. Use get_raw_scan if you want the stored record without our rules applied. |
| get_raw_scan | 10 | The whole stored analysis record for one build, exactly as stored and with none of our rules applied — for when you want to judge the facts yourself rather than read our conclusions. It carries two things no other tool publishes: the EVIDENCE DIGEST every SDK match is drawn from (dex package prefixes, manifest component names, intent actions, how much code was read), so you can match it against your own corpus; and each signing certificate's serial and signature algorithm. Also the raw VirusTotal counters and every flagging vendor. Prefer the narrower tools when you want an answer — get_version_security, get_protection, list_trackers, list_permissions, get_manifest — and reach for this when you want the inputs. A build nobody has scanned is `scanned: false`, which is not a clean result. |
| lookup_hash | 5 | Which app and which build a file checksum names. Use this when you have a hash and no package name — an APK pulled off a device, a sample from a threat feed, a digest in an incident report. Takes an MD5 (32 hex), SHA-1 (40) or SHA-256 (64) and works out which from the length, so do not try to label it; colons, spaces and upper case are fine and are normalised. `found: false` is a real answer, not an error: it means we hold no build with that checksum, and says nothing about whether the file exists elsewhere. READ `note`: only SHA-256 is carried by every stored build, so a SHA-1 or MD5 miss can mean we hold the file and never computed that digest for it — re-check with the SHA-256 if you have it. Once you have the packageName and apkId, the per-build tools will tell you what the file is. |
| lookup_certificate | 5 | Every package we hold that one signing certificate is on record for. Android only installs an update signed by the key that signed what is already on the device, which makes the signing key the closest thing an app has to an identity across package names — so a key you found on one repackaged app is worth asking about, because it has usually signed others. Takes the SHA-256 fingerprint of the DER certificate (64 hex, colons and case fine). A SHA-1 IS REFUSED, free, with the reason: the record stores one fingerprint per certificate and a SHA-1 cannot be resolved to it — get the SHA-256 (keytool prints both). BOUNDED, and `note` says how: it covers packages we have ingested and scanned, never every app the key has signed, so an empty result means we hold nothing signed by it and not that it signs nothing. `blocked: true` means the key is on our catalogue-wide blocklist and nothing it signed can be downloaded here. Taken-down packages are counted in `blockedCount` and not listed, so a page may be shorter than `perPage`. |
| get_signing_history | 5 | Which certificate signed which releases of a package, and where the key moved. Android will only install an update signed by the key that signed what is already on the device, so a `hasKeyReplacement: true` means the newer build is not an update of the older one — it is a different app under the same package name, which is what a repackaged app looks like. `unreadVersions` lists releases whose signature we never read; those are gaps, not agreement. |
| compare_versions | 1 | What changed between two releases: permissions gained and lost, whether the signing key moved, size and minimum Android. Name either side by version name or by version code — the code is tried first, because a version name is free text the developer sets and is often a placeholder. Order does not matter; the higher build number is treated as the newer. Check `permissionsKnown` before reading two empty permission lists as 'nothing changed'. |
| get_apps | 1 | Summary rows for up to 25 package ids in one call — use this instead of calling get_app in a loop. Costs 1 credit PER PACKAGE, so twenty ids cost twenty credits however they arrive. Ids we do not hold come back in `notFound` rather than failing the call. Returns the SUMMARY row, not the full record: it is cheaper than get_app (5) because it answers less, so reach for get_app only when you need the description or the screenshots for a particular one. |
| get_account | free | How many credits are left on the key you are using, and what that key is. Free. Every successful call also reports this in `_meta`, so reach for this one when you need to know before you spend — a download costs 10 — or when your calls have been failing and you have not seen a balance in a while. |
Spending is reported as it happens: every successful call carries the cost and the balance left in the result's _meta under com.apkcube/credits, and running out comes back as a tool error naming the price and where to top up — not as a transport failure the model will read as a broken server and retry. That running figure rides a successful result, though, so an agent whose calls are all failing never sees one. get_account answers the balance on its own, for free, which is what to reach for before a 10-credit mint. It reports the key and the balance and nothing else — no name, no email, no account id, none of which answers “may I spend this”. GET /me returns the same shape.
5 prompts, served over prompts/list and prompts/get. Most clients surface them as slash commands. They are free — a prompt is metadata, like the tool list.
They exist because a tool description is only ever read on its own, and some things are true of the sequence rather than of any one call: that you check the signing key before you tell someone to install something, that a missing scan is not a clean scan, that you pick a build by versionCode and not by its name. That is what these write down.
| Prompt | Arguments | What it does |
|---|---|---|
| vet_app | packageName | Work out whether a package is safe to install, and say why — listing, scan verdict, permissions and signing key. |
| find_and_download | query, requirements? | Search for an app by name, pick the right build, and mint a download URL — with the checks that stop you paying for the wrong one. |
| whats_changed | packageName, from?, to? | Diff two builds of a package and report what actually matters — permissions gained, and whether the signing key moved. |
| audit_sdks | packageName | List the other companies' code inside an app — advertising, analytics, attribution, crash reporting — and say honestly how far we could see. |
| trace_artifact | artifact | Start from a checksum or a signing-key fingerprint and find out what it is, who signed it, and what else that key signs. |
The tools compose, so plenty of useful requests are just a sentence:
Streamable HTTP, stateless. The server implements revision 2026-07-28: every request carries its protocol version, client identity and capabilities as per-request metadata, and there is no handshake and no session.
| Transport | Streamable HTTP. POST only — GET and DELETE return 405. |
| Version | 2026-07-28. Anything else is a 400 + -32022. |
| Response | A single application/json object. Nothing here streams, so no SSE. |
| Sessions | None. Mcp-Session-Id is not accepted and never minted. |
| Methods | server/discover, tools/list, tools/call, prompts/list, prompts/get, ping. Anything else is a 404 + -32601. |
| Discovery | server/discover, tools/list and prompts/list are publicly cacheable for 5 minutes — none varies by caller. |
Errors follow the specification's split, which matters for how an agent recovers: an unknown tool or a malformed envelope is a JSON-RPC protocol error, while “no such package”, “this app is taken down” and “you are out of credit” come back as ordinary results with isError: true and a sentence the model can act on.
OAuth 2.1, which is what the specification's HTTP profile asks for. This site is both the authorization server and the resource server, so there is no third party in the flow and no account anywhere else to make.
| Protected resource | https://apkcube.com/api/mcp |
| Resource metadata | https://apkcube.com/.well-known/oauth-protected-resource/api/mcp |
| Issuer | https://apkcube.com |
| Server metadata | https://apkcube.com/.well-known/oauth-authorization-server |
| Registration | Dynamic (RFC 7591), open, rate limited |
| Flow | Authorization code with PKCE (S256). Refresh tokens issued. |
| Token binding | Every token is audience-bound to the resource above, so one minted elsewhere is refused here. |
Two scopes, and the split is the one worth having a consent screen for. Reading the catalogue costs between nothing and 10 credits a call; minting a download costs 10 and records a download against the account. A client that only answers questions about apps can be connected without ever holding the ability to spend.
| Scope | Grants |
|---|---|
| catalog:read | Every tool but the mint — 19 of 20. |
| catalog:download | get_download_url, the one tool that spends and records a download. |
| offline_access | A refresh token, so the client is not sent back through the browser every hour. |
Calling a tool with a token that lacks its scope is a 403 carrying an RFC 6750 insufficient_scope challenge naming what is missing — a client can step up in one round trip rather than treating it as a dead end.
One deviation worth knowing, unchanged by any of this: requests carrying an Origin header are accepted only from this site's own origin, as the transport specification requires for DNS-rebinding protection. Non-browser clients send no Origin and are unaffected; a browser-hosted MCP client on another origin must proxy through its own backend.