Skip to content

Commit 2f45e40

Browse files
author
andersh
committed
fix: update the format returned by all tools, improve description and logging
1 parent 2b89cce commit 2f45e40

10 files changed

Lines changed: 237 additions & 161 deletions

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,46 @@ This project provides an MCP (Message Context Protocol) server for [Parseable](h
1717
- Modular MCP tool registration for easy extension
1818
- Supports both HTTP and stdio MCP modes
1919
- Environment variable and flag-based configuration
20+
- The mcp server returns responses in json where the payload is both in text and structured format.
21+
22+
23+
# Testing
24+
To test the server you can use the [mcp-cli](https://github.com/philschmid/mcp-cli)
25+
```shell
26+
mcp-cli call parseable get_roles
27+
```
28+
Returns
29+
```json
30+
{
31+
"content": [
32+
{
33+
"type": "text",
34+
"text": "{\"admins\":[{\"privilege\":\"admin\"}],\"network_role\":[{\"privilege\":\"reader\",\"resource\":{\"stream\":\"network_logstream\"}}],\"otel_gateway\":[{\"privilege\":\"editor\"}]}"
35+
}
36+
],
37+
"structuredContent": {
38+
"admins": [
39+
{
40+
"privilege": "admin"
41+
}
42+
],
43+
"network_role": [
44+
{
45+
"privilege": "reader",
46+
"resource": {
47+
"stream": "network_logstream"
48+
}
49+
}
50+
],
51+
"otel_gateway": [
52+
{
53+
"privilege": "editor"
54+
}
55+
]
56+
}
57+
}
58+
59+
```
2060

2161
# Building
2262

cmd/mcp_parseable_server/main.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package main
22

33
import (
44
"flag"
5-
"log"
5+
"log/slog"
66
"os"
77

88
"github.com/mark3labs/mcp-go/server"
@@ -21,6 +21,12 @@ func main() {
2121
versionFlag := flag.Bool("version", false, "print version and exit")
2222
flag.Parse()
2323

24+
// Setup structured logger for stdout
25+
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
26+
Level: slog.LevelInfo,
27+
}))
28+
slog.SetDefault(logger)
29+
2430
if *versionFlag {
2531
println("mcp-parseable-server " + version)
2632
os.Exit(0)
@@ -72,16 +78,18 @@ Try not to second guess information - if you don't know something or lack inform
7278
tools.RegisterParseableTools(mcpServer)
7379

7480
if *mode == "stdio" {
75-
log.Printf("MCP server running in stdio mode (Parseable at %s)", tools.ParseableBaseURL)
81+
slog.Info("MCP server running in stdio mode", "parseable_url", tools.ParseableBaseURL)
7682
if err := server.ServeStdio(mcpServer); err != nil {
77-
log.Fatalf("MCP stdio server failed: %v", err)
83+
slog.Error("MCP stdio server failed", "error", err)
84+
os.Exit(1)
7885
}
7986
return
8087
}
8188

8289
httpServer := server.NewStreamableHTTPServer(mcpServer)
83-
log.Printf("MCP server running on %s, Parseable at %s", *listenAddr, tools.ParseableBaseURL)
90+
slog.Info("MCP server running", "address", *listenAddr, "parseable_url", tools.ParseableBaseURL)
8491
if err := httpServer.Start(*listenAddr); err != nil {
85-
log.Fatalf("MCP server failed: %v", err)
92+
slog.Error("MCP server failed", "error", err)
93+
os.Exit(1)
8694
}
8795
}

tools/parseable.go

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"crypto/tls"
66
"encoding/json"
77
"io"
8-
"log"
8+
"log/slog"
99
"net/http"
1010
"os"
1111
"strconv"
@@ -31,7 +31,7 @@ func init() {
3131
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
3232
},
3333
}
34-
log.Printf("UNSECURE=true: HTTP client will skip TLS verification")
34+
slog.Info("UNSECURE=true: HTTP client will skip TLS verification")
3535
} else {
3636
HTTPClient = http.DefaultClient
3737
}
@@ -60,7 +60,11 @@ func doParseableQuery(query string, streamName string, startTime string, endTime
6060
if err != nil {
6161
return nil, err
6262
}
63-
defer resp.Body.Close()
63+
defer func() {
64+
if err := resp.Body.Close(); err != nil {
65+
slog.Error("failed to close response body", "error", err)
66+
}
67+
}()
6468
body, _ := io.ReadAll(resp.Body)
6569

6670
// Try to unmarshal as array of rows
@@ -84,7 +88,7 @@ func listParseableStreams() ([]string, error) {
8488
}
8589
defer func() {
8690
if err := resp.Body.Close(); err != nil {
87-
log.Printf("failed to close response body: %v", err)
91+
slog.Error("failed to close response body", "error", err)
8892
}
8993
}()
9094
var apiResult []struct {
@@ -100,7 +104,7 @@ func listParseableStreams() ([]string, error) {
100104
return streams, nil
101105
}
102106

103-
func getParseableSchema(stream string) (map[string]string, error) {
107+
func getParseableSchema(stream string) (map[string]interface{}, error) {
104108
url := ParseableBaseURL + "/api/v1/logstream/" + stream + "/schema"
105109
httpReq, err := http.NewRequest("GET", url, nil)
106110
if err != nil {
@@ -113,28 +117,14 @@ func getParseableSchema(stream string) (map[string]string, error) {
113117
}
114118
defer func() {
115119
if err := resp.Body.Close(); err != nil {
116-
log.Printf("failed to close response body: %v", err)
120+
slog.Error("failed to close response body", "error", err)
117121
}
118122
}()
119-
var result struct {
120-
Fields []struct {
121-
Name string `json:"name"`
122-
DataType json.RawMessage `json:"data_type"`
123-
} `json:"fields"`
124-
}
123+
var result map[string]interface{}
125124
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
126125
return nil, err
127126
}
128-
schema := make(map[string]string)
129-
for _, field := range result.Fields {
130-
var dtStr string
131-
if err := json.Unmarshal(field.DataType, &dtStr); err == nil {
132-
schema[field.Name] = dtStr
133-
continue
134-
}
135-
schema[field.Name] = string(field.DataType)
136-
}
137-
return schema, nil
127+
return result, nil
138128
}
139129

140130
func getParseableStats(streamName string) (map[string]interface{}, error) {
@@ -185,7 +175,7 @@ func doSimpleGet(url string) (map[string]interface{}, map[string]interface{}, er
185175
}
186176
defer func() {
187177
if err := resp.Body.Close(); err != nil {
188-
log.Printf("failed to close response body: %v", err)
178+
slog.Error("failed to close response body", "error", err)
189179
}
190180
}()
191181
var stats map[string]interface{}

tools/parseable_about.go

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ package tools
22

33
import (
44
"context"
5-
"fmt"
6-
"strings"
5+
"log/slog"
76

87
"github.com/mark3labs/mcp-go/mcp"
98
"github.com/mark3labs/mcp-go/server"
@@ -12,37 +11,46 @@ import (
1211
func RegisterGetAboutTool(mcpServer *server.MCPServer) {
1312
mcpServer.AddTool(mcp.NewTool(
1413
"get_about",
15-
mcp.WithDescription(`Get information about the Parseable instance. Calls /api/v1/about.
16-
17-
Returned fields:
18-
- version: version of Parseable
19-
- uiVersion: the UI version of Parseable
20-
- commit: the git commit hash
21-
- deploymentId: the deployment ID of Parseable
22-
- updateAvailable: if updates of Parseable is available
23-
- latestVersion: the latest version of Parseable
24-
- llmActive: if the Parseable is configured with LLM support
25-
- llmProvider: what LLM provider is used
26-
- oidcActive: if Parseable is configured with OpenID Connect support
27-
- license: the license of Parseable
28-
- mode: if Parseable is running as Standalone or Cluster mode
29-
- staging: the staging path
30-
- hotTier: if hot tier is enabled or disabled
31-
- grpcPort: the grpc port of Parseable
32-
- store: the storage type used for Parseable like local or object store
33-
- analytics: if analytics is enabled or disabled
14+
mcp.WithDescription(`Get configuration and version information about the Parseable instance.
15+
Use this to understand the Parseable deployment, available features, and version compatibility.
16+
Calls /api/v1/about.
17+
18+
Returns a JSON object with deployment and configuration details:
19+
20+
Deployment Info:
21+
- version: semantic version of Parseable (e.g., "1.2.0")
22+
- uiVersion: version of the web UI
23+
- commit: git commit hash of the Parseable build
24+
- deploymentId: unique identifier for this Parseable instance
25+
- mode: deployment mode ("Standalone" or "Cluster")
26+
27+
Update Information:
28+
- updateAvailable: boolean indicating if a newer version is available
29+
- latestVersion: the latest available version of Parseable
30+
31+
Configuration & Features:
32+
- llmActive: boolean indicating if LLM support is enabled
33+
- llmProvider: name of the LLM provider if configured (e.g., "openai", "anthropic", or null)
34+
- oidcActive: boolean indicating if OpenID Connect authentication is enabled
35+
- analytics: boolean indicating if analytics collection is enabled
36+
- hotTier: boolean indicating if hot tier (fast storage) is enabled
37+
38+
Storage & Infrastructure:
39+
- store: storage backend type (e.g., "local", "s3", "gcs")
40+
- staging: staging/cache path for data processing
41+
- grpcPort: port number for gRPC API connections
42+
43+
Licensing:
44+
- license: license type or status of Parseable
45+
46+
Use this tool to check Parseable capabilities, version information, and configuration state.
3447
`),
3548
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
3649
about, err := getParseableAbout()
3750
if err != nil {
51+
slog.Error("failed to get response", "tool", "get_about", "error", err)
3852
return mcp.NewToolResultError(err.Error()), nil
3953
}
40-
var lines []string
41-
for k, v := range about {
42-
lines = append(lines, k+": "+fmt.Sprintf("%v", v))
43-
}
44-
return mcp.NewToolResultText(strings.Join(lines, "\n")), nil
45-
// Optionally, for structured output:
46-
// return mcp.NewToolResultStructured(map[string]interface{}{"info": info}, "Info returned"), nil
54+
return mcp.NewToolResultJSON(about)
4755
})
4856
}

tools/parseable_get_schema.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package tools
22

33
import (
44
"context"
5-
"strings"
5+
"log/slog"
66

77
"github.com/mark3labs/mcp-go/mcp"
88
"github.com/mark3labs/mcp-go/server"
@@ -11,24 +11,31 @@ import (
1111
func RegisterGetDataStreamSchemaTool(mcpServer *server.MCPServer) {
1212
mcpServer.AddTool(mcp.NewTool(
1313
"get_data_stream_schema",
14-
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."),
15-
mcp.WithString("stream", mcp.Required(), mcp.Description("Data stream name")),
14+
mcp.WithDescription(`Get the complete field schema for a Parseable data stream.
15+
Use this to discover field names, data types, and structure before constructing queries.
16+
Calls /api/v1/logstream/<streamName>/schema.
17+
18+
Returns a JSON object with a 'fields' array containing field definitions for each available field in the stream.
19+
Each field includes:
20+
- name: the field name (string)
21+
- data_type: the data type of the field (e.g., "String", "i64", "f64", "bool", "DateTime")
22+
23+
Use this tool to understand what fields are available for filtering, grouping, or selecting in query_data_stream operations.
24+
`),
25+
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.")),
1626
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
17-
stream := mcp.ParseString(req, "stream", "")
27+
stream := mcp.ParseString(req, "streamName", "")
1828
if stream == "" {
19-
return mcp.NewToolResultError("missing stream in context"), nil
29+
slog.Warn("Missing parameter", "tool", "get_data_stream_schema", "parameter", "streamName")
30+
return mcp.NewToolResultError("missing required field: streamName"), nil
2031
}
32+
2133
schema, err := getParseableSchema(stream)
2234
if err != nil {
35+
slog.Error("failed to get response", "tool", "get_data_stream_schema", "streamName", stream, "error", err)
2336
return mcp.NewToolResultError(err.Error()), nil
2437
}
25-
// Default: return as text
26-
var lines []string
27-
for field, typ := range schema {
28-
lines = append(lines, field+": "+typ)
29-
}
30-
return mcp.NewToolResultText(strings.Join(lines, "\n")), nil
31-
// Optionally, for structured output:
32-
// return mcp.NewToolResultStructured(map[string]interface{}{"schema": schema}, "Schema returned"), nil
38+
39+
return mcp.NewToolResultJSON(schema)
3340
})
3441
}

tools/parseable_info.go

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ package tools
22

33
import (
44
"context"
5-
"fmt"
6-
"strings"
5+
"log/slog"
76

87
"github.com/mark3labs/mcp-go/mcp"
98
"github.com/mark3labs/mcp-go/server"
@@ -12,35 +11,37 @@ import (
1211
func RegisterGetDataStreamInfoTool(mcpServer *server.MCPServer) {
1312
mcpServer.AddTool(mcp.NewTool(
1413
"get_data_stream_info",
15-
mcp.WithDescription(`Get info for a Parseable data stream by name. Calls /api/v1/logstream/<streamName>/info.
16-
17-
Returned fields:
18-
- createdAt: when the data stream was created (ISO 8601)
19-
- firstEventAt: timestamp of the first event (ISO 8601)
20-
- latestEventAt: timestamp of the latest event (ISO 8601)
21-
- streamType: type of data stream (e.g. UserDefined)
22-
- logSource: array of log source objects
23-
- log_source_format: format of the log source (e.g. otel-logs)
24-
- fields: list of field names in the log source
25-
- telemetryType: type of telemetry (e.g. logs, metrics, traces)
14+
mcp.WithDescription(`Get comprehensive metadata information for a Parseable data stream.
15+
Use this to understand stream composition, available fields, and data ingestion timeline.
16+
Calls /api/v1/logstream/<streamName>/info.
17+
18+
Returns a JSON object with the following structure:
19+
20+
- createdAt: ISO 8601 timestamp when the data stream was created
21+
- firstEventAt: ISO 8601 timestamp of the first event (null if stream has no events)
22+
- latestEventAt: ISO 8601 timestamp of the most recent event (null if stream has no events)
23+
- streamType: classification of the stream (e.g., "UserDefined", "System")
24+
- logSource: array of log source objects describing data sources
25+
- log_source_format: format of the ingested data (e.g., "otel-logs", "json", "logfmt")
26+
- fields: array of field names available in this data source
27+
- telemetryType: category of telemetry data (e.g., "logs", "metrics", "traces")
28+
29+
Use this tool before querying a stream to understand its fields and structure.
2630
`),
27-
mcp.WithString("streamName", mcp.Required(), mcp.Description("Name of the data stream (e.g. otellogs)")),
31+
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.")),
2832
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
2933
streamName := mcp.ParseString(req, "streamName", "")
3034
if streamName == "" {
35+
slog.Warn("called with missing parameter", "parameter", "streamName", "tool", "get_data_stream_info")
3136
return mcp.NewToolResultError("missing required field: streamName"), nil
3237
}
38+
3339
info, err := getParseableInfo(streamName)
3440
if err != nil {
41+
slog.Error("failed to get response", "streamName", streamName, "error", err, "tool", "get_data_stream_info")
3542
return mcp.NewToolResultError("failed to get info: " + err.Error()), nil
3643
}
37-
// Default: return as text
38-
var lines []string
39-
for k, v := range info {
40-
lines = append(lines, k+": "+fmt.Sprintf("%v", v))
41-
}
42-
return mcp.NewToolResultText(strings.Join(lines, "\n")), nil
43-
// Optionally, for structured output:
44-
// return mcp.NewToolResultStructured(map[string]interface{}{"info": info}, "Info returned"), nil
44+
45+
return mcp.NewToolResultJSON(info)
4546
})
4647
}

0 commit comments

Comments
 (0)