@malloy-publisher/server 0.0.234 → 0.0.236

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 (318) hide show
  1. package/dist/app/api-doc.yaml +47 -17
  2. package/dist/app/assets/{EnvironmentPage-DTZQ4Gxc.js → EnvironmentPage-BsAnavYN.js} +1 -1
  3. package/dist/app/assets/{HomePage-C5mlDPXK.js → HomePage-CADE138j.js} +1 -1
  4. package/dist/app/assets/{LightMode-DGNmhG0u.js → LightMode-Cfh7KzN8.js} +1 -1
  5. package/dist/app/assets/{MainPage-CVL_wmP4.js → MainPage-CO3pRlnV.js} +1 -1
  6. package/dist/app/assets/{MaterializationsPage-DmzMBCpy.js → MaterializationsPage-p9YjkRXZ.js} +1 -1
  7. package/dist/app/assets/{ModelPage-Dbvf4QbB.js → ModelPage-C1OSTv-x.js} +1 -1
  8. package/dist/app/assets/{PackagePage-DxdHc2Qs.js → PackagePage-e4kN75YR.js} +1 -1
  9. package/dist/app/assets/{RouteError-OJdT4tCd.js → RouteError-CzbfOkng.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-Bk7s0KXY.js → ThemeEditorPage-CciagFTq.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-j_vCWdN3.js → WorkbookPage-DNWmkCXa.js} +1 -1
  12. package/dist/app/assets/{core-Rj_4rRnA.es-DoIfLxDJ.js → core-Rj_4rRnA.es-BZyvITuO.js} +1 -1
  13. package/dist/app/assets/{index-B_jKMR35.js → index-CH2AcDzc.js} +4 -4
  14. package/dist/app/assets/{index-hw-xn0X7.js → index-DQa463gC.js} +2 -2
  15. package/dist/app/assets/{index-DWIe_hK0.js → index-DQpV7MyA.js} +1 -1
  16. package/dist/app/assets/{index-BabP-V-S.js → index-DzaYbhnD.js} +1 -1
  17. package/dist/app/assets/{index-D-rDyK11.js → index-VBbcc8s6.js} +1 -1
  18. package/dist/app/index.html +1 -1
  19. package/dist/package_load_worker.mjs +8 -1
  20. package/dist/server.mjs +471 -54
  21. package/package.json +16 -13
  22. package/.eslintrc.json +0 -44
  23. package/.prettierignore +0 -1
  24. package/.prettierrc +0 -4
  25. package/build.ts +0 -97
  26. package/eslint.config.mjs +0 -76
  27. package/k6-tests/README.md +0 -144
  28. package/k6-tests/bun.lock +0 -797
  29. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  30. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  31. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  32. package/k6-tests/load-test/load-test-crud.ts +0 -142
  33. package/k6-tests/load-test/load-test.ts +0 -784
  34. package/k6-tests/package.json +0 -27
  35. package/k6-tests/scripts/run-with-otel.sh +0 -50
  36. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  37. package/k6-tests/tsconfig.json +0 -29
  38. package/k6-tests/utils/client_factory.ts +0 -128
  39. package/k6-tests/utils/common.ts +0 -489
  40. package/k6-tests/utils/logger.ts +0 -62
  41. package/k6-tests/utils/types.d.ts +0 -35
  42. package/malloy_mcp.dxt +0 -0
  43. package/publisher.config.example.bigquery.json +0 -33
  44. package/publisher.config.example.duckdb.json +0 -23
  45. package/publisher.config.json +0 -23
  46. package/scripts/bake-duckdb-extensions.js +0 -110
  47. package/src/config.spec.ts +0 -1535
  48. package/src/config.theme.spec.ts +0 -178
  49. package/src/config.ts +0 -1142
  50. package/src/constants.ts +0 -105
  51. package/src/controller/compile.controller.ts +0 -40
  52. package/src/controller/connection.controller.spec.ts +0 -849
  53. package/src/controller/connection.controller.ts +0 -901
  54. package/src/controller/database.controller.ts +0 -24
  55. package/src/controller/materialization.controller.spec.ts +0 -293
  56. package/src/controller/materialization.controller.ts +0 -319
  57. package/src/controller/model.controller.ts +0 -164
  58. package/src/controller/package.controller.spec.ts +0 -273
  59. package/src/controller/package.controller.ts +0 -306
  60. package/src/controller/query.controller.ts +0 -152
  61. package/src/controller/theme.controller.ts +0 -83
  62. package/src/controller/watch-mode.controller.ts +0 -243
  63. package/src/data_styles.ts +0 -71
  64. package/src/default-publisher.config.json +0 -23
  65. package/src/ducklake_version.spec.ts +0 -163
  66. package/src/ducklake_version.ts +0 -153
  67. package/src/errors.spec.ts +0 -119
  68. package/src/errors.ts +0 -237
  69. package/src/filter_deprecation.spec.ts +0 -64
  70. package/src/filter_deprecation.ts +0 -42
  71. package/src/health.spec.ts +0 -123
  72. package/src/health.ts +0 -233
  73. package/src/heap_check.spec.ts +0 -144
  74. package/src/heap_check.ts +0 -144
  75. package/src/instrumentation.ts +0 -214
  76. package/src/json_utils.spec.ts +0 -51
  77. package/src/json_utils.ts +0 -33
  78. package/src/logger.spec.ts +0 -210
  79. package/src/logger.ts +0 -251
  80. package/src/malloy_pin_prereqs.spec.ts +0 -25
  81. package/src/materialization_metrics.spec.ts +0 -169
  82. package/src/materialization_metrics.ts +0 -346
  83. package/src/mcp/error_messages.spec.ts +0 -35
  84. package/src/mcp/error_messages.ts +0 -183
  85. package/src/mcp/handler_utils.spec.ts +0 -108
  86. package/src/mcp/handler_utils.ts +0 -251
  87. package/src/mcp/mcp_constants.ts +0 -6
  88. package/src/mcp/query_envelope.spec.ts +0 -229
  89. package/src/mcp/query_envelope.ts +0 -240
  90. package/src/mcp/server.protocol.spec.ts +0 -250
  91. package/src/mcp/server.ts +0 -87
  92. package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
  93. package/src/mcp/skills/build_skills_bundle.ts +0 -176
  94. package/src/mcp/skills/skills_bundle.json +0 -1
  95. package/src/mcp/skills/skills_bundle.spec.ts +0 -171
  96. package/src/mcp/tool_response.spec.ts +0 -108
  97. package/src/mcp/tool_response.ts +0 -138
  98. package/src/mcp/tools/compile_tool.spec.ts +0 -315
  99. package/src/mcp/tools/compile_tool.ts +0 -208
  100. package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
  101. package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
  102. package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
  103. package/src/mcp/tools/docs_search_tool.ts +0 -128
  104. package/src/mcp/tools/embedding_index.spec.ts +0 -1236
  105. package/src/mcp/tools/embedding_index.ts +0 -808
  106. package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
  107. package/src/mcp/tools/execute_query_tool.ts +0 -316
  108. package/src/mcp/tools/get_context_eval.ts +0 -275
  109. package/src/mcp/tools/get_context_tool.spec.ts +0 -592
  110. package/src/mcp/tools/get_context_tool.ts +0 -585
  111. package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
  112. package/src/mcp/tools/reload_package_tool.ts +0 -132
  113. package/src/mcp_config.spec.ts +0 -919
  114. package/src/mcp_config.ts +0 -425
  115. package/src/oom_guards.integration.spec.ts +0 -269
  116. package/src/package_load/package_load_pool.spec.ts +0 -252
  117. package/src/package_load/package_load_pool.ts +0 -931
  118. package/src/package_load/package_load_worker.ts +0 -1034
  119. package/src/package_load/protocol.ts +0 -364
  120. package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
  121. package/src/package_load/rpc_wait_accountant.ts +0 -76
  122. package/src/package_load_metrics.spec.ts +0 -114
  123. package/src/package_load_metrics.ts +0 -127
  124. package/src/path_safety.spec.ts +0 -158
  125. package/src/path_safety.ts +0 -146
  126. package/src/pg_helpers.spec.ts +0 -223
  127. package/src/pg_helpers.ts +0 -52
  128. package/src/query_cap_metrics.spec.ts +0 -89
  129. package/src/query_cap_metrics.ts +0 -116
  130. package/src/query_concurrency.spec.ts +0 -247
  131. package/src/query_concurrency.ts +0 -237
  132. package/src/query_metadata_metrics.ts +0 -49
  133. package/src/query_param_utils.ts +0 -29
  134. package/src/query_timeout.spec.ts +0 -224
  135. package/src/query_timeout.ts +0 -179
  136. package/src/runtime/publisher.js +0 -328
  137. package/src/server-old.ts +0 -988
  138. package/src/server.ts +0 -2059
  139. package/src/service/annotations.spec.ts +0 -118
  140. package/src/service/annotations.ts +0 -91
  141. package/src/service/authorize.spec.ts +0 -154
  142. package/src/service/authorize.ts +0 -498
  143. package/src/service/authorize_integration.spec.ts +0 -2041
  144. package/src/service/build_plan.spec.ts +0 -685
  145. package/src/service/build_plan.ts +0 -797
  146. package/src/service/compile_authorize.spec.ts +0 -85
  147. package/src/service/compile_fragment_techniques.spec.ts +0 -156
  148. package/src/service/connection.spec.ts +0 -2360
  149. package/src/service/connection.ts +0 -2187
  150. package/src/service/connection_config.spec.ts +0 -734
  151. package/src/service/connection_config.ts +0 -899
  152. package/src/service/connection_federation.spec.ts +0 -184
  153. package/src/service/connection_fingerprint.spec.ts +0 -102
  154. package/src/service/connection_service.spec.ts +0 -711
  155. package/src/service/connection_service.ts +0 -291
  156. package/src/service/cron_evaluator.spec.ts +0 -79
  157. package/src/service/cron_evaluator.ts +0 -105
  158. package/src/service/db_utils.spec.ts +0 -1085
  159. package/src/service/db_utils.ts +0 -1502
  160. package/src/service/duckdb_instance_isolation.spec.ts +0 -137
  161. package/src/service/ducklake_lazy_attach.spec.ts +0 -110
  162. package/src/service/embedding_provider.spec.ts +0 -329
  163. package/src/service/embedding_provider.ts +0 -236
  164. package/src/service/environment.ts +0 -2191
  165. package/src/service/environment_admission.spec.ts +0 -344
  166. package/src/service/environment_compile.spec.ts +0 -197
  167. package/src/service/environment_store.spec.ts +0 -2274
  168. package/src/service/environment_store.ts +0 -2434
  169. package/src/service/environment_store_anchoring.spec.ts +0 -107
  170. package/src/service/environment_store_clone.spec.ts +0 -350
  171. package/src/service/explore_visibility.spec.ts +0 -434
  172. package/src/service/exports_probe.spec.ts +0 -107
  173. package/src/service/extension_fetch_policy.spec.ts +0 -256
  174. package/src/service/filter.spec.ts +0 -458
  175. package/src/service/filter.ts +0 -341
  176. package/src/service/filter_bypass.spec.ts +0 -418
  177. package/src/service/filter_integration.spec.ts +0 -1004
  178. package/src/service/freshness.spec.ts +0 -183
  179. package/src/service/freshness.ts +0 -112
  180. package/src/service/gcs_s3_utils.ts +0 -427
  181. package/src/service/given.ts +0 -105
  182. package/src/service/givens_integration.spec.ts +0 -219
  183. package/src/service/manifest_loader.spec.ts +0 -189
  184. package/src/service/manifest_loader.ts +0 -156
  185. package/src/service/materialization_build_session.spec.ts +0 -435
  186. package/src/service/materialization_build_session.ts +0 -681
  187. package/src/service/materialization_config_validation.spec.ts +0 -99
  188. package/src/service/materialization_config_validation.ts +0 -120
  189. package/src/service/materialization_eligibility.spec.ts +0 -158
  190. package/src/service/materialization_eligibility.ts +0 -305
  191. package/src/service/materialization_schedule_surface.spec.ts +0 -256
  192. package/src/service/materialization_scheduler.spec.ts +0 -289
  193. package/src/service/materialization_scheduler.ts +0 -304
  194. package/src/service/materialization_serve_transform.spec.ts +0 -1003
  195. package/src/service/materialization_serve_transform.ts +0 -779
  196. package/src/service/materialization_service.spec.ts +0 -2373
  197. package/src/service/materialization_service.ts +0 -2342
  198. package/src/service/materialization_test_fixtures.ts +0 -249
  199. package/src/service/model.spec.ts +0 -1157
  200. package/src/service/model.ts +0 -3370
  201. package/src/service/model_limits.spec.ts +0 -209
  202. package/src/service/model_limits.ts +0 -131
  203. package/src/service/model_storage_serve.spec.ts +0 -193
  204. package/src/service/model_storage_serve_joins.spec.ts +0 -193
  205. package/src/service/package.spec.ts +0 -612
  206. package/src/service/package.ts +0 -1799
  207. package/src/service/package_manifest.spec.ts +0 -230
  208. package/src/service/package_manifest.ts +0 -241
  209. package/src/service/package_memory_governor.spec.ts +0 -173
  210. package/src/service/package_memory_governor.ts +0 -233
  211. package/src/service/package_quote_bound_tables.spec.ts +0 -171
  212. package/src/service/package_race.spec.ts +0 -208
  213. package/src/service/package_reload_safety.spec.ts +0 -193
  214. package/src/service/package_rollback.spec.ts +0 -190
  215. package/src/service/package_worker_path.spec.ts +0 -645
  216. package/src/service/path_injection.spec.ts +0 -39
  217. package/src/service/persist_annotation_validation.spec.ts +0 -89
  218. package/src/service/persist_annotation_validation.ts +0 -59
  219. package/src/service/persistence_policy.spec.ts +0 -374
  220. package/src/service/proxy.spec.ts +0 -414
  221. package/src/service/proxy.ts +0 -248
  222. package/src/service/query_boundary.spec.ts +0 -470
  223. package/src/service/query_metadata.spec.ts +0 -408
  224. package/src/service/query_metadata.ts +0 -492
  225. package/src/service/query_metadata_identity.spec.ts +0 -149
  226. package/src/service/query_text.spec.ts +0 -79
  227. package/src/service/query_text.ts +0 -43
  228. package/src/service/quoting.spec.ts +0 -135
  229. package/src/service/quoting.ts +0 -78
  230. package/src/service/resolve_environment.ts +0 -15
  231. package/src/service/restricted_mode.spec.ts +0 -299
  232. package/src/service/source_extraction.ts +0 -225
  233. package/src/service/theme_store.ts +0 -199
  234. package/src/storage/DatabaseInterface.ts +0 -198
  235. package/src/storage/StorageManager.ts +0 -113
  236. package/src/storage/duckdb/ConnectionRepository.ts +0 -143
  237. package/src/storage/duckdb/DuckDBConnection.ts +0 -123
  238. package/src/storage/duckdb/DuckDBRepository.ts +0 -208
  239. package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
  240. package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
  241. package/src/storage/duckdb/MaterializationRepository.ts +0 -336
  242. package/src/storage/duckdb/PackageRepository.ts +0 -135
  243. package/src/storage/duckdb/schema.ts +0 -229
  244. package/src/stream_helpers.spec.ts +0 -280
  245. package/src/stream_helpers.ts +0 -162
  246. package/src/telemetry.ts +0 -20
  247. package/src/test_helpers/metrics_harness.ts +0 -166
  248. package/src/theme_key_parity.spec.ts +0 -57
  249. package/src/utils.ts +0 -29
  250. package/tests/fixtures/authorize-compile/model.malloy +0 -9
  251. package/tests/fixtures/authorize-compile/publisher.json +0 -4
  252. package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
  253. package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
  254. package/tests/fixtures/html-pages-test/data.csv +0 -3
  255. package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
  256. package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
  257. package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
  258. package/tests/fixtures/html-pages-test/public/data.json +0 -1
  259. package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
  260. package/tests/fixtures/html-pages-test/public/index.html +0 -9
  261. package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
  262. package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
  263. package/tests/fixtures/html-pages-test/public/slides.html +0 -12
  264. package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
  265. package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
  266. package/tests/fixtures/html-pages-test/publisher.json +0 -5
  267. package/tests/fixtures/html-pages-test/report.malloy +0 -1
  268. package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
  269. package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
  270. package/tests/fixtures/persist-multi-level/publisher.json +0 -5
  271. package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
  272. package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
  273. package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
  274. package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
  275. package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
  276. package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
  277. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  278. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  279. package/tests/fixtures/persist-test/publisher.json +0 -5
  280. package/tests/fixtures/publisher.config.json +0 -15
  281. package/tests/fixtures/query-givens/data/orders.csv +0 -7
  282. package/tests/fixtures/query-givens/model.malloy +0 -34
  283. package/tests/fixtures/query-givens/publisher.json +0 -5
  284. package/tests/fixtures/xlsx/database.xlsx +0 -0
  285. package/tests/harness/mcp_test_setup.ts +0 -332
  286. package/tests/harness/rest_e2e.ts +0 -68
  287. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
  288. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
  289. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
  290. package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
  291. package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
  292. package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
  293. package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
  294. package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
  295. package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
  296. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
  297. package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
  298. package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
  299. package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
  300. package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
  301. package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
  302. package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
  303. package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
  304. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
  305. package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
  306. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
  307. package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
  308. package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
  309. package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
  310. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
  311. package/tests/unit/controller/theme.controller.spec.ts +0 -141
  312. package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
  313. package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
  314. package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
  315. package/tests/unit/duckdb/repositories.test.ts +0 -208
  316. package/tests/unit/ducklake/ducklake.test.ts +0 -952
  317. package/tests/unit/service/theme_store.spec.ts +0 -139
  318. package/tsconfig.json +0 -32
package/src/mcp_config.ts DELETED
@@ -1,425 +0,0 @@
1
- /**
2
- * Put this server in the host's MCP config, so an agent opened here finds the
3
- * `malloy_*` tools without being told how.
4
- *
5
- * An MCP client only knows a server exists if something registered it: a
6
- * `.mcp.json` in the directory the session starts in, or a manual
7
- * `claude mcp add`. A running server is otherwise invisible to it, and starting
8
- * the server first does not help, because registration and ordering are separate
9
- * requirements. The repo ships a committed `.mcp.json` and the scaffolder writes
10
- * one, so `npx @malloy-publisher/server` was the only way in that did not
11
- * register itself, which is the only reason getting-started had to teach
12
- * `claude mcp add` before a reader could ask a question.
13
- *
14
- * Creates only, never edits, and never reads. The file may hold other servers
15
- * and their credentials, reading it can block when it is a FIFO, and a boot
16
- * that rewrites it would churn version-controlled files. The scaffolder can
17
- * afford to merge because it runs once at the user's request; a server boot
18
- * cannot.
19
- *
20
- * A symlink named `.mcp.json` is never written through. `wx` (`O_EXCL`) refuses
21
- * one on POSIX whether or not its target exists, but Windows resolves the
22
- * reparse point first, so a DANGLING link there gets its target created. So the
23
- * symlink is rejected explicitly, using the same `lstat` that reports a stale
24
- * config, and `wx` stays as the race-free backstop for everything else.
25
- */
26
- import * as fs from "fs";
27
- import * as os from "os";
28
- import * as path from "path";
29
- import { parseBoolEnv } from "./config";
30
- import { logger } from "./logger";
31
-
32
- export const MCP_CONFIG_FILENAME = ".mcp.json";
33
-
34
- /** What happened, so the caller can log it and tests can assert on it. */
35
- export type McpConfigOutcome =
36
- | {
37
- action: "skipped-home";
38
- dir: string;
39
- endpoint: string;
40
- staleConfig: string | undefined;
41
- }
42
- | {
43
- action: "skipped-git";
44
- dir: string;
45
- /** The directory holding `.git`, which is often far above `dir`. */
46
- gitRoot: string;
47
- /** A `.mcp.json` already up-tree, named so the reader can go look. */
48
- rootConfig: string | undefined;
49
- endpoint: string;
50
- staleConfig: string | undefined;
51
- }
52
- | {
53
- action: "skipped-root";
54
- dir: string;
55
- endpoint: string;
56
- staleConfig: string | undefined;
57
- }
58
- | {
59
- action: "skipped-unstable-port";
60
- dir: string;
61
- endpoint: string;
62
- requestedPort: number;
63
- boundPort: number;
64
- staleConfig: string | undefined;
65
- }
66
- | { action: "created"; file: string }
67
- | { action: "exists"; file: string; endpoint: string }
68
- | { action: "failed"; file: string; problem: string; endpoint: string };
69
-
70
- /** Pinned rather than `Record<string, string>`, so `type` cannot drift. */
71
- type McpServerEntry = { type: "http"; url: string };
72
-
73
- function malloyServer(endpoint: string): McpServerEntry {
74
- return { type: "http", url: endpoint };
75
- }
76
-
77
- /**
78
- * The port to write, given what was asked for and what the OS handed back.
79
- *
80
- * Extracted from the listen callback so it can be tested: this is the piece
81
- * that makes "no ports to know" true, and a silent fallback to the requested
82
- * value would write a config naming a port nothing is on.
83
- */
84
- export function resolveBoundPort(
85
- address: ReturnType<import("net").Server["address"]>,
86
- requestedPort: number,
87
- ): number {
88
- return typeof address === "object" && address ? address.port : requestedPort;
89
- }
90
-
91
- /**
92
- * The host an agent on this machine should dial to reach the bound socket. A
93
- * wildcard becomes the loopback literal of its own family; a specific bind
94
- * address is written as-is, since when the server is bound only to some address
95
- * that address is the only one that reaches it.
96
- *
97
- * Not `localhost`, which is ambiguous: it resolves to both `127.0.0.1` and
98
- * `::1`, while the server binds exactly one family (`0.0.0.0` is the IPv4
99
- * wildcard and does not cover IPv6). With the server on IPv4, `::1` is free, any
100
- * local process can bind the same port there, and clients using `fetch` prefer
101
- * the IPv6 answer, so that process receives the traffic. Reproduced 3 times out
102
- * of 3 before this was changed.
103
- *
104
- * This is unambiguous naming, not isolation. On macOS a process can still bind
105
- * the more specific `127.0.0.1:PORT` while this server holds the wildcard, and
106
- * any attacker able to do that already runs as this user and could rewrite the
107
- * file directly. `displayHost` in `service/environment_store.ts` answers the
108
- * same question with `localhost`, as do the committed root `.mcp.json` and the
109
- * scaffolder. Reconciling all three is its own change: one feeds a documented
110
- * line scripts parse, and another ships from a different package.
111
- */
112
- export function resolveClientHost(
113
- address: ReturnType<import("net").Server["address"]>,
114
- fallbackHost: string,
115
- ): string {
116
- // fallbackHost is the one user-controlled input here (everything else is
117
- // OS-derived), and it is only consulted on a path no runtime reaches today.
118
- // Anything that is not an IP literal is discarded rather than interpolated:
119
- // a hostname would reintroduce the ambiguity this function exists to remove,
120
- // and a quote would break out of the quoting in addCommand, in a line the
121
- // docs tell people to paste.
122
- const usableFallback = /^[0-9a-fA-F:.]+$/.test(fallbackHost)
123
- ? fallbackHost
124
- : "";
125
- const fromSocket =
126
- typeof address === "object" && address ? address.address : undefined;
127
- // Both runtimes populate `address` for a TCP socket. This runs in a listen
128
- // callback where a throw kills an already-bound server, so a missing or
129
- // non-string value falls back rather than throwing, and both no-address
130
- // shapes fall back the same way.
131
- const raw =
132
- typeof fromSocket === "string" && fromSocket
133
- ? fromSocket
134
- : usableFallback;
135
- if (raw === "0.0.0.0" || raw === "") return "127.0.0.1";
136
- if (raw === "::" || raw === "::0") return "[::1]";
137
- if (!raw.includes(":")) return raw;
138
- // Bracket an IPv6 literal so it is a legal URL authority, and drop any zone
139
- // index first: node keeps `%en0` in address() where bun strips it, and no
140
- // HTTP client can dial a zone anyway, so keeping it writes a URL that fails
141
- // to parse rather than one that merely cannot connect.
142
- return `[${raw.split("%")[0]}]`;
143
- }
144
-
145
- /** The URL to write into the config and to print in advice. */
146
- export function mcpEndpoint(host: string, port: number): string {
147
- return `http://${host}:${port}/mcp`;
148
- }
149
-
150
- /**
151
- * Is this directory inside a git working tree?
152
- *
153
- * A checkout belongs to somebody, and dropping an untracked file into one is both
154
- * a surprise in `git status` and a thing that gets committed by accident. A clone
155
- * of this repo is the sharp case: the documented `bun run start` chdirs into
156
- * `packages/server`, so without this the file landed there, untracked, and not at
157
- * the root where a session actually reads it.
158
- *
159
- * This is deliberately blunt: one `.git` anywhere above the working directory is
160
- * enough, so it also covers a user's own project, which is a normal place to run
161
- * the server. That case gets no file, so the caller logs the manual command
162
- * rather than leaving them with nothing.
163
- *
164
- * Walks up rather than shelling out to git, and treats `.git` as present whether
165
- * it is a directory or the file that worktrees and submodules use.
166
- */
167
- function findGitWorkTreeRoot(dir: string): string | undefined {
168
- let current = path.resolve(dir);
169
- for (;;) {
170
- if (fs.existsSync(path.join(current, ".git"))) return current;
171
- const parent = path.dirname(current);
172
- if (parent === current) return undefined;
173
- current = parent;
174
- }
175
- }
176
-
177
- /**
178
- * Is the feature on?
179
- *
180
- * `parseBoolEnv`, not a bespoke set: `config.ts` already owns boolean env
181
- * parsing for this server and throws on an unrecognised value, so a typo in a
182
- * manifest is loud rather than silently flipping a filesystem write. An earlier
183
- * version of this function invented its own spellings, including stripping
184
- * surrounding quotes, which is a problem every flag here would share and so
185
- * belongs in the shared helper if it belongs anywhere.
186
- */
187
- export function mcpConfigEnabled(): boolean {
188
- return !(parseBoolEnv("PUBLISHER_NO_MCP_CONFIG") ?? false);
189
- }
190
-
191
- /**
192
- * Create `<dir>/.mcp.json` if this directory should have one and does not.
193
- *
194
- * Never throws. A convenience file must not be able to stop a server booting, so
195
- * every failure path returns an outcome instead.
196
- */
197
- export function ensureMcpConfig(options: {
198
- dir: string;
199
- /** The URL an agent on this machine should dial, from `mcpEndpoint`. */
200
- endpoint: string;
201
- /**
202
- * What was asked for, and what the OS actually gave. Required as a pair: when
203
- * only one was supplied the guard below silently did nothing, which is the
204
- * defect it exists to prevent.
205
- */
206
- requestedPort: number;
207
- boundPort: number;
208
- /**
209
- * The home directory to stay out of. Defaults to `os.homedir()`; injected by
210
- * tests so they never depend on the developer's real one, where a regression
211
- * in guard ordering would write the file instead of going red.
212
- */
213
- homeDir?: string;
214
- }): McpConfigOutcome {
215
- const { dir, endpoint, requestedPort, boundPort, homeDir } = options;
216
-
217
- const file = path.join(dir, MCP_CONFIG_FILENAME);
218
- try {
219
- // Probed before the guards, not just via EEXIST on the write. Every skip
220
- // returns before writing, so without this a stale config sitting in this
221
- // very directory goes unmentioned in exactly the cases where it is the
222
- // most useful thing to say.
223
- // lstat, not existsSync: they disagree on a dangling symlink, which is
224
- // the case this module's header calls out, and the write path (via
225
- // EEXIST) counts that as present. Reporting and writing must agree.
226
- const existing = (():
227
- | { path: string; isSymlink: boolean }
228
- | undefined => {
229
- try {
230
- return {
231
- path: file,
232
- isSymlink: fs.lstatSync(file).isSymbolicLink(),
233
- };
234
- } catch {
235
- return undefined;
236
- }
237
- })();
238
- const staleConfig = existing?.path;
239
-
240
- // Only write a port that will still be this port next boot. Comparing
241
- // bound against requested catches every way they diverge with one
242
- // condition: `--mcp_port 0` asks for any free port, and a non-numeric
243
- // MCP_PORT becomes NaN, which bun binds ephemerally too (k8s injects
244
- // MCP_PORT=tcp://... into every pod in a namespace with a Service named
245
- // `mcp`). Create-never-edit means the first boot's file is never
246
- // corrected, so a file naming a port that moves is permanently wrong.
247
- if (boundPort !== requestedPort) {
248
- return {
249
- action: "skipped-unstable-port",
250
- dir,
251
- endpoint,
252
- requestedPort,
253
- boundPort,
254
- staleConfig,
255
- };
256
- }
257
- // Inside the try because os.homedir() throws when HOME is unset and the uid
258
- // has no passwd entry, which is the ordinary distroless container shape.
259
- // realpath, not resolve: `resolve` normalises `.` and `..` but not
260
- // symlinks, while process.cwd() is always fully resolved. With HOME on a
261
- // symlinked path (an automounted /home is the common one) the two forms
262
- // never matched and the file landed in the home directory this guard
263
- // exists to protect. Falls back to the unresolved path when it does not
264
- // exist, since realpathSync throws on a missing directory.
265
- const realish = (p: string) => {
266
- try {
267
- return fs.realpathSync(p);
268
- } catch {
269
- return path.resolve(p);
270
- }
271
- };
272
- if (realish(dir) === realish(homeDir ?? os.homedir())) {
273
- return { action: "skipped-home", dir, endpoint, staleConfig };
274
- }
275
- // The filesystem root is the other directory nobody opens an agent in,
276
- // and it is where a process manager puts you by default: systemd gives
277
- // system units a working directory of `/`.
278
- if (path.resolve(dir) === path.parse(path.resolve(dir)).root) {
279
- return { action: "skipped-root", dir, endpoint, staleConfig };
280
- }
281
- const gitRoot = findGitWorkTreeRoot(dir);
282
- if (gitRoot !== undefined) {
283
- // Existence only, never a read. Reported rather than acted on: it tells
284
- // the reader where to look, and cannot tell us whether that file names
285
- // this server or some unrelated one, so it must not decide whether to
286
- // stay quiet.
287
- const rootCandidate = path.join(gitRoot, MCP_CONFIG_FILENAME);
288
- return {
289
- action: "skipped-git",
290
- dir,
291
- gitRoot,
292
- rootConfig: fs.existsSync(rootCandidate)
293
- ? rootCandidate
294
- : undefined,
295
- endpoint,
296
- staleConfig,
297
- };
298
- }
299
-
300
- // Refuse a symlink before attempting the write, rather than relying on
301
- // `wx` to refuse it. `O_EXCL` does that on POSIX whether or not the
302
- // target exists, but Windows resolves the reparse point first, so
303
- // CREATE_NEW on a DANGLING link creates the target: the write-through
304
- // this module exists to prevent, on the one platform where `wx` does not
305
- // prevent it. Caught by the cross-platform CI run, not by reasoning.
306
- // `wx` stays as the race-free backstop for the non-symlink case.
307
- if (existing?.isSymlink) return { action: "exists", file, endpoint };
308
-
309
- const body =
310
- JSON.stringify(
311
- { mcpServers: { malloy: malloyServer(endpoint) } },
312
- null,
313
- 2,
314
- ) + "\n";
315
- fs.writeFileSync(file, body, { encoding: "utf8", flag: "wx" });
316
- return { action: "created", file };
317
- } catch (error) {
318
- const code = (error as NodeJS.ErrnoException)?.code;
319
- // EEXIST covers both an ordinary existing file and a dangling symlink, and
320
- // the response to each is the same: leave it alone, unread.
321
- if (code === "EEXIST") return { action: "exists", file, endpoint };
322
- return {
323
- action: "failed",
324
- file,
325
- problem: error instanceof Error ? error.message : String(error),
326
- endpoint,
327
- };
328
- }
329
- }
330
-
331
- /**
332
- * The command to register this server by hand, at the DEFAULT (local) scope.
333
- *
334
- * Not `-s user`. Claude Code resolves a duplicate name by precedence, local then
335
- * project then user, and takes the winning entry whole rather than merging. Every
336
- * branch that prints this either has a `.mcp.json` in the directory or may have
337
- * one up-tree, and that file is project scope, so a user-scoped registration is
338
- * silently shadowed by the very file that caused the message. Local outranks it
339
- * and works. `-s user` still belongs in the README's separate "register once for
340
- * every directory" instruction, where nothing shadows it.
341
- */
342
- export function addCommand(endpoint: string): string {
343
- // Quoted because an IPv6 endpoint contains brackets, which zsh (the macOS
344
- // default) treats as a glob and refuses outright: `no matches found`. The
345
- // docs tell the reader to paste this line as printed.
346
- return `claude mcp add --transport http malloy '${endpoint}'`;
347
- }
348
-
349
- /**
350
- * Say whether an agent opened here will find this server, and what to do when it
351
- * will not.
352
- *
353
- * Every branch that leaves the user without a file says so at `info`, with the
354
- * endpoint and the command. Noise is preferred to silence on purpose: a skip
355
- * that says nothing is indistinguishable from a broken install, and `debug` is
356
- * not a middle ground, because it disappears entirely at `LOG_LEVEL=info`.
357
- */
358
- /** The stale-file warning, appended to whichever skip fired. */
359
- function staleNote(
360
- staleConfig: string | undefined,
361
- alreadyNamed?: string,
362
- ): string {
363
- // `alreadyNamed` is the git branch's rootConfig: when cwd is the repo root
364
- // the two are the same path, and naming it twice reads like two files.
365
- return staleConfig === undefined || staleConfig === alreadyNamed
366
- ? ""
367
- : `. Note that ${staleConfig} is already here and was not written by this run, so an agent started here uses whatever that names.`;
368
- }
369
-
370
- export function logMcpConfigOutcome(outcome: McpConfigOutcome): void {
371
- switch (outcome.action) {
372
- case "created":
373
- logger.info(
374
- `Wrote ${outcome.file} so an agent started in this directory finds this server. Disable with --no-mcp-config.`,
375
- );
376
- return;
377
- case "exists":
378
- // States the endpoint rather than speculating that the file is wrong.
379
- // This fires on every boot of a scaffolded package, where the file is
380
- // almost always correct, so it must not read as a warning.
381
- logger.info(
382
- `Left the existing ${outcome.file} alone, unread. This server is at ${outcome.endpoint}. If an agent started here reaches a different Publisher than you expect, ask it to run malloy_getContext, which names the environment and packages it is actually talking to. To point it here: ${addCommand(outcome.endpoint)}`,
383
- );
384
- return;
385
- case "failed":
386
- // info, not warn: nothing is broken. The server is serving, and a
387
- // convenience file was not created. On a read-only filesystem this
388
- // fires on every pod of every rollout, where a warning would be picked
389
- // up by alerting and read by someone who cannot act on it.
390
- logger.info(
391
- `Could not write ${outcome.file} (${outcome.problem}). An agent started here will not find this server on its own. To connect one, run: ${addCommand(outcome.endpoint)}`,
392
- );
393
- return;
394
- case "skipped-git":
395
- logger.info(
396
- outcome.rootConfig !== undefined
397
- ? // Named, not acted on: it may register an unrelated server, or
398
- // name a port this run is not on, and we never read it to find
399
- // out. So the endpoint and the command are still given.
400
- `Did not write ${MCP_CONFIG_FILENAME} into ${outcome.dir} because it is inside the git working tree at ${outcome.gitRoot}, which already has ${outcome.rootConfig}. This server is at ${outcome.endpoint}; if your agent does not list malloy, run this from the directory you start it in: ${addCommand(outcome.endpoint)}${staleNote(outcome.staleConfig, outcome.rootConfig)}`
401
- : `Did not write ${MCP_CONFIG_FILENAME} into ${outcome.dir} because it is inside the git working tree at ${outcome.gitRoot}. To connect an agent, run this from the directory you start it in: ${addCommand(outcome.endpoint)}${staleNote(outcome.staleConfig)}`,
402
- );
403
- return;
404
- case "skipped-home":
405
- logger.info(
406
- `Did not write ${MCP_CONFIG_FILENAME} into your home directory (${outcome.dir}). To connect an agent, run this from the directory you start it in: ${addCommand(outcome.endpoint)}${staleNote(outcome.staleConfig)}`,
407
- );
408
- return;
409
- case "skipped-unstable-port":
410
- logger.info(
411
- `Did not write ${MCP_CONFIG_FILENAME}: this server asked for MCP port ${outcome.requestedPort} and got ${outcome.boundPort}, so the port changes from run to run and a saved config would be wrong next boot. To connect an agent to this run: ${addCommand(outcome.endpoint)}. That registration outlives this run, and the port will have moved by the next one, so undo it with claude mcp remove malloy${staleNote(outcome.staleConfig)}`,
412
- );
413
- return;
414
- case "skipped-root":
415
- logger.info(
416
- `Did not write ${MCP_CONFIG_FILENAME} into the filesystem root (${outcome.dir}). To connect an agent, run this from the directory you start it in: ${addCommand(outcome.endpoint)}${staleNote(outcome.staleConfig)}`,
417
- );
418
- return;
419
- default: {
420
- // A new variant must be handled above rather than logging nothing.
421
- const exhaustive: never = outcome;
422
- return exhaustive;
423
- }
424
- }
425
- }