@nifrajs/cli 3.5.0 → 4.0.2

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 (404) hide show
  1. package/README.md +42 -3
  2. package/dist/app-files.d.ts +11 -0
  3. package/dist/app-files.d.ts.map +1 -0
  4. package/dist/app-files.js +34 -0
  5. package/dist/app-files.js.map +1 -0
  6. package/dist/assure-hydration.d.ts +7 -1
  7. package/dist/assure-hydration.d.ts.map +1 -1
  8. package/dist/assure-hydration.js +128 -43
  9. package/dist/assure-hydration.js.map +1 -1
  10. package/dist/assure.d.ts.map +1 -1
  11. package/dist/assure.js +16 -2
  12. package/dist/assure.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts +5 -1
  14. package/dist/capabilities-tool.d.ts.map +1 -1
  15. package/dist/capabilities-tool.js +32 -10
  16. package/dist/capabilities-tool.js.map +1 -1
  17. package/dist/cdn-check.d.ts +41 -0
  18. package/dist/cdn-check.d.ts.map +1 -0
  19. package/dist/cdn-check.js +207 -0
  20. package/dist/cdn-check.js.map +1 -0
  21. package/dist/check-diagnostics.d.ts +4 -0
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +13 -0
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +49 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +291 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +99 -63
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +42 -16
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +262 -82
  35. package/dist/cli.js.map +1 -1
  36. package/dist/command-catalog.d.ts +62 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +275 -35
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/config-target.d.ts +8 -0
  41. package/dist/config-target.d.ts.map +1 -0
  42. package/dist/config-target.js +45 -0
  43. package/dist/config-target.js.map +1 -0
  44. package/dist/contracts.d.ts.map +1 -1
  45. package/dist/contracts.js +52 -13
  46. package/dist/contracts.js.map +1 -1
  47. package/dist/db-audit.d.ts +37 -0
  48. package/dist/db-audit.d.ts.map +1 -0
  49. package/dist/db-audit.js +99 -0
  50. package/dist/db-audit.js.map +1 -0
  51. package/dist/db-child.d.ts +64 -0
  52. package/dist/db-child.d.ts.map +1 -0
  53. package/dist/db-child.js +213 -0
  54. package/dist/db-child.js.map +1 -0
  55. package/dist/db-config.d.ts +60 -0
  56. package/dist/db-config.d.ts.map +1 -0
  57. package/dist/db-config.js +176 -0
  58. package/dist/db-config.js.map +1 -0
  59. package/dist/db-tool.d.ts +84 -0
  60. package/dist/db-tool.d.ts.map +1 -0
  61. package/dist/db-tool.js +537 -0
  62. package/dist/db-tool.js.map +1 -0
  63. package/dist/dev-bun-config.d.ts +20 -13
  64. package/dist/dev-bun-config.d.ts.map +1 -1
  65. package/dist/dev-bun-config.js +98 -41
  66. package/dist/dev-bun-config.js.map +1 -1
  67. package/dist/dev-feed-tool.d.ts +106 -0
  68. package/dist/dev-feed-tool.d.ts.map +1 -0
  69. package/dist/dev-feed-tool.js +713 -0
  70. package/dist/dev-feed-tool.js.map +1 -0
  71. package/dist/dev-server-client.d.ts +67 -0
  72. package/dist/dev-server-client.d.ts.map +1 -0
  73. package/dist/dev-server-client.js +195 -0
  74. package/dist/dev-server-client.js.map +1 -0
  75. package/dist/diff-tool.d.ts +3 -3
  76. package/dist/diff-tool.d.ts.map +1 -1
  77. package/dist/diff-tool.js +6 -5
  78. package/dist/diff-tool.js.map +1 -1
  79. package/dist/docs-search.d.ts.map +1 -1
  80. package/dist/docs-search.js +12 -4
  81. package/dist/docs-search.js.map +1 -1
  82. package/dist/doctor.d.ts +17 -0
  83. package/dist/doctor.d.ts.map +1 -1
  84. package/dist/doctor.js +76 -15
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/env-file.d.ts +15 -0
  87. package/dist/env-file.d.ts.map +1 -1
  88. package/dist/env-file.js +73 -1
  89. package/dist/env-file.js.map +1 -1
  90. package/dist/fix-recipes.d.ts.map +1 -1
  91. package/dist/fix-recipes.js +48 -22
  92. package/dist/fix-recipes.js.map +1 -1
  93. package/dist/frontend-guidance.js +5 -5
  94. package/dist/frontend-guidance.js.map +1 -1
  95. package/dist/i18n-check.d.ts +16 -0
  96. package/dist/i18n-check.d.ts.map +1 -0
  97. package/dist/i18n-check.js +154 -0
  98. package/dist/i18n-check.js.map +1 -0
  99. package/dist/init-agents.d.ts +49 -13
  100. package/dist/init-agents.d.ts.map +1 -1
  101. package/dist/init-agents.js +239 -34
  102. package/dist/init-agents.js.map +1 -1
  103. package/dist/internal/code-unit-order.d.ts +4 -0
  104. package/dist/internal/code-unit-order.d.ts.map +1 -0
  105. package/dist/internal/code-unit-order.js +6 -0
  106. package/dist/internal/code-unit-order.js.map +1 -0
  107. package/dist/internal/script-kind.d.ts +7 -0
  108. package/dist/internal/script-kind.d.ts.map +1 -0
  109. package/dist/internal/script-kind.js +12 -0
  110. package/dist/internal/script-kind.js.map +1 -0
  111. package/dist/internal/source-facts.d.ts +5 -0
  112. package/dist/internal/source-facts.d.ts.map +1 -1
  113. package/dist/internal/source-facts.js +61 -5
  114. package/dist/internal/source-facts.js.map +1 -1
  115. package/dist/internal/typescript-import.d.ts +49 -24
  116. package/dist/internal/typescript-import.d.ts.map +1 -1
  117. package/dist/internal/typescript-import.js +507 -67
  118. package/dist/internal/typescript-import.js.map +1 -1
  119. package/dist/introspect.d.ts +5 -1
  120. package/dist/introspect.d.ts.map +1 -1
  121. package/dist/introspect.js +92 -27
  122. package/dist/introspect.js.map +1 -1
  123. package/dist/learn.js +3 -3
  124. package/dist/learn.js.map +1 -1
  125. package/dist/levels-tool.d.ts +10 -0
  126. package/dist/levels-tool.d.ts.map +1 -1
  127. package/dist/levels-tool.js +33 -3
  128. package/dist/levels-tool.js.map +1 -1
  129. package/dist/load.d.ts +67 -17
  130. package/dist/load.d.ts.map +1 -1
  131. package/dist/load.js +78 -18
  132. package/dist/load.js.map +1 -1
  133. package/dist/manifest-tool.d.ts.map +1 -1
  134. package/dist/manifest-tool.js +3 -2
  135. package/dist/manifest-tool.js.map +1 -1
  136. package/dist/mcp-app-tool.d.ts.map +1 -1
  137. package/dist/mcp-app-tool.js +2 -0
  138. package/dist/mcp-app-tool.js.map +1 -1
  139. package/dist/mcp-context.js +1 -1
  140. package/dist/mcp-context.js.map +1 -1
  141. package/dist/mcp-delegate.d.ts +57 -0
  142. package/dist/mcp-delegate.d.ts.map +1 -0
  143. package/dist/mcp-delegate.js +130 -0
  144. package/dist/mcp-delegate.js.map +1 -0
  145. package/dist/mcp-docs-tools.d.ts.map +1 -1
  146. package/dist/mcp-docs-tools.js +23 -5
  147. package/dist/mcp-docs-tools.js.map +1 -1
  148. package/dist/mcp-exec.d.ts +2 -0
  149. package/dist/mcp-exec.d.ts.map +1 -1
  150. package/dist/mcp-exec.js +132 -108
  151. package/dist/mcp-exec.js.map +1 -1
  152. package/dist/mcp-http.d.ts.map +1 -1
  153. package/dist/mcp-http.js +17 -5
  154. package/dist/mcp-http.js.map +1 -1
  155. package/dist/mcp-io.d.ts +14 -0
  156. package/dist/mcp-io.d.ts.map +1 -1
  157. package/dist/mcp-io.js +45 -0
  158. package/dist/mcp-io.js.map +1 -1
  159. package/dist/mcp-isolate.d.ts +48 -0
  160. package/dist/mcp-isolate.d.ts.map +1 -0
  161. package/dist/mcp-isolate.js +222 -0
  162. package/dist/mcp-isolate.js.map +1 -0
  163. package/dist/mcp-project-child.d.ts +74 -0
  164. package/dist/mcp-project-child.d.ts.map +1 -0
  165. package/dist/mcp-project-child.js +130 -0
  166. package/dist/mcp-project-child.js.map +1 -0
  167. package/dist/mcp-render.js +2 -2
  168. package/dist/mcp-render.js.map +1 -1
  169. package/dist/mcp-root.d.ts +37 -9
  170. package/dist/mcp-root.d.ts.map +1 -1
  171. package/dist/mcp-root.js +110 -23
  172. package/dist/mcp-root.js.map +1 -1
  173. package/dist/mcp-run.d.ts +18 -1
  174. package/dist/mcp-run.d.ts.map +1 -1
  175. package/dist/mcp-run.js +82 -9
  176. package/dist/mcp-run.js.map +1 -1
  177. package/dist/mcp-ws.js +4 -1
  178. package/dist/mcp-ws.js.map +1 -1
  179. package/dist/mcp.d.ts +16 -3
  180. package/dist/mcp.d.ts.map +1 -1
  181. package/dist/mcp.js +81 -53
  182. package/dist/mcp.js.map +1 -1
  183. package/dist/migrate-layout.d.ts +44 -0
  184. package/dist/migrate-layout.d.ts.map +1 -0
  185. package/dist/migrate-layout.js +1155 -0
  186. package/dist/migrate-layout.js.map +1 -0
  187. package/dist/openapi-tool.d.ts +2 -2
  188. package/dist/openapi-tool.d.ts.map +1 -1
  189. package/dist/openapi-tool.js.map +1 -1
  190. package/dist/openapi-types.d.ts +2 -1
  191. package/dist/openapi-types.d.ts.map +1 -1
  192. package/dist/openapi-types.js +240 -12
  193. package/dist/openapi-types.js.map +1 -1
  194. package/dist/pipeline-report.d.ts +3 -2
  195. package/dist/pipeline-report.d.ts.map +1 -1
  196. package/dist/pipeline-report.js +19 -16
  197. package/dist/pipeline-report.js.map +1 -1
  198. package/dist/port.d.ts +5 -4
  199. package/dist/port.d.ts.map +1 -1
  200. package/dist/port.js +46 -29
  201. package/dist/port.js.map +1 -1
  202. package/dist/project-facts.d.ts +5 -0
  203. package/dist/project-facts.d.ts.map +1 -1
  204. package/dist/project-facts.js.map +1 -1
  205. package/dist/recipes/4.0.0.d.ts +9 -0
  206. package/dist/recipes/4.0.0.d.ts.map +1 -0
  207. package/dist/recipes/4.0.0.js +36 -0
  208. package/dist/recipes/4.0.0.js.map +1 -0
  209. package/dist/recipes/index.d.ts.map +1 -1
  210. package/dist/recipes/index.js +2 -0
  211. package/dist/recipes/index.js.map +1 -1
  212. package/dist/release-verification.d.ts +11 -1
  213. package/dist/release-verification.d.ts.map +1 -1
  214. package/dist/release-verification.js +47 -4
  215. package/dist/release-verification.js.map +1 -1
  216. package/dist/review.d.ts.map +1 -1
  217. package/dist/review.js +12 -8
  218. package/dist/review.js.map +1 -1
  219. package/dist/route-types.d.ts +28 -0
  220. package/dist/route-types.d.ts.map +1 -0
  221. package/dist/route-types.js +106 -0
  222. package/dist/route-types.js.map +1 -0
  223. package/dist/rules/codes.d.ts +9 -0
  224. package/dist/rules/codes.d.ts.map +1 -1
  225. package/dist/rules/codes.js +9 -0
  226. package/dist/rules/codes.js.map +1 -1
  227. package/dist/rules/data-guard.d.ts +12 -0
  228. package/dist/rules/data-guard.d.ts.map +1 -0
  229. package/dist/rules/data-guard.js +213 -0
  230. package/dist/rules/data-guard.js.map +1 -0
  231. package/dist/rules/index.d.ts +3 -0
  232. package/dist/rules/index.d.ts.map +1 -1
  233. package/dist/rules/index.js +17 -5
  234. package/dist/rules/index.js.map +1 -1
  235. package/dist/rules/islands.d.ts.map +1 -1
  236. package/dist/rules/islands.js +7 -4
  237. package/dist/rules/islands.js.map +1 -1
  238. package/dist/rules/legacy.d.ts.map +1 -1
  239. package/dist/rules/legacy.js +71 -38
  240. package/dist/rules/legacy.js.map +1 -1
  241. package/dist/rules/nano.d.ts.map +1 -1
  242. package/dist/rules/nano.js +15 -6
  243. package/dist/rules/nano.js.map +1 -1
  244. package/dist/rules/pages.d.ts +15 -0
  245. package/dist/rules/pages.d.ts.map +1 -0
  246. package/dist/rules/pages.js +88 -0
  247. package/dist/rules/pages.js.map +1 -0
  248. package/dist/rules/routes.d.ts +11 -0
  249. package/dist/rules/routes.d.ts.map +1 -1
  250. package/dist/rules/routes.js +180 -2
  251. package/dist/rules/routes.js.map +1 -1
  252. package/dist/rules/secrets.d.ts +10 -0
  253. package/dist/rules/secrets.d.ts.map +1 -0
  254. package/dist/rules/secrets.js +169 -0
  255. package/dist/rules/secrets.js.map +1 -0
  256. package/dist/rules/security.d.ts +7 -0
  257. package/dist/rules/security.d.ts.map +1 -1
  258. package/dist/rules/security.js +171 -65
  259. package/dist/rules/security.js.map +1 -1
  260. package/dist/rules/typescript.d.ts +16 -0
  261. package/dist/rules/typescript.d.ts.map +1 -0
  262. package/dist/rules/typescript.js +59 -0
  263. package/dist/rules/typescript.js.map +1 -0
  264. package/dist/rules/zones.d.ts +17 -0
  265. package/dist/rules/zones.d.ts.map +1 -0
  266. package/dist/rules/zones.js +174 -0
  267. package/dist/rules/zones.js.map +1 -0
  268. package/dist/scaffold.d.ts +25 -12
  269. package/dist/scaffold.d.ts.map +1 -1
  270. package/dist/scaffold.js +132 -87
  271. package/dist/scaffold.js.map +1 -1
  272. package/dist/sdk.d.ts +17 -3
  273. package/dist/sdk.d.ts.map +1 -1
  274. package/dist/sdk.js +941 -153
  275. package/dist/sdk.js.map +1 -1
  276. package/dist/search-terms.d.ts +2 -0
  277. package/dist/search-terms.d.ts.map +1 -1
  278. package/dist/search-terms.js +23 -8
  279. package/dist/search-terms.js.map +1 -1
  280. package/dist/smoke.d.ts +79 -0
  281. package/dist/smoke.d.ts.map +1 -0
  282. package/dist/smoke.js +494 -0
  283. package/dist/smoke.js.map +1 -0
  284. package/dist/sync-manifest.d.ts +10 -2
  285. package/dist/sync-manifest.d.ts.map +1 -1
  286. package/dist/sync-manifest.js +44 -19
  287. package/dist/sync-manifest.js.map +1 -1
  288. package/dist/sync-routes.d.ts +4 -3
  289. package/dist/sync-routes.d.ts.map +1 -1
  290. package/dist/sync-routes.js +4 -3
  291. package/dist/sync-routes.js.map +1 -1
  292. package/dist/test-tool.d.ts.map +1 -1
  293. package/dist/test-tool.js +30 -11
  294. package/dist/test-tool.js.map +1 -1
  295. package/dist/types-search.d.ts.map +1 -1
  296. package/dist/types-search.js +11 -5
  297. package/dist/types-search.js.map +1 -1
  298. package/dist/upgrade.d.ts +36 -6
  299. package/dist/upgrade.d.ts.map +1 -1
  300. package/dist/upgrade.js +167 -29
  301. package/dist/upgrade.js.map +1 -1
  302. package/dist/verification-plan.d.ts.map +1 -1
  303. package/dist/verification-plan.js +4 -0
  304. package/dist/verification-plan.js.map +1 -1
  305. package/dist/verification.d.ts.map +1 -1
  306. package/dist/verification.js +45 -12
  307. package/dist/verification.js.map +1 -1
  308. package/dist/web-app-options.d.ts +19 -0
  309. package/dist/web-app-options.d.ts.map +1 -0
  310. package/dist/web-app-options.js +49 -0
  311. package/dist/web-app-options.js.map +1 -0
  312. package/dist/work-graph.d.ts.map +1 -1
  313. package/dist/work-graph.js +29 -13
  314. package/dist/work-graph.js.map +1 -1
  315. package/docs/examples.json +185 -29
  316. package/docs/llms-full.txt +3807 -580
  317. package/docs/types.json +14229 -10939
  318. package/package.json +19 -12
  319. package/src/app-files.ts +40 -0
  320. package/src/assure-hydration.ts +139 -46
  321. package/src/assure.ts +17 -2
  322. package/src/capabilities-tool.ts +35 -9
  323. package/src/cdn-check.ts +268 -0
  324. package/src/check-diagnostics.ts +17 -0
  325. package/src/check-scan.ts +358 -63
  326. package/src/check.ts +97 -62
  327. package/src/cli.ts +296 -87
  328. package/src/command-catalog.ts +389 -48
  329. package/src/config-target.ts +47 -0
  330. package/src/contracts.ts +54 -12
  331. package/src/db-audit.ts +128 -0
  332. package/src/db-child.ts +285 -0
  333. package/src/db-config.ts +224 -0
  334. package/src/db-tool.ts +690 -0
  335. package/src/dev-bun-config.ts +116 -40
  336. package/src/dev-feed-tool.ts +912 -0
  337. package/src/dev-server-client.ts +267 -0
  338. package/src/diff-tool.ts +6 -5
  339. package/src/docs-search.ts +10 -3
  340. package/src/doctor.ts +98 -15
  341. package/src/env-file.ts +78 -1
  342. package/src/fix-recipes.ts +53 -32
  343. package/src/frontend-guidance.ts +5 -5
  344. package/src/i18n-check.ts +179 -0
  345. package/src/init-agents.ts +301 -38
  346. package/src/internal/code-unit-order.ts +5 -0
  347. package/src/internal/script-kind.ts +11 -0
  348. package/src/internal/source-facts.ts +96 -22
  349. package/src/internal/typescript-import.ts +663 -68
  350. package/src/introspect.ts +90 -27
  351. package/src/learn.ts +3 -3
  352. package/src/levels-tool.ts +47 -2
  353. package/src/load.ts +140 -33
  354. package/src/manifest-tool.ts +3 -2
  355. package/src/mcp-app-tool.ts +2 -0
  356. package/src/mcp-context.ts +1 -1
  357. package/src/mcp-delegate.ts +162 -0
  358. package/src/mcp-docs-tools.ts +23 -5
  359. package/src/mcp-exec.ts +160 -132
  360. package/src/mcp-http.ts +19 -5
  361. package/src/mcp-io.ts +57 -0
  362. package/src/mcp-isolate.ts +287 -0
  363. package/src/mcp-project-child.ts +182 -0
  364. package/src/mcp-render.ts +2 -2
  365. package/src/mcp-root.ts +117 -22
  366. package/src/mcp-run.ts +104 -10
  367. package/src/mcp-ws.ts +4 -1
  368. package/src/mcp.ts +106 -64
  369. package/src/migrate-layout.ts +1338 -0
  370. package/src/openapi-tool.ts +7 -3
  371. package/src/openapi-types.ts +315 -13
  372. package/src/pipeline-report.ts +22 -18
  373. package/src/port.ts +53 -30
  374. package/src/project-facts.ts +5 -0
  375. package/src/recipes/4.0.0.ts +37 -0
  376. package/src/recipes/index.ts +2 -0
  377. package/src/release-verification.ts +74 -5
  378. package/src/review.ts +13 -8
  379. package/src/route-types.ts +133 -0
  380. package/src/rules/codes.ts +9 -0
  381. package/src/rules/data-guard.ts +230 -0
  382. package/src/rules/index.ts +18 -5
  383. package/src/rules/islands.ts +12 -4
  384. package/src/rules/legacy.ts +71 -39
  385. package/src/rules/nano.ts +18 -6
  386. package/src/rules/pages.ts +99 -0
  387. package/src/rules/routes.ts +181 -2
  388. package/src/rules/secrets.ts +174 -0
  389. package/src/rules/security.ts +198 -70
  390. package/src/rules/typescript.ts +77 -0
  391. package/src/rules/zones.ts +195 -0
  392. package/src/scaffold.ts +147 -95
  393. package/src/sdk.ts +1215 -158
  394. package/src/search-terms.ts +24 -7
  395. package/src/smoke.ts +607 -0
  396. package/src/sync-manifest.ts +50 -19
  397. package/src/sync-routes.ts +4 -3
  398. package/src/test-tool.ts +32 -11
  399. package/src/types-search.ts +11 -4
  400. package/src/upgrade.ts +205 -25
  401. package/src/verification-plan.ts +13 -0
  402. package/src/verification.ts +50 -12
  403. package/src/web-app-options.ts +57 -0
  404. package/src/work-graph.ts +27 -13
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Where `nifra mcp` keeps project code: in a fresh subprocess per call (`mcp-project-child.ts`), never
3
+ * in the server. The server lists the tools, resources and prompts and forwards each call; the child,
4
+ * started in the project's directory, loads `.env` there, imports the config and the backend, answers
5
+ * and exits. So the server holds neither the project's environment nor its modules, and every call
6
+ * sees the project's current code.
7
+ */
8
+
9
+ import { randomUUID } from "node:crypto"
10
+ import { existsSync } from "node:fs"
11
+ import { resolve } from "node:path"
12
+ import { fileURLToPath } from "node:url"
13
+ import { CONFIG_FILE } from "./app-files.ts"
14
+ import type { NifraMonorepoConfig } from "./load.ts"
15
+ import { appFingerprint } from "./mcp-exec.ts"
16
+ import { CHILD_TIMEOUT_MS, readBoundedLines, readBoundedStream } from "./mcp-io.ts"
17
+ import type {
18
+ AppSurface,
19
+ ProjectChildAnswer,
20
+ ProjectChildMessage,
21
+ ProjectChildRequest,
22
+ } from "./mcp-project-child.ts"
23
+ import type {
24
+ McpPrompt,
25
+ McpPromptMessage,
26
+ McpResource,
27
+ McpTool,
28
+ McpToolContext,
29
+ McpToolResult,
30
+ } from "./mcp-protocol.ts"
31
+
32
+ /**
33
+ * Project tools that stay in the server, because none runs project code in this process: each starts
34
+ * its own subprocess (run, render, ws, hydrate, test, the database tools), or reads only the
35
+ * framework's docs or the dev server's feed. `mcp-project-isolation.test.ts` calls each one and fails
36
+ * if the server evaluated project code, or if a name here has no probe there.
37
+ */
38
+ export const IN_PROCESS_TOOLS: ReadonlySet<string> = new Set([
39
+ "nifra_run",
40
+ "nifra_render",
41
+ "nifra_ws",
42
+ "nifra_hydrate",
43
+ "nifra_test",
44
+ "nifra_db_schema",
45
+ "nifra_db_query",
46
+ "nifra_db_role",
47
+ "nifra_errors",
48
+ "nifra_logs",
49
+ "nifra_inspect",
50
+ "nifra_explain",
51
+ "nifra_docs",
52
+ "nifra_example",
53
+ "nifra_types",
54
+ "nifra_learn",
55
+ "nifra_frontend",
56
+ ])
57
+
58
+ /** A tool result is bounded again by the stdio message limit when the server sends it. */
59
+ const ANSWER_MAX_BYTES = 8 * 1024 * 1024
60
+
61
+ function childPath(): string {
62
+ return fileURLToPath(new URL(import.meta.url)).replace(
63
+ /mcp-isolate\.(ts|js)$/,
64
+ "mcp-project-child.$1",
65
+ )
66
+ }
67
+
68
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
69
+ typeof value === "object" && value !== null && !Array.isArray(value)
70
+
71
+ const isAnswer = (value: unknown): value is ProjectChildAnswer =>
72
+ isRecord(value) &&
73
+ (value.ok === true || (value.ok === false && typeof value.message === "string"))
74
+
75
+ function parseMessage(text: string): ProjectChildMessage | undefined {
76
+ let value: unknown
77
+ try {
78
+ value = JSON.parse(text)
79
+ } catch {
80
+ return undefined
81
+ }
82
+ if (!isRecord(value)) return undefined
83
+ if (value.type === "answer" && isAnswer(value.answer)) {
84
+ return { type: "answer", answer: value.answer }
85
+ }
86
+ if (value.type === "progress" && typeof value.progress === "number") {
87
+ return typeof value.total === "number"
88
+ ? { type: "progress", progress: value.progress, total: value.total }
89
+ : { type: "progress", progress: value.progress }
90
+ }
91
+ return undefined
92
+ }
93
+
94
+ const failed = (message: string): ProjectChildAnswer => ({
95
+ ok: false,
96
+ message: `[nifra] ${message}`,
97
+ })
98
+
99
+ export interface ProjectChildOptions {
100
+ /** Progress is forwarded to it, and its cancellation kills the subprocess. */
101
+ readonly context?: McpToolContext
102
+ /** Kill the subprocess after this long. Tool calls have none: a client cancels them instead. */
103
+ readonly timeoutMs?: number
104
+ }
105
+
106
+ /** Answer `request` in a fresh subprocess in `cwd`; a crash, a timeout or no answer is `ok: false`. */
107
+ export async function askProjectChild(
108
+ cwd: string,
109
+ request: ProjectChildRequest,
110
+ options: ProjectChildOptions = {},
111
+ ): Promise<ProjectChildAnswer> {
112
+ const { context, timeoutMs } = options
113
+ if (context?.signal.aborted) return failed("the call was cancelled before it started")
114
+ const token = randomUUID()
115
+ let proc: Bun.Subprocess<"pipe", "pipe", "pipe">
116
+ try {
117
+ proc = Bun.spawn([process.execPath, childPath(), cwd], {
118
+ cwd,
119
+ // Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
120
+ env: process.env,
121
+ stdin: "pipe",
122
+ stdout: "pipe",
123
+ stderr: "pipe",
124
+ })
125
+ } catch (error) {
126
+ return failed(
127
+ `cannot start a process in ${cwd}: ${error instanceof Error ? error.message : String(error)}`,
128
+ )
129
+ }
130
+ let stopped: "timeout" | "cancel" | undefined
131
+ const stop = (reason: "timeout" | "cancel"): void => {
132
+ if (stopped !== undefined) return
133
+ stopped = reason
134
+ proc.kill("SIGKILL")
135
+ }
136
+ const timer = timeoutMs === undefined ? undefined : setTimeout(() => stop("timeout"), timeoutMs)
137
+ const onAbort = (): void => stop("cancel")
138
+ context?.signal.addEventListener("abort", onAbort, { once: true })
139
+ try {
140
+ proc.stdin.write(JSON.stringify({ ...request, token }))
141
+ await proc.stdin.end()
142
+ const stderr = readBoundedStream(proc.stderr, 16 * 1024)
143
+ let answer: ProjectChildAnswer | undefined
144
+ let tooLarge = false
145
+ for await (const item of readBoundedLines(proc.stdout, ANSWER_MAX_BYTES)) {
146
+ if (item.kind === "too-large") {
147
+ tooLarge = true
148
+ continue
149
+ }
150
+ if (!item.text.startsWith(`${token} `)) continue
151
+ const message = parseMessage(item.text.slice(token.length + 1))
152
+ if (message?.type === "progress") context?.reportProgress(message.progress, message.total)
153
+ else if (message?.type === "answer") answer = message.answer
154
+ }
155
+ const [errors] = await Promise.all([stderr, proc.exited])
156
+ if (answer !== undefined) return answer
157
+ if (stopped === "timeout") {
158
+ return failed(`the project process did not answer within ${timeoutMs}ms and was killed`)
159
+ }
160
+ if (stopped === "cancel") return failed("the call was cancelled")
161
+ if (tooLarge) return failed(`the answer exceeded ${ANSWER_MAX_BYTES} bytes`)
162
+ const tail = errors.text.trim().slice(-600)
163
+ const how = proc.signalCode === null ? `with code ${proc.exitCode}` : `on ${proc.signalCode}`
164
+ return failed(
165
+ `the project process exited ${how} before answering${tail === "" ? "" : `: ${tail}`}`,
166
+ )
167
+ } finally {
168
+ clearTimeout(timer)
169
+ context?.signal.removeEventListener("abort", onAbort)
170
+ }
171
+ }
172
+
173
+ async function callTool(
174
+ cwd: string,
175
+ name: string,
176
+ args: Record<string, unknown>,
177
+ context: McpToolContext,
178
+ ): Promise<string | McpToolResult> {
179
+ const answer = await askProjectChild(cwd, { op: "tool", name, args }, { context })
180
+ if (!answer.ok) throw new Error(answer.message)
181
+ if ("result" in answer) return answer.result
182
+ throw new Error(`[nifra] the project process answered ${name} without a result`)
183
+ }
184
+
185
+ async function readResource(
186
+ cwd: string,
187
+ uri: string,
188
+ ): Promise<{ readonly text: string; readonly mimeType?: string }> {
189
+ const answer = await askProjectChild(
190
+ cwd,
191
+ { op: "resource", uri },
192
+ { timeoutMs: CHILD_TIMEOUT_MS },
193
+ )
194
+ if (!answer.ok) throw new Error(answer.message)
195
+ if ("resource" in answer) return answer.resource
196
+ throw new Error(`[nifra] the project process answered ${uri} without its text`)
197
+ }
198
+
199
+ async function getPrompt(
200
+ cwd: string,
201
+ name: string,
202
+ args: Record<string, unknown>,
203
+ ): Promise<readonly McpPromptMessage[]> {
204
+ const answer = await askProjectChild(
205
+ cwd,
206
+ { op: "prompt", name, args },
207
+ { timeoutMs: CHILD_TIMEOUT_MS },
208
+ )
209
+ if (!answer.ok) throw new Error(answer.message)
210
+ if ("messages" in answer) return answer.messages
211
+ throw new Error(`[nifra] the project process answered ${name} without messages`)
212
+ }
213
+
214
+ /** Each tool runs in a fresh project subprocess, unless it is one that runs no project code here. */
215
+ export function isolateTools(cwd: string, tools: readonly McpTool[]): McpTool[] {
216
+ return tools.map((tool) =>
217
+ IN_PROCESS_TOOLS.has(tool.name)
218
+ ? tool
219
+ : { ...tool, handler: (args, context) => callTool(cwd, tool.name, args, context) },
220
+ )
221
+ }
222
+
223
+ /** Each resource is read in a fresh project subprocess. */
224
+ export function isolateResources(cwd: string, resources: readonly McpResource[]): McpResource[] {
225
+ return resources.map((resource) => ({ ...resource, read: () => readResource(cwd, resource.uri) }))
226
+ }
227
+
228
+ /** The app's own tools, resources and prompts, each forwarded to a project subprocess. */
229
+ export interface IsolatedSurface {
230
+ readonly tools: readonly McpTool[]
231
+ readonly resources: readonly McpResource[]
232
+ readonly prompts: readonly McpPrompt[]
233
+ }
234
+
235
+ const EMPTY_SURFACE: AppSurface = { tools: [], resources: [], prompts: [] }
236
+
237
+ async function readSurface(cwd: string): Promise<IsolatedSurface> {
238
+ const answer = await askProjectChild(cwd, { op: "surface" }, { timeoutMs: CHILD_TIMEOUT_MS })
239
+ const surface = answer.ok && "surface" in answer ? answer.surface : EMPTY_SURFACE
240
+ return {
241
+ tools: surface.tools.map((tool) => ({
242
+ ...tool,
243
+ handler: (args, context) => callTool(cwd, tool.name, args, context),
244
+ })),
245
+ resources: surface.resources.map((resource) => ({
246
+ ...resource,
247
+ read: () => readResource(cwd, resource.uri),
248
+ })),
249
+ prompts: surface.prompts.map((prompt) => ({
250
+ ...prompt,
251
+ handler: (args) => getPrompt(cwd, prompt.name, args),
252
+ })),
253
+ }
254
+ }
255
+
256
+ /**
257
+ * What the app declares on its backend (`app.tool(...)` and its resources and prompts), read in a
258
+ * project subprocess and kept until `nifra.config.ts`, `backend/framework.ts` or `backend/app.ts`
259
+ * changes. An app that cannot be loaded declares nothing: its absence must not withdraw the built-in
260
+ * tools, and a broken app must not start a subprocess on every message.
261
+ */
262
+ export function createAppSurface(cwd: string): () => Promise<IsolatedSurface> {
263
+ let cached:
264
+ | { readonly fingerprint: string; readonly surface: Promise<IsolatedSurface> }
265
+ | undefined
266
+ return async () => {
267
+ const fingerprint = await appFingerprint(cwd).catch(() => "unreadable")
268
+ if (cached?.fingerprint !== fingerprint) {
269
+ cached = { fingerprint, surface: readSurface(cwd) }
270
+ }
271
+ return cached.surface
272
+ }
273
+ }
274
+
275
+ /**
276
+ * `detectMonorepo`, with the root config evaluated in a project subprocess. Only a directory with a
277
+ * `nifra.config.ts` and no `routes/` can be a monorepo root, so no other directory starts one.
278
+ */
279
+ export async function detectMonorepoIsolated(cwd: string): Promise<NifraMonorepoConfig | null> {
280
+ if (!existsSync(resolve(cwd, CONFIG_FILE)) || existsSync(resolve(cwd, "routes"))) return null
281
+ const answer = await askProjectChild(cwd, { op: "monorepo" }, { timeoutMs: CHILD_TIMEOUT_MS })
282
+ if (!answer.ok || !("apps" in answer) || answer.apps === null) return null
283
+ const apps = Object.entries(answer.apps).filter(
284
+ (entry): entry is [string, string] => typeof entry[1] === "string",
285
+ )
286
+ return { apps: Object.fromEntries(apps) }
287
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The subprocess `nifra mcp` runs project code in. The server (`mcp-isolate.ts`) spawns a fresh one
3
+ * per call in the project's directory, so Bun loads that directory's `.env` files here, and the
4
+ * config, the backend and everything they import run here and exit with the process. The tools,
5
+ * resources and prompts answered here are the same ones the server lists: the project tools, and the
6
+ * ones the app declares on its backend.
7
+ *
8
+ * Protocol: one JSON request on stdin; on stdout, progress lines and then one answer line, each
9
+ * prefixed with the request's token, so anything project code prints cannot be read as an answer.
10
+ */
11
+
12
+ import { detectMonorepo } from "./load.ts"
13
+ import { projectResources } from "./mcp-context.ts"
14
+ import { createCachedAppLoader, projectTools } from "./mcp-exec.ts"
15
+ import { CHILD_INPUT_MAX_BYTES, readBoundedStream } from "./mcp-io.ts"
16
+ import type {
17
+ McpPrompt,
18
+ McpPromptMessage,
19
+ McpResource,
20
+ McpTool,
21
+ McpToolResult,
22
+ } from "./mcp-protocol.ts"
23
+ import {
24
+ extractBackendPrompts,
25
+ extractBackendResources,
26
+ extractBackendTools,
27
+ } from "./mcp-reflect.ts"
28
+
29
+ export type ProjectChildRequest =
30
+ | { readonly op: "monorepo" }
31
+ | { readonly op: "surface" }
32
+ | { readonly op: "tool"; readonly name: string; readonly args: Record<string, unknown> }
33
+ | { readonly op: "resource"; readonly uri: string }
34
+ | { readonly op: "prompt"; readonly name: string; readonly args: Record<string, unknown> }
35
+
36
+ /** What the server lists for the tools, resources and prompts the app declares on its backend. */
37
+ export interface AppSurface {
38
+ readonly tools: readonly Omit<McpTool, "handler">[]
39
+ readonly resources: readonly Omit<McpResource, "read">[]
40
+ readonly prompts: readonly Omit<McpPrompt, "handler">[]
41
+ }
42
+
43
+ export type ProjectChildAnswer =
44
+ | { readonly ok: true; readonly apps: Readonly<Record<string, string>> | null }
45
+ | { readonly ok: true; readonly surface: AppSurface }
46
+ | { readonly ok: true; readonly result: string | McpToolResult }
47
+ | { readonly ok: true; readonly resource: { readonly text: string; readonly mimeType?: string } }
48
+ | { readonly ok: true; readonly messages: readonly McpPromptMessage[] }
49
+ | { readonly ok: false; readonly message: string }
50
+
51
+ export type ProjectChildMessage =
52
+ | { readonly type: "progress"; readonly progress: number; readonly total?: number }
53
+ | { readonly type: "answer"; readonly answer: ProjectChildAnswer }
54
+
55
+ /**
56
+ * Answer `request` for the project at `cwd`. A tool, resource or prompt that throws answers
57
+ * `ok: false` with its message; an app that cannot be loaded declares nothing, as in the server.
58
+ */
59
+ export async function answerProjectRequest(
60
+ cwd: string,
61
+ request: ProjectChildRequest,
62
+ reportProgress: (progress: number, total?: number) => void = () => {},
63
+ ): Promise<ProjectChildAnswer> {
64
+ try {
65
+ if (request.op === "monorepo") {
66
+ return { ok: true, apps: (await detectMonorepo(cwd))?.apps ?? null }
67
+ }
68
+ const loadAppCached = createCachedAppLoader(cwd)
69
+ const backend = (): Promise<unknown> =>
70
+ loadAppCached().then(
71
+ (app) => app.backend,
72
+ () => undefined,
73
+ )
74
+ if (request.op === "surface") {
75
+ const declared = await backend()
76
+ return {
77
+ ok: true,
78
+ surface: {
79
+ tools: extractBackendTools(declared),
80
+ resources: extractBackendResources(declared),
81
+ prompts: extractBackendPrompts(declared),
82
+ },
83
+ }
84
+ }
85
+ if (request.op === "tool") {
86
+ const tool =
87
+ projectTools(cwd, loadAppCached).find((t) => t.name === request.name) ??
88
+ extractBackendTools(await backend()).find((t) => t.name === request.name)
89
+ if (tool === undefined) return { ok: false, message: `unknown tool: ${request.name}` }
90
+ const result = await tool.handler(request.args, {
91
+ signal: new AbortController().signal,
92
+ requestId: null,
93
+ reportProgress,
94
+ })
95
+ return { ok: true, result }
96
+ }
97
+ if (request.op === "resource") {
98
+ const resource =
99
+ projectResources(cwd, loadAppCached).find((r) => r.uri === request.uri) ??
100
+ extractBackendResources(await backend()).find((r) => r.uri === request.uri)
101
+ if (resource === undefined) return { ok: false, message: `unknown resource: ${request.uri}` }
102
+ return { ok: true, resource: await resource.read() }
103
+ }
104
+ const prompt = extractBackendPrompts(await backend()).find((p) => p.name === request.name)
105
+ if (prompt === undefined) return { ok: false, message: `unknown prompt: ${request.name}` }
106
+ return { ok: true, messages: await prompt.handler(request.args) }
107
+ } catch (error) {
108
+ return { ok: false, message: error instanceof Error ? error.message : String(error) }
109
+ }
110
+ }
111
+
112
+ const isArgs = (value: unknown): value is Record<string, unknown> =>
113
+ typeof value === "object" && value !== null && !Array.isArray(value)
114
+
115
+ function parseRequest(text: string): { token: string; request: ProjectChildRequest } | undefined {
116
+ let value: unknown
117
+ try {
118
+ value = JSON.parse(text)
119
+ } catch {
120
+ return undefined
121
+ }
122
+ if (!isArgs(value)) return undefined
123
+ const { token, op, name, uri, args } = value
124
+ if (typeof token !== "string" || !/^[0-9a-f-]{16,64}$/.test(token)) return undefined
125
+ if (op === "monorepo" || op === "surface") return { token, request: { op } }
126
+ if (op === "resource" && typeof uri === "string") return { token, request: { op, uri } }
127
+ if ((op === "tool" || op === "prompt") && typeof name === "string" && isArgs(args)) {
128
+ return { token, request: { op, name, args } }
129
+ }
130
+ return undefined
131
+ }
132
+
133
+ /**
134
+ * Serve one request: read it from `input`, answer it and write each message through `write`,
135
+ * prefixed with the request's token. Returns false (writing nothing) for a malformed request.
136
+ */
137
+ export async function serveProjectChild(
138
+ cwd: string,
139
+ input: ReadableStream<Uint8Array>,
140
+ write: (line: string) => Promise<void>,
141
+ ): Promise<boolean> {
142
+ const text = await readBoundedStream(input, CHILD_INPUT_MAX_BYTES)
143
+ const parsed = text.truncated ? undefined : parseRequest(text.text)
144
+ if (parsed === undefined) return false
145
+ const send = (message: ProjectChildMessage): Promise<void> =>
146
+ write(`${parsed.token} ${JSON.stringify(message)}\n`)
147
+ const answer = await answerProjectRequest(cwd, parsed.request, (progress, total) => {
148
+ void send(
149
+ total === undefined ? { type: "progress", progress } : { type: "progress", progress, total },
150
+ )
151
+ })
152
+ let line: string
153
+ try {
154
+ line = JSON.stringify({ type: "answer", answer } satisfies ProjectChildMessage)
155
+ } catch {
156
+ line = JSON.stringify({
157
+ type: "answer",
158
+ answer: { ok: false, message: "the answer is not serializable" },
159
+ } satisfies ProjectChildMessage)
160
+ }
161
+ await write(`${parsed.token} ${line}\n`)
162
+ return true
163
+ }
164
+
165
+ if (import.meta.main) {
166
+ // Bound before project code loads, so nothing it patches can reach the protocol channel.
167
+ const stdout = process.stdout.write.bind(process.stdout)
168
+ const toStderr = (...args: unknown[]): void => {
169
+ process.stderr.write(`${args.map((arg) => String(arg)).join(" ")}\n`)
170
+ }
171
+ console.log = toStderr
172
+ console.info = toStderr
173
+ console.debug = toStderr
174
+ const served = await serveProjectChild(
175
+ process.argv[2] ?? process.cwd(),
176
+ Bun.stdin.stream(),
177
+ (line) => new Promise((done) => stdout(line, () => done())),
178
+ )
179
+ if (!served) process.stderr.write("nifra mcp: invalid project request\n")
180
+ // Project code may leave a handle open (a pool, an interval); the answer is written, so stop here.
181
+ process.exit(served ? 0 : 2)
182
+ }
package/src/mcp-render.ts CHANGED
@@ -10,7 +10,6 @@
10
10
  * `{ error }` so the agent gets the actionable message.
11
11
  */
12
12
 
13
- import { inProcessClient } from "@nifrajs/client"
14
13
  import { type AppLike, runApp } from "@nifrajs/runner"
15
14
  import { createWebApp, type RenderAdapter } from "@nifrajs/web"
16
15
  import { discoverRoutes } from "@nifrajs/web/fs"
@@ -22,6 +21,7 @@ import {
22
21
  readBoundedStream,
23
22
  serializeBoundedJson,
24
23
  } from "./mcp-io.ts"
24
+ import { frameworkWebAppOptions } from "./web-app-options.ts"
25
25
 
26
26
  const errString = (err: unknown): string =>
27
27
  err instanceof Error ? `${err.name}: ${err.message}` : String(err)
@@ -54,7 +54,7 @@ async function buildWebApp(cwd: string): Promise<{ app: AppLike } | { error: str
54
54
  adapter: app.framework.adapter as RenderAdapter,
55
55
  manifest: discoverRoutes(app.routesDir),
56
56
  clientEntry: "/_nifra-render-only.js", // placeholder; SSR HTML does not need the built client
57
- ...(app.backend !== undefined ? { api: inProcessClient(app.backend as never) } : {}),
57
+ ...frameworkWebAppOptions(app.framework, app.backend),
58
58
  }) as unknown as AppLike
59
59
  return { app: webApp }
60
60
  } catch (err) {