-
Notifications
You must be signed in to change notification settings - Fork 42
First Asserts tool #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
xujiaxj
wants to merge
11
commits into
main
Choose a base branch
from
jiaxu/draft/asserts-tools
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+201
−2
Open
First Asserts tool #105
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
73dda54
First Asserts tool
xujiaxj 2418340
Try to fix the test
xujiaxj 771591e
Only cloud test Asserts
xujiaxj 6b177bc
Only unit test
xujiaxj 4126c01
Fix assertion list
xujiaxj 7a63d6b
fix unit test
xujiaxj 7b23410
Resolve merge conflicts in main.go: combine enabledTools handling wit…
xujiaxj 1062420
revert back to use RFC3339 time as input
xujiaxj 7faa70c
Resolve merge conflicts in main.go: include both asserts and sift tools
xujiaxj 7274e52
Address review comments
xujiaxj 3d555ba
Fix lint errors
xujiaxj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package tools | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
mcpgrafana "github.com/grafana/mcp-grafana" | ||
"github.com/mark3labs/mcp-go/server" | ||
) | ||
|
||
func newAssertsClient(ctx context.Context) (*Client, error) { | ||
grafanaURL, apiKey := mcpgrafana.GrafanaURLFromContext(ctx), mcpgrafana.GrafanaAPIKeyFromContext(ctx) | ||
url := fmt.Sprintf("%s/api/plugins/grafana-asserts-app/resources/asserts/api-server", strings.TrimRight(grafanaURL, "/")) | ||
|
||
client := &http.Client{ | ||
Transport: &authRoundTripper{ | ||
apiKey: apiKey, | ||
underlying: http.DefaultTransport, | ||
}, | ||
} | ||
|
||
return &Client{ | ||
httpClient: client, | ||
baseURL: url, | ||
}, nil | ||
} | ||
|
||
type GetAssertionsParams struct { | ||
StartRFC3339 string `json:"startRfc3339" jsonschema:"required,description=The start time in RFC3339 format"` | ||
EndRFC3339 string `json:"endRfc3339" jsonschema:"required,description=The end time in RFC3339 format"` | ||
EntityType string `json:"entityType" jsonschema:"description=The type of the entity to list (e.g. Service\\, Node\\, Pod\\, etc.)"` | ||
EntityName string `json:"entityName" jsonschema:"description=The name of the entity to list"` | ||
Env string `json:"env" jsonschema:"description=The env of the entity to list"` | ||
Site string `json:"site" jsonschema:"description=The site of the entity to list"` | ||
Namespace string `json:"namespace" jsonschema:"description=The namespace of the entity to list"` | ||
} | ||
|
||
type scope struct { | ||
Env string `json:"env"` | ||
Site string `json:"site"` | ||
Namespace string `json:"namespace"` | ||
} | ||
|
||
type entity struct { | ||
Name string `json:"name"` | ||
Type string `json:"type"` | ||
Scope scope `json:"scope"` | ||
} | ||
|
||
type requestBody struct { | ||
StartTime int64 `json:"startTime"` | ||
EndTime int64 `json:"endTime"` | ||
EntityKeys []entity `json:"entityKeys"` | ||
SuggestionSrcEntities []entity `json:"suggestionSrcEntities"` | ||
AlertCategories []string `json:"alertCategories"` | ||
} | ||
|
||
func (c *Client) fetchAssertsData(ctx context.Context, urlPath string, method string, reqBody any) (string, error) { | ||
jsonData, err := json.Marshal(reqBody) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to marshal request body: %w", err) | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+urlPath, bytes.NewBuffer(jsonData)) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to create request: %w", err) | ||
} | ||
req.Header.Set("Content-Type", "application/json") | ||
|
||
resp, err := c.httpClient.Do(req) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to execute request: %w", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
body, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to read response body: %w", err) | ||
} | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return "", fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body)) | ||
} | ||
|
||
return string(body), nil | ||
} | ||
|
||
func getAssertions(ctx context.Context, args GetAssertionsParams) (string, error) { | ||
client, err := newAssertsClient(ctx) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to create Asserts client: %w", err) | ||
} | ||
|
||
startTime, err := time.Parse(time.RFC3339, args.StartRFC3339) | ||
if err != nil { | ||
return "", fmt.Errorf("parsing start time: %w", err) | ||
} | ||
|
||
endTime, err := time.Parse(time.RFC3339, args.EndRFC3339) | ||
if err != nil { | ||
return "", fmt.Errorf("parsing end time: %w", err) | ||
} | ||
|
||
// Create request body | ||
reqBody := requestBody{ | ||
StartTime: startTime.UnixMilli(), | ||
EndTime: endTime.UnixMilli(), | ||
EntityKeys: []entity{ | ||
{ | ||
Name: args.EntityName, | ||
Type: args.EntityType, | ||
Scope: scope{ | ||
Env: args.Env, | ||
Site: args.Site, | ||
Namespace: args.Namespace, | ||
}, | ||
}, | ||
}, | ||
SuggestionSrcEntities: []entity{}, | ||
AlertCategories: []string{"saturation", "amend", "anomaly", "failure", "error"}, | ||
} | ||
|
||
data, err := client.fetchAssertsData(ctx, "/v1/assertions/llm-summary", "POST", reqBody) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to fetch data: %w", err) | ||
} | ||
|
||
return data, nil | ||
} | ||
|
||
var GetAssertions = mcpgrafana.MustTool( | ||
"get_assertions", | ||
"Get assertion summary for a given entity with its type, name, env, site, namespace, and a time range", | ||
getAssertions, | ||
) | ||
|
||
func AddAssertsTools(mcp *server.MCPServer) { | ||
GetAssertions.Register(mcp) | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
//go:build unit | ||
// +build unit | ||
|
||
package tools | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
mcpgrafana "github.com/grafana/mcp-grafana" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func setupMockAssertsServer(handler http.HandlerFunc) (*httptest.Server, context.Context) { | ||
server := httptest.NewServer(handler) | ||
ctx := context.Background() | ||
ctx = mcpgrafana.WithGrafanaURL(ctx, server.URL) | ||
ctx = mcpgrafana.WithGrafanaAPIKey(ctx, "test-api-key") | ||
return server, ctx | ||
} | ||
|
||
func TestAssertTools(t *testing.T) { | ||
t.Run("get assertions", func(t *testing.T) { | ||
server, ctx := setupMockAssertsServer(func(w http.ResponseWriter, r *http.Request) { | ||
require.Equal(t, "/api/plugins/grafana-asserts-app/resources/asserts/api-server/v1/assertions/llm-summary", r.URL.Path) | ||
require.Equal(t, "Bearer test-api-key", r.Header.Get("Authorization")) | ||
|
||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(http.StatusOK) | ||
_, err := w.Write([]byte(`{"summary": "test summary"}`)) | ||
require.NoError(t, err) | ||
}) | ||
defer server.Close() | ||
|
||
result, err := getAssertions(ctx, GetAssertionsParams{ | ||
StartRFC3339: "2025-04-23T10:00:00Z", | ||
EndRFC3339: "2025-04-23T16:00:00Z", | ||
EntityType: "Service", | ||
EntityName: "mongodb", | ||
Env: "asserts-demo", | ||
Site: "app", | ||
Namespace: "robot-shop", | ||
}) | ||
require.NoError(t, err) | ||
assert.NotNil(t, result) | ||
assert.Equal(t, `{"summary": "test summary"}`, result) | ||
}) | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you un-export all of the structs (other than
GetAssertionsParams
andGetAssertions
) so we keep thetools
package clean when imported? 🙏