@malloy-publisher/server 0.0.75 → 0.0.77

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.
@@ -1,8 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import {
3
- McpError,
4
- ErrorCode,
5
- } from "@modelcontextprotocol/sdk/types.js";
2
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
6
3
  import { z } from "zod";
7
4
  import { ProjectStore } from "../../service/project_store";
8
5
  import { getModelForQuery } from "../handler_utils";
@@ -13,16 +10,23 @@ import { buildMalloyUri } from "../handler_utils";
13
10
  // Zod shape defining required/optional params for executeQuery
14
11
  const executeQueryShape = {
15
12
  // projectName is required; other fields mirror SDK expectations
16
- projectName: z.string().describe("Project name"),
17
- packageName: z.string().describe("Package containing the model"),
13
+ projectName: z
14
+ .string()
15
+ .describe(
16
+ "Project name. Project names are listed in the malloy resource list.",
17
+ ),
18
+ packageName: z
19
+ .string()
20
+ .describe(
21
+ "Package containing the model. Package names are listed in the malloy resource list.",
22
+ ),
18
23
  modelPath: z.string().describe("Path to the .malloy model file"),
19
24
  query: z.string().optional().describe("Ad-hoc Malloy query code"),
20
25
  sourceName: z.string().optional().describe("Source name for a view"),
21
26
  queryName: z.string().optional().describe("Named query or view"),
22
27
  };
23
28
 
24
- // Infer the type from the Zod shape for use in the handler
25
- type ExecuteQueryParams = z.infer<z.ZodObject<typeof executeQueryShape>>;
29
+ // Type inference is handled automatically by the MCP server based on the executeQueryShape
26
30
 
27
31
  /**
28
32
  * Registers the malloy/executeQuery tool with the MCP server.
@@ -32,11 +36,11 @@ export function registerExecuteQueryTool(
32
36
  projectStore: ProjectStore,
33
37
  ): void {
34
38
  mcpServer.tool(
35
- "malloy/executeQuery",
39
+ "malloy_executeQuery",
36
40
  "Executes a Malloy query (either ad-hoc or a named query/view defined in a model) against the specified model and returns the results as JSON.",
37
41
  executeQueryShape,
38
42
  /** Handles requests for the malloy/executeQuery tool */
39
- async (params: ExecuteQueryParams) => {
43
+ async (params) => {
40
44
  // Destructure projectName as well
41
45
  const {
42
46
  projectName,
@@ -2,6 +2,7 @@ import { PostgresConnection } from "@malloydata/db-postgres";
2
2
  import { BigQueryConnection } from "@malloydata/db-bigquery";
3
3
  import { SnowflakeConnection } from "@malloydata/db-snowflake";
4
4
  import { TrinoConnection } from "@malloydata/db-trino";
5
+ import { MySQLConnection } from "@malloydata/db-mysql";
5
6
  import { v4 as uuidv4 } from "uuid";
6
7
  import { Connection } from "@malloydata/malloy";
7
8
  import { components } from "../api";
@@ -20,6 +21,7 @@ export type InternalConnection = ApiConnection & {
20
21
  bigqueryConnection?: components["schemas"]["BigqueryConnection"];
21
22
  snowflakeConnection?: components["schemas"]["SnowflakeConnection"];
22
23
  trinoConnection?: components["schemas"]["TrinoConnection"];
24
+ mysqlConnection?: components["schemas"]["MysqlConnection"];
23
25
  };
24
26
 
25
27
  export async function readConnectionConfig(
@@ -82,6 +84,26 @@ export async function createConnections(basePath: string): Promise<{
82
84
  break;
83
85
  }
84
86
 
87
+ case "mysql": {
88
+ if (!connection.mysqlConnection) {
89
+ throw "Invalid connection configuration. No mysql connection.";
90
+ }
91
+ const config = {
92
+ host: connection.mysqlConnection.host,
93
+ port: connection.mysqlConnection.port,
94
+ username: connection.mysqlConnection.user,
95
+ password: connection.mysqlConnection.password,
96
+ database: connection.mysqlConnection.database,
97
+ };
98
+ const mysqlConnection = new MySQLConnection(
99
+ connection.name,
100
+ config,
101
+ );
102
+ connectionMap.set(connection.name, mysqlConnection);
103
+ connection.attributes = getConnectionAttributes(mysqlConnection);
104
+ break;
105
+ }
106
+
85
107
  case "bigquery": {
86
108
  if (!connection.bigqueryConnection) {
87
109
  throw "Invalid connection configuration. No bigquery connection.";
@@ -209,10 +231,16 @@ export async function createConnections(basePath: string): Promise<{
209
231
  function getConnectionAttributes(
210
232
  connection: Connection,
211
233
  ): ApiConnectionAttributes {
234
+ let canStream = false;
235
+ try {
236
+ canStream = connection.canStream();
237
+ } catch {
238
+ // pass
239
+ }
212
240
  return {
213
241
  dialectName: connection.dialectName,
214
242
  isPool: connection.isPool(),
215
243
  canPersist: connection.canPersist(),
216
- canStream: connection.canStream(),
244
+ canStream: canStream,
217
245
  };
218
246
  }
@@ -9,6 +9,7 @@ import {
9
9
  ApiConnection,
10
10
  PostgresConnection,
11
11
  SnowflakeConnection,
12
+ MysqlConnection,
12
13
  } from "./model";
13
14
  import { components } from "../api";
14
15
 
@@ -28,6 +29,21 @@ async function getPostgresConnection(
28
29
  });
29
30
  }
30
31
 
32
+ async function getMysqlConnection(apiMysqlConnection: MysqlConnection) {
33
+ // Dynamically import mysql2/promise to avoid import issues if not needed
34
+ const mysql = await import("mysql2/promise");
35
+ return mysql.createPool({
36
+ host: apiMysqlConnection.host,
37
+ port: apiMysqlConnection.port,
38
+ user: apiMysqlConnection.user,
39
+ password: apiMysqlConnection.password,
40
+ database: apiMysqlConnection.database,
41
+ waitForConnections: true,
42
+ connectionLimit: 10,
43
+ queueLimit: 0,
44
+ });
45
+ }
46
+
31
47
  function getBigqueryConnection(apiConnection: ApiConnection): BigQuery {
32
48
  if (!apiConnection.bigqueryConnection?.serviceAccountKeyJson) {
33
49
  // Use default credentials
@@ -74,22 +90,32 @@ export async function getSchemasForConnection(
74
90
  if (!connection.bigqueryConnection) {
75
91
  throw new Error("BigQuery connection is required");
76
92
  }
77
- const bigquery = getBigqueryConnection(connection);
78
- // Set the projectId if it's provided in the bigqueryConnection
79
- const [datasets] = await bigquery.getDatasets({
80
- ...(connection.bigqueryConnection.defaultProjectId
81
- ? { projectId: connection.bigqueryConnection.defaultProjectId }
82
- : {}),
83
- });
84
- return datasets
85
- .filter((dataset) => dataset.id)
86
- .map((dataset) => {
87
- return {
88
- name: dataset.id,
89
- isHidden: false,
90
- isDefault: false,
91
- };
93
+ try {
94
+ const bigquery = getBigqueryConnection(connection);
95
+ // Set the projectId if it's provided in the bigqueryConnection
96
+ const [datasets] = await bigquery.getDatasets({
97
+ ...(connection.bigqueryConnection.defaultProjectId
98
+ ? { projectId: connection.bigqueryConnection.defaultProjectId }
99
+ : {}),
92
100
  });
101
+ return datasets
102
+ .filter((dataset) => dataset.id)
103
+ .map((dataset) => {
104
+ return {
105
+ name: dataset.id,
106
+ isHidden: false,
107
+ isDefault: false,
108
+ };
109
+ });
110
+ } catch (error) {
111
+ console.error(
112
+ `Error getting schemas for BigQuery connection ${connection.name}:`,
113
+ error,
114
+ );
115
+ throw new Error(
116
+ `Failed to get schemas for BigQuery connection ${connection.name}: ${(error as Error).message}`,
117
+ );
118
+ }
93
119
  } else if (connection.type === "postgres") {
94
120
  if (!connection.postgresConnection) {
95
121
  throw new Error("Postgres connection is required");
@@ -107,6 +133,17 @@ export async function getSchemasForConnection(
107
133
  isDefault: row.schema_name === "public",
108
134
  };
109
135
  });
136
+ } else if (connection.type === "mysql") {
137
+ if (!connection.mysqlConnection) {
138
+ throw new Error("Mysql connection is required");
139
+ }
140
+ return [
141
+ {
142
+ name: connection.mysqlConnection.database || "mysql",
143
+ isHidden: false,
144
+ isDefault: true,
145
+ },
146
+ ];
110
147
  } else if (connection.type === "snowflake") {
111
148
  if (!connection.snowflakeConnection) {
112
149
  throw new Error("Snowflake connection is required");
@@ -133,8 +170,8 @@ export async function getTablesForSchema(
133
170
  schemaName: string,
134
171
  ): Promise<string[]> {
135
172
  if (connection.type === "bigquery") {
136
- const bigquery = getBigqueryConnection(connection);
137
173
  try {
174
+ const bigquery = getBigqueryConnection(connection);
138
175
  const options = connection.bigqueryConnection?.defaultProjectId
139
176
  ? {
140
177
  projectId: connection.bigqueryConnection?.defaultProjectId,
@@ -151,11 +188,24 @@ export async function getTablesForSchema(
151
188
  const [tables] = await dataset.getTables();
152
189
  return tables.map((table) => table.id).filter((id) => id) as string[];
153
190
  } catch (error) {
154
- console.error(`Error getting tables for schema ${schemaName}`, error);
155
- throw new Error(`Error getting tables for schema ${schemaName}`, {
156
- cause: error,
157
- });
191
+ console.error(
192
+ `Error getting tables for BigQuery schema ${schemaName} in connection ${connection.name}:`,
193
+ error,
194
+ );
195
+ throw new Error(
196
+ `Failed to get tables for BigQuery schema ${schemaName} in connection ${connection.name}: ${(error as Error).message}`,
197
+ );
198
+ }
199
+ } else if (connection.type === "mysql") {
200
+ if (!connection.mysqlConnection) {
201
+ throw new Error("Mysql connection is required");
158
202
  }
203
+ const pool = await getMysqlConnection(connection.mysqlConnection);
204
+ const [rows] = await pool.query(
205
+ "SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = ? AND table_type = 'BASE TABLE'",
206
+ [schemaName],
207
+ );
208
+ return (rows as { TABLE_NAME: string }[]).map((row) => row.TABLE_NAME);
159
209
  } else if (connection.type === "postgres") {
160
210
  if (!connection.postgresConnection) {
161
211
  throw new Error("Postgres connection is required");
@@ -633,4 +633,15 @@ export class Model {
633
633
  public getModelType(): ModelType {
634
634
  return this.modelType;
635
635
  }
636
+
637
+ public async getFileText(packagePath: string): Promise<string> {
638
+ const fullPath = path.join(packagePath, this.modelPath);
639
+ try {
640
+ return await fs.readFile(fullPath, "utf8");
641
+ } catch {
642
+ throw new ModelNotFoundError(
643
+ `Model file not found: ${this.modelPath}`,
644
+ );
645
+ }
646
+ }
636
647
  }
@@ -47,7 +47,9 @@ describe("service/package", () => {
47
47
  it("should create a package instance", async () => {
48
48
  // Using 'as any' for simplified mock Map value in test
49
49
  const pkg = new Package(
50
+ "testProject",
50
51
  "testPackage",
52
+ testPackageDirectory,
51
53
  { name: "testPackage", description: "Test package" },
52
54
  [],
53
55
  new Map([
@@ -133,6 +135,7 @@ describe("service/package", () => {
133
135
  const packageInstance = new Package(
134
136
  "testProject",
135
137
  "testPackage",
138
+ testPackageDirectory,
136
139
  { name: "testPackage", description: "Test package" },
137
140
  [],
138
141
  new Map([
@@ -37,6 +37,7 @@ export class Package {
37
37
  private databases: ApiDatabase[];
38
38
  private models: Map<string, Model> = new Map();
39
39
  private scheduler: Scheduler | undefined;
40
+ private packagePath: string;
40
41
  private static meter = metrics.getMeter("publisher");
41
42
  private static packageLoadHistogram = this.meter.createHistogram(
42
43
  "malloy_package_load_duration",
@@ -49,6 +50,7 @@ export class Package {
49
50
  constructor(
50
51
  projectName: string,
51
52
  packageName: string,
53
+ packagePath: string,
52
54
  packageMetadata: ApiPackage,
53
55
  databases: ApiDatabase[],
54
56
  models: Map<string, Model>,
@@ -56,6 +58,7 @@ export class Package {
56
58
  ) {
57
59
  this.projectName = projectName;
58
60
  this.packageName = packageName;
61
+ this.packagePath = packagePath;
59
62
  this.packageMetadata = packageMetadata;
60
63
  this.databases = databases;
61
64
  this.models = models;
@@ -106,6 +109,7 @@ export class Package {
106
109
  return new Package(
107
110
  projectName,
108
111
  packageName,
112
+ packagePath,
109
113
  packageConfig,
110
114
  databases,
111
115
  models,
@@ -143,6 +147,14 @@ export class Package {
143
147
  return this.models.get(modelPath);
144
148
  }
145
149
 
150
+ public async getModelFileText(modelPath: string): Promise<string> {
151
+ const model = this.getModel(modelPath);
152
+ if (!model) {
153
+ throw new Error(`Model not found: ${modelPath}`);
154
+ }
155
+ return await model.getFileText(this.packagePath);
156
+ }
157
+
146
158
  public async listModels(): Promise<ApiModel[]> {
147
159
  const values = await Promise.all(
148
160
  Array.from(this.models.keys())