import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { BirstClient } from "../../client/birstClient.js";
import { z } from "zod";
// Response types
interface ColumnMetadata {
name: string;
label: string;
dataType: number;
colIndex: number;
}
interface ExecuteQueryResponse {
data: Record<string, string>[];
metadata: {
numRows: number;
numColumns: number;
columns: ColumnMetadata[];
};
}
const contextParameterSchema = z.object({
name: z.string().describe("Parameter name (e.g., 'Time.Year')"),
operator: z.string().default("=").describe("Comparison operator (default: '=')"),
selectedValues: z.array(z.string()).describe("Values for comparison"),
});
const inputSchema = z.object({
bql: z.string().describe("The BQL query to execute"),
spaceId: z.string().describe("The Birst space ID (logical ID)"),
product: z.string().default("BIRST").describe("Product type (default: BIRST)"),
connectionId: z.string().optional().describe("Optional connection ID within the space"),
contextParameters: z.array(contextParameterSchema).optional().describe(
"Optional context/filter parameters"
),
});
export function registerExecuteQuery(server: McpServer, client: BirstClient): void {
server.tool(
"birst_execute_query",
"Execute a BQL query and return the results. Use after generating BQL or with known queries.",
inputSchema.shape,
async (args) => {
const { bql, spaceId, product, connectionId, contextParameters } = inputSchema.parse(args);
const requestBody: Record<string, unknown> = {
query: bql,
};
if (connectionId) {
requestBody.connectionId = connectionId;
}
if (contextParameters && contextParameters.length > 0) {
requestBody.contextParameters = contextParameters;
}
const response = await client.icw<ExecuteQueryResponse>("/query/execute/", {
method: "POST",
body: requestBody,
queryParams: {
product,
appId: spaceId,
},
});
// Transform column-based data to more readable format
const columns = response.metadata?.columns || [];
const columnMap = new Map(columns.map((c) => [c.label, c.name]));
const rows = response.data?.map((row) => {
const readable: Record<string, string> = {};
for (const [label, value] of Object.entries(row)) {
const columnName = columnMap.get(label) || label;
readable[columnName] = value;
}
return readable;
}) || [];
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
success: true,
rowCount: response.metadata?.numRows || 0,
columnCount: response.metadata?.numColumns || 0,
columns: columns.map((c) => ({
name: c.name,
dataType: c.dataType,
})),
rows,
},
null,
2
),
},
],
};
}
);
}