Skip to content
Merged
7 changes: 4 additions & 3 deletions cmd/jaeger/internal/extension/jaegermcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This approach prevents context-window exhaustion in LLMs and enables more effici

## Status

🚧 **Phase 1: Foundation (In Progress)** - Extension scaffold and lifecycle management
**Phase 1: Foundation (Complete)** - Extension scaffold, lifecycle management, and MCP SDK integration

Future phases will add:
- Phase 2: Basic MCP tools (search, span details, errors)
Expand Down Expand Up @@ -54,5 +54,6 @@ Phase 1 implements:
- ✅ Configuration validation
- ✅ Factory implementation
- ✅ Server lifecycle management
- ✅ Basic health endpoint
- 🚧 MCP SDK integration (coming in Phase 2)
- ✅ MCP SDK integration
- ✅ Streamable HTTP transport
- ✅ Basic health tool (placeholder for Phase 2)
5 changes: 4 additions & 1 deletion cmd/jaeger/internal/extension/jaegermcp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Config struct {
ServerName string `mapstructure:"server_name"`

// ServerVersion is the version of the MCP server.
ServerVersion string `mapstructure:"server_version"`
ServerVersion string `mapstructure:"server_version" valid:"required"`

// MaxSpanDetailsPerRequest limits the number of spans that can be fetched in a single request.
MaxSpanDetailsPerRequest int `mapstructure:"max_span_details_per_request" valid:"range(1|100)"`
Expand All @@ -29,6 +29,9 @@ type Config struct {

// Validate checks if the configuration is valid.
func (cfg *Config) Validate() error {
// if cfg.ServerVersion == "" {
// cfg.ServerVersion = version.Get().GitVersion
// }
_, err := govalidator.ValidateStruct(cfg)
return err
}
Expand Down
10 changes: 4 additions & 6 deletions cmd/jaeger/internal/extension/jaegermcp/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,9 @@ func TestValidate(t *testing.T) {
expectError bool
}{
{
name: "Empty config - valid",
config: &Config{},
expectError: false,
},
{
name: "Valid config",
name: "Valid config with ServerVersion",
config: &Config{
ServerVersion: "1.0.0",
MaxSpanDetailsPerRequest: 20,
MaxSearchResults: 100,
},
Expand All @@ -31,6 +27,7 @@ func TestValidate(t *testing.T) {
{
name: "Invalid MaxSpanDetailsPerRequest (too high)",
config: &Config{
ServerVersion: "1.0.0",
MaxSpanDetailsPerRequest: 101,
MaxSearchResults: 100,
},
Expand All @@ -39,6 +36,7 @@ func TestValidate(t *testing.T) {
{
name: "Invalid MaxSearchResults (too high)",
config: &Config{
ServerVersion: "1.0.0",
MaxSpanDetailsPerRequest: 20,
MaxSearchResults: 1001,
},
Expand Down
6 changes: 5 additions & 1 deletion cmd/jaeger/internal/extension/jaegermcp/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ func NewFactory() extension.Factory {

// createDefaultConfig creates the default configuration for the extension.
func createDefaultConfig() component.Config {
ver := version.Get().GitVersion
if ver == "" {
ver = "dev"
}
return &Config{
HTTP: confighttp.ServerConfig{
Endpoint: ports.PortToHostPort(ports.MCPHTTP),
},
ServerName: "jaeger",
ServerVersion: version.Get().GitVersion,
ServerVersion: ver,
MaxSpanDetailsPerRequest: 20,
MaxSearchResults: 100,
}
Expand Down
69 changes: 59 additions & 10 deletions cmd/jaeger/internal/extension/jaegermcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"time"

"github.com/modelcontextprotocol/go-sdk/mcp"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/extension"
"go.opentelemetry.io/collector/extension/extensioncapabilities"
Expand All @@ -30,6 +31,7 @@ type server struct {
telset component.TelemetrySettings
httpServer *http.Server
listener net.Listener
mcpServer *mcp.Server
}

// newServer creates a new MCP server instance.
Expand All @@ -47,28 +49,52 @@ func (*server) Dependencies() []component.ID {
}

// Start initializes and starts the MCP server.
func (s *server) Start(_ context.Context, host component.Host) error {
func (s *server) Start(ctx context.Context, host component.Host) error {
s.telset.Logger.Info("Starting Jaeger MCP server", zap.String("endpoint", s.config.HTTP.Endpoint))

// TODO Phase 2: Get QueryService from jaegerquery extension
// TODO Phase 2 (part 2): Get QueryService from jaegerquery extension
// This will require jaegerquery to expose QueryService through an Extension interface,
// similar to how jaegerstorage exposes storage factories.
// For now, we just verify that jaegerquery extension is available.
_ = host

// TODO: Initialize MCP server with Streamable HTTP transport
// This will be implemented in Phase 2 once we add the MCP SDK dependency

// For Phase 1, we just set up a basic HTTP server to validate the extension lifecycle
//nolint:noctx // Phase 1 temporary implementation, will be replaced with MCP SDK in Phase 2
listener, err := net.Listen("tcp", s.config.HTTP.Endpoint)
// Initialize MCP server with implementation details
impl := &mcp.Implementation{
Name: s.config.ServerName,
Version: s.config.ServerVersion,
}
// Pass empty ServerOptions to use default settings.
// Custom options (e.g., logging, handlers) can be added in Phase 2 if needed.
s.mcpServer = mcp.NewServer(impl, &mcp.ServerOptions{})

// Register a placeholder health tool for Phase 1 Part 2
// Actual MCP tools will be implemented in Phase 2
mcp.AddTool(s.mcpServer, &mcp.Tool{
Name: "health",
Description: "Check if the Jaeger MCP server is running",
}, s.healthTool)

// Set up TCP listener with context
lc := net.ListenConfig{}
listener, err := lc.Listen(ctx, "tcp", s.config.HTTP.Endpoint)
if err != nil {
return fmt.Errorf("failed to listen on %s: %w", s.config.HTTP.Endpoint, err)
}
s.listener = listener

// Create a basic HTTP server
// Create MCP streamable HTTP handler
mcpHandler := mcp.NewStreamableHTTPHandler(
func(_ *http.Request) *mcp.Server { return s.mcpServer },
&mcp.StreamableHTTPOptions{
JSONResponse: false, // Use SSE for streamed events
Stateless: false, // Session state management
SessionTimeout: 5 * time.Minute,
},
)

// Create HTTP server with MCP handler and health endpoint
mux := http.NewServeMux()
mux.Handle("/mcp", mcpHandler)
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("MCP server is running"))
Expand All @@ -86,7 +112,9 @@ func (s *server) Start(_ context.Context, host component.Host) error {
}
}()

s.telset.Logger.Info("Jaeger MCP server started successfully", zap.String("endpoint", s.config.HTTP.Endpoint))
s.telset.Logger.Info("Jaeger MCP server started successfully",
zap.String("endpoint", s.config.HTTP.Endpoint),
zap.String("mcp_endpoint", "http://"+s.config.HTTP.Endpoint+"/mcp"))
return nil
}

Expand All @@ -103,3 +131,24 @@ func (s *server) Shutdown(ctx context.Context) error {

return errors.Join(errs...)
}

// HealthToolOutput is the strongly-typed output for the health tool.
type HealthToolOutput struct {
Status string `json:"status" jsonschema:"Server status (ok/error)"`
Server string `json:"server" jsonschema:"Server name"`
Version string `json:"version" jsonschema:"Server version"`
}

// healthTool is a placeholder MCP tool that checks server health.
// Actual MCP tools for trace querying will be implemented in Phase 2.
func (s *server) healthTool(
_ context.Context,
_ *mcp.CallToolRequest,
_ struct{},
) (*mcp.CallToolResult, HealthToolOutput, error) {
return nil, HealthToolOutput{
Comment thread
yurishkuro marked this conversation as resolved.
Status: "ok",
Server: s.config.ServerName,
Version: s.config.ServerVersion,
}, nil
}
Loading
Loading