@topline-build/harness-mcp 0.5.0 → 0.5.2

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 CHANGED
@@ -4,11 +4,17 @@ All notable customer-visible Build MCP changes are recorded here. A source chang
4
4
 
5
5
  ## Unreleased
6
6
 
7
- - Generate a versioned runtime reference and machine-readable documentation manifest from the registry used by MCP `tools/list`.
8
- - Enforce documentation freshness in tests, continuous integration, and release validation.
9
- - Define documentation ownership, pull request impact review, release synchronization, and recurring maintenance.
10
- - Notify the website of an exact npm release immediately, with daily recovery and a review-only pull request boundary.
11
- - Add machine-readable Codex, Claude Code, and Cursor certification with distinct CLI, protocol, client-mediated read, structured failure, and reviewed apply evidence.
7
+ ## 0.5.2 - 2026-09-26
8
+
9
+ - Report rejected bearer credentials as `authorization_required`, with scoped cache-clearing and browser reconnect guidance instead of input-correction advice.
10
+ - Guide connection-specific cache cleanup and recovery of existing deployment progress before a write is retried.
11
+ - 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.
12
+
13
+ ## 0.5.1 - 2026-09-18
14
+
15
+ - Harden the bundled Claude certification script with strict tool isolation, effective-runtime proof, and noninteractive JSONL validation.
16
+ - Update certification setup documentation and regression coverage.
17
+ - Harden live certification CI and add an exact-version website documentation dispatch probe.
12
18
 
13
19
  ## 0.5.0
14
20
 
package/README.md CHANGED
@@ -22,6 +22,8 @@ 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
+
25
27
  ## Typical workflow
26
28
 
27
29
  1. Use `platform_init_artifact_app` to create a starter, or point at an existing artifact app.
@@ -75,9 +77,11 @@ can consume provider model quota and runs only for clients whose CLI can reject
75
77
  user/global MCP configuration and disable every built-in tool. Claude Code is
76
78
  currently the supported mediated client; Codex and Cursor report
77
79
  `strict_client_tool_isolation_unavailable` instead of starting an agent with
78
- broader access. The Claude process uses strict MCP configuration, safe mode,
80
+ broader access. The Claude process uses strict MCP configuration, bare mode,
79
81
  no built-in tools, an allowlisted environment, an isolated OAuth cache, and a
80
- server exposing only `platform_inspect_artifact_app`. The write credential is
82
+ server exposing only `platform_inspect_artifact_app`. Its bare noninteractive
83
+ permission bypass is accepted only when Claude's structured init event proves
84
+ that this is the sole effective tool and MCP server. The write credential is
81
85
  available only to the direct exact-SHA apply path. Missing client CLIs, provider
82
86
  login, URL/app inputs, or credentials produce explicit `skipped` results and
83
87
  exit successfully by default; `--require-all` exits 2 for any skip. A failed
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.0";
35
+ export declare const HARNESS_MCP_VERSION = "0.5.2";
36
36
  export declare const DEFAULT_OAUTH_SCOPES: readonly string[];
37
37
  export interface BuildMcpSetupOption {
38
38
  readonly flag: string;
package/dist/index.js CHANGED
@@ -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.0";
16
+ export const HARNESS_MCP_VERSION = "0.5.2";
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." },
@@ -1063,6 +1063,7 @@ function toolError(message) {
1063
1063
  return structuredToolError(message);
1064
1064
  }
1065
1065
  const BUILD_MCP_RECOVERY_ERROR_VALUES = {
1066
+ 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
1067
  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
1068
  platform_mcp_tool_error: { retryable: false, recoveryAction: "Read the returned error code, correct the request or OAuth scopes, then retry." },
1068
1069
  artifact_deploy_wait_timeout: { retryable: true, recoveryAction: "Call platform_wait_artifact_deploy again with the same artifactId and operationId." },
@@ -1087,6 +1088,8 @@ function structuredToolError(message) {
1087
1088
  };
1088
1089
  }
1089
1090
  function recoveryErrorCode(message) {
1091
+ if (/^platform_harness_http_error:401(?::|$)/.test(message))
1092
+ return "authorization_required";
1090
1093
  const known = [
1091
1094
  "artifact_delete_plan_stale",
1092
1095
  "artifact_deploy_wait_timeout",
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "package": {
4
4
  "name": "@topline-build/harness-mcp",
5
- "version": "0.5.0",
5
+ "version": "0.5.2",
6
6
  "bin": {
7
7
  "topline-mcp": "dist/cli.js"
8
8
  }
@@ -12,7 +12,7 @@
12
12
  "defaultVersion": "2025-11-25",
13
13
  "server": {
14
14
  "name": "topline-build-stdio",
15
- "version": "0.5.0"
15
+ "version": "0.5.2"
16
16
  }
17
17
  },
18
18
  "generatedFrom": {
@@ -171,6 +171,11 @@
171
171
  "recoveryAction": "Inspect the code and message, correct the input if needed, then retry the same tool."
172
172
  },
173
173
  "catalog": [
174
+ {
175
+ "code": "authorization_required",
176
+ "retryable": false,
177
+ "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."
178
+ },
174
179
  {
175
180
  "code": "platform_harness_timeout",
176
181
  "retryable": true,
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Generated from `src/index.ts#BUILD_MCP_TOOLS`. Do not edit this file by hand.
4
4
 
5
- Package: `@topline-build/harness-mcp@0.5.0`
5
+ Package: `@topline-build/harness-mcp@0.5.2`
6
6
  Transport: `stdio`
7
7
  Default protocol: `2025-11-25`
8
8
  Canonical guide: https://topline.build/docs/build-mcp-guide
@@ -32,6 +32,7 @@ The first authenticated tool call normally opens browser approval. Set `TOPLINE_
32
32
 
33
33
  Tool failures set `isError: true` and return `structuredContent.error` with `code`, `message`, `retryable`, and `recoveryAction`.
34
34
 
35
+ - `authorization_required` (not automatically retryable): 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.
35
36
  - `platform_harness_timeout` (retryable): Retry the call. For a deployment, use platform_list_artifact_drafts to recover the artifact and platform_wait_artifact_deploy to resume progress.
36
37
  - `platform_mcp_tool_error` (not automatically retryable): Read the returned error code, correct the request or OAuth scopes, then retry.
37
38
  - `artifact_deploy_wait_timeout` (retryable): Call platform_wait_artifact_deploy again with the same artifactId and operationId.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topline-build/harness-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Topline Build MCP for creating and deploying artifact app drafts.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,6 +20,23 @@ const EXPECTED_TOOLS = [
20
20
  "platform_validate_artifact",
21
21
  "platform_plan_artifact_draft_delete"
22
22
  ];
23
+ const PROVIDER_ENV_KEYS = [
24
+ "ANTHROPIC_API_KEY",
25
+ "TOPLINE_CERTIFICATION_ALLOW_ANTHROPIC_API_KEY",
26
+ "TOPLINE_CERTIFICATION_ALLOW_BEDROCK",
27
+ "CLAUDE_CODE_USE_BEDROCK",
28
+ "ANTHROPIC_MODEL",
29
+ "ANTHROPIC_SMALL_FAST_MODEL",
30
+ "DISABLE_PROMPT_CACHING",
31
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
32
+ "AWS_ACCESS_KEY_ID",
33
+ "AWS_SECRET_ACCESS_KEY",
34
+ "AWS_SESSION_TOKEN",
35
+ "AWS_REGION",
36
+ "AWS_DEFAULT_REGION",
37
+ "AWS_EC2_METADATA_DISABLED",
38
+ "AWS_STS_REGIONAL_ENDPOINTS"
39
+ ];
23
40
  const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
24
41
  const manifest = JSON.parse(await readFile(new URL("../docs/generated/build-mcp-manifest.json", import.meta.url), "utf8"));
25
42
  let args;
@@ -38,8 +55,7 @@ if (process.argv[2] === "--certification-read-only-server") {
38
55
  }
39
56
 
40
57
  async function runCertificationReadOnlyServer(values) {
41
- delete process.env.ANTHROPIC_API_KEY;
42
- delete process.env.TOPLINE_CERTIFICATION_ALLOW_ANTHROPIC_API_KEY;
58
+ for (const key of PROVIDER_ENV_KEYS) delete process.env[key];
43
59
  const opts = parseHarnessOptions(values, withoutManualToken(process.env));
44
60
  const server = createHarnessMcpStdioServer(opts, ["platform_inspect_artifact_app"]);
45
61
  await server.connect(new StdioServerTransport());
@@ -179,10 +195,7 @@ async function agentMediatedRead(name, command, scratch, apiUrl, appDir, workspa
179
195
  if (!appDir) return skipped("certification_app_missing");
180
196
  const server = {
181
197
  command: process.execPath,
182
- env: {
183
- ANTHROPIC_API_KEY: "",
184
- TOPLINE_CERTIFICATION_ALLOW_ANTHROPIC_API_KEY: ""
185
- },
198
+ env: Object.fromEntries(PROVIDER_ENV_KEYS.map((key) => [key, ""])),
186
199
  args: [
187
200
  fileURLToPath(import.meta.url),
188
201
  "--certification-read-only-server",
@@ -198,21 +211,20 @@ async function agentMediatedRead(name, command, scratch, apiUrl, appDir, workspa
198
211
  const configPath = path.join(configDir, "mcp.json");
199
212
  await writeFile(configPath, JSON.stringify({ mcpServers: { "topline-build": server } }, null, 2), "utf8");
200
213
  const result = run(command, [
201
- "--mcp-config", configPath, "--strict-mcp-config", "--safe-mode",
214
+ "--mcp-config", configPath, "--strict-mcp-config", "--bare",
202
215
  "--tools", "", "--allowedTools", "mcp__topline-build__platform_inspect_artifact_app",
203
- "--permission-mode", "dontAsk", "--no-chrome", "--disable-slash-commands",
216
+ "--dangerously-skip-permissions", "--no-chrome", "--disable-slash-commands",
204
217
  "--no-session-persistence", "--max-budget-usd", "0.25",
205
- "-p", "--output-format", "stream-json", prompt
218
+ "-p", "--verbose", "--output-format", "stream-json", prompt
206
219
  ], scratch, 180000, strictAgentEnvironment(process.env));
207
220
  if (!result.ok) {
208
- return authUnavailable(result.output)
209
- ? skipped("provider_credentials_or_client_session_unavailable")
210
- : failed("client_mediated_read_failed", result.exitCode);
221
+ return clientMediatedFailure(result);
211
222
  }
212
223
  const usedTool = hasToolCallEvidence(result.output, "platform_inspect_artifact_app");
213
224
  const certified = hasAssistantCertificationEvidence(result.output);
214
- return usedTool && certified
215
- ? passed({ tool: "platform_inspect_artifact_app", providerModelUsed: true })
225
+ const isolated = hasStrictClaudeInitEvidence(result.output);
226
+ return usedTool && certified && isolated
227
+ ? passed({ tool: "platform_inspect_artifact_app", providerModelUsed: true, strictToolIsolation: true, provider: process.env.CLAUDE_CODE_USE_BEDROCK === "1" ? "bedrock" : "anthropic" })
216
228
  : failed("client_mediated_tool_evidence_missing", result.exitCode);
217
229
  }
218
230
 
@@ -330,6 +342,12 @@ async function certifyProtocol({ apiUrl, token, appDir, workspaceRoot }) {
330
342
  }
331
343
 
332
344
  function selfTestReport() {
345
+ const timedOutProbe = run({ executable: process.execPath, prefix: [] }, ["-e", "console.log('api key authentication required'); setInterval(() => {}, 1000)"], process.cwd(), 1000);
346
+ const timedOutFailure = clientMediatedFailure(timedOutProbe);
347
+ if (!timedOutProbe.timedOut || !authUnavailable(timedOutProbe.output) || timedOutFailure.status !== "failed" || timedOutFailure.reason !== "client_mediated_read_timed_out"
348
+ || clientMediatedFailure({ timedOut: false, output: "not logged in", exitCode: 1 }).status !== "skipped") {
349
+ throw new Error("client_timeout_classification_self_test_failed");
350
+ }
333
351
  const toolName = "platform_inspect_artifact_app";
334
352
  const promptOnly = JSON.stringify({ type: "user", content: `Call ${toolName} and print TOPLINE_CLIENT_READ_CERTIFIED.` });
335
353
  const toolEvent = JSON.stringify({ type: "item.completed", item: { type: "mcp_tool_call", tool: toolName } });
@@ -355,6 +373,18 @@ function selfTestReport() {
355
373
  ANTHROPIC_API_KEY: "anthropic-secret",
356
374
  TOPLINE_CERTIFICATION_ALLOW_ANTHROPIC_API_KEY: "1"
357
375
  });
376
+ const bedrockStrictEnvironment = strictAgentEnvironment({
377
+ PATH: "kept-path",
378
+ AWS_ACCESS_KEY_ID: "temporary-access-key",
379
+ AWS_SECRET_ACCESS_KEY: "temporary-secret-key",
380
+ AWS_SESSION_TOKEN: "temporary-session-token",
381
+ AWS_REGION: "us-west-2",
382
+ AWS_DEFAULT_REGION: "us-west-2",
383
+ CLAUDE_CODE_USE_BEDROCK: "1",
384
+ ANTHROPIC_MODEL: "global.anthropic.claude-sonnet-4-6",
385
+ ANTHROPIC_SMALL_FAST_MODEL: "global.anthropic.claude-sonnet-4-6",
386
+ TOPLINE_CERTIFICATION_ALLOW_BEDROCK: "1"
387
+ });
358
388
  if (hasToolCallEvidence(promptOnly, toolName)
359
389
  || !hasToolCallEvidence(toolEvent, toolName)
360
390
  || hasAssistantCertificationEvidence(promptOnly)
@@ -367,7 +397,12 @@ function selfTestReport() {
367
397
  || strictEnvironment.USERPROFILE !== "kept-profile"
368
398
  || ["AWS_ACCESS_KEY_ID", "DATABASE_URL", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "CURSOR_API_KEY", "TOPLINE_MCP_TOKEN"].some((key) => key in strictEnvironment)
369
399
  || optedInStrictEnvironment.ANTHROPIC_API_KEY !== "anthropic-secret"
370
- || "TOPLINE_CERTIFICATION_ALLOW_ANTHROPIC_API_KEY" in optedInStrictEnvironment) {
400
+ || "TOPLINE_CERTIFICATION_ALLOW_ANTHROPIC_API_KEY" in optedInStrictEnvironment
401
+ || bedrockStrictEnvironment.CLAUDE_CODE_USE_BEDROCK !== "1"
402
+ || bedrockStrictEnvironment.AWS_ACCESS_KEY_ID !== "temporary-access-key"
403
+ || bedrockStrictEnvironment.AWS_SECRET_ACCESS_KEY !== "temporary-secret-key"
404
+ || bedrockStrictEnvironment.AWS_SESSION_TOKEN !== "temporary-session-token"
405
+ || "TOPLINE_CERTIFICATION_ALLOW_BEDROCK" in bedrockStrictEnvironment) {
371
406
  throw new Error("client_tool_evidence_self_test_failed");
372
407
  }
373
408
  return {
@@ -435,7 +470,7 @@ function clientCommand(name) {
435
470
  return { executable, prefix: [] };
436
471
  }
437
472
 
438
- function run(command, commandArgs, cwd, timeout = 30000, env = withoutManualToken(process.env)) {
473
+ function run(command, commandArgs, cwd, timeout = 30000, env = clientProbeEnvironment(process.env)) {
439
474
  const result = spawnSync(command.executable, [...command.prefix, ...commandArgs], {
440
475
  cwd,
441
476
  encoding: "utf8",
@@ -446,14 +481,22 @@ function run(command, commandArgs, cwd, timeout = 30000, env = withoutManualToke
446
481
  return {
447
482
  ok: result.status === 0,
448
483
  missing: result.error?.code === "ENOENT",
484
+ timedOut: result.error?.code === "ETIMEDOUT",
449
485
  exitCode: result.status,
450
486
  stdout: result.stdout || "",
451
487
  output: `${result.stdout || ""}\n${result.stderr || ""}`
452
488
  };
453
489
  }
454
490
 
491
+ function clientMediatedFailure(result) {
492
+ if (result.timedOut) return failed("client_mediated_read_timed_out", result.exitCode);
493
+ return authUnavailable(result.output)
494
+ ? skipped("provider_credentials_or_client_session_unavailable")
495
+ : failed("client_mediated_read_failed", result.exitCode);
496
+ }
497
+
455
498
  function authUnavailable(output) {
456
- return /not logged in|not authenticated|authentication required|api key|login required|please login|sign in/i.test(output);
499
+ return /not logged in|not authenticated|authentication required|api key|login required|please login|sign in|unable to locate credentials|security token|AccessDeniedException|UnrecognizedClientException/i.test(output);
457
500
  }
458
501
 
459
502
  function hasToolCallEvidence(output, toolName) {
@@ -468,6 +511,29 @@ function hasToolCallEvidence(output, toolName) {
468
511
  return false;
469
512
  }
470
513
 
514
+ function hasStrictClaudeInitEvidence(output) {
515
+ for (const line of output.split(/\r?\n/)) {
516
+ if (!line.trim().startsWith("{")) continue;
517
+ try {
518
+ const value = JSON.parse(line);
519
+ if (value?.type !== "system" || value?.subtype !== "init") continue;
520
+ return Array.isArray(value.tools)
521
+ && value.tools.length === 1
522
+ && value.tools[0] === "mcp__topline-build__platform_inspect_artifact_app"
523
+ && Array.isArray(value.mcp_servers)
524
+ && value.mcp_servers.length === 1
525
+ && value.mcp_servers[0]?.name === "topline-build"
526
+ && value.mcp_servers[0]?.status === "connected"
527
+ && value.permissionMode === "bypassPermissions"
528
+ && Array.isArray(value.slash_commands)
529
+ && value.slash_commands.length === 0;
530
+ } catch {
531
+ // Only the structured Claude init event can prove the effective tool set.
532
+ }
533
+ }
534
+ return false;
535
+ }
536
+
471
537
  function containsToolCall(value, toolName) {
472
538
  if (!value || typeof value !== "object") return false;
473
539
  const type = typeof value.type === "string" ? value.type : "";
@@ -510,17 +576,34 @@ function withoutManualToken(source) {
510
576
  }
511
577
 
512
578
  function strictAgentEnvironment(source) {
579
+ const env = clientProbeEnvironment(source);
580
+ if (source.TOPLINE_CERTIFICATION_ALLOW_ANTHROPIC_API_KEY === "1" && source.ANTHROPIC_API_KEY) {
581
+ env.ANTHROPIC_API_KEY = source.ANTHROPIC_API_KEY;
582
+ }
583
+ const bedrockCredentialKeys = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"];
584
+ if (source.TOPLINE_CERTIFICATION_ALLOW_BEDROCK === "1"
585
+ && source.CLAUDE_CODE_USE_BEDROCK === "1"
586
+ && bedrockCredentialKeys.every((key) => typeof source[key] === "string" && source[key].length > 0)) {
587
+ for (const key of [
588
+ ...bedrockCredentialKeys,
589
+ "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_EC2_METADATA_DISABLED", "AWS_STS_REGIONAL_ENDPOINTS",
590
+ "CLAUDE_CODE_USE_BEDROCK", "ANTHROPIC_MODEL", "ANTHROPIC_SMALL_FAST_MODEL",
591
+ "DISABLE_PROMPT_CACHING", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"
592
+ ]) {
593
+ if (typeof source[key] === "string" && source[key].length > 0) env[key] = source[key];
594
+ }
595
+ }
596
+ return env;
597
+ }
598
+
599
+ function clientProbeEnvironment(source) {
513
600
  const allowed = new Set([
514
601
  "ALL_PROXY", "APPDATA", "CI", "COMSPEC", "FORCE_COLOR", "HTTP_PROXY", "HTTPS_PROXY",
515
602
  "LANG", "LC_ALL", "LOCALAPPDATA", "NODE_EXTRA_CA_CERTS", "NO_COLOR", "NO_PROXY", "PATH",
516
603
  "PATHEXT", "PROGRAMDATA", "SSL_CERT_DIR", "SSL_CERT_FILE", "SYSTEMROOT", "TEMP", "TMP",
517
604
  "USERPROFILE", "WINDIR"
518
605
  ]);
519
- const env = Object.fromEntries(Object.entries(source).filter(([key]) => allowed.has(key.toUpperCase())));
520
- if (source.TOPLINE_CERTIFICATION_ALLOW_ANTHROPIC_API_KEY === "1" && source.ANTHROPIC_API_KEY) {
521
- env.ANTHROPIC_API_KEY = source.ANTHROPIC_API_KEY;
522
- }
523
- return env;
606
+ return Object.fromEntries(Object.entries(source).filter(([key]) => allowed.has(key.toUpperCase())));
524
607
  }
525
608
 
526
609
  function structured(result) {