From 7a9212fdd58cf4f38934224e397dc15bbc3fc53f Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:22:11 +0000 Subject: [PATCH 1/3] feat(server): extension calls and MCP App routes for host apps Redrob Desktop calls three engine routes that answered 404: - POST /experimental/extensions/call { extensionId, action, args } The image extension (openai-image-gen: generate, edit, blend) through the Redrob gateway on the engine's Redrob key, the same path the image_generate tool uses. args: prompt, size ("1536x1024", sent as image_config.aspect_ratio), images (file:// paths or data URLs, 1 for edit, 2-4 for blend), model. Answers 200 { ok, result: { b64, mime, model, costUsd? } } or { ok: false, error }. - POST /mcp-apps/resolve { server, uri } -> { html, mimeType?, server } Reads a connector's ui:// resource (text/html only, 2 MB cap). - POST /mcp-apps/call { server, name, arguments? } -> the MCP CallToolResult. server matches a connected client by exact name, sanitised name, or the single client whose tool-name prefix it is. Unknown server is 400, MCP failures 502, so a host can tell them from a missing route (404). Unblocks Image Studio (CH-075), image result actions (CH-119), the image settings test (CH-232) and MCP App views (CH-148). --- .../src/server/routes/instance/httpapi/api.ts | 2 + .../instance/httpapi/groups/extensions.ts | 119 ++++++++++ .../instance/httpapi/handlers/extensions.ts | 208 ++++++++++++++++++ .../server/routes/instance/httpapi/server.ts | 2 + packages/redrob/src/tool/image_generate.ts | 2 +- 5 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 packages/redrob/src/server/routes/instance/httpapi/groups/extensions.ts create mode 100644 packages/redrob/src/server/routes/instance/httpapi/handlers/extensions.ts diff --git a/packages/redrob/src/server/routes/instance/httpapi/api.ts b/packages/redrob/src/server/routes/instance/httpapi/api.ts index 25afe4b856..d7457036ec 100644 --- a/packages/redrob/src/server/routes/instance/httpapi/api.ts +++ b/packages/redrob/src/server/routes/instance/httpapi/api.ts @@ -12,6 +12,7 @@ import { ControlApi } from "./groups/control" import { ControlPlaneApi } from "./groups/control-plane" import { EventApi } from "./groups/event" import { ExperimentalApi } from "./groups/experimental" +import { ExtensionsApi } from "./groups/extensions" import { FileApi } from "./groups/file" import { InstanceApi } from "./groups/instance" import { McpApi } from "./groups/mcp" @@ -64,6 +65,7 @@ export const RootHttpApi = HttpApi.make("redrob-root") export const InstanceHttpApi = HttpApi.make("redrob-instance") .addHttpApi(ConfigApi) .addHttpApi(ExperimentalApi) + .addHttpApi(ExtensionsApi) .addHttpApi(FileApi) .addHttpApi(InstanceApi) .addHttpApi(McpApi) diff --git a/packages/redrob/src/server/routes/instance/httpapi/groups/extensions.ts b/packages/redrob/src/server/routes/instance/httpapi/groups/extensions.ts new file mode 100644 index 0000000000..9c347f515f --- /dev/null +++ b/packages/redrob/src/server/routes/instance/httpapi/groups/extensions.ts @@ -0,0 +1,119 @@ +import { Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { InvalidRequestError, UpstreamError } from "../errors" +import { Authorization } from "../middleware/authorization" +import { InstanceContextMiddleware } from "../middleware/instance-context" +import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing" +import { described } from "./metadata" + +/** + * Routes a host app (Redrob Desktop) calls for features that run inside the engine because the + * engine holds the credential or the MCP connection: + * + * - `POST /experimental/extensions/call`: a named extension action. Today the image extension + * (`openai-image-gen`: generate, edit, blend), served through the Redrob gateway on the engine's + * Redrob key, the same account the chat is billed to. Failures answer 200 with `ok: false`, so a + * host can tell "the engine has no such route" (404) from "the engine refused this call". + * - `POST /mcp-apps/resolve` and `POST /mcp-apps/call`: an MCP App view (a connector's `ui://` + * resource) and a tool call made from inside that view, after the person confirmed it in the host. + */ +export const ExtensionCallPayload = Schema.Struct({ + extensionId: Schema.String, + action: Schema.String, + args: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), +}) + +export const ExtensionCallResponse = Schema.Struct({ + ok: Schema.Boolean, + result: Schema.optional( + Schema.Struct({ + b64: Schema.optional(Schema.String), + mime: Schema.optional(Schema.String), + path: Schema.optional(Schema.String), + model: Schema.optional(Schema.String), + costUsd: Schema.optional(Schema.Number), + }), + ), + error: Schema.optional(Schema.String), +}).annotate({ identifier: "ExtensionCallResponse" }) + +export const McpAppResolvePayload = Schema.Struct({ + server: Schema.String.annotate({ description: "MCP server name, or the prefix of its projected tool names" }), + uri: Schema.String.annotate({ description: "The ui:// resource the tool result pointed at" }), +}) + +export const McpAppResolveResponse = Schema.Struct({ + html: Schema.String, + mimeType: Schema.optional(Schema.String), + server: Schema.String, +}).annotate({ identifier: "McpAppResource" }) + +export const McpAppCallPayload = Schema.Struct({ + server: Schema.String, + name: Schema.String.annotate({ description: "Tool name as the MCP server knows it" }), + arguments: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), +}) + +export const ExtensionsPaths = { + call: "/experimental/extensions/call", + mcpAppResolve: "/mcp-apps/resolve", + mcpAppCall: "/mcp-apps/call", +} as const + +export const ExtensionsApi = HttpApi.make("extensions") + .add( + HttpApiGroup.make("extensions") + .add( + HttpApiEndpoint.post("call", ExtensionsPaths.call, { + query: WorkspaceRoutingQuery, + payload: ExtensionCallPayload, + success: described(ExtensionCallResponse, "The extension's answer"), + }).annotateMerge( + OpenApi.annotations({ + identifier: "experimental.extensions.call", + summary: "Call an extension", + description: "Run one action of a built-in extension (image generation) with the engine's credential.", + }), + ), + HttpApiEndpoint.post("mcpAppResolve", ExtensionsPaths.mcpAppResolve, { + query: WorkspaceRoutingQuery, + payload: McpAppResolvePayload, + success: described(McpAppResolveResponse, "The app view's HTML"), + error: [InvalidRequestError, UpstreamError], + }).annotateMerge( + OpenApi.annotations({ + identifier: "mcp.app.resolve", + summary: "Resolve an MCP App view", + description: "Read a connector's ui:// resource so a host can show it in a sandboxed frame.", + }), + ), + HttpApiEndpoint.post("mcpAppCall", ExtensionsPaths.mcpAppCall, { + query: WorkspaceRoutingQuery, + payload: McpAppCallPayload, + success: described(Schema.Unknown, "The MCP tool result"), + error: [InvalidRequestError, UpstreamError], + }).annotateMerge( + OpenApi.annotations({ + identifier: "mcp.app.call", + summary: "Call a tool from an MCP App view", + description: "Call a connector's tool for its app view. The host confirms the call with the person first.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "extensions", + description: "Extension calls and MCP App views for host apps.", + }), + ) + .middleware(InstanceContextMiddleware) + .middleware(WorkspaceRoutingMiddleware) + .middleware(Authorization), + ) + .annotateMerge( + OpenApi.annotations({ + title: "redrob HttpApi", + version: "0.0.1", + description: "Effect HttpApi surface for instance routes.", + }), + ) diff --git a/packages/redrob/src/server/routes/instance/httpapi/handlers/extensions.ts b/packages/redrob/src/server/routes/instance/httpapi/handlers/extensions.ts new file mode 100644 index 0000000000..5795433cbb --- /dev/null +++ b/packages/redrob/src/server/routes/instance/httpapi/handlers/extensions.ts @@ -0,0 +1,208 @@ +import { Auth } from "@/auth" +import { MCP } from "@/mcp" +import { McpCatalog } from "@/mcp/catalog" +import { + DEFAULT_IMAGE_MODEL, + decodeImageDataUrl, + decodeResponse, + gatewayMessage, + gatewayUrl, + redrobKey, +} from "@/tool/image_generate" +import { SharedAccount } from "@redrob-code/core/shared-account" +import { Effect, Schema } from "effect" +import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { readFile } from "node:fs/promises" +import * as path from "node:path" +import { InstanceHttpApi } from "../api" +import { InvalidRequestError, UpstreamError } from "../errors" +import type { ExtensionCallPayload, McpAppCallPayload, McpAppResolvePayload } from "../groups/extensions" + +/** The image extension's id as the desktop names it (Cowork's `openai-image-gen`). */ +const IMAGE_EXTENSIONS = new Set(["openai-image-gen", "image-gen"]) +const IMAGE_ACTIONS = new Set(["generate", "edit", "blend"]) +const IMAGE_TIMEOUT_MS = 180_000 +const MAX_REFERENCE_BYTES = 20 * 1024 * 1024 +const MAX_REFERENCES = 4 +const REFERENCE_MIME: Record = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".gif": "image/gif", +} + +/** MCP Apps limits, as Cowork's mcp-app-host had them. */ +const MAX_APP_HTML_BYTES = 2 * 1024 * 1024 +const APP_CALL_TIMEOUT_MS = 120_000 + +/** `1536x1024` -> `3:2`, the aspect the gateway's image models take. */ +function aspectOf(size: unknown): string | undefined { + const match = typeof size === "string" ? /^(\d+)x(\d+)$/.exec(size.trim()) : null + if (!match) return undefined + const w = Number(match[1]) + const h = Number(match[2]) + if (!w || !h) return undefined + const gcd = (a: number, b: number): number => (b ? gcd(b, a % b) : a) + const d = gcd(w, h) + return `${w / d}:${h / d}` +} + +function message(error: unknown): string { + if (error instanceof Error) return error.message + if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message + return String(error) +} + +/** A reference image as a data URL: a `file://` path the desktop sent, or a data URL as is. */ +async function referenceUrl(ref: string): Promise { + if (ref.startsWith("data:image/")) return ref + const file = ref.startsWith("file://") ? ref.slice("file://".length) : ref + const mime = REFERENCE_MIME[path.extname(file).toLowerCase()] + if (!mime) throw new Error(`Not an image this engine can send: ${path.basename(file)}`) + const bytes = await readFile(file) + if (bytes.byteLength === 0 || bytes.byteLength > MAX_REFERENCE_BYTES) + throw new Error(`${path.basename(file)} is empty or larger than 20 MB.`) + return `data:${mime};base64,${bytes.toString("base64")}` +} + +function imagePrompt(action: string, prompt: string, references: number): string { + if (action === "edit") return `Edit the attached image. ${prompt}` + if (action === "blend") return `Blend the ${references} attached images into one new image. ${prompt}` + return prompt +} + +export const extensionsHandlers = HttpApiBuilder.group(InstanceHttpApi, "extensions", (handlers) => + Effect.gen(function* () { + const http = yield* HttpClient.HttpClient + const auth = yield* Auth.Service + const mcp = yield* MCP.Service + + const image = Effect.fnUntraced(function* (action: string, args: Record) { + if (!IMAGE_ACTIONS.has(action)) return { ok: false, error: `Unknown image action: ${action}` } + const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "" + if (!prompt) return { ok: false, error: "Describe the image to make." } + const refs = Array.isArray(args.images) ? args.images.filter((x): x is string => typeof x === "string") : [] + if (action === "edit" && refs.length !== 1) return { ok: false, error: "Edit takes exactly one image." } + if (action === "blend" && refs.length < 2) return { ok: false, error: "Blend takes two or more images." } + if (refs.length > MAX_REFERENCES) return { ok: false, error: `At most ${MAX_REFERENCES} reference images.` } + + const stored = yield* auth.get("redrob").pipe(Effect.orElseSucceed(() => undefined)) + const key = redrobKey(stored) ?? (yield* Effect.promise(() => SharedAccount.apiKey())) + if (!key) return { ok: false, error: "Sign in to Redrob to make images: no Redrob API key is available." } + + const model = typeof args.model === "string" && args.model.trim() ? args.model.trim() : DEFAULT_IMAGE_MODEL + const urls = yield* Effect.tryPromise({ + try: () => Promise.all(refs.map(referenceUrl)), + catch: (error) => new Error(message(error)), + }) + const text = imagePrompt(action, prompt, urls.length) + const aspect = aspectOf(args.size) + const request = yield* HttpClientRequest.post(`${gatewayUrl()}/chat/completions`).pipe( + HttpClientRequest.bearerToken(key), + HttpClientRequest.acceptJson, + HttpClientRequest.bodyJson({ + model, + messages: [ + { + role: "user", + content: urls.length + ? [{ type: "text", text }, ...urls.map((url) => ({ type: "image_url", image_url: { url } }))] + : text, + }, + ], + modalities: ["image", "text"], + ...(aspect ? { image_config: { aspect_ratio: aspect } } : {}), + }), + ) + const response = yield* http + .execute(request) + .pipe(Effect.timeoutOrElse({ duration: IMAGE_TIMEOUT_MS, orElse: () => Effect.fail(new Error("Image generation timed out.")) })) + const body = yield* response.text + if (response.status < 200 || response.status >= 300) + return { ok: false, error: `The Redrob gateway answered ${response.status}: ${gatewayMessage(body)}` } + const payload = yield* decodeResponse(body) + const reply = payload.choices[0]?.message + const picture = (reply?.images ?? []).map((item) => decodeImageDataUrl(item.image_url.url)).find(Boolean) + if (!picture) { + const said = reply?.content?.trim() + return { ok: false, error: `${model} returned no image${said ? `. It said: ${said.slice(0, 500)}` : "."}` } + } + const costUsd = payload.redrob?.costUsd + return { + ok: true, + result: { + b64: Buffer.from(picture.bytes).toString("base64"), + mime: picture.mime, + model, + ...(costUsd === undefined ? {} : { costUsd }), + }, + } + }) + + const call = Effect.fn("ExtensionsHttpApi.call")(function* (ctx: { + payload: Schema.Schema.Type + }) { + const { extensionId, action, args } = ctx.payload + if (!IMAGE_EXTENSIONS.has(extensionId)) return { ok: false, error: `Unknown extension: ${extensionId}` } + return yield* image(action, args ?? {}).pipe( + Effect.catch((error) => Effect.succeed({ ok: false, error: message(error) })), + ) + }) + + /** The connected client a host means: its exact name, its sanitised name, or the prefix of its tool names. */ + const client = Effect.fnUntraced(function* (server: string) { + const clients = yield* mcp.clients() + if (clients[server]) return { name: server, client: clients[server] } + const names = Object.keys(clients) + const exact = names.find((name) => McpCatalog.sanitize(name) === server) + const prefixed = names.filter((name) => McpCatalog.sanitize(name).startsWith(`${server}_`)) + const name = exact ?? (prefixed.length === 1 ? prefixed[0] : undefined) + if (!name) + return yield* new InvalidRequestError({ message: `No connected MCP server named ${server}`, field: "server" }) + return { name, client: clients[name] } + }) + + const mcpAppResolve = Effect.fn("ExtensionsHttpApi.mcpAppResolve")(function* (ctx: { + payload: Schema.Schema.Type + }) { + const { uri } = ctx.payload + if (!uri.startsWith("ui://")) + return yield* new InvalidRequestError({ message: "An MCP App view is a ui:// resource.", field: "uri" }) + const { name } = yield* client(ctx.payload.server) + const read = yield* mcp.readResource(name, uri) + if (!read) return yield* new UpstreamError({ message: `${name} did not return ${uri}`, service: name }) + const item = read.contents.find((c) => c.uri === uri) ?? read.contents[0] + if (!item) return yield* new UpstreamError({ message: `${name} returned ${uri} empty`, service: name }) + if (item.mimeType && !item.mimeType.startsWith("text/html")) + return yield* new InvalidRequestError({ message: `${uri} is ${item.mimeType}, not an HTML view.`, field: "uri" }) + const html = + "text" in item && typeof item.text === "string" + ? item.text + : "blob" in item && typeof item.blob === "string" + ? Buffer.from(item.blob, "base64").toString("utf8") + : "" + if (!html) return yield* new UpstreamError({ message: `${name} returned ${uri} empty`, service: name }) + if (Buffer.byteLength(html) > MAX_APP_HTML_BYTES) + return yield* new UpstreamError({ message: `${uri} is larger than 2 MB`, service: name }) + return { html, mimeType: item.mimeType, server: name } + }) + + const mcpAppCall = Effect.fn("ExtensionsHttpApi.mcpAppCall")(function* (ctx: { + payload: Schema.Schema.Type + }) { + const target = yield* client(ctx.payload.server) + const result: unknown = yield* Effect.tryPromise({ + try: () => + target.client.callTool({ name: ctx.payload.name, arguments: ctx.payload.arguments ?? {} }, undefined, { + timeout: APP_CALL_TIMEOUT_MS, + }), + catch: (error) => new UpstreamError({ message: message(error), service: target.name }), + }) + return result + }) + + return handlers.handle("call", call).handle("mcpAppResolve", mcpAppResolve).handle("mcpAppCall", mcpAppCall) + }), +) diff --git a/packages/redrob/src/server/routes/instance/httpapi/server.ts b/packages/redrob/src/server/routes/instance/httpapi/server.ts index d2915064f1..862c1741ce 100644 --- a/packages/redrob/src/server/routes/instance/httpapi/server.ts +++ b/packages/redrob/src/server/routes/instance/httpapi/server.ts @@ -92,6 +92,7 @@ import { fileHandlers } from "./handlers/file" import { globalHandlers } from "./handlers/global" import { instanceHandlers } from "./handlers/instance" import { mcpHandlers } from "./handlers/mcp" +import { extensionsHandlers } from "./handlers/extensions" import { memoryHandlers } from "./handlers/memory" import { permissionHandlers } from "./handlers/permission" import { projectHandlers } from "./handlers/project" @@ -158,6 +159,7 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe( Layer.provide([ configHandlers, experimentalHandlers, + extensionsHandlers, fileHandlers, instanceHandlers, mcpHandlers, diff --git a/packages/redrob/src/tool/image_generate.ts b/packages/redrob/src/tool/image_generate.ts index 12ff427f09..5af3b09a6f 100644 --- a/packages/redrob/src/tool/image_generate.ts +++ b/packages/redrob/src/tool/image_generate.ts @@ -68,7 +68,7 @@ const GatewayResponse = Schema.Struct({ redrob: Schema.optional(Schema.Struct({ costUsd: Schema.optional(Schema.Number) })), }) -const decodeResponse = Schema.decodeUnknownEffect(Schema.fromJsonString(GatewayResponse)) +export const decodeResponse = Schema.decodeUnknownEffect(Schema.fromJsonString(GatewayResponse)) /** The Redrob key: the engine's stored credential first, then the environment, as the provider resolves it. */ export function redrobKey(stored: Auth.Info | undefined): string | undefined { From 0c2e5e39687eb2d49e02af28f204f9f8b55a1eaa Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:26:08 +0000 Subject: [PATCH 2/3] fix(windows): hide console windows of the last unhidden engine spawns The shared spawn helpers already pass windowsHide on Windows (core cross-spawn-spawner, redrob util/process, MCP SDK stdio transport), so shell, ripgrep, git, LSP, formatters and MCP stdio servers stay hidden. The remaining gaps were the detached daemon started by `connect` (a detached child gets its own console) and the SDK's `redrob serve` launch. --- packages/redrob/src/daemon/connect.ts | 2 ++ packages/sdk/js/src/server.ts | 1 + packages/sdk/js/src/v2/server.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/packages/redrob/src/daemon/connect.ts b/packages/redrob/src/daemon/connect.ts index 059a3ff377..3027ea0dee 100644 --- a/packages/redrob/src/daemon/connect.ts +++ b/packages/redrob/src/daemon/connect.ts @@ -87,6 +87,8 @@ export async function connectOrStart(options: StartOptions = {}): Promise Date: Wed, 28 Oct 2026 03:34:42 +0000 Subject: [PATCH 3/3] chore(sdk): regenerate for the extension and MCP App routes --- packages/sdk/js/src/v2/gen/sdk.gen.ts | 593 +++++++++++++++--------- packages/sdk/js/src/v2/gen/types.gen.ts | 133 ++++++ 2 files changed, 504 insertions(+), 222 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 3361cc54bf..6333bb0c9e 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -43,6 +43,8 @@ import type { ExperimentalConsoleSwitchOrgResponses, ExperimentalControlPlaneMoveSessionErrors, ExperimentalControlPlaneMoveSessionResponses, + ExperimentalExtensionsCallErrors, + ExperimentalExtensionsCallResponses, ExperimentalProjectCopyGenerateNameErrors, ExperimentalProjectCopyGenerateNameResponses, ExperimentalResourceListErrors, @@ -100,6 +102,10 @@ import type { LspStatusResponses, McpAddErrors, McpAddResponses, + McpAppCallErrors, + McpAppCallResponses, + McpAppResolveErrors, + McpAppResolveResponses, McpAuthAuthenticateErrors, McpAuthAuthenticateResponses, McpAuthCallbackErrors, @@ -1087,6 +1093,55 @@ export class Resource extends HeyApiClient { } } +export class Extensions extends HeyApiClient { + /** + * Call an extension + * + * Run one action of a built-in extension (image generation) with the engine's credential. + */ + public call( + parameters?: { + directory?: string + workspace?: string + extensionId?: string + action?: string + args?: { + [key: string]: unknown + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "extensionId" }, + { in: "body", key: "action" }, + { in: "body", key: "args" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalExtensionsCallResponses, + ExperimentalExtensionsCallErrors, + ThrowOnError + >({ + url: "/experimental/extensions/call", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class ProjectCopy extends HeyApiClient { /** * Generate project copy name @@ -1431,6 +1486,11 @@ export class Experimental extends HeyApiClient { return (this._resource ??= new Resource({ client: this.client })) } + private _extensions?: Extensions + get extensions(): Extensions { + return (this._extensions ??= new Extensions({ client: this.client })) + } + private _projectCopy?: ProjectCopy get projectCopy(): ProjectCopy { return (this._projectCopy ??= new ProjectCopy({ client: this.client })) @@ -1887,17 +1947,18 @@ export class Worktree extends HeyApiClient { } } -export class Find extends HeyApiClient { +export class App2 extends HeyApiClient { /** - * Find text + * Resolve an MCP App view * - * Search for text patterns across files in the project using ripgrep. + * Read a connector's ui:// resource so a host can show it in a sandboxed frame. */ - public text( - parameters: { + public resolve( + parameters?: { directory?: string workspace?: string - pattern: string + server?: string + uri?: string }, options?: Options, ) { @@ -1908,31 +1969,38 @@ export class Find extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "pattern" }, + { in: "body", key: "server" }, + { in: "body", key: "uri" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/find", + return (options?.client ?? this.client).post({ + url: "/mcp-apps/resolve", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } /** - * Find files + * Call a tool from an MCP App view * - * Search for files or directories by name or pattern in the project directory. + * Call a connector's tool for its app view. The host confirms the call with the person first. */ - public files( - parameters: { + public call( + parameters?: { directory?: string workspace?: string - query: string - dirs?: "true" | "false" - type?: "file" | "directory" - limit?: number + server?: string + name?: string + arguments?: { + [key: string]: unknown + } }, options?: Options, ) { @@ -1943,31 +2011,37 @@ export class Find extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "query" }, - { in: "query", key: "dirs" }, - { in: "query", key: "type" }, - { in: "query", key: "limit" }, + { in: "body", key: "server" }, + { in: "body", key: "name" }, + { in: "body", key: "arguments" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/find/file", + return (options?.client ?? this.client).post({ + url: "/mcp-apps/call", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } +} +export class Auth2 extends HeyApiClient { /** - * Find symbols + * Remove MCP OAuth * - * Search for workspace symbols like functions, classes, and variables using LSP. + * Remove OAuth credentials for an MCP server. */ - public symbols( + public remove( parameters: { + name: string directory?: string workspace?: string - query: string }, options?: Options, ) { @@ -1976,32 +2050,30 @@ export class Find extends HeyApiClient { [ { args: [ + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "query" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/find/symbol", + return (options?.client ?? this.client).delete({ + url: "/mcp/{name}/auth", ...options, ...params, }) } -} -export class File extends HeyApiClient { /** - * List files + * Start MCP OAuth * - * List files and directories in a specified path. + * Start OAuth authentication flow for a Model Context Protocol (MCP) server. */ - public list( + public start( parameters: { + name: string directory?: string workspace?: string - path: string }, options?: Options, ) { @@ -2010,30 +2082,31 @@ export class File extends HeyApiClient { [ { args: [ + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "path" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/file", + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth", ...options, ...params, }) } /** - * Read file + * Complete MCP OAuth * - * Read the content of a specified file. + * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code. */ - public read( + public callback( parameters: { + name: string directory?: string workspace?: string - path: string + code?: string }, options?: Options, ) { @@ -2042,24 +2115,66 @@ export class File extends HeyApiClient { [ { args: [ + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "path" }, + { in: "body", key: "code" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/file/content", + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth/callback", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } /** - * Get file status + * Authenticate MCP OAuth * - * Get the git status of all files in the project. + * Start OAuth flow and wait for callback (opens browser). + */ + public authenticate( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post( + { + url: "/mcp/{name}/auth/authenticate", + ...options, + ...params, + }, + ) + } +} + +export class Mcp extends HeyApiClient { + /** + * Get MCP status + * + * Get the status of all Model Context Protocol (MCP) servers. */ public status( parameters?: { @@ -2079,24 +2194,24 @@ export class File extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/file/status", + return (options?.client ?? this.client).get({ + url: "/mcp", ...options, ...params, }) } -} -export class Instance extends HeyApiClient { /** - * Dispose instance + * Add MCP server * - * Clean up and dispose the current RedrobCode instance, releasing all resources. + * Dynamically add a new Model Context Protocol (MCP) server to the system. */ - public dispose( + public add( parameters?: { directory?: string workspace?: string + name?: string + config?: McpLocalConfig | McpRemoteConfig }, options?: Options, ) { @@ -2107,26 +2222,30 @@ export class Instance extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "config" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/instance/dispose", + return (options?.client ?? this.client).post({ + url: "/mcp", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } -} -export class Path extends HeyApiClient { /** - * Get paths - * - * Retrieve the current working directory and related path information for the RedrobCode instance. + * Connect an MCP server. */ - public get( - parameters?: { + public connect( + parameters: { + name: string directory?: string workspace?: string }, @@ -2137,30 +2256,72 @@ export class Path extends HeyApiClient { [ { args: [ + { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/path", + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/connect", + ...options, + ...params, + }) + } + + /** + * Disconnect an MCP server. + */ + public disconnect( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/disconnect", ...options, ...params, }) } + + private _app?: App2 + get app(): App2 { + return (this._app ??= new App2({ client: this.client })) + } + + private _auth?: Auth2 + get auth(): Auth2 { + return (this._auth ??= new Auth2({ client: this.client })) + } } -export class Diff extends HeyApiClient { +export class Find extends HeyApiClient { /** - * Get raw VCS diff + * Find text * - * Retrieve a raw patch for current uncommitted changes. + * Search for text patterns across files in the project using ripgrep. */ - public raw( - parameters?: { + public text( + parameters: { directory?: string workspace?: string + pattern: string }, options?: Options, ) { @@ -2171,28 +2332,31 @@ export class Diff extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "pattern" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/vcs/diff/raw", + return (options?.client ?? this.client).get({ + url: "/find", ...options, ...params, }) } -} -export class Vcs extends HeyApiClient { /** - * Get VCS info + * Find files * - * Retrieve version control system (VCS) information for the current project, such as git branch. + * Search for files or directories by name or pattern in the project directory. */ - public get( - parameters?: { + public files( + parameters: { directory?: string workspace?: string + query: string + dirs?: "true" | "false" + type?: "file" | "directory" + limit?: number }, options?: Options, ) { @@ -2203,26 +2367,31 @@ export class Vcs extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "query" }, + { in: "query", key: "dirs" }, + { in: "query", key: "type" }, + { in: "query", key: "limit" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/vcs", + return (options?.client ?? this.client).get({ + url: "/find/file", ...options, ...params, }) } /** - * Get VCS status + * Find symbols * - * Retrieve changed files in the current working tree without patches. + * Search for workspace symbols like functions, classes, and variables using LSP. */ - public status( - parameters?: { + public symbols( + parameters: { directory?: string workspace?: string + query: string }, options?: Options, ) { @@ -2233,28 +2402,30 @@ export class Vcs extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "query" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/vcs/status", + return (options?.client ?? this.client).get({ + url: "/find/symbol", ...options, ...params, }) } +} +export class File extends HeyApiClient { /** - * Get VCS diff + * List files * - * Retrieve the current git diff for the working tree or against the default branch. + * List files and directories in a specified path. */ - public diff( + public list( parameters: { directory?: string workspace?: string - mode: "git" | "branch" - context?: number + path: string }, options?: Options, ) { @@ -2265,29 +2436,28 @@ export class Vcs extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "query", key: "mode" }, - { in: "query", key: "context" }, + { in: "query", key: "path" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/vcs/diff", + return (options?.client ?? this.client).get({ + url: "/file", ...options, ...params, }) } /** - * Apply VCS patch + * Read file * - * Apply a raw patch to the current working tree. + * Read the content of a specified file. */ - public apply( - parameters?: { + public read( + parameters: { directory?: string workspace?: string - patch?: string + path: string }, options?: Options, ) { @@ -2298,36 +2468,24 @@ export class Vcs extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "patch" }, + { in: "query", key: "path" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/vcs/apply", + return (options?.client ?? this.client).get({ + url: "/file/content", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } - private _diff?: Diff - get diff2(): Diff { - return (this._diff ??= new Diff({ client: this.client })) - } -} - -export class Command extends HeyApiClient { /** - * List commands + * Get file status * - * Get a list of all available commands in the RedrobCode system. + * Get the git status of all files in the project. */ - public list( + public status( parameters?: { directory?: string workspace?: string @@ -2345,21 +2503,21 @@ export class Command extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/command", + return (options?.client ?? this.client).get({ + url: "/file/status", ...options, ...params, }) } } -export class Lsp extends HeyApiClient { +export class Instance extends HeyApiClient { /** - * Get LSP status + * Dispose instance * - * Get LSP server status + * Clean up and dispose the current RedrobCode instance, releasing all resources. */ - public status( + public dispose( parameters?: { directory?: string workspace?: string @@ -2377,21 +2535,21 @@ export class Lsp extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/lsp", + return (options?.client ?? this.client).post({ + url: "/instance/dispose", ...options, ...params, }) } } -export class Formatter extends HeyApiClient { +export class Path extends HeyApiClient { /** - * Get formatter status + * Get paths * - * Get formatter status + * Retrieve the current working directory and related path information for the RedrobCode instance. */ - public status( + public get( parameters?: { directory?: string workspace?: string @@ -2409,23 +2567,22 @@ export class Formatter extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).get({ - url: "/formatter", + return (options?.client ?? this.client).get({ + url: "/path", ...options, ...params, }) } } -export class Auth2 extends HeyApiClient { +export class Diff extends HeyApiClient { /** - * Remove MCP OAuth + * Get raw VCS diff * - * Remove OAuth credentials for an MCP server. + * Retrieve a raw patch for current uncommitted changes. */ - public remove( - parameters: { - name: string + public raw( + parameters?: { directory?: string workspace?: string }, @@ -2436,28 +2593,28 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).delete({ - url: "/mcp/{name}/auth", + return (options?.client ?? this.client).get({ + url: "/vcs/diff/raw", ...options, ...params, }) } +} +export class Vcs extends HeyApiClient { /** - * Start MCP OAuth + * Get VCS info * - * Start OAuth authentication flow for a Model Context Protocol (MCP) server. + * Retrieve version control system (VCS) information for the current project, such as git branch. */ - public start( - parameters: { - name: string + public get( + parameters?: { directory?: string workspace?: string }, @@ -2468,31 +2625,28 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/auth", + return (options?.client ?? this.client).get({ + url: "/vcs", ...options, ...params, }) } /** - * Complete MCP OAuth + * Get VCS status * - * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code. + * Retrieve changed files in the current working tree without patches. */ - public callback( - parameters: { - name: string + public status( + parameters?: { directory?: string workspace?: string - code?: string }, options?: Options, ) { @@ -2501,36 +2655,30 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "code" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/auth/callback", + return (options?.client ?? this.client).get({ + url: "/vcs/status", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } /** - * Authenticate MCP OAuth + * Get VCS diff * - * Start OAuth flow and wait for callback (opens browser). + * Retrieve the current git diff for the working tree or against the default branch. */ - public authenticate( + public diff( parameters: { - name: string directory?: string workspace?: string + mode: "git" | "branch" + context?: number }, options?: Options, ) { @@ -2539,33 +2687,31 @@ export class Auth2 extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "query", key: "mode" }, + { in: "query", key: "context" }, ], }, ], ) - return (options?.client ?? this.client).post( - { - url: "/mcp/{name}/auth/authenticate", - ...options, - ...params, - }, - ) + return (options?.client ?? this.client).get({ + url: "/vcs/diff", + ...options, + ...params, + }) } -} -export class Mcp extends HeyApiClient { /** - * Get MCP status + * Apply VCS patch * - * Get the status of all Model Context Protocol (MCP) servers. + * Apply a raw patch to the current working tree. */ - public status( + public apply( parameters?: { directory?: string workspace?: string + patch?: string }, options?: Options, ) { @@ -2576,28 +2722,39 @@ export class Mcp extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "patch" }, ], }, ], ) - return (options?.client ?? this.client).get({ - url: "/mcp", + return (options?.client ?? this.client).post({ + url: "/vcs/apply", ...options, ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, }) } + private _diff?: Diff + get diff2(): Diff { + return (this._diff ??= new Diff({ client: this.client })) + } +} + +export class Command extends HeyApiClient { /** - * Add MCP server + * List commands * - * Dynamically add a new Model Context Protocol (MCP) server to the system. + * Get a list of all available commands in the RedrobCode system. */ - public add( + public list( parameters?: { directory?: string workspace?: string - name?: string - config?: McpLocalConfig | McpRemoteConfig }, options?: Options, ) { @@ -2608,30 +2765,26 @@ export class Mcp extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, - { in: "body", key: "name" }, - { in: "body", key: "config" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp", + return (options?.client ?? this.client).get({ + url: "/command", ...options, ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, }) } +} +export class Lsp extends HeyApiClient { /** - * Connect an MCP server. + * Get LSP status + * + * Get LSP server status */ - public connect( - parameters: { - name: string + public status( + parameters?: { directory?: string workspace?: string }, @@ -2642,26 +2795,28 @@ export class Mcp extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/connect", + return (options?.client ?? this.client).get({ + url: "/lsp", ...options, ...params, }) } +} +export class Formatter extends HeyApiClient { /** - * Disconnect an MCP server. + * Get formatter status + * + * Get formatter status */ - public disconnect( - parameters: { - name: string + public status( + parameters?: { directory?: string workspace?: string }, @@ -2672,24 +2827,18 @@ export class Mcp extends HeyApiClient { [ { args: [ - { in: "path", key: "name" }, { in: "query", key: "directory" }, { in: "query", key: "workspace" }, ], }, ], ) - return (options?.client ?? this.client).post({ - url: "/mcp/{name}/disconnect", + return (options?.client ?? this.client).get({ + url: "/formatter", ...options, ...params, }) } - - private _auth?: Auth2 - get auth(): Auth2 { - return (this._auth ??= new Auth2({ client: this.client })) - } } export class Memory extends HeyApiClient { @@ -8297,6 +8446,11 @@ export class RedrobClient extends HeyApiClient { return (this._worktree ??= new Worktree({ client: this.client })) } + private _mcp?: Mcp + get mcp(): Mcp { + return (this._mcp ??= new Mcp({ client: this.client })) + } + private _find?: Find get find(): Find { return (this._find ??= new Find({ client: this.client })) @@ -8337,11 +8491,6 @@ export class RedrobClient extends HeyApiClient { return (this._formatter ??= new Formatter({ client: this.client })) } - private _mcp?: Mcp - get mcp(): Mcp { - return (this._mcp ??= new Mcp({ client: this.client })) - } - private _memory?: Memory get memory(): Memory { return (this._memory ??= new Memory({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 115563b6f0..14f344d1e6 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2432,6 +2432,24 @@ export type McpResource = { client: string } +export type ExtensionCallResponse = { + ok: boolean + result?: { + b64?: string + mime?: string + path?: string + model?: string + costUsd?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + } + error?: string +} + +export type McpAppResource = { + html: string + mimeType?: string + server: string +} + export type Symbol = { name: string kind: number @@ -8728,6 +8746,121 @@ export type ExperimentalResourceListResponses = { export type ExperimentalResourceListResponse = ExperimentalResourceListResponses[keyof ExperimentalResourceListResponses] +export type ExperimentalExtensionsCallData = { + body?: { + extensionId: string + action: string + args?: { + [key: string]: unknown + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/extensions/call" +} + +export type ExperimentalExtensionsCallErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalExtensionsCallError = ExperimentalExtensionsCallErrors[keyof ExperimentalExtensionsCallErrors] + +export type ExperimentalExtensionsCallResponses = { + /** + * The extension's answer + */ + 200: ExtensionCallResponse +} + +export type ExperimentalExtensionsCallResponse = + ExperimentalExtensionsCallResponses[keyof ExperimentalExtensionsCallResponses] + +export type McpAppResolveData = { + body?: { + /** + * MCP server name, or the prefix of its projected tool names + */ + server: string + /** + * The ui:// resource the tool result pointed at + */ + uri: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/mcp-apps/resolve" +} + +export type McpAppResolveErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UpstreamError + */ + 502: UpstreamError +} + +export type McpAppResolveError = McpAppResolveErrors[keyof McpAppResolveErrors] + +export type McpAppResolveResponses = { + /** + * The app view's HTML + */ + 200: McpAppResource +} + +export type McpAppResolveResponse = McpAppResolveResponses[keyof McpAppResolveResponses] + +export type McpAppCallData = { + body?: { + server: string + /** + * Tool name as the MCP server knows it + */ + name: string + arguments?: { + [key: string]: unknown + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/mcp-apps/call" +} + +export type McpAppCallErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UpstreamError + */ + 502: UpstreamError +} + +export type McpAppCallError = McpAppCallErrors[keyof McpAppCallErrors] + +export type McpAppCallResponses = { + /** + * The MCP tool result + */ + 200: unknown +} + export type FindTextData = { body?: never path?: never