@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.
Files changed (69) hide show
  1. package/README.md +2 -11
  2. package/dist/package_load_worker.mjs +86 -24
  3. package/dist/server.mjs +655 -6994
  4. package/package.json +1 -4
  5. package/src/health.ts +3 -8
  6. package/src/mcp/error_messages.ts +8 -37
  7. package/src/mcp/handler_utils.ts +10 -129
  8. package/src/mcp/mcp_constants.ts +0 -16
  9. package/src/mcp/{agent_server.protocol.spec.ts → server.protocol.spec.ts} +14 -12
  10. package/src/mcp/server.ts +29 -37
  11. package/src/mcp/skills/build_skills_bundle.ts +1 -1
  12. package/src/mcp/skills/skills_bundle.json +1 -1
  13. package/src/mcp/tools/docs_search_tool.ts +1 -1
  14. package/src/mcp/tools/execute_query_tool.ts +2 -2
  15. package/src/mcp/tools/get_context_eval.ts +3 -3
  16. package/src/mcp/tools/get_context_tool.spec.ts +196 -1
  17. package/src/mcp/tools/get_context_tool.ts +165 -67
  18. package/src/package_load/package_load_pool.ts +3 -0
  19. package/src/package_load/package_load_worker.ts +68 -24
  20. package/src/package_load/protocol.ts +16 -0
  21. package/src/package_load/rpc_wait_accountant.spec.ts +109 -0
  22. package/src/package_load/rpc_wait_accountant.ts +76 -0
  23. package/src/package_load_metrics.spec.ts +114 -0
  24. package/src/package_load_metrics.ts +127 -0
  25. package/src/pg_helpers.spec.ts +2 -206
  26. package/src/pg_helpers.ts +4 -120
  27. package/src/server.ts +0 -16
  28. package/src/service/environment.ts +1 -1
  29. package/src/service/package.ts +82 -42
  30. package/src/test_helpers/metrics_harness.ts +40 -0
  31. package/tests/harness/mcp_test_setup.ts +1 -1
  32. package/tests/integration/mcp/mcp_transport.integration.spec.ts +7 -31
  33. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -6
  34. package/dxt/malloy_bridge.py +0 -354
  35. package/dxt/manifest.json +0 -22
  36. package/src/dto/connection.dto.spec.ts +0 -186
  37. package/src/dto/connection.dto.ts +0 -308
  38. package/src/dto/index.ts +0 -2
  39. package/src/dto/package.dto.spec.ts +0 -42
  40. package/src/dto/package.dto.ts +0 -27
  41. package/src/dto/validate.spec.ts +0 -76
  42. package/src/dto/validate.ts +0 -31
  43. package/src/ducklake_version.spec.ts +0 -43
  44. package/src/ducklake_version.ts +0 -26
  45. package/src/mcp/agent_server.spec.ts +0 -18
  46. package/src/mcp/agent_server.ts +0 -144
  47. package/src/mcp/prompts/handlers.ts +0 -84
  48. package/src/mcp/prompts/index.ts +0 -11
  49. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  50. package/src/mcp/prompts/prompt_service.ts +0 -67
  51. package/src/mcp/prompts/utils.ts +0 -62
  52. package/src/mcp/resource_metadata.ts +0 -47
  53. package/src/mcp/resources/environment_resource.ts +0 -187
  54. package/src/mcp/resources/model_resource.ts +0 -155
  55. package/src/mcp/resources/notebook_resource.ts +0 -137
  56. package/src/mcp/resources/package_resource.ts +0 -373
  57. package/src/mcp/resources/query_resource.ts +0 -122
  58. package/src/mcp/resources/source_resource.ts +0 -141
  59. package/src/mcp/resources/view_resource.ts +0 -136
  60. package/src/mcp/tools/discovery_tools.ts +0 -280
  61. package/src/storage/BaseRepository.ts +0 -31
  62. package/src/storage/StorageManager.mock.ts +0 -50
  63. package/tests/harness/e2e.ts +0 -96
  64. package/tests/harness/mocks.ts +0 -39
  65. package/tests/harness/uris.ts +0 -31
  66. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -655
  67. package/tests/integration/mcp/setup.spec.ts +0 -5
  68. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  69. 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.226",
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
- // Keep the process alive on SIGTERM do not close the server.
80
- // K8s will SIGKILL after terminationGracePeriodSeconds (which cannot be caught).
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 or URI (${resourceUriOrContext}) is spelled correctly and exists. Check capitalization and path separators.`,
40
- `If using a URI, ensure it follows the correct format (e.g., malloy://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., 'GetResource', 'ListResources').
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., 'GetResource (model)', 'executeQuery').
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 try requesting the resource details for the source URI (e.g., 'malloy://.../sources/${sourceName}') to see the list of available views. Views are defined within sources like 'source: ${sourceName} is ... extend { view: ${viewName} is { ... } }'.`,
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 model using ListResources('malloy://.../${modelIdentifier}').`,
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 ListResources('malloy://.../${modelIdentifier}').`,
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} definition using GetResource.`,
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;
@@ -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 valid malloy:// URI string from its components.
206
- * Handles encoding of path segments.
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 An object containing the URI parts (environment, package, model, etc.)
209
- * @param fragment Optional fragment identifier (e.g., #queryResult)
210
- * @returns A valid malloy:// URI string.
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
- model?: string;
217
- resourceType?:
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 seems to normalize malloy://path to malloy:///path
259
- // which breaks the tests expecting malloy://environment/...
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) {
@@ -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 { initializeAgentMcpServer } from "./agent_server";
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 agent MCP server over the real MCP protocol, using
9
- * the SDK's in-memory transport (no HTTP, no network, no DuckDB). Exercises tool
10
- * registration, the dual-channel prompts, a real tool call (searchDocs over the
11
- * bundled index), and the getContext error path.
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("agent MCP server over the MCP protocol (in-memory)", () => {
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 = initializeAgentMcpServer(stubStore);
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: "agent-protocol-test", version: "0.0.0" });
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 exactly the two agent tools", async () => {
34
+ it("exposes the agent retrieval tools alongside the core tools", async () => {
34
35
  const { tools } = await client.listTools();
35
- expect(new Set(tools.map((t) => t.name))).toEqual(
36
- new Set(["malloy_getContext", "malloy_searchDocs"]),
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 { registerPromptCapability } from "./prompts/prompt_service.js";
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
- registerTools(mcpServer, environmentStore);
54
-
55
- // Note: malloy_getContext and malloy_searchDocs are intentionally NOT registered
56
- // here. They run on a separate, isolated agent MCP server (see mcp/agent_server.ts).
57
-
58
- logger.info("[MCP Init] Registering prompt capability...");
59
- registerPromptCapability(mcpServer, environmentStore);
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 agent MCP server can expose them as
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
  *