Skip to content

Commit ec75899

Browse files
Merge pull request #1397 from otaviof/RHTAP-5997
RHTAP-5997: MCP Notes Tool
2 parents 3906837 + 50f9288 commit ec75899

File tree

9 files changed

+329
-91
lines changed

9 files changed

+329
-91
lines changed

pkg/deployer/helm.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ func (h *Helm) Verify() error {
147147
return nil
148148
}
149149

150+
// VerifyWithRetry attempts to verify the Helm deployment multiple times with a
151+
// delay between retries.
150152
func (h *Helm) VerifyWithRetry() error {
151153
var err error
152154
var retries = 3
@@ -178,6 +180,19 @@ func (h *Helm) VisitReleaseResources(
178180
})
179181
}
180182

183+
// GetNotes retrieves the latest release (version 0) of the Helm chart, printing
184+
// out the notes from the info section.
185+
func (h *Helm) GetNotes() (string, error) {
186+
c := action.NewGet(h.actionCfg)
187+
c.Version = 0
188+
189+
res, err := c.Run(h.chart.Name())
190+
if err != nil {
191+
return "", err
192+
}
193+
return res.Info.Notes, nil
194+
}
195+
181196
// NewHelm creates a new Helm instance, setting up the Helm action configuration
182197
// to be used on subsequent interactions. The Helm instance is bound to a single
183198
// Helm Chart.

pkg/mcpserver/instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,4 @@ I will guide you with suggestions for the next logical action in my responses. L
6767
The installer has finished successfully and all components are running as expected.
6868

6969
- Use `tssc_status` to view the overall installer status.
70+
- Use `tssc_notes` to retrieve instructions on how to connect to a service (product) deployed by the installer. You must provide the product name.

pkg/mcptools/deploytools.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,7 @@ func (d *DeployTools) deployHandler(
4646
// error to inform the user about MCP configuration tools.
4747
cfg, err := d.cm.GetConfig(ctx)
4848
if err != nil {
49-
return mcp.NewToolResultError(fmt.Sprintf(`
50-
The cluster is not configured yet, use the tool %q to identify the cluster
51-
installation status, and the next actions after that.
52-
53-
> %s`,
54-
StatusToolName, err.Error(),
55-
)), nil
49+
return mcp.NewToolResultError(missingClusterConfigErrorFromErr(err)), nil
5650
}
5751

5852
// Validating the topology as a whole, dependencies and integrations to ensure

pkg/mcptools/notestool.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package mcptools
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
8+
"github.com/redhat-appstudio/tssc-cli/pkg/config"
9+
"github.com/redhat-appstudio/tssc-cli/pkg/constants"
10+
"github.com/redhat-appstudio/tssc-cli/pkg/deployer"
11+
"github.com/redhat-appstudio/tssc-cli/pkg/flags"
12+
"github.com/redhat-appstudio/tssc-cli/pkg/installer"
13+
"github.com/redhat-appstudio/tssc-cli/pkg/k8s"
14+
"github.com/redhat-appstudio/tssc-cli/pkg/resolver"
15+
16+
"github.com/mark3labs/mcp-go/mcp"
17+
"github.com/mark3labs/mcp-go/server"
18+
)
19+
20+
// NotesTool a MCP tool to provide connection instructions for products. These
21+
// products must be completely deployed before its "NOTES.txt" is generated.
22+
type NotesTool struct {
23+
logger *slog.Logger // application logger
24+
flags *flags.Flags // global flags
25+
kube *k8s.Kube // kubernetes client
26+
cm *config.ConfigMapManager // cluster configuration
27+
tb *resolver.TopologyBuilder // topology builder
28+
job *installer.Job // cluster deployment job
29+
}
30+
31+
var _ Interface = &NotesTool{}
32+
33+
const (
34+
// NotesToolName retrieves the connection instruction for a product.
35+
NotesToolName = constants.AppName + "_notes"
36+
)
37+
38+
// notesHandler retrieves the Helm chart notes (NOTES.txt) for a specified Red Hat
39+
// product. It ensures the product name is provided, checks if the cluster
40+
// installation is in a "completed" phase, and then uses a Helm client to fetch
41+
// and return the notes.
42+
func (n *NotesTool) notesHandler(
43+
ctx context.Context,
44+
ctr mcp.CallToolRequest,
45+
) (*mcp.CallToolResult, error) {
46+
// Ensure the user has provided the product name.
47+
name := ctr.GetString(NameArg, "")
48+
if name == "" {
49+
return mcp.NewToolResultError(`
50+
You must inform the Red Hat product name`,
51+
), nil
52+
}
53+
54+
// Check if the cluster is ready. If not, provide instructions on how to
55+
// proceed. The installer must be on "completed" status.
56+
phase, err := getInstallerPhase(ctx, n.cm, n.tb, n.job)
57+
currentStatus := fmt.Sprintf(`
58+
# Current Status: %q
59+
60+
The cluster is not ready, use the tool %q to check the overall status and general
61+
directions on how to proceed.`,
62+
phase, StatusToolName,
63+
)
64+
if err != nil {
65+
return mcp.NewToolResultText(fmt.Sprintf(`%s
66+
67+
Inspecting the cluster returned the following error:
68+
69+
> %s`,
70+
currentStatus, err.Error(),
71+
)), nil
72+
}
73+
if phase != CompletedPhase {
74+
return mcp.NewToolResultText(currentStatus), nil
75+
}
76+
77+
dep, err := n.tb.GetCollection().GetProductDependency(name)
78+
if err != nil {
79+
return mcp.NewToolResultErrorFromErr(
80+
fmt.Sprintf(`
81+
Unable to find the dependency for the informed product name %q`,
82+
name,
83+
),
84+
err,
85+
), nil
86+
}
87+
88+
hc, err := deployer.NewHelm(
89+
n.logger, n.flags, n.kube, dep.Namespace(), dep.Chart())
90+
if err != nil {
91+
return mcp.NewToolResultErrorFromErr(
92+
fmt.Sprintf(`
93+
Error trying to instantiate a Helm client for the chart %q on namespace %q.`,
94+
dep.Chart().Name(),
95+
dep.Namespace(),
96+
),
97+
err,
98+
), nil
99+
}
100+
101+
notes, err := hc.GetNotes()
102+
if err != nil {
103+
return mcp.NewToolResultErrorFromErr(
104+
fmt.Sprintf(`
105+
Unable to get "NOTES.txt" for the chart %q on namespace %q.`,
106+
dep.Chart().Name(),
107+
dep.Namespace(),
108+
),
109+
err,
110+
), nil
111+
}
112+
113+
return mcp.NewToolResultText(notes), nil
114+
}
115+
116+
func (n *NotesTool) Init(s *server.MCPServer) {
117+
s.AddTools([]server.ServerTool{{
118+
Tool: mcp.NewTool(
119+
NotesToolName,
120+
mcp.WithDescription(`
121+
Retrieve the service notes, the initial coordinates to utilize services deployed
122+
by this installer, from the informed product name.`,
123+
),
124+
mcp.WithString(
125+
NameArg,
126+
mcp.Description(`
127+
The name of the Red Hat product to retrieve connection information.`,
128+
),
129+
),
130+
),
131+
Handler: n.notesHandler,
132+
}}...)
133+
}
134+
135+
func NewNotesTool(
136+
logger *slog.Logger,
137+
f *flags.Flags,
138+
kube *k8s.Kube,
139+
cm *config.ConfigMapManager,
140+
tb *resolver.TopologyBuilder,
141+
) *NotesTool {
142+
return &NotesTool{
143+
logger: logger,
144+
flags: f,
145+
kube: kube,
146+
cm: cm,
147+
tb: tb,
148+
job: installer.NewJob(kube),
149+
}
150+
}

pkg/mcptools/status.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package mcptools
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"github.com/redhat-appstudio/tssc-cli/pkg/config"
8+
"github.com/redhat-appstudio/tssc-cli/pkg/installer"
9+
"github.com/redhat-appstudio/tssc-cli/pkg/resolver"
10+
)
11+
12+
func getInstallerPhase(
13+
ctx context.Context,
14+
cm *config.ConfigMapManager,
15+
tb *resolver.TopologyBuilder,
16+
job *installer.Job,
17+
) (string, error) {
18+
// Ensure the cluster is configured.
19+
cfg, err := cm.GetConfig(ctx)
20+
if err != nil {
21+
// If config is missing, we are in AwaitingConfigurationPhase.
22+
// The specific error will be used by the caller for detailed messaging.
23+
return AwaitingConfigurationPhase, err
24+
}
25+
26+
// Given the cluster is configured, inspect the topology to ensure all
27+
// dependencies and integrations are resolved.
28+
if _, err = tb.Build(ctx, cfg); err != nil {
29+
// If topology build fails, we are in AwaitingIntegrationsPhase.
30+
// The specific resolver error will be used by the caller for detailed messaging.
31+
return AwaitingIntegrationsPhase, err
32+
}
33+
34+
// Given integrations are in place, inspect the current state of the
35+
// cluster deployment job.
36+
jobState, err := job.GetState(ctx)
37+
if err != nil {
38+
// If job state cannot be determined, it's an operational error.
39+
// Return InstallerErrorPhase with the original error.
40+
return InstallerErrorPhase, err
41+
}
42+
43+
// Map the job state to an installer phase.
44+
switch jobState {
45+
case installer.NotFound:
46+
return ReadyToDeployPhase, nil
47+
case installer.Deploying, installer.Failed:
48+
// Both 'Deploying' and 'Failed' states indicate that the deployment
49+
// process is active or has attempted to run, thus falling under
50+
// the 'DeployingPhase' for overall status reporting.
51+
return DeployingPhase, nil
52+
case installer.Done:
53+
return CompletedPhase, nil
54+
default:
55+
// Unrecognized installer state from s.job.GetState.
56+
// This is also an operational error.
57+
return InstallerErrorPhase,
58+
errors.New("unknown installer job state reported by cluster")
59+
}
60+
}

0 commit comments

Comments
 (0)