@topline-build/harness-mcp 0.1.2 → 0.2.0
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/README.md +31 -4
- package/dist/index.d.ts +44 -2
- package/dist/index.js +200 -30
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Topline
|
|
1
|
+
# Topline Build MCP
|
|
2
2
|
|
|
3
3
|
Local stdio MCP connector for deploying Topline artifact app drafts from Codex,
|
|
4
4
|
Claude Code, or another local MCP host.
|
|
@@ -13,6 +13,7 @@ Required environment:
|
|
|
13
13
|
|
|
14
14
|
Optional environment:
|
|
15
15
|
|
|
16
|
+
- `TOPLINE_MCP_SURFACE` (`build` by default; `harness` for legacy compatibility)
|
|
16
17
|
- `TOPLINE_MCP_PROTOCOL_VERSION`
|
|
17
18
|
- `TOPLINE_WORKSPACE_ROOT`
|
|
18
19
|
- `TOPLINE_MCP_AUTH_FILE`
|
|
@@ -43,6 +44,32 @@ npx -y @topline-build/harness-mcp --url https://hawx.topline.build
|
|
|
43
44
|
Set `TOPLINE_WORKSPACE_ROOT` when your MCP host starts outside the repo that
|
|
44
45
|
contains the artifact app.
|
|
45
46
|
|
|
46
|
-
Topline Developer Access provides
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
Topline Developer Access provides Build MCP snippets for Codex and Claude Code.
|
|
48
|
+
Those snippets contain no raw token; the first tool call opens a browser
|
|
49
|
+
approval flow scoped to artifact building.
|
|
50
|
+
|
|
51
|
+
## Storybook
|
|
52
|
+
|
|
53
|
+
Use the `platform_storybook_access` tool to get the tenant Storybook preview URL
|
|
54
|
+
and the HTTP MCP client config. For clients with HTTP MCP OAuth support, use
|
|
55
|
+
the returned `cursorMcpJson`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"storybook": {
|
|
61
|
+
"url": "https://your-topline-host/api/design-systems/storybook/mcp",
|
|
62
|
+
"type": "http"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Storybook MCP access uses the harness bearer/OAuth credentials with
|
|
69
|
+
`harness:artifact:read`; OAuth discovery is available at
|
|
70
|
+
`/.well-known/oauth-protected-resource/mcp/build`.
|
|
71
|
+
|
|
72
|
+
If your MCP client does not support HTTP MCP OAuth, use the returned
|
|
73
|
+
`manualBearerMcpJson` template with a read-scoped `TOPLINE_MCP_TOKEN` in the
|
|
74
|
+
client's secret mechanism. Opening the visual preview URL in a browser still
|
|
75
|
+
requires a signed-in Topline session.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
-
import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { type CallToolResult, type ListToolsResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
export interface HarnessMcpStdioOptions {
|
|
4
4
|
apiUrl: string;
|
|
5
|
+
/** Defaults to the Build MCP. Use harness only for legacy deployments. */
|
|
6
|
+
surface?: "build" | "harness";
|
|
5
7
|
token?: string;
|
|
6
8
|
authFile: string;
|
|
7
9
|
oauthScopes: string[];
|
|
@@ -27,7 +29,7 @@ export interface PackedArtifactAppBundle {
|
|
|
27
29
|
sha256: string;
|
|
28
30
|
excluded: string[];
|
|
29
31
|
}
|
|
30
|
-
export declare const HARNESS_MCP_VERSION = "0.
|
|
32
|
+
export declare const HARNESS_MCP_VERSION = "0.2.0";
|
|
31
33
|
export declare function parseHarnessOptions(argv: string[], env?: Record<string, string | undefined>): HarnessMcpStdioOptions;
|
|
32
34
|
export declare function runHarnessMcpStdioCli(): Promise<void>;
|
|
33
35
|
export declare function cliInfo(argv: string[]): string | null;
|
|
@@ -72,7 +74,47 @@ export declare function openArtifact(opts: HarnessMcpStdioOptions, artifactId: s
|
|
|
72
74
|
liveUrl: string | null;
|
|
73
75
|
versionUrl: string | null;
|
|
74
76
|
}>;
|
|
77
|
+
export declare function storybookAccess(opts: HarnessMcpStdioOptions): {
|
|
78
|
+
ok: boolean;
|
|
79
|
+
storybookUrl: string;
|
|
80
|
+
previewUrl: string;
|
|
81
|
+
mcpUrl: string;
|
|
82
|
+
mcpServers: {
|
|
83
|
+
storybook: {
|
|
84
|
+
url: string;
|
|
85
|
+
type: string;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
cursorMcpJson: {
|
|
89
|
+
mcpServers: {
|
|
90
|
+
storybook: {
|
|
91
|
+
url: string;
|
|
92
|
+
type: string;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
manualBearerMcpJson: {
|
|
97
|
+
mcpServers: {
|
|
98
|
+
storybook: {
|
|
99
|
+
url: string;
|
|
100
|
+
type: string;
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: string;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
auth: {
|
|
108
|
+
mcp: string;
|
|
109
|
+
preview: string;
|
|
110
|
+
resourceMetadataUrl: string;
|
|
111
|
+
scope: string;
|
|
112
|
+
manualBearerEnv: string;
|
|
113
|
+
};
|
|
114
|
+
operatorAction: string;
|
|
115
|
+
};
|
|
75
116
|
export declare function packArtifactAppDirectory(appDir: string, archivePrefix?: string): Promise<PackedArtifactAppBundle>;
|
|
117
|
+
export declare function listTools(): ListToolsResult;
|
|
76
118
|
export declare function getBearerToken(opts: HarnessMcpStdioOptions): Promise<string>;
|
|
77
119
|
export declare function browserOpenCommand(url: string, platform?: string): {
|
|
78
120
|
command: string;
|
package/dist/index.js
CHANGED
|
@@ -8,11 +8,12 @@ import path from "node:path";
|
|
|
8
8
|
import { gzipSync } from "node:zlib";
|
|
9
9
|
import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
-
import {
|
|
11
|
+
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv";
|
|
12
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
12
13
|
const DEFAULT_MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
13
14
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
14
|
-
const SERVER_NAME = "topline-
|
|
15
|
-
export const HARNESS_MCP_VERSION = "0.
|
|
15
|
+
const SERVER_NAME = "topline-build-stdio";
|
|
16
|
+
export const HARNESS_MCP_VERSION = "0.2.0";
|
|
16
17
|
const DEFAULT_OAUTH_SCOPES = ["harness:artifact:read", "harness:artifact:write", "harness:artifact:deploy"];
|
|
17
18
|
const BLOCK_SIZE = 512;
|
|
18
19
|
const MAX_BUNDLE_BYTES = 64 * 1024 * 1024;
|
|
@@ -34,6 +35,9 @@ export function parseHarnessOptions(argv, env = process.env) {
|
|
|
34
35
|
const token = getArg("--token") || firstEnv(env, "TOPLINE_MCP_TOKEN", "PLATFORM_MCP_TOKEN", "OVERSITE_MCP_TOKEN") || undefined;
|
|
35
36
|
const timeoutRaw = getArg("--timeout-ms") || firstEnv(env, "TOPLINE_MCP_TIMEOUT_MS", "PLATFORM_MCP_TIMEOUT_MS", "OVERSITE_MCP_TIMEOUT_MS");
|
|
36
37
|
const protocolVersion = getArg("--protocol-version") || firstEnv(env, "TOPLINE_MCP_PROTOCOL_VERSION", "PLATFORM_MCP_PROTOCOL_VERSION") || DEFAULT_MCP_PROTOCOL_VERSION;
|
|
38
|
+
const surfaceRaw = getArg("--surface") || firstEnv(env, "TOPLINE_MCP_SURFACE") || "build";
|
|
39
|
+
if (surfaceRaw !== "build" && surfaceRaw !== "harness")
|
|
40
|
+
throw new Error("TOPLINE_MCP_SURFACE must be build or harness");
|
|
37
41
|
const workspaceRoot = path.resolve(getArg("--workspace-root") || firstEnv(env, "TOPLINE_WORKSPACE_ROOT", "PLATFORM_WORKSPACE_ROOT") || process.cwd());
|
|
38
42
|
const timeoutMs = timeoutRaw ? Number(timeoutRaw) : DEFAULT_TIMEOUT_MS;
|
|
39
43
|
if (!apiUrl)
|
|
@@ -41,13 +45,14 @@ export function parseHarnessOptions(argv, env = process.env) {
|
|
|
41
45
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
42
46
|
throw new Error("TOPLINE_MCP_TIMEOUT_MS must be a positive number");
|
|
43
47
|
const normalizedApiUrl = normalizeApiUrl(apiUrl);
|
|
44
|
-
const authFile = path.resolve(getArg("--auth-file") || firstEnv(env, "TOPLINE_MCP_AUTH_FILE", "PLATFORM_MCP_AUTH_FILE") || defaultAuthFile(normalizedApiUrl));
|
|
48
|
+
const authFile = path.resolve(getArg("--auth-file") || firstEnv(env, "TOPLINE_MCP_AUTH_FILE", "PLATFORM_MCP_AUTH_FILE") || defaultAuthFile(normalizedApiUrl, surfaceRaw));
|
|
45
49
|
const scopes = (getArg("--scopes") || firstEnv(env, "TOPLINE_MCP_SCOPES", "PLATFORM_MCP_SCOPES") || DEFAULT_OAUTH_SCOPES.join(" "))
|
|
46
50
|
.split(/[\s,]+/)
|
|
47
51
|
.map((scope) => scope.trim())
|
|
48
52
|
.filter(Boolean);
|
|
49
53
|
return {
|
|
50
54
|
apiUrl: normalizedApiUrl,
|
|
55
|
+
surface: surfaceRaw,
|
|
51
56
|
token,
|
|
52
57
|
authFile,
|
|
53
58
|
oauthScopes: scopes.length > 0 ? [...new Set(scopes)] : DEFAULT_OAUTH_SCOPES,
|
|
@@ -78,6 +83,7 @@ export function cliInfo(argv) {
|
|
|
78
83
|
"Environment:",
|
|
79
84
|
" TOPLINE_API_URL Required unless --url is passed",
|
|
80
85
|
" TOPLINE_WORKSPACE_ROOT Defaults to the current working directory",
|
|
86
|
+
" TOPLINE_MCP_SURFACE Defaults to build; harness keeps legacy OAuth compatibility",
|
|
81
87
|
" TOPLINE_MCP_TOKEN Optional manual token fallback",
|
|
82
88
|
" TOPLINE_MCP_AUTH_FILE Optional OAuth credential cache path",
|
|
83
89
|
" TOPLINE_MCP_TIMEOUT_MS Defaults to 120000",
|
|
@@ -90,11 +96,21 @@ export function cliInfo(argv) {
|
|
|
90
96
|
export function createHarnessMcpStdioServer(opts) {
|
|
91
97
|
const mcp = new McpServer({ name: SERVER_NAME, version: HARNESS_MCP_VERSION }, { capabilities: { tools: {} } });
|
|
92
98
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => listTools());
|
|
93
|
-
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
99
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
100
|
+
const reportsProgress = req.params.name === "platform_deploy_artifact_app";
|
|
101
|
+
if (reportsProgress)
|
|
102
|
+
await sendProgress(extra, 0, 1, "Starting artifact deployment");
|
|
94
103
|
try {
|
|
95
|
-
|
|
104
|
+
const result = await callHarnessTool(opts, req.params.name, req.params.arguments ?? {});
|
|
105
|
+
if (reportsProgress)
|
|
106
|
+
await sendProgress(extra, 1, 1, "Completed artifact deployment");
|
|
107
|
+
return result;
|
|
96
108
|
}
|
|
97
109
|
catch (err) {
|
|
110
|
+
if (err instanceof McpError)
|
|
111
|
+
throw err;
|
|
112
|
+
if (reportsProgress)
|
|
113
|
+
await sendProgress(extra, 1, 1, "Failed artifact deployment");
|
|
98
114
|
return {
|
|
99
115
|
content: [{ type: "text", text: redactSecrets(err instanceof Error ? err.message : String(err), opts) }],
|
|
100
116
|
isError: true
|
|
@@ -105,18 +121,36 @@ export function createHarnessMcpStdioServer(opts) {
|
|
|
105
121
|
}
|
|
106
122
|
export async function callHarnessTool(opts, name, args) {
|
|
107
123
|
const input = isRecord(args) ? args : {};
|
|
124
|
+
const tool = listTools().tools.find((candidate) => candidate.name === name);
|
|
125
|
+
if (!tool)
|
|
126
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${name}`);
|
|
127
|
+
const inputError = validateJsonSchema(tool.inputSchema, input);
|
|
128
|
+
if (inputError)
|
|
129
|
+
return toolError(`Invalid arguments for ${name}: ${inputError}`);
|
|
130
|
+
let result;
|
|
108
131
|
switch (name) {
|
|
109
132
|
case "platform_deploy_artifact_app":
|
|
110
|
-
|
|
133
|
+
result = toolResult(await deployArtifactApp(opts, input));
|
|
134
|
+
break;
|
|
111
135
|
case "platform_artifact_status":
|
|
112
|
-
|
|
136
|
+
result = toolResult(await getArtifactStatus(opts, stringArg(input, "artifactId"), numberArg(input, "limit")));
|
|
137
|
+
break;
|
|
113
138
|
case "platform_read_artifact_logs":
|
|
114
|
-
|
|
139
|
+
result = toolResult(await readArtifactLogs(opts, input));
|
|
140
|
+
break;
|
|
115
141
|
case "platform_open_artifact":
|
|
116
|
-
|
|
142
|
+
result = toolResult(await openArtifact(opts, stringArg(input, "artifactId")));
|
|
143
|
+
break;
|
|
144
|
+
case "platform_storybook_access":
|
|
145
|
+
result = toolResult(storybookAccess(opts));
|
|
146
|
+
break;
|
|
117
147
|
default:
|
|
118
|
-
|
|
148
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${name}`);
|
|
119
149
|
}
|
|
150
|
+
const outputError = tool.outputSchema
|
|
151
|
+
? validateJsonSchema(tool.outputSchema, result.structuredContent)
|
|
152
|
+
: null;
|
|
153
|
+
return outputError ? toolError(`Invalid output for ${name}: ${outputError}`) : result;
|
|
120
154
|
}
|
|
121
155
|
export async function deployArtifactApp(opts, input) {
|
|
122
156
|
const appDir = stringArg(input, "appDir");
|
|
@@ -223,6 +257,36 @@ export async function openArtifact(opts, artifactId) {
|
|
|
223
257
|
versionUrl: typeof draft?.versionUrl === "string" ? draft.versionUrl : typeof live?.versionUrl === "string" ? live.versionUrl : null
|
|
224
258
|
};
|
|
225
259
|
}
|
|
260
|
+
export function storybookAccess(opts) {
|
|
261
|
+
const baseUrl = platformBaseUrl(opts);
|
|
262
|
+
const storybookUrl = `${baseUrl}/api/design-systems/storybook/`;
|
|
263
|
+
const mcpUrl = `${baseUrl}/api/design-systems/storybook/mcp`;
|
|
264
|
+
const mcpServers = { storybook: { url: mcpUrl, type: "http" } };
|
|
265
|
+
const manualBearerMcpServers = {
|
|
266
|
+
storybook: {
|
|
267
|
+
url: mcpUrl,
|
|
268
|
+
type: "http",
|
|
269
|
+
headers: { Authorization: "Bearer <TOPLINE_MCP_TOKEN>" }
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
ok: true,
|
|
274
|
+
storybookUrl,
|
|
275
|
+
previewUrl: storybookUrl,
|
|
276
|
+
mcpUrl,
|
|
277
|
+
mcpServers,
|
|
278
|
+
cursorMcpJson: { mcpServers },
|
|
279
|
+
manualBearerMcpJson: { mcpServers: manualBearerMcpServers },
|
|
280
|
+
auth: {
|
|
281
|
+
mcp: "topline_harness_bearer_with_harness_artifact_read",
|
|
282
|
+
preview: "topline_browser_session",
|
|
283
|
+
resourceMetadataUrl: `${baseUrl}/.well-known/oauth-protected-resource/mcp/${mcpSurface(opts)}`,
|
|
284
|
+
scope: "harness:artifact:read",
|
|
285
|
+
manualBearerEnv: "TOPLINE_MCP_TOKEN"
|
|
286
|
+
},
|
|
287
|
+
operatorAction: "Use cursorMcpJson for MCP clients with HTTP OAuth support. If your client requires static headers, use manualBearerMcpJson with a read-scoped TOPLINE_MCP_TOKEN. Open the preview URL in a browser signed in to Topline."
|
|
288
|
+
};
|
|
289
|
+
}
|
|
226
290
|
export async function packArtifactAppDirectory(appDir, archivePrefix = "") {
|
|
227
291
|
const root = path.resolve(appDir);
|
|
228
292
|
const prefix = archivePrefix ? validateArchivePrefix(archivePrefix) : "";
|
|
@@ -393,72 +457,170 @@ async function fetchJson(opts, url, init) {
|
|
|
393
457
|
clearTimeout(timeout);
|
|
394
458
|
}
|
|
395
459
|
}
|
|
396
|
-
function listTools() {
|
|
460
|
+
export function listTools() {
|
|
397
461
|
return {
|
|
398
462
|
tools: [
|
|
399
463
|
{
|
|
400
464
|
name: "platform_deploy_artifact_app",
|
|
401
465
|
description: "Package a local artifact app directory, upload it, deploy a private draft, and return status.",
|
|
466
|
+
annotations: mcpToolAnnotations("platform_deploy_artifact_app", { readOnly: false, openWorld: true }),
|
|
402
467
|
inputSchema: {
|
|
403
468
|
type: "object",
|
|
404
469
|
properties: {
|
|
405
|
-
appDir: { type: "string" },
|
|
470
|
+
appDir: { type: "string", minLength: 1 },
|
|
406
471
|
artifactId: { type: "string" },
|
|
407
472
|
title: { type: "string" },
|
|
408
473
|
description: { type: "string" },
|
|
409
474
|
projectId: { type: "string" }
|
|
410
475
|
},
|
|
411
|
-
required: ["appDir"]
|
|
476
|
+
required: ["appDir"],
|
|
477
|
+
additionalProperties: false
|
|
478
|
+
},
|
|
479
|
+
outputSchema: {
|
|
480
|
+
type: "object",
|
|
481
|
+
properties: {
|
|
482
|
+
ok: { type: "boolean" },
|
|
483
|
+
artifactId: { type: "string" },
|
|
484
|
+
artifactVersionId: { type: "string" },
|
|
485
|
+
bundle: { type: "object" },
|
|
486
|
+
create: { type: "object" },
|
|
487
|
+
deploy: { type: "object" }
|
|
488
|
+
},
|
|
489
|
+
required: ["ok", "artifactId", "artifactVersionId", "bundle", "create", "deploy"]
|
|
412
490
|
}
|
|
413
491
|
},
|
|
414
492
|
{
|
|
415
493
|
name: "platform_artifact_status",
|
|
416
494
|
description: "Read private draft deployment status for a hosted artifact app.",
|
|
495
|
+
annotations: mcpToolAnnotations("platform_artifact_status", { readOnly: true, openWorld: true }),
|
|
417
496
|
inputSchema: {
|
|
418
497
|
type: "object",
|
|
419
|
-
properties: { artifactId: { type: "string" }, limit: { type: "
|
|
420
|
-
required: ["artifactId"]
|
|
498
|
+
properties: { artifactId: { type: "string", minLength: 1 }, limit: { type: "integer", minimum: 1, maximum: 50 } },
|
|
499
|
+
required: ["artifactId"],
|
|
500
|
+
additionalProperties: false
|
|
501
|
+
},
|
|
502
|
+
outputSchema: {
|
|
503
|
+
type: "object",
|
|
504
|
+
properties: {
|
|
505
|
+
ok: { type: "boolean" }, phase: { type: "string" }, artifactId: { type: "string" },
|
|
506
|
+
deployments: { type: "array", items: { type: "object" } }
|
|
507
|
+
},
|
|
508
|
+
required: ["ok", "phase", "artifactId", "deployments"]
|
|
421
509
|
}
|
|
422
510
|
},
|
|
423
511
|
{
|
|
424
512
|
name: "platform_read_artifact_logs",
|
|
425
513
|
description: "Read bounded, redacted deployment logs for a hosted artifact app.",
|
|
514
|
+
annotations: mcpToolAnnotations("platform_read_artifact_logs", { readOnly: true, openWorld: true }),
|
|
426
515
|
inputSchema: {
|
|
427
516
|
type: "object",
|
|
428
517
|
properties: {
|
|
429
|
-
artifactId: { type: "string" },
|
|
518
|
+
artifactId: { type: "string", minLength: 1 },
|
|
430
519
|
deploymentRecordId: { type: "string" },
|
|
431
520
|
environment: { type: "string", enum: ["draft", "live"] },
|
|
432
|
-
limit: { type: "
|
|
521
|
+
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
433
522
|
severity: { type: "string" },
|
|
434
523
|
contains: { type: "string" },
|
|
435
|
-
since: { type: "string" },
|
|
436
|
-
windowMinutes: { type: "
|
|
524
|
+
since: { type: "string", format: "date-time" },
|
|
525
|
+
windowMinutes: { type: "integer", minimum: 1, maximum: 1440 },
|
|
437
526
|
trace: { type: "string" },
|
|
438
527
|
requestId: { type: "string" },
|
|
439
528
|
route: { type: "string" }
|
|
440
529
|
},
|
|
441
|
-
required: ["artifactId"]
|
|
530
|
+
required: ["artifactId"],
|
|
531
|
+
additionalProperties: false
|
|
532
|
+
},
|
|
533
|
+
outputSchema: {
|
|
534
|
+
type: "object",
|
|
535
|
+
properties: {
|
|
536
|
+
ok: { type: "boolean" },
|
|
537
|
+
phase: { type: "string" },
|
|
538
|
+
artifactId: { type: "string" },
|
|
539
|
+
entries: { type: "array", items: { type: "object" } }
|
|
540
|
+
},
|
|
541
|
+
required: ["ok", "phase", "artifactId"]
|
|
442
542
|
}
|
|
443
543
|
},
|
|
444
544
|
{
|
|
445
545
|
name: "platform_open_artifact",
|
|
446
546
|
description: "Return the operator URL and latest draft/live URLs for a hosted artifact app.",
|
|
547
|
+
annotations: mcpToolAnnotations("platform_open_artifact", { readOnly: true, openWorld: true }),
|
|
548
|
+
inputSchema: {
|
|
549
|
+
type: "object",
|
|
550
|
+
properties: { artifactId: { type: "string", minLength: 1 } },
|
|
551
|
+
required: ["artifactId"],
|
|
552
|
+
additionalProperties: false
|
|
553
|
+
},
|
|
554
|
+
outputSchema: {
|
|
555
|
+
type: "object",
|
|
556
|
+
properties: {
|
|
557
|
+
ok: { type: "boolean" }, artifactId: { type: "string" }, openUrl: { type: "string" },
|
|
558
|
+
draftUrl: { type: ["string", "null"] }, liveUrl: { type: ["string", "null"] }, versionUrl: { type: ["string", "null"] }
|
|
559
|
+
},
|
|
560
|
+
required: ["ok", "artifactId", "openUrl", "draftUrl", "liveUrl", "versionUrl"]
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "platform_storybook_access",
|
|
565
|
+
description: "Return the tenant Storybook preview URL and HTTP MCP server config.",
|
|
566
|
+
annotations: mcpToolAnnotations("platform_storybook_access", { readOnly: true }),
|
|
447
567
|
inputSchema: {
|
|
448
568
|
type: "object",
|
|
449
|
-
properties: {
|
|
450
|
-
|
|
569
|
+
properties: {},
|
|
570
|
+
additionalProperties: false
|
|
571
|
+
},
|
|
572
|
+
outputSchema: {
|
|
573
|
+
type: "object",
|
|
574
|
+
properties: {
|
|
575
|
+
ok: { type: "boolean" }, storybookUrl: { type: "string" }, mcpUrl: { type: "string" }, mcpServers: { type: "object" }
|
|
576
|
+
},
|
|
577
|
+
required: ["ok", "storybookUrl", "mcpUrl", "mcpServers"]
|
|
451
578
|
}
|
|
452
579
|
}
|
|
453
580
|
]
|
|
454
581
|
};
|
|
455
582
|
}
|
|
456
|
-
function
|
|
583
|
+
function mcpToolAnnotations(name, policy) {
|
|
457
584
|
return {
|
|
458
|
-
|
|
459
|
-
|
|
585
|
+
title: name.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" "),
|
|
586
|
+
readOnlyHint: policy.readOnly,
|
|
587
|
+
destructiveHint: policy.destructive ?? false,
|
|
588
|
+
...(policy.idempotent ? { idempotentHint: true } : {}),
|
|
589
|
+
openWorldHint: policy.openWorld ?? false
|
|
460
590
|
};
|
|
461
591
|
}
|
|
592
|
+
function toolResult(value) {
|
|
593
|
+
const structuredContent = value;
|
|
594
|
+
const content = [{ type: "text", text: JSON.stringify(value, null, 2) }];
|
|
595
|
+
const seen = new Set();
|
|
596
|
+
for (const field of ["url", "openUrl", "promotionUrl", "draftUrl", "liveUrl", "versionUrl", "storybookUrl", "mcpUrl"]) {
|
|
597
|
+
const uri = structuredContent[field];
|
|
598
|
+
if (typeof uri !== "string" || !/^https?:\/\//.test(uri) || seen.has(uri))
|
|
599
|
+
continue;
|
|
600
|
+
seen.add(uri);
|
|
601
|
+
content.push({ type: "resource_link", uri, name: field });
|
|
602
|
+
}
|
|
603
|
+
return { content, structuredContent };
|
|
604
|
+
}
|
|
605
|
+
function toolError(message) {
|
|
606
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
607
|
+
}
|
|
608
|
+
const jsonSchemaValidator = new AjvJsonSchemaValidator();
|
|
609
|
+
function validateJsonSchema(schema, value) {
|
|
610
|
+
const result = jsonSchemaValidator.getValidator(schema)(value);
|
|
611
|
+
return result.valid ? null : result.errorMessage;
|
|
612
|
+
}
|
|
613
|
+
async function sendProgress(extra, progress, total, message) {
|
|
614
|
+
const progressToken = extra?._meta?.progressToken;
|
|
615
|
+
if (typeof progressToken !== "string" && typeof progressToken !== "number")
|
|
616
|
+
return;
|
|
617
|
+
try {
|
|
618
|
+
await extra.sendNotification({ method: "notifications/progress", params: { progressToken, progress, total, message } });
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
// Progress is optional; a disconnected client must not change the tool result.
|
|
622
|
+
}
|
|
623
|
+
}
|
|
462
624
|
async function jsonHeaders(opts) {
|
|
463
625
|
const bearerToken = await getBearerToken(opts);
|
|
464
626
|
return {
|
|
@@ -514,7 +676,7 @@ function isRefreshTokenRejected(err) {
|
|
|
514
676
|
}
|
|
515
677
|
async function runOAuthLogin(opts) {
|
|
516
678
|
const issuer = platformBaseUrl(opts);
|
|
517
|
-
const resource = `${issuer}/mcp
|
|
679
|
+
const resource = `${issuer}/mcp/${mcpSurface(opts)}`;
|
|
518
680
|
const callback = await startOAuthCallbackServer();
|
|
519
681
|
try {
|
|
520
682
|
const registration = await oauthFetchJson(opts, `${issuer}/oauth/register`, {
|
|
@@ -617,6 +779,11 @@ async function readStoredOAuthCredentials(opts) {
|
|
|
617
779
|
const scopes = Array.isArray(parsed.scopes) ? parsed.scopes.filter((scope) => typeof scope === "string") : DEFAULT_OAUTH_SCOPES;
|
|
618
780
|
if (!issuer || !resource || !clientId)
|
|
619
781
|
return null;
|
|
782
|
+
const expectedIssuer = platformBaseUrl(opts);
|
|
783
|
+
if (issuer !== expectedIssuer || resource !== `${expectedIssuer}/mcp/${mcpSurface(opts)}`)
|
|
784
|
+
return null;
|
|
785
|
+
if (!opts.oauthScopes.every((scope) => scopes.includes(scope)))
|
|
786
|
+
return null;
|
|
620
787
|
return {
|
|
621
788
|
issuer,
|
|
622
789
|
resource,
|
|
@@ -720,9 +887,12 @@ function artifactAppsPath(opts) {
|
|
|
720
887
|
function platformBaseUrl(opts) {
|
|
721
888
|
return opts.apiUrl.replace(/\/api\/harness$/, "").replace(/\/$/, "");
|
|
722
889
|
}
|
|
723
|
-
function
|
|
724
|
-
|
|
725
|
-
|
|
890
|
+
function mcpSurface(opts) {
|
|
891
|
+
return opts.surface ?? "build";
|
|
892
|
+
}
|
|
893
|
+
function defaultAuthFile(apiUrl, surface) {
|
|
894
|
+
const key = createHash("sha256").update(`${platformBaseUrl({ apiUrl })}:${surface}`).digest("hex").slice(0, 16);
|
|
895
|
+
return path.join(os.homedir(), ".topline", "build-mcp", `${key}.json`);
|
|
726
896
|
}
|
|
727
897
|
function normalizeApiUrl(raw) {
|
|
728
898
|
let url;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topline-build/harness-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Topline Build MCP for creating and deploying artifact app drafts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|