@malloy-publisher/server 0.0.382-dev → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.docker.md +145 -20
  2. package/README.md +65 -12
  3. package/dist/app/api-doc.html +5 -0
  4. package/dist/app/api-doc.yaml +3903 -583
  5. package/dist/app/assets/EnvironmentPage-DiUgLuCH.js +1 -0
  6. package/dist/app/assets/HomePage-MhhjhnFj.js +1 -0
  7. package/dist/app/assets/LightMode-BEyvNUzT.js +1 -0
  8. package/dist/app/assets/MainPage-D4Cfee9o.js +2 -0
  9. package/dist/app/assets/MaterializationsPage-nGgGqwSN.js +1 -0
  10. package/dist/app/assets/ModelPage-DLAcEw-F.js +1 -0
  11. package/dist/app/assets/PackagePage-Df0ikH7w.js +1 -0
  12. package/dist/app/assets/RouteError-BFQwtroH.js +1 -0
  13. package/dist/app/assets/ThemeEditorPage-OoskoQYN.js +1 -0
  14. package/dist/app/assets/WorkbookPage-CpFgvUDC.js +1 -0
  15. package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-lb28vQkR.es-BoVSOc75.js} +1 -1
  16. package/dist/app/assets/github-dark-DenFmJkN.es-DHJKELXO.js +1 -0
  17. package/dist/app/assets/index-5eLCcNmP.css +1 -0
  18. package/dist/app/assets/index-6ypuFH6g.js +1774 -0
  19. package/dist/app/assets/index-B8pnU_V_.js +615 -0
  20. package/dist/app/assets/index-BImcHmVP.js +1 -0
  21. package/dist/app/assets/index-DSoCUF-f.js +2725 -0
  22. package/dist/app/assets/index-DfqzSItA.js +2 -0
  23. package/dist/app/assets/index-w_0OQJgZ.js +23 -0
  24. package/dist/app/index.html +8 -4
  25. package/dist/cpufeatures-1yrn0vtw.node +0 -0
  26. package/dist/default-publisher.config.json +23 -0
  27. package/dist/{instrumentation.js → instrumentation.mjs} +10631 -10573
  28. package/dist/package_load_worker.mjs +15219 -0
  29. package/dist/runtime/publisher.js +331 -0
  30. package/dist/{server.js → server.mjs} +115548 -49590
  31. package/dist/sshcrypto-vd2k5hq9.node +0 -0
  32. package/package.json +41 -28
  33. package/publisher.config.example.bigquery.json +33 -0
  34. package/publisher.config.example.duckdb.json +23 -0
  35. package/.eslintrc.json +0 -44
  36. package/.prettierignore +0 -1
  37. package/.prettierrc +0 -4
  38. package/build.ts +0 -38
  39. package/dist/app/assets/HomePage-Dn3E4CuB.js +0 -1
  40. package/dist/app/assets/MainPage-BzB3yoqi.js +0 -2
  41. package/dist/app/assets/ModelPage-C9O_sAXT.js +0 -1
  42. package/dist/app/assets/PackagePage-DcxKEjBX.js +0 -1
  43. package/dist/app/assets/ProjectPage-BDj307rF.js +0 -1
  44. package/dist/app/assets/RouteError-DAShbVCG.js +0 -1
  45. package/dist/app/assets/WorkbookPage-Cs_XYEaB.js +0 -1
  46. package/dist/app/assets/index-15BOvhp0.js +0 -456
  47. package/dist/app/assets/index-Bb2jqquW.js +0 -676
  48. package/dist/app/assets/index-CMlGQMcl.css +0 -1
  49. package/dist/app/assets/index-D68X76-7.js +0 -1276
  50. package/dist/app/assets/index.umd-DGBekgSu.js +0 -1145
  51. package/dxt/malloy_bridge.py +0 -354
  52. package/dxt/manifest.json +0 -22
  53. package/eslint.config.mjs +0 -76
  54. package/k6-tests/README.md +0 -144
  55. package/k6-tests/bun.lock +0 -797
  56. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  57. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  58. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  59. package/k6-tests/load-test/load-test-crud.ts +0 -142
  60. package/k6-tests/load-test/load-test.ts +0 -784
  61. package/k6-tests/package.json +0 -27
  62. package/k6-tests/scripts/run-with-otel.sh +0 -50
  63. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  64. package/k6-tests/tsconfig.json +0 -29
  65. package/k6-tests/utils/client_factory.ts +0 -128
  66. package/k6-tests/utils/common.ts +0 -489
  67. package/k6-tests/utils/logger.ts +0 -62
  68. package/k6-tests/utils/types.d.ts +0 -35
  69. package/malloy_mcp.dxt +0 -0
  70. package/publisher.config.json +0 -33
  71. package/src/config.spec.ts +0 -850
  72. package/src/config.ts +0 -305
  73. package/src/constants.ts +0 -10
  74. package/src/controller/compile.controller.ts +0 -35
  75. package/src/controller/connection.controller.ts +0 -515
  76. package/src/controller/database.controller.ts +0 -21
  77. package/src/controller/manifest.controller.ts +0 -29
  78. package/src/controller/materialization.controller.ts +0 -125
  79. package/src/controller/model.controller.ts +0 -107
  80. package/src/controller/package.controller.ts +0 -182
  81. package/src/controller/query.controller.ts +0 -60
  82. package/src/controller/watch-mode.controller.ts +0 -98
  83. package/src/data_styles.ts +0 -71
  84. package/src/dto/connection.dto.spec.ts +0 -131
  85. package/src/dto/connection.dto.ts +0 -174
  86. package/src/dto/index.ts +0 -2
  87. package/src/dto/package.dto.spec.ts +0 -42
  88. package/src/dto/package.dto.ts +0 -12
  89. package/src/dto/validate.spec.ts +0 -76
  90. package/src/dto/validate.ts +0 -31
  91. package/src/errors.ts +0 -116
  92. package/src/health.ts +0 -189
  93. package/src/instrumentation.ts +0 -148
  94. package/src/logger.ts +0 -154
  95. package/src/mcp/error_messages.ts +0 -199
  96. package/src/mcp/handler_utils.ts +0 -239
  97. package/src/mcp/mcp_constants.ts +0 -22
  98. package/src/mcp/prompts/handlers.ts +0 -84
  99. package/src/mcp/prompts/index.ts +0 -11
  100. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  101. package/src/mcp/prompts/prompt_service.ts +0 -67
  102. package/src/mcp/prompts/utils.ts +0 -62
  103. package/src/mcp/resource_metadata.ts +0 -47
  104. package/src/mcp/resources/model_resource.ts +0 -153
  105. package/src/mcp/resources/notebook_resource.ts +0 -137
  106. package/src/mcp/resources/package_resource.ts +0 -370
  107. package/src/mcp/resources/project_resource.ts +0 -184
  108. package/src/mcp/resources/query_resource.ts +0 -117
  109. package/src/mcp/resources/source_resource.ts +0 -141
  110. package/src/mcp/resources/view_resource.ts +0 -136
  111. package/src/mcp/server.ts +0 -62
  112. package/src/mcp/tools/discovery_tools.ts +0 -258
  113. package/src/mcp/tools/execute_query_tool.ts +0 -265
  114. package/src/server.ts +0 -1195
  115. package/src/service/connection.spec.ts +0 -1331
  116. package/src/service/connection.ts +0 -1511
  117. package/src/service/connection_service.spec.ts +0 -641
  118. package/src/service/connection_service.ts +0 -181
  119. package/src/service/db_utils.spec.ts +0 -714
  120. package/src/service/db_utils.ts +0 -1227
  121. package/src/service/filter.spec.ts +0 -447
  122. package/src/service/filter.ts +0 -337
  123. package/src/service/filter_integration.spec.ts +0 -825
  124. package/src/service/gcs_s3_utils.ts +0 -427
  125. package/src/service/manifest_service.spec.ts +0 -201
  126. package/src/service/manifest_service.ts +0 -106
  127. package/src/service/materialization_service.spec.ts +0 -648
  128. package/src/service/materialization_service.ts +0 -929
  129. package/src/service/materialized_table_gc.spec.ts +0 -383
  130. package/src/service/materialized_table_gc.ts +0 -279
  131. package/src/service/model.spec.ts +0 -257
  132. package/src/service/model.ts +0 -1069
  133. package/src/service/package.spec.ts +0 -236
  134. package/src/service/package.ts +0 -463
  135. package/src/service/project.ts +0 -681
  136. package/src/service/project_compile.spec.ts +0 -197
  137. package/src/service/project_store.spec.ts +0 -859
  138. package/src/service/project_store.ts +0 -1531
  139. package/src/service/quoting.ts +0 -41
  140. package/src/service/resolve_project.ts +0 -13
  141. package/src/storage/BaseRepository.ts +0 -31
  142. package/src/storage/DatabaseInterface.ts +0 -180
  143. package/src/storage/StorageManager.mock.ts +0 -59
  144. package/src/storage/StorageManager.ts +0 -210
  145. package/src/storage/duckdb/ConnectionRepository.ts +0 -141
  146. package/src/storage/duckdb/DuckDBConnection.ts +0 -177
  147. package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
  148. package/src/storage/duckdb/DuckDBRepository.ts +0 -224
  149. package/src/storage/duckdb/ManifestRepository.ts +0 -119
  150. package/src/storage/duckdb/MaterializationRepository.ts +0 -249
  151. package/src/storage/duckdb/PackageRepository.ts +0 -135
  152. package/src/storage/duckdb/ProjectRepository.ts +0 -148
  153. package/src/storage/duckdb/manifest_store.spec.ts +0 -133
  154. package/src/storage/duckdb/schema.ts +0 -147
  155. package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -146
  156. package/src/utils.ts +0 -13
  157. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  158. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  159. package/tests/fixtures/persist-test/publisher.json +0 -5
  160. package/tests/fixtures/publisher.config.json +0 -15
  161. package/tests/harness/e2e.ts +0 -96
  162. package/tests/harness/mcp_test_setup.ts +0 -344
  163. package/tests/harness/mocks.ts +0 -37
  164. package/tests/harness/rest_e2e.ts +0 -68
  165. package/tests/harness/uris.ts +0 -31
  166. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -470
  167. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -434
  168. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -663
  169. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -122
  170. package/tests/integration/mcp/setup.spec.ts +0 -5
  171. package/tests/unit/duckdb/attached_databases.test.ts +0 -1132
  172. package/tests/unit/ducklake/ducklake.test.ts +0 -950
  173. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  174. package/tests/unit/mcp/prompt_happy.test.ts +0 -51
  175. package/tsconfig.json +0 -32
@@ -1,147 +0,0 @@
1
- import { logger } from "../../logger";
2
- import { DuckDBConnection } from "./DuckDBConnection";
3
-
4
- export async function initializeSchema(
5
- db: DuckDBConnection,
6
- force: boolean = false,
7
- ): Promise<void> {
8
- const initialized = await db.isInitialized();
9
-
10
- if (initialized && !force) {
11
- return;
12
- }
13
-
14
- if (force) {
15
- logger.info(
16
- "Reinitializing database schema dropping and recreating all tables",
17
- );
18
- await dropAllTables(db);
19
- } else {
20
- logger.info("Creating database schema for the first time...");
21
- }
22
-
23
- // Projects table
24
- await db.run(`
25
- CREATE TABLE IF NOT EXISTS projects (
26
- id VARCHAR PRIMARY KEY,
27
- name VARCHAR NOT NULL UNIQUE,
28
- path VARCHAR NOT NULL,
29
- description VARCHAR,
30
- metadata JSON,
31
- created_at TIMESTAMP NOT NULL,
32
- updated_at TIMESTAMP NOT NULL
33
- )
34
- `);
35
-
36
- // Packages table
37
- await db.run(`
38
- CREATE TABLE IF NOT EXISTS packages (
39
- id VARCHAR PRIMARY KEY,
40
- project_id VARCHAR NOT NULL,
41
- name VARCHAR NOT NULL,
42
- description VARCHAR,
43
- manifest_path VARCHAR NOT NULL,
44
- metadata JSON,
45
- created_at TIMESTAMP NOT NULL,
46
- updated_at TIMESTAMP NOT NULL,
47
- FOREIGN KEY (project_id) REFERENCES projects(id),
48
- UNIQUE (project_id, name)
49
- )
50
- `);
51
-
52
- // Connections table
53
- await db.run(`
54
- CREATE TABLE IF NOT EXISTS connections (
55
- id VARCHAR PRIMARY KEY,
56
- project_id VARCHAR NOT NULL,
57
- name VARCHAR NOT NULL,
58
- type VARCHAR NOT NULL,
59
- config JSON NOT NULL,
60
- created_at TIMESTAMP NOT NULL,
61
- updated_at TIMESTAMP NOT NULL,
62
- FOREIGN KEY (project_id) REFERENCES projects(id),
63
- UNIQUE (project_id, name)
64
- )
65
- `);
66
-
67
- // Materializations table.
68
- //
69
- // `active_key` enforces at-most-one active (PENDING or RUNNING)
70
- // materialization per (project, package) at the DB layer. It is set to
71
- // `{project_id}|{package_name}` while the row is active and cleared
72
- // to NULL on transition to any terminal state. A unique index on
73
- // `active_key` (see below) makes the insert-then-check race impossible —
74
- // a second concurrent create fails with a constraint violation, which the
75
- // service layer translates to `MaterializationConflictError`.
76
- await db.run(`
77
- CREATE TABLE IF NOT EXISTS materializations (
78
- id VARCHAR PRIMARY KEY,
79
- project_id VARCHAR NOT NULL,
80
- package_name VARCHAR NOT NULL,
81
- status VARCHAR NOT NULL,
82
- active_key VARCHAR,
83
- started_at TIMESTAMP,
84
- completed_at TIMESTAMP,
85
- error TEXT,
86
- metadata JSON,
87
- created_at TIMESTAMP NOT NULL,
88
- updated_at TIMESTAMP NOT NULL,
89
- FOREIGN KEY (project_id) REFERENCES projects(id)
90
- )
91
- `);
92
-
93
- // Build manifests table
94
- await db.run(`
95
- CREATE TABLE IF NOT EXISTS build_manifests (
96
- id VARCHAR PRIMARY KEY,
97
- project_id VARCHAR NOT NULL,
98
- package_name VARCHAR NOT NULL,
99
- build_id VARCHAR NOT NULL,
100
- table_name VARCHAR NOT NULL,
101
- source_name VARCHAR NOT NULL,
102
- connection_name VARCHAR NOT NULL,
103
- created_at TIMESTAMP NOT NULL,
104
- updated_at TIMESTAMP NOT NULL,
105
- FOREIGN KEY (project_id) REFERENCES projects(id),
106
- UNIQUE (project_id, package_name, build_id)
107
- )
108
- `);
109
-
110
- // Create indexes for better query performance
111
- await db.run(
112
- "CREATE INDEX IF NOT EXISTS idx_packages_project_id ON packages(project_id)",
113
- );
114
- await db.run(
115
- "CREATE INDEX IF NOT EXISTS idx_connections_project_id ON connections(project_id)",
116
- );
117
- await db.run(
118
- "CREATE INDEX IF NOT EXISTS idx_materializations_project_package ON materializations(project_id, package_name)",
119
- );
120
- await db.run(
121
- "CREATE UNIQUE INDEX IF NOT EXISTS idx_materializations_active_key ON materializations(active_key)",
122
- );
123
- await db.run(
124
- "CREATE INDEX IF NOT EXISTS idx_build_manifests_project_package ON build_manifests(project_id, package_name)",
125
- );
126
- }
127
-
128
- async function dropAllTables(db: DuckDBConnection): Promise<void> {
129
- const tables = [
130
- "build_manifests",
131
- "materializations",
132
- "packages",
133
- "connections",
134
- "projects",
135
- ];
136
-
137
- logger.info("Dropping tables:", tables.join(", "));
138
-
139
- for (const table of tables) {
140
- try {
141
- await db.run(`DROP TABLE IF EXISTS ${table} `);
142
- logger.info(`Dropped table: ${table}`);
143
- } catch (err) {
144
- logger.warn(` Warning: Could not drop table ${table}:`, err);
145
- }
146
- }
147
- }
@@ -1,146 +0,0 @@
1
- import { logger } from "../../logger";
2
- import {
3
- BuildManifest,
4
- ManifestEntry,
5
- ManifestStore,
6
- } from "../DatabaseInterface";
7
- import { DuckDBConnection } from "../duckdb/DuckDBConnection";
8
-
9
- /**
10
- * DuckLake-backed ManifestStore used in orchestrated mode.
11
- *
12
- * Reads and writes manifest entries through a DuckLake catalog attached to
13
- * the publisher's internal DuckDB. All workers sharing the same DuckLake
14
- * catalog see the same manifest, enabling multi-worker coordination.
15
- *
16
- * The catalog is attached by {@link StorageManager} before this store is
17
- * instantiated. The first worker to call {@link bootstrapSchema} creates the
18
- * `build_manifests` table idempotently. Schema ownership lives in the
19
- * publisher so that DDL and code co-evolve in the same repo.
20
- *
21
- * **Scope: manifest sync only.** Orchestrated mode only shares manifest
22
- * state across workers. The active-materialization lock (the unique index
23
- * on `materializations.active_key`) still lives in each worker's local
24
- * DuckDB, so two workers can run builds concurrently and race on physical
25
- * table names, staging table names, and manifest writes. Until a shared
26
- * build lease lives in the DuckLake catalog, deployments running
27
- * orchestrated mode must ensure builds are externally single-writer (e.g.,
28
- * one designated build worker, or an external job scheduler). Other
29
- * workers should only call `manifest?action=reload` to pick up manifests produced
30
- * by the build worker.
31
- */
32
- export class DuckLakeManifestStore implements ManifestStore {
33
- private readonly table: string;
34
-
35
- constructor(
36
- private db: DuckDBConnection,
37
- catalogName: string,
38
- ) {
39
- this.table = `${catalogName}.build_manifests`;
40
- }
41
-
42
- /**
43
- * Idempotently creates the `build_manifests` table and indices in the
44
- * DuckLake catalog. Safe to call from every worker on startup.
45
- */
46
- async bootstrapSchema(): Promise<void> {
47
- await this.db.run(`
48
- CREATE TABLE IF NOT EXISTS ${this.table} (
49
- id VARCHAR,
50
- project_id VARCHAR NOT NULL,
51
- package_name VARCHAR NOT NULL,
52
- build_id VARCHAR NOT NULL,
53
- table_name VARCHAR NOT NULL,
54
- source_name VARCHAR NOT NULL,
55
- connection_name VARCHAR NOT NULL,
56
- created_at TIMESTAMP NOT NULL,
57
- updated_at TIMESTAMP NOT NULL
58
- )
59
- `);
60
- logger.info(`DuckLake manifest table bootstrapped: ${this.table}`);
61
- }
62
-
63
- async getManifest(
64
- projectId: string,
65
- packageName: string,
66
- ): Promise<BuildManifest> {
67
- const rows = await this.db.all<Record<string, unknown>>(
68
- `SELECT * FROM ${this.table} WHERE project_id = ? AND package_name = ? ORDER BY created_at DESC`,
69
- [projectId, packageName],
70
- );
71
- const manifest: BuildManifest = { entries: {}, strict: false };
72
- for (const row of rows) {
73
- const buildId = row.build_id as string;
74
- // Rows are ordered newest-first; keep only the latest per build_id
75
- // to handle rare duplicates from cross-worker races.
76
- if (!manifest.entries[buildId]) {
77
- manifest.entries[buildId] = {
78
- tableName: row.table_name as string,
79
- };
80
- }
81
- }
82
- return manifest;
83
- }
84
-
85
- /**
86
- * Insert a manifest entry. If a row with the same `build_id` already
87
- * exists (retry after crash), the duplicate is harmless:
88
- * {@link getManifest} deduplicates by build_id keeping the newest row.
89
- */
90
- async writeEntry(
91
- projectId: string,
92
- packageName: string,
93
- buildId: string,
94
- tableName: string,
95
- sourceName: string,
96
- connectionName: string,
97
- ): Promise<void> {
98
- const now = new Date().toISOString();
99
- const id = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
100
-
101
- await this.db.run(
102
- `INSERT INTO ${this.table} (id, project_id, package_name, build_id, table_name, source_name, connection_name, created_at, updated_at)
103
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
104
- [
105
- id,
106
- projectId,
107
- packageName,
108
- buildId,
109
- tableName,
110
- sourceName,
111
- connectionName,
112
- now,
113
- now,
114
- ],
115
- );
116
- }
117
-
118
- async deleteEntry(id: string): Promise<void> {
119
- await this.db.run(`DELETE FROM ${this.table} WHERE id = ?`, [id]);
120
- }
121
-
122
- async listEntries(
123
- projectId: string,
124
- packageName: string,
125
- ): Promise<ManifestEntry[]> {
126
- const rows = await this.db.all<Record<string, unknown>>(
127
- `SELECT * FROM ${this.table} WHERE project_id = ? AND package_name = ? ORDER BY created_at DESC`,
128
- [projectId, packageName],
129
- );
130
- return rows.map(this.mapToEntry);
131
- }
132
-
133
- private mapToEntry(row: Record<string, unknown>): ManifestEntry {
134
- return {
135
- id: row.id as string,
136
- projectId: row.project_id as string,
137
- packageName: row.package_name as string,
138
- buildId: row.build_id as string,
139
- tableName: row.table_name as string,
140
- sourceName: row.source_name as string,
141
- connectionName: row.connection_name as string,
142
- createdAt: new Date(row.created_at as string),
143
- updatedAt: new Date(row.updated_at as string),
144
- };
145
- }
146
- }
package/src/utils.ts DELETED
@@ -1,13 +0,0 @@
1
- import { URLReader } from "@malloydata/malloy";
2
- import * as fs from "fs";
3
- import { fileURLToPath } from "url";
4
-
5
- export const URL_READER: URLReader = {
6
- readURL: (url: URL) => {
7
- let path = url.toString();
8
- if (url.protocol == "file:") {
9
- path = fileURLToPath(url);
10
- }
11
- return fs.promises.readFile(path, "utf8");
12
- },
13
- };
@@ -1,5 +0,0 @@
1
- category,amount
2
- electronics,100
3
- clothing,50
4
- electronics,200
5
- clothing,75
@@ -1,11 +0,0 @@
1
- ##! experimental.persistence
2
-
3
- source: raw_orders is duckdb.table('data/orders.csv')
4
-
5
- #@ persist name="order_summary"
6
- source: order_summary is raw_orders -> {
7
- group_by: category
8
- aggregate:
9
- total_orders is count()
10
- total_revenue is amount.sum()
11
- }
@@ -1,5 +0,0 @@
1
- {
2
- "name": "persist-test",
3
- "version": "1.0.0",
4
- "description": "DuckDB persist test fixture"
5
- }
@@ -1,15 +0,0 @@
1
- {
2
- "frozenConfig": false,
3
- "projects": [
4
- {
5
- "name": "test-project",
6
- "packages": [
7
- {
8
- "name": "persist-test",
9
- "location": "./persist-test"
10
- }
11
- ],
12
- "connections": []
13
- }
14
- ]
15
- }
@@ -1,96 +0,0 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
- import type {
4
- Notification,
5
- Request,
6
- Result,
7
- } from "@modelcontextprotocol/sdk/types.js";
8
- import http from "http";
9
- import path from "path";
10
- import { fileURLToPath } from "url";
11
- import { URL } from "url";
12
-
13
- /**
14
- * E2E environment descriptor returned by {@link startE2E}.
15
- */
16
- export interface E2EEnv {
17
- httpServer: http.Server;
18
- serverUrl: string;
19
- mcpClient: Client<Request, Notification, Result>;
20
- }
21
-
22
- let originalServerRoot: string | undefined;
23
-
24
- /**
25
- * Spin-up the real Express app (via mcp/server.ts), expose its base URL and
26
- * return a fully-connected MCP client. The caller **must** invoke {@link stop}
27
- * once its suite finishes.
28
- */
29
- export async function startE2E(): Promise<E2EEnv & { stop(): Promise<void> }> {
30
- //--------------------------------------------------------------------------
31
- // 1. Set SERVER_ROOT so ProjectStore loader finds publisher.config.json
32
- //--------------------------------------------------------------------------
33
- originalServerRoot = process.env.SERVER_ROOT;
34
- // Use import.meta.url for cross-platform compatibility (works on Windows)
35
- const __filename = fileURLToPath(import.meta.url);
36
- const __dirname = path.dirname(__filename);
37
- const serverPackageDir = path.resolve(__dirname, "../../../"); // packages/server
38
- process.env.SERVER_ROOT = serverPackageDir;
39
-
40
- //--------------------------------------------------------------------------
41
- // 2. Dynamically import the configured Express app AFTER env var mutation
42
- //--------------------------------------------------------------------------
43
- const { mcpApp } = await import("../../src/server");
44
-
45
- //--------------------------------------------------------------------------
46
- // 3. Start HTTP server on a predictable high port (4042 by default)
47
- //--------------------------------------------------------------------------
48
- const TEST_PORT = Number(process.env.MCP_PORT || 4040) + 2;
49
-
50
- const httpServer: http.Server = await new Promise<http.Server>(
51
- (resolve, reject) => {
52
- const srv = http
53
- .createServer(mcpApp)
54
- .listen(TEST_PORT, "127.0.0.1", () => resolve(srv));
55
-
56
- srv.on("error", (err: NodeJS.ErrnoException) => {
57
- /* c8 ignore next 3 */
58
- console.error("[E2E] server listen error", err);
59
- reject(err);
60
- });
61
- },
62
- );
63
-
64
- const serverUrl = `http://127.0.0.1:${TEST_PORT}`;
65
-
66
- //--------------------------------------------------------------------------
67
- // 4. Connect MCP client over HTTP streaming bridge
68
- //--------------------------------------------------------------------------
69
- const mcpClient = new Client<Request, Notification, Result>({
70
- name: "mcp-e2e-client",
71
- version: "1.0",
72
- });
73
-
74
- const transport = new StreamableHTTPClientTransport(
75
- new URL(`${serverUrl}/mcp`),
76
- );
77
-
78
- await mcpClient.connect(transport);
79
-
80
- //--------------------------------------------------------------------------
81
- // 5. Return env + graceful stop helper
82
- //--------------------------------------------------------------------------
83
- const stop = async (): Promise<void> => {
84
- try {
85
- await mcpClient.close();
86
- } finally {
87
- httpServer.closeAllConnections?.();
88
- await new Promise<void>((r) => httpServer.close(() => r()));
89
- // Restore SERVER_ROOT
90
- if (originalServerRoot === undefined) delete process.env.SERVER_ROOT;
91
- else process.env.SERVER_ROOT = originalServerRoot;
92
- }
93
- };
94
-
95
- return { httpServer, serverUrl, mcpClient, stop };
96
- }