@showly/mcp-server 0.3.0 → 0.4.1

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 CHANGED
@@ -60,9 +60,9 @@ Write tools for version history and deletion:
60
60
  - `rollback_to_version` — restore a previous site version
61
61
  - `delete_preview`, `delete_site`
62
62
 
63
- Write tools for the guest trial flow:
63
+ Write tool for adopting a site created through Showly's public trial flow:
64
64
 
65
- - `create_trial_site`, `claim_trial_site`
65
+ - `claim_trial_site`
66
66
 
67
67
  `publish_site` is exposed through a narrower `publish:confirm` scope and never
68
68
  publishes on its first call: it returns a short-lived, deployment-bound token
@@ -116,4 +116,4 @@ experimental_use_rmcp_client = true
116
116
  ## License
117
117
 
118
118
  Proprietary — © Showly. This package is distributed for use with the Showly
119
- hosting service; it is not open source. See https://showly.ai/terms.
119
+ hosting service; it is not open source. See https://showly.ai/legal/terms.
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  export type Target = "claude-code" | "codex" | "stdout";
3
+ /**
4
+ * The host that will RECEIVE a token printed to stdout.
5
+ *
6
+ * `--to` describes where this CLI writes the credential; `--agent` describes
7
+ * which host will use it. They are the same for the two hosts we can write
8
+ * directly, but intentionally separate for `--print-token`, where another
9
+ * agent (OpenClaw / Cursor / Cline) receives the value after this process
10
+ * exits.
11
+ */
12
+ export declare const LOGIN_AGENTS: readonly ["claude-code", "codex", "cursor", "openclaw", "cline"];
13
+ export type LoginAgent = (typeof LOGIN_AGENTS)[number];
3
14
  /**
4
15
  * The version of THIS copy of the package, read from the manifest that ships
5
16
  * beside it. `manifest.test.ts` pins manifest.json, package.json,
@@ -67,6 +78,17 @@ export declare function performSkillInstall(target: Exclude<Target, "stdout">, e
67
78
  * human to check the code echo instead.
68
79
  */
69
80
  export declare const LOGIN_CLIENT_ID = "showly-mcp-cli";
81
+ /**
82
+ * Exact, self-declared device client ids emitted by THIS CLI.
83
+ *
84
+ * These are not verified product identities: `/oauth/device` is public and a
85
+ * client_id has no secret. Their value is that the token keeps the host the
86
+ * human selected instead of collapsing every device login into the generic
87
+ * `showly-mcp-cli` bucket. The API maps only these exact values to display
88
+ * names; lookalikes stay neutral.
89
+ */
90
+ export declare const LOGIN_AGENT_CLIENT_IDS: Record<LoginAgent, string>;
91
+ export declare function loginClientId(target: Target, agent?: LoginAgent): string;
70
92
  /** The env var name emitted into config snippets that must not hold a secret. */
71
93
  export declare const TOKEN_ENV_VAR = "SHOWLY_TOKEN";
72
94
  export type DeviceStart = {
@@ -195,7 +217,7 @@ export declare function createCancelScope(target?: SignalTarget): {
195
217
  * requests/min against the token endpoint.
196
218
  */
197
219
  export declare const SERVER_EXTENSION_ALLOWANCE_MS: number;
198
- export declare function startDeviceFlow(apiUrl: string, deps?: LoginDeps): Promise<DeviceStart>;
220
+ export declare function startDeviceFlow(apiUrl: string, deps?: LoginDeps, clientId?: string): Promise<DeviceStart>;
199
221
  /**
200
222
  * Poll /oauth/token until the human decides, per RFC 8628 §3.4-3.5.
201
223
  *
@@ -215,6 +237,7 @@ export declare function pollForDeviceToken(input: {
215
237
  deviceCode: string;
216
238
  intervalSec: number;
217
239
  expiresAt: Date;
240
+ clientId?: string;
218
241
  }, deps?: LoginDeps): Promise<DeviceToken>;
219
242
  export type LoginResult = {
220
243
  target: Target;
@@ -237,6 +260,7 @@ export type LoginResult = {
237
260
  */
238
261
  export declare function performLogin(opts: {
239
262
  target: Target;
263
+ agent?: LoginAgent;
240
264
  env?: NodeJS.ProcessEnv;
241
265
  }, deps?: LoginDeps): Promise<LoginResult>;
242
266
  /**
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@
7
7
  // showly-mcp install --to codex --with-skill # also installs showly-hosting
8
8
  // showly-mcp install --to stdout # prints the snippet for manual paste
9
9
  // showly-mcp login --to claude-code # RFC 8628 device flow, no browser here
10
+ // showly-mcp login --agent openclaw --print-token # label a token handed to OpenClaw
10
11
  // showly-mcp manifest # prints manifest.json
11
12
  // showly-mcp --help / --version
12
13
  // showly-mcp <command> --help # exits 0 iff this copy has <command>
@@ -29,12 +30,28 @@
29
30
  // even when it is advertised. So a human whose agent runs on a box with no
30
31
  // browser, or who is holding a phone rather than sitting at the machine, had a
31
32
  // working server-side flow and no way to reach it.
32
- import { readFileSync, mkdirSync, rmSync, writeFileSync, existsSync, realpathSync, } from "node:fs";
33
+ import { readFileSync, readdirSync, mkdirSync, rmSync, writeFileSync, existsSync, realpathSync, } from "node:fs";
33
34
  import { dirname, join } from "node:path";
34
35
  import { homedir } from "node:os";
35
36
  import { pathToFileURL } from "node:url";
36
37
  import { loadManifest } from "./index.js";
37
- import { SHOWLY_HOSTING_SKILL_MARKDOWN, SHOWLY_HOSTING_SKILL_NAME, SHOWLY_LEGACY_SKILL_NAME, } from "./showly-hosting-skill.js";
38
+ import { SHOWLY_HOSTING_SKILL_DIRECTORY, SHOWLY_HOSTING_SKILL_NAME, SHOWLY_LEGACY_SKILL_NAME, } from "./showly-hosting-skill.js";
39
+ /**
40
+ * The host that will RECEIVE a token printed to stdout.
41
+ *
42
+ * `--to` describes where this CLI writes the credential; `--agent` describes
43
+ * which host will use it. They are the same for the two hosts we can write
44
+ * directly, but intentionally separate for `--print-token`, where another
45
+ * agent (OpenClaw / Cursor / Cline) receives the value after this process
46
+ * exits.
47
+ */
48
+ export const LOGIN_AGENTS = [
49
+ "claude-code",
50
+ "codex",
51
+ "cursor",
52
+ "openclaw",
53
+ "cline",
54
+ ];
38
55
  /**
39
56
  * The version of THIS copy of the package, read from the manifest that ships
40
57
  * beside it. `manifest.test.ts` pins manifest.json, package.json,
@@ -65,7 +82,7 @@ function usage() {
65
82
  "",
66
83
  "Usage:",
67
84
  " showly-mcp install --to <claude-code|codex|stdout> [--with-skill]",
68
- " showly-mcp login [--to <claude-code|codex|stdout>] [--print-token]",
85
+ " showly-mcp login [--to <claude-code|codex|stdout>] [--agent <host>] [--print-token]",
69
86
  " showly-mcp manifest",
70
87
  " showly-mcp --version",
71
88
  "",
@@ -74,7 +91,8 @@ function usage() {
74
91
  "config. --to codex writes a config that reads the token from SHOWLY_TOKEN,",
75
92
  "so login also prints the export line that sets it. --print-token writes",
76
93
  "ONLY the token to stdout (everything else goes to stderr) so CI can",
77
- "capture it without it touching a file.",
94
+ "capture it without it touching a file. When that token is for another",
95
+ "host, pass --agent <cursor|openclaw|cline> so My Agents can name it.",
78
96
  "",
79
97
  "Environment overrides:",
80
98
  " SHOWLY_MCP_URL full URL to your MCP endpoint (default https://mcp.showly.ai)",
@@ -219,15 +237,35 @@ export function performSkillInstall(target, env = process.env) {
219
237
  ? codexHome || join(homedir(), ".codex")
220
238
  : join(homedir(), ".claude");
221
239
  const skillsRoot = join(hostDirectory, "skills");
222
- const path = join(skillsRoot, SHOWLY_HOSTING_SKILL_NAME, "SKILL.md");
240
+ const skillDirectory = join(skillsRoot, SHOWLY_HOSTING_SKILL_NAME);
241
+ const path = join(skillDirectory, "SKILL.md");
223
242
  const removedLegacyPath = removeLegacySkill(skillsRoot);
224
- const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
225
- if (existing === SHOWLY_HOSTING_SKILL_MARKDOWN) {
226
- return { path, wrote: false, alreadyConfigured: true, removedLegacyPath };
243
+ let wrote = false;
244
+ for (const relativePath of skillFiles(SHOWLY_HOSTING_SKILL_DIRECTORY)) {
245
+ const sourcePath = join(SHOWLY_HOSTING_SKILL_DIRECTORY, relativePath);
246
+ const destinationPath = join(skillDirectory, relativePath);
247
+ const expected = readFileSync(sourcePath);
248
+ const existing = existsSync(destinationPath)
249
+ ? readFileSync(destinationPath)
250
+ : null;
251
+ if (existing?.equals(expected))
252
+ continue;
253
+ mkdirSync(dirname(destinationPath), { recursive: true });
254
+ writeFileSync(destinationPath, expected);
255
+ wrote = true;
227
256
  }
228
- mkdirSync(dirname(path), { recursive: true });
229
- writeFileSync(path, SHOWLY_HOSTING_SKILL_MARKDOWN, "utf8");
230
- return { path, wrote: true, alreadyConfigured: false, removedLegacyPath };
257
+ return { path, wrote, alreadyConfigured: !wrote, removedLegacyPath };
258
+ }
259
+ function skillFiles(root, relativeDirectory = "") {
260
+ const directory = join(root, relativeDirectory);
261
+ return readdirSync(directory, { withFileTypes: true })
262
+ .sort((left, right) => left.name.localeCompare(right.name))
263
+ .flatMap((entry) => {
264
+ const relativePath = join(relativeDirectory, entry.name);
265
+ return entry.isDirectory()
266
+ ? skillFiles(root, relativePath)
267
+ : [relativePath];
268
+ });
231
269
  }
232
270
  /**
233
271
  * Delete the superseded showly-publish skill directory, if we wrote it.
@@ -300,6 +338,28 @@ function writeCredentialFile(path, contents) {
300
338
  * human to check the code echo instead.
301
339
  */
302
340
  export const LOGIN_CLIENT_ID = "showly-mcp-cli";
341
+ /**
342
+ * Exact, self-declared device client ids emitted by THIS CLI.
343
+ *
344
+ * These are not verified product identities: `/oauth/device` is public and a
345
+ * client_id has no secret. Their value is that the token keeps the host the
346
+ * human selected instead of collapsing every device login into the generic
347
+ * `showly-mcp-cli` bucket. The API maps only these exact values to display
348
+ * names; lookalikes stay neutral.
349
+ */
350
+ export const LOGIN_AGENT_CLIENT_IDS = {
351
+ "claude-code": `${LOGIN_CLIENT_ID}/claude-code`,
352
+ codex: `${LOGIN_CLIENT_ID}/codex`,
353
+ cursor: `${LOGIN_CLIENT_ID}/cursor`,
354
+ openclaw: `${LOGIN_CLIENT_ID}/openclaw`,
355
+ cline: `${LOGIN_CLIENT_ID}/cline`,
356
+ };
357
+ export function loginClientId(target, agent) {
358
+ const resolvedAgent = agent ?? (target === "stdout" ? undefined : target);
359
+ return resolvedAgent
360
+ ? LOGIN_AGENT_CLIENT_IDS[resolvedAgent]
361
+ : LOGIN_CLIENT_ID;
362
+ }
303
363
  /** The env var name emitted into config snippets that must not hold a secret. */
304
364
  export const TOKEN_ENV_VAR = "SHOWLY_TOKEN";
305
365
  /**
@@ -518,7 +578,7 @@ function errorSummary(error, timeoutMs) {
518
578
  }
519
579
  /** Said on both routes to a dead code: the server's answer, and our deadline. */
520
580
  const EXPIRED_MESSAGE = "The code expired before it was approved. Run this command again for a fresh one.";
521
- export async function startDeviceFlow(apiUrl, deps = {}) {
581
+ export async function startDeviceFlow(apiUrl, deps = {}, clientId = LOGIN_CLIENT_ID) {
522
582
  const doFetch = deps.fetchImpl ?? fetch;
523
583
  // `scope` is optional on /oauth/device, and this used to send none at all.
524
584
  // The consent screen then listed no permissions, the human approved that,
@@ -539,7 +599,7 @@ export async function startDeviceFlow(apiUrl, deps = {}) {
539
599
  method: "POST",
540
600
  headers: { "content-type": "application/json" },
541
601
  body: JSON.stringify({
542
- client_id: LOGIN_CLIENT_ID,
602
+ client_id: clientId,
543
603
  scope: manifest.mcp.auth.default_scopes.join(" "),
544
604
  }),
545
605
  signal,
@@ -582,6 +642,7 @@ export async function pollForDeviceToken(input, deps = {}) {
582
642
  const doFetch = deps.fetchImpl ?? fetch;
583
643
  const sleep = deps.sleep ?? defaultSleep;
584
644
  const now = deps.now ?? (() => Date.now());
645
+ const log = deps.log ?? (() => { });
585
646
  const cancel = deps.signal;
586
647
  const timeoutMs = deps.requestTimeoutMs ?? POLL_REQUEST_TIMEOUT_MS;
587
648
  let intervalMs = Math.max(1, input.intervalSec) * 1000;
@@ -590,6 +651,8 @@ export async function pollForDeviceToken(input, deps = {}) {
590
651
  // failure and then said only "the code expired" would send the human to look
591
652
  // at their approval when the fault was never on their side.
592
653
  let lastTransportError;
654
+ let lastPendingLogAt;
655
+ let transientFailureLogged = false;
593
656
  for (;;) {
594
657
  if (cancel?.aborted)
595
658
  throw new LoginCancelledError();
@@ -610,7 +673,7 @@ export async function pollForDeviceToken(input, deps = {}) {
610
673
  body: JSON.stringify({
611
674
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
612
675
  device_code: input.deviceCode,
613
- client_id: LOGIN_CLIENT_ID,
676
+ client_id: input.clientId ?? LOGIN_CLIENT_ID,
614
677
  }),
615
678
  signal: attempt.signal,
616
679
  });
@@ -623,6 +686,10 @@ export async function pollForDeviceToken(input, deps = {}) {
623
686
  // either way — throwing on a dropped packet would burn it and make them
624
687
  // start over. Fall through to the next tick; `stopAt` still bounds this.
625
688
  lastTransportError = errorSummary(error, timeoutMs);
689
+ if (!transientFailureLogged) {
690
+ log(`Could not reach Showly (${lastTransportError}). Retrying the same approval; keep this command running.`);
691
+ transientFailureLogged = true;
692
+ }
626
693
  continue;
627
694
  }
628
695
  finally {
@@ -637,8 +704,32 @@ export async function pollForDeviceToken(input, deps = {}) {
637
704
  expires_in: body.expires_in,
638
705
  };
639
706
  }
707
+ // A gateway rate limit or server deploy is not an OAuth decision. The
708
+ // human may already have spent the one approval, so retry this device_code
709
+ // instead of turning availability into a fake terminal sign-in failure.
710
+ if (res.status === 408 ||
711
+ res.status === 425 ||
712
+ res.status === 429 ||
713
+ res.status >= 500) {
714
+ lastTransportError = `HTTP ${res.status}`;
715
+ if (!transientFailureLogged) {
716
+ log(`Showly is temporarily unavailable (${lastTransportError}). Retrying the same approval; keep this command running.`);
717
+ transientFailureLogged = true;
718
+ }
719
+ continue;
720
+ }
721
+ transientFailureLogged = false;
640
722
  switch (body.error) {
641
723
  case "authorization_pending":
724
+ // The initial prompt already says the command waits. Repeat the one
725
+ // critical instruction when the server confirms it is still pending,
726
+ // then only at a low cadence so a long wait is visible without turning
727
+ // one five-second poll into terminal spam.
728
+ if (lastPendingLogAt === undefined ||
729
+ now() - lastPendingLogAt >= 30_000) {
730
+ log("Approval is still pending. Keep this command running for the same approval; do not start another code.");
731
+ lastPendingLogAt = now();
732
+ }
642
733
  continue;
643
734
  // RFC 8628 §3.5: back off by 5 seconds and keep going. This is the one
644
735
  // error that is not terminal and not a no-op.
@@ -670,7 +761,8 @@ export async function performLogin(opts, deps = {}) {
670
761
  const env = opts.env ?? process.env;
671
762
  const { url, apiUrl } = resolveUrls(env);
672
763
  const log = deps.log ?? ((line) => console.error(line));
673
- const started = await startDeviceFlow(apiUrl, deps);
764
+ const clientId = loginClientId(opts.target, opts.agent);
765
+ const started = await startDeviceFlow(apiUrl, deps, clientId);
674
766
  const expiresAt = new Date(Date.now() + started.expires_in * 1000);
675
767
  log(buildLoginPrompt({
676
768
  verificationUri: started.verification_uri,
@@ -683,7 +775,13 @@ export async function performLogin(opts, deps = {}) {
683
775
  deviceCode: started.device_code,
684
776
  intervalSec: started.interval,
685
777
  expiresAt,
686
- }, deps);
778
+ clientId,
779
+ },
780
+ // `log` may be the production stderr default created above. Passing the
781
+ // original deps object silently dropped it when callers did not inject a
782
+ // sink, which made every real CLI wait quiet even though the poller had
783
+ // progress messages.
784
+ { ...deps, log });
687
785
  // Showly issues no refresh token, so this date is the moment a working agent
688
786
  // stops working and a human has to approve again. Say it out loud now, while
689
787
  // there is context, instead of leaving a 401 to be diagnosed in 90 days.
@@ -869,7 +967,7 @@ export function buildLoginOutput(result, opts = {}) {
869
967
  */
870
968
  const COMMAND_FLAGS = {
871
969
  install: { "--to": true, "--with-skill": false },
872
- login: { "--to": true, "--print-token": false },
970
+ login: { "--to": true, "--agent": true, "--print-token": false },
873
971
  manifest: {},
874
972
  };
875
973
  /** Accepted after any command, and handled before the command runs. */
@@ -937,6 +1035,14 @@ function parseTarget(parsed, fallback) {
937
1035
  return null;
938
1036
  return value;
939
1037
  }
1038
+ function parseLoginAgent(parsed) {
1039
+ const value = parsed.values.get("--agent");
1040
+ if (value === undefined)
1041
+ return undefined;
1042
+ if (!LOGIN_AGENTS.includes(value))
1043
+ return null;
1044
+ return value;
1045
+ }
940
1046
  const consoleIo = {
941
1047
  out: (line) => console.log(line),
942
1048
  err: (line) => console.error(line),
@@ -995,6 +1101,15 @@ export async function runCli(argv, io = consoleIo, env = process.env) {
995
1101
  io.err("login: --to must be claude-code, codex or stdout");
996
1102
  return 2;
997
1103
  }
1104
+ const agent = parseLoginAgent(parsed);
1105
+ if (agent === null) {
1106
+ io.err("login: --agent must be claude-code, codex, cursor, openclaw or cline");
1107
+ return 2;
1108
+ }
1109
+ if (target !== "stdout" && agent !== undefined && agent !== target) {
1110
+ io.err(`login: --agent ${agent} conflicts with --to ${target}; omit --agent or make them match`);
1111
+ return 2;
1112
+ }
998
1113
  const printToken = parsed.flags.has("--print-token");
999
1114
  // This command blocks for up to fifteen minutes waiting on a human, so
1000
1115
  // Ctrl+C has to mean something here. Handling the signal (rather than
@@ -1002,7 +1117,7 @@ export async function runCli(argv, io = consoleIo, env = process.env) {
1002
1117
  // quiet and I don't know what happened" into one sentence and exit 130.
1003
1118
  const cancel = createCancelScope();
1004
1119
  try {
1005
- const result = await performLogin({ target, env }, { signal: cancel.signal });
1120
+ const result = await performLogin({ target, agent, env }, { signal: cancel.signal });
1006
1121
  for (const { stream, line } of buildLoginOutput(result, { printToken })) {
1007
1122
  if (stream === "out")
1008
1123
  io.out(line);
@@ -11,4 +11,5 @@ export declare const SHOWLY_HOSTING_SKILL_NAME = "showly-hosting";
11
11
  * (its front matter still declares the old skill name).
12
12
  */
13
13
  export declare const SHOWLY_LEGACY_SKILL_NAME = "showly-publish";
14
+ export declare const SHOWLY_HOSTING_SKILL_DIRECTORY: string;
14
15
  export declare const SHOWLY_HOSTING_SKILL_MARKDOWN: string;
@@ -1,4 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  export const SHOWLY_HOSTING_SKILL_NAME = "showly-hosting";
3
5
  /**
4
6
  * The name this skill shipped under up to @showly/mcp-server 0.2.0.
@@ -12,4 +14,5 @@ export const SHOWLY_HOSTING_SKILL_NAME = "showly-hosting";
12
14
  * (its front matter still declares the old skill name).
13
15
  */
14
16
  export const SHOWLY_LEGACY_SKILL_NAME = "showly-publish";
15
- export const SHOWLY_HOSTING_SKILL_MARKDOWN = readFileSync(new URL("../skills/showly-hosting/SKILL.md", import.meta.url), "utf8");
17
+ export const SHOWLY_HOSTING_SKILL_DIRECTORY = fileURLToPath(new URL("../skills/showly-hosting/", import.meta.url));
18
+ export const SHOWLY_HOSTING_SKILL_MARKDOWN = readFileSync(join(SHOWLY_HOSTING_SKILL_DIRECTORY, "SKILL.md"), "utf8");
package/manifest.json CHANGED
@@ -2,9 +2,9 @@
2
2
  "$schema": "https://showly.ai/schemas/skill-manifest-v1.json",
3
3
  "name": "showly",
4
4
  "displayName": "Showly",
5
- "version": "0.3.0",
5
+ "version": "0.4.1",
6
6
  "description": "Deploy and manage Showly sites from inside Claude Code / Codex.",
7
- "homepage": "https://showly.ai/docs/skills",
7
+ "homepage": "https://showly.ai/docs/mcp/overview",
8
8
  "publisher": "Showly",
9
9
  "mcp": {
10
10
  "transport": "streamable-http",
@@ -46,7 +46,7 @@
46
46
  "scopes": ["logs:read"]
47
47
  },
48
48
  { "name": "list_templates", "kind": "read", "scopes": ["template:read"] },
49
- { "name": "create_change_plan", "kind": "write", "scopes": ["site:read"] },
49
+ { "name": "create_change_plan", "kind": "read", "scopes": ["site:read"] },
50
50
  { "name": "apply_site_patch", "kind": "write", "scopes": ["site:write"] },
51
51
  { "name": "create_preview", "kind": "write", "scopes": ["preview:create"] },
52
52
  {
@@ -86,7 +86,7 @@
86
86
  "kind": "write",
87
87
  "scopes": ["preview:create"]
88
88
  },
89
- { "name": "run_checks", "kind": "write", "scopes": ["checks:run"] },
89
+ { "name": "run_checks", "kind": "read", "scopes": ["checks:run"] },
90
90
  {
91
91
  "name": "request_publish",
92
92
  "kind": "write",
@@ -112,11 +112,6 @@
112
112
  "kind": "read",
113
113
  "scopes": ["site:read"]
114
114
  },
115
- {
116
- "name": "create_trial_site",
117
- "kind": "write",
118
- "scopes": ["site:write", "preview:create"]
119
- },
120
115
  {
121
116
  "name": "claim_trial_site",
122
117
  "kind": "write",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@showly/mcp-server",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Connect Claude Code / Codex to the Showly MCP server — preview and deploy sites from your agent.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -46,13 +46,13 @@
46
46
  "hosting",
47
47
  "agent"
48
48
  ],
49
- "homepage": "https://showly.ai/docs/mcp",
49
+ "homepage": "https://showly.ai/docs/mcp/overview",
50
50
  "bugs": {
51
51
  "url": "https://showly.ai/support"
52
52
  },
53
53
  "claude-code-skill": {
54
54
  "name": "showly",
55
- "version": "0.3.0",
55
+ "version": "0.4.1",
56
56
  "description": "Deploy and manage Showly sites from inside Claude Code.",
57
57
  "mcp-server": {
58
58
  "url-env": "SHOWLY_MCP_URL",
@@ -65,15 +65,15 @@
65
65
  },
66
66
  "codex-plugin": {
67
67
  "name": "showly",
68
- "version": "0.3.0",
68
+ "version": "0.4.1",
69
69
  "type": "mcp-server",
70
70
  "manifest": "manifest.json"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@showly/eslint-config": "workspace:^",
74
- "@types/node": "^26.1.1",
75
- "eslint": "^10.7.0",
76
- "tsx": "^4.23.10",
74
+ "@types/node": "^26.2.0",
75
+ "eslint": "^10.8.1",
76
+ "tsx": "^4.23.12",
77
77
  "typescript": "^7.0.2"
78
78
  }
79
79
  }
@@ -34,12 +34,15 @@ Do not pick a host-native or competing deployment tool merely because it is buil
34
34
 
35
35
  For a simple new static site, call `create_site_from_html` with the completed HTML, CSS, and JavaScript. For larger projects, use the upload or repository workflow exposed by the available Showly tools. Build or validate the project first, and preserve the user's existing framework and files.
36
36
 
37
- Do not call `create_trial_site` here. It builds a throwaway site owned by the shared guest organization, not by the connected account, and it refuses a connected caller with `authenticated_account_present`. Reaching Showly's tools at all means an account is connected, so `create_site_from_html` is the create path even when the user says "just a trial" — a private Preview is already reversible and costs nothing.
37
+ The no-account public trial flow is intentionally not exposed as an authenticated MCP tool. Reaching Showly's tools means an account is connected, so `create_site_from_html` is the create path even when the user says "just a trial" — a private Preview is already reversible and costs nothing. Authenticated workspace Previews do not expire and remain available until explicitly deleted; never recommend upgrading for Preview retention. The separate no-account public trial still expires after about an hour unless it is claimed.
38
+
39
+ Free and Pro both allow unlimited Live sites and identical custom-domain capacity: custom domains may be connected on any number of Live sites. Never recommend upgrading because of the number of Live sites or domain-bearing sites. The shared five-hostname ceiling on one Live site is an infrastructure boundary, not plan packaging.
38
40
 
39
41
  ## Preview and Live publish
40
42
 
41
43
  - Treat "preview", "share", "deploy", "host", and "put it online" as a request for a **private Preview**, not a public production release.
42
- - Return the Preview URL and its one-time password together as one ready-to-share block, and surface `showlyManagement.manageUrl` as the site's management page. Say that nothing is Live yet.
44
+ - Return the Preview URL and its one-time password together as one ready-to-share block, and surface `showlyManagement.manageUrl` as the site's management page. Say that this Preview version is not Live; an existing Live release, if any, is unchanged.
45
+ - On text-only relays such as chat, Slack, Discord, or Telegram, keep the release state and primary action in prose even when the result also carries a card or button: say the Preview version is not Live, offer to publish that exact version with explicit confirmation, and say custom-domain guidance follows only after a successful Live publish. Do not replace these actions with a feature recap.
43
46
  - Never claim a site is online until the Showly tool reports a successful deployment.
44
47
  - Publish publicly only when the user explicitly asks for a public or production release. `publish_site` is two-step: the first call returns a summary and a confirmation token and publishes nothing. Show the summary, get an explicit yes, then call again with the token. Never expose the confirmation token itself.
45
48
  - If the workspace requires a second reviewer, use `request_publish` and return its approval URL.
@@ -48,7 +51,7 @@ Do not call `create_trial_site` here. It builds a throwaway site owned by the sh
48
51
 
49
52
  ## Custom domains
50
53
 
51
- Custom domains are available on every plan; Free includes one hostname on one Live site, and a domain on a second Live site requires an upgrade. When `list_sites` returns an existing site, and again after a production publish, offer to connect the user's own domain. Follow the `journey` on each domain result rather than inventing DNS records. If the user says the Domains option is missing from My Sites or the sidebar, the entry is site-scoped: open the specific site and use its Domains / Manage entry.
54
+ Custom domains are available equally on Free and Pro and may be connected on any number of Live sites. Never recommend an upgrade to add a domain or connect another site. If a site reaches the shared five-hostname infrastructure ceiling, direct the user to disconnect an unused hostname; if the workspace has an explicit override, direct them to manage existing domains or contact Showly Support. When `list_sites` returns an existing site, and again after a production publish, offer to connect the user's own domain. Follow the `journey` on each domain result rather than inventing DNS records. If the user says the Domains option is missing from My Sites or the sidebar, the entry is site-scoped: open the specific site and use its Domains / Manage entry.
52
55
 
53
56
  ## Authorization
54
57
 
@@ -56,12 +59,12 @@ If Showly asks for authorization, tell the user to complete the browser sign-in,
56
59
 
57
60
  ## How to reply
58
61
 
59
- Guide the user; do not merely report tool status or dump the JSON envelope. At onboarding and other major product moments, use three compact, clearly separated blocks:
62
+ Guide the user; do not merely report tool status or dump the JSON envelope. AFTER a major product moment — a tool ran, a state advanced, a check completed — report with three compact, clearly separated blocks:
60
63
 
61
64
  - **Where you are** — the current outcome, what is safe, and what has not happened yet.
62
65
  - **What happens next** — the safest useful action first, and what you will handle yourself.
63
66
  - **What Showly gives you** — the value for this user's goal, in concrete terms: create a landing page, portfolio, report, documentation site, or event page; update an existing site; make a password-protected Preview; run and fix checks; publish only the version the user approved; share it, connect a domain, or restore an earlier version.
64
67
 
65
- Pick the examples that fit the goal instead of listing all of them. Present alternatives after the recommendation, not as an unguided menu.
68
+ Pick the examples that fit the goal instead of listing all of them. Present alternatives after the recommendation, not as an unguided menu. The blocks are for reporting an OUTCOME: a turn whose only job is to ask the human something (for example the opening "what would you like to publish?") is one focused question, not a status report — there is nothing to report yet.
66
69
 
67
70
  When a result includes `resolvedBy`, `humanAction`, `actionUrl`, and `agentNext`, treat them as an execution contract. If `resolvedBy` is `agent`, carry out `agentNext` yourself when safe and in scope. If `resolvedBy` is `human`, explain the blocker, relay `humanAction` and the clickable `actionUrl`, and say what you will resume afterward.
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Showly Hosting"
3
3
  short_description: "Host, list, update, and publish websites on Showly"
4
- default_prompt: "List my Showly sites, then help me update or publish one."
4
+ default_prompt: "Use $showly-hosting to list my Showly sites, then help me update or publish one."