Skip to content

Commit 888d4bb

Browse files
author
andersh
committed
fix: add log level, change list_data_stream to get_data_stream, add count on all array responses, add users tool
1 parent 78897af commit 888d4bb

7 files changed

Lines changed: 138 additions & 63 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ This project provides an MCP (Message Context Protocol) server for [Parseable](h
1919
- Environment variable and flag-based configuration
2020
- The mcp server returns responses in json where the payload is both in text and structured format.
2121

22+
> In Parseable dataset and data stream names are used interchangeably, as Parseable's datasets are essentially
23+
> named data streams. In all tool description we try to use the term data stream to avoid confusion with the term dataset which can have different
24+
> meanings in other contexts.
2225
2326
# Testing
2427
To test the server you can use the [mcp-cli](https://github.com/philschmid/mcp-cli)
@@ -96,6 +99,8 @@ You can configure the Parseable connection using environment variables or flags:
9699
- `PARSEABLE_PASSWORD` or `--parseable-pass` (default: admin)
97100
- `LISTEN_ADDR` or `--listen` - the address when running the mcp server in http mode (default: :9034)
98101
- `INSECURE` - set to `true` to skip TLS verification (default: false)`
102+
- `LOG_LEVEL` - set log level. Supported levels are debug, info, warn and error (default: info)
103+
99104
Example:
100105
```sh
101106
PARSEABLE_URL="http://your-parseable-host:8000" PARSEABLE_USER="admin" PARSEABLE_PASS="admin" ./mcp-parseable-server
@@ -112,7 +117,7 @@ Execute a SQL query against a data stream.
112117
- `endTime`: ISO 8601 end time
113118
- **Returns:** Query result
114119

115-
## 2. `list_data_streams`
120+
## 2. `get_data_streams`
116121
List all available data streams in Parseable.
117122
- **Returns:** Array of stream names
118123

cmd/mcp_parseable_server/main.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,34 @@ func main() {
1818
parseableUserFlag := flag.String("parseable-username", "", "Parseable basic auth username (or set PARSEABLE_USER env var)")
1919
parseablePassFlag := flag.String("parseable-password", "", "Parseable basic auth password (or set PARSEABLE_PASS env var)")
2020
listenAddr := flag.String("listen", ":9034", "address to listen on")
21+
logLevel := flag.String("log-level", "info", "log level: debug, info, warn, error (or set LOG_LEVEL env var)")
2122
versionFlag := flag.Bool("version", false, "print version and exit")
2223
flag.Parse()
2324

25+
// Determine log level from environment variable or flag
26+
logLevelStr := os.Getenv("LOG_LEVEL")
27+
if logLevelStr == "" {
28+
logLevelStr = *logLevel
29+
}
30+
31+
// Parse log level string to slog.Level
32+
var level slog.Level
33+
switch logLevelStr {
34+
case "debug":
35+
level = slog.LevelDebug
36+
case "info":
37+
level = slog.LevelInfo
38+
case "warn":
39+
level = slog.LevelWarn
40+
case "error":
41+
level = slog.LevelError
42+
default:
43+
level = slog.LevelInfo
44+
}
45+
2446
// Setup structured logger for stdout
2547
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
26-
Level: slog.LevelInfo,
48+
Level: level,
2749
}))
2850
slog.SetDefault(logger)
2951

tools/parseable.go

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -75,33 +75,9 @@ func doParseableQuery(query string, streamName string, startTime string, endTime
7575
return arrResult, nil
7676
}
7777

78-
func listParseableStreams() ([]string, error) {
78+
func listParseableStreams() ([]map[string]interface{}, error) {
7979
url := ParseableBaseURL + "/api/v1/logstream"
80-
httpReq, err := http.NewRequest("GET", url, nil)
81-
if err != nil {
82-
return nil, err
83-
}
84-
addBasicAuth(httpReq)
85-
resp, err := HTTPClient.Do(httpReq)
86-
if err != nil {
87-
return nil, err
88-
}
89-
defer func() {
90-
if err := resp.Body.Close(); err != nil {
91-
slog.Error("failed to close response body", "error", err)
92-
}
93-
}()
94-
var apiResult []struct {
95-
Name string `json:"name"`
96-
}
97-
if err := json.NewDecoder(resp.Body).Decode(&apiResult); err != nil {
98-
return nil, err
99-
}
100-
streams := make([]string, 0, len(apiResult))
101-
for _, s := range apiResult {
102-
streams = append(streams, s.Name)
103-
}
104-
return streams, nil
80+
return doSimpleGetArray(url)
10581
}
10682

10783
func getParseableSchema(stream string) (map[string]interface{}, error) {
@@ -129,38 +105,31 @@ func getParseableSchema(stream string) (map[string]interface{}, error) {
129105

130106
func getParseableStats(streamName string) (map[string]interface{}, error) {
131107
url := ParseableBaseURL + "/api/v1/logstream/" + streamName + "/stats"
132-
stats, m, err := doSimpleGet(url)
133-
if err != nil {
134-
return m, err
135-
}
136-
return stats, nil
108+
stats, _, err := doSimpleGet(url)
109+
return stats, err
137110
}
138111

139112
func getParseableInfo(streamName string) (map[string]interface{}, error) {
140113
url := ParseableBaseURL + "/api/v1/logstream/" + streamName + "/info"
141-
info, m, err := doSimpleGet(url)
142-
if err != nil {
143-
return m, err
144-
}
145-
return info, nil
114+
info, _, err := doSimpleGet(url)
115+
return info, err
146116
}
147117

148118
func getParseableAbout() (map[string]interface{}, error) {
149119
url := ParseableBaseURL + "/api/v1/about"
150-
about, m, err := doSimpleGet(url)
151-
if err != nil {
152-
return m, err
153-
}
154-
return about, nil
120+
about, _, err := doSimpleGet(url)
121+
return about, err
155122
}
156123

157124
func getParseableRoles() (map[string]interface{}, error) {
158125
url := ParseableBaseURL + "/api/v1/roles"
159-
roles, m, err := doSimpleGet(url)
160-
if err != nil {
161-
return m, err
162-
}
163-
return roles, nil
126+
roles, _, err := doSimpleGet(url)
127+
return roles, err
128+
}
129+
130+
func getParseableUsers() ([]map[string]interface{}, error) {
131+
url := ParseableBaseURL + "/api/v1/users"
132+
return doSimpleGetArray(url)
164133
}
165134

166135
func doSimpleGet(url string) (map[string]interface{}, map[string]interface{}, error) {
@@ -178,9 +147,31 @@ func doSimpleGet(url string) (map[string]interface{}, map[string]interface{}, er
178147
slog.Error("failed to close response body", "error", err)
179148
}
180149
}()
181-
var stats map[string]interface{}
182-
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
150+
var response map[string]interface{}
151+
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
183152
return nil, nil, err
184153
}
185-
return stats, nil, nil
154+
return response, nil, nil
155+
}
156+
157+
func doSimpleGetArray(url string) ([]map[string]interface{}, error) {
158+
httpReq, err := http.NewRequest("GET", url, nil)
159+
if err != nil {
160+
return nil, err
161+
}
162+
addBasicAuth(httpReq)
163+
resp, err := HTTPClient.Do(httpReq)
164+
if err != nil {
165+
return nil, err
166+
}
167+
defer func() {
168+
if err := resp.Body.Close(); err != nil {
169+
slog.Error("failed to close response body", "error", err)
170+
}
171+
}()
172+
var response []map[string]interface{}
173+
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
174+
return nil, err
175+
}
176+
return response, nil
186177
}

tools/parseable_list_streams.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,22 @@ import (
1010

1111
func RegisterListDataStreamsTool(mcpServer *server.MCPServer) {
1212
mcpServer.AddTool(mcp.NewTool(
13-
"list_data_streams",
13+
"get_data_streams",
1414
mcp.WithDescription("List all available data streams in Parseable. "+
1515
"Use this tool to discover which data streams are available before executing queries. "+
1616
"Each stream is a table-like collection of data and must be referenced by exact name in query_data_stream operations. "+
17-
"Returns a JSON object with a 'streams' array containing stream names as strings. "+
17+
"Returns a JSON object with a 'streams' array containing stream objects with metadata (including 'name' field for the stream name) and 'count' (number of streams). "+
1818
"All returned streams are accessible and queryable by the current user."),
1919
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
20-
slog.Info("listing all data streams")
2120
streams, err := listParseableStreams()
2221
if err != nil {
23-
slog.Error("failed to get response", "error", err, "tool", "list_data_streams")
22+
slog.Error("failed to get response", "error", err, "tool", "get_data_streams")
2423
return mcp.NewToolResultError(err.Error()), nil
2524
}
2625

27-
return mcp.NewToolResultJSON(map[string]interface{}{"streams": streams})
26+
return mcp.NewToolResultJSON(map[string]interface{}{
27+
"streams": streams,
28+
"count": len(streams),
29+
})
2830
})
2931
}

tools/parseable_roles.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,23 @@ func RegisterGetRolesTool(mcpServer *server.MCPServer) {
1212
mcpServer.AddTool(mcp.NewTool(
1313
"get_roles",
1414
mcp.WithDescription(`Get role-based access control (RBAC) information for the Parseable instance.
15-
Use this to understand user roles, permissions, and dataset access controls.
15+
Use this to understand user roles, permissions, and data stream access controls.
1616
Calls /api/v1/role.
1717
1818
Returns a JSON object where each key is a role name and the value is an array of privileges assigned to that role.
1919
2020
Available Privilege Types:
2121
- admin: Full system access with all privileges (no resource restrictions)
2222
- editor: Limited administrative privileges for cluster features (no resource restrictions)
23-
- reader: Read-only access to specific datasets (requires at least one dataset resource)
24-
- writer: Read and write access to specific datasets (requires at least one dataset resource)
25-
- ingestor: Write-only access to specific datasets for data ingestion (requires at least one dataset resource)
23+
- reader: Read-only access to specific data streams (requires at least one stream resource)
24+
- writer: Read and write access to specific data streams (requires at least one stream resource)
25+
- ingestor: Write-only access to specific data streams for data ingestion (requires at least one stream resource)
2626
2727
Resource Assignment:
2828
- admin and editor roles apply globally across the entire Parseable instance
29-
- reader, writer, and ingestor roles are always associated with specific dataset resources
30-
- Each role entry includes the list of datasets (resources) the role has access to
29+
- reader, writer, and ingestor roles are always associated with specific stream resources
30+
- Each role entry includes the list of data streams (resources) the role has access to
31+
- Note: In Parseable, "dataset" and "data stream" are synonymous terms referring to the same concept
3132
3233
Use this tool to understand access controls before querying or ingesting data.
3334
For detailed RBAC documentation, see: https://www.parseable.com/docs/user-guide/rbac
@@ -38,7 +39,6 @@ For detailed RBAC documentation, see: https://www.parseable.com/docs/user-guide/
3839
slog.Error("failed to get roles", "error", err)
3940
return mcp.NewToolResultError(err.Error()), nil
4041
}
41-
4242
return mcp.NewToolResultJSON(roles)
4343
})
4444
}

tools/parseable_users.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
7+
"github.com/mark3labs/mcp-go/mcp"
8+
"github.com/mark3labs/mcp-go/server"
9+
)
10+
11+
func RegisterGetUsersTool(mcpServer *server.MCPServer) {
12+
mcpServer.AddTool(mcp.NewTool(
13+
"get_users",
14+
mcp.WithDescription(`Get all configured users in the Parseable instance with their authentication methods and role assignments.
15+
Use this to understand user access, authentication configuration, and role-based permissions.
16+
Calls /api/v1/users.
17+
18+
Returns a JSON object with a 'users' array where each element is a user object with the following structure, plus 'count' (number of users):
19+
20+
User Information:
21+
- id: unique user identifier
22+
- username: the user's login name
23+
- method: authentication method ("native" for local auth, "oidc" for OpenID Connect)
24+
- email: user's email address (null if not configured)
25+
- picture: user's profile picture URL (null if not set)
26+
27+
Role Assignments:
28+
- roles: object mapping role names to arrays of privilege grants
29+
Each privilege grant contains:
30+
- privilege: the permission level ("admin", "editor", "reader", "writer", or "ingestor")
31+
- resource: object specifying the resource this privilege applies to
32+
- stream: the data stream name this privilege grants access to
33+
- group_roles: object containing roles inherited from group membership
34+
- user_groups: array of groups this user belongs to
35+
36+
Use this tool to:
37+
- Verify user access permissions before executing operations
38+
- Understand which users have access to specific data streams
39+
- Check authentication methods configured for users
40+
- Audit user-role-stream relationships
41+
`),
42+
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
43+
users, err := getParseableUsers()
44+
if err != nil {
45+
slog.Error("failed to get users", "error", err)
46+
return mcp.NewToolResultError(err.Error()), nil
47+
}
48+
49+
return mcp.NewToolResultJSON(map[string]interface{}{
50+
"users": users,
51+
"count": len(users),
52+
})
53+
})
54+
}

tools/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ func RegisterParseableTools(mcpServer *server.MCPServer) {
1010
RegisterGetDataStreamInfoTool(mcpServer)
1111
RegisterGetAboutTool(mcpServer)
1212
RegisterGetRolesTool(mcpServer)
13+
RegisterGetUsersTool(mcpServer)
1314
}

0 commit comments

Comments
 (0)