@malloy-publisher/server 0.0.226 → 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/package_load_worker.mjs +86 -24
- package/dist/server.mjs +655 -6994
- package/package.json +1 -4
- package/src/health.ts +3 -8
- 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 +3 -0
- package/src/package_load/package_load_worker.ts +68 -24
- package/src/package_load/protocol.ts +16 -0
- 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/environment.ts +1 -1
- package/src/service/package.ts +82 -42
- 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/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
|
@@ -1,655 +0,0 @@
|
|
|
1
|
-
/// <reference types="bun-types" />
|
|
2
|
-
|
|
3
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
-
import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
6
|
-
import {
|
|
7
|
-
cleanupE2ETestEnvironment,
|
|
8
|
-
McpE2ETestEnvironment,
|
|
9
|
-
setupE2ETestEnvironment,
|
|
10
|
-
} from "../../harness/mcp_test_setup";
|
|
11
|
-
|
|
12
|
-
// Define an interface for the expected structure of package content entries
|
|
13
|
-
interface PackageContentEntry {
|
|
14
|
-
uri?: string;
|
|
15
|
-
metadata?: {
|
|
16
|
-
description?: string;
|
|
17
|
-
// Add other expected metadata fields if known
|
|
18
|
-
};
|
|
19
|
-
// Add other expected fields if known
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// --- Test Suite ---
|
|
23
|
-
// Serial: one shared MCP client; concurrent it() blocks can interleave StreamableHTTP requests.
|
|
24
|
-
describe.serial("MCP Resource Handlers (E2E Integration)", () => {
|
|
25
|
-
let env: McpE2ETestEnvironment | null = null;
|
|
26
|
-
let mcpClient: Client;
|
|
27
|
-
|
|
28
|
-
// --- Setup/Teardown using E2E Scaffolding ---
|
|
29
|
-
beforeAll(async () => {
|
|
30
|
-
env = await setupE2ETestEnvironment();
|
|
31
|
-
mcpClient = env.mcpClient;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterAll(async () => {
|
|
35
|
-
await cleanupE2ETestEnvironment(env);
|
|
36
|
-
env = null;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// --- Test Constants ---
|
|
40
|
-
const homeProjectUri = "malloy://environment/malloy-samples";
|
|
41
|
-
const faaPackageUri = "malloy://environment/malloy-samples/package/faa";
|
|
42
|
-
const flightsModelUri =
|
|
43
|
-
"malloy://environment/malloy-samples/package/faa/models/flights.malloy";
|
|
44
|
-
const FLIGHTS_SOURCE = "flights";
|
|
45
|
-
const FLIGHTS_CARRIER_QUERY = "flights_by_carrier";
|
|
46
|
-
const FLIGHTS_MONTH_VIEW = "flights_by_month";
|
|
47
|
-
const OVERVIEW_NOTEBOOK = "overview.malloynb";
|
|
48
|
-
const nonExistentPackageUri =
|
|
49
|
-
"malloy://environment/malloy-samples/package/nonexistent";
|
|
50
|
-
const nonExistentModelUri =
|
|
51
|
-
"malloy://environment/malloy-samples/package/faa/models/nonexistent.malloy";
|
|
52
|
-
const nonExistentProjectUri = "malloy://environment/invalid_project";
|
|
53
|
-
const invalidUri = "invalid://format";
|
|
54
|
-
|
|
55
|
-
const validSourceUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/sources/${FLIGHTS_SOURCE}`;
|
|
56
|
-
const validQueryUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/queries/${FLIGHTS_CARRIER_QUERY}`;
|
|
57
|
-
const validViewUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/sources/${FLIGHTS_SOURCE}/views/${FLIGHTS_MONTH_VIEW}`;
|
|
58
|
-
const validNotebookUri = `malloy://environment/malloy-samples/package/faa/notebooks/${OVERVIEW_NOTEBOOK}`;
|
|
59
|
-
const nonExistentSourceUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/sources/non_existent_source`;
|
|
60
|
-
const nonExistentQueryUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/queries/non_existent_query`;
|
|
61
|
-
const nonExistentViewUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/sources/${FLIGHTS_SOURCE}/views/non_existent_view`;
|
|
62
|
-
const nonExistentNotebookUri = `malloy://environment/malloy-samples/package/faa/notebooks/non_existent.malloynb`;
|
|
63
|
-
|
|
64
|
-
describe("client.listResources", () => {
|
|
65
|
-
it(
|
|
66
|
-
"should list all resources, including package entries (models/notebooks/sources)",
|
|
67
|
-
async () => {
|
|
68
|
-
console.log(
|
|
69
|
-
"[TEST LOG] Starting: listResources - check package entries",
|
|
70
|
-
);
|
|
71
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
72
|
-
const result = await mcpClient.listResources({});
|
|
73
|
-
console.log(
|
|
74
|
-
"[TEST LOG] listResources result received (count):",
|
|
75
|
-
result?.resources?.length ?? 0,
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
expect(result).toBeDefined();
|
|
79
|
-
expect(result.resources).toBeDefined();
|
|
80
|
-
expect(Array.isArray(result.resources)).toBe(true);
|
|
81
|
-
expect(result.resources.length).toBeGreaterThan(0);
|
|
82
|
-
|
|
83
|
-
const faaPackageEntry = result.resources.find(
|
|
84
|
-
(r) => r.uri === faaPackageUri,
|
|
85
|
-
);
|
|
86
|
-
expect(faaPackageEntry).toBeDefined();
|
|
87
|
-
expect(faaPackageEntry?.name).toBe("faa");
|
|
88
|
-
|
|
89
|
-
// Assert shape on a known row (order of listResources is not guaranteed).
|
|
90
|
-
const sample = faaPackageEntry!;
|
|
91
|
-
expect(sample.uri).toBeDefined();
|
|
92
|
-
expect(typeof sample.uri).toBe("string");
|
|
93
|
-
expect(sample.uri).toMatch(/^malloy:\/\//);
|
|
94
|
-
expect(sample.name).toBeDefined();
|
|
95
|
-
expect(typeof sample.name).toBe("string");
|
|
96
|
-
expect(sample.description).toBeDefined();
|
|
97
|
-
expect(typeof sample.description).toBe("string");
|
|
98
|
-
if (sample.mimeType) {
|
|
99
|
-
expect(typeof sample.mimeType).toBe("string");
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
{ timeout: 30000 },
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
it("should resolve even if called with a specific URI (behavior might vary)", async () => {
|
|
106
|
-
console.log("[TEST LOG] Starting: listResources - with URI");
|
|
107
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
108
|
-
const result = await mcpClient.listResources({ uri: homeProjectUri });
|
|
109
|
-
console.log(
|
|
110
|
-
"[TEST LOG] listResources (with URI) result received (count):",
|
|
111
|
-
result?.resources?.length ?? 0,
|
|
112
|
-
);
|
|
113
|
-
expect(result).toBeDefined();
|
|
114
|
-
expect(result.resources).toBeDefined();
|
|
115
|
-
expect(Array.isArray(result.resources)).toBe(true);
|
|
116
|
-
expect(result.resources.length).toBeGreaterThan(0);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe("client.readResource (Basic Types)", () => {
|
|
121
|
-
it("should return details for the project URI", async () => {
|
|
122
|
-
console.log("[TEST LOG] Starting: readResource - project success");
|
|
123
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
124
|
-
const resource = await mcpClient.readResource({ uri: homeProjectUri });
|
|
125
|
-
console.log(
|
|
126
|
-
"[TEST LOG] readResource (project) result received:",
|
|
127
|
-
resource ? "object" : "null",
|
|
128
|
-
);
|
|
129
|
-
expect(resource).toBeDefined();
|
|
130
|
-
expect(resource.contents).toBeDefined();
|
|
131
|
-
expect(Array.isArray(resource.contents)).toBe(true);
|
|
132
|
-
expect(resource.contents).toHaveLength(1);
|
|
133
|
-
expect(resource.contents[0].type).toBe("application/json");
|
|
134
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
135
|
-
const responsePayload = JSON.parse((resource.contents[0] as any).text);
|
|
136
|
-
expect(responsePayload).toBeDefined();
|
|
137
|
-
expect(responsePayload.definition).toBeDefined();
|
|
138
|
-
expect(responsePayload.metadata).toBeDefined();
|
|
139
|
-
// Check definition content - Project name is 'malloy-samples' from URI param
|
|
140
|
-
expect(responsePayload.definition.name).toBe("malloy-samples");
|
|
141
|
-
// Check metadata content (can be more specific if needed)
|
|
142
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
143
|
-
expect(typeof responsePayload.metadata.description).toBe("string");
|
|
144
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
145
|
-
expect(typeof responsePayload.metadata.usage).toBe("string");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("should return details for a valid package URI", async () => {
|
|
149
|
-
console.log("[TEST LOG] Starting: readResource - package success");
|
|
150
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
151
|
-
const resource = await mcpClient.readResource({ uri: faaPackageUri });
|
|
152
|
-
console.log(
|
|
153
|
-
"[TEST LOG] readResource (package) result received:",
|
|
154
|
-
resource ? "object" : "null",
|
|
155
|
-
);
|
|
156
|
-
expect(resource).toBeDefined();
|
|
157
|
-
expect(resource.contents).toBeDefined();
|
|
158
|
-
expect(Array.isArray(resource.contents)).toBe(true);
|
|
159
|
-
expect(resource.contents).toHaveLength(1);
|
|
160
|
-
expect(resource.contents[0].type).toBe("application/json");
|
|
161
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
162
|
-
const responsePayload = JSON.parse((resource.contents[0] as any).text);
|
|
163
|
-
expect(responsePayload).toBeDefined();
|
|
164
|
-
expect(responsePayload.definition).toBeDefined();
|
|
165
|
-
expect(responsePayload.metadata).toBeDefined();
|
|
166
|
-
// Check definition content (matches old packageMetadata check)
|
|
167
|
-
expect(responsePayload.definition.name).toBe("faa");
|
|
168
|
-
expect(responsePayload.definition.description).toBeDefined();
|
|
169
|
-
expect(typeof responsePayload.definition.description).toBe("string");
|
|
170
|
-
// Check metadata content
|
|
171
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
-
expect(typeof responsePayload.metadata.description).toBe("string");
|
|
173
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
|
-
expect(typeof responsePayload.metadata.usage).toBe("string");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("should return a list of contained resources for a valid package contents URI", async () => {
|
|
178
|
-
console.log(
|
|
179
|
-
"[TEST LOG] Starting: readResource - package contents success",
|
|
180
|
-
);
|
|
181
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
182
|
-
const contentsUri = `${faaPackageUri}/contents`; // Construct the contents URI
|
|
183
|
-
const resource = await mcpClient.readResource({ uri: contentsUri });
|
|
184
|
-
|
|
185
|
-
console.log(
|
|
186
|
-
"[TEST DEBUG] Received Package Contents:",
|
|
187
|
-
JSON.stringify(resource, null, 2),
|
|
188
|
-
);
|
|
189
|
-
console.log(
|
|
190
|
-
"[TEST LOG] readResource (package contents) result received:",
|
|
191
|
-
resource ? "object" : "null",
|
|
192
|
-
);
|
|
193
|
-
expect(resource).toBeDefined();
|
|
194
|
-
expect(resource.isError).not.toBe(true);
|
|
195
|
-
expect(resource.contents).toBeDefined();
|
|
196
|
-
expect(Array.isArray(resource.contents)).toBe(true);
|
|
197
|
-
expect(resource.contents).toHaveLength(1);
|
|
198
|
-
expect(resource.contents[0].type).toBe("application/json");
|
|
199
|
-
|
|
200
|
-
// The content should be a JSON array of resource definitions
|
|
201
|
-
// Explicitly type the parsed array
|
|
202
|
-
const contentArray = JSON.parse(
|
|
203
|
-
(resource.contents[0] as { text: string }).text, // Use a specific type for content item
|
|
204
|
-
) as PackageContentEntry[];
|
|
205
|
-
expect(Array.isArray(contentArray)).toBe(true);
|
|
206
|
-
// Note: Package contents may be empty if models are not properly recognized
|
|
207
|
-
// This is a known issue with the package contents handler
|
|
208
|
-
expect(contentArray.length).toBeGreaterThanOrEqual(0);
|
|
209
|
-
|
|
210
|
-
// Only check for specific entries if the array is not empty
|
|
211
|
-
if (contentArray.length > 0) {
|
|
212
|
-
// Check for a specific known source entry (e.g., flights.malloy)
|
|
213
|
-
// Find the entry by URI suffix, don't assume order
|
|
214
|
-
// Remove 'any' type from entry parameter
|
|
215
|
-
const flightsEntry = contentArray.find((entry) =>
|
|
216
|
-
entry?.uri?.endsWith("/sources/flights.malloy"),
|
|
217
|
-
);
|
|
218
|
-
if (flightsEntry) {
|
|
219
|
-
expect(flightsEntry.uri).toBe(
|
|
220
|
-
"malloy://environment/malloy-samples/package/faa/sources/flights.malloy",
|
|
221
|
-
);
|
|
222
|
-
expect(flightsEntry.metadata).toBeDefined();
|
|
223
|
-
expect(flightsEntry.metadata!.description).toContain(
|
|
224
|
-
"Represents a table or dataset",
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Check for a specific known notebook entry (e.g., aircraft_analysis.malloynb)
|
|
229
|
-
// Remove 'any' type from entry parameter
|
|
230
|
-
const notebookEntry = contentArray.find((entry) =>
|
|
231
|
-
entry?.uri?.endsWith("/notebooks/aircraft_analysis.malloynb"),
|
|
232
|
-
);
|
|
233
|
-
if (notebookEntry) {
|
|
234
|
-
expect(notebookEntry.uri).toBe(
|
|
235
|
-
"malloy://environment/malloy-samples/package/faa/notebooks/aircraft_analysis.malloynb",
|
|
236
|
-
);
|
|
237
|
-
expect(notebookEntry.metadata).toBeDefined();
|
|
238
|
-
expect(notebookEntry.metadata!.description).toContain(
|
|
239
|
-
"interactive document",
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Check overall structure of the first item in the *original array*
|
|
244
|
-
const firstItem = contentArray[0];
|
|
245
|
-
expect(firstItem.uri).toBeDefined();
|
|
246
|
-
expect(typeof firstItem.uri).toBe("string");
|
|
247
|
-
expect(firstItem.metadata).toBeDefined();
|
|
248
|
-
expect(typeof firstItem.metadata!.description).toBe("string");
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it("should return details for a valid model URI", async () => {
|
|
253
|
-
console.log("[TEST LOG] Starting: readResource - model success");
|
|
254
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
255
|
-
const resource = await mcpClient.readResource({
|
|
256
|
-
uri: flightsModelUri,
|
|
257
|
-
});
|
|
258
|
-
console.log(
|
|
259
|
-
"[TEST LOG] readResource (model) result received:",
|
|
260
|
-
resource ? "object" : "null",
|
|
261
|
-
);
|
|
262
|
-
expect(resource).toBeDefined();
|
|
263
|
-
expect(resource.isError).not.toBe(true);
|
|
264
|
-
expect(resource.contents).toBeDefined();
|
|
265
|
-
expect(Array.isArray(resource.contents)).toBe(true);
|
|
266
|
-
expect(resource.contents).toHaveLength(1);
|
|
267
|
-
expect(resource.contents[0].type).toBe("application/json");
|
|
268
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
269
|
-
const responsePayload = JSON.parse((resource.contents[0] as any).text);
|
|
270
|
-
expect(responsePayload).toBeDefined();
|
|
271
|
-
expect(responsePayload.definition).toBeDefined();
|
|
272
|
-
expect(responsePayload.metadata).toBeDefined();
|
|
273
|
-
// Check definition content (like old compiledModel check)
|
|
274
|
-
expect(responsePayload.definition.modelPath).toBe("flights.malloy");
|
|
275
|
-
expect(responsePayload.definition.packageName).toBe("faa");
|
|
276
|
-
expect(responsePayload.definition.sources).toBeDefined(); // Example check
|
|
277
|
-
// Check metadata content
|
|
278
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
279
|
-
expect(typeof responsePayload.metadata.description).toBe("string");
|
|
280
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
281
|
-
expect(typeof responsePayload.metadata.usage).toBe("string");
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("should reject with InvalidParams for an invalid URI structure", async () => {
|
|
285
|
-
console.log(
|
|
286
|
-
"[TEST LOG] Starting: readResource - invalid URI structure",
|
|
287
|
-
);
|
|
288
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
289
|
-
await expect(
|
|
290
|
-
mcpClient.readResource({ uri: invalidUri }),
|
|
291
|
-
).rejects.toMatchObject({
|
|
292
|
-
code: ErrorCode.InvalidParams,
|
|
293
|
-
message: expect.stringMatching(/Resource .* not found/i),
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("should return application error response if package not found", async () => {
|
|
298
|
-
console.log("[TEST LOG] Starting: readResource - package not found");
|
|
299
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
300
|
-
const result = await mcpClient.readResource({
|
|
301
|
-
uri: nonExistentPackageUri,
|
|
302
|
-
});
|
|
303
|
-
console.log(
|
|
304
|
-
"[TEST LOG] readResource (package not found) result received:",
|
|
305
|
-
result ? "object" : "null",
|
|
306
|
-
);
|
|
307
|
-
expect(result).toBeDefined();
|
|
308
|
-
expect(result.isError).toBe(true);
|
|
309
|
-
expect(result.contents).toBeDefined();
|
|
310
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
311
|
-
expect(result.contents).toHaveLength(1);
|
|
312
|
-
expect(result.contents[0].type).toBe("application/json");
|
|
313
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
314
|
-
const responsePayload = JSON.parse((result.contents[0] as any).text);
|
|
315
|
-
expect(responsePayload).toBeDefined();
|
|
316
|
-
expect(responsePayload.error).toBeDefined();
|
|
317
|
-
expect(responsePayload.suggestions).toBeDefined();
|
|
318
|
-
expect(Array.isArray(responsePayload.suggestions)).toBe(true);
|
|
319
|
-
// Adjust regex to match the actual "Resource not found: Package..." message format
|
|
320
|
-
expect(responsePayload.error).toMatch(/^Resource not found: Package/i);
|
|
321
|
-
expect(responsePayload.suggestions.length).toBeGreaterThan(0);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("should return application error response if model not found", async () => {
|
|
325
|
-
console.log("[TEST LOG] Starting: readResource - model not found");
|
|
326
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
327
|
-
const result = await mcpClient.readResource({
|
|
328
|
-
uri: nonExistentModelUri,
|
|
329
|
-
});
|
|
330
|
-
console.log(
|
|
331
|
-
"[TEST LOG] readResource (model not found) result received:",
|
|
332
|
-
result ? "object" : "null",
|
|
333
|
-
);
|
|
334
|
-
expect(result).toBeDefined();
|
|
335
|
-
expect(result.isError).toBe(true);
|
|
336
|
-
expect(result.contents).toBeDefined();
|
|
337
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
338
|
-
expect(result.contents).toHaveLength(1);
|
|
339
|
-
expect(result.contents[0].type).toBe("application/json");
|
|
340
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
341
|
-
const responsePayload = JSON.parse((result.contents[0] as any).text);
|
|
342
|
-
expect(responsePayload).toBeDefined();
|
|
343
|
-
expect(responsePayload.error).toBeDefined();
|
|
344
|
-
expect(responsePayload.suggestions).toBeDefined();
|
|
345
|
-
expect(Array.isArray(responsePayload.suggestions)).toBe(true);
|
|
346
|
-
expect(responsePayload.error).toMatch(/Resource not found/i);
|
|
347
|
-
expect(responsePayload.suggestions.length).toBeGreaterThan(0);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it("should return application error response if project not found", async () => {
|
|
351
|
-
console.log("[TEST LOG] Starting: readResource - project not found");
|
|
352
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
353
|
-
const result = await mcpClient.readResource({
|
|
354
|
-
uri: nonExistentProjectUri,
|
|
355
|
-
});
|
|
356
|
-
console.log(
|
|
357
|
-
"[TEST LOG] readResource (project not found) result received:",
|
|
358
|
-
result ? "object" : "null",
|
|
359
|
-
);
|
|
360
|
-
expect(result).toBeDefined();
|
|
361
|
-
expect(result.isError).toBe(true);
|
|
362
|
-
expect(result.contents).toBeDefined();
|
|
363
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
364
|
-
expect(result.contents).toHaveLength(1);
|
|
365
|
-
expect(result.contents[0].type).toBe("application/json");
|
|
366
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
367
|
-
const responsePayload = JSON.parse((result.contents[0] as any).text);
|
|
368
|
-
expect(responsePayload).toBeDefined();
|
|
369
|
-
expect(responsePayload.error).toBeDefined();
|
|
370
|
-
expect(responsePayload.suggestions).toBeDefined();
|
|
371
|
-
expect(Array.isArray(responsePayload.suggestions)).toBe(true);
|
|
372
|
-
expect(responsePayload.error).toMatch(/Resource not found/i);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it("should reject for syntactically invalid URI string", async () => {
|
|
376
|
-
console.log(
|
|
377
|
-
"[TEST LOG] Starting: readResource - syntactically invalid URI",
|
|
378
|
-
);
|
|
379
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
380
|
-
const syntacticallyInvalidUri = "i am /// not valid?";
|
|
381
|
-
await expect(
|
|
382
|
-
mcpClient.readResource({ uri: syntacticallyInvalidUri }),
|
|
383
|
-
).rejects.toMatchObject({
|
|
384
|
-
code: expect.any(Number),
|
|
385
|
-
message: expect.any(String),
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it("should return application error response if package contents URI references non-existent package", async () => {
|
|
390
|
-
console.log(
|
|
391
|
-
"[TEST LOG] Starting: readResource - package contents not found",
|
|
392
|
-
);
|
|
393
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
394
|
-
const nonExistentContentsUri = `${nonExistentPackageUri}/contents`;
|
|
395
|
-
const result = await mcpClient.readResource({
|
|
396
|
-
uri: nonExistentContentsUri,
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
console.log(
|
|
400
|
-
"[TEST DEBUG] Received Package Contents (Not Found Case):",
|
|
401
|
-
JSON.stringify(result, null, 2),
|
|
402
|
-
);
|
|
403
|
-
console.log(
|
|
404
|
-
"[TEST LOG] readResource (package contents not found) result received:",
|
|
405
|
-
result ? "object" : "null",
|
|
406
|
-
);
|
|
407
|
-
expect(result).toBeDefined();
|
|
408
|
-
expect(result.isError).toBe(true);
|
|
409
|
-
expect(result.contents).toBeDefined();
|
|
410
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
411
|
-
expect(result.contents).toHaveLength(1);
|
|
412
|
-
expect(result.contents[0].type).toBe("application/json");
|
|
413
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
414
|
-
const responsePayload = JSON.parse((result.contents[0] as any).text);
|
|
415
|
-
expect(responsePayload).toBeDefined();
|
|
416
|
-
expect(responsePayload.error).toBeDefined();
|
|
417
|
-
expect(responsePayload.error).toMatch(/Resource not found/i);
|
|
418
|
-
expect(responsePayload.suggestions).toBeDefined();
|
|
419
|
-
expect(Array.isArray(responsePayload.suggestions)).toBe(true);
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
describe("client.readResource (Source/Query/View/Notebook)", () => {
|
|
424
|
-
it("should get a valid source resource", async () => {
|
|
425
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
426
|
-
const result = await mcpClient.readResource({ uri: validSourceUri });
|
|
427
|
-
expect(result.isError).not.toBe(true);
|
|
428
|
-
expect(result.contents).toBeDefined();
|
|
429
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
430
|
-
expect(result.contents).toHaveLength(1);
|
|
431
|
-
expect(result.contents[0].type).toBe("application/json");
|
|
432
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
433
|
-
const responsePayload = JSON.parse((result.contents[0] as any).text);
|
|
434
|
-
expect(responsePayload).toBeDefined();
|
|
435
|
-
expect(responsePayload.definition).toBeDefined();
|
|
436
|
-
expect(responsePayload.metadata).toBeDefined();
|
|
437
|
-
expect(responsePayload.definition.name).toBe(FLIGHTS_SOURCE);
|
|
438
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
439
|
-
expect(typeof responsePayload.metadata.description).toBe("string");
|
|
440
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
441
|
-
expect(typeof responsePayload.metadata.usage).toBe("string");
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
it("should return structured app error for a query that is not found", async () => {
|
|
445
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
446
|
-
const result = await mcpClient.readResource({ uri: validQueryUri });
|
|
447
|
-
expect(result.isError).toBe(true);
|
|
448
|
-
expect(result.contents).toBeDefined();
|
|
449
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
450
|
-
expect(result.contents).toHaveLength(1);
|
|
451
|
-
expect(result.contents?.[0]?.type).toBe("application/json");
|
|
452
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
453
|
-
const errorPayload = JSON.parse((result.contents![0] as any).text);
|
|
454
|
-
expect(errorPayload.error).toBeDefined();
|
|
455
|
-
expect(errorPayload.error).toMatch(/Resource not found/i);
|
|
456
|
-
// Check for the specific query name in the message
|
|
457
|
-
expect(errorPayload.error).toMatch(
|
|
458
|
-
new RegExp(`Query '${FLIGHTS_CARRIER_QUERY}'`),
|
|
459
|
-
);
|
|
460
|
-
expect(errorPayload.suggestions).toBeDefined();
|
|
461
|
-
expect(Array.isArray(errorPayload.suggestions)).toBe(true);
|
|
462
|
-
expect(errorPayload.suggestions.length).toBeGreaterThan(0);
|
|
463
|
-
expect(errorPayload.suggestions[0]).toContain(
|
|
464
|
-
"Verify the identifier or URI",
|
|
465
|
-
); // Check suggestion content
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it("should return structured app error for a view that is not found", async () => {
|
|
469
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
470
|
-
const result = await mcpClient.readResource({ uri: validViewUri });
|
|
471
|
-
expect(result.isError).toBe(true);
|
|
472
|
-
expect(result.contents).toBeDefined();
|
|
473
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
474
|
-
expect(result.contents).toHaveLength(1);
|
|
475
|
-
expect(result.contents?.[0]?.type).toBe("application/json");
|
|
476
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
477
|
-
const errorPayload = JSON.parse((result.contents![0] as any).text);
|
|
478
|
-
expect(errorPayload.error).toBeDefined();
|
|
479
|
-
expect(errorPayload.error).toMatch(/Resource not found/i);
|
|
480
|
-
// Check for the specific view name in the message
|
|
481
|
-
expect(errorPayload.error).toMatch(
|
|
482
|
-
new RegExp(`View '${FLIGHTS_MONTH_VIEW}'`),
|
|
483
|
-
);
|
|
484
|
-
expect(errorPayload.suggestions).toBeDefined();
|
|
485
|
-
expect(Array.isArray(errorPayload.suggestions)).toBe(true);
|
|
486
|
-
expect(errorPayload.suggestions.length).toBeGreaterThan(0);
|
|
487
|
-
expect(errorPayload.suggestions[0]).toContain(
|
|
488
|
-
"Verify the identifier or URI",
|
|
489
|
-
); // Check suggestion content
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it("should return structured app error for a notebook that is not found or invalid", async () => {
|
|
493
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
494
|
-
const result = await mcpClient.readResource({ uri: validNotebookUri });
|
|
495
|
-
expect(result.isError).toBe(true);
|
|
496
|
-
expect(result.contents).toBeDefined();
|
|
497
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
498
|
-
expect(result.contents).toHaveLength(1);
|
|
499
|
-
expect(result.contents?.[0]?.type).toBe("application/json");
|
|
500
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
501
|
-
const errorPayload = JSON.parse((result.contents![0] as any).text);
|
|
502
|
-
expect(errorPayload.error).toBeDefined();
|
|
503
|
-
expect(errorPayload.error).toMatch(/Resource not found/i);
|
|
504
|
-
// Check for the specific notebook name and context in the message
|
|
505
|
-
// Adjust test to expect the generic "Resource not found" error, as the specific
|
|
506
|
-
// "not a notebook" detail isn't easily surfaced in the standard error format.
|
|
507
|
-
expect(errorPayload.error).toMatch(/Notebook 'overview.malloynb'/);
|
|
508
|
-
expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
|
|
509
|
-
expect(errorPayload.suggestions).toBeDefined();
|
|
510
|
-
expect(Array.isArray(errorPayload.suggestions)).toBe(true);
|
|
511
|
-
expect(errorPayload.suggestions.length).toBeGreaterThan(0);
|
|
512
|
-
expect(errorPayload.suggestions[0]).toContain(
|
|
513
|
-
"Verify the identifier or URI",
|
|
514
|
-
);
|
|
515
|
-
// This case might also hit ModelCompilationError if the .malloynb exists but is invalid
|
|
516
|
-
// Adding a check for compilation-related suggestions could be useful if applicable
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
it("should return structured app error for non-existent source", async () => {
|
|
520
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
521
|
-
const result = await mcpClient.readResource({
|
|
522
|
-
uri: nonExistentSourceUri,
|
|
523
|
-
});
|
|
524
|
-
expect(result.isError).toBe(true);
|
|
525
|
-
expect(result.contents).toBeDefined();
|
|
526
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
527
|
-
expect(result.contents).toHaveLength(1);
|
|
528
|
-
expect(result.contents?.[0]?.type).toBe("application/json");
|
|
529
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
530
|
-
const errorPayload = JSON.parse((result.contents![0] as any).text);
|
|
531
|
-
expect(errorPayload.error).toBeDefined();
|
|
532
|
-
expect(errorPayload.error).toMatch(/Resource not found/i);
|
|
533
|
-
// Check for the specific source name in the message
|
|
534
|
-
expect(errorPayload.error).toMatch(/Source 'non_existent_source'/);
|
|
535
|
-
expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
|
|
536
|
-
expect(errorPayload.suggestions).toBeDefined();
|
|
537
|
-
expect(Array.isArray(errorPayload.suggestions)).toBe(true);
|
|
538
|
-
expect(errorPayload.suggestions.length).toBeGreaterThan(0);
|
|
539
|
-
expect(errorPayload.suggestions[0]).toContain(
|
|
540
|
-
"Verify the identifier or URI",
|
|
541
|
-
);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it("should return structured app error for non-existent query", async () => {
|
|
545
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
546
|
-
const result = await mcpClient.readResource({
|
|
547
|
-
uri: nonExistentQueryUri,
|
|
548
|
-
});
|
|
549
|
-
expect(result.isError).toBe(true);
|
|
550
|
-
expect(result.contents).toBeDefined();
|
|
551
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
552
|
-
expect(result.contents).toHaveLength(1);
|
|
553
|
-
expect(result.contents?.[0]?.type).toBe("application/json");
|
|
554
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
555
|
-
const errorPayload = JSON.parse((result.contents![0] as any).text);
|
|
556
|
-
expect(errorPayload.error).toBeDefined();
|
|
557
|
-
expect(errorPayload.error).toMatch(/Resource not found/i);
|
|
558
|
-
// Check for the specific query name in the message
|
|
559
|
-
expect(errorPayload.error).toMatch(/Query 'non_existent_query'/);
|
|
560
|
-
expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
|
|
561
|
-
expect(errorPayload.suggestions).toBeDefined();
|
|
562
|
-
expect(Array.isArray(errorPayload.suggestions)).toBe(true);
|
|
563
|
-
expect(errorPayload.suggestions.length).toBeGreaterThan(0);
|
|
564
|
-
expect(errorPayload.suggestions[0]).toContain(
|
|
565
|
-
"Verify the identifier or URI",
|
|
566
|
-
);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
it("should return structured app error for non-existent view", async () => {
|
|
570
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
571
|
-
const result = await mcpClient.readResource({
|
|
572
|
-
uri: nonExistentViewUri,
|
|
573
|
-
});
|
|
574
|
-
expect(result.isError).toBe(true);
|
|
575
|
-
expect(result.contents).toBeDefined();
|
|
576
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
577
|
-
expect(result.contents).toHaveLength(1);
|
|
578
|
-
expect(result.contents?.[0]?.type).toBe("application/json");
|
|
579
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
580
|
-
const errorPayload = JSON.parse((result.contents![0] as any).text);
|
|
581
|
-
expect(errorPayload.error).toBeDefined();
|
|
582
|
-
expect(errorPayload.error).toMatch(/Resource not found/i);
|
|
583
|
-
// Check for the specific view name in the message
|
|
584
|
-
expect(errorPayload.error).toMatch(/View 'non_existent_view'/);
|
|
585
|
-
expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
|
|
586
|
-
expect(errorPayload.suggestions).toBeDefined();
|
|
587
|
-
expect(Array.isArray(errorPayload.suggestions)).toBe(true);
|
|
588
|
-
expect(errorPayload.suggestions.length).toBeGreaterThan(0);
|
|
589
|
-
expect(errorPayload.suggestions[0]).toContain(
|
|
590
|
-
"Verify the identifier or URI",
|
|
591
|
-
);
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
it("should return structured app error for non-existent notebook", async () => {
|
|
595
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
596
|
-
const result = await mcpClient.readResource({
|
|
597
|
-
uri: nonExistentNotebookUri,
|
|
598
|
-
});
|
|
599
|
-
expect(result.isError).toBe(true);
|
|
600
|
-
expect(result.contents).toBeDefined();
|
|
601
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
602
|
-
expect(result.contents).toHaveLength(1);
|
|
603
|
-
expect(result.contents?.[0]?.type).toBe("application/json");
|
|
604
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
605
|
-
const errorPayload = JSON.parse((result.contents![0] as any).text);
|
|
606
|
-
expect(errorPayload.error).toBeDefined();
|
|
607
|
-
expect(errorPayload.error).toMatch(/Resource not found/i);
|
|
608
|
-
// Check for the specific notebook name and context in the message
|
|
609
|
-
// Adjust test to expect the generic "Resource not found" error
|
|
610
|
-
expect(errorPayload.error).toMatch(/Notebook 'non_existent.malloynb'/);
|
|
611
|
-
expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
|
|
612
|
-
expect(errorPayload.suggestions).toBeDefined();
|
|
613
|
-
expect(Array.isArray(errorPayload.suggestions)).toBe(true);
|
|
614
|
-
expect(errorPayload.suggestions.length).toBeGreaterThan(0);
|
|
615
|
-
expect(errorPayload.suggestions[0]).toContain(
|
|
616
|
-
"Verify the identifier or URI",
|
|
617
|
-
);
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
it("should return structured app error when requesting view from wrong source", async () => {
|
|
621
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
622
|
-
const wrongSourceUri = `malloy://environment/malloy-samples/package/faa/models/flights.malloy/sources/aircraft/views/${FLIGHTS_MONTH_VIEW}`;
|
|
623
|
-
const result = await mcpClient.readResource({ uri: wrongSourceUri });
|
|
624
|
-
expect(result.isError).toBe(true);
|
|
625
|
-
expect(result.contents).toBeDefined();
|
|
626
|
-
expect(Array.isArray(result.contents)).toBe(true);
|
|
627
|
-
expect(result.contents).toHaveLength(1);
|
|
628
|
-
expect(result.contents?.[0]?.type).toBe("application/json");
|
|
629
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
630
|
-
const errorPayload = JSON.parse((result.contents![0] as any).text);
|
|
631
|
-
expect(errorPayload.error).toBeDefined();
|
|
632
|
-
expect(errorPayload.error).toMatch(/Resource not found/i);
|
|
633
|
-
expect(errorPayload.error).toMatch(/View 'flights_by_month'/);
|
|
634
|
-
expect(errorPayload.error).toMatch(/source 'aircraft'/);
|
|
635
|
-
expect(errorPayload.error).toMatch(/environment 'malloy-samples'/);
|
|
636
|
-
expect(errorPayload.suggestions).toBeDefined();
|
|
637
|
-
expect(Array.isArray(errorPayload.suggestions)).toBe(true);
|
|
638
|
-
expect(errorPayload.suggestions.length).toBe(3);
|
|
639
|
-
expect(errorPayload.suggestions[0]).toContain(
|
|
640
|
-
"Verify the identifier or URI",
|
|
641
|
-
);
|
|
642
|
-
expect(errorPayload.suggestions[0]).toContain("flights_by_month");
|
|
643
|
-
expect(errorPayload.suggestions[0]).toContain("is spelled correctly");
|
|
644
|
-
expect(errorPayload.suggestions[0]).toContain(
|
|
645
|
-
"Check capitalization and path separators",
|
|
646
|
-
);
|
|
647
|
-
expect(errorPayload.suggestions[1]).toContain(
|
|
648
|
-
"If using a URI, ensure it follows the correct format",
|
|
649
|
-
);
|
|
650
|
-
expect(errorPayload.suggestions[2]).toContain(
|
|
651
|
-
"Check if the resource exists and is correctly named",
|
|
652
|
-
);
|
|
653
|
-
});
|
|
654
|
-
});
|
|
655
|
-
});
|