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
25 changes: 25 additions & 0 deletions .oxlintrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,31 @@
"executor/no-try-catch-or-throw": "off",
},
},
{
// boundary: the MCP-Apps shell is browser code, not Effect domain code.
// It runs inside a sandboxed iframe and talks to the host over
// postMessage and the MCP bridge — both of which are throw/Promise APIs
// with no Effect runtime present. `new Function` evaluation of generated
// JSX, CSS harvesting off `document.styleSheets`, and JSON on the wire
// all surface untyped JS errors that must be caught and rendered rather
// than modelled. Scoped to the iframe payload plus its build config; the
// package's Effect-facing loader (`src/shell-html.ts`) is deliberately
// NOT listed and uses inline suppressions instead.
"files": [
"packages/hosts/mcp-apps-shell/src/shell/**/*.{ts,tsx}",
"packages/hosts/mcp-apps-shell/vite.config.shell.ts",
],
"rules": {
"executor/no-double-cast": "off",
"executor/no-error-constructor": "off",
"executor/no-instanceof-error": "off",
"executor/no-json-parse": "off",
"executor/no-promise-catch": "off",
"executor/no-promise-reject": "off",
"executor/no-try-catch-or-throw": "off",
"executor/no-unknown-error-message": "off",
},
},
{
"files": ["packages/kernel/core/src/code-recovery.ts"],
"rules": {
Expand Down
31 changes: 31 additions & 0 deletions apps/cli/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,32 @@ const resolveQuickJsWasmPath = (): string => {
return wasmPath;
};

// ---------------------------------------------------------------------------
// MCP-Apps shell (mcp-app.html)
//
// The MCP host serves the shell as the `ui://executor/shell.html` resource, and
// `@executor-js/mcp-apps-shell` reads it from disk at runtime. That runtime
// `fs.readFile` is invisible to `bun build --compile`, so without this the
// packaged binary would serve the "Shell not built" placeholder and every
// generated UI would render blank. Build it if missing and copy it next to the
// executable, where the loader finds it via `process.execPath` — the same
// colocation trick used for `libsql.node` / `emscripten-module.wasm`.
// ---------------------------------------------------------------------------

const mcpAppsShellDir = resolve(repoRoot, "packages/hosts/mcp-apps-shell");
const mcpAppsShellHtml = join(mcpAppsShellDir, "dist/mcp-app.html");

const ensureMcpAppsShell = async (): Promise<string> => {
if (!existsSync(mcpAppsShellHtml)) {
console.log("Building MCP-Apps shell (mcp-app.html)...");
await $`bun run --cwd ${mcpAppsShellDir} build:shell`.quiet();
}
if (!existsSync(mcpAppsShellHtml)) {
throw new Error(`MCP-Apps shell missing at ${mcpAppsShellHtml} after build:shell`);
}
return mcpAppsShellHtml;
};

const resolveOnePasswordCoreWasmPath = (): string => {
const req = createRequire(join(repoRoot, "packages/plugins/onepassword/package.json"));
const sdkPkg = req.resolve("@1password/sdk/package.json");
Expand Down Expand Up @@ -398,6 +424,7 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => {
await writeFile(embeddedMigrationsPath, `${embeddedMigrations}\n`);

const quickJsWasmPath = resolveQuickJsWasmPath();
const mcpAppsShellPath = await ensureMcpAppsShell();
const onePasswordCoreWasmPath = resolveOnePasswordCoreWasmPath();
const workerBundlerDistPath = resolveWorkerBundlerDistPath();
const packedWorkerBundlerSource = await createPackedWorkerBundlerSource(workerBundlerDistPath);
Expand All @@ -423,6 +450,10 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => {
// Copy QuickJS WASM next to binary — loaded at runtime by the server
await cp(quickJsWasmPath, join(binDir, "emscripten-module.wasm"));

// Copy the MCP-Apps shell next to the binary. Platform-independent HTML,
// read from execDir at runtime by @executor-js/mcp-apps-shell.
await cp(mcpAppsShellPath, join(binDir, "mcp-app.html"));

// Copy 1Password's sdk-core WASM next to the binary. The patched
// sdk-core loader falls back to this sibling file when bun --compile
// bakes a build-machine node_modules path into __dirname.
Expand Down
1 change: 1 addition & 0 deletions apps/cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@executor-js/execution": "workspace:*",
"@executor-js/fumadb": "workspace:*",
"@executor-js/host-mcp": "workspace:*",
"@executor-js/mcp-apps-shell": "workspace:*",
"@executor-js/plugin-apps": "workspace:*",
"@executor-js/plugin-graphql": "workspace:*",
"@executor-js/plugin-mcp": "workspace:*",
Expand Down
5 changes: 5 additions & 0 deletions apps/cloud/src/mcp/session-durable-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
createExecutorMcpServer,
} from "@executor-js/host-mcp/tool-server";
import { buildResumeApprovalUrl } from "@executor-js/host-mcp/browser-approval";
import { artifactUrlFor } from "@executor-js/host-mcp/render-ui";
import { loadMcpAppsShellHtml } from "@executor-js/mcp-apps-shell";
import {
McpAgentSessionDOBase,
type BuiltMcpServer,
Expand Down Expand Up @@ -250,6 +252,9 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD
const mcpServer = yield* createExecutorMcpServer({
engine,
description,
artifacts: executor.artifacts,
loadAppShellHtml: loadMcpAppsShellHtml,
artifactUrl: artifactUrlFor(env.VITE_PUBLIC_SITE_URL ?? "https://executor.sh"),
parentSpan: () => self.currentParentSpan(),
debug: env.EXECUTOR_MCP_DEBUG === "true",
browserApprovalStore: self.browserApprovalStore,
Expand Down
1 change: 1 addition & 0 deletions apps/host-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@executor-js/execution": "workspace:*",
"@executor-js/fumadb": "workspace:*",
"@executor-js/host-mcp": "workspace:*",
"@executor-js/mcp-apps-shell": "workspace:*",
"@executor-js/plugin-encrypted-secrets": "workspace:*",
"@executor-js/plugin-graphql": "workspace:*",
"@executor-js/plugin-mcp": "workspace:*",
Expand Down
11 changes: 10 additions & 1 deletion apps/host-cloudflare/src/mcp/session-durable-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
createExecutorMcpServer,
} from "@executor-js/host-mcp/tool-server";
import { buildResumeApprovalUrl } from "@executor-js/host-mcp/browser-approval";
import { artifactUrlFor } from "@executor-js/host-mcp/render-ui";
import { loadMcpAppsShellHtml } from "@executor-js/mcp-apps-shell";
import type { ExecutorDbHandle } from "@executor-js/api/server";
import {
McpAgentSessionDOBase,
Expand Down Expand Up @@ -113,7 +115,7 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession
// QuickJS-WASM must be loaded before the executor layer builds it (the
// default variant can't fetch its .wasm on Workers). Idempotent per isolate.
yield* Effect.promise(() => preloadQuickJs());
const { engine } = yield* makeExecutionStack(
const { engine, executor } = yield* makeExecutionStack(
sessionMeta.userId,
sessionMeta.organizationId,
sessionMeta.organizationName,
Expand All @@ -124,8 +126,15 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession
// the console resume page. The URL origin is the create request's origin
// (captured by the base), falling back to the configured site URL.
const elicitationMode = sessionMeta.elicitationMode ?? "model";
// Same origin story as the approval URL below: an artifact deep link is
// only offered when this deployment knows its own public URL. Without one
// the artifact is still saved, and the tool says so.
const artifactOrigin = sessionMeta.webOrigin ?? config.webBaseUrl;
const mcpServer = yield* createExecutorMcpServer({
engine,
artifacts: executor.artifacts,
loadAppShellHtml: loadMcpAppsShellHtml,
...(artifactOrigin ? { artifactUrl: artifactUrlFor(artifactOrigin) } : {}),
browserApprovalStore: self.browserApprovalStore,
pausedExecutionHooks: self.pausedExecutionHooks,
pausedExecutionLeaseMs: PAUSED_APPROVAL_TIMEOUT_MS,
Expand Down
1 change: 1 addition & 0 deletions apps/host-selfhost/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@executor-js/execution": "workspace:*",
"@executor-js/fumadb": "workspace:*",
"@executor-js/host-mcp": "workspace:*",
"@executor-js/mcp-apps-shell": "workspace:*",
"@executor-js/plugin-apps": "workspace:*",
"@executor-js/plugin-encrypted-secrets": "workspace:*",
"@executor-js/plugin-graphql": "workspace:*",
Expand Down
3 changes: 3 additions & 0 deletions apps/host-selfhost/src/mcp/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { SelfHostExecutionStackLayer } from "../execution";
// identical seam with its own stack layer.
// ---------------------------------------------------------------------------

import { loadMcpAppsShellHtml } from "@executor-js/mcp-apps-shell";

export { McpEngineBuildError } from "@executor-js/host-mcp/in-memory-session-store";

/**
Expand All @@ -36,6 +38,7 @@ export const makeSelfHostMcpSessionStore = (
makeInMemoryMcpSessionStore(
makeMcpBuildServer(
SelfHostExecutionStackLayer.pipe(Layer.provide(Layer.succeed(SelfHostDb)(db))),
{ loadAppShellHtml: loadMcpAppsShellHtml },
),
{ webBaseUrl },
);
Expand Down
1 change: 1 addition & 0 deletions apps/local/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@executor-js/fumadb": "workspace:*",
"@executor-js/host-mcp": "workspace:*",
"@executor-js/integrations-registry": "workspace:*",
"@executor-js/mcp-apps-shell": "workspace:*",
"@executor-js/plugin-apps": "workspace:*",
"@executor-js/plugin-desktop-settings": "workspace:*",
"@executor-js/plugin-example": "workspace:*",
Expand Down
7 changes: 6 additions & 1 deletion apps/local/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ const loadLocalPlugins = (options: LocalExecutorOptions = {}) =>
interface LocalExecutorBundle {
readonly executor: Executor<LocalPlugins>;
readonly plugins: LocalPlugins;
/** Where this daemon's web UI is reachable, resolved once at boot. Surfaced
* so callers building user-facing links (MCP artifact deep links) use the
* same origin the executor itself was configured with. */
readonly webBaseUrl: string;
}

class LocalExecutorTag extends Context.Service<LocalExecutorTag, LocalExecutorBundle>()(
Expand Down Expand Up @@ -233,7 +237,7 @@ const createLocalExecutorLayer = (options: LocalExecutorOptions = {}) => {
);
}

return { executor, plugins };
return { executor, plugins, webBaseUrl };
}),
);
};
Expand All @@ -246,6 +250,7 @@ export const createExecutorHandle = async (options: LocalExecutorOptions = {}) =
return {
executor: bundle.executor,
plugins: bundle.plugins,
webBaseUrl: bundle.webBaseUrl,
dispose: async () => {
await Effect.runPromise(Effect.ignore(bundle.executor.close()));
await ignorePromiseFailure("disposeRuntime", () => runtime.dispose());
Expand Down
23 changes: 19 additions & 4 deletions apps/local/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Context, Data, Effect, Layer, ManagedRuntime } from "effect";

import { createExecutionEngine } from "@executor-js/execution";
import { artifactUrlFor } from "@executor-js/host-mcp/render-ui";
import { loadMcpAppsShellHtml } from "@executor-js/mcp-apps-shell";
import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";
import { makeLocalApiHandler } from "./app";
import { createExecutorHandle, disposeExecutor, getExecutorBundle } from "./executor";
Expand Down Expand Up @@ -81,15 +83,24 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
// engine instance (the browser-approval + stdio surface is local-only and not
// part of the shared API). Reuse the shared boot bundle so the MCP executor is
// byte-identical to the one the API serves.
const { executor } = await getExecutorBundle();
const { executor, webBaseUrl } = await getExecutorBundle();
const engine = createExecutionEngine({
executor,
codeExecutor: makeQuickJsExecutor(),
});
// The generative-UI surface, shared by every resource this daemon serves.
// Each toolkit gets its own executor, so `artifacts` is bound per resource
// below rather than hoisted with the rest.
const appsConfig = {
loadAppShellHtml: loadMcpAppsShellHtml,
artifactUrl: artifactUrlFor(webBaseUrl),
};
mcp = createMcpRequestHandler({
defaultConfig: { engine },
defaultConfig: { engine, artifacts: executor.artifacts, ...appsConfig },
createConfigForResource: async (resource) => {
if (resource.kind === "default") return { config: { engine } };
if (resource.kind === "default") {
return { config: { engine, artifacts: executor.artifacts, ...appsConfig } };
}
const handle = await createExecutorHandle({
activeToolkitSlug: resource.slug,
});
Expand All @@ -98,7 +109,11 @@ export const createServerHandlers = async (token: string): Promise<ServerHandler
codeExecutor: makeQuickJsExecutor(),
});
return {
config: { engine: toolkitEngine },
config: {
engine: toolkitEngine,
artifacts: handle.executor.artifacts,
...appsConfig,
},
close: handle.dispose,
};
},
Expand Down
Loading
Loading