Appy

MCP

MCP server

Appy runs a Model Context Protocol server so AI assistants can create and edit smart links and answer questions about their clicks and installs, with your secret key.

Endpoint and authentication

Your accountonly this key’s accountBearer appy_sk_…403 forbiddensame as REST APIAI assistantClaude Code, Cursor…Publishable keyor dashboard sessionMCP servermcp.appy.toLinksClicksDomainsInstalls
The secret key decides which account the assistant sees. Publishable keys and dashboard sessions get 403 forbidden.
Endpointhttps://mcp.appy.to
TransportStreamable HTTP, stateless, JSON responses. GET and DELETE answer 405.
AuthenticationAuthorization: Bearer appy_sk_..., a secret key from the dashboard’s API keys page
Protocol versionsNegotiated by the server; current clients use 2025-06-18 or later.
Rate limit600 requests a minute per key, shared with the key’s REST API calls.
PlansBusiness (11 tools) and Enterprise (13 tools). Other plans receive 403 plan_required.

Create a dedicated key for each assistant or teammate, so you can revoke one without touching the others.

Connect a client

  1. 1

    Create a secret key

    On a Business or Enterprise account, open the API keys page and create a key for this assistant. Name it after where it lives, such as “Claude Code, work laptop”. It starts with appy_sk_, is shown once, and an account can hold 10 active keys.

  2. 2

    Add the server to your client

    Use the command or configuration for your client below and replace appy_sk_... with your key. The dashboard’s MCP page shows the same snippets.

  3. 3

    Check the connection

    Ask something that changes nothing, such as “Which plan is this key on?”. The assistant calls get_account, a read-only tool, and answers with your plan.

Claude Code

Run this in your terminal. Add --scope user to have the server in every project.

terminal
claude mcp add --transport http appy https://mcp.appy.to --header "Authorization: Bearer appy_sk_..."

Codex

Codex saves the server in ~/.codex/config.toml and reads the key from the APPY_API_KEY environment variable, which it sends as a bearer token.

terminal
export APPY_API_KEY=appy_sk_...
codex mcp add appy --url https://mcp.appy.to --bearer-token-env-var APPY_API_KEY

Cursor

Add this to ~/.cursor/mcp.json, or to .cursor/mcp.json inside a project.

~/.cursor/mcp.json
{
  "mcpServers": {
    "appy": {
      "url": "https://mcp.appy.to",
      "headers": {
        "Authorization": "Bearer appy_sk_..."
      }
    }
  }
}

VS Code

Add this to .vscode/mcp.json in your workspace.

.vscode/mcp.json
{
  "servers": {
    "appy": {
      "type": "http",
      "url": "https://mcp.appy.to",
      "headers": {
        "Authorization": "Bearer appy_sk_..."
      }
    }
  }
}

Claude Desktop and other local-only clients

Clients that only start local servers reach the endpoint through the mcp-remote bridge, which runs with npx and needs Node.js. The header value sits in the APPY_AUTH environment variable so the argument has no spaces, which Windows would otherwise split.

claude_desktop_config.json
{
  "mcpServers": {
    "appy": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://mcp.appy.to",
        "--header",
        "Authorization:${APPY_AUTH}"
      ],
      "env": {
        "APPY_AUTH": "Bearer appy_sk_..."
      }
    }
  }
}

Windsurf and other clients

Windsurf and any other client that supports Streamable HTTP take the same URL, https://mcp.appy.to, and the same Authorization header. Clients that can only start local servers use the mcp-remote configuration above.

Keep the key out of config files

The snippets above hold the key in plain text. Two safer variants: Cursor can read it from an environment variable, and VS Code can prompt for it instead of keeping it in the file.

Cursor: key from APPY_API_KEY
{
  "mcpServers": {
    "appy": {
      "url": "https://mcp.appy.to",
      "headers": {
        "Authorization": "Bearer ${env:APPY_API_KEY}"
      }
    }
  }
}
VS Code: prompt for the key
{
  "inputs": [
    {
      "type": "promptString",
      "id": "appy-key",
      "description": "Appy secret key",
      "password": true
    }
  ],
  "servers": {
    "appy": {
      "type": "http",
      "url": "https://mcp.appy.to",
      "headers": {
        "Authorization": "Bearer ${input:appy-key}"
      }
    }
  }
}

Keep keys out of shared repositories. A project-level config such as .mcp.json, .cursor/mcp.json or .vscode/mcp.json is shared with your team: use one of the variants above, add the file to .gitignore, or use your client’s support for environment variables.

Tools

Business keys see eleven tools. Enterprise keys see thirteen: list_apps and get_app_attribution are only listed when the key’s plan includes the SDK, so a Business assistant never offers them. Every tool carries MCP annotations, and none of them reaches outside Appy (openWorldHint: false).

ToolWhat it doesHintsPlan
get_accountPlan, included features (api, mcp, sdk), link limit (null means unlimited) and current link count.read-onlyBusiness
list_linksLinks, newest first, with destinations and targets. search matches slug or title; limit 1 to 100 (default 20) and offset page through the rest.read-onlyBusiness
get_linkOne link by slug: public url, fallbackUrl, deeplinkUrl, per-platform targets and settings.read-onlyBusiness
create_linkA new link. The website (fallbackUrl) is required; slug, title, deeplinkUrl, targets (iphone, ipad, android, huawei) and parameterForwarding are optional.additiveBusiness
update_linkChanges only the fields you send. An empty string removes title, deeplinkUrl or a target; targets you leave out stay as they are.destructive, idempotentBusiness
delete_linkStops a link from redirecting immediately. The slug stays reserved.destructive, idempotentBusiness
get_link_statsTotal and unique clicks, clicks per day, and breakdowns by country, OS, device, browser, referrer and outcome (store, app opened, website).read-onlyBusiness
get_link_timeseriesClicks per day for one link, without the breakdowns.read-onlyBusiness
get_account_statsThe same totals, daily series and breakdowns across every link in the account.read-onlyBusiness
get_top_linksLinks ranked by clicks in a date range, best first. limit 1 to 100, default 10.read-onlyBusiness
list_domainsCustom domains with their status, the DNS record to add, which one is the default for link URLs, and limits (used, max).read-onlyBusiness
list_appsSDK apps with their link domain and iOS and Android identifiers.read-onlyEnterprise
get_app_attributionFor one app: installs (total, attributed, verified, organic, by store), a row per day, results by UTM source, medium and campaign, in-app events and revenue, and the links that brought them. Optional link narrows every number to one link.read-onlyEnterprise

Behavior worth knowing

  • Dates are UTC days in YYYY-MM-DD form. Without start and end, statistics cover the last 30 days, ending today.
  • A custom slug is 5 to 64 lowercase letters, digits or hyphens; leave it out for a random one. create_link refuses a slug that is already taken (conflict) instead of overwriting it.
  • The slug of an existing link never changes. Rename by creating a new link and deleting the old one.
  • Deleting a link stops it from redirecting, removes it from your link count and keeps the slug reserved, so nobody else can claim it.
  • Revenue from get_app_attribution is summed per currency, as the app reports it.
  • Every tool returns structured content and the same result as JSON text, so clients without structured output support still work.
  • Deleting installs or user data is not available through MCP; use the REST API.

Errors

Errors reach the assistant in two ways. Problems with the key are answered at the HTTP level, before any tool runs, with the same JSON error body as the REST API. Clients usually show these as a failed connection.

StatuscodeWhen
401unauthorizedThe Authorization header is missing, or the key is unknown or revoked.
403forbiddenA dashboard session or a publishable key was sent instead of a secret key.
403plan_requiredThe account is below Business.
429rate_limitedThe key used up its 600 requests for the minute. Honor Retry-After.

Problems inside a call come back as a tool error whose text starts with the API error code, followed by the message, so the assistant can explain what went wrong in plain words:

tool error
conflict: This slug is already in use. Choose another one.
codeTypical cause
validation_failedA value is not acceptable, for example a website (fallbackUrl) that is not an http(s) URL.
not_foundThe slug or app does not exist, was deleted, or belongs to another account.
conflictThe slug is already taken.
limit_reachedThe account is at its link limit.
internal_errorSomething failed on Appy’s side. Reads are safe to retry.

What the assistant calls

Nobody names a tool. On connect the assistant receives every tool’s description and input schema plus the server’s short instructions (read a link before changing it, confirm before deleting one), then picks a tool for each step and fills in the arguments. Three requests, start to finish. The account, links and numbers are made up.

Request: “Make a copy of spring-sale for the autumn campaign that opens acme://sale/autumn in the app, with acme.com/autumn as the website.”

The assistant reads the original first, so it can reuse its store targets. get_link is read-only, so clients run it without asking.

get_link · arguments
{
  "slug": "spring-sale"
}
get_link · result
{"slug": "spring-sale",
 "url": "https://appy.to/spring-sale",
 "deeplinkUrl": "acme://sale/spring",
 "targets": {"iphone": "https://apps.apple.com/app/id123456789",
             "android": "https://play.google.com/store/apps/details?id=com.acme.shop"},
 ...}

Then it creates the copy. create_link is additive: it can only add a link, and a taken slug is refused rather than overwritten.

create_link · arguments
{
  "slug": "autumn-sale",
  "fallbackUrl": "https://acme.com/autumn",
  "deeplinkUrl": "acme://sale/autumn",
  "targets": {
    "iphone": "https://apps.apple.com/app/id123456789",
    "android": "https://play.google.com/store/apps/details?id=com.acme.shop"
  }
}
create_link · result
{"slug": "autumn-sale",
 "url": "https://appy.to/autumn-sale",
 "createdAt": "2026-09-25T10:00:00Z",
 ...}

Reply: “autumn-sale is live at appy.to/autumn-sale, with the same App Store and Google Play pages as spring-sale.”

Request: “Which three links got the most clicks last week?” The assistant turns “last week” into UTC dates and asks for three results.

get_top_links · arguments
{
  "start": "2026-09-14",
  "end": "2026-09-20",
  "limit": 3
}
get_top_links · result
{"start": "2026-09-14", "end": "2026-09-20",
 "data": [
   {"slug": "spring-sale", "title": null, "clicks": 4812},
   {"slug": "app-download", "title": "App download", "clicks": 2236},
   {"slug": "newsletter-sep", "title": null, "clicks": 1390}
 ],
 ...}

Delete with confirmation

Request: “Delete our test links.” The assistant looks first:

list_links · arguments
{
  "search": "test"
}
list_links · result
{"links": [{"slug": "spring-test", ...}, {"slug": "qr-test", ...}],
 "total": 2, "limit": 20, "offset": 0}

It then asks: “Two links match: spring-test and qr-test. Deleting stops them from redirecting. Delete both?” The user answers “Only spring-test.” Because delete_link carries the destructive hint, the client also asks for approval before it runs the call.

delete_link · arguments
{
  "slug": "spring-test"
}
delete_link · result
{"slug": "spring-test", "deleted": true}

Example prompts

  • “Create a link called spring-sale that opens acme://sale/spring in our app, sends iPhones to our App Store page and Android phones to Google Play, and everyone else to acme.com/spring.”
  • “Which five links had the most clicks last week, and where did the clicks on the top one come from?”
  • “Compare daily clicks on spring-sale and summer-sale for September.”
  • “Point every link whose website is the old campaign page at the new one.” The assistant lists the links first and asks before changing them.
  • On Enterprise: “How many installs did the spring campaign bring last month, how many installs were organic, and what did the spring installs spend?”

Guardrails and security

  • A key acts as its account and nothing else. The MCP server sees exactly what the REST API sees for that account; a slug from another account comes back as not_found.
  • update_link and delete_link carry MCP’s destructive hint, so clients that honor it ask before running them. Read tools are marked read-only, and create_link only adds.
  • The server’s instructions tell the assistant to read a link before changing it and to confirm with the user before deleting one.
  • Keys cannot create or revoke keys. /v1/api-keys only accepts a signed-in dashboard session, so an assistant can never hand out access.
  • Dashboard sessions and publishable keys are refused by the MCP server with 403 forbidden.
  • The server keeps no MCP session state. Every request is authenticated and plan-checked on its own.
  • Revoking a key in the dashboard stops the assistant with the next request. A downgrade below Business does the same with 403 plan_required; the keys stay listed, so you can still revoke them.
  • Each key has its own limit of 600 requests a minute, shared with its REST calls. An assistant stuck in a loop only exhausts its own key.
  • OAuth sign-in for clients that cannot send headers, such as web chat connectors, is not available yet.