Skip to content

Commit a224879

Browse files
authored
Merge pull request #1 from thenodon/development
Minor feature and fixes
2 parents c022d52 + 7fcc9bd commit a224879

7 files changed

Lines changed: 201 additions & 56 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ This mode is used for CLI or agent-to-agent workflows.
5252
You can configure the Parseable connection using environment variables or flags:
5353

5454
- `PARSEABLE_URL` or `--parseable-url`- url to the parseable instance (default: http://localhost:8000)
55-
- `PARSEABLE_USER` or `--parseable-user` (default: admin)
56-
- `PARSEABLE_PASS` or `--parseable-pass` (default: admin)
55+
- `PARSEABLE_USERNAME` or `--parseable-user` (default: admin)
56+
- `PARSEABLE_PASSWORD` or `--parseable-pass` (default: admin)
5757
- `LISTEN_ADDR` or `--listen` - the address when running the mcp server in http mode (default: :9034)
58-
58+
- `INSECURE` - set to `true` to skip TLS verification (default: false)`
5959
Example:
6060
```sh
6161
PARSEABLE_URL="http://your-parseable-host:8000" PARSEABLE_USER="admin" PARSEABLE_PASS="admin" ./mcp-parseable-server

cmd/mcp_parseable_server/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ func main() {
3535
tools.ParseableBaseURL = "http://localhost:8000"
3636
}
3737
}
38-
tools.ParseableUser = os.Getenv("PARSEABLE_USER")
38+
tools.ParseableUser = os.Getenv("PARSEABLE_USERNAME")
3939
if tools.ParseableUser == "" {
4040
if *parseableUserFlag != "" {
4141
tools.ParseableUser = *parseableUserFlag
4242
} else {
4343
tools.ParseableUser = "admin"
4444
}
4545
}
46-
tools.ParseablePass = os.Getenv("PARSEABLE_PASS")
46+
tools.ParseablePass = os.Getenv("PARSEABLE_PASSWORD")
4747
if tools.ParseablePass == "" {
4848
if *parseablePassFlag != "" {
4949
tools.ParseablePass = *parseablePassFlag

tools/parseable.go

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package tools
22

33
import (
4+
"bytes"
5+
"crypto/tls"
46
"encoding/json"
7+
"io"
58
"log"
69
"net/http"
10+
"os"
11+
"strconv"
712
)
813

914
// These variables must be set by main.go before calling RegisterParseableTools
@@ -13,18 +18,67 @@ var (
1318
ParseablePass string
1419
)
1520

21+
// package-level HTTP client; initialized in init() to respect UNSECURE env var
22+
var HTTPClient *http.Client
23+
24+
func init() {
25+
// UNSECURE environment variable controls whether TLS verification is skipped.
26+
// Accepts the same values as strconv.ParseBool (true/1/t etc.).
27+
unsecureEnv := os.Getenv("UNSECURE")
28+
if ok, _ := strconv.ParseBool(unsecureEnv); ok {
29+
HTTPClient = &http.Client{
30+
Transport: &http.Transport{
31+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
32+
},
33+
}
34+
log.Printf("UNSECURE=true: HTTP client will skip TLS verification")
35+
} else {
36+
HTTPClient = http.DefaultClient
37+
}
38+
}
39+
1640
func addBasicAuth(req *http.Request) {
1741
req.SetBasicAuth(ParseableUser, ParseablePass)
1842
}
1943

44+
func doParseableQuery(query string, streamName string, startTime string, endTime string) ([]map[string]interface{}, error) {
45+
payload := map[string]string{
46+
"query": query,
47+
"streamName": streamName,
48+
"startTime": startTime,
49+
"endTime": endTime,
50+
}
51+
jsonPayload, _ := json.Marshal(payload)
52+
url := ParseableBaseURL + parseableSQLPath
53+
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload))
54+
if err != nil {
55+
return nil, err
56+
}
57+
httpReq.Header.Set("Content-Type", "application/json")
58+
addBasicAuth(httpReq)
59+
resp, err := HTTPClient.Do(httpReq)
60+
if err != nil {
61+
return nil, err
62+
}
63+
defer resp.Body.Close()
64+
body, _ := io.ReadAll(resp.Body)
65+
66+
// Try to unmarshal as array of rows
67+
var arrResult []map[string]interface{}
68+
if err := json.Unmarshal(body, &arrResult); err != nil {
69+
return nil, err
70+
}
71+
return arrResult, nil
72+
}
73+
2074
func listParseableStreams() ([]string, error) {
2175
url := ParseableBaseURL + "/api/v1/logstream"
2276
httpReq, err := http.NewRequest("GET", url, nil)
2377
if err != nil {
2478
return nil, err
2579
}
2680
addBasicAuth(httpReq)
27-
resp, err := http.DefaultClient.Do(httpReq)
81+
resp, err := HTTPClient.Do(httpReq)
2882
if err != nil {
2983
return nil, err
3084
}
@@ -53,7 +107,7 @@ func getParseableSchema(stream string) (map[string]string, error) {
53107
return nil, err
54108
}
55109
addBasicAuth(httpReq)
56-
resp, err := http.DefaultClient.Do(httpReq)
110+
resp, err := HTTPClient.Do(httpReq)
57111
if err != nil {
58112
return nil, err
59113
}
@@ -85,46 +139,58 @@ func getParseableSchema(stream string) (map[string]string, error) {
85139

86140
func getParseableStats(streamName string) (map[string]interface{}, error) {
87141
url := ParseableBaseURL + "/api/v1/logstream/" + streamName + "/stats"
88-
httpReq, err := http.NewRequest("GET", url, nil)
142+
stats, m, err := doSimpleGet(url)
89143
if err != nil {
90-
return nil, err
91-
}
92-
addBasicAuth(httpReq)
93-
resp, err := http.DefaultClient.Do(httpReq)
94-
if err != nil {
95-
return nil, err
96-
}
97-
defer func() {
98-
if err := resp.Body.Close(); err != nil {
99-
log.Printf("failed to close response body: %v", err)
100-
}
101-
}()
102-
var stats map[string]interface{}
103-
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
104-
return nil, err
144+
return m, err
105145
}
106146
return stats, nil
107147
}
108148

109149
func getParseableInfo(streamName string) (map[string]interface{}, error) {
110150
url := ParseableBaseURL + "/api/v1/logstream/" + streamName + "/info"
151+
info, m, err := doSimpleGet(url)
152+
if err != nil {
153+
return m, err
154+
}
155+
return info, nil
156+
}
157+
158+
func getParseableAbout() (map[string]interface{}, error) {
159+
url := ParseableBaseURL + "/api/v1/about"
160+
about, m, err := doSimpleGet(url)
161+
if err != nil {
162+
return m, err
163+
}
164+
return about, nil
165+
}
166+
167+
func getParseableRoles() (map[string]interface{}, error) {
168+
url := ParseableBaseURL + "/api/v1/roles"
169+
roles, m, err := doSimpleGet(url)
170+
if err != nil {
171+
return m, err
172+
}
173+
return roles, nil
174+
}
175+
176+
func doSimpleGet(url string) (map[string]interface{}, map[string]interface{}, error) {
111177
httpReq, err := http.NewRequest("GET", url, nil)
112178
if err != nil {
113-
return nil, err
179+
return nil, nil, err
114180
}
115181
addBasicAuth(httpReq)
116-
resp, err := http.DefaultClient.Do(httpReq)
182+
resp, err := HTTPClient.Do(httpReq)
117183
if err != nil {
118-
return nil, err
184+
return nil, nil, err
119185
}
120186
defer func() {
121187
if err := resp.Body.Close(); err != nil {
122188
log.Printf("failed to close response body: %v", err)
123189
}
124190
}()
125-
var info map[string]interface{}
126-
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
127-
return nil, err
191+
var stats map[string]interface{}
192+
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
193+
return nil, nil, err
128194
}
129-
return info, nil
195+
return stats, nil, nil
130196
}

tools/parseable_about.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/mark3labs/mcp-go/mcp"
9+
"github.com/mark3labs/mcp-go/server"
10+
)
11+
12+
func RegisterGetAboutTool(mcpServer *server.MCPServer) {
13+
mcpServer.AddTool(mcp.NewTool(
14+
"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
34+
`),
35+
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
36+
about, err := getParseableAbout()
37+
if err != nil {
38+
return mcp.NewToolResultError(err.Error()), nil
39+
}
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
47+
})
48+
}

tools/parseable_query.go

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
package tools
22

33
import (
4-
"bytes"
54
"context"
65
"encoding/json"
76
"fmt"
8-
"io"
9-
"net/http"
107

118
"github.com/mark3labs/mcp-go/mcp"
129
"github.com/mark3labs/mcp-go/server"
@@ -16,7 +13,7 @@ func RegisterQueryDataStreamTool(mcpServer *server.MCPServer) {
1613
mcpServer.AddTool(mcp.NewTool(
1714
"query_data_stream",
1815
mcp.WithDescription("Execute a SQL query against a data stream in Parseable. All fields are required. All times must be in ISO 8601 format."),
19-
mcp.WithString("query", mcp.Required(), mcp.Description("SQL query to run")),
16+
mcp.WithString("query", mcp.Required(), mcp.Description("SQL query to run, but the FROM must always be set to streamName")),
2017
mcp.WithString("streamName", mcp.Required(), mcp.Description("Name of the data stream (table)")),
2118
mcp.WithString("startTime", mcp.Required(), mcp.Description("Query start time in ISO 8601 (format: yyyy-MM-ddTHH:mm:ss+hh:mm)")),
2219
mcp.WithString("endTime", mcp.Required(), mcp.Description("Query end time in ISO 8601 (format: yyyy-MM-ddTHH:mm:ss+hh:mm)")),
@@ -28,32 +25,24 @@ func RegisterQueryDataStreamTool(mcpServer *server.MCPServer) {
2825
if query == "" || streamName == "" || startTime == "" || endTime == "" {
2926
return mcp.NewToolResultError("missing required fields: query, streamName, startTime, and endTime are required"), nil
3027
}
31-
payload := map[string]string{
32-
"query": query,
33-
"streamName": streamName,
34-
"startTime": startTime,
35-
"endTime": endTime,
36-
}
37-
jsonPayload, _ := json.Marshal(payload)
38-
url := ParseableBaseURL + parseableSQLPath
39-
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload))
40-
if err != nil {
41-
return mcp.NewToolResultError("failed to create request"), nil
42-
}
43-
httpReq.Header.Set("Content-Type", "application/json")
44-
addBasicAuth(httpReq)
45-
resp, err := http.DefaultClient.Do(httpReq)
28+
queryResult, err := doParseableQuery(query, streamName, startTime, endTime)
4629
if err != nil {
4730
return mcp.NewToolResultError("query failed: " + err.Error()), nil
4831
}
49-
defer resp.Body.Close()
50-
var result interface{}
51-
body, _ := io.ReadAll(resp.Body)
52-
if err := json.Unmarshal(body, &result); err != nil {
53-
return mcp.NewToolResultError("failed to parse parseable response"), nil
32+
33+
b, err := json.MarshalIndent(queryResult, "", " ")
34+
if err != nil {
35+
return nil, err
5436
}
55-
// Default: return as text
56-
return mcp.NewToolResultText(fmt.Sprintf("%v", result)), nil
37+
38+
// Optional: a one-liner summary that sets expectations.
39+
text := fmt.Sprintf(
40+
"Returned %d rows as JSON (array of objects). Use keys exactly as shown.\n```json\n%s\n```",
41+
len(queryResult),
42+
string(b),
43+
)
44+
45+
return mcp.NewToolResultText(text), nil
5746
// Optionally, for structured output:
5847
// return mcp.NewToolResultStructured(map[string]interface{}{"result": result}, "Query successful"), nil
5948
})

tools/parseable_roles.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/mark3labs/mcp-go/mcp"
9+
"github.com/mark3labs/mcp-go/server"
10+
)
11+
12+
func RegisterGetRolesTool(mcpServer *server.MCPServer) {
13+
mcpServer.AddTool(mcp.NewTool(
14+
"get_roles",
15+
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.
16+
17+
Data is returned as a dictionary with the role name and a list of privilege. The privilege can be of the following:
18+
- admin - have all privileges
19+
- editor - have limited privileges like cluster features
20+
- reader - allow read from datasets
21+
- writer - allow read and write from datasets
22+
- ingestor - allow write from datasets
23+
24+
For reader, writer and ingestor role there is always at least one resource connected to the role. This resources is typical a dataset.
25+
For full description of roles and RBAC use https://www.parseable.com/docs/user-guide/rbac
26+
`),
27+
), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
28+
about, err := getParseableRoles()
29+
if err != nil {
30+
return mcp.NewToolResultError(err.Error()), nil
31+
}
32+
var lines []string
33+
for k, v := range about {
34+
lines = append(lines, k+": "+fmt.Sprintf("%v", v))
35+
}
36+
return mcp.NewToolResultText(strings.Join(lines, "\n")), nil
37+
// Optionally, for structured output:
38+
// return mcp.NewToolResultStructured(map[string]interface{}{"info": info}, "Info returned"), nil
39+
})
40+
}

tools/register.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ func RegisterParseableTools(mcpServer *server.MCPServer) {
88
RegisterGetDataStreamSchemaTool(mcpServer)
99
RegisterGetDataStreamStatsTool(mcpServer)
1010
RegisterGetDataStreamInfoTool(mcpServer)
11+
RegisterGetAboutTool(mcpServer)
12+
RegisterGetRolesTool(mcpServer)
1113
}

0 commit comments

Comments
 (0)