From 79efb1eef830c65599f59f26c08e145102f841e6 Mon Sep 17 00:00:00 2001 From: Gage Krumbach Date: Tue, 6 Jan 2026 16:36:26 -0600 Subject: [PATCH 01/11] refactor: Update command handling and UI components for better integration visibility - Changed the slash command handling in `content.go` to use the full command name. - Updated the `IntegrationsClient` to include the `GoogleDriveConnectionCard`. - Refactored `McpIntegrationsAccordion` to display MCP server statuses with improved UI elements and removed deprecated Google Drive integration code. - Enhanced `MessagesTab` to directly show command names instead of derived titles, improving clarity in command representation. - Adjusted the `ClaudeCodeAdapter` to ensure default MCP configurations are set correctly and improved workspace prompt generation for clarity. These changes aim to streamline command usage and enhance user experience across the application. --- components/backend/handlers/content.go | 15 +- .../app/integrations/IntegrationsClient.tsx | 2 + .../accordions/mcp-integrations-accordion.tsx | 202 +++++++----------- .../google-drive-connection-card.tsx | 133 ++++++++++++ .../src/components/session/MessagesTab.tsx | 17 +- .../runners/claude-code-runner/adapter.py | 63 +++--- components/runners/claude-code-runner/main.py | 14 +- 7 files changed, 268 insertions(+), 178 deletions(-) create mode 100644 components/frontend/src/components/google-drive-connection-card.tsx diff --git a/components/backend/handlers/content.go b/components/backend/handlers/content.go index 0732cc67d..891d0cfbc 100644 --- a/components/backend/handlers/content.go +++ b/components/backend/handlers/content.go @@ -511,17 +511,12 @@ func ContentWorkflowMetadata(c *gin.Context) { displayName = commandName } - // Extract short command (last segment after final dot) - shortCommand := commandName - if lastDot := strings.LastIndex(commandName, "."); lastDot != -1 { - shortCommand = commandName[lastDot+1:] - } - + // Use full command name as slash command (e.g., /speckit.rfe.start) commands = append(commands, map[string]interface{}{ "id": commandName, "name": displayName, "description": metadata["description"], - "slashCommand": "/" + shortCommand, + "slashCommand": "/" + commandName, "icon": metadata["icon"], }) } @@ -648,9 +643,9 @@ func parseAmbientConfig(workflowDir string) *AmbientConfig { // findActiveWorkflowDir finds the active workflow directory for a session func findActiveWorkflowDir(sessionName string) string { - // Workflows are stored at {StateBaseDir}/sessions/{session-name}/workspace/workflows/{workflow-name} - // The runner creates this nested structure - workflowsBase := filepath.Join(StateBaseDir, "sessions", sessionName, "workspace", "workflows") + // Workflows are stored at {StateBaseDir}/workflows/{workflow-name} + // The runner clones workflows to /workspace/workflows/ at runtime + workflowsBase := filepath.Join(StateBaseDir, "workflows") entries, err := os.ReadDir(workflowsBase) if err != nil { diff --git a/components/frontend/src/app/integrations/IntegrationsClient.tsx b/components/frontend/src/app/integrations/IntegrationsClient.tsx index 8c21d1bf7..7893c568a 100644 --- a/components/frontend/src/app/integrations/IntegrationsClient.tsx +++ b/components/frontend/src/app/integrations/IntegrationsClient.tsx @@ -2,6 +2,7 @@ import React from 'react' import { GitHubConnectionCard } from '@/components/github-connection-card' +import { GoogleDriveConnectionCard } from '@/components/google-drive-connection-card' import { PageHeader } from '@/components/page-header' type Props = { appSlug?: string } @@ -24,6 +25,7 @@ export default function IntegrationsClient({ appSlug }: Props) {
+
diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/accordions/mcp-integrations-accordion.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/accordions/mcp-integrations-accordion.tsx index e161272f5..874576f18 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/accordions/mcp-integrations-accordion.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/accordions/mcp-integrations-accordion.tsx @@ -1,13 +1,11 @@ 'use client' -import { useState } from 'react' -import { Plug, Check, Loader2 } from 'lucide-react' +import { Plug, CheckCircle2, XCircle, AlertCircle } from 'lucide-react' import { AccordionItem, AccordionTrigger, AccordionContent, } from '@/components/ui/accordion' -import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' type McpIntegrationsAccordionProps = { @@ -15,60 +13,62 @@ type McpIntegrationsAccordionProps = { sessionName: string } +// MCP Server status - hardcoded for now +// TODO: Fetch from backend API based on session's MCP configuration +type McpServer = { + name: string + displayName: string + status: 'connected' | 'disconnected' | 'error' + icon?: string +} + +const mcpServers: McpServer[] = [ + { + name: 'google-workspace-mcp', + displayName: 'Google Workspace MCP', + status: 'connected', + }, + // Add more servers as they're configured +] + export function McpIntegrationsAccordion({ projectName, sessionName, }: McpIntegrationsAccordionProps) { - const [googleConnected, setGoogleConnected] = useState(false) - const [connecting, setConnecting] = useState(false) - - const handleConnectGoogle = async () => { - setConnecting(true) - - try { - // Call backend to get OAuth URL - const response = await fetch( - `/api/projects/${projectName}/agentic-sessions/${sessionName}/oauth/google/url` - ) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Failed to get OAuth URL') - } - - const data = await response.json() - const authUrl = data.url - - // Open OAuth flow in popup window - const width = 600 - const height = 700 - const left = window.screen.width / 2 - width / 2 - const top = window.screen.height / 2 - height / 2 - - const popup = window.open( - authUrl, - 'Google OAuth', - `width=${width},height=${height},left=${left},top=${top}` - ) - - // Poll for popup close (credentials will be stored server-side) - const pollTimer = setInterval(() => { - if (popup?.closed) { - clearInterval(pollTimer) - setConnecting(false) - // TODO: Check if credentials were successfully stored - setGoogleConnected(true) - } - }, 500) - } catch (error) { - console.error('Failed to initiate Google OAuth:', error) - setConnecting(false) + const getStatusIcon = (status: McpServer['status']) => { + switch (status) { + case 'connected': + return + case 'error': + return + case 'disconnected': + default: + return } } - const handleDisconnectGoogle = () => { - // TODO: Implement disconnect - remove credentials from session - setGoogleConnected(false) + const getStatusBadge = (status: McpServer['status']) => { + switch (status) { + case 'connected': + return ( + + Connected + + ) + case 'error': + return ( + + Error + + ) + case 'disconnected': + default: + return ( + + Disconnected + + ) + } } return ( @@ -76,83 +76,43 @@ export function McpIntegrationsAccordion({
- MCP Integrations + MCP Server Status
-
- {/* Google Drive Integration */} -
-
-
- -
-
-
-

Google Drive

- {googleConnected && ( - - - Connected - - )} +
+ {mcpServers.length > 0 ? ( + mcpServers.map((server) => ( +
+
+
+ {getStatusIcon(server.status)} +
+
+

{server.displayName}

+

+ {server.name} +

+
+
+
+ {getStatusBadge(server.status)}
-

- Access Drive files in this session -

+ )) + ) : ( +
+

+ No MCP servers configured for this session +

+

+ Configure MCP servers in your workflow or project settings +

-
- {googleConnected ? ( - - ) : ( - - )} -
-
- - {/* Placeholder for future MCP integrations */} -

- More integrations coming soon... -

+ )}
diff --git a/components/frontend/src/components/google-drive-connection-card.tsx b/components/frontend/src/components/google-drive-connection-card.tsx new file mode 100644 index 000000000..0e3fbfd95 --- /dev/null +++ b/components/frontend/src/components/google-drive-connection-card.tsx @@ -0,0 +1,133 @@ +'use client' + +import React, { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Card } from '@/components/ui/card' +import { Loader2 } from 'lucide-react' +import { successToast, errorToast } from '@/hooks/use-toast' + +type Props = { + showManageButton?: boolean +} + +export function GoogleDriveConnectionCard({ showManageButton = true }: Props) { + const [googleConnected, setGoogleConnected] = useState(false) + const [connecting, setConnecting] = useState(false) + const [isLoading] = useState(false) + + const handleConnect = async () => { + setConnecting(true) + + try { + // Note: This will need to be updated to use a project-level OAuth flow + // Currently MCP integrations are per-session, but should be project-level + errorToast('Google Drive integration setup coming soon. Currently available per-session.') + setConnecting(false) + } catch (error) { + console.error('Failed to initiate Google OAuth:', error) + errorToast('Failed to connect Google Drive') + setConnecting(false) + } + } + + const handleDisconnect = async () => { + try { + // TODO: Implement disconnect - remove credentials + setGoogleConnected(false) + successToast('Google Drive disconnected successfully') + } catch (error) { + errorToast('Failed to disconnect Google Drive') + } + } + + const handleManage = () => { + window.open('https://myaccount.google.com/permissions', '_blank') + } + + return ( + +
+ {/* Header section with icon and title */} +
+
+ +
+
+

Google Drive

+

Access Drive files across all sessions

+
+
+ + {/* Status section */} +
+
+ + + {googleConnected ? 'Connected' : 'Not Connected'} + +
+

+ Connect to Google Drive to access files in your sessions via MCP +

+
+ + {/* Action buttons */} +
+ {googleConnected ? ( + <> + {showManageButton && ( + + )} + + + ) : ( + + )} +
+
+
+ ) +} + diff --git a/components/frontend/src/components/session/MessagesTab.tsx b/components/frontend/src/components/session/MessagesTab.tsx index e37891e8a..f717d580c 100644 --- a/components/frontend/src/components/session/MessagesTab.tsx +++ b/components/frontend/src/components/session/MessagesTab.tsx @@ -492,9 +492,6 @@ const MessagesTab: React.FC = ({ session, streamMessages, chat ); } else { const cmd = item as { id: string; name: string; slashCommand: string; description?: string }; - const commandTitle = cmd.name.includes('.') - ? cmd.name.split('.').pop() - : cmd.name; return (
= ({ session, streamMessages, chat onMouseEnter={() => setAutocompleteSelectedIndex(index)} >
{cmd.slashCommand}
-
- {commandTitle} +
+ {cmd.name}
); @@ -636,18 +633,14 @@ const MessagesTab: React.FC = ({ session, streamMessages, chat className="max-h-[400px] overflow-y-scroll space-y-2 pr-2 scrollbar-thin" > {workflowMetadata.commands.map((cmd) => { - const commandTitle = cmd.name.includes('.') - ? cmd.name.split('.').pop() - : cmd.name; - return (
-

- {commandTitle} +

+ {cmd.name}

{cmd.description && ( diff --git a/components/runners/claude-code-runner/adapter.py b/components/runners/claude-code-runner/adapter.py index 4b4e8c30f..0a3f124ee 100644 --- a/components/runners/claude-code-runner/adapter.py +++ b/components/runners/claude-code-runner/adapter.py @@ -400,7 +400,21 @@ async def _run_claude_agent_sdk( # Load MCP server configuration mcp_servers = self._load_mcp_config(cwd_path) - allowed_tools = ["Read", "Write", "Bash", "Glob", "Grep", "Edit", "MultiEdit", "WebSearch", "WebFetch"] + + # Add WebFetch.MCP as default if no MCP config exists + if not mcp_servers: + mcp_servers = {} + + # Always include WebFetch.MCP for better web content extraction + if "webfetch" not in mcp_servers: + mcp_servers["webfetch"] = { + "command": "npx", + "args": ["-y", "@manooll/webfetch-mcp"] + } + logger.info("Added WebFetch.MCP as default web fetch provider") + + # Disable built-in WebFetch in favor of WebFetch.MCP + allowed_tools = ["Read", "Write", "Bash", "Glob", "Grep", "Edit", "MultiEdit", "WebSearch"] if mcp_servers: for server_name in mcp_servers.keys(): allowed_tools.append(f"mcp__{server_name}") @@ -1251,49 +1265,44 @@ def _load_ambient_config(self, cwd_path: str) -> dict: return {} def _build_workspace_context_prompt(self, repos_cfg, workflow_name, artifacts_path, ambient_config): - """Generate comprehensive system prompt describing workspace layout.""" - prompt = "You are Claude Code working in a structured development workspace.\n\n" + """Generate concise system prompt describing workspace layout.""" + prompt = "# Workspace Structure\n\n" + # Workflow directory (if active) if workflow_name: - prompt += "## Current Workflow\n" - prompt += f"Working directory: workflows/{workflow_name}/\n" - prompt += "This directory contains workflow logic and automation scripts.\n\n" + prompt += f"**Working Directory**: workflows/{workflow_name}/ (workflow logic - do not create files here)\n\n" - prompt += "## User-Uploaded Files (IMPORTANT)\n" - prompt += "Location: file-uploads/\n" - prompt += "Purpose: User-uploaded context files (screenshots, documents, images, PDFs, specs, designs).\n" - prompt += "ALWAYS check this directory when starting a new task - it often contains critical context.\n\n" + # Artifacts + prompt += f"**Artifacts**: {artifacts_path} (create all output files here)\n\n" + # Uploaded files file_uploads_path = Path(self.context.workspace_path) / "file-uploads" if file_uploads_path.exists() and file_uploads_path.is_dir(): try: files = sorted([f.name for f in file_uploads_path.iterdir() if f.is_file()]) if files: - prompt += f"Currently uploaded files ({len(files)}):\n" - for filename in files: - prompt += f" - {filename}\n" - prompt += "READ THESE FILES if they're relevant to the user's task!\n" + max_display = 10 + if len(files) <= max_display: + prompt += f"**Uploaded Files**: {', '.join(files)}\n\n" + else: + prompt += f"**Uploaded Files** ({len(files)} total): {', '.join(files[:max_display])}, and {len(files) - max_display} more\n\n" except Exception: pass + else: + prompt += "**Uploaded Files**: None\n\n" - prompt += "\n## Shared Artifacts Directory\n" - prompt += f"Location: {artifacts_path}\n" - prompt += "Purpose: Create all output artifacts (documents, specs, reports) here.\n\n" - + # Repositories if repos_cfg: - prompt += "## Available Code Repositories\n" - prompt += "Location: repos/\n" - for i, repo in enumerate(repos_cfg): - name = repo.get('name', f'repo-{i}') - prompt += f"- repos/{name}/\n" - prompt += "\nThese repositories contain source code you can read or modify.\n\n" + repo_names = [repo.get('name', f'repo-{i}') for i, repo in enumerate(repos_cfg)] + if len(repo_names) <= 5: + prompt += f"**Repositories**: {', '.join([f'repos/{name}/' for name in repo_names])}\n\n" + else: + prompt += f"**Repositories** ({len(repo_names)} total): {', '.join([f'repos/{name}/' for name in repo_names[:5]])}, and {len(repo_names) - 5} more\n\n" + # Workflow instructions (if any) if ambient_config.get("systemPrompt"): prompt += f"## Workflow Instructions\n{ambient_config['systemPrompt']}\n\n" - prompt += "## Navigation\n" - prompt += "All directories are accessible via relative or absolute paths.\n" - return prompt diff --git a/components/runners/claude-code-runner/main.py b/components/runners/claude-code-runner/main.py index 412ce70bd..d5f9c4e99 100644 --- a/components/runners/claude-code-runner/main.py +++ b/components/runners/claude-code-runner/main.py @@ -101,16 +101,14 @@ async def lifespan(app: FastAPI): # This is set by the operator when restarting a stopped/completed/failed session is_resume = os.getenv("IS_RESUME", "").strip().lower() == "true" if is_resume: - logger.info("IS_RESUME=true - this is a resumed session, will skip INITIAL_PROMPT") + logger.info("IS_RESUME=true - this is a resumed session") - # Check for INITIAL_PROMPT and auto-execute (only if not a resume) + # INITIAL_PROMPT is no longer auto-executed on startup + # User must explicitly send the first message to start the conversation + # Workflow greetings are still triggered when a workflow is activated initial_prompt = os.getenv("INITIAL_PROMPT", "").strip() - if initial_prompt and not is_resume: - delay = os.getenv("INITIAL_PROMPT_DELAY_SECONDS", "1") - logger.info(f"INITIAL_PROMPT detected ({len(initial_prompt)} chars), will auto-execute after {delay}s delay") - asyncio.create_task(auto_execute_initial_prompt(initial_prompt, session_id)) - elif initial_prompt and is_resume: - logger.info("INITIAL_PROMPT detected but IS_RESUME=true - skipping (this is a resume)") + if initial_prompt: + logger.info(f"INITIAL_PROMPT detected ({len(initial_prompt)} chars) but not auto-executing (user will send first message)") logger.info(f"AG-UI server ready for session {session_id}") From a922e55ec11d1162c5c9c59126fe47c7fe02d654 Mon Sep 17 00:00:00 2001 From: Gage Krumbach Date: Tue, 6 Jan 2026 17:14:32 -0600 Subject: [PATCH 02/11] feat: Add MCP status endpoint and integrate with frontend components - Introduced a new endpoint for retrieving MCP server statuses in the backend, allowing the frontend to display real-time integration statuses. - Updated the `SelectWorkflow` function to handle MCP status checks during workflow selection. - Enhanced the `McpIntegrationsAccordion` component to fetch and display MCP server statuses dynamically. - Removed deprecated model configuration and session creation components to streamline the codebase. These changes improve the visibility of MCP integrations and enhance user experience by providing real-time feedback on server statuses. --- components/backend/handlers/sessions.go | 53 +++- components/backend/routes.go | 3 + components/backend/websocket/agui_proxy.go | 82 +++++ .../accordions/mcp-integrations-accordion.tsx | 26 +- .../[name]/sessions/[sessionName]/page.tsx | 2 +- .../sessions/new/model-configuration.tsx | 119 ------- .../app/projects/[name]/sessions/new/page.tsx | 290 ------------------ .../[name]/sessions/new/repository-dialog.tsx | 106 ------- .../[name]/sessions/new/repository-list.tsx | 100 ------ .../src/components/create-session-dialog.tsx | 88 +----- .../google-drive-connection-card.tsx | 6 +- .../frontend/src/services/api/sessions.ts | 24 ++ .../frontend/src/services/queries/use-mcp.ts | 23 ++ components/runners/claude-code-runner/main.py | 66 ++++ 14 files changed, 256 insertions(+), 732 deletions(-) delete mode 100644 components/frontend/src/app/projects/[name]/sessions/new/model-configuration.tsx delete mode 100644 components/frontend/src/app/projects/[name]/sessions/new/page.tsx delete mode 100644 components/frontend/src/app/projects/[name]/sessions/new/repository-dialog.tsx delete mode 100644 components/frontend/src/app/projects/[name]/sessions/new/repository-list.tsx create mode 100644 components/frontend/src/services/queries/use-mcp.ts diff --git a/components/backend/handlers/sessions.go b/components/backend/handlers/sessions.go index 276d0b019..4c8883ac3 100644 --- a/components/backend/handlers/sessions.go +++ b/components/backend/handlers/sessions.go @@ -1192,6 +1192,51 @@ func SelectWorkflow(c *gin.Context) { return } + // Build workflow config + branch := req.Branch + if branch == "" { + branch = "main" + } + + // Call runner to clone and activate the workflow (if session is running) + status, _ := item.Object["status"].(map[string]interface{}) + phase, _ := status["phase"].(string) + if phase == "Running" { + runnerURL := fmt.Sprintf("http://session-%s.%s.svc.cluster.local:8001/workflow", sessionName, project) + runnerReq := map[string]string{ + "gitUrl": req.GitURL, + "branch": branch, + "path": req.Path, + } + reqBody, _ := json.Marshal(runnerReq) + + log.Printf("Calling runner to activate workflow: %s@%s (path: %s) -> %s", req.GitURL, branch, req.Path, runnerURL) + httpReq, err := http.NewRequestWithContext(c.Request.Context(), "POST", runnerURL, bytes.NewReader(reqBody)) + if err != nil { + log.Printf("Failed to create runner request: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create runner request"}) + return + } + httpReq.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 120 * time.Second} // Allow time for clone + resp, err := client.Do(httpReq) + if err != nil { + log.Printf("Failed to call runner to activate workflow: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to activate workflow (runner not reachable)"}) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + log.Printf("Runner failed to activate workflow (status %d): %s", resp.StatusCode, string(body)) + c.JSON(resp.StatusCode, gin.H{"error": fmt.Sprintf("Failed to activate workflow: %s", string(body))}) + return + } + log.Printf("Runner successfully activated workflow %s@%s for session %s", req.GitURL, branch, sessionName) + } + // Update activeWorkflow in spec spec, ok := item.Object["spec"].(map[string]interface{}) if !ok { @@ -1202,11 +1247,7 @@ func SelectWorkflow(c *gin.Context) { // Set activeWorkflow workflowMap := map[string]interface{}{ "gitUrl": req.GitURL, - } - if req.Branch != "" { - workflowMap["branch"] = req.Branch - } else { - workflowMap["branch"] = "main" + "branch": branch, } if req.Path != "" { workflowMap["path"] = req.Path @@ -1221,7 +1262,7 @@ func SelectWorkflow(c *gin.Context) { return } - log.Printf("Workflow updated for session %s: %s@%s", sessionName, req.GitURL, workflowMap["branch"]) + log.Printf("Workflow updated for session %s: %s@%s", sessionName, req.GitURL, branch) // Respond with updated session summary session := types.AgenticSession{ diff --git a/components/backend/routes.go b/components/backend/routes.go index 539ca4ea5..8f0481cd0 100644 --- a/components/backend/routes.go +++ b/components/backend/routes.go @@ -89,6 +89,9 @@ func registerRoutes(r *gin.Engine) { projectGroup.GET("/agentic-sessions/:sessionName/agui/events", websocket.HandleAGUIEvents) projectGroup.GET("/agentic-sessions/:sessionName/agui/history", websocket.HandleAGUIHistory) projectGroup.GET("/agentic-sessions/:sessionName/agui/runs", websocket.HandleAGUIRuns) + + // MCP status endpoint + projectGroup.GET("/agentic-sessions/:sessionName/mcp/status", websocket.HandleMCPStatus) // Session export projectGroup.GET("/agentic-sessions/:sessionName/export", websocket.HandleExportSession) diff --git a/components/backend/websocket/agui_proxy.go b/components/backend/websocket/agui_proxy.go index 36a32f5ad..7c2ec931b 100644 --- a/components/backend/websocket/agui_proxy.go +++ b/components/backend/websocket/agui_proxy.go @@ -406,6 +406,88 @@ func HandleAGUIInterrupt(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Interrupt signal sent"}) } +// HandleMCPStatus proxies MCP status requests to runner +// GET /api/projects/:projectName/agentic-sessions/:sessionName/mcp/status +func HandleMCPStatus(c *gin.Context) { + projectName := c.Param("projectName") + sessionName := c.Param("sessionName") + + // SECURITY: Authenticate user and get user-scoped K8s client + reqK8s, _ := handlers.GetK8sClientsForRequest(c) + if reqK8s == nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or missing token"}) + c.Abort() + return + } + + // SECURITY: Verify user has permission to read this session + ctx := context.Background() + ssar := &authv1.SelfSubjectAccessReview{ + Spec: authv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authv1.ResourceAttributes{ + Group: "vteam.ambient-code", + Resource: "agenticsessions", + Verb: "get", + Namespace: projectName, + Name: sessionName, + }, + }, + } + res, err := reqK8s.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, ssar, metav1.CreateOptions{}) + if err != nil || !res.Status.Allowed { + log.Printf("MCP Status: User not authorized to read session %s/%s", projectName, sessionName) + c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } + + // Get runner endpoint + runnerURL, err := getRunnerEndpoint(projectName, sessionName) + if err != nil { + log.Printf("MCP Status: Failed to get runner endpoint: %v", err) + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Runner not available"}) + return + } + + mcpStatusURL := strings.TrimSuffix(runnerURL, "/") + "/mcp/status" + log.Printf("MCP Status: Forwarding to runner: %s", mcpStatusURL) + + // GET from runner's MCP status endpoint + req, err := http.NewRequest("GET", mcpStatusURL, nil) + if err != nil { + log.Printf("MCP Status: Failed to create request: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + log.Printf("MCP Status: Request failed: %v", err) + // Runner might not be running yet - return empty list + c.JSON(http.StatusOK, gin.H{"servers": []interface{}{}, "totalCount": 0}) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + log.Printf("MCP Status: Runner returned %d: %s", resp.StatusCode, string(body)) + c.JSON(http.StatusOK, gin.H{"servers": []interface{}{}, "totalCount": 0}) + return + } + + // Forward runner response to client + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + log.Printf("MCP Status: Failed to decode response: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse runner response"}) + return + } + + c.JSON(http.StatusOK, result) +} + // getRunnerEndpoint returns the AG-UI server endpoint for a session // The operator creates a Service named "session-{sessionName}" in the project namespace func getRunnerEndpoint(projectName, sessionName string) (string, error) { diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/accordions/mcp-integrations-accordion.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/accordions/mcp-integrations-accordion.tsx index 874576f18..f53e119ad 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/accordions/mcp-integrations-accordion.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/accordions/mcp-integrations-accordion.tsx @@ -7,35 +7,21 @@ import { AccordionContent, } from '@/components/ui/accordion' import { Badge } from '@/components/ui/badge' +import { useMcpStatus } from '@/services/queries/use-mcp' type McpIntegrationsAccordionProps = { projectName: string sessionName: string } -// MCP Server status - hardcoded for now -// TODO: Fetch from backend API based on session's MCP configuration -type McpServer = { - name: string - displayName: string - status: 'connected' | 'disconnected' | 'error' - icon?: string -} - -const mcpServers: McpServer[] = [ - { - name: 'google-workspace-mcp', - displayName: 'Google Workspace MCP', - status: 'connected', - }, - // Add more servers as they're configured -] - export function McpIntegrationsAccordion({ projectName, sessionName, }: McpIntegrationsAccordionProps) { - const getStatusIcon = (status: McpServer['status']) => { + // Fetch real MCP status from runner + const { data: mcpStatus } = useMcpStatus(projectName, sessionName) + const mcpServers = mcpStatus?.servers || [] + const getStatusIcon = (status: 'connected' | 'disconnected' | 'error') => { switch (status) { case 'connected': return @@ -47,7 +33,7 @@ export function McpIntegrationsAccordion({ } } - const getStatusBadge = (status: McpServer['status']) => { + const getStatusBadge = (status: 'connected' | 'disconnected' | 'error') => { switch (status) { case 'connected': return ( diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx index 2617325b9..00101af62 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx @@ -1476,7 +1476,7 @@ export default function ProjectSessionDetailPage({ onNavigateBack={artifactsOps.navigateBack} /> - diff --git a/components/frontend/src/app/projects/[name]/sessions/new/model-configuration.tsx b/components/frontend/src/app/projects/[name]/sessions/new/model-configuration.tsx deleted file mode 100644 index a4e442135..000000000 --- a/components/frontend/src/app/projects/[name]/sessions/new/model-configuration.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client"; - -import { Control } from "react-hook-form"; -import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; - -const models = [ - { value: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" }, - { value: "claude-opus-4-5", label: "Claude Opus 4.5" }, - { value: "claude-opus-4-1", label: "Claude Opus 4.1" }, - { value: "claude-haiku-4-5", label: "Claude Haiku 4.5" }, -]; - -type ModelConfigurationProps = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - control: Control; -}; - -export function ModelConfiguration({ control }: ModelConfigurationProps) { - return ( -
-
- ( - - Model - - - - )} - /> - - ( - - Temperature - - field.onChange(parseFloat(e.target.value))} - /> - - Controls randomness (0.0 - 2.0) - - - )} - /> -
- -
- ( - - Max Output Tokens - - field.onChange(parseInt(e.target.value))} - /> - - Maximum response length (100-8000) - - - )} - /> - - ( - - Timeout (seconds) - - field.onChange(parseInt(e.target.value))} - /> - - Session timeout (60-1800 seconds) - - - )} - /> -
-
- ); -} diff --git a/components/frontend/src/app/projects/[name]/sessions/new/page.tsx b/components/frontend/src/app/projects/[name]/sessions/new/page.tsx deleted file mode 100644 index 147d651a6..000000000 --- a/components/frontend/src/app/projects/[name]/sessions/new/page.tsx +++ /dev/null @@ -1,290 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { Loader2 } from "lucide-react"; -import { useForm, useFieldArray } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import * as z from "zod"; - -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Textarea } from "@/components/ui/textarea"; -import type { CreateAgenticSessionRequest } from "@/types/agentic-session"; -import { Checkbox } from "@/components/ui/checkbox"; -import { errorToast } from "@/hooks/use-toast"; -import { Breadcrumbs } from "@/components/breadcrumbs"; -import { RepositoryDialog } from "./repository-dialog"; -import { RepositoryList } from "./repository-list"; -import { ModelConfiguration } from "./model-configuration"; -import { useCreateSession } from "@/services/queries/use-sessions"; - -const formSchema = z - .object({ - initialPrompt: z.string(), - model: z.string().min(1, "Please select a model"), - temperature: z.number().min(0).max(2), - maxTokens: z.number().min(100).max(8000), - timeout: z.number().min(60).max(1800), - interactive: z.boolean().default(false), - // Unified multi-repo array - repos: z - .array(z.object({ - url: z.string().url(), - branch: z.string().optional(), - })) - .optional() - .default([]), - // Runner behavior - autoPushOnComplete: z.boolean().default(false), - }) - .superRefine((data, ctx) => { - const isInteractive = Boolean(data.interactive); - const promptLength = (data.initialPrompt || "").trim().length; - if (!isInteractive && promptLength < 10) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["initialPrompt"], - message: "Prompt must be at least 10 characters long", - }); - } - }); - -type FormValues = z.input; - -export default function NewProjectSessionPage({ params }: { params: Promise<{ name: string }> }) { - const router = useRouter(); - const [projectName, setProjectName] = useState(""); - const [editingRepoIndex, setEditingRepoIndex] = useState(null); - const [repoDialogOpen, setRepoDialogOpen] = useState(false); - const [tempRepo, setTempRepo] = useState<{ url: string; branch?: string }>({ url: "", branch: "main" }); - - // React Query hooks - const createSessionMutation = useCreateSession(); - - useEffect(() => { - params.then(({ name }) => setProjectName(name)); - }, [params]); - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - initialPrompt: "", - model: "claude-sonnet-4-5", - temperature: 0.7, - maxTokens: 4000, - timeout: 300, - interactive: false, - autoPushOnComplete: false, - repos: [], - }, - }); - - // Field arrays for multi-repo configuration - const { append: appendRepo, remove: removeRepo, update: updateRepo } = useFieldArray({ control: form.control, name: "repos" }); - - // Watch interactive to adjust prompt field hints - const isInteractive = form.watch("interactive"); - - - - - - const onSubmit = async (values: FormValues) => { - if (!projectName) return; - - const promptToSend = values.interactive && !values.initialPrompt.trim() - ? "Greet the user and briefly explain the workspace capabilities: they can select workflows, add code repositories for context, use commands, and you'll help with software engineering tasks. Keep it friendly and concise." - : values.initialPrompt; - const request: CreateAgenticSessionRequest = { - initialPrompt: promptToSend, - llmSettings: { - model: values.model, - temperature: values.temperature, - maxTokens: values.maxTokens, - }, - timeout: values.timeout, - interactive: values.interactive, - autoPushOnComplete: values.autoPushOnComplete, - }; - - // Apply labels if projectName is present - if (projectName) { - request.labels = { - ...(request.labels || {}), - project: projectName, - }; - } - - - // Multi-repo configuration (simplified format) - const repos = (values.repos || []).filter(r => r && r.url); - if (repos.length > 0) { - request.repos = repos; - } - - createSessionMutation.mutate( - { projectName, data: request }, - { - onSuccess: (session) => { - const sessionName = session.metadata.name; - router.push(`/projects/${encodeURIComponent(projectName)}/sessions/${sessionName}`); - }, - onError: (error) => { - errorToast(error.message || "Failed to create session"); - }, - } - ); - }; - - return ( -
- - - - - New Agentic Session - Create a new agentic session that will analyze a website - - -
- - ( - - - field.onChange(Boolean(v))} /> - -
- Interactive chat - - When enabled, the session runs in chat mode. You can send messages and receive streamed responses. - -
- -
- )} - /> - - {!isInteractive && ( - ( - - Agentic Prompt - -