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
38 changes: 28 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ async function runInit({ force }: { force: boolean }): Promise<void> {
port,
allowedRoots,
publicBaseUrl,
widgets: files.config.widgets ?? "off",
subagents: resolveSubagentsFlag(files.config),
};
const auth = {
Expand All @@ -177,6 +178,7 @@ async function runInit({ force }: { force: boolean }): Promise<void> {
...seededSkillPaths.map((path) => `Default skill: ${path}`),
`Local MCP URL: http://${config.host}:${config.port}/mcp`,
...(publicBaseUrl ? [`Public MCP URL: ${publicBaseUrl}/mcp`] : []),
`UI widgets: ${config.widgets} (owner setting)`,
];
prompts.note(lines.join("\n"), "DevSpace configured");
prompts.note(
Expand Down Expand Up @@ -219,6 +221,7 @@ async function serve(): Promise<void> {
console.log(`public base url: ${config.publicBaseUrl}`);
console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
console.log(`allowed hosts: ${config.allowedHosts.join(", ")}`);
console.log(`widgets: ${config.widgets}`);
if (config.allowedHosts.includes("*")) {
console.warn("warning: Host header allowlist is disabled because DEVSPACE_ALLOWED_HOSTS=*");
}
Expand Down Expand Up @@ -264,6 +267,7 @@ async function runDoctor(): Promise<void> {
console.log(`Public MCP URL: ${new URL("/mcp", config.publicBaseUrl).toString()}`);
console.log(`Allowed roots: ${config.allowedRoots.join(", ")}`);
console.log(`Allowed hosts: ${config.allowedHosts.join(", ")}`);
console.log(`Widgets: ${config.widgets}`);
} catch (error) {
console.log(`Config status: ${error instanceof Error ? error.message : String(error)}`);
}
Expand All @@ -281,20 +285,33 @@ function runConfigCommand(args: string[]): void {
if (subcommand !== "set") {
throw new Error(`Unknown config command: ${subcommand}`);
}
if (key !== "publicBaseUrl") {
throw new Error("Only `devspace config set publicBaseUrl <url|null>` is supported right now.");
const value = rest.join(" ").trim();
if (key === "publicBaseUrl") {
if (!value) throw new Error("Missing publicBaseUrl value.");
writeDevspaceConfig({
...files.config,
publicBaseUrl: normalizeOptionalPublicBaseUrl(value),
});
console.log(`Updated ${files.configPath}`);
return;
}

const value = rest.join(" ").trim();
if (!value) {
throw new Error("Missing publicBaseUrl value.");
if (key === "widgets") {
if (value !== "off" && value !== "changes" && value !== "full") {
throw new Error("widgets must be one of: off, changes, full.");
}
writeDevspaceConfig({
...files.config,
widgets: value,
});
console.log(`Updated ${files.configPath}`);
console.log("Restart DevSpace and refresh/reconnect the MCP App for the UI mode change to take effect.");
return;
}

writeDevspaceConfig({
...files.config,
publicBaseUrl: normalizeOptionalPublicBaseUrl(value),
});
console.log(`Updated ${files.configPath}`);
throw new Error(
"Supported settings: publicBaseUrl <url|null>, widgets <off|changes|full>.",
);
}

function printHelp(): void {
Expand All @@ -309,6 +326,7 @@ function printHelp(): void {
" devspace doctor Show config, runtime, and native dependency status",
" devspace config get Print persisted config",
" devspace config set publicBaseUrl <url|null>",
" devspace config set widgets <off|changes|full>",
" devspace agents ls List subagent sessions",
" devspace agents run <profile-or-provider-or-id> [--model <model>] <prompt>",
" devspace agents show <id>",
Expand Down
8 changes: 7 additions & 1 deletion src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const baseEnv = {
DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
};

assert.equal(loadConfig(baseEnv).widgets, "full");
assert.equal(loadConfig(baseEnv).widgets, "off");
assert.equal(loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "changes" }).widgets, "changes");
assert.equal(loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "full" }).widgets, "full");
assert.equal(loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "off" }).widgets, "off");
Expand Down Expand Up @@ -162,6 +162,7 @@ writeFileSync(
port: 8787,
allowedRoots: [process.cwd()],
publicBaseUrl: "https://devspace.example.com",
widgets: "changes",
subagents: true,
}),
);
Expand All @@ -176,6 +177,11 @@ const fileConfig = loadConfig({ DEVSPACE_CONFIG_DIR: configDir });
assert.equal(fileConfig.port, 8787);
assert.equal(fileConfig.oauth.ownerToken, "persisted-owner-token-long-enough");
assert.equal(fileConfig.publicBaseUrl, "https://devspace.example.com");
assert.equal(fileConfig.widgets, "changes");
assert.equal(
loadConfig({ DEVSPACE_CONFIG_DIR: configDir, DEVSPACE_WIDGETS: "full" }).widgets,
"full",
);
assert.equal(fileConfig.subagents, true);
assert.deepEqual(fileConfig.allowedHosts, [
"localhost",
Expand Down
6 changes: 3 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ function parseLoggingConfig(env: NodeJS.ProcessEnv): LoggingConfig {
}

function parseWidgetMode(value: string | undefined): WidgetMode {
if (!value || value === "full") return "full";
if (value === "off" || value === "changes") return value;
if (!value || value === "off") return "off";
if (value === "changes" || value === "full") return value;

throw new Error(`Invalid DEVSPACE_WIDGETS: ${value}`);
}
Expand Down Expand Up @@ -223,7 +223,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {
allowedHosts: parseAllowedHosts(env.DEVSPACE_ALLOWED_HOSTS, derivedAllowedHosts),
publicBaseUrl,
toolMode: parseToolMode(env),
widgets: parseWidgetMode(env.DEVSPACE_WIDGETS),
widgets: parseWidgetMode(env.DEVSPACE_WIDGETS ?? files.config.widgets),
stateDir: resolve(expandHomePath(env.DEVSPACE_STATE_DIR ?? files.config.stateDir ?? defaultStateDir())),
worktreeRoot: resolve(expandHomePath(env.DEVSPACE_WORKTREE_ROOT ?? files.config.worktreeRoot ?? defaultWorktreeRoot())),
skillsEnabled: env.DEVSPACE_SKILLS === undefined ? true : parseBoolean(env.DEVSPACE_SKILLS),
Expand Down
33 changes: 31 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
import {
registerAppResource,
registerAppTool,
registerAppTool as registerUiTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import express from "express";
Expand Down Expand Up @@ -155,6 +155,32 @@ function toolWidgetDescriptorMeta(
};
}

const widgetModeByServer = new WeakMap<McpServer, WidgetMode>();

function stripAppMeta<T>(value: T): T {
if (!value || typeof value !== "object") return value;
const { _meta: _discardedMeta, ...nativeValue } = value as Record<string, unknown>;
return nativeValue as T;
}

// Preserve the upstream MCP App path for changes/full, but use native
// MCP tools in off mode so hosts do not allocate embedded UI cards.
const registerAppTool = ((
server: McpServer,
name: string,
definition: any,
handler: (...args: any[]) => any,
) => {
if (widgetModeByServer.get(server) !== "off") {
return registerUiTool(server, name, definition, handler);
}

return server.registerTool(
name,
stripAppMeta(definition),
async (...args: any[]) => stripAppMeta(await handler(...args)),
);
}) as typeof registerUiTool;
const toolNames = {
openWorkspace: "open_workspace",
read: "read",
Expand Down Expand Up @@ -701,8 +727,10 @@ function createMcpServer(
instructions: serverInstructions(config),
},
);
widgetModeByServer.set(server, config.widgets);

registerAppResource(
if (config.widgets !== "off") {
registerAppResource(
server,
"DevSpace Diff Card",
WORKSPACE_APP_URI,
Expand Down Expand Up @@ -732,6 +760,7 @@ function createMcpServer(
};
},
);
}

registerAppTool(
server,
Expand Down
1 change: 1 addition & 0 deletions src/user-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface DevspaceUserConfig {
port?: number;
allowedRoots?: string[];
publicBaseUrl?: string | null;
widgets?: "off" | "changes" | "full";
allowedHosts?: string[];
stateDir?: string;
worktreeRoot?: string;
Expand Down