UNPKG

@mcp-apps/kusto-mcp-server

Version:

MCP server for interacting with Kusto databases

176 lines 7.52 kB
import { KustoConnectionStringBuilder, Client as KustoClient } from "azure-kusto-data"; import { AzureCliCredential, AzureDeveloperCliCredential, ChainedTokenCredential, DeviceCodeCredential, VisualStudioCodeCredential, } from "@azure/identity"; const log = (...args) => console.error("[kusto-mcp]", ...args); // Kusto scope for authentication const KUSTO_SCOPE = "https://kusto.kusto.windows.net/.default"; export class KustoService { static getCredential() { if (!KustoService.credential) { // Prefer non-interactive sources first so background/headless MCP runs // don't pop the WAM account picker when the user is already signed in. const tenantId = process.env.AZURE_TENANT_ID || process.env.TENANT_ID; const tenantOpts = tenantId ? { tenantId, additionallyAllowedTenants: ["*"] } : { additionallyAllowedTenants: ["*"] }; const credentials = [ new AzureCliCredential(tenantOpts), new AzureDeveloperCliCredential(tenantOpts), new VisualStudioCodeCredential(tenantOpts), new DeviceCodeCredential({ ...tenantOpts, userPromptCallback: (info) => { log(`To sign in, open ${info.verificationUri} and enter code ${info.userCode}`); }, }), ]; KustoService.credential = new ChainedTokenCredential(...credentials); } return KustoService.credential; } static async createConnectionString(clusterUrl) { const credential = KustoService.getCredential(); // Get a token to trigger authentication log("Requesting authentication token..."); try { await credential.getToken(KUSTO_SCOPE); log("Authentication successful"); } catch (error) { log("Authentication failed:", error.message); throw error; } return KustoConnectionStringBuilder.withTokenCredential(clusterUrl, credential); } /** * Get a client for a specific cluster URL and database */ static async getClient(clusterUrl, database) { if (!clusterUrl) { throw new Error("Cluster URL is required"); } if (!database) { throw new Error("Database name is required"); } try { log(`Creating Kusto client for ${clusterUrl}`); const connectionString = await KustoService.createConnectionString(clusterUrl); return new KustoClient(connectionString); } catch (error) { console.error("Failed to get Kusto client:", error.message); throw new Error(`Failed to connect to Kusto: ${error.message}`); } } /** * Execute a KQL query against the Kusto database */ static async executeQuery(clusterUrl, database, query) { try { const client = await KustoService.getClient(clusterUrl, database); const response = await client.execute(database, query); return response.primaryResults[0].toJSON(); } catch (error) { console.error("Error executing Kusto query:", error.message); throw new Error(`Error executing query: ${error.message}`); } } /** * Get list of tables in the database */ static async getTables(clusterUrl, database) { try { // Query for internal tables const internalQuery = `.show tables | project TableName, IsExternal = false`; const internalResult = await KustoService.executeQuery(clusterUrl, database, internalQuery); // Query for external tables const externalQuery = `.show external tables | project TableName, IsExternal = true`; const externalResult = await KustoService.executeQuery(clusterUrl, database, externalQuery); // Combine results const internalTables = internalResult.data.map((item) => ({ TableName: item.TableName, IsExternal: false, })); const externalTables = externalResult.data.map((item) => ({ TableName: item.TableName, IsExternal: true, })); return [...internalTables, ...externalTables]; } catch (error) { console.error("Error retrieving tables:", error.message); // Return an empty array instead of throwing when no tables are found return []; } } /** * Get detailed schema information for all tables in the database */ static async getAllTableSchemas(clusterUrl, database) { try { const query = `.show tables | project TableName`; const tables = await KustoService.executeQuery(clusterUrl, database, query); const tableNames = tables.data.map((item) => item.TableName); const schemaInfo = {}; // For each table, get its schema details for (const tableName of tableNames) { try { const tableSchema = await KustoService.getTableSchema(clusterUrl, database, tableName); schemaInfo[tableName] = tableSchema; } catch (error) { console.warn(`Skipping schema retrieval for table ${tableName} due to error`); } } return schemaInfo; } catch (error) { console.error("Error retrieving all table schemas:", error); return {}; // Return an empty object for graceful failure } } /** * Get the schema for a specific table */ static async getTableSchema(clusterUrl, database, tableName, isExternal = false) { try { const query = isExternal ? `.show external table ['${tableName}'] schema as json` : `.show table ['${tableName}'] schema as json`; const result = await KustoService.executeQuery(clusterUrl, database, query); if (!result || !result.data || result.data.length === 0) { throw new Error(`No schema found for table ${tableName}`); } return JSON.parse(result.data[0].Schema); } catch (error) { console.error(`Error getting schema for table ${tableName}:`, error.message); throw new Error(`Failed to get schema for table ${tableName}: ${error.message}`); } } /** * Get sample data from a table (top N rows) */ static async getTableSample(clusterUrl, database, tableName, sampleSize = 10) { try { const query = `${tableName} | take ${sampleSize}`; return await KustoService.executeQuery(clusterUrl, database, query); } catch (error) { console.error(`Error getting sample data from table ${tableName}:`, error.message); throw new Error(`Failed to get sample data from table ${tableName}: ${error.message}`); } } /** * Check if the connection to Kusto is working properly */ static async testConnection(clusterUrl, database) { try { await KustoService.getTables(clusterUrl, database); return true; } catch (error) { return false; } } } KustoService.credential = null; //# sourceMappingURL=kustoService.js.map