Skip to content

Commit ce74b4d

Browse files
committed
OCM-20282 | feat: Implement a MCP server for ROSA CLI
- Add MCP server with stdio and HTTP transport support - Implement hierarchical tool registry based on Cobra command structure - Add interactive chat interface (rosa mcp chat) Signed-off-by: Paul Czarkowski <[email protected]>
1 parent 19bb209 commit ce74b4d

File tree

228 files changed

+91904
-39
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

228 files changed

+91904
-39
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.idea/
22
.vscode/
3+
.history/
34
/docs
45
/rosa
56
/rosa-darwin-amd64

cmd/mcp/chat/chat_suite_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright (c) 2020 Red Hat, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package chat_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestChat(t *testing.T) {
27+
RegisterFailHandler(Fail)
28+
RunSpecs(t, "Chat Command Suite")
29+
}

cmd/mcp/chat/cmd.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
Copyright (c) 2020 Red Hat, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package chat
18+
19+
import (
20+
"bufio"
21+
"fmt"
22+
"os"
23+
"strings"
24+
25+
"github.com/spf13/cobra"
26+
27+
"github.com/openshift/rosa/pkg/arguments"
28+
"github.com/openshift/rosa/pkg/color"
29+
"github.com/openshift/rosa/pkg/mcp"
30+
"github.com/openshift/rosa/pkg/reporter"
31+
)
32+
33+
var args struct {
34+
apiKey string
35+
apiURL string
36+
model string
37+
debug bool
38+
message string
39+
stdin bool
40+
systemMessageFile string
41+
showSystemMessage bool
42+
}
43+
44+
var Cmd = &cobra.Command{
45+
Use: "chat",
46+
Short: "Start an AI chat interface with ROSA CLI tools",
47+
Long: `Start an interactive chat interface powered by an AI assistant.
48+
The AI has access to all ROSA CLI commands through the MCP tool registry,
49+
allowing you to interact with ROSA using natural language.
50+
51+
Requires an OpenAI-compatible API key. Set OPENAI_API_KEY environment variable
52+
or use --api-key flag.`,
53+
Example: ` # Using default OpenAI API
54+
export OPENAI_API_KEY=sk-...
55+
rosa mcp chat
56+
57+
# Using custom OpenAI-compatible endpoint
58+
rosa mcp chat --api-url https://api.anthropic.com/v1 --model claude-3-opus
59+
60+
# Using localhost model server
61+
rosa mcp chat --api-url http://localhost:8080/v1 --model local-model
62+
63+
# Non-interactive mode: send a single message
64+
rosa mcp chat --message "list all clusters"
65+
66+
# Read message from stdin
67+
echo "who am I logged in as?" | rosa mcp chat --stdin
68+
69+
# View the default system message
70+
rosa mcp chat --show-system-message
71+
72+
# Use a custom system message from a file
73+
rosa mcp chat --system-message-file ./custom-system-message.txt`,
74+
RunE: runE,
75+
Args: cobra.NoArgs,
76+
}
77+
78+
func init() {
79+
flags := Cmd.Flags()
80+
flags.StringVar(
81+
&args.apiKey,
82+
"api-key",
83+
"",
84+
"API key for OpenAI-compatible service (defaults to OPENAI_API_KEY env var)",
85+
)
86+
flags.StringVar(
87+
&args.apiURL,
88+
"api-url",
89+
"",
90+
"Base URL for OpenAI-compatible API (defaults to https://api.openai.com/v1)",
91+
)
92+
flags.StringVar(
93+
&args.model,
94+
"model",
95+
"gpt-4o",
96+
"Model to use for chat completions (gpt-4o recommended for larger context, gpt-4-turbo also supports more tokens than gpt-4)",
97+
)
98+
flags.BoolVar(
99+
&args.debug,
100+
"debug",
101+
false,
102+
"Enable debug output for troubleshooting",
103+
)
104+
flags.StringVar(
105+
&args.message,
106+
"message",
107+
"",
108+
"Non-interactive mode: send a single message and exit",
109+
)
110+
flags.BoolVar(
111+
&args.stdin,
112+
"stdin",
113+
false,
114+
"Read message from stdin instead of interactive mode",
115+
)
116+
flags.StringVar(
117+
&args.systemMessageFile,
118+
"system-message-file",
119+
"",
120+
"Path to a file containing a custom system message to override the default",
121+
)
122+
flags.BoolVar(
123+
&args.showSystemMessage,
124+
"show-system-message",
125+
false,
126+
"Display the default system message and exit",
127+
)
128+
}
129+
130+
func runE(cmd *cobra.Command, _ []string) error {
131+
rprtr := reporter.CreateReporter()
132+
133+
// Show system message if requested
134+
if args.showSystemMessage {
135+
fmt.Println(mcp.GetDefaultSystemMessage())
136+
return nil
137+
}
138+
139+
// Get API key from flag or environment variable
140+
apiKey := args.apiKey
141+
142+
if apiKey == "" {
143+
envKey := os.Getenv("OPENAI_API_KEY")
144+
if envKey == "" {
145+
if args.debug {
146+
// Debug: Check if variable exists but is empty, or doesn't exist at all
147+
allEnvVars := os.Environ()
148+
found := false
149+
for _, envVar := range allEnvVars {
150+
if len(envVar) >= 16 && envVar[:16] == "OPENAI_API_KEY=" {
151+
found = true
152+
if len(envVar) == 16 {
153+
rprtr.Debugf("OPENAI_API_KEY exists but is empty")
154+
} else {
155+
rprtr.Debugf("OPENAI_API_KEY exists with length %d (first 10 chars: %s...)", len(envVar)-16, envVar[16:26])
156+
}
157+
break
158+
}
159+
}
160+
if !found {
161+
rprtr.Debugf("OPENAI_API_KEY not found in environment variables")
162+
rprtr.Debugf("Total env vars: %d", len(allEnvVars))
163+
}
164+
}
165+
166+
return rprtr.Errorf("API key required. Set OPENAI_API_KEY environment variable or use --api-key flag")
167+
}
168+
apiKey = envKey
169+
}
170+
171+
// Validate API key is not empty
172+
if apiKey == "" {
173+
return rprtr.Errorf("API key is empty. Please provide a valid API key")
174+
}
175+
176+
// Create root command with all commands registered
177+
// We need to do this here to avoid circular dependency
178+
rootCmd := &cobra.Command{
179+
Use: "rosa",
180+
Short: "Command line tool for ROSA.",
181+
}
182+
183+
// Initialize flags
184+
fs := rootCmd.PersistentFlags()
185+
color.AddFlag(rootCmd)
186+
arguments.AddDebugFlag(fs)
187+
188+
// Register all commands using helper to avoid circular dependency
189+
registerCommandsForChat(rootCmd)
190+
191+
// Create MCP server to get access to tool and resource registries
192+
mcpServer := mcp.NewServer(rootCmd)
193+
194+
// Create chat client
195+
// API key should be validated above, but double-check
196+
if apiKey == "" {
197+
return rprtr.Errorf("API key is empty after validation. This should not happen")
198+
}
199+
200+
// Read system message from file if provided, otherwise use default
201+
systemMessage := ""
202+
if args.systemMessageFile != "" {
203+
content, err := os.ReadFile(args.systemMessageFile)
204+
if err != nil {
205+
return rprtr.Errorf("Error reading system message file: %v", err)
206+
}
207+
systemMessage = string(content)
208+
}
209+
210+
chatClient := mcp.NewChatClient(mcpServer, apiKey, args.apiURL, args.model, args.debug, systemMessage)
211+
212+
// Determine input source for non-interactive mode
213+
var userInput string
214+
if args.message != "" {
215+
// Non-interactive mode with --message flag
216+
userInput = args.message
217+
} else if args.stdin {
218+
// Read from stdin
219+
scanner := bufio.NewScanner(os.Stdin)
220+
var lines []string
221+
for scanner.Scan() {
222+
lines = append(lines, scanner.Text())
223+
}
224+
if err := scanner.Err(); err != nil {
225+
return rprtr.Errorf("Error reading from stdin: %v", err)
226+
}
227+
userInput = strings.Join(lines, "\n")
228+
}
229+
230+
if userInput != "" {
231+
// Non-interactive mode: process single message and exit
232+
if err := chatClient.ProcessMessage(userInput); err != nil {
233+
return rprtr.Errorf("Error processing message: %v", err)
234+
}
235+
return nil
236+
}
237+
238+
// Interactive mode: start REPL loop
239+
if err := chatClient.RunChatLoop(); err != nil {
240+
return rprtr.Errorf("Error running chat: %v", err)
241+
}
242+
return nil
243+
}

0 commit comments

Comments
 (0)