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
403 forbidden.| Endpoint | https://mcp.appy.to |
| Transport | Streamable HTTP, stateless, JSON responses. GET and DELETE answer 405. |
| Authentication | Authorization: Bearer appy_sk_..., a secret key from the dashboard’s API keys page |
| Protocol versions | Negotiated by the server; current clients use 2025-06-18 or later. |
| Rate limit | 600 requests a minute per key, shared with the key’s REST API calls. |
| Plans | Business (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
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
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
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.
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.
export APPY_API_KEY=appy_sk_...
codex mcp add appy --url https://mcp.appy.to --bearer-token-env-var APPY_API_KEYCursor
Add this to ~/.cursor/mcp.json, or to .cursor/mcp.json inside a project.
{
"mcpServers": {
"appy": {
"url": "https://mcp.appy.to",
"headers": {
"Authorization": "Bearer appy_sk_..."
}
}
}
}VS Code
Add this to .vscode/mcp.json in your workspace.
{
"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.
{
"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.
{
"mcpServers": {
"appy": {
"url": "https://mcp.appy.to",
"headers": {
"Authorization": "Bearer ${env:APPY_API_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).
| Tool | What it does | Hints | Plan |
|---|---|---|---|
get_account | Plan, included features (api, mcp, sdk), link limit (null means unlimited) and current link count. | read-only | Business |
list_links | Links, newest first, with destinations and targets. search matches slug or title; limit 1 to 100 (default 20) and offset page through the rest. | read-only | Business |
get_link | One link by slug: public url, fallbackUrl, deeplinkUrl, per-platform targets and settings. | read-only | Business |
create_link | A new link. The website (fallbackUrl) is required; slug, title, deeplinkUrl, targets (iphone, ipad, android, huawei) and parameterForwarding are optional. | additive | Business |
update_link | Changes only the fields you send. An empty string removes title, deeplinkUrl or a target; targets you leave out stay as they are. | destructive, idempotent | Business |
delete_link | Stops a link from redirecting immediately. The slug stays reserved. | destructive, idempotent | Business |
get_link_stats | Total and unique clicks, clicks per day, and breakdowns by country, OS, device, browser, referrer and outcome (store, app opened, website). | read-only | Business |
get_link_timeseries | Clicks per day for one link, without the breakdowns. | read-only | Business |
get_account_stats | The same totals, daily series and breakdowns across every link in the account. | read-only | Business |
get_top_links | Links ranked by clicks in a date range, best first. limit 1 to 100, default 10. | read-only | Business |
list_domains | Custom domains with their status, the DNS record to add, which one is the default for link URLs, and limits (used, max). | read-only | Business |
list_apps | SDK apps with their link domain and iOS and Android identifiers. | read-only | Enterprise |
get_app_attribution | For 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-only | Enterprise |
Behavior worth knowing
- Dates are UTC days in
YYYY-MM-DDform. Withoutstartandend, statistics cover the last 30 days, ending today. - A custom
slugis 5 to 64 lowercase letters, digits or hyphens; leave it out for a random one.create_linkrefuses 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_attributionis 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.
| Status | code | When |
|---|---|---|
| 401 | unauthorized | The Authorization header is missing, or the key is unknown or revoked. |
| 403 | forbidden | A dashboard session or a publishable key was sent instead of a secret key. |
| 403 | plan_required | The account is below Business. |
| 429 | rate_limited | The 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:
conflict: This slug is already in use. Choose another one.code | Typical cause |
|---|---|
validation_failed | A value is not acceptable, for example a website (fallbackUrl) that is not an http(s) URL. |
not_found | The slug or app does not exist, was deleted, or belongs to another account. |
conflict | The slug is already taken. |
limit_reached | The account is at its link limit. |
internal_error | Something 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.
Copy a link for a new campaign
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.
{
"slug": "spring-sale"
}{"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.
{
"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"
}
}{"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.”
Rank links by clicks
Request: “Which three links got the most clicks last week?” The assistant turns “last week” into UTC dates and asks for three results.
{
"start": "2026-09-14",
"end": "2026-09-20",
"limit": 3
}{"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:
{
"search": "test"
}{"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.
{
"slug": "spring-test"
}{"slug": "spring-test", "deleted": true}Example prompts
- “Create a link called spring-sale that opens
acme://sale/springin 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-saleandsummer-salefor 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_linkanddelete_linkcarry MCP’s destructive hint, so clients that honor it ask before running them. Read tools are marked read-only, andcreate_linkonly 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-keysonly 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.