unframer 4.0.4 → 4.1.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 (235) hide show
  1. package/README.md +67 -0
  2. package/dist/cli-readonly-server-api.test.d.ts +2 -0
  3. package/dist/cli-readonly-server-api.test.d.ts.map +1 -0
  4. package/dist/cli-readonly-server-api.test.js +96 -0
  5. package/dist/cli-readonly-server-api.test.js.map +1 -0
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +242 -64
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +18 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/{lib/config.js → config.js} +1 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/esbuild.d.ts +5 -1
  15. package/dist/esbuild.d.ts.map +1 -1
  16. package/dist/esbuild.js +2 -1
  17. package/dist/esbuild.js.map +1 -1
  18. package/dist/example-code.test.js +53 -53
  19. package/dist/example-code.test.js.map +1 -1
  20. package/dist/framer-client.server.d.ts +4 -0
  21. package/dist/framer-client.server.d.ts.map +1 -0
  22. package/dist/framer-client.server.js +45 -0
  23. package/dist/framer-client.server.js.map +1 -0
  24. package/dist/framer.js +9269 -6630
  25. package/dist/plugin-mcp-dist/lib/client-websocket.d.ts +5 -0
  26. package/dist/plugin-mcp-dist/lib/client-websocket.d.ts.map +1 -0
  27. package/dist/plugin-mcp-dist/lib/client-websocket.js +103 -0
  28. package/dist/plugin-mcp-dist/lib/client-websocket.js.map +1 -0
  29. package/dist/plugin-mcp-dist/lib/cms.d.ts +10 -0
  30. package/dist/plugin-mcp-dist/lib/cms.d.ts.map +1 -0
  31. package/dist/plugin-mcp-dist/lib/cms.js +58 -0
  32. package/dist/plugin-mcp-dist/lib/cms.js.map +1 -0
  33. package/dist/plugin-mcp-dist/lib/errors.d.ts +5 -0
  34. package/dist/plugin-mcp-dist/lib/errors.d.ts.map +1 -0
  35. package/dist/plugin-mcp-dist/lib/errors.js +48 -0
  36. package/dist/plugin-mcp-dist/lib/errors.js.map +1 -0
  37. package/dist/plugin-mcp-dist/lib/framer-client.d.ts +2 -0
  38. package/dist/plugin-mcp-dist/lib/framer-client.d.ts.map +1 -0
  39. package/dist/plugin-mcp-dist/lib/framer-client.js +4 -0
  40. package/dist/plugin-mcp-dist/lib/framer-client.js.map +1 -0
  41. package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts +2 -0
  42. package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts.map +1 -0
  43. package/dist/plugin-mcp-dist/lib/framer-client.server.js +4 -0
  44. package/dist/plugin-mcp-dist/lib/framer-client.server.js.map +1 -0
  45. package/dist/plugin-mcp-dist/lib/framer.d.ts +36 -0
  46. package/dist/plugin-mcp-dist/lib/framer.d.ts.map +1 -0
  47. package/dist/plugin-mcp-dist/lib/framer.js +1000 -0
  48. package/dist/plugin-mcp-dist/lib/framer.js.map +1 -0
  49. package/dist/plugin-mcp-dist/lib/hooks.d.ts +6 -0
  50. package/dist/plugin-mcp-dist/lib/hooks.d.ts.map +1 -0
  51. package/dist/plugin-mcp-dist/lib/hooks.js +46 -0
  52. package/dist/plugin-mcp-dist/lib/hooks.js.map +1 -0
  53. package/dist/plugin-mcp-dist/lib/mcp-client.d.ts +141 -0
  54. package/dist/plugin-mcp-dist/lib/mcp-client.d.ts.map +1 -0
  55. package/dist/plugin-mcp-dist/lib/mcp-client.js +40 -0
  56. package/dist/plugin-mcp-dist/lib/mcp-client.js.map +1 -0
  57. package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts +378 -0
  58. package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -0
  59. package/dist/plugin-mcp-dist/lib/mcp-handlers.js +1807 -0
  60. package/dist/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -0
  61. package/dist/plugin-mcp-dist/lib/mcp-tools.d.ts +5 -0
  62. package/dist/plugin-mcp-dist/lib/mcp-tools.d.ts.map +1 -0
  63. package/dist/plugin-mcp-dist/lib/mcp-tools.js +5 -0
  64. package/dist/plugin-mcp-dist/lib/mcp-tools.js.map +1 -0
  65. package/dist/plugin-mcp-dist/lib/mcp-websocket.d.ts +10 -0
  66. package/dist/plugin-mcp-dist/lib/mcp-websocket.d.ts.map +1 -0
  67. package/dist/plugin-mcp-dist/lib/mcp-websocket.js +88 -0
  68. package/dist/plugin-mcp-dist/lib/mcp-websocket.js.map +1 -0
  69. package/dist/plugin-mcp-dist/lib/mcp.test.d.ts +2 -0
  70. package/dist/plugin-mcp-dist/lib/mcp.test.d.ts.map +1 -0
  71. package/dist/plugin-mcp-dist/lib/mcp.test.js +1315 -0
  72. package/dist/plugin-mcp-dist/lib/mcp.test.js.map +1 -0
  73. package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts +5 -0
  74. package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -0
  75. package/dist/plugin-mcp-dist/lib/plugin-websocket.js +168 -0
  76. package/dist/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -0
  77. package/dist/plugin-mcp-dist/lib/react-export.d.ts +51 -0
  78. package/dist/plugin-mcp-dist/lib/react-export.d.ts.map +1 -0
  79. package/dist/plugin-mcp-dist/lib/react-export.js +340 -0
  80. package/dist/plugin-mcp-dist/lib/react-export.js.map +1 -0
  81. package/dist/plugin-mcp-dist/lib/schema.d.ts +261 -0
  82. package/dist/plugin-mcp-dist/lib/schema.d.ts.map +1 -0
  83. package/dist/plugin-mcp-dist/lib/schema.js +871 -0
  84. package/dist/plugin-mcp-dist/lib/schema.js.map +1 -0
  85. package/dist/plugin-mcp-dist/lib/store.d.ts +2 -0
  86. package/dist/plugin-mcp-dist/lib/store.d.ts.map +1 -0
  87. package/dist/plugin-mcp-dist/lib/store.js +8 -0
  88. package/dist/plugin-mcp-dist/lib/store.js.map +1 -0
  89. package/dist/plugin-mcp-dist/lib/tree-utils.d.ts +6 -0
  90. package/dist/plugin-mcp-dist/lib/tree-utils.d.ts.map +1 -0
  91. package/dist/plugin-mcp-dist/lib/tree-utils.js +141 -0
  92. package/dist/plugin-mcp-dist/lib/tree-utils.js.map +1 -0
  93. package/dist/plugin-mcp-dist/lib/types.d.ts +110 -0
  94. package/dist/plugin-mcp-dist/lib/types.d.ts.map +1 -0
  95. package/dist/plugin-mcp-dist/lib/types.js +188 -0
  96. package/dist/plugin-mcp-dist/lib/types.js.map +1 -0
  97. package/dist/plugin-mcp-dist/lib/utils.d.ts +21 -0
  98. package/dist/plugin-mcp-dist/lib/utils.d.ts.map +1 -0
  99. package/dist/plugin-mcp-dist/lib/utils.js +53 -0
  100. package/dist/plugin-mcp-dist/lib/utils.js.map +1 -0
  101. package/dist/plugin-mcp-dist/lib/websocket-server.d.ts +10 -0
  102. package/dist/plugin-mcp-dist/lib/websocket-server.d.ts.map +1 -0
  103. package/dist/plugin-mcp-dist/lib/websocket-server.js +88 -0
  104. package/dist/plugin-mcp-dist/lib/websocket-server.js.map +1 -0
  105. package/dist/plugin-mcp-dist/lib/xml.d.ts +10 -0
  106. package/dist/plugin-mcp-dist/lib/xml.d.ts.map +1 -0
  107. package/dist/plugin-mcp-dist/lib/xml.js +395 -0
  108. package/dist/plugin-mcp-dist/lib/xml.js.map +1 -0
  109. package/dist/plugin-mcp-dist/lib/xml.test.d.ts +2 -0
  110. package/dist/plugin-mcp-dist/lib/xml.test.d.ts.map +1 -0
  111. package/dist/plugin-mcp-dist/lib/xml.test.js +1030 -0
  112. package/dist/plugin-mcp-dist/lib/xml.test.js.map +1 -0
  113. package/dist/version.d.ts +1 -1
  114. package/dist/version.js +1 -1
  115. package/package.json +13 -3
  116. package/src/cli-readonly-server-api.test.ts +152 -0
  117. package/src/cli.ts +315 -69
  118. package/src/{lib/config.ts → config.ts} +11 -9
  119. package/src/esbuild.ts +2 -0
  120. package/src/example-code.test.ts +53 -53
  121. package/src/framer-chunks/chunk-2JNOE5PX.js +51 -0
  122. package/src/framer-chunks/chunk-76VXR6QG.js +91 -0
  123. package/src/framer-chunks/chunk-A2PMVMFI.js +91 -0
  124. package/src/framer-chunks/chunk-DBJCHRFG.js +105 -0
  125. package/src/framer-chunks/chunk-H7SXYDQJ.js +68 -0
  126. package/src/framer-chunks/chunk-OAKBJJLO.js +105 -0
  127. package/src/framer-chunks/chunk-VUHWYTYT.js +105 -0
  128. package/src/framer-client.server.ts +97 -0
  129. package/src/framer.js +9269 -6630
  130. package/src/plugin-mcp-dist/lib/client-websocket.d.ts +6 -0
  131. package/src/plugin-mcp-dist/lib/client-websocket.d.ts.map +1 -0
  132. package/src/plugin-mcp-dist/lib/client-websocket.js +102 -0
  133. package/src/plugin-mcp-dist/lib/client-websocket.js.map +1 -0
  134. package/src/plugin-mcp-dist/lib/cms.d.ts +7 -0
  135. package/src/plugin-mcp-dist/lib/cms.d.ts.map +1 -0
  136. package/src/plugin-mcp-dist/lib/cms.js +57 -0
  137. package/src/plugin-mcp-dist/lib/cms.js.map +1 -0
  138. package/src/plugin-mcp-dist/lib/errors.d.ts +5 -0
  139. package/src/plugin-mcp-dist/lib/errors.d.ts.map +1 -0
  140. package/src/plugin-mcp-dist/lib/errors.js +47 -0
  141. package/src/plugin-mcp-dist/lib/errors.js.map +1 -0
  142. package/src/plugin-mcp-dist/lib/framer-client.d.ts +2 -0
  143. package/src/plugin-mcp-dist/lib/framer-client.d.ts.map +1 -0
  144. package/src/plugin-mcp-dist/lib/framer-client.js +3 -0
  145. package/src/plugin-mcp-dist/lib/framer-client.js.map +1 -0
  146. package/src/plugin-mcp-dist/lib/framer-client.server.d.ts +2 -0
  147. package/src/plugin-mcp-dist/lib/framer-client.server.d.ts.map +1 -0
  148. package/src/plugin-mcp-dist/lib/framer-client.server.js +3 -0
  149. package/src/plugin-mcp-dist/lib/framer-client.server.js.map +1 -0
  150. package/src/plugin-mcp-dist/lib/framer.d.ts +69 -0
  151. package/src/plugin-mcp-dist/lib/framer.d.ts.map +1 -0
  152. package/src/plugin-mcp-dist/lib/framer.js +999 -0
  153. package/src/plugin-mcp-dist/lib/framer.js.map +1 -0
  154. package/src/plugin-mcp-dist/lib/hooks.d.ts +6 -0
  155. package/src/plugin-mcp-dist/lib/hooks.d.ts.map +1 -0
  156. package/src/plugin-mcp-dist/lib/hooks.js +45 -0
  157. package/src/plugin-mcp-dist/lib/hooks.js.map +1 -0
  158. package/src/plugin-mcp-dist/lib/mcp-client.d.ts +50 -0
  159. package/src/plugin-mcp-dist/lib/mcp-client.d.ts.map +1 -0
  160. package/src/plugin-mcp-dist/lib/mcp-client.js +39 -0
  161. package/src/plugin-mcp-dist/lib/mcp-client.js.map +1 -0
  162. package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts +375 -0
  163. package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -0
  164. package/src/plugin-mcp-dist/lib/mcp-handlers.js +1806 -0
  165. package/src/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -0
  166. package/src/plugin-mcp-dist/lib/mcp-tools.d.ts.map +1 -0
  167. package/src/plugin-mcp-dist/lib/mcp-tools.js +4 -0
  168. package/src/plugin-mcp-dist/lib/mcp-tools.js.map +1 -0
  169. package/src/plugin-mcp-dist/lib/mcp-websocket.d.ts +21 -0
  170. package/src/plugin-mcp-dist/lib/mcp-websocket.d.ts.map +1 -0
  171. package/src/plugin-mcp-dist/lib/mcp-websocket.js +87 -0
  172. package/src/plugin-mcp-dist/lib/mcp-websocket.js.map +1 -0
  173. package/src/plugin-mcp-dist/lib/mcp.test.d.ts +2 -0
  174. package/src/plugin-mcp-dist/lib/mcp.test.d.ts.map +1 -0
  175. package/src/plugin-mcp-dist/lib/mcp.test.js +1314 -0
  176. package/src/plugin-mcp-dist/lib/mcp.test.js.map +1 -0
  177. package/src/plugin-mcp-dist/lib/plugin-websocket.d.ts +6 -0
  178. package/src/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -0
  179. package/src/plugin-mcp-dist/lib/plugin-websocket.js +167 -0
  180. package/src/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -0
  181. package/src/plugin-mcp-dist/lib/react-export.d.ts +101 -0
  182. package/src/plugin-mcp-dist/lib/react-export.d.ts.map +1 -0
  183. package/src/plugin-mcp-dist/lib/react-export.js +339 -0
  184. package/src/plugin-mcp-dist/lib/react-export.js.map +1 -0
  185. package/src/plugin-mcp-dist/lib/schema.d.ts +358 -0
  186. package/src/plugin-mcp-dist/lib/schema.d.ts.map +1 -0
  187. package/src/plugin-mcp-dist/lib/schema.js +870 -0
  188. package/src/plugin-mcp-dist/lib/schema.js.map +1 -0
  189. package/src/plugin-mcp-dist/lib/snapshots/add-frame-to-section.patch +1 -0
  190. package/src/plugin-mcp-dist/lib/snapshots/code-file-insert-info.md +1 -0
  191. package/src/plugin-mcp-dist/lib/snapshots/component-insert-info.md +1 -0
  192. package/src/plugin-mcp-dist/lib/snapshots/component.html +1 -0
  193. package/src/plugin-mcp-dist/lib/snapshots/create-nodes-with-layout.patch +1 -0
  194. package/src/plugin-mcp-dist/lib/snapshots/page.html +1 -0
  195. package/src/plugin-mcp-dist/lib/snapshots/project.html +1 -0
  196. package/src/plugin-mcp-dist/lib/snapshots/tools-schema.yaml +908 -0
  197. package/src/plugin-mcp-dist/lib/snapshots/tools.jsonc +906 -0
  198. package/src/plugin-mcp-dist/lib/store.d.ts +8 -0
  199. package/src/plugin-mcp-dist/lib/store.d.ts.map +1 -0
  200. package/src/plugin-mcp-dist/lib/store.js +7 -0
  201. package/src/plugin-mcp-dist/lib/store.js.map +1 -0
  202. package/src/plugin-mcp-dist/lib/tree-utils.d.ts +18 -0
  203. package/src/plugin-mcp-dist/lib/tree-utils.d.ts.map +1 -0
  204. package/src/plugin-mcp-dist/lib/tree-utils.js +140 -0
  205. package/src/plugin-mcp-dist/lib/tree-utils.js.map +1 -0
  206. package/src/plugin-mcp-dist/lib/types.d.ts +139 -0
  207. package/src/plugin-mcp-dist/lib/types.d.ts.map +1 -0
  208. package/src/plugin-mcp-dist/lib/types.js +187 -0
  209. package/src/plugin-mcp-dist/lib/types.js.map +1 -0
  210. package/src/plugin-mcp-dist/lib/utils.d.ts +23 -0
  211. package/src/plugin-mcp-dist/lib/utils.d.ts.map +1 -0
  212. package/src/plugin-mcp-dist/lib/utils.js +52 -0
  213. package/src/plugin-mcp-dist/lib/utils.js.map +1 -0
  214. package/src/plugin-mcp-dist/lib/websocket-server.d.ts +21 -0
  215. package/src/plugin-mcp-dist/lib/websocket-server.d.ts.map +1 -0
  216. package/src/plugin-mcp-dist/lib/websocket-server.js +87 -0
  217. package/src/plugin-mcp-dist/lib/websocket-server.js.map +1 -0
  218. package/src/plugin-mcp-dist/lib/xml.d.ts +33 -0
  219. package/src/plugin-mcp-dist/lib/xml.d.ts.map +1 -0
  220. package/src/plugin-mcp-dist/lib/xml.js +394 -0
  221. package/src/plugin-mcp-dist/lib/xml.js.map +1 -0
  222. package/src/plugin-mcp-dist/lib/xml.test.d.ts +2 -0
  223. package/src/plugin-mcp-dist/lib/xml.test.d.ts.map +1 -0
  224. package/src/plugin-mcp-dist/lib/xml.test.js +1029 -0
  225. package/src/plugin-mcp-dist/lib/xml.test.js.map +1 -0
  226. package/src/styles/framer.css +7 -7
  227. package/src/version.ts +1 -1
  228. package/dist/lib/config.d.ts +0 -17
  229. package/dist/lib/config.d.ts.map +0 -1
  230. package/dist/lib/config.js.map +0 -1
  231. package/dist/lib/mcp-to-cli.d.ts +0 -25
  232. package/dist/lib/mcp-to-cli.d.ts.map +0 -1
  233. package/dist/lib/mcp-to-cli.js +0 -176
  234. package/dist/lib/mcp-to-cli.js.map +0 -1
  235. package/src/lib/mcp-to-cli.ts +0 -229
package/README.md CHANGED
@@ -314,3 +314,70 @@ Here is the below landing page Lighthouse score when using Astro:
314
314
  ## Example
315
315
 
316
316
  Look at the [nextjs-app source code folder](./nextjs-app) for an example and [the deployed website here](https://unframer-nextjs-app.vercel.app/)
317
+
318
+ ## MCP CLI Commands
319
+
320
+ Unframer can act as a command-line client for the [Framer MCP plugin](https://www.framer.com/marketplace/plugins/mcp/), allowing you to interact with your Framer project directly from the terminal.
321
+
322
+ ### Setup
323
+
324
+ ```sh
325
+ # Login with your MCP URL (get it from the Framer MCP plugin)
326
+ npx unframer mcp login
327
+ ```
328
+
329
+ After login, all MCP commands become available. Run `npx unframer --help` to see the full list.
330
+
331
+ ### Example Commands
332
+
333
+ ```sh
334
+ # Get project structure as XML
335
+ npx unframer mcp getProjectXml
336
+
337
+ # Get a specific node's XML by ID
338
+ npx unframer mcp getNodeXml --nodeId "abc123"
339
+
340
+ # Update node text or attributes
341
+ npx unframer mcp updateXmlForNode --nodeId "abc123" --xml '<Text nodeId="abc123">New text</Text>'
342
+
343
+ # Search for fonts
344
+ npx unframer mcp searchFonts --query "Inter"
345
+
346
+ # Export React components
347
+ npx unframer mcp exportReactComponents
348
+
349
+ # Get published website URL
350
+ npx unframer mcp getProjectWebsiteUrl
351
+ ```
352
+
353
+ ### CMS Operations
354
+
355
+ ```sh
356
+ # List all CMS collections
357
+ npx unframer mcp getCMSCollections
358
+
359
+ # Get items from a collection
360
+ npx unframer mcp getCMSItems --collectionId "col123"
361
+
362
+ # Create or update a CMS item
363
+ npx unframer mcp upsertCMSItem --collectionId "col123" --slug "my-post" --fieldData '{"title": "Hello"}'
364
+ ```
365
+
366
+ ### Code Files
367
+
368
+ ```sh
369
+ # Create a new code component
370
+ npx unframer mcp createCodeFile --name "MyComponent.tsx" --content "export default function MyComponent() { return <div>Hello</div> }"
371
+
372
+ # Read existing code file
373
+ npx unframer mcp readCodeFile --codeFileId "code123"
374
+
375
+ # Update code file content
376
+ npx unframer mcp updateCodeFile --codeFileId "code123" --content "// updated code"
377
+ ```
378
+
379
+ Run any command with `--help` for detailed options:
380
+
381
+ ```sh
382
+ npx unframer mcp updateXmlForNode --help
383
+ ```
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli-readonly-server-api.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-readonly-server-api.test.d.ts","sourceRoot":"","sources":["../src/cli-readonly-server-api.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,96 @@
1
+ // Read-only integration checks for server-api CLI commands and reconnect latency.
2
+ import childProcess from 'node:child_process';
3
+ import process from 'node:process';
4
+ import util from 'node:util';
5
+ import { expect, test } from 'vitest';
6
+ const execFile = util.promisify(childProcess.execFile);
7
+ const defaultProjectUrl = 'https://framer.com/projects/Framer-MCP-project-Designor-Framer-Template-copy--lfAw10qcrLpLLEznmZmo-irrP1?node=CpFAHygNJ';
8
+ const hasApiKey = Boolean(process.env.FRAMER_API_KEY);
9
+ function sleep({ ms }) {
10
+ return new Promise((resolve) => {
11
+ setTimeout(() => {
12
+ resolve();
13
+ }, ms);
14
+ });
15
+ }
16
+ async function runServerApiCliReadOnly({ args, timeoutMs = 180_000, }) {
17
+ const projectUrl = process.env.FRAMER_PROJECT_URL || defaultProjectUrl;
18
+ const startMs = performance.now();
19
+ const { stdout, stderr } = await execFile('pnpm', ['tsx', 'src/bin.ts', ...args, '--project', projectUrl], {
20
+ cwd: process.cwd(),
21
+ env: process.env,
22
+ timeout: timeoutMs,
23
+ maxBuffer: 8 * 1024 * 1024,
24
+ });
25
+ return {
26
+ stdout,
27
+ stderr,
28
+ durationMs: performance.now() - startMs,
29
+ };
30
+ }
31
+ async function runServerApiCliReadOnlyWithRetry({ args, timeoutMs = 180_000, attempt = 1, maxAttempts = 3, }) {
32
+ try {
33
+ return await runServerApiCliReadOnly({ args, timeoutMs });
34
+ }
35
+ catch (error) {
36
+ if (attempt >= maxAttempts) {
37
+ throw error;
38
+ }
39
+ await sleep({ ms: 1_000 * attempt });
40
+ return runServerApiCliReadOnlyWithRetry({
41
+ args,
42
+ timeoutMs,
43
+ attempt: attempt + 1,
44
+ maxAttempts,
45
+ });
46
+ }
47
+ }
48
+ function extractJsonFromCliOutput({ output }) {
49
+ const jsonStart = output.indexOf('{');
50
+ if (jsonStart < 0) {
51
+ throw new Error(`Could not find JSON in CLI output:\n${output}`);
52
+ }
53
+ return JSON.parse(output.slice(jsonStart));
54
+ }
55
+ test.skipIf(!hasApiKey)('server-api read-only cli commands return expected output', async () => {
56
+ const websiteRun = await runServerApiCliReadOnly({
57
+ args: ['mcp', 'getProjectWebsiteUrl'],
58
+ });
59
+ const website = extractJsonFromCliOutput({ output: websiteRun.stdout });
60
+ expect(Object.keys(website).length).toBeGreaterThan(0);
61
+ const websiteJson = JSON.stringify(website);
62
+ expect(websiteJson.includes('https://')).toBe(true);
63
+ expect(websiteRun.stderr.includes('Running getProjectWebsiteUrl')).toBe(true);
64
+ const xmlRun = await runServerApiCliReadOnly({
65
+ args: ['mcp', 'getProjectXml'],
66
+ });
67
+ expect(xmlRun.stdout.includes('# Project structure:')).toBe(true);
68
+ expect(xmlRun.stdout.includes('<Project>')).toBe(true);
69
+ expect(xmlRun.stderr.includes('Running getProjectXml')).toBe(true);
70
+ const selectedNodesRun = await runServerApiCliReadOnly({
71
+ args: ['mcp', 'getSelectedNodesXml'],
72
+ });
73
+ expect(selectedNodesRun.stdout.includes('No nodes are currently selected.')).toBe(true);
74
+ const focusedNodeMatch = xmlRun.stdout.match(/currently focused [^`]* ID is: `([^`]+)`/);
75
+ const focusedNodeId = focusedNodeMatch?.[1];
76
+ expect(Boolean(focusedNodeId)).toBe(true);
77
+ const zoomRun = await runServerApiCliReadOnly({
78
+ args: ['mcp', 'zoomIntoView', '--nodeId', focusedNodeId || ''],
79
+ });
80
+ expect(zoomRun.stdout.includes('Zoomed into view for node')).toBe(true);
81
+ }, 180_000);
82
+ test.skipIf(!hasApiKey)('server-api reconnects cleanly for repeated read-only calls', async () => {
83
+ const firstRun = await runServerApiCliReadOnly({
84
+ args: ['mcp', 'getProjectWebsiteUrl'],
85
+ });
86
+ const secondRun = await runServerApiCliReadOnlyWithRetry({
87
+ args: ['mcp', 'getProjectWebsiteUrl'],
88
+ });
89
+ expect(firstRun.durationMs).toBeGreaterThan(0);
90
+ expect(secondRun.durationMs).toBeGreaterThan(0);
91
+ expect(firstRun.durationMs).toBeLessThan(120_000);
92
+ expect(secondRun.durationMs).toBeLessThan(120_000);
93
+ const reconnectDeltaMs = secondRun.durationMs - firstRun.durationMs;
94
+ console.info(`getProjectWebsiteUrl latency ms: first=${firstRun.durationMs.toFixed(0)}, second=${secondRun.durationMs.toFixed(0)}, delta=${reconnectDeltaMs.toFixed(0)}`);
95
+ }, 240_000);
96
+ //# sourceMappingURL=cli-readonly-server-api.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-readonly-server-api.test.js","sourceRoot":"","sources":["../src/cli-readonly-server-api.test.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import './sentry.js';
2
2
  import { StyleToken } from './exporter.js';
3
3
  import { BreakpointSizes } from './css.js';
4
- export declare const cli: import("@xmorse/cac").CAC;
4
+ export declare const cli: import("goke").Goke;
5
5
  export type Config = {
6
6
  jsx?: boolean;
7
7
  components: {
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAKA,OAAO,aAAa,CAAA;AAGpB,OAAO,EAAU,UAAU,EAA8B,MAAM,eAAe,CAAA;AAU9E,OAAO,EAAE,eAAe,EAA0B,MAAM,UAAU,CAAA;AAmBlE,eAAO,MAAM,GAAG,2BAAkB,CAAA;AAgVlC,MAAM,MAAM,MAAM,GAAG;IACjB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,UAAU,EAAE;QACR,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KACzB,CAAA;IACD,oBAAoB,CAAC,EAAE;QACnB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,aAAa,EAAE,MAAM,CAAA;QACrB,cAAc,EAAE,MAAM,CAAA;QACtB,KAAK,EAAE,MAAM,CAAA;KAChB,EAAE,CAAA;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;QACrB,IAAI,EAAE,MAAM,CAAA;KACf,EAAE,CAAA;IAEH,OAAO,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACf,EAAE,CAAA;IACH,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,MAAM,CAAC,EAAE,UAAU,EAAE,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6BAA6B,EAAE,uBAAuB,EAAE,CAAA;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAE/B,CAAA;AAED,KAAK,uBAAuB,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;IAEjB,SAAS,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAsB,eAAe,CAAC,EAClC,SAAS,EACT,gBAAiC,EACjC,WAAmB,EACnB,KAAU,EACV,MAAwC,EAC3C;;;;;;CAAA;;;;GAyHA"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAMA,OAAO,aAAa,CAAA;AAGpB,OAAO,EAAU,UAAU,EAA8B,MAAM,eAAe,CAAA;AAU9E,OAAO,EAAE,eAAe,EAA0B,MAAM,UAAU,CAAA;AAsBlE,eAAO,MAAM,GAAG,qBAAmB,CAAA;AAkkBnC,MAAM,MAAM,MAAM,GAAG;IACjB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,UAAU,EAAE;QACR,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KACzB,CAAA;IACD,oBAAoB,CAAC,EAAE;QACnB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,aAAa,EAAE,MAAM,CAAA;QACrB,cAAc,EAAE,MAAM,CAAA;QACtB,KAAK,EAAE,MAAM,CAAA;KAChB,EAAE,CAAA;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;QACrB,IAAI,EAAE,MAAM,CAAA;KACf,EAAE,CAAA;IAEH,OAAO,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACf,EAAE,CAAA;IACH,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,MAAM,CAAC,EAAE,UAAU,EAAE,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6BAA6B,EAAE,uBAAuB,EAAE,CAAA;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAE/B,CAAA;AAED,KAAK,uBAAuB,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;IAEjB,SAAS,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAsB,eAAe,CAAC,EAClC,SAAS,EACT,gBAAiC,EACjC,WAAmB,EACnB,KAAU,EACV,MAAwC,EAC3C;;;;;;CAAA;;;;GAyHA"}
package/dist/cli.js CHANGED
@@ -1,14 +1,15 @@
1
+ import { z } from 'zod';
1
2
  import { setMaxListeners } from 'events';
2
3
  import pkg from '../package.json' with { type: 'json' };
3
4
  import pico from 'picocolors';
4
5
  const { blue, bgBlue, green } = pico;
5
6
  import { fetch } from 'undici';
6
7
  import './sentry.js';
7
- import { input } from '@inquirer/prompts';
8
+ import { input, select, password } from '@inquirer/prompts';
8
9
  import { bundle, createExampleComponentCode } from './exporter.js';
9
10
  import { createClient } from './generated/api-client.js';
10
11
  import { generateStackblitzFiles } from './stackblitz.js';
11
- import { cac } from '@xmorse/cac';
12
+ import { goke, wrapJsonSchema } from 'goke';
12
13
  import { exec } from 'child_process';
13
14
  import { promisify } from 'util';
14
15
  import fs from 'fs';
@@ -18,27 +19,22 @@ import { componentNameToPath, dedent, isTruthy, logger, sleep, spinner, } from '
18
19
  import { getPackageManager } from './package-manager.js';
19
20
  import { notifyError } from './sentry.js';
20
21
  import { dispatcher } from './undici-dispatcher.js';
21
- import { loadConfig, saveConfig, getConfigPath } from './lib/config.js';
22
- import { addMcpCommands } from './lib/mcp-to-cli.js';
23
- import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
22
+ import { loadConfig, saveConfig, getConfigPath, } from './config.js';
23
+ import { addMcpCommands } from '@goke/mcp';
24
24
  const configNames = ['unframer.config.json', 'unframer.json'];
25
- export const cli = cac('unframer');
25
+ export const cli = goke('unframer');
26
26
  let defaultOutDir = 'framer';
27
27
  cli.command('[projectId]', 'Run unframer with optional project ID')
28
- .option('--outDir <dir>', 'Output directory', { default: defaultOutDir })
29
- .option('--external [package]', 'Make some package external, do not pass a package name to make all packages external', {
30
- default: true,
31
- })
32
- .option('--watch', 'Watch for changes and rebuild', { default: false })
33
- .option('--jsx', 'Output jsx code instead of minified .js code', {
34
- default: true,
35
- })
36
- .option('--debug', 'Enable debug logging', { default: false })
37
- .option('--metafile', 'Generate meta.json file with build metadata', {
38
- default: false,
39
- })
28
+ .option('--outDir <dir>', 'Output directory')
29
+ .option('--external [package]', 'Make some package external, do not pass a package name to make all packages external')
30
+ .option('--watch', 'Watch for changes and rebuild')
31
+ .option('--jsx', 'Output jsx code instead of minified .js code')
32
+ .option('--debug', 'Enable debug logging')
33
+ .option('--metafile', 'Generate meta.json file with build metadata')
40
34
  .action(async function main(projectId, options) {
41
- const external_ = options.external;
35
+ const outDir = options.outDir || defaultOutDir;
36
+ const jsx = options.jsx ?? true;
37
+ const external_ = options.external ?? true;
42
38
  const allExternal = external_ === true;
43
39
  const externalPackages = Array.isArray(external_)
44
40
  ? external_.filter((x) => x.trim())
@@ -49,7 +45,6 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
49
45
  if (options.debug) {
50
46
  logger.debug = true;
51
47
  }
52
- const outDir = options.outDir;
53
48
  const controller = new AbortController();
54
49
  const signal = controller.signal;
55
50
  const watch = options.watch;
@@ -60,7 +55,6 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
60
55
  outDir,
61
56
  projectId,
62
57
  });
63
- let jsx = options.jsx;
64
58
  const { rebuild, buildContext } = await bundle({
65
59
  config: {
66
60
  jsx,
@@ -149,16 +143,14 @@ function fixOldUnframerPath() {
149
143
  const version = pkg.version;
150
144
  cli.version(version).help();
151
145
  cli.command('example-app <projectId>', 'Create an example app with Framer components')
152
- .option('--outDir <dir>', 'Output directory', {
153
- default: 'example-unframer-app',
154
- })
146
+ .option('--outDir <dir>', 'Output directory')
155
147
  .action(async (projectId, options) => {
156
148
  if (!projectId?.trim()) {
157
149
  console.log(`unframer example-app requires a project id positional param`);
158
150
  process.exit(1);
159
151
  }
160
152
  try {
161
- const outDir = options.outDir;
153
+ const outDir = options.outDir || 'example-unframer-app';
162
154
  console.log(`Creating example app in ${outDir}`);
163
155
  // Create the output directory
164
156
  const absoluteOutDir = path.resolve(process.cwd(), outDir);
@@ -257,52 +249,238 @@ cli.command('example-app <projectId>', 'Create an example app with Framer compon
257
249
  throw error;
258
250
  }
259
251
  });
260
- cli.command('mcp login [url]', 'Store MCP server URL. After login, other MCP commands will appear in --help. The MCP URL is visible in the Framer MCP plugin.').action(async (url) => {
261
- // Prompt for URL if not provided, avoids shell escaping issues with & in URLs
262
- if (!url) {
263
- const shortcut = process.platform === 'darwin' ? 'Cmd+K' : 'Ctrl+K';
264
- console.log('\nTo get your MCP URL:');
265
- console.log(' 1. Go to https://framer.com and open your project');
266
- console.log(` 2. Press ${shortcut} and search for "MCP" plugin`);
267
- console.log(' 3. Copy the URL shown in the plugin\n');
268
- }
269
- let mcpUrl = url;
270
- if (!mcpUrl) {
271
- try {
272
- mcpUrl = await input({ message: 'Paste MCP URL:' });
252
+ cli.command('mcp login [url]', 'Login to Framer MCP. Choose between plugin mode (requires Framer open) or server API mode (works headlessly with API key).').action(async (url) => {
253
+ try {
254
+ // If URL is passed directly, use plugin mode
255
+ if (url) {
256
+ saveConfig({ mode: 'plugin', mcpUrl: url });
257
+ console.log(`MCP URL saved to ${getConfigPath()}`);
258
+ console.log(`Run \`unframer --help\` to see all available MCP commands`);
259
+ return;
273
260
  }
274
- catch (error) {
275
- // Handle Ctrl+C gracefully
276
- if (error instanceof Error && error.name === 'ExitPromptError') {
277
- process.exit(0);
261
+ // Show mode selection dropdown
262
+ const mode = await select({
263
+ message: 'Select authentication mode:',
264
+ choices: [
265
+ {
266
+ value: 'plugin',
267
+ name: 'MCP Plugin (Recommended)',
268
+ description: 'Requires Framer to be open with MCP plugin running. Paste URL from plugin.',
269
+ },
270
+ {
271
+ value: 'server-api',
272
+ name: 'Server API',
273
+ description: 'Works without Framer open. Requires API key from project settings.',
274
+ },
275
+ ],
276
+ });
277
+ if (mode === 'plugin') {
278
+ const shortcut = process.platform === 'darwin' ? 'Cmd+K' : 'Ctrl+K';
279
+ console.log('\nTo get your MCP URL:');
280
+ console.log(' 1. Go to https://framer.com and open your project');
281
+ console.log(` 2. Press ${shortcut} and search for "MCP" plugin`);
282
+ console.log(' 3. Copy the URL shown in the plugin\n');
283
+ const mcpUrl = await input({ message: 'Paste MCP URL:' });
284
+ if (!mcpUrl) {
285
+ console.error('MCP URL is required');
286
+ process.exit(1);
287
+ }
288
+ saveConfig({ mode: 'plugin', mcpUrl });
289
+ console.log(`\nMCP URL saved to ${getConfigPath()}`);
290
+ console.log(`Run \`unframer --help\` to see all available MCP commands`);
291
+ }
292
+ else {
293
+ // Server API mode
294
+ console.log('\nTo get your API key:');
295
+ console.log(' 1. Open your Framer project at https://framer.com/projects');
296
+ console.log(' 2. Go to Project Settings > API');
297
+ console.log(' 3. Generate or copy your API key\n');
298
+ const apiKey = await password({ message: 'Enter Framer API key:', mask: '*' });
299
+ if (!apiKey) {
300
+ console.error('API key is required');
301
+ process.exit(1);
302
+ }
303
+ console.log('\nTo get your project URL:');
304
+ console.log(' Copy the URL from your browser when viewing the project');
305
+ console.log(' Example: https://framer.com/projects/MyProject--abc123\n');
306
+ const projectUrl = await input({
307
+ message: 'Enter Framer project URL (optional, can use --project later):',
308
+ });
309
+ saveConfig({
310
+ mode: 'server-api',
311
+ framerApiKey: apiKey,
312
+ framerProjectUrl: projectUrl || undefined,
313
+ });
314
+ console.log(`\nServer API credentials saved to ${getConfigPath()}`);
315
+ console.log(`\nUsage:`);
316
+ if (projectUrl) {
317
+ console.log(` unframer mcp getProjectXml`);
318
+ }
319
+ else {
320
+ console.log(` unframer mcp getProjectXml --project "https://framer.com/projects/..."`);
278
321
  }
279
- throw error;
322
+ console.log(`\nOr set FRAMER_PROJECT_URL environment variable`);
280
323
  }
281
324
  }
282
- if (!mcpUrl) {
283
- console.error('MCP URL is required');
284
- process.exit(1);
325
+ catch (error) {
326
+ if (error instanceof Error && error.name === 'ExitPromptError') {
327
+ process.exit(0);
328
+ }
329
+ throw error;
285
330
  }
286
- saveConfig({ mcpUrl });
287
- console.log(`MCP URL saved to ${getConfigPath()}`);
288
331
  });
289
- // Add MCP tool commands - only registered if transport is available
290
- await addMcpCommands({
291
- cli,
292
- commandPrefix: 'mcp',
293
- getMcpTransport: (sessionId) => {
294
- const config = loadConfig();
295
- if (!config.mcpUrl) {
296
- return null;
297
- }
298
- const url = new URL(config.mcpUrl);
299
- // Use /mcp endpoint for StreamableHTTP
300
- if (url.pathname.endsWith('/sse')) {
301
- url.pathname = url.pathname.replace(/\/sse$/, '/mcp');
332
+ // Add MCP tool commands
333
+ const config = loadConfig();
334
+ const cliArgs = process.argv.slice(2);
335
+ const hasMcpCommand = cliArgs.includes('mcp');
336
+ const hasProjectOption = cliArgs.some((arg) => {
337
+ return arg === '--project' || arg.startsWith('--project=');
338
+ });
339
+ const shouldUseServerApiForProjectOption = hasMcpCommand && hasProjectOption;
340
+ const mcpMode = shouldUseServerApiForProjectOption
341
+ ? 'server-api'
342
+ : config.mode || (config.mcpUrl ? 'plugin' : undefined);
343
+ if (mcpMode === 'server-api') {
344
+ // Server API mode - use framer-api directly
345
+ // Commands are registered via registerServerApiCommands below
346
+ await registerServerApiCommands();
347
+ }
348
+ else {
349
+ // Plugin mode - use MCP transport
350
+ await addMcpCommands({
351
+ cli,
352
+ commandPrefix: 'mcp',
353
+ getMcpTransport: async (sessionId) => {
354
+ const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
355
+ // UNFRAMER_MCP_URL env var overrides config file (contains full URL with auth)
356
+ const mcpUrl = process.env.UNFRAMER_MCP_URL || loadConfig().mcpUrl;
357
+ if (!mcpUrl) {
358
+ return null;
359
+ }
360
+ const url = new URL(mcpUrl);
361
+ // Use /mcp endpoint for StreamableHTTP
362
+ if (url.pathname.endsWith('/sse')) {
363
+ url.pathname = url.pathname.replace(/\/sse$/, '/mcp');
364
+ }
365
+ return new StreamableHTTPClientTransport(url, { sessionId });
366
+ },
367
+ loadCache: () => {
368
+ return loadConfig().cachedMcpTools;
369
+ },
370
+ saveCache: (cache) => {
371
+ const configNow = loadConfig();
372
+ saveConfig({ ...configNow, cachedMcpTools: cache });
373
+ },
374
+ }).catch((e) => console.error(e));
375
+ }
376
+ /**
377
+ * Register MCP commands for server-api mode using framer-api directly.
378
+ * This bypasses the MCP transport and calls handlers directly.
379
+ */
380
+ async function registerServerApiCommands() {
381
+ // Dynamic import to avoid loading framer-api in plugin mode
382
+ const { connect } = await import('framer-api');
383
+ // Import tool definitions and handler from plugin-mcp
384
+ // Note: Run `pnpm --filter plugin-mcp build && pnpm --filter plugin-mcp gen-unframer` first
385
+ const { mcpTools, mcpToolHandler } = await import('./plugin-mcp-dist/lib/mcp-handlers.js');
386
+ if (!mcpTools || !mcpToolHandler) {
387
+ return;
388
+ }
389
+ // Register a command for each MCP tool
390
+ for (const [toolName, toolDef] of Object.entries(mcpTools)) {
391
+ const cmd = cli.command(`mcp ${toolName}`, toolDef.description.split('\n')[0]);
392
+ cmd.option('--project <url>', 'Framer project URL. Uses server-api mode (framer-api headless). Works alongside plugin mode login, pass --project to switch to server-api for a single command. Also reads FRAMER_PROJECT_URL env var.');
393
+ // Add options based on tool input schema, using zod v4 native JSON Schema conversion
394
+ const inputSchema = toolDef.input;
395
+ if (inputSchema) {
396
+ const jsonSchema = z.toJSONSchema(inputSchema);
397
+ const properties = jsonSchema.properties || {};
398
+ const required = new Set(jsonSchema.required || []);
399
+ for (const [key, prop] of Object.entries(properties)) {
400
+ const isRequired = required.has(key);
401
+ const isBooleanType = prop.type === 'boolean';
402
+ const optionName = isBooleanType
403
+ ? `--${key}`
404
+ : `--${key} <value>`;
405
+ const optionDescription = [
406
+ prop.description || key,
407
+ isRequired ? '(required)' : '',
408
+ ]
409
+ .filter(Boolean)
410
+ .join(' ');
411
+ if (isBooleanType && prop.default === undefined) {
412
+ cmd.option(optionName, optionDescription);
413
+ continue;
414
+ }
415
+ cmd.option(optionName, wrapJsonSchema({
416
+ ...prop,
417
+ description: optionDescription,
418
+ }));
419
+ }
302
420
  }
303
- return new StreamableHTTPClientTransport(url, { sessionId });
304
- },
305
- }).catch(e => console.error(e));
421
+ cmd.action(async (options) => {
422
+ const projectOption = typeof options.project === 'string'
423
+ ? options.project
424
+ : undefined;
425
+ const projectUrl = projectOption ||
426
+ process.env.FRAMER_PROJECT_URL ||
427
+ loadConfig().framerProjectUrl;
428
+ const apiKey = process.env.FRAMER_API_KEY || loadConfig().framerApiKey;
429
+ if (!projectUrl) {
430
+ console.error('Project URL required. Use --project option, FRAMER_PROJECT_URL env var, or set during login.');
431
+ process.exit(1);
432
+ }
433
+ if (!apiKey) {
434
+ console.error('API key required. Set FRAMER_API_KEY env var or run `unframer mcp login` first.');
435
+ process.exit(1);
436
+ }
437
+ // Remove CLI-specific options from input
438
+ const toolInput = Object.fromEntries(Object.entries(options).filter(([key]) => {
439
+ return key !== 'project';
440
+ }));
441
+ const globalWithFramer = globalThis;
442
+ let framerClient = undefined;
443
+ let actionError = undefined;
444
+ try {
445
+ spinner.start(`Connecting to Framer...`);
446
+ framerClient = await connect(projectUrl, apiKey);
447
+ // Set global framer for utility functions that use it
448
+ globalWithFramer.framer = framerClient;
449
+ spinner.start(`Running ${toolName}...`);
450
+ const result = await mcpToolHandler({
451
+ type: toolName,
452
+ input: toolInput,
453
+ });
454
+ spinner.stop('');
455
+ // Output result
456
+ if (typeof result === 'string') {
457
+ console.log(result);
458
+ }
459
+ else {
460
+ console.log(JSON.stringify(result, null, 2));
461
+ }
462
+ }
463
+ catch (error) {
464
+ actionError = error;
465
+ }
466
+ finally {
467
+ if (framerClient) {
468
+ try {
469
+ await framerClient.disconnect();
470
+ }
471
+ catch (disconnectError) {
472
+ logger.error('Failed disconnecting from Framer:', disconnectError);
473
+ }
474
+ }
475
+ delete globalWithFramer.framer;
476
+ }
477
+ if (actionError) {
478
+ spinner.error(`Failed: ${actionError instanceof Error ? actionError.message : String(actionError)}`);
479
+ process.exit(1);
480
+ }
481
+ });
482
+ }
483
+ }
306
484
  export async function configFromFetch({ projectId, externalPackages = [], allExternal = false, agent = '', outDir = undefined, }) {
307
485
  logger.log(`Fetching config for project ${projectId}`);
308
486
  const url = process.env.UNFRAMER_SERVER_URL;