Skip to content

Commit fc6e4bf

Browse files
committed
Add MCP method to list service logs for a cluster.
1 parent 7fece50 commit fc6e4bf

File tree

2 files changed

+77
-16
lines changed

2 files changed

+77
-16
lines changed

cmd/mcp/cmd.go

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,39 @@ import (
88
"github.com/google/jsonschema-go/jsonschema"
99
"github.com/modelcontextprotocol/go-sdk/mcp"
1010
"github.com/openshift/osdctl/cmd/cluster"
11+
"github.com/openshift/osdctl/cmd/servicelog"
1112
"github.com/spf13/cobra"
1213
)
1314

14-
type ClusterContextInput struct {
15+
// This is reusable Input type for commands that only need the cluster id and nothing else.
16+
type ClusterIdInput struct {
1517
ClusterId string `json:"cluster_id" jsonschema:"ID of the cluster to retrieve information for"`
1618
}
1719

18-
var ClusterContextInputSchema, _ = jsonschema.For[ClusterContextInput](&jsonschema.ForOptions{})
20+
var ClusterIdInputSchema, _ = jsonschema.For[ClusterIdInput](&jsonschema.ForOptions{})
1921

20-
type ClusterContextOutput struct {
22+
// This is just the most generic type of output. Used for e.g. the context command that already provides a JSON output
23+
// option, but the data types that make it up can't be converted to a JSONSCHEMA because Jira types are
24+
// self-referential.
25+
type MCPStringOutput struct {
2126
Context string `json:"context"`
2227
}
2328

24-
var ClusterContextOutputSchema, err = jsonschema.For[ClusterContextOutput](&jsonschema.ForOptions{})
29+
var MCPStringOutputSchema, _ = jsonschema.For[MCPStringOutput](&jsonschema.ForOptions{})
30+
31+
type MCPServiceLogInput struct {
32+
ClusterId string `json:"cluster_id" jsonschema:"ID of the cluster to retrieve information for"`
33+
Internal bool `json:"internal" jsonschema:"Include internal servicelogs"`
34+
All bool `json:"all" jsonschema:"List all servicelogs"`
35+
}
36+
37+
var MCPServiceLogInputSchema, _ = jsonschema.For[MCPServiceLogInput](&jsonschema.ForOptions{})
38+
39+
type MCPServiceLogOutput struct {
40+
ServiceLogs servicelog.LogEntryResponseView `json:"service_logs"`
41+
}
42+
43+
var MCPServiceLogOutputSchema, _ = jsonschema.For[MCPServiceLogOutput](&jsonschema.ForOptions{})
2544

2645
var MCPCmd = &cobra.Command{
2746
Use: "mcp",
@@ -46,10 +65,17 @@ func runMCP(cmd *cobra.Command, argv []string) error {
4665
mcp.AddTool(server, &mcp.Tool{
4766
Name: "context",
4867
Description: "Retrieve cluster context for a given cluster id",
49-
InputSchema: ClusterContextInputSchema,
50-
OutputSchema: ClusterContextOutputSchema,
68+
InputSchema: ClusterIdInputSchema,
69+
OutputSchema: MCPStringOutputSchema,
5170
Title: "cluster context",
5271
}, GenerateContext)
72+
mcp.AddTool(server, &mcp.Tool{
73+
Name: "service_logs",
74+
Description: "Retrieve cluster service logs for a given cluster id",
75+
InputSchema: MCPServiceLogInputSchema,
76+
OutputSchema: MCPServiceLogOutputSchema,
77+
Title: "cluster service logs",
78+
}, ListServiceLogs)
5379
if useHttp {
5480
// Create the streamable HTTP handler.
5581
handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server {
@@ -65,9 +91,30 @@ func runMCP(cmd *cobra.Command, argv []string) error {
6591
return nil
6692
}
6793

68-
func GenerateContext(ctx context.Context, req *mcp.CallToolRequest, input ClusterContextInput) (*mcp.CallToolResult, ClusterContextOutput, error) {
94+
func GenerateContext(ctx context.Context, req *mcp.CallToolRequest, input ClusterIdInput) (*mcp.CallToolResult, MCPStringOutput, error) {
6995
context, _ := cluster.GenerateContextData(input.ClusterId)
70-
return nil, ClusterContextOutput{
96+
return nil, MCPStringOutput{
7197
context,
7298
}, nil
7399
}
100+
101+
func ListServiceLogs(ctx context.Context, req *mcp.CallToolRequest, input MCPServiceLogInput) (*mcp.CallToolResult, MCPServiceLogOutput, error) {
102+
output := MCPServiceLogOutput{}
103+
serviceLogs, err := servicelog.FetchServiceLogs(input.ClusterId, input.All, input.Internal)
104+
if err != nil {
105+
return &mcp.CallToolResult{
106+
Meta: mcp.Meta{},
107+
Content: []mcp.Content{},
108+
StructuredContent: nil,
109+
IsError: true,
110+
}, output, err
111+
}
112+
view := servicelog.ConvertOCMSlToLogEntryView(serviceLogs)
113+
output.ServiceLogs = view
114+
return &mcp.CallToolResult{
115+
Meta: mcp.Meta{},
116+
Content: []mcp.Content{},
117+
StructuredContent: output,
118+
IsError: false,
119+
}, output, nil
120+
}

cmd/servicelog/list.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ func listServiceLogs(clusterID string, opts *listCmdOptions) error {
6363
}
6464

6565
func printServiceLogResponse(response *slv1.ClustersClusterLogsListResponse) error {
66+
view := ConvertOCMSlToLogEntryView(response)
67+
68+
viewBytes, err := json.Marshal(view)
69+
if err != nil {
70+
return fmt.Errorf("failed to marshal response for output: %w", err)
71+
}
72+
73+
return dump.Pretty(os.Stdout, viewBytes)
74+
}
75+
76+
func ConvertOCMSlToLogEntryView(response *slv1.ClustersClusterLogsListResponse) LogEntryResponseView {
6677
entryViews := logEntryToView(response.Items().Slice())
6778
slices.Reverse(entryViews)
6879
view := LogEntryResponseView{
@@ -72,13 +83,7 @@ func printServiceLogResponse(response *slv1.ClustersClusterLogsListResponse) err
7283
Size: response.Size(),
7384
Total: response.Total(),
7485
}
75-
76-
viewBytes, err := json.Marshal(view)
77-
if err != nil {
78-
return fmt.Errorf("failed to marshal response for output: %w", err)
79-
}
80-
81-
return dump.Pretty(os.Stdout, viewBytes)
86+
return view
8287
}
8388

8489
type LogEntryResponseView struct {
@@ -110,15 +115,24 @@ type LogEntryView struct {
110115
}
111116

112117
func logEntryToView(entries []*slv1.LogEntry) []*LogEntryView {
118+
// Forces an empty array to actual be [] when Marshalled and not null - this is a JSONSCHEMA error that is
119+
// configurable json v2: https://pkg.go.dev/encoding/json/v2#FormatNilSliceAsNull
120+
emptyDocReference := []string{}
113121
entryViews := make([]*LogEntryView, 0, len(entries))
114122
for _, entry := range entries {
123+
var docRef []string
124+
if len(entry.DocReferences()) > 0 {
125+
docRef = entry.DocReferences()
126+
} else {
127+
docRef = emptyDocReference
128+
}
115129
entryView := &LogEntryView{
116130
ClusterID: entry.ClusterID(),
117131
ClusterUUID: entry.ClusterUUID(),
118132
CreatedAt: entry.CreatedAt(),
119133
CreatedBy: entry.CreatedBy(),
120134
Description: entry.Description(),
121-
DocReferences: entry.DocReferences(),
135+
DocReferences: docRef,
122136
EventStreamID: entry.EventStreamID(),
123137
Href: entry.HREF(),
124138
ID: entry.ID(),

0 commit comments

Comments
 (0)