@topline-build/harness-mcp 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +224 -11
- package/docs/generated/build-mcp-manifest.json +532 -2
- package/docs/generated/build-mcp-reference.md +587 -1
- package/package.json +1 -1
- package/scripts/certify-live.mjs +15 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable customer-visible Build MCP changes are recorded here. A source chang
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.5.3 - pending publication
|
|
8
|
+
|
|
9
|
+
- Add tenant-scoped Storyboard state, guarded edit, and exact-draft publication tools to Build MCP.
|
|
10
|
+
- Check out the published Storyboard source for local Storybook iteration and submit source-only edits as a private, server-validated draft.
|
|
11
|
+
|
|
12
|
+
## 0.5.2 - 2026-09-26
|
|
13
|
+
|
|
14
|
+
- Report rejected bearer credentials as `authorization_required`, with scoped cache-clearing and browser reconnect guidance instead of input-correction advice.
|
|
15
|
+
- Guide connection-specific cache cleanup and recovery of existing deployment progress before a write is retried.
|
|
16
|
+
- Report native client certification timeouts as failures even when partial output mentions authentication; a timed-out process is not proof of a missing provider session.
|
|
17
|
+
|
|
7
18
|
## 0.5.1 - 2026-09-18
|
|
8
19
|
|
|
9
20
|
- Harden the bundled Claude certification script with strict tool isolation, effective-runtime proof, and noninteractive JSONL validation.
|
package/README.md
CHANGED
|
@@ -22,6 +22,10 @@ npx -y @topline-build/harness-mcp
|
|
|
22
22
|
|
|
23
23
|
Run `npx -y @topline-build/harness-mcp --help` for every supported flag and environment variable. `TOPLINE_MCP_TOKEN` is an advanced manual-token fallback; browser-approved OAuth is preferred.
|
|
24
24
|
|
|
25
|
+
If a tool returns `authorization_required`, clear only that connection's OAuth cache and reconnect through browser sign-in. Replace an expired or revoked manual token if using the fallback. Inspect existing artifact/deployment progress before retrying a write; the helper does not silently replay the rejected request.
|
|
26
|
+
|
|
27
|
+
Storyboard authoring uses separate `storyboard:read` and `storyboard:write` OAuth scopes. Request them explicitly with `--scopes` (or `TOPLINE_MCP_SCOPES`) and reconnect so Topline can approve the expanded grant; they are not part of the default artifact scopes. `platform_get_design_system_state` shows the published commit and current draft. `platform_checkout_design_system` creates a local checkout of that published source; run `npm ci` and `npm run storybook` there to iterate, then call `platform_submit_design_system_draft` with the exact published commit and draft ID from the state read. Wait for a successful server build, inspect the private preview, and call `platform_publish_design_system` with the reviewed run and commit values. Alternatively, `platform_edit_design_system` asks Topline's guarded coding agent to edit a draft in an existing writable Storyboard conversation. A timed-out write should be recovered by reading state before another submission. The separate Storybook reference MCP endpoint does not accept source writes.
|
|
28
|
+
|
|
25
29
|
## Typical workflow
|
|
26
30
|
|
|
27
31
|
1. Use `platform_init_artifact_app` to create a starter, or point at an existing artifact app.
|
package/dist/index.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface PackedArtifactAppBundle {
|
|
|
32
32
|
type ToolProgressReporter = (progress: number, message: string) => void | Promise<void>;
|
|
33
33
|
export declare const DEFAULT_MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
34
34
|
export declare const BUILD_MCP_SERVER_NAME = "topline-build-stdio";
|
|
35
|
-
export declare const HARNESS_MCP_VERSION = "0.5.
|
|
35
|
+
export declare const HARNESS_MCP_VERSION = "0.5.3";
|
|
36
36
|
export declare const DEFAULT_OAUTH_SCOPES: readonly string[];
|
|
37
37
|
export interface BuildMcpSetupOption {
|
|
38
38
|
readonly flag: string;
|
|
@@ -161,6 +161,7 @@ export declare function storybookAccess(opts: HarnessMcpStdioOptions): {
|
|
|
161
161
|
operatorAction: string;
|
|
162
162
|
};
|
|
163
163
|
export declare function packArtifactAppDirectory(appDir: string, archivePrefix?: string): Promise<PackedArtifactAppBundle>;
|
|
164
|
+
export declare function packStoryboardSourceDirectory(appDir: string): Promise<PackedArtifactAppBundle>;
|
|
164
165
|
export declare function listTools(): ListToolsResult;
|
|
165
166
|
export interface BuildMcpRecoveryGuidance {
|
|
166
167
|
readonly retryable: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { mkdir, readdir, readFile, realpath, lstat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { mkdir, mkdtemp, readdir, readFile, realpath, lstat, writeFile, rm, rename } from "node:fs/promises";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
@@ -13,7 +13,7 @@ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } fr
|
|
|
13
13
|
export const DEFAULT_MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
14
14
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
15
15
|
export const BUILD_MCP_SERVER_NAME = "topline-build-stdio";
|
|
16
|
-
export const HARNESS_MCP_VERSION = "0.5.
|
|
16
|
+
export const HARNESS_MCP_VERSION = "0.5.3";
|
|
17
17
|
export const DEFAULT_OAUTH_SCOPES = Object.freeze(["harness:artifact:read", "harness:artifact:write", "harness:artifact:deploy", "harness:artifact:delete"]);
|
|
18
18
|
export const BUILD_MCP_SETUP_OPTIONS = Object.freeze([
|
|
19
19
|
{ flag: "--url", env: "TOPLINE_API_URL", aliases: ["PLATFORM_API_URL", "OVERSITE_API_URL"], required: true, description: "Topline tenant base URL. HTTPS is required except for loopback development." },
|
|
@@ -146,6 +146,9 @@ export async function callHarnessTool(opts, name, args, hooks) {
|
|
|
146
146
|
let result;
|
|
147
147
|
switch (name) {
|
|
148
148
|
case "platform_connection_check":
|
|
149
|
+
case "platform_get_design_system_state":
|
|
150
|
+
case "platform_edit_design_system":
|
|
151
|
+
case "platform_publish_design_system":
|
|
149
152
|
case "platform_list_build_workspaces":
|
|
150
153
|
case "platform_list_artifact_drafts":
|
|
151
154
|
case "platform_deploy_operation":
|
|
@@ -155,6 +158,12 @@ export async function callHarnessTool(opts, name, args, hooks) {
|
|
|
155
158
|
case "platform_delete_artifact_draft":
|
|
156
159
|
result = toolResult(await callHostedBuildTool(opts, name, input));
|
|
157
160
|
break;
|
|
161
|
+
case "platform_checkout_design_system":
|
|
162
|
+
result = toolResult(await checkoutDesignSystem(opts, input));
|
|
163
|
+
break;
|
|
164
|
+
case "platform_submit_design_system_draft":
|
|
165
|
+
result = toolResult(await submitDesignSystemDraft(opts, input));
|
|
166
|
+
break;
|
|
158
167
|
case "platform_deploy_artifact_app":
|
|
159
168
|
result = toolResult(await deployArtifactApp(opts, input, hooks?.onProgress));
|
|
160
169
|
break;
|
|
@@ -313,6 +322,132 @@ async function callHostedBuildTool(opts, name, input) {
|
|
|
313
322
|
return result.structuredContent;
|
|
314
323
|
throw new Error(`platform_mcp_tool_output_missing:${name}`);
|
|
315
324
|
}
|
|
325
|
+
async function checkoutDesignSystem(opts, input) {
|
|
326
|
+
if (mcpSurface(opts) !== "build")
|
|
327
|
+
throw new Error("storyboard_requires_build_surface");
|
|
328
|
+
const targetDir = stringArg(input, "targetDir");
|
|
329
|
+
const target = await resolveWorkspacePathSafely(opts.workspaceRoot, targetDir);
|
|
330
|
+
if (existsSync(target))
|
|
331
|
+
throw new Error("storyboard_checkout_target_exists");
|
|
332
|
+
await assertAuthFileOutsideAppDir(opts.authFile, target);
|
|
333
|
+
const controller = new AbortController();
|
|
334
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
|
|
335
|
+
let bundle;
|
|
336
|
+
let commitSha;
|
|
337
|
+
try {
|
|
338
|
+
const response = await (opts.fetchImpl ?? fetch)(`${platformBaseUrl(opts)}/api/harness/storyboard/source-bundle`, {
|
|
339
|
+
headers: await jsonHeaders(opts), signal: controller.signal
|
|
340
|
+
});
|
|
341
|
+
if (!response.ok)
|
|
342
|
+
throw new Error(`platform_harness_http_error:${response.status}:storyboard_source_unavailable`);
|
|
343
|
+
commitSha = response.headers.get("x-topline-storyboard-commit") ?? "";
|
|
344
|
+
if (!/^[a-f0-9]{40}$/.test(commitSha))
|
|
345
|
+
throw new Error("storyboard_source_commit_invalid");
|
|
346
|
+
bundle = await readBoundedStoryboardBundle(response);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (controller.signal.aborted)
|
|
350
|
+
throw new Error("platform_harness_timeout");
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
clearTimeout(timer);
|
|
355
|
+
}
|
|
356
|
+
const temp = await mkdtemp(path.join(os.tmpdir(), "topline-storyboard-checkout-"));
|
|
357
|
+
try {
|
|
358
|
+
const bundlePath = path.join(temp, "source.bundle");
|
|
359
|
+
await writeFile(bundlePath, bundle);
|
|
360
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
361
|
+
const staging = await mkdtemp(path.join(path.dirname(target), ".topline-storyboard-checkout-"));
|
|
362
|
+
try {
|
|
363
|
+
const candidate = path.join(staging, "source");
|
|
364
|
+
await runLocalGit(["clone", "--quiet", bundlePath, candidate]);
|
|
365
|
+
await runLocalGit(["-C", candidate, "checkout", "--quiet", "--detach", commitSha]);
|
|
366
|
+
await runLocalGit(["-C", candidate, "remote", "remove", "origin"]);
|
|
367
|
+
const actual = (await runLocalGit(["-C", candidate, "rev-parse", "HEAD"])).trim();
|
|
368
|
+
if (actual !== commitSha)
|
|
369
|
+
throw new Error("storyboard_source_commit_mismatch");
|
|
370
|
+
if (existsSync(target))
|
|
371
|
+
throw new Error("storyboard_checkout_target_exists");
|
|
372
|
+
await rename(candidate, target);
|
|
373
|
+
return { ok: true, directory: path.relative(opts.workspaceRoot, target).replaceAll("\\", "/"), currentCommit: commitSha, localCommands: ["npm ci", "npm run storybook"] };
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
await rm(staging, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
await rm(temp, { recursive: true, force: true });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function readBoundedStoryboardBundle(response) {
|
|
384
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
385
|
+
if (!Number.isSafeInteger(declared) || declared < 0 || declared > MAX_BUNDLE_BYTES) {
|
|
386
|
+
throw new Error("storyboard_source_bundle_size_invalid");
|
|
387
|
+
}
|
|
388
|
+
const reader = response.body?.getReader();
|
|
389
|
+
if (!reader)
|
|
390
|
+
throw new Error("storyboard_source_bundle_size_invalid");
|
|
391
|
+
const chunks = [];
|
|
392
|
+
let bytes = 0;
|
|
393
|
+
for (;;) {
|
|
394
|
+
const { done, value } = await reader.read();
|
|
395
|
+
if (done)
|
|
396
|
+
break;
|
|
397
|
+
bytes += value.byteLength;
|
|
398
|
+
if (bytes > MAX_BUNDLE_BYTES) {
|
|
399
|
+
await reader.cancel().catch(() => undefined);
|
|
400
|
+
throw new Error("storyboard_source_bundle_size_invalid");
|
|
401
|
+
}
|
|
402
|
+
chunks.push(Buffer.from(value));
|
|
403
|
+
}
|
|
404
|
+
if (bytes === 0 || (declared > 0 && declared !== bytes))
|
|
405
|
+
throw new Error("storyboard_source_bundle_size_invalid");
|
|
406
|
+
return Buffer.concat(chunks, bytes);
|
|
407
|
+
}
|
|
408
|
+
async function submitDesignSystemDraft(opts, input) {
|
|
409
|
+
if (mcpSurface(opts) !== "build")
|
|
410
|
+
throw new Error("storyboard_requires_build_surface");
|
|
411
|
+
const localDir = await resolveWorkspacePathSafely(opts.workspaceRoot, stringArg(input, "localDir"));
|
|
412
|
+
await assertAuthFileOutsideAppDir(opts.authFile, localDir);
|
|
413
|
+
const expectedCurrentCommit = stringArg(input, "expectedCurrentCommit");
|
|
414
|
+
const expectedDraftRunId = input.expectedDraftRunId === null ? null : stringArg(input, "expectedDraftRunId");
|
|
415
|
+
const bundle = await packStoryboardSourceDirectory(localDir);
|
|
416
|
+
const digest = createHash("sha256").update(`${platformBaseUrl(opts)}:${expectedCurrentCommit}:${expectedDraftRunId ?? "none"}:${bundle.sha256}`).digest();
|
|
417
|
+
digest[6] = (digest[6] & 0x0f) | 0x40;
|
|
418
|
+
digest[8] = (digest[8] & 0x3f) | 0x80;
|
|
419
|
+
const hex = digest.subarray(0, 16).toString("hex");
|
|
420
|
+
const requestId = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
421
|
+
const payload = await fetchJson(opts, `${platformBaseUrl(opts)}/api/harness/storyboard/draft-bundle`, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: {
|
|
424
|
+
...await jsonHeaders(opts), "content-type": "application/gzip",
|
|
425
|
+
"x-topline-request-id": requestId,
|
|
426
|
+
"x-topline-expected-current-commit": expectedCurrentCommit,
|
|
427
|
+
"x-topline-expected-draft-run-id": expectedDraftRunId ?? "none"
|
|
428
|
+
},
|
|
429
|
+
body: new Uint8Array(bundle.body)
|
|
430
|
+
}, Math.max(opts.timeoutMs, 120_000));
|
|
431
|
+
return { ...asRecord(payload), requestId, bundleSha256: bundle.sha256 };
|
|
432
|
+
}
|
|
433
|
+
async function runLocalGit(args) {
|
|
434
|
+
return new Promise((resolve, reject) => {
|
|
435
|
+
const child = spawn("git", ["-c", "core.fsmonitor=false", ...args], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
436
|
+
let output = "";
|
|
437
|
+
const collect = (chunk) => { output = (output + chunk.toString()).slice(-2000); };
|
|
438
|
+
child.stdout.on("data", collect);
|
|
439
|
+
child.stderr.on("data", collect);
|
|
440
|
+
const timer = setTimeout(() => child.kill(), 60_000);
|
|
441
|
+
child.on("error", reject);
|
|
442
|
+
child.on("close", (code) => {
|
|
443
|
+
clearTimeout(timer);
|
|
444
|
+
if (code === 0)
|
|
445
|
+
resolve(output);
|
|
446
|
+
else
|
|
447
|
+
reject(new Error(`storyboard_git_command_failed:${code ?? "unknown"}`));
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
}
|
|
316
451
|
export async function waitForArtifactDeploy(opts, input, onProgress) {
|
|
317
452
|
const artifactId = stringArg(input, "artifactId");
|
|
318
453
|
const operationId = stringArg(input, "operationId");
|
|
@@ -469,13 +604,19 @@ export function storybookAccess(opts) {
|
|
|
469
604
|
};
|
|
470
605
|
}
|
|
471
606
|
export async function packArtifactAppDirectory(appDir, archivePrefix = "") {
|
|
607
|
+
return packDirectory(appDir, archivePrefix, shouldExclude);
|
|
608
|
+
}
|
|
609
|
+
export async function packStoryboardSourceDirectory(appDir) {
|
|
610
|
+
return packDirectory(appDir, "", shouldExcludeStoryboard);
|
|
611
|
+
}
|
|
612
|
+
async function packDirectory(appDir, archivePrefix, exclude) {
|
|
472
613
|
const root = path.resolve(appDir);
|
|
473
614
|
if ((await lstat(root)).isSymbolicLink())
|
|
474
615
|
throw new Error("artifact_app_bundle_symlink_forbidden:.");
|
|
475
616
|
const prefix = archivePrefix ? validateArchivePrefix(archivePrefix) : "";
|
|
476
617
|
const files = [];
|
|
477
618
|
const excluded = [];
|
|
478
|
-
await walk(root, root, files, excluded);
|
|
619
|
+
await walk(root, root, files, excluded, exclude);
|
|
479
620
|
if (files.length === 0)
|
|
480
621
|
throw new Error("artifact_app_bundle_empty");
|
|
481
622
|
let byteCount = 0;
|
|
@@ -506,21 +647,21 @@ export async function packArtifactAppDirectory(appDir, archivePrefix = "") {
|
|
|
506
647
|
function compareUtf8Paths(left, right) {
|
|
507
648
|
return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
|
|
508
649
|
}
|
|
509
|
-
async function walk(root, current, files, excluded) {
|
|
650
|
+
async function walk(root, current, files, excluded, exclude) {
|
|
510
651
|
const entries = await readdir(current, { withFileTypes: true });
|
|
511
652
|
for (const entry of entries) {
|
|
512
653
|
const absolute = path.join(current, entry.name);
|
|
513
654
|
const relative = toPosix(path.relative(root, absolute));
|
|
514
655
|
validateRelativePath(relative);
|
|
515
|
-
|
|
516
|
-
if (stat.isSymbolicLink())
|
|
517
|
-
throw new Error(`artifact_app_bundle_symlink_forbidden:${relative}`);
|
|
518
|
-
if (shouldExclude(relative, entry.isDirectory())) {
|
|
656
|
+
if (exclude(relative, entry.isDirectory())) {
|
|
519
657
|
excluded.push(relative);
|
|
520
658
|
continue;
|
|
521
659
|
}
|
|
660
|
+
const stat = await lstat(absolute);
|
|
661
|
+
if (stat.isSymbolicLink())
|
|
662
|
+
throw new Error(`artifact_app_bundle_symlink_forbidden:${relative}`);
|
|
522
663
|
if (entry.isDirectory()) {
|
|
523
|
-
await walk(root, absolute, files, excluded);
|
|
664
|
+
await walk(root, absolute, files, excluded, exclude);
|
|
524
665
|
}
|
|
525
666
|
else if (entry.isFile()) {
|
|
526
667
|
if (stat.nlink > 1)
|
|
@@ -650,6 +791,13 @@ function shouldExclude(relative, isDirectory) {
|
|
|
650
791
|
return true;
|
|
651
792
|
return false;
|
|
652
793
|
}
|
|
794
|
+
function shouldExcludeStoryboard(relative, isDirectory) {
|
|
795
|
+
const parts = relative.split("/").filter(Boolean);
|
|
796
|
+
const name = parts.at(-1) ?? "";
|
|
797
|
+
return name.startsWith(".env") || parts.some((part) => part === ".git" || part === "node_modules"
|
|
798
|
+
|| part === "storybook-static" || part === ".topline" || part === ".cursor")
|
|
799
|
+
|| (isDirectory && (name === ".vite" || name === ".cache"));
|
|
800
|
+
}
|
|
653
801
|
function tarHeader(name, size) {
|
|
654
802
|
const header = Buffer.alloc(BLOCK_SIZE);
|
|
655
803
|
const { fileName, prefix } = splitTarName(name);
|
|
@@ -698,9 +846,9 @@ function padSize(size) {
|
|
|
698
846
|
const remainder = size % BLOCK_SIZE;
|
|
699
847
|
return remainder === 0 ? 0 : BLOCK_SIZE - remainder;
|
|
700
848
|
}
|
|
701
|
-
async function fetchJson(opts, url, init) {
|
|
849
|
+
async function fetchJson(opts, url, init, timeoutMs = opts.timeoutMs) {
|
|
702
850
|
const controller = new AbortController();
|
|
703
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
851
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
704
852
|
try {
|
|
705
853
|
const response = await (opts.fetchImpl ?? fetch)(url, { ...init, signal: controller.signal });
|
|
706
854
|
const text = await response.text();
|
|
@@ -940,6 +1088,68 @@ const BUILD_MCP_TOOLS = [
|
|
|
940
1088
|
required: ["ok", "storybookUrl", "mcpUrl", "mcpServers"]
|
|
941
1089
|
}
|
|
942
1090
|
},
|
|
1091
|
+
{
|
|
1092
|
+
name: "platform_get_design_system_state",
|
|
1093
|
+
description: "Get the tenant Storyboard's published and draft commits, run status, and preview URL before editing or publishing. May reconcile a completed managed draft.",
|
|
1094
|
+
annotations: mcpToolAnnotations("platform_get_design_system_state", { readOnly: false }),
|
|
1095
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1096
|
+
outputSchema: { type: "object" }
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
name: "platform_checkout_design_system",
|
|
1100
|
+
description: "Download the exact published tenant Storyboard source into a new local directory for Codex or Claude Code to edit and run with npm ci and npm run storybook. Never overwrites an existing directory.",
|
|
1101
|
+
annotations: mcpToolAnnotations("platform_checkout_design_system", { readOnly: false, openWorld: true }),
|
|
1102
|
+
inputSchema: { type: "object", properties: { targetDir: { type: "string", minLength: 1 } }, required: ["targetDir"], additionalProperties: false },
|
|
1103
|
+
outputSchema: { type: "object", properties: { ok: { type: "boolean" }, directory: { type: "string" }, currentCommit: { type: "string" }, localCommands: { type: "array", items: { type: "string" } } }, required: ["ok", "directory", "currentCommit", "localCommands"] }
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
name: "platform_submit_design_system_draft",
|
|
1107
|
+
description: "Package a locally edited Storyboard source directory and submit it as a private draft against exact published and draft revisions. The server validates and builds it before it can be published.",
|
|
1108
|
+
annotations: mcpToolAnnotations("platform_submit_design_system_draft", { readOnly: false, openWorld: true }),
|
|
1109
|
+
inputSchema: {
|
|
1110
|
+
type: "object",
|
|
1111
|
+
properties: {
|
|
1112
|
+
localDir: { type: "string", minLength: 1 },
|
|
1113
|
+
expectedCurrentCommit: { type: "string", pattern: "^[a-f0-9]{40}$" },
|
|
1114
|
+
expectedDraftRunId: { anyOf: [{ type: "string", format: "uuid" }, { type: "null" }] }
|
|
1115
|
+
},
|
|
1116
|
+
required: ["localDir", "expectedCurrentCommit", "expectedDraftRunId"], additionalProperties: false
|
|
1117
|
+
},
|
|
1118
|
+
outputSchema: { type: "object", properties: { ok: { type: "boolean" }, runId: { type: "string" }, draftCommit: { type: "string" }, draftState: { type: "string" }, previewUrl: { type: "string" }, requestId: { type: "string" }, bundleSha256: { type: "string" } }, required: ["ok", "runId", "draftCommit", "draftState", "previewUrl", "requestId", "bundleSha256"] }
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
name: "platform_edit_design_system",
|
|
1122
|
+
description: "Start an asynchronous Storyboard draft edit in an existing writable Topline Storyboard conversation. Pass expectedDraftRunId from the last state read (null for no draft); stale or concurrent retries are rejected.",
|
|
1123
|
+
annotations: mcpToolAnnotations("platform_edit_design_system", { readOnly: false, openWorld: true }),
|
|
1124
|
+
inputSchema: {
|
|
1125
|
+
type: "object",
|
|
1126
|
+
properties: {
|
|
1127
|
+
conversationId: { type: "string", format: "uuid" },
|
|
1128
|
+
instruction: { type: "string", minLength: 1, maxLength: 10000 },
|
|
1129
|
+
expectedDraftRunId: { anyOf: [{ type: "string", format: "uuid" }, { type: "null" }] }
|
|
1130
|
+
},
|
|
1131
|
+
required: ["conversationId", "instruction", "expectedDraftRunId"],
|
|
1132
|
+
additionalProperties: false
|
|
1133
|
+
},
|
|
1134
|
+
outputSchema: { type: "object", properties: { runId: { type: "string" }, previewUrl: { type: "string" } }, required: ["runId", "previewUrl"] }
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
name: "platform_publish_design_system",
|
|
1138
|
+
description: "Publish the exact reviewed Storyboard draft after its build passes. Managed edits require their writable Storyboard conversation; local submissions require the submitting actor. Always pass state-bound run, draft, and current commits.",
|
|
1139
|
+
annotations: mcpToolAnnotations("platform_publish_design_system", { readOnly: false, destructive: true, openWorld: true }),
|
|
1140
|
+
inputSchema: {
|
|
1141
|
+
type: "object",
|
|
1142
|
+
properties: {
|
|
1143
|
+
conversationId: { type: "string", format: "uuid" },
|
|
1144
|
+
expectedDraftRunId: { type: "string", format: "uuid" },
|
|
1145
|
+
expectedDraftCommit: { type: "string", pattern: "^[a-f0-9]{40}$" },
|
|
1146
|
+
expectedCurrentCommit: { anyOf: [{ type: "string", pattern: "^[a-f0-9]{40}$" }, { type: "null" }] }
|
|
1147
|
+
},
|
|
1148
|
+
required: ["expectedDraftRunId", "expectedDraftCommit", "expectedCurrentCommit"],
|
|
1149
|
+
additionalProperties: false
|
|
1150
|
+
},
|
|
1151
|
+
outputSchema: { type: "object", properties: { publishedCommit: { type: "string" } }, required: ["publishedCommit"] }
|
|
1152
|
+
},
|
|
943
1153
|
{
|
|
944
1154
|
name: "platform_validate_artifact",
|
|
945
1155
|
description: "Read trusted smoke and health evidence for a private artifact draft without requiring a browser session.",
|
|
@@ -1063,6 +1273,7 @@ function toolError(message) {
|
|
|
1063
1273
|
return structuredToolError(message);
|
|
1064
1274
|
}
|
|
1065
1275
|
const BUILD_MCP_RECOVERY_ERROR_VALUES = {
|
|
1276
|
+
authorization_required: { retryable: false, recoveryAction: "Clear only this connection's cached OAuth credentials, then reconnect the Build MCP through browser sign-in. If using a manual bearer token, replace the expired or revoked token. Recover existing artifact/deployment progress before retrying a write." },
|
|
1066
1277
|
platform_harness_timeout: { retryable: true, recoveryAction: "Retry the call. For a deployment, use platform_list_artifact_drafts to recover the artifact and platform_wait_artifact_deploy to resume progress." },
|
|
1067
1278
|
platform_mcp_tool_error: { retryable: false, recoveryAction: "Read the returned error code, correct the request or OAuth scopes, then retry." },
|
|
1068
1279
|
artifact_deploy_wait_timeout: { retryable: true, recoveryAction: "Call platform_wait_artifact_deploy again with the same artifactId and operationId." },
|
|
@@ -1087,6 +1298,8 @@ function structuredToolError(message) {
|
|
|
1087
1298
|
};
|
|
1088
1299
|
}
|
|
1089
1300
|
function recoveryErrorCode(message) {
|
|
1301
|
+
if (/^platform_harness_http_error:401(?::|$)/.test(message))
|
|
1302
|
+
return "authorization_required";
|
|
1090
1303
|
const known = [
|
|
1091
1304
|
"artifact_delete_plan_stale",
|
|
1092
1305
|
"artifact_deploy_wait_timeout",
|