@@ -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+
1838func 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