Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ This will automatically configure the MCP server for use with Claude Code. Make
| `AI_VISION_IMAGE_MAX_WIDTH` | Optional | Max image width in pixels before compression (default: `1080`) |
| `AI_VISION_IMAGE_QUALITY` | Optional | JPEG quality 1–100 for compressed screenshots sent to the vision API (default: `80`) |
| `SENTENCE_TRANSFORMERS_MODEL` | Optional | Hugging Face model used for semantic search in Appium documentation queries (default: `Xenova/all-MiniLM-L6-v2`) |
| `APPIUM_MCP_PERSIST_REMOTE_SESSIONS_PATH` | Optional | Absolute file path to persist attached remote session info across server restarts (JSON format) |
| `APPIUM_MCP_PERSIST_REMOTE_SESSIONS_PATH` | Optional | Directory path for persisted attached remote session info. When set, attached remote sessions are stored as JSON files in that directory and can be rehydrated after restart. |
| `APPIUM_MCP_EVIDENCE` | Optional | Set to `true` or `1` to attach a structured **action evidence record** (locator, resolved element id, context, timing, normalized error code) to `appium_find_element` and `appium_gesture` responses as an `application/vnd.appium.evidence+json` resource block, for CI/debugging. Disabled by default; responses are unchanged when unset. |
| `APPIUM_MCP_OTEL_ENABLED` | Optional | Set to `true` to enable OpenTelemetry tracing (disabled by default). |
| `APPIUM_MCP_OTEL_INCLUDE_ARGUMENT_VALUES` | Optional | Set to `true` to include sanitized non-sensitive argument values in spans; disabled by default because values may contain sensitive data. |
Expand Down Expand Up @@ -239,7 +239,11 @@ For **CI**, **device farms**, or **multi-session** setups:

#### Multi-session and `sessionId`

The process keeps one **active** Appium session; tools use it when **`sessionId` is omitted**. If more than one session exists (see **`appium_session_management`** with **`action=list`**), pass **`sessionId` on every tool call** that must target a specific session. Do not assume the active session is stable if other clients or flows can create, select, or delete sessions.
The process keeps one **active** Appium session; tools use it when **`sessionId` is omitted**. If a tool call does not include a `sessionId`, it will target the active session instead of a specific one. If more than one session exists (see **`appium_session_management`** with **`action=list`**), pass **`sessionId` on every tool call** that must target a specific session. Do not assume the active session is stable if other clients or flows can create, select, or delete sessions.

#### Session persistence

If **`APPIUM_MCP_PERSIST_REMOTE_SESSIONS_PATH`** is set, MCP Appium persists **attached remote sessions** to that directory as JSON files. The path may be absolute or relative to the current working directory. Each session is stored under a canonical filename derived from a hash of the `sessionId`; older legacy filenames are migrated, and duplicate files for the same session are removed when the directory is read. When a persisted attached session is used again, the server tries to reattach to the remote Appium session; unreachable entries are pruned automatically.

#### Client disconnect

Expand Down
19 changes: 19 additions & 0 deletions src/tests/tools/session/file-transfer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@ describe('appium_mobile_file', () => {
);
});

test('returns error with sessionId when no driver is active for that session', async () => {
const tool = await registerTool();

const result = await tool.execute(
{
action: 'push',
remotePath: '/sdcard/x.txt',
payloadBase64: 'YQ==',
sessionId: 'session-123',
},
undefined
);

expect(result.isError).toBe(true);
expect(result.content[0].text).toBe(
"No active driver session for session 'session-123'. Use appium_session_management (action=create or action=attach), or pass a valid sessionId."
);
});

test('push: Android uses path and data', async () => {
const tool = await registerTool();
mockGetDriver.mockReturnValue({} as any);
Expand Down
10 changes: 6 additions & 4 deletions src/tools/tool-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export async function resolveDriver(
): Promise<DriverOrError> {
let driver = getDriver(sessionId);
if (!driver) {
if (!sessionId) {
// No active session or no sessionId specified.
return { ok: false, result: noActiveDriverSessionResult() };
}
const rehydrated = await rehydrateAttachedSession(sessionId);
if (rehydrated) {
driver = getDriver(sessionId ?? rehydrated.sessionId);
Expand All @@ -121,15 +125,13 @@ export function platformMismatch(
}

async function rehydrateAttachedSession(
sessionId?: string
sessionId: string
): Promise<{ sessionId: string } | null> {
const persisted = await readAllPersistedSessions();
if (persisted.length === 0) {
return null;
}
const candidates = sessionId
? persisted.filter((p) => p.sessionId === sessionId)
: persisted;
const candidates = persisted.filter((p) => p.sessionId === sessionId);
for (const entry of candidates) {
try {
const client = await attachToRemoteSession({
Expand Down
Loading