Files
traces/integration/api/mcp_test.go
T
Joe ElliottandGitHub 863b79ba21 Tempo 3.0: Integration Tests Rewrite (#6088)
* First pass test harness

Signed-off-by: Joe Elliott <number101010@gmail.com>

* limits - passed

Signed-off-by: Joe Elliott <number101010@gmail.com>

* metrics generator - passed

Signed-off-by: Joe Elliott <number101010@gmail.com>

* harness cleanup/fix target info test

Signed-off-by: Joe Elliott <number101010@gmail.com>

* deployments - passed

Signed-off-by: Joe Elliott <number101010@gmail.com>

* receivers - passed

Signed-off-by: Joe Elliott <number101010@gmail.com>

* harness cleanup

Signed-off-by: Joe Elliott <number101010@gmail.com>

* storage - passed

Signed-off-by: Joe Elliott <number101010@gmail.com>

* api - passing

Signed-off-by: Joe Elliott <number101010@gmail.com>

* cleanup and CI

Signed-off-by: Joe Elliott <number101010@gmail.com>

* Additional tests

Signed-off-by: Joe Elliott <number101010@gmail.com>

* it's me and you linter. how many times are you going to do this to me?

Signed-off-by: Joe Elliott <number101010@gmail.com>

* lint and tests

Signed-off-by: Joe Elliott <number101010@gmail.com>

* restored live store partition tests

Signed-off-by: Joe Elliott <number101010@gmail.com>

* massive lint

Signed-off-by: Joe Elliott <number101010@gmail.com>

* cleanup

Signed-off-by: Joe Elliott <number101010@gmail.com>

* lint >.<

Signed-off-by: Joe Elliott <number101010@gmail.com>

* wups

Signed-off-by: Joe Elliott <number101010@gmail.com>

* cleanup and stabilizing

Signed-off-by: Joe Elliott <number101010@gmail.com>

* cleanup

Signed-off-by: Joe Elliott <number101010@gmail.com>

* omg

Signed-off-by: Joe Elliott <number101010@gmail.com>

* added config file. removed empty file. deleted some debug code

Signed-off-by: Joe Elliott <number101010@gmail.com>

* even more fixes

Signed-off-by: Joe Elliott <number101010@gmail.com>

* maybe this works? maybe it was all local networking issues

Signed-off-by: Joe Elliott <number101010@gmail.com>

* review

Signed-off-by: Joe Elliott <number101010@gmail.com>

* backed off huge write timeout. added missing config file

Signed-off-by: Joe Elliott <number101010@gmail.com>

* restore disable parallelism setting. restore beefy write timeout

Signed-off-by: Joe Elliott <number101010@gmail.com>

* apparently another test relied on the http receiver

Signed-off-by: Joe Elliott <number101010@gmail.com>

* livestore avail zone!

Signed-off-by: Joe Elliott <number101010@gmail.com>

* add support for v1 traces and tags endpoints

Signed-off-by: Joe Elliott <number101010@gmail.com>

* review

Signed-off-by: Joe Elliott <number101010@gmail.com>

* changed the location of the changelog entry to avoid conflicts

Signed-off-by: Joe Elliott <number101010@gmail.com>

---------

Signed-off-by: Joe Elliott <number101010@gmail.com>
2026-01-07 08:31:11 -05:00

149 lines
4.2 KiB
Go

package api
import (
"context"
"path"
"sort"
"strings"
"testing"
"time"
"github.com/grafana/e2e"
"github.com/grafana/tempo/integration/util"
"github.com/grafana/tempo/pkg/api"
tempoUtil "github.com/grafana/tempo/pkg/util"
"github.com/stretchr/testify/require"
mcpclient "github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)
// TestMCP is a smoke test that starts up a tempo instance, writes a trace and queries it back via MCP.
// It also verifies that all expected tools are available.
func TestMCP(t *testing.T) {
util.RunIntegrationTests(t, util.TestHarnessConfig{
ConfigOverlay: "config-mcp.yaml",
}, func(h *util.TempoHarness) {
h.WaitTracesWritable(t)
// Write a trace
info := tempoUtil.NewTraceInfo(time.Now(), "")
require.NoError(t, h.WriteTraceInfo(info, ""))
_, err := info.ConstructTraceFromEpoch()
require.NoError(t, err)
// Wait for the trace to be written to the WAL
h.WaitTracesQueryable(t, 1)
// now query it back with mcp
queryFrontend := h.Services[util.ServiceQueryFrontend]
mcpClient := createMCPClient(t, queryFrontend)
tools := listTools(t, mcpClient)
// confirm all tools are listed as read only and no open world
for _, tool := range tools {
require.NotNil(t, tool.Annotations.DestructiveHint, "tool %s doesn't specify destructive", tool.Name)
require.False(t, *tool.Annotations.DestructiveHint, "tool %s is marked destructive", tool.Name)
require.NotNil(t, tool.Annotations.OpenWorldHint, "tool %s doesn't specify open world", tool.Name)
require.False(t, *tool.Annotations.OpenWorldHint, "tool %s is marked open world", tool.Name)
require.NotNil(t, tool.Annotations.ReadOnlyHint, "tool %s doesn't specify read only", tool.Name)
require.True(t, *tool.Annotations.ReadOnlyHint, "tool %s is marked write", tool.Name)
}
// Verify all expected tools are available
expectedTools := []string{
"traceql-search",
"traceql-metrics-instant",
"traceql-metrics-range",
"get-trace",
"get-attribute-names",
"get-attribute-values",
"docs-traceql",
}
actualTools := make([]string, len(tools))
for i, tool := range tools {
actualTools[i] = tool.Name
}
// sort both lists
sort.Strings(actualTools)
sort.Strings(expectedTools)
require.Equal(t, expectedTools, actualTools)
assertTraceOverMCP(t, mcpClient, info.HexID())
})
}
func createMCPClient(t *testing.T, tempo *e2e.HTTPService) mcpclient.MCPClient {
mcpClient, err := mcpclient.NewStreamableHttpClient("http://" + path.Join(tempo.Endpoint(3200), api.PathMCP))
require.NoError(t, err)
// Initialize the connection with required parameters
initReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
Capabilities: mcp.ClientCapabilities{},
ClientInfo: mcp.Implementation{
Name: "tempo-e2e-test",
Version: "1.0.0",
},
},
}
_, err = mcpClient.Initialize(context.Background(), initReq)
if err != nil {
t.Fatalf("failed to initialize MCP client: %v", err)
}
return mcpClient
}
func listTools(t *testing.T, mcpClient mcpclient.MCPClient) []mcp.Tool {
toolsResponse, err := mcpClient.ListTools(context.Background(), mcp.ListToolsRequest{})
if err != nil {
t.Fatalf("failed to list tools: %v", err)
}
return toolsResponse.Tools
}
func assertTraceOverMCP(t *testing.T, mcpClient mcpclient.MCPClient, traceID string) {
resp, err := mcpClient.CallTool(context.Background(), mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: "get-trace",
Arguments: map[string]interface{}{"trace_id": traceID},
},
})
require.NoError(t, err)
str := toolResult(t, resp)
// the trace format is subject is to change so let's keep it simple.
if !strings.Contains(str, traceID) {
t.Fatalf("expected trace ID %s not found in response", traceID)
}
}
func toolResult(t *testing.T, resp *mcp.CallToolResult) string {
if resp.IsError {
t.Fatalf("tool call failed: %v", resp.Content)
}
var result strings.Builder
for _, content := range resp.Content {
// Handle different content types
switch c := content.(type) {
case mcp.TextContent:
result.WriteString(c.Text)
default:
t.Fatalf("unhandled content type: %T", c)
}
}
return result.String()
}