metabase-agent-mcp-server 0.1.0
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/.dockerignore +4 -0
- package/.env.example +5 -0
- package/.prettierrc +5 -0
- package/Dockerfile +17 -0
- package/README.md +106 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +252 -0
- package/dist/index.js.map +1 -0
- package/dist/metabase-client.d.ts +203 -0
- package/dist/metabase-client.js +75 -0
- package/dist/metabase-client.js.map +1 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +48 -0
- package/dist/utils.js.map +1 -0
- package/eslint.config.mjs +10 -0
- package/package.json +47 -0
- package/src/__tests__/metabase-client.test.ts +203 -0
- package/src/index.ts +338 -0
- package/src/metabase-client.ts +332 -0
- package/tsconfig.json +17 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { MetabaseClient } from "./metabase-client.js";
|
|
7
|
+
|
|
8
|
+
const METABASE_URL = process.env.METABASE_URL;
|
|
9
|
+
const METABASE_API_KEY = process.env.METABASE_API_KEY;
|
|
10
|
+
|
|
11
|
+
if (!METABASE_URL || !METABASE_API_KEY) {
|
|
12
|
+
console.error(
|
|
13
|
+
"Required environment variables: METABASE_URL, METABASE_API_KEY",
|
|
14
|
+
);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const client = new MetabaseClient({
|
|
19
|
+
url: METABASE_URL,
|
|
20
|
+
apiKey: METABASE_API_KEY,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const server = new McpServer({
|
|
24
|
+
name: "metabase-agent",
|
|
25
|
+
version: "0.1.0",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
type ToolResult = {
|
|
33
|
+
content: { type: "text"; text: string }[];
|
|
34
|
+
isError?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
async function handleToolCall<T>(
|
|
38
|
+
fn: () => Promise<T>,
|
|
39
|
+
format: (result: T) => string,
|
|
40
|
+
): Promise<ToolResult> {
|
|
41
|
+
try {
|
|
42
|
+
const result = await fn();
|
|
43
|
+
return { content: [{ type: "text", text: format(result) }] };
|
|
44
|
+
} catch (err: unknown) {
|
|
45
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
46
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const json = (v: unknown) => JSON.stringify(v, null, 2);
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Tool: search
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
server.tool(
|
|
56
|
+
"search",
|
|
57
|
+
"Search for tables and metrics in Metabase. Supports term-based and semantic search queries. Results are ranked using Reciprocal Rank Fusion when both query types are provided.",
|
|
58
|
+
{
|
|
59
|
+
term_queries: z
|
|
60
|
+
.array(z.string().min(1))
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Term-based search queries"),
|
|
63
|
+
semantic_queries: z
|
|
64
|
+
.array(z.string().min(1))
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Semantic search queries"),
|
|
67
|
+
},
|
|
68
|
+
async ({ term_queries, semantic_queries }) =>
|
|
69
|
+
handleToolCall(
|
|
70
|
+
() => client.search({ term_queries, semantic_queries }),
|
|
71
|
+
json,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Tool: get_table
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
server.tool(
|
|
79
|
+
"get_table",
|
|
80
|
+
"Get details for a table including fields, related tables, metrics, measures, and segments.",
|
|
81
|
+
{
|
|
82
|
+
id: z.number().int().min(1).describe("Table ID"),
|
|
83
|
+
with_fields: z
|
|
84
|
+
.boolean()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Include table fields (default: true)"),
|
|
87
|
+
with_field_values: z
|
|
88
|
+
.boolean()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("Include sample field values (default: true)"),
|
|
91
|
+
with_related_tables: z
|
|
92
|
+
.boolean()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("Include related tables via FK (default: true)"),
|
|
95
|
+
with_metrics: z
|
|
96
|
+
.boolean()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Include metrics associated with this table (default: true)"),
|
|
99
|
+
with_measures: z
|
|
100
|
+
.boolean()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe("Include reusable measure definitions (default: false)"),
|
|
103
|
+
with_segments: z
|
|
104
|
+
.boolean()
|
|
105
|
+
.optional()
|
|
106
|
+
.describe("Include predefined filter segments (default: false)"),
|
|
107
|
+
},
|
|
108
|
+
async (args) =>
|
|
109
|
+
handleToolCall(
|
|
110
|
+
() =>
|
|
111
|
+
client.getTable(args.id, {
|
|
112
|
+
withFields: args.with_fields,
|
|
113
|
+
withFieldValues: args.with_field_values,
|
|
114
|
+
withRelatedTables: args.with_related_tables,
|
|
115
|
+
withMetrics: args.with_metrics,
|
|
116
|
+
withMeasures: args.with_measures,
|
|
117
|
+
withSegments: args.with_segments,
|
|
118
|
+
}),
|
|
119
|
+
json,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Tool: get_metric
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
server.tool(
|
|
127
|
+
"get_metric",
|
|
128
|
+
"Get details for a metric including its queryable dimensions and segments.",
|
|
129
|
+
{
|
|
130
|
+
id: z.number().int().min(1).describe("Metric ID"),
|
|
131
|
+
with_queryable_dimensions: z
|
|
132
|
+
.boolean()
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Include queryable dimension fields (default: true)"),
|
|
135
|
+
with_field_values: z
|
|
136
|
+
.boolean()
|
|
137
|
+
.optional()
|
|
138
|
+
.describe("Include sample field values for dimensions (default: true)"),
|
|
139
|
+
with_default_temporal_breakout: z
|
|
140
|
+
.boolean()
|
|
141
|
+
.optional()
|
|
142
|
+
.describe(
|
|
143
|
+
"Include default time dimension for temporal breakouts (default: true)",
|
|
144
|
+
),
|
|
145
|
+
with_segments: z
|
|
146
|
+
.boolean()
|
|
147
|
+
.optional()
|
|
148
|
+
.describe("Include predefined filter segments (default: false)"),
|
|
149
|
+
},
|
|
150
|
+
async (args) =>
|
|
151
|
+
handleToolCall(
|
|
152
|
+
() =>
|
|
153
|
+
client.getMetric(args.id, {
|
|
154
|
+
withQueryableDimensions: args.with_queryable_dimensions,
|
|
155
|
+
withFieldValues: args.with_field_values,
|
|
156
|
+
withDefaultTemporalBreakout: args.with_default_temporal_breakout,
|
|
157
|
+
withSegments: args.with_segments,
|
|
158
|
+
}),
|
|
159
|
+
json,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Tool: get_field_values
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
server.tool(
|
|
167
|
+
"get_field_values",
|
|
168
|
+
"Get statistics and sample values for a specific field. Useful for understanding data distribution, valid filter values, and field characteristics.",
|
|
169
|
+
{
|
|
170
|
+
entity_type: z
|
|
171
|
+
.enum(["table", "metric"])
|
|
172
|
+
.describe("Whether the field belongs to a table or metric"),
|
|
173
|
+
entity_id: z.number().int().min(1).describe("Table or metric ID"),
|
|
174
|
+
field_id: z
|
|
175
|
+
.string()
|
|
176
|
+
.min(1)
|
|
177
|
+
.describe(
|
|
178
|
+
"Field ID (format: 't<id>-<index>' for table fields, 'c<id>-<index>' for metric fields)",
|
|
179
|
+
),
|
|
180
|
+
},
|
|
181
|
+
async ({ entity_type, entity_id, field_id }) =>
|
|
182
|
+
handleToolCall(
|
|
183
|
+
() => client.getFieldValues(entity_type, entity_id, field_id),
|
|
184
|
+
json,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Shared schemas for query construction
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
const FilterSchema = z.union([
|
|
193
|
+
z.object({ segment_id: z.number().int() }).describe("Segment filter"),
|
|
194
|
+
z
|
|
195
|
+
.object({
|
|
196
|
+
field_id: z.string(),
|
|
197
|
+
operation: z.string(),
|
|
198
|
+
value: z.any().optional(),
|
|
199
|
+
values: z.array(z.any()).optional(),
|
|
200
|
+
bucket: z.string().optional(),
|
|
201
|
+
})
|
|
202
|
+
.describe("Field filter"),
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
const GroupBySchema = z.object({
|
|
206
|
+
field_id: z.string(),
|
|
207
|
+
field_granularity: z
|
|
208
|
+
.enum([
|
|
209
|
+
"minute",
|
|
210
|
+
"hour",
|
|
211
|
+
"day",
|
|
212
|
+
"week",
|
|
213
|
+
"month",
|
|
214
|
+
"quarter",
|
|
215
|
+
"year",
|
|
216
|
+
"day-of-week",
|
|
217
|
+
])
|
|
218
|
+
.nullable()
|
|
219
|
+
.optional(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const FieldRefSchema = z.object({
|
|
223
|
+
field_id: z.string(),
|
|
224
|
+
bucket: z.string().optional(),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const OrderBySchema = z.object({
|
|
228
|
+
field: FieldRefSchema,
|
|
229
|
+
direction: z.enum(["asc", "desc"]),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const AggregationSchema = z.union([
|
|
233
|
+
z.object({
|
|
234
|
+
function: z.literal("count"),
|
|
235
|
+
sort_order: z.enum(["asc", "desc"]).nullable().optional(),
|
|
236
|
+
}),
|
|
237
|
+
z.object({
|
|
238
|
+
field_id: z.string(),
|
|
239
|
+
function: z.enum(["avg", "count-distinct", "max", "min", "sum"]),
|
|
240
|
+
sort_order: z.enum(["asc", "desc"]).nullable().optional(),
|
|
241
|
+
}),
|
|
242
|
+
z.object({
|
|
243
|
+
measure_id: z.number().int(),
|
|
244
|
+
sort_order: z.enum(["asc", "desc"]).nullable().optional(),
|
|
245
|
+
}),
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Tool: run_query
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
server.tool(
|
|
252
|
+
"run_query",
|
|
253
|
+
`Construct and execute a query against Metabase, returning results directly.
|
|
254
|
+
|
|
255
|
+
For tables: supports filters, fields, aggregations, group_by, order_by, limit.
|
|
256
|
+
For metrics: supports filters, group_by (aggregation is defined by the metric).
|
|
257
|
+
|
|
258
|
+
Provide EITHER table_id OR metric_id, not both.
|
|
259
|
+
Row limits: 2000 for simple queries, 10000 for aggregated queries.`,
|
|
260
|
+
{
|
|
261
|
+
table_id: z
|
|
262
|
+
.number()
|
|
263
|
+
.int()
|
|
264
|
+
.min(1)
|
|
265
|
+
.optional()
|
|
266
|
+
.describe("Table ID (mutually exclusive with metric_id)"),
|
|
267
|
+
metric_id: z
|
|
268
|
+
.number()
|
|
269
|
+
.int()
|
|
270
|
+
.min(1)
|
|
271
|
+
.optional()
|
|
272
|
+
.describe("Metric ID (mutually exclusive with table_id)"),
|
|
273
|
+
filters: z
|
|
274
|
+
.array(FilterSchema)
|
|
275
|
+
.optional()
|
|
276
|
+
.describe("Filter conditions to apply"),
|
|
277
|
+
fields: z
|
|
278
|
+
.array(FieldRefSchema)
|
|
279
|
+
.optional()
|
|
280
|
+
.describe(
|
|
281
|
+
"Specific fields to select (omit for all fields, table queries only)",
|
|
282
|
+
),
|
|
283
|
+
aggregations: z
|
|
284
|
+
.array(AggregationSchema)
|
|
285
|
+
.optional()
|
|
286
|
+
.describe(
|
|
287
|
+
"Aggregation functions: count, avg, sum, min, max, count-distinct, or measure_id (table queries only)",
|
|
288
|
+
),
|
|
289
|
+
group_by: z
|
|
290
|
+
.array(GroupBySchema)
|
|
291
|
+
.optional()
|
|
292
|
+
.describe("Fields to group by, with optional temporal granularity"),
|
|
293
|
+
order_by: z
|
|
294
|
+
.array(OrderBySchema)
|
|
295
|
+
.optional()
|
|
296
|
+
.describe(
|
|
297
|
+
"Order by fields. To order by aggregation, use sort_order on the aggregation instead (table queries only)",
|
|
298
|
+
),
|
|
299
|
+
limit: z
|
|
300
|
+
.number()
|
|
301
|
+
.int()
|
|
302
|
+
.min(1)
|
|
303
|
+
.optional()
|
|
304
|
+
.describe("Maximum rows to return (table queries only)"),
|
|
305
|
+
},
|
|
306
|
+
async (args) => {
|
|
307
|
+
try {
|
|
308
|
+
const constructed = await client.constructQuery(
|
|
309
|
+
args as Parameters<typeof client.constructQuery>[0],
|
|
310
|
+
);
|
|
311
|
+
const result = await client.executeQuery({ query: constructed.query });
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: "text" as const, text: json(result) }],
|
|
314
|
+
...(result.status === "failed" && { isError: true }),
|
|
315
|
+
};
|
|
316
|
+
} catch (err: unknown) {
|
|
317
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
318
|
+
return {
|
|
319
|
+
content: [{ type: "text" as const, text: msg }],
|
|
320
|
+
isError: true,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Start the server
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
async function main() {
|
|
330
|
+
const transport = new StdioServerTransport();
|
|
331
|
+
await server.connect(transport);
|
|
332
|
+
console.error("Metabase Agent MCP server running on stdio");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
main().catch((err) => {
|
|
336
|
+
console.error("Fatal error:", err);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
const AGENT_API_BASE = "/api/agent/v1";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
3
|
+
|
|
4
|
+
export interface MetabaseClientConfig {
|
|
5
|
+
url: string;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class MetabaseClient {
|
|
11
|
+
private baseUrl: string;
|
|
12
|
+
private apiKey: string;
|
|
13
|
+
private timeoutMs: number;
|
|
14
|
+
|
|
15
|
+
constructor(config: MetabaseClientConfig) {
|
|
16
|
+
this.baseUrl = config.url.replace(/\/+$/, "");
|
|
17
|
+
this.apiKey = config.apiKey;
|
|
18
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private async request<T>(
|
|
22
|
+
method: string,
|
|
23
|
+
path: string,
|
|
24
|
+
body?: unknown,
|
|
25
|
+
): Promise<T> {
|
|
26
|
+
const url = `${this.baseUrl}${AGENT_API_BASE}${path}`;
|
|
27
|
+
const headers: Record<string, string> = {
|
|
28
|
+
"X-API-Key": this.apiKey,
|
|
29
|
+
};
|
|
30
|
+
if (body !== undefined) {
|
|
31
|
+
headers["Content-Type"] = "application/json";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const res = await fetch(url, {
|
|
35
|
+
method,
|
|
36
|
+
headers,
|
|
37
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
38
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const text = await res.text().catch(() => "");
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Metabase Agent API error ${res.status} ${res.statusText}: ${text}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return res.json() as Promise<T>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async search(params: {
|
|
52
|
+
term_queries?: string[];
|
|
53
|
+
semantic_queries?: string[];
|
|
54
|
+
}): Promise<SearchResponse> {
|
|
55
|
+
return this.request("POST", "/search", params);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getTable(
|
|
59
|
+
id: number,
|
|
60
|
+
opts?: {
|
|
61
|
+
withFields?: boolean;
|
|
62
|
+
withFieldValues?: boolean;
|
|
63
|
+
withRelatedTables?: boolean;
|
|
64
|
+
withMetrics?: boolean;
|
|
65
|
+
withMeasures?: boolean;
|
|
66
|
+
withSegments?: boolean;
|
|
67
|
+
},
|
|
68
|
+
): Promise<Table> {
|
|
69
|
+
const params = new URLSearchParams();
|
|
70
|
+
if (opts?.withFields !== undefined)
|
|
71
|
+
params.set("with-fields", String(opts.withFields));
|
|
72
|
+
if (opts?.withFieldValues !== undefined)
|
|
73
|
+
params.set("with-field-values", String(opts.withFieldValues));
|
|
74
|
+
if (opts?.withRelatedTables !== undefined)
|
|
75
|
+
params.set("with-related-tables", String(opts.withRelatedTables));
|
|
76
|
+
if (opts?.withMetrics !== undefined)
|
|
77
|
+
params.set("with-metrics", String(opts.withMetrics));
|
|
78
|
+
if (opts?.withMeasures !== undefined)
|
|
79
|
+
params.set("with-measures", String(opts.withMeasures));
|
|
80
|
+
if (opts?.withSegments !== undefined)
|
|
81
|
+
params.set("with-segments", String(opts.withSegments));
|
|
82
|
+
|
|
83
|
+
const qs = params.toString();
|
|
84
|
+
return this.request("GET", `/table/${id}${qs ? `?${qs}` : ""}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getFieldValues(
|
|
88
|
+
entityType: "table" | "metric",
|
|
89
|
+
entityId: number,
|
|
90
|
+
fieldId: string,
|
|
91
|
+
): Promise<FieldValues> {
|
|
92
|
+
return this.request(
|
|
93
|
+
"GET",
|
|
94
|
+
`/${entityType}/${entityId}/field/${encodeURIComponent(fieldId)}/values`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async getMetric(
|
|
99
|
+
id: number,
|
|
100
|
+
opts?: {
|
|
101
|
+
withQueryableDimensions?: boolean;
|
|
102
|
+
withFieldValues?: boolean;
|
|
103
|
+
withDefaultTemporalBreakout?: boolean;
|
|
104
|
+
withSegments?: boolean;
|
|
105
|
+
},
|
|
106
|
+
): Promise<Metric> {
|
|
107
|
+
const params = new URLSearchParams();
|
|
108
|
+
if (opts?.withQueryableDimensions !== undefined)
|
|
109
|
+
params.set(
|
|
110
|
+
"with-queryable-dimensions",
|
|
111
|
+
String(opts.withQueryableDimensions),
|
|
112
|
+
);
|
|
113
|
+
if (opts?.withFieldValues !== undefined)
|
|
114
|
+
params.set("with-field-values", String(opts.withFieldValues));
|
|
115
|
+
if (opts?.withDefaultTemporalBreakout !== undefined)
|
|
116
|
+
params.set(
|
|
117
|
+
"with-default-temporal-breakout",
|
|
118
|
+
String(opts.withDefaultTemporalBreakout),
|
|
119
|
+
);
|
|
120
|
+
if (opts?.withSegments !== undefined)
|
|
121
|
+
params.set("with-segments", String(opts.withSegments));
|
|
122
|
+
|
|
123
|
+
const qs = params.toString();
|
|
124
|
+
return this.request("GET", `/metric/${id}${qs ? `?${qs}` : ""}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async constructQuery(
|
|
128
|
+
params: ConstructQueryRequest,
|
|
129
|
+
): Promise<ConstructQueryResponse> {
|
|
130
|
+
return this.request("POST", "/construct-query", params);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async executeQuery(
|
|
134
|
+
params: ExecuteQueryRequest,
|
|
135
|
+
): Promise<ExecuteQueryResponse> {
|
|
136
|
+
return this.request("POST", "/execute", params);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Response types ---
|
|
141
|
+
|
|
142
|
+
export interface SearchResponse {
|
|
143
|
+
data: SearchResultItem[];
|
|
144
|
+
total_count: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface SearchResultItem {
|
|
148
|
+
id: number;
|
|
149
|
+
type: "table" | "metric";
|
|
150
|
+
name: string;
|
|
151
|
+
display_name?: string | null;
|
|
152
|
+
description?: string | null;
|
|
153
|
+
database_id?: number | null;
|
|
154
|
+
database_schema?: string | null;
|
|
155
|
+
verified?: boolean | null;
|
|
156
|
+
created_at?: string | null;
|
|
157
|
+
updated_at?: string | null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface Field {
|
|
161
|
+
field_id: string;
|
|
162
|
+
name: string;
|
|
163
|
+
type?: "boolean" | "date" | "datetime" | "time" | "number" | "string" | null;
|
|
164
|
+
description?: string | null;
|
|
165
|
+
database_type?: string | null;
|
|
166
|
+
semantic_type?: string | null;
|
|
167
|
+
field_values?: unknown[] | null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface Segment {
|
|
171
|
+
id: number;
|
|
172
|
+
name: string;
|
|
173
|
+
description?: string | null;
|
|
174
|
+
display_name?: string | null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface Measure {
|
|
178
|
+
id: number;
|
|
179
|
+
name: string;
|
|
180
|
+
description?: string | null;
|
|
181
|
+
display_name?: string | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface MetricSummary {
|
|
185
|
+
id: number;
|
|
186
|
+
type: "metric";
|
|
187
|
+
name: string;
|
|
188
|
+
description?: string | null;
|
|
189
|
+
default_time_dimension_field_id?: string | null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface RelatedTable {
|
|
193
|
+
id: number;
|
|
194
|
+
type: "table";
|
|
195
|
+
name: string;
|
|
196
|
+
display_name?: string | null;
|
|
197
|
+
description?: string | null;
|
|
198
|
+
database_id?: number | null;
|
|
199
|
+
database_engine?: string | null;
|
|
200
|
+
database_schema?: string | null;
|
|
201
|
+
related_by?: string | null;
|
|
202
|
+
fields?: Field[] | null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface Table {
|
|
206
|
+
id: number;
|
|
207
|
+
type: "table" | "metric";
|
|
208
|
+
name: string;
|
|
209
|
+
display_name: string;
|
|
210
|
+
description?: string | null;
|
|
211
|
+
database_id: number;
|
|
212
|
+
database_engine: string;
|
|
213
|
+
database_schema?: string | null;
|
|
214
|
+
fields: Field[];
|
|
215
|
+
related_tables?: RelatedTable[] | null;
|
|
216
|
+
metrics?: MetricSummary[] | null;
|
|
217
|
+
measures?: Measure[] | null;
|
|
218
|
+
segments?: Segment[] | null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface Metric {
|
|
222
|
+
id: number;
|
|
223
|
+
type: "metric";
|
|
224
|
+
name: string;
|
|
225
|
+
description?: string | null;
|
|
226
|
+
verified?: boolean | null;
|
|
227
|
+
default_time_dimension_field_id?: string | null;
|
|
228
|
+
queryable_dimensions?: Field[] | null;
|
|
229
|
+
segments?: Segment[] | null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface Statistics {
|
|
233
|
+
distinct_count?: number | null;
|
|
234
|
+
percent_null?: number | null;
|
|
235
|
+
min?: number | null;
|
|
236
|
+
max?: number | null;
|
|
237
|
+
avg?: number | null;
|
|
238
|
+
sd?: number | null;
|
|
239
|
+
q1?: number | null;
|
|
240
|
+
q3?: number | null;
|
|
241
|
+
earliest?: string | null;
|
|
242
|
+
latest?: string | null;
|
|
243
|
+
average_length?: number | null;
|
|
244
|
+
percent_email?: number | null;
|
|
245
|
+
percent_url?: number | null;
|
|
246
|
+
percent_state?: number | null;
|
|
247
|
+
percent_json?: number | null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface FieldValues {
|
|
251
|
+
field_id?: string | null;
|
|
252
|
+
values?: unknown[] | null;
|
|
253
|
+
statistics?: Statistics | null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// --- Query construction types ---
|
|
257
|
+
|
|
258
|
+
export type Filter =
|
|
259
|
+
| { segment_id: number }
|
|
260
|
+
| {
|
|
261
|
+
field_id: string;
|
|
262
|
+
operation: string;
|
|
263
|
+
value?: unknown;
|
|
264
|
+
values?: unknown[];
|
|
265
|
+
bucket?: string;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export interface GroupBy {
|
|
269
|
+
field_id: string;
|
|
270
|
+
field_granularity?: string | null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface FieldRef {
|
|
274
|
+
field_id: string;
|
|
275
|
+
bucket?: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface OrderBy {
|
|
279
|
+
field: FieldRef;
|
|
280
|
+
direction: "asc" | "desc";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export type Aggregation =
|
|
284
|
+
| { function: "count"; sort_order?: "asc" | "desc" | null }
|
|
285
|
+
| {
|
|
286
|
+
field_id: string;
|
|
287
|
+
function: "avg" | "count-distinct" | "max" | "min" | "sum";
|
|
288
|
+
sort_order?: "asc" | "desc" | null;
|
|
289
|
+
}
|
|
290
|
+
| { measure_id: number; sort_order?: "asc" | "desc" | null };
|
|
291
|
+
|
|
292
|
+
export type ConstructQueryRequest =
|
|
293
|
+
| {
|
|
294
|
+
table_id: number;
|
|
295
|
+
filters?: Filter[] | null;
|
|
296
|
+
fields?: FieldRef[] | null;
|
|
297
|
+
aggregations?: Aggregation[] | null;
|
|
298
|
+
group_by?: GroupBy[] | null;
|
|
299
|
+
order_by?: OrderBy[] | null;
|
|
300
|
+
limit?: number | null;
|
|
301
|
+
}
|
|
302
|
+
| {
|
|
303
|
+
metric_id: number;
|
|
304
|
+
filters?: Filter[] | null;
|
|
305
|
+
group_by?: GroupBy[] | null;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export interface ConstructQueryResponse {
|
|
309
|
+
query: string;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export interface ColumnMetadata {
|
|
313
|
+
name: string;
|
|
314
|
+
base_type: string;
|
|
315
|
+
display_name: string;
|
|
316
|
+
effective_type?: string | null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface ExecuteQueryRequest {
|
|
320
|
+
query: string;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface ExecuteQueryResponse {
|
|
324
|
+
status: "completed" | "failed";
|
|
325
|
+
data?: {
|
|
326
|
+
cols: ColumnMetadata[];
|
|
327
|
+
rows: unknown[][];
|
|
328
|
+
};
|
|
329
|
+
row_count?: number;
|
|
330
|
+
running_time?: number;
|
|
331
|
+
error?: string;
|
|
332
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist", "src/__tests__"]
|
|
17
|
+
}
|