@nifrajs/cli 3.5.0 → 4.0.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 (403) 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 +57 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +270 -26
  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 +41 -21
  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 +982 -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 +34 -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.map +1 -1
  285. package/dist/sync-manifest.js +2 -1
  286. package/dist/sync-manifest.js.map +1 -1
  287. package/dist/sync-routes.d.ts +4 -3
  288. package/dist/sync-routes.d.ts.map +1 -1
  289. package/dist/sync-routes.js +4 -3
  290. package/dist/sync-routes.js.map +1 -1
  291. package/dist/test-tool.d.ts.map +1 -1
  292. package/dist/test-tool.js +30 -11
  293. package/dist/test-tool.js.map +1 -1
  294. package/dist/types-search.d.ts.map +1 -1
  295. package/dist/types-search.js +11 -5
  296. package/dist/types-search.js.map +1 -1
  297. package/dist/upgrade.d.ts +36 -6
  298. package/dist/upgrade.d.ts.map +1 -1
  299. package/dist/upgrade.js +167 -29
  300. package/dist/upgrade.js.map +1 -1
  301. package/dist/verification-plan.d.ts.map +1 -1
  302. package/dist/verification-plan.js +4 -0
  303. package/dist/verification-plan.js.map +1 -1
  304. package/dist/verification.d.ts.map +1 -1
  305. package/dist/verification.js +45 -12
  306. package/dist/verification.js.map +1 -1
  307. package/dist/web-app-options.d.ts +19 -0
  308. package/dist/web-app-options.d.ts.map +1 -0
  309. package/dist/web-app-options.js +49 -0
  310. package/dist/web-app-options.js.map +1 -0
  311. package/dist/work-graph.d.ts.map +1 -1
  312. package/dist/work-graph.js +29 -13
  313. package/dist/work-graph.js.map +1 -1
  314. package/docs/examples.json +185 -29
  315. package/docs/llms-full.txt +3807 -580
  316. package/docs/types.json +14229 -10939
  317. package/package.json +19 -12
  318. package/src/app-files.ts +40 -0
  319. package/src/assure-hydration.ts +139 -46
  320. package/src/assure.ts +17 -2
  321. package/src/capabilities-tool.ts +35 -9
  322. package/src/cdn-check.ts +268 -0
  323. package/src/check-diagnostics.ts +17 -0
  324. package/src/check-scan.ts +358 -63
  325. package/src/check.ts +97 -62
  326. package/src/cli.ts +296 -87
  327. package/src/command-catalog.ts +374 -38
  328. package/src/config-target.ts +47 -0
  329. package/src/contracts.ts +54 -12
  330. package/src/db-audit.ts +128 -0
  331. package/src/db-child.ts +285 -0
  332. package/src/db-config.ts +224 -0
  333. package/src/db-tool.ts +690 -0
  334. package/src/dev-bun-config.ts +116 -40
  335. package/src/dev-feed-tool.ts +912 -0
  336. package/src/dev-server-client.ts +267 -0
  337. package/src/diff-tool.ts +6 -5
  338. package/src/docs-search.ts +10 -3
  339. package/src/doctor.ts +98 -15
  340. package/src/env-file.ts +78 -1
  341. package/src/fix-recipes.ts +44 -31
  342. package/src/frontend-guidance.ts +5 -5
  343. package/src/i18n-check.ts +179 -0
  344. package/src/init-agents.ts +301 -38
  345. package/src/internal/code-unit-order.ts +5 -0
  346. package/src/internal/script-kind.ts +11 -0
  347. package/src/internal/source-facts.ts +96 -22
  348. package/src/internal/typescript-import.ts +663 -68
  349. package/src/introspect.ts +90 -27
  350. package/src/learn.ts +3 -3
  351. package/src/levels-tool.ts +47 -2
  352. package/src/load.ts +140 -33
  353. package/src/manifest-tool.ts +3 -2
  354. package/src/mcp-app-tool.ts +2 -0
  355. package/src/mcp-context.ts +1 -1
  356. package/src/mcp-delegate.ts +162 -0
  357. package/src/mcp-docs-tools.ts +23 -5
  358. package/src/mcp-exec.ts +160 -132
  359. package/src/mcp-http.ts +19 -5
  360. package/src/mcp-io.ts +57 -0
  361. package/src/mcp-isolate.ts +287 -0
  362. package/src/mcp-project-child.ts +182 -0
  363. package/src/mcp-render.ts +2 -2
  364. package/src/mcp-root.ts +117 -22
  365. package/src/mcp-run.ts +104 -10
  366. package/src/mcp-ws.ts +4 -1
  367. package/src/mcp.ts +106 -64
  368. package/src/migrate-layout.ts +1121 -0
  369. package/src/openapi-tool.ts +7 -3
  370. package/src/openapi-types.ts +315 -13
  371. package/src/pipeline-report.ts +22 -18
  372. package/src/port.ts +53 -30
  373. package/src/project-facts.ts +5 -0
  374. package/src/recipes/4.0.0.ts +35 -0
  375. package/src/recipes/index.ts +2 -0
  376. package/src/release-verification.ts +74 -5
  377. package/src/review.ts +13 -8
  378. package/src/route-types.ts +133 -0
  379. package/src/rules/codes.ts +9 -0
  380. package/src/rules/data-guard.ts +230 -0
  381. package/src/rules/index.ts +18 -5
  382. package/src/rules/islands.ts +12 -4
  383. package/src/rules/legacy.ts +71 -39
  384. package/src/rules/nano.ts +18 -6
  385. package/src/rules/pages.ts +99 -0
  386. package/src/rules/routes.ts +181 -2
  387. package/src/rules/secrets.ts +174 -0
  388. package/src/rules/security.ts +198 -70
  389. package/src/rules/typescript.ts +77 -0
  390. package/src/rules/zones.ts +195 -0
  391. package/src/scaffold.ts +147 -95
  392. package/src/sdk.ts +1215 -158
  393. package/src/search-terms.ts +24 -7
  394. package/src/smoke.ts +607 -0
  395. package/src/sync-manifest.ts +2 -1
  396. package/src/sync-routes.ts +4 -3
  397. package/src/test-tool.ts +32 -11
  398. package/src/types-search.ts +11 -4
  399. package/src/upgrade.ts +205 -25
  400. package/src/verification-plan.ts +13 -0
  401. package/src/verification.ts +50 -12
  402. package/src/web-app-options.ts +57 -0
  403. package/src/work-graph.ts +27 -13
package/src/mcp-exec.ts CHANGED
@@ -6,17 +6,20 @@
6
6
  * replaceable.
7
7
  */
8
8
 
9
- import { stat } from "node:fs/promises"
10
- import { resolve } from "node:path"
9
+ import { type Dirent, realpathSync } from "node:fs"
10
+ import { readdir, stat } from "node:fs/promises"
11
+ import { relative, resolve } from "node:path"
11
12
  import { fileURLToPath } from "node:url"
12
- import { Glob } from "bun"
13
+ import { BACKEND_APP_FILE, CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
13
14
  import {
14
15
  type CommandCatalogEntry,
15
16
  type CommandCtx,
16
17
  type CommandSpec,
17
18
  commandCatalog,
19
+ commandMcpInputSchema,
18
20
  commandMcpName,
19
21
  findCommandSpec,
22
+ toCommandCatalogEntry,
20
23
  } from "./command-catalog.ts"
21
24
  import { collectContractProof } from "./contract-proof.ts"
22
25
  import { loadDocsCorpus } from "./docs-search.ts"
@@ -33,9 +36,6 @@ import {
33
36
  CHILD_INPUT_MAX_BYTES,
34
37
  CHILD_OUTPUT_MAX_BYTES,
35
38
  CHILD_TIMEOUT_MS,
36
- LOCAL_TOOL_FETCH_TIMEOUT_MS,
37
- notNifraResponse,
38
- readBoundedResponse,
39
39
  readBoundedStream,
40
40
  timeoutMessage,
41
41
  validateLocalPort,
@@ -44,6 +44,24 @@ import { mcpProjectPathError, resolveMcpProjectPath } from "./mcp-path.ts"
44
44
  import type { McpTool, McpToolContext } from "./mcp-protocol.ts"
45
45
  import { loadTypesCorpus } from "./types-search.ts"
46
46
 
47
+ const CODEFRAME_SOURCE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|mdx)$/i
48
+
49
+ /** The files a stack the caller supplies may show a codeframe from: project source, never a dotfile
50
+ * or anything under a dot directory (`.env`, `.git/`, `.nifra/`). */
51
+ function explainSourceGate(root: string): (file: string) => boolean {
52
+ // The codeframe reads a native realpath, which expands a Windows short name (`RUNNER~1`); the root
53
+ // has to be spelled the same way or every file looks like it sits outside it.
54
+ let base = resolve(root)
55
+ try {
56
+ base = realpathSync.native(base)
57
+ } catch {}
58
+ return (file) =>
59
+ CODEFRAME_SOURCE.test(file) &&
60
+ !relative(base, file)
61
+ .split(/[\\/]/)
62
+ .some((segment) => segment.startsWith("."))
63
+ }
64
+
47
65
  /** Path to a sibling child entry (`mcp-run` / `mcp-render` / `mcp-ws`), resolved next to this module (`.ts` in
48
66
  * dev, `.js` once built). Each runs in a FRESH subprocess per call so the project's current code loads. */
49
67
 
@@ -57,12 +75,21 @@ export function toMcpTool(
57
75
  const { cwd } = options
58
76
  const loadAppCached = options.loadAppCached ?? createCachedAppLoader(cwd)
59
77
  const pathFields = ["config", "out", "lockfile", "before", "after", "baseline", "file"] as const
78
+ const catalogEntry = entry ?? toCommandCatalogEntry(spec)
60
79
  return {
61
- name: commandMcpName(entry?.name ?? spec.name),
62
- description: entry?.summary ?? spec.summary,
63
- inputSchema: entry?.inputSchema ?? spec.input.jsonSchema,
80
+ name: commandMcpName(catalogEntry.name),
81
+ description: catalogEntry.summary,
82
+ inputSchema: commandMcpInputSchema(catalogEntry),
64
83
  handler: async (args: Record<string, unknown>, context: McpToolContext) => {
65
84
  const raw = { ...args }
85
+ for (const field of catalogEntry.cliOnlyFields) {
86
+ if (raw[field] !== undefined)
87
+ return JSON.stringify(
88
+ { ok: false, error: `${field} is available only from the nifra CLI` },
89
+ null,
90
+ 2,
91
+ )
92
+ }
66
93
  const dir = raw.dir
67
94
  if (dir !== undefined && typeof dir !== "string") return dirError(undefined)
68
95
  const target = resolveProjectDir(cwd, dir as string | undefined)
@@ -138,7 +165,7 @@ export function projectTools(
138
165
  {
139
166
  name: "nifra_verify",
140
167
  description:
141
- "Run the shared repository verification plan. Set release=true for the full release plan; the response preserves the declarative gate order and each gate's remediation.",
168
+ "Run the shared repository verification plan. Set release=true for the full release plan; the response preserves the declarative gate order and each gate's remediation. A gate whose script the project's package.json does not declare is reported as undeclared and not run; the result passes when every gate that ran passed.",
142
169
  inputSchema: {
143
170
  type: "object",
144
171
  properties: {
@@ -243,7 +270,7 @@ export function projectTools(
243
270
  {
244
271
  name: "nifra_explain",
245
272
  description:
246
- "Turn a nifra error into a STRUCTURED diagnostic instead of eyeballing a stack trace: a stable `code`, the top frame in YOUR source, a codeframe around the offending line, and - when nifra recognises the failure - the plain-language `cause` + `fix` + docs anchor. Pass `error` (and `stack` if you have it, e.g. from nifra_run/nifra_test output or a failing build) to explain a specific failure; or pass `port` to fetch the running dev server's most recent SSR failure from `/__nifra/last-error`. Returns the same JSON the dev overlay renders.",
273
+ "Turn a nifra error into a STRUCTURED diagnostic instead of eyeballing a stack trace: a stable `code`, the top frame in YOUR source, a codeframe around the offending line, and - when nifra recognises the failure - the plain-language `cause` + `fix` + docs anchor. Pass `error` (and `stack` if you have it, e.g. from nifra_run/nifra_test output or a failing build) to explain a specific failure. With no `error`, returns the running dev server's most recent error of any kind (SSR, loader, API, build, browser, hydration) with its request id - the server is found automatically; `port`/`dir` pick one when several run. nifra_errors lists them all. Returns the same JSON the dev overlay renders.",
247
274
  inputSchema: {
248
275
  type: "object",
249
276
  properties: {
@@ -265,144 +292,119 @@ export function projectTools(
265
292
  port: {
266
293
  type: "number",
267
294
  description:
268
- "Instead of a pasted error, fetch the running dev server's most recent SSR failure from this port.",
295
+ "The dev server's port, when the workspace runs several. Optional: the project's `nifra dev` server is found automatically.",
296
+ },
297
+ dir: {
298
+ type: "string",
299
+ description: "The app directory, when the workspace runs more than one dev server.",
269
300
  },
270
301
  },
271
302
  additionalProperties: false,
272
303
  },
273
- handler: async (args) => {
274
- const { error, stack, name, port } = args as {
304
+ handler: async (args, context) => {
305
+ const { error, stack, name, port, dir } = args as {
275
306
  error?: string
276
307
  stack?: string
277
308
  name?: string
278
309
  port?: number
310
+ dir?: string
279
311
  }
280
312
  if (error !== undefined || stack !== undefined) {
281
313
  const { buildDiagnostic } = await import("@nifrajs/web/diagnostic")
282
314
  const e = new Error(error ?? "")
283
315
  if (name !== undefined) e.name = name
284
316
  if (stack !== undefined) e.stack = stack
285
- return JSON.stringify(buildDiagnostic(e, { root: cwd }), null, 2)
317
+ return JSON.stringify(
318
+ buildDiagnostic(e, { root: cwd, showSource: explainSourceGate(cwd) }),
319
+ null,
320
+ 2,
321
+ )
286
322
  }
287
- if (port !== undefined) {
288
- const validPort = validateLocalPort(port)
289
- if (validPort === undefined) {
290
- return JSON.stringify(
291
- { code: "NIFRA_INVALID_PORT", message: "port must be an integer from 1 to 65535." },
292
- null,
293
- 2,
294
- )
295
- }
296
- const { LAST_ERROR_PATH } = await import("@nifrajs/web/diagnostic")
297
- try {
298
- const res = await fetch(`http://127.0.0.1:${validPort}${LAST_ERROR_PATH}`, {
299
- signal: AbortSignal.timeout(LOCAL_TOOL_FETCH_TIMEOUT_MS),
300
- })
301
- if (res.headers.get("x-nifra-diagnostic") !== "true")
302
- return notNifraResponse("diagnostic")
303
- if (!res.ok) {
304
- return JSON.stringify(
305
- {
306
- code: "NIFRA_NONE",
307
- message: `dev server at :${validPort} returned ${res.status}`,
308
- },
309
- null,
310
- 2,
311
- )
312
- }
313
- return await readBoundedResponse(res)
314
- } catch (cause) {
315
- return JSON.stringify(
316
- {
317
- code: "NIFRA_NONE",
318
- message: `could not reach a nifra dev server at :${validPort} - ${cause instanceof Error ? cause.message : String(cause)}`,
319
- },
320
- null,
321
- 2,
322
- )
323
- }
323
+ const target = resolveProjectDir(cwd, dir)
324
+ if (target === null) return dirError(dir)
325
+ if (port !== undefined && validateLocalPort(port) === undefined) {
326
+ return JSON.stringify(
327
+ { code: "NIFRA_INVALID_PORT", message: "port must be an integer from 1 to 65535." },
328
+ null,
329
+ 2,
330
+ )
331
+ }
332
+ const { explainLatest } = await import("./dev-feed-tool.ts")
333
+ try {
334
+ return JSON.stringify(await explainLatest(target, port, context.signal), null, 2)
335
+ } catch (cause) {
336
+ return JSON.stringify(
337
+ {
338
+ code: "NIFRA_NONE",
339
+ message: `could not read the dev server's errors - ${cause instanceof Error ? cause.message : String(cause)}`,
340
+ },
341
+ null,
342
+ 2,
343
+ )
324
344
  }
325
- return JSON.stringify(
326
- {
327
- code: "NIFRA_NONE",
328
- message:
329
- "Pass `error` (and `stack` if available) to explain a failure, or `port` to fetch the dev server's last error.",
330
- },
331
- null,
332
- 2,
333
- )
334
345
  },
335
346
  },
336
347
  {
337
348
  name: "nifra_inspect",
338
349
  description:
339
- "Observe what your requests ACTUALLY did on the running dev server - the recent request traces the DevTools plugin records: `{ method, path, status, durationMs, isrStatus, bodyBytes }` per request. The read no other tool gives you: after nifra_run or a real browser request, call this to SEE the outcome (which route answered, the status, how long, ISR hit/miss) instead of guessing. Pass `port` (the running dev server); narrow with `path` (a path prefix) or `limit` (most recent N). Requires the app to mount `@nifrajs/web`'s `devtools()` plugin (which auto-enables in development).",
350
+ "Observe what your requests ACTUALLY did on the running dev server: one trace per request, `{ requestId, method, path, status, durationMs, bytes, isr, errorIds, logCount }`. After nifra_run or a real browser request, call this to SEE the outcome (the status, how long, ISR hit/miss, whether it logged or failed) instead of guessing; pass a trace's `requestId` to nifra_errors/nifra_logs for what that request failed with or printed. Every `nifra dev` server keeps these traces and is found automatically; `port`/`dir` pick one when several run. Narrow with `path` (a path prefix), `requestId`, `since` (a cursor from an earlier call) or `limit` (most recent N). An explicit `port` this project did not start falls back to the `@nifrajs/devtools` plugin's traces.",
340
351
  inputSchema: {
341
352
  type: "object",
342
353
  properties: {
343
- port: { type: "number", description: "The running dev server's port." },
354
+ port: {
355
+ type: "number",
356
+ description:
357
+ "The dev server's port. Optional: the project's server is found automatically.",
358
+ },
344
359
  path: { type: "string", description: "Only traces whose path starts with this prefix." },
360
+ requestId: { type: "string", description: "Only the trace with this request id." },
361
+ since: {
362
+ type: "number",
363
+ description: "Only traces after this cursor (the `cursor` an earlier call returned).",
364
+ },
345
365
  limit: { type: "number", description: "Return only the most recent N traces." },
366
+ dir: {
367
+ type: "string",
368
+ description: "The app directory, when the workspace runs more than one dev server.",
369
+ },
346
370
  },
347
- required: ["port"],
348
371
  additionalProperties: false,
349
372
  },
350
- handler: async (args) => {
351
- const { port, path, limit } = args as { port?: number; path?: string; limit?: number }
352
- if (port === undefined) {
373
+ handler: async (args, context) => {
374
+ const { port, path, requestId, since, limit, dir } = args as {
375
+ port?: number
376
+ path?: string
377
+ requestId?: string
378
+ since?: number
379
+ limit?: number
380
+ dir?: string
381
+ }
382
+ const target = resolveProjectDir(cwd, dir)
383
+ if (target === null) return dirError(dir)
384
+ if (port !== undefined && validateLocalPort(port) === undefined) {
353
385
  return JSON.stringify(
354
386
  {
355
- events: [],
356
- note: "Pass `port` - the running dev server whose request traces to read.",
387
+ requests: [],
388
+ code: "NIFRA_INVALID_PORT",
389
+ note: "port must be an integer from 1 to 65535.",
357
390
  },
358
391
  null,
359
392
  2,
360
393
  )
361
394
  }
395
+ const { inspectRequests } = await import("./dev-feed-tool.ts")
362
396
  try {
363
- const validPort = validateLocalPort(port)
364
- if (validPort === undefined) {
365
- return JSON.stringify(
366
- {
367
- events: [],
368
- code: "NIFRA_INVALID_PORT",
369
- note: "port must be an integer from 1 to 65535.",
370
- },
371
- null,
372
- 2,
373
- )
374
- }
375
- const url = new URL(`http://127.0.0.1:${validPort}/_nifra/devtools/state`)
376
- if (path !== undefined) url.searchParams.set("path", path)
377
- if (limit !== undefined) url.searchParams.set("limit", String(limit))
378
- const res = await fetch(url, {
379
- signal: AbortSignal.timeout(LOCAL_TOOL_FETCH_TIMEOUT_MS),
380
- })
381
- if (res.headers.get("x-nifra-devtools") !== "true") return notNifraResponse("DevTools")
382
- if (res.status === 404) {
383
- return JSON.stringify(
384
- {
385
- events: [],
386
- note: "No DevTools endpoint on that server. Mount `devtools()` from @nifrajs/web and run in development.",
387
- },
388
- null,
389
- 2,
390
- )
391
- }
392
- if (!res.ok) {
393
- return JSON.stringify(
394
- { events: [], note: `DevTools state returned ${res.status}.` },
395
- null,
396
- 2,
397
- )
398
- }
399
- return await readBoundedResponse(res)
397
+ const result = await inspectRequests(
398
+ target,
399
+ { port, path, requestId, since, limit },
400
+ context.signal,
401
+ )
402
+ return JSON.stringify(result, null, 2)
400
403
  } catch (cause) {
401
- const validPort = validateLocalPort(port)
402
404
  return JSON.stringify(
403
405
  {
404
- events: [],
405
- note: `Could not reach a dev server at :${validPort ?? String(port)} - ${cause instanceof Error ? cause.message : String(cause)}`,
406
+ requests: [],
407
+ note: `Could not read the dev server's request traces - ${cause instanceof Error ? cause.message : String(cause)}`,
406
408
  },
407
409
  null,
408
410
  2,
@@ -413,7 +415,7 @@ export function projectTools(
413
415
  {
414
416
  name: "nifra_openapi",
415
417
  description:
416
- 'Return this project\'s backend OpenAPI 3.1 document generated from backend.ts route schemas via @nifrajs/schema. Use `format:"json"` for machine edits (default) or `format:"yaml"` for humans. Pass `path` (a route prefix like /api/orders, mirroring nifra_routes) to narrow a large backend to operations under that prefix instead of the whole document. Frontend-only apps return a valid empty paths object.',
418
+ 'Return this project\'s backend OpenAPI 3.1 document generated from backend/app.ts route schemas via @nifrajs/schema. Use `format:"json"` for machine edits (default) or `format:"yaml"` for humans. Pass `path` (a route prefix like /api/orders, mirroring nifra_routes) to narrow a large backend to operations under that prefix instead of the whole document. Frontend-only apps return a valid empty paths object.',
417
419
  inputSchema: {
418
420
  type: "object",
419
421
  properties: {
@@ -435,7 +437,7 @@ export function projectTools(
435
437
  {
436
438
  name: "nifra_run",
437
439
  description:
438
- "Run HTTP requests through this project's backend and return structured results (status, headers, parsed body, and any thrown error). Use it to verify code after editing: by default the backend is re-loaded in a fresh process each call. Pass warm:true to reuse a hot worker while source files are unchanged; it restarts automatically when files change. Each request: { method?, path, body?, headers? }.",
440
+ "Run HTTP requests through this project's backend and return structured results: status, headers, parsed body, `logs` (what the handler printed, per request, secrets redacted) and `errors` (a Diagnostic with codeframe, cause and fix for a thrown error or the unhandled error behind a bare 500). Use it to verify code after editing: by default the backend is re-loaded in a fresh process each call. Pass warm:true to reuse a hot worker while source files are unchanged; it restarts automatically when files change. Each request: { method?, path, body?, headers? }.",
439
441
  inputSchema: {
440
442
  type: "object",
441
443
  properties: {
@@ -454,7 +456,7 @@ export function projectTools(
454
456
  },
455
457
  entry: {
456
458
  type: "string",
457
- description: "Backend entry file (default: backend.ts | app.ts).",
459
+ description: "Backend entry file (default: backend/app.ts | app.ts).",
458
460
  },
459
461
  warm: {
460
462
  type: "boolean",
@@ -524,7 +526,7 @@ export function projectTools(
524
526
  },
525
527
  entry: {
526
528
  type: "string",
527
- description: "Backend entry file (default: backend.ts | app.ts).",
529
+ description: "Backend entry file (default: backend/app.ts | app.ts).",
528
530
  },
529
531
  },
530
532
  required: ["path"],
@@ -541,7 +543,8 @@ export function projectTools(
541
543
  properties: {
542
544
  pattern: {
543
545
  type: "string",
544
- description: "Optional test file/path pattern passed as an argv item to `bun test`.",
546
+ description:
547
+ "Optional test file/path pattern inside the project, passed as an argv item to `bun test`.",
545
548
  },
546
549
  timeoutMs: {
547
550
  type: "number",
@@ -573,7 +576,7 @@ export function projectTools(
573
576
  {
574
577
  name: "nifra_scaffold",
575
578
  description:
576
- 'Map a URL path to the CORRECT routes/ file and get a contract-correct page stub. Agents routinely place file routes wrong - this applies the convention for you: ":id"/"[id]" → [id], "*rest" → [...rest], "/" → index. Pass path (e.g. "/users/:id"). Returns the file to create + the route-module contract (loader/action/meta/default) + a stub (ready-to-write for react/preact/solid; path+contract for vue/svelte/vanilla - use nifra_example for those bodies).',
579
+ 'Map a URL path to the CORRECT routes/ files and get a contract-correct route pair: the page and its .backend.ts half (loader + output schema). Agents routinely place file routes wrong - this applies the convention for you: ":id"/"[id]" → [id], "*rest" → [...rest], "/" → index. Pass path (e.g. "/users/:id"). Returns both files + the route contract + stubs (the page is ready-to-write for react/preact/solid/vanilla; for vue/svelte use nifra_example for the page body).',
577
580
  inputSchema: {
578
581
  type: "object",
579
582
  properties: {
@@ -584,7 +587,7 @@ export function projectTools(
584
587
  write: {
585
588
  type: "boolean",
586
589
  description:
587
- "When true, create the file if a verified ready-to-write stub exists. Refuses overwrite.",
590
+ "When true, create both files if a verified ready-to-write page stub exists. Refuses overwrite.",
588
591
  },
589
592
  variant: {
590
593
  type: "string",
@@ -612,7 +615,7 @@ export function projectTools(
612
615
  if (write !== true) return renderScaffold(path, framework, flavour)
613
616
  const result = await writeScaffoldRoute(cwd, path, framework, flavour)
614
617
  const status = result.written
615
- ? `Written: \`${result.file}\``
618
+ ? `Written: \`${result.file}\` and \`${result.backend.file}\``
616
619
  : `Not written: ${result.reason ?? "no write performed"}`
617
620
  return `${status}\n\n${renderScaffold(path, framework, flavour)}`
618
621
  },
@@ -628,13 +631,17 @@ export function projectTools(
628
631
  },
629
632
  additionalProperties: false,
630
633
  },
631
- handler: async (args) => {
634
+ handler: async (args, context) => {
632
635
  const opts = args as { dir?: string; interact?: boolean }
633
636
  const target = resolveProjectDir(cwd, opts.dir)
634
637
  if (target === null) return dirError(opts.dir)
635
638
  const { runHydrationAssurance } = await import("./assure-hydration.ts")
636
639
  return JSON.stringify(
637
- await runHydrationAssurance(target, { interact: opts.interact === true }),
640
+ await runHydrationAssurance(
641
+ target,
642
+ { interact: opts.interact === true },
643
+ { signal: context.signal },
644
+ ),
638
645
  null,
639
646
  2,
640
647
  )
@@ -678,7 +685,10 @@ export async function spawnChild(
678
685
  if (new TextEncoder().encode(encodedInput).byteLength > CHILD_INPUT_MAX_BYTES)
679
686
  return `${label} input exceeded ${CHILD_INPUT_MAX_BYTES} bytes.`
680
687
  if (signal?.aborted) return `${label} cancelled${cancellationSuffix(signal)}.`
681
- const proc = Bun.spawn(["bun", childPath(child), cwd], {
688
+ const proc = Bun.spawn([process.execPath, childPath(child), cwd], {
689
+ cwd,
690
+ // Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
691
+ env: process.env,
682
692
  stdin: "pipe",
683
693
  stdout: "pipe",
684
694
  stderr: "pipe",
@@ -732,24 +742,38 @@ export async function spawnChild(
732
742
  }
733
743
  }
734
744
 
735
- const WARM_RUN_GLOB = new Glob("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json}")
736
- const WARM_RUN_IGNORED =
737
- /(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)\//
745
+ const WARM_RUN_SOURCE = /\.(?:[cm]?[jt]sx?|json)$/
746
+ // Pruned while walking, not filtered after: a scan that descends into node_modules on every warm call
747
+ // costs more than the cold start the worker saves.
748
+ const WARM_RUN_SKIPPED_DIR = /^(?:\..*|node_modules|dist(?:-[a-z0-9]+)?|build|coverage)$/
738
749
  const WARM_RUN_EXTRA_FILES = ["bun.lock", "bun.lockb"] as const
739
750
  const MAX_WARM_PENDING = 64
740
751
 
741
752
  async function warmRunFingerprint(cwd: string): Promise<string> {
742
753
  const parts: string[] = []
743
- for await (const rawRel of WARM_RUN_GLOB.scan({ cwd, dot: false })) {
744
- const rel = rawRel.replaceAll("\\", "/")
745
- if (WARM_RUN_IGNORED.test(rel)) continue
754
+ const walk = async (dir: string): Promise<void> => {
755
+ let entries: Dirent[]
746
756
  try {
747
- const s = await stat(resolve(cwd, rel))
748
- if (s.isFile()) parts.push(`${rel}:${s.mtimeMs}:${s.size}`)
757
+ entries = await readdir(resolve(cwd, dir), { withFileTypes: true })
749
758
  } catch {
750
- // A file can disappear while an agent is editing. The next call will rescan the settled tree.
759
+ return
760
+ }
761
+ for (const entry of entries) {
762
+ const rel = dir === "" ? entry.name : `${dir}/${entry.name}`
763
+ if (entry.isDirectory()) {
764
+ if (!WARM_RUN_SKIPPED_DIR.test(entry.name)) await walk(rel)
765
+ continue
766
+ }
767
+ if (entry.name.startsWith(".") || !WARM_RUN_SOURCE.test(entry.name)) continue
768
+ try {
769
+ const s = await stat(resolve(cwd, rel))
770
+ if (s.isFile()) parts.push(`${rel}:${s.mtimeMs}:${s.size}`)
771
+ } catch {
772
+ // A file can disappear while an agent is editing. The next call will rescan the settled tree.
773
+ }
751
774
  }
752
775
  }
776
+ await walk("")
753
777
  for (const rel of WARM_RUN_EXTRA_FILES) {
754
778
  parts.push(`${rel}:${await fileFingerprint(resolve(cwd, rel))}`)
755
779
  }
@@ -795,7 +819,10 @@ export class WarmWorker {
795
819
  readonly fingerprint: string,
796
820
  private readonly label: string,
797
821
  ) {
798
- this.proc = Bun.spawn(["bun", childPath(child), cwd, "--worker"], {
822
+ this.proc = Bun.spawn([process.execPath, childPath(child), cwd, "--worker"], {
823
+ cwd,
824
+ // Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
825
+ env: process.env,
799
826
  stdin: "pipe",
800
827
  stdout: "pipe",
801
828
  stderr: "pipe",
@@ -1081,7 +1108,7 @@ export interface CachedAppLoaderOptions {
1081
1108
  readonly fingerprint?: (cwd: string) => Promise<string>
1082
1109
  }
1083
1110
 
1084
- const APP_FINGERPRINT_FILES = ["nifra.config.ts", "framework.ts", "backend.ts"] as const
1111
+ const APP_FINGERPRINT_FILES = [CONFIG_FILE, FRAMEWORK_FILE, BACKEND_APP_FILE] as const
1085
1112
 
1086
1113
  function cacheToken(input: string): string {
1087
1114
  let hash = 2166136261
@@ -1104,7 +1131,8 @@ async function fileFingerprint(path: string): Promise<string> {
1104
1131
  }
1105
1132
  }
1106
1133
 
1107
- async function appFingerprint(cwd: string): Promise<string> {
1134
+ /** `mtime:size` of the app's config, framework and backend entry files: what notices an edit. */
1135
+ export async function appFingerprint(cwd: string): Promise<string> {
1108
1136
  return (
1109
1137
  await Promise.all(
1110
1138
  APP_FINGERPRINT_FILES.map(
package/src/mcp-http.ts CHANGED
@@ -29,7 +29,7 @@ export { docsTools } from "./mcp-docs-tools.ts"
29
29
  export type { TypeEntry } from "./types-search.ts"
30
30
 
31
31
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
32
- const VERSION = "3.5.0"
32
+ const VERSION = "4.0.0"
33
33
  const SERVER_INFO = { name: "nifra-docs", version: VERSION }
34
34
  // Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
35
35
  const docsHealth = (tools: McpTool[]): string =>
@@ -40,10 +40,12 @@ const docsHealth = (tools: McpTool[]): string =>
40
40
  /** The project-independent tools, reading the package's bundled corpus from disk (CLI use): the text
41
41
  * docs tools plus the `nifra_gallery` MCP Apps widget tool. */
42
42
  export function publicDocsTools(): McpTool[] {
43
- return [
44
- ...docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus),
45
- examplesAppTool(loadExamplesCorpus),
46
- ]
43
+ return [...openAiDocsTools(), examplesAppTool(loadExamplesCorpus)]
44
+ }
45
+
46
+ /** The text-only tool surface used by the public OpenAI Agent Plugin package. */
47
+ function openAiDocsTools(): McpTool[] {
48
+ return docsTools(loadDocsCorpus, loadExamplesCorpus, loadTypesCorpus)
47
49
  }
48
50
 
49
51
  /**
@@ -59,9 +61,21 @@ export function respondMcpHttp(
59
61
  return respondMcpHttpCore(request, tools, SERVER_INFO, { health: docsHealth(tools), ...options })
60
62
  }
61
63
 
64
+ /** The text-only handler used by the OpenAI Agent Plugin surface. */
65
+ function handleOpenAiMcpHttp(request: Request): Promise<Response> {
66
+ return respondMcpHttp(request, openAiDocsTools(), {
67
+ features: {
68
+ instructions:
69
+ "Nifra's public documentation tools for the OpenAI plugin. The server is stateless, read-only, and limited to Nifra's bundled documentation corpus, verified examples, API types, learning path, and frontend guidance.",
70
+ },
71
+ allowAnyOrigin: true,
72
+ })
73
+ }
74
+
62
75
  /** The CLI HTTP handler: serves the disk-backed corpus tools + registers the examples widget's `ui://`
63
76
  * resource so MCP Apps hosts can render `nifra_gallery`. (`nifra docs-mcp` / `bun run` this file.) */
64
77
  export function handleMcpHttp(request: Request): Promise<Response> {
78
+ if (new URL(request.url).pathname === "/openai") return handleOpenAiMcpHttp(request)
65
79
  return respondMcpHttp(request, publicDocsTools(), {
66
80
  features: {
67
81
  resources: [examplesWidget.resource],
package/src/mcp-io.ts CHANGED
@@ -154,6 +154,63 @@ export function serializeBoundedJson(
154
154
  return text
155
155
  }
156
156
 
157
+ /** What {@link readHeadTail} kept: the start and end of a stream, and how many bytes fell between. */
158
+ export interface HeadTailOutput {
159
+ readonly head: string
160
+ readonly tail: string
161
+ /** Bytes dropped between `head` and `tail`; when 0, `head + tail` is the whole stream. */
162
+ readonly droppedBytes: number
163
+ }
164
+
165
+ /**
166
+ * Drain a stream to its end, keeping at most `headBytes` from its start and `tailBytes` from its end,
167
+ * so a child that prints without limit costs bounded memory and still runs to completion.
168
+ */
169
+ export async function readHeadTail(
170
+ stream: ReadableStream<Uint8Array>,
171
+ headBytes: number,
172
+ tailBytes: number,
173
+ ): Promise<HeadTailOutput> {
174
+ const head: Uint8Array[] = []
175
+ let headSize = 0
176
+ const tail: Uint8Array[] = []
177
+ let tailSize = 0
178
+ let droppedBytes = 0
179
+ for await (let chunk of stream) {
180
+ if (headSize < headBytes) {
181
+ const taken = chunk.subarray(0, headBytes - headSize)
182
+ head.push(taken)
183
+ headSize += taken.byteLength
184
+ chunk = chunk.subarray(taken.byteLength)
185
+ }
186
+ if (chunk.byteLength === 0) continue
187
+ tail.push(chunk)
188
+ tailSize += chunk.byteLength
189
+ while (tailSize > tailBytes) {
190
+ const first = tail[0]!
191
+ const excess = tailSize - tailBytes
192
+ if (first.byteLength <= excess) {
193
+ tail.shift()
194
+ tailSize -= first.byteLength
195
+ droppedBytes += first.byteLength
196
+ } else {
197
+ tail[0] = first.subarray(excess)
198
+ tailSize -= excess
199
+ droppedBytes += excess
200
+ }
201
+ }
202
+ }
203
+ const decode = (chunks: readonly Uint8Array[]): string => Buffer.concat(chunks).toString("utf8")
204
+ return { head: decode(head), tail: decode(tail), droppedBytes }
205
+ }
206
+
207
+ /** A {@link HeadTailOutput} as one string, marking where bytes were dropped. */
208
+ export function joinHeadTail(output: HeadTailOutput): string {
209
+ return output.droppedBytes === 0
210
+ ? output.head + output.tail
211
+ : `${output.head}\n…(${output.droppedBytes} bytes omitted)…\n${output.tail}`
212
+ }
213
+
157
214
  /** Read child output incrementally and cancel as soon as the byte budget is crossed. */
158
215
  export async function readBoundedStream(
159
216
  stream: ReadableStream<Uint8Array>,