内容
# appium-mcp-auth
> 为 [appium-mcp](https://github.com/appium/appium-mcp) 提供身份验证和授权功能,当其托管在 **SSE / HTTP 流**上时 — 完全基于 appium-mcp **插件 API**(无需核心更改)。
当您通过 SSE 公开 appium-mcp 时,任何可以访问该端口的人都可以获得完整的 Appium 会话并驱动真实设备。`appium-mcp-auth` 作为一个插件添加了一层安全保护:
- 🔑 **Bearer API 密钥** (`ak_<id>_<secret>`) — 存储时哈希,比较时恒定时间。适用于机器 / CI。
- 🪪 **OAuth JWT** 访问令牌 — 验证 against 发行者的 JWKS (`iss`/`aud`/`exp`)。适用于人类 / IDE 客户端。
- 🎫 **会话令牌** — 通过 `auth_login` 交换 API 密钥为短时令牌。
- 🛂 **基于范围的授权** — 每个工具所需的范围,管理员角色绕过。
- 🧑🤝🧑 **每个调用者的会话所有权** — 调用者只能看到和驱动他们创建的 Appium 会话(多租户隔离)。
- 🚦 **速率限制和会话配额** — 每个主体。
---
## 它是如何工作的(请先阅读本节)
一个插件在 appium-mcp 的 `beforeCall` / `afterCall` 钩子中运行,**无法读取 HTTP 头** — 因此凭证作为 **工具参数**(默认 `authToken`)传递。身份验证和授权都在 `beforeCall` 中发生;如果调用被拒绝,则在工具运行之前会短路。
```
┌────────────┐ 工具调用 { …args, authToken } ┌───────────────────────────┐
│ MCP 客户端 │ ─────────────────────────────────► │ appium-mcp + auth 插件 │
└────────────┘ └─────────────┬─────────────┘
▲ │ beforeCall
│ ┌───────────────────▼──────────────────┐
│ │ ① 验证 authToken → 身份 │
│ │ (ak_… 密钥 · st_…令牌 · JWT) │
│ │ ② 速率限制 每个主体 │
│ │ ③ 授权 范围 vs 工具 │
│ │ (管理员角色绕过) │
│ │ ④ 所有权 会话 ID 必须 │
│ │ 属于调用者 │
│ │ ⑤ 配额 每个主体的会话 │
│ │ 限额 — 创建会话的工具 │
│ └─────────┬─────────────────┬──────────┘
│ 拒绝 │ │ 允许
│◄──────── 错误结果 ────────────────────────┘ ▼
│ (工具从未运行) 工具运行
│ │
│ afterCall ▼
│◄──────── 工具结果 ───────────────── 绑定创建的会话到调用者 ·
释放删除的会话
```
**影响(按设计):**
- 在服务器前面终止 **TLS** — 凭证在请求体中传输。
- 没有 OAuth `.well-known` 发现端点(插件无法提供它们);如果您的 MCP 客户端需要自动协商,请前面加一个网关。此包仍然**验证** JWT。
- 调用省略 `sessionId` 会命中 Appium 的全局活动会话 — 在多租户模式下,要求每个调用都有一个拥有的 `sessionId`。
---
## 安装
```bash
npm install @appclaw/appium-mcp-auth
```
就是这样 — `appium-mcp`(自带 `fastmcp` 和 `zod`)和 `jose` 用于 JWT 验证会自动作为常规依赖项。插件解析 `appium-mcp` 在运行时使用的确切 `fastmcp`/`zod` 副本,因此无需进行同级依赖项管理。
需要 Node.js **≥ 20**。
---
## 在您的项目中实施
有两种使用方法。请选择一种。
### 选项 1 — 一键式 CLI(最快)
运行此包的二进制文件**代替** `appium-mcp`。它是带有身份验证插件的完整 appium-mcp SSE 服务器 — 一个进程,相同的 `/sse` 端点。
```bash
# 1. 生成凭证 — 打印客户端密钥(ak_…)和服务器记录
npx @appclaw/appium-mcp-auth keygen --id=ci --subject=ci-bot --scopes=appium:use
# 2. 将打印的记录注册到服务器(它存储哈希值,而不是秘密)
export APPIUM_MCP_AUTH_API_KEYS='[{"id":"ci","hash":"c65c…","subject":"ci-bot","kind":"service","scopes":["appium:use"]}]'
# 3. 启动受身份验证保护的 SSE 服务器
npx @appclaw/appium-mcp-auth --httpStream --port=8080 --endpoint=/sse
# → SSE 监听 http://localhost:8080/sse
```
客户端使用步骤 1 中的 `ak_…` 密钥进行身份验证 — 或通过 `auth_login` 工具将其交换为短时令牌。完整的 `keygen` 标志和密钥解剖:[创建 API 密钥](#创建-api-密钥-内置命令)。
### 选项 2 — 将插件组合到您自己的服务器(最多控制)
如果您已经构建了一个自定义的 appium-mcp 服务器,只需添加插件。**无需核心更改** — `createAppiumMcpServer` 已经接受插件。
```ts
import { createAppiumMcpServer } from 'appium-mcp/core';
import { createAuthPluginFromEnv } from '@appclaw/appium-mcp-auth';
const server = await createAppiumMcpServer({
plugins: [createAuthPluginFromEnv()], // 读取 APPIUM_MCP_AUTH_* 环境变量
});
await server.start({
transportType: 'httpStream',
httpStream: { endpoint: '/sse', port: 8080 },
});
```
更喜欢显式配置而非环境变量?自己构建配置:
```ts
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'), // 存储哈希值,而不是秘密
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)],
});
```
---
## 客户端如何进行身份验证
无论使用哪个 MCP 客户端,凭证都作为工具调用的 **`authToken` 参数**提供。
1. **交换 API 密钥以获取会话令牌**(`auth_login` 工具是公开的):
```json
{ "tool": "auth_login", "arguments": { "apiKey": "ak_ci_CHANGE-ME" } }
→ { "sessionToken": "st_…", "expiresAt": "…" }
```
2. **在后续每个调用中传递令牌**(或 API 密钥,或 OAuth JWT)作为 `authToken:
```json
{ "tool": "appium_session_management", "arguments": { "action": "create", "authToken": "st_…" } }
{ "tool": "appium_gesture", "arguments": { "sessionId": "…", "authToken": "st_…" } }
```
3. `auth_whoami` 回显调用者身份;`auth_logout` 撤销会话令牌。
### 我的 MCP 客户端会有效吗?
| 客户端行为 | 有效? | 如何 |
|---|---|---|
| 发送 `Authorization: Bearer` 头(Cursor,Claude Desktop,大多数 SSE 客户端) | ✅ | 运行 **网关模式**(如下)— 它读取头并注入凭证。无需每个调用参数。 |
| 逐字转发代理选择的工具参数(例如 AppClaw) | ✅ | 将令牌作为 `authToken` 参数传递(上图)。 |
| 在每个工具调用中注入固定参数 | ✅ | 确定性地设置 `authToken` 而不是依赖 LLM。 |
> 提示:对于 LLM 驱动的客户端,更喜欢**会话令牌** (`auth_login`) 而不是原始 API 密钥,以便长寿命秘密不会在每个提示/跟踪中重复。
---
## Cursor / Claude Desktop 的头部身份验证(网关模式)
插件无法读取 HTTP 头,因此头部客户端由内置的**凭证注入反向代理**提供服务。它读取 `Authorization` 头,重写每个 `tools/call` 以添加 `authToken` 参数,并转发到环回的 appium-mcp 服务器。**appium-mcp 核心中没有任何更改**。
```
Cursor ──(Authorization: Bearer ak_…)──► 网关(公开:8080) ──► appium-mcp + 插件(127.0.0.1:8790)
读取头, beforeCall 看到 authToken,
注入 authToken 参数 授权与正常情况相同
```
启动它:
```bash
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
# 网关(头部身份验证)在 http://localhost:8080/sse
# 上游在 http://127.0.0.1:8790/sse(环回 — 防火墙此端口)
```
将 Cursor 指向它 — `.cursor/mcp.json`(项目)或 `~/.cursor/mcp.json`(全局):
```json
{
"mcpServers": {
"appium-auth": {
"url": "http://localhost:8080/sse",
"headers": {
"Authorization": "Bearer ak_dev_CHANGE-ME"
}
}
}
}
```
Claude Desktop 通过 `mcp-remote` 桥接连接:
```json
{
"mcpServers": {
"appium-auth": {
"command": "npx",
"args": [
"-y", "mcp-remote", "http://localhost:8080/sse",
"--header", "Authorization: Bearer ak_dev_CHANGE-ME"
]
}
}
}
```
`Bearer` 值是任何接受的凭证:API 密钥 (`ak_…`),`auth_login` 会话令牌 (`st_…`),或 OAuth JWT。没有凭证的请求会得到 `401`。`GET /health` 始终允许用于探测。
网关选项:`--port`(公开),`--upstream-port`(环回内部服务器),`--endpoint`;`APPIUM_MCP_AUTH_GATEWAY_HEADER` 更改读取哪个头(默认为 `authorization`)。
> **安全:** 在前面放置 TLS(令牌在头中),并**防火墙上游端口** — 内部服务器信任注入的 `authToken`,因此它必须仅通过网关可访问。
---
## 配置
所有设置都是环境变量(由 `createAuthPluginFromEnv` 和 CLI 使用)。完整示例见 [`.env.example`](./.env.example)。
| 变量 | 目的 | 默认值 |
|---|---|---|
| `APPIUM_MCP_AUTH_API_KEYS` | JSON 数组的 API 密钥记录(`id`,`hash` **或** `secret`,`subject`,`kind`,`scopes`,`roles?`,`expiresAt?`) | — |
| `APPIUM_MCP_AUTH_OAUTH` | JSON OAuth JWT 验证配置(`issuer`,`audience`,`jwksUri?`,`scopeClaim?`,`rolesClaim?`) | — |
| `APPIUM_MCP_AUTH_ARG` | 携带凭证的工具参数名称 | `authToken` |
| `APPIUM_MCP_AUTH_PUBLIC_TOOLS` | 逗号分隔的工具列表,跳过身份验证 | `auth_login` |
| `APPIUM_MCP_AUTH_TOOL_SCOPES` | JSON 映射 `tool → scope | scope[]` | `{}` |
| `APPIUM_MCP_AUTH_DEFAULT_SCOPES` | 未列出的工具所需的范围 | `appium:use` |
| `APPIUM_MCP_AUTH_ADMIN_ROLE` | 绕过范围检查的角色 | `admin` |
| `APPIUM_MCP_AUTH_SESSION_TTL_MS` | 会话令牌生命周期 | `3600000` |
| `APPIUM_MCP_AUTH_RATE_LIMIT` | `<limit>/<windowMs>` 每主体 | 禁用 |
| `APPIUM_MCP_AUTH_MAX_SESSIONS` | 每主体 Appium 会话上限(0 = 关闭) | `0` |
| `APPIUM_MCP_AUTH_ENFORCE_OWNERSHIP` | 强制执行所有权 | `true` |
| `APPIUM_MCP_AUTH_SESSION_ID_ARGS` | 携带会话 ID 的参数名称 | `sessionId` |
| `APPIUM_MCP_AUTH_SESSION_TOOLS` | 创建会话的工具 | `appium_session_management` |
| `APPIUM_MCP_AUTH_AUDIT` | 将 JSON 审计行输出到 stderr | `true` |
> 如果既没有配置 API 密钥也没有配置 OAuth,**每个受保护的调用都被拒绝**,并且服务器在启动时记录警告。
### 创建 API 密钥(内置命令)
使用 `keygen` 命令 — 它打印**客户端 Bearer 令牌**和**服务器配置记录**(存储哈希值,而不是秘密):
```bash
npx @appclaw/appium-mcp-auth keygen --id=ci --subject=ci-bot --scopes=appium:use
```
```
给 CLIENT(Authorization 头) — 显示一次,安全存储:
Authorization: Bearer ak_ci_Tgaz5NSbOTqgz3A4s_CdPeV7FePHLExS
将此记录添加到服务器上的 APPIUM_MCP_AUTH_API_KEYS(存储哈希值,而不是秘密):
{"id":"ci","hash":"c65c…","subject":"ci-bot","kind":"service","scopes":["appium:use"]}
```
标志:`--id` `--subject` `[--scopes=a,b]` `[--kind=service|user]` `[--roles=admin]` `[--name="…"]` `[--expires-in=30d]` `[--secret=…]` `[--json]`。
这三个字符串是链接的:客户端呈现 `ak_<id>_<secret>`;服务器存储 `hash = SHA256(secret)`;在每个调用中,它检查 `SHA256(presented secret) === stored hash`(恒定时间)。明文秘密永远不会离开客户端,并且 `--json` 形式对脚本/轮换很方便。
---
## 授权模型
- **范围** — 每个工具都需要一个范围集(`APPIUM_MCP_AUTH_TOOL_SCOPES`),默认为 `APPIUM_MCP_AUTH_DEFAULT_SCOPES`。调用者需要**所有**这些范围。
- **管理员角色** — 具有 `admin` 角色的调用者绕过范围检查。
- **所有权** — 调用者创建的会话与其 `subject` 绑定;拒绝引用其他人跟踪的 `sessionId`(`not_session_owner`)。未跟踪的 ID(预先存在的 / 附件流程)会通过。
- **配额 / 速率限制** — 每主体,通过 `MAX_SESSIONS` 和 `RATE_LIMIT`。
## 公共 API
```ts
import {
AppiumAuthPlugin, // 插件类
createAuthPlugin, // 从 AuthConfig 构建
createAuthPluginFromEnv, // 从环境构建
buildAuthenticatedServer, // 完整的服务器(CLI 使用)
loadConfig, sha256Hex, // 配置帮助程序
// 构建块:Authenticator, Authorizer, KeyStore, OAuthValidator,
// OwnershipRegistry, RateLimiter, AuditLog
} from '@appclaw/appium-mcp-auth';
```
## 安全注意事项
- 存储 API 密钥**哈希值**,而非明文秘密,在提交的配置中。
- 为服务和人类提供**不同的范围**,以便泄露的 CI 密钥无法作为人类。
- 固定 OAuth `issuer` 和 `audience`,以便为其他应用颁发的令牌被拒绝。
- 速率限制器和所有权映射是**进程本地的** — 前面加共享存储(例如 Redis),如果您运行多个 SSE 副本。
- 凭证永远不会写入审计日志。
## 开发
```bash
npm install
npm run typecheck # tsc --noEmit
npm test # node:test via tsx(35 tests)
npm run build # emit dist/
```
## 许可证
[Apache-2.0](./LICENSE)
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器