@malloy-publisher/server 0.0.225 → 0.0.227
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/README.md +2 -11
- package/dist/app/api-doc.yaml +29 -24
- package/dist/app/assets/{EnvironmentPage-DYTeXDll.js → EnvironmentPage-DvOJ7L_b.js} +1 -1
- package/dist/app/assets/{HomePage-pDK2BPJY.js → HomePage-CXguJsXS.js} +1 -1
- package/dist/app/assets/{LightMode-C2bwGPY1.js → LightMode-ZsshUznu.js} +1 -1
- package/dist/app/assets/{MainPage-WtBulMH_.js → MainPage-BIe0VwBa.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-hMgOtflG.js → MaterializationsPage-BuZ6UJVx.js} +1 -1
- package/dist/app/assets/{ModelPage-B2N5kYII.js → ModelPage-DsPf-s8B.js} +1 -1
- package/dist/app/assets/{PackagePage-CEN90nQG.js → PackagePage-CEVNAKZa.js} +1 -1
- package/dist/app/assets/{RouteError-BG2c5Zf0.js → RouteError-Chn7lL96.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-DNfeUwEZ.js → ThemeEditorPage-DWC_FdNU.js} +1 -1
- package/dist/app/assets/{WorkbookPage-NKI1BhFS.js → WorkbookPage-CGrsFz8p.js} +1 -1
- package/dist/app/assets/{core-C6anj5c0.es-DDLHqpzt.js → core-vVgoO8IR.es-BD_THWs_.js} +1 -1
- package/dist/app/assets/{index-DMQtnaf4.js → index-BioohWQj.js} +1 -1
- package/dist/app/assets/{index-CzNqKMVl.js → index-D6YtyiJ0.js} +1 -1
- package/dist/app/assets/{index-C6gZ6sSY.js → index-DNUZpnaa.js} +4 -4
- package/dist/app/assets/{index-JXgvyZna.js → index-gEWxu09x.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +71 -15
- package/dist/package_load_worker.mjs +169 -40
- package/dist/server.mjs +782 -7050
- package/package.json +1 -4
- package/src/controller/package.controller.spec.ts +17 -17
- package/src/controller/package.controller.ts +7 -6
- package/src/health.ts +3 -8
- package/src/logger.spec.ts +193 -0
- package/src/logger.ts +115 -18
- package/src/mcp/error_messages.ts +8 -37
- package/src/mcp/handler_utils.ts +10 -129
- package/src/mcp/mcp_constants.ts +0 -16
- package/src/mcp/{agent_server.protocol.spec.ts → server.protocol.spec.ts} +14 -12
- package/src/mcp/server.ts +29 -37
- package/src/mcp/skills/build_skills_bundle.ts +1 -1
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/mcp/tools/docs_search_tool.ts +1 -1
- package/src/mcp/tools/execute_query_tool.ts +2 -2
- package/src/mcp/tools/get_context_eval.ts +3 -3
- package/src/mcp/tools/get_context_tool.spec.ts +196 -1
- package/src/mcp/tools/get_context_tool.ts +165 -67
- package/src/package_load/package_load_pool.ts +8 -1
- package/src/package_load/package_load_worker.ts +75 -24
- package/src/package_load/protocol.ts +21 -1
- package/src/package_load/rpc_wait_accountant.spec.ts +109 -0
- package/src/package_load/rpc_wait_accountant.ts +76 -0
- package/src/package_load_metrics.spec.ts +114 -0
- package/src/package_load_metrics.ts +127 -0
- package/src/pg_helpers.spec.ts +2 -206
- package/src/pg_helpers.ts +4 -120
- package/src/server.ts +0 -16
- package/src/service/build_plan.spec.ts +42 -60
- package/src/service/build_plan.ts +37 -61
- package/src/service/environment.ts +5 -1
- package/src/service/materialization_test_fixtures.ts +6 -8
- package/src/service/package.ts +165 -86
- package/src/service/package_manifest.spec.ts +22 -1
- package/src/service/package_manifest.ts +29 -0
- package/src/service/persistence_policy.spec.ts +234 -0
- package/src/test_helpers/metrics_harness.ts +40 -0
- package/tests/harness/mcp_test_setup.ts +1 -1
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +7 -31
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -6
- package/dxt/malloy_bridge.py +0 -354
- package/dxt/manifest.json +0 -22
- package/src/dto/connection.dto.spec.ts +0 -186
- package/src/dto/connection.dto.ts +0 -308
- package/src/dto/index.ts +0 -2
- package/src/dto/package.dto.spec.ts +0 -42
- package/src/dto/package.dto.ts +0 -27
- package/src/dto/validate.spec.ts +0 -76
- package/src/dto/validate.ts +0 -31
- package/src/ducklake_version.spec.ts +0 -43
- package/src/ducklake_version.ts +0 -26
- package/src/mcp/agent_server.spec.ts +0 -18
- package/src/mcp/agent_server.ts +0 -144
- package/src/mcp/prompts/handlers.ts +0 -84
- package/src/mcp/prompts/index.ts +0 -11
- package/src/mcp/prompts/prompt_definitions.ts +0 -160
- package/src/mcp/prompts/prompt_service.ts +0 -67
- package/src/mcp/prompts/utils.ts +0 -62
- package/src/mcp/resource_metadata.ts +0 -47
- package/src/mcp/resources/environment_resource.ts +0 -187
- package/src/mcp/resources/model_resource.ts +0 -155
- package/src/mcp/resources/notebook_resource.ts +0 -137
- package/src/mcp/resources/package_resource.ts +0 -373
- package/src/mcp/resources/query_resource.ts +0 -122
- package/src/mcp/resources/source_resource.ts +0 -141
- package/src/mcp/resources/view_resource.ts +0 -136
- package/src/mcp/tools/discovery_tools.ts +0 -280
- package/src/service/materialization_cron_gate.spec.ts +0 -181
- package/src/storage/BaseRepository.ts +0 -31
- package/src/storage/StorageManager.mock.ts +0 -50
- package/tests/harness/e2e.ts +0 -96
- package/tests/harness/mocks.ts +0 -39
- package/tests/harness/uris.ts +0 -31
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -655
- package/tests/integration/mcp/setup.spec.ts +0 -5
- package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
- package/tests/unit/mcp/prompt_happy.test.ts +0 -51
|
@@ -86,7 +86,7 @@ const SEARCH_DOCS_DESCRIPTION = `Search the Malloy documentation by keyword and
|
|
|
86
86
|
|
|
87
87
|
## When to use
|
|
88
88
|
- Before writing unfamiliar Malloy syntax (window functions, autobin, dialect-specific functions, rendering tags) or when a query fails with a syntax error you do not recognize.
|
|
89
|
-
- Do NOT use it to look up field or source names in a model; use malloy_getContext
|
|
89
|
+
- Do NOT use it to look up field or source names in a model; use malloy_getContext for that.
|
|
90
90
|
|
|
91
91
|
## Parameters
|
|
92
92
|
- query (required): keywords describing what you need.
|
|
@@ -20,12 +20,12 @@ const executeQueryShape = {
|
|
|
20
20
|
environmentName: z
|
|
21
21
|
.string()
|
|
22
22
|
.describe(
|
|
23
|
-
"Environment name.
|
|
23
|
+
"Environment name. Call malloy_getContext with no arguments to list the available environments.",
|
|
24
24
|
),
|
|
25
25
|
packageName: z
|
|
26
26
|
.string()
|
|
27
27
|
.describe(
|
|
28
|
-
"Package containing the model.
|
|
28
|
+
"Package containing the model. Call malloy_getContext with just environmentName to list its packages.",
|
|
29
29
|
),
|
|
30
30
|
modelPath: z.string().describe("Path to the .malloy model file"),
|
|
31
31
|
query: z.string().optional().describe("Ad-hoc Malloy query code"),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lexical-baseline eval for malloy_getContext.
|
|
3
3
|
*
|
|
4
|
-
* Runs a labeled set of plain-English queries against the live
|
|
4
|
+
* Runs a labeled set of plain-English queries against the live MCP endpoint
|
|
5
5
|
* and reports recall@K: whether the expected entity appears in the top-K results.
|
|
6
6
|
* This establishes the lexical (lunr/BM25) baseline so retrieval quality is
|
|
7
7
|
* measurable; an embeddings comparison is deferred to if/when an embedding
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* bun run packages/server/src/mcp/tools/get_context_eval.ts [K]
|
|
11
11
|
*
|
|
12
|
-
* Targets
|
|
12
|
+
* Targets MCP_URL (default http://localhost:4040/mcp) over the demo
|
|
13
13
|
* environment (EVAL_ENV, default "samples"); start the server first.
|
|
14
14
|
*/
|
|
15
15
|
|
|
@@ -44,7 +44,7 @@ const CASES: EvalCase[] = [
|
|
|
44
44
|
{ pkg: "faa", query: "departure delay", expect: "delay" },
|
|
45
45
|
];
|
|
46
46
|
|
|
47
|
-
const ENDPOINT = process.env.
|
|
47
|
+
const ENDPOINT = process.env.MCP_URL || "http://localhost:4040/mcp";
|
|
48
48
|
const EVAL_ENV = process.env.EVAL_ENV || "samples";
|
|
49
49
|
const K = Number(process.argv[2] || 5);
|
|
50
50
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { docText, sanitize } from "./get_context_tool";
|
|
2
|
+
import { docText, sanitize, registerGetContextTool } from "./get_context_tool";
|
|
3
|
+
import type { EnvironmentStore } from "../../service/environment_store";
|
|
3
4
|
|
|
4
5
|
describe("get_context docText", () => {
|
|
5
6
|
it("extracts #(doc) text from annotation lines", () => {
|
|
@@ -42,3 +43,197 @@ describe("get_context sanitize", () => {
|
|
|
42
43
|
);
|
|
43
44
|
});
|
|
44
45
|
});
|
|
46
|
+
|
|
47
|
+
// Capture the tool handler that registerGetContextTool passes to McpServer.tool,
|
|
48
|
+
// so each discovery tier can be exercised against a mocked EnvironmentStore.
|
|
49
|
+
type Handler = (params: Record<string, unknown>) => Promise<{
|
|
50
|
+
isError?: boolean;
|
|
51
|
+
content: Array<{ resource: { text: string } }>;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
function captureHandler(store: Partial<EnvironmentStore>): Handler {
|
|
55
|
+
let handler: Handler | undefined;
|
|
56
|
+
const fakeServer = {
|
|
57
|
+
tool: (_name: string, _desc: string, _shape: unknown, h: Handler) => {
|
|
58
|
+
handler = h;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
registerGetContextTool(fakeServer as never, store as EnvironmentStore);
|
|
62
|
+
if (!handler) throw new Error("handler was not registered");
|
|
63
|
+
return handler;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parse(result: { content: Array<{ resource: { text: string } }> }) {
|
|
67
|
+
return JSON.parse(result.content[0].resource.text);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A model with one source (order_items) carrying one dimension (state).
|
|
71
|
+
const mockModel = {
|
|
72
|
+
getSourceInfos: () => [
|
|
73
|
+
{
|
|
74
|
+
name: "order_items",
|
|
75
|
+
annotations: [],
|
|
76
|
+
schema: {
|
|
77
|
+
fields: [{ kind: "dimension", name: "state", annotations: [] }],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
getQueries: () => [],
|
|
82
|
+
};
|
|
83
|
+
const mockPackage = {
|
|
84
|
+
listModels: async () => [{ path: "ecommerce.malloy" }],
|
|
85
|
+
getModel: () => mockModel,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// A package with more than 10 sources, to prove the enumeration tier is not
|
|
89
|
+
// silently capped the way ranked retrieval is.
|
|
90
|
+
const manySourceNames = Array.from({ length: 12 }, (_, i) => `s${i}`);
|
|
91
|
+
const mockManySourceModel = {
|
|
92
|
+
getSourceInfos: () =>
|
|
93
|
+
manySourceNames.map((name) => ({
|
|
94
|
+
name,
|
|
95
|
+
annotations: [],
|
|
96
|
+
schema: { fields: [] },
|
|
97
|
+
})),
|
|
98
|
+
getQueries: () => [],
|
|
99
|
+
};
|
|
100
|
+
const mockManySourcePackage = {
|
|
101
|
+
listModels: async () => [{ path: "many.malloy" }],
|
|
102
|
+
getModel: () => mockManySourceModel,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
describe("get_context discovery tiers", () => {
|
|
106
|
+
it("tier 1: no environment lists environments with their package names", async () => {
|
|
107
|
+
const handler = captureHandler({
|
|
108
|
+
listEnvironments: async () =>
|
|
109
|
+
[
|
|
110
|
+
{
|
|
111
|
+
name: "malloy-samples",
|
|
112
|
+
packages: [{ name: "ecommerce" }, { name: "imdb" }],
|
|
113
|
+
},
|
|
114
|
+
] as never,
|
|
115
|
+
});
|
|
116
|
+
const { results } = parse(await handler({}));
|
|
117
|
+
expect(results).toEqual([
|
|
118
|
+
{
|
|
119
|
+
kind: "environment",
|
|
120
|
+
name: "malloy-samples",
|
|
121
|
+
packages: ["ecommerce", "imdb"],
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("tier 2: environment without a package lists the packages", async () => {
|
|
127
|
+
const handler = captureHandler({
|
|
128
|
+
getEnvironment: async () =>
|
|
129
|
+
({
|
|
130
|
+
listPackages: async () => [
|
|
131
|
+
{ name: "ecommerce", description: "Ecommerce demo" },
|
|
132
|
+
],
|
|
133
|
+
}) as never,
|
|
134
|
+
});
|
|
135
|
+
const { results } = parse(
|
|
136
|
+
await handler({ environmentName: "malloy-samples" }),
|
|
137
|
+
);
|
|
138
|
+
expect(results).toEqual([
|
|
139
|
+
{
|
|
140
|
+
kind: "package",
|
|
141
|
+
name: "ecommerce",
|
|
142
|
+
description: "Ecommerce demo",
|
|
143
|
+
environmentName: "malloy-samples",
|
|
144
|
+
},
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("tier 2: surfaces an unresolved environment as a tool error", async () => {
|
|
149
|
+
const handler = captureHandler({
|
|
150
|
+
getEnvironment: async () => {
|
|
151
|
+
throw new Error("Environment 'nope' could not be resolved");
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const result = await handler({ environmentName: "nope" });
|
|
155
|
+
expect(result.isError).toBe(true);
|
|
156
|
+
const parsed = parse(result);
|
|
157
|
+
expect(parsed.results).toEqual([]);
|
|
158
|
+
expect(parsed.error).toContain("could not be resolved");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("tier 3: package without a query lists only its sources", async () => {
|
|
162
|
+
const handler = captureHandler({
|
|
163
|
+
getEnvironment: async () =>
|
|
164
|
+
({ getPackage: async () => mockPackage }) as never,
|
|
165
|
+
});
|
|
166
|
+
const { results } = parse(
|
|
167
|
+
await handler({
|
|
168
|
+
environmentName: "malloy-samples",
|
|
169
|
+
packageName: "ecommerce",
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
expect(results).toEqual([
|
|
173
|
+
{
|
|
174
|
+
kind: "source",
|
|
175
|
+
name: "order_items",
|
|
176
|
+
source: "order_items",
|
|
177
|
+
environmentName: "malloy-samples",
|
|
178
|
+
packageName: "ecommerce",
|
|
179
|
+
modelPath: "ecommerce.malloy",
|
|
180
|
+
doc: "",
|
|
181
|
+
},
|
|
182
|
+
]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("tier 4: a query retrieves the matching entity", async () => {
|
|
186
|
+
const handler = captureHandler({
|
|
187
|
+
getEnvironment: async () =>
|
|
188
|
+
({ getPackage: async () => mockPackage }) as never,
|
|
189
|
+
});
|
|
190
|
+
const { results } = parse(
|
|
191
|
+
await handler({
|
|
192
|
+
environmentName: "malloy-samples",
|
|
193
|
+
packageName: "ecommerce",
|
|
194
|
+
query: "state",
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
expect(
|
|
198
|
+
results.some(
|
|
199
|
+
(r: { name: string; kind: string }) =>
|
|
200
|
+
r.name === "state" && r.kind === "dimension",
|
|
201
|
+
),
|
|
202
|
+
).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("tier 1: surfaces a listEnvironments failure as a tool error", async () => {
|
|
206
|
+
const handler = captureHandler({
|
|
207
|
+
listEnvironments: async () => {
|
|
208
|
+
throw new Error("environment store not initialized");
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
const result = await handler({});
|
|
212
|
+
expect(result.isError).toBe(true);
|
|
213
|
+
const parsed = parse(result);
|
|
214
|
+
expect(parsed.results).toEqual([]);
|
|
215
|
+
expect(parsed.error).toContain("not initialized");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("tier 3: lists every source, not just the first 10", async () => {
|
|
219
|
+
const handler = captureHandler({
|
|
220
|
+
getEnvironment: async () =>
|
|
221
|
+
({ getPackage: async () => mockManySourcePackage }) as never,
|
|
222
|
+
});
|
|
223
|
+
const { results } = parse(
|
|
224
|
+
await handler({ environmentName: "e", packageName: "p" }),
|
|
225
|
+
);
|
|
226
|
+
expect(results).toHaveLength(12);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("tier 3: honors an explicit limit when given", async () => {
|
|
230
|
+
const handler = captureHandler({
|
|
231
|
+
getEnvironment: async () =>
|
|
232
|
+
({ getPackage: async () => mockManySourcePackage }) as never,
|
|
233
|
+
});
|
|
234
|
+
const { results } = parse(
|
|
235
|
+
await handler({ environmentName: "e", packageName: "p", limit: 5 }),
|
|
236
|
+
);
|
|
237
|
+
expect(results).toHaveLength(5);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -24,19 +24,20 @@ interface Entity {
|
|
|
24
24
|
const getContextShape = {
|
|
25
25
|
environmentName: z
|
|
26
26
|
.string()
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
),
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Environment name. Omit to list the available environments."),
|
|
30
29
|
packageName: z
|
|
31
30
|
.string()
|
|
31
|
+
.optional()
|
|
32
32
|
.describe(
|
|
33
|
-
"Package name.
|
|
33
|
+
"Package name. Omit, with environmentName set, to list the packages in that environment.",
|
|
34
34
|
),
|
|
35
35
|
query: z
|
|
36
36
|
.string()
|
|
37
37
|
.max(500)
|
|
38
|
+
.optional()
|
|
38
39
|
.describe(
|
|
39
|
-
'Plain-English description of what you need, e.g. "revenue by product category"
|
|
40
|
+
'Plain-English description of what you need, e.g. "revenue by product category". Omit, with environmentName and packageName set, to list the package\'s sources.',
|
|
40
41
|
),
|
|
41
42
|
sourceName: z
|
|
42
43
|
.string()
|
|
@@ -50,7 +51,9 @@ const getContextShape = {
|
|
|
50
51
|
.positive()
|
|
51
52
|
.max(50)
|
|
52
53
|
.optional()
|
|
53
|
-
.describe(
|
|
54
|
+
.describe(
|
|
55
|
+
"Maximum results to return (max 50). Ranked retrieval defaults to 10; the listing tiers return everything unless you set this.",
|
|
56
|
+
),
|
|
54
57
|
};
|
|
55
58
|
type GetContextParams = z.infer<z.ZodObject<typeof getContextShape>>;
|
|
56
59
|
|
|
@@ -203,33 +206,58 @@ async function getPackageIndex(
|
|
|
203
206
|
return built;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
|
-
const GET_CONTEXT_DESCRIPTION = `
|
|
209
|
+
const GET_CONTEXT_DESCRIPTION = `Discover what a Publisher deployment exposes and retrieve the model entities most relevant to a plain-English question, so you can ground a query in what the model actually defines instead of guessing. This is the starting point when you do not yet know the environment, package, or model names.
|
|
207
210
|
|
|
208
|
-
##
|
|
209
|
-
|
|
210
|
-
-
|
|
211
|
+
## Progressive discovery
|
|
212
|
+
Call it with as much as you know and omit the rest; it answers at the appropriate level:
|
|
213
|
+
- No arguments: lists the available environments, each with its package names.
|
|
214
|
+
- environmentName only: lists the packages in that environment, with descriptions.
|
|
215
|
+
- environmentName + packageName: lists that package's sources.
|
|
216
|
+
- environmentName + packageName + query: returns the sources, views, named queries, and dimension/measure fields most relevant to the question.
|
|
211
217
|
|
|
212
218
|
## Parameters
|
|
213
|
-
- environmentName
|
|
214
|
-
-
|
|
215
|
-
-
|
|
216
|
-
-
|
|
219
|
+
- environmentName (optional): omit to list environments.
|
|
220
|
+
- packageName (optional): omit, with environmentName set, to list packages.
|
|
221
|
+
- query (optional): a plain-English description of what you need; omit, with environmentName and packageName set, to list the package's sources.
|
|
222
|
+
- sourceName (optional): narrow retrieval to entities within one source (the drill-down phase).
|
|
223
|
+
- limit (optional): cap the number of results (max 50). Retrieval defaults to 10; the listing tiers return all unless set.
|
|
217
224
|
|
|
218
225
|
## Response
|
|
219
|
-
A JSON object with results
|
|
226
|
+
A JSON object with a results array whose items carry a kind field. For retrieval, each entity has kind (source / view / query / dimension / measure), name, source, modelPath, and doc; environmentName, packageName, modelPath, and source map directly onto malloy_executeQuery parameters, and for a view or named query you pass its name as queryName with sourceName.
|
|
220
227
|
|
|
221
228
|
## Contract rules
|
|
222
|
-
- Use the names verbatim; do not invent entities not in the results.
|
|
223
|
-
-
|
|
229
|
+
- Use the names verbatim; do not invent environments, packages, or entities not in the results.
|
|
230
|
+
- Start broad and narrow down: list environments, then packages, then sources, then query.
|
|
224
231
|
|
|
225
232
|
## Worked example
|
|
226
|
-
{ "environmentName": "samples", "packageName": "ecommerce", "query": "revenue by product category" }`;
|
|
233
|
+
{ "environmentName": "malloy-samples", "packageName": "ecommerce", "query": "revenue by product category" }`;
|
|
227
234
|
|
|
228
235
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
* Wrap a JSON payload in the MCP resource-content shape every tier of this tool
|
|
237
|
+
* returns. isError marks a tool-level error (e.g. an unknown environment/package).
|
|
238
|
+
*/
|
|
239
|
+
function jsonResource(uri: string, payload: unknown, isError = false) {
|
|
240
|
+
const content = [
|
|
241
|
+
{
|
|
242
|
+
type: "resource" as const,
|
|
243
|
+
resource: {
|
|
244
|
+
type: "application/json",
|
|
245
|
+
uri,
|
|
246
|
+
text: JSON.stringify(payload),
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
return isError ? { isError: true, content } : { content };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Registers the malloy_getContext MCP tool. It is a progressive-discovery tool:
|
|
255
|
+
* with no environment it lists environments, with an environment but no package
|
|
256
|
+
* it lists packages, with a package but no query it lists the package's sources,
|
|
257
|
+
* and with a query it runs lexical (lunr/BM25) retrieval over the package's model
|
|
258
|
+
* entities (sources, views, dimension/measure fields, named queries). The entity
|
|
259
|
+
* index is built once per Package and cached (see getPackageIndex), rebuilding
|
|
260
|
+
* automatically when the package reloads.
|
|
233
261
|
*/
|
|
234
262
|
export function registerGetContextTool(
|
|
235
263
|
mcpServer: McpServer,
|
|
@@ -251,6 +279,76 @@ export function registerGetContextTool(
|
|
|
251
279
|
limit,
|
|
252
280
|
});
|
|
253
281
|
|
|
282
|
+
// Tier 1: no environment -> enumerate the available environments, each
|
|
283
|
+
// with its package names, so an agent with no prior knowledge can start.
|
|
284
|
+
if (!environmentName) {
|
|
285
|
+
try {
|
|
286
|
+
const environments = await environmentStore.listEnvironments();
|
|
287
|
+
const results = environments.map((env) => ({
|
|
288
|
+
kind: "environment" as const,
|
|
289
|
+
name: env.name,
|
|
290
|
+
packages: (env.packages ?? [])
|
|
291
|
+
.map((p) => p.name)
|
|
292
|
+
.filter((n): n is string => Boolean(n)),
|
|
293
|
+
}));
|
|
294
|
+
return jsonResource(buildMalloyUri({}, "get-context"), {
|
|
295
|
+
results,
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
const message =
|
|
299
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
300
|
+
logger.warn(
|
|
301
|
+
"[MCP Tool getContext] listing environments failed",
|
|
302
|
+
{ error: message },
|
|
303
|
+
);
|
|
304
|
+
return jsonResource(
|
|
305
|
+
buildMalloyUri({}, "get-context"),
|
|
306
|
+
{ error: message, results: [] },
|
|
307
|
+
true,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Tier 2: environment but no package -> enumerate its packages.
|
|
313
|
+
if (!packageName) {
|
|
314
|
+
try {
|
|
315
|
+
const environment = await environmentStore.getEnvironment(
|
|
316
|
+
environmentName,
|
|
317
|
+
false,
|
|
318
|
+
);
|
|
319
|
+
const packages = await environment.listPackages();
|
|
320
|
+
const results = packages.map((pkg) => ({
|
|
321
|
+
kind: "package" as const,
|
|
322
|
+
name: pkg.name,
|
|
323
|
+
description: pkg.description,
|
|
324
|
+
environmentName,
|
|
325
|
+
}));
|
|
326
|
+
return jsonResource(
|
|
327
|
+
buildMalloyUri(
|
|
328
|
+
{ environment: environmentName },
|
|
329
|
+
"get-context",
|
|
330
|
+
),
|
|
331
|
+
{ results },
|
|
332
|
+
);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const message =
|
|
335
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
336
|
+
logger.warn("[MCP Tool getContext] listing packages failed", {
|
|
337
|
+
environmentName,
|
|
338
|
+
error: message,
|
|
339
|
+
});
|
|
340
|
+
return jsonResource(
|
|
341
|
+
buildMalloyUri(
|
|
342
|
+
{ environment: environmentName },
|
|
343
|
+
"get-context",
|
|
344
|
+
),
|
|
345
|
+
{ error: message, results: [] },
|
|
346
|
+
true,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Tiers 3 and 4 need the package's entity index.
|
|
254
352
|
let pkgIndex: PackageIndex;
|
|
255
353
|
try {
|
|
256
354
|
pkgIndex = await getPackageIndex(
|
|
@@ -267,41 +365,55 @@ export function registerGetContextTool(
|
|
|
267
365
|
sourceName,
|
|
268
366
|
error: message,
|
|
269
367
|
});
|
|
270
|
-
return
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
{
|
|
279
|
-
environment: environmentName,
|
|
280
|
-
package: packageName,
|
|
281
|
-
},
|
|
282
|
-
"get-context",
|
|
283
|
-
),
|
|
284
|
-
text: JSON.stringify({ error: message, results: [] }),
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
|
-
],
|
|
288
|
-
};
|
|
368
|
+
return jsonResource(
|
|
369
|
+
buildMalloyUri(
|
|
370
|
+
{ environment: environmentName, package: packageName },
|
|
371
|
+
"get-context",
|
|
372
|
+
),
|
|
373
|
+
{ error: message, results: [] },
|
|
374
|
+
true,
|
|
375
|
+
);
|
|
289
376
|
}
|
|
290
377
|
|
|
291
378
|
const { byId, index } = pkgIndex;
|
|
379
|
+
const uri = buildMalloyUri(
|
|
380
|
+
{ environment: environmentName, package: packageName },
|
|
381
|
+
"get-context",
|
|
382
|
+
);
|
|
292
383
|
|
|
293
|
-
|
|
384
|
+
// Tier 3: package but no query -> list the package's sources as an
|
|
385
|
+
// overview the agent can then query or drill into.
|
|
386
|
+
const sanitized = query ? sanitize(query) : "";
|
|
387
|
+
if (!sanitized) {
|
|
388
|
+
// Enumeration: return every source unless the caller sets an explicit
|
|
389
|
+
// limit. slice(0, undefined) keeps the whole list, so discovery is
|
|
390
|
+
// not silently capped the way ranked retrieval (tier 4) is.
|
|
391
|
+
const results = Array.from(byId.values())
|
|
392
|
+
.filter((e) => e.kind === "source")
|
|
393
|
+
.filter((e) => !sourceName || e.source === sourceName)
|
|
394
|
+
.slice(0, limit)
|
|
395
|
+
.map((e) => ({
|
|
396
|
+
kind: e.kind,
|
|
397
|
+
name: e.name,
|
|
398
|
+
source: e.source,
|
|
399
|
+
environmentName,
|
|
400
|
+
packageName,
|
|
401
|
+
modelPath: e.modelPath,
|
|
402
|
+
doc: e.doc,
|
|
403
|
+
}));
|
|
404
|
+
return jsonResource(uri, { results });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Tier 4: retrieval over the package's entities.
|
|
294
408
|
let hits: lunr.Index.Result[] = [];
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
hits = [];
|
|
304
|
-
}
|
|
409
|
+
try {
|
|
410
|
+
hits = index.search(sanitized);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
logger.warn("[MCP Tool getContext] lunr search failed", {
|
|
413
|
+
query,
|
|
414
|
+
error: error instanceof Error ? error.message : String(error),
|
|
415
|
+
});
|
|
416
|
+
hits = [];
|
|
305
417
|
}
|
|
306
418
|
|
|
307
419
|
// Defensive: skip any hit whose ref is missing from the entity map.
|
|
@@ -321,21 +433,7 @@ export function registerGetContextTool(
|
|
|
321
433
|
doc: e.doc,
|
|
322
434
|
}));
|
|
323
435
|
|
|
324
|
-
return {
|
|
325
|
-
content: [
|
|
326
|
-
{
|
|
327
|
-
type: "resource" as const,
|
|
328
|
-
resource: {
|
|
329
|
-
type: "application/json",
|
|
330
|
-
uri: buildMalloyUri(
|
|
331
|
-
{ environment: environmentName, package: packageName },
|
|
332
|
-
"get-context",
|
|
333
|
-
),
|
|
334
|
-
text: JSON.stringify({ results }),
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
],
|
|
338
|
-
};
|
|
436
|
+
return jsonResource(uri, { results });
|
|
339
437
|
},
|
|
340
438
|
);
|
|
341
439
|
}
|
|
@@ -81,7 +81,10 @@ import { fileURLToPath, pathToFileURL } from "url";
|
|
|
81
81
|
|
|
82
82
|
import { ModelCompilationError } from "../errors";
|
|
83
83
|
import { logger } from "../logger";
|
|
84
|
-
import type {
|
|
84
|
+
import type {
|
|
85
|
+
PackageMaterializationConfig,
|
|
86
|
+
PackageScope,
|
|
87
|
+
} from "../service/package_manifest";
|
|
85
88
|
import type {
|
|
86
89
|
ConnectionMetadataRequest,
|
|
87
90
|
ConnectionMetadataResponse,
|
|
@@ -235,6 +238,7 @@ export interface LoadPackageOutcome {
|
|
|
235
238
|
queryableSources?: "declared" | "all";
|
|
236
239
|
manifestLocation?: string | null;
|
|
237
240
|
materialization?: PackageMaterializationConfig | null;
|
|
241
|
+
scope?: PackageScope;
|
|
238
242
|
};
|
|
239
243
|
models: Array<
|
|
240
244
|
Omit<SerializedModel, "modelDef" | "sourceInfos"> & {
|
|
@@ -243,6 +247,8 @@ export interface LoadPackageOutcome {
|
|
|
243
247
|
}
|
|
244
248
|
>;
|
|
245
249
|
loadDurationMs: number;
|
|
250
|
+
/** Per-load phase timing breakdown (see {@link LoadPackageResult.timings}). */
|
|
251
|
+
timings: LoadPackageResult["timings"];
|
|
246
252
|
}
|
|
247
253
|
|
|
248
254
|
// ──────────────────────────────────────────────────────────────────────
|
|
@@ -840,6 +846,7 @@ function adaptResult(result: LoadPackageResult): LoadPackageOutcome {
|
|
|
840
846
|
sourceInfos: m.sourceInfos as Malloy.SourceInfo[] | undefined,
|
|
841
847
|
})),
|
|
842
848
|
loadDurationMs: result.loadDurationMs,
|
|
849
|
+
timings: result.timings,
|
|
843
850
|
};
|
|
844
851
|
}
|
|
845
852
|
|