mcp.js 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. #!/usr/bin/env node
  2. /**
  3. * mcp.js — AutoX-MCP 桥的最小客户端
  4. *
  5. * 桥地址:http://127.0.0.1:18789/mcp (JSON-RPC 2.0,streamable HTTP)
  6. * 只实现我们真正用到的几个工具,保持这一层薄且可读。
  7. * 注意:该端点是 PC 本机的 loopback,**只能在 PC 上调用**,
  8. * 在 adb shell 里 curl 它查的是手机的 loopback,必然连不上。
  9. */
  10. 'use strict';
  11. const ENDPOINT = process.env.TTA_MCP_ENDPOINT || 'http://127.0.0.1:18789/mcp';
  12. const HEALTH_URL = process.env.TTA_MCP_HEALTH || 'http://127.0.0.1:18789/health';
  13. let nextId = 1;
  14. async function rpc(method, params) {
  15. const body = {
  16. jsonrpc: '2.0',
  17. id: nextId++,
  18. method,
  19. params: params || {}
  20. };
  21. const response = await fetch(ENDPOINT, {
  22. method: 'POST',
  23. headers: { 'Content-Type': 'application/json' },
  24. body: JSON.stringify(body)
  25. });
  26. if (!response.ok) {
  27. throw new Error(`MCP HTTP ${response.status} ${response.statusText}`);
  28. }
  29. const payload = await response.json();
  30. if (payload.error) {
  31. throw new Error(`MCP 错误:${JSON.stringify(payload.error)}`);
  32. }
  33. return payload.result;
  34. }
  35. /** 工具返回的 content[0].text 里再套一层 JSON,这里统一拆开 */
  36. function unwrap(result) {
  37. if (!result || !Array.isArray(result.content) || result.content.length === 0) {
  38. return result;
  39. }
  40. const text = result.content[0].text;
  41. try {
  42. return JSON.parse(text);
  43. } catch (error) {
  44. return { raw: text };
  45. }
  46. }
  47. async function callTool(name, args) {
  48. return unwrap(await rpc('tools/call', { name, arguments: args || {} }));
  49. }
  50. async function health() {
  51. const response = await fetch(HEALTH_URL);
  52. return response.json();
  53. }
  54. async function listDevices() {
  55. return callTool('device_list', {});
  56. }
  57. async function connect(serial) {
  58. const args = { mode: 'adb' };
  59. if (serial) {
  60. args.serial = serial;
  61. }
  62. return callTool('device_connect', args);
  63. }
  64. /**
  65. * 在设备上运行一段脚本。
  66. * 返回 {status:'ok'|'error'|'timeout'|'needs_confirmation', logs, error}
  67. * —— status 必须显式判断,不能只看 logs 里有没有内容。
  68. */
  69. async function runCode(code, options = {}) {
  70. const args = { code };
  71. if (options.timeoutMs) {
  72. args.timeoutMs = options.timeoutMs;
  73. }
  74. if (options.confirmToken) {
  75. args.confirmToken = options.confirmToken;
  76. }
  77. if (options.deviceId) {
  78. args.deviceId = options.deviceId;
  79. }
  80. return callTool('device_run_code', args);
  81. }
  82. async function getLayout(args = {}) {
  83. return callTool('device_get_layout', args);
  84. }
  85. module.exports = {
  86. ENDPOINT,
  87. HEALTH_URL,
  88. rpc,
  89. callTool,
  90. health,
  91. listDevices,
  92. connect,
  93. runCode,
  94. getLayout
  95. };