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