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
@@ -1,13 +1,20 @@
'use client'

import { Plug, CheckCircle2, XCircle, AlertCircle } from 'lucide-react'
import { Plug, CheckCircle2, XCircle, AlertCircle, KeyRound, KeyRoundIcon } from 'lucide-react'
import {
AccordionItem,
AccordionTrigger,
AccordionContent,
} from '@/components/ui/accordion'
import { Badge } from '@/components/ui/badge'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { useMcpStatus } from '@/services/queries/use-mcp'
import type { McpServer } from '@/services/api/sessions'

type McpIntegrationsAccordionProps = {
projectName: string
Expand All @@ -21,8 +28,19 @@ export function McpIntegrationsAccordion({
// Fetch real MCP status from runner
const { data: mcpStatus } = useMcpStatus(projectName, sessionName)
const mcpServers = mcpStatus?.servers || []
const getStatusIcon = (status: 'configured' | 'connected' | 'disconnected' | 'error') => {
switch (status) {

const getStatusIcon = (server: McpServer) => {
// If we have auth info, use that for the icon
if (server.authenticated !== undefined) {
if (server.authenticated) {
return <CheckCircle2 className="h-4 w-4 text-green-600" />
} else {
return <KeyRound className="h-4 w-4 text-amber-500" />
}
}

// Fall back to status-based icons
switch (server.status) {
case 'configured':
case 'connected':
return <CheckCircle2 className="h-4 w-4 text-blue-600" />
Expand All @@ -34,8 +52,28 @@ export function McpIntegrationsAccordion({
}
}

const getStatusBadge = (status: 'configured' | 'connected' | 'disconnected' | 'error') => {
switch (status) {
const getAuthBadge = (server: McpServer) => {
// If auth info is available, show auth status
if (server.authenticated !== undefined) {
if (server.authenticated) {
return (
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-200">
<KeyRoundIcon className="h-3 w-3 mr-1" />
Authenticated
</Badge>
)
} else {
return (
<Badge variant="outline" className="text-xs bg-amber-50 text-amber-700 border-amber-200">
<KeyRound className="h-3 w-3 mr-1" />
Not Authenticated
</Badge>
)
}
}

// Fall back to status-based badges
switch (server.status) {
case 'configured':
return (
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
Expand Down Expand Up @@ -82,17 +120,28 @@ export function McpIntegrationsAccordion({
>
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
{getStatusIcon(server.status)}
{getStatusIcon(server)}
</div>
<div className="flex-1">
<h4 className="font-medium text-sm">{server.displayName}</h4>
<p className="text-xs text-muted-foreground mt-0.5">
{server.name}
{server.authMessage || server.name}
</p>
</div>
</div>
<div className="flex-shrink-0">
{getStatusBadge(server.status)}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
{getAuthBadge(server)}
</TooltipTrigger>
{server.authMessage && (
<TooltipContent>
<p>{server.authMessage}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
</div>
))
Expand Down
2 changes: 2 additions & 0 deletions components/frontend/src/services/api/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type McpServer = {
name: string;
displayName: string;
status: 'configured' | 'connected' | 'disconnected' | 'error';
authenticated?: boolean;
authMessage?: string;
source?: string;
command?: string;
};
Expand Down
64 changes: 62 additions & 2 deletions components/operator/internal/handlers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2115,6 +2115,7 @@ func copySecretToNamespace(ctx context.Context, sourceSecret *corev1.Secret, tar
}

// deleteAmbientVertexSecret deletes the ambient-vertex secret from a namespace if it was copied
// and no other active sessions in the namespace still need it.
func deleteAmbientVertexSecret(ctx context.Context, namespace string) error {
secret, err := config.K8sClient.CoreV1().Secrets(namespace).Get(ctx, types.AmbientVertexSecretName, v1.GetOptions{})
if err != nil {
Expand All @@ -2131,7 +2132,36 @@ func deleteAmbientVertexSecret(ctx context.Context, namespace string) error {
return nil
}

log.Printf("Deleting copied %s secret from namespace %s", types.AmbientVertexSecretName, namespace)
// Check if there are other active sessions in this namespace that might need this secret
// Don't delete the shared secret if other sessions are Running, Creating, or Pending
gvr := types.GetAgenticSessionResource()
sessions, err := config.DynamicClient.Resource(gvr).Namespace(namespace).List(ctx, v1.ListOptions{})
if err != nil {
log.Printf("Warning: failed to list sessions in namespace %s, skipping secret deletion: %v", namespace, err)
return nil // Don't delete if we can't verify no other sessions need it
}

activeCount := 0
for _, session := range sessions.Items {
status, _, _ := unstructured.NestedMap(session.Object, "status")
phase := ""
if status != nil {
if p, ok := status["phase"].(string); ok {
phase = p
}
}
// Count sessions that are active and might need the vertex secret
if phase == "Running" || phase == "Creating" || phase == "Pending" {
activeCount++
}
}

if activeCount > 0 {
log.Printf("Skipping %s secret deletion in namespace %s: %d active session(s) may still need it", types.AmbientVertexSecretName, namespace, activeCount)
return nil
}

log.Printf("Deleting copied %s secret from namespace %s (no active sessions)", types.AmbientVertexSecretName, namespace)
err = config.K8sClient.CoreV1().Secrets(namespace).Delete(ctx, types.AmbientVertexSecretName, v1.DeleteOptions{})
if err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("failed to delete %s secret: %w", types.AmbientVertexSecretName, err)
Expand All @@ -2141,6 +2171,7 @@ func deleteAmbientVertexSecret(ctx context.Context, namespace string) error {
}

// deleteAmbientLangfuseSecret deletes the ambient-admin-langfuse-secret from a namespace if it was copied
// and no other active sessions in the namespace still need it.
func deleteAmbientLangfuseSecret(ctx context.Context, namespace string) error {
const langfuseSecretName = "ambient-admin-langfuse-secret"
secret, err := config.K8sClient.CoreV1().Secrets(namespace).Get(ctx, langfuseSecretName, v1.GetOptions{})
Expand All @@ -2158,7 +2189,36 @@ func deleteAmbientLangfuseSecret(ctx context.Context, namespace string) error {
return nil
}

log.Printf("Deleting copied %s secret from namespace %s", langfuseSecretName, namespace)
// Check if there are other active sessions in this namespace that might need this secret
// Don't delete the shared secret if other sessions are Running, Creating, or Pending
gvr := types.GetAgenticSessionResource()
sessions, err := config.DynamicClient.Resource(gvr).Namespace(namespace).List(ctx, v1.ListOptions{})
if err != nil {
log.Printf("Warning: failed to list sessions in namespace %s, skipping secret deletion: %v", namespace, err)
return nil // Don't delete if we can't verify no other sessions need it
}

activeCount := 0
for _, session := range sessions.Items {
status, _, _ := unstructured.NestedMap(session.Object, "status")
phase := ""
if status != nil {
if p, ok := status["phase"].(string); ok {
phase = p
}
}
// Count sessions that are active and might need the langfuse secret
if phase == "Running" || phase == "Creating" || phase == "Pending" {
activeCount++
}
}

if activeCount > 0 {
log.Printf("Skipping %s secret deletion in namespace %s: %d active session(s) may still need it", langfuseSecretName, namespace, activeCount)
return nil
}

log.Printf("Deleting copied %s secret from namespace %s (no active sessions)", langfuseSecretName, namespace)
err = config.K8sClient.CoreV1().Secrets(namespace).Delete(ctx, langfuseSecretName, v1.DeleteOptions{})
if err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("failed to delete %s secret: %w", langfuseSecretName, err)
Expand Down
4 changes: 4 additions & 0 deletions components/runners/claude-code-runner/.mcp.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"mcpServers": {
"webfetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
},
"mcp-atlassian": {
"command": "mcp-atlassian",
"args": [],
Expand Down
18 changes: 3 additions & 15 deletions components/runners/claude-code-runner/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,22 +398,10 @@ async def _run_claude_agent_sdk(
logger.info(f"Claude SDK CWD: {cwd_path}")
logger.info(f"Claude SDK additional directories: {add_dirs}")

# Load MCP server configuration
mcp_servers = self._load_mcp_config(cwd_path)
# Load MCP server configuration (webfetch is included in static .mcp.json)
mcp_servers = self._load_mcp_config(cwd_path) or {}

# 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
# Disable built-in WebFetch in favor of WebFetch.MCP from config
allowed_tools = ["Read", "Write", "Bash", "Glob", "Grep", "Edit", "MultiEdit", "WebSearch"]
if mcp_servers:
for server_name in mcp_servers.keys():
Expand Down
70 changes: 60 additions & 10 deletions components/runners/claude-code-runner/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,14 +284,54 @@ async def interrupt_run():
raise HTTPException(status_code=500, detail=str(e))


def _check_mcp_authentication(server_name: str) -> tuple[bool | None, str | None]:
"""
Check if credentials are available for known MCP servers.

Returns:
Tuple of (is_authenticated, auth_message)
Returns (None, None) for servers we don't know how to check
"""
from pathlib import Path

# Google Workspace MCP - we know how to check this
if server_name == "google-workspace":
# Check mounted secret location first, then workspace copy
secret_path = Path("/app/.google_workspace_mcp/credentials/credentials.json")
workspace_path = Path("/workspace/.google_workspace_mcp/credentials/credentials.json")

for cred_path in [workspace_path, secret_path]:
if cred_path.exists():
try:
if cred_path.stat().st_size > 0:
return True, "Google OAuth credentials available"
except OSError:
pass
return False, "Google OAuth not configured - authenticate via Integrations page"

# Jira/Atlassian MCP - we know how to check this
if server_name in ("mcp-atlassian", "jira"):
jira_url = os.getenv("JIRA_URL", "").strip()
jira_token = os.getenv("JIRA_API_TOKEN", "").strip()

if jira_url and jira_token:
return True, "Jira credentials configured"
elif jira_url:
return False, "Jira URL set but API token missing"
else:
return False, "Jira not configured - set credentials in Workspace Settings"

# For all other servers (webfetch, unknown) - don't claim to know auth status
return None, None


@app.get("/mcp/status")
async def get_mcp_status():
"""
Returns MCP servers configured for this session.
Returns MCP servers configured for this session with authentication status.
Goes straight to the source - uses adapter's _load_mcp_config() method.

Note: Status is "configured" not "connected" - we can't verify runtime state
without introspecting the Claude SDK's internal MCP server processes.
For known integrations (Google, Jira), also checks if credentials are present.
"""
try:
global adapter
Expand Down Expand Up @@ -323,21 +363,31 @@ async def get_mcp_status():

if mcp_config:
for server_name, server_config in mcp_config.items():
# Check if this is WebFetch (auto-added by adapter)
is_webfetch = server_name == "webfetch"
# Check authentication status for known servers (Google, Jira)
is_authenticated, auth_message = _check_mcp_authentication(server_name)

# Platform servers are built-in (webfetch), workflow servers come from config
is_platform = server_name == "webfetch"

mcp_servers_list.append({
server_info = {
"name": server_name,
"displayName": server_name.replace('-', ' ').replace('_', ' ').title(),
"status": "configured", # Honest: we know it's configured, not if it's actually running
"status": "configured",
"command": server_config.get("command", ""),
"source": "platform" if is_webfetch else "workflow"
})
"source": "platform" if is_platform else "workflow"
}

# Only include auth fields for servers we know how to check
if is_authenticated is not None:
server_info["authenticated"] = is_authenticated
server_info["authMessage"] = auth_message

mcp_servers_list.append(server_info)

return {
"servers": mcp_servers_list,
"totalCount": len(mcp_servers_list),
"note": "Status shows 'configured' - actual runtime state requires SDK introspection"
"note": "Status shows 'configured' - check 'authenticated' field for credential status"
}

except Exception as e:
Expand Down
Loading