Skip to content

Commit 73d0b18

Browse files
authored
Merge pull request #7 from axiomhq/add-tools
Add User-Agent header for MCP tracking and getQueryHistory tool
2 parents 13495ff + 65d7362 commit 73d0b18

File tree

3 files changed

+198
-25
lines changed

3 files changed

+198
-25
lines changed

README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ A [Model Context Protocol](https://modelcontextprotocol.io/) server implementati
44

55
## Status
66

7-
Works with Claude desktop app. Implements two MCP [tools](https://modelcontextprotocol.io/docs/concepts/tools):
7+
Works with Claude desktop app. Implements seven MCP [tools](https://modelcontextprotocol.io/docs/concepts/tools):
88

99
- queryApl: Execute APL queries against Axiom datasets
1010
- listDatasets: List available Axiom datasets
11+
- getDatasetSchema: Get dataset schema
12+
- getSavedQueries: Retrieve saved/starred APL queries
13+
- getMonitors: List monitoring configurations
14+
- getMonitorsHistory: Get monitor execution history
15+
- getQueryHistory: Get your recent APL query execution history (shows your queries by default)
16+
17+
**Note:** All tools require a Personal Access Token (PAT) for authentication. Use your PAT as the `token` parameter.
1118

1219
No support for MCP [resources](https://modelcontextprotocol.io/docs/concepts/resources) or [prompts](https://modelcontextprotocol.io/docs/concepts/prompts) yet.
1320

@@ -29,7 +36,7 @@ Configure using one of these methods:
2936

3037
### Config File Example (config.txt):
3138
```txt
32-
token xaat-your-token
39+
token xapt-your-personal-access-token
3340
url https://api.axiom.co
3441
org-id your-org-id
3542
query-rate 1
@@ -41,7 +48,7 @@ datasets-burst 1
4148
### Command Line Flags:
4249
```bash
4350
axiom-mcp \
44-
-token xaat-your-token \
51+
-token xapt-your-personal-access-token \
4552
-url https://api.axiom.co \
4653
-org-id your-org-id \
4754
-query-rate 1 \
@@ -52,7 +59,7 @@ axiom-mcp \
5259

5360
### Environment Variables:
5461
```bash
55-
export AXIOM_TOKEN=xaat-your-token
62+
export AXIOM_TOKEN=xapt-your-personal-access-token
5663
export AXIOM_URL=https://api.axiom.co
5764
export AXIOM_ORG_ID=your-org-id
5865
export AXIOM_QUERY_RATE=1
@@ -65,7 +72,7 @@ export AXIOM_DATASETS_BURST=1
6572

6673
1. Create a config file:
6774
```bash
68-
echo "token xaat-your-token" > config.txt
75+
echo "token xapt-your-personal-access-token" > config.txt
6976
```
7077

7178
2. Configure the Claude app to use the MCP server:
@@ -81,7 +88,7 @@ code ~/Library/Application\ Support/Claude/claude_desktop_config.json
8188
"command": "/path/to/your/axiom-mcp-binary",
8289
"args" : ["--config", "/path/to/your/config.txt"],
8390
"env": { // Alternatively, you can set the environment variables here
84-
"AXIOM_TOKEN": "xaat-your-token",
91+
"AXIOM_TOKEN": "xapt-your-personal-access-token",
8592
"AXIOM_URL": "https://api.axiom.co",
8693
"AXIOM_ORG_ID": "your-org-id"
8794
}

main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ var (
4343
// config holds the server configuration parameters
4444
type config struct {
4545
// Axiom connection settings
46-
token string // API token for authentication
46+
token string // Personal Access Token (PAT)
4747
url string // Optional custom API URL
4848
orgID string // Organization ID
4949

@@ -61,7 +61,7 @@ func setupConfig() (config, error) {
6161
fs := flag.NewFlagSet("axiom-mcp", flag.ExitOnError)
6262

6363
var cfg config
64-
fs.StringVar(&cfg.token, "token", "", "Axiom API token")
64+
fs.StringVar(&cfg.token, "token", "", "Axiom Personal Access Token (PAT)")
6565
fs.StringVar(&cfg.url, "url", "", "Axiom API URL (optional)")
6666
fs.StringVar(&cfg.orgID, "org-id", "", "Axiom organization ID")
6767
fs.Float64Var(&cfg.queryRateLimit, "query-rate", 1, "Queries per second limit")
@@ -84,7 +84,7 @@ func setupConfig() (config, error) {
8484
}
8585

8686
if cfg.token == "" {
87-
return cfg, errors.New("Axiom token must be provided via -token flag, AXIOM_TOKEN env var, or config file")
87+
return cfg, errors.New("Axiom Personal Access Token (PAT) must be provided via -token flag, AXIOM_TOKEN env var, or config file")
8888
}
8989

9090
return cfg, nil

tools.go

Lines changed: 182 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,35 @@ import (
1414
"golang.org/x/time/rate"
1515
)
1616

17-
// createTools creates the MCP tool definitions with appropriate rate limits
17+
var MCP_USER_AGENT = fmt.Sprintf("mcp-server-axiom/%s", Version)
18+
19+
func createAxiomHTTPClient() *http.Client {
20+
return &http.Client{
21+
Transport: &mcpTransport{
22+
base: http.DefaultTransport,
23+
},
24+
}
25+
}
26+
27+
type mcpTransport struct {
28+
base http.RoundTripper
29+
}
30+
31+
func (t *mcpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
32+
// Clone the request to avoid modifying the original
33+
reqCopy := req.Clone(req.Context())
34+
reqCopy.Header.Set("User-Agent", MCP_USER_AGENT)
35+
return t.base.RoundTrip(reqCopy)
36+
}
37+
1838
func createTools(cfg config) ([]mcp.ToolDefinition, error) {
39+
httpClient := createAxiomHTTPClient()
40+
1941
client, err := axiom.NewClient(
2042
axiom.SetToken(cfg.token),
2143
axiom.SetURL(cfg.url),
2244
axiom.SetOrganizationID(cfg.orgID),
45+
axiom.SetClient(httpClient),
2346
)
2447
if err != nil {
2548
return nil, fmt.Errorf("failed to create Axiom client: %w", err)
@@ -81,7 +104,7 @@ func createTools(cfg config) ([]mcp.ToolDefinition, error) {
81104
Properties: mcp.ToolInputSchemaProperties{},
82105
},
83106
},
84-
Execute: newGetSavedQueriesHandler(client, cfg),
107+
Execute: newGetSavedQueriesHandler(cfg, httpClient),
85108
RateLimit: rate.NewLimiter(rate.Limit(1), 1),
86109
},
87110
{
@@ -93,7 +116,7 @@ func createTools(cfg config) ([]mcp.ToolDefinition, error) {
93116
Properties: mcp.ToolInputSchemaProperties{},
94117
},
95118
},
96-
Execute: newGetMonitorsHandler(client, cfg),
119+
Execute: newGetMonitorsHandler(cfg, httpClient),
97120
RateLimit: rate.NewLimiter(rate.Limit(cfg.monitorsRateLimit), cfg.monitorsRateBurst),
98121
},
99122
{
@@ -111,9 +134,35 @@ func createTools(cfg config) ([]mcp.ToolDefinition, error) {
111134
},
112135
},
113136
},
114-
Execute: newGetMonitorsHistoryHandler(client, cfg),
137+
Execute: newGetMonitorsHistoryHandler(cfg, httpClient),
115138
RateLimit: rate.NewLimiter(rate.Limit(cfg.monitorsRateLimit), cfg.monitorsRateBurst),
116139
},
140+
{
141+
Metadata: mcp.Tool{
142+
Name: "getQueryHistory",
143+
Description: ptr("Get your recent APL query execution history"),
144+
InputSchema: mcp.ToolInputSchema{
145+
Type: "object",
146+
Properties: mcp.ToolInputSchemaProperties{
147+
"limit": map[string]any{
148+
"type": "number",
149+
"description": "Maximum number of query history entries to return (default: 50, max: 500)",
150+
"default": 50,
151+
},
152+
"user": map[string]any{
153+
"type": "string",
154+
"description": "Filter by specific user ID (optional - defaults to current user)",
155+
},
156+
"dataset": map[string]any{
157+
"type": "string",
158+
"description": "Filter by dataset name (optional)",
159+
},
160+
},
161+
},
162+
},
163+
Execute: newGetQueryHistoryHandler(client, cfg, httpClient),
164+
RateLimit: rate.NewLimiter(rate.Limit(cfg.queryRateLimit), cfg.queryRateBurst),
165+
},
117166
}, nil
118167
}
119168

@@ -290,8 +339,17 @@ type SavedQuery struct {
290339
ID string `json:"id"`
291340
}
292341

342+
// QueryHistoryEntry represents a query execution record from the axiom-history dataset
343+
type QueryHistoryEntry struct {
344+
Timestamp string `json:"timestamp"`
345+
Dataset string `json:"dataset"`
346+
Query string `json:"query"`
347+
UserID string `json:"userId"`
348+
Created string `json:"created"`
349+
}
350+
293351
// newGetSavedQueriesHandler creates a handler for retrieving saved queries
294-
func newGetSavedQueriesHandler(client *axiom.Client, cfg config) func(mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
352+
func newGetSavedQueriesHandler(cfg config, httpClient *http.Client) func(mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
295353
return func(params mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
296354
ctx := context.Background()
297355

@@ -305,9 +363,9 @@ func newGetSavedQueriesHandler(client *axiom.Client, cfg config) func(mcp.CallTo
305363

306364
req.Header.Set("Authorization", "Bearer "+cfg.token)
307365
req.Header.Set("Accept", "application/json")
366+
req.Header.Set("X-AXIOM-ORG-ID", cfg.orgID)
308367

309-
client := &http.Client{}
310-
resp, err := client.Do(req)
368+
resp, err := httpClient.Do(req)
311369
if err != nil {
312370
return mcp.CallToolResult{}, fmt.Errorf("failed to execute request: %w", err)
313371
}
@@ -354,7 +412,7 @@ func newGetSavedQueriesHandler(client *axiom.Client, cfg config) func(mcp.CallTo
354412
}
355413

356414
// newGetMonitorsHandler creates a handler for retrieving monitors
357-
func newGetMonitorsHandler(client *axiom.Client, cfg config) func(mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
415+
func newGetMonitorsHandler(cfg config, httpClient *http.Client) func(mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
358416
return func(params mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
359417
ctx := context.Background()
360418

@@ -368,9 +426,9 @@ func newGetMonitorsHandler(client *axiom.Client, cfg config) func(mcp.CallToolRe
368426

369427
req.Header.Set("Authorization", "Bearer "+cfg.token)
370428
req.Header.Set("Accept", "application/json")
429+
req.Header.Set("X-AXIOM-ORG-ID", cfg.orgID)
371430

372-
clientHTTP := &http.Client{}
373-
resp, err := clientHTTP.Do(req)
431+
resp, err := httpClient.Do(req)
374432
if err != nil {
375433
return mcp.CallToolResult{}, fmt.Errorf("failed to execute request: %w", err)
376434
}
@@ -405,7 +463,7 @@ func newGetMonitorsHandler(client *axiom.Client, cfg config) func(mcp.CallToolRe
405463
}
406464

407465
// newGetMonitorsHistoryHandler creates a handler for retrieving monitor history
408-
func newGetMonitorsHistoryHandler(client *axiom.Client, cfg config) func(mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
466+
func newGetMonitorsHistoryHandler(cfg config, httpClient *http.Client) func(mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
409467
return func(params mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
410468
ctx := context.Background()
411469

@@ -435,9 +493,9 @@ func newGetMonitorsHistoryHandler(client *axiom.Client, cfg config) func(mcp.Cal
435493
return mcp.CallToolResult{}, fmt.Errorf("at least one valid monitor ID is required")
436494
}
437495

438-
baseURL := cfg.url
439-
fullURL := fmt.Sprintf("%s/api/internal/monitors/history?monitorIds=%s",
440-
baseURL,
496+
// Convert API URL to App URL for internal endpoint
497+
baseURL := strings.Replace(cfg.url, "://api.", "://app.", 1)
498+
fullURL := fmt.Sprintf("%s/api/internal/monitors/history?monitorIds=%s", baseURL,
441499
strings.Join(monitorIds, ","))
442500

443501
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
@@ -447,9 +505,9 @@ func newGetMonitorsHistoryHandler(client *axiom.Client, cfg config) func(mcp.Cal
447505

448506
req.Header.Set("Authorization", "Bearer "+cfg.token)
449507
req.Header.Set("Accept", "application/json")
508+
req.Header.Set("X-AXIOM-ORG-ID", cfg.orgID)
450509

451-
clientHTTP := &http.Client{}
452-
resp, err := clientHTTP.Do(req)
510+
resp, err := httpClient.Do(req)
453511
if err != nil {
454512
return mcp.CallToolResult{}, fmt.Errorf("failed to execute request: %w", err)
455513
}
@@ -495,3 +553,111 @@ func newGetMonitorsHistoryHandler(client *axiom.Client, cfg config) func(mcp.Cal
495553
}, nil
496554
}
497555
}
556+
557+
// getCurrentUserId gets the current user ID from /v2/user endpoint using PAT
558+
func getCurrentUserId(cfg config, httpClient *http.Client) (string, error) {
559+
ctx := context.Background()
560+
561+
if cfg.token == "" {
562+
return "", fmt.Errorf("personal Access Token (PAT) is required")
563+
}
564+
565+
baseURL := cfg.url
566+
fullURL := baseURL + "/v2/user"
567+
568+
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
569+
if err != nil {
570+
return "", fmt.Errorf("failed to create request: %w", err)
571+
}
572+
573+
req.Header.Set("Authorization", "Bearer "+cfg.token)
574+
req.Header.Set("Accept", "application/json")
575+
576+
resp, err := httpClient.Do(req)
577+
if err != nil {
578+
return "", fmt.Errorf("failed to execute request: %w", err)
579+
}
580+
defer resp.Body.Close()
581+
582+
if resp.StatusCode != http.StatusOK {
583+
body, _ := io.ReadAll(resp.Body)
584+
return "", fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body))
585+
}
586+
587+
body, err := io.ReadAll(resp.Body)
588+
if err != nil {
589+
return "", fmt.Errorf("failed to read response body: %w", err)
590+
}
591+
var userResponse struct {
592+
ID string `json:"id"`
593+
}
594+
if err := json.Unmarshal(body, &userResponse); err != nil {
595+
return "", fmt.Errorf("failed to parse user response: %w", err)
596+
}
597+
598+
if userResponse.ID == "" {
599+
return "", fmt.Errorf("user ID not found in response")
600+
}
601+
602+
return userResponse.ID, nil
603+
}
604+
605+
// newGetQueryHistoryHandler creates a handler for retrieving query execution history from axiom-history dataset
606+
func newGetQueryHistoryHandler(client *axiom.Client, cfg config, httpClient *http.Client) func(mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
607+
return func(params mcp.CallToolRequestParams) (mcp.CallToolResult, error) {
608+
ctx := context.Background()
609+
610+
limit := 50
611+
if limitParam, ok := params.Arguments["limit"].(float64); ok && limitParam > 0 {
612+
limit = int(limitParam)
613+
if limit > 500 {
614+
limit = 500
615+
}
616+
}
617+
618+
currentUserId, err := getCurrentUserId(cfg, httpClient)
619+
if err != nil {
620+
return mcp.CallToolResult{}, fmt.Errorf("failed to get current user: %w", err)
621+
}
622+
623+
var whereFilters []string
624+
whereFilters = append(whereFilters, "kind == \"apl\"")
625+
626+
// Use current user by default, but allow override - if the user provides another ID
627+
userToFilter := currentUserId
628+
if userParam, ok := params.Arguments["user"].(string); ok && userParam != "" {
629+
userToFilter = userParam
630+
}
631+
whereFilters = append(whereFilters, fmt.Sprintf("who == \"%s\"", userToFilter))
632+
633+
// Optional dataset filter
634+
if datasetParam, ok := params.Arguments["dataset"].(string); ok && datasetParam != "" {
635+
whereFilters = append(whereFilters, fmt.Sprintf("dataset == \"%s\"", datasetParam))
636+
}
637+
638+
aplQuery := fmt.Sprintf(
639+
"[\"axiom-history\"] | where %s | sort by _time desc | take %d | project _time, dataset, [\"query.apl\"], who, created",
640+
strings.Join(whereFilters, " and "),
641+
limit,
642+
)
643+
644+
result, err := client.Query(ctx, aplQuery)
645+
if err != nil {
646+
return mcp.CallToolResult{}, fmt.Errorf("failed to execute query history query: %w", err)
647+
}
648+
649+
jsonData, err := json.MarshalIndent(result, "", " ")
650+
if err != nil {
651+
return mcp.CallToolResult{}, fmt.Errorf("failed to marshal query history response: %w", err)
652+
}
653+
654+
return mcp.CallToolResult{
655+
Content: []any{
656+
mcp.TextContent{
657+
Text: string(jsonData),
658+
Type: "text",
659+
},
660+
},
661+
}, nil
662+
}
663+
}

0 commit comments

Comments
 (0)