@ultimat3/cli 29.4.0 → 30.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 (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +11 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +9 -0
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +41 -81
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +7 -1
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1313 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
@@ -0,0 +1,753 @@
1
+ # MCP and AI
2
+
3
+ The differentiator. Not a chat widget, not an "AI SDK integration" — the framework is built so an agent can read it, drive it, and verify its own work, and so the apps it generates have the same property.
4
+
5
+ `As of 2026-08`. Stable API — semver from here ([Upgrading](Upgrading)). The MCP registry, wire protocol, dev-tool catalog, read-only SQL guard, and action projection are built, and so are the four that used to be contracted: `llm()` is an action factory ([`packages/ai/src/llm.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/ai/src/llm.ts)), prompts are versioned, `postgresVectorStore()` fuses pgvector cosine with Postgres FTS via RRF, and evals gate on a committed baseline inside `x verify`'s `eval` step.
6
+
7
+ ## Built-in MCP dev server
8
+
9
+ `x mcp serve` is the dev-tool server, over stdio or HTTP; `x dev` does not start it. Point any MCP client at it and the agent stops guessing. A new app's `.mcp.json` already names it — [Client config](#client-config).
10
+
11
+ The whole catalog, spelled exactly as each tool must be called; `bunx x mcp tools --json` reads it off the server rather than a count here. No aliases; renaming one is a major.
12
+
13
+ | Tool | Introspects / does | Replaces the agent's usual guess |
14
+ |---|---|---|
15
+ | `routes.list` | the `x routes --json` table: pages (render mode, hydrate, offline, budget), then every action, query and `runtime.ts` route | grepping a router directory |
16
+ | `schema.describe` | entities with columns, types and invariants | reading migration files in order |
17
+ | `policies.list` | every `policy`: permission, subject, where it is enforced | "is this endpoint protected?" |
18
+ | `actions.describe` | every action **and query**: input/output schema, policy, cache tags, MCP exposure | reading `api/` by hand |
19
+ | `jobs.inspect` | job definitions, retry policy and steps; omit `name` for all | reading `jobs.ts` and guessing the retry |
20
+ | `queue.depth` | pending, running and failed counts per queue | tailing a worker to see if it keeps up |
21
+ | `manifest.read` | the whole `x.manifest.json`, as text | ten separate reads |
22
+ | `errors.explain` | `X_*` code → cause, exact fix command, docs URL | web search |
23
+ | `graph.query` | `{ node }` (a file path, a primitive name or a node id) → every route, action, query, job, MCP tool, published operation, test and budget a change to it reaches, each with the provenance of the edges that reached it — `x impact`'s answer. `{ from, to }` → the chain of edges linking two nodes — `x graph --from --to`'s. An unknown name is `X_GRAPH_NODE_UNKNOWN` naming the nearest three; any other combination of arguments is an `isError` naming the two shapes. Rebuilt per call, so it answers for the code as it is now | grepping importers before an edit, and missing the route that reads it through a query |
24
+ | `db.query` | **read-only** SQL, 100-row default and 1000-row maximum, `EXPLAIN` on request | inventing a query and hoping |
25
+ | `db.migrate` | apply pending migrations **in a branch DB only** | mutating the dev database |
26
+ | `tests.run` | run the suite or a substring filter, structured results | parsing terminal output |
27
+ | `verify.run` | the whole gate; `fix: true` applies safe autofixes | guessing whether the work is shippable |
28
+ | `logs.tail` | last N structured log lines, filterable by runtime role | scrollback archaeology |
29
+ | `ui.shot` | photograph one route at a named viewport (`phone`/`tablet`/`desktop`, or `width`+`height`) in `light` or `dark` (`theme`: `prefers-color-scheme` is emulated **and** the scheme is stored as the visitor's choice under `THEME_STORAGE_KEY` before navigation, so an app with `theme.defaultMode: 'dark'` still captures light when asked — `As of 2026-09-19`), against the running `x dev`; returns the PNG **path** and the same verdict `x shot` writes — console, page errors, refused requests, whether every island mounted. Refuses a route with no `budget.js` | a hand-written puppeteer script, and a picture nobody judged |
30
+ | `ui.island` | `x shot --island <name> [--state]` as a tool: every declared state photographed and judged | the same, per component |
31
+ | `ui.inspect` | DOM, computed-style and accessibility facts for up to 20 selectors in **one** navigation of a route: per match the tag, text (≤200 chars), box, visibility, attributes (≤20), the computed `styles` you name (≤32, kebab-case; the rest land in `droppedStyles`) and with `a11y: true` the browser-computed role and name; plus `title`, `data-theme`, the focused element, the island count, console and page errors. ≤25 matches per selector, 64 KB on the wire (matches are dropped from the LAST selectors first, `truncated: true`); an unparsable selector answers `valid: false`. Takes the same PNG and verdict as `ui.shot` under `inspect/`; same budget gate, same `theme` semantics (the reported `data-theme` is the one asked for, on a dark-default app too) | `page.evaluate` scripts written per question, and a browser launched per question |
32
+ | `ui.interact` | drive one route through at most 12 steps — `{click}`, `{type: {selector, text}}` (≤500 chars), `{press}` (a key chord), `{focus}`, `{wait: ms ≤ 5000 \| selector}` (waited visible) — then photograph it (`fullPage` defaults to **false**: a dialog is judged on the fold) and, with an `inspect` block, read the same facts `ui.inspect` reads, in **one** navigation. Same `theme` semantics as `ui.shot`. After every step the islands settle again and one poll interval passes for the CSS transition; per step you get `{ index, kind, ms, navigated, url }`. Refuses whole, never trims: a list over the bounds (`X_UI_INTERACT_STEPS_INVALID`), typing into `<input type="password">` before any keystroke (`X_UI_INTERACT_SECRET_FIELD`), a step that leaves the dev server's origin (`X_UI_INTERACT_LEFT_APP`; a same-origin navigation is allowed and reported `navigated: true`), a step the driver refused (`X_UI_INTERACT_STEP_FAILED`, `meta.step` names it). PNG and verdict under `interact-<hash of the steps>/`, never over `ui.shot`'s; `ok` is the verdict's, except that a navigation a step caused is not the redirect the verdict fails a capture for | a puppeteer script per scene, and a picture of the closed state |
33
+ | `ui.diff` | compare two PNGs the other `ui.*` tools wrote — `before`/`after` relative to the app root and confined to `.x/shot/` (`X_UI_DIFF_PATH_OUTSIDE` otherwise, symlinks resolved) — pixel by pixel: `changedPixels`, `changedPercent` (two decimals), `changedBox` (the bounding box, or `null`) and the path of a diff PNG written beside `after` (`diff-<hash8 of before>.png`, or `out`): the `after` capture faded to a quarter over grey, changed pixels solid red. A pixel is changed when any channel moved by more than `threshold × 255` (default `0.1`); no anti-alias detection. Two sizes are `X_UI_DIFF_SIZE_MISMATCH`, a missing file `X_UI_DIFF_FILE_MISSING`. **No browser, no dependency** | a pixelmatch install, or two pictures eyeballed |
34
+
35
+ | Class | Tools | Exposure |
36
+ |---|---|---|
37
+ | read | `routes.list`, `schema.describe`, `policies.list`, `actions.describe`, `jobs.inspect`, `queue.depth`, `manifest.read`, `errors.explain`, `graph.query`, `ui.diff` | scope `dev:read`, unrestricted in dev — `ui.diff` reads files, and sits here because it reads only `.x/shot/` (resolve + prefix check, symlinks resolved) and launches nothing |
38
+ | gated read | `db.query`, `logs.tail` | scope `db:read` / `dev:logs` |
39
+ | executes code | `tests.run`, `verify.run`, `ui.shot`, `ui.island`, `ui.inspect`, `ui.interact` | scope `dev:test`; all six declare `destructive: true`, so none is metered as read chatter — the four `ui.*` tools launch a browser |
40
+ | write | `db.migrate` | scope `db:migrate`, **branch environments only** |
41
+
42
+ None of them is exposed in `ROLE=web`. `db.query` accepts one statement, whose leading keyword must be `SELECT`/`WITH`/`EXPLAIN`/`SHOW`/`TABLE`/`VALUES` — necessary, never sufficient. Batches, any write keyword at statement level (a data-modifying CTE included), locking clauses (`FOR UPDATE`/`FOR SHARE`), `EXPLAIN ANALYZE`, and whole function families matched by prefix of the called name, quoted and schema-qualified spellings included — file access (`pg_read_*`, `pg_ls_*`, `lo_*`, `dblink`), locks (`pg_advisory_*`), session settings (`set_config`), sleeps (`pg_sleep*`), catalog writes (`pg_import_*`) and the functions that run a query handed to them as text (`query_to_xml*`, `cursor_to_xml*`, `table_to_xml*`, `schema_to_xml*`, `database_to_xml*`, `ts_stat`, `ts_rewrite`) — are **refused**, not discouraged — `X_MCP_QUERY_REJECTED`, enforced before the host sees the string. What the scan cannot read is refused, never decoded: a Unicode-escaped quoted identifier (`U&"…"`) is `X_MCP_QUERY_REJECTED` (`As of 2026-10`). A keyword is matched as Postgres lexes an identifier, so a column `set2` is a column, not `SET`. Its Postgres SELECT-only role is conditional on the connection's own rights; the answer's `guards` array names the defences that engaged. `db.migrate` refuses a target that is not a branch database — `X_MCP_NOT_BRANCH_DB`.
43
+
44
+
45
+ ### Client config
46
+
47
+ `x new` writes one client config, `.mcp.json` at the app root — the format several MCP clients read — and no per-vendor directory: a second copy is a second thing to drift.
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "myapp": {
53
+ "command": "bunx",
54
+ "args": ["@ultimat3/cli", "mcp", "serve", "--transport", "stdio"]
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ | Client wants | Give it |
61
+ |---|---|
62
+ | `.mcp.json` | nothing: it reads this file |
63
+ | a command and its arguments | `bunx @ultimat3/cli mcp serve --transport stdio`, started in the app's root — the command needs an app (`X_NOT_IN_APP` otherwise) |
64
+ | a URL | `bunx x mcp serve --transport http --port 9229`, then `http://127.0.0.1:9229/mcp` and the bearer token it prints |
65
+ | its own config file | the same command, in that file's own syntax; the framework generates none |
66
+
67
+ **`bunx @ultimat3/cli`, never `bunx x`, in a client config.** A client may spawn the command outside the installed app, and there `bunx x` fetches an unrelated npm package named `x`. Inside an app's shell, `bunx x` resolves the app's own CLI. `scaffold-mcp-json.test.ts` pins the spelling, and CI's `scaffold-smoke` spawns the file's own command and asks it `initialize` and `tools/list` on every push.
68
+
69
+ ### Rate limits on the HTTP transport
70
+
71
+ **Enforced `As of 2026-08-24`.** `mcpHttpRoute` meters itself, per caller, per class, per minute. It has to: all MCP traffic is one `POST /mcp`, so the HTTP pipeline's own buckets — which key on the route — cannot tell an `initialize` handshake from a migration.
72
+
73
+ | | Default | Spent by |
74
+ |---|---|---|
75
+ | `read` | 120 / minute | every method that is not `tools/call`, plus any tool that does not declare `destructive: true` |
76
+ | `write` | 20 / minute | a `tools/call` naming a `destructive: true` tool, and **any call this server cannot resolve** — fail-closed, so a probing client never gets the cheap bucket |
77
+ | `unauthenticated` | 20 / minute, per **address** | a missing token, or one `resolveToken` answers `null`. Past it the address is refused `429` **before** `resolveToken` runs — a valid guess included, so the answer says nothing about the token (`As of 2026-10`). Keyed on `handle(request, { address })` — the host's resolved client address; a host that passes none is not metered here |
78
+
79
+ | Rule | Detail |
80
+ |---|---|
81
+ | Metered after the parse, before the tool | the class comes out of the body, so it cannot move above the JSON parse. An unauthenticated caller is answered `401` four lines earlier and can never spend an actor's allowance |
82
+ | The key is the ACTOR | `mcp:<class>\|actor:<id>`, and it never reaches the caller: a 429 is provokable by anyone holding a valid token, so an actor or org id in one is a leak wearing a throttle's clothes |
83
+ | Refusal | `X_MCP_RATE_LIMITED`, 429, with `Retry-After`. Its **own** code and not `X_RATE_LIMITED`, because that one's `fix:` names the HTTP pipeline's buckets, which do not govern this route — a fix line that runs and changes nothing is worse than none |
84
+ | The knob | `mcpHttpRoute({ rateLimits: { read, write, unauthenticated } })`, or `defineAppMcp({ rateLimits })` |
85
+ | Behind N replicas | pass `rateLimitStore: postgresRateLimitStore({ executor })` as well. The default is a per-process memory store — honest for `x mcp serve`, and a lie for N replicas behind one URL, each enforcing the full allowance on its own |
86
+
87
+
88
+ ### Protocol revisions: modern and legacy, one endpoint
89
+
90
+ `As of 2026-11`, read from the spec on 2026-11-02. Every server is **dual-era**: the era is decided per request, never per connection.
91
+
92
+ | Request | Era | Answered |
93
+ |---|---|---|
94
+ | `params._meta["io.modelcontextprotocol/protocolVersion"]` in `MCP_MODERN_VERSIONS` (`2026-07-28`) | modern, stateless | `server/discover` and the seven shared methods. Every result carries `resultType: 'complete'` and `_meta["io.modelcontextprotocol/serverInfo"]`; the lists, `resources/read` and `server/discover` add `ttlMs: 0`, `cacheScope: 'private'`. No `initialize`, no `ping` |
95
+ | no `_meta` version, or one in `MCP_LEGACY_VERSIONS` (`2025-11-25`, `2025-06-18`, `2025-03-26`) | legacy | `initialize` echoes an offered legacy revision, else answers `2025-11-25`; `ping`; the seven shared methods |
96
+ | any other `_meta` version | — | `-32022` `X_MCP_PROTOCOL_UNSUPPORTED`, `data: { supported, requested }` |
97
+
98
+ `MCP_PROTOCOL_VERSION` is gone (`As of 2026-11`): name the era — `MCP_MODERN_VERSIONS[0]`, or `MCP_LEGACY_VERSIONS`. `resources/templates/list` is `{ resourceTemplates: [] }` on both eras. A resource not found, hidden or absent, is one answer: `-32601` legacy, `-32602` modern. Not served and not advertised: `subscriptions/listen`, multi-round-trip input, the tasks extension, `x-mcp-header`, SSE responses; every `listChanged` is `false`.
99
+
100
+ Over HTTP:
101
+
102
+ | Request | Answer |
103
+ |---|---|
104
+ | `Origin` present and core's `proveSameOrigin` cannot show it is this app: not the route's public origin, not listed in `http.cors.origins` (CSRF's, the sync upgrade's and `/_x`'s rule) | `403` `X_MCP_ORIGIN_REFUSED`, **before** `resolveToken` runs. No `Origin` (a client process) passes. A page rebound to this server's address IS same-origin and passes here: `x mcp serve` refuses a non-loopback Host first (`421` `X_MCP_HOST_REFUSED`), and `x new`'s development token is honoured only with no `Origin` at all |
105
+ | modern, `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` (`tools/call`, `resources/read`, `prompts/get`) absent or unequal to the body (`=?base64?…?=` decoded first) | `400`, `-32020` `X_MCP_HEADER_MISMATCH` |
106
+ | `MCP-Protocol-Version` naming no served revision | `400`, `-32022` |
107
+ | modern, a method this server does not answer | `404`, `-32601` |
108
+ | `GET` or `DELETE` on the endpoint — `x mcp serve` and an app's own mounted `/mcp` alike | `405` `X_METHOD_NOT_ALLOWED`, `Allow: POST` — `@ultimat3/http`'s, the pipeline's own |
109
+ | `x mcp serve`: a Host that is not `localhost`, `*.localhost` or a loopback address | `421` `X_MCP_HOST_REFUSED`, before anything else — `/_x`'s rule |
110
+
111
+ ## Every action is an MCP tool
112
+
113
+ ```ts
114
+ mcp: { expose: true, description: 'Publish a draft post' },
115
+ ```
116
+
117
+ That line is the entire integration. From the existing declaration:
118
+
119
+ | MCP requirement | Source |
120
+ |---|---|
121
+ | tool name | the action's export name, **verbatim** — `publishPost`, never `publish_post`. The one name `tools/call` accepts, `scopes:` is keyed on, and every published catalog spells. `As of 2026-08`: `openapi.json`'s `x-ultimate.mcpTool` and `describe().mcp.tool` published a snake_case name until then, and an agent that trusted either called a tool the server answers ToolNotFound for |
122
+ | JSON Schema for input | the `input` schema (Standard Schema → JSON Schema, via `introspect()`) |
123
+ | output schema | `output` — published as the tool's `outputSchema` when its root is an object ([below](#what-a-client-is-told-beyond-the-schema)) |
124
+ | description | `mcp.description`, ending `Declared refusals: <codes>.` when the primitive declares `errors:` — the same sentence on the MCP tool and the in-app agent's tool, on both protocol eras, so an agent plans around a refusal before it calls ([Actions](Actions#declared-refusals-errors)). `As of 2026-11` |
125
+ | title | `mcp.title` — omitted, none is published |
126
+ | annotations | derived from the kind, `mcp.annotations` overrides key by key ([below](#what-a-client-is-told-beyond-the-schema)) |
127
+ | **authorization** | the action's `policy` — unchanged, unwrapped, identical |
128
+ | idempotency | `idempotent: true` adds one optional argument, `idempotencyKey` (string, 1–255), to the tool's `inputSchema`. `tools/call` removes it from the arguments and passes it to `invoke` as the key — the `Idempotency-Key` header's MCP twin, filed under action, caller and key — so a retry with the same key replays the first result and the same key with other arguments is `X_IDEMPOTENCY_CONFLICT`. The action's input never sees it. An argument, not `params._meta`: a model-driven client cannot set `_meta`, and the retry that needs the key is the agent's |
129
+ | audit trail | the same OTel span and log line as an HTTP call |
130
+
131
+ The projected tool's `run` **is** `invoke` — the same entry point the HTTP route calls (`packages/mcp/src/projectable.ts`). Policy runs inside it, so there is nothing to keep in sync. The projection itself adds **no** MCP scope: a second gate hard-coded into the projection would sit in front of the only gate that matters, and the two would eventually disagree. `defineAppMcp`'s `scopes:` map may still attach one from outside — a property of the connection's token, never invented by the projection.
132
+
133
+ No MCP-specific permission table, no service account with broad rights. Exposure is opt-in; silence exposes nothing.
134
+
135
+ The user's own agents can therefore operate the user's product — refund an order, re-run an import, publish a post — with the exact permissions that user has in the UI. See [Admin dashboard](Admin-Dashboard) and [Actions](Actions).
136
+
137
+ ### What a client is told beyond the schema
138
+
139
+ `As of 22.10`, MCP 2025-06-18. Every piece is optional for the app; the defaults come from the
140
+ declaration already written.
141
+
142
+ | Field | Where it comes from | Default |
143
+ |---|---|---|
144
+ | `initialize` → `instructions` | `defineAppMcp({ instructions })` — a string, or `(caller) => string \| undefined` for per-population advice (the same shape as `surface`) | none sent. A function that throws or answers blank sends none; the handshake still answers |
145
+ | tool `title` | `mcp: { title }` on the action/query; `title` on a hand-written tool | none |
146
+ | `annotations.readOnlyHint` | the kind | query `true`, action `false` |
147
+ | `annotations.destructiveHint` | the kind | action `true` — the spec's own default made explicit: the framework cannot tell an insert from a delete. Declare `false` for an additive write |
148
+ | `annotations.idempotentHint` | `idempotent: true` on the action | `false` |
149
+ | `annotations.openWorldHint` | `mcp.annotations` only | not published (only the author knows a write sends mail) |
150
+ | `outputSchema` | an action's `output` whose root is an object; a query's declared `rows` as `{ rows: [<row>] }` — or the row itself for a `single: true` read, which answers one row or `X_NOT_FOUND` as its route does (`As of 2026-10`) | none — text only |
151
+ | `structuredContent` | every successful call of a tool that publishes `outputSchema`: the serialized answer read back (a `Date` is its string). A LIST query's rows sit under `rows`; a `single: true` query's answer is the row object itself, with no wrapper | absent |
152
+
153
+ ```ts
154
+ mcp: {
155
+ expose: true,
156
+ description: 'Tag a post. Adds, never removes.',
157
+ title: 'Tag post',
158
+ annotations: { destructiveHint: false },
159
+ },
160
+ ```
161
+
162
+ An `outputSchema` is **structure only** — `type`, `properties`, `required`, `items`, `enum`,
163
+ `const`, `anyOf`. A client validates `structuredContent` against it and refuses the call on a
164
+ miss, so a bound, a pattern or `additionalProperties: false` on rows read from the database would
165
+ be a promise nothing enforces on the way out. The meta tools publish their own hints:
166
+ `list_resources` / `describe_resource` read-only, `manage_resource` a write (it reaches every
167
+ grouped tool); `manage_resource` answers the inner tool's `structuredContent` byte for byte.
168
+
169
+ **Split by effect** (`surface: 'meta-split'`, `As of 2026-10-18`): `read_resource` (queries,
170
+ `readOnlyHint: true`), `write_resource` (`mcp: { effect: 'write' }`) and `delete_resource`
171
+ (`mcp: { effect: 'delete' }`, `destructiveHint: true`; `{ resource, action, params, live,
172
+ confirmed_by_user }` — without `live: true` it answers a preview (the framework's runs nothing; an action declaring `live` answers its own), with it it runs
173
+ only on `confirmed_by_user: true`) replace `manage_resource`, for clients and directories that
174
+ refuse a dispatcher which both reads and writes. A grouped write without an effect fails boot
175
+ (`X_MCP_EFFECT_UNDECLARED`); an action called on the wrong dispatcher is `X_MCP_WRONG_TOOL`
176
+ naming the right one; an unconfirmed commit is `X_MCP_USER_CONFIRMATION_REQUIRED`. Full contract:
177
+ [`packages/mcp/README.md`](https://github.com/developerz-ai/ultimate/blob/main/packages/mcp/README.md).
178
+
179
+ **What the model reads is compact.** A tool's text block is one-line JSON (the 2-space form until
180
+ 22.10 spent a third of a large answer on indentation). `list_resources` answers **plain text**, one
181
+ line per resource and one per action, each fact said once where it is cheapest (`As of 2026-10`):
182
+
183
+ | Rule | Rendered |
184
+ |---|---|
185
+ | a scope every action of a resource shares | once, on the resource line: `(scope posts)` |
186
+ | a scope every action of one KIND shares | once, per kind: `(query scope posts:read; action scope posts:write)` |
187
+ | any other scope, or an unscoped action among scoped ones | on the action itself: `(scope …)` |
188
+ | kind | `(action)` only, from `destructive`; untagged = read-only query — never inferred from the name |
189
+ | `confirms: true` | `(confirms)`, always on the action |
190
+ | params | every required field, then optional ones to 4 fields / 100 chars, then `…`; an enum past 8 literals ends `\|…` |
191
+
192
+ ```text
193
+ 2 resource(s). Run one with manage_resource({resource, action, params}); describe_resource({resources:["<name>"]}) has the full input schemas. Untagged = read-only query; (action) may write; (confirms) waits for a human; … = more in describe_resource.
194
+
195
+ org (scope app:use) — The organisation
196
+ transferOrg (action) {} — Transfer the org
197
+
198
+ posts (query scope app:use; action scope posts:write) — Blog posts
199
+ listPosts {status_eq?: string|number|boolean, title_cont?: string, sort?: "createdAt"|"-createdAt", …} — Posts, filtered
200
+ publishPost (action; confirms) {postId: string} — Publish a draft post
201
+ ```
202
+
203
+ The same catalog as data, for a test: `mcp.server.catalog(caller)` — every action keeps its own
204
+ `scope` there. `describe_resource` stays compact JSON with the full schema — a schema is JSON.
205
+
206
+ **Hold the standing surface to a number.** `tools/list` rides every step and `list_resources` sits
207
+ in the transcript all session:
208
+
209
+ ```ts
210
+ import { assertMcpSurfaceBudget } from '@ultimat3/mcp';
211
+
212
+ test('the staff surface stays small', async () => {
213
+ // The derivation beside the number: the size measureMcpSurface reported, × a margin.
214
+ await assertMcpSurfaceBudget(mcp.server, staffCaller, { toolsList: 4_000, listResources: 12_000 });
215
+ });
216
+ ```
217
+
218
+ `measureMcpSurface(server, caller)` returns `{ toolsList, listResources, instructions }` in
219
+ characters, off the wire; the assert refuses every surface over its ceiling in one
220
+ `X_MCP_SURFACE_OVER_BUDGET`.
221
+
222
+ **Refusals speak to the caller.** `defineAppMcp` renders an error's `callerFix` — what a remote
223
+ agent can do — where the framework's own `fix` names a command only the app's developer can run:
224
+ `X_FORBIDDEN` says to ask the account owner for the permission, not `x policy explain`; a missing
225
+ scope says to ask for a token that carries it; an invalid argument says to correct the field
226
+ against the published schema. The developer's `fix` stays in the log line, `--json`, and on
227
+ `mcpServer` (the dev server), whose default is `errorAudience: 'developer'`;
228
+ `defineAppMcp({ errorAudience: 'developer' })` restores it for an app. **A 5xx cause is the
229
+ server's own business** (`As of 2026-10`): for the caller audience, a thrown 5xx code core's
230
+ `hasPublicCause` does not list (`X_DB_STATEMENT_FAILED` carries the Postgres message and the
231
+ statement) renders a fixed sentence as its `cause` and `x errors explain <code> --json` (or its
232
+ `callerFix`) as its `fix` — in a tool result and in a `resources/read` error's `data` alike, the
233
+ verdict a production problem document gives. The developer audience keeps both. An app's own error takes the
234
+ same two fields, plus `docs` — declared once with `defineError`:
235
+
236
+ ```ts
237
+ import { defineError } from '@ultimat3/core';
238
+
239
+ export const RefundWindowClosed = defineError<{ readonly ageDays: number }>({
240
+ code: 'X_REFUND_WINDOW_CLOSED',
241
+ title: 'the refund window has closed',
242
+ status: 403,
243
+ cause: (p) => `the payment is ${p.ageDays} days old; refunds are limited to 90 days`,
244
+ fix: 'x policy explain refund:create --json',
245
+ callerFix: 'issue a credit note instead',
246
+ docs: 'docs://recipes/issue-a-credit-note',
247
+ });
248
+ ```
249
+
250
+ A `docs` other than the framework's one Error-Codes page is a fourth line of the tool result —
251
+ ` docs: docs://recipes/issue-a-credit-note` — and is the `docs` member of the problem document
252
+ over HTTP. A production problem document carries `callerFix` as its `fix`; a dev one keeps the
253
+ developer's.
254
+
255
+ ### The app's own endpoint is mounted by the web role
256
+
257
+ `As of 2026-09-05`. `defineAppMcp({ …, resolveToken })` builds `mcp.route`, and `app.config.ts`
258
+ declares `ai: { mcp: { expose: true } }` **by default** (the path is `defineAppMcp`'s own, `/mcp` by default) — and until this date nothing
259
+ between the two served it: `POST /mcp` was `X_ROUTE_NOT_FOUND` under `x dev` and in every
260
+ container. The contract is one file:
261
+
262
+ | Piece | Where | Rule |
263
+ |---|---|---|
264
+ | the declaration | `apps/<app>/mcp.ts` | `export const mcp = defineAppMcp({ include: 'exposed', resolveToken })` |
265
+ | the switch and the path | `app.config.ts` → `ai.mcp` | `expose` (default `true`), `path` (default `/mcp`) |
266
+ | the mount | `x dev` **and** `runRole`, through one call | `POST <path>` → `mcp.route.handle(request)`; the boot log says `app mcp mounted`, `x dev`'s summary prints `mcp POST /mcp` |
267
+ | auth | the route's own | `auth: 'public'`, `enforcedBy: 'handler'` on the http route — the bearer token is read by `resolveToken`, never pre-judged by the pipeline |
268
+ | exposed and unmountable | `X_MCP_APP_UNMOUNTED`, logged once | no file exports `mcp`, or it was built without `resolveToken` (no `route`); the fix names the file to write |
269
+ | a new app | `x new` writes it | `apps/web/mcp.ts` over the `@<app>/mcp` catalog, `resolveToken` from `apps/web/app/auth/agent-token.ts`: an API key (`issueApiKey`, `x_api_keys`) is an agent in every environment; `dev-agent.<role>` is one under `x dev` alone, and only from a loopback address with no `Origin` (a page rebound to 127.0.0.1 sends one). No token, or one it does not know: 401 |
270
+
271
+ **A token from an external issuer** (`As of 2026-11-05`): an app whose agents present an IdP's JWT
272
+ rather than an API key hands `defineAppMcp` `jwtBearerResolver` from `@ultimat3/auth` — the twin of
273
+ `apiKeyResolver`. The signature, issuer, audience and time checks are `verifyWorkloadToken`'s; the
274
+ app keeps only its claim check and its actor:
275
+
276
+ ```ts
277
+ // apps/web/app/auth/agent-token.ts
278
+ import { actorFromService, jwksClient, jwtBearerResolver } from '@ultimat3/auth';
279
+
280
+ const ISSUER = 'https://login.example.com/tenant-a/v2.0';
281
+ const TENANT = 'tenant-a';
282
+ const ORG_ID = '00000000-0000-4000-8000-000000000001';
283
+ const keys = jwksClient({ provider: ISSUER, jwksUri: `${ISSUER}/discovery/v2.0/keys` });
284
+
285
+ export const resolveAgentToken = jwtBearerResolver({
286
+ keys: () => keys, // read per token: no network at import
287
+ issuers: [ISSUER],
288
+ audience: 'api://ledger',
289
+ requiredScope: 'ledger.agent', // optional; the scope name is the app's
290
+ accept: (raw) => raw['tid'] === TENANT, // the app's own claim, after the signature held
291
+ actorOf: (token) => actorFromService({ ...token.identity, orgId: ORG_ID }),
292
+ });
293
+ ```
294
+
295
+ Every refusal is `null` — the mount's `401` — and one `auth.jwt_bearer.refused` line naming the
296
+ check (`signature`, `issuer`, `audience`, `expiry`, `not-before`, `scope`, `accept`, `key-set`),
297
+ never a claim's value. A key set that cannot be read and an unknown `kid` both fail closed.
298
+
299
+ #### Several endpoints, one per population
300
+
301
+ `As of 22.14.0`. `mcp` may be a non-empty **array** of `defineAppMcp` values — one endpoint per
302
+ population, each with its own catalog, `instructions`, `groups`, `scopes` and `oauth`. Same export
303
+ name, same file; the order is the contract:
304
+
305
+ ```ts
306
+ // apps/web/mcp.ts
307
+ export const mcp = [
308
+ defineAppMcp({ name: 'acme', include: 'exposed', scopes: CUSTOMER_SCOPES, resolveToken: customerToken, oauth }),
309
+ defineAppMcp({ name: 'acme-admin', path: '/mcp/admin', tools: staffTools, surface: 'meta', groups, scopes: STAFF_SCOPES, resolveToken: staffToken, oauth }),
310
+ defineAppMcp({ name: 'acme-afiliados', path: '/mcp/afiliados', tools: affiliateTools, scopes: AFFILIATE_SCOPES, resolveToken: affiliateToken, oauth }),
311
+ ];
312
+ ```
313
+
314
+ | Rule | Detail |
315
+ |---|---|
316
+ | endpoint #0 | mounts at its own `defineAppMcp({ path })` (default `/mcp`), exactly as a single export does, and owns the root `/.well-known/oauth-protected-resource` |
317
+ | every other endpoint | mounts at its own `defineAppMcp({ path })` (default `/mcp`), route name `mcp:<path>` |
318
+ | isolation | each endpoint is its own `McpServer`: a tool, resource or prompt of one is absent from another's `tools/list` and answers `-32601` there |
319
+ | two endpoints on one route | `X_MCP_PATH_DUPLICATE`, **thrown** at boot — a second endpoint that forgot `path` collides with the default `/mcp` |
320
+ | one endpoint without `resolveToken` | `X_MCP_APP_UNMOUNTED` names it; the others still mount |
321
+ | boot report | one `app mcp mounted` log line and one `mcp POST <path>` summary line per endpoint; `x dev --json` carries `mcp` (endpoint #0) and `mcpPaths` (all) |
322
+
323
+ The same discovery serves an app's `RuntimeOverrides`: `apps/<app>/runtime.ts` exporting `runtime`
324
+ reaches `x dev` (its middleware, rate-limit store and plain `routes`) and `runRole` when
325
+ `apps/web/server.ts` passes none — one middleware chain in development and in the container, not two.
326
+ `runtime.routes` (`As of 22.6.0`) is the one escape hatch for a URL no primitive projects to and a
327
+ wire format none speaks — an OAuth token endpoint answering RFC 6749 JSON to a form-encoded POST —
328
+ mounted after the framework's routes and before the pages, through the whole pipeline.
329
+
330
+ ### OAuth discovery for remote connectors
331
+
332
+ `defineAppMcp({ oauth })` makes the endpoint an OAuth 2.1 protected resource per the MCP
333
+ authorization spec (2025-06-18; unchanged in 2025-11-25) and RFC 9728 (`As of 22.6.0`):
334
+
335
+ ```ts
336
+ export const mcp = defineAppMcp({
337
+ include: 'exposed',
338
+ scopes: MCP_SCOPES,
339
+ resolveToken,
340
+ oauth: {
341
+ authorizationServers: ['https://www.example.com'], // issuer(s); https, or http on localhost
342
+ resourceName: 'Example',
343
+ // resource: 'https://www.example.com/mcp', // omitted: the request's PUBLIC origin + path
344
+ // scopesSupported: [...], // omitted: Object.keys(scopes)
345
+ },
346
+ });
347
+ ```
348
+
349
+ | What | Answer |
350
+ |---|---|
351
+ | 401 (no token) | `WWW-Authenticate: Bearer realm="ultimate-mcp", resource_metadata="<origin>/.well-known/oauth-protected-resource/mcp"` |
352
+ | 401 (token did not resolve) | the same plus `error="invalid_token"` |
353
+ | `GET /.well-known/oauth-protected-resource/mcp` and `GET /.well-known/oauth-protected-resource` | `{ resource, authorization_servers, bearer_methods_supported: ['header'], scopes_supported, resource_name }`, public, `access-control-allow-origin: *` — mounted by both boots beside `POST /mcp` |
354
+ | several endpoints | each serves its own path-inserted document (RFC 9728 §3.1) — `/.well-known/oauth-protected-resource/mcp/admin` for `POST /mcp/admin` — with its own `resource` and `scopes_supported`, and its 401 names that document; the root document stays endpoint #0's |
355
+ | origin | `ctx.https` + host as the pipeline resolved them, so a TLS-terminating ingress yields `https://…` |
356
+
357
+ The authorization server is `mcpAuthorizationServer` (`As of 2026-11`): `@ultimat3/auth`'s
358
+ `authorizationServer` ([Auth](Auth#authorization-server)) bound to this endpoint. One call, one
359
+ consent page:
360
+
361
+ ```ts
362
+ // apps/web/mcp-oauth.ts
363
+ import { postgresAuthServerStore } from '@ultimat3/auth';
364
+ import type { Actor, PgExecutor } from '@ultimat3/core';
365
+ import type { McpScopes } from '@ultimat3/mcp';
366
+ import { defineAppMcp, mcpAuthorizationServer } from '@ultimat3/mcp';
367
+
368
+ declare const executor: PgExecutor;
369
+ declare const MCP_SCOPES: McpScopes; // the scope → tool names map, as above
370
+ declare function agentFor(subject: string): Promise<Actor | null>;
371
+
372
+ export const as = mcpAuthorizationServer({
373
+ issuer: 'https://www.example.com',
374
+ resource: 'https://www.example.com/mcp',
375
+ store: postgresAuthServerStore({ executor }),
376
+ consentPath: '/oauth/consent',
377
+ lifetimes: { accessSeconds: 600, refreshIdleDays: 30 },
378
+ scopes: Object.keys(MCP_SCOPES),
379
+ registration: 'dynamic', // omitted: metadata-document client ids only
380
+ actor: (grant) => agentFor(grant.subject), // per call: a removed member is refused at once
381
+ });
382
+ export const mcp = defineAppMcp({ include: 'exposed', scopes: MCP_SCOPES, oauth: as.oauth, resolveToken: as.resolveToken });
383
+ // apps/web/runtime.ts: routes: [...as.routes]
384
+ ```
385
+
386
+ | What | Answer |
387
+ |---|---|
388
+ | `resource` | every grant binds to it, a request naming none included; `resolveToken` refuses a token bound to anything else, or to nothing |
389
+ | `oauth` | `{ authorizationServers: [issuer], resource, scopesSupported: scopes }` for `defineAppMcp` |
390
+ | audit | each server decision is one `mcp.auth.<event>` line — `issued`, `refused`, `revoked` at `info`; `code-replayed`, `refresh-replayed`, `grant-disabled` at `warn` |
391
+ | the protocol | none of it here: every rule is `authorizationServer`'s, so a hand-written server and plain `oauth:` keep working |
392
+
393
+ Without `oauth`, the 401 is `Bearer realm="ultimate-mcp"` as before. Bad block:
394
+ `X_MCP_OAUTH_INVALID` at definition.
395
+
396
+ ### Per-caller tools and one token across tenants
397
+
398
+ `As of 2026-11`. Two `defineAppMcp` options, both resolved per request and both behind the same
399
+ gates as every other tool:
400
+
401
+ | Option | What it does | Refusals |
402
+ |---|---|---|
403
+ | `catalog: (caller) => ({ tools, groups? })` | tools that exist per caller (a tenant's connectors), each a `runtimeTool({ name, description, inputSchema, permission, run })` borrowing a **declared** permission; asked on every `tools/list` and `tools/call`, never cached | `X_MCP_CATALOG_TOOL_DUPLICATE` (a name taken twice), `X_MCP_CATALOG_UNRESOLVED` (the resolver threw, or answered something unservable) — the request fails, never a smaller list |
404
+ | `tenant: { arg, resolve, list }` | the token resolves to a person; every call names its tenant under `arg`, stripped before validation; `resolve` builds the call's actor, so policy and tenancy guards see one org per call; `list_tenants` lists them | `X_MCP_TENANT_REQUIRED` (no tenant named — fix `list_tenants({})`), `X_MCP_TENANT_UNRESOLVED` (the lookup threw), `X_MCP_TENANT_INVALID` (boot: a tool declares the arg) |
405
+
406
+ A tenant the person is not in (`resolve` answers `null`) gets exactly the answer a hidden tool gets.
407
+ Another caller's runtime tool is an absent tool. The audit line carries `tenant`.
408
+
409
+ **`tools/list` is person-scoped, and not a promise.** MCP's `tools/list` names no tenant, so `visibleTo` (function form), `can()` and `catalog` run there as the person; `tools/call` runs them as the tenant actor and is authoritative. A tool visible to the person but not the tenant actor is listed and answers as a hidden tool; one visible only through a tenant role or a tenant's catalog row is unlisted and still runs when called by name.
410
+
411
+ ```ts
412
+ import { agentActor } from '@ultimat3/core';
413
+ import { defineAppMcp, runtimeTool } from '@ultimat3/mcp';
414
+
415
+ declare function roleIn(personId: string, org: string): Promise<string | undefined>;
416
+
417
+ defineAppMcp({
418
+ include: 'exposed',
419
+ tenant: {
420
+ arg: 'org',
421
+ resolve: async ({ person, value }) => {
422
+ const role = await roleIn(person.id, value);
423
+ return role === undefined ? null : agentActor({ id: person.id, orgId: value, roles: [role] });
424
+ },
425
+ list: () => [],
426
+ },
427
+ catalog: (caller) => ({
428
+ tools: caller.actor.orgId === undefined ? [] : [
429
+ runtimeTool({
430
+ name: 'org_ping',
431
+ description: 'Ping the acting org. Read-only.',
432
+ inputSchema: { type: 'object', properties: {} },
433
+ permission: 'org:read',
434
+ destructive: false,
435
+ run: ({ ctx }) => ({ org: ctx.actor.orgId }),
436
+ }),
437
+ ],
438
+ }),
439
+ });
440
+ ```
441
+
442
+ Worked example: Postly's `quote_<plan>` tools and its multi-org token —
443
+ `examples/dummy/packages/mcp/src/{quotes,tenants}.ts`, proven over its own endpoint by
444
+ `tenant.contract.test.ts`. Full rules: `packages/mcp/README.md`.
445
+
446
+ ### Long tools: `jobTool()` and `job_status`
447
+
448
+ A tool whose work outlives a client's timeout. `tools: { reindex: jobTool({ job, policy, description, result, input?, jobInput?, inlineMs? }) }`.
449
+
450
+ | Fact | Rule |
451
+ |---|---|
452
+ | Durable | enqueues at once (`job.as(caller)`); never an in-process promise |
453
+ | Input | the tool's `input` (default: the job's); `jobInput(input, ctx)` builds the job's from it and the caller — a member id read off the request, never an argument an agent fills in |
454
+ | Inline | polls `introspect.job(id)` every 250 ms up to `inlineMs` (default 5,000, ceiling 25,000), honours `ctx.signal`. Also bounded by the poll count, so a clock that does not move still ends the wait. `inlineMs: 0` answers `running` at once — right for a model run |
455
+ | Answers | `done` + the `result` query's rows, read **as the caller** · `running` + `poll: { tool: 'job_status', arguments: { jobId } }` · `failed` + the code it was thrown with (`X_INTERNAL` for an uncoded throw, as HTTP answers one), never the row's text |
456
+ | `job_status` | projected once beside any job tool; no scope (gated per row); another actor, another org and an unknown id are the same `X_JOB_NOT_FOUND` |
457
+ | Dedupe | the job's own `idempotencyKey`: a repeated call attaches to the live run |
458
+ | Refused | `X_MCP_JOB_TOOL_INVALID`: not a `job()` handle, `inlineMs` out of range, a driver with no `introspect` |
459
+
460
+ The reference app's `draftReview` (`examples/dummy/apps/web/mcp-tools.ts`) queues `reviewDraftLater` for the caller's own member and reads `postReview` back; `mcp-tools.contract.test.ts` drives it over the wire.
461
+
462
+ ### Recipes: `docs://recipes/<name>` and the `docs` tool
463
+
464
+ `defineAppMcp({ recipes: [{ name, title, description, steps: [{ tool, why, example? }], notes? }] })`.
465
+
466
+ | Fact | Rule |
467
+ |---|---|
468
+ | Checked at boot | every `step.tool` is in the final catalog (`X_MCP_RECIPE_TOOL_UNKNOWN`), every `example` passes that tool's input (`X_MCP_RECIPE_EXAMPLE_INVALID`) — all offenders in one boot |
469
+ | Served | an `McpResource` (`text/markdown`, rendered from the structure) and a read-only `docs({ name? })` tool |
470
+ | Visibility | only to a caller who can see every step's tool and holds its scope; hidden = unknown, byte for byte |
471
+ | The prefix | with `recipes:`, `resources:` may not hold `docs://recipes/` (`X_MCP_RECIPE_URI_RESERVED`) |
472
+ | Error links | `defineError({ docs: 'docs://recipes/<name>' })` must name a recipe some endpoint serves — `X_MCP_RECIPE_UNKNOWN` at mount; a throw-site link to none is dropped from the render and logged |
473
+
474
+ **From a markdown folder.** `recipes: await markdownRecipes({ dir: join(import.meta.dir, 'docs') })` reads one `*.md` per recipe and serves its body verbatim. The file name is the recipe name. `title:` and `description:` come from `---` frontmatter, else from the first `# ` heading and the paragraph under it. A fence tagged `call` holds one step, `tool {"arg": "value"}`, and is checked at boot like any `step.example`. A whole `"<postId>"` value is a placeholder, so any value passes at boot. A `docs://recipes/<name>` link in the text that names no recipe is `X_MCP_RECIPE_LINK_DANGLING`.
475
+
476
+ **Searching.** `docs({})` lists what this caller can follow. `docs({ name })` and `docs({ uri })` read one recipe. `docs({ q })` inlines the best match, scoring 3 for each word in the name or title, 2 in the description and 1 in the body. On a miss it names the nearest recipes, so a caller with any visible recipe never gets an empty answer.
477
+
478
+ **Per caller.** `extraRecipes: (caller) => recipes` adds the caller's own recipes, such as a tenant's runbooks. They are listed as `source: 'caller'` on `docs`, and on `resources/list` and `resources/read` to that caller only. They are not checked at boot. A recipe that names a tool the caller cannot call is not shown.
479
+
480
+ **Replayed.** In a contract test, `await replayRecipes(server, recipes, { caller, placeholders })` calls every step's example through the real server as `caller` and reads every link. Three answers fail it with `X_MCP_RECIPE_EXAMPLE_INVALID`: no such tool, refused arguments, and a policy that refuses that caller. A dangling link fails it with `X_MCP_RECIPE_LINK_DANGLING`. A tool's own refusal (`X_NOT_FOUND` for a sample id) counts as the tool working. The reference app replays its recipes as an org owner: `examples/dummy/packages/mcp/src/recipes-replay.contract.test.ts`.
481
+
482
+ ### Consuming another server: `mcpTools()`
483
+
484
+ Remote MCP tools as this app's actions — the app's `policy`, rate limit, confirmation gate and manifest row; one tool list for an agent.
485
+
486
+ | Fact | Rule |
487
+ |---|---|
488
+ | Declared | `mcpTools({ name, url, auth, allow, policy, lock })` → `tools: Map<remoteName, Action>`, action names `<name>_<tool>`. `policy` is an app `resource:verb` permission, as a hand-written tool takes: the input is the remote's schema, so a predicate over it would decide nothing |
489
+ | Reviewed | `allow` is required; `lock` is the committed `tools.lock.json` written by `x mcp lock <name>`; `x mcp lock <name> --check` writes nothing and fails on drift |
490
+ | Drift | a missing, re-described or re-shaped tool is `X_MCP_REMOTE_TOOL_CHANGED` — at declaration against the lock, before the first call against the live server |
491
+ | Wire | `outbound()` (`dial: 'public'` by default, deadline, size cap); modern era first, then legacy `initialize` + `Mcp-Session-Id`; JSON or SSE answers |
492
+ | Untrusted | the answer arrives as `{ untrusted, isError }`, fenced `<untrusted source="mcp:<name>/<tool>">`, middle-cut past `maxResultChars` |
493
+ | Read-only | only when a reviewer sets `readOnly: true` in the lock — the remote's hint is never trusted |
494
+ | Failure | `X_MCP_REMOTE_REFUSED` (502), the cause names the status |
495
+
496
+ **Five outcomes of a call.** `meta.failure` tells them apart without reading a cause:
497
+
498
+ | Outcome | What the caller gets |
499
+ |---|---|
500
+ | success, text only | `{ isError: false, text }`. No `structuredContent` and no output schema is still a success |
501
+ | the tool's own `isError: true` | a RESULT `{ isError: true, text }`, handed to the model like any tool result |
502
+ | no answer (DNS, connect, deadline) | `X_MCP_REMOTE_REFUSED`, `meta.failure: 'transport'`. An abort from the caller's own signal passes through as `X_ABORTED` |
503
+ | an HTTP status | `X_MCP_REMOTE_REFUSED`, `meta.failure: 'status'`, `meta.status` |
504
+ | a JSON-RPC `error` | `X_MCP_REMOTE_REFUSED`, `meta.failure: 'rpc'`, `meta.rpcCode` |
505
+ | a body that is no JSON-RPC answer | `X_MCP_REMOTE_REFUSED`, `meta.failure: 'malformed'` |
506
+ | `401` | `X_MCP_REMOTE_AUTH_REQUIRED`, `meta.resourceMetadata` from the `WWW-Authenticate` challenge (RFC 9728) when the server sent one |
507
+
508
+ **Streaming.** A server may answer any POST as JSON or as `text/event-stream`. The client reads the answer with its own id out of the stream, and acts on any `notifications/tools/list_changed` riding with it. A legacy (2025-era) session also gets one long-lived `GET` stream for the server's own messages. It is opened after `initialize`, detached from the request that opened it, and lives `listenMs` (default 5 minutes) before the next call opens another. A `405` means the server offers no such stream, and the client never asks again. `client.close()` ends it. A modern server is stateless, so its changes ride response streams only. Either way, a change makes the next call re-list and hold the list to the lock again.
509
+
510
+ **OAuth to the upstream.** `mcpOAuthClient({ clientId, clientName, redirectUri, scopes? })` is the client half of the MCP authorization spec:
511
+
512
+ 1. `begin(serverUrl)` reads the server's RFC 9728 protected-resource document, then its authorization server's RFC 8414 metadata. It answers `{ authorizeUrl, handshake }`, with PKCE S256 from `@ultimat3/auth` and `resource` per RFC 8707.
513
+ 2. Redirect the person to `authorizeUrl`. Keep `handshake` server-side, in the person's session.
514
+ 3. On the callback, `exchange(handshake, { code, state })` answers `{ accessToken, refreshToken, expiresIn }`. `refresh(handshake, refreshToken)` answers the same shape.
515
+ 4. Store the tokens per connection. Return the access token from `mcpTools({ auth })`, which is read on every call, so a rotated token is never cached.
516
+
517
+ `clientId` is the HTTPS URL at which this app serves `metadata()` (a client ID metadata document), or an id the authorization server issued when the app was registered by hand. A `401` mid-session is `X_MCP_REMOTE_AUTH_REQUIRED`: run the handshake again.
518
+
519
+ **Proved on itself.** `examples/dummy/packages/mcp/src/remote-self.contract.test.ts` points `mcpTools()` at the reference app's own MCP route. It locks the live list, calls `seatReport`, and meets three refusals: the remote policy's `X_FORBIDDEN` as a result, a stolen token as `X_MCP_REMOTE_AUTH_REQUIRED`, and a schema moved since the lock as `X_MCP_REMOTE_TOOL_CHANGED`.
520
+
521
+ ## Three outcomes, deliberately different
522
+
523
+ Role, scope and policy refuse in three distinguishable ways. The difference is the security property, not an implementation detail.
524
+
525
+ | Situation | Response | Wire |
526
+ |---|---|---|
527
+ | The actor's role can never invoke the tool | absent from `tools/list`; a direct call answers ToolNotFound | JSON-RPC `-32601`, message `tool not found: <name> — call tools/list to read the catalog this caller may use`, no `data` at all. The hint is the same sentence on both branches — absent and hidden — so it instructs without saying which |
528
+ | The role could invoke it, but the connection's scope does not include it | explicit refusal naming the missing scope | JSON-RPC `-32600`, `data: { code: 'X_MCP_SCOPE_DENIED', scope, fix }` |
529
+ | The tool was invoked and the policy denied this input | `X_FORBIDDEN` with the denial reason | a normal `result` with `isError: true` — identical to the HTTP answer for the same call |
530
+ | The caller may see and call it, and the arguments fail its published schema | `X_INPUT_INVALID`, each issue addressed by path | a normal `result` with `isError: true` (`As of 22.10`; a `-32602` until then, which clients hide from the model). `-32602` remains for a call that is not one — no `params`, a non-string `name` |
531
+
532
+ Hidden means hidden: `Forbidden` on a hidden tool is an enumeration oracle — an agent, or an attacker driving one, walks a name list and reads the org's feature set, entity names and internal operations off the difference between "not found" and "forbidden". A scope refusal is the opposite case: a well-behaved client can legitimately fix it, so hiding it would only strand the caller.
533
+
534
+ | Rule | Detail |
535
+ |---|---|
536
+ | `visibleTo` takes two forms | a **role allowlist**, or a **predicate over the caller**. A tool that declares neither is visible to everyone |
537
+ | Both forms are **fail-closed** | a role list admits only the roles it names, so a caller whose role is not in it — including a caller with no role at all — is refused; a caller with no role sees only tools that declare no `visibleTo` |
538
+ | `tools/list` is per connection | answered per caller, never a static catalog |
539
+ | Visibility is input-independent | the predicate takes the caller and nothing else — it structurally cannot read call arguments, so existence cannot be probed by varying them |
540
+ | Gate order is fixed | visibility → scope → the policy's actor half → arguments → policy. Scope runs before the policy, so a refusal never depends on evaluating a policy against attacker-supplied input; a caller the policy refuses whatever they send gets the policy's `X_FORBIDDEN` (the tool's `admit`) before their arguments are checked, so an issue list never describes a tool they may not call; the rest of the policy — predicates over the arguments — runs after them |
541
+ | Every outcome is audited | one structured log line per `tools/call`: `surface: 'mcp'`, tool name, actor id, outcome. ToolNotFound, scope denials and policy denials log at `warn`, a successful call at `info` — ToolNotFound is `warn` on purpose, because an enumeration attempt is a detectable pattern |
542
+ | Audit lines carry no payload | tool name, outcome and error code only — never call arguments, never row data |
543
+ | No trusted-tool mode | there is no flag that skips policy evaluation, on any MCP surface |
544
+ | The actor cannot exceed the human | the actor is the signed-in user's session; an agent inherits exactly those permissions |
545
+
546
+ Where the first two outcomes are declared:
547
+
548
+ | Outcome | Declared | Property |
549
+ |---|---|---|
550
+ | Hidden (role) | `mcp: { visibleTo: [...] }`, on the action or query itself | `readonly string[]` — a role allowlist. A primitive declares the list form only: a declared fact stays static and serialisable. The predicate form of `McpVisibility` is for a surface that builds its catalog programmatically (`@ultimat3/admin` derives visibility from the actor's admin permissions) and hands `@ultimat3/mcp` a tool directly. Both are fail-closed: an unnamed role — including a caller carrying no role at all — gets ToolNotFound, never Forbidden. A catalog audience, not an authz rule; the primitive's `policy` still decides every call |
551
+ | Hidden (prompt) | `visibleTo` on an `McpPrompt` passed to `defineAppMcp({ prompts })` | the same `McpVisibility`, the same fail-closed evaluation: absent from `prompts/list`, and `prompts/get` answers exactly as for a prompt that does not exist (`As of 22.14.0`). Resources take the same field |
552
+ | Scope | `scopes:` on `defineAppMcp` | `Readonly<Record<string, readonly string[]>>` — scope name → tool names. A capability of the connection's token, so it is declared once per app rather than beside every primitive. A name the catalog does not contain, or one claimed by two scope entries, refuses at boot: `X_MCP_SCOPE_UNKNOWN`, `X_MCP_SCOPE_CONFLICT` |
553
+
554
+ Rationale for each: [`docs/architecture/11-ai-surface.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/architecture/11-ai-surface.md).
555
+
556
+ ## Generated facts, hand-written conventions
557
+
558
+ | Artifact | Author | Contents | Rule |
559
+ |---|---|---|---|
560
+ | `x.manifest.json` | **generated**, every build | routes, entities, actions, mutators, queries, jobs, tasks, policies, cache tags, MCP tools, budgets, build ID | never hand-edited; drift is a `x verify` failure |
561
+ | `openapi.json` | **generated** | HTTP surface from action/query declarations | contract diff in `x verify` |
562
+ | `AGENTS.md` | **human-authored**, short | project-specific conventions an agent cannot infer | never generated, never auto-appended |
563
+ | `CLAUDE.md` | **human-authored**, short | same, compressed-config style, <600 lines | never generated, never auto-appended |
564
+
565
+ LLM-generated context files measurably reduce task success. A model writing "here is what this codebase does" produces confident, plausible, partly-wrong prose, and the next agent treats it as ground truth — errors compound and cannot be distinguished from facts. So: facts come from code (structured, verifiable, regenerated every build), conventions come from a human (short, opinionated, stable). Ultimate never generates prose documentation at runtime, and `x new` scaffolds `AGENTS.md` as a terse human-editable stub, not an essay.
566
+
567
+ ## LLM gateway
568
+
569
+ One typed entry point for model calls — provider-agnostic, observable, cached, evaluated.
570
+
571
+ ```ts
572
+ export const summarize = llm({
573
+ model: 'claude-sonnet-5-5',
574
+ input: t.object({ postId: t.uuid }),
575
+ output: t.object({ summary: t.string, tags: t.string.array() }),
576
+ prompt: summarizePrompt, // versioned artifact
577
+ cache: { semantic: { threshold: 0.97, ttl: '7d' } },
578
+ budget: { tokensIn: 8000, costPerCall: { minor: 5, currency: 'USD' } },
579
+ policy: can('post:read'),
580
+ });
581
+ ```
582
+
583
+ | Feature | Behavior |
584
+ |---|---|
585
+ | Structured output | `output` schema drives tool-use/JSON mode; a parse failure retries once, then throws `X_LLM_OUTPUT_INVALID` |
586
+ | Streaming | `.stream()` on the returned action; policy, budget, cache and span unchanged; the `done` chunk carries the validated value |
587
+ | Cost + token accounting | per call, per tenant, per prompt version; exceeding `budget` throws before spending |
588
+ | Retries | typed on provider errors; rate limits back off, content refusals do not retry |
589
+ | Breaker | one per provider, on by default (`{ failures: 5, cooldownMs: 30_000 }`): a provider whose retries ran out five passes running is skipped, no call and no sleep, until one probe after the cooldown answers; a 400 never opens it. `providerGateway({ breaker: (provider) => breaker({ name: provider.name, failures: 3, cooldownMs: 10_000 }) })` chooses your own, `breaker: 'off'` declines. [Outbound HTTP](Outbound-HTTP#breaker-stop-paying-for-a-dependency-that-is-down) |
590
+ | Caching | semantic cache from [Caching and invalidation](Caching-And-Invalidation) |
591
+ | Provider prompt caching | the provider keeps a request's stable prefix; on by default for `agent()`, off for `llm()` → [Agents](Agents#provider-prompt-caching) |
592
+ | Tracing | one OTel span per call with model, tokens, cache hit, prompt version |
593
+ | Fallback | ordered provider list per model; the provider that answered is on the span as `llm.provider`, never silent. A refusal names a more capable model for the declaration to adopt — models are never swapped at runtime |
594
+ | Money | `Money = { minor, currency }` — never a float → [Money](Money) |
595
+
596
+ **A stream is the same action over a different transport, with one difference that is forced rather than chosen: there is no repair turn.** The non-streaming call retries once on a parse failure; a stream cannot, because the tokens are already on the reader's screen and a second answer is two answers to one question. So a stream yields **unvalidated** text increments plus one final `done` carrying the value that did satisfy `output`, and a schema disagreement is `X_LLM_STREAM_INVALID` naming the non-streaming call as the fix. A structured schema wants the non-streaming call.
597
+
598
+ **Showing what a model wrote is `Markdown` from `@ultimat3/ui`**, never `innerHTML`: the text is
599
+ parsed to a tree and rendered as elements, so a `<script>` or a `javascript:` link in an answer is
600
+ text, and a streaming answer re-parses per token while finished blocks keep their nodes. Tool calls
601
+ and usage records are `JsonView`; a model's proposed edit against the original is `DiffView`
602
+ → [UI components](UI-Components).
603
+
604
+ Long or multi-call chains are `job`s with steps, so a model call that fails on step 4 retries step 4 only. See [Jobs and workflows](Jobs-And-Workflows).
605
+
606
+ ## Agents, hives and agent jobs → [Agents](Agents)
607
+
608
+ `llm()` is one model call. A **tool-using loop** is `agent()`, and it is the same kind of thing — an action factory, not a ninth primitive. Everything the loop needs is a factory over a primitive already in the vocabulary:
609
+
610
+ | Factory | Over | Gives you |
611
+ |---|---|---|
612
+ | `llm()` | `action()` | one model call, streamable, semantically cacheable |
613
+ | `agent()` | `action()` | the turn loop: real actions as tools, forced structured output, `maxTurns`, a run budget, `ctx.signal`, `onTurn`. **No `.stream()`** |
614
+ | `hive()` | `action()` | one action fanned out over many inputs: bounded concurrency, split-order results, three-way `ok`/`failed`/`skipped` member outcomes |
615
+ | `agentJob()` | `job()` | an agent as durable, retried, cancellable queue work — `As of 2026-08` the only way an agent reaches a queue |
616
+ | `backfill()` | `job()` | the resumable, paced sweep those run over → [Migrations and backfills](Migrations-And-Backfills) |
617
+
618
+ A tool is one of two things. A real `action()` carrying `mcp: { expose: true }` — the **same** predicate an external MCP client is filtered by, so an in-app agent and an external one are offered exactly the same catalogue of actions and authorize identically. Or an `inProcessTool({ name, description, input, execute })`: a tool that is this process's alone, with no `mcp` flag, in no MCP catalogue, route or `openapi.json`, running as `ctx.actor` under the agent's own policy → [In-process tools](Agents#in-process-tools). An `agent()` returns an action, so an agent is a tool of another agent with no supervisor primitive anywhere.
619
+
620
+ A tool in `mcpConfirmations({ tools })` waits for a person on **both** surfaces: `X_MCP_CONFIRMATION_PENDING` over `tools/call`, on a server handed `confirmations` or not, and as an agent's `tool_result`. A name no tool answers to is `X_MCP_CONFIRMATION_TOOL_UNKNOWN` at the `manifest` step. One list, no second option → [A tool that waits for a person](Agents#a-tool-that-waits-for-a-person).
621
+
622
+ **Quorum and signed decisions** (`As of 2026-11`) are two more options on the same factory, never a second gate:
623
+
624
+ | Option | What it holds |
625
+ |---|---|
626
+ | `approvers: ({ args }) => ({ quorum, from })` | N of M distinct approvers matching `'role:<name>'` / `'actor:<id>'`, resolved per call from the validated arguments and stored on the row. The requesting actor never counts (nor an impersonated decider): `X_FORBIDDEN`. One approver counts once. Short of the quorum, the agent's repeat answers `X_MCP_CONFIRMATION_QUORUM_PENDING`; a bar nobody can meet is `X_MCP_CONFIRMATION_QUORUM_INVALID` before a row opens |
627
+ | `evidence: { verify }` | every vote carries `signature` over the `signingPayload` the view returns (confirmation id, tool, keyed argument digest), checked by the app's `verify` against keys registered to the approver. A missing or failing one stores no vote; the stored row is re-verified before the call runs, so a row written around the decision runs nothing. Both are `X_MCP_CONFIRMATION_EVIDENCE_INVALID` |
628
+
629
+ `verifyConfirmationEvidence(row, { evidence, quorum, from, arguments })` is exported so a separate process that executes the action re-checks the stored evidence with public keys alone. `ttlMs` expires a partly approved row like any other; one rejection rejects. The full table: [`@ultimat3/mcp` README](https://github.com/developerz-ai/ultimate/blob/main/packages/mcp/README.md#quorum-and-signed-decisions).
630
+
631
+ Full reference, including the at-least-once trap that makes every mutating tool's idempotency your obligation: [Agents](Agents).
632
+
633
+ ## Prompts as versioned artifacts
634
+
635
+ ```
636
+ apps/web/app/posts/prompts/summarize.v3.md # the prompt, plain markdown + typed slots
637
+ apps/web/app/posts/prompts/summarize.evals.ts # evals attached to it
638
+ ```
639
+
640
+ | Rule | Why |
641
+ |---|---|
642
+ | A prompt is a file with a version, not a string literal | diffable, reviewable, attributable in traces |
643
+ | Editing a prompt requires a version bump | invalidates the semantic cache; keeps A/B honest |
644
+ | Slots are typed | a missing variable is a compile error, not a `{{undefined}}` in production |
645
+ | **Every prompt has an evals file** — no evals is a `x verify` failure | an unevaluated prompt is untested code |
646
+ | Old versions retained | traces stay interpretable; rollback is a config line |
647
+
648
+ ## Vectors and hybrid search
649
+
650
+ pgvector in the same Postgres. No second datastore.
651
+
652
+ | Piece | Detail |
653
+ |---|---|
654
+ | Embeddings | an `Embedder` value, never an entity option: `remoteEmbedder({ name, dimension, baseUrl? })` speaks the `/v1/embeddings` shape (`EMBEDDINGS_API_KEY`), `hashEmbedder()` is the offline default |
655
+ | Writing | `indexDocument({ store, embedder, document })` chunks, embeds, upserts and prunes the stale tail; a sweep over many rows is the app's `backfill()` calling it per page |
656
+ | Index | HNSW: `postgresVectorStore({ name, dimension }).ddl()` prints the table and both indexes, pasted into a migration by the app |
657
+ | Hybrid search | one `query` primitive fusing pgvector cosine + Postgres FTS with Reciprocal Rank Fusion; weights are config |
658
+ | Filtering | tenant + policy filters applied **in SQL**, so vector search cannot leak across tenants; a store with no tenant bound, read inside a request acting for an org, is `X_VECTOR_UNSCOPED` — a backfill opts in with `scope: UNSCOPED` |
659
+ | Re-embed | `indexDocument` again: every chunk is embedded on each call, so a job decides when text changed |
660
+
661
+ ## Evals as a test type
662
+
663
+ `eval` is one of the six test types in [Testing](Testing).
664
+
665
+ | Aspect | Detail |
666
+ |---|---|
667
+ | Shape | fixture set + assertions: exact, schema, rubric (LLM judge), or regression-vs-baseline |
668
+ | Agents | `defineEval({ agent, cases, tools })` runs the whole loop with each tool a double; an undoubled call is `X_EVAL_TOOL_UNDOUBLED` → [Testing](Testing#evals) |
669
+ | Determinism | temperature 0 where possible; judge model and prompt version pinned |
670
+ | Gate | `x verify` fails on a score drop beyond the declared tolerance, not on absolute score |
671
+ | Cost | reported per run; `x test eval --sample 20` for the fast local loop |
672
+ | Output | `--json` with per-case scores, so an agent iterating on a prompt sees which case it broke |
673
+
674
+ ## Branch environments
675
+
676
+ The shipped surface is `x db branch`, with three verbs. `x branch` (no `db`) is **planned** and exits `X_NOT_IMPLEMENTED`; the build-id and MCP-socket rows below are what it will add.
677
+
678
+ ```bash
679
+ x db branch create feat-new-billing --json
680
+ x db branch list --json
681
+ x db branch delete feat-new-billing --json
682
+ ```
683
+
684
+ | Property | Detail | `As of 2026-08` |
685
+ |---|---|---|
686
+ | DB | `CREATE DATABASE "<source>_branch_<slug>" TEMPLATE "<source>"` copy-on-write clone — cheap, isolated, disposable. `<slug>` is the name with every character outside `[A-Za-z0-9_]` replaced by `_` — a hyphen is not legal in an unquoted Postgres identifier — so `create feat-new-billing` clones into `myapp_branch_feat_new_billing`. Embedded: a copied `pgdata-<name>` directory, which keeps the name **as typed** | **shipped** |
687
+ | Migrations | `db.migrate` applies here, never to the shared dev DB (`X_MCP_NOT_BRANCH_DB`) | **shipped** |
688
+ | Preview URL | `http://<name>.localhost:<PORT>`, reported on `data.preview` | **the URL is computed**; nothing routes that subdomain for you |
689
+ | Teardown | `x db branch delete <name>` — it may only delete what `list` shows | **shipped** |
690
+ | **Build ID scopes the SW** | a per-branch build id giving the branch its own SW scope and cache namespace, so a preview can never poison prod cache → [PWA and offline](PWA-And-Offline) | **planned**, part of `x branch` |
691
+ | Scoped MCP socket | `ws://localhost:9229/<name>` | **planned**, part of `x branch` |
692
+ | Agent use | an agent can migrate, seed, test, and browse a preview without risking anything shared | |
693
+
694
+ ## `--json` everywhere
695
+
696
+ Every command and every error has a machine-readable form. Same content, different encoding.
697
+
698
+ ```
699
+ $ x verify --json
700
+ {"ok":false,"checks":[{"name":"budgets","ok":false,"failures":[
701
+ {"route":"site/pricing","metric":"js","actual":"61kb","limit":"40kb",
702
+ "cause":"chart.js via shared/ui/button.tsx",
703
+ "fix":"x fix boundary site/pricing/page.tsx"}]}]}
704
+ ```
705
+
706
+ | Surface | Machine form |
707
+ |---|---|
708
+ | CLI | `--json` on every subcommand |
709
+ | Errors | `UltimateError` serializes to `{ code, cause, fix, docs }` |
710
+ | HTTP errors | same JSON body, same codes |
711
+ | Dev overlay | the identical string a terminal shows |
712
+ | MCP | tool errors carry the same code + fix |
713
+
714
+ ## Errors
715
+
716
+ | Code | Cause | Fix |
717
+ |---|---|---|
718
+ | `X_MCP_TOOL_UNKNOWN` | no visible tool answers that name (role-hidden and absent are indistinguishable) | `tools/list` to read the catalog this caller may use |
719
+ | `X_INPUT_INVALID` | arguments failed the tool's published `inputSchema` — an `isError` result naming each field (`As of 22.10`) | correct the named fields against `inputSchema` (`tools/list`, or `describe_resource` on the meta surface) and resend |
720
+ | `X_MCP_ARGS_INVALID` | not raised on the wire since 22.10 — invalid arguments answer `X_INPUT_INVALID` as a tool result; `McpArgsInvalidError` stays exported | — |
721
+ | `X_MCP_SURFACE_OVER_BUDGET` | `assertMcpSurfaceBudget` measured `tools/list`, `list_resources` or `instructions` over the budget declared for them | shorten what it names, group tools behind `surface: 'meta'`, or raise the budget with the measured size and the reason in the same diff |
722
+ | `X_MCP_IDEMPOTENCY_KEY_SHADOWED` | an `idempotent: true` action's input declares `idempotencyKey`, the tool argument reserved for the retry key — refused at boot | rename the action's input field |
723
+ | `X_MCP_SCOPE_DENIED` | the connection's token does not carry the tool's scope | reconnect with a token whose scopes include the one `cause` names — the app's `resolveToken(token)` is what returns them — or drop that scope from `defineAppMcp({ scopes })`. Scopes are fixed for the life of a connection, so a grant takes effect on the next one. **Not** `x token grant`: that command is `PLANNED` and exits `X_NOT_IMPLEMENTED` |
724
+ | `X_MCP_SCOPE_UNKNOWN` | `defineAppMcp`'s `scopes:` names a tool the server does not project | spell the name as one of the tools the server actually projects, or drop it from that `scopes` entry |
725
+ | `X_MCP_SCOPE_CONFLICT` | two `scopes:` entries claim the same tool | keep the tool under the single scope a token must hold for it, and remove the other entry |
726
+ | `X_MCP_QUERY_REJECTED` | `db.query` was not given one read-only statement | send exactly one **read-only** `SELECT`/`WITH`/`EXPLAIN`/`SHOW`/`TABLE`/`VALUES` — a data-modifying CTE is not a read |
727
+ | `X_MCP_NOT_BRANCH_DB` | `db.migrate` pointed at a database that is not a branch | `x db branch create <name> # then retry db.migrate` — point the host at the database the create reported (`DATABASE_URL=…/<source>_branch_<slug>`). The target is read from the database's own name, so a shared one can never pass |
728
+ | `X_MCP_PROTOCOL` | malformed envelope, a JSON-RPC **batch** (an array — this server answers one request per message and never walks one), or an unsupported method — a client bug, not an authz outcome | send a JSON-RPC 2.0 body, one request per `POST /mcp` (one per line over stdio); the `-32600` carries `data: { code, fix }` naming which |
729
+ | `X_MCP_BODY_TOO_LARGE` | one message over `bodyLimitBytes` (HTTP, `413`) or `lineLimitBytes` (stdio) — on a JSON-RPC `-32600` with `data: { code, cause, fix, limit }` | send less in one message, or raise the cap where the route is built: `mcpHttpRoute({ bodyLimitBytes })` / `defineAppMcp({ bodyLimitBytes })` / `serveStdio({ lineLimitBytes })` |
730
+ | `X_MCP_RATE_LIMITED` | this caller spent its per-minute allowance for the request's class ([above](#rate-limits-on-the-http-transport)) | wait out the `Retry-After`, or raise it where the route is built: `mcpHttpRoute({ rateLimits })` / `defineAppMcp({ rateLimits })`. Never `X_RATE_LIMITED`'s buckets — they do not govern this route |
731
+ | `X_FORBIDDEN` | the action's policy refused this actor — identical to the HTTP denial | over an app's MCP: the caller's fix — ask the account owner or an administrator for the permission; a retry is refused the same way. As the app's developer: `policies.list` for the permission this tool enforces, then grant it to the actor's role in `apps/web/shared/policies.ts` |
732
+ | `X_LLM_OUTPUT_INVALID` | model output failed the `output` schema twice | tighten the prompt or widen the schema; bump the prompt version |
733
+ | `X_AGENT_TOOL_UNEXPOSED` | an `agent()` lists an action that is not MCP-exposed and is not an `inProcessTool()` — refused at **declaration** | `inProcessTool({ … })` for a tool only the agent may call, or add `mcp: { expose: true, description }` to the action, or drop it from `tools` |
734
+ | `X_AGENT_MAX_TURNS` | an `agent()` used every turn without answering | tell the template when to stop and answer through the respond tool, then bump its version |
735
+ | `X_HIVE_EMPTY` | a `hive()` split into zero members | return at least one member input, or guard the call site |
736
+ | `X_NOT_IMPLEMENTED` | a remote driver stub was reached | configure the local/PGlite driver, or wait for the release named in `fix` |
737
+
738
+ Full list: [Error codes](Error-Codes). CLI surface: [CLI reference](CLI-Reference).
739
+
740
+ ## Rules
741
+
742
+ - One authz system. An MCP call and an HTTP call reach the same `policy` with the same actor resolution.
743
+ - One name. A projected tool is called the primitive's export name, verbatim, in `tools/list`, in `scopes:`, in the LLM tool list, in `openapi.json` and in `describe()`. There is no second spelling to derive.
744
+ - Exposure is opt-in per action; the projection carries no scope of its own — `defineAppMcp`'s `scopes:` map may still attach one, from outside the primitive.
745
+ - Visibility is fail-closed and computed per connection. A hidden tool answers ToolNotFound, never Forbidden.
746
+ - Gate order is visibility → scope → arguments → policy, and every outcome is audited. There is no trusted-tool mode.
747
+ - Write tools are branch-scoped. The dev server is never reachable in `ROLE=web`.
748
+ - Facts are generated every build; conventions are hand-written and short.
749
+ - Never generate prose documentation at runtime.
750
+ - Every command and every error has a `--json` form; budgets throw before spending, in `Money`.
751
+ - Every prompt is a versioned file with an evals file.
752
+
753
+ Source: [`packages/mcp/src`](https://github.com/developerz-ai/ultimate/blob/main/packages/mcp/src)