#
tokens: 6588/50000 12/12 files
lines: off (toggle) GitHub
raw markdown copy
# 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:

![kusto mcp server in action](mcp_kusto_test.png)

## 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
}

```