@malloy-publisher/server 0.0.195 → 0.0.197-dev

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 (100) hide show
  1. package/dist/app/api-doc.yaml +213 -214
  2. package/dist/app/assets/EnvironmentPage-1j6QDWAy.js +1 -0
  3. package/dist/app/assets/HomePage-DMop21VG.js +1 -0
  4. package/dist/app/assets/MainPage-BbE8ETz1.js +2 -0
  5. package/dist/app/assets/ModelPage-D2jvfe3t.js +1 -0
  6. package/dist/app/assets/PackagePage-BbnhGoD3.js +1 -0
  7. package/dist/app/assets/{RouteError-DefbDO7F.js → RouteError-D3LGEZ3i.js} +1 -1
  8. package/dist/app/assets/WorkbookPage-DttVIj4u.js +1 -0
  9. package/dist/app/assets/{core-BrfQApxh.es-DnvCX4oH.js → core-w79IMXAG.es-Bd0UlzOL.js} +1 -1
  10. package/dist/app/assets/{index-Bu0ub036.js → index-5K9YjIxF.js} +117 -117
  11. package/dist/app/assets/{index-CkzK3JIl.js → index-C513UodQ.js} +1 -1
  12. package/dist/app/assets/{index-CoA6HIGS.js → index-DIgzgp69.js} +1 -1
  13. package/dist/app/assets/{index.umd-B6Ms2PpL.js → index.umd-BMeMPq_9.js} +1 -1
  14. package/dist/app/index.html +1 -1
  15. package/dist/server.mjs +1352 -1310
  16. package/package.json +2 -2
  17. package/publisher.config.json +2 -2
  18. package/src/config.spec.ts +74 -66
  19. package/src/config.ts +50 -47
  20. package/src/controller/compile.controller.ts +10 -7
  21. package/src/controller/connection.controller.ts +79 -58
  22. package/src/controller/database.controller.ts +10 -7
  23. package/src/controller/manifest.controller.ts +23 -14
  24. package/src/controller/materialization.controller.ts +14 -14
  25. package/src/controller/model.controller.ts +35 -20
  26. package/src/controller/package.controller.ts +83 -49
  27. package/src/controller/query.controller.ts +11 -8
  28. package/src/controller/watch-mode.controller.ts +35 -29
  29. package/src/errors.ts +2 -2
  30. package/src/mcp/error_messages.ts +2 -2
  31. package/src/mcp/handler_utils.ts +23 -20
  32. package/src/mcp/mcp_constants.ts +1 -1
  33. package/src/mcp/prompts/handlers.ts +3 -3
  34. package/src/mcp/prompts/prompt_service.ts +5 -5
  35. package/src/mcp/prompts/utils.ts +12 -12
  36. package/src/mcp/resource_metadata.ts +3 -3
  37. package/src/mcp/resources/environment_resource.ts +187 -0
  38. package/src/mcp/resources/model_resource.ts +19 -17
  39. package/src/mcp/resources/notebook_resource.ts +13 -13
  40. package/src/mcp/resources/package_resource.ts +30 -27
  41. package/src/mcp/resources/query_resource.ts +15 -10
  42. package/src/mcp/resources/source_resource.ts +10 -10
  43. package/src/mcp/resources/view_resource.ts +11 -11
  44. package/src/mcp/server.ts +16 -14
  45. package/src/mcp/tools/discovery_tools.ts +67 -49
  46. package/src/mcp/tools/execute_query_tool.ts +14 -14
  47. package/src/server.ts +175 -159
  48. package/src/service/connection.spec.ts +158 -133
  49. package/src/service/connection.ts +42 -39
  50. package/src/service/connection_config.spec.ts +13 -11
  51. package/src/service/connection_config.ts +28 -19
  52. package/src/service/connection_service.spec.ts +63 -43
  53. package/src/service/connection_service.ts +106 -89
  54. package/src/service/{project.ts → environment.ts} +92 -77
  55. package/src/service/{project_compile.spec.ts → environment_compile.spec.ts} +1 -1
  56. package/src/service/{project_store.spec.ts → environment_store.spec.ts} +99 -83
  57. package/src/service/{project_store.ts → environment_store.ts} +373 -327
  58. package/src/service/manifest_service.spec.ts +15 -15
  59. package/src/service/manifest_service.ts +26 -21
  60. package/src/service/materialization_service.spec.ts +93 -59
  61. package/src/service/materialization_service.ts +71 -62
  62. package/src/service/materialized_table_gc.spec.ts +15 -15
  63. package/src/service/materialized_table_gc.ts +3 -3
  64. package/src/service/model.ts +4 -4
  65. package/src/service/package.spec.ts +2 -2
  66. package/src/service/package.ts +23 -21
  67. package/src/service/resolve_environment.ts +15 -0
  68. package/src/storage/DatabaseInterface.ts +34 -25
  69. package/src/storage/StorageManager.mock.ts +3 -3
  70. package/src/storage/StorageManager.ts +64 -28
  71. package/src/storage/duckdb/ConnectionRepository.ts +13 -11
  72. package/src/storage/duckdb/DuckDBConnection.ts +1 -1
  73. package/src/storage/duckdb/DuckDBManifestStore.ts +6 -6
  74. package/src/storage/duckdb/DuckDBRepository.ts +47 -47
  75. package/src/storage/duckdb/{ProjectRepository.ts → EnvironmentRepository.ts} +35 -35
  76. package/src/storage/duckdb/ManifestRepository.ts +21 -20
  77. package/src/storage/duckdb/MaterializationRepository.ts +31 -28
  78. package/src/storage/duckdb/PackageRepository.ts +11 -11
  79. package/src/storage/duckdb/manifest_store.spec.ts +2 -2
  80. package/src/storage/duckdb/schema.ts +20 -20
  81. package/src/storage/ducklake/DuckLakeManifestStore.ts +20 -11
  82. package/tests/fixtures/publisher.config.json +1 -1
  83. package/tests/harness/e2e.ts +1 -1
  84. package/tests/harness/mcp_test_setup.ts +12 -24
  85. package/tests/harness/mocks.ts +10 -8
  86. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +4 -4
  87. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +28 -49
  88. package/tests/integration/mcp/mcp_resource.integration.spec.ts +39 -47
  89. package/tests/integration/mcp/mcp_transport.integration.spec.ts +1 -1
  90. package/tests/unit/duckdb/attached_databases.test.ts +51 -33
  91. package/tests/unit/ducklake/ducklake.test.ts +24 -22
  92. package/tests/unit/mcp/prompt_happy.test.ts +8 -8
  93. package/dist/app/assets/HomePage-DbZS0N7G.js +0 -1
  94. package/dist/app/assets/MainPage-CBuWkbmr.js +0 -2
  95. package/dist/app/assets/ModelPage-Bt37smot.js +0 -1
  96. package/dist/app/assets/PackagePage-DLZe50WG.js +0 -1
  97. package/dist/app/assets/ProjectPage-FQTEPXP4.js +0 -1
  98. package/dist/app/assets/WorkbookPage-CkAo16ar.js +0 -1
  99. package/src/mcp/resources/project_resource.ts +0 -184
  100. package/src/service/resolve_project.ts +0 -13
@@ -3,7 +3,7 @@ import { RequestHandler } from "express";
3
3
  import path from "path";
4
4
  import { components } from "../api";
5
5
  import { logger } from "../logger";
6
- import { ProjectStore } from "../service/project_store";
6
+ import { EnvironmentStore } from "../service/environment_store";
7
7
 
8
8
  type StartWatchReq = components["schemas"]["StartWatchRequest"];
9
9
  type WatchStatusRes = components["schemas"]["WatchStatus"];
@@ -11,19 +11,19 @@ type Handler<Req = object, Res = void> = RequestHandler<object, Res, Req>;
11
11
 
12
12
  export class WatchModeController {
13
13
  watchingPath: string | null;
14
- watchingProjectName: string | null;
14
+ watchingEnvironmentName: string | null;
15
15
  watcher: FSWatcher;
16
16
 
17
- constructor(private projectStore: ProjectStore) {
17
+ constructor(private environmentStore: EnvironmentStore) {
18
18
  this.watchingPath = null;
19
- this.watchingProjectName = null;
19
+ this.watchingEnvironmentName = null;
20
20
  }
21
21
 
22
22
  public getWatchStatus: Handler<void, WatchStatusRes> = async (_req, res) => {
23
23
  return res.json({
24
24
  enabled: !!this.watchingPath,
25
25
  watchingPath: this.watchingPath ?? "",
26
- projectName: this.watchingProjectName ?? "",
26
+ environmentName: this.watchingEnvironmentName ?? "",
27
27
  });
28
28
  };
29
29
 
@@ -31,25 +31,31 @@ export class WatchModeController {
31
31
  req,
32
32
  res,
33
33
  ) => {
34
- const projectManifest = await ProjectStore.reloadProjectManifest(
35
- this.projectStore.serverRootPath,
36
- );
37
- this.watchingProjectName = req.body.projectName;
34
+ const watchName = req.body.environmentName ?? "";
35
+ const environmentManifest =
36
+ await EnvironmentStore.reloadEnvironmentManifest(
37
+ this.environmentStore.serverRootPath,
38
+ );
39
+ this.watchingEnvironmentName = watchName || null;
38
40
 
39
- // Find the project in the new array structure
40
- const project = projectManifest.projects.find(
41
- (p) => p.name === req.body.projectName,
41
+ // Find the environment in the manifest
42
+ const environment = environmentManifest.environments.find(
43
+ (e) => e.name === watchName,
42
44
  );
43
- if (!project || !project.packages || project.packages.length === 0) {
45
+ if (
46
+ !environment ||
47
+ !environment.packages ||
48
+ environment.packages.length === 0
49
+ ) {
44
50
  res.status(404).json({
45
- error: `Project ${req.body.projectName} not found or has no packages`,
51
+ error: `Environment ${watchName} not found or has no packages`,
46
52
  });
47
53
  return;
48
54
  }
49
55
 
50
56
  this.watchingPath = path.join(
51
- this.projectStore.serverRootPath,
52
- req.body.projectName,
57
+ this.environmentStore.serverRootPath,
58
+ watchName,
53
59
  );
54
60
  this.watcher = chokidar.watch(this.watchingPath, {
55
61
  ignored: (path, stats) =>
@@ -58,33 +64,33 @@ export class WatchModeController {
58
64
  !path.endsWith(".md"),
59
65
  ignoreInitial: true,
60
66
  });
61
- const reloadProject = async () => {
62
- // Overwrite the project with it's existing metadata to trigger a re-read
63
- const project = await this.projectStore.getProject(
64
- req.body.projectName,
67
+ const reloadEnvironment = async () => {
68
+ // Overwrite the environment with its existing metadata to trigger a re-read
69
+ const environment = await this.environmentStore.getEnvironment(
70
+ watchName,
65
71
  true,
66
72
  );
67
- await this.projectStore.addProject(project.metadata);
68
- logger.info(`Reloaded ${req.body.projectName}`);
73
+ await this.environmentStore.addEnvironment(environment.metadata);
74
+ logger.info(`Reloaded environment ${watchName}`);
69
75
  };
70
76
 
71
77
  this.watcher.on("add", async (path) => {
72
78
  logger.info(
73
- `Detected new file ${path}, reloading ${req.body.projectName}`,
79
+ `Detected new file ${path}, reloading environment ${watchName}`,
74
80
  );
75
- await reloadProject();
81
+ await reloadEnvironment();
76
82
  });
77
83
  this.watcher.on("unlink", async (path) => {
78
84
  logger.info(
79
- `Detected deletion of ${path}, reloading ${req.body.projectName}`,
85
+ `Detected deletion of ${path}, reloading environment ${watchName}`,
80
86
  );
81
- await reloadProject();
87
+ await reloadEnvironment();
82
88
  });
83
89
  this.watcher.on("change", async (path) => {
84
90
  logger.info(
85
- `Detected change on ${path}, reloading ${req.body.projectName}`,
91
+ `Detected change on ${path}, reloading environment ${watchName}`,
86
92
  );
87
- await reloadProject();
93
+ await reloadEnvironment();
88
94
  });
89
95
  res.json();
90
96
  };
@@ -92,7 +98,7 @@ export class WatchModeController {
92
98
  public stopWatchMode: Handler = async (_req, res) => {
93
99
  this.watcher.close();
94
100
  this.watchingPath = null;
95
- this.watchingProjectName = null;
101
+ this.watchingEnvironmentName = null;
96
102
  res.json();
97
103
  };
98
104
  }
package/src/errors.ts CHANGED
@@ -6,7 +6,7 @@ export function internalErrorToHttpError(error: Error) {
6
6
  return httpError(400, error.message);
7
7
  } else if (error instanceof FrozenConfigError) {
8
8
  return httpError(403, error.message);
9
- } else if (error instanceof ProjectNotFoundError) {
9
+ } else if (error instanceof EnvironmentNotFoundError) {
10
10
  return httpError(404, error.message);
11
11
  } else if (error instanceof PackageNotFoundError) {
12
12
  return httpError(404, error.message);
@@ -53,7 +53,7 @@ export class BadRequestError extends Error {
53
53
  }
54
54
  }
55
55
 
56
- export class ProjectNotFoundError extends Error {
56
+ export class EnvironmentNotFoundError extends Error {
57
57
  constructor(message: string) {
58
58
  super(message);
59
59
  }
@@ -37,11 +37,11 @@ export function getNotFoundError(resourceUriOrContext: string): ErrorDetails {
37
37
  const baseMessage = `Resource not found: ${resourceUriOrContext}`;
38
38
  const suggestions: string[] = [
39
39
  `Verify the identifier or URI (${resourceUriOrContext}) is spelled correctly and exists. Check capitalization and path separators.`,
40
- `If using a URI, ensure it follows the correct format (e.g., malloy://project/...) and includes the right path segments (e.g., /models/, /sources/, /queries/, /views/).`,
40
+ `If using a URI, ensure it follows the correct format (e.g., malloy://environment/...) and includes the right path segments (e.g., /models/, /sources/, /queries/, /views/).`,
41
41
  ];
42
42
 
43
43
  suggestions.push(
44
- "Check if the resource exists and is correctly named in your Malloy project structure or the specific model file.",
44
+ "Check if the resource exists and is correctly named in your Malloy environment structure or the specific model file.",
45
45
  );
46
46
 
47
47
  return {
@@ -1,12 +1,12 @@
1
1
  import { URL } from "url";
2
2
  import type { ResourceMetadata } from "@modelcontextprotocol/sdk/server/mcp";
3
3
  import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
4
- import { ProjectStore } from "../service/project_store";
4
+ import { EnvironmentStore } from "../service/environment_store";
5
5
  import {
6
6
  PackageNotFoundError,
7
7
  ModelNotFoundError,
8
8
  ModelCompilationError,
9
- ProjectNotFoundError,
9
+ EnvironmentNotFoundError,
10
10
  } from "../errors";
11
11
  import {
12
12
  getNotFoundError,
@@ -48,7 +48,7 @@ export async function handleResourceGet<
48
48
  >(
49
49
  uri: URL,
50
50
  params: TParams,
51
- resourceType: string, // e.g., 'project', 'package' for logging/errors
51
+ resourceType: string, // e.g., 'environment', 'package' for logging/errors
52
52
  getData: GetDataLogic<TParams, TDefinition>,
53
53
  resourceMetadata: ResourceMetadata | undefined,
54
54
  ): Promise<ReadResourceResult> {
@@ -122,19 +122,22 @@ export async function handleResourceGet<
122
122
  * @returns An object containing the Model instance or a pre-formatted ErrorDetails object.
123
123
  */
124
124
  export async function getModelForQuery(
125
- projectStore: ProjectStore,
126
- projectName: string,
125
+ environmentStore: EnvironmentStore,
126
+ environmentName: string,
127
127
  packageName: string,
128
128
  modelPath: string,
129
129
  ): Promise<{ model: Model } | { error: ErrorDetails }> {
130
130
  try {
131
- const project = await projectStore.getProject(projectName, false);
132
- const pkg = await project.getPackage(packageName, false);
131
+ const environment = await environmentStore.getEnvironment(
132
+ environmentName,
133
+ false,
134
+ );
135
+ const pkg = await environment.getPackage(packageName, false);
133
136
  const model = pkg.getModel(modelPath);
134
137
  if (!model || model.getModelType() === "notebook") {
135
138
  // Ensure it's actually a model
136
139
  const details = getNotFoundError(
137
- `model '${modelPath}' in package '${packageName}' for project '${projectName}'`,
140
+ `model '${modelPath}' in package '${packageName}' for environment '${environmentName}'`,
138
141
  );
139
142
  return { error: details };
140
143
  }
@@ -144,20 +147,20 @@ export async function getModelForQuery(
144
147
  } catch (error) {
145
148
  // Handle errors during package/model access or initial compilation
146
149
  let errorDetails: ErrorDetails;
147
- if (error instanceof ProjectNotFoundError) {
148
- errorDetails = getNotFoundError(`project '${projectName}'`);
150
+ if (error instanceof EnvironmentNotFoundError) {
151
+ errorDetails = getNotFoundError(`environment '${environmentName}'`);
149
152
  } else if (error instanceof PackageNotFoundError) {
150
153
  errorDetails = getNotFoundError(
151
- `package '${packageName}' in project '${projectName}'`,
154
+ `package '${packageName}' in environment '${environmentName}'`,
152
155
  );
153
156
  } else if (error instanceof ModelNotFoundError) {
154
157
  errorDetails = getNotFoundError(
155
- `model '${modelPath}' in package '${packageName}' for project '${projectName}'`,
158
+ `model '${modelPath}' in package '${packageName}' for environment '${environmentName}'`,
156
159
  );
157
160
  } else if (error instanceof ModelCompilationError) {
158
161
  errorDetails = getMalloyErrorDetails(
159
162
  "executeQuery (load model)",
160
- `${projectName}/${packageName}/${modelPath}`,
163
+ `${environmentName}/${packageName}/${modelPath}`,
161
164
  error,
162
165
  );
163
166
  } else {
@@ -165,7 +168,7 @@ export async function getModelForQuery(
165
168
  errorDetails = getInternalError("executeQuery (Setup)", error);
166
169
  }
167
170
  logger.error(
168
- `[MCP Server Error] Error accessing package/model for query: ${projectName}/${packageName}/${modelPath}`,
171
+ `[MCP Server Error] Error accessing package/model for query: ${environmentName}/${packageName}/${modelPath}`,
169
172
  { error },
170
173
  );
171
174
  return { error: errorDetails };
@@ -176,13 +179,13 @@ export async function getModelForQuery(
176
179
  * Constructs a valid malloy:// URI string from its components.
177
180
  * Handles encoding of path segments.
178
181
  *
179
- * @param components An object containing the URI parts (project, package, model, etc.)
182
+ * @param components An object containing the URI parts (environment, package, model, etc.)
180
183
  * @param fragment Optional fragment identifier (e.g., #queryResult)
181
184
  * @returns A valid malloy:// URI string.
182
185
  */
183
186
  export function buildMalloyUri(
184
187
  components: {
185
- project?: string;
188
+ environment?: string;
186
189
  package?: string;
187
190
  model?: string;
188
191
  resourceType?:
@@ -198,10 +201,10 @@ export function buildMalloyUri(
198
201
  },
199
202
  fragment?: string,
200
203
  ): string {
201
- let path = "/project/";
204
+ let path = "/environment/";
202
205
 
203
- if (components.project) {
204
- path += encodeURIComponent(components.project);
206
+ if (components.environment) {
207
+ path += encodeURIComponent(components.environment);
205
208
  } else {
206
209
  // Default to 'home' if not provided, consistent with current behavior
207
210
  path += "home";
@@ -227,7 +230,7 @@ export function buildMalloyUri(
227
230
  }
228
231
 
229
232
  // The URL constructor seems to normalize malloy://path to malloy:///path
230
- // which breaks the tests expecting malloy://project/...
233
+ // which breaks the tests expecting malloy://environment/...
231
234
  // We manually construct the string instead.
232
235
  let uriString = "malloy:/" + path; // Start with one slash after scheme
233
236
 
@@ -8,7 +8,7 @@ export const MCP_ERROR_MESSAGES = {
8
8
 
9
9
  // Application-level errors (runtime)
10
10
  PROJECT_NOT_FOUND: (projectName: string) =>
11
- `Project '${projectName}' is not available or does not exist.`,
11
+ `Environment '${projectName}' is not available or does not exist.`,
12
12
  PACKAGE_NOT_FOUND: (packageName: string) =>
13
13
  `Package manifest for ${packageName} does not exist.`,
14
14
  MODEL_NOT_FOUND: (packageName: string, modelPath: string) =>
@@ -6,7 +6,7 @@ import {
6
6
  } from "@modelcontextprotocol/sdk/types.js";
7
7
  import { PROMPTS } from "./prompt_definitions";
8
8
  import { getCompiledModel } from "./utils";
9
- import { ProjectStore } from "../../service/project_store";
9
+ import { EnvironmentStore } from "../../service/environment_store";
10
10
 
11
11
  /* eslint-disable @typescript-eslint/no-explicit-any */
12
12
 
@@ -31,7 +31,7 @@ export function makePromptHandler(id: string) {
31
31
  const templateFn = Handlebars.compile(def.template, { noEscape: true });
32
32
 
33
33
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
- return async (params: any, ps: ProjectStore) => {
34
+ return async (params: any, ps: EnvironmentStore) => {
35
35
  const data: Record<string, unknown> = { ...params };
36
36
 
37
37
  // Dynamically add model content / schema context if the args include URIs
@@ -79,6 +79,6 @@ export const promptHandlerMap = Object.fromEntries(
79
79
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
80
  (
81
81
  params: any,
82
- ps: ProjectStore,
82
+ ps: EnvironmentStore,
83
83
  ) => Promise<z.infer<typeof GetPromptResultSchema>>
84
84
  >;
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { logger } from "../../logger";
4
- import { ProjectStore } from "../../service/project_store";
4
+ import { EnvironmentStore } from "../../service/environment_store";
5
5
  import { promptHandlerMap } from "./handlers";
6
6
  import { MALLOY_PROMPTS } from "./prompt_definitions";
7
7
 
@@ -9,11 +9,11 @@ import { MALLOY_PROMPTS } from "./prompt_definitions";
9
9
  * Registers all defined Malloy prompts with the MCP server.
10
10
  *
11
11
  * @param mcpServer The McpServer instance.
12
- * @param projectStore The ProjectStore instance for handlers to access project data.
12
+ * @param environmentStore The EnvironmentStore instance for handlers to access environment data.
13
13
  */
14
14
  export function registerPromptCapability(
15
15
  mcpServer: McpServer,
16
- projectStore: ProjectStore,
16
+ environmentStore: EnvironmentStore,
17
17
  ): void {
18
18
  logger.info("[MCP Init] Registering prompt capability...");
19
19
  const startTime = performance.now();
@@ -24,10 +24,10 @@ export function registerPromptCapability(
24
24
  const handler = promptHandlerMap[promptDefinition.id];
25
25
 
26
26
  if (handler && promptDefinition.argsSchema instanceof z.ZodObject) {
27
- // Prepare a handler that injects the projectStore
27
+ // Prepare a handler that injects the environmentStore
28
28
  const preparedHandler = (
29
29
  args: z.infer<typeof promptDefinition.argsSchema>,
30
- ) => handler(args, projectStore);
30
+ ) => handler(args, environmentStore);
31
31
 
32
32
  // Register using prompt ID, the Zod schema shape, and the prepared handler.
33
33
  mcpServer.prompt(
@@ -1,4 +1,4 @@
1
- import { ProjectStore } from "../../service/project_store";
1
+ import { EnvironmentStore } from "../../service/environment_store";
2
2
  import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
3
3
 
4
4
  // Type alias for compiled model pulled from OpenAPI-generated types.
@@ -7,20 +7,20 @@ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
7
7
  export type ApiCompiledModel = any;
8
8
 
9
9
  export interface ParsedMalloyUri {
10
- projectName: string;
10
+ environmentName: string;
11
11
  packageName: string;
12
12
  modelPath: string;
13
13
  }
14
14
 
15
15
  /**
16
16
  * Parse a canonical Malloy model URI of the form:
17
- * malloy://project/{project}/package/{package}/models/{modelPath}
17
+ * malloy://environment/{environment}/package/{package}/models/{modelPath}
18
18
  *
19
19
  * Throws an McpError(InvalidParams) if the URI is malformed.
20
20
  */
21
21
  export function parseMalloyUri(uri: string): ParsedMalloyUri {
22
22
  const match = uri.match(
23
- /^malloy:\/\/project\/([^/]+)\/package\/([^/]+)\/models\/(.+)$/,
23
+ /^malloy:\/\/environment\/([^/]+)\/package\/([^/]+)\/models\/(.+)$/,
24
24
  );
25
25
 
26
26
  if (!match) {
@@ -28,7 +28,7 @@ export function parseMalloyUri(uri: string): ParsedMalloyUri {
28
28
  }
29
29
 
30
30
  return {
31
- projectName: match[1],
31
+ environmentName: match[1],
32
32
  packageName: match[2],
33
33
  modelPath: match[3],
34
34
  };
@@ -36,18 +36,18 @@ export function parseMalloyUri(uri: string): ParsedMalloyUri {
36
36
 
37
37
  /**
38
38
  * Retrieve the compiled model for the given Malloy URI, using the provided
39
- * ProjectStore. Results are memoised in `compiledModelCache`.
39
+ * EnvironmentStore. Results are memoised in `compiledModelCache`.
40
40
  */
41
41
  export async function getCompiledModel(
42
42
  uri: string,
43
- projectStore: ProjectStore,
43
+ environmentStore: EnvironmentStore,
44
44
  ): Promise<ApiCompiledModel> {
45
- const { projectName, packageName, modelPath } = parseMalloyUri(uri);
45
+ const { environmentName, packageName, modelPath } = parseMalloyUri(uri);
46
46
 
47
- // Look up the model via the ProjectStore hierarchy.
48
- const model = await projectStore
49
- .getProject(projectName, false)
50
- .then((p) => p.getPackage(packageName, false))
47
+ // Look up the model via the EnvironmentStore hierarchy.
48
+ const model = await environmentStore
49
+ .getEnvironment(environmentName, false)
50
+ .then((e) => e.getPackage(packageName, false))
51
51
  .then((pkg) => pkg.getModel(modelPath));
52
52
 
53
53
  if (!model || model.getModelType() === "notebook") {
@@ -8,7 +8,7 @@ type McpResourceType = "model" | "source" | "view" | "query" | "notebook";
8
8
  // Type assertion ensures compatibility with the SDK's ResourceMetadata,
9
9
  // which might include optional fields or an index signature.
10
10
  export const RESOURCE_METADATA: Record<
11
- McpResourceType | "package" | "project",
11
+ McpResourceType | "package" | "environment",
12
12
  SdkResourceMetadata // Use the imported SDK type here
13
13
  > = {
14
14
  model: {
@@ -40,8 +40,8 @@ export const RESOURCE_METADATA: Record<
40
40
  description: "A folder grouping related Models and Notebooks.",
41
41
  usage: "Use `ListResources` on this Package's URI to discover the Models and Notebooks it contains. You can then use `GetResource` on those items' URIs to explore them further.",
42
42
  },
43
- project: {
43
+ environment: {
44
44
  description: "The main workspace folder holding Packages, Models, etc.",
45
- usage: "Use `ListResources` on this Project's URI to see the Packages inside and begin navigating your data assets.",
45
+ usage: "Use `ListResources` on this environment's URI to see the Packages inside and begin navigating your data assets.",
46
46
  },
47
47
  };
@@ -0,0 +1,187 @@
1
+ import {
2
+ McpServer,
3
+ ResourceTemplate,
4
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import type { ListResourcesResult } from "@modelcontextprotocol/sdk/types.js"; // Needed for list return type
6
+ import { logger } from "../../logger";
7
+ import { EnvironmentStore } from "../../service/environment_store";
8
+ import { getInternalError, getNotFoundError } from "../error_messages"; // Needed for error handling in list AND get
9
+ import { handleResourceGet, McpGetResourceError } from "../handler_utils";
10
+ import { RESOURCE_METADATA } from "../resource_metadata";
11
+
12
+ // Define an interface for the package object augmented with environment name
13
+ interface PackageWithEnvironment {
14
+ name?: string;
15
+ // Add other relevant package properties if needed
16
+ environmentName: string;
17
+ }
18
+
19
+ /**
20
+ * Registers the Malloy environment resource type with the MCP server (URI scheme malloy://environment/...).
21
+ * Lists packages across environments and resolves environment metadata on read.
22
+ */
23
+ export function registerEnvironmentResource(
24
+ mcpServer: McpServer,
25
+ environmentStore: EnvironmentStore,
26
+ ): void {
27
+ mcpServer.resource(
28
+ "environment",
29
+ new ResourceTemplate("malloy://environment/{environmentName}", {
30
+ /**
31
+ * Handles ListResources requests.
32
+ * If environmentName is specified, lists packages for that environment (only 'home' supported).
33
+ * If environmentName is not specified (general ListResources call), lists packages for the default 'home' environment.
34
+ */
35
+ list: async (/* extra: ListEnvironmentExtra - Deleted */): Promise<ListResourcesResult> => {
36
+ logger.info(
37
+ "[MCP LOG] Entering ListResources (environment) handler (listing ALL packages)...",
38
+ );
39
+ // Ignore parameters from 'extra' as URI path params aren't passed to list handlers.
40
+
41
+ try {
42
+ const allEnvironments =
43
+ await environmentStore.listEnvironments();
44
+ logger.info(
45
+ `[MCP LOG] Found ${allEnvironments.length} environments defined.`,
46
+ );
47
+
48
+ const packagePromises = allEnvironments.map(async (env) => {
49
+ try {
50
+ logger.info(
51
+ `[MCP LOG] Getting environment '${env.name}' to list its packages...`,
52
+ );
53
+ const environmentInstance =
54
+ await environmentStore.getEnvironment(env.name!, false);
55
+ const packages = await environmentInstance.listPackages();
56
+ logger.info(
57
+ `[MCP LOG] Found ${packages.length} packages in environment '${env.name}'.`,
58
+ );
59
+ // Return packages along with their environment name for URI construction
60
+ return packages.map((pkg) => ({
61
+ ...pkg,
62
+ environmentName: env.name,
63
+ }));
64
+ } catch (environmentError) {
65
+ logger.error(
66
+ `[MCP Server Error] Error getting/listing packages for environment ${env.name}:`,
67
+ { error: environmentError },
68
+ );
69
+ return []; // Return empty array for this environment on error
70
+ }
71
+ });
72
+
73
+ const results = await Promise.allSettled(packagePromises);
74
+ const allPackagesWithEnvironmentName = results
75
+ .filter((result) => result.status === "fulfilled")
76
+ .flatMap(
77
+ (result) =>
78
+ (
79
+ result as PromiseFulfilledResult<
80
+ PackageWithEnvironment[]
81
+ >
82
+ ).value,
83
+ );
84
+
85
+ logger.info(
86
+ `[MCP LOG] Total packages found across all environments: ${allPackagesWithEnvironmentName.length}`,
87
+ );
88
+
89
+ const packageMetadata = RESOURCE_METADATA.package;
90
+ const mappedResources = allPackagesWithEnvironmentName.map(
91
+ (pkg) => {
92
+ const name = pkg.name || "unknown";
93
+ // Construct URI using the package's specific environmentName
94
+ const uri = `malloy://environment/${pkg.environmentName}/package/${name}`;
95
+ return {
96
+ uri: uri,
97
+ name: name,
98
+ type: "package",
99
+ description: packageMetadata?.description as
100
+ | string
101
+ | undefined,
102
+ metadata: packageMetadata,
103
+ };
104
+ },
105
+ );
106
+
107
+ logger.info(
108
+ `[MCP LOG] ListResources (environment): Returning ${mappedResources.length} package resources.`,
109
+ );
110
+ return {
111
+ resources: mappedResources,
112
+ };
113
+ } catch (error) {
114
+ // Catch errors from environmentStore.listEnvironments() itself
115
+ logger.error(`[MCP Server Error] Error listing environments:`, {
116
+ error,
117
+ });
118
+ const errorDetails = getInternalError(
119
+ `ListResources (environment - initial list)`,
120
+ error,
121
+ );
122
+ logger.error("MCP ListResources (environment) error:", {
123
+ error: errorDetails.message,
124
+ });
125
+ logger.info(
126
+ "[MCP LOG] ListResources (environment): Returning empty on error listing environments.",
127
+ );
128
+ return { resources: [] };
129
+ }
130
+ },
131
+ }),
132
+ /** Handles GetResource requests for Malloy environments */
133
+ (uri, params) =>
134
+ handleResourceGet(
135
+ uri,
136
+ params,
137
+ "environment",
138
+ async ({ environmentName }: { environmentName?: unknown }) => {
139
+ logger.info(
140
+ `[MCP LOG] Entering GetResource (environment) handler for environmentName: ${environmentName}`,
141
+ );
142
+ // Validate environment name parameter
143
+ if (typeof environmentName !== "string") {
144
+ logger.error(
145
+ "[MCP LOG] GetResource (environment): Invalid environment name param.",
146
+ );
147
+ throw new Error("Invalid environment name parameter.");
148
+ }
149
+
150
+ try {
151
+ logger.info(
152
+ `[MCP LOG] GetResource: Getting environment '${environmentName}'...`,
153
+ );
154
+ // Get the environment instance, but we might not need its metadata directly
155
+ await environmentStore.getEnvironment(environmentName, false);
156
+ // Construct the definition object expected by the test
157
+ const definition = { name: environmentName };
158
+ logger.info(
159
+ `[MCP LOG] GetResource (environment): Returning definition for '${environmentName}'.`,
160
+ );
161
+ // Return the explicit definition structure
162
+ return definition;
163
+ } catch (error) {
164
+ logger.error(
165
+ `[MCP LOG] GetResource (environment): Error caught for '${environmentName}':`,
166
+ { error },
167
+ );
168
+ // Catch expected errors from this specific resource logic
169
+ if (error instanceof Error) {
170
+ // Use getNotFoundError for the specific environment not found case
171
+ // or a generic message for the invalid param case.
172
+ const errorDetails = getNotFoundError(
173
+ error.message.includes("not found")
174
+ ? `Environment '${environmentName}'` // More specific context
175
+ : `Invalid environment identifier provided for URI ${uri.href}`, // Generic but informative
176
+ );
177
+ // Re-throw structured error for handleResourceGet to catch
178
+ throw new McpGetResourceError(errorDetails);
179
+ }
180
+ // Re-throw unexpected errors to be caught by handleResourceGet's generic handler
181
+ throw error;
182
+ }
183
+ },
184
+ RESOURCE_METADATA.environment,
185
+ ),
186
+ );
187
+ }