Skip to content

Commit 354fb13

Browse files
gwennegclaude
andauthored
RHCLOUD-46248 Add integrations MCP tools (#4456)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 989fb74 commit 354fb13

3 files changed

Lines changed: 307 additions & 2 deletions

File tree

mcp/src/main/java/com/redhat/cloud/notifications/mcp/BackendRestClient.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
99
import org.jboss.resteasy.reactive.RestHeader;
1010
import org.jboss.resteasy.reactive.RestPath;
11+
import org.jboss.resteasy.reactive.RestQuery;
1112

1213
import java.time.temporal.ChronoUnit;
14+
import java.util.List;
15+
import java.util.UUID;
1316

1417
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
1518

@@ -36,4 +39,24 @@ public interface BackendRestClient {
3639
@Path("/api/notifications/v1.0/notifications/bundles/{bundleName}/applications/{applicationName}/eventTypes/{eventTypeName}")
3740
@Produces(APPLICATION_JSON)
3841
String getEventType(@RestHeader("x-rh-identity") String xRhIdentity, @RestPath String bundleName, @RestPath String applicationName, @RestPath String eventTypeName);
42+
43+
@GET
44+
@Path("/api/integrations/v2.0/endpoints")
45+
@Produces(APPLICATION_JSON)
46+
String getEndpoints(@RestHeader("x-rh-identity") String xRhIdentity, @RestQuery List<String> type, @RestQuery Boolean active, @RestQuery String name, @RestQuery Integer limit, @RestQuery Integer pageNumber);
47+
48+
@GET
49+
@Path("/api/integrations/v2.0/endpoints/{id}")
50+
@Produces(APPLICATION_JSON)
51+
String getEndpoint(@RestHeader("x-rh-identity") String xRhIdentity, @RestPath UUID id);
52+
53+
@GET
54+
@Path("/api/integrations/v2.0/endpoints/{id}/history")
55+
@Produces(APPLICATION_JSON)
56+
String getEndpointHistory(@RestHeader("x-rh-identity") String xRhIdentity, @RestPath UUID id, @RestQuery Boolean includeDetail, @RestQuery Integer limit, @RestQuery Integer pageNumber);
57+
58+
@GET
59+
@Path("/api/integrations/v1.0/endpoints/{id}/history/{history_id}/details")
60+
@Produces(APPLICATION_JSON)
61+
String getEndpointHistoryDetails(@RestHeader("x-rh-identity") String xRhIdentity, @RestPath UUID id, @RestPath("history_id") UUID historyId);
3962
}

mcp/src/main/java/com/redhat/cloud/notifications/mcp/McpServerTools.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import jakarta.ws.rs.WebApplicationException;
1515
import org.eclipse.microprofile.rest.client.inject.RestClient;
1616

17+
import java.util.List;
18+
import java.util.UUID;
1719
import java.util.function.Supplier;
1820

1921
/**
@@ -93,6 +95,54 @@ public String getEventType(
9395
() -> backendClient.getEventType(principal.getRawHeader(), bundleName, applicationName, eventTypeName));
9496
}
9597

98+
@Tool(description = "Lists all integrations with optional filtering by type, active status, or name")
99+
public String getIntegrations(
100+
@ToolArg(description = "Filter by integration type: webhook, email_subscription, camel, ansible, drawer, pagerduty. Camel subtypes use colon notation: camel:slack, camel:teams, camel:google_chat, camel:splunk, camel:servicenow", required = false) List<String> type,
101+
@ToolArg(description = "Filter by active status", required = false) Boolean active,
102+
@ToolArg(description = "Filter by integration name", required = false) String name,
103+
@ToolArg(description = "Number of items per page", required = false, defaultValue = "20") Integer limit,
104+
@ToolArg(description = "Page number (starts at 0)", required = false) Integer pageNumber) {
105+
McpPrincipal principal = (McpPrincipal) securityIdentity.getPrincipal();
106+
return executeRestCall("getIntegrations", principal,
107+
() -> backendClient.getEndpoints(principal.getRawHeader(), type, active, name, limit, pageNumber));
108+
}
109+
110+
@Tool(description = "Retrieves a specific integration by ID")
111+
public String getIntegration(
112+
@NotBlank @ToolArg(description = "The UUID of the integration") String id) {
113+
McpPrincipal principal = (McpPrincipal) securityIdentity.getPrincipal();
114+
return executeRestCall("getIntegration", principal,
115+
() -> backendClient.getEndpoint(principal.getRawHeader(), parseUuid("id", id)));
116+
}
117+
118+
@Tool(description = "Retrieves notification history for an integration")
119+
public String getIntegrationHistory(
120+
@NotBlank @ToolArg(description = "The UUID of the integration") String id,
121+
@ToolArg(description = "Include detailed information in the reply", required = false) Boolean includeDetail,
122+
@ToolArg(description = "Number of items per page", required = false, defaultValue = "20") Integer limit,
123+
@ToolArg(description = "Page number (starts at 0)", required = false) Integer pageNumber) {
124+
McpPrincipal principal = (McpPrincipal) securityIdentity.getPrincipal();
125+
return executeRestCall("getIntegrationHistory", principal,
126+
() -> backendClient.getEndpointHistory(principal.getRawHeader(), parseUuid("id", id), includeDetail, limit, pageNumber));
127+
}
128+
129+
@Tool(description = "Retrieves detailed information about a specific integration notification event")
130+
public String getIntegrationHistoryDetails(
131+
@NotBlank @ToolArg(description = "The UUID of the integration") String integrationId,
132+
@NotBlank @ToolArg(description = "The UUID of the notification history event") String historyId) {
133+
McpPrincipal principal = (McpPrincipal) securityIdentity.getPrincipal();
134+
return executeRestCall("getIntegrationHistoryDetails", principal,
135+
() -> backendClient.getEndpointHistoryDetails(principal.getRawHeader(), parseUuid("integrationId", integrationId), parseUuid("historyId", historyId)));
136+
}
137+
138+
private static UUID parseUuid(String paramName, String value) {
139+
try {
140+
return UUID.fromString(value);
141+
} catch (IllegalArgumentException e) {
142+
throw new ToolCallException("Invalid UUID for " + paramName + ": " + value);
143+
}
144+
}
145+
96146
/**
97147
* Executes a REST client call, translating REST exceptions into MCP ToolCallExceptions.
98148
* Unexpected exceptions are left unhandled so the framework's default handler logs and

mcp/src/test/java/com/redhat/cloud/notifications/mcp/McpAuthTest.java

Lines changed: 234 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,61 @@ void afterEach() {
192192
}
193193
""";
194194

195+
private static final String GET_INTEGRATIONS_BODY = """
196+
{
197+
"jsonrpc": "2.0",
198+
"method": "tools/call",
199+
"id": 9,
200+
"params": {
201+
"name": "getIntegrations",
202+
"arguments": {}
203+
}
204+
}
205+
""";
206+
207+
private static final String GET_INTEGRATION_BODY = """
208+
{
209+
"jsonrpc": "2.0",
210+
"method": "tools/call",
211+
"id": 10,
212+
"params": {
213+
"name": "getIntegration",
214+
"arguments": {
215+
"id": "12345678-abcd-1234-abcd-1234567890ab"
216+
}
217+
}
218+
}
219+
""";
220+
221+
private static final String GET_INTEGRATION_HISTORY_BODY = """
222+
{
223+
"jsonrpc": "2.0",
224+
"method": "tools/call",
225+
"id": 11,
226+
"params": {
227+
"name": "getIntegrationHistory",
228+
"arguments": {
229+
"id": "12345678-abcd-1234-abcd-1234567890ab"
230+
}
231+
}
232+
}
233+
""";
234+
235+
private static final String GET_INTEGRATION_HISTORY_DETAILS_BODY = """
236+
{
237+
"jsonrpc": "2.0",
238+
"method": "tools/call",
239+
"id": 12,
240+
"params": {
241+
"name": "getIntegrationHistoryDetails",
242+
"arguments": {
243+
"integrationId": "12345678-abcd-1234-abcd-1234567890ab",
244+
"historyId": "abcd1234-abcd-1234-abcd-1234567890ab"
245+
}
246+
}
247+
}
248+
""";
249+
195250
// --- Helpers ---
196251

197252
private static String validIdentity() {
@@ -311,9 +366,10 @@ public void testMcpInitializeWithEmptyUsernameIsRejected() {
311366
public void testToolsListWithValidIdentity() {
312367
postMcp(validIdentity(), TOOLS_LIST_BODY)
313368
.statusCode(200)
314-
.body("result.tools.size()", greaterThanOrEqualTo(6))
369+
.body("result.tools.size()", greaterThanOrEqualTo(10))
315370
.body("result.tools.name", hasItems("serverInfo", "whoami", "getSeverities",
316-
"getBundle", "getApplication", "getEventType"));
371+
"getBundle", "getApplication", "getEventType",
372+
"getIntegrations", "getIntegration", "getIntegrationHistory", "getIntegrationHistoryDetails"));
317373
}
318374

319375
@Test
@@ -554,6 +610,182 @@ public void testGetEventTypeWhenBackendReturns404() {
554610
micrometerAssertionHelper.assertCounterIncrement(AUTH_SUCCESS_COUNTER, 1);
555611
}
556612

613+
// --- getIntegrations tool tests ---
614+
615+
@Test
616+
public void testGetIntegrationsWithValidIdentity() {
617+
String integrationsJson = "{\"data\":[{\"id\":\"12345678-abcd-1234-abcd-1234567890ab\",\"name\":\"My Webhook\",\"type\":\"webhook\"}],\"meta\":{\"count\":1},\"links\":{}}";
618+
MockServerLifecycleManager.getClient().stubFor(
619+
get(urlPathEqualTo("/api/integrations/v2.0/endpoints"))
620+
.withHeader("x-rh-identity", equalTo(validIdentity()))
621+
.willReturn(aResponse()
622+
.withHeader("Content-Type", "application/json")
623+
.withBody(integrationsJson))
624+
);
625+
626+
postMcp(validIdentity(), GET_INTEGRATIONS_BODY)
627+
.statusCode(200)
628+
.body("result.content[0].text", containsString("My Webhook"))
629+
.body("result.content[0].text", containsString("webhook"));
630+
micrometerAssertionHelper.assertCounterIncrement(AUTH_SUCCESS_COUNTER, 1);
631+
632+
MockServerLifecycleManager.getClient().verify(
633+
getRequestedFor(urlPathEqualTo("/api/integrations/v2.0/endpoints"))
634+
.withHeader("x-rh-identity", equalTo(validIdentity()))
635+
);
636+
}
637+
638+
@Test
639+
public void testGetIntegrationsWithoutIdentityIsRejected() {
640+
postMcp(null, GET_INTEGRATIONS_BODY).statusCode(401);
641+
}
642+
643+
@Test
644+
public void testGetIntegrationsWhenBackendReturns404() {
645+
MockServerLifecycleManager.getClient().stubFor(
646+
get(urlPathEqualTo("/api/integrations/v2.0/endpoints"))
647+
.willReturn(aResponse().withStatus(404))
648+
);
649+
650+
postMcp(validIdentity(), GET_INTEGRATIONS_BODY)
651+
.statusCode(200)
652+
.body("result.isError", is(true))
653+
.body("result.content[0].text", containsString("Resource not found"));
654+
micrometerAssertionHelper.assertCounterIncrement(AUTH_SUCCESS_COUNTER, 1);
655+
}
656+
657+
// --- getIntegration tool tests ---
658+
659+
@Test
660+
public void testGetIntegrationWithValidIdentity() {
661+
String integrationJson = "{\"id\":\"12345678-abcd-1234-abcd-1234567890ab\",\"name\":\"My Webhook\",\"type\":\"webhook\",\"enabled\":true}";
662+
MockServerLifecycleManager.getClient().stubFor(
663+
get(urlPathEqualTo("/api/integrations/v2.0/endpoints/12345678-abcd-1234-abcd-1234567890ab"))
664+
.withHeader("x-rh-identity", equalTo(validIdentity()))
665+
.willReturn(aResponse()
666+
.withHeader("Content-Type", "application/json")
667+
.withBody(integrationJson))
668+
);
669+
670+
postMcp(validIdentity(), GET_INTEGRATION_BODY)
671+
.statusCode(200)
672+
.body("result.content[0].text", containsString("My Webhook"))
673+
.body("result.content[0].text", containsString("12345678-abcd-1234-abcd-1234567890ab"));
674+
micrometerAssertionHelper.assertCounterIncrement(AUTH_SUCCESS_COUNTER, 1);
675+
676+
MockServerLifecycleManager.getClient().verify(
677+
getRequestedFor(urlPathEqualTo("/api/integrations/v2.0/endpoints/12345678-abcd-1234-abcd-1234567890ab"))
678+
.withHeader("x-rh-identity", equalTo(validIdentity()))
679+
);
680+
}
681+
682+
@Test
683+
public void testGetIntegrationWithoutIdentityIsRejected() {
684+
postMcp(null, GET_INTEGRATION_BODY).statusCode(401);
685+
}
686+
687+
@Test
688+
public void testGetIntegrationWhenBackendReturns404() {
689+
MockServerLifecycleManager.getClient().stubFor(
690+
get(urlPathEqualTo("/api/integrations/v2.0/endpoints/12345678-abcd-1234-abcd-1234567890ab"))
691+
.willReturn(aResponse().withStatus(404))
692+
);
693+
694+
postMcp(validIdentity(), GET_INTEGRATION_BODY)
695+
.statusCode(200)
696+
.body("result.isError", is(true))
697+
.body("result.content[0].text", containsString("Resource not found"));
698+
micrometerAssertionHelper.assertCounterIncrement(AUTH_SUCCESS_COUNTER, 1);
699+
}
700+
701+
// --- getIntegrationHistory tool tests ---
702+
703+
@Test
704+
public void testGetIntegrationHistoryWithValidIdentity() {
705+
String historyJson = "{\"data\":[{\"id\":\"abcd1234-abcd-1234-abcd-1234567890ab\",\"status\":\"SUCCESS\"}],\"meta\":{\"count\":1},\"links\":{}}";
706+
MockServerLifecycleManager.getClient().stubFor(
707+
get(urlPathEqualTo("/api/integrations/v2.0/endpoints/12345678-abcd-1234-abcd-1234567890ab/history"))
708+
.withHeader("x-rh-identity", equalTo(validIdentity()))
709+
.willReturn(aResponse()
710+
.withHeader("Content-Type", "application/json")
711+
.withBody(historyJson))
712+
);
713+
714+
postMcp(validIdentity(), GET_INTEGRATION_HISTORY_BODY)
715+
.statusCode(200)
716+
.body("result.content[0].text", containsString("SUCCESS"))
717+
.body("result.content[0].text", containsString("abcd1234"));
718+
micrometerAssertionHelper.assertCounterIncrement(AUTH_SUCCESS_COUNTER, 1);
719+
720+
MockServerLifecycleManager.getClient().verify(
721+
getRequestedFor(urlPathEqualTo("/api/integrations/v2.0/endpoints/12345678-abcd-1234-abcd-1234567890ab/history"))
722+
.withHeader("x-rh-identity", equalTo(validIdentity()))
723+
);
724+
}
725+
726+
@Test
727+
public void testGetIntegrationHistoryWithoutIdentityIsRejected() {
728+
postMcp(null, GET_INTEGRATION_HISTORY_BODY).statusCode(401);
729+
}
730+
731+
@Test
732+
public void testGetIntegrationHistoryWhenBackendReturns404() {
733+
MockServerLifecycleManager.getClient().stubFor(
734+
get(urlPathEqualTo("/api/integrations/v2.0/endpoints/12345678-abcd-1234-abcd-1234567890ab/history"))
735+
.willReturn(aResponse().withStatus(404))
736+
);
737+
738+
postMcp(validIdentity(), GET_INTEGRATION_HISTORY_BODY)
739+
.statusCode(200)
740+
.body("result.isError", is(true))
741+
.body("result.content[0].text", containsString("Resource not found"));
742+
micrometerAssertionHelper.assertCounterIncrement(AUTH_SUCCESS_COUNTER, 1);
743+
}
744+
745+
// --- getIntegrationHistoryDetails tool tests ---
746+
747+
@Test
748+
public void testGetIntegrationHistoryDetailsWithValidIdentity() {
749+
String detailsJson = "{\"type\":\"com.redhat.console.notification.toCamel.webhook\",\"target\":\"https://example.com/hook\",\"outcome\":\"SUCCESS\"}";
750+
MockServerLifecycleManager.getClient().stubFor(
751+
get(urlPathEqualTo("/api/integrations/v1.0/endpoints/12345678-abcd-1234-abcd-1234567890ab/history/abcd1234-abcd-1234-abcd-1234567890ab/details"))
752+
.withHeader("x-rh-identity", equalTo(validIdentity()))
753+
.willReturn(aResponse()
754+
.withHeader("Content-Type", "application/json")
755+
.withBody(detailsJson))
756+
);
757+
758+
postMcp(validIdentity(), GET_INTEGRATION_HISTORY_DETAILS_BODY)
759+
.statusCode(200)
760+
.body("result.content[0].text", containsString("webhook"))
761+
.body("result.content[0].text", containsString("SUCCESS"));
762+
micrometerAssertionHelper.assertCounterIncrement(AUTH_SUCCESS_COUNTER, 1);
763+
764+
MockServerLifecycleManager.getClient().verify(
765+
getRequestedFor(urlPathEqualTo("/api/integrations/v1.0/endpoints/12345678-abcd-1234-abcd-1234567890ab/history/abcd1234-abcd-1234-abcd-1234567890ab/details"))
766+
.withHeader("x-rh-identity", equalTo(validIdentity()))
767+
);
768+
}
769+
770+
@Test
771+
public void testGetIntegrationHistoryDetailsWithoutIdentityIsRejected() {
772+
postMcp(null, GET_INTEGRATION_HISTORY_DETAILS_BODY).statusCode(401);
773+
}
774+
775+
@Test
776+
public void testGetIntegrationHistoryDetailsWhenBackendReturns404() {
777+
MockServerLifecycleManager.getClient().stubFor(
778+
get(urlPathEqualTo("/api/integrations/v1.0/endpoints/12345678-abcd-1234-abcd-1234567890ab/history/abcd1234-abcd-1234-abcd-1234567890ab/details"))
779+
.willReturn(aResponse().withStatus(404))
780+
);
781+
782+
postMcp(validIdentity(), GET_INTEGRATION_HISTORY_DETAILS_BODY)
783+
.statusCode(200)
784+
.body("result.isError", is(true))
785+
.body("result.content[0].text", containsString("Resource not found"));
786+
micrometerAssertionHelper.assertCounterIncrement(AUTH_SUCCESS_COUNTER, 1);
787+
}
788+
557789
// --- Health/metrics bypass tests ---
558790

559791
@Test

0 commit comments

Comments
 (0)