Skip to content

Commit 34da392

Browse files
committed
load mcp ui app cards on cloud task runs
1 parent 9d72d59 commit 34da392

4 files changed

Lines changed: 74 additions & 1 deletion

File tree

packages/core/src/mcp-apps/mcp-apps.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class McpAppsService extends TypedEventEmitter<McpAppsServiceEvents> {
6666
private toolAssociations = new Map<string, McpToolUiAssociation>();
6767
private toolDefinitions = new Map<string, Tool>();
6868
private serverConfigs = new Map<string, McpServerConnectionConfig>();
69+
private configResolver?: (serverName: string) => Promise<void>;
6970
private pendingConnections = new Map<string, Promise<ServerConnection>>();
7071
private pendingFetches = new Map<string, Promise<McpUiResource | null>>();
7172
private resourceMetaCache = new Map<string, McpResourceUiMeta>();
@@ -93,6 +94,29 @@ export class McpAppsService extends TypedEventEmitter<McpAppsServiceEvents> {
9394
}
9495
}
9596

97+
/**
98+
* Merge server configs without clearing existing ones. Cloud runs never run a
99+
* local agent session (the agent lives in the sandbox), so setServerConfigs is
100+
* never called for them and a cloud run's UI-app resource fetch has no config
101+
* to connect through ("No server config for: posthog"). This registers the
102+
* config on demand so the card can load.
103+
*/
104+
addServerConfigs(configs: McpServerConnectionConfig[]): void {
105+
for (const config of configs) {
106+
this.serverConfigs.set(config.name, config);
107+
}
108+
}
109+
110+
/**
111+
* Register a fallback that lazily supplies a missing server config (expected to
112+
* call addServerConfigs). getOrCreateConnection invokes it when a config is
113+
* absent — the path cloud runs hit, since no local session ever registered
114+
* their servers — so a UI-app resource fetch self-heals instead of throwing.
115+
*/
116+
setConfigResolver(resolver: (serverName: string) => Promise<void>): void {
117+
this.configResolver = resolver;
118+
}
119+
96120
/**
97121
* Called when the agent confirms MCP servers are connected.
98122
* Connects to each server, calls listTools() to discover _meta.ui fields
@@ -206,7 +230,11 @@ export class McpAppsService extends TypedEventEmitter<McpAppsServiceEvents> {
206230
return pending;
207231
}
208232

209-
const config = this.serverConfigs.get(serverName);
233+
let config = this.serverConfigs.get(serverName);
234+
if (!config && this.configResolver) {
235+
await this.configResolver(serverName);
236+
config = this.serverConfigs.get(serverName);
237+
}
210238
if (!config) {
211239
throw new Error(`No server config for: ${serverName}`);
212240
}

packages/workspace-server/src/services/agent/agent.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,17 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
440440
this.log = loggerFactory.scope("agent-service");
441441
this.onAgentLog = makeOnAgentLog(loggerFactory);
442442

443+
// Cloud runs never start a local session (the agent lives in the sandbox), so
444+
// getOrCreateSession never registers their MCP servers with the mcp-apps
445+
// service. Resolve them on demand from the current auth state the first time a
446+
// cloud-run UI-app resource is fetched, so the review card loads.
447+
this.mcpAppsService.setConfigResolver(async () => {
448+
const credentials = await this.agentAuthAdapter.getCurrentCredentials();
449+
if (credentials) {
450+
await this.ensureMcpAppsServerConfigs(credentials);
451+
}
452+
});
453+
443454
powerManager.onResume(() => this.checkIdleDeadlines());
444455
}
445456

@@ -698,6 +709,26 @@ If a repository IS genuinely required, attach one in this priority order:
698709
return this.toSessionResponse(session);
699710
}
700711

712+
/**
713+
* Register the MCP server configs (posthog + installations) with the mcp-apps
714+
* service without starting an agent session. A cloud run's agent lives in the
715+
* sandbox, so getOrCreateSession never runs on the desktop and the mcp-apps
716+
* service has no config to fetch a UI-app resource through — the review card
717+
* then fails with "No server config for: posthog" and renders as text. The
718+
* task view calls this when a cloud-run tool result carries a UI app.
719+
*/
720+
async ensureMcpAppsServerConfigs(credentials: Credentials): Promise<void> {
721+
const { servers } =
722+
await this.agentAuthAdapter.buildMcpServers(credentials);
723+
this.mcpAppsService.addServerConfigs(
724+
servers.map((s) => ({
725+
name: s.name,
726+
url: s.url,
727+
headers: Object.fromEntries(s.headers.map((h) => [h.name, h.value])),
728+
})),
729+
);
730+
}
731+
701732
async reconnectSession(
702733
params: ReconnectSessionInput,
703734
): Promise<SessionResponse | null> {

packages/workspace-server/src/services/agent/auth-adapter.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ export class AgentAuthAdapter {
7979
};
8080
}
8181

82+
/**
83+
* The current signed-in credentials from auth state, or null when no project is
84+
* selected. Lets the mcp-apps config resolver register servers for a cloud run
85+
* without a session (where the renderer never supplies credentials).
86+
*/
87+
async getCurrentCredentials(): Promise<Credentials | null> {
88+
const { apiHost } = await this.authService.getValidAccessToken();
89+
const projectId = this.authService.getState().currentProjectId;
90+
return projectId === null ? null : { apiHost, projectId };
91+
}
92+
8293
async buildMcpServers(credentials: Credentials): Promise<{
8394
servers: AcpMcpServer[];
8495
toolApprovals: McpToolApprovals;

packages/workspace-server/src/services/agent/ports.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface AgentMcpServerConnectionConfig {
2828
export interface AgentMcpApps {
2929
handleDiscovery(serverNames: string[]): Promise<void>;
3030
setServerConfigs(configs: AgentMcpServerConnectionConfig[]): void;
31+
addServerConfigs(configs: AgentMcpServerConnectionConfig[]): void;
32+
setConfigResolver(resolver: (serverName: string) => Promise<void>): void;
3133
notifyToolCancelled(toolKey: string, toolCallId: string): void;
3234
notifyToolInput(toolKey: string, toolCallId: string, args: unknown): void;
3335
notifyToolResult(
@@ -56,6 +58,7 @@ type AgentFetchLike = (
5658
export interface AgentAuth {
5759
getValidAccessToken(): Promise<{ accessToken: string; apiHost: string }>;
5860
refreshAccessToken(): Promise<{ accessToken: string; apiHost: string }>;
61+
getState(): { currentProjectId: number | null };
5962
authenticatedFetch(
6063
fetchImpl: AgentFetchLike,
6164
input: string | Request,

0 commit comments

Comments
 (0)