@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.
Files changed (98) hide show
  1. package/README.md +2 -11
  2. package/dist/app/api-doc.yaml +29 -24
  3. package/dist/app/assets/{EnvironmentPage-DYTeXDll.js → EnvironmentPage-DvOJ7L_b.js} +1 -1
  4. package/dist/app/assets/{HomePage-pDK2BPJY.js → HomePage-CXguJsXS.js} +1 -1
  5. package/dist/app/assets/{LightMode-C2bwGPY1.js → LightMode-ZsshUznu.js} +1 -1
  6. package/dist/app/assets/{MainPage-WtBulMH_.js → MainPage-BIe0VwBa.js} +2 -2
  7. package/dist/app/assets/{MaterializationsPage-hMgOtflG.js → MaterializationsPage-BuZ6UJVx.js} +1 -1
  8. package/dist/app/assets/{ModelPage-B2N5kYII.js → ModelPage-DsPf-s8B.js} +1 -1
  9. package/dist/app/assets/{PackagePage-CEN90nQG.js → PackagePage-CEVNAKZa.js} +1 -1
  10. package/dist/app/assets/{RouteError-BG2c5Zf0.js → RouteError-Chn7lL96.js} +1 -1
  11. package/dist/app/assets/{ThemeEditorPage-DNfeUwEZ.js → ThemeEditorPage-DWC_FdNU.js} +1 -1
  12. package/dist/app/assets/{WorkbookPage-NKI1BhFS.js → WorkbookPage-CGrsFz8p.js} +1 -1
  13. package/dist/app/assets/{core-C6anj5c0.es-DDLHqpzt.js → core-vVgoO8IR.es-BD_THWs_.js} +1 -1
  14. package/dist/app/assets/{index-DMQtnaf4.js → index-BioohWQj.js} +1 -1
  15. package/dist/app/assets/{index-CzNqKMVl.js → index-D6YtyiJ0.js} +1 -1
  16. package/dist/app/assets/{index-C6gZ6sSY.js → index-DNUZpnaa.js} +4 -4
  17. package/dist/app/assets/{index-JXgvyZna.js → index-gEWxu09x.js} +1 -1
  18. package/dist/app/index.html +1 -1
  19. package/dist/instrumentation.mjs +71 -15
  20. package/dist/package_load_worker.mjs +169 -40
  21. package/dist/server.mjs +782 -7050
  22. package/package.json +1 -4
  23. package/src/controller/package.controller.spec.ts +17 -17
  24. package/src/controller/package.controller.ts +7 -6
  25. package/src/health.ts +3 -8
  26. package/src/logger.spec.ts +193 -0
  27. package/src/logger.ts +115 -18
  28. package/src/mcp/error_messages.ts +8 -37
  29. package/src/mcp/handler_utils.ts +10 -129
  30. package/src/mcp/mcp_constants.ts +0 -16
  31. package/src/mcp/{agent_server.protocol.spec.ts → server.protocol.spec.ts} +14 -12
  32. package/src/mcp/server.ts +29 -37
  33. package/src/mcp/skills/build_skills_bundle.ts +1 -1
  34. package/src/mcp/skills/skills_bundle.json +1 -1
  35. package/src/mcp/tools/docs_search_tool.ts +1 -1
  36. package/src/mcp/tools/execute_query_tool.ts +2 -2
  37. package/src/mcp/tools/get_context_eval.ts +3 -3
  38. package/src/mcp/tools/get_context_tool.spec.ts +196 -1
  39. package/src/mcp/tools/get_context_tool.ts +165 -67
  40. package/src/package_load/package_load_pool.ts +8 -1
  41. package/src/package_load/package_load_worker.ts +75 -24
  42. package/src/package_load/protocol.ts +21 -1
  43. package/src/package_load/rpc_wait_accountant.spec.ts +109 -0
  44. package/src/package_load/rpc_wait_accountant.ts +76 -0
  45. package/src/package_load_metrics.spec.ts +114 -0
  46. package/src/package_load_metrics.ts +127 -0
  47. package/src/pg_helpers.spec.ts +2 -206
  48. package/src/pg_helpers.ts +4 -120
  49. package/src/server.ts +0 -16
  50. package/src/service/build_plan.spec.ts +42 -60
  51. package/src/service/build_plan.ts +37 -61
  52. package/src/service/environment.ts +5 -1
  53. package/src/service/materialization_test_fixtures.ts +6 -8
  54. package/src/service/package.ts +165 -86
  55. package/src/service/package_manifest.spec.ts +22 -1
  56. package/src/service/package_manifest.ts +29 -0
  57. package/src/service/persistence_policy.spec.ts +234 -0
  58. package/src/test_helpers/metrics_harness.ts +40 -0
  59. package/tests/harness/mcp_test_setup.ts +1 -1
  60. package/tests/integration/mcp/mcp_transport.integration.spec.ts +7 -31
  61. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -6
  62. package/dxt/malloy_bridge.py +0 -354
  63. package/dxt/manifest.json +0 -22
  64. package/src/dto/connection.dto.spec.ts +0 -186
  65. package/src/dto/connection.dto.ts +0 -308
  66. package/src/dto/index.ts +0 -2
  67. package/src/dto/package.dto.spec.ts +0 -42
  68. package/src/dto/package.dto.ts +0 -27
  69. package/src/dto/validate.spec.ts +0 -76
  70. package/src/dto/validate.ts +0 -31
  71. package/src/ducklake_version.spec.ts +0 -43
  72. package/src/ducklake_version.ts +0 -26
  73. package/src/mcp/agent_server.spec.ts +0 -18
  74. package/src/mcp/agent_server.ts +0 -144
  75. package/src/mcp/prompts/handlers.ts +0 -84
  76. package/src/mcp/prompts/index.ts +0 -11
  77. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  78. package/src/mcp/prompts/prompt_service.ts +0 -67
  79. package/src/mcp/prompts/utils.ts +0 -62
  80. package/src/mcp/resource_metadata.ts +0 -47
  81. package/src/mcp/resources/environment_resource.ts +0 -187
  82. package/src/mcp/resources/model_resource.ts +0 -155
  83. package/src/mcp/resources/notebook_resource.ts +0 -137
  84. package/src/mcp/resources/package_resource.ts +0 -373
  85. package/src/mcp/resources/query_resource.ts +0 -122
  86. package/src/mcp/resources/source_resource.ts +0 -141
  87. package/src/mcp/resources/view_resource.ts +0 -136
  88. package/src/mcp/tools/discovery_tools.ts +0 -280
  89. package/src/service/materialization_cron_gate.spec.ts +0 -181
  90. package/src/storage/BaseRepository.ts +0 -31
  91. package/src/storage/StorageManager.mock.ts +0 -50
  92. package/tests/harness/e2e.ts +0 -96
  93. package/tests/harness/mocks.ts +0 -39
  94. package/tests/harness/uris.ts +0 -31
  95. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -655
  96. package/tests/integration/mcp/setup.spec.ts +0 -5
  97. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  98. 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 or malloy_modelGetText for that.
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. Names are listed in the malloy resource list.",
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. Package names are listed in the malloy resource list.",
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 agent MCP endpoint
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 AGENT_MCP_URL (default http://localhost:4041/mcp) over the demo
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.AGENT_MCP_URL || "http://localhost:4041/mcp";
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
- .describe(
28
- "Environment name. Names are listed in the malloy_environmentList tool.",
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. Package names are listed in malloy_packageList.",
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" or "customer churn".',
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("Maximum number of entities to return. Default 10, max 50."),
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 = `Retrieve the model entities (sources, views, named queries, and dimension/measure fields) most relevant to a plain-English question, so you can ground a query in what the model actually defines instead of guessing.
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
- ## When to use
209
- - Before writing a query, to find which source and which prebuilt views fit the question.
210
- - Two-phase: call once with just a query to discover the most relevant sources and views (discovery), then optionally call again with sourceName set to focus on one source (drill-down).
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, packageName (required): where to look.
214
- - query (required): a plain-English description of what you need.
215
- - sourceName (optional): narrow to entities within one source.
216
- - limit (optional): maximum entities to return; default 10.
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: a ranked list of entities, each with kind (source / view / query / dimension / measure), name, source, modelPath, and doc. The environmentName, packageName, modelPath, and source map directly onto malloy_executeQuery parameters; for a view or named query, pass its name as queryName with sourceName.
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
- - Results include field-level dimensions and measures. If you need the full field list of a source or want to confirm exact spelling, read the model with malloy_modelGetText on the modelPath this returns.
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
- * Registers the malloy_getContext MCP tool: lexical (lunr/BM25) retrieval over a
230
- * package's model entities (sources, views, dimension/measure fields, named
231
- * queries). The entity index is built once per Package and cached (see
232
- * getPackageIndex), rebuilding automatically when the package reloads.
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
- isError: true,
272
- content: [
273
- {
274
- type: "resource" as const,
275
- resource: {
276
- type: "application/json",
277
- uri: buildMalloyUri(
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
- const sanitized = sanitize(query);
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
- if (sanitized) {
296
- try {
297
- hits = index.search(sanitized);
298
- } catch (error) {
299
- logger.warn("[MCP Tool getContext] lunr search failed", {
300
- query,
301
- error: error instanceof Error ? error.message : String(error),
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 { PackageMaterializationConfig } from "../service/package_manifest";
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