Skip to content
Draft
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
118 changes: 118 additions & 0 deletions apps/api/src/handlers/mcp/__tests__/screenshot-preparation.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions apps/api/src/handlers/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { vercelMcp } from './vercel';
import { createHttpIntegrationsMcp } from './http-integrations';
import { developmentFixturesMcp } from './development-fixtures';
import { publicUrlFetchRoute } from './public-url-fetch-route';
import { screenshotPreparationRoute } from './screenshot-preparation';

export const mcp = new Hono<{ Variables: Variables }>();

Expand Down Expand Up @@ -75,6 +76,8 @@ mcp.route('/custom/:serverId', createCustomMcpProxy());
mcp.route('/development-fixtures', developmentFixturesMcp);
mcp.use('/public-url-fetch', mcpAuthMiddleware);
mcp.route('/public-url-fetch', publicUrlFetchRoute);
mcp.use('/screenshot-preparation', mcpAuthMiddleware);
mcp.route('/screenshot-preparation', screenshotPreparationRoute);

// Brain (deployment-hosted gbrain): a native-mode catalog
// integration with a custom handler, like snowflake/grafana below. The
Expand Down
52 changes: 52 additions & 0 deletions apps/api/src/handlers/mcp/screenshot-preparation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Hono } from 'hono';

import { Env } from '@roomote/env';
import {
screenshotPreparationInputSchema,
type ScreenshotPreparationInput,
} from '@roomote/types';
import { prepareScreenshotStep } from '@roomote/cloud-agents/server/screenshot-preparation';

import type { Variables } from '../../types';
import type { McpAuth } from './middleware';

export const screenshotPreparationRoute = new Hono<{
Variables: Variables & { mcpAuth: McpAuth };
}>();

screenshotPreparationRoute.post('/', async (c) => {
const auth = c.get('mcpAuth');

if (auth.authContext.tokenType !== 'run' || !auth.authContext.runId) {
return c.json(
{ error: 'Screenshot preparation requires a task run token.' },
403,
);
}

let body: unknown;
try {
body = await c.req.json();
} catch {
return c.json(
{ error: 'A valid screenshot preparation request is required.' },
400,
);
}

const parsed = screenshotPreparationInputSchema.safeParse(body);
if (!parsed.success) {
return c.json(
{ error: parsed.error.issues.map((issue) => issue.message).join(' ') },
400,
);
}

const result = await prepareScreenshotStep({
runId: String(auth.authContext.runId),
input: parsed.data as ScreenshotPreparationInput,
enabled: Env.R_SCREENSHOT_PREPARATION_JEV_ENABLED,
});

return c.json(result);
});
1 change: 1 addition & 0 deletions apps/docs/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ manifest changes.
| `R_VOICE_OPENAI_API_KEY` | Optional | OpenAI key dedicated to GPT-Live voice conversations. Requires project access to `gpt-live-1`; the general `OPENAI_API_KEY` is not used for Voice. The key stays on the control plane; browsers receive only a server-negotiated WebRTC session answer. |
| `R_TYPESAFE_API_KEY` | Optional | TypeSafe key for the optional [judgment model](/models#judgment-model). A key alone turns on Jev via TypeSafe unless `R_JUDGMENT_MODEL` or Settings chooses otherwise. The key stays on the control plane. |
| `R_JUDGMENT_MODEL` | Optional | Selects the [judgment model](/models#judgment-model): `typesafe`, `openrouter` (using `OPENROUTER_API_KEY`), `vercel` (using `AI_GATEWAY_API_KEY`), or `off`. Overrides the choice in **Settings > Models**. |
| `R_SCREENSHOT_PREPARATION_JEV_ENABLED` | Optional | Explicitly enables the bounded Jev screenshot-preparation prototype. It is off by default; the existing browser capture flow remains the fallback, and the prototype never receives or executes browser commands. |
| `R_JUDGMENT_UPSTREAM_URL` | Optional | Base URL of a Roomote-run judgment model that answers the same typed decisions request as Jev at `/v1/decisions`. It is evaluation-only: nothing acts on its answers, and it is called only when `R_JUDGMENT_SHADOW` is `on`. |
| `R_JUDGMENT_UPSTREAM_API_KEY` | Optional | Bearer credential for `R_JUDGMENT_UPSTREAM_URL`. Leave unset for a private-network upstream without auth. The credential stays on the control plane. |
| `R_JUDGMENT_SHADOW` | Optional | `on` also scores every Jev [judgment](/models#judgment-model) with the upstream at `R_JUDGMENT_UPSTREAM_URL` and logs how the two agree (question keys and probabilities only), without changing the answer Roomote acts on. |
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions apps/worker/src/mcp/roomote-mcp-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
LIST_REPOSITORIES_DEFAULT_LIMIT,
LIST_REPOSITORIES_MAX_LIMIT,
LIST_REPOSITORIES_TOOL_NAME,
SCREENSHOT_PREPARATION_TOOL,
} from '@roomote/types';
import {
captureWorkerException,
Expand Down Expand Up @@ -116,6 +117,7 @@ import { handleGetRelayUpdates } from './relay-updates.js';
import { handleCloneRepository } from './clone-repository.js';
import { handleListRepositories } from './list-repositories.js';
import { handlePublicUrlFetch } from './public-url-fetch.js';
import { handlePrepareScreenshot } from './screenshot-preparation.js';
import {
CLONE_REPOSITORY_TOOL_NAME,
ON_DEMAND_REPOSITORIES_MANIFEST_FILE,
Expand Down Expand Up @@ -150,6 +152,24 @@ roomoteMcpServer.registerTool(
},
);

roomoteMcpServer.registerTool(
SCREENSHOT_PREPARATION_TOOL.name,
{
title: SCREENSHOT_PREPARATION_TOOL.title,
description: SCREENSHOT_PREPARATION_TOOL.description,
inputSchema: SCREENSHOT_PREPARATION_TOOL.inputSchema,
annotations: SCREENSHOT_PREPARATION_TOOL.annotations,
},
async (params, extra): Promise<ToolResult> => {
const config = getRoomoteConfig();
if (!config) {
return errorResult('ROOMOTE_CLOUD_TOKEN environment variable not set');
}

return handlePrepareScreenshot(params, config, extra.signal);
},
);

let hasSubmittedAutomationSlackSummary = false;
const manageArtifactsUploadTypeSchema = z.enum(['general', 'visual-proof']);
const nonEmptyStringSchema = z.string().refine((value) => value.length > 0, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
screenshotPreparationInputSchema,
type ScreenshotPreparationResponse,
} from '@roomote/types';

import {
buildApiHeaders,
fetchWithTimeout,
parseApiError,
} from './api-client.js';
import { catchError, successResult } from './tool-result.js';
import type { RoomoteConfig, ToolResult } from './types.js';

export async function handlePrepareScreenshot(
input: unknown,
config: RoomoteConfig,
signal?: AbortSignal,
): Promise<ToolResult> {
try {
const params = screenshotPreparationInputSchema.parse(input);
const response = await fetchWithTimeout(
`${config.platformApiUrl}/api/mcp/screenshot-preparation`,
{
method: 'POST',
headers: buildApiHeaders(config, {
'Content-Type': 'application/json',
}),
body: JSON.stringify(params),
signal,
},
{ label: 'Screenshot preparation failed', timeoutMs: 10_000 },
);

if (!response.ok) {
throw new Error(await parseApiError(response));
}

const result = (await response.json()) as ScreenshotPreparationResponse;
return successResult(result as unknown as Record<string, unknown>);
} catch (error) {
return catchError(error);
}
}
Loading
Loading