diff --git a/CHANGELOG.md b/CHANGELOG.md index ef1191468..b545aafb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,12 @@ writing `airplane_mode_on` and broadcasting `ACTION_AIRPLANE_MODE_CHANGED` — a broadcast Android refuses for non-system callers, so the old path failed *after* writing the setting and left the device reporting airplane mode with the network still up (#2223). The response now reports the - `airplaneMode` the connectivity service holds after the change, and an Android build that does not - expose that command is refused with `UNSUPPORTED_OPERATION` before anything is written. + `airplaneMode` the connectivity service holds after the change, and an Android build that does not + expose that command is refused with `UNSUPPORTED_OPERATION` before anything is written. +- Fixed: the MCP registry entry (`server.json`) now declares the fixed `mcp` subcommand via + `packageArguments`, so a registry-format launcher — the MCP Registry or the website's + `/.well-known/mcp.json` discovery manifest — starts the stdio MCP server. Previously it ran + `agent-device` with no subcommand, i.e. the bare CLI (#2275). - Breaking (0.21): iOS Appium/WebDriver snapshots now expose engine-owned acquisition facts and typed fidelity warnings. The SDK snapshot `truncated` field is optional when Appium cannot report hierarchy completeness; regular snapshots fail closed without valid viewport evidence, while diff --git a/scripts/__tests__/mcp-metadata.test.ts b/scripts/__tests__/mcp-metadata.test.ts new file mode 100644 index 000000000..32c72a450 --- /dev/null +++ b/scripts/__tests__/mcp-metadata.test.ts @@ -0,0 +1,48 @@ +/** + * Guard for the registry-format launch argument. + * + * The MCP registry entry (and the website's `/.well-known/mcp.json` discovery manifest) tell a + * launcher how to start the server: the npm package, the stdio transport, and + * `packageArguments`. Without the fixed `mcp` positional argument, a client that honors the + * manifest runs `agent-device` with no subcommand — the bare CLI — instead of the stdio MCP + * server (`src/bin.ts` only starts the server for the `mcp` subcommand). + * + * `check:mcp-metadata` compares the file against a regeneration, which proves the corrected file + * is self-consistent; this test owns the invariant directly against the checked-in file and fails + * in both directions — a missing argument and a wrong one. + */ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { test } from 'vitest'; + +const ROOT = join(import.meta.dirname, '..', '..'); + +const EXPECTED_MCP_PACKAGE_ARGUMENTS = [{ type: 'positional', value: 'mcp' }]; + +type ServerPackage = { + registryType?: string; + identifier?: string; + transport?: { type?: string }; + packageArguments?: unknown; +}; + +test('the published registry entry starts the MCP server, not the bare CLI', async () => { + const pkg = JSON.parse(await readFile(join(ROOT, 'package.json'), 'utf8')) as { + name: string; + }; + const server = JSON.parse(await readFile(join(ROOT, 'server.json'), 'utf8')) as { + packages?: ServerPackage[]; + }; + + const entry = (server.packages ?? []).find((candidate) => candidate.identifier === pkg.name); + assert.ok(entry, `server.json must describe the ${pkg.name} npm package`); + assert.equal(entry.registryType, 'npm'); + assert.deepEqual(entry.transport, { type: 'stdio' }); + + assert.deepEqual( + entry.packageArguments, + EXPECTED_MCP_PACKAGE_ARGUMENTS, + 'registry-format launchers must start the stdio MCP server; without the fixed mcp subcommand they run the bare CLI', + ); +}); diff --git a/scripts/sync-mcp-metadata.mjs b/scripts/sync-mcp-metadata.mjs index 856b51372..9ce6aff39 100644 --- a/scripts/sync-mcp-metadata.mjs +++ b/scripts/sync-mcp-metadata.mjs @@ -12,6 +12,7 @@ const server = readJson(serverPath); const expectedName = pkg.mcpName; const expectedVersion = pkg.version; const registryDescriptionMaxLength = 100; +const mcpPackageArguments = [{ type: 'positional', value: 'mcp' }]; if (typeof expectedName !== 'string' || expectedName.length === 0) { fail('package.json must define mcpName.'); @@ -32,6 +33,7 @@ if (Array.isArray(server.packages)) { for (const packageEntry of server.packages) { if (packageEntry?.identifier === pkg.name) { packageEntry.version = expectedVersion; + packageEntry.packageArguments = mcpPackageArguments; } } } diff --git a/server.json b/server.json index 20c5e32a6..4382bb449 100644 --- a/server.json +++ b/server.json @@ -15,7 +15,13 @@ "version": "0.20.11-dev", "transport": { "type": "stdio" - } + }, + "packageArguments": [ + { + "type": "positional", + "value": "mcp" + } + ] } ] } diff --git a/vitest.config.ts b/vitest.config.ts index e9908d267..c43702cd7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -147,6 +147,9 @@ export default defineConfig({ // Publish preparation spawns only fixture-owned scripts and proves both Android // helper families are rebuilt through the shared release/size-report owner. 'scripts/__tests__/prepare-publish-assets.test.ts', + // Parse-only guard on the checked-in registry entry: the npm package must declare + // the fixed mcp subcommand, or registry-format launchers run the bare CLI. + 'scripts/__tests__/mcp-metadata.test.ts', 'scripts/ios-snapshot-benchmark/*.test.ts', // Parses CI configuration only, so this action guard needs no device or subprocess lane. 'test/ci/upload-agent-device-artifacts.test.ts',