Files
webstudio/packages/cli/src/api-connection.ts
T
Oleg Isonen 98d5c607ee feat: Improve MCP safety, context efficiency, and agent workflows (#5895)
## Summary

This combines the three reported MCP improvements into one release-ready
change:

- Safer mutation planning and confirmation for destructive operations.
- Much smaller MCP handshake and default discovery responses.
- Better agent workflows for checkpoints, multi-project work, and page
duplication.

## Safer destructive operations

- Exposes `dryRun` in MCP schemas for every local-capable mutation.
- Returns planned transactions through `meta.session.transaction`.
- Requires a two-step confirmation flow for destructive semantic
operations.
- Returns a short-lived token bound to the project, build version,
input, and exact generated patch plan.
- Rejects expired tokens, altered inputs, changed versions, and changed
plans.
- Sets `destructiveHint: true` only for operations that actually require
confirmation.
- Explicitly protects destructive operations whose names do not begin
with delete or replace, including replacing all redirects and clearing
all styles from a style source.

## Smaller, focused MCP context

- Defers oversized tool input schemas from the initial MCP handshake.
- Keeps complete focused schemas available through `meta.get_more_tools`
and CLI schema inspection.
- Makes template registry lists compact while preserving complete
payloads through focused get calls.
- Makes component summaries compact by default and supports paginated
component details.
- Replaces placeholder operation descriptions with useful command
descriptions.
- Constrains previously untyped JSON schema fields so clients can
validate inputs earlier.
- Keeps runtime output schemas compatible with strict MCP clients.

## Better agent workflows

- Allows read-only discovery and inspection while a workflow checkpoint
is awaiting acknowledgement.
- Continues blocking mutations until the checkpoint is acknowledged.
- Checks persisted checkpoints before every workflow call rather than
only the first call.
- Adds `--project` support so saved projects can be selected without
relinking the current directory.
- Isolates project sessions and checkpoints by selected project.
- Adds atomic `duplicate-page` substitutions for copied fixed text and
copied variable values.
- Prevents substitutions from modifying source content or global root
state.
- Rejects ambiguous or missing copied variables without leaving a
partial duplicate.

## Realistic agent verification

Minimal-context tests used a weak agent against the local packaged CLI
and stdio MCP:

- Planned a page deletion, stopped for explicit user confirmation,
committed the unchanged plan, and verified the page was absent.
- Tried to reuse a confirmation token with altered input; Webstudio
returned `DESTRUCTIVE_CONFIRMATION_INVALID` and preserved both pages.
- Verified compact handshake schemas and focused retrieval of the
complete oversized schema.
- Verified compact template and component discovery responses.
- Selected a saved project explicitly without relying on the directory
link.
- Verified checkpointed workflows permit reads but block mutations.
- Duplicated a London page into a Paris page with atomic text and
variable substitutions.
- Verified source content remained London, copied content became Paris,
and temporary pages were removed.

## Validation

- Project-build focused suites: 188 tests passed.
- CLI focused suites: 183 tests passed.
- Protocol focused suites: 21 tests passed.
- CLI, project-build, and protocol typechecks passed.
- Repository lint passed with zero warnings.
- Package-boundary validation passed.
- Generated runtime operation contracts were regenerated from source.
- Real local CLI/MCP tests passed against a running Builder server.

Closes #5887
Closes #5888
Closes #5889
2026-07-17 00:39:09 +02:00

68 lines
2.0 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { cwd } from "node:process";
import { join } from "node:path";
import {
GLOBAL_CONFIG_FILE,
LOCAL_CONFIG_FILE,
jsonToGlobalConfig,
jsonToLocalConfig,
} from "./config";
import { isFileExists } from "./fs-utils";
export type ApiConnection = {
origin: string;
authToken: string;
projectId: string;
headers?: Record<string, string | undefined>;
};
export type ApiConnectionDependencies = {
isFileExists: typeof isFileExists;
readFile: (path: string, encoding: "utf-8") => Promise<string>;
};
export const defaultApiConnectionDependencies: ApiConnectionDependencies = {
isFileExists,
readFile,
};
export const resolveApiConnection = async (
dependencies = defaultApiConnectionDependencies,
projectRoot = cwd(),
projectId?: string
): Promise<ApiConnection> => {
const isExplicitProject = projectId !== undefined;
if (projectId === undefined) {
const localConfigFile = join(projectRoot, LOCAL_CONFIG_FILE);
if ((await dependencies.isFileExists(localConfigFile)) === false) {
throw new Error(
"Local config file is not found. Run webstudio init --link <api-share-link> from a Webstudio project."
);
}
projectId = jsonToLocalConfig(
JSON.parse(await dependencies.readFile(localConfigFile, "utf-8"))
).projectId;
}
if (projectId.trim() === "") {
throw new Error("Project id must not be empty.");
}
const globalConfig = jsonToGlobalConfig(
JSON.parse(await dependencies.readFile(GLOBAL_CONFIG_FILE, "utf-8"))
);
const projectConfig = globalConfig[projectId];
if (projectConfig === undefined) {
throw new Error(
isExplicitProject
? `Project config for "${projectId}" is not found. Run webstudio init --link <api-share-link> once to save its credentials.`
: "Project config is not found. Run webstudio init --link <api-share-link>."
);
}
return {
origin: projectConfig.origin,
authToken: projectConfig.token,
projectId,
};
};