@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.
- package/dist/app/api-doc.yaml +213 -214
- package/dist/app/assets/EnvironmentPage-1j6QDWAy.js +1 -0
- package/dist/app/assets/HomePage-DMop21VG.js +1 -0
- package/dist/app/assets/MainPage-BbE8ETz1.js +2 -0
- package/dist/app/assets/ModelPage-D2jvfe3t.js +1 -0
- package/dist/app/assets/PackagePage-BbnhGoD3.js +1 -0
- package/dist/app/assets/{RouteError-DefbDO7F.js → RouteError-D3LGEZ3i.js} +1 -1
- package/dist/app/assets/WorkbookPage-DttVIj4u.js +1 -0
- package/dist/app/assets/{core-BrfQApxh.es-DnvCX4oH.js → core-w79IMXAG.es-Bd0UlzOL.js} +1 -1
- package/dist/app/assets/{index-Bu0ub036.js → index-5K9YjIxF.js} +117 -117
- package/dist/app/assets/{index-CkzK3JIl.js → index-C513UodQ.js} +1 -1
- package/dist/app/assets/{index-CoA6HIGS.js → index-DIgzgp69.js} +1 -1
- package/dist/app/assets/{index.umd-B6Ms2PpL.js → index.umd-BMeMPq_9.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +1352 -1310
- package/package.json +2 -2
- package/publisher.config.json +2 -2
- package/src/config.spec.ts +74 -66
- package/src/config.ts +50 -47
- package/src/controller/compile.controller.ts +10 -7
- package/src/controller/connection.controller.ts +79 -58
- package/src/controller/database.controller.ts +10 -7
- package/src/controller/manifest.controller.ts +23 -14
- package/src/controller/materialization.controller.ts +14 -14
- package/src/controller/model.controller.ts +35 -20
- package/src/controller/package.controller.ts +83 -49
- package/src/controller/query.controller.ts +11 -8
- package/src/controller/watch-mode.controller.ts +35 -29
- package/src/errors.ts +2 -2
- package/src/mcp/error_messages.ts +2 -2
- package/src/mcp/handler_utils.ts +23 -20
- package/src/mcp/mcp_constants.ts +1 -1
- package/src/mcp/prompts/handlers.ts +3 -3
- package/src/mcp/prompts/prompt_service.ts +5 -5
- package/src/mcp/prompts/utils.ts +12 -12
- package/src/mcp/resource_metadata.ts +3 -3
- package/src/mcp/resources/environment_resource.ts +187 -0
- package/src/mcp/resources/model_resource.ts +19 -17
- package/src/mcp/resources/notebook_resource.ts +13 -13
- package/src/mcp/resources/package_resource.ts +30 -27
- package/src/mcp/resources/query_resource.ts +15 -10
- package/src/mcp/resources/source_resource.ts +10 -10
- package/src/mcp/resources/view_resource.ts +11 -11
- package/src/mcp/server.ts +16 -14
- package/src/mcp/tools/discovery_tools.ts +67 -49
- package/src/mcp/tools/execute_query_tool.ts +14 -14
- package/src/server.ts +175 -159
- package/src/service/connection.spec.ts +158 -133
- package/src/service/connection.ts +42 -39
- package/src/service/connection_config.spec.ts +13 -11
- package/src/service/connection_config.ts +28 -19
- package/src/service/connection_service.spec.ts +63 -43
- package/src/service/connection_service.ts +106 -89
- package/src/service/{project.ts → environment.ts} +92 -77
- package/src/service/{project_compile.spec.ts → environment_compile.spec.ts} +1 -1
- package/src/service/{project_store.spec.ts → environment_store.spec.ts} +99 -83
- package/src/service/{project_store.ts → environment_store.ts} +373 -327
- package/src/service/manifest_service.spec.ts +15 -15
- package/src/service/manifest_service.ts +26 -21
- package/src/service/materialization_service.spec.ts +93 -59
- package/src/service/materialization_service.ts +71 -62
- package/src/service/materialized_table_gc.spec.ts +15 -15
- package/src/service/materialized_table_gc.ts +3 -3
- package/src/service/model.ts +4 -4
- package/src/service/package.spec.ts +2 -2
- package/src/service/package.ts +23 -21
- package/src/service/resolve_environment.ts +15 -0
- package/src/storage/DatabaseInterface.ts +34 -25
- package/src/storage/StorageManager.mock.ts +3 -3
- package/src/storage/StorageManager.ts +64 -28
- package/src/storage/duckdb/ConnectionRepository.ts +13 -11
- package/src/storage/duckdb/DuckDBConnection.ts +1 -1
- package/src/storage/duckdb/DuckDBManifestStore.ts +6 -6
- package/src/storage/duckdb/DuckDBRepository.ts +47 -47
- package/src/storage/duckdb/{ProjectRepository.ts → EnvironmentRepository.ts} +35 -35
- package/src/storage/duckdb/ManifestRepository.ts +21 -20
- package/src/storage/duckdb/MaterializationRepository.ts +31 -28
- package/src/storage/duckdb/PackageRepository.ts +11 -11
- package/src/storage/duckdb/manifest_store.spec.ts +2 -2
- package/src/storage/duckdb/schema.ts +20 -20
- package/src/storage/ducklake/DuckLakeManifestStore.ts +20 -11
- package/tests/fixtures/publisher.config.json +1 -1
- package/tests/harness/e2e.ts +1 -1
- package/tests/harness/mcp_test_setup.ts +12 -24
- package/tests/harness/mocks.ts +10 -8
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +4 -4
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +28 -49
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +39 -47
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +1 -1
- package/tests/unit/duckdb/attached_databases.test.ts +51 -33
- package/tests/unit/ducklake/ducklake.test.ts +24 -22
- package/tests/unit/mcp/prompt_happy.test.ts +8 -8
- package/dist/app/assets/HomePage-DbZS0N7G.js +0 -1
- package/dist/app/assets/MainPage-CBuWkbmr.js +0 -2
- package/dist/app/assets/ModelPage-Bt37smot.js +0 -1
- package/dist/app/assets/PackagePage-DLZe50WG.js +0 -1
- package/dist/app/assets/ProjectPage-FQTEPXP4.js +0 -1
- package/dist/app/assets/WorkbookPage-CkAo16ar.js +0 -1
- package/src/mcp/resources/project_resource.ts +0 -184
- 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 {
|
|
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
|
-
|
|
14
|
+
watchingEnvironmentName: string | null;
|
|
15
15
|
watcher: FSWatcher;
|
|
16
16
|
|
|
17
|
-
constructor(private
|
|
17
|
+
constructor(private environmentStore: EnvironmentStore) {
|
|
18
18
|
this.watchingPath = null;
|
|
19
|
-
this.
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
40
|
-
const
|
|
41
|
-
(
|
|
41
|
+
// Find the environment in the manifest
|
|
42
|
+
const environment = environmentManifest.environments.find(
|
|
43
|
+
(e) => e.name === watchName,
|
|
42
44
|
);
|
|
43
|
-
if (
|
|
45
|
+
if (
|
|
46
|
+
!environment ||
|
|
47
|
+
!environment.packages ||
|
|
48
|
+
environment.packages.length === 0
|
|
49
|
+
) {
|
|
44
50
|
res.status(404).json({
|
|
45
|
-
error: `
|
|
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.
|
|
52
|
-
|
|
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
|
|
62
|
-
// Overwrite the
|
|
63
|
-
const
|
|
64
|
-
|
|
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.
|
|
68
|
-
logger.info(`Reloaded ${
|
|
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 ${
|
|
79
|
+
`Detected new file ${path}, reloading environment ${watchName}`,
|
|
74
80
|
);
|
|
75
|
-
await
|
|
81
|
+
await reloadEnvironment();
|
|
76
82
|
});
|
|
77
83
|
this.watcher.on("unlink", async (path) => {
|
|
78
84
|
logger.info(
|
|
79
|
-
`Detected deletion of ${path}, reloading ${
|
|
85
|
+
`Detected deletion of ${path}, reloading environment ${watchName}`,
|
|
80
86
|
);
|
|
81
|
-
await
|
|
87
|
+
await reloadEnvironment();
|
|
82
88
|
});
|
|
83
89
|
this.watcher.on("change", async (path) => {
|
|
84
90
|
logger.info(
|
|
85
|
-
`Detected change on ${path}, reloading ${
|
|
91
|
+
`Detected change on ${path}, reloading environment ${watchName}`,
|
|
86
92
|
);
|
|
87
|
-
await
|
|
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.
|
|
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
|
|
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
|
|
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://
|
|
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
|
|
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 {
|
package/src/mcp/handler_utils.ts
CHANGED
|
@@ -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 {
|
|
4
|
+
import { EnvironmentStore } from "../service/environment_store";
|
|
5
5
|
import {
|
|
6
6
|
PackageNotFoundError,
|
|
7
7
|
ModelNotFoundError,
|
|
8
8
|
ModelCompilationError,
|
|
9
|
-
|
|
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., '
|
|
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
|
-
|
|
126
|
-
|
|
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
|
|
132
|
-
|
|
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
|
|
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
|
|
148
|
-
errorDetails = getNotFoundError(`
|
|
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
|
|
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
|
|
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
|
-
`${
|
|
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: ${
|
|
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 (
|
|
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
|
-
|
|
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 = "/
|
|
204
|
+
let path = "/environment/";
|
|
202
205
|
|
|
203
|
-
if (components.
|
|
204
|
-
path += encodeURIComponent(components.
|
|
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://
|
|
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
|
|
package/src/mcp/mcp_constants.ts
CHANGED
|
@@ -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
|
-
`
|
|
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 {
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
|
12
|
+
* @param environmentStore The EnvironmentStore instance for handlers to access environment data.
|
|
13
13
|
*/
|
|
14
14
|
export function registerPromptCapability(
|
|
15
15
|
mcpServer: McpServer,
|
|
16
|
-
|
|
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
|
|
27
|
+
// Prepare a handler that injects the environmentStore
|
|
28
28
|
const preparedHandler = (
|
|
29
29
|
args: z.infer<typeof promptDefinition.argsSchema>,
|
|
30
|
-
) => handler(args,
|
|
30
|
+
) => handler(args, environmentStore);
|
|
31
31
|
|
|
32
32
|
// Register using prompt ID, the Zod schema shape, and the prepared handler.
|
|
33
33
|
mcpServer.prompt(
|
package/src/mcp/prompts/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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://
|
|
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:\/\/
|
|
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
|
-
|
|
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
|
-
*
|
|
39
|
+
* EnvironmentStore. Results are memoised in `compiledModelCache`.
|
|
40
40
|
*/
|
|
41
41
|
export async function getCompiledModel(
|
|
42
42
|
uri: string,
|
|
43
|
-
|
|
43
|
+
environmentStore: EnvironmentStore,
|
|
44
44
|
): Promise<ApiCompiledModel> {
|
|
45
|
-
const {
|
|
45
|
+
const { environmentName, packageName, modelPath } = parseMalloyUri(uri);
|
|
46
46
|
|
|
47
|
-
// Look up the model via the
|
|
48
|
-
const model = await
|
|
49
|
-
.
|
|
50
|
-
.then((
|
|
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" | "
|
|
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
|
-
|
|
43
|
+
environment: {
|
|
44
44
|
description: "The main workspace folder holding Packages, Models, etc.",
|
|
45
|
-
usage: "Use `ListResources` on this
|
|
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
|
+
}
|