mcpmake 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/dist/commands/bundle.d.ts +1 -0
  2. package/dist/commands/bundle.d.ts.map +1 -0
  3. package/dist/commands/bundle.js +5 -4
  4. package/dist/commands/bundle.js.map +1 -0
  5. package/dist/commands/ci.d.ts +1 -0
  6. package/dist/commands/ci.d.ts.map +1 -0
  7. package/dist/commands/ci.js +3 -2
  8. package/dist/commands/ci.js.map +1 -0
  9. package/dist/commands/deploy.d.ts +1 -0
  10. package/dist/commands/deploy.d.ts.map +1 -0
  11. package/dist/commands/deploy.js +4 -3
  12. package/dist/commands/deploy.js.map +1 -0
  13. package/dist/commands/diff.d.ts +1 -0
  14. package/dist/commands/diff.d.ts.map +1 -0
  15. package/dist/commands/diff.js +5 -4
  16. package/dist/commands/diff.js.map +1 -0
  17. package/dist/commands/from/describe.d.ts +1 -0
  18. package/dist/commands/from/describe.d.ts.map +1 -0
  19. package/dist/commands/from/describe.js +11 -10
  20. package/dist/commands/from/describe.js.map +1 -0
  21. package/dist/commands/from/har.d.ts +1 -0
  22. package/dist/commands/from/har.d.ts.map +1 -0
  23. package/dist/commands/from/har.js +14 -13
  24. package/dist/commands/from/har.js.map +1 -0
  25. package/dist/commands/from/openapi.d.ts +1 -0
  26. package/dist/commands/from/openapi.d.ts.map +1 -0
  27. package/dist/commands/from/openapi.js +17 -16
  28. package/dist/commands/from/openapi.js.map +1 -0
  29. package/dist/commands/from/postman.d.ts +1 -0
  30. package/dist/commands/from/postman.d.ts.map +1 -0
  31. package/dist/commands/from/postman.js +13 -12
  32. package/dist/commands/from/postman.js.map +1 -0
  33. package/dist/commands/from/stainless.d.ts +110 -0
  34. package/dist/commands/from/stainless.d.ts.map +1 -0
  35. package/dist/commands/from/stainless.js +272 -0
  36. package/dist/commands/from/stainless.js.map +1 -0
  37. package/dist/commands/from/target-support.d.ts +1 -0
  38. package/dist/commands/from/target-support.d.ts.map +1 -0
  39. package/dist/commands/from/target-support.js +2 -1
  40. package/dist/commands/from/target-support.js.map +1 -0
  41. package/dist/commands/from/url.d.ts +1 -0
  42. package/dist/commands/from/url.d.ts.map +1 -0
  43. package/dist/commands/from/url.js +14 -13
  44. package/dist/commands/from/url.js.map +1 -0
  45. package/dist/commands/from/website.d.ts +1 -0
  46. package/dist/commands/from/website.d.ts.map +1 -0
  47. package/dist/commands/from/website.js +17 -16
  48. package/dist/commands/from/website.js.map +1 -0
  49. package/dist/commands/lint.d.ts +1 -0
  50. package/dist/commands/lint.d.ts.map +1 -0
  51. package/dist/commands/lint.js +6 -5
  52. package/dist/commands/lint.js.map +1 -0
  53. package/dist/commands/merge.d.ts +1 -0
  54. package/dist/commands/merge.d.ts.map +1 -0
  55. package/dist/commands/merge.js +3 -2
  56. package/dist/commands/merge.js.map +1 -0
  57. package/dist/commands/publish.d.ts +1 -0
  58. package/dist/commands/publish.d.ts.map +1 -0
  59. package/dist/commands/publish.js +4 -3
  60. package/dist/commands/publish.js.map +1 -0
  61. package/dist/commands/rescan.d.ts +1 -0
  62. package/dist/commands/rescan.d.ts.map +1 -0
  63. package/dist/commands/rescan.js +12 -11
  64. package/dist/commands/rescan.js.map +1 -0
  65. package/dist/commands/update.d.ts +1 -0
  66. package/dist/commands/update.d.ts.map +1 -0
  67. package/dist/commands/update.js +10 -9
  68. package/dist/commands/update.js.map +1 -0
  69. package/dist/commands/verify.d.ts +1 -0
  70. package/dist/commands/verify.d.ts.map +1 -0
  71. package/dist/commands/verify.js +7 -6
  72. package/dist/commands/verify.js.map +1 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +23 -2
  76. package/dist/index.js.map +1 -0
  77. package/dist/registry/official-registry.d.ts +1 -0
  78. package/dist/registry/official-registry.d.ts.map +1 -0
  79. package/dist/registry/official-registry.js +1 -0
  80. package/dist/registry/official-registry.js.map +1 -0
  81. package/package.json +24 -42
  82. package/README.md +0 -691
  83. package/dist/analyzer/auth-detector.d.ts +0 -12
  84. package/dist/analyzer/auth-detector.js +0 -142
  85. package/dist/analyzer/dom-parser.d.ts +0 -10
  86. package/dist/analyzer/dom-parser.js +0 -259
  87. package/dist/analyzer/goal-crawler.d.ts +0 -25
  88. package/dist/analyzer/goal-crawler.js +0 -177
  89. package/dist/analyzer/hybrid-detector.d.ts +0 -28
  90. package/dist/analyzer/hybrid-detector.js +0 -96
  91. package/dist/analyzer/index.d.ts +0 -12
  92. package/dist/analyzer/index.js +0 -8
  93. package/dist/analyzer/screenshot-capture.d.ts +0 -29
  94. package/dist/analyzer/screenshot-capture.js +0 -42
  95. package/dist/analyzer/selector-builder.d.ts +0 -19
  96. package/dist/analyzer/selector-builder.js +0 -199
  97. package/dist/analyzer/semantic-analyzer.d.ts +0 -13
  98. package/dist/analyzer/semantic-analyzer.js +0 -145
  99. package/dist/analyzer/site-crawler.d.ts +0 -38
  100. package/dist/analyzer/site-crawler.js +0 -235
  101. package/dist/cloud/billing/billing-engine.d.ts +0 -44
  102. package/dist/cloud/billing/billing-engine.js +0 -81
  103. package/dist/cloud/billing/credit-store.d.ts +0 -64
  104. package/dist/cloud/billing/credit-store.js +0 -168
  105. package/dist/cloud/billing/index.d.ts +0 -4
  106. package/dist/cloud/billing/index.js +0 -2
  107. package/dist/cloud/billing/usage-store.d.ts +0 -42
  108. package/dist/cloud/billing/usage-store.js +0 -85
  109. package/dist/cloud/billing/usage-tracker.d.ts +0 -38
  110. package/dist/cloud/billing/usage-tracker.js +0 -95
  111. package/dist/cloud/build-pipeline.d.ts +0 -39
  112. package/dist/cloud/build-pipeline.js +0 -310
  113. package/dist/cloud/build-queue.d.ts +0 -30
  114. package/dist/cloud/build-queue.js +0 -70
  115. package/dist/cloud/caddy-manager.d.ts +0 -18
  116. package/dist/cloud/caddy-manager.js +0 -97
  117. package/dist/cloud/container-backend.d.ts +0 -62
  118. package/dist/cloud/container-backend.js +0 -59
  119. package/dist/cloud/container-manager.d.ts +0 -64
  120. package/dist/cloud/container-manager.js +0 -301
  121. package/dist/cloud/crypto.d.ts +0 -27
  122. package/dist/cloud/crypto.js +0 -63
  123. package/dist/cloud/db/index.d.ts +0 -27
  124. package/dist/cloud/db/index.js +0 -53
  125. package/dist/cloud/db/migrations.d.ts +0 -12
  126. package/dist/cloud/db/migrations.js +0 -329
  127. package/dist/cloud/db/pg-store.d.ts +0 -45
  128. package/dist/cloud/db/pg-store.js +0 -336
  129. package/dist/cloud/failure-tracker.d.ts +0 -51
  130. package/dist/cloud/failure-tracker.js +0 -102
  131. package/dist/cloud/idle-monitor.d.ts +0 -30
  132. package/dist/cloud/idle-monitor.js +0 -70
  133. package/dist/cloud/mailer.d.ts +0 -21
  134. package/dist/cloud/mailer.js +0 -193
  135. package/dist/cloud/mcp-proxy.d.ts +0 -58
  136. package/dist/cloud/mcp-proxy.js +0 -203
  137. package/dist/cloud/metric-samples.d.ts +0 -43
  138. package/dist/cloud/metric-samples.js +0 -85
  139. package/dist/cloud/metrics.d.ts +0 -26
  140. package/dist/cloud/metrics.js +0 -59
  141. package/dist/cloud/multipart.d.ts +0 -26
  142. package/dist/cloud/multipart.js +0 -132
  143. package/dist/cloud/observability.d.ts +0 -27
  144. package/dist/cloud/observability.js +0 -98
  145. package/dist/cloud/rate-limiter.d.ts +0 -31
  146. package/dist/cloud/rate-limiter.js +0 -58
  147. package/dist/cloud/request-security.d.ts +0 -5
  148. package/dist/cloud/request-security.js +0 -74
  149. package/dist/cloud/resource-monitor.d.ts +0 -69
  150. package/dist/cloud/resource-monitor.js +0 -130
  151. package/dist/cloud/secret-store.d.ts +0 -38
  152. package/dist/cloud/secret-store.js +0 -103
  153. package/dist/cloud/security.d.ts +0 -26
  154. package/dist/cloud/security.js +0 -142
  155. package/dist/cloud/server.d.ts +0 -21
  156. package/dist/cloud/server.js +0 -1079
  157. package/dist/cloud/shared-state.d.ts +0 -72
  158. package/dist/cloud/shared-state.js +0 -159
  159. package/dist/cloud/ssrf.d.ts +0 -43
  160. package/dist/cloud/ssrf.js +0 -150
  161. package/dist/cloud/store.d.ts +0 -41
  162. package/dist/cloud/store.js +0 -75
  163. package/dist/cloud/stripe.d.ts +0 -78
  164. package/dist/cloud/stripe.js +0 -317
  165. package/dist/cloud/telemetry-store.d.ts +0 -53
  166. package/dist/cloud/telemetry-store.js +0 -108
  167. package/dist/cloud/web/auth.d.ts +0 -225
  168. package/dist/cloud/web/auth.js +0 -555
  169. package/dist/cloud/web/charts.d.ts +0 -70
  170. package/dist/cloud/web/charts.js +0 -178
  171. package/dist/cloud/web/csrf.d.ts +0 -14
  172. package/dist/cloud/web/csrf.js +0 -22
  173. package/dist/cloud/web/docs.d.ts +0 -40
  174. package/dist/cloud/web/docs.js +0 -174
  175. package/dist/cloud/web/router.d.ts +0 -25
  176. package/dist/cloud/web/router.js +0 -1921
  177. package/dist/cloud/web/static/alpine.min.js +0 -5
  178. package/dist/cloud/web/static/favicon.svg +0 -4
  179. package/dist/cloud/web/static/htmx-sse.js +0 -290
  180. package/dist/cloud/web/static/htmx.min.js +0 -1
  181. package/dist/cloud/web/static/style.css +0 -2683
  182. package/dist/cloud/web/static-server.d.ts +0 -13
  183. package/dist/cloud/web/static-server.js +0 -73
  184. package/dist/cloud/web/template-engine.d.ts +0 -27
  185. package/dist/cloud/web/template-engine.js +0 -146
  186. package/dist/cloud/web/templates/layouts/admin.hbs +0 -57
  187. package/dist/cloud/web/templates/layouts/auth.hbs +0 -138
  188. package/dist/cloud/web/templates/layouts/base.hbs +0 -16
  189. package/dist/cloud/web/templates/layouts/dashboard.hbs +0 -39
  190. package/dist/cloud/web/templates/layouts/landing.hbs +0 -82
  191. package/dist/cloud/web/templates/pages/admin/overview.hbs +0 -123
  192. package/dist/cloud/web/templates/pages/admin/servers.hbs +0 -129
  193. package/dist/cloud/web/templates/pages/admin/telemetry.hbs +0 -39
  194. package/dist/cloud/web/templates/pages/admin/user-edit.hbs +0 -91
  195. package/dist/cloud/web/templates/pages/admin/users.hbs +0 -179
  196. package/dist/cloud/web/templates/pages/auth/forgot-password.hbs +0 -25
  197. package/dist/cloud/web/templates/pages/auth/login.hbs +0 -33
  198. package/dist/cloud/web/templates/pages/auth/register.hbs +0 -32
  199. package/dist/cloud/web/templates/pages/auth/reset-password.hbs +0 -34
  200. package/dist/cloud/web/templates/pages/dashboard/billing.hbs +0 -140
  201. package/dist/cloud/web/templates/pages/dashboard/create.hbs +0 -173
  202. package/dist/cloud/web/templates/pages/dashboard/index.hbs +0 -8
  203. package/dist/cloud/web/templates/pages/dashboard/server-detail.hbs +0 -280
  204. package/dist/cloud/web/templates/pages/dashboard/server-logs.hbs +0 -35
  205. package/dist/cloud/web/templates/pages/dashboard/server-metrics.hbs +0 -63
  206. package/dist/cloud/web/templates/pages/dashboard/servers-partial.hbs +0 -21
  207. package/dist/cloud/web/templates/pages/dashboard/servers.hbs +0 -44
  208. package/dist/cloud/web/templates/pages/docs/show.hbs +0 -16
  209. package/dist/cloud/web/templates/pages/errors/404.hbs +0 -9
  210. package/dist/cloud/web/templates/pages/errors/500.hbs +0 -8
  211. package/dist/cloud/web/templates/pages/landing/index.hbs +0 -223
  212. package/dist/cloud/web/templates/pages/legal/privacy.hbs +0 -71
  213. package/dist/cloud/web/templates/pages/legal/terms.hbs +0 -73
  214. package/dist/cloud/web/templates/partials/admin-stats.hbs +0 -52
  215. package/dist/cloud/web/templates/partials/flash-message.hbs +0 -6
  216. package/dist/cloud/web/templates/partials/pricing-table.hbs +0 -103
  217. package/dist/cloud/web/templates/partials/server-card.hbs +0 -19
  218. package/dist/cloud/web/templates/partials/status-badge.hbs +0 -1
  219. package/dist/config/configurable-command.d.ts +0 -13
  220. package/dist/config/configurable-command.js +0 -70
  221. package/dist/config/mcpmake-config.d.ts +0 -68
  222. package/dist/config/mcpmake-config.js +0 -207
  223. package/dist/docs/cli.md +0 -400
  224. package/dist/docs/mcp-2026-07-28-migration.md +0 -78
  225. package/dist/docs/migrate-from-stainless.md +0 -94
  226. package/dist/docs/quickstart.md +0 -166
  227. package/dist/docs/show-hn.md +0 -26
  228. package/dist/docs/website-servers.md +0 -169
  229. package/dist/emitter/code-writer.d.ts +0 -8
  230. package/dist/emitter/code-writer.js +0 -25
  231. package/dist/emitter/index.d.ts +0 -32
  232. package/dist/emitter/index.js +0 -280
  233. package/dist/emitter/mcpb-bundler.d.ts +0 -31
  234. package/dist/emitter/mcpb-bundler.js +0 -172
  235. package/dist/emitter/project-scaffolder.d.ts +0 -4
  236. package/dist/emitter/project-scaffolder.js +0 -89
  237. package/dist/emitter/python-template-loader.d.ts +0 -4
  238. package/dist/emitter/python-template-loader.js +0 -30
  239. package/dist/emitter/python-templates/dockerfile.hbs +0 -14
  240. package/dist/emitter/python-templates/env.example.hbs +0 -6
  241. package/dist/emitter/python-templates/requirements.txt.hbs +0 -4
  242. package/dist/emitter/python-templates/server.py.hbs +0 -77
  243. package/dist/emitter/site-scaffolder.d.ts +0 -13
  244. package/dist/emitter/site-scaffolder.js +0 -70
  245. package/dist/emitter/site-template-loader.d.ts +0 -5
  246. package/dist/emitter/site-template-loader.js +0 -47
  247. package/dist/emitter/site-templates/browser-manager.ts.hbs +0 -233
  248. package/dist/emitter/site-templates/config.ts.hbs +0 -28
  249. package/dist/emitter/site-templates/dockerfile.hbs +0 -31
  250. package/dist/emitter/site-templates/env.example.hbs +0 -19
  251. package/dist/emitter/site-templates/package.json.hbs +0 -26
  252. package/dist/emitter/site-templates/server-main-http.ts.hbs +0 -108
  253. package/dist/emitter/site-templates/server-main.ts.hbs +0 -23
  254. package/dist/emitter/site-templates/tool-handler-action.ts.hbs +0 -86
  255. package/dist/emitter/site-templates/tool-handler-form.ts.hbs +0 -116
  256. package/dist/emitter/site-templates/tool-handler-lifecycle.ts.hbs +0 -146
  257. package/dist/emitter/site-templates/tool-index.ts.hbs +0 -11
  258. package/dist/emitter/template-loader.d.ts +0 -1
  259. package/dist/emitter/template-loader.js +0 -27
  260. package/dist/emitter/templates/auth-provider.ts.hbs +0 -57
  261. package/dist/emitter/templates/config.ts.hbs +0 -63
  262. package/dist/emitter/templates/discovery.ts.hbs +0 -301
  263. package/dist/emitter/templates/dockerfile.hbs +0 -34
  264. package/dist/emitter/templates/env.example.hbs +0 -28
  265. package/dist/emitter/templates/gitignore.hbs +0 -5
  266. package/dist/emitter/templates/http-executor.ts.hbs +0 -117
  267. package/dist/emitter/templates/oauth.ts.hbs +0 -188
  268. package/dist/emitter/templates/package.json.hbs +0 -25
  269. package/dist/emitter/templates/prompts.ts.hbs +0 -22
  270. package/dist/emitter/templates/readme.md.hbs +0 -123
  271. package/dist/emitter/templates/resources.ts.hbs +0 -63
  272. package/dist/emitter/templates/server-main-http.ts.hbs +0 -407
  273. package/dist/emitter/templates/server-main.ts.hbs +0 -40
  274. package/dist/emitter/templates/task-handlers.ts.hbs +0 -189
  275. package/dist/emitter/templates/task-manager.ts.hbs +0 -139
  276. package/dist/emitter/templates/task-sse.ts.hbs +0 -105
  277. package/dist/emitter/templates/tool-handler.ts.hbs +0 -124
  278. package/dist/emitter/templates/tool-index.ts.hbs +0 -11
  279. package/dist/emitter/templates/tool-test.ts.hbs +0 -57
  280. package/dist/emitter/templates/trace.ts.hbs +0 -79
  281. package/dist/emitter/templates/tsconfig.json.hbs +0 -16
  282. package/dist/emitter/templates/types.ts.hbs +0 -5
  283. package/dist/emitter/worker-template-loader.d.ts +0 -5
  284. package/dist/emitter/worker-template-loader.js +0 -33
  285. package/dist/emitter/worker-templates/config.ts.hbs +0 -54
  286. package/dist/emitter/worker-templates/dev-vars.example.hbs +0 -10
  287. package/dist/emitter/worker-templates/gitignore.hbs +0 -6
  288. package/dist/emitter/worker-templates/package.json.hbs +0 -24
  289. package/dist/emitter/worker-templates/readme.md.hbs +0 -53
  290. package/dist/emitter/worker-templates/server.test.ts.hbs +0 -20
  291. package/dist/emitter/worker-templates/tool-handler.ts.hbs +0 -85
  292. package/dist/emitter/worker-templates/tool-index.ts.hbs +0 -28
  293. package/dist/emitter/worker-templates/tsconfig.json.hbs +0 -17
  294. package/dist/emitter/worker-templates/worker.ts.hbs +0 -242
  295. package/dist/emitter/worker-templates/wrangler.toml.hbs +0 -19
  296. package/dist/generator/spec-generator.d.ts +0 -6
  297. package/dist/generator/spec-generator.js +0 -50
  298. package/dist/parser/har-filter.d.ts +0 -8
  299. package/dist/parser/har-filter.js +0 -71
  300. package/dist/parser/har-loader.d.ts +0 -2
  301. package/dist/parser/har-loader.js +0 -14
  302. package/dist/parser/har-normalizer.d.ts +0 -20
  303. package/dist/parser/har-normalizer.js +0 -78
  304. package/dist/parser/index.d.ts +0 -10
  305. package/dist/parser/index.js +0 -6
  306. package/dist/parser/openapi-loader.d.ts +0 -6
  307. package/dist/parser/openapi-loader.js +0 -308
  308. package/dist/parser/operation-extractor.d.ts +0 -13
  309. package/dist/parser/operation-extractor.js +0 -155
  310. package/dist/parser/overlay-loader.d.ts +0 -10
  311. package/dist/parser/overlay-loader.js +0 -184
  312. package/dist/parser/postman-loader.d.ts +0 -9
  313. package/dist/parser/postman-loader.js +0 -106
  314. package/dist/parser/schema-converter.d.ts +0 -12
  315. package/dist/parser/schema-converter.js +0 -117
  316. package/dist/plugins/adapter.d.ts +0 -40
  317. package/dist/plugins/adapter.js +0 -15
  318. package/dist/plugins/loader.d.ts +0 -25
  319. package/dist/plugins/loader.js +0 -58
  320. package/dist/pricing.d.ts +0 -55
  321. package/dist/pricing.js +0 -133
  322. package/dist/providers/index.d.ts +0 -15
  323. package/dist/providers/index.js +0 -56
  324. package/dist/recorder/browser-recorder.d.ts +0 -22
  325. package/dist/recorder/browser-recorder.js +0 -205
  326. package/dist/rescan/diff-engine.d.ts +0 -5
  327. package/dist/rescan/diff-engine.js +0 -312
  328. package/dist/rescan/index.d.ts +0 -3
  329. package/dist/rescan/index.js +0 -2
  330. package/dist/rescan/rescan-runner.d.ts +0 -42
  331. package/dist/rescan/rescan-runner.js +0 -69
  332. package/dist/rescan/rescan-scheduler.d.ts +0 -41
  333. package/dist/rescan/rescan-scheduler.js +0 -179
  334. package/dist/site-transformer/browser-tools.d.ts +0 -10
  335. package/dist/site-transformer/browser-tools.js +0 -59
  336. package/dist/site-transformer/index.d.ts +0 -2
  337. package/dist/site-transformer/index.js +0 -2
  338. package/dist/site-transformer/selector-healer.d.ts +0 -8
  339. package/dist/site-transformer/selector-healer.js +0 -106
  340. package/dist/site-transformer/tool-generator.d.ts +0 -13
  341. package/dist/site-transformer/tool-generator.js +0 -245
  342. package/dist/transformer/auth-detector.d.ts +0 -13
  343. package/dist/transformer/auth-detector.js +0 -90
  344. package/dist/transformer/catalog-builder.d.ts +0 -18
  345. package/dist/transformer/catalog-builder.js +0 -56
  346. package/dist/transformer/client-compat.d.ts +0 -6
  347. package/dist/transformer/client-compat.js +0 -44
  348. package/dist/transformer/har-clusterer.d.ts +0 -9
  349. package/dist/transformer/har-clusterer.js +0 -27
  350. package/dist/transformer/har-dedup.d.ts +0 -10
  351. package/dist/transformer/har-dedup.js +0 -81
  352. package/dist/transformer/har-schema-inferrer.d.ts +0 -15
  353. package/dist/transformer/har-schema-inferrer.js +0 -90
  354. package/dist/transformer/har-to-operations.d.ts +0 -13
  355. package/dist/transformer/har-to-operations.js +0 -192
  356. package/dist/transformer/index.d.ts +0 -8
  357. package/dist/transformer/index.js +0 -6
  358. package/dist/transformer/llm-namer.d.ts +0 -6
  359. package/dist/transformer/llm-namer.js +0 -59
  360. package/dist/transformer/naming.d.ts +0 -4
  361. package/dist/transformer/naming.js +0 -30
  362. package/dist/transformer/operation-filter.d.ts +0 -13
  363. package/dist/transformer/operation-filter.js +0 -52
  364. package/dist/transformer/resource-builder.d.ts +0 -12
  365. package/dist/transformer/resource-builder.js +0 -80
  366. package/dist/transformer/schema-merger.d.ts +0 -14
  367. package/dist/transformer/schema-merger.js +0 -65
  368. package/dist/transformer/tool-builder.d.ts +0 -3
  369. package/dist/transformer/tool-builder.js +0 -114
  370. package/dist/types/index.d.ts +0 -131
  371. package/dist/types/index.js +0 -1
  372. package/dist/types/site.d.ts +0 -284
  373. package/dist/types/site.js +0 -8
  374. package/dist/utils/fail.d.ts +0 -48
  375. package/dist/utils/fail.js +0 -204
  376. package/dist/utils/fs.d.ts +0 -5
  377. package/dist/utils/fs.js +0 -28
  378. package/dist/utils/interactive.d.ts +0 -6
  379. package/dist/utils/interactive.js +0 -30
  380. package/dist/utils/logger.d.ts +0 -1
  381. package/dist/utils/logger.js +0 -2
  382. package/dist/utils/sanitize.d.ts +0 -28
  383. package/dist/utils/sanitize.js +0 -44
  384. package/dist/utils/watcher.d.ts +0 -11
  385. package/dist/utils/watcher.js +0 -36
@@ -1,1079 +0,0 @@
1
- /**
2
- * Hosting backend HTTP server.
3
- *
4
- * A minimal Node.js HTTP server (no Express) that accepts spec uploads,
5
- * generates MCP servers, and manages Docker container lifecycles.
6
- *
7
- * Endpoints:
8
- * POST /api/servers — Create a new hosted MCP server
9
- * GET /api/servers — List servers
10
- * GET /api/servers/:slug — Get server status
11
- * DELETE /api/servers/:slug — Stop and remove server
12
- * PUT /api/servers/:slug — Re-deploy with updated spec
13
- * GET /api/servers/:slug/logs — Tail container logs (SSE)
14
- * GET /health — Health check
15
- */
16
- import http from 'node:http';
17
- import { writeFile, mkdir, unlink, rm } from 'node:fs/promises';
18
- import { join } from 'node:path';
19
- import { randomBytes, timingSafeEqual, createHash } from 'node:crypto';
20
- import { logger } from '../utils/logger.js';
21
- import { buildFromSpec, detectFormat, detectFormatFromContent } from './build-pipeline.js';
22
- import { getContainerBackend } from './container-backend.js';
23
- import { generateBearerToken, hashToken, verifyToken, hashSpec } from './security.js';
24
- import { IdleMonitor } from './idle-monitor.js';
25
- import { parseMultipart, isAllowedSpecFile, getSafeExtension } from './multipart.js';
26
- import { handleWebRequest, render500Page } from './web/router.js';
27
- import { getSessionToken, getSession, getUserById, countUsers } from './web/auth.js';
28
- import { getClientIp } from './request-security.js';
29
- import { handleWebhookEvent, isStripeConfigured } from './stripe.js';
30
- import { extractSlug, authorizeBearer, readBoundedBody, inspectMcpBody, proxyToUpstream, } from './mcp-proxy.js';
31
- import { checkCallQuota } from './billing/billing-engine.js';
32
- import { FAMILY_A_PRICING } from '../pricing.js';
33
- import { getBillingPeriod } from './billing/usage-store.js';
34
- import { buildQueue } from './build-queue.js';
35
- import { reportError, installGlobalErrorHandlers, validateStartupConfig } from './observability.js';
36
- import { ResourceMonitor, gatherResourceReading, } from './resource-monitor.js';
37
- import { store, ports, uploadLimiter, loginLimiter, telemetryLimiter, metrics, usageTracker, failureTracker, getUsageStore, getCreditStore, getMetricSampleStore, getTelemetryStore, MAX_SPEC_SIZE, MAX_NAME_LENGTH, UPLOAD_DIR, getPgServerStore, getSecretStore, initStores, } from './shared-state.js';
38
- const DEFAULT_PORT = 3001;
39
- const DEFAULT_DOMAIN = 'mcpmake.dev';
40
- /** Active host resource monitor, exposed so /health can report the gauges. */
41
- let resourceMonitor = null;
42
- /**
43
- * Start the hosting backend server.
44
- */
45
- export async function startServer(port = DEFAULT_PORT, domain = DEFAULT_DOMAIN) {
46
- // Fail fast on missing production secrets; report crashes instead of dying.
47
- validateStartupConfig();
48
- installGlobalErrorHandlers();
49
- // Initialise Pg-backed stores when DATABASE_URL is set
50
- await initStores();
51
- // Start idle monitor
52
- const idleMonitor = new IdleMonitor(store, (p) => ports.release(p), {
53
- idleThresholdMs: parseInt(process.env.IDLE_THRESHOLD_MS ?? '3600000', 10),
54
- domain,
55
- });
56
- idleMonitor.start();
57
- // Monitor host resource pressure (disk / RAM / active containers) and alert
58
- // via the observability hook before the single host runs out of headroom.
59
- // The same 5-min tick also persists a time-series sample for the admin charts.
60
- const dataDir = process.env.DATA_DIR ?? '/';
61
- resourceMonitor = new ResourceMonitor(async () => {
62
- // Prefer the Pg store: the in-memory `store` is empty after a restart in
63
- // Pg mode, which would under-report active containers.
64
- const pg = getPgServerStore();
65
- const all = pg ? await pg.list() : store.list();
66
- const running = all.filter((s) => s.status === 'running').length;
67
- return gatherResourceReading(running, dataDir);
68
- }, {
69
- onSample: (reading) => {
70
- void persistMetricSample(reading);
71
- },
72
- });
73
- resourceMonitor.start();
74
- // Periodic cleanup of rate limiters and expired sessions
75
- const cleanupTimer = setInterval(() => {
76
- uploadLimiter.cleanup();
77
- loginLimiter.cleanup();
78
- telemetryLimiter.cleanup();
79
- // cleanupSessions is async (handles Pg + in-memory) — fire and forget
80
- import('./web/auth.js').then(({ cleanupSessions }) => cleanupSessions().catch(() => { }));
81
- }, 3_600_000);
82
- cleanupTimer.unref();
83
- const server = http.createServer(async (req, res) => {
84
- // Count every completed response by status class for the API failure rate.
85
- // Registered here (the one entry point for all responses) rather than in
86
- // sendJson/sendHtml, which miss the proxy, redirect, and SSE paths.
87
- // 'finish' = response fully sent; 'close' without writableFinished = the
88
- // socket was aborted before the response completed.
89
- res.on('finish', () => failureTracker.record(res.statusCode));
90
- res.on('close', () => {
91
- if (!res.writableFinished)
92
- failureTracker.recordAborted();
93
- });
94
- try {
95
- await handleRequest(req, res, domain);
96
- }
97
- catch (err) {
98
- reportError(err, { method: req.method, url: req.url });
99
- if (res.headersSent) {
100
- res.destroy();
101
- return;
102
- }
103
- const accept = req.headers.accept ?? '';
104
- if (accept.includes('text/html') && !accept.includes('application/json')) {
105
- render500Page(res);
106
- }
107
- else {
108
- sendJson(res, 500, { error: 'Internal server error' });
109
- }
110
- }
111
- });
112
- server.listen(port, () => {
113
- logger.info(`Hosting backend listening on http://localhost:${port}`);
114
- });
115
- server.on('close', () => {
116
- idleMonitor.stop();
117
- resourceMonitor?.stop();
118
- clearInterval(cleanupTimer);
119
- });
120
- return server;
121
- }
122
- // ---------------------------------------------------------------------------
123
- // Router
124
- // ---------------------------------------------------------------------------
125
- export async function handleRequest(req, res, domain) {
126
- const method = req.method ?? 'GET';
127
- const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
128
- const path = url.pathname;
129
- logger.info(`${method} ${path}`);
130
- // Served MCP traffic arrives on a per-server subdomain ({slug}.{domain}).
131
- // The backend is the authoritative entry point: validate the token, enforce
132
- // quota, meter usage, then stream-proxy to the container. This is wired
133
- // before all apex routes because subdomains are a separate virtual host.
134
- const proxySlug = extractSlug(req.headers.host, domain);
135
- if (proxySlug) {
136
- await handleMcpSubdomain(req, res, proxySlug);
137
- return;
138
- }
139
- // HEAD probes from monitors / uptime tools. Mirror the GET 200 for the apex
140
- // landing and health endpoints (headers only, no body per the HTTP spec) so
141
- // a HEAD check doesn't read as an outage while GET returns 200.
142
- if (method === 'HEAD' && (path === '/' || path === '/health')) {
143
- res.writeHead(200, {
144
- 'Content-Type': path === '/health' ? 'application/json' : 'text/html; charset=utf-8',
145
- });
146
- res.end();
147
- return;
148
- }
149
- // Stripe webhook — must be handled before anything that consumes the body,
150
- // and must NOT go through CSRF validation (Stripe cannot send CSRF tokens).
151
- // Requires the raw body as a Buffer for signature verification.
152
- if (method === 'POST' && path === '/api/webhooks/stripe') {
153
- await handleStripeWebhook(req, res);
154
- return;
155
- }
156
- // Web routes (HTML pages, static files) — skip for API and health paths
157
- if (!path.startsWith('/api/') && path !== '/health') {
158
- const handled = await handleWebRequest(req, res, { domain, store, ports, metrics });
159
- if (handled)
160
- return;
161
- }
162
- // Health check
163
- if (method === 'GET' && path === '/health') {
164
- const pgStore = getPgServerStore();
165
- const serverCount = pgStore ? (await pgStore.list()).length : store.list().length;
166
- const reading = resourceMonitor?.lastReading();
167
- const fw = failureTracker.window(60);
168
- sendJson(res, 200, {
169
- status: 'ok',
170
- servers: serverCount,
171
- ports: ports.allocatedCount,
172
- requestsLastHour: fw.total,
173
- serverErrorRatePct: Math.round(fw.serverErrorRatePct * 10) / 10,
174
- ...(reading
175
- ? {
176
- activeContainers: reading.activeContainers,
177
- memUsedPct: Math.round(reading.memUsedPct),
178
- diskUsedPct: Math.round(reading.diskUsedPct),
179
- }
180
- : {}),
181
- });
182
- return;
183
- }
184
- // GET /api/pricing — Public pricing (the source of truth for the CLI).
185
- // The installed CLI fetches this so an old binary never prints stale prices;
186
- // it falls back to its bundled copy only when the backend is unreachable.
187
- if (method === 'GET' && path === '/api/pricing') {
188
- sendJson(res, 200, { familyA: FAMILY_A_PRICING });
189
- return;
190
- }
191
- // POST /api/telemetry/report — Receive opt-in CLI crash reports.
192
- // Unauthenticated (no secrets, no PII) but heavily rate-limited and size-
193
- // capped; see handleTelemetryReport for the abuse hardening.
194
- if (method === 'POST' && path === '/api/telemetry/report') {
195
- await handleTelemetryReport(req, res);
196
- return;
197
- }
198
- // POST /api/servers — Create
199
- if (method === 'POST' && path === '/api/servers') {
200
- await handleCreateServer(req, res, domain);
201
- return;
202
- }
203
- // GET /api/servers — List (requires admin token)
204
- if (method === 'GET' && path === '/api/servers') {
205
- if (!authenticateAdmin(req, res))
206
- return;
207
- await handleListServers(res);
208
- return;
209
- }
210
- // Match /api/servers/:slug routes (slugs: alphanumeric + hyphens only, no dots)
211
- const slugMatch = path.match(/^\/api\/servers\/([a-z0-9][a-z0-9-]*)$/);
212
- if (slugMatch) {
213
- const slug = slugMatch[1];
214
- if (method === 'GET') {
215
- if (!(await authenticateRequest(req, res, slug)))
216
- return;
217
- await handleGetServer(res, slug);
218
- return;
219
- }
220
- if (method === 'DELETE') {
221
- if (!(await authenticateRequest(req, res, slug)))
222
- return;
223
- await handleDeleteServer(res, slug, domain);
224
- return;
225
- }
226
- if (method === 'PUT') {
227
- if (!(await authenticateRequest(req, res, slug)))
228
- return;
229
- await handleUpdateServer(req, res, slug, domain);
230
- return;
231
- }
232
- }
233
- // Match /api/servers/:slug/logs
234
- const logsMatch = path.match(/^\/api\/servers\/([a-z0-9][a-z0-9-]*)\/logs$/);
235
- if (logsMatch && method === 'GET') {
236
- if (!(await authenticateRequest(req, res, logsMatch[1])))
237
- return;
238
- await handleGetLogs(res, logsMatch[1]);
239
- return;
240
- }
241
- // Match /api/servers/:slug/metrics
242
- const metricsMatch = path.match(/^\/api\/servers\/([a-z0-9][a-z0-9-]*)\/metrics$/);
243
- if (metricsMatch && method === 'GET') {
244
- if (!(await authenticateRequest(req, res, metricsMatch[1])))
245
- return;
246
- await handleGetMetrics(res, metricsMatch[1]);
247
- return;
248
- }
249
- sendJson(res, 404, { error: 'Not found' });
250
- }
251
- /**
252
- * Handle served MCP traffic for a single hosted server (subdomain request).
253
- *
254
- * Validates the bearer token against the server's stored hash, enforces the
255
- * owner's monthly tool-call quota, meters usage, keeps the server marked
256
- * active, and stream-proxies the request to the container.
257
- */
258
- async function handleMcpSubdomain(req, res, slug) {
259
- const pgStore = getPgServerStore();
260
- const record = pgStore ? await pgStore.get(slug) : store.get(slug);
261
- if (!record) {
262
- sendJson(res, 404, { error: 'Server not found' });
263
- return;
264
- }
265
- // 1. Authenticate against the stored token hash.
266
- const auth = authorizeBearer(req, record.bearerToken);
267
- if (!auth.ok) {
268
- sendJson(res, auth.status, { error: auth.error });
269
- return;
270
- }
271
- if (record.status !== 'running') {
272
- sendJson(res, 503, { error: 'Server is not running. It may be sleeping or rebuilding.' });
273
- return;
274
- }
275
- // 2. Buffer the body (bounded) so we can meter tool calls and enforce quota.
276
- const body = await readBoundedBody(req);
277
- if (body === null) {
278
- sendJson(res, 413, { error: 'Request body too large' });
279
- return;
280
- }
281
- const { toolCalls, primaryTool } = inspectMcpBody(body, req.headers['content-type']);
282
- // 3. Enforce the owner's monthly tool-call quota before forwarding. When the
283
- // plan quota is exhausted, fall back to any promotional credit balance
284
- // (all-or-nothing for this request) before returning 429.
285
- const billable = !!record.userId && toolCalls > 0;
286
- let period;
287
- let coveredByCredits = false;
288
- if (billable) {
289
- const user = await getUserById(record.userId);
290
- const plan = (user?.plan ?? 'free');
291
- period = getBillingPeriod(new Date(), user?.subscriptionPeriodEnd);
292
- const used = await getUsageStore().getToolCalls(record.userId, period);
293
- const quota = checkCallQuota(plan, used);
294
- if (!quota.allowed) {
295
- // Atomic consume — never read-modify-write (lost-update would leak credits).
296
- const credit = await getCreditStore()
297
- .consume(record.userId, toolCalls)
298
- .catch(() => ({ consumed: false, balance: 0 }));
299
- if (!credit.consumed) {
300
- sendJson(res, 429, {
301
- error: `Monthly tool-call quota exceeded (${quota.limit}). Upgrade your plan or ask for promotional credits.`,
302
- });
303
- return;
304
- }
305
- coveredByCredits = true;
306
- }
307
- }
308
- // 4. Meter: per-server metrics, keep-alive timestamp, and durable usage.
309
- // Persist the tool-call count BEFORE the (slow, streaming) proxy call and
310
- // await it, so a concurrent request for the same user reads the incremented
311
- // counter when checking quota. The DB upsert is atomic, so counts never get
312
- // lost; this only narrows the read-before-write window to a single round trip
313
- // (rather than the full proxy duration). A metering failure must not block
314
- // serving, so we log and continue.
315
- const now = new Date().toISOString();
316
- metrics.recordRequest(slug, primaryTool);
317
- store.update(slug, { lastActiveAt: now });
318
- if (pgStore) {
319
- pgStore.update(slug, { lastActiveAt: now }).catch(() => { });
320
- }
321
- // Credit-covered calls are NOT counted against the plan's monthly quota — the
322
- // user paid for them with promotional credits, which were already decremented.
323
- if (billable && period && !coveredByCredits) {
324
- const meta = primaryTool ? { tool: primaryTool } : undefined;
325
- for (let i = 0; i < toolCalls; i++) {
326
- usageTracker.recordToolCall(record.userId, slug, meta);
327
- }
328
- try {
329
- await getUsageStore().record(record.userId, period, toolCalls, 1);
330
- }
331
- catch (err) {
332
- logger.warn(`Failed to persist usage for ${slug}: ${err}`);
333
- }
334
- }
335
- // 5. Stream-proxy to the container.
336
- await proxyToUpstream(req, res, record.port, body);
337
- }
338
- // ---------------------------------------------------------------------------
339
- // Endpoint Handlers
340
- // ---------------------------------------------------------------------------
341
- /**
342
- * POST /api/servers — Create a new hosted MCP server.
343
- * Accepts multipart form data with a spec file and optional name field.
344
- */
345
- async function handleCreateServer(req, res, domain) {
346
- // Gate the unauthenticated JSON API. When ADMIN_TOKEN is configured (the
347
- // production default), creating servers via the API requires it — otherwise
348
- // anyone could spin up containers. In dev / self-host (no ADMIN_TOKEN set)
349
- // the path stays open for the CLI.
350
- if (process.env.ADMIN_TOKEN && !authenticateAdmin(req, res))
351
- return;
352
- // Rate limiting by IP. Trust X-Forwarded-For only when TRUST_PROXY=true.
353
- const clientIp = getClientIp(req);
354
- const rateCheck = uploadLimiter.check(clientIp);
355
- if (!rateCheck.allowed) {
356
- res.setHeader('Retry-After', String(Math.ceil((rateCheck.resetAt - Date.now()) / 1000)));
357
- sendJson(res, 429, {
358
- error: 'Rate limit exceeded. Try again later.',
359
- remaining: 0,
360
- resetAt: new Date(rateCheck.resetAt).toISOString(),
361
- });
362
- return;
363
- }
364
- // Parse multipart form data
365
- const contentType = req.headers['content-type'] ?? '';
366
- if (!contentType.includes('multipart/form-data')) {
367
- sendJson(res, 400, { error: 'Content-Type must be multipart/form-data' });
368
- return;
369
- }
370
- const parsed = await parseMultipart(req, contentType);
371
- if (!parsed) {
372
- sendJson(res, 400, { error: 'Failed to parse multipart body' });
373
- return;
374
- }
375
- const specFile = parsed.files.get('spec');
376
- if (!specFile) {
377
- sendJson(res, 400, { error: 'Missing "spec" file field' });
378
- return;
379
- }
380
- // Validate file size
381
- if (specFile.data.length > MAX_SPEC_SIZE) {
382
- sendJson(res, 400, {
383
- error: `Spec file too large (${Math.round(specFile.data.length / 1024)}KB). Maximum is 5MB.`,
384
- });
385
- return;
386
- }
387
- // Validate file type by name extension
388
- const fileName = specFile.filename ?? 'spec';
389
- if (!isAllowedSpecFile(fileName)) {
390
- sendJson(res, 400, {
391
- error: 'Invalid file type. Accepted: .yaml, .yml, .json, .har',
392
- });
393
- return;
394
- }
395
- let name = parsed.fields.get('name') ?? undefined;
396
- if (name && name.length > MAX_NAME_LENGTH) {
397
- name = name.slice(0, MAX_NAME_LENGTH);
398
- }
399
- // Strip non-printable characters from name
400
- if (name) {
401
- name = name.replace(/[^\x20-\x7E]/g, '');
402
- }
403
- // Save uploaded file to temp dir
404
- await mkdir(UPLOAD_DIR, { recursive: true });
405
- const tempId = randomBytes(8).toString('hex');
406
- const safeExtension = getSafeExtension(fileName);
407
- const specPath = join(UPLOAD_DIR, `${tempId}${safeExtension}`);
408
- await writeFile(specPath, specFile.data);
409
- try {
410
- // Detect format
411
- let format = detectFormat(specPath);
412
- if (safeExtension === '.json') {
413
- // Ambiguous — inspect content
414
- format = await detectFormatFromContent(specPath);
415
- }
416
- // Build pipeline: generate project from spec
417
- logger.info(`Building from ${format} spec: ${fileName}`);
418
- const buildResult = await buildFromSpec(specPath, { name, format });
419
- // Allocate port and generate bearer token
420
- const port = ports.allocate();
421
- const bearerToken = generateBearerToken();
422
- const tokenHash = hashToken(bearerToken);
423
- const specHash = hashSpec(specFile.data);
424
- // Build Docker image
425
- await buildQueue.run(() => getContainerBackend().buildImage(buildResult.buildDir, buildResult.imageName, buildResult.imageTag));
426
- // Clean up build directory — image is built, source no longer needed
427
- try {
428
- await rm(buildResult.buildDir, { recursive: true, force: true });
429
- }
430
- catch {
431
- // Non-critical: log and continue
432
- logger.warn(`Failed to clean up build directory: ${buildResult.buildDir}`);
433
- }
434
- // Retrieve any stored secrets for this slug to inject as env vars
435
- const secrets = await getSecretStore().getSecretsAsEnv(buildResult.slug);
436
- // Start container (Playwright servers get more resources).
437
- // Inject the raw bearer token so the container authenticates /mcp traffic
438
- // with the same token shown to the user in the dashboard config.
439
- const containerId = await getContainerBackend().startContainer({
440
- slug: buildResult.slug,
441
- imageName: buildResult.imageName,
442
- imageTag: buildResult.imageTag,
443
- envVars: { MCP_AUTH_TOKEN: bearerToken },
444
- port,
445
- serverType: buildResult.serverType,
446
- secrets,
447
- });
448
- // No per-container Caddy route: routing is backend-authoritative. The Caddy
449
- // wildcard sends {slug}.{domain} to this backend, which authenticates,
450
- // meters, and proxies to the container. Adding a direct route here would
451
- // let traffic bypass auth/quota/metering.
452
- const now = new Date().toISOString();
453
- const endpoint = `https://${buildResult.slug}.${domain}`;
454
- const serverRecord = {
455
- slug: buildResult.slug,
456
- specHash,
457
- containerId,
458
- port,
459
- bearerToken: tokenHash,
460
- status: 'running',
461
- toolCount: buildResult.toolCount,
462
- serverType: buildResult.serverType,
463
- createdAt: now,
464
- lastActiveAt: now,
465
- };
466
- // Persist to Pg when available, always keep in-memory copy
467
- store.create(serverRecord);
468
- const pgStore = getPgServerStore();
469
- if (pgStore) {
470
- await pgStore.create(serverRecord);
471
- }
472
- // Initialize metrics tracking
473
- metrics.init(buildResult.slug);
474
- sendJson(res, 201, {
475
- slug: buildResult.slug,
476
- endpoint,
477
- bearerToken, // Return raw token once; it's stored hashed
478
- status: 'running',
479
- toolCount: buildResult.toolCount,
480
- claudeDesktopConfig: {
481
- mcpServers: {
482
- [buildResult.slug]: {
483
- url: `${endpoint}/mcp`,
484
- headers: {
485
- Authorization: `Bearer ${bearerToken}`,
486
- },
487
- },
488
- },
489
- },
490
- });
491
- }
492
- catch (err) {
493
- const message = err instanceof Error ? err.message : String(err);
494
- logger.error(`Build failed: ${message}`);
495
- sendJson(res, 500, { error: 'Build failed. Check server logs for details.' });
496
- }
497
- finally {
498
- // Clean up uploaded file
499
- try {
500
- await unlink(specPath);
501
- }
502
- catch {
503
- // Ignore cleanup errors
504
- }
505
- }
506
- }
507
- /**
508
- * GET /api/servers — List all servers.
509
- */
510
- async function handleListServers(res) {
511
- const pgStore = getPgServerStore();
512
- const allServers = pgStore ? await pgStore.list() : store.list();
513
- const servers = allServers.map((s) => ({
514
- slug: s.slug,
515
- status: s.status,
516
- toolCount: s.toolCount,
517
- createdAt: s.createdAt,
518
- lastActiveAt: s.lastActiveAt,
519
- }));
520
- sendJson(res, 200, { servers });
521
- }
522
- /**
523
- * GET /api/servers/:slug — Get server status.
524
- */
525
- async function handleGetServer(res, slug) {
526
- const pgStore = getPgServerStore();
527
- const record = pgStore ? await pgStore.get(slug) : store.get(slug);
528
- if (!record) {
529
- sendJson(res, 404, { error: `Server "${slug}" not found` });
530
- return;
531
- }
532
- // Check live container status
533
- let liveStatus = record.status;
534
- if (record.containerId && record.status === 'running') {
535
- try {
536
- liveStatus = await getContainerBackend().getContainerStatus(record.containerId);
537
- if (liveStatus !== record.status) {
538
- if (pgStore) {
539
- await pgStore.update(slug, { status: liveStatus });
540
- }
541
- store.update(slug, { status: liveStatus });
542
- }
543
- }
544
- catch {
545
- // Docker might not be reachable; use stored status
546
- }
547
- }
548
- sendJson(res, 200, {
549
- slug: record.slug,
550
- status: liveStatus,
551
- toolCount: record.toolCount,
552
- createdAt: record.createdAt,
553
- lastActiveAt: record.lastActiveAt,
554
- });
555
- }
556
- /**
557
- * DELETE /api/servers/:slug — Stop and remove a server.
558
- */
559
- async function handleDeleteServer(res, slug, domain) {
560
- const pgStore = getPgServerStore();
561
- const record = pgStore ? await pgStore.get(slug) : store.get(slug);
562
- if (!record) {
563
- sendJson(res, 404, { error: `Server "${slug}" not found` });
564
- return;
565
- }
566
- // Stop container
567
- if (record.containerId) {
568
- try {
569
- await getContainerBackend().stopContainer(record.containerId);
570
- }
571
- catch (err) {
572
- logger.warn(`Failed to stop container for ${slug}: ${err}`);
573
- }
574
- }
575
- // No Caddy route to remove — routing is backend-authoritative (wildcard).
576
- // Release port
577
- ports.release(record.port);
578
- // Remove from store + metrics
579
- if (pgStore) {
580
- await pgStore.delete(slug);
581
- }
582
- store.delete(slug);
583
- metrics.delete(slug);
584
- sendJson(res, 200, { slug, status: 'deleted' });
585
- }
586
- /**
587
- * PUT /api/servers/:slug — Re-deploy with an updated spec.
588
- */
589
- async function handleUpdateServer(req, res, slug, domain) {
590
- const pgStore = getPgServerStore();
591
- const record = pgStore ? await pgStore.get(slug) : store.get(slug);
592
- if (!record) {
593
- sendJson(res, 404, { error: `Server "${slug}" not found` });
594
- return;
595
- }
596
- // Parse multipart (same as create)
597
- const contentType = req.headers['content-type'] ?? '';
598
- if (!contentType.includes('multipart/form-data')) {
599
- sendJson(res, 400, { error: 'Content-Type must be multipart/form-data' });
600
- return;
601
- }
602
- const parsed = await parseMultipart(req, contentType);
603
- if (!parsed) {
604
- sendJson(res, 400, { error: 'Failed to parse multipart body' });
605
- return;
606
- }
607
- const specFile = parsed.files.get('spec');
608
- if (!specFile) {
609
- sendJson(res, 400, { error: 'Missing "spec" file field' });
610
- return;
611
- }
612
- if (specFile.data.length > MAX_SPEC_SIZE) {
613
- sendJson(res, 400, { error: 'Spec file too large. Maximum is 5MB.' });
614
- return;
615
- }
616
- const fileName = specFile.filename ?? 'spec';
617
- if (!isAllowedSpecFile(fileName)) {
618
- sendJson(res, 400, { error: 'Invalid file type. Accepted: .yaml, .yml, .json, .har' });
619
- return;
620
- }
621
- // Check if spec actually changed
622
- const newSpecHash = hashSpec(specFile.data);
623
- if (newSpecHash === record.specHash) {
624
- sendJson(res, 200, { slug, status: record.status, message: 'No changes detected' });
625
- return;
626
- }
627
- if (pgStore) {
628
- await pgStore.update(slug, { status: 'building' });
629
- }
630
- store.update(slug, { status: 'building' });
631
- // Save uploaded file
632
- await mkdir(UPLOAD_DIR, { recursive: true });
633
- const tempId = randomBytes(8).toString('hex');
634
- const safeExtension = getSafeExtension(fileName);
635
- const specPath = join(UPLOAD_DIR, `${tempId}${safeExtension}`);
636
- await writeFile(specPath, specFile.data);
637
- try {
638
- let format = detectFormat(specPath);
639
- if (safeExtension === '.json') {
640
- format = await detectFormatFromContent(specPath);
641
- }
642
- // Rebuild
643
- const buildResult = await buildFromSpec(specPath, {
644
- name: slug.replace(/-[a-f0-9]{4}$/, ''),
645
- format,
646
- });
647
- // Build new image
648
- await buildQueue.run(() => getContainerBackend().buildImage(buildResult.buildDir, buildResult.imageName, buildResult.imageTag));
649
- // Clean up build directory
650
- try {
651
- await rm(buildResult.buildDir, { recursive: true, force: true });
652
- }
653
- catch {
654
- logger.warn(`Failed to clean up build directory: ${buildResult.buildDir}`);
655
- }
656
- // Stop old container
657
- if (record.containerId) {
658
- try {
659
- await getContainerBackend().stopContainer(record.containerId);
660
- }
661
- catch (err) {
662
- logger.warn(`Failed to stop old container: ${err}`);
663
- }
664
- }
665
- // Retrieve stored secrets for the slug
666
- const secrets = await getSecretStore().getSecretsAsEnv(slug);
667
- // Rotate the bearer token on re-deploy. We only store the token's hash, so
668
- // the old raw token cannot be recovered to re-inject into the new container.
669
- // A fresh token keeps the container authenticating and is returned below.
670
- const newBearerToken = generateBearerToken();
671
- const newTokenHash = hashToken(newBearerToken);
672
- // Start new container on the same port (Playwright servers get more resources)
673
- const containerId = await getContainerBackend().startContainer({
674
- slug: buildResult.slug,
675
- imageName: buildResult.imageName,
676
- imageTag: buildResult.imageTag,
677
- envVars: { MCP_AUTH_TOKEN: newBearerToken },
678
- port: record.port,
679
- serverType: buildResult.serverType,
680
- secrets,
681
- });
682
- const updateFields = {
683
- containerId,
684
- specHash: newSpecHash,
685
- bearerToken: newTokenHash,
686
- status: 'running',
687
- toolCount: buildResult.toolCount,
688
- serverType: buildResult.serverType,
689
- lastActiveAt: new Date().toISOString(),
690
- };
691
- if (pgStore) {
692
- await pgStore.update(slug, updateFields);
693
- }
694
- store.update(slug, updateFields);
695
- // The re-deploy reused the image tag, so the previous build is now dangling.
696
- // Reclaim it (best-effort, non-blocking) to keep the host disk from filling.
697
- void getContainerBackend().pruneDanglingImages();
698
- sendJson(res, 200, {
699
- slug,
700
- status: 'running',
701
- toolCount: buildResult.toolCount,
702
- bearerToken: newBearerToken, // Rotated — update your client config
703
- message: 'Re-deployed successfully. Bearer token was rotated.',
704
- });
705
- }
706
- catch (err) {
707
- const message = err instanceof Error ? err.message : String(err);
708
- if (pgStore) {
709
- await pgStore.update(slug, { status: 'error' }).catch(() => { });
710
- }
711
- store.update(slug, { status: 'error' });
712
- logger.error(`Re-deploy failed for ${slug}: ${message}`);
713
- sendJson(res, 500, { error: 'Re-deploy failed. Check server logs for details.' });
714
- }
715
- finally {
716
- try {
717
- await unlink(specPath);
718
- }
719
- catch {
720
- // Ignore
721
- }
722
- }
723
- }
724
- /**
725
- * GET /api/servers/:slug/logs — Stream container logs via SSE.
726
- */
727
- async function handleGetLogs(res, slug) {
728
- const pgStore = getPgServerStore();
729
- const record = pgStore ? await pgStore.get(slug) : store.get(slug);
730
- if (!record) {
731
- sendJson(res, 404, { error: `Server "${slug}" not found` });
732
- return;
733
- }
734
- if (!record.containerId) {
735
- sendJson(res, 400, { error: 'No container associated with this server' });
736
- return;
737
- }
738
- // Set up SSE headers
739
- res.writeHead(200, {
740
- 'Content-Type': 'text/event-stream',
741
- 'Cache-Control': 'no-cache',
742
- Connection: 'keep-alive',
743
- 'X-Content-Type-Options': 'nosniff',
744
- 'X-Frame-Options': 'DENY',
745
- });
746
- try {
747
- const logStream = getContainerBackend().getContainerLogs(record.containerId);
748
- for await (const line of logStream) {
749
- if (res.destroyed)
750
- break;
751
- res.write(`data: ${JSON.stringify(line)}\n\n`);
752
- }
753
- }
754
- catch (err) {
755
- const message = err instanceof Error ? err.message : String(err);
756
- res.write(`event: error\ndata: ${JSON.stringify({ error: message })}\n\n`);
757
- }
758
- res.end();
759
- }
760
- /**
761
- * GET /api/servers/:slug/metrics — Get usage metrics.
762
- */
763
- async function handleGetMetrics(res, slug) {
764
- const pgStore = getPgServerStore();
765
- const record = pgStore ? await pgStore.get(slug) : store.get(slug);
766
- if (!record) {
767
- sendJson(res, 404, { error: `Server "${slug}" not found` });
768
- return;
769
- }
770
- const serverMetrics = metrics.get(slug);
771
- const topTools = metrics.getTopTools(slug);
772
- sendJson(res, 200, {
773
- slug,
774
- totalRequests: serverMetrics?.totalRequests ?? 0,
775
- topTools,
776
- lastActiveAt: serverMetrics?.lastActiveAt ?? record.lastActiveAt,
777
- createdAt: record.createdAt,
778
- });
779
- }
780
- /**
781
- * POST /api/webhooks/stripe — Receive Stripe webhook events.
782
- *
783
- * Reads the raw request body as a Buffer (required for signature verification),
784
- * then delegates to the stripe module's handleWebhookEvent.
785
- */
786
- async function handleStripeWebhook(req, res) {
787
- if (!isStripeConfigured()) {
788
- sendJson(res, 404, { error: 'Not found' });
789
- return;
790
- }
791
- const signature = req.headers['stripe-signature'];
792
- if (!signature || typeof signature !== 'string') {
793
- sendJson(res, 400, { error: 'Missing stripe-signature header' });
794
- return;
795
- }
796
- // Read raw body as Buffer for signature verification
797
- const MAX_WEBHOOK_BODY = 1024 * 1024; // 1 MB
798
- const chunks = [];
799
- let totalSize = 0;
800
- try {
801
- for await (const chunk of req) {
802
- totalSize += chunk.length;
803
- if (totalSize > MAX_WEBHOOK_BODY) {
804
- sendJson(res, 413, { error: 'Payload too large' });
805
- return;
806
- }
807
- chunks.push(chunk);
808
- }
809
- }
810
- catch {
811
- sendJson(res, 400, { error: 'Failed to read request body' });
812
- return;
813
- }
814
- const payload = Buffer.concat(chunks);
815
- try {
816
- await handleWebhookEvent(payload, signature);
817
- sendJson(res, 200, { received: true });
818
- }
819
- catch (err) {
820
- const message = err instanceof Error ? err.message : String(err);
821
- logger.error(`[stripe] Webhook error: ${message}`);
822
- sendJson(res, 400, { error: 'Webhook processing failed' });
823
- }
824
- }
825
- // Multipart parser and file validation re-exported from shared module
826
- // (used by both server.ts and web/router.ts)
827
- // ---------------------------------------------------------------------------
828
- // Time-series sampling
829
- // ---------------------------------------------------------------------------
830
- /**
831
- * Persist one time-series sample for the admin charts. The request/error counts
832
- * are PER-INTERVAL deltas read from the rolling failure tracker (last ~5 min),
833
- * not cumulative totals. Best-effort: a metrics write must never disrupt the
834
- * sampler or serving.
835
- */
836
- async function persistMetricSample(reading) {
837
- try {
838
- const fw = failureTracker.window(5); // ~one sampler interval
839
- const pg = getPgServerStore();
840
- const totalServers = pg ? (await pg.list()).length : store.list().length;
841
- let totalUsers = 0;
842
- try {
843
- totalUsers = await countUsers();
844
- }
845
- catch {
846
- // best-effort; leave at 0 if the count query fails
847
- }
848
- const samples = getMetricSampleStore();
849
- await samples.record({
850
- sampledAt: new Date().toISOString(),
851
- requests: fw.total,
852
- errors4xx: fw.c4xx,
853
- errors5xx: fw.c5xx + fw.aborted,
854
- activeContainers: reading.activeContainers,
855
- memUsedPct: Math.round(reading.memUsedPct * 10) / 10,
856
- diskUsedPct: Math.round(reading.diskUsedPct * 10) / 10,
857
- totalUsers,
858
- totalServers,
859
- });
860
- // Retain 30 days of history.
861
- await samples.prune(30 * 24 * 3_600_000);
862
- }
863
- catch (err) {
864
- logger.warn(`Failed to persist metric sample: ${err}`);
865
- }
866
- }
867
- // ---------------------------------------------------------------------------
868
- // CLI telemetry endpoint
869
- // ---------------------------------------------------------------------------
870
- /**
871
- * A crash report is tiny — cap it far below the 5MB spec ceiling so a hostile
872
- * client can't stream junk at us.
873
- */
874
- const MAX_TELEMETRY_BODY = 64 * 1024; // 64 KB
875
- const TELEMETRY_FIELD_CAPS = {
876
- command: 200,
877
- errorMessage: 2_000,
878
- stack: 16_000,
879
- cliVersion: 40,
880
- platform: 60,
881
- nodeVersion: 40,
882
- };
883
- /**
884
- * POST /api/telemetry/report — Receive an opt-in CLI crash report.
885
- *
886
- * Unauthenticated by design (no secrets, no PII — the CLI redacts home-dir
887
- * paths and tokens before sending), but hardened against abuse:
888
- * - rejects any Content-Encoding (we never decompress → no decompression bomb)
889
- * - rejects oversized bodies by declared length, then again while streaming
890
- * - per-IP rate limit
891
- * - strict JSON whitelist with per-field truncation; unknown fields dropped
892
- * - groups duplicates by a fingerprint; never logs the raw body
893
- */
894
- async function handleTelemetryReport(req, res) {
895
- // These early rejections return WITHOUT draining the request body. On a
896
- // keep-alive connection the unread bytes would desync the next request on the
897
- // socket, so close the connection instead of trying to drain (draining the
898
- // oversized case would mean reading the very body we're rejecting).
899
- const rejectClosing = (status, body) => {
900
- res.setHeader('Connection', 'close');
901
- sendJson(res, status, body);
902
- };
903
- // 1. Never decompress untrusted input. Reject any encoded body outright.
904
- if (req.headers['content-encoding']) {
905
- rejectClosing(415, { error: 'Content-Encoding is not supported' });
906
- return;
907
- }
908
- // 2. Reject oversized bodies up front using the (untrusted) Content-Length.
909
- const declared = Number(req.headers['content-length']);
910
- if (Number.isFinite(declared) && declared > MAX_TELEMETRY_BODY) {
911
- rejectClosing(413, { error: 'Payload too large' });
912
- return;
913
- }
914
- // 3. Per-IP flood cap.
915
- const rate = telemetryLimiter.check(getClientIp(req));
916
- if (!rate.allowed) {
917
- res.setHeader('Retry-After', String(Math.ceil((rate.resetAt - Date.now()) / 1000)));
918
- rejectClosing(429, { error: 'Rate limit exceeded. Try again later.' });
919
- return;
920
- }
921
- // 4. Read the body, hard-capped regardless of the declared length.
922
- let raw = '';
923
- let total = 0;
924
- try {
925
- for await (const chunk of req) {
926
- total += chunk.length;
927
- if (total > MAX_TELEMETRY_BODY) {
928
- rejectClosing(413, { error: 'Payload too large' });
929
- return;
930
- }
931
- raw += chunk.toString('utf-8');
932
- }
933
- }
934
- catch {
935
- sendJson(res, 400, { error: 'Failed to read request body' });
936
- return;
937
- }
938
- // 5. Strict parse + whitelist + per-field truncation. Unknown fields dropped.
939
- let parsed;
940
- try {
941
- parsed = JSON.parse(raw);
942
- }
943
- catch {
944
- sendJson(res, 400, { error: 'Invalid JSON' });
945
- return;
946
- }
947
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
948
- sendJson(res, 400, { error: 'Invalid report' });
949
- return;
950
- }
951
- const src = parsed;
952
- const str = (v, cap) => (typeof v === 'string' ? v.slice(0, cap) : '');
953
- const command = str(src.command, TELEMETRY_FIELD_CAPS.command);
954
- const errorMessage = str(src.errorMessage, TELEMETRY_FIELD_CAPS.errorMessage);
955
- const stack = str(src.stack, TELEMETRY_FIELD_CAPS.stack);
956
- const cliVersion = str(src.cliVersion, TELEMETRY_FIELD_CAPS.cliVersion);
957
- const platform = str(src.platform, TELEMETRY_FIELD_CAPS.platform);
958
- const nodeVersion = str(src.nodeVersion, TELEMETRY_FIELD_CAPS.nodeVersion);
959
- if (!command && !errorMessage) {
960
- sendJson(res, 400, { error: 'Report must include a command or error message' });
961
- return;
962
- }
963
- // 6. Fingerprint = command + normalized message (digits/hex collapsed) so
964
- // identical failures cluster together in the admin view.
965
- const normalized = errorMessage
966
- .toLowerCase()
967
- .replace(/0x[0-9a-f]+/g, '')
968
- .replace(/[0-9a-f]{8,}/g, '')
969
- .replace(/\d+/g, 'n');
970
- const fingerprint = createHash('sha256')
971
- .update(`${command}|${normalized}`)
972
- .digest('hex')
973
- .slice(0, 16);
974
- // Hash the client IP for abuse correlation without storing the raw address.
975
- const clientIp = createHash('sha256').update(getClientIp(req)).digest('hex').slice(0, 16);
976
- try {
977
- await getTelemetryStore().save({
978
- receivedAt: new Date().toISOString(),
979
- clientIp,
980
- cliVersion,
981
- platform,
982
- nodeVersion,
983
- command,
984
- errorMessage,
985
- stack,
986
- fingerprint,
987
- });
988
- }
989
- catch (err) {
990
- // Never echo the body back; just log that a save failed.
991
- logger.warn(`Failed to store telemetry report: ${err}`);
992
- }
993
- sendJson(res, 202, { received: true });
994
- }
995
- // ---------------------------------------------------------------------------
996
- // Utilities
997
- // ---------------------------------------------------------------------------
998
- function sendJson(res, status, body) {
999
- const payload = JSON.stringify(body);
1000
- res.writeHead(status, {
1001
- 'Content-Type': 'application/json',
1002
- 'Content-Length': Buffer.byteLength(payload),
1003
- 'X-Content-Type-Options': 'nosniff',
1004
- 'X-Frame-Options': 'DENY',
1005
- });
1006
- res.end(payload);
1007
- }
1008
- /**
1009
- * Authenticate a request using bearer token for a specific server slug.
1010
- * Falls back to cookie-based session auth if no Authorization header is present.
1011
- * This fallback is needed because EventSource (used by htmx SSE) cannot set custom headers.
1012
- */
1013
- async function authenticateRequest(req, res, slug) {
1014
- const pgStore = getPgServerStore();
1015
- const record = pgStore ? await pgStore.get(slug) : store.get(slug);
1016
- if (!record) {
1017
- sendJson(res, 404, { error: 'Server not found' });
1018
- return false;
1019
- }
1020
- // Try bearer token auth first
1021
- const authHeader = req.headers.authorization;
1022
- if (authHeader && authHeader.startsWith('Bearer ')) {
1023
- const token = authHeader.slice(7);
1024
- if (verifyToken(token, record.bearerToken)) {
1025
- return true;
1026
- }
1027
- sendJson(res, 403, { error: 'Invalid bearer token' });
1028
- return false;
1029
- }
1030
- // Fallback: cookie-based session auth (uses Pg-aware helpers)
1031
- const sessionToken = getSessionToken(req);
1032
- if (sessionToken) {
1033
- const session = await getSession(sessionToken);
1034
- if (session && session.userId !== '__anon__') {
1035
- const user = await getUserById(session.userId);
1036
- if (user && record.userId && record.userId === user.id) {
1037
- return true;
1038
- }
1039
- }
1040
- }
1041
- sendJson(res, 401, { error: 'Missing or invalid Authorization header' });
1042
- return false;
1043
- }
1044
- /**
1045
- * Authenticate admin requests using ADMIN_TOKEN environment variable.
1046
- */
1047
- function authenticateAdmin(req, res) {
1048
- const adminToken = process.env.ADMIN_TOKEN;
1049
- if (!adminToken) {
1050
- sendJson(res, 403, { error: 'Admin access not configured (set ADMIN_TOKEN)' });
1051
- return false;
1052
- }
1053
- const authHeader = req.headers.authorization;
1054
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
1055
- sendJson(res, 401, { error: 'Missing or invalid Authorization header' });
1056
- return false;
1057
- }
1058
- const token = authHeader.slice(7);
1059
- const tokenBuf = Buffer.from(token);
1060
- const adminBuf = Buffer.from(adminToken);
1061
- if (tokenBuf.length !== adminBuf.length || !timingSafeEqual(tokenBuf, adminBuf)) {
1062
- sendJson(res, 403, { error: 'Invalid admin token' });
1063
- return false;
1064
- }
1065
- return true;
1066
- }
1067
- // File validation functions from shared module
1068
- // ---------------------------------------------------------------------------
1069
- // CLI entry point (when run directly)
1070
- // ---------------------------------------------------------------------------
1071
- const isDirectRun = process.argv[1]?.endsWith('cloud/server.js') || process.argv[1]?.endsWith('cloud/server.ts');
1072
- if (isDirectRun) {
1073
- const port = parseInt(process.env.PORT ?? String(DEFAULT_PORT), 10);
1074
- const domain = process.env.DOMAIN ?? DEFAULT_DOMAIN;
1075
- startServer(port, domain).catch((err) => {
1076
- logger.error(`Failed to start server: ${err}`);
1077
- process.exit(1);
1078
- });
1079
- }