diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 69e4fbcb9..4f51c1c65 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -34,6 +34,7 @@ import { getSession, getStudioAccess, getRuntimeStudioToolCapabilities, + getRuntimeMcpCredentials, getRuntimes, listApps, listEnvironments, @@ -150,7 +151,10 @@ import { AgentCreationModePicker } from "./create/AgentCreationModePicker"; import { CodePackageCreate } from "./create/CodePackageCreate"; import { MigrationWorkspace } from "./migrations/MigrationWorkspace"; import type { AgentDraft } from "./create/types"; -import { configuredMcpEnvKeys } from "./create/mcpAuth"; +import { + configuredMcpEnvKeys, + hydrateMcpCredentialValues, +} from "./create/mcpAuth"; import { hydrateRuntimeModelSelection, isRuntimeModelSelectionEnv, @@ -7274,11 +7278,33 @@ export default function App() { hydratedDraft, arkModelIds, ); + let editorDraft = classifiedDraft; + if (configuredMcpEnvKeys(classifiedDraft).length > 0) { + try { + const credentials = await getRuntimeMcpCredentials({ + runtimeId: capability.runtime.runtimeId, + region: capability.runtime.region, + appName: capability.agent.appName, + etag: capability.etag, + }); + editorDraft = hydrateMcpCredentialValues( + classifiedDraft, + credentials, + ); + } catch (credentialError) { + setError( + credentialError instanceof Error + ? credentialError.message + : appText("errors.runtimeDeploymentConfigUnavailable"), + ); + return; + } + } exitAgentDetailContext(); - setImportedDraft(classifiedDraft); + setImportedDraft(editorDraft); setCustomCreateMode("custom"); setCustomCreationSurface( - classifiedDraft.dynamicAgentDelegation === true + editorDraft.dynamicAgentDelegation === true ? "vulcan" : "traditional", ); @@ -7301,7 +7327,7 @@ export default function App() { capability.editMode === "source-preserving" ? "source-preserving" : "regenerate", - configuredMcpEnvKeys: configuredMcpEnvKeys(classifiedDraft), + configuredMcpEnvKeys: configuredMcpEnvKeys(editorDraft), configuredRuntimeEnvKeys: capability.runtime.configuredEnvKeys, }); diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index c1a402ba9..da3a485d4 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -30,6 +30,7 @@ import { import type { AgentProject } from "../create/project"; import type { AgentDraft, + McpCredentialValue, NetworkConfig, SelectedSkill, } from "../create/types"; @@ -4578,6 +4579,57 @@ export interface RuntimeUpdateCapability { } | null; } +/** Fetch the exact MCP credentials for one authorized update snapshot. */ +export async function getRuntimeMcpCredentials({ + runtimeId, + region, + appName, + etag, + signal, +}: { + runtimeId: string; + region: string; + appName: string; + etag: string; + signal?: AbortSignal; +}): Promise { + const res = await apiFetch("/web/runtime-mcp-credentials", { + method: "POST", + cache: "no-store", + signal, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ runtimeId, region, appName, etag }), + }); + if (!res.ok) { + throw new Error( + await httpErrorMessage(res, adkT("client.loadMcpCredentialsFailed")), + ); + } + const payload = (await res.json().catch(() => null)) as { + credentials?: unknown; + } | null; + if (!Array.isArray(payload?.credentials)) { + throw new Error(adkT("client.invalidMcpCredentials")); + } + return payload.credentials.map((item) => { + if (!item || typeof item !== "object") { + throw new Error(adkT("client.invalidMcpCredentials")); + } + const raw = item as Record; + const credential = { + agentName: raw.agentName, + name: raw.name, + url: raw.url, + authTokenEnv: raw.authTokenEnv, + value: raw.value, + }; + if (Object.values(credential).some((value) => typeof value !== "string")) { + throw new Error(adkT("client.invalidMcpCredentials")); + } + return credential as McpCredentialValue; + }); +} + interface RuntimeUpdateCapabilityRequest { runtimeId: string; region: string; diff --git a/frontend/src/create/CustomCreate.css b/frontend/src/create/CustomCreate.css index a8da4f158..f70d4f472 100644 --- a/frontend/src/create/CustomCreate.css +++ b/frontend/src/create/CustomCreate.css @@ -3752,50 +3752,39 @@ line-height: 1.5; color: hsl(var(--muted-foreground)); } -.cw-mcp-auth-state { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - min-height: 32px; - padding: 7px 10px; - border: 1px solid hsl(var(--border)); - border-radius: 8px; - background: hsl(var(--muted)); - color: hsl(var(--muted-foreground)); - font-size: 12px; - line-height: 1.45; +.cw-mcp-token-field { + position: relative; } -.cw-mcp-auth-state button { - flex: 0 0 auto; - min-height: 28px; - padding: 0 9px; - border: 1px solid hsl(var(--border)); +.cw-mcp-token-field .cw-input { + padding-right: 40px; +} +.cw-mcp-token-toggle { + position: absolute; + top: 50%; + right: 4px; + display: inline-grid; + width: 28px; + height: 28px; + padding: 0; + border: 0; border-radius: 6px; - background: hsl(var(--panel)); - color: hsl(var(--foreground)); + background: transparent; + color: hsl(var(--muted-foreground)); cursor: pointer; - font: inherit; + place-items: center; + transform: translateY(-50%); } -.cw-mcp-auth-state button:hover { - background: hsl(var(--accent)); +.cw-mcp-token-toggle:hover { + background: hsl(var(--muted)); + color: hsl(var(--foreground)); } -.cw-mcp-auth-state button:focus-visible { +.cw-mcp-token-toggle:focus-visible { outline: 2px solid hsl(var(--primary)); outline-offset: 1px; } -.cw-mcp-auth-state.is-warning { - align-items: flex-start; - border-color: hsl(var(--destructive) / 0.35); - background: hsl(var(--destructive) / 0.06); - color: hsl(var(--foreground)); -} -.cw-mcp-auth-actions { - display: flex; - flex: 0 0 auto; - flex-wrap: wrap; - justify-content: flex-end; - gap: 6px; +.cw-mcp-token-toggle svg { + width: 16px; + height: 16px; } .cw-mcp-warning { display: flex; diff --git a/frontend/src/create/CustomCreate.tsx b/frontend/src/create/CustomCreate.tsx index 026351fe3..fb1d699e1 100644 --- a/frontend/src/create/CustomCreate.tsx +++ b/frontend/src/create/CustomCreate.tsx @@ -78,6 +78,7 @@ import { firstMissingRuntimeEnv, firstInvalidRuntimeEnv, runtimeEnvConfiguration, + runtimeDebugEnvVars, runtimeEnvJsonError, runtimeEnvVars, type RuntimeEnvConfiguration, @@ -93,18 +94,12 @@ import { import { localPickerMatches } from "./localPickerSearch"; import { draftToYaml } from "./configYaml"; import { - confirmMcpCredentialReuse, - clearMcpConfiguredAuth, deploymentMcpSecretValues, type McpConfigurationConflict, mcpAuthTokenInputValue, - mcpCredentialActionRequired, mcpConfigurationConflict, - mcpCredentialReuseValues, mcpUrlNeedsPathWarning, prepareMcpAuth, - removeMcpCredentialForChangedUrl, - replaceMcpCredentialForChangedUrl, removedConfiguredMcpEnvKeys, sourcePreservingMcpSecretValues, updateMcpAuthTokenInput, @@ -1909,6 +1904,34 @@ function VikingMemorySelect({ * (http / stdio) and shows the matching fields. http -> url + optional * bearer token; stdio -> command + space-separated args. Optional name. * ---------------------------------------------------------------- */ +function McpTokenVisibilityIcon({ hidden }: { hidden: boolean }) { + return ( + + ); +} + function McpToolEditor({ tools, conflict, @@ -1922,11 +1945,20 @@ function McpToolEditor({ }) { const { t } = useTranslation("create"); const conflictErrorId = useId(); + const [revealedTokenIndex, setRevealedTokenIndex] = useState( + null, + ); const visibleConflict = showConflict ? conflict : null; const update = (i: number, p: Partial) => onChange(tools.map((tool, idx) => (idx === i ? { ...tool, ...p } : tool))); - const remove = (i: number) => onChange(tools.filter((_, idx) => idx !== i)); + const remove = (i: number) => { + setRevealedTokenIndex((current) => { + if (current == null || current < i) return current; + return current === i ? null : current - 1; + }); + onChange(tools.filter((_, idx) => idx !== i)); + }; const add = () => onChange([...tools, { name: "", transport: "http", url: "" }]); @@ -2024,119 +2056,49 @@ function McpToolEditor({

)} - - onChange( - tools.map((tool, index) => - index === i - ? updateMcpAuthTokenInput(tool, e.target.value) - : tool, - ), - ) - } - /> - {tool.credentialUpdate === "pending" && ( -
+ + onChange( + tools.map((tool, index) => + index === i + ? updateMcpAuthTokenInput(tool, e.target.value) + : tool, + ), + ) + } + /> + - - -
- - )} - {tool.credentialUpdate === "reuse" && ( -
- {t("traditional.mcp.reuseHint")} - -
- )} - {tool.credentialConfigured && - !tool.authToken && - !tool.credentialUpdate && ( -
- {t("traditional.mcp.credentialConfigured")} - -
- )} +