# Directory Structure
```
├── .github
│ └── workflows
│ └── test.yml
├── common
│ └── client.go
├── go.mod
├── go.sum
├── main.go
├── mcp_kusto_test.png
├── mcp.json
├── README.md
└── tools
├── common.go
├── databases_test.go
├── databases.go
├── query_test.go
├── query.go
├── tables_test.go
└── tables.go
```
# Files
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Vibe querying with MCP server for Azure Data Explorer (Kusto)
This is an implementation of an MCP server for Azure Data Explorer (Kusto) built using its [Go SDK](https://github.com/Azure/azure-kusto-go). You can use this with [VS Code](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode) (or other MCP tools) for making data analysis and exploration easier.
It exposes tools for interacting with Azure Data Explorer:
1. **list_databases** - Lists all databases in a specific Azure Data Explorer cluster.
2. **list_tables** - Lists all tables in a specific Azure Data Explorer database.
3. **get_table_schema** - Gets the schema of a specific table in an Azure Data Explorer database.
4. **execute_query** - Executes a read-only KQL query against a database.
> Word(s) of caution: As much as I want folks to benefit from this, I have to call out that Large Language Models (LLMs) are non-deterministic by nature and can make mistakes. I would recommend you to **always validate** the results and queries before making any decisions based on them.
Here is a sneak peek:

## How to run
```bash
git clone https://github.com/abhirockzz/mcp_kusto
cd mcp_kusto
go build -o mcp_kusto main.go
```
### Configure the MCP server
This will differ based on the MCP client/tool you use. For VS Code you can [follow these instructions](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server) on how to configure this server using a `mcp.json` file.
Here is an example of the [mcp.json file](mcp.json):
```json
{
"servers": {
"Kusto MCP server": {
"type": "stdio",
"command": "enter path to binary e.g. /Users/demo/Desktop/mcp_kusto",
"args": []
},
//other MCP servers...
}
}
```
Here is an example of Claude Desktop configuration:
```json
{
"mcpServers": {
"Kusto MCP server": {
"command": "enter path to binary e.g. /Users/demo/Desktop/mcp_kusto",
"args": []
},
//other MCP servers...
}
}
```
### Authentication
- The user principal you use should have permissions required for `.show databases`, `.show table`, `.show tables`, and execute queries on the database. Refer to the documentation for [Azure Data Explorer](https://learn.microsoft.com/en-us/kusto/management/security-roles?view=azure-data-explorer) for more details.
- Authentication (Local credentials) - To keep things secure and simple, the MCP server uses [DefaultAzureCredential](https://learn.microsoft.com/en-us/azure/developer/go/sdk/authentication/credential-chains#defaultazurecredential-overview). This approach looks in the environment variables for an application service principal or at locally installed developer tools, such as the Azure CLI, for a set of developer credentials. Either approach can be used to authenticate the MCP server to Azure Data Explorer. For example, just login locally using Azure CLI ([az login](https://learn.microsoft.com/en-us/cli/azure/authenticate-azure-cli)).
You are good to go! Now spin up VS Code, Claude Desktop, or any other MCP tool and start vibe querying your Azure Data Explorer (Kusto) cluster!
## Local dev/testing
Start with [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) - `npx @modelcontextprotocol/inspector ./mcp_kusto`
```
--------------------------------------------------------------------------------
/mcp.json:
--------------------------------------------------------------------------------
```json
{
"servers": {
"Kusto MCP server": {
"type": "stdio",
"command": "enter path to binary e.g. /Users/demo/Desktop/mcp_kusto",
"args": []
}
}
}
```
--------------------------------------------------------------------------------
/tools/common.go:
--------------------------------------------------------------------------------
```go
package tools
const CLUSTER_PARAMETER_DESCRIPTION = "Name of the cluster. If not available, ask the user to provide the cluster name. Do not use a random cluster name of your choice."
const clusterNameFormat = "https://%s.kusto.windows.net/"
```
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
```go
package main
import (
"fmt"
"github.com/abhirockzz/mcp_kusto/tools"
"github.com/mark3labs/mcp-go/server"
)
func main() {
s := server.NewMCPServer(
"Kusto MCP server",
"0.0.5",
server.WithLogging(),
)
s.AddTool(tools.ListDatabases())
s.AddTool(tools.ListTables())
s.AddTool(tools.GetTableSchema())
s.AddTool(tools.ExecuteQuery())
// Start the stdio server
if err := server.ServeStdio(s); err != nil {
fmt.Printf("Server error: %v\n", err)
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
```yaml
name: Run Tests
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
env:
CLUSTER_NAME: ${{ secrets.CLUSTER_NAME }}
DB_NAME: testdb
TABLE_NAME: test_table
COLUMN_NAMES: column1,column2,column3
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
steps:
# - name: Azure login
# uses: azure/login@v2
# with:
# creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.23
- name: Install dependencies
run: go mod tidy
- name: Run tests
run: go test ./...
```
--------------------------------------------------------------------------------
/tools/query_test.go:
--------------------------------------------------------------------------------
```go
package tools
import (
"context"
"os"
"testing"
"github.com/mark3labs/mcp-go/mcp"
)
func TestExecuteQueryHandler(t *testing.T) {
ctx := context.Background()
// Fetch values from environment variables
clusterName := os.Getenv("CLUSTER_NAME")
if clusterName == "" {
t.Fatal("Environment variable CLUSTER_NAME is not set")
}
dbName := os.Getenv("DB_NAME")
if dbName == "" {
t.Fatal("Environment variable DB_NAME is not set")
}
tableName := os.Getenv("TABLE_NAME")
if tableName == "" {
t.Fatal("Environment variable TABLE_NAME is not set")
}
// query := os.Getenv("QUERY")
// if query == "" {
// t.Fatal("Environment variable QUERY is not set")
// }
query := tableName + " | count"
request := mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "execute_query",
Arguments: map[string]any{
"cluster": clusterName,
"database": dbName,
"query": query,
},
},
}
result, err := executeQueryHandler(ctx, request)
if err != nil {
t.Fatalf("executeQueryHandler failed: %v", err)
}
if result == nil {
t.Fatal("Expected result, got nil")
}
content := result.Content[0].(mcp.TextContent)
// t.Logf("Content: %s", content.Text)
if content.Text == "" {
t.Fatal("Expected non-empty content")
}
//validate content is not empty
if content.Text == "" {
t.Fatal("Expected non-empty content")
}
}
```
--------------------------------------------------------------------------------
/tools/databases.go:
--------------------------------------------------------------------------------
```go
package tools
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/Azure/azure-kusto-go/azkustodata/kql"
"github.com/abhirockzz/mcp_kusto/common"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func ListDatabases() (mcp.Tool, server.ToolHandlerFunc) {
return listDatabases(), listDatabasesHandler
}
func listDatabases() mcp.Tool {
return mcp.NewTool("list_databases",
mcp.WithString("cluster",
mcp.Required(),
mcp.Description(CLUSTER_PARAMETER_DESCRIPTION),
),
mcp.WithDescription("List all databases in a specific Azure Data Explorer cluster"),
)
}
func listDatabasesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
clusterName, ok := request.Params.Arguments["cluster"].(string)
if !ok {
return nil, errors.New("cluster name missing")
}
client, err := common.GetClient(fmt.Sprintf(clusterNameFormat, clusterName))
if err != nil {
return nil, err
}
defer client.Close()
// Use .show databases command
dataset, err := client.Mgmt(ctx, "", kql.New(".show databases"))
if err != nil {
return nil, err
}
databaseNames := []string{}
// Process the results
for _, row := range dataset.Tables()[0].Rows() {
// Access database name by column name
databaseName, err := row.StringByName("DatabaseName")
if err != nil {
return nil, err
}
//fmt.Println("Database:", databaseName)
databaseNames = append(databaseNames, databaseName)
}
var result ListDatabasesResponse
result.Databases = databaseNames
jsonResult, err := json.Marshal(result)
if err != nil {
return nil, err
}
return mcp.NewToolResultText(string(jsonResult)), nil
}
type ListDatabasesResponse struct {
Databases []string `json:"databases"`
}
```
--------------------------------------------------------------------------------
/common/client.go:
--------------------------------------------------------------------------------
```go
package common
import (
"github.com/Azure/azure-kusto-go/azkustodata"
)
func GetClient(endpoint string) (*azkustodata.Client, error) {
// Create a connection string builder with authentication
kustoConnectionString := azkustodata.NewConnectionStringBuilder(endpoint).WithDefaultAzureCredential()
// Initialize the client
client, err := azkustodata.New(kustoConnectionString)
if err != nil {
return nil, err
}
return client, nil
}
// const clusterNameFormat = "https://%s.kusto.windows.net/"
// func CreateTable(clusterName, dbName, createTableCommand string) error {
// endpoint := fmt.Sprintf(clusterNameFormat, clusterName)
// fmt.Println("Cluster URL:", endpoint)
// // Initialize the client
// client, err := GetClient(endpoint)
// if err != nil {
// return fmt.Errorf("error creating Kusto client: %w", err)
// }
// defer client.Close()
// ctx := context.Background()
// _, err = client.Mgmt(ctx, dbName, kql.New("").AddUnsafe(createTableCommand))
// if err != nil {
// return fmt.Errorf("error executing create table: %w", err)
// }
// log.Println("table created successfully with command", createTableCommand)
// return nil
// }
// func DropTable(clusterName, dbName, tableName string) error {
// endpoint := fmt.Sprintf(clusterNameFormat, clusterName)
// fmt.Println("Cluster URL:", endpoint)
// // Initialize the client
// client, err := GetClient(endpoint)
// if err != nil {
// return fmt.Errorf("error creating Kusto client: %w", err)
// }
// defer client.Close()
// ctx := context.Background()
// _, err = client.Mgmt(ctx, dbName, kql.New(". drop table ").AddUnsafe(tableName))
// if err != nil {
// return fmt.Errorf("error executing drop table command: %w", err)
// }
// log.Println("table dropped successfully", tableName)
// return nil
// }
```
--------------------------------------------------------------------------------
/tools/databases_test.go:
--------------------------------------------------------------------------------
```go
package tools
import (
"context"
"encoding/json"
"os"
"testing"
"github.com/mark3labs/mcp-go/mcp"
)
func TestListDatabasesHandler(t *testing.T) {
ctx := context.Background()
clusterName := os.Getenv("CLUSTER_NAME")
if clusterName == "" {
t.Fatal("Environment variable CLUSTER_NAME is not set")
}
dbName := os.Getenv("DB_NAME")
if dbName == "" {
t.Fatal("Environment variable DB_NAME is not set")
}
request := mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "list_databases",
Arguments: map[string]any{
"cluster": clusterName,
},
},
}
result, err := listDatabasesHandler(ctx, request)
if err != nil {
t.Fatalf("listDatabasesHandler failed: %v", err)
}
if result == nil {
t.Fatal("Expected result, got nil")
}
content := result.Content[0].(mcp.TextContent)
t.Logf("Content: %s", content.Text)
if content.Text == "" {
t.Fatal("Expected non-empty content")
}
// Unmarshal the content
var output ListDatabasesResponse
if err := json.Unmarshal([]byte(content.Text), &output); err != nil {
t.Fatalf("Failed to unmarshal content: %v", err)
}
if len(output.Databases) == 0 {
t.Fatal("Expected 'databases' key in unmarshaled output with non-empty slice")
}
if output.Databases[0] != dbName {
t.Fatalf("Expected database name %s, got %s", dbName, output.Databases[0])
}
}
// func createDatabase(clusterName, dbName string) error {
// if dbName == "" {
// return errors.New("database name cannot be empty")
// }
// client, err := common.GetClient(fmt.Sprintf(clusterNameFormat, clusterName))
// if err != nil {
// return fmt.Errorf("failed to get client: %v", err)
// }
// _, err = client.Mgmt(context.Background(), "", kql.New("").AddUnsafe(".create database "+dbName))
// if err != nil {
// return fmt.Errorf("failed to create database: %v", err)
// }
// return nil
// }
// func deleteDatabase(clusterName, dbName string) error {
// if dbName == "" {
// return errors.New("database name cannot be empty")
// }
// client, err := common.GetClient(fmt.Sprintf(clusterNameFormat, clusterName))
// if err != nil {
// return fmt.Errorf("failed to get client: %v", err)
// }
// _, err = client.Mgmt(context.Background(), "", kql.New(".drop database ").AddUnsafe(dbName))
// if err != nil {
// return fmt.Errorf("failed to drop database: %v", err)
// }
// return nil
// }
```
--------------------------------------------------------------------------------
/tools/query.go:
--------------------------------------------------------------------------------
```go
package tools
import (
"context"
"errors"
"fmt"
"github.com/Azure/azure-kusto-go/azkustodata/kql"
"github.com/abhirockzz/mcp_kusto/common"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func ExecuteQuery() (mcp.Tool, server.ToolHandlerFunc) {
return executeQuery(), executeQueryHandler
}
func executeQuery() mcp.Tool {
return mcp.NewTool("execute_query",
mcp.WithString("cluster",
mcp.Required(),
mcp.Description(CLUSTER_PARAMETER_DESCRIPTION),
),
mcp.WithString("database",
mcp.Required(),
mcp.Description("Name of the database."),
),
// mcp.WithString("table",
// mcp.Required(),
// mcp.Description("Name of the table."),
// ),
mcp.WithString("query",
mcp.Required(),
mcp.Description("The query to execute."),
),
mcp.WithDescription("Execute a read-only query. Ask the user for permission before executing the query. It has to be a valid KQL query. Write queries are not allowed. Result truncation is a limit set by default on the result set returned by the query. Kusto limits the number of records returned to the client to 500,000, and the overall data size for those records to 64 MB. When either of these limits is exceeded, the query fails with a partial query failure. Exceeding these limits will generate an exception. Reduce the result set size by modifying the query to only return interesting data. There are several strategies to avoid this. 1/ Use the summarize operator group and aggregate over similar records in the query output. 2/ Potentially sample some columns by using the take_any aggregation function. 3/ Use a take operator to sample the query output. 4/Use the substring function to trim wide free-text columns. 5/ Use the project operator to drop any uninteresting column from the result set."),
)
}
func executeQueryHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
clusterName, ok := request.Params.Arguments["cluster"].(string)
if !ok {
return nil, errors.New("cluster name missing")
}
dbName, ok := request.Params.Arguments["database"].(string)
if !ok {
return nil, errors.New("database name missing")
}
// table, ok := request.Params.Arguments["table"].(string)
// if !ok {
// return nil, errors.New("table name missing")
// }
query, ok := request.Params.Arguments["query"].(string)
if !ok {
return nil, errors.New("query missing")
}
client, err := common.GetClient(fmt.Sprintf(clusterNameFormat, clusterName))
if err != nil {
return nil, err
}
defer client.Close()
stmt := kql.New("").AddUnsafe(query)
queryResponse, err := client.QueryToJson(context.Background(), dbName, stmt)
if err != nil {
return nil, err
}
return mcp.NewToolResultText(queryResponse), nil
}
```
--------------------------------------------------------------------------------
/tools/tables_test.go:
--------------------------------------------------------------------------------
```go
package tools
import (
"context"
"encoding/json"
"os"
"strings"
"testing"
"slices"
"github.com/mark3labs/mcp-go/mcp"
)
func TestListTablesHandler(t *testing.T) {
ctx := context.Background()
// Fetch values from environment variables
clusterName := os.Getenv("CLUSTER_NAME")
if clusterName == "" {
t.Fatal("Environment variable CLUSTER_NAME is not set")
}
dbName := os.Getenv("DB_NAME")
if dbName == "" {
t.Fatal("Environment variable DB_NAME is not set")
}
tableName := os.Getenv("TABLE_NAME")
if tableName == "" {
t.Fatal("Environment variable TABLE_NAME is not set")
}
request := mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "list_tables",
Arguments: map[string]any{
"cluster": clusterName,
"database": dbName,
},
},
}
result, err := listTablesHandler(ctx, request)
if err != nil {
t.Fatalf("listTablesHandler failed: %v", err)
}
if result == nil {
t.Fatal("Expected result, got nil")
}
content := result.Content[0].(mcp.TextContent)
t.Logf("Content: %s", content.Text)
if content.Text == "" {
t.Fatal("Expected non-empty content")
}
// Unmarshal the content to check for the response struct
var output ListTablesResponse
if err := json.Unmarshal([]byte(content.Text), &output); err != nil {
t.Fatalf("Failed to unmarshal content: %v", err)
}
// Validate the result contains the expected cluster name
expectedClusterName := clusterName
if output.Cluster != expectedClusterName {
t.Fatalf("Expected cluster name '%s', but got '%v'", expectedClusterName, output.Cluster)
}
// Validate the result contains the expected database name
expectedDatabaseName := dbName
if output.Database != expectedDatabaseName {
t.Fatalf("Expected database name '%s', but got '%v'", expectedDatabaseName, output.Database)
}
// Validate the result contains the expected table name
expectedTableName := tableName
found := slices.Contains(output.Tables, expectedTableName)
if !found {
t.Fatalf("Expected table name '%s' not found in tables", expectedTableName)
}
}
func TestGetSchemaHandler(t *testing.T) {
ctx := context.Background()
clusterName := os.Getenv("CLUSTER_NAME")
if clusterName == "" {
t.Fatal("Environment variable CLUSTER_NAME is not set")
}
dbName := os.Getenv("DB_NAME")
if dbName == "" {
t.Fatal("Environment variable DB_NAME is not set")
}
tableName := os.Getenv("TABLE_NAME")
if tableName == "" {
t.Fatal("Environment variable TABLE_NAME is not set")
}
// columnName := os.Getenv("COLUMN_NAME")
// if columnName == "" {
// t.Fatal("Environment variable COLUMN_NAME is not set")
// }
request := mcp.CallToolRequest{
Params: struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
Meta *struct {
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
} `json:"_meta,omitempty"`
}{
Name: "get_table_schema",
Arguments: map[string]any{
"cluster": clusterName,
"database": dbName,
"table": tableName,
},
},
}
// Call the handler
result, err := getSchemaHandler(ctx, request)
if err != nil {
t.Fatalf("Handler returned an error: %v", err)
}
// Validate the result
if result == nil {
t.Fatal("Result is nil")
}
schema := result.Content[0].(mcp.TextContent)
if schema.Text == "" {
t.Fatal("Schema text is empty")
}
t.Logf("Schema text: %s", schema.Text)
// Verify the schema response
var schemaResponse TableSchemaResponse
if err := json.Unmarshal([]byte(schema.Text), &schemaResponse); err != nil {
t.Fatalf("Failed to unmarshal schema response: %v", err)
}
// Check table name
if schemaResponse.Name != tableName {
t.Fatalf("Expected table name '%s', but got '%v'", tableName, schemaResponse.Name)
}
// Check ordered columns
expectedColumnNames := os.Getenv("COLUMN_NAMES")
if expectedColumnNames == "" {
t.Fatal("Environment variable COLUMN_NAMES is not set")
}
expectedColumns := make(map[string]bool)
for _, col := range strings.Split(expectedColumnNames, ",") {
expectedColumns[col] = true
}
for _, col := range schemaResponse.OrderedColumns {
delete(expectedColumns, col.Name)
}
if len(expectedColumns) > 0 {
t.Fatalf("Expected column names not found: %v", expectedColumns)
}
}
```
--------------------------------------------------------------------------------
/tools/tables.go:
--------------------------------------------------------------------------------
```go
package tools
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/Azure/azure-kusto-go/azkustodata/kql"
"github.com/abhirockzz/mcp_kusto/common"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func ListTables() (mcp.Tool, server.ToolHandlerFunc) {
return listTables(), listTablesHandler
}
// listTables returns a tool that lists all tables in a specific Azure Data Explorer database.
func listTables() mcp.Tool {
return mcp.NewTool("list_tables",
mcp.WithString("cluster",
mcp.Required(),
mcp.Description(CLUSTER_PARAMETER_DESCRIPTION),
),
mcp.WithString("database",
mcp.Required(),
mcp.Description("Name of the database to list tables from."),
),
mcp.WithDescription("List all tables in a specific Azure Data Explorer database"),
)
}
// Define a struct to represent the response for listTablesHandler
// This struct will replace the map currently used
type ListTablesResponse struct {
Cluster string `json:"cluster"`
Database string `json:"database"`
Tables []string `json:"tables"`
}
// listTablesHandler handles the request to list all tables in a specific Azure Data Explorer database.
func listTablesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
clusterName, ok := request.Params.Arguments["cluster"].(string)
if !ok {
return nil, errors.New("cluster name missing")
}
dbName, ok := request.Params.Arguments["database"].(string)
if !ok {
return nil, errors.New("database name missing")
}
client, err := common.GetClient(fmt.Sprintf(clusterNameFormat, clusterName))
if err != nil {
return nil, err
}
defer client.Close()
dataset, err := client.Mgmt(ctx, dbName, kql.New(".show tables"))
if err != nil {
return nil, err
}
tableNames := []string{}
// Process the results
for _, row := range dataset.Tables()[0].Rows() {
// Access table name by column name
tableName, err := row.StringByName("TableName")
if err != nil {
return nil, err
}
tableNames = append(tableNames, tableName)
}
response := ListTablesResponse{
Cluster: clusterName,
Database: dbName,
Tables: tableNames,
}
jsonResult, err := json.Marshal(response)
if err != nil {
return nil, err
}
return mcp.NewToolResultText(string(jsonResult)), nil
}
// GetTableSchema returns a tool that retrieves the schema of a specific table in an Azure Data Explorer database.
func GetTableSchema() (mcp.Tool, server.ToolHandlerFunc) {
return getSchema(), getSchemaHandler
}
// getSchema returns a tool that retrieves the schema of a specific table in an Azure Data Explorer database.
func getSchema() mcp.Tool {
return mcp.NewTool("get_table_schema",
mcp.WithString("cluster",
mcp.Required(),
mcp.Description(CLUSTER_PARAMETER_DESCRIPTION),
),
mcp.WithString("database",
mcp.Required(),
mcp.Description("Name of the database."),
),
mcp.WithString("table",
mcp.Required(),
mcp.Description("Name of the table to get the schema for."),
),
mcp.WithDescription("Get the schema of a specific table in an Azure Data Explorer database"),
)
}
// Define a struct to represent the schema response
// This is to aid testing
type TableSchemaResponse struct {
Name string `json:"Name"`
OrderedColumns []struct {
Name string `json:"Name"`
Type string `json:"Type"`
CslType string `json:"CslType"`
} `json:"OrderedColumns"`
}
func getSchemaHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
clusterName, ok := request.Params.Arguments["cluster"].(string)
if !ok {
return nil, errors.New("cluster name missing")
}
dbName, ok := request.Params.Arguments["database"].(string)
if !ok {
return nil, errors.New("database name missing")
}
table, ok := request.Params.Arguments["table"].(string)
if !ok {
return nil, errors.New("table name missing")
}
client, err := common.GetClient(fmt.Sprintf(clusterNameFormat, clusterName))
if err != nil {
return nil, err
}
defer client.Close()
command := kql.New(".show table ").AddTable(table).AddLiteral(" schema as json")
//fmt.Println("Command:", command.String())
dataset, err := client.Mgmt(ctx, dbName, command)
if err != nil {
return nil, err
}
// Process the schema information
//fmt.Println("Schema for table", table)
jsonSchema, err := dataset.Tables()[0].Rows()[0].StringByName("Schema")
if err != nil {
return nil, err
}
// var schemaResponse TableSchemaResponse
// err = json.Unmarshal([]byte(jsonSchema), &schemaResponse)
// if err != nil {
// return nil, err
// }
// responseJSON, err := json.Marshal(schemaResponse)
// if err != nil {
// return nil, err
// }
//return mcp.NewToolResultText(string(responseJSON)), nil
return mcp.NewToolResultText(jsonSchema), nil
}
```