@malloy-publisher/server 0.0.235 → 0.0.237

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 (319) hide show
  1. package/dist/app/api-doc.yaml +83 -27
  2. package/dist/app/assets/{EnvironmentPage-BsAnavYN.js → EnvironmentPage-DaP6CpgK.js} +1 -1
  3. package/dist/app/assets/{HomePage-CADE138j.js → HomePage-C9yNw3iu.js} +1 -1
  4. package/dist/app/assets/{LightMode-Cfh7KzN8.js → LightMode-Be4-G-dX.js} +1 -1
  5. package/dist/app/assets/{MainPage-CO3pRlnV.js → MainPage-J_rU2SgI.js} +2 -2
  6. package/dist/app/assets/{MaterializationsPage-p9YjkRXZ.js → MaterializationsPage-DHgPI4MO.js} +1 -1
  7. package/dist/app/assets/ModelPage-BzXfjTu5.js +1 -0
  8. package/dist/app/assets/{PackagePage-e4kN75YR.js → PackagePage-ByoSTLJ4.js} +1 -1
  9. package/dist/app/assets/RouteError-CrSeKN2r.js +1 -0
  10. package/dist/app/assets/{ThemeEditorPage-CciagFTq.js → ThemeEditorPage-Btxnmhtk.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-DNWmkCXa.js → WorkbookPage-D6JLPnWO.js} +1 -1
  12. package/dist/app/assets/{core-Rj_4rRnA.es-BZyvITuO.js → core-BSRgt1mG.es-Bc_D2Lq4.js} +1 -1
  13. package/dist/app/assets/{index-CH2AcDzc.js → index-BRijWj6K.js} +15 -15
  14. package/dist/app/assets/{index-DQa463gC.js → index-BX4D94xw.js} +1 -1
  15. package/dist/app/assets/{index-VBbcc8s6.js → index-Cvy0xtb7.js} +1 -1
  16. package/dist/app/assets/{index-DQpV7MyA.js → index-DMAhIJNC.js} +1 -1
  17. package/dist/app/index.html +1 -1
  18. package/dist/package_load_worker.mjs +59 -20
  19. package/dist/server.mjs +1342 -616
  20. package/package.json +8 -2
  21. package/.eslintrc.json +0 -44
  22. package/.prettierignore +0 -1
  23. package/.prettierrc +0 -4
  24. package/build.ts +0 -97
  25. package/dist/app/assets/ModelPage-C1OSTv-x.js +0 -1
  26. package/dist/app/assets/RouteError-CzbfOkng.js +0 -1
  27. package/eslint.config.mjs +0 -76
  28. package/k6-tests/README.md +0 -144
  29. package/k6-tests/bun.lock +0 -797
  30. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  31. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  32. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  33. package/k6-tests/load-test/load-test-crud.ts +0 -142
  34. package/k6-tests/load-test/load-test.ts +0 -784
  35. package/k6-tests/package.json +0 -27
  36. package/k6-tests/scripts/run-with-otel.sh +0 -50
  37. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  38. package/k6-tests/tsconfig.json +0 -29
  39. package/k6-tests/utils/client_factory.ts +0 -128
  40. package/k6-tests/utils/common.ts +0 -489
  41. package/k6-tests/utils/logger.ts +0 -62
  42. package/k6-tests/utils/types.d.ts +0 -35
  43. package/malloy_mcp.dxt +0 -0
  44. package/publisher.config.example.bigquery.json +0 -33
  45. package/publisher.config.example.duckdb.json +0 -23
  46. package/publisher.config.json +0 -23
  47. package/scripts/bake-duckdb-extensions.js +0 -110
  48. package/src/config.spec.ts +0 -1535
  49. package/src/config.theme.spec.ts +0 -178
  50. package/src/config.ts +0 -1142
  51. package/src/constants.ts +0 -105
  52. package/src/controller/compile.controller.ts +0 -40
  53. package/src/controller/connection.controller.spec.ts +0 -849
  54. package/src/controller/connection.controller.ts +0 -901
  55. package/src/controller/database.controller.ts +0 -24
  56. package/src/controller/materialization.controller.spec.ts +0 -293
  57. package/src/controller/materialization.controller.ts +0 -319
  58. package/src/controller/model.controller.ts +0 -164
  59. package/src/controller/package.controller.spec.ts +0 -273
  60. package/src/controller/package.controller.ts +0 -306
  61. package/src/controller/query.controller.ts +0 -152
  62. package/src/controller/theme.controller.ts +0 -83
  63. package/src/controller/watch-mode.controller.ts +0 -243
  64. package/src/data_styles.ts +0 -71
  65. package/src/default-publisher.config.json +0 -23
  66. package/src/ducklake_version.spec.ts +0 -163
  67. package/src/ducklake_version.ts +0 -153
  68. package/src/errors.spec.ts +0 -119
  69. package/src/errors.ts +0 -237
  70. package/src/filter_deprecation.spec.ts +0 -64
  71. package/src/filter_deprecation.ts +0 -42
  72. package/src/health.spec.ts +0 -123
  73. package/src/health.ts +0 -233
  74. package/src/heap_check.spec.ts +0 -144
  75. package/src/heap_check.ts +0 -144
  76. package/src/instrumentation.ts +0 -214
  77. package/src/json_utils.spec.ts +0 -51
  78. package/src/json_utils.ts +0 -33
  79. package/src/logger.spec.ts +0 -210
  80. package/src/logger.ts +0 -251
  81. package/src/malloy_pin_prereqs.spec.ts +0 -25
  82. package/src/materialization_metrics.spec.ts +0 -169
  83. package/src/materialization_metrics.ts +0 -346
  84. package/src/mcp/error_messages.spec.ts +0 -35
  85. package/src/mcp/error_messages.ts +0 -183
  86. package/src/mcp/handler_utils.spec.ts +0 -108
  87. package/src/mcp/handler_utils.ts +0 -251
  88. package/src/mcp/mcp_constants.ts +0 -6
  89. package/src/mcp/query_envelope.spec.ts +0 -229
  90. package/src/mcp/query_envelope.ts +0 -240
  91. package/src/mcp/server.protocol.spec.ts +0 -250
  92. package/src/mcp/server.ts +0 -87
  93. package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
  94. package/src/mcp/skills/build_skills_bundle.ts +0 -176
  95. package/src/mcp/skills/skills_bundle.json +0 -1
  96. package/src/mcp/skills/skills_bundle.spec.ts +0 -171
  97. package/src/mcp/tool_response.spec.ts +0 -108
  98. package/src/mcp/tool_response.ts +0 -138
  99. package/src/mcp/tools/compile_tool.spec.ts +0 -315
  100. package/src/mcp/tools/compile_tool.ts +0 -208
  101. package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
  102. package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
  103. package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
  104. package/src/mcp/tools/docs_search_tool.ts +0 -128
  105. package/src/mcp/tools/embedding_index.spec.ts +0 -1236
  106. package/src/mcp/tools/embedding_index.ts +0 -808
  107. package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
  108. package/src/mcp/tools/execute_query_tool.ts +0 -316
  109. package/src/mcp/tools/get_context_eval.ts +0 -275
  110. package/src/mcp/tools/get_context_tool.spec.ts +0 -592
  111. package/src/mcp/tools/get_context_tool.ts +0 -585
  112. package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
  113. package/src/mcp/tools/reload_package_tool.ts +0 -132
  114. package/src/mcp_config.spec.ts +0 -919
  115. package/src/mcp_config.ts +0 -425
  116. package/src/oom_guards.integration.spec.ts +0 -269
  117. package/src/package_load/package_load_pool.spec.ts +0 -252
  118. package/src/package_load/package_load_pool.ts +0 -931
  119. package/src/package_load/package_load_worker.ts +0 -1034
  120. package/src/package_load/protocol.ts +0 -364
  121. package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
  122. package/src/package_load/rpc_wait_accountant.ts +0 -76
  123. package/src/package_load_metrics.spec.ts +0 -114
  124. package/src/package_load_metrics.ts +0 -127
  125. package/src/path_safety.spec.ts +0 -158
  126. package/src/path_safety.ts +0 -146
  127. package/src/pg_helpers.spec.ts +0 -223
  128. package/src/pg_helpers.ts +0 -52
  129. package/src/query_cap_metrics.spec.ts +0 -89
  130. package/src/query_cap_metrics.ts +0 -116
  131. package/src/query_concurrency.spec.ts +0 -247
  132. package/src/query_concurrency.ts +0 -237
  133. package/src/query_metadata_metrics.ts +0 -49
  134. package/src/query_param_utils.ts +0 -29
  135. package/src/query_timeout.spec.ts +0 -224
  136. package/src/query_timeout.ts +0 -179
  137. package/src/runtime/publisher.js +0 -328
  138. package/src/server-old.ts +0 -988
  139. package/src/server.ts +0 -2059
  140. package/src/service/annotations.spec.ts +0 -118
  141. package/src/service/annotations.ts +0 -91
  142. package/src/service/authorize.spec.ts +0 -154
  143. package/src/service/authorize.ts +0 -498
  144. package/src/service/authorize_integration.spec.ts +0 -2041
  145. package/src/service/build_plan.spec.ts +0 -685
  146. package/src/service/build_plan.ts +0 -797
  147. package/src/service/compile_authorize.spec.ts +0 -85
  148. package/src/service/compile_fragment_techniques.spec.ts +0 -156
  149. package/src/service/connection.spec.ts +0 -2360
  150. package/src/service/connection.ts +0 -2187
  151. package/src/service/connection_config.spec.ts +0 -734
  152. package/src/service/connection_config.ts +0 -899
  153. package/src/service/connection_federation.spec.ts +0 -184
  154. package/src/service/connection_fingerprint.spec.ts +0 -102
  155. package/src/service/connection_service.spec.ts +0 -711
  156. package/src/service/connection_service.ts +0 -291
  157. package/src/service/cron_evaluator.spec.ts +0 -79
  158. package/src/service/cron_evaluator.ts +0 -105
  159. package/src/service/db_utils.spec.ts +0 -1085
  160. package/src/service/db_utils.ts +0 -1502
  161. package/src/service/duckdb_instance_isolation.spec.ts +0 -137
  162. package/src/service/ducklake_lazy_attach.spec.ts +0 -110
  163. package/src/service/embedding_provider.spec.ts +0 -329
  164. package/src/service/embedding_provider.ts +0 -236
  165. package/src/service/environment.ts +0 -2191
  166. package/src/service/environment_admission.spec.ts +0 -344
  167. package/src/service/environment_compile.spec.ts +0 -197
  168. package/src/service/environment_store.spec.ts +0 -2274
  169. package/src/service/environment_store.ts +0 -2434
  170. package/src/service/environment_store_anchoring.spec.ts +0 -107
  171. package/src/service/environment_store_clone.spec.ts +0 -350
  172. package/src/service/explore_visibility.spec.ts +0 -434
  173. package/src/service/exports_probe.spec.ts +0 -107
  174. package/src/service/extension_fetch_policy.spec.ts +0 -256
  175. package/src/service/filter.spec.ts +0 -458
  176. package/src/service/filter.ts +0 -341
  177. package/src/service/filter_bypass.spec.ts +0 -418
  178. package/src/service/filter_integration.spec.ts +0 -1004
  179. package/src/service/freshness.spec.ts +0 -183
  180. package/src/service/freshness.ts +0 -112
  181. package/src/service/gcs_s3_utils.ts +0 -427
  182. package/src/service/given.ts +0 -105
  183. package/src/service/givens_integration.spec.ts +0 -219
  184. package/src/service/manifest_loader.spec.ts +0 -189
  185. package/src/service/manifest_loader.ts +0 -156
  186. package/src/service/materialization_build_session.spec.ts +0 -435
  187. package/src/service/materialization_build_session.ts +0 -681
  188. package/src/service/materialization_config_validation.spec.ts +0 -99
  189. package/src/service/materialization_config_validation.ts +0 -120
  190. package/src/service/materialization_eligibility.spec.ts +0 -158
  191. package/src/service/materialization_eligibility.ts +0 -305
  192. package/src/service/materialization_schedule_surface.spec.ts +0 -256
  193. package/src/service/materialization_scheduler.spec.ts +0 -289
  194. package/src/service/materialization_scheduler.ts +0 -304
  195. package/src/service/materialization_serve_transform.spec.ts +0 -1003
  196. package/src/service/materialization_serve_transform.ts +0 -779
  197. package/src/service/materialization_service.spec.ts +0 -2373
  198. package/src/service/materialization_service.ts +0 -2342
  199. package/src/service/materialization_test_fixtures.ts +0 -249
  200. package/src/service/model.spec.ts +0 -1157
  201. package/src/service/model.ts +0 -3370
  202. package/src/service/model_limits.spec.ts +0 -209
  203. package/src/service/model_limits.ts +0 -131
  204. package/src/service/model_storage_serve.spec.ts +0 -193
  205. package/src/service/model_storage_serve_joins.spec.ts +0 -193
  206. package/src/service/package.spec.ts +0 -612
  207. package/src/service/package.ts +0 -1799
  208. package/src/service/package_manifest.spec.ts +0 -230
  209. package/src/service/package_manifest.ts +0 -241
  210. package/src/service/package_memory_governor.spec.ts +0 -173
  211. package/src/service/package_memory_governor.ts +0 -233
  212. package/src/service/package_quote_bound_tables.spec.ts +0 -171
  213. package/src/service/package_race.spec.ts +0 -208
  214. package/src/service/package_reload_safety.spec.ts +0 -193
  215. package/src/service/package_rollback.spec.ts +0 -190
  216. package/src/service/package_worker_path.spec.ts +0 -645
  217. package/src/service/path_injection.spec.ts +0 -39
  218. package/src/service/persist_annotation_validation.spec.ts +0 -89
  219. package/src/service/persist_annotation_validation.ts +0 -59
  220. package/src/service/persistence_policy.spec.ts +0 -374
  221. package/src/service/proxy.spec.ts +0 -414
  222. package/src/service/proxy.ts +0 -248
  223. package/src/service/query_boundary.spec.ts +0 -470
  224. package/src/service/query_metadata.spec.ts +0 -408
  225. package/src/service/query_metadata.ts +0 -492
  226. package/src/service/query_metadata_identity.spec.ts +0 -149
  227. package/src/service/query_text.spec.ts +0 -79
  228. package/src/service/query_text.ts +0 -43
  229. package/src/service/quoting.spec.ts +0 -135
  230. package/src/service/quoting.ts +0 -78
  231. package/src/service/resolve_environment.ts +0 -15
  232. package/src/service/restricted_mode.spec.ts +0 -299
  233. package/src/service/source_extraction.ts +0 -225
  234. package/src/service/theme_store.ts +0 -199
  235. package/src/storage/DatabaseInterface.ts +0 -198
  236. package/src/storage/StorageManager.ts +0 -113
  237. package/src/storage/duckdb/ConnectionRepository.ts +0 -143
  238. package/src/storage/duckdb/DuckDBConnection.ts +0 -123
  239. package/src/storage/duckdb/DuckDBRepository.ts +0 -208
  240. package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
  241. package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
  242. package/src/storage/duckdb/MaterializationRepository.ts +0 -336
  243. package/src/storage/duckdb/PackageRepository.ts +0 -135
  244. package/src/storage/duckdb/schema.ts +0 -229
  245. package/src/stream_helpers.spec.ts +0 -280
  246. package/src/stream_helpers.ts +0 -162
  247. package/src/telemetry.ts +0 -20
  248. package/src/test_helpers/metrics_harness.ts +0 -166
  249. package/src/theme_key_parity.spec.ts +0 -57
  250. package/src/utils.ts +0 -29
  251. package/tests/fixtures/authorize-compile/model.malloy +0 -9
  252. package/tests/fixtures/authorize-compile/publisher.json +0 -4
  253. package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
  254. package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
  255. package/tests/fixtures/html-pages-test/data.csv +0 -3
  256. package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
  257. package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
  258. package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
  259. package/tests/fixtures/html-pages-test/public/data.json +0 -1
  260. package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
  261. package/tests/fixtures/html-pages-test/public/index.html +0 -9
  262. package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
  263. package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
  264. package/tests/fixtures/html-pages-test/public/slides.html +0 -12
  265. package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
  266. package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
  267. package/tests/fixtures/html-pages-test/publisher.json +0 -5
  268. package/tests/fixtures/html-pages-test/report.malloy +0 -1
  269. package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
  270. package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
  271. package/tests/fixtures/persist-multi-level/publisher.json +0 -5
  272. package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
  273. package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
  274. package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
  275. package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
  276. package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
  277. package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
  278. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  279. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  280. package/tests/fixtures/persist-test/publisher.json +0 -5
  281. package/tests/fixtures/publisher.config.json +0 -15
  282. package/tests/fixtures/query-givens/data/orders.csv +0 -7
  283. package/tests/fixtures/query-givens/model.malloy +0 -34
  284. package/tests/fixtures/query-givens/publisher.json +0 -5
  285. package/tests/fixtures/xlsx/database.xlsx +0 -0
  286. package/tests/harness/mcp_test_setup.ts +0 -332
  287. package/tests/harness/rest_e2e.ts +0 -68
  288. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
  289. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
  290. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
  291. package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
  292. package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
  293. package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
  294. package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
  295. package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
  296. package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
  297. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
  298. package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
  299. package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
  300. package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
  301. package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
  302. package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
  303. package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
  304. package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
  305. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
  306. package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
  307. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
  308. package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
  309. package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
  310. package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
  311. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
  312. package/tests/unit/controller/theme.controller.spec.ts +0 -141
  313. package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
  314. package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
  315. package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
  316. package/tests/unit/duckdb/repositories.test.ts +0 -208
  317. package/tests/unit/ducklake/ducklake.test.ts +0 -952
  318. package/tests/unit/service/theme_store.spec.ts +0 -139
  319. 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
- }