@miosa/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +327 -0
  2. package/dist/bin/miosa.d.ts +3 -0
  3. package/dist/bin/miosa.d.ts.map +1 -0
  4. package/dist/bin/miosa.js +139 -0
  5. package/dist/bin/miosa.js.map +1 -0
  6. package/dist/client.d.ts +74 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +523 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/commands/agent.d.ts +18 -0
  11. package/dist/commands/agent.d.ts.map +1 -0
  12. package/dist/commands/agent.js +468 -0
  13. package/dist/commands/agent.js.map +1 -0
  14. package/dist/commands/alerts.d.ts +3 -0
  15. package/dist/commands/alerts.d.ts.map +1 -0
  16. package/dist/commands/alerts.js +41 -0
  17. package/dist/commands/alerts.js.map +1 -0
  18. package/dist/commands/api-keys.d.ts +3 -0
  19. package/dist/commands/api-keys.d.ts.map +1 -0
  20. package/dist/commands/api-keys.js +119 -0
  21. package/dist/commands/api-keys.js.map +1 -0
  22. package/dist/commands/api-resource.d.ts +20 -0
  23. package/dist/commands/api-resource.d.ts.map +1 -0
  24. package/dist/commands/api-resource.js +120 -0
  25. package/dist/commands/api-resource.js.map +1 -0
  26. package/dist/commands/apps.d.ts +3 -0
  27. package/dist/commands/apps.d.ts.map +1 -0
  28. package/dist/commands/apps.js +218 -0
  29. package/dist/commands/apps.js.map +1 -0
  30. package/dist/commands/audit.d.ts +3 -0
  31. package/dist/commands/audit.d.ts.map +1 -0
  32. package/dist/commands/audit.js +25 -0
  33. package/dist/commands/audit.js.map +1 -0
  34. package/dist/commands/auth.d.ts +3 -0
  35. package/dist/commands/auth.d.ts.map +1 -0
  36. package/dist/commands/auth.js +363 -0
  37. package/dist/commands/auth.js.map +1 -0
  38. package/dist/commands/backups.d.ts +3 -0
  39. package/dist/commands/backups.d.ts.map +1 -0
  40. package/dist/commands/backups.js +23 -0
  41. package/dist/commands/backups.js.map +1 -0
  42. package/dist/commands/checkpoints.d.ts +3 -0
  43. package/dist/commands/checkpoints.d.ts.map +1 -0
  44. package/dist/commands/checkpoints.js +33 -0
  45. package/dist/commands/checkpoints.js.map +1 -0
  46. package/dist/commands/computers.d.ts +3 -0
  47. package/dist/commands/computers.d.ts.map +1 -0
  48. package/dist/commands/computers.js +118 -0
  49. package/dist/commands/computers.js.map +1 -0
  50. package/dist/commands/config.d.ts +3 -0
  51. package/dist/commands/config.d.ts.map +1 -0
  52. package/dist/commands/config.js +114 -0
  53. package/dist/commands/config.js.map +1 -0
  54. package/dist/commands/connect.d.ts +3 -0
  55. package/dist/commands/connect.d.ts.map +1 -0
  56. package/dist/commands/connect.js +96 -0
  57. package/dist/commands/connect.js.map +1 -0
  58. package/dist/commands/containers.d.ts +3 -0
  59. package/dist/commands/containers.d.ts.map +1 -0
  60. package/dist/commands/containers.js +20 -0
  61. package/dist/commands/containers.js.map +1 -0
  62. package/dist/commands/cp.d.ts +3 -0
  63. package/dist/commands/cp.d.ts.map +1 -0
  64. package/dist/commands/cp.js +102 -0
  65. package/dist/commands/cp.js.map +1 -0
  66. package/dist/commands/cron.d.ts +3 -0
  67. package/dist/commands/cron.d.ts.map +1 -0
  68. package/dist/commands/cron.js +65 -0
  69. package/dist/commands/cron.js.map +1 -0
  70. package/dist/commands/databases.d.ts +3 -0
  71. package/dist/commands/databases.d.ts.map +1 -0
  72. package/dist/commands/databases.js +222 -0
  73. package/dist/commands/databases.js.map +1 -0
  74. package/dist/commands/db.d.ts +3 -0
  75. package/dist/commands/db.d.ts.map +1 -0
  76. package/dist/commands/db.js +174 -0
  77. package/dist/commands/db.js.map +1 -0
  78. package/dist/commands/deploy.d.ts +3 -0
  79. package/dist/commands/deploy.d.ts.map +1 -0
  80. package/dist/commands/deploy.js +579 -0
  81. package/dist/commands/deploy.js.map +1 -0
  82. package/dist/commands/desktop.d.ts +3 -0
  83. package/dist/commands/desktop.d.ts.map +1 -0
  84. package/dist/commands/desktop.js +276 -0
  85. package/dist/commands/desktop.js.map +1 -0
  86. package/dist/commands/dev.d.ts +3 -0
  87. package/dist/commands/dev.d.ts.map +1 -0
  88. package/dist/commands/dev.js +246 -0
  89. package/dist/commands/dev.js.map +1 -0
  90. package/dist/commands/doctor.d.ts +3 -0
  91. package/dist/commands/doctor.d.ts.map +1 -0
  92. package/dist/commands/doctor.js +241 -0
  93. package/dist/commands/doctor.js.map +1 -0
  94. package/dist/commands/domains.d.ts +3 -0
  95. package/dist/commands/domains.d.ts.map +1 -0
  96. package/dist/commands/domains.js +31 -0
  97. package/dist/commands/domains.js.map +1 -0
  98. package/dist/commands/enterprise-util.d.ts +37 -0
  99. package/dist/commands/enterprise-util.d.ts.map +1 -0
  100. package/dist/commands/enterprise-util.js +185 -0
  101. package/dist/commands/enterprise-util.js.map +1 -0
  102. package/dist/commands/exec.d.ts +3 -0
  103. package/dist/commands/exec.d.ts.map +1 -0
  104. package/dist/commands/exec.js +68 -0
  105. package/dist/commands/exec.js.map +1 -0
  106. package/dist/commands/functions.d.ts +3 -0
  107. package/dist/commands/functions.d.ts.map +1 -0
  108. package/dist/commands/functions.js +47 -0
  109. package/dist/commands/functions.js.map +1 -0
  110. package/dist/commands/gha-runners.d.ts +3 -0
  111. package/dist/commands/gha-runners.d.ts.map +1 -0
  112. package/dist/commands/gha-runners.js +33 -0
  113. package/dist/commands/gha-runners.js.map +1 -0
  114. package/dist/commands/groups.d.ts +3 -0
  115. package/dist/commands/groups.d.ts.map +1 -0
  116. package/dist/commands/groups.js +38 -0
  117. package/dist/commands/groups.js.map +1 -0
  118. package/dist/commands/host.d.ts +3 -0
  119. package/dist/commands/host.d.ts.map +1 -0
  120. package/dist/commands/host.js +74 -0
  121. package/dist/commands/host.js.map +1 -0
  122. package/dist/commands/hosts.d.ts +3 -0
  123. package/dist/commands/hosts.d.ts.map +1 -0
  124. package/dist/commands/hosts.js +90 -0
  125. package/dist/commands/hosts.js.map +1 -0
  126. package/dist/commands/link.d.ts +8 -0
  127. package/dist/commands/link.d.ts.map +1 -0
  128. package/dist/commands/link.js +124 -0
  129. package/dist/commands/link.js.map +1 -0
  130. package/dist/commands/login.d.ts +3 -0
  131. package/dist/commands/login.d.ts.map +1 -0
  132. package/dist/commands/login.js +172 -0
  133. package/dist/commands/login.js.map +1 -0
  134. package/dist/commands/logout.d.ts +3 -0
  135. package/dist/commands/logout.d.ts.map +1 -0
  136. package/dist/commands/logout.js +17 -0
  137. package/dist/commands/logout.js.map +1 -0
  138. package/dist/commands/logs.d.ts +3 -0
  139. package/dist/commands/logs.d.ts.map +1 -0
  140. package/dist/commands/logs.js +94 -0
  141. package/dist/commands/logs.js.map +1 -0
  142. package/dist/commands/ls.d.ts +3 -0
  143. package/dist/commands/ls.d.ts.map +1 -0
  144. package/dist/commands/ls.js +67 -0
  145. package/dist/commands/ls.js.map +1 -0
  146. package/dist/commands/machines.d.ts +3 -0
  147. package/dist/commands/machines.d.ts.map +1 -0
  148. package/dist/commands/machines.js +29 -0
  149. package/dist/commands/machines.js.map +1 -0
  150. package/dist/commands/mcp.d.ts +21 -0
  151. package/dist/commands/mcp.d.ts.map +1 -0
  152. package/dist/commands/mcp.js +1021 -0
  153. package/dist/commands/mcp.js.map +1 -0
  154. package/dist/commands/meshes.d.ts +3 -0
  155. package/dist/commands/meshes.d.ts.map +1 -0
  156. package/dist/commands/meshes.js +27 -0
  157. package/dist/commands/meshes.js.map +1 -0
  158. package/dist/commands/network-policy.d.ts +3 -0
  159. package/dist/commands/network-policy.d.ts.map +1 -0
  160. package/dist/commands/network-policy.js +40 -0
  161. package/dist/commands/network-policy.js.map +1 -0
  162. package/dist/commands/project.d.ts +4 -0
  163. package/dist/commands/project.d.ts.map +1 -0
  164. package/dist/commands/project.js +25 -0
  165. package/dist/commands/project.js.map +1 -0
  166. package/dist/commands/pull.d.ts +3 -0
  167. package/dist/commands/pull.d.ts.map +1 -0
  168. package/dist/commands/pull.js +155 -0
  169. package/dist/commands/pull.js.map +1 -0
  170. package/dist/commands/regions.d.ts +3 -0
  171. package/dist/commands/regions.d.ts.map +1 -0
  172. package/dist/commands/regions.js +67 -0
  173. package/dist/commands/regions.js.map +1 -0
  174. package/dist/commands/releases.d.ts +3 -0
  175. package/dist/commands/releases.d.ts.map +1 -0
  176. package/dist/commands/releases.js +176 -0
  177. package/dist/commands/releases.js.map +1 -0
  178. package/dist/commands/rm.d.ts +3 -0
  179. package/dist/commands/rm.d.ts.map +1 -0
  180. package/dist/commands/rm.js +42 -0
  181. package/dist/commands/rm.js.map +1 -0
  182. package/dist/commands/run.d.ts +3 -0
  183. package/dist/commands/run.d.ts.map +1 -0
  184. package/dist/commands/run.js +131 -0
  185. package/dist/commands/run.js.map +1 -0
  186. package/dist/commands/sandbox.d.ts +3 -0
  187. package/dist/commands/sandbox.d.ts.map +1 -0
  188. package/dist/commands/sandbox.js +352 -0
  189. package/dist/commands/sandbox.js.map +1 -0
  190. package/dist/commands/schedules.d.ts +3 -0
  191. package/dist/commands/schedules.d.ts.map +1 -0
  192. package/dist/commands/schedules.js +37 -0
  193. package/dist/commands/schedules.js.map +1 -0
  194. package/dist/commands/secrets.d.ts +3 -0
  195. package/dist/commands/secrets.d.ts.map +1 -0
  196. package/dist/commands/secrets.js +194 -0
  197. package/dist/commands/secrets.js.map +1 -0
  198. package/dist/commands/services.d.ts +3 -0
  199. package/dist/commands/services.d.ts.map +1 -0
  200. package/dist/commands/services.js +70 -0
  201. package/dist/commands/services.js.map +1 -0
  202. package/dist/commands/shell.d.ts +16 -0
  203. package/dist/commands/shell.d.ts.map +1 -0
  204. package/dist/commands/shell.js +527 -0
  205. package/dist/commands/shell.js.map +1 -0
  206. package/dist/commands/snapshot.d.ts +10 -0
  207. package/dist/commands/snapshot.d.ts.map +1 -0
  208. package/dist/commands/snapshot.js +181 -0
  209. package/dist/commands/snapshot.js.map +1 -0
  210. package/dist/commands/ssh.d.ts +3 -0
  211. package/dist/commands/ssh.d.ts.map +1 -0
  212. package/dist/commands/ssh.js +37 -0
  213. package/dist/commands/ssh.js.map +1 -0
  214. package/dist/commands/status.d.ts +3 -0
  215. package/dist/commands/status.d.ts.map +1 -0
  216. package/dist/commands/status.js +300 -0
  217. package/dist/commands/status.js.map +1 -0
  218. package/dist/commands/storage.d.ts +3 -0
  219. package/dist/commands/storage.d.ts.map +1 -0
  220. package/dist/commands/storage.js +180 -0
  221. package/dist/commands/storage.js.map +1 -0
  222. package/dist/commands/tenant.d.ts +3 -0
  223. package/dist/commands/tenant.d.ts.map +1 -0
  224. package/dist/commands/tenant.js +87 -0
  225. package/dist/commands/tenant.js.map +1 -0
  226. package/dist/commands/tunnel.d.ts +3 -0
  227. package/dist/commands/tunnel.d.ts.map +1 -0
  228. package/dist/commands/tunnel.js +418 -0
  229. package/dist/commands/tunnel.js.map +1 -0
  230. package/dist/commands/up.d.ts +14 -0
  231. package/dist/commands/up.d.ts.map +1 -0
  232. package/dist/commands/up.js +703 -0
  233. package/dist/commands/up.js.map +1 -0
  234. package/dist/commands/util.d.ts +19 -0
  235. package/dist/commands/util.d.ts.map +1 -0
  236. package/dist/commands/util.js +116 -0
  237. package/dist/commands/util.js.map +1 -0
  238. package/dist/commands/volumes.d.ts +3 -0
  239. package/dist/commands/volumes.d.ts.map +1 -0
  240. package/dist/commands/volumes.js +196 -0
  241. package/dist/commands/volumes.js.map +1 -0
  242. package/dist/commands/watch.d.ts +3 -0
  243. package/dist/commands/watch.d.ts.map +1 -0
  244. package/dist/commands/watch.js +398 -0
  245. package/dist/commands/watch.js.map +1 -0
  246. package/dist/commands/webhooks.d.ts +3 -0
  247. package/dist/commands/webhooks.d.ts.map +1 -0
  248. package/dist/commands/webhooks.js +23 -0
  249. package/dist/commands/webhooks.js.map +1 -0
  250. package/dist/commands/whoami.d.ts +3 -0
  251. package/dist/commands/whoami.d.ts.map +1 -0
  252. package/dist/commands/whoami.js +84 -0
  253. package/dist/commands/whoami.js.map +1 -0
  254. package/dist/commands/workspaces.d.ts +3 -0
  255. package/dist/commands/workspaces.d.ts.map +1 -0
  256. package/dist/commands/workspaces.js +87 -0
  257. package/dist/commands/workspaces.js.map +1 -0
  258. package/dist/config.d.ts +28 -0
  259. package/dist/config.d.ts.map +1 -0
  260. package/dist/config.js +129 -0
  261. package/dist/config.js.map +1 -0
  262. package/dist/errors.d.ts +22 -0
  263. package/dist/errors.d.ts.map +1 -0
  264. package/dist/errors.js +62 -0
  265. package/dist/errors.js.map +1 -0
  266. package/dist/framework-detector.d.ts +22 -0
  267. package/dist/framework-detector.d.ts.map +1 -0
  268. package/dist/framework-detector.js +373 -0
  269. package/dist/framework-detector.js.map +1 -0
  270. package/dist/pty/raw-mode.d.ts +7 -0
  271. package/dist/pty/raw-mode.d.ts.map +1 -0
  272. package/dist/pty/raw-mode.js +22 -0
  273. package/dist/pty/raw-mode.js.map +1 -0
  274. package/dist/pty/ws-pty-client.d.ts +12 -0
  275. package/dist/pty/ws-pty-client.d.ts.map +1 -0
  276. package/dist/pty/ws-pty-client.js +69 -0
  277. package/dist/pty/ws-pty-client.js.map +1 -0
  278. package/dist/types.d.ts +326 -0
  279. package/dist/types.d.ts.map +1 -0
  280. package/dist/types.js +16 -0
  281. package/dist/types.js.map +1 -0
  282. package/dist/ui/progress.d.ts +10 -0
  283. package/dist/ui/progress.d.ts.map +1 -0
  284. package/dist/ui/progress.js +36 -0
  285. package/dist/ui/progress.js.map +1 -0
  286. package/dist/ui/spinner.d.ts +4 -0
  287. package/dist/ui/spinner.d.ts.map +1 -0
  288. package/dist/ui/spinner.js +7 -0
  289. package/dist/ui/spinner.js.map +1 -0
  290. package/dist/ui/table.d.ts +8 -0
  291. package/dist/ui/table.d.ts.map +1 -0
  292. package/dist/ui/table.js +46 -0
  293. package/dist/ui/table.js.map +1 -0
  294. package/package.json +53 -0
@@ -0,0 +1,1021 @@
1
+ /**
2
+ * miosa mcp serve — expose every major CLI command as an MCP server over stdio.
3
+ *
4
+ * Protocol: JSON-RPC 2.0 over stdin/stdout (newline-delimited).
5
+ * initialize → server info + capabilities
6
+ * tools/list → all tool schemas
7
+ * tools/call → dispatch to MIOSA API
8
+ *
9
+ * Add to .claude/mcp.json:
10
+ * {
11
+ * "mcpServers": {
12
+ * "miosa": {
13
+ * "command": "miosa",
14
+ * "args": ["mcp", "serve"]
15
+ * }
16
+ * }
17
+ * }
18
+ */
19
+ import { createInterface } from "node:readline";
20
+ import { spawn, spawnSync } from "node:child_process";
21
+ import { request } from "undici";
22
+ import chalk from "chalk";
23
+ import { loadConfig } from "../config.js";
24
+ import { MiosaClient } from "../client.js";
25
+ // ── Tool definitions — mirror the Python MCP server exactly ─────────────────
26
+ const TOOL_LIST = [
27
+ // Lifecycle
28
+ {
29
+ name: "computer_create",
30
+ description: "Create a new MIOSA computer and wait until it is active. Returns the computer ID. The new computer becomes the active computer for subsequent calls.",
31
+ inputSchema: {
32
+ type: "object",
33
+ properties: {
34
+ name: {
35
+ type: "string",
36
+ description: "Human-readable name for the computer",
37
+ },
38
+ template_type: {
39
+ type: "string",
40
+ description: "Template to boot from (default: miosa-desktop)",
41
+ default: "miosa-desktop",
42
+ },
43
+ size: {
44
+ type: "string",
45
+ enum: ["small", "medium", "large", "xl"],
46
+ description: "VM size (default: small)",
47
+ default: "small",
48
+ },
49
+ workspace_id: {
50
+ type: "string",
51
+ description: "Workspace ID to assign the computer to (optional)",
52
+ },
53
+ external_workspace_id: {
54
+ type: "string",
55
+ description: "Your internal workspace ID for attribution (optional)",
56
+ },
57
+ external_project_id: {
58
+ type: "string",
59
+ description: "Your internal project ID for attribution (optional)",
60
+ },
61
+ },
62
+ required: ["name"],
63
+ },
64
+ },
65
+ {
66
+ name: "computer_list",
67
+ description: "List all computers in the tenant.",
68
+ inputSchema: { type: "object", properties: {} },
69
+ },
70
+ {
71
+ name: "computer_destroy",
72
+ description: "Permanently destroy a computer.",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ computer_id: {
77
+ type: "string",
78
+ description: "ID of the computer to destroy.",
79
+ },
80
+ },
81
+ required: ["computer_id"],
82
+ },
83
+ },
84
+ // Screenshot
85
+ {
86
+ name: "computer_screenshot",
87
+ description: "Capture a PNG screenshot of the computer desktop. Returns the image so you can see the current screen state.",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {
91
+ computer_id: { type: "string", description: "Computer ID." },
92
+ },
93
+ required: ["computer_id"],
94
+ },
95
+ },
96
+ // Pointer
97
+ {
98
+ name: "computer_click",
99
+ description: "Click a mouse button at the given screen coordinates.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ computer_id: { type: "string", description: "Computer ID." },
104
+ x: { type: "integer", description: "X coordinate in pixels" },
105
+ y: { type: "integer", description: "Y coordinate in pixels" },
106
+ button: {
107
+ type: "string",
108
+ enum: ["left", "right", "middle"],
109
+ description: "Mouse button (default: left)",
110
+ default: "left",
111
+ },
112
+ },
113
+ required: ["computer_id", "x", "y"],
114
+ },
115
+ },
116
+ {
117
+ name: "computer_double_click",
118
+ description: "Double-click at the given screen coordinates.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ computer_id: { type: "string" },
123
+ x: { type: "integer" },
124
+ y: { type: "integer" },
125
+ },
126
+ required: ["computer_id", "x", "y"],
127
+ },
128
+ },
129
+ {
130
+ name: "computer_move_cursor",
131
+ description: "Move the mouse cursor to the given coordinates without clicking.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ computer_id: { type: "string" },
136
+ x: { type: "integer" },
137
+ y: { type: "integer" },
138
+ },
139
+ required: ["computer_id", "x", "y"],
140
+ },
141
+ },
142
+ {
143
+ name: "computer_drag",
144
+ description: "Click-and-drag from one screen position to another.",
145
+ inputSchema: {
146
+ type: "object",
147
+ properties: {
148
+ computer_id: { type: "string" },
149
+ from_x: { type: "integer" },
150
+ from_y: { type: "integer" },
151
+ to_x: { type: "integer" },
152
+ to_y: { type: "integer" },
153
+ },
154
+ required: ["computer_id", "from_x", "from_y", "to_x", "to_y"],
155
+ },
156
+ },
157
+ {
158
+ name: "computer_scroll",
159
+ description: "Scroll in a direction on the desktop.",
160
+ inputSchema: {
161
+ type: "object",
162
+ properties: {
163
+ computer_id: { type: "string" },
164
+ direction: {
165
+ type: "string",
166
+ enum: ["up", "down", "left", "right"],
167
+ default: "down",
168
+ },
169
+ clicks: {
170
+ type: "integer",
171
+ description: "Number of scroll detents (default: 3)",
172
+ default: 3,
173
+ },
174
+ x: { type: "integer", description: "Optional X position for scroll" },
175
+ y: { type: "integer", description: "Optional Y position for scroll" },
176
+ },
177
+ required: ["computer_id"],
178
+ },
179
+ },
180
+ // Keyboard
181
+ {
182
+ name: "computer_type",
183
+ description: "Type text into the currently focused field.",
184
+ inputSchema: {
185
+ type: "object",
186
+ properties: {
187
+ computer_id: { type: "string" },
188
+ text: { type: "string", description: "Text to type" },
189
+ },
190
+ required: ["computer_id", "text"],
191
+ },
192
+ },
193
+ {
194
+ name: "computer_key",
195
+ description: "Press a single key. Use standard key names: Return, Tab, Escape, BackSpace, Delete, space, F1-F12, ctrl, shift, alt, super.",
196
+ inputSchema: {
197
+ type: "object",
198
+ properties: {
199
+ computer_id: { type: "string" },
200
+ key: { type: "string", description: "Key name to press" },
201
+ },
202
+ required: ["computer_id", "key"],
203
+ },
204
+ },
205
+ {
206
+ name: "computer_hotkey",
207
+ description: "Press a keyboard shortcut (multiple keys simultaneously). Example: ['ctrl', 'c'] for copy.",
208
+ inputSchema: {
209
+ type: "object",
210
+ properties: {
211
+ computer_id: { type: "string" },
212
+ keys: {
213
+ type: "array",
214
+ items: { type: "string" },
215
+ description: "Keys to press together, e.g. ['ctrl', 'c']",
216
+ },
217
+ },
218
+ required: ["computer_id", "keys"],
219
+ },
220
+ },
221
+ // Display info
222
+ {
223
+ name: "computer_get_screen_size",
224
+ description: "Get the screen resolution (width and height in pixels).",
225
+ inputSchema: {
226
+ type: "object",
227
+ properties: {
228
+ computer_id: { type: "string" },
229
+ },
230
+ required: ["computer_id"],
231
+ },
232
+ },
233
+ {
234
+ name: "computer_get_cursor_position",
235
+ description: "Get the current mouse cursor position (x, y).",
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ computer_id: { type: "string" },
240
+ },
241
+ required: ["computer_id"],
242
+ },
243
+ },
244
+ // Clipboard
245
+ {
246
+ name: "computer_get_clipboard",
247
+ description: "Read the current clipboard text content.",
248
+ inputSchema: {
249
+ type: "object",
250
+ properties: {
251
+ computer_id: { type: "string" },
252
+ },
253
+ required: ["computer_id"],
254
+ },
255
+ },
256
+ {
257
+ name: "computer_set_clipboard",
258
+ description: "Set the clipboard text content.",
259
+ inputSchema: {
260
+ type: "object",
261
+ properties: {
262
+ computer_id: { type: "string" },
263
+ text: { type: "string", description: "Text to put in clipboard" },
264
+ },
265
+ required: ["computer_id", "text"],
266
+ },
267
+ },
268
+ // Window management
269
+ {
270
+ name: "computer_windows",
271
+ description: "List all open windows on the desktop with their IDs, titles, and positions.",
272
+ inputSchema: {
273
+ type: "object",
274
+ properties: {
275
+ computer_id: { type: "string" },
276
+ },
277
+ required: ["computer_id"],
278
+ },
279
+ },
280
+ {
281
+ name: "computer_launch",
282
+ description: "Launch an application by name (e.g. 'firefox', 'gedit', 'xterm').",
283
+ inputSchema: {
284
+ type: "object",
285
+ properties: {
286
+ computer_id: { type: "string" },
287
+ app: { type: "string", description: "Application name to launch" },
288
+ },
289
+ required: ["computer_id", "app"],
290
+ },
291
+ },
292
+ // Shell & Files
293
+ {
294
+ name: "computer_bash",
295
+ description: "Execute a bash command on the computer and return stdout + stderr. Commands run as the default user inside the VM.",
296
+ inputSchema: {
297
+ type: "object",
298
+ properties: {
299
+ computer_id: { type: "string" },
300
+ command: { type: "string", description: "Bash command to run" },
301
+ timeout: {
302
+ type: "integer",
303
+ description: "Timeout in seconds (optional)",
304
+ },
305
+ },
306
+ required: ["computer_id", "command"],
307
+ },
308
+ },
309
+ {
310
+ name: "computer_write_file",
311
+ description: "Write text content to a file path inside the computer.",
312
+ inputSchema: {
313
+ type: "object",
314
+ properties: {
315
+ computer_id: { type: "string" },
316
+ path: { type: "string", description: "Absolute path inside the VM" },
317
+ content: { type: "string", description: "File content to write" },
318
+ },
319
+ required: ["computer_id", "path", "content"],
320
+ },
321
+ },
322
+ {
323
+ name: "computer_read_file",
324
+ description: "Read a file from inside the computer and return its content as text.",
325
+ inputSchema: {
326
+ type: "object",
327
+ properties: {
328
+ computer_id: { type: "string" },
329
+ path: { type: "string", description: "Absolute path inside the VM" },
330
+ },
331
+ required: ["computer_id", "path"],
332
+ },
333
+ },
334
+ // Sandboxes
335
+ {
336
+ name: "sandbox_create",
337
+ description: "Create a new lightweight code sandbox (Firecracker microVM without desktop).",
338
+ inputSchema: {
339
+ type: "object",
340
+ properties: {
341
+ name: {
342
+ type: "string",
343
+ description: "Human-readable name for the sandbox",
344
+ },
345
+ template_id: {
346
+ type: "string",
347
+ description: "Template / image ID (default: miosa-sandbox)",
348
+ },
349
+ cpu_count: { type: "integer", description: "vCPU count" },
350
+ memory_mb: { type: "integer", description: "Memory in MB" },
351
+ timeout_sec: {
352
+ type: "integer",
353
+ description: "Idle timeout in seconds",
354
+ },
355
+ },
356
+ },
357
+ },
358
+ {
359
+ name: "sandbox_exec",
360
+ description: "Run a bash command inside a sandbox and return the output.",
361
+ inputSchema: {
362
+ type: "object",
363
+ properties: {
364
+ sandbox_id: { type: "string", description: "Sandbox ID" },
365
+ command: { type: "string", description: "Command to run" },
366
+ cwd: { type: "string", description: "Working directory (optional)" },
367
+ timeout: {
368
+ type: "integer",
369
+ description: "Timeout in seconds (optional)",
370
+ },
371
+ },
372
+ required: ["sandbox_id", "command"],
373
+ },
374
+ },
375
+ {
376
+ name: "sandbox_destroy",
377
+ description: "Destroy a sandbox permanently.",
378
+ inputSchema: {
379
+ type: "object",
380
+ properties: {
381
+ sandbox_id: { type: "string", description: "Sandbox ID to destroy" },
382
+ },
383
+ required: ["sandbox_id"],
384
+ },
385
+ },
386
+ // Deploy
387
+ {
388
+ name: "deploy",
389
+ description: "Trigger a redeploy for an existing MIOSA deployment.",
390
+ inputSchema: {
391
+ type: "object",
392
+ properties: {
393
+ deployment_id: {
394
+ type: "string",
395
+ description: "Deployment ID to redeploy",
396
+ },
397
+ },
398
+ required: ["deployment_id"],
399
+ },
400
+ },
401
+ ];
402
+ // ── Wire helpers ─────────────────────────────────────────────────────────────
403
+ function ok(text) {
404
+ return { content: [{ type: "text", text }] };
405
+ }
406
+ function err(msg) {
407
+ return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
408
+ }
409
+ function image(pngBytes) {
410
+ return {
411
+ content: [
412
+ {
413
+ type: "image",
414
+ data: pngBytes.toString("base64"),
415
+ mimeType: "image/png",
416
+ },
417
+ ],
418
+ };
419
+ }
420
+ function send(response) {
421
+ process.stdout.write(JSON.stringify(response) + "\n");
422
+ }
423
+ // ── Tool dispatch ────────────────────────────────────────────────────────────
424
+ async function dispatchTool(client, name, args) {
425
+ const cid = typeof args["computer_id"] === "string" ? args["computer_id"] : undefined;
426
+ try {
427
+ // ── Lifecycle ──────────────────────────────────────────────────────────
428
+ if (name === "computer_create") {
429
+ const body = { name: args["name"] };
430
+ if (args["template_type"])
431
+ body["template_type"] = args["template_type"];
432
+ if (args["size"])
433
+ body["size"] = args["size"];
434
+ if (args["workspace_id"])
435
+ body["workspace_id"] = args["workspace_id"];
436
+ if (args["external_workspace_id"])
437
+ body["external_workspace_id"] = args["external_workspace_id"];
438
+ if (args["external_project_id"])
439
+ body["external_project_id"] = args["external_project_id"];
440
+ const computer = await client.apiPost("/api/v1/computers", body);
441
+ const data = unwrapData(computer);
442
+ const id = String(data["id"] ?? "");
443
+ const compName = String(data["name"] ?? args["name"]);
444
+ // Poll until the computer reaches "active" state (mirrors Python MCP behaviour)
445
+ const POLL_INTERVAL_MS = 2_000;
446
+ const POLL_TIMEOUT_MS = 30_000;
447
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
448
+ let finalStatus = String(data["status"] ?? data["state"] ?? "created");
449
+ while (finalStatus !== "active" && Date.now() < deadline) {
450
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
451
+ try {
452
+ const poll = await client.apiGet(`/api/v1/computers/${encodeURIComponent(id)}`);
453
+ const pollData = unwrapData(poll);
454
+ finalStatus = String(pollData["status"] ?? pollData["state"] ?? finalStatus);
455
+ }
456
+ catch {
457
+ // Transient error during poll — keep waiting
458
+ }
459
+ }
460
+ return ok(`Created computer '${compName}' (id=${id}, status=${finalStatus}).`);
461
+ }
462
+ if (name === "computer_list") {
463
+ const result = await client.apiGet("/api/v1/computers");
464
+ const items = listOf(result);
465
+ if (items.length === 0)
466
+ return ok("No computers found.");
467
+ const lines = ["Available computers:"];
468
+ for (const c of items) {
469
+ const r = c;
470
+ lines.push(` ${r["id"]} ${r["name"]} ${r["status"] ?? r["state"]}`);
471
+ }
472
+ return ok(lines.join("\n"));
473
+ }
474
+ if (name === "computer_destroy") {
475
+ if (!cid)
476
+ return err("computer_id is required");
477
+ await client.apiDelete(`/api/v1/computers/${encodeURIComponent(cid)}`);
478
+ return ok(`Computer ${cid} destroyed.`);
479
+ }
480
+ // ── Screenshot ────────────────────────────────────────────────────────
481
+ if (name === "computer_screenshot") {
482
+ if (!cid)
483
+ return err("computer_id is required");
484
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/screenshot`);
485
+ // The API returns { data: { image: "<base64>", format: "png" } }
486
+ const data = unwrapData(result);
487
+ const b64 = typeof data["image"] === "string" ? data["image"] : null;
488
+ if (!b64)
489
+ return err("Screenshot API returned no image data");
490
+ return image(Buffer.from(b64, "base64"));
491
+ }
492
+ // ── Pointer ───────────────────────────────────────────────────────────
493
+ if (name === "computer_click") {
494
+ if (!cid)
495
+ return err("computer_id is required");
496
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/click`, {
497
+ x: args["x"],
498
+ y: args["y"],
499
+ button: args["button"] ?? "left",
500
+ });
501
+ return ok(`Clicked (${args["x"]}, ${args["y"]}) button=${args["button"] ?? "left"}`);
502
+ }
503
+ if (name === "computer_double_click") {
504
+ if (!cid)
505
+ return err("computer_id is required");
506
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/double-click`, {
507
+ x: args["x"],
508
+ y: args["y"],
509
+ });
510
+ return ok(`Double-clicked (${args["x"]}, ${args["y"]})`);
511
+ }
512
+ if (name === "computer_move_cursor") {
513
+ if (!cid)
514
+ return err("computer_id is required");
515
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/move`, {
516
+ x: args["x"],
517
+ y: args["y"],
518
+ });
519
+ return ok(`Moved cursor to (${args["x"]}, ${args["y"]})`);
520
+ }
521
+ if (name === "computer_drag") {
522
+ if (!cid)
523
+ return err("computer_id is required");
524
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/drag`, {
525
+ from_x: args["from_x"],
526
+ from_y: args["from_y"],
527
+ to_x: args["to_x"],
528
+ to_y: args["to_y"],
529
+ });
530
+ return ok(`Dragged from (${args["from_x"]}, ${args["from_y"]}) to (${args["to_x"]}, ${args["to_y"]})`);
531
+ }
532
+ if (name === "computer_scroll") {
533
+ if (!cid)
534
+ return err("computer_id is required");
535
+ const body = {
536
+ direction: args["direction"] ?? "down",
537
+ clicks: args["clicks"] ?? 3,
538
+ };
539
+ if (args["x"] !== undefined)
540
+ body["x"] = args["x"];
541
+ if (args["y"] !== undefined)
542
+ body["y"] = args["y"];
543
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/scroll`, body);
544
+ return ok(`Scrolled ${body["direction"]} by ${body["clicks"]} clicks`);
545
+ }
546
+ // ── Keyboard ──────────────────────────────────────────────────────────
547
+ if (name === "computer_type") {
548
+ if (!cid)
549
+ return err("computer_id is required");
550
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/type`, {
551
+ text: args["text"],
552
+ });
553
+ const preview = typeof args["text"] === "string"
554
+ ? args["text"].slice(0, 40) + (args["text"].length > 40 ? "..." : "")
555
+ : "";
556
+ return ok(`Typed: ${JSON.stringify(preview)}`);
557
+ }
558
+ if (name === "computer_key") {
559
+ if (!cid)
560
+ return err("computer_id is required");
561
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/key`, {
562
+ key: args["key"],
563
+ });
564
+ return ok(`Pressed key: ${args["key"]}`);
565
+ }
566
+ if (name === "computer_hotkey") {
567
+ if (!cid)
568
+ return err("computer_id is required");
569
+ const keys = args["keys"];
570
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/hotkey`, {
571
+ keys,
572
+ });
573
+ return ok(`Pressed hotkey: ${keys.join("+")}`);
574
+ }
575
+ // ── Display info ──────────────────────────────────────────────────────
576
+ if (name === "computer_get_screen_size") {
577
+ if (!cid)
578
+ return err("computer_id is required");
579
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/screen-size`);
580
+ const data = unwrapData(result);
581
+ return ok(`Screen size: ${data["width"]}x${data["height"]} px`);
582
+ }
583
+ if (name === "computer_get_cursor_position") {
584
+ if (!cid)
585
+ return err("computer_id is required");
586
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/cursor`);
587
+ const data = unwrapData(result);
588
+ return ok(`Cursor position: x=${data["x"]}, y=${data["y"]}`);
589
+ }
590
+ // ── Clipboard ─────────────────────────────────────────────────────────
591
+ if (name === "computer_get_clipboard") {
592
+ if (!cid)
593
+ return err("computer_id is required");
594
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/clipboard`);
595
+ const data = unwrapData(result);
596
+ return ok(`Clipboard content:\n${data["text"] ?? ""}`);
597
+ }
598
+ if (name === "computer_set_clipboard") {
599
+ if (!cid)
600
+ return err("computer_id is required");
601
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/clipboard`, {
602
+ text: args["text"],
603
+ });
604
+ return ok("Clipboard updated.");
605
+ }
606
+ // ── Window management ─────────────────────────────────────────────────
607
+ if (name === "computer_windows") {
608
+ if (!cid)
609
+ return err("computer_id is required");
610
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/windows`);
611
+ const windows = listOf(result);
612
+ if (windows.length === 0)
613
+ return ok("No open windows found.");
614
+ const lines = ["Open windows:"];
615
+ for (const w of windows) {
616
+ const r = w;
617
+ const focused = r["focused"] ? " [focused]" : "";
618
+ lines.push(` id=${r["id"]} title=${JSON.stringify(r["title"])} app=${JSON.stringify(r["app"])}` +
619
+ ` pos=(${r["x"]},${r["y"]}) size=${r["width"]}x${r["height"]}${focused}`);
620
+ }
621
+ return ok(lines.join("\n"));
622
+ }
623
+ if (name === "computer_launch") {
624
+ if (!cid)
625
+ return err("computer_id is required");
626
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/launch`, {
627
+ app: args["app"],
628
+ });
629
+ return ok(`Launched: ${args["app"]}`);
630
+ }
631
+ // ── Shell & Files ─────────────────────────────────────────────────────
632
+ if (name === "computer_bash") {
633
+ if (!cid)
634
+ return err("computer_id is required");
635
+ const body = { command: args["command"] };
636
+ if (args["timeout"] !== undefined)
637
+ body["timeout"] = args["timeout"];
638
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/exec`, body);
639
+ const data = unwrapData(result);
640
+ const parts = [];
641
+ if (data["output"] ?? data["stdout"]) {
642
+ parts.push(`stdout:\n${data["output"] ?? data["stdout"]}`);
643
+ }
644
+ if (data["stderr"])
645
+ parts.push(`stderr:\n${data["stderr"]}`);
646
+ parts.push(`exit_code: ${data["exit_code"] ?? 0}`);
647
+ return ok(parts.join("\n"));
648
+ }
649
+ if (name === "computer_write_file") {
650
+ if (!cid)
651
+ return err("computer_id is required");
652
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/files/write`, {
653
+ path: args["path"],
654
+ content: args["content"],
655
+ encoding: "utf8",
656
+ });
657
+ const len = typeof args["content"] === "string" ? args["content"].length : 0;
658
+ return ok(`Wrote ${len} bytes to ${args["path"]}`);
659
+ }
660
+ if (name === "computer_read_file") {
661
+ if (!cid)
662
+ return err("computer_id is required");
663
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/files/download?path=${encodeURIComponent(String(args["path"] ?? ""))}`);
664
+ const data = unwrapData(result);
665
+ const content = typeof data["content"] === "string"
666
+ ? Buffer.from(data["content"], "base64").toString("utf8")
667
+ : typeof data["text"] === "string"
668
+ ? data["text"]
669
+ : JSON.stringify(data);
670
+ return ok(content);
671
+ }
672
+ // ── Sandboxes ─────────────────────────────────────────────────────────
673
+ if (name === "sandbox_create") {
674
+ const body = {};
675
+ if (args["name"])
676
+ body["name"] = args["name"];
677
+ if (args["template_id"])
678
+ body["template_id"] = args["template_id"];
679
+ if (args["cpu_count"] !== undefined)
680
+ body["cpu_count"] = args["cpu_count"];
681
+ if (args["memory_mb"] !== undefined)
682
+ body["memory_mb"] = args["memory_mb"];
683
+ if (args["timeout_sec"] !== undefined)
684
+ body["timeout_sec"] = args["timeout_sec"];
685
+ const result = await client.apiPost("/api/v1/sandboxes", body);
686
+ const data = unwrapData(result);
687
+ const sid = String(data["id"] ?? "");
688
+ return ok(`Created sandbox '${data["name"] ?? sid}' (id=${sid}).`);
689
+ }
690
+ if (name === "sandbox_exec") {
691
+ const sid = String(args["sandbox_id"] ?? "");
692
+ if (!sid)
693
+ return err("sandbox_id is required");
694
+ const body = { command: args["command"] };
695
+ if (args["cwd"])
696
+ body["cwd"] = args["cwd"];
697
+ if (args["timeout"] !== undefined)
698
+ body["timeout"] = args["timeout"];
699
+ const result = await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/exec`, body);
700
+ const data = unwrapData(result);
701
+ const parts = [];
702
+ if (data["output"] ?? data["stdout"]) {
703
+ parts.push(`stdout:\n${data["output"] ?? data["stdout"]}`);
704
+ }
705
+ if (data["stderr"])
706
+ parts.push(`stderr:\n${data["stderr"]}`);
707
+ parts.push(`exit_code: ${data["exit_code"] ?? 0}`);
708
+ return ok(parts.join("\n"));
709
+ }
710
+ if (name === "sandbox_destroy") {
711
+ const sid = String(args["sandbox_id"] ?? "");
712
+ if (!sid)
713
+ return err("sandbox_id is required");
714
+ await client.apiDelete(`/api/v1/sandboxes/${encodeURIComponent(sid)}`);
715
+ return ok(`Sandbox ${sid} destroyed.`);
716
+ }
717
+ // ── Deploy ────────────────────────────────────────────────────────────
718
+ if (name === "deploy") {
719
+ const did = String(args["deployment_id"] ?? "");
720
+ if (!did)
721
+ return err("deployment_id is required");
722
+ const result = await client.apiPost(`/api/v1/deployments/${encodeURIComponent(did)}/redeploy`, {});
723
+ const data = unwrapData(result);
724
+ return ok(`Redeploy queued (build id: ${data["id"] ?? "unknown"})`);
725
+ }
726
+ return err(`Unknown tool: ${name}`);
727
+ }
728
+ catch (e) {
729
+ const msg = e instanceof Error ? e.message : String(e);
730
+ return err(msg);
731
+ }
732
+ }
733
+ // ── Payload helpers ───────────────────────────────────────────────────────────
734
+ function unwrapData(payload) {
735
+ if (payload !== null &&
736
+ typeof payload === "object" &&
737
+ !Array.isArray(payload) &&
738
+ "data" in payload) {
739
+ return payload["data"];
740
+ }
741
+ return payload;
742
+ }
743
+ function listOf(payload) {
744
+ if (Array.isArray(payload))
745
+ return payload;
746
+ if (payload !== null &&
747
+ typeof payload === "object" &&
748
+ !Array.isArray(payload)) {
749
+ const r = payload;
750
+ if (Array.isArray(r["data"]))
751
+ return r["data"];
752
+ }
753
+ return [];
754
+ }
755
+ // ── MCP server loop ───────────────────────────────────────────────────────────
756
+ async function runServer() {
757
+ const config = loadConfig();
758
+ if (!config.api_key) {
759
+ process.stderr.write("Error: MIOSA_API_KEY is not set and no saved auth found.\n" +
760
+ "Set MIOSA_API_KEY env var or run: miosa login\n");
761
+ process.exit(1);
762
+ }
763
+ const client = new MiosaClient(config);
764
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
765
+ for await (const line of rl) {
766
+ const trimmed = line.trim();
767
+ if (!trimmed)
768
+ continue;
769
+ let req;
770
+ try {
771
+ req = JSON.parse(trimmed);
772
+ }
773
+ catch {
774
+ send({
775
+ jsonrpc: "2.0",
776
+ id: null,
777
+ error: { code: -32700, message: "Parse error" },
778
+ });
779
+ continue;
780
+ }
781
+ const id = req.id ?? null;
782
+ switch (req.method) {
783
+ case "initialize": {
784
+ send({
785
+ jsonrpc: "2.0",
786
+ id,
787
+ result: {
788
+ protocolVersion: "2024-11-05",
789
+ capabilities: { tools: {} },
790
+ serverInfo: { name: "miosa-mcp", version: "0.1.1" },
791
+ },
792
+ });
793
+ break;
794
+ }
795
+ case "notifications/initialized":
796
+ // No response required for notifications
797
+ break;
798
+ case "tools/list": {
799
+ send({
800
+ jsonrpc: "2.0",
801
+ id,
802
+ result: { tools: TOOL_LIST },
803
+ });
804
+ break;
805
+ }
806
+ case "tools/call": {
807
+ const params = req.params;
808
+ const toolName = String(params?.["name"] ?? "");
809
+ const toolArgs = (params?.["arguments"] ?? {});
810
+ const toolResult = await dispatchTool(client, toolName, toolArgs);
811
+ send({ jsonrpc: "2.0", id, result: toolResult });
812
+ break;
813
+ }
814
+ case "ping": {
815
+ send({ jsonrpc: "2.0", id, result: {} });
816
+ break;
817
+ }
818
+ default: {
819
+ send({
820
+ jsonrpc: "2.0",
821
+ id,
822
+ error: { code: -32601, message: `Method not found: ${req.method}` },
823
+ });
824
+ }
825
+ }
826
+ }
827
+ }
828
+ const MCP_REMOTE_URL = "https://api.miosa.ai/api/v1/mcp";
829
+ const MCP_SERVER_NAME = "miosa";
830
+ function sleep(ms) {
831
+ return new Promise((resolve) => setTimeout(resolve, ms));
832
+ }
833
+ function openUrl(url) {
834
+ const command = process.platform === "darwin"
835
+ ? "open"
836
+ : process.platform === "win32"
837
+ ? "cmd"
838
+ : "xdg-open";
839
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
840
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
841
+ child.unref();
842
+ }
843
+ async function postJson(endpoint, path, body) {
844
+ const res = await request(`${endpoint.replace(/\/$/, "")}${path}`, {
845
+ method: "POST",
846
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
847
+ body: JSON.stringify(body),
848
+ });
849
+ const text = await res.body.text();
850
+ const parsed = text ? JSON.parse(text) : {};
851
+ return { status: res.statusCode, body: parsed };
852
+ }
853
+ async function runDeviceFlow(endpoint, clientName) {
854
+ const start = await postJson(endpoint, "/api/v1/auth/cli/start", {
855
+ client_name: clientName,
856
+ });
857
+ if (start.status >= 400) {
858
+ throw new Error(`Failed to start auth flow (HTTP ${start.status})`);
859
+ }
860
+ const flow = start.body;
861
+ console.log();
862
+ console.log(chalk.bold("Authorize MIOSA MCP for this device"));
863
+ console.log();
864
+ console.log(` Open: ${chalk.cyan(flow.verification_uri_complete)}`);
865
+ console.log(` Code: ${chalk.bold(flow.user_code)}`);
866
+ console.log();
867
+ try {
868
+ openUrl(flow.verification_uri_complete);
869
+ console.log(chalk.dim(" Browser opened. Waiting for approval..."));
870
+ }
871
+ catch {
872
+ console.log(chalk.dim(" Could not open a browser automatically."));
873
+ }
874
+ const deadline = Date.now() + flow.expires_in * 1000;
875
+ const intervalMs = Math.max(flow.interval || 3, 1) * 1000;
876
+ while (Date.now() < deadline) {
877
+ await sleep(intervalMs);
878
+ const poll = await postJson(endpoint, "/api/v1/auth/cli/token", { device_code: flow.device_code });
879
+ if (poll.status === 200 && poll.body.api_key) {
880
+ return poll.body.api_key;
881
+ }
882
+ if (poll.status === 428 || poll.body.error === "authorization_pending") {
883
+ continue;
884
+ }
885
+ if (poll.body.error === "access_denied") {
886
+ throw new Error("Login was denied in the browser.");
887
+ }
888
+ if (poll.body.error === "expired_token" || poll.status === 410) {
889
+ throw new Error("Login request expired. Run the command again.");
890
+ }
891
+ throw new Error(`Login failed: ${poll.body.error ?? `HTTP ${poll.status}`}`);
892
+ }
893
+ throw new Error("Login timed out. Run the command again.");
894
+ }
895
+ function wireClaudeCode(apiKey, remoteUrl, scope) {
896
+ // `claude mcp remove` first so re-running install replaces cleanly.
897
+ spawnSync("claude", ["mcp", "remove", MCP_SERVER_NAME, "--scope", scope], {
898
+ stdio: "ignore",
899
+ });
900
+ const result = spawnSync("claude", [
901
+ "mcp",
902
+ "add",
903
+ "--transport",
904
+ "http",
905
+ "--scope",
906
+ scope,
907
+ MCP_SERVER_NAME,
908
+ remoteUrl,
909
+ "--header",
910
+ `Authorization: Bearer ${apiKey}`,
911
+ ], { stdio: "pipe", encoding: "utf8" });
912
+ if (result.error) {
913
+ return {
914
+ ok: false,
915
+ reason: `Could not run \`claude\` CLI: ${result.error.message}`,
916
+ };
917
+ }
918
+ if (typeof result.status === "number" && result.status !== 0) {
919
+ return {
920
+ ok: false,
921
+ reason: result.stderr?.trim() || `claude mcp add exited ${result.status}`,
922
+ };
923
+ }
924
+ return { ok: true };
925
+ }
926
+ function printManualSnippet(client, apiKey, remoteUrl) {
927
+ const masked = apiKey.length > 12
928
+ ? apiKey.slice(0, 6) + "…" + apiKey.slice(-4)
929
+ : "msk_u_…";
930
+ console.log();
931
+ console.log(chalk.bold("Manual install snippet"));
932
+ console.log(chalk.dim(` (your key: ${masked} — saved to ~/.miosa/config.json)`));
933
+ console.log();
934
+ if (client === "cursor") {
935
+ console.log(chalk.dim(" Add to ~/.cursor/mcp.json:"));
936
+ console.log();
937
+ console.log(JSON.stringify({
938
+ mcpServers: {
939
+ miosa: {
940
+ transport: "http",
941
+ url: remoteUrl,
942
+ headers: { Authorization: `Bearer ${apiKey}` },
943
+ },
944
+ },
945
+ }, null, 2));
946
+ return;
947
+ }
948
+ if (client === "gemini") {
949
+ console.log(` ${chalk.cyan(`gemini mcp add ${MCP_SERVER_NAME} ${remoteUrl} \\`)}`);
950
+ console.log(` ${chalk.cyan(`--header "Authorization: Bearer ${apiKey}"`)}`);
951
+ return;
952
+ }
953
+ // claude / manual
954
+ console.log(` ${chalk.cyan(`claude mcp add --transport http --scope user ${MCP_SERVER_NAME} \\`)}`);
955
+ console.log(` ${chalk.cyan(remoteUrl + " \\")}`);
956
+ console.log(` ${chalk.cyan(`--header "Authorization: Bearer ${apiKey}"`)}`);
957
+ }
958
+ async function runInstall(opts) {
959
+ const config = loadConfig();
960
+ const clientName = `MIOSA MCP (${opts.client === "manual" ? "manual" : opts.client})`;
961
+ console.log(chalk.bold("MIOSA MCP installer"), chalk.dim(`— wiring ${opts.client} → ${opts.remoteUrl}`));
962
+ const apiKey = await runDeviceFlow(config.endpoint, clientName);
963
+ if (opts.client === "claude") {
964
+ const wired = wireClaudeCode(apiKey, opts.remoteUrl, opts.scope);
965
+ if (wired.ok) {
966
+ console.log();
967
+ console.log(chalk.green("✓"), `MCP server '${MCP_SERVER_NAME}' added to Claude Code (${opts.scope} scope).`);
968
+ console.log();
969
+ console.log(chalk.dim("Verify:"));
970
+ console.log(` ${chalk.cyan("claude mcp list")}`);
971
+ console.log();
972
+ console.log(chalk.dim("Try in a fresh Claude Code session:"));
973
+ console.log(chalk.dim(` "Create a MIOSA sandbox, run \`python -c 'print(2+2)'\`, then destroy it."`));
974
+ return;
975
+ }
976
+ console.log();
977
+ console.log(chalk.yellow("!"), `Could not auto-wire Claude Code: ${wired.reason}`);
978
+ console.log(chalk.yellow(" Falling back to manual snippet:"));
979
+ printManualSnippet("claude", apiKey, opts.remoteUrl);
980
+ return;
981
+ }
982
+ // cursor / gemini / manual — print the snippet
983
+ printManualSnippet(opts.client, apiKey, opts.remoteUrl);
984
+ }
985
+ // ── Commander registration ────────────────────────────────────────────────────
986
+ export function register(program) {
987
+ const mcp = program
988
+ .command("mcp")
989
+ .description("Model Context Protocol — expose MIOSA tools to AI agents");
990
+ mcp
991
+ .command("serve")
992
+ .description("Start an MCP server over stdio (JSON-RPC 2.0). Add to .claude/mcp.json to use with Claude.")
993
+ .action(() => {
994
+ runServer().catch((e) => {
995
+ const msg = e instanceof Error ? e.message : String(e);
996
+ process.stderr.write(`miosa mcp serve fatal: ${msg}\n`);
997
+ process.exit(1);
998
+ });
999
+ });
1000
+ mcp
1001
+ .command("install")
1002
+ .description("Install the hosted MIOSA MCP server (https://api.miosa.ai/api/v1/mcp) into your AI client. Opens a browser to log in and wire your account.")
1003
+ .option("-c, --client <client>", "Which AI client to wire: claude (default), cursor, gemini, manual", "claude")
1004
+ .option("-s, --scope <scope>", "Claude Code config scope: local, user (default), or project", "user")
1005
+ .option("--url <url>", "Override the hosted MCP URL (default: https://api.miosa.ai/api/v1/mcp)", MCP_REMOTE_URL)
1006
+ .action(async (opts) => {
1007
+ const client = (["claude", "cursor", "gemini", "manual"].includes(opts.client)
1008
+ ? opts.client
1009
+ : "claude");
1010
+ const scope = (["local", "user", "project"].includes(opts.scope) ? opts.scope : "user");
1011
+ try {
1012
+ await runInstall({ client, scope, remoteUrl: opts.url });
1013
+ }
1014
+ catch (e) {
1015
+ const msg = e instanceof Error ? e.message : String(e);
1016
+ console.error(chalk.red(`Error: ${msg}`));
1017
+ process.exit(3);
1018
+ }
1019
+ });
1020
+ }
1021
+ //# sourceMappingURL=mcp.js.map