|
1 | 1 | import assert from 'node:assert/strict'; |
2 | 2 | import { test } from 'vitest'; |
3 | 3 | import { handleMcpMessage } from '../router.ts'; |
4 | | -import { handleMcpPayload } from '../server.ts'; |
5 | 4 |
|
6 | | -test('MCP router exposes status install and help tools only', () => { |
| 5 | +test('MCP initialize advertises discovery-only tool capability', () => { |
7 | 6 | const response = handleMcpMessage({ |
8 | 7 | jsonrpc: '2.0', |
9 | 8 | id: 1, |
10 | | - method: 'tools/list', |
| 9 | + method: 'initialize', |
| 10 | + params: { |
| 11 | + protocolVersion: '2099-01-01', |
| 12 | + }, |
11 | 13 | }); |
12 | 14 |
|
13 | | - assert.equal(response?.jsonrpc, '2.0'); |
14 | 15 | 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: {} }); |
20 | 22 | }); |
21 | 23 |
|
22 | | -test('MCP help tool returns versioned workflow guidance', () => { |
| 24 | +test('MCP tools/list exposes only status', () => { |
23 | 25 | const response = handleMcpMessage({ |
24 | 26 | jsonrpc: '2.0', |
25 | 27 | id: 2, |
26 | | - method: 'tools/call', |
27 | | - params: { |
28 | | - name: 'help', |
29 | | - arguments: { topic: 'workflow' }, |
30 | | - }, |
| 28 | + method: 'tools/list', |
31 | 29 | }); |
32 | 30 |
|
33 | 31 | 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'); |
38 | 40 | }); |
39 | 41 |
|
40 | | -test('MCP install tool can return npx client config', () => { |
| 42 | +test('MCP status tool returns structured CLI handoff guidance', () => { |
41 | 43 | const response = handleMcpMessage({ |
42 | 44 | jsonrpc: '2.0', |
43 | 45 | id: 3, |
44 | 46 | method: 'tools/call', |
45 | 47 | params: { |
46 | | - name: 'install', |
47 | | - arguments: { global: false, client: 'Cline' }, |
| 48 | + name: 'status', |
48 | 49 | }, |
49 | 50 | }); |
50 | 51 |
|
51 | 52 | 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); |
140 | 75 |
|
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/); |
145 | 103 | }); |
0 commit comments