anyapi-mcp is one local MCP server that lets your model talk to almost any HTTP API. Add it once to Claude Code or Claude Desktop, then point it at any API you find (an OpenAPI spec, a GraphQL endpoint, a SOAP/WSDL service, or an AT Protocol/XRPC service like Bluesky) and your model can search and call it right away. No separate server or custom integration per API: one server covers everything you register, and it stays token-efficient however many calls a task takes.
Under the hood it generates a typed client from the API's own description and
exposes a small set of tools: search to find operations, execute to run a
short TypeScript program against that client, and add_api/list_apis/
remove_api to manage what's registered. Instead of one MCP tool per endpoint
clogging the context window, the model writes code and runs it in a locked-down
sandbox; intermediate results stay in that subprocess rather than round-tripping
through the model, so a ten-step workflow costs one tool call and a handful of
tokens, not ten.
Because that client is fully typed, the model drives the API without guesswork: a wrong argument is a type error, caught before any request leaves the machine.
Point an MCP client at anyapi-mcp serve with nothing registered yet and
the server explains what it is and how to register one, so an empty server
self-onboards instead of dead-ending. APIs added mid-session work immediately,
no restart.
anyapi-mcp add https://example.com/openapi.json --docs https://example.com/docs --token
anyapi-mcp list
anyapi-mcp serve # stdio MCP server, for Claude Desktop / Claude Code- Deno 2.x (tested on 2.8).
- macOS or Linux.
- macOS: tokens are stored in the login keychain via
security. - Linux: tokens are stored via
secret-tool- installlibsecret-tools(e.g.apt-get install libsecret-tools).
- macOS: tokens are stored in the login keychain via
executeruns the model's code in adenosubprocess, sodenomust be onPATH- including when you run the compiled binary (which is otherwise self-contained).
The quickest way - builds from source, so you'll need Deno 2.x (it's a runtime dependency too; see Requirements):
curl -fsSL https://gabrielbauman.github.io/anyapi-mcp/install.sh | shIt fetches the source, runs deno task compile, drops an anyapi-mcp binary in
~/.local/bin (override the location with ANYAPI_MCP_BIN_DIR), and registers
it with Claude Code, Claude Desktop, and OpenCode so it's ready to use (skip
that last step with ANYAPI_MCP_NO_INSTALL=1).
Or build it yourself:
deno task compile # produces a self-contained ./anyapi-mcp binary
mv ./anyapi-mcp ~/.local/bin/ # or anywhere on your PATH
anyapi-mcp install # register it with Claude Code, Claude Desktop, and OpenCodeOr run from source during development:
deno task dev list # = deno run -A src/main.ts listInspects the source (an OpenAPI spec, a GraphQL endpoint, a WSDL, or an atproto PDS/service URL), derives the base URL and host, builds a searchable operation index, generates a typed client, and writes a registry entry.
| Option | Description |
|---|---|
--kind <openapi|graphql|soap|atproto> |
Protocol (default: openapi). graphql introspects an endpoint; soap reads a WSDL URL; atproto takes a PDS/service URL (e.g. https://bsky.social). |
--id <slug> |
Id used on the CLI and in execute (default: the base URL in reverse-DNS form, e.g. com.github.api). |
--name <name> |
Human-friendly name (default: spec info.title). |
--base-url <url> |
Override the base URL (otherwise derived from the spec's document-, path-, or operation-level servers). |
--docs <url> |
Documentation URL to store and surface (not parsed). |
--token |
Store a bearer token. Read without echo from a TTY, or piped via stdin. |
--identifier <handle> |
atproto: the account handle/email to authenticate as (not a secret). |
--app-password |
atproto: store an app password now. Read without echo from a TTY, or piped via stdin. |
--oauth |
Treat the API as OAuth 2.0 even if the spec doesn't declare it. |
--auth-url / --token-url |
OAuth authorize / token endpoints (override the spec's values). |
--scope <name> |
Scope to request at login (repeatable; default: the spec's scopes). |
--scope-separator <sep> |
Scope separator in the authorize URL (default " "; Strava uses ","). |
--no-auth |
Register without authentication. |
--force |
Overwrite an existing API with the same id instead of failing (e.g. to fix a wrong base URL). |
If the base URL can't be derived and would fall back to a raw spec-hosting host
(e.g. raw.githubusercontent.com), add fails loudly instead of registering a
broken base — pass --base-url with the real API origin.
OpenAPI specs that declare an OAuth 2.0 authorization-code flow are detected
automatically: the API is registered as oauth2, and you run
login once to authenticate (see OAuth).
# bearer auth, prompted without echo:
anyapi-mcp add https://api.github.com/openapi.json --token
# bearer auth, piped (CI):
echo "$GITHUB_TOKEN" | anyapi-mcp add <spec-url> --token
# no auth - id defaults to the reverse-DNS base URL (here: io.swagger.petstore3.api.v3):
anyapi-mcp add https://petstore3.swagger.io/api/v3/openapi.json
# OAuth is auto-detected from the spec; just add, then `login`:
anyapi-mcp add https://developers.strava.com/swagger/swagger.json --id com.strava.api
# graphql - introspect an endpoint (id: com.trevorblades.countries):
anyapi-mcp add https://countries.trevorblades.com/ --kind graphql
# soap - read a public WSDL pointing at a live service:
anyapi-mcp add "http://www.dneonline.com/calculator.asmx?WSDL" --kind soap
# atproto - anonymous public reads (no login), via the public AppView:
anyapi-mcp add https://public.api.bsky.app --kind atproto --id bsky-public
# atproto - register your account to post / read private data (PDS + login):
anyapi-mcp add https://bsky.social --kind atproto --id bsky --identifier you.bsky.social
anyapi-mcp login bsky # prompts for an app password (no echo)Lists registered APIs with id, name, base URL, operation count, and auth kind.
For OAuth APIs it also shows live login status and token expiry (e.g.
oauth2 (logged in, expires in 5h) or oauth2 (not logged in)).
Rebuilds the generated client code (typed .d.ts/client module + operation
index) for registered APIs by re-fetching each one's saved source. With no id it
does all of them; pass ids to target specific APIs, or --stale-only to rebuild
just those whose code predates the current build.
This only refreshes generated code: saved credentials and OAuth config are
left untouched (bearer tokens, OAuth client credentials, and login state all
survive; baseUrl and the host allowlist are preserved too). Run it after
upgrading anyapi-mcp so cached code matches the new generators - though
serve also does this automatically (see below), so you rarely need to run it
by hand. Each API is independent: if one source can't be re-fetched, the rest
still regenerate and that API keeps its existing (working) code.
Authenticates an OAuth 2.0 API in the browser (see OAuth). Stores
the OAuth app credentials in the keystore (the client secret is read without
echo, like add --token), opens the provider's consent page, captures the
redirect on a local one-shot callback server, and saves the resulting tokens
(which then refresh automatically). Re-running it re-authenticates.
| Option | Description |
|---|---|
--client-id <id> |
OAuth app client id (required on first login). |
--client-secret <s> |
Client secret (omit to be prompted without echo; or pipe via stdin). |
--scope <name> |
Scope to request (repeatable; default: the API's configured scopes). |
--scope-separator <sep> |
Scope separator in the authorize URL (default " "; Strava uses ","). |
--redirect-uri <url> |
Local callback URL to listen on (default http://localhost:9876/callback). |
--port <n> |
Shortcut to set the callback port (host localhost, path /callback). |
--auth-url / --token-url |
Override the stored authorize / token endpoint. |
--no-browser |
Print the authorize URL instead of opening a browser (headless/SSH). |
Removes the stored OAuth tokens for an API (it stays registered). By default the
OAuth app credentials are kept so the next login needs no flags;
--forget-client removes those too.
Removes the registry entry, deletes stored secrets from the keystore (bearer token, or OAuth client credentials + tokens), and cleans up cached files.
Runs the stdio MCP server. It re-reads the registry on each call (so newly
registered APIs are picked up without a restart). On startup, if the build's
codegen version has changed since an API's code was generated (i.e. you upgraded
anyapi-mcp), it regenerates that API's code first - the same work as
regenerate, limited to stale entries, with credentials preserved and any
re-fetch failure logged but non-fatal. It exposes:
search-{ query, api? }→ compact operation matches (api,method,path,operationId,summary,params,requestBodyHint). Each param also carries adescriptionand its allowedenumvalues when the spec provides them, so the model can pick valid arguments without a failed call.execute-{ api, code, check?, timeoutMs? }→ runscodeagainst a typedclientand returns{ stdout, stderr, exitCode }verbatim. For OAuth APIs the access token is refreshed automatically first; if the API isn't authenticated, the result explains how to fix it (callauthenticate, or runanyapi-mcp login).check:falseskips type-checking for one run (use when a stale specenumrejects a value the live API still accepts);timeoutMsraises the 30s default (capped at 120s) for long or paginated runs.authenticate-{ api }→ opens the user's browser to (re-)authenticate an OAuth API and stores the tokens. Lets the model recover from an expired or revoked session without leaving the chat. It never accepts secrets — the user must have runanyapi-mcp loginonce to set up the OAuth app credentials.configure_oauth-{ api, scopes?, scopeSeparator?, extraAuthParams? }→ lets the model fix OAuth provider quirks it discovers (wrong scope set, comma-vs-space separator, an extra authorize param likeaccess_type=offline). It only touches safe request params — never the authorize/token endpoints (those carry the client secret and stay CLI-only), and reserved params likeredirect_uri/client_idare rejected. Call with just{ api }to read the current config.add_api-{ specUrl, kind?, id?, name?, baseUrl?, docsUrl?, force? }→ registers a new API (kindopenapi,graphql, orsoap) so the model can self-serve public APIs. Secrets are not accepted here; for authenticated APIs useanyapi-mcp add … --token(bearer) oranyapi-mcp login(OAuth) so the secret goes to the OS keychain, not the conversation.force:trueoverwrites an existing id in place (e.g. to correct a wrongbaseUrl) instead of failing; an OAuth API stays logged in across the overwrite.list_apis-{}→ the registered APIs as JSON (id, name, kind, baseUrl, operation count, auth/login status, docsUrl). Lets the model see what's available and confirm anadd_api/remove_apitook effect mid-session.remove_api-{ api }→ unregisters an API: removes its entry, deletes any stored secrets from the keychain, and cleans up its cached types and ops index. The model's own way to clean up a mistaken registration.
The server also sends MCP instructions at connect time describing the
workflow, and - when the registry is empty - exactly how to register an API.
Many user-facing APIs (Strava, Google, GitHub, …) use OAuth 2.0. anyapi-mcp supports the authorization-code flow end to end:
- Add the API. OpenAPI specs that declare an authorization-code flow are
detected automatically (
anyapi-mcp add <spec>registers it asoauth2). For sources that don't declare one, pass--oauth --auth-url … --token-url …. - Create an OAuth app with the provider and set its redirect/callback URL
to the one
add/loginprints (defaulthttp://localhost:9876/callback). You get a client id and client secret. - Log in once:
This opens your browser, you approve, and the tokens are stored in the OS keychain. The access token then refreshes automatically before each
anyapi-mcp login com.strava.api --client-id <id> --client-secret <secret> \ --scope read --scope activity:read_all
execute— you don't log in again until you revoke access.
From then on the model just calls search/execute as usual. If a session
can't be refreshed (e.g. you revoked the app), the model can call the
authenticate tool to re-open the browser login — no secrets pass through the
conversation, since the client credentials are already in the keychain.
A small built-in quirks table fixes well-known providers whose specs are wrong
or non-standard — e.g. Strava's spec lists /api/v3/oauth/* (the live endpoints
are /oauth/*) and Strava wants comma-separated scopes. For anything else, the
registry entry is the override (it's what every refresh reads):
anyapi-mcp login --auth-url/--token-url/--scope-separator/--scope writes the
corrected values onto it. The model can also fix the safe params it
discovers (scopes, scope separator, extra authorize params) with the
configure_oauth tool — but the authorize/token endpoints are CLI-only, since
tokenUrl is where the client secret is POSTed and shouldn't be agent-writable.
Notes & limits: only the authorization-code grant is supported (no PKCE,
implicit, client-credentials, or password grants). Built-in quirks seed defaults
at add time only, so a quirk discovered later won't retroactively rewrite an
already-registered API — re-add, login --auth-url …, or configure_oauth
(safe params) to apply it.
Wires anyapi-mcp into your local MCP clients so you don't edit configs by hand.
With no arguments it sets up whatever it finds: Claude Code (via
claude mcp add, user scope) and, on macOS, Claude Desktop (adds an
mcpServers.anyapi-mcp entry to its config, leaving existing servers untouched;
restart Desktop to load it). Run the installed binary so it registers its own
path. Options: --client code|desktop|all, --command <path>,
--scope user|project|local, --name <name>, --dry-run.
The quickest path is the install command above: anyapi-mcp install registers
anyapi-mcp with Claude Code and/or Claude Desktop for you. To wire it up by hand
instead:
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"anyapi-mcp": {
"command": "/absolute/path/to/anyapi-mcp",
"args": ["serve"]
}
}
}Claude Code:
claude mcp add anyapi-mcp -- /absolute/path/to/anyapi-mcp serveThen ask the model to, for example, "search the github API for listing a user's
repos, then fetch detail on the most recently updated one." It will search,
write a short TypeScript program, and execute it - typically a single
multi-step execute call rather than many tool round-trips.
For each call, anyapi-mcp writes a wrapper file shaped like:
import createClient from "npm:openapi-fetch@0.17.0";
import type { paths } from "file:///…/<id>.d.ts";
const token = Deno.env.get("ANYAPI_MCP_TOKEN");
const client = createClient<paths>({
baseUrl: "<baseUrl>",
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
await (async () => {
/* your code, with `client` in scope */
})();and runs it in a deno subprocess scoped with:
--allow-net=<api hosts only>- even buggy or hostile code can't reach anything but the registered API.--allow-env=ANYAPI_MCP_TOKEN- the only environment variable the code can read.- no
--allow-read/--allow-write/--allow-run. - a 30-second timeout by default (raise it per call with
execute'stimeoutMs, capped at 120s).
The token is injected into the subprocess environment only; the model never sees
it, and the net allowlist means it can't be exfiltrated. For OAuth APIs the
parent process refreshes the access token (in the keychain) before building
the harness, so the sandbox only ever receives a currently-valid token and never
touches the refresh token or token endpoint. Type-checking is on by default
(--check) so the model sees type errors and can self-correct - calling an
operation with the wrong arguments returns the exact expected shape. Passing
check:false to execute skips type-checking for that one run - useful when a
spec's enum is stale and rejects a value the live API still accepts (the model
trades type feedback for the call). It removes only --check; the net/env
sandbox above is unchanged.
Writing code with openapi-fetch: response is always present, so check
response.status; on success data is set, on an HTTP error error is set (no
throw). For operations whose spec declares no error schema, error is typed
never - narrow on data (or read response.status) rather than if (error).
For a GraphQL API the harness instead exposes
client.query(query, variables) and client.mutate(...) (a typed POST wrapper
returning { data, errors }) plus the introspected schema types as Schema.* -
annotate results like client.query<{ user: Schema.User }>(...).
For a SOAP API the harness exposes one method per operation -
client.<Operation>({ ...args }) with typed args - returning
{ status, data, raw } where data is the parsed SOAP Body. anyapi-mcp
builds the envelope and parses the response for you.
For an atproto (AT Protocol / lexicon / XRPC) API the harness exposes
client.query(nsid, params) and client.procedure(nsid, input). The NSID
string literal selects the official @atproto/api types (imported type-only, so
none of the SDK runs in the sandbox), so params, input, and the awaited result
are all typed -
const tl = await client.query("app.bsky.feed.getTimeline", { limit: 20 })
gives a typed tl.feed. Public reads work anonymously - register against the
public AppView (https://public.api.bsky.app) with no --identifier. Writes
and your own private data need a session: register against your PDS (e.g.
https://bsky.social) with --identifier, then run
login once to store an app password.
- Secrets live in the OS keystore (service
anyapi-mcp), never in the registry. The registry only records keystore account names (tokenKey; for OAuth alsoclientKey; for atprotopasswordKey/sessionKey). OAuth and atproto refresh/login run in the parent process — the execute sandbox only ever receives a ready access token/JWT, never the client secret, refresh token, app password, or refresh JWT. - Files: registry at
$XDG_CONFIG_HOME/anyapi-mcp/apis.jsonl(default~/.config/anyapi-mcp); generated.d.tsand operation indexes at$XDG_CACHE_HOME/anyapi-mcp(default~/.cache/anyapi-mcp). - Ids: the default id is the base URL in reverse-DNS form - the host
reversed plus the base-path segments (
https://api.github.com→com.github.api;https://petstore3.swagger.io/api/v3→io.swagger.petstore3.api.v3). Including the path keeps distinct APIs on one host (e.g./v1vs/v2) distinct. Pass--id(oradd_api'sid) to override. - stdout hygiene: in
serve, stdout carries only MCP frames; all logging goes to stderr. - Protocols & adapters: each protocol is an in-tree
ProtocolAdapter(src/adapter.ts) - aprepare()that turns a source into base URL + hosts + an operation index + generated types, and abuildHarness()that puts a typedclientin scope. OpenAPI, GraphQL, SOAP/WSDL, and AT Protocol/XRPC ship today; adding one is a new arm insrc/adapters.ts, not a plugin system. The registry,search, and the execute sandbox are protocol-agnostic. - SOAP scope: WSDL 1.1, SOAP 1.1/1.2, document/literal, public WSDL → live service. Not covered: rpc/encoded, WS-Security / SOAP headers, MTOM, external XSD imports, WSDL 2.0.
- v1 scope: OpenAPI specs in JSON or YAML - OpenAPI 3.x, or Swagger 2.0
auto-converted to 3.0 - plus GraphQL endpoints, SOAP/WSDL services, and AT
Protocol/lexicon/XRPC (the lexicon set shipped in the pinned
@atproto/api). Auth: none / bearer / OAuth 2.0 authorization-code (no PKCE, implicit, client-credentials, or password grants), or atproto app-password sessions (or anonymous, for public reads; atproto OAuth, whose tokens are DPoP-bound, isn't supported yet).searchis keyword-based over the operation index - no embeddings or freeform-doc search. Eachexecuteis a fresh subprocess (no persistent state between calls); OAuth tokens and atproto sessions persist in the keychain and refresh automatically.
deno task dev <subcommand> # run from source
deno task compile # build ./anyapi-mcp
deno task check # type-check
deno task lint # lint
deno task fmt # check formatting (drop --check in deno.json to apply)