diff --git a/README.md b/README.md index 50ee13dc..b5d25f0d 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,8 @@ This will automatically configure the MCP server for use with Claude Code. Make | `AI_VISION_COORD_TYPE` | Optional | Coordinate type: `normalized` (default) or `absolute` | | `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_DOCS_ENABLED` | Optional | Set to `true` (or `1`/`yes`/`on`) to register the documentation tools (`appium_documentation_query`, `appium_skills`). **Opt-in and disabled by default.** Requires the optional `@appium/mcp-documentation` package (embeddings cache + ML stack) to be installed separately; when unset it is **never downloaded**. See [Documentation Tools (opt-in)](#documentation-tools-opt-in). | +| `SENTENCE_TRANSFORMERS_MODEL` | Optional | Hugging Face model used for semantic search in Appium documentation queries (default: `Xenova/all-MiniLM-L6-v2`). Only applies when `APPIUM_MCP_DOCS_ENABLED` is set. | | `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). | @@ -372,6 +373,42 @@ The following tools return lightweight text-only responses when NO_UI is enabled - ✅ Scripted automation where human interaction is not needed - ❌ Interactive debugging and exploration (keep UI enabled for better experience) +#### Documentation Tools (opt-in) + +The documentation tools — `appium_documentation_query` (RAG search over the Appium docs) and `appium_skills` — live in a separate package, `@appium/mcp-documentation`, that carries a multi-megabyte embeddings cache and pulls in a heavy ML stack (`@xenova/transformers`, `@langchain/*`). To keep the default install lean, **this package is not a runtime dependency of appium-mcp and is never downloaded unless you opt in.** It is declared as an _optional peer dependency_. + +Enabling the tools is a two-step opt-in: + +**1. Install the optional package** (in the same project/environment as appium-mcp): + +```bash +npm install @appium/mcp-documentation +``` + +Installing it with your own package manager dedupes against appium-mcp's existing dependencies, so only the genuinely new code is added. + +**2. Set `APPIUM_MCP_DOCS_ENABLED`** in your MCP server config: + +```json +{ + "appium-mcp": { + "env": { + "APPIUM_MCP_DOCS_ENABLED": "true", + "ANDROID_HOME": "/path/to/android/sdk" + } + } +} +``` + +Behavior: + +- **Unset / not truthy (default):** the documentation tools are **not registered**, and nothing related to them (cache, embeddings, ML dependencies) is loaded. +- **Truthy (`true`/`1`/`yes`/`on`):** the server registers the documentation tools if `@appium/mcp-documentation` is installed. If the flag is set but the package is **not** installed, the server starts normally **without** the documentation tools and logs a hint to run `npm install @appium/mcp-documentation`. + +The gate is governed by the env var, not by mere presence of the package: with `APPIUM_MCP_DOCS_ENABLED` unset, the tools stay hidden even if the package happens to be installed. + +Pre-installing it that way also avoids the first-run download delay. + #### MCP disconnect behavior By default (`APPIUM_MCP_ON_CLIENT_DISCONNECT` unset or `delete_all`), when the **MCP client disconnects**, this server **deletes every MCP-owned Appium session** (the same sessions `safeDeleteAllSessions` targets) so embedded drivers are not left running after a short-lived assistant run. **Attached** sessions (`ownership=attached`) are unchanged by this teardown. @@ -569,8 +606,8 @@ The default regex pattern allows any URL that starts with `http://` or `https:// | ---------------------------- | ------------------------------------------------------------------------------------------------- | | `generate_locators` | Generate intelligent locators for all interactive elements on the current screen | | `appium_generate_tests` | Generate automated test code from natural language scenarios | -| `appium_documentation_query` | Query Appium documentation using RAG for help and guidance | -| `appium_skills` | Return ordered setup or troubleshooting skills from `appium/skills` for local Appium environments | +| `appium_documentation_query` | **Opt-in (gated by `APPIUM_MCP_DOCS_ENABLED`).** Query Appium documentation using RAG for help and guidance | +| `appium_skills` | **Opt-in (gated by `APPIUM_MCP_DOCS_ENABLED`).** Return ordered setup or troubleshooting skills from `appium/skills` for local Appium environments | ## 🤖 Client Support diff --git a/package.json b/package.json index 81697640..41d3b310 100644 --- a/package.json +++ b/package.json @@ -57,10 +57,8 @@ "license": "Apache-2.0", "description": "Intelligent MCP server providing AI assistants with powerful tools and resources for Appium mobile automation", "dependencies": { - "@appium/mcp-documentation": "^1.0.1", "@appium/support": "^7.0.2", "@modelcontextprotocol/sdk": "^1.22.0", - "@xenova/transformers": "^2.17.2", "@xmldom/xmldom": "^0.9.8", "appium-adb": "^15.0.0", "appium-ios-device": "^3.1.0", @@ -70,8 +68,8 @@ "applesign": "^5.0.0", "fastmcp": "^4.0.0", "ios-mobileprovision-finder": "^1.2.1", - "node-simctl": "^8.0.4", "lru-cache": "^11.5.0", + "node-simctl": "^8.0.4", "webdriver": "^9.23.0", "xpath": "^0.0.34", "zod": "^4.3.6" @@ -81,6 +79,14 @@ "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", "@opentelemetry/sdk-node": "^0.219.0" }, + "peerDependencies": { + "@appium/mcp-documentation": "^1.0.1" + }, + "peerDependenciesMeta": { + "@appium/mcp-documentation": { + "optional": true + } + }, "devDependencies": { "@appium/eslint-config-appium-ts": "^3.0.0", "@appium/tsconfig": "^1.1.2", diff --git a/src/documentation.ts b/src/documentation.ts new file mode 100644 index 00000000..268da58a --- /dev/null +++ b/src/documentation.ts @@ -0,0 +1,75 @@ +/** + * Optional Appium documentation plugin (RAG docs query + skills tools). + * + * The documentation feature lives in a separate package, `@appium/mcp-documentation`, + * which carries a multi-megabyte embeddings cache and pulls in a heavy ML stack + * (`@xenova/transformers`, `@langchain/*`). To keep the default install lean, that + * package is declared as an OPTIONAL peer dependency: it is not installed by + * default, and nothing related to it is downloaded unless the user opts in. + * + * Contract: + * - `APPIUM_MCP_DOCS_ENABLED` unset / not truthy → docs tools are NOT registered. + * This is the default; nothing extra is loaded. + * - `APPIUM_MCP_DOCS_ENABLED` truthy → the plugin is loaded if + * `@appium/mcp-documentation` is installed. If it is not installed, the server + * logs an actionable install hint and starts normally without the docs tools. + * + * Installation is intentionally left to the user's package manager (run at install + * time, where it belongs) rather than shelled out from the running server: that + * dedupes against appium-mcp's existing dependencies, works across npm/pnpm/yarn, + * and never blocks server startup. + */ + +import type { AppiumMcpPlugin } from './core.js'; +import log from './logger.js'; +import { isTruthyEnvValue } from './utils/env.js'; + +const ENABLED_FLAG = 'APPIUM_MCP_DOCS_ENABLED'; +const PACKAGE_NAME = '@appium/mcp-documentation'; + +/** True when the user has opted into the documentation tools. */ +export function isDocumentationEnabled(): boolean { + return isTruthyEnvValue(process.env[ENABLED_FLAG]); +} + +/** + * Load the documentation plugin when the user has opted in. + * + * @returns the plugin instance, or `null` if the optional package is not + * installed or fails to load (in which case the server runs without the + * documentation tools). + */ +export async function loadDocumentationPlugin(): Promise { + try { + // Widen to `string` so TypeScript treats this as a fully dynamic import and + // does not require @appium/mcp-documentation to be resolvable at build time + // (it is an optional, opt-in dependency, not installed by default). + const specifier: string = PACKAGE_NAME; + const mod = (await import(specifier)) as { + AppiumDocumentation: new () => AppiumMcpPlugin; + }; + const plugin = new mod.AppiumDocumentation(); + log.info(`Documentation tools enabled (${PACKAGE_NAME} loaded).`); + return plugin; + } catch (err) { + if (isModuleNotFound(err)) { + log.warn( + `${ENABLED_FLAG} is set but ${PACKAGE_NAME} is not installed. ` + + 'The documentation tools (appium_documentation_query, appium_skills) ' + + 'will be unavailable. Install the package to enable them:\n' + + ` npm install ${PACKAGE_NAME}` + ); + } else { + log.error(`${PACKAGE_NAME} is installed but failed to load:`, err); + } + return null; + } +} + +function isModuleNotFound(err: unknown): boolean { + if (typeof err !== 'object' || err === null || !('code' in err)) { + return false; + } + const code = (err as { code?: unknown }).code; + return code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND'; +} diff --git a/src/server.ts b/src/server.ts index e2b6bbf6..e386830b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,12 +1,21 @@ import type { AppiumMcpPlugin } from './core.js'; import { createAppiumMcpServer } from './create-server.js'; +import { + isDocumentationEnabled, + loadDocumentationPlugin, +} from './documentation.js'; const plugins: AppiumMcpPlugin[] = []; -try { - const { AppiumDocumentation } = await import('@appium/mcp-documentation'); - plugins.push(new AppiumDocumentation()); -} catch (_err) {} +// Documentation tools (RAG docs query + skills) are opt-in. They live in a +// separate package only installed when the user sets +// APPIUM_MCP_DOCS_ENABLED. See ./documentation.ts for the full contract. +if (isDocumentationEnabled()) { + const documentationPlugin = await loadDocumentationPlugin(); + if (documentationPlugin) { + plugins.push(documentationPlugin); + } +} const server = await createAppiumMcpServer({ plugins }); export default server;