diff --git a/pkg/econet/econetmcp/server.go b/pkg/econet/econetmcp/server.go deleted file mode 100644 index 7edc706..0000000 --- a/pkg/econet/econetmcp/server.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Package econetmcp exposes an MCP server, mounted as an HTTP handler at -/api/mcp, whose tools wrap the EconetService gRPC API. -*/ -package econetmcp - -import ( - "context" - "net/http" - - "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts" - "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/rs/zerolog" - - "gitea.libretechconsulting.com/rmcguire/econet-exporter/pkg/config" - "gitea.libretechconsulting.com/rmcguire/econet-exporter/pkg/econet/econetgrpc" -) - -var EconetMCPImpl = &mcp.Implementation{ - Name: "Econet MCP Server", - Title: "Econet Exporter MCP", -} - -type EconetMCPServer struct { - ctx context.Context - cfg *config.ServiceConfig - log *zerolog.Logger - server *mcp.Server - econetGRPC *econetgrpc.EconetGRPCServer -} - -func NewEconetMCPServer(ctx context.Context, cfg *config.ServiceConfig, - grpc *econetgrpc.EconetGRPCServer, -) *EconetMCPServer { - return &EconetMCPServer{ - ctx: ctx, - cfg: cfg, - log: zerolog.Ctx(ctx), - econetGRPC: grpc, - server: mcp.NewServer(EconetMCPImpl, &mcp.ServerOptions{ - Instructions: "Use these tools to inspect Rheem EcoNet water heaters: " + - "current mode, setpoint, hot water availability, connectivity, and alerts.", - HasTools: true, - }), - } -} - -func (m *EconetMCPServer) GetHandlers() []opts.HTTPHandler { - // NOTE: Add other tools here - m.addListDevicesTool() - - handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { - return m.server - }, &mcp.StreamableHTTPOptions{}) - - m.log.Debug().Msg("Econet MCP tools ready") - - return []opts.HTTPHandler{ - { - Prefix: "/api/mcp", - Handler: handler, - }, - } -} diff --git a/pkg/econet/econetmcp/tool.go b/pkg/econet/econetmcp/tool.go deleted file mode 100644 index 355fb14..0000000 --- a/pkg/econet/econetmcp/tool.go +++ /dev/null @@ -1,76 +0,0 @@ -package econetmcp - -import ( - "context" - "fmt" - "strings" - - "github.com/modelcontextprotocol/go-sdk/mcp" - "k8s.io/utils/ptr" - - pb "gitea.libretechconsulting.com/rmcguire/econet-exporter/api/econet/v1alpha1" -) - -var ListDevicesTool = &mcp.Tool{ - Name: "list_water_heaters", - Title: "List EcoNet Water Heaters", - Description: "Lists Rheem EcoNet devices and their current state (mode, setpoint, " + - "hot water availability, connectivity). Optionally filter to one serial number.", - Annotations: &mcp.ToolAnnotations{ - ReadOnlyHint: true, - OpenWorldHint: ptr.To(true), - }, -} - -type ListDevicesParams struct { - SerialNumber string `json:"serial_number,omitempty" jsonschema:"Optional device serial number to return a single device"` -} - -func (m *EconetMCPServer) addListDevicesTool() { - mcp.AddTool(m.server, ListDevicesTool, m.listDevicesHandler) -} - -func (m *EconetMCPServer) listDevicesHandler(ctx context.Context, _ *mcp.CallToolRequest, args *ListDevicesParams) ( - *mcp.CallToolResult, any, error, -) { - m.log.Debug().Str("serial", args.SerialNumber).Msg("list water heaters tool called") - - devices, err := m.lookup(ctx, args.SerialNumber) - if err != nil { - return &mcp.CallToolResult{IsError: true, Meta: mcp.Meta{"error": err.Error()}}, nil, err - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{Text: summarizeDevices(devices)}}, - }, nil, nil -} - -func (m *EconetMCPServer) lookup(ctx context.Context, serial string) ([]*pb.Device, error) { - if serial != "" { - resp, err := m.econetGRPC.GetDevice(ctx, &pb.GetDeviceRequest{SerialNumber: serial}) - if err != nil { - return nil, err - } - return []*pb.Device{resp.GetDevice()}, nil - } - - resp, err := m.econetGRPC.ListDevices(ctx, &pb.ListDevicesRequest{}) - if err != nil { - return nil, err - } - return resp.GetDevices(), nil -} - -func summarizeDevices(devices []*pb.Device) string { - if len(devices) == 0 { - return "No EcoNet devices found." - } - var b strings.Builder - for _, d := range devices { - fmt.Fprintf(&b, "%s (%s, serial %s): mode=%s setpoint=%d°F running=%t connected=%t hotWater=%d%% alerts=%d\n", - d.GetFriendlyName(), d.GetGenericType(), d.GetSerialNumber(), - d.GetMode(), d.GetSetpoint(), d.GetRunning(), d.GetConnected(), - d.GetHotWaterAvailability(), d.GetAlertCount()) - } - return strings.TrimRight(b.String(), "\n") -} diff --git a/pkg/econet/econetmcp/tool_test.go b/pkg/econet/econetmcp/tool_test.go deleted file mode 100644 index f83605d..0000000 --- a/pkg/econet/econetmcp/tool_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package econetmcp - -import ( - "strings" - "testing" - - pb "gitea.libretechconsulting.com/rmcguire/econet-exporter/api/econet/v1alpha1" -) - -func TestSummarizeDevices(t *testing.T) { - tests := []struct { - name string - devices []*pb.Device - want string // substring that must appear - }{ - {"empty", nil, "No EcoNet devices found."}, - { - name: "single", - devices: []*pb.Device{{ - FriendlyName: "Garage", - GenericType: "heatpumpWaterHeater", - SerialNumber: "ABC123", - Mode: "heat-pump", - Setpoint: 125, - }}, - want: "Garage (heatpumpWaterHeater, serial ABC123): mode=heat-pump setpoint=125", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := summarizeDevices(tt.devices) - if !strings.Contains(got, tt.want) { - t.Errorf("summarizeDevices() = %q, want substring %q", got, tt.want) - } - }) - } -}