Skip to content
Open
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
23 changes: 20 additions & 3 deletions apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,14 @@ import * as Option from "effect/Option";
import * as Cause from "effect/Cause";

import { ExecutorApi } from "@executor-js/api";
import { startServer, runMcpStdioServer, getExecutor } from "@executor-js/local";
import {
startServer,
runMcpStdioServer,
getExecutorBundle,
filterDynamicUiMcpPlugins,
isGeneratedUiMcpAppsEnabled,
makeLocalEnvFeatureFlags,
} from "@executor-js/local";
import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";
import { fetchIntegrations } from "./integrations";
import {
Expand Down Expand Up @@ -758,7 +765,7 @@ const runStdioMcpSession = (input: { readonly elicitationMode: "browser" | "mode
const restoreWebBaseUrl = installDefaultExecutorWebBaseUrl(baseUrl);

try {
const executor = await getExecutor();
const executor = await getExecutorBundle();
const server = await startServer({
port,
hostname: host,
Expand All @@ -774,10 +781,20 @@ const runStdioMcpSession = (input: { readonly elicitationMode: "browser" | "mode
);

try {
const featureFlags = makeLocalEnvFeatureFlags();
const generatedUiMcpAppsEnabled = yield* isGeneratedUiMcpAppsEnabled(featureFlags);
const mcpPlugins = filterDynamicUiMcpPlugins(web.executor.plugins, generatedUiMcpAppsEnabled);

yield* Effect.promise(() =>
runMcpStdioServer({
executor: web.executor,
executor: web.executor.executor,
codeExecutor: makeQuickJsExecutor(),
plugins: mcpPlugins,
renderUiFallbackUrl: (code) => {
const url = new URL("/plugins/dynamic-ui/render", web.baseUrl);
url.hash = `code=${encodeURIComponent(code)}`;
return url.toString();
},
elicitationMode:
input.elicitationMode === "browser"
? {
Expand Down
2 changes: 2 additions & 0 deletions apps/cloud/executor.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineExecutorConfig } from "@executor-js/sdk";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import { dynamicUiPlugin } from "@executor-js/plugin-dynamic-ui";
import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";
import { graphqlHttpPlugin } from "@executor-js/plugin-graphql/api";
import { workosVaultPlugin, type WorkOSVaultClient } from "@executor-js/plugin-workos-vault";
Expand Down Expand Up @@ -39,6 +40,7 @@ export default defineExecutorConfig({
plugins: ({ workosCredentials, workosVaultClient }: CloudPluginDeps = {}) =>
[
openApiHttpPlugin(),
dynamicUiPlugin(),
mcpHttpPlugin({
dangerouslyAllowStdioMCP: false,
}),
Expand Down
1 change: 1 addition & 0 deletions apps/cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@executor-js/api": "workspace:*",
"@executor-js/execution": "workspace:*",
"@executor-js/host-mcp": "workspace:*",
"@executor-js/plugin-dynamic-ui": "workspace:*",
"@executor-js/plugin-graphql": "workspace:*",
"@executor-js/plugin-mcp": "workspace:*",
"@executor-js/plugin-openapi": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions apps/cloud/src/env-augment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ declare global {
VITE_PUBLIC_SENTRY_DSN?: string;
VITE_PUBLIC_POSTHOG_KEY?: string;
VITE_PUBLIC_POSTHOG_HOST?: string;
POSTHOG_HOST?: string;

// Datastore. Prod uses HYPERDRIVE when the binding exists; direct
// DATABASE_URL is only selected when explicitly requested for local/test.
Expand Down
33 changes: 31 additions & 2 deletions apps/cloud/src/mcp-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { drizzle } from "drizzle-orm/postgres-js";
import postgres, { type Sql } from "postgres";

import { createExecutorMcpServer } from "@executor-js/host-mcp";
import { filterDynamicUiMcpPlugins } from "@executor-js/plugin-dynamic-ui";
import {
buildExecuteDescription,
formatPausedExecution,
Expand All @@ -31,6 +32,7 @@ import { UserStoreService } from "./auth/context";
import { resolveOrganization } from "./auth/resolve-organization";
import { DbService, combinedSchema, resolveConnectionString } from "./services/db";
import { makeExecutionStack } from "./services/execution-stack";
import { isGeneratedUiMcpAppsEnabled, PostHogFeatureFlags } from "./services/feature-flags";
import { makeMcpWorkerTransport, type McpWorkerTransport } from "./services/mcp-worker-transport";
import { DoTelemetryLive } from "./services/telemetry";
import { captureCause } from "./observability";
Expand Down Expand Up @@ -230,7 +232,8 @@ const makeResolveOrganizationServices = (dbHandle: DbHandle) => {
// child span from the outer `McpSessionDO.init` / `McpSessionDO.handleRequest`
// trace. Tracer comes from the outermost `Effect.provide(DoTelemetryLive)`
// at the DO method boundary.
const makeSessionServices = (dbHandle: DbHandle) => makeResolveOrganizationServices(dbHandle);
const makeSessionServices = (dbHandle: DbHandle) =>
Layer.mergeAll(makeResolveOrganizationServices(dbHandle), PostHogFeatureFlags);

const resolveSessionMeta = Effect.fn("McpSessionDO.resolveSessionMeta")(function* (
organizationId: string,
Expand Down Expand Up @@ -353,11 +356,30 @@ export class McpSessionDO extends DurableObject {
) {
const self = this;
return Effect.gen(function* () {
const { executor, engine } = yield* makeExecutionStack(
const featureFlagContext = {
distinctId: sessionMeta.userId,
accountId: sessionMeta.userId,
organizationId: sessionMeta.organizationId,
groups: { organization: sessionMeta.organizationId },
};
const generatedUiMcpAppsEnabled = yield* isGeneratedUiMcpAppsEnabled(featureFlagContext).pipe(
Effect.catch((error: unknown) =>
Effect.sync(() => {
console.error("[executor:mcp] generated UI feature flag failed", error);
return false;
}),
),
);
yield* Effect.annotateCurrentSpan({
"feature.generated_ui_mcp_apps.enabled": generatedUiMcpAppsEnabled,
});

const { executor, engine, plugins } = yield* makeExecutionStack(
sessionMeta.userId,
sessionMeta.organizationId,
sessionMeta.organizationName,
);
const mcpPlugins = filterDynamicUiMcpPlugins(plugins, generatedUiMcpAppsEnabled);
// Build the description here so the postgres query it runs
// (`executor.sources.list`) lands as a child of
// `McpSessionDO.createRuntime`. host-mcp would otherwise call
Expand All @@ -368,8 +390,15 @@ export class McpSessionDO extends DurableObject {
const mcpServer = yield* createExecutorMcpServer({
engine,
description,
plugins: mcpPlugins,
parentSpan: () => self.currentRequestSpan ?? undefined,
debug: env.EXECUTOR_MCP_DEBUG === "true",
renderUiFallbackUrl: (code) => {
const origin = env.VITE_PUBLIC_SITE_URL ?? "https://executor.sh";
const url = new URL("/plugins/dynamic-ui/render", origin);
url.hash = `code=${encodeURIComponent(code)}`;
return url.toString();
},
browserApprovalStore: {
takeResponse: (executionId) => self.takeApprovalResponse(executionId),
waitForResponse: (executionId) => self.waitForApprovalResponse(executionId),
Expand Down
21 changes: 21 additions & 0 deletions apps/cloud/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Route as SourcesNamespaceRouteImport } from './routes/sources.$namespac
import { Route as ResumeExecutionIdRouteImport } from './routes/resume.$executionId'
import { Route as BillingPlansRouteImport } from './routes/billing_.plans'
import { Route as SourcesAddPluginKeyRouteImport } from './routes/sources.add.$pluginKey'
import { Route as PluginsPluginIdSplatRouteImport } from './routes/plugins.$pluginId.$'

const ToolsRoute = ToolsRouteImport.update({
id: '/tools',
Expand Down Expand Up @@ -94,6 +95,11 @@ const SourcesAddPluginKeyRoute = SourcesAddPluginKeyRouteImport.update({
path: '/sources/add/$pluginKey',
getParentRoute: () => rootRouteImport,
} as any)
const PluginsPluginIdSplatRoute = PluginsPluginIdSplatRouteImport.update({
id: '/plugins/$pluginId/$',
path: '/plugins/$pluginId/$',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
Expand All @@ -109,6 +115,7 @@ export interface FileRoutesByFullPath {
'/billing/plans': typeof BillingPlansRoute
'/resume/$executionId': typeof ResumeExecutionIdRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
'/plugins/$pluginId/$': typeof PluginsPluginIdSplatRoute
'/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
}
export interface FileRoutesByTo {
Expand All @@ -125,6 +132,7 @@ export interface FileRoutesByTo {
'/billing/plans': typeof BillingPlansRoute
'/resume/$executionId': typeof ResumeExecutionIdRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
'/plugins/$pluginId/$': typeof PluginsPluginIdSplatRoute
'/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
}
export interface FileRoutesById {
Expand All @@ -142,6 +150,7 @@ export interface FileRoutesById {
'/billing_/plans': typeof BillingPlansRoute
'/resume/$executionId': typeof ResumeExecutionIdRoute
'/sources/$namespace': typeof SourcesNamespaceRoute
'/plugins/$pluginId/$': typeof PluginsPluginIdSplatRoute
'/sources/add/$pluginKey': typeof SourcesAddPluginKeyRoute
}
export interface FileRouteTypes {
Expand All @@ -160,6 +169,7 @@ export interface FileRouteTypes {
| '/billing/plans'
| '/resume/$executionId'
| '/sources/$namespace'
| '/plugins/$pluginId/$'
| '/sources/add/$pluginKey'
fileRoutesByTo: FileRoutesByTo
to:
Expand All @@ -176,6 +186,7 @@ export interface FileRouteTypes {
| '/billing/plans'
| '/resume/$executionId'
| '/sources/$namespace'
| '/plugins/$pluginId/$'
| '/sources/add/$pluginKey'
id:
| '__root__'
Expand All @@ -192,6 +203,7 @@ export interface FileRouteTypes {
| '/billing_/plans'
| '/resume/$executionId'
| '/sources/$namespace'
| '/plugins/$pluginId/$'
| '/sources/add/$pluginKey'
fileRoutesById: FileRoutesById
}
Expand All @@ -209,6 +221,7 @@ export interface RootRouteChildren {
BillingPlansRoute: typeof BillingPlansRoute
ResumeExecutionIdRoute: typeof ResumeExecutionIdRoute
SourcesNamespaceRoute: typeof SourcesNamespaceRoute
PluginsPluginIdSplatRoute: typeof PluginsPluginIdSplatRoute
SourcesAddPluginKeyRoute: typeof SourcesAddPluginKeyRoute
}

Expand Down Expand Up @@ -291,6 +304,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SourcesNamespaceRouteImport
parentRoute: typeof rootRouteImport
}
'/plugins/$pluginId/$': {
id: '/plugins/$pluginId/$'
path: '/plugins/$pluginId/$'
fullPath: '/plugins/$pluginId/$'
preLoaderRoute: typeof PluginsPluginIdSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/resume/$executionId': {
id: '/resume/$executionId'
path: '/resume/$executionId'
Expand Down Expand Up @@ -329,6 +349,7 @@ const rootRouteChildren: RootRouteChildren = {
BillingPlansRoute: BillingPlansRoute,
ResumeExecutionIdRoute: ResumeExecutionIdRoute,
SourcesNamespaceRoute: SourcesNamespaceRoute,
PluginsPluginIdSplatRoute: PluginsPluginIdSplatRoute,
SourcesAddPluginKeyRoute: SourcesAddPluginKeyRoute,
}
export const routeTree = rootRouteImport
Expand Down
27 changes: 27 additions & 0 deletions apps/cloud/src/routes/plugins.$pluginId.$.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createFileRoute, notFound } from "@tanstack/react-router";
import { useClientPlugins } from "@executor-js/sdk/client";

export const Route = createFileRoute("/plugins/$pluginId/$")({
component: PluginRouteComponent,
});

function normalizePath(input: string): string {
if (!input || input === "/") return "/";
return input.startsWith("/") ? input : `/${input}`;
}

function PluginRouteComponent() {
const { pluginId, _splat: rest } = Route.useParams();
const plugins = useClientPlugins();
const plugin = plugins.find((p) => p.id === pluginId);
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: TanStack Router represents not-found from components by throwing notFound()
if (!plugin) throw notFound();

const target = normalizePath(rest ?? "/");
const page = plugin.pages?.find((p) => normalizePath(p.path) === target);
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: TanStack Router represents not-found from components by throwing notFound()
if (!page) throw notFound();

const Component = page.component;
return <Component />;
}
14 changes: 9 additions & 5 deletions apps/cloud/src/services/execution-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,27 @@ import { makeDynamicWorkerExecutor } from "@executor-js/runtime-dynamic-worker";

import { withExecutionUsageTracking } from "../api/execution-usage";
import { AutumnService } from "./autumn";
import { createScopedExecutor } from "./executor";
import { createCloudPlugins, createScopedExecutor } from "./executor";

export const makeExecutionStack = (
userId: string,
organizationId: string,
organizationName: string,
) =>
Effect.gen(function* () {
const executor = yield* createScopedExecutor(userId, organizationId, organizationName).pipe(
Effect.withSpan("McpSessionDO.createScopedExecutor"),
);
const plugins = createCloudPlugins();
const executor = yield* createScopedExecutor(
userId,
organizationId,
organizationName,
plugins,
).pipe(Effect.withSpan("McpSessionDO.createScopedExecutor"));
const codeExecutor = makeDynamicWorkerExecutor({ loader: env.LOADER });
const autumn = yield* AutumnService;
const engine = withExecutionUsageTracking(
organizationId,
createExecutionEngine({ executor, codeExecutor }),
(orgId) => Effect.runFork(autumn.trackExecution(orgId)),
);
return { executor, engine };
return { executor, engine, plugins };
}).pipe(Effect.withSpan("McpSessionDO.makeExecutionStack"));
4 changes: 2 additions & 2 deletions apps/cloud/src/services/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { createDrizzleFumaDb } from "./fuma";

export type CloudPlugins = ReturnType<typeof executorConfig.plugins>;

const orgPlugins = (): CloudPlugins =>
export const createCloudPlugins = (): CloudPlugins =>
executorConfig.plugins({
workosCredentials: {
apiKey: env.WORKOS_API_KEY,
Expand All @@ -59,11 +59,11 @@ export const createScopedExecutor = (
userId: string,
organizationId: string,
organizationName: string,
plugins: CloudPlugins = createCloudPlugins(),
) =>
Effect.gen(function* () {
const { db } = yield* DbService;

const plugins = orgPlugins();
const httpClientLayer = makeHostedHttpClientLayer({
allowLocalNetwork: env.NODE_ENV === "test",
});
Expand Down
Loading
Loading