Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ export function ControlDetailSection({ controlData }: ControlDetailSectionProps)
<div className="flex-1 min-h-0 bg-base-100 rounded-2xl overflow-hidden flex flex-col shadow-xl">
{/* Requirement breadcrumb header */}
<div className="bg-base-200 px-6 py-2 flex items-center justify-between border-b border-base-300">
<h2 className="text-sm font-bold flex items-center gap-2 text-blue-700">
<IoShieldCheckmarkOutline className="text-blue-700" />
<h2 className="text-sm font-bold flex items-center gap-2 text-primary">
<IoShieldCheckmarkOutline className="text-primary" />
<span>{controlData.controlName}</span>
<span className="text-gray-400">/</span>
<span>Requirement</span>
Expand All @@ -115,14 +115,14 @@ export function ControlDetailSection({ controlData }: ControlDetailSectionProps)
<div role="tablist" className="tabs tabs-boxed tabs-xs bg-base-100">
<button
role="tab"
className={`tab ${reqViewMode === 'readable' ? 'tab-active !bg-blue-700 !text-white' : ''}`}
className={`tab ${reqViewMode === 'readable' ? 'tab-active !bg-primary !text-primary-content' : ''}`}
onClick={() => setReqViewMode('readable')}
>
Readable
</button>
<button
role="tab"
className={`tab ${reqViewMode === 'raw' ? 'tab-active !bg-blue-700 !text-white' : ''}`}
className={`tab ${reqViewMode === 'raw' ? 'tab-active !bg-primary !text-primary-content' : ''}`}
onClick={() => setReqViewMode('raw')}
>
Raw JSON
Expand All @@ -138,7 +138,7 @@ export function ControlDetailSection({ controlData }: ControlDetailSectionProps)
<button
key={v}
role="tab"
className={`tab gap-1 rounded-lg ${selectedReqVersion === v ? 'tab-active !bg-blue-700 !text-white' : ''}`}
className={`tab gap-1 rounded-lg ${selectedReqVersion === v ? 'tab-active !bg-primary !text-primary-content' : ''}`}
onClick={() => handleReqVersionClick(v)}
>
{v}
Expand Down Expand Up @@ -185,14 +185,14 @@ export function ControlDetailSection({ controlData }: ControlDetailSectionProps)
<div role="tablist" className="tabs tabs-boxed tabs-xs bg-base-100">
<button
role="tab"
className={`tab ${cfgViewMode === 'readable' ? 'tab-active !bg-accent !text-white' : ''}`}
className={`tab ${cfgViewMode === 'readable' ? 'tab-active !bg-primary !text-primary-content' : ''}`}
onClick={() => setCfgViewMode('readable')}
>
Readable
</button>
<button
role="tab"
className={`tab ${cfgViewMode === 'raw' ? 'tab-active !bg-accent !text-white' : ''}`}
className={`tab ${cfgViewMode === 'raw' ? 'tab-active !bg-primary !text-primary-content' : ''}`}
onClick={() => setCfgViewMode('raw')}
>
Raw JSON
Expand All @@ -208,7 +208,7 @@ export function ControlDetailSection({ controlData }: ControlDetailSectionProps)
<button
key={cid}
role="tab"
className={`tab gap-1 rounded-lg ${selectedConfigId === cid ? 'tab-active !bg-accent !text-white' : ''}`}
className={`tab gap-1 rounded-lg ${selectedConfigId === cid ? 'tab-active !bg-primary !text-primary-content' : ''}`}
onClick={() => handleConfigClick(cid)}
>
Config {cid}
Expand All @@ -225,7 +225,7 @@ export function ControlDetailSection({ controlData }: ControlDetailSectionProps)
<button
key={v}
role="tab"
className={`tab gap-1 rounded-lg ${selectedConfigVersion === v ? 'tab-active !bg-accent !text-white' : ''}`}
className={`tab gap-1 rounded-lg ${selectedConfigVersion === v ? 'tab-active !bg-primary !text-primary-content' : ''}`}
onClick={() => handleConfigVersionClick(v)}
>
{v}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,25 @@ public class MongoMcpIntegration {
}
""";

private static final String CONTROL_REQUIREMENT_JSON = """
{
"control-id": "mcp-test-control",
"name": "MCP Test Control",
"description": "Integration test control requirement"
}
""";

private static final String CONTROL_CONFIGURATION_JSON = """
{
"control-id": "mcp-test-control",
"value": "enforced",
"environment": "integration-test"
}
""";

private static int createdArchitectureId;
private static int createdDecoratorId;
private static int createdControlId;

@Inject
ArchitectureTools architectureTools;
Expand Down Expand Up @@ -350,4 +367,70 @@ void mcp_validation_rejects_decorator_type_filter_with_newline() {
assertThat(result.isError(), is(true));
assertThat(text(result), containsString("Type filter"));
}

// --- Control Tools (create paths) ---

@Test
@Order(26)
void mcp_create_control_requirement() {
ToolResponse result = controlTools.createControlRequirement(
"security", "MCP Test Control", "Integration test control requirement", CONTROL_REQUIREMENT_JSON);
assertThat(result.isError(), is(false));
assertThat(text(result), containsString("created successfully"));

Matcher matcher = ID_PATTERN.matcher(text(result));
assertThat("Response should contain control ID", matcher.find());
createdControlId = Integer.parseInt(matcher.group(1));
logger.info("Created control with ID: {}", createdControlId);
}

@Test
@Order(27)
void mcp_list_controls_contains_created() {
ToolResponse result = controlTools.listControls("security");
assertThat(result.isError(), is(false));
assertThat(text(result), containsString("MCP Test Control"));
}

@Test
@Order(28)
void mcp_list_control_versions_after_create() {
ToolResponse result = controlTools.listControlVersions("security", createdControlId);
assertThat(result.isError(), is(false));
assertThat(text(result), containsString("1.0.0"));
}

@Test
@Order(29)
void mcp_get_control_after_create() {
ToolResponse result = controlTools.getControl("security", createdControlId, "1.0.0");
assertThat(result.isError(), is(false));
assertThat(text(result), containsString("mcp-test-control"));
}

@Test
@Order(30)
void mcp_create_control_configuration() {
ToolResponse result = controlTools.createControlConfiguration(
"security", createdControlId, CONTROL_CONFIGURATION_JSON);
assertThat(result.isError(), is(false));
assertThat(text(result), containsString("created successfully"));
}

@Test
@Order(31)
void mcp_create_control_configuration_for_missing_control_returns_error() {
ToolResponse result = controlTools.createControlConfiguration(
"security", 99999, CONTROL_CONFIGURATION_JSON);
assertThat(result.isError(), is(true));
assertThat(text(result), containsString("not found"));
}

@Test
@Order(32)
void mcp_create_control_requirement_rejects_invalid_json() {
ToolResponse result = controlTools.createControlRequirement(
"security", "Bad", "desc", "not-json");
assertThat(result.isError(), is(true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,25 @@ public class NitriteMcpIntegration {
}
""";

private static final String CONTROL_REQUIREMENT_JSON = """
{
"control-id": "mcp-nitrite-control",
"name": "MCP Nitrite Control",
"description": "Nitrite integration test control requirement"
}
""";

private static final String CONTROL_CONFIGURATION_JSON = """
{
"control-id": "mcp-nitrite-control",
"value": "enforced",
"environment": "nitrite-test"
}
""";

private static int createdArchitectureId;
private static int createdDecoratorId;
private static int createdControlId;

@Inject
ArchitectureTools architectureTools;
Expand Down Expand Up @@ -310,4 +327,70 @@ void mcp_validation_rejects_decorator_type_filter_with_newline() {
assertThat(result.isError(), is(true));
assertThat(text(result), containsString("Type filter"));
}

// --- Control Tools (create paths) ---

@Test
@Order(26)
void mcp_create_control_requirement() {
ToolResponse result = controlTools.createControlRequirement(
"security", "MCP Nitrite Control", "Nitrite integration test control requirement", CONTROL_REQUIREMENT_JSON);
assertThat(result.isError(), is(false));
assertThat(text(result), containsString("created successfully"));

Matcher matcher = ID_PATTERN.matcher(text(result));
assertThat("Response should contain control ID", matcher.find());
createdControlId = Integer.parseInt(matcher.group(1));
logger.info("Created control with ID: {}", createdControlId);
}

@Test
@Order(27)
void mcp_list_controls_contains_created() {
ToolResponse result = controlTools.listControls("security");
assertThat(result.isError(), is(false));
assertThat(text(result), containsString("MCP Nitrite Control"));
}

@Test
@Order(28)
void mcp_list_control_versions_after_create() {
ToolResponse result = controlTools.listControlVersions("security", createdControlId);
assertThat(result.isError(), is(false));
assertThat(text(result), containsString("1.0.0"));
}

@Test
@Order(29)
void mcp_get_control_after_create() {
ToolResponse result = controlTools.getControl("security", createdControlId, "1.0.0");
assertThat(result.isError(), is(false));
assertThat(text(result), containsString("mcp-nitrite-control"));
}

@Test
@Order(30)
void mcp_create_control_configuration() {
ToolResponse result = controlTools.createControlConfiguration(
"security", createdControlId, CONTROL_CONFIGURATION_JSON);
assertThat(result.isError(), is(false));
assertThat(text(result), containsString("created successfully"));
}

@Test
@Order(31)
void mcp_create_control_configuration_for_missing_control_returns_error() {
ToolResponse result = controlTools.createControlConfiguration(
"security", 99999, CONTROL_CONFIGURATION_JSON);
assertThat(result.isError(), is(true));
assertThat(text(result), containsString("not found"));
}

@Test
@Order(32)
void mcp_create_control_requirement_rejects_invalid_json() {
ToolResponse result = controlTools.createControlRequirement(
"security", "Bad", "desc", "not-json");
assertThat(result.isError(), is(true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public ToolResponse createArchitecture(
Optional<ToolResponse> err = McpValidationHelper.firstError(
() -> McpValidationHelper.checkEnabled(mcpEnabled),
() -> McpValidationHelper.validateNamespace(namespace),
() -> McpValidationHelper.validateMaxLength(name, 200, "Architecture name"),
() -> McpValidationHelper.validateMaxLength(name, McpValidationHelper.MAX_NAME_LENGTH, "Architecture name"),
() -> McpValidationHelper.validateDescriptionLength(description, "Architecture description"),
() -> McpValidationHelper.validateNotBlank(architectureJson, "Architecture JSON"),
() -> McpValidationHelper.validateJson(architectureJson, "Architecture JSON"));
Expand Down
63 changes: 19 additions & 44 deletions calm-hub/src/main/java/org/finos/calm/mcp/tools/ControlTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,30 +127,17 @@ public ToolResponse createControlRequirement(
@ToolArg(description = "The name of the control requirement") String name,
@ToolArg(description = "A description of the control requirement") String description,
@ToolArg(description = "The full control requirement JSON content") String requirementJson) {
String error = McpValidationHelper.checkEnabled(mcpEnabled);
if (error != null) {
return ToolResponse.error(error);
}
error = McpValidationHelper.validateDomain(domain);
if (error != null) {
return ToolResponse.error(error);
}
error = McpValidationHelper.validateNotBlank(name, "Name");
if (error != null) {
return ToolResponse.error(error);
}
error = McpValidationHelper.validateNotBlank(description, "Description");
if (error != null) {
return ToolResponse.error(error);
}
error = McpValidationHelper.validateNotBlank(requirementJson, "Requirement JSON");
if (error != null) {
return ToolResponse.error(error);
}
error = McpValidationHelper.validateJson(requirementJson, "Requirement JSON");
if (error != null) {
return ToolResponse.error(error);
}
Optional<ToolResponse> err = McpValidationHelper.firstError(
() -> McpValidationHelper.checkEnabled(mcpEnabled),
() -> McpValidationHelper.validateDomain(domain),
() -> McpValidationHelper.validateNotBlank(name, "Name"),
() -> McpValidationHelper.validateMaxLength(name, McpValidationHelper.MAX_NAME_LENGTH, "Name"),
() -> McpValidationHelper.validateNotBlank(description, "Description"),
() -> McpValidationHelper.validateDescriptionLength(description, "Description"),
() -> McpValidationHelper.validateNotBlank(requirementJson, "Requirement JSON"),
() -> McpValidationHelper.validateMaxLength(requirementJson, McpValidationHelper.MAX_JSON_PAYLOAD_LENGTH, "Requirement JSON"),
() -> McpValidationHelper.validateJson(requirementJson, "Requirement JSON"));
if (err.isPresent()) return err.get();

try {
CreateControlRequirement request = new CreateControlRequirement(name, description, requirementJson);
Expand All @@ -168,26 +155,14 @@ public ToolResponse createControlConfiguration(
@ToolArg(description = "The domain containing the control (e.g. 'security')") String domain,
@ToolArg(description = "The control ID (positive integer) to create a configuration for") int controlId,
@ToolArg(description = "The full control configuration JSON content") String configurationJson) {
String error = McpValidationHelper.checkEnabled(mcpEnabled);
if (error != null) {
return ToolResponse.error(error);
}
error = McpValidationHelper.validateDomain(domain);
if (error != null) {
return ToolResponse.error(error);
}
error = McpValidationHelper.validatePositiveId(controlId, "Control ID");
if (error != null) {
return ToolResponse.error(error);
}
error = McpValidationHelper.validateNotBlank(configurationJson, "Configuration JSON");
if (error != null) {
return ToolResponse.error(error);
}
error = McpValidationHelper.validateJson(configurationJson, "Configuration JSON");
if (error != null) {
return ToolResponse.error(error);
}
Optional<ToolResponse> err = McpValidationHelper.firstError(
() -> McpValidationHelper.checkEnabled(mcpEnabled),
() -> McpValidationHelper.validateDomain(domain),
() -> McpValidationHelper.validatePositiveId(controlId, "Control ID"),
() -> McpValidationHelper.validateNotBlank(configurationJson, "Configuration JSON"),
() -> McpValidationHelper.validateMaxLength(configurationJson, McpValidationHelper.MAX_JSON_PAYLOAD_LENGTH, "Configuration JSON"),
() -> McpValidationHelper.validateJson(configurationJson, "Configuration JSON"));
if (err.isPresent()) return err.get();

try {
CreateControlConfiguration request = new CreateControlConfiguration(configurationJson);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@ final class McpValidationHelper {
static final String MCP_DISABLED_MESSAGE =
"Error: MCP tools are currently disabled. Set the environment variable CALM_MCP_ENABLED=true to enable.";

static final int MAX_NAME_LENGTH = 200;

private static final int MAX_DESCRIPTION_LENGTH = 1024;

/**
* Default upper bound for JSON payloads accepted by MCP create tools.
* Sized for typical CALM control / decorator / architecture documents
* while preventing an authenticated client from storing unbounded blobs.
*/
static final int MAX_JSON_PAYLOAD_LENGTH = 100_000;

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private McpValidationHelper() {
Expand Down
Loading
Loading