From 2f45e403cb9fb8288d0249ed0abc9baf71f4886a Mon Sep 17 00:00:00 2001 From: andersh Date: Thu, 12 Feb 2026 20:48:38 +0100 Subject: [PATCH] fix: update the format returned by all tools, improve description and logging --- README.md | 40 ++++++++++++++++++++ cmd/mcp_parseable_server/main.go | 18 ++++++--- tools/parseable.go | 36 +++++++----------- tools/parseable_about.go | 64 ++++++++++++++++++-------------- tools/parseable_get_schema.go | 33 +++++++++------- tools/parseable_info.go | 45 +++++++++++----------- tools/parseable_list_stats.go | 52 +++++++++++++------------- tools/parseable_list_streams.go | 13 +++++-- tools/parseable_query.go | 54 ++++++++++++++++----------- tools/parseable_roles.go | 43 +++++++++++---------- 10 files changed, 237 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index 362c236..1db1414 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,46 @@ This project provides an MCP (Message Context Protocol) server for [Parseable](h - Modular MCP tool registration for easy extension - Supports both HTTP and stdio MCP modes - Environment variable and flag-based configuration +- The mcp server returns responses in json where the payload is both in text and structured format. + + +# Testing +To test the server you can use the [mcp-cli](https://github.com/philschmid/mcp-cli) +```shell +mcp-cli call parseable get_roles +``` +Returns +```json +{ + "content": [ + { + "type": "text", + "text": "{\"admins\":[{\"privilege\":\"admin\"}],\"network_role\":[{\"privilege\":\"reader\",\"resource\":{\"stream\":\"network_logstream\"}}],\"otel_gateway\":[{\"privilege\":\"editor\"}]}" + } + ], + "structuredContent": { + "admins": [ + { + "privilege": "admin" + } + ], + "network_role": [ + { + "privilege": "reader", + "resource": { + "stream": "network_logstream" + } + } + ], + "otel_gateway": [ + { + "privilege": "editor" + } + ] + } +} + +``` # Building diff --git a/cmd/mcp_parseable_server/main.go b/cmd/mcp_parseable_server/main.go index 9afd07e..8be7f4d 100644 --- a/cmd/mcp_parseable_server/main.go +++ b/cmd/mcp_parseable_server/main.go @@ -2,7 +2,7 @@ package main import ( "flag" - "log" + "log/slog" "os" "github.com/mark3labs/mcp-go/server" @@ -21,6 +21,12 @@ func main() { versionFlag := flag.Bool("version", false, "print version and exit") flag.Parse() + // Setup structured logger for stdout + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + slog.SetDefault(logger) + if *versionFlag { println("mcp-parseable-server " + version) os.Exit(0) @@ -72,16 +78,18 @@ Try not to second guess information - if you don't know something or lack inform tools.RegisterParseableTools(mcpServer) if *mode == "stdio" { - log.Printf("MCP server running in stdio mode (Parseable at %s)", tools.ParseableBaseURL) + slog.Info("MCP server running in stdio mode", "parseable_url", tools.ParseableBaseURL) if err := server.ServeStdio(mcpServer); err != nil { - log.Fatalf("MCP stdio server failed: %v", err) + slog.Error("MCP stdio server failed", "error", err) + os.Exit(1) } return } httpServer := server.NewStreamableHTTPServer(mcpServer) - log.Printf("MCP server running on %s, Parseable at %s", *listenAddr, tools.ParseableBaseURL) + slog.Info("MCP server running", "address", *listenAddr, "parseable_url", tools.ParseableBaseURL) if err := httpServer.Start(*listenAddr); err != nil { - log.Fatalf("MCP server failed: %v", err) + slog.Error("MCP server failed", "error", err) + os.Exit(1) } } diff --git a/tools/parseable.go b/tools/parseable.go index 65a3884..b0ecff2 100644 --- a/tools/parseable.go +++ b/tools/parseable.go @@ -5,7 +5,7 @@ import ( "crypto/tls" "encoding/json" "io" - "log" + "log/slog" "net/http" "os" "strconv" @@ -31,7 +31,7 @@ func init() { TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } - log.Printf("UNSECURE=true: HTTP client will skip TLS verification") + slog.Info("UNSECURE=true: HTTP client will skip TLS verification") } else { HTTPClient = http.DefaultClient } @@ -60,7 +60,11 @@ func doParseableQuery(query string, streamName string, startTime string, endTime if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if err := resp.Body.Close(); err != nil { + slog.Error("failed to close response body", "error", err) + } + }() body, _ := io.ReadAll(resp.Body) // Try to unmarshal as array of rows @@ -84,7 +88,7 @@ func listParseableStreams() ([]string, error) { } defer func() { if err := resp.Body.Close(); err != nil { - log.Printf("failed to close response body: %v", err) + slog.Error("failed to close response body", "error", err) } }() var apiResult []struct { @@ -100,7 +104,7 @@ func listParseableStreams() ([]string, error) { return streams, nil } -func getParseableSchema(stream string) (map[string]string, error) { +func getParseableSchema(stream string) (map[string]interface{}, error) { url := ParseableBaseURL + "/api/v1/logstream/" + stream + "/schema" httpReq, err := http.NewRequest("GET", url, nil) if err != nil { @@ -113,28 +117,14 @@ func getParseableSchema(stream string) (map[string]string, error) { } defer func() { if err := resp.Body.Close(); err != nil { - log.Printf("failed to close response body: %v", err) + slog.Error("failed to close response body", "error", err) } }() - var result struct { - Fields []struct { - Name string `json:"name"` - DataType json.RawMessage `json:"data_type"` - } `json:"fields"` - } + var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } - schema := make(map[string]string) - for _, field := range result.Fields { - var dtStr string - if err := json.Unmarshal(field.DataType, &dtStr); err == nil { - schema[field.Name] = dtStr - continue - } - schema[field.Name] = string(field.DataType) - } - return schema, nil + return result, nil } func getParseableStats(streamName string) (map[string]interface{}, error) { @@ -185,7 +175,7 @@ func doSimpleGet(url string) (map[string]interface{}, map[string]interface{}, er } defer func() { if err := resp.Body.Close(); err != nil { - log.Printf("failed to close response body: %v", err) + slog.Error("failed to close response body", "error", err) } }() var stats map[string]interface{} diff --git a/tools/parseable_about.go b/tools/parseable_about.go index 895d9fc..69bbe00 100644 --- a/tools/parseable_about.go +++ b/tools/parseable_about.go @@ -2,8 +2,7 @@ package tools import ( "context" - "fmt" - "strings" + "log/slog" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -12,37 +11,46 @@ import ( func RegisterGetAboutTool(mcpServer *server.MCPServer) { mcpServer.AddTool(mcp.NewTool( "get_about", - mcp.WithDescription(`Get information about the Parseable instance. Calls /api/v1/about. - -Returned fields: -- version: version of Parseable -- uiVersion: the UI version of Parseable -- commit: the git commit hash -- deploymentId: the deployment ID of Parseable -- updateAvailable: if updates of Parseable is available -- latestVersion: the latest version of Parseable -- llmActive: if the Parseable is configured with LLM support -- llmProvider: what LLM provider is used -- oidcActive: if Parseable is configured with OpenID Connect support -- license: the license of Parseable -- mode: if Parseable is running as Standalone or Cluster mode -- staging: the staging path -- hotTier: if hot tier is enabled or disabled -- grpcPort: the grpc port of Parseable -- store: the storage type used for Parseable like local or object store -- analytics: if analytics is enabled or disabled + mcp.WithDescription(`Get configuration and version information about the Parseable instance. +Use this to understand the Parseable deployment, available features, and version compatibility. +Calls /api/v1/about. + +Returns a JSON object with deployment and configuration details: + +Deployment Info: +- version: semantic version of Parseable (e.g., "1.2.0") +- uiVersion: version of the web UI +- commit: git commit hash of the Parseable build +- deploymentId: unique identifier for this Parseable instance +- mode: deployment mode ("Standalone" or "Cluster") + +Update Information: +- updateAvailable: boolean indicating if a newer version is available +- latestVersion: the latest available version of Parseable + +Configuration & Features: +- llmActive: boolean indicating if LLM support is enabled +- llmProvider: name of the LLM provider if configured (e.g., "openai", "anthropic", or null) +- oidcActive: boolean indicating if OpenID Connect authentication is enabled +- analytics: boolean indicating if analytics collection is enabled +- hotTier: boolean indicating if hot tier (fast storage) is enabled + +Storage & Infrastructure: +- store: storage backend type (e.g., "local", "s3", "gcs") +- staging: staging/cache path for data processing +- grpcPort: port number for gRPC API connections + +Licensing: +- license: license type or status of Parseable + +Use this tool to check Parseable capabilities, version information, and configuration state. `), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { about, err := getParseableAbout() if err != nil { + slog.Error("failed to get response", "tool", "get_about", "error", err) return mcp.NewToolResultError(err.Error()), nil } - var lines []string - for k, v := range about { - lines = append(lines, k+": "+fmt.Sprintf("%v", v)) - } - return mcp.NewToolResultText(strings.Join(lines, "\n")), nil - // Optionally, for structured output: - // return mcp.NewToolResultStructured(map[string]interface{}{"info": info}, "Info returned"), nil + return mcp.NewToolResultJSON(about) }) } diff --git a/tools/parseable_get_schema.go b/tools/parseable_get_schema.go index 9c1f2a2..d3a1a7e 100644 --- a/tools/parseable_get_schema.go +++ b/tools/parseable_get_schema.go @@ -2,7 +2,7 @@ package tools import ( "context" - "strings" + "log/slog" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -11,24 +11,31 @@ import ( func RegisterGetDataStreamSchemaTool(mcpServer *server.MCPServer) { mcpServer.AddTool(mcp.NewTool( "get_data_stream_schema", - mcp.WithDescription("Get the schema for a specific data stream in Parseable. The full content of the stream is typically in the 'body' field as a string."), - mcp.WithString("stream", mcp.Required(), mcp.Description("Data stream name")), + mcp.WithDescription(`Get the complete field schema for a Parseable data stream. +Use this to discover field names, data types, and structure before constructing queries. +Calls /api/v1/logstream//schema. + +Returns a JSON object with a 'fields' array containing field definitions for each available field in the stream. +Each field includes: +- name: the field name (string) +- data_type: the data type of the field (e.g., "String", "i64", "f64", "bool", "DateTime") + +Use this tool to understand what fields are available for filtering, grouping, or selecting in query_data_stream operations. +`), + mcp.WithString("streamName", mcp.Required(), mcp.Description("Name of the data stream to get the schema for. Example: 'otellogs' or 'monitor_logstream'. Stream must exist in Parseable.")), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - stream := mcp.ParseString(req, "stream", "") + stream := mcp.ParseString(req, "streamName", "") if stream == "" { - return mcp.NewToolResultError("missing stream in context"), nil + slog.Warn("Missing parameter", "tool", "get_data_stream_schema", "parameter", "streamName") + return mcp.NewToolResultError("missing required field: streamName"), nil } + schema, err := getParseableSchema(stream) if err != nil { + slog.Error("failed to get response", "tool", "get_data_stream_schema", "streamName", stream, "error", err) return mcp.NewToolResultError(err.Error()), nil } - // Default: return as text - var lines []string - for field, typ := range schema { - lines = append(lines, field+": "+typ) - } - return mcp.NewToolResultText(strings.Join(lines, "\n")), nil - // Optionally, for structured output: - // return mcp.NewToolResultStructured(map[string]interface{}{"schema": schema}, "Schema returned"), nil + + return mcp.NewToolResultJSON(schema) }) } diff --git a/tools/parseable_info.go b/tools/parseable_info.go index 54d8ce6..63a6c2c 100644 --- a/tools/parseable_info.go +++ b/tools/parseable_info.go @@ -2,8 +2,7 @@ package tools import ( "context" - "fmt" - "strings" + "log/slog" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -12,35 +11,37 @@ import ( func RegisterGetDataStreamInfoTool(mcpServer *server.MCPServer) { mcpServer.AddTool(mcp.NewTool( "get_data_stream_info", - mcp.WithDescription(`Get info for a Parseable data stream by name. Calls /api/v1/logstream//info. - -Returned fields: -- createdAt: when the data stream was created (ISO 8601) -- firstEventAt: timestamp of the first event (ISO 8601) -- latestEventAt: timestamp of the latest event (ISO 8601) -- streamType: type of data stream (e.g. UserDefined) -- logSource: array of log source objects - - log_source_format: format of the log source (e.g. otel-logs) - - fields: list of field names in the log source -- telemetryType: type of telemetry (e.g. logs, metrics, traces) + mcp.WithDescription(`Get comprehensive metadata information for a Parseable data stream. +Use this to understand stream composition, available fields, and data ingestion timeline. +Calls /api/v1/logstream//info. + +Returns a JSON object with the following structure: + +- createdAt: ISO 8601 timestamp when the data stream was created +- firstEventAt: ISO 8601 timestamp of the first event (null if stream has no events) +- latestEventAt: ISO 8601 timestamp of the most recent event (null if stream has no events) +- streamType: classification of the stream (e.g., "UserDefined", "System") +- logSource: array of log source objects describing data sources + - log_source_format: format of the ingested data (e.g., "otel-logs", "json", "logfmt") + - fields: array of field names available in this data source +- telemetryType: category of telemetry data (e.g., "logs", "metrics", "traces") + +Use this tool before querying a stream to understand its fields and structure. `), - mcp.WithString("streamName", mcp.Required(), mcp.Description("Name of the data stream (e.g. otellogs)")), + mcp.WithString("streamName", mcp.Required(), mcp.Description("Name of the data stream to get info for. Example: 'otellogs' or 'monitor_logstream'. Stream must exist in Parseable.")), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { streamName := mcp.ParseString(req, "streamName", "") if streamName == "" { + slog.Warn("called with missing parameter", "parameter", "streamName", "tool", "get_data_stream_info") return mcp.NewToolResultError("missing required field: streamName"), nil } + info, err := getParseableInfo(streamName) if err != nil { + slog.Error("failed to get response", "streamName", streamName, "error", err, "tool", "get_data_stream_info") return mcp.NewToolResultError("failed to get info: " + err.Error()), nil } - // Default: return as text - var lines []string - for k, v := range info { - lines = append(lines, k+": "+fmt.Sprintf("%v", v)) - } - return mcp.NewToolResultText(strings.Join(lines, "\n")), nil - // Optionally, for structured output: - // return mcp.NewToolResultStructured(map[string]interface{}{"info": info}, "Info returned"), nil + + return mcp.NewToolResultJSON(info) }) } diff --git a/tools/parseable_list_stats.go b/tools/parseable_list_stats.go index c7de5da..5c233b9 100644 --- a/tools/parseable_list_stats.go +++ b/tools/parseable_list_stats.go @@ -2,8 +2,7 @@ package tools import ( "context" - "fmt" - "strings" + "log/slog" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -12,42 +11,43 @@ import ( func RegisterGetDataStreamStatsTool(mcpServer *server.MCPServer) { mcpServer.AddTool(mcp.NewTool( "get_data_stream_stats", - mcp.WithDescription(`Get stats for a Parseable data stream by name. Calls /api/v1/logstream//stats. + mcp.WithDescription(`Get comprehensive statistics for a Parseable data stream, including ingestion and storage metrics. +Use this to monitor stream health, data ingestion rates, and storage usage. Calls /api/v1/logstream//stats. + +Returns a JSON object with the following structure: -Returned fields: - stream: data stream name -- time: stats timestamp (ISO 8601) -- ingestion: object with ingestion stats - - count: number of ingested records - - size: total bytes ingested - - format: data format (e.g. json) - - lifetime_count: cumulative ingested records - - lifetime_size: cumulative ingested bytes +- time: stats timestamp (ISO 8601 format) +- ingestion: object with ingestion metrics + - count: number of records ingested in the current period + - size: total bytes ingested in the current period + - format: data format (e.g., "json") + - lifetime_count: cumulative total records ever ingested + - lifetime_size: cumulative total bytes ever ingested - deleted_count: number of deleted records - - deleted_size: bytes deleted -- storage: object with storage stats - - size: total bytes stored - - format: storage format (e.g. parquet) - - lifetime_size: cumulative stored bytes - - deleted_size: bytes deleted from storage + - deleted_size: bytes freed from deleted records +- storage: object with storage metrics + - size: total bytes currently stored + - format: storage format (e.g., "parquet") + - lifetime_size: cumulative total bytes ever stored + - deleted_size: bytes freed from storage deletions + +All metrics are calculated at the time of the API call and reflect the current state of the stream. `), - mcp.WithString("streamName", mcp.Required(), mcp.Description("Name of the data stream (e.g. otellogs)")), + mcp.WithString("streamName", mcp.Required(), mcp.Description("Name of the data stream to get stats for. Example: 'otellogs' or 'monitor_logstream'. Stream must exist in Parseable.")), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { streamName := mcp.ParseString(req, "streamName", "") if streamName == "" { + slog.Warn("called with missing parameter", "parameter", "streamName", "tool", "get_data_stream_stats") return mcp.NewToolResultError("missing required field: streamName"), nil } + stats, err := getParseableStats(streamName) if err != nil { + slog.Error("failed to get response", "streamName", streamName, "error", err, "tool", "get_data_stream_stats") return mcp.NewToolResultError("failed to get stats: " + err.Error()), nil } - // Default: return as text - var lines []string - for k, v := range stats { - lines = append(lines, k+": "+fmt.Sprintf("%v", v)) - } - return mcp.NewToolResultText(strings.Join(lines, "\n")), nil - // Optionally, for structured output: - // return mcp.NewToolResultStructured(map[string]interface{}{"stats": stats}, "Stats returned"), nil + + return mcp.NewToolResultJSON(stats) }) } diff --git a/tools/parseable_list_streams.go b/tools/parseable_list_streams.go index 60fb322..1100ab2 100644 --- a/tools/parseable_list_streams.go +++ b/tools/parseable_list_streams.go @@ -2,7 +2,7 @@ package tools import ( "context" - "strings" + "log/slog" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -11,14 +11,19 @@ import ( func RegisterListDataStreamsTool(mcpServer *server.MCPServer) { mcpServer.AddTool(mcp.NewTool( "list_data_streams", - mcp.WithDescription("List all available data streams in Parseable. A stream is equivalent to a table in the SQL query."), + mcp.WithDescription("List all available data streams in Parseable. "+ + "Use this tool to discover which data streams are available before executing queries. "+ + "Each stream is a table-like collection of data and must be referenced by exact name in query_data_stream operations. "+ + "Returns a JSON object with a 'streams' array containing stream names as strings. "+ + "All returned streams are accessible and queryable by the current user."), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + slog.Info("listing all data streams") streams, err := listParseableStreams() if err != nil { + slog.Error("failed to get response", "error", err, "tool", "list_data_streams") return mcp.NewToolResultError(err.Error()), nil } - //return mcp.NewToolResultStructured(map[string]interface{}{"streams": streams}, "Streams listed"), nil - return mcp.NewToolResultText(strings.Join(streams, "\n")), nil + return mcp.NewToolResultJSON(map[string]interface{}{"streams": streams}) }) } diff --git a/tools/parseable_query.go b/tools/parseable_query.go index 7cee2e2..40bceb6 100644 --- a/tools/parseable_query.go +++ b/tools/parseable_query.go @@ -2,8 +2,7 @@ package tools import ( "context" - "encoding/json" - "fmt" + "log/slog" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -12,38 +11,51 @@ import ( func RegisterQueryDataStreamTool(mcpServer *server.MCPServer) { mcpServer.AddTool(mcp.NewTool( "query_data_stream", - mcp.WithDescription("Execute a SQL query against a data stream in Parseable. All fields are required. All times must be in ISO 8601 format."), - mcp.WithString("query", mcp.Required(), mcp.Description("SQL query to run, but the FROM must always be set to streamName")), - mcp.WithString("streamName", mcp.Required(), mcp.Description("Name of the data stream (table)")), - mcp.WithString("startTime", mcp.Required(), mcp.Description("Query start time in ISO 8601 (format: yyyy-MM-ddTHH:mm:ss+hh:mm)")), - mcp.WithString("endTime", mcp.Required(), mcp.Description("Query end time in ISO 8601 (format: yyyy-MM-ddTHH:mm:ss+hh:mm)")), + mcp.WithDescription("Execute a SQL query against a data stream in Parseable and retrieve rows of data. "+ + "All parameters are required. "+ + "Supported SQL operations: SELECT with column selection, WHERE conditions (but not time-based), GROUP BY, ORDER BY, LIMIT, and aggregate functions (COUNT, SUM, AVG, MIN, MAX). "+ + "Time filtering is handled by startTime and endTime parameters - do not include time conditions in the WHERE clause. "+ + "The FROM clause table name must exactly match the streamName parameter. "+ + "Returns a JSON object with 'rows' (array of data objects) and 'count' (number of rows returned)."), + mcp.WithString("query", mcp.Required(), mcp.Description("SQL query to execute. FROM clause table must exactly match the streamName parameter. Example: 'SELECT field1, field2 FROM streamName WHERE field1 > 100 ORDER BY timestamp DESC LIMIT 100'")), + mcp.WithString("streamName", mcp.Required(), mcp.Description("Exact name of the data stream (table) to query. Must match the table name in the FROM clause. Example: 'monitor_logstream'")), + mcp.WithString("startTime", mcp.Required(), mcp.Description("Query start time in ISO 8601 format with timezone. Examples: '2026-02-12T00:00:00Z' or '2026-02-12T00:00:00+00:00'")), + mcp.WithString("endTime", mcp.Required(), mcp.Description("Query end time in ISO 8601 format with timezone. Must be after startTime. Examples: '2026-02-12T23:59:59Z' or '2026-02-12T23:59:59+00:00'")), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { query := mcp.ParseString(req, "query", "") streamName := mcp.ParseString(req, "streamName", "") startTime := mcp.ParseString(req, "startTime", "") endTime := mcp.ParseString(req, "endTime", "") if query == "" || streamName == "" || startTime == "" || endTime == "" { + slog.Warn("called with missing parameter", + "query", query, + "streamName", streamName, + "startTime", startTime, + "endTime", endTime) return mcp.NewToolResultError("missing required fields: query, streamName, startTime, and endTime are required"), nil } + + slog.Debug("executing query_data_stream", + "streamName", streamName, + "startTime", startTime, + "endTime", endTime, + "query", query) + queryResult, err := doParseableQuery(query, streamName, startTime, endTime) if err != nil { + slog.Error("failed to get response", + "streamName", streamName, + "error", err, "tool", "query_data_stream", "query", query) return mcp.NewToolResultError("query failed: " + err.Error()), nil } - b, err := json.MarshalIndent(queryResult, "", " ") - if err != nil { - return nil, err - } - - // Optional: a one-liner summary that sets expectations. - text := fmt.Sprintf( - "Returned %d rows as JSON (array of objects). Use keys exactly as shown.\n```json\n%s\n```", - len(queryResult), - string(b), - ) + slog.Debug("query_data_stream completed successfully", + "streamName", streamName, + "rowCount", len(queryResult)) - return mcp.NewToolResultText(text), nil - // Optionally, for structured output: - // return mcp.NewToolResultStructured(map[string]interface{}{"result": result}, "Query successful"), nil + return mcp.NewToolResultJSON(map[string]interface{}{ + "rows": queryResult, + "count": len(queryResult), + }) }) } diff --git a/tools/parseable_roles.go b/tools/parseable_roles.go index ef18e20..babd975 100644 --- a/tools/parseable_roles.go +++ b/tools/parseable_roles.go @@ -2,8 +2,7 @@ package tools import ( "context" - "fmt" - "strings" + "log/slog" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -12,29 +11,35 @@ import ( func RegisterGetRolesTool(mcpServer *server.MCPServer) { mcpServer.AddTool(mcp.NewTool( "get_roles", - mcp.WithDescription(`Get information about the Parseable roles. Roles is used for handling RBAC permissions/privilege and define access to datasets. Calls /api/v1/roles. + mcp.WithDescription(`Get role-based access control (RBAC) information for the Parseable instance. +Use this to understand user roles, permissions, and dataset access controls. +Calls /api/v1/role. -Data is returned as a dictionary with the role name and a list of privilege. The privilege can be of the following: -- admin - have all privileges -- editor - have limited privileges like cluster features -- reader - allow read from datasets -- writer - allow read and write from datasets -- ingestor - allow write from datasets +Returns a JSON object where each key is a role name and the value is an array of privileges assigned to that role. -For reader, writer and ingestor role there is always at least one resource connected to the role. This resources is typical a dataset. -For full description of roles and RBAC use https://www.parseable.com/docs/user-guide/rbac +Available Privilege Types: +- admin: Full system access with all privileges (no resource restrictions) +- editor: Limited administrative privileges for cluster features (no resource restrictions) +- reader: Read-only access to specific datasets (requires at least one dataset resource) +- writer: Read and write access to specific datasets (requires at least one dataset resource) +- ingestor: Write-only access to specific datasets for data ingestion (requires at least one dataset resource) + +Resource Assignment: +- admin and editor roles apply globally across the entire Parseable instance +- reader, writer, and ingestor roles are always associated with specific dataset resources +- Each role entry includes the list of datasets (resources) the role has access to + +Use this tool to understand access controls before querying or ingesting data. +For detailed RBAC documentation, see: https://www.parseable.com/docs/user-guide/rbac `), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - about, err := getParseableRoles() + slog.Info("fetching roles information") + roles, err := getParseableRoles() if err != nil { + slog.Error("failed to get roles", "error", err) return mcp.NewToolResultError(err.Error()), nil } - var lines []string - for k, v := range about { - lines = append(lines, k+": "+fmt.Sprintf("%v", v)) - } - return mcp.NewToolResultText(strings.Join(lines, "\n")), nil - // Optionally, for structured output: - // return mcp.NewToolResultStructured(map[string]interface{}{"info": info}, "Info returned"), nil + slog.Info("successfully retrieved roles", "count", len(roles)) + return mcp.NewToolResultJSON(roles) }) }