appium-mcp-auth
Provides authentication and authorization for Appium MCP, enabling secure access to Appium sessions with API keys, OAuth JWT, and session tokens, along with rate limiting and session quotas.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@appium-mcp-authAuthenticate using bearer token and start an Android session."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
appium-mcp-auth
Authentication & authorization for appium-mcp when hosted over SSE / HTTP Stream β built entirely on the appium-mcp Plugin API (no core changes).
When you expose appium-mcp over SSE, anyone who can reach the port gets a full
Appium session and can drive real devices. appium-mcp-auth adds a security
layer as a drop-in plugin:
π Bearer API keys (
ak_<id>_<secret>) β hashed at rest, constant-time compared. For machines / CI.πͺͺ OAuth JWT access tokens β validated against the issuer JWKS (
iss/aud/exp). For humans / IDE clients.π« Session tokens β exchange an API key for a short-lived token via
auth_login.π Scope-based authorization β per-tool required scopes, admin-role bypass.
π§βπ€βπ§ Per-caller session ownership β callers only see/drive the Appium sessions they created (multi-tenant isolation).
π¦ Rate limiting & session quotas β per subject.
How it works (read this first)
A plugin runs inside appium-mcp's beforeCall / afterCall hooks and cannot
read HTTP headers β so the credential is passed as a tool argument
(default authToken). Authentication and authorization both happen in
beforeCall; a denied call is short-circuited before the tool runs.
ββββββββββββββ tool call { β¦args, authToken } βββββββββββββββββββββββββββββ
β MCP client β ββββββββββββββββββββββββββββββββββΊ β appium-mcp + auth plugin β
ββββββββββββββ βββββββββββββββ¬ββββββββββββββ
β² β beforeCall
β βββββββββββββββββββββΌβββββββββββββββββββ
β β β authenticate authToken β Identity β
β β (ak_β¦ key Β· st_β¦ token Β· JWT) β
β β β‘ rate-limit per subject β
β β β’ authorize scopes vs tool β
β β (admin role bypasses) β
β β β£ ownership sessionId must β
β β belong to the caller β
β β β€ quota per-subject session β
β β cap on session-creating tools β
β βββββββββββ¬ββββββββββββββββββ¬βββββββββββ
β deny β β allow
ββββββββββ error result βββββββββββββββββββββββββ βΌ
β (tool never runs) tool runs
β β
β afterCall βΌ
ββββββββββ tool result βββββββββββββββββ bind created session to caller Β·
release deleted sessionsImplications (by design):
Terminate TLS in front of the server β the credential travels in the request body.
No OAuth
.well-knowndiscovery endpoints (a plugin can't serve them); front with a gateway if your MCP client needs auto-negotiation. This package still validates JWTs.Calls that omit
sessionIdhit Appium's global active session β in multi-tenant mode, require an ownedsessionIdon every call.
Related MCP server: MCP Server with Authentication
Install
npm install @appclaw/appium-mcp-authThat's it β appium-mcp (which brings fastmcp and zod) and jose for JWT
validation come along automatically as regular dependencies. The plugin
resolves the exact fastmcp/zod copies that appium-mcp uses at runtime, so
there is no peer-dependency juggling.
Requires Node.js β₯ 20.
Implement it in your project
There are two ways to use it. Pick one.
Option 1 β Turnkey CLI (fastest)
Run this package's binary instead of appium-mcp's. It is the full
appium-mcp SSE server with the auth plugin composed in β one process, same
/sse endpoint.
# 1. Generate a credential β prints the client key (ak_β¦) AND the server record
npx @appclaw/appium-mcp-auth keygen --id=ci --subject=ci-bot --scopes=appium:use
# 2. Register the printed record with the server (it stores the hash, never the secret)
export APPIUM_MCP_AUTH_API_KEYS='[{"id":"ci","hash":"c65cβ¦","subject":"ci-bot","kind":"service","scopes":["appium:use"]}]'
# 3. Start the auth-protected SSE server
npx @appclaw/appium-mcp-auth --httpStream --port=8080 --endpoint=/sse
# β SSE listening on http://localhost:8080/sseClients authenticate with the ak_β¦ key from step 1 β or exchange it for a
short-lived st_β¦ token via the auth_login tool. Full keygen flags and
key anatomy: Create an API key.
Option 2 β Compose the plugin into your own server (most control)
If you already build a custom appium-mcp server, just add the plugin. No core
changes required β createAppiumMcpServer already accepts plugins.
import { createAppiumMcpServer } from 'appium-mcp/core';
import { createAuthPluginFromEnv } from '@appclaw/appium-mcp-auth';
const server = await createAppiumMcpServer({
plugins: [createAuthPluginFromEnv()], // reads APPIUM_MCP_AUTH_* env vars
});
await server.start({
transportType: 'httpStream',
httpStream: { endpoint: '/sse', port: 8080 },
});Prefer explicit config over environment variables? Build the config yourself:
import { createAppiumMcpServer } from 'appium-mcp/core';
import { createAuthPlugin, sha256Hex, type AuthConfig } from '@appclaw/appium-mcp-auth';
const config: AuthConfig = {
credentialArg: 'authToken',
publicTools: ['auth_login'],
apiKeys: [
{
id: 'ci',
hash: sha256Hex('CHANGE-ME'), // store the hash, not the secret
subject: 'ci-bot',
kind: 'service',
scopes: ['appium:use'],
},
],
toolScopes: { mobile_clear_app: ['appium:admin'] },
defaultScopes: ['appium:use'],
adminRole: 'admin',
sessionTokenTtlMs: 3_600_000,
rateLimit: { limit: 120, windowMs: 60_000 },
maxSessionsPerSubject: 3,
enforceOwnership: true,
sessionIdArgs: ['sessionId'],
sessionCreatingTools: ['appium_session_management'],
audit: true,
};
const server = await createAppiumMcpServer({
plugins: [createAuthPlugin(config)],
});How clients authenticate
Whatever the MCP client, the credential is supplied as the authToken
argument on tool calls.
Exchange an API key for a session token (the
auth_logintool is public):{ "tool": "auth_login", "arguments": { "apiKey": "ak_ci_CHANGE-ME" } } β { "sessionToken": "st_β¦", "expiresAt": "β¦" }Pass the token (or the API key, or an OAuth JWT) as
authTokenon every subsequent call:{ "tool": "appium_session_management", "arguments": { "action": "create", "authToken": "st_β¦" } } { "tool": "appium_gesture", "arguments": { "sessionId": "β¦", "authToken": "st_β¦" } }auth_whoamiechoes the caller identity;auth_logoutrevokes a session token.
Will my MCP client work?
Client behavior | Works? | How |
Sends an | β | Run gateway mode (below) β it reads the header and injects the credential. No per-call argument needed. |
Forwards agent-chosen tool arguments verbatim (e.g. AppClaw) | β | Pass the token as the |
Injects a fixed argument on every tool call | β | Set |
Tip: for LLM-driven clients, prefer a session token (
auth_login) over the raw API key so the long-lived secret isn't repeated in every prompt/trace.
Header auth for Cursor / Claude Desktop (gateway mode)
A plugin can't read HTTP headers, so header-based clients are served by a
built-in credential-injecting reverse proxy. It reads the Authorization
header, rewrites each tools/call to add the authToken argument, and forwards
to the appium-mcp server on loopback. Nothing in appium-mcp core changes.
Cursor ββ(Authorization: Bearer ak_β¦)βββΊ gateway (public :8080) βββΊ appium-mcp + plugin (127.0.0.1:8790)
reads header, beforeCall sees authToken,
injects authToken arg authorizes exactly as normalStart it:
export APPIUM_MCP_AUTH_API_KEYS='[{"id":"dev","secret":"CHANGE-ME","subject":"dev","kind":"user","roles":["admin"],"scopes":["appium:use","appium:admin"]}]'
npx @appclaw/appium-mcp-auth --gateway --port=8080 --endpoint=/sse
# gateway (header auth) on http://localhost:8080/sse
# upstream on http://127.0.0.1:8790/sse (loopback β firewall this port)Point Cursor at it β .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"appium-auth": {
"url": "http://localhost:8080/sse",
"headers": {
"Authorization": "Bearer ak_dev_CHANGE-ME"
}
}
}
}Claude Desktop connects via the mcp-remote bridge:
{
"mcpServers": {
"appium-auth": {
"command": "npx",
"args": [
"-y", "mcp-remote", "http://localhost:8080/sse",
"--header", "Authorization: Bearer ak_dev_CHANGE-ME"
]
}
}
}The Bearer value is any accepted credential: an API key (ak_β¦), a session
token from auth_login (st_β¦), or an OAuth JWT. Requests with no/invalid
credential get 401. GET /health is always allowed for probes.
Gateway options: --port (public), --upstream-port (loopback inner server),
--endpoint; APPIUM_MCP_AUTH_GATEWAY_HEADER changes which header is read
(default authorization).
Security: put TLS in front (the token is in a header) and firewall the upstream port β the inner server trusts the injected
authToken, so it must only be reachable through the gateway.
Configuration
All settings are environment variables (used by createAuthPluginFromEnv and the
CLI). Full examples in .env.example.
Variable | Purpose | Default |
| JSON array of API-key records ( | β |
| JSON OAuth JWT validation config ( | β |
| Tool-argument name carrying the credential |
|
| Comma list of tools that skip auth |
|
| JSON map `tool β scope | scope[]` |
| Scopes required for unlisted tools |
|
| Role that bypasses scope checks |
|
| Session-token lifetime |
|
|
| disabled |
| Per-subject Appium session cap (0 = off) |
|
| Enforce session ownership |
|
| Arg names carrying a session id |
|
| Tools that create sessions |
|
| Emit JSON audit lines to stderr |
|
If neither API keys nor OAuth are configured, every protected call is denied and the server logs a warning at startup.
Create an API key (built-in command)
Use the keygen command β it prints the client bearer token and the
server config record (which stores the hash, never the secret):
npx @appclaw/appium-mcp-auth keygen --id=ci --subject=ci-bot --scopes=appium:useGive this to the CLIENT (Authorization header) β shown once, store it securely:
Authorization: Bearer ak_ci_Tgaz5NSbOTqgz3A4s_CdPeV7FePHLExS
Add this record to APPIUM_MCP_AUTH_API_KEYS on the SERVER (stores the hash, not the secret):
{"id":"ci","hash":"c65cβ¦","subject":"ci-bot","kind":"service","scopes":["appium:use"]}Flags: --id --subject [--scopes=a,b] [--kind=service|user] [--roles=admin]
[--name="β¦"] [--expires-in=30d] [--secret=β¦] [--json].
The three strings are linked: the client presents ak_<id>_<secret>; the server
stores hash = SHA256(secret); on each call it checks
SHA256(presented secret) === stored hash (constant-time). The plaintext secret
never leaves the client, and the --json form is handy for scripting/rotation.
Authorization model
Scopes β each tool requires a scope set (
APPIUM_MCP_AUTH_TOOL_SCOPES), falling back toAPPIUM_MCP_AUTH_DEFAULT_SCOPES. A caller needs all of them.Admin role β a caller with the
adminrole bypasses scope checks.Ownership β sessions a caller creates are bound to its
subject; a call referencing someone else's trackedsessionIdis denied (not_session_owner). Untracked ids (pre-existing / attach flows) pass through.Quota / rate limit β per subject, via
MAX_SESSIONSandRATE_LIMIT.
Public API
import {
AppiumAuthPlugin, // the plugin class
createAuthPlugin, // build from an AuthConfig
createAuthPluginFromEnv, // build from environment
buildAuthenticatedServer, // full server (used by the CLI)
loadConfig, sha256Hex, // config helpers
// building blocks: Authenticator, Authorizer, KeyStore, OAuthValidator,
// OwnershipRegistry, RateLimiter, AuditLog
} from '@appclaw/appium-mcp-auth';Security notes
Store API-key hashes, never plaintext secrets, in committed config.
Give services and humans distinct scopes so a leaked CI key can't act as a human.
Pin OAuth
issuerandaudienceso tokens minted for other apps are rejected.Rate limiter and ownership map are process-local β front with a shared store (e.g. Redis) if you run multiple SSE replicas.
Credentials are never written to audit logs.
Development
npm install
npm run typecheck # tsc --noEmit
npm test # node:test via tsx (35 tests)
npm run build # emit dist/License
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityFmaintenanceSecurity middleware for MCP servers. Trust-based access control, rate limiting, and audit logging. Zero dependencies.16MIT
- AlicenseCqualityDmaintenanceImplements a secure MCP server with API Key and JWT authentication, providing tools like echo, login, secure_action, and admin_action. Includes MCP Inspector integration for testing and debugging.13MIT
- AlicenseAqualityDmaintenanceMCP server for enterprise authentication and authorization β JWT validation, OIDC token inspection, OAuth 2.0 introspection, and role-based access control for AI agents.8MIT
- Flicense-qualityCmaintenanceEnables secure remote access to a Playwright-based MCP server with authentication and role-based access control powered by Descope.
Related MCP Connectors
Hash passwords with bcrypt and issue/verify JWT session tokens over A2A + MCP.
MCP server for Argo RPG Platform β connects AI assistants to campaign data via OAuth2
Artifact store for AI agents. Hosted OAuth at mcp.artifacta.io/mcp; local stdio via npm/PyPI.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/appclawhq/appium-mcp-auth'
If you have feedback or need assistance with the MCP directory API, please join our Discord server