Skip to content

Commit 433b5e1

Browse files
EPMRPP-108935 || MCP Server. Tools. Fix rp project parameter configuration
1 parent 6f46779 commit 433b5e1

File tree

10 files changed

+195
-93
lines changed

10 files changed

+195
-93
lines changed

cmd/reportportal-mcp-server/main.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ func buildHTTPServerConfig(cmd *cli.Command) (mcpreportportal.HTTPServerConfig,
203203
// Retrieve required parameters from CLI flags
204204
host := cmd.String("rp-host")
205205
token := cmd.String("token")
206-
project := cmd.String("project")
207206
userID := cmd.String("user-id")
208207
analyticsAPISecret := mcpreportportal.GetAnalyticArg()
209208
analyticsOff := cmd.Bool("analytics-off")
@@ -226,7 +225,6 @@ func buildHTTPServerConfig(cmd *cli.Command) (mcpreportportal.HTTPServerConfig,
226225
Version: fmt.Sprintf("%s (%s) %s", version, commit, date),
227226
HostURL: hostUrl,
228227
FallbackRPToken: token,
229-
DefaultProject: project,
230228
UserID: userID,
231229
GA4Secret: analyticsAPISecret,
232230
AnalyticsOn: !analyticsOff,
@@ -239,7 +237,6 @@ func newMCPServer(cmd *cli.Command) (*server.MCPServer, *mcpreportportal.Analyti
239237
// Retrieve required parameters from the command flags
240238
token := cmd.String("token") // API token
241239
host := cmd.String("rp-host") // ReportPortal host URL
242-
project := cmd.String("project") // ReportPortal project name
243240
userID := cmd.String("user-id") // Unified user ID for analytics
244241
analyticsAPISecret := mcpreportportal.GetAnalyticArg() // Analytics API secret
245242
analyticsOff := cmd.Bool("analytics-off") // Disable analytics flag
@@ -254,7 +251,6 @@ func newMCPServer(cmd *cli.Command) (*server.MCPServer, *mcpreportportal.Analyti
254251
version,
255252
hostUrl,
256253
token,
257-
project,
258254
userID,
259255
analyticsAPISecret,
260256
!analyticsOff, // Convert analyticsOff to analyticsOn
@@ -267,6 +263,11 @@ func newMCPServer(cmd *cli.Command) (*server.MCPServer, *mcpreportportal.Analyti
267263

268264
// runStdioServer starts the ReportPortal MCP server in stdio mode.
269265
func runStdioServer(ctx context.Context, cmd *cli.Command) error {
266+
rpProject := cmd.String("project")
267+
if rpProject != "" {
268+
// Add project to request context default project name from Environment variable
269+
ctx = mcpreportportal.WithProjectInContext(ctx, rpProject)
270+
}
270271
mcpServer, analytics, err := newMCPServer(cmd)
271272
if err != nil {
272273
return fmt.Errorf("failed to create ReportPortal MCP server: %w", err)

internal/reportportal/http_server.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ type HTTPServerConfig struct {
4040
Version string
4141
HostURL *url.URL
4242
FallbackRPToken string
43-
DefaultProject string
4443
UserID string
4544
GA4Secret string
4645
AnalyticsOn bool
@@ -141,7 +140,7 @@ func (hs *HTTPServer) initializeTools() error {
141140
rpClient.APIClient.GetConfig().Middleware = QueryParamsMiddleware
142141

143142
// Add launch management tools with analytics
144-
launches := NewLaunchResources(rpClient, hs.config.DefaultProject, hs.analytics)
143+
launches := NewLaunchResources(rpClient, hs.analytics)
145144

146145
hs.mcpServer.AddTool(launches.toolGetLaunches())
147146
hs.mcpServer.AddTool(launches.toolGetLastLaunchByName())
@@ -154,7 +153,7 @@ func (hs *HTTPServer) initializeTools() error {
154153
hs.mcpServer.AddResourceTemplate(launches.resourceLaunch())
155154

156155
// Add test item tools
157-
testItems := NewTestItemResources(rpClient, hs.config.DefaultProject, hs.analytics)
156+
testItems := NewTestItemResources(rpClient, hs.analytics)
158157

159158
hs.mcpServer.AddTool(testItems.toolGetTestItemById())
160159
hs.mcpServer.AddTool(testItems.toolGetTestItemsByFilter())

internal/reportportal/integration_project_test.go

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,40 @@ func TestIntegration_ProjectExtractionFlow(t *testing.T) {
1818
expectError bool
1919
}{
2020
{
21-
name: "HTTP header project takes precedence",
21+
name: "request project takes precedence over HTTP header",
2222
httpHeaders: map[string]string{"X-Project": "http-project"},
2323
requestProject: "request-project",
24-
expectedProject: "http-project",
24+
expectedProject: "request-project",
2525
expectError: false,
2626
},
2727
{
28-
name: "fallback to request when no HTTP header",
28+
name: "use request project when no HTTP header",
2929
httpHeaders: map[string]string{},
3030
requestProject: "request-project",
3131
expectedProject: "request-project",
3232
expectError: false,
3333
},
3434
{
35-
name: "fallback to request when empty HTTP header",
35+
name: "use request project when empty HTTP header",
3636
httpHeaders: map[string]string{"X-Project": ""},
3737
requestProject: "request-project",
3838
expectedProject: "request-project",
3939
expectError: false,
4040
},
4141
{
42-
name: "fallback to request when whitespace HTTP header",
42+
name: "use request project when whitespace HTTP header",
4343
httpHeaders: map[string]string{"X-Project": " "},
4444
requestProject: "request-project",
4545
expectedProject: "request-project",
4646
expectError: false,
4747
},
48+
{
49+
name: "fallback to HTTP header when no request project",
50+
httpHeaders: map[string]string{"X-Project": "http-project"},
51+
requestProject: "",
52+
expectedProject: "http-project",
53+
expectError: false,
54+
},
4855
{
4956
name: "error when no project anywhere",
5057
httpHeaders: map[string]string{},
@@ -53,12 +60,19 @@ func TestIntegration_ProjectExtractionFlow(t *testing.T) {
5360
expectError: true,
5461
},
5562
{
56-
name: "HTTP header with whitespace is trimmed",
63+
name: "HTTP header with whitespace is trimmed when used",
5764
httpHeaders: map[string]string{"X-Project": " http-project "},
58-
requestProject: "request-project",
65+
requestProject: "",
5966
expectedProject: "http-project",
6067
expectError: false,
6168
},
69+
{
70+
name: "request project with whitespace is trimmed",
71+
httpHeaders: map[string]string{"X-Project": "http-project"},
72+
requestProject: " request-project ",
73+
expectedProject: "request-project",
74+
expectError: false,
75+
},
6276
}
6377

6478
for _, tt := range tests {
@@ -101,17 +115,19 @@ func TestIntegration_ProjectExtractionFlow(t *testing.T) {
101115

102116
func TestIntegration_CompleteHTTPFlow(t *testing.T) {
103117
// Test the complete flow from HTTP request to tool execution
118+
// Request parameter should take precedence over HTTP header
104119
req := httptest.NewRequest("GET", "/test", nil)
105-
req.Header.Set("X-Project", "integration-test-project")
120+
req.Header.Set("X-Project", "header-project")
106121

107122
var capturedProject string
108123
var projectFound bool
109124

110125
// Create a handler that simulates the MCP tool execution
111126
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
112-
// Simulate MCP tool request
127+
// Simulate MCP tool request with explicit project parameter
128+
// This should take precedence over the HTTP header
113129
mcpRequest := MockCallToolRequest{
114-
project: "fallback-project",
130+
project: "request-project",
115131
}
116132

117133
// Extract project using our function
@@ -133,8 +149,8 @@ func TestIntegration_CompleteHTTPFlow(t *testing.T) {
133149
// Execute request
134150
middleware.ServeHTTP(rr, req)
135151

136-
// Verify results
152+
// Verify results - request parameter should win
137153
assert.Equal(t, http.StatusOK, rr.Code)
138154
assert.True(t, projectFound)
139-
assert.Equal(t, "integration-test-project", capturedProject)
155+
assert.Equal(t, "request-project", capturedProject)
140156
}

internal/reportportal/items.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ type TestItemResources struct {
2424

2525
func NewTestItemResources(
2626
client *gorp.Client,
27-
defaultProject string,
2827
analytics *Analytics,
2928
) *TestItemResources {
3029
return &TestItemResources{
31-
client: client,
32-
projectParameter: newProjectParameter(defaultProject),
33-
analytics: analytics,
30+
client: client,
31+
projectParameter: mcp.WithString("project", // Parameter for specifying the project name)
32+
mcp.Description("Project name"),
33+
),
34+
analytics: analytics,
3435
}
3536
}
3637

internal/reportportal/launches.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ type LaunchResources struct {
2424

2525
func NewLaunchResources(
2626
client *gorp.Client,
27-
defaultProject string,
2827
analytics *Analytics,
2928
) *LaunchResources {
3029
return &LaunchResources{
31-
client: client,
32-
projectParameter: newProjectParameter(defaultProject),
33-
analytics: analytics,
30+
client: client,
31+
projectParameter: mcp.WithString("project", // Parameter for specifying the project name)
32+
mcp.Description("Project name"),
33+
),
34+
analytics: analytics,
3435
}
3536
}
3637

internal/reportportal/launches_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestListLaunchesTool(t *testing.T) {
4343
srv := mcptest.NewUnstartedServer(t)
4444

4545
serverURL, _ := url.Parse(mockServer.URL)
46-
launchTools := NewLaunchResources(gorp.NewClient(serverURL, ""), testProject, nil)
46+
launchTools := NewLaunchResources(gorp.NewClient(serverURL, ""), nil)
4747
srv.AddTool(launchTools.toolGetLaunches())
4848

4949
err := srv.Start(ctx)

internal/reportportal/mock_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,15 @@ func (m MockCallToolRequest) RequireStringSlice(key string) ([]string, error) {
5959
}
6060

6161
// extractProjectWithMock is a test helper that works with MockCallToolRequest
62+
// This mimics the actual extractProject function's priority order
6263
func extractProjectWithMock(ctx context.Context, rq MockCallToolRequest) (string, error) {
63-
// First try to get project from context (from HTTP header)
64+
// Use project parameter from request (highest priority)
65+
if project := strings.TrimSpace(rq.GetString("project", "")); project != "" {
66+
return project, nil
67+
}
68+
// Fallback to project from context (request's HTTP header or environment variable, depends on MCP mode)
6469
if project, ok := GetProjectFromContext(ctx); ok {
6570
return project, nil
6671
}
67-
68-
project, err := rq.RequireString("project")
69-
return strings.TrimSpace(project), err
72+
return "", assert.AnError
7073
}

internal/reportportal/server.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var promptFiles embed.FS
1919
func NewServer(
2020
version string,
2121
hostUrl *url.URL,
22-
token, defaultProject string,
22+
token,
2323
userID, analyticsAPISecret string,
2424
analyticsOn bool,
2525
) (*server.MCPServer, *Analytics, error) {
@@ -47,7 +47,7 @@ func NewServer(
4747
}
4848
}
4949

50-
launches := NewLaunchResources(rpClient, defaultProject, analytics)
50+
launches := NewLaunchResources(rpClient, analytics)
5151
s.AddTool(launches.toolGetLaunches())
5252
s.AddTool(launches.toolGetLastLaunchByName())
5353
s.AddTool(launches.toolForceFinishLaunch())
@@ -57,7 +57,7 @@ func NewServer(
5757
s.AddTool(launches.toolRunQualityGate())
5858
s.AddResourceTemplate(launches.resourceLaunch())
5959

60-
testItems := NewTestItemResources(rpClient, defaultProject, analytics)
60+
testItems := NewTestItemResources(rpClient, analytics)
6161
s.AddTool(testItems.toolGetTestItemById())
6262
s.AddTool(testItems.toolGetTestItemsByFilter())
6363
s.AddTool(testItems.toolGetTestItemLogsByFilter())

internal/reportportal/utils.go

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,18 @@ func applyPaginationOptions[T PaginatedRequest[T]](
8383
PageSort(pageSort)
8484
}
8585

86-
func newProjectParameter(defaultProject string) mcp.ToolOption {
87-
return mcp.WithString("project", // Parameter for specifying the project name)
88-
mcp.Description("Project name"),
89-
mcp.DefaultString(defaultProject),
90-
mcp.Required(),
91-
)
92-
}
93-
9486
func extractProject(ctx context.Context, rq mcp.CallToolRequest) (string, error) {
95-
// First try to get project from context (from HTTP header)
87+
// Use project parameter from request
88+
if project := strings.TrimSpace(rq.GetString("project", "")); project != "" {
89+
return project, nil
90+
}
91+
// Fallback to project from context (request's HTTP header or environment variable, depends on MCP mode)
9692
if project, ok := GetProjectFromContext(ctx); ok {
9793
return project, nil
9894
}
99-
100-
project, err := rq.RequireString("project")
101-
return strings.TrimSpace(project), err
95+
return "", fmt.Errorf(
96+
"no project parameter found in request, HTTP header, or environment variable",
97+
)
10298
}
10399

104100
func extractResponseError(err error, rs *http.Response) (errText string) {

0 commit comments

Comments
 (0)