@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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/server",
|
|
3
3
|
"description": "Malloy Publisher Server",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.227",
|
|
5
5
|
"main": "dist/server.mjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"malloy-publisher": "dist/server.mjs"
|
|
@@ -58,8 +58,6 @@
|
|
|
58
58
|
"aws-sdk": "^2.1692.0",
|
|
59
59
|
"body-parser": "^1.20.2",
|
|
60
60
|
"chokidar": "^4.0.3",
|
|
61
|
-
"class-transformer": "^0.5.1",
|
|
62
|
-
"class-validator": "^0.14.1",
|
|
63
61
|
"cors": "^2.8.5",
|
|
64
62
|
"express": "^4.21.0",
|
|
65
63
|
"extract-zip": "^2.0.1",
|
|
@@ -74,7 +72,6 @@
|
|
|
74
72
|
"uuid": "^11.0.3"
|
|
75
73
|
},
|
|
76
74
|
"devDependencies": {
|
|
77
|
-
"@anthropic-ai/dxt": "^0.1.0",
|
|
78
75
|
"@eslint/compat": "^1.2.7",
|
|
79
76
|
"@eslint/eslintrc": "^3.3.1",
|
|
80
77
|
"@eslint/js": "^9.23.0",
|
package/src/health.ts
CHANGED
|
@@ -74,10 +74,10 @@ export function registerSignalHandlers(
|
|
|
74
74
|
mcpServer: Server,
|
|
75
75
|
shutdownDrainDurationSeconds: number = 0,
|
|
76
76
|
shutdownGracefulCloseTimeoutSeconds: number = 0,
|
|
77
|
-
agentMcpServer?: Server,
|
|
78
77
|
): void {
|
|
79
|
-
//
|
|
80
|
-
// K8s will SIGKILL after terminationGracePeriodSeconds
|
|
78
|
+
// On SIGTERM, drain in-flight requests before closing the servers (see the
|
|
79
|
+
// shutdown sequence above). K8s will SIGKILL after terminationGracePeriodSeconds
|
|
80
|
+
// if this handler does not exit first.
|
|
81
81
|
process.once("SIGTERM", async () => {
|
|
82
82
|
logger.info("========== SIGTERM RECEIVED ==========");
|
|
83
83
|
markNotReady();
|
|
@@ -97,7 +97,6 @@ export function registerSignalHandlers(
|
|
|
97
97
|
server,
|
|
98
98
|
mcpServer,
|
|
99
99
|
shutdownGracefulCloseTimeoutSeconds,
|
|
100
|
-
agentMcpServer,
|
|
101
100
|
);
|
|
102
101
|
});
|
|
103
102
|
}
|
|
@@ -116,7 +115,6 @@ export async function performGracefulShutdownAfterDrain(
|
|
|
116
115
|
server: Server,
|
|
117
116
|
mcpServer: Server,
|
|
118
117
|
shutdownGracefulCloseTimeoutSeconds: number,
|
|
119
|
-
agentMcpServer?: Server,
|
|
120
118
|
): Promise<void> {
|
|
121
119
|
const closeServer = (server: Server, name: string) =>
|
|
122
120
|
new Promise<void>((resolve) => {
|
|
@@ -137,9 +135,6 @@ export async function performGracefulShutdownAfterDrain(
|
|
|
137
135
|
await Promise.all([
|
|
138
136
|
closeServer(server, "Main server"),
|
|
139
137
|
closeServer(mcpServer, "MCP server"),
|
|
140
|
-
...(agentMcpServer
|
|
141
|
-
? [closeServer(agentMcpServer, "Agent MCP server")]
|
|
142
|
-
: []),
|
|
143
138
|
]);
|
|
144
139
|
|
|
145
140
|
try {
|
|
@@ -3,31 +3,6 @@ export interface ErrorDetails {
|
|
|
3
3
|
suggestions: string[];
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
export interface InvalidParamDetail {
|
|
7
|
-
name: string;
|
|
8
|
-
reason: string;
|
|
9
|
-
value?: unknown;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getInvalidParamsError(
|
|
13
|
-
params: InvalidParamDetail[],
|
|
14
|
-
): ErrorDetails {
|
|
15
|
-
const reasons = params
|
|
16
|
-
.map(
|
|
17
|
-
(p) =>
|
|
18
|
-
`${p.name}: ${p.reason}${p.value !== undefined ? ` (value: ${JSON.stringify(p.value)})` : ""}`,
|
|
19
|
-
)
|
|
20
|
-
.join("; ");
|
|
21
|
-
return {
|
|
22
|
-
message: `Invalid parameter(s) provided. ${reasons}`,
|
|
23
|
-
suggestions: [
|
|
24
|
-
"Check the parameter names and values against the prompt's argument definition.",
|
|
25
|
-
"Ensure all required parameters are provided and have the correct data types.",
|
|
26
|
-
"For URI parameters, ensure they are correctly formatted.",
|
|
27
|
-
],
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
6
|
/**
|
|
32
7
|
* Generates error details for a resource not found scenario.
|
|
33
8
|
* @param resourceUriOrContext The URI that was not found, or a context string (e.g., "Package 'X'", "Model 'Y' in package 'X'").
|
|
@@ -36,13 +11,9 @@ export function getInvalidParamsError(
|
|
|
36
11
|
export function getNotFoundError(resourceUriOrContext: string): ErrorDetails {
|
|
37
12
|
const baseMessage = `Resource not found: ${resourceUriOrContext}`;
|
|
38
13
|
const suggestions: string[] = [
|
|
39
|
-
`Verify the identifier
|
|
40
|
-
`If using a URI, ensure it follows the correct format (e.g., malloy://environment/...) and includes the right path segments (e.g., /models/, /sources/, /queries/, /views/).`,
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
suggestions.push(
|
|
14
|
+
`Verify the identifier (${resourceUriOrContext}) is spelled correctly and exists. Check capitalization and path separators.`,
|
|
44
15
|
"Check if the resource exists and is correctly named in your Malloy environment structure or the specific model file.",
|
|
45
|
-
|
|
16
|
+
];
|
|
46
17
|
|
|
47
18
|
return {
|
|
48
19
|
message: baseMessage,
|
|
@@ -52,7 +23,7 @@ export function getNotFoundError(resourceUriOrContext: string): ErrorDetails {
|
|
|
52
23
|
|
|
53
24
|
/**
|
|
54
25
|
* Generates generic error details for internal server errors.
|
|
55
|
-
* @param operation The operation that failed (e.g., '
|
|
26
|
+
* @param operation The operation that failed (e.g., 'executeQuery').
|
|
56
27
|
* @param error Optional: The underlying error object or message.
|
|
57
28
|
* @returns ErrorDetails object.
|
|
58
29
|
*/
|
|
@@ -73,7 +44,7 @@ export function getInternalError(
|
|
|
73
44
|
|
|
74
45
|
/**
|
|
75
46
|
* Generates detailed error information for Malloy compilation or query execution errors.
|
|
76
|
-
* @param operation The operation that failed (e.g., '
|
|
47
|
+
* @param operation The operation that failed (e.g., 'executeQuery (load model)', 'executeQuery').
|
|
77
48
|
* @param modelIdentifier A path or URI identifying the model/notebook involved.
|
|
78
49
|
* @param error The underlying Malloy error object or other error.
|
|
79
50
|
* @returns ErrorDetails object.
|
|
@@ -144,21 +115,21 @@ export function getMalloyErrorDetails(
|
|
|
144
115
|
refined = true;
|
|
145
116
|
const [, viewName, sourceName] = viewNotFoundMatch;
|
|
146
117
|
suggestions.unshift(
|
|
147
|
-
`Suggestion: View '${viewName}' was not found in source '${sourceName}'. Check the view name spelling or
|
|
118
|
+
`Suggestion: View '${viewName}' was not found in source '${sourceName}'. Check the view name spelling or call malloy_getContext with sourceName '${sourceName}' to see the list of available views. Views are defined within sources like 'source: ${sourceName} is ... extend { view: ${viewName} is { ... } }'.`,
|
|
148
119
|
);
|
|
149
120
|
} else if (sourceNotFoundMatch) {
|
|
150
121
|
refined = true;
|
|
151
122
|
const [, sourceName] = sourceNotFoundMatch;
|
|
152
123
|
suggestions.unshift(
|
|
153
124
|
`Suggestion: Source '${sourceName}' was not found or could not be accessed. Verify its definition (e.g., \`source: ${sourceName} is table('...')\` or \`duckdb.sql("...")\`) and ensure any associated connections are valid.`,
|
|
154
|
-
`Suggestion: Check the spelling of '${sourceName}'. You can list sources in the
|
|
125
|
+
`Suggestion: Check the spelling of '${sourceName}'. You can list the sources in the package using malloy_getContext.`,
|
|
155
126
|
);
|
|
156
127
|
} else if (queryNotFoundMatch) {
|
|
157
128
|
refined = true;
|
|
158
129
|
const [, queryName] = queryNotFoundMatch;
|
|
159
130
|
suggestions.unshift(
|
|
160
131
|
`Suggestion: Named query '${queryName}' was not found. Verify its definition (e.g., \`query: ${queryName} is source_name -> { ... }\`) within the model '${modelIdentifier}'.`,
|
|
161
|
-
`Suggestion: Check the spelling of '${queryName}'. Ensure it's a named query (defined with \`query:\`), not a view. You can list named queries using
|
|
132
|
+
`Suggestion: Check the spelling of '${queryName}'. Ensure it's a named query (defined with \`query:\`), not a view. You can list named queries using malloy_getContext.`,
|
|
162
133
|
);
|
|
163
134
|
} else if (fieldNotFoundMatch) {
|
|
164
135
|
refined = true;
|
|
@@ -166,7 +137,7 @@ export function getMalloyErrorDetails(
|
|
|
166
137
|
fieldNotFoundMatch;
|
|
167
138
|
suggestions.unshift(
|
|
168
139
|
`Suggestion: Field '${fieldName}' was not found in ${fieldContextType} '${fieldContextName}'. Check the spelling of the field name within the definition of '${fieldContextName}'.`,
|
|
169
|
-
`Suggestion: Ensure the field is defined directly or inherited correctly in the '${fieldContextName}' ${fieldContextType}. You can inspect the ${fieldContextType}
|
|
140
|
+
`Suggestion: Ensure the field is defined directly or inherited correctly in the '${fieldContextName}' ${fieldContextType}. You can inspect the ${fieldContextType}'s fields using malloy_getContext.`,
|
|
170
141
|
);
|
|
171
142
|
} else if (referenceErrorMatch) {
|
|
172
143
|
refined = true;
|
package/src/mcp/handler_utils.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { URL } from "url";
|
|
2
|
-
import type { ResourceMetadata } from "@modelcontextprotocol/sdk/server/mcp";
|
|
3
|
-
import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
1
|
import { EnvironmentStore } from "../service/environment_store";
|
|
5
2
|
import {
|
|
6
3
|
AccessDeniedError,
|
|
@@ -19,105 +16,6 @@ import {
|
|
|
19
16
|
import type { Model } from "../service/model";
|
|
20
17
|
import { logger } from "../logger";
|
|
21
18
|
|
|
22
|
-
// Custom error to wrap specific GetResource application errors
|
|
23
|
-
export class McpGetResourceError extends Error {
|
|
24
|
-
details: ErrorDetails;
|
|
25
|
-
|
|
26
|
-
constructor(details: ErrorDetails) {
|
|
27
|
-
super(details.message); // Pass message to the base Error constructor
|
|
28
|
-
this.name = "McpGetResourceError"; // Custom error name
|
|
29
|
-
this.details = details; // Store the structured details
|
|
30
|
-
|
|
31
|
-
// Maintain stack trace (important for V8)
|
|
32
|
-
if (Error.captureStackTrace) {
|
|
33
|
-
Error.captureStackTrace(this, McpGetResourceError);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Helper type for the data fetching logic within a resource handler
|
|
39
|
-
type GetDataLogic<TParams, TDefinition> = (
|
|
40
|
-
params: TParams,
|
|
41
|
-
uri: URL,
|
|
42
|
-
) => Promise<TDefinition>;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Handles the common logic for GetResource handlers, fetching data and formatting the response or error.
|
|
46
|
-
*/
|
|
47
|
-
export async function handleResourceGet<
|
|
48
|
-
TParams extends Record<string, unknown>,
|
|
49
|
-
TDefinition,
|
|
50
|
-
>(
|
|
51
|
-
uri: URL,
|
|
52
|
-
params: TParams,
|
|
53
|
-
resourceType: string, // e.g., 'environment', 'package' for logging/errors
|
|
54
|
-
getData: GetDataLogic<TParams, TDefinition>,
|
|
55
|
-
resourceMetadata: ResourceMetadata | undefined,
|
|
56
|
-
): Promise<ReadResourceResult> {
|
|
57
|
-
try {
|
|
58
|
-
const definition = await getData(params, uri);
|
|
59
|
-
|
|
60
|
-
// Combine definition and metadata
|
|
61
|
-
const responsePayload = JSON.stringify(
|
|
62
|
-
{ definition: definition, metadata: resourceMetadata },
|
|
63
|
-
null,
|
|
64
|
-
2,
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
contents: [
|
|
69
|
-
{
|
|
70
|
-
type: "application/json",
|
|
71
|
-
uri: uri.href,
|
|
72
|
-
text: responsePayload,
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
};
|
|
76
|
-
} catch (error) {
|
|
77
|
-
logger.error(
|
|
78
|
-
`[MCP Server Error] Error reading ${resourceType} ${uri.href}:`,
|
|
79
|
-
{ error },
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
let errorDetails: ErrorDetails;
|
|
83
|
-
|
|
84
|
-
// Determine the correct error details based on the error type
|
|
85
|
-
if (error instanceof McpGetResourceError) {
|
|
86
|
-
// The getData function already caught, formatted, and wrapped the error
|
|
87
|
-
errorDetails = error.details;
|
|
88
|
-
} else {
|
|
89
|
-
// Catch-all for truly unexpected errors not handled by the specific getData logic
|
|
90
|
-
logger.error(
|
|
91
|
-
"[MCP Server Error] Unexpected error type caught in handleResourceGet:",
|
|
92
|
-
{ error },
|
|
93
|
-
);
|
|
94
|
-
errorDetails = getInternalError(
|
|
95
|
-
`GetResource (${resourceType})`,
|
|
96
|
-
error,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Format the error response consistently
|
|
101
|
-
return {
|
|
102
|
-
isError: true,
|
|
103
|
-
contents: [
|
|
104
|
-
{
|
|
105
|
-
type: "application/json", // Keep JSON for structured error
|
|
106
|
-
uri: uri.href,
|
|
107
|
-
text: JSON.stringify(
|
|
108
|
-
{
|
|
109
|
-
error: errorDetails.message,
|
|
110
|
-
suggestions: errorDetails.suggestions,
|
|
111
|
-
},
|
|
112
|
-
null,
|
|
113
|
-
2,
|
|
114
|
-
),
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
19
|
/**
|
|
122
20
|
* Fetches and validates the Package and Model instances needed for query execution.
|
|
123
21
|
* Handles errors related to package/model access and initial compilation.
|
|
@@ -202,28 +100,20 @@ export async function getModelForQuery(
|
|
|
202
100
|
}
|
|
203
101
|
|
|
204
102
|
/**
|
|
205
|
-
* Constructs a
|
|
206
|
-
*
|
|
103
|
+
* Constructs a malloy:// URI string identifying the model a tool response came
|
|
104
|
+
* from. Attached as `uri` metadata on tool results (get-context, execute-query,
|
|
105
|
+
* docs-search); it is not a fetchable MCP resource.
|
|
207
106
|
*
|
|
208
|
-
* @param components
|
|
209
|
-
* @param fragment Optional fragment identifier (e.g.,
|
|
210
|
-
* @returns A
|
|
107
|
+
* @param components The URI parts (environment, package, and optionally the model).
|
|
108
|
+
* @param fragment Optional fragment identifier (e.g., "result", "get-context").
|
|
109
|
+
* @returns A malloy:// URI string.
|
|
211
110
|
*/
|
|
212
111
|
export function buildMalloyUri(
|
|
213
112
|
components: {
|
|
214
113
|
environment?: string;
|
|
215
114
|
package?: string;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
| "models"
|
|
219
|
-
| "packages"
|
|
220
|
-
| "notebooks"
|
|
221
|
-
| "sources"
|
|
222
|
-
| "queries"
|
|
223
|
-
| "views"; // Type of resource list or specific resource
|
|
224
|
-
resourceName?: string; // Specific name for model, query, etc.
|
|
225
|
-
subResourceType?: "views"; // For views within sources
|
|
226
|
-
subResourceName?: string; // Name of the view
|
|
115
|
+
resourceType?: "models";
|
|
116
|
+
resourceName?: string;
|
|
227
117
|
},
|
|
228
118
|
fragment?: string,
|
|
229
119
|
): string {
|
|
@@ -244,20 +134,11 @@ export function buildMalloyUri(
|
|
|
244
134
|
path += "/" + components.resourceType;
|
|
245
135
|
if (components.resourceName) {
|
|
246
136
|
path += "/" + encodeURIComponent(components.resourceName);
|
|
247
|
-
|
|
248
|
-
if (components.subResourceType && components.subResourceName) {
|
|
249
|
-
path +=
|
|
250
|
-
"/" +
|
|
251
|
-
components.subResourceType +
|
|
252
|
-
"/" +
|
|
253
|
-
encodeURIComponent(components.subResourceName);
|
|
254
|
-
}
|
|
255
137
|
}
|
|
256
138
|
}
|
|
257
139
|
|
|
258
|
-
// The URL constructor
|
|
259
|
-
//
|
|
260
|
-
// We manually construct the string instead.
|
|
140
|
+
// The URL constructor normalizes malloy://path to malloy:///path, so we
|
|
141
|
+
// build the string manually to keep the malloy://environment/... shape.
|
|
261
142
|
let uriString = "malloy:/" + path; // Start with one slash after scheme
|
|
262
143
|
|
|
263
144
|
if (fragment) {
|
package/src/mcp/mcp_constants.ts
CHANGED
|
@@ -3,20 +3,4 @@ export const MCP_ERROR_MESSAGES = {
|
|
|
3
3
|
MISSING_REQUIRED_PARAMS:
|
|
4
4
|
"Either 'query' or both 'sourceName' and 'queryName' must be provided",
|
|
5
5
|
MUTUALLY_EXCLUSIVE_PARAMS: "Cannot provide both 'query' and 'queryName'",
|
|
6
|
-
INVALID_ARGUMENTS: (toolName: string, errors: string[]) =>
|
|
7
|
-
`Invalid arguments for tool ${toolName}: ${errors.join(", ")}`,
|
|
8
|
-
|
|
9
|
-
// Application-level errors (runtime)
|
|
10
|
-
PROJECT_NOT_FOUND: (projectName: string) =>
|
|
11
|
-
`Environment '${projectName}' is not available or does not exist.`,
|
|
12
|
-
PACKAGE_NOT_FOUND: (packageName: string) =>
|
|
13
|
-
`Package manifest for ${packageName} does not exist.`,
|
|
14
|
-
MODEL_NOT_FOUND: (packageName: string, modelPath: string) =>
|
|
15
|
-
`Model '${modelPath}' not found in package '${packageName}'`,
|
|
16
|
-
QUERY_NOT_FOUND: (queryName: string) => `'${queryName}' is not defined`,
|
|
17
|
-
COMPILATION_ERROR: (error: string) => `Error(s) compiling model:\n${error}`,
|
|
18
|
-
|
|
19
|
-
// Error message prefixes
|
|
20
|
-
ERROR_EXECUTING_QUERY: (packagePath: string) =>
|
|
21
|
-
`Error executing query on '${packagePath}':`,
|
|
22
6
|
} as const;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { beforeAll, describe, expect, it } from "bun:test";
|
|
2
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
3
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
4
|
-
import {
|
|
4
|
+
import { initializeMcpServer } from "./server";
|
|
5
5
|
import type { EnvironmentStore } from "../service/environment_store";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* End-to-end coverage of the
|
|
9
|
-
* the SDK's in-memory transport (no HTTP, no network, no DuckDB).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* End-to-end coverage of the unified MCP server over the real MCP protocol,
|
|
9
|
+
* using the SDK's in-memory transport (no HTTP, no network, no DuckDB).
|
|
10
|
+
* Exercises tool registration (including the agent retrieval tools that now
|
|
11
|
+
* live on the same server), the dual-channel skill prompts, a real tool call
|
|
12
|
+
* (searchDocs over the bundled index), and the getContext error path.
|
|
12
13
|
*/
|
|
13
|
-
describe("
|
|
14
|
+
describe("MCP server over the MCP protocol (in-memory)", () => {
|
|
14
15
|
let client: Client;
|
|
15
16
|
|
|
16
17
|
beforeAll(async () => {
|
|
@@ -22,19 +23,20 @@ describe("agent MCP server over the MCP protocol (in-memory)", () => {
|
|
|
22
23
|
},
|
|
23
24
|
} as unknown as EnvironmentStore;
|
|
24
25
|
|
|
25
|
-
const server =
|
|
26
|
+
const server = initializeMcpServer(stubStore);
|
|
26
27
|
const [clientTransport, serverTransport] =
|
|
27
28
|
InMemoryTransport.createLinkedPair();
|
|
28
29
|
await server.connect(serverTransport);
|
|
29
|
-
client = new Client({ name: "
|
|
30
|
+
client = new Client({ name: "mcp-protocol-test", version: "0.0.0" });
|
|
30
31
|
await client.connect(clientTransport);
|
|
31
32
|
});
|
|
32
33
|
|
|
33
|
-
it("exposes
|
|
34
|
+
it("exposes the agent retrieval tools alongside the core tools", async () => {
|
|
34
35
|
const { tools } = await client.listTools();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
36
|
+
const names = new Set(tools.map((t) => t.name));
|
|
37
|
+
expect(names.has("malloy_getContext")).toBe(true);
|
|
38
|
+
expect(names.has("malloy_searchDocs")).toBe(true);
|
|
39
|
+
expect(names.has("malloy_executeQuery")).toBe(true);
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
it("exposes the skill set as dual-channel prompts", async () => {
|
package/src/mcp/server.ts
CHANGED
|
@@ -2,16 +2,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { EnvironmentStore } from "../service/environment_store";
|
|
3
3
|
|
|
4
4
|
import { formatDuration, logger } from "../logger";
|
|
5
|
-
import {
|
|
6
|
-
import { registerEnvironmentResource } from "./resources/environment_resource";
|
|
7
|
-
import { registerModelResource } from "./resources/model_resource";
|
|
8
|
-
import { registerNotebookResource } from "./resources/notebook_resource";
|
|
9
|
-
import { registerPackageResource } from "./resources/package_resource";
|
|
10
|
-
import { registerQueryResource } from "./resources/query_resource";
|
|
11
|
-
import { registerSourceResource } from "./resources/source_resource";
|
|
12
|
-
import { registerViewResource } from "./resources/view_resource";
|
|
13
|
-
import { registerTools } from "./tools/discovery_tools";
|
|
5
|
+
import { registerDocsSearchTool } from "./tools/docs_search_tool";
|
|
14
6
|
import { registerExecuteQueryTool } from "./tools/execute_query_tool";
|
|
7
|
+
import { registerGetContextTool } from "./tools/get_context_tool";
|
|
8
|
+
import skillsBundle from "./skills/skills_bundle.json";
|
|
15
9
|
|
|
16
10
|
export const testServerInfo = {
|
|
17
11
|
name: "malloy-publisher-mcp-server",
|
|
@@ -20,6 +14,14 @@ export const testServerInfo = {
|
|
|
20
14
|
description: "Provides access to Malloy models and query execution via MCP.",
|
|
21
15
|
};
|
|
22
16
|
|
|
17
|
+
// Build-time bundle of the agent skills (see skills/build_skills_bundle.ts),
|
|
18
|
+
// exposed as MCP prompts for the dual-channel delivery below.
|
|
19
|
+
const AGENT_SKILLS = (
|
|
20
|
+
skillsBundle as {
|
|
21
|
+
skills: { name: string; description: string; body: string }[];
|
|
22
|
+
}
|
|
23
|
+
).skills;
|
|
24
|
+
|
|
23
25
|
export function initializeMcpServer(
|
|
24
26
|
environmentStore: EnvironmentStore,
|
|
25
27
|
): McpServer {
|
|
@@ -28,35 +30,25 @@ export function initializeMcpServer(
|
|
|
28
30
|
|
|
29
31
|
const mcpServer = new McpServer(testServerInfo);
|
|
30
32
|
|
|
31
|
-
logger.info("[MCP Init] Registering environment resource...");
|
|
32
|
-
registerEnvironmentResource(mcpServer, environmentStore);
|
|
33
|
-
logger.info("[MCP Init] Registering package resource...");
|
|
34
|
-
registerPackageResource(mcpServer, environmentStore);
|
|
35
|
-
|
|
36
|
-
// Register more specific templates first
|
|
37
|
-
logger.info("[MCP Init] Registering notebook resource...");
|
|
38
|
-
registerNotebookResource(mcpServer, environmentStore);
|
|
39
|
-
logger.info("[MCP Init] Registering source resource...");
|
|
40
|
-
registerSourceResource(mcpServer, environmentStore);
|
|
41
|
-
logger.info("[MCP Init] Registering query resource...");
|
|
42
|
-
registerQueryResource(mcpServer, environmentStore);
|
|
43
|
-
logger.info("[MCP Init] Registering view resource...");
|
|
44
|
-
registerViewResource(mcpServer, environmentStore);
|
|
45
|
-
|
|
46
|
-
// Register the general model template last among resource types
|
|
47
|
-
logger.info("[MCP Init] Registering model resource...");
|
|
48
|
-
registerModelResource(mcpServer, environmentStore);
|
|
49
|
-
|
|
50
|
-
logger.info("[MCP Init] Registering executeQuery tool...");
|
|
51
33
|
registerExecuteQueryTool(mcpServer, environmentStore);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
34
|
+
registerGetContextTool(mcpServer, environmentStore);
|
|
35
|
+
registerDocsSearchTool(mcpServer, environmentStore);
|
|
36
|
+
|
|
37
|
+
// Dual-channel: also expose each skill as an MCP prompt, so hosts that ingest
|
|
38
|
+
// MCP but do not load skill files (e.g. Codex, ChatGPT, Cursor) can pull the
|
|
39
|
+
// same guidance. Skill-aware hosts (Claude Code/Desktop) use the native skill
|
|
40
|
+
// files. Note: MCP prompts are on-demand, so always-on "prevention" skills
|
|
41
|
+
// become on-demand here; see docs/agent-skills/design-principles.md.
|
|
42
|
+
for (const skill of AGENT_SKILLS) {
|
|
43
|
+
mcpServer.prompt(skill.name, skill.description, () => ({
|
|
44
|
+
messages: [
|
|
45
|
+
{
|
|
46
|
+
role: "user" as const,
|
|
47
|
+
content: { type: "text" as const, text: skill.body },
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
60
52
|
|
|
61
53
|
const endTime = performance.now();
|
|
62
54
|
logger.info(`[MCP Init] Finished initializeMcpServer`, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Build a bundled JSON of the agent skills so the
|
|
2
|
+
* Build a bundled JSON of the agent skills so the MCP server can expose them as
|
|
3
3
|
* MCP prompts (dual-channel): skill-aware hosts read the skill files directly, while
|
|
4
4
|
* hosts that only ingest MCP get the same guidance through these prompts.
|
|
5
5
|
*
|