Skip to content

Commit 41508d2

Browse files
authored
fix: make mcp discovery-only (#556)
1 parent 59d28e8 commit 41508d2

11 files changed

Lines changed: 236 additions & 379 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,15 @@ If you install skills separately, keep the CLI on `agent-device >= 0.14.0`. Olde
5555

5656
- **Agent + terminal**: in Cursor, Codex, Claude Code, Windsurf, and similar clients, run `agent-device` in the integrated terminal. Start planning with `agent-device help workflow`; CLI help is authoritative.
5757
- **Skills or rules**: install the skill with `npx skills add callstackincubator/agent-device`, use the bundled [agent-device skill](skills/agent-device/SKILL.md), or mirror it as a thin project rule, so the agent checks the installed version and reads `agent-device help workflow` before acting. Use `agent-device help react-native` for React Native apps, overlays, Metro/Fast Refresh blockers, and routing to React DevTools or debugging evidence.
58-
- **MCP router**: use `agent-device mcp` when an MCP-aware client needs install, status, and version-matched help discovery. MCP is intentionally a thin router; device automation still runs through CLI commands.
58+
- **MCP router**: use `agent-device mcp` when an MCP-aware client needs to discover the CLI package, install command, version check, and first help command. MCP is discovery-only; device automation still runs through terminal CLI commands.
5959

6060
For client-specific setup, see [AI Agent Setup](https://incubator.callstack.com/agent-device/docs/agent-setup). For agent-readable docs, use [llms-full.txt](https://incubator.callstack.com/agent-device/llms-full.txt).
6161

6262
### MCP Router
6363

64-
`agent-device` ships an official stdio MCP router for discovery-oriented clients. It exposes only `status`, `install`, and `help` tools plus workflow prompts/resources; it does not expose device automation or generic shell execution over MCP.
64+
`agent-device` ships an official stdio MCP router for discovery-oriented clients. It exposes only a `status` tool that returns structured CLI handoff guidance: npm package name, installed version, CLI command name, install command, verify command, starting help command, and an explicit note that automation happens through the CLI.
65+
66+
MCP clients must not use this server as a device automation surface or generic shell runner. If the CLI is missing, agents should ask a human before installing or updating packages, then verify with `agent-device --version` and start with `agent-device help workflow`.
6567

6668
Paste one of these into clients that accept `mcpServers`, such as Cursor project `.cursor/mcp.json` or user-level MCP settings.
6769

server.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
33
"name": "io.github.callstackincubator/agent-device",
44
"title": "agent-device",
5-
"description": "Discovery router for the agent-device CLI with status, install, help, prompts, and resources.",
5+
"description": "Let AI agents inspect, control, and debug real iOS, Android, desktop, and TV apps",
66
"repository": {
77
"url": "https://github.com/callstackincubator/agent-device",
88
"source": "github"

src/mcp/__tests__/router.test.ts

Lines changed: 72 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,103 @@
11
import assert from 'node:assert/strict';
22
import { test } from 'vitest';
33
import { handleMcpMessage } from '../router.ts';
4-
import { handleMcpPayload } from '../server.ts';
54

6-
test('MCP router exposes status install and help tools only', () => {
5+
test('MCP initialize advertises discovery-only tool capability', () => {
76
const response = handleMcpMessage({
87
jsonrpc: '2.0',
98
id: 1,
10-
method: 'tools/list',
9+
method: 'initialize',
10+
params: {
11+
protocolVersion: '2099-01-01',
12+
},
1113
});
1214

13-
assert.equal(response?.jsonrpc, '2.0');
1415
assert.ok(response && 'result' in response);
15-
const tools = (response.result as { tools: Array<{ name: string }> }).tools;
16-
assert.deepEqual(
17-
tools.map((tool) => tool.name),
18-
['status', 'install', 'help'],
19-
);
16+
const result = response.result as {
17+
protocolVersion: string;
18+
capabilities: Record<string, unknown>;
19+
};
20+
assert.equal(result.protocolVersion, '2025-11-25');
21+
assert.deepEqual(result.capabilities, { tools: {} });
2022
});
2123

22-
test('MCP help tool returns versioned workflow guidance', () => {
24+
test('MCP tools/list exposes only status', () => {
2325
const response = handleMcpMessage({
2426
jsonrpc: '2.0',
2527
id: 2,
26-
method: 'tools/call',
27-
params: {
28-
name: 'help',
29-
arguments: { topic: 'workflow' },
30-
},
28+
method: 'tools/list',
3129
});
3230

3331
assert.ok(response && 'result' in response);
34-
const result = response.result as { content: Array<{ text: string }>; isError: boolean };
35-
assert.equal(result.isError, false);
36-
assert.match(result.content[0]?.text ?? '', /agent-device help workflow/);
37-
assert.match(result.content[0]?.text ?? '', /snapshot -i/);
32+
const tools = (
33+
response.result as { tools: Array<{ name: string; outputSchema?: { type: string } }> }
34+
).tools;
35+
assert.deepEqual(
36+
tools.map((tool) => tool.name),
37+
['status'],
38+
);
39+
assert.equal(tools[0]?.outputSchema?.type, 'object');
3840
});
3941

40-
test('MCP install tool can return npx client config', () => {
42+
test('MCP status tool returns structured CLI handoff guidance', () => {
4143
const response = handleMcpMessage({
4244
jsonrpc: '2.0',
4345
id: 3,
4446
method: 'tools/call',
4547
params: {
46-
name: 'install',
47-
arguments: { global: false, client: 'Cline' },
48+
name: 'status',
4849
},
4950
});
5051

5152
assert.ok(response && 'result' in response);
52-
const result = response.result as { content: Array<{ text: string }> };
53-
const text = result.content[0]?.text ?? '';
54-
assert.match(text, /npx -y agent-device mcp/);
55-
assert.match(text, /"args": \["-y","agent-device","mcp"\]/);
56-
assert.match(text, /Client hint: Cline/);
57-
});
58-
59-
test('MCP exposes help resources and workflow prompts', () => {
60-
const resources = handleMcpMessage({
61-
jsonrpc: '2.0',
62-
id: 4,
63-
method: 'resources/list',
64-
});
65-
assert.ok(resources && 'result' in resources);
66-
const resourceUris = (resources.result as { resources: Array<{ uri: string }> }).resources.map(
67-
(resource) => resource.uri,
68-
);
69-
assert.ok(resourceUris.includes('agent-device://help/workflow'));
70-
assert.ok(resourceUris.includes('agent-device://help/react-native'));
71-
72-
const prompt = handleMcpMessage({
73-
jsonrpc: '2.0',
74-
id: 5,
75-
method: 'prompts/get',
76-
params: {
77-
name: 'agent-device-dogfood',
78-
arguments: { target: 'SampleApp on iOS' },
79-
},
80-
});
81-
assert.ok(prompt && 'result' in prompt);
82-
const result = prompt.result as { messages: Array<{ content: { text: string } }> };
83-
assert.match(result.messages[0]?.content.text ?? '', /dogfood/);
84-
assert.match(result.messages[0]?.content.text ?? '', /SampleApp on iOS/);
85-
});
86-
87-
test('MCP React Native prompt routes to RN workflow guidance', () => {
88-
const prompt = handleMcpMessage({
89-
jsonrpc: '2.0',
90-
id: 6,
91-
method: 'prompts/get',
92-
params: {
93-
name: 'agent-device-react-native',
94-
},
95-
});
96-
97-
assert.ok(prompt && 'result' in prompt);
98-
const result = prompt.result as { messages: Array<{ content: { text: string } }> };
99-
assert.match(result.messages[0]?.content.text ?? '', /react-native/);
100-
});
101-
102-
test('MCP initialize returns supported protocol version and unknown methods use JSON-RPC code', () => {
103-
const initialized = handleMcpMessage({
104-
jsonrpc: '2.0',
105-
id: 6,
106-
method: 'initialize',
107-
params: {
108-
protocolVersion: '2099-01-01',
109-
},
110-
});
111-
assert.ok(initialized && 'result' in initialized);
112-
assert.equal((initialized.result as { protocolVersion: string }).protocolVersion, '2025-11-25');
113-
114-
const unknown = handleMcpMessage({
115-
jsonrpc: '2.0',
116-
id: 7,
117-
method: 'unknown/method',
118-
});
119-
assert.ok(unknown && 'error' in unknown);
120-
assert.equal(unknown.error.code, -32601);
121-
});
122-
123-
test('MCP batch requests return one JSON-RPC array response', () => {
124-
const response = handleMcpPayload([
125-
{
126-
jsonrpc: '2.0',
127-
id: 8,
128-
method: 'ping',
129-
},
130-
{
131-
jsonrpc: '2.0',
132-
method: 'notifications/initialized',
133-
},
134-
{
135-
jsonrpc: '2.0',
136-
id: 9,
137-
method: 'tools/list',
138-
},
139-
]);
53+
const result = response.result as {
54+
content: Array<{ text: string }>;
55+
isError: boolean;
56+
structuredContent: {
57+
packageName: string;
58+
cliCommandName: string;
59+
installCommand: string;
60+
verifyCommand: string;
61+
startingHelpCommand: string;
62+
supportedTargets: string[];
63+
capabilities: string[];
64+
prerequisites: string[];
65+
docsUrl: string;
66+
agentDocsUrl: string;
67+
firstCommands: string[];
68+
automationInterface: string;
69+
automationNote: string;
70+
installRequiresHumanApproval: boolean;
71+
installSafetyNote: string;
72+
};
73+
};
74+
assert.equal(result.isError, false);
14075

141-
assert.ok(Array.isArray(response));
142-
assert.equal(response.length, 2);
143-
assert.equal(response[0]?.id, 8);
144-
assert.equal(response[1]?.id, 9);
76+
const handoff = result.structuredContent;
77+
assert.deepEqual(JSON.parse(result.content[0]?.text ?? ''), handoff);
78+
assert.equal(handoff.packageName, 'agent-device');
79+
assert.equal(handoff.cliCommandName, 'agent-device');
80+
assert.equal(handoff.installCommand, 'npm install -g agent-device@latest');
81+
assert.equal(handoff.verifyCommand, 'agent-device --version');
82+
assert.equal(handoff.startingHelpCommand, 'agent-device help workflow');
83+
assert.ok(handoff.supportedTargets.includes('ios-simulator'));
84+
assert.ok(handoff.supportedTargets.includes('android-emulator'));
85+
assert.ok(handoff.capabilities.includes('inspect-ui'));
86+
assert.ok(handoff.capabilities.includes('interact-with-elements'));
87+
assert.ok(handoff.capabilities.includes('accessibility-snapshot'));
88+
assert.ok(handoff.capabilities.includes('react-native'));
89+
assert.ok(handoff.capabilities.includes('expo'));
90+
assert.ok(handoff.capabilities.includes('android-adb'));
91+
assert.ok(handoff.capabilities.includes('ios-xcuitest'));
92+
assert.ok(handoff.prerequisites.includes('node>=22'));
93+
assert.ok(handoff.prerequisites.includes('xcode-for-ios'));
94+
assert.ok(handoff.prerequisites.includes('android-sdk-adb-for-android'));
95+
assert.equal(handoff.docsUrl, 'https://agent-device.dev/');
96+
assert.equal(handoff.agentDocsUrl, 'https://incubator.callstack.com/agent-device/llms-full.txt');
97+
assert.ok(handoff.firstCommands.includes('agent-device apps --platform ios'));
98+
assert.ok(handoff.firstCommands.includes('agent-device apps --platform android'));
99+
assert.equal(handoff.automationInterface, 'cli');
100+
assert.match(handoff.automationNote, /discovery-only/);
101+
assert.equal(handoff.installRequiresHumanApproval, true);
102+
assert.match(handoff.installSafetyNote, /human has approved/);
145103
});

0 commit comments

Comments
 (0)