From 0b16a59ead4896926fb50498f2fa721119b26cd0 Mon Sep 17 00:00:00 2001 From: betterclever Date: Thu, 29 Jan 2026 10:55:40 +0530 Subject: [PATCH 1/5] feat: add stdio MCP proxy and cleanup Signed-off-by: betterclever --- backend/src/mcp/internal-mcp.controller.ts | 6 ++ docker/mcp-stdio-proxy/Dockerfile | 13 +++ docker/mcp-stdio-proxy/README.md | 31 +++++++ docker/mcp-stdio-proxy/package.json | 14 +++ docker/mcp-stdio-proxy/server.mjs | 89 +++++++++++++++++++ worker/src/components/core/mcp-server.ts | 44 ++++++++- .../src/temporal/activities/mcp.activity.ts | 31 +++++++ worker/src/temporal/types.ts | 5 ++ worker/src/temporal/workers/dev.worker.ts | 3 + worker/src/temporal/workflows/index.ts | 6 ++ 10 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 docker/mcp-stdio-proxy/Dockerfile create mode 100644 docker/mcp-stdio-proxy/README.md create mode 100644 docker/mcp-stdio-proxy/package.json create mode 100644 docker/mcp-stdio-proxy/server.mjs diff --git a/backend/src/mcp/internal-mcp.controller.ts b/backend/src/mcp/internal-mcp.controller.ts index 5eb28c82..3bbf0e0d 100644 --- a/backend/src/mcp/internal-mcp.controller.ts +++ b/backend/src/mcp/internal-mcp.controller.ts @@ -50,4 +50,10 @@ export class InternalMcpController { await this.toolRegistry.registerLocalMcp(body); return { success: true }; } + + @Post('cleanup') + async cleanupRun(@Body() body: { runId: string }) { + const containerIds = await this.toolRegistry.cleanupRun(body.runId); + return { containerIds }; + } } diff --git a/docker/mcp-stdio-proxy/Dockerfile b/docker/mcp-stdio-proxy/Dockerfile new file mode 100644 index 00000000..fd2bf783 --- /dev/null +++ b/docker/mcp-stdio-proxy/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json ./ +RUN npm install --omit=dev + +COPY server.mjs ./ + +ENV PORT=8080 +EXPOSE 8080 + +CMD ["node", "server.mjs"] diff --git a/docker/mcp-stdio-proxy/README.md b/docker/mcp-stdio-proxy/README.md new file mode 100644 index 00000000..9785829b --- /dev/null +++ b/docker/mcp-stdio-proxy/README.md @@ -0,0 +1,31 @@ +# MCP Stdio Proxy + +This image wraps a stdio-based MCP server and exposes it over Streamable HTTP. + +## Build + +```bash +docker build -t shipsec/mcp-stdio-proxy:latest docker/mcp-stdio-proxy +``` + +## Run + +```bash +docker run --rm -p 8080:8080 \ + -e MCP_COMMAND=uvx \ + -e MCP_ARGS='["awslabs-cloudwatch-mcp-server"]' \ + shipsec/mcp-stdio-proxy:latest +``` + +The proxy will expose MCP on `http://localhost:8080/mcp` and a basic health endpoint at `/health`. + +## Environment + +- `MCP_COMMAND` (required): Command to launch the stdio MCP server. +- `MCP_ARGS` (optional): JSON array or space-delimited list of arguments. +- `PORT` / `MCP_PORT` (optional): Port for the HTTP server (default: 8080). + +## Notes + +- The proxy lists tools once at startup and registers them. Restart the container if tools change. +- Make sure the stdio server binary is present in the image. For third-party tools, build a derived image that installs them. diff --git a/docker/mcp-stdio-proxy/package.json b/docker/mcp-stdio-proxy/package.json new file mode 100644 index 00000000..e766511d --- /dev/null +++ b/docker/mcp-stdio-proxy/package.json @@ -0,0 +1,14 @@ +{ + "name": "mcp-stdio-proxy", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "HTTP proxy for stdio-based MCP servers", + "scripts": { + "start": "node server.mjs" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.3", + "express": "^5.2.1" + } +} diff --git a/docker/mcp-stdio-proxy/server.mjs b/docker/mcp-stdio-proxy/server.mjs new file mode 100644 index 00000000..515374c9 --- /dev/null +++ b/docker/mcp-stdio-proxy/server.mjs @@ -0,0 +1,89 @@ +import express from 'express'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +function parseArgs(raw) { + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) return parsed.map(String); + } catch { + // fall through + } + return raw + .split(' ') + .map((entry) => entry.trim()) + .filter(Boolean); +} + +const command = process.env.MCP_COMMAND; +const args = parseArgs(process.env.MCP_ARGS || ''); +const port = Number.parseInt(process.env.PORT || process.env.MCP_PORT || '8080', 10); + +if (!command) { + console.error('MCP_COMMAND is required to start the stdio MCP server.'); + process.exit(1); +} + +const client = new Client({ name: 'shipsec-mcp-stdio-proxy', version: '1.0.0' }); +const clientTransport = new StdioClientTransport({ + command, + args, +}); + +await client.connect(clientTransport); + +const toolsResponse = await client.listTools(); +const tools = toolsResponse.tools ?? []; + +const server = new McpServer({ + name: 'shipsec-mcp-stdio-proxy', + version: '1.0.0', +}); + +for (const tool of tools) { + server.registerTool( + tool.name, + { + description: tool.description, + inputSchema: tool.inputSchema, + }, + async (toolArgs) => { + return client.callTool({ + name: tool.name, + arguments: toolArgs ?? {}, + }); + }, + ); +} + +const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => 'stdio-proxy', + enableJsonResponse: true, +}); +await server.connect(transport); + +const app = express(); +app.use(express.json({ limit: '2mb' })); + +app.all('/mcp', async (req, res) => { + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('Failed to handle MCP request', error); + if (!res.headersSent) { + res.status(500).send('MCP proxy error'); + } + } +}); + +app.get('/health', (_req, res) => { + res.json({ status: 'ok', toolCount: tools.length }); +}); + +app.listen(port, '0.0.0.0', () => { + console.log(`MCP stdio proxy listening on http://0.0.0.0:${port}/mcp`); + console.log(`Proxied MCP command: ${command} ${args.join(' ')}`); +}); diff --git a/worker/src/components/core/mcp-server.ts b/worker/src/components/core/mcp-server.ts index 23e850d5..bf318b9c 100644 --- a/worker/src/components/core/mcp-server.ts +++ b/worker/src/components/core/mcp-server.ts @@ -21,11 +21,35 @@ const outputSchema = outputs({ }); const parameterSchema = parameters({ + mode: param(z.enum(['http', 'stdio']).default('http').describe('How to launch the MCP server.'), { + label: 'Mode', + editor: 'select', + options: [ + { label: 'HTTP Server', value: 'http' }, + { label: 'Stdio Server (Proxy)', value: 'stdio' }, + ], + description: 'HTTP starts a native MCP HTTP server. Stdio starts a proxy container.', + }), image: param(z.string().describe('Docker image for the MCP server'), { label: 'Docker Image', editor: 'text', - placeholder: 'mcp/myserver:latest', + placeholder: 'shipsec/mcp-stdio-proxy:latest', }), + stdioCommand: param( + z.string().optional().describe('Stdio MCP command to run inside the proxy container.'), + { + label: 'Stdio Command', + editor: 'text', + placeholder: 'uvx', + }, + ), + stdioArgs: param( + z.array(z.string()).default([]).describe('Arguments for the stdio MCP command.'), + { + label: 'Stdio Args', + editor: 'variable-list', + }, + ), command: param(z.array(z.string()).default([]).describe('Entrypoint command'), { label: 'Command', editor: 'variable-list', @@ -60,7 +84,7 @@ const definition = defineComponent({ inputs: inputSchema, outputs: outputSchema, parameters: parameterSchema, - docs: 'Starts an external MCP server in a Docker container and registers it as a tool source.', + docs: 'Starts an MCP server in a Docker container and registers it as a tool source. Use stdio mode with the MCP stdio proxy image to wrap CLI-based MCP servers.', ui: { slug: 'mcp-server', version: '1.0.0', @@ -79,14 +103,26 @@ const definition = defineComponent({ const serverPort = params.port || 8080; // Determine the port once if (params.image) { + const isStdioMode = params.mode === 'stdio'; // Manually construct runner config to resolve parameters, // as `this.runner` is not interpolated when used directly in `execute`. const runnerConfig = { kind: 'docker' as const, // Explicitly type as 'docker' literal image: params.image, // Combine command and args into a single array for the Docker command - command: [...(params.command || []), ...(params.args || [])], - env: params.env, + command: isStdioMode + ? [] + : [...(params.command || []), ...(params.args || [])], + env: { + ...params.env, + ...(isStdioMode + ? { + MCP_COMMAND: params.stdioCommand ?? '', + MCP_ARGS: JSON.stringify(params.stdioArgs ?? []), + MCP_PORT: String(serverPort), + } + : {}), + }, detached: true, // Map the internal server port to the same host port ports: { [serverPort]: serverPort }, diff --git a/worker/src/temporal/activities/mcp.activity.ts b/worker/src/temporal/activities/mcp.activity.ts index 2ef9aeea..483dd743 100644 --- a/worker/src/temporal/activities/mcp.activity.ts +++ b/worker/src/temporal/activities/mcp.activity.ts @@ -6,6 +6,7 @@ import { ServiceError, } from '@shipsec/component-sdk'; import { + CleanupLocalMcpActivityInput, RegisterComponentToolActivityInput, RegisterLocalMcpActivityInput, RegisterRemoteMcpActivityInput, @@ -80,6 +81,36 @@ export async function registerLocalMcpActivity( }); } +export async function cleanupLocalMcpActivity( + input: CleanupLocalMcpActivityInput, +): Promise { + const response = await callInternalApi('cleanup', { runId: input.runId }); + const containerIds = Array.isArray(response?.containerIds) ? response.containerIds : []; + + if (containerIds.length === 0) { + return; + } + + const { exec } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const execAsync = promisify(exec); + + await Promise.all( + containerIds.map(async (containerId: string) => { + if (!containerId || typeof containerId !== 'string') return; + if (!/^[a-zA-Z0-9_.-]+$/.test(containerId)) { + console.warn(`[MCP Cleanup] Skipping container with unsafe id: ${containerId}`); + return; + } + try { + await execAsync(`docker rm -f ${containerId}`); + } catch (error) { + console.warn(`[MCP Cleanup] Failed to remove container ${containerId}:`, error); + } + }), + ); +} + export async function prepareAndRegisterToolActivity(input: { runId: string; nodeId: string; diff --git a/worker/src/temporal/types.ts b/worker/src/temporal/types.ts index 0cdfaab0..e7839161 100644 --- a/worker/src/temporal/types.ts +++ b/worker/src/temporal/types.ts @@ -218,6 +218,7 @@ export interface RegisterLocalMcpActivityInput { containerId: string; } + export interface PrepareAndRegisterToolActivityInput { runId: string; nodeId: string; @@ -225,3 +226,7 @@ export interface PrepareAndRegisterToolActivityInput { inputs: Record; params: Record; } + +export interface CleanupLocalMcpActivityInput { + runId: string; +} diff --git a/worker/src/temporal/workers/dev.worker.ts b/worker/src/temporal/workers/dev.worker.ts index 5fda2905..88b0ff3e 100644 --- a/worker/src/temporal/workers/dev.worker.ts +++ b/worker/src/temporal/workers/dev.worker.ts @@ -29,6 +29,7 @@ import { registerComponentToolActivity, registerLocalMcpActivity, registerRemoteMcpActivity, + cleanupLocalMcpActivity, prepareAndRegisterToolActivity, } from '../activities/mcp.activity'; @@ -221,6 +222,7 @@ async function main() { registerComponentToolActivity, registerLocalMcpActivity, registerRemoteMcpActivity, + cleanupLocalMcpActivity, }).join(', ')}`, ); @@ -256,6 +258,7 @@ async function main() { registerComponentToolActivity, registerLocalMcpActivity, registerRemoteMcpActivity, + cleanupLocalMcpActivity, prepareAndRegisterToolActivity, }, bundlerOptions: { diff --git a/worker/src/temporal/workflows/index.ts b/worker/src/temporal/workflows/index.ts index ad647387..8b366b6c 100644 --- a/worker/src/temporal/workflows/index.ts +++ b/worker/src/temporal/workflows/index.ts @@ -28,6 +28,7 @@ import type { WorkflowAction, PrepareRunPayloadActivityInput, RegisterComponentToolActivityInput, + CleanupLocalMcpActivityInput, RegisterLocalMcpActivityInput, RegisterRemoteMcpActivityInput, PrepareAndRegisterToolActivityInput, @@ -41,6 +42,7 @@ const { expireHumanInputRequestActivity, registerLocalMcpActivity, registerRemoteMcpActivity, + cleanupLocalMcpActivity, prepareAndRegisterToolActivity, } = proxyActivities<{ runComponentActivity(input: RunComponentActivityInput): Promise; @@ -70,6 +72,7 @@ const { registerComponentToolActivity(input: RegisterComponentToolActivityInput): Promise; registerLocalMcpActivity(input: RegisterLocalMcpActivityInput): Promise; registerRemoteMcpActivity(input: RegisterRemoteMcpActivityInput): Promise; + cleanupLocalMcpActivity(input: CleanupLocalMcpActivityInput): Promise; prepareAndRegisterToolActivity(input: PrepareAndRegisterToolActivityInput): Promise; }>({ startToCloseTimeout: '10 minutes', @@ -886,6 +889,9 @@ export async function shipsecWorkflowRun( [{ outputs, error: normalizedError.message }], ); } finally { + await cleanupLocalMcpActivity({ runId: input.runId }).catch((err) => { + console.error(`[Workflow] Failed to cleanup MCP containers for run ${input.runId}`, err); + }); await finalizeRunActivity({ runId: input.runId }).catch((err) => { console.error(`[Workflow] Failed to finalize run ${input.runId}`, err); }); From 55844dea27b2749856c389154f67489965fa1ab9 Mon Sep 17 00:00:00 2001 From: betterclever Date: Thu, 29 Jan 2026 11:25:10 +0530 Subject: [PATCH 2/5] feat: add AWS MCP server components Signed-off-by: betterclever --- .ai/PLAN.md | 201 +++++++++++------- docker/mcp-aws-cloudtrail/Dockerfile | 8 + docker/mcp-aws-cloudtrail/README.md | 22 ++ docker/mcp-aws-cloudwatch/Dockerfile | 8 + docker/mcp-aws-cloudwatch/README.md | 22 ++ .../src/components/workflow/WorkflowNode.tsx | 40 +++- worker/src/components/core/mcp-runtime.ts | 54 +++++ worker/src/components/core/mcp-server.ts | 76 +++---- worker/src/components/index.ts | 2 + .../components/security/aws-cloudtrail-mcp.ts | 123 +++++++++++ .../components/security/aws-cloudwatch-mcp.ts | 123 +++++++++++ worker/src/temporal/workflows/index.ts | 82 ++++--- 12 files changed, 607 insertions(+), 154 deletions(-) create mode 100644 docker/mcp-aws-cloudtrail/Dockerfile create mode 100644 docker/mcp-aws-cloudtrail/README.md create mode 100644 docker/mcp-aws-cloudwatch/Dockerfile create mode 100644 docker/mcp-aws-cloudwatch/README.md create mode 100644 worker/src/components/core/mcp-runtime.ts create mode 100644 worker/src/components/security/aws-cloudtrail-mcp.ts create mode 100644 worker/src/components/security/aws-cloudwatch-mcp.ts diff --git a/.ai/PLAN.md b/.ai/PLAN.md index 3896183b..c0a703db 100644 --- a/.ai/PLAN.md +++ b/.ai/PLAN.md @@ -1,82 +1,119 @@ -# ENG-101: Frontend: Tool Mode & Agent Node UI Implementation Plan - -## Overview -This plan details the frontend implementation for supporting Agentic workflows, including a Tool Mode toggle for nodes, an MCP Server node type, and enhancements to the Run Timeline to visualize agent execution and reasoning. - -## User Review Required -> [!IMPORTANT] -> - Confirm if `core.mcp.server` component definition exists in the backend or if it needs to be mocked/created in frontend for now. -> - Clarify if "MCP Server node type in palette" implies a specific UI for browsing a catalog (e.g., distinct from normal list). We will implement it as a distinct category in the existing Sidebar for now. - -## Proposed Changes - -### 1. Tool Mode Toggle -**Goal**: Allow users to toggle nodes into "Tool Mode", changing their visual representation and port exposure. - -#### [MODIFY] [WorkflowNode.tsx](file:///Users/betterclever/shipsec/shipsec-studio/frontend/src/components/workflow/WorkflowNode.tsx) -- Add a "Tool Mode" toggle button to the node header (visible for agent-callable nodes). -- **State**: Track `isToolMode` state (likely in `node.data.config.isToolMode` or similar). -- **Rendering**: - - When `isToolMode` is active: - - Show "Exposed" inputs/outputs (the ones the Agent sees). - - Hide internal wiring ports not relevant to the Agent? Or show them differently? - - Apply a distinct visual style (e.g., "Tool" badge, different border color). - - **Visual Distinction**: "Visual distinction for tool calls vs normal nodes". - - Add a "Tool" icon/badge. - - Change border style (e.g., dashed vs solid, or a specific color like purple/indigo for tools). - -### 2. MCP Server Node & Palette -**Goal**: Add MCP Server nodes to the palette with catalog selection. - -#### [MODIFY] [Sidebar.tsx](file:///Users/betterclever/shipsec/shipsec-studio/frontend/src/components/layout/Sidebar.tsx) -- Add `mcp_server` to `categoryOrder` and `categoryColors`. -- Ensure MCP Server components are correctly categorized and displayed. -- **Catalog Selection**: - - If a specific Catalog UI is needed, we might need a "Add from Catalog" button in the `mcp_server` section or a separate view. - - *Plan*: Integrate into the existing accordion list for now, ensuring `mcp_server` category is prominent. - -#### [NEW] [MCPServerNode.tsx] (Optional) -- If MCP Server nodes require special rendering (e.g., connection status to external server), create a custom node type. -- *Default*: Use `WorkflowNode` but with "MCP" styling. - -### 3. Agent Node & Tools Port -**Goal**: Enhance the Agent Node UI to support multi-connection tools port. - -#### [MODIFY] [WorkflowNode.tsx](file:///Users/betterclever/shipsec/shipsec-studio/frontend/src/components/workflow/WorkflowNode.tsx) -- **Tools Port**: - - Ensure the `tools` input port (anchor) handles multiple connections visually. - - ReactFlow `Handle` supports `isConnectable={true}` (default). - - **Visual**: Style the "Tools" port distinctly (e.g., different shape or color) to indicate it's a "Tool Collection" port. - -### 4. Run Timeline Enhancements -**Goal**: Visualize agent execution, reasoning, and tool calls. - -#### [MODIFY] [ExecutionTimeline.tsx](file:///Users/betterclever/shipsec/shipsec-studio/frontend/src/components/timeline/ExecutionTimeline.tsx) -- **Expandable Tool Calls**: - - Show Agent events (steps) on the timeline track. - - Allow clicking an Agent event to expand/focus it. - - `agentMarkers` seem implemented. Ensure they are fully wired up to show sub-steps. - -#### [MODIFY] [AgentTracePanel.tsx](file:///Users/betterclever/shipsec/shipsec-studio/frontend/src/components/timeline/AgentTracePanel.tsx) -- **Thinking/Reasoning**: - - Ensure `step.thought` is displayed prominently (it is currently `ExpandableText`). - - **Tool Calls**: - - `AgentStepCard` shows tool calls. Improve visual distinction. - - Add "Thinking" section (e.g., "Agent is thinking..." animation or collapsible "Reasoning" block). - -## Verification Plan - -### Manual Verification -1. **Tool Mode**: - - Drag a component (e.g., "Recursive Web Scraper") to canvas. - - Toggle "Tool Mode". Verify visual change (border/badge) and port changes. - - Connect it to an Agent node's "Tools" port. -2. **MCP Server**: - - Check Palette for "MCP Servers" category. - - Drag an MCP Server node to canvas. -3. **Run Timeline**: - - Run a workflow with an Agent. - - Open "Execution" view. - - Check Timeline for Agent markers. - - Click Agent node -> Check "Agent Trace" panel. - - Verify "Thinking" and "Tool Calls" are shown clearly. +# MCP AWS Servers Plan (CloudTrail + CloudWatch) + +Goal: Make AWS CloudTrail + CloudWatch MCP servers usable locally via OpenCode and the MCP Gateway, with a standardized MCP-node lifecycle (start → register → cleanup), and UI support through tool-mode nodes. + +This plan is split into commit-sized phases. No code changes beyond the plan should happen until you approve. + +--- + +## Constraints and Principles +- MCP server nodes are tool-mode only. +- MCP servers are long-lived per workflow run (start once, expose tools, cleanup on finalize). +- All MCP servers exposed to agents must be HTTP (stdio is wrapped with proxy). +- AWS credentials are provided via a credential bundle input (or equivalent). + +--- + +## Commit Plan + +### Commit 1 — MCP lifecycle helper (worker) +Scope: Standardize MCP node lifecycle with shared helper utilities. +- Create a shared helper (e.g., `worker/src/components/core/mcp-runtime.ts`): + - `runMcpServerInToolMode(...)` to start container, inject env, return endpoint/containerId. + - `registerMcpServer(...)` wrapper (call `registerLocalMcpActivity`). + - `assertToolModeOnly(context)` guard. +- Update `core.mcp.server` to call the helper and enforce tool-mode. +- Keep current stdio proxy support (already added) as a supported mode. + +Artifacts: +- Helper module added. +- `core.mcp.server` uses helper and rejects non-tool mode. + +--- + +### Commit 2 — AWS MCP proxy images (docker) +Scope: Build derived images that include AWS MCP stdio servers. +- Add `docker/mcp-aws-cloudtrail/`: + - Dockerfile builds from `shipsec/mcp-stdio-proxy` and installs CloudTrail MCP (e.g., `uvx awslabs-cloudtrail-mcp-server`). +- Add `docker/mcp-aws-cloudwatch/`: + - Dockerfile builds from same base and installs CloudWatch MCP. +- Document build commands and expected env vars. + +Artifacts: +- New docker folders + README for each image. + +--- + +### Commit 3 — AWS MCP components (tool-mode only) +Scope: Add two components that encapsulate AWS MCP lifecycle. +- `worker/src/components/security/aws-cloudtrail-mcp.ts` +- `worker/src/components/security/aws-cloudwatch-mcp.ts` + +Each component: +- Runner: docker, image points to the new AWS proxy image. +- Parameters: + - `region` + - `mcpPort` (default 8080) + - optional `extraArgs` (array) +- Inputs: + - `awsCredentials` (credential bundle: accessKeyId, secretAccessKey, sessionToken) +- Execute: + - Assert tool-mode only. + - Start container with env: AWS creds + region + MCP_COMMAND/MCP_ARGS. + - Register with gateway via activity. + - Output: endpoint + containerId (for debug) + +Artifacts: +- New components registered in index. +- Optional unit tests for components (basic config validation). + +--- + +### Commit 4 — UI tool-mode safeguards + AWS MCP visibility +Scope: UI clarity and guardrails. +- Add “tool-mode only” badge or warning for MCP server components. +- Update component metadata / docs to reflect tool-mode only behavior. +- Ensure AWS MCP components appear in the palette under `security` (or a new `mcp` category if desired). + +Artifacts: +- UI hints in config panel / node rendering. + +--- + +## Lint & Typecheck Instructions +Run after each commit (or at least after Commit 4): + +```bash +bun --cwd worker run lint +bun --cwd worker run typecheck +bun --cwd backend run lint +bun --cwd backend run typecheck +bun --cwd frontend run lint +bun --cwd frontend run typecheck +``` + +If only worker changes are made in a commit, it is acceptable to run just the worker checks for that commit and the full set at the end. + +--- + +## Manual Validation (you will run) +1. Build the AWS proxy images. +2. In UI, add AWS CloudTrail MCP node and set tool mode. +3. Connect to OpenCode tools port. +4. Run workflow and verify tools show up in the gateway. +5. Confirm containers stop after workflow completes. + +--- + +## Open Questions to Resolve Before Coding +- Should AWS MCP components live under `security` or a new `mcp` category? +- Do we want a single generic “AWS MCP” component with a server selector, or separate components? +- What is the final “credential bundle” schema in the UI? + +--- + +## Implementation Order (after plan approval) +1) Commit 1 +2) Commit 2 +3) Commit 3 +4) Commit 4 diff --git a/docker/mcp-aws-cloudtrail/Dockerfile b/docker/mcp-aws-cloudtrail/Dockerfile new file mode 100644 index 00000000..860a858f --- /dev/null +++ b/docker/mcp-aws-cloudtrail/Dockerfile @@ -0,0 +1,8 @@ +ARG BASE_IMAGE=shipsec/mcp-stdio-proxy:latest +FROM ${BASE_IMAGE} + +RUN apk add --no-cache python3 py3-pip \ + && pip install --no-cache-dir uv + +ENV MCP_COMMAND=uvx +ENV MCP_ARGS='["awslabs-cloudtrail-mcp-server"]' diff --git a/docker/mcp-aws-cloudtrail/README.md b/docker/mcp-aws-cloudtrail/README.md new file mode 100644 index 00000000..7828a5d6 --- /dev/null +++ b/docker/mcp-aws-cloudtrail/README.md @@ -0,0 +1,22 @@ +# AWS CloudTrail MCP Proxy Image + +This image extends the MCP stdio proxy and installs the CloudTrail MCP server. + +## Build + +```bash +docker build -t shipsec/mcp-aws-cloudtrail:latest docker/mcp-aws-cloudtrail +``` + +## Run (example) + +```bash +docker run --rm -p 8080:8080 \ + -e AWS_ACCESS_KEY_ID=... \ + -e AWS_SECRET_ACCESS_KEY=... \ + -e AWS_SESSION_TOKEN=... \ + -e AWS_REGION=us-east-1 \ + shipsec/mcp-aws-cloudtrail:latest +``` + +The proxy exposes MCP on `http://localhost:8080/mcp`. diff --git a/docker/mcp-aws-cloudwatch/Dockerfile b/docker/mcp-aws-cloudwatch/Dockerfile new file mode 100644 index 00000000..baab5b7e --- /dev/null +++ b/docker/mcp-aws-cloudwatch/Dockerfile @@ -0,0 +1,8 @@ +ARG BASE_IMAGE=shipsec/mcp-stdio-proxy:latest +FROM ${BASE_IMAGE} + +RUN apk add --no-cache python3 py3-pip \ + && pip install --no-cache-dir uv + +ENV MCP_COMMAND=uvx +ENV MCP_ARGS='["awslabs-cloudwatch-mcp-server"]' diff --git a/docker/mcp-aws-cloudwatch/README.md b/docker/mcp-aws-cloudwatch/README.md new file mode 100644 index 00000000..4b8f57d0 --- /dev/null +++ b/docker/mcp-aws-cloudwatch/README.md @@ -0,0 +1,22 @@ +# AWS CloudWatch MCP Proxy Image + +This image extends the MCP stdio proxy and installs the CloudWatch MCP server. + +## Build + +```bash +docker build -t shipsec/mcp-aws-cloudwatch:latest docker/mcp-aws-cloudwatch +``` + +## Run (example) + +```bash +docker run --rm -p 8080:8080 \ + -e AWS_ACCESS_KEY_ID=... \ + -e AWS_SECRET_ACCESS_KEY=... \ + -e AWS_SESSION_TOKEN=... \ + -e AWS_REGION=us-east-1 \ + shipsec/mcp-aws-cloudwatch:latest +``` + +The proxy exposes MCP on `http://localhost:8080/mcp`. diff --git a/frontend/src/components/workflow/WorkflowNode.tsx b/frontend/src/components/workflow/WorkflowNode.tsx index 45e9a198..4cb8ebdb 100644 --- a/frontend/src/components/workflow/WorkflowNode.tsx +++ b/frontend/src/components/workflow/WorkflowNode.tsx @@ -446,6 +446,11 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => { const MAX_TEXT_HEIGHT = 1200; const DEFAULT_TEXT_WIDTH = 320; const DEFAULT_TEXT_HEIGHT = 300; + const TOOL_MODE_ONLY_COMPONENTS = new Set([ + 'core.mcp.server', + 'security.aws-cloudtrail-mcp', + 'security.aws-cloudwatch-mcp', + ]); const [textSize, setTextSize] = useState<{ width: number; height: number }>(() => { const uiSize = (data as any)?.ui?.size as { width?: number; height?: number } | undefined; return { @@ -488,6 +493,7 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => { const component = getComponent(componentRef); const isTextBlock = component?.id === 'core.ui.text'; const isEntryPoint = component?.id === 'core.workflow.entrypoint'; + const isToolModeOnly = component?.id ? TOOL_MODE_ONLY_COMPONENTS.has(component.id) : false; // Detect dark mode using theme store (reacts to theme changes) const theme = useThemeStore((state) => state.theme); @@ -517,8 +523,32 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => { // Tool Mode State const isToolMode = (nodeData.config as any)?.isToolMode || false; + useEffect(() => { + if (!isToolModeOnly || isToolMode) return; + setNodes((nds) => + nds.map((n) => { + if (n.id !== id) return n; + const currentConfig = (n.data as any).config || {}; + return { + ...n, + data: { + ...n.data, + config: { + ...currentConfig, + isToolMode: true, + }, + }, + }; + }), + ); + markDirty(); + }, [id, isToolMode, isToolModeOnly, markDirty, setNodes]); + const toggleToolMode = (e: React.MouseEvent) => { e.stopPropagation(); + if (isToolModeOnly) { + return; + } setNodes((nds) => nds.map((n) => { if (n.id !== id) return n; @@ -1074,13 +1104,21 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => {