From 310997c3aea270d47b2bec90781daa76e1502cff Mon Sep 17 00:00:00 2001 From: Andrii Arsenin Date: Tue, 14 Apr 2026 14:45:55 +0300 Subject: [PATCH] feat(mcp): add credential and all-runs tools; replace deprecated z.string().uuid() - add list_scenario_credentials, add_scenario_credential, remove_scenario_credential tools - add list_all_runs tool (global across all scenarios) - replace all deprecated z.string().uuid() with z.uuid() throughout mcp.service --- server/src/mcp/mcp.service.ts | 181 +++++++++++++++++++++++++++++----- 1 file changed, 156 insertions(+), 25 deletions(-) diff --git a/server/src/mcp/mcp.service.ts b/server/src/mcp/mcp.service.ts index 96720fc..0934b86 100644 --- a/server/src/mcp/mcp.service.ts +++ b/server/src/mcp/mcp.service.ts @@ -86,7 +86,7 @@ export class McpService { { description: "Delete a session by numeric ID (closes it first if open)", inputSchema: { - id: z.string().uuid().describe("Session ID to delete"), + id: z.uuid().describe("Session ID to delete"), }, }, async ({ id }) => { @@ -151,7 +151,7 @@ export class McpService { { description: "Get an environment record by ID", inputSchema: { - id: z.string().uuid().describe("Environment ID"), + id: z.uuid().describe("Environment ID"), }, }, async ({ id }) => { @@ -207,7 +207,7 @@ export class McpService { { description: "Update an existing environment (name and/or data)", inputSchema: { - id: z.string().uuid().describe("Environment ID to update"), + id: z.uuid().describe("Environment ID to update"), name: z.string().optional().describe("New name"), data: z .record(z.string(), z.string()) @@ -238,7 +238,7 @@ export class McpService { { description: "Delete an environment by ID", inputSchema: { - id: z.string().uuid().describe("Environment ID to delete"), + id: z.uuid().describe("Environment ID to delete"), }, }, async ({ id }) => { @@ -392,7 +392,7 @@ export class McpService { { description: "Get a scenario with its steps by ID", inputSchema: { - id: z.string().uuid().describe("Scenario ID"), + id: z.uuid().describe("Scenario ID"), }, }, async ({ id }) => { @@ -442,7 +442,7 @@ export class McpService { { description: "Update a scenario name", inputSchema: { - id: z.string().uuid().describe("Scenario ID"), + id: z.uuid().describe("Scenario ID"), name: z.string().optional().describe("New name"), }, }, @@ -468,7 +468,7 @@ export class McpService { { description: "Delete a scenario by ID", inputSchema: { - id: z.string().uuid().describe("Scenario ID"), + id: z.uuid().describe("Scenario ID"), }, }, async ({ id }) => { @@ -493,7 +493,7 @@ export class McpService { { description: "Add a step to a scenario", inputSchema: { - scenarioId: z.string().uuid().describe("Parent scenario ID"), + scenarioId: z.uuid().describe("Parent scenario ID"), order: z .number() .int() @@ -527,8 +527,8 @@ export class McpService { { description: "Get a single step of a scenario", inputSchema: { - scenarioId: z.string().uuid().describe("Scenario ID"), - stepId: z.string().uuid().describe("Step ID"), + scenarioId: z.uuid().describe("Scenario ID"), + stepId: z.uuid().describe("Step ID"), }, }, async ({ scenarioId, stepId }) => { @@ -551,8 +551,8 @@ export class McpService { { description: "Update a step within a scenario", inputSchema: { - scenarioId: z.string().uuid().describe("Scenario ID"), - stepId: z.string().uuid().describe("Step ID"), + scenarioId: z.uuid().describe("Scenario ID"), + stepId: z.uuid().describe("Step ID"), order: z .number() .int() @@ -591,8 +591,8 @@ export class McpService { { description: "Delete a step from a scenario", inputSchema: { - scenarioId: z.string().uuid().describe("Scenario ID"), - stepId: z.string().uuid().describe("Step ID"), + scenarioId: z.uuid().describe("Scenario ID"), + stepId: z.uuid().describe("Step ID"), }, }, async ({ scenarioId, stepId }) => { @@ -621,7 +621,7 @@ export class McpService { description: "List runs for a scenario (paginated, optionally filtered by status)", inputSchema: { - scenarioId: z.string().uuid().describe("Scenario ID"), + scenarioId: z.uuid().describe("Scenario ID"), status: z .enum(["pending", "in_progress", "pass", "fail"]) .optional() @@ -664,11 +664,8 @@ export class McpService { { description: "Trigger an immediate run of a scenario by ID", inputSchema: { - id: z.string().uuid().describe("Scenario ID to run"), - environmentId: z - .string() - .uuid() - .describe("Environment ID to run the scenario in"), + id: z.uuid().describe("Scenario ID to run"), + environmentId: z.uuid().describe("Environment ID to run the scenario in"), }, }, async ({ id, environmentId }) => { @@ -692,8 +689,8 @@ export class McpService { description: "Get a specific scenario run with all step runs and their outputs", inputSchema: { - scenarioId: z.string().uuid().describe("Scenario ID"), - runId: z.string().uuid().describe("Run ID"), + scenarioId: z.uuid().describe("Scenario ID"), + runId: z.uuid().describe("Run ID"), }, }, async ({ scenarioId, runId }) => { @@ -717,8 +714,8 @@ export class McpService { description: "Block until a scenario run reaches pass or fail (max 5 min), then return the full run with step outputs", inputSchema: { - scenarioId: z.string().uuid().describe("Scenario ID"), - runId: z.string().uuid().describe("Run ID"), + scenarioId: z.uuid().describe("Scenario ID"), + runId: z.uuid().describe("Run ID"), }, }, async ({ scenarioId, runId }) => { @@ -736,13 +733,147 @@ export class McpService { }, ); + server.registerTool( + "list_scenario_credentials", + { + description: "List credentials assigned to a scenario", + inputSchema: { + scenarioId: z.uuid().describe("Scenario ID"), + }, + }, + async ({ scenarioId }) => { + try { + const creds = + await this.scenarioService.findScenarioCredentials(scenarioId); + return { + content: [{ type: "text" as const, text: JSON.stringify(creds) }], + }; + } catch (err) { + return { + isError: true, + content: [{ type: "text" as const, text: (err as Error).message }], + }; + } + }, + ); + + server.registerTool( + "add_scenario_credential", + { + description: + "Add a credential to a scenario with an alias (used in snippets)", + inputSchema: { + scenarioId: z.uuid().describe("Scenario ID"), + credentialId: z.uuid().describe("Credential ID to associate"), + alias: z + .string() + .describe( + "Alias used to reference this credential in step code, e.g. pkcs_key", + ), + }, + }, + async ({ scenarioId, credentialId, alias }) => { + try { + const result = await this.scenarioService.addScenarioCredential( + scenarioId, + { credentialId, alias }, + ); + return { + content: [ + { type: "text" as const, text: JSON.stringify(result) }, + ], + }; + } catch (err) { + return { + isError: true, + content: [{ type: "text" as const, text: (err as Error).message }], + }; + } + }, + ); + + server.registerTool( + "remove_scenario_credential", + { + description: "Remove a credential assignment from a scenario", + inputSchema: { + scenarioId: z.uuid().describe("Scenario ID"), + scenarioCredentialId: z.uuid().describe("Scenario-credential assignment ID (not the credential ID)"), + }, + }, + async ({ scenarioId, scenarioCredentialId }) => { + try { + await this.scenarioService.removeScenarioCredential( + scenarioId, + scenarioCredentialId, + ); + return { + content: [ + { + type: "text" as const, + text: `Credential assignment ${scenarioCredentialId} removed from scenario ${scenarioId}`, + }, + ], + }; + } catch (err) { + return { + isError: true, + content: [{ type: "text" as const, text: (err as Error).message }], + }; + } + }, + ); + + server.registerTool( + "list_all_runs", + { + description: + "List runs across all scenarios (paginated, filterable by status)", + inputSchema: { + status: z + .enum(["pending", "in_progress", "pass", "fail"]) + .optional() + .describe("Filter by run status"), + page: z + .number() + .int() + .min(1) + .optional() + .describe("Page number (default 1)"), + limit: z + .number() + .int() + .min(1) + .optional() + .describe("Items per page (default 20)"), + }, + }, + async ({ status, page, limit }) => { + try { + const result = await this.scenarioService.findAllRuns({ + status, + page, + limit, + }); + return { + content: [{ type: "text" as const, text: JSON.stringify(result) }], + }; + } catch (err) { + return { + isError: true, + content: [{ type: "text" as const, text: (err as Error).message }], + }; + } + }, + ); + server.registerTool( "export_scenario", { description: "Export a scenario as a portable JSON payload (name + steps)", inputSchema: { - id: z.string().uuid().describe("Scenario ID to export"), + id: z.uuid().describe("Scenario ID to export"), }, }, async ({ id }) => {