@showly/mcp-server 0.1.0 → 0.3.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 CHANGED
@@ -6,37 +6,75 @@ you can preview and deploy a site directly from your terminal AI.
6
6
  ## Quickstart
7
7
 
8
8
  ```sh
9
- npx @showly/mcp-server install --to claude-code # or --to codex
9
+ npx @showly/mcp-server install --to codex --with-skill
10
+ # or: npx @showly/mcp-server install --to claude-code --with-skill
10
11
  ```
11
12
 
12
13
  The installer writes the Showly MCP server block to `~/.claude.json`
13
- (or `~/.codex/config.toml`). No token to paste: the next time you call a Showly
14
+ (or `~/.codex/config.toml`) and installs the reusable `showly-hosting` skill.
15
+ The skill covers the whole lifecycle of a hosted site — listing and inspecting
16
+ the sites you already have, creating one, updating it, sharing a private
17
+ password-protected Preview, publishing it Live, rolling back, and connecting a
18
+ custom domain. It supersedes the narrower `showly-publish` skill, which
19
+ `install` removes from your skills directory when it finds it.
20
+
21
+ No token to paste: the next time you call a Showly
14
22
  tool, your agent discovers Showly's OAuth authorization server from the
15
23
  endpoint and opens a browser tab for you to sign in and approve the scopes
16
24
  (standard MCP authorization).
17
25
 
18
26
  ## What you get
19
27
 
28
+ `manifest.json` in this package is the authoritative list — the server
29
+ derives `INVOKABLE_TOOLS` and `MCP_BLOCKED_TOOLS` from it, so read it
30
+ rather than trusting a hand-kept summary. It currently ships 33 tools,
31
+ grouped below by its own `kind` field.
32
+
20
33
  Read-only tools available to any token:
21
34
 
22
35
  - `list_projects`, `list_sites`, `get_site_context`, `list_templates`
23
36
  - `get_preview_status`, `get_deployment_logs`, `diagnose_deployment`
37
+ - `list_deployments`, `list_site_versions`, `diff_site_versions`, `get_site_files`
38
+ - `list_site_domains`, `request_download_url`
24
39
 
25
40
  Write tools that stage and materialise previews:
26
41
 
27
42
  - `create_change_plan` — turn a natural-language request into a plan
28
43
  - `apply_site_patch` — stage one or more file edits as a _changeset_
29
44
  - `create_preview` — build the changeset and return a real preview URL
45
+ - `create_github_preview` — build a private preview from the latest commit on a connected GitHub branch (static targets; dynamic container builds fail before enqueue)
30
46
  - `retry_deployment` — re-run a failed preview build
31
47
  - `run_checks` — read the lint / typecheck / build status of a preview
32
48
  - `create_site_from_template` — bootstrap a new site from a template
49
+ - `create_site_from_html` — create a site from inline HTML
50
+ - `request_upload_url` — signed URL for a file too large to pass inline
51
+ - `set_preview_access` — change who can open a preview URL
33
52
  - `request_publish` — open a publish approval (completed in the web UI)
34
53
 
35
- Production paths (`publish_site`, `rollback_deployment`) are deliberately
36
- not exposed via MCP at all — they don't appear in `tools/list`. The agent's
37
- path to production is `request_publish`, whose success response includes a
38
- `webApprovalUrl` deep link; the user opens that link to complete approval
39
- and step-up MFA in the Showly web UI.
54
+ Write tools for custom domains (ADR-0015):
55
+
56
+ - `add_custom_domain`, `verify_custom_domain`
57
+
58
+ Write tools for version history and deletion:
59
+
60
+ - `rollback_to_version` — restore a previous site version
61
+ - `delete_preview`, `delete_site`
62
+
63
+ Write tools for the guest trial flow:
64
+
65
+ - `create_trial_site`, `claim_trial_site`
66
+
67
+ `publish_site` is exposed through a narrower `publish:confirm` scope and never
68
+ publishes on its first call: it returns a short-lived, deployment-bound token
69
+ that the agent may echo only after the user confirms. Team and Enterprise
70
+ workspaces use `request_publish` and its `webApprovalUrl` for second-person
71
+ approval. Ordinary Live publishing does not require OTP/MFA. The legacy
72
+ `rollback_deployment` path remains Web-only.
73
+
74
+ Both publish paths require the human user bound to the MCP token to have a
75
+ verified Showly account email. An `email_verification_required` result includes
76
+ `webVerificationUrl`; the agent must direct the user there and must not report
77
+ the site as live until verification succeeds and publishing is retried.
40
78
 
41
79
  ## Environment
42
80
 
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,30 @@
1
1
  #!/usr/bin/env node
2
- type Target = "claude-code" | "codex" | "stdout";
2
+ export type Target = "claude-code" | "codex" | "stdout";
3
+ /**
4
+ * The version of THIS copy of the package, read from the manifest that ships
5
+ * beside it. `manifest.test.ts` pins manifest.json, package.json,
6
+ * claude-code-skill and codex-plugin to one string, so this cannot print a
7
+ * version the package does not actually have.
8
+ *
9
+ * It is printed on every help screen and on every unknown-command error, and
10
+ * that is the whole point. When an agent runs `showly-mcp` from a stale npx
11
+ * cache and sees a command list that is missing what the docs told it to run,
12
+ * the most natural inference is "the docs are wrong" or "I misremembered the
13
+ * name" — because nothing on screen suggests a newer copy exists. A version
14
+ * string plus the `@latest` hint is the one signal that redirects that guess
15
+ * from "the instructions are wrong" to "my copy is old".
16
+ */
17
+ export declare const CLI_VERSION: string;
18
+ /** The commands this copy dispatches. Anything else is an error, never a no-op. */
19
+ export declare const KNOWN_COMMANDS: readonly ["install", "login", "manifest"];
20
+ export type KnownCommand = (typeof KNOWN_COMMANDS)[number];
21
+ /**
22
+ * The sentence that turns "this tool cannot do that" into "this COPY cannot do
23
+ * that". Printed on unknown commands and on unknown flags, because both are
24
+ * reached by the same route: documentation written against a newer release
25
+ * than the one npx resolved.
26
+ */
27
+ export declare const UPGRADE_HINT = "If you expected this, your copy is out of date \u2014 re-run with `npx @showly/mcp-server@latest`.";
3
28
  export declare function buildClaudeCodeSnippet(opts: {
4
29
  url: string;
5
30
  apiUrl?: string;
@@ -18,8 +43,272 @@ export type InstallResult = {
18
43
  snippet: string;
19
44
  wrote: boolean;
20
45
  alreadyConfigured: boolean;
46
+ skill: SkillInstallResult | null;
47
+ };
48
+ export type SkillInstallResult = {
49
+ path: string;
50
+ wrote: boolean;
51
+ alreadyConfigured: boolean;
52
+ /** Path of the superseded showly-publish skill this install removed, if any. */
53
+ removedLegacyPath: string | null;
54
+ };
55
+ export type InstallOptions = {
56
+ withSkill?: boolean;
21
57
  };
22
- export declare function performInstall(target: Target, env?: NodeJS.ProcessEnv): InstallResult;
58
+ export declare function performInstall(target: Target, env?: NodeJS.ProcessEnv, options?: InstallOptions): InstallResult;
59
+ export declare function performSkillInstall(target: Exclude<Target, "stdout">, env?: NodeJS.ProcessEnv): SkillInstallResult;
60
+ /**
61
+ * The client_id this CLI starts device flows under.
62
+ *
63
+ * Deliberately NOT a first-party id (the API rejects those on /oauth/device
64
+ * precisely so nobody can wear the product's identity on a consent screen),
65
+ * and deliberately not registered: a device client_id is free text, so the
66
+ * consent page treats whatever name it carries as self-declared and asks the
67
+ * human to check the code echo instead.
68
+ */
69
+ export declare const LOGIN_CLIENT_ID = "showly-mcp-cli";
70
+ /** The env var name emitted into config snippets that must not hold a secret. */
71
+ export declare const TOKEN_ENV_VAR = "SHOWLY_TOKEN";
72
+ export type DeviceStart = {
73
+ device_code: string;
74
+ user_code: string;
75
+ verification_uri: string;
76
+ verification_uri_complete: string;
77
+ expires_in: number;
78
+ interval: number;
79
+ };
80
+ export type DeviceToken = {
81
+ access_token: string;
82
+ scope: string;
83
+ expires_in?: number;
84
+ };
85
+ /**
86
+ * The exact text `login` prints while it waits. Pure, and pinned by a test,
87
+ * because this block is the entire user interface of headless sign-in — the
88
+ * docs quote it verbatim so the page and the binary cannot drift.
89
+ *
90
+ * Reading order is not cosmetic:
91
+ * • The bare URL plus the separately-printed code comes FIRST, because that
92
+ * is the pair that works when the human is on a phone or another machine —
93
+ * which is the entire reason this flow exists. The one-click deep link is
94
+ * offered second, for the case where the browser is on this box.
95
+ * • The code is repeated three times on purpose. RFC 8628 §3.3.1's code echo
96
+ * is the ONLY anti-phishing signal that survives an attacker-controlled
97
+ * client_name (Storm-2372 phished 340+ M365 tenants on exactly this flow),
98
+ * so the human has to be told what they will see and what to do if it
99
+ * differs.
100
+ * • The expiry is a wall-clock time, not a duration. A human who wanders off
101
+ * to create an account and verify an email cannot subtract "15 minutes"
102
+ * from a moment they have forgotten.
103
+ */
104
+ export declare function buildLoginPrompt(input: {
105
+ verificationUri: string;
106
+ verificationUriComplete: string;
107
+ userCode: string;
108
+ expiresAt: Date;
109
+ }): string;
110
+ /** Claude Code reads a `headers` map on an http MCP server entry. */
111
+ export declare function buildClaudeCodeAuthSnippet(opts: {
112
+ url: string;
113
+ token: string;
114
+ }): Record<string, unknown>;
115
+ export declare function buildCodexAuthSnippet(opts: {
116
+ url: string;
117
+ tokenEnvVar?: string;
118
+ }): string;
119
+ export type LoginDeps = {
120
+ fetchImpl?: typeof fetch;
121
+ /**
122
+ * Injectable wait. Takes the cancellation signal too, so the real one can
123
+ * return the instant Ctrl+C arrives instead of finishing a five-second nap
124
+ * first — a cancel the user has to wait out does not read as a cancel.
125
+ */
126
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
127
+ /** Injectable clock (ms). Lets the poll deadline be tested without waiting. */
128
+ now?: () => number;
129
+ /** stderr sink. Keeps stdout clean for --print-token. */
130
+ log?: (line: string) => void;
131
+ /**
132
+ * Cancellation from outside — Ctrl+C, in production (see createCancelScope).
133
+ *
134
+ * Without it there was no way out of this command but killing the process,
135
+ * which is a poor deal for the human who is already stuck: `login` blocks for
136
+ * a quarter of an hour by design, and the whole reason a reader reaches it is
137
+ * that something else (their editor's browser window) failed to appear.
138
+ */
139
+ signal?: AbortSignal;
140
+ /**
141
+ * Per-request deadline override, in ms. Production uses the constants below;
142
+ * the tests use a tiny value so a hung server can be exercised in
143
+ * milliseconds rather than in the fifteen seconds a real one gets.
144
+ */
145
+ requestTimeoutMs?: number;
146
+ };
147
+ /**
148
+ * How long ONE HTTP request may take before it is abandoned.
149
+ *
150
+ * Neither fetch had any bound at all, and both are inside the flow whose entire
151
+ * job is to unstick a human: a TCP connection that opens and then goes quiet
152
+ * (a captive portal, a proxy that swallows the response, a machine suspended
153
+ * mid-poll) leaves `login` hanging with no output and no deadline — the same
154
+ * silent wait, one layer down, that this command exists to replace.
155
+ *
156
+ * The start POST is short because nothing has been printed yet: until it
157
+ * returns there is no code, no URL, and nothing on screen, so the reader cannot
158
+ * tell a slow network from a dead one. A poll gets longer, because by then the
159
+ * prompt is on screen and the loop can absorb a slow answer without the reader
160
+ * seeing anything at all.
161
+ */
162
+ export declare const START_REQUEST_TIMEOUT_MS = 15000;
163
+ export declare const POLL_REQUEST_TIMEOUT_MS = 30000;
164
+ /** Thrown when the human cancels; the CLI exits 130 rather than 1 on it. */
165
+ export declare class LoginCancelledError extends Error {
166
+ constructor();
167
+ }
168
+ /**
169
+ * Wire Ctrl+C (and SIGTERM) to an AbortSignal the login flow watches.
170
+ *
171
+ * Registering a SIGINT listener also SUPPRESSES Node's default "die now", which
172
+ * is the point: the flow gets to unwind, say what happened, and exit with the
173
+ * conventional 130 instead of leaving the reader guessing whether anything was
174
+ * half-written. Exported so the wiring itself is testable — `main` is not.
175
+ */
176
+ export type SignalTarget = {
177
+ on(event: string, listener: () => void): unknown;
178
+ off(event: string, listener: () => void): unknown;
179
+ };
180
+ export declare function createCancelScope(target?: SignalTarget): {
181
+ signal: AbortSignal;
182
+ release: () => void;
183
+ };
184
+ /**
185
+ * How far PAST the expiry we were handed at start the poll keeps going.
186
+ *
187
+ * `expires_in` from POST /oauth/device is a floor, not a deadline: when a
188
+ * signed-in human lands on the consent page the server pushes expires_at out
189
+ * (see DEVICE_LOOKUP_EXTENSION_MS), so a client that stopped at the advertised
190
+ * expiry would hang up on the human mid-approval. It is still bounded — the
191
+ * server caps a device flow at 30 minutes from creation whatever happens — so
192
+ * one absolute cap past the advertised expiry is comfortably beyond any answer
193
+ * that could still become a token, and stopping there is what keeps a deploy
194
+ * that stops resolving dead rows from turning this loop into a permanent 300
195
+ * requests/min against the token endpoint.
196
+ */
197
+ export declare const SERVER_EXTENSION_ALLOWANCE_MS: number;
198
+ export declare function startDeviceFlow(apiUrl: string, deps?: LoginDeps): Promise<DeviceStart>;
199
+ /**
200
+ * Poll /oauth/token until the human decides, per RFC 8628 §3.4-3.5.
201
+ *
202
+ * Every terminal answer is turned into a sentence a human can act on. The
203
+ * server sends `error_description` for exactly this reason, so we prefer it
204
+ * over anything we could invent, and fall back only when an older deploy
205
+ * sends the bare code.
206
+ *
207
+ * `expiresAt` is the client-side backstop, not the contract: the server's own
208
+ * `expired_token` / `invalid_grant` is what normally ends the loop, and this
209
+ * only fires when no answer ever arrives at all. It sits a full
210
+ * SERVER_EXTENSION_ALLOWANCE_MS past the advertised expiry so an arrival
211
+ * extension is never cut short.
212
+ */
213
+ export declare function pollForDeviceToken(input: {
214
+ apiUrl: string;
215
+ deviceCode: string;
216
+ intervalSec: number;
217
+ expiresAt: Date;
218
+ }, deps?: LoginDeps): Promise<DeviceToken>;
219
+ export type LoginResult = {
220
+ target: Target;
221
+ token: string;
222
+ scope: string;
223
+ expiresAt: Date | null;
224
+ path: string | null;
225
+ wrote: boolean;
226
+ snippet: string;
227
+ };
228
+ /**
229
+ * Run the whole headless sign-in and put the credential where the host will
230
+ * find it.
231
+ *
232
+ * No secret is ever accepted on argv — there is no `--token` flag, and the
233
+ * only credential this command handles is the one it just fetched. Anything
234
+ * pasted from /app/admin/mcp-tokens goes in through the environment
235
+ * (SHOWLY_TOKEN) or the host config by hand, so it stays out of shell history
236
+ * and out of every `ps` listing on the machine.
237
+ */
238
+ export declare function performLogin(opts: {
239
+ target: Target;
240
+ env?: NodeJS.ProcessEnv;
241
+ }, deps?: LoginDeps): Promise<LoginResult>;
242
+ /**
243
+ * The env var names a host config tells its client to read the credential
244
+ * from: Codex's `bearer_token_env_var = "NAME"`, and the `${NAME}` placeholder
245
+ * in the pasteable snippet.
246
+ *
247
+ * Deliberately derived FROM the config text rather than hardcoded, because the
248
+ * bug this closes was a config and an output that disagreed. Whatever a future
249
+ * host schema calls its indirection, the name it references is what the human
250
+ * has to be handed a value for.
251
+ */
252
+ export declare function envVarsReferencedBy(configText: string): string[];
253
+ export type LoginOutputLine = {
254
+ stream: "out" | "err";
255
+ line: string;
256
+ };
257
+ /**
258
+ * Everything `login` says after the token lands. Pure, so the one property
259
+ * that matters can be tested: a human who ran this command ends up CONNECTED.
260
+ *
261
+ * `--to codex` used to break that. It writes `bearer_token_env_var =
262
+ * "SHOWLY_TOKEN"` — Codex reads the credential from the environment, never
263
+ * from the file — and then printed `Connected. Wrote <path>` and dropped the
264
+ * token on the floor. Nothing on the machine ever set SHOWLY_TOKEN, so every
265
+ * tool call went out with no Authorization header and 401'd, after the human
266
+ * had already spent their one approval; recovering meant a second device flow
267
+ * and a second approval, which is the dead end this whole command exists to
268
+ * remove. `--to stdout` had the same hole with a sentence over it ("re-run
269
+ * with --print-token"), which is also a second approval.
270
+ *
271
+ * So the value is supplied here, once, on stderr — the only place it can go
272
+ * without landing in a file. The config keeps the env indirection, which is
273
+ * what makes it safe to commit; stdout keeps carrying only the snippet (or,
274
+ * under --print-token, only the token) so both remain pipeable.
275
+ */
276
+ export declare function buildLoginOutput(result: LoginResult, opts?: {
277
+ printToken?: boolean;
278
+ }): LoginOutputLine[];
279
+ export type ParsedCommandArgs = {
280
+ ok: true;
281
+ help: boolean;
282
+ flags: Set<string>;
283
+ values: Map<string, string>;
284
+ } | {
285
+ ok: false;
286
+ message: string;
287
+ };
288
+ /**
289
+ * Strict argv parse for one command: every token must be a flag this command
290
+ * declares, a value for a flag that takes one, or `--help`.
291
+ *
292
+ * Bare positionals are rejected too, for the same reason flags are: `install
293
+ * claude-code` (no `--to`) is a plausible typo whose only old outcome was a
294
+ * usage error about the missing `--to`, and `manifest extra` simply printed the
295
+ * manifest as if the word were not there.
296
+ */
297
+ export declare function parseCommandArgs(command: KnownCommand, rest: string[]): ParsedCommandArgs;
298
+ /** stdout/stderr sinks, injected so the dispatcher is testable in-process. */
299
+ export type CliIo = {
300
+ out: (line: string) => void;
301
+ err: (line: string) => void;
302
+ };
303
+ /**
304
+ * The whole command dispatcher, as a function that RETURNS its exit code.
305
+ *
306
+ * It used to be a `main` that assigned `process.exitCode` and was neither
307
+ * exported nor reachable from a test, which is why nothing noticed that the
308
+ * failure paths through it were not failures at all. The code is the contract
309
+ * here — the strings around it are not — so it is what a caller gets back.
310
+ */
311
+ export declare function runCli(argv: string[], io?: CliIo, env?: NodeJS.ProcessEnv): Promise<number>;
23
312
  /**
24
313
  * True when this module is the program entrypoint.
25
314
  *
@@ -35,4 +324,3 @@ export declare function performInstall(target: Target, env?: NodeJS.ProcessEnv):
35
324
  * exercised from an in-process test runner otherwise).
36
325
  */
37
326
  export declare function isMainModule(argv1: string | undefined, metaUrl: string): boolean;
38
- export {};