@nearform/fling 0.4.0 → 0.5.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/dist/cli.js CHANGED
@@ -373,6 +373,7 @@ var CommentAuthorSchema = z3.discriminatedUnion("kind", [
373
373
  z3.object({ kind: z3.literal("guest"), name: z3.string() })
374
374
  ]);
375
375
  var ANCHOR_SELECTOR_MAX = 512;
376
+ var ANCHOR_QUOTE_MAX = 500;
376
377
  var px = z3.number().finite().min(-1e7).max(1e7);
377
378
  var percent = z3.number().min(0).max(100);
378
379
  var CommentAnchorSchema = z3.object({
@@ -387,7 +388,12 @@ var CommentAnchorSchema = z3.object({
387
388
  scrollY: px.optional(),
388
389
  vw: z3.number().finite().min(0).max(1e5).optional(),
389
390
  vh: z3.number().finite().min(0).max(1e5).optional(),
390
- hash: z3.string().max(COMMENT_PATH_MAX).optional()
391
+ hash: z3.string().max(COMMENT_PATH_MAX).optional(),
392
+ ew: percent.optional(),
393
+ eh: percent.optional(),
394
+ docW: z3.number().finite().min(0).max(1e7).optional(),
395
+ docH: z3.number().finite().min(0).max(1e7).optional(),
396
+ quote: z3.string().min(1).max(ANCHOR_QUOTE_MAX).optional()
391
397
  });
392
398
  var CommentSchema = z3.object({
393
399
  id: z3.string(),
@@ -982,7 +988,10 @@ var ShellMessageSchema = z7.discriminatedUnion("type", [
982
988
  z7.object({ ...tag, type: z7.literal("ready") }),
983
989
  /** the frame is on `path` (normalised, see normalizeCommentPath) with fragment `hash` ('' or '#…') */
984
990
  z7.object({ ...tag, type: z7.literal("location"), path, hash, title: z7.string().max(300) }),
991
+ /** a pin placed while placing: a click (a spot) or a drag (a box) */
985
992
  z7.object({ ...tag, type: z7.literal("pinPlaced"), anchor: CommentAnchorSchema }),
993
+ /** the reviewer selected text in the page: an anchor on it, with its quote */
994
+ z7.object({ ...tag, type: z7.literal("textSelected"), anchor: CommentAnchorSchema }),
986
995
  z7.object({ ...tag, type: z7.literal("pinCancelled") }),
987
996
  z7.object({ ...tag, type: z7.literal("pinClicked"), id: z7.string() }),
988
997
  /** this reviewer's pointer on the page; null when it left the frame */
@@ -1088,7 +1097,7 @@ function exitCodeFor(error) {
1088
1097
  // package.json
1089
1098
  var package_default = {
1090
1099
  name: "@nearform/fling",
1091
- version: "0.4.0",
1100
+ version: "0.5.1",
1092
1101
  license: "MIT",
1093
1102
  description: "Fling a folder of static files at a Fling server and get a link back. CLI + MCP server.",
1094
1103
  type: "module",
@@ -1308,7 +1317,9 @@ var ENV = {
1308
1317
  configDir: "FLING_CONFIG_DIR",
1309
1318
  debug: "FLING_DEBUG",
1310
1319
  /** Where to look for files given by name (the .mcpb extension sets it from its settings). */
1311
- downloadsDir: "FLING_DOWNLOADS_DIR"
1320
+ downloadsDir: "FLING_DOWNLOADS_DIR",
1321
+ /** A server the local MCP server adds to the remembered ones on start (the .mcpb bakes it in). */
1322
+ addServer: "FLING_ADD_SERVER"
1312
1323
  };
1313
1324
  var serverEntrySchema = z9.object({
1314
1325
  /** What the server said about itself at login. A copy this CLI can't read is just refetched. */
@@ -1327,6 +1338,9 @@ function fromEnv(value) {
1327
1338
  const v = value?.trim();
1328
1339
  return v && !v.startsWith("${") ? v : void 0;
1329
1340
  }
1341
+ function serverToAdd(env = process.env) {
1342
+ return fromEnv(env[ENV.addServer]);
1343
+ }
1330
1344
  function resolveConfigDir(env = process.env) {
1331
1345
  return env[ENV.configDir] ?? join(env.HOME ?? homedir(), CONFIG_DIR_NAME);
1332
1346
  }
@@ -2207,6 +2221,7 @@ async function runComments(ctx, name, opts) {
2207
2221
  }
2208
2222
  const state = c.resolvedAt ? chalk6.green(" \u2713 resolved") : "";
2209
2223
  lines.push(`${chalk6.dim(c.id)}${state} ${describe(c, list.site.currentVersionId)}`);
2224
+ if (c.anchor?.quote) lines.push(chalk6.dim(` > ${c.anchor.quote}`));
2210
2225
  lines.push(...c.body.split("\n").map((l) => ` ${l}`));
2211
2226
  for (const r of replies) {
2212
2227
  lines.push(` ${chalk6.dim("\u21B3")} ${describe(r, list.site.currentVersionId)}`);
@@ -3841,11 +3856,132 @@ async function findFiles(opts, find = {}) {
3841
3856
  return { files, searched: roots, complete };
3842
3857
  }
3843
3858
 
3859
+ // src/mcp-servers.ts
3860
+ function serverLabel(url) {
3861
+ const u = new URL(url);
3862
+ return `${u.host}${u.pathname}`.replace(/\/+$/, "");
3863
+ }
3864
+ function describeServer(s) {
3865
+ const facts = [
3866
+ s.instance?.name,
3867
+ s.signedIn ? s.email ? `signed in as ${s.email}` : "signed in" : "signed out",
3868
+ s.current ? "current: the user's usual one" : void 0
3869
+ ].filter(Boolean);
3870
+ return `${serverLabel(s.url)} (${facts.join("; ")})`;
3871
+ }
3872
+ function knownServersOf(config) {
3873
+ return Object.entries(config.servers).map(([url, entry]) => ({
3874
+ url,
3875
+ instance: entry.instance,
3876
+ current: url === config.current,
3877
+ signedIn: Boolean(entry.apiKey),
3878
+ ...entry.apiKey && entry.user?.email ? { email: entry.user.email } : {}
3879
+ }));
3880
+ }
3881
+ var listing = (servers) => servers.map((s) => `- ${describeServer(s)}`).join("\n");
3882
+ var ServerNeededError = class extends CliError {
3883
+ servers;
3884
+ constructor(servers, tool2) {
3885
+ super(
3886
+ `Not run: several Flings are set up on this computer, so ${tool2} needs \`server\`.
3887
+ ${listing(servers)}
3888
+ Unless the conversation already says which Fling to use, ask the user (suggest their usual one, marked current). Then call ${tool2} again with \`server\` (e.g. "${serverLabel(servers[0]?.url ?? "https://fling.example.com")}"), and pass the same \`server\` on every call for the rest of the conversation.`
3889
+ );
3890
+ this.name = "ServerNeededError";
3891
+ this.servers = servers;
3892
+ }
3893
+ };
3894
+ function rememberedServer(config, input) {
3895
+ const servers = knownServersOf(config);
3896
+ const known = servers.length ? `The Flings set up on this computer:
3897
+ ${listing(servers)}` : "No Fling is set up on this computer yet.";
3898
+ let match;
3899
+ try {
3900
+ match = matchServer(Object.keys(config.servers), input, { loose: true });
3901
+ } catch (error) {
3902
+ throw new CliError(error.message, { hint: known });
3903
+ }
3904
+ if (!match.remembered)
3905
+ throw new CliError(`"${input}" is not a Fling set up on this computer`, {
3906
+ hint: `${known}
3907
+ To add one, call fling_login with its address as \`server\`.`
3908
+ });
3909
+ return match.url;
3910
+ }
3911
+ var isPinned = (config) => config.serverUrlSource === "flag" || config.serverUrlSource === "env";
3912
+ async function within(promise, ms, fallback) {
3913
+ let timer;
3914
+ const late = new Promise((r) => {
3915
+ timer = setTimeout(() => r(fallback), ms);
3916
+ });
3917
+ try {
3918
+ return await Promise.race([promise.catch(() => fallback), late]);
3919
+ } finally {
3920
+ clearTimeout(timer);
3921
+ }
3922
+ }
3923
+ var START_FETCH_MS = 5e3;
3924
+ async function addServerOnStart(store, address, fetch2) {
3925
+ if (!address) return;
3926
+ let url;
3927
+ try {
3928
+ url = normalizeServerUrl(address);
3929
+ } catch {
3930
+ return;
3931
+ }
3932
+ if (url in (await store.load()).servers) return;
3933
+ const instance = await within(fetchInstance(url, fetch2), START_FETCH_MS, null);
3934
+ await store.putServer(url, instance ? { instance } : {});
3935
+ }
3936
+ async function knownServers(store, fetch2, pin) {
3937
+ const resolved = await store.resolve({ serverUrl: pin });
3938
+ const config = await store.load();
3939
+ const all = knownServersOf(config);
3940
+ const servers = isPinned(resolved) ? resolved.serverUrl ? [
3941
+ all.find((s) => s.url === resolved.serverUrl) ?? {
3942
+ url: resolved.serverUrl,
3943
+ current: false,
3944
+ signedIn: Boolean(resolved.apiKey)
3945
+ }
3946
+ ] : [] : all;
3947
+ return Promise.all(
3948
+ servers.map(async (s) => {
3949
+ if (s.instance) return s;
3950
+ const instance = await within(fetchInstance(s.url, fetch2), START_FETCH_MS, null);
3951
+ if (instance && s.url in config.servers)
3952
+ await store.putServer(s.url, { instance }, { makeCurrent: false }).catch(() => {
3953
+ });
3954
+ return { ...s, instance };
3955
+ })
3956
+ );
3957
+ }
3958
+ function serverArgDescription(servers) {
3959
+ const [only] = servers;
3960
+ if (servers.length > 1)
3961
+ return `Which Fling: ${servers.map((s) => serverLabel(s.url)).join(", ")} (an address, host or unique part of it). Required while several Flings are set up: the one settled on for this conversation, on every call`;
3962
+ return `Which Fling, by address or host (any set up on this computer; see fling_servers). Omit it to use ${only ? serverLabel(only.url) : "the one this computer is signed in to"}`;
3963
+ }
3964
+ function serverToolNote(servers) {
3965
+ if (servers.length < 2) return "";
3966
+ return ` Pass \`server\`: several Flings are set up (${servers.map((s) => serverLabel(s.url)).join(", ")}), and every call names the one settled on for this conversation.`;
3967
+ }
3968
+ function serversInstructions(servers) {
3969
+ if (servers.length < 2) return void 0;
3970
+ const usual = servers.find((s) => s.current);
3971
+ return [
3972
+ `Several Flings are set up on this computer:
3973
+ ${listing(servers)}`,
3974
+ `Settle which one this conversation uses before anything else: from the user's words ("on ${serverLabel(servers[0]?.url ?? "")}", "on the ${servers[0]?.instance?.name ?? "other"} one") or, if they don't say, by asking them${usual ? ` (suggest ${serverLabel(usual.url)}, their usual one)` : ""}. Say which Fling you are using, and pass it as \`server\` on every call for the rest of the conversation; switch only when the user asks. Calls without \`server\` do not run while several Flings are set up. Each Fling has its own sites, sign-in and sites domain: give the user the links that come back, which name the Fling.`,
3975
+ "This list was read when this server started and may have changed: call fling_servers to see the Flings set up now. To sign in to a Fling, or add one, call fling_login with its address as `server`."
3976
+ ].join("\n\n");
3977
+ }
3978
+
3844
3979
  // src/mcp.ts
3845
3980
  var LOGIN_HINT = `Not logged in to Fling. Call fling_login (it opens the browser to sign in), or ask the user to run \`${NPX_COMMAND} login\` in a terminal (in Claude Code: \`! ${NPX_COMMAND} login\`), then try again.`;
3846
- var NO_SERVER_HINT = `No Fling server is set up on this computer. Ask the user for their Fling's address and to run \`${NPX_COMMAND} login <url>\` in a terminal (in Claude Code: \`! ${NPX_COMMAND} login <url>\`), then try again.`;
3981
+ var NO_SERVER_HINT = `No Fling server is set up on this computer. Ask the user for their Fling's address, then call fling_login with it as \`server\` (or ask them to run \`${NPX_COMMAND} login <url>\` in a terminal; in Claude Code: \`! ${NPX_COMMAND} login <url>\`), then try again.`;
3847
3982
  var mcpLocalInstructions = (w, opts = {}) => [
3848
3983
  `${w.name} publishes static sites (HTML/CSS/JS, images, documents) at ${w.sitesPattern}. This server runs on the user's computer.`,
3984
+ ...[serversInstructions(opts.servers ?? [])].filter((p) => p !== void 0),
3849
3985
  "Content you created in this conversation (an HTML page, a report, a small app): call fling_publish with every file inline (text as utf8, binary as base64). Never pass file paths from your sandbox (e.g. /mnt/user-data/outputs/\u2026, /sessions/\u2026) \u2014 this computer cannot see them \u2014 and never curl or upload from the sandbox.",
3850
3986
  'A file or folder the user has on this computer (a downloaded .html, a .zip, a build folder): call fling_deploy with its path, or just its file name (e.g. "Pitch Deck.html") \u2014 it is looked up in Downloads, Desktop, Documents/Claude and Claude Desktop output folders. Use fling_find_files to see recent candidates when unsure which file the user means.',
3851
3987
  "Confirm the site name with the user before publishing (fling_check_name shows whether it is free and, for a path, the generated name). Names are global, first come first served; when one is taken, offer the suggestions.",
@@ -3855,20 +3991,26 @@ var mcpLocalInstructions = (w, opts = {}) => [
3855
3991
  `Sites are private (only ${w.privateAudience}) by default; only make one public if asked. Use fling_share for a link people who cannot sign in can open. ${opts.comments ? "Always give the user the links that come back: the review link first, as the one to share for feedback, then the site URL." : "Always give the user the URL that comes back."}`,
3856
3992
  ...opts.comments ? [COMMENTS_NOTE(w)] : []
3857
3993
  ].join("\n\n");
3858
- var toolDef = (def) => ({
3859
- title: def.title,
3860
- description: def.description,
3861
- inputSchema: def.inputSchema,
3862
- annotations: def.annotations
3863
- });
3994
+ var onFling = (url, instance) => `
3995
+ Fling: ${serverLabel(url)}${instance ? ` (${instance.name})` : ""}`;
3864
3996
  function createMcpServer(deps = {}) {
3865
3997
  const store = deps.store ?? createConfigStore();
3866
3998
  const fetchImpl = deps.fetch ?? ((input, init) => fetch(input, init));
3867
3999
  const cwd = deps.cwd ?? process.cwd();
3868
4000
  const locate = deps.locate ?? { ...systemLocateOptions(), cwd };
3869
4001
  const waitReady = deps.waitReady ?? createReadyWaiter();
3870
- const wording = mcpWording(deps.instance);
4002
+ const servers = deps.servers ?? [];
4003
+ const several = servers.length > 1;
4004
+ const wording = mcpWording(several ? null : servers[0]?.instance);
3871
4005
  const mcpTools2 = mcpToolsFor(wording);
4006
+ const serverArg = z11.string().min(1).max(500).optional().describe(serverArgDescription(servers));
4007
+ const note = serverToolNote(servers);
4008
+ const serverTool = (def, description = def.description) => ({
4009
+ title: def.title,
4010
+ description: `${description}${note}`,
4011
+ inputSchema: { ...def.inputSchema, server: serverArg },
4012
+ annotations: def.annotations
4013
+ });
3872
4014
  async function edgeReady(url, paths) {
3873
4015
  const r = await waitReady(url, paths).catch(() => ({ ready: false, waitedMs: 0 }));
3874
4016
  return {
@@ -3877,27 +4019,44 @@ function createMcpServer(deps = {}) {
3877
4019
  Not reachable yet at ${url}: ${NOT_READY_NOTE}.`
3878
4020
  };
3879
4021
  }
3880
- async function client() {
3881
- const config = await store.resolve({ serverUrl: deps.server });
4022
+ async function choose(server2, tool2) {
4023
+ if (server2 !== void 0)
4024
+ return store.resolve({ serverUrl: rememberedServer(await store.load(), server2) });
4025
+ const resolved = await store.resolve({ serverUrl: deps.server });
4026
+ if (isPinned(resolved)) return resolved;
4027
+ const config = await store.load();
4028
+ const known = knownServersOf(config);
4029
+ if (known.length > 1) throw new ServerNeededError(known, tool2);
4030
+ const only = known[0];
4031
+ return !resolved.serverUrl && only ? store.resolve({ serverUrl: only.url }) : resolved;
4032
+ }
4033
+ async function target(server2, tool2) {
4034
+ const config = await choose(server2, tool2);
3882
4035
  if (!config.serverUrl) throw new NoServerError("fling");
3883
4036
  if (!config.apiKey) throw new CliError("Not logged in", { exitCode: EXIT_NOT_LOGGED_IN });
3884
- return createApiClient({ serverUrl: config.serverUrl, apiKey: config.apiKey, fetch: fetchImpl });
3885
- }
3886
- async function sitesBase() {
3887
- const config = await store.resolve({ serverUrl: deps.server });
3888
- return sitesBaseOf(await loadInstance(store, config, fetchImpl));
4037
+ const url = config.serverUrl;
4038
+ let instance;
4039
+ return {
4040
+ client: createApiClient({ serverUrl: url, apiKey: config.apiKey, fetch: fetchImpl }),
4041
+ url,
4042
+ config,
4043
+ instance: () => {
4044
+ instance ??= loadInstance(store, config, fetchImpl);
4045
+ return instance;
4046
+ }
4047
+ };
3889
4048
  }
3890
- async function run(op) {
4049
+ async function run(tool2, server2, op) {
3891
4050
  try {
3892
- const { summary, data } = await op(await client());
3893
- return { content: [{ type: "text", text: `${summary}
4051
+ const { summary, data } = await op(await target(server2, tool2));
4052
+ return textResult(`${summary}
3894
4053
 
3895
- ${JSON.stringify(data, null, 2)}` }] };
4054
+ ${JSON.stringify(data, null, 2)}`);
3896
4055
  } catch (error) {
3897
- return { isError: true, content: [{ type: "text", text: describeError(error) }] };
4056
+ return failure(error);
3898
4057
  }
3899
4058
  }
3900
- async function deployFolder(c, dir, opts) {
4059
+ async function deployFolder(t2, dir, opts) {
3901
4060
  let local = await buildManifest(dir);
3902
4061
  local.skipped.dotfiles.unshift(...opts.skippedDotfiles ?? []);
3903
4062
  const skippedNote = describeSkipped(local.skipped).map((l) => `
@@ -3924,14 +4083,14 @@ ${l}.`).join("");
3924
4083
  }
3925
4084
  }
3926
4085
  const result = await deploy(
3927
- { client: c, fetch: fetchImpl },
4086
+ { client: t2.client, fetch: fetchImpl },
3928
4087
  { dir, name: opts.name, manifest: local, spa: opts.spa, public: opts.public }
3929
4088
  ).catch((error) => {
3930
4089
  if (!(error instanceof NameTakenError)) throw error;
3931
4090
  const how = opts.nameGiven ? "" : ` (generated from ${opts.nameFrom ?? "the folder"})`;
3932
4091
  const pick = error.suggestions.length ? ` Free alternatives: ${error.suggestions.join(", ")}.` : "";
3933
4092
  throw new CliError(
3934
- `Not deployed: the site name "${error.siteName}"${how} is taken by someone else.${pick} Ask the user which name to use (one of these or their own), then call ${opts.retryWith} again with \`name\`.`
4093
+ `Not deployed: the site name "${error.siteName}"${how} is taken by someone else on ${serverLabel(t2.url)}.${pick} Ask the user which name to use (one of these or their own), then call ${opts.retryWith} again with \`name\`.`
3935
4094
  );
3936
4095
  });
3937
4096
  const what = result.status === "unchanged" ? "Unchanged \u2014 already live" : result.status === "repointed" ? `Restored version ${result.versionId}` : `Deployed ${result.fileCount} files (uploaded ${result.uploaded})`;
@@ -3942,19 +4101,20 @@ Warnings:
3942
4101
  result.url,
3943
4102
  local.files.map((f) => f.path)
3944
4103
  );
3945
- const instance = deps.instance;
4104
+ const instance = await t2.instance();
3946
4105
  const review = instance && commentsEnabled(instance) ? reviewUrl(instance.appOrigin, result.name) : void 0;
3947
- const listing = generatedIndex ? " \u2014 with a generated index.html listing the HTML pages" : "";
3948
- const head = review ? `${what}${listing}.
4106
+ const listing2 = generatedIndex ? " \u2014 with a generated index.html listing the HTML pages" : "";
4107
+ const head = review ? `${what}${listing2}.
3949
4108
  ${shareForFeedback(review, instance?.name ?? "Fling")}
3950
- Site: ${result.url} (${result.site.visibility})` : `${what}: ${result.url} (${result.site.visibility})${listing}`;
4109
+ Site: ${result.url} (${result.site.visibility})` : `${what}: ${result.url} (${result.site.visibility})${listing2}`;
3951
4110
  return {
3952
4111
  kind: "deployed",
3953
- summary: `${head}${warnings}${skippedNote}${edge.note}
4112
+ summary: `${head}${warnings}${skippedNote}${edge.note}${onFling(t2.url, instance)}
3954
4113
  To check exactly which files the site has, call fling_files with name "${result.name}".`,
3955
4114
  data: {
3956
4115
  status: result.status,
3957
4116
  ready: edge.ready,
4117
+ server: t2.url,
3958
4118
  name: result.name,
3959
4119
  nameGenerated: !opts.nameGiven,
3960
4120
  versionId: result.versionId,
@@ -3970,7 +4130,8 @@ To check exactly which files the site has, call fling_files with name "${result.
3970
4130
  { name: "fling", version: VERSION },
3971
4131
  {
3972
4132
  instructions: mcpLocalInstructions(wording, {
3973
- comments: commentsEnabled(deps.instance)
4133
+ comments: servers.some((s) => commentsEnabled(s.instance)),
4134
+ servers
3974
4135
  })
3975
4136
  }
3976
4137
  );
@@ -3979,7 +4140,7 @@ To check exactly which files the site has, call fling_files with name "${result.
3979
4140
  "fling_deploy",
3980
4141
  {
3981
4142
  title: "Deploy a file or folder from this computer",
3982
- description: `Fling something that is on the user's computer and return its stable URL (${wording.sitesPattern}): a folder of static files (e.g. a build output like dist/), a single .html file (served as the site's index.html), a .zip of a site (extracted; a single top-level folder is unwrapped), or any other single file (.pdf, .svg, .png\u2026, served next to a generated page that shows it). \`path\` can be absolute, relative to the working directory, or just a file name ("Pitch Deck.html"): names are looked up, newest first, in Downloads, Desktop, Documents/Claude and Claude Desktop output folders, and the result says where the file was found. Never pass paths from your own sandbox (/mnt/user-data/\u2026, /sessions/\u2026): for content you created, use fling_publish. Without \`name\`, a name is generated: from the file name for files ("Pitch Deck (1).html" \u2192 pitch-deck), from the project and folder for folders (~/work/rocketpod/docs \u2192 rocketpod-docs). Site names are one global namespace, first come first served \u2014 confirm the name with the user first (fling_check_name with \`path\` shows the generated one). If the name is taken it fails with free suggestions \u2014 ask the user to pick one, then retry with \`name\`. If there is no index.html but there are HTML pages, it does not deploy: ask the user whether to generate an index page listing them (\`listHtml: true\`) or deploy without one (\`listHtml: false\`), then retry. Re-deploying identical content is a no-op. Sites are private (only ${wording.privateAudience}) unless \`public\` is set. ` + NEVER_PUBLISHED,
4143
+ description: `Fling something that is on the user's computer and return its stable URL (${wording.sitesPattern}): a folder of static files (e.g. a build output like dist/), a single .html file (served as the site's index.html), a .zip of a site (extracted; a single top-level folder is unwrapped), or any other single file (.pdf, .svg, .png\u2026, served next to a generated page that shows it). \`path\` can be absolute, relative to the working directory, or just a file name ("Pitch Deck.html"): names are looked up, newest first, in Downloads, Desktop, Documents/Claude and Claude Desktop output folders, and the result says where the file was found. Never pass paths from your own sandbox (/mnt/user-data/\u2026, /sessions/\u2026): for content you created, use fling_publish. Without \`name\`, a name is generated: from the file name for files ("Pitch Deck (1).html" \u2192 pitch-deck), from the project and folder for folders (~/work/rocketpod/docs \u2192 rocketpod-docs). Site names are one global namespace, first come first served \u2014 confirm the name with the user first (fling_check_name with \`path\` shows the generated one). If the name is taken it fails with free suggestions \u2014 ask the user to pick one, then retry with \`name\`. If there is no index.html but there are HTML pages, it does not deploy: ask the user whether to generate an index page listing them (\`listHtml: true\`) or deploy without one (\`listHtml: false\`), then retry. Re-deploying identical content is a no-op. Sites are private (only ${wording.privateAudience}) unless \`public\` is set. ${NEVER_PUBLISHED}${note}`,
3983
4144
  inputSchema: {
3984
4145
  path: z11.string().min(1).describe(
3985
4146
  "File or folder on this computer: absolute, relative to the working directory, or just its name (looked up in Downloads, Desktop and Claude output folders)"
@@ -3989,16 +4150,17 @@ To check exactly which files the site has, call fling_files with name "${result.
3989
4150
  ),
3990
4151
  spa: spa2,
3991
4152
  public: isPublic,
3992
- listHtml: listHtml2
4153
+ listHtml: listHtml2,
4154
+ server: serverArg
3993
4155
  },
3994
4156
  annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true }
3995
4157
  },
3996
- (args) => run(async (c) => {
4158
+ (args) => run("fling_deploy", args.server, async (t2) => {
3997
4159
  const located = await resolveInput(args.path, locate);
3998
4160
  const prepared = await prepareInput(located.path);
3999
4161
  try {
4000
4162
  const name = args.name !== void 0 ? resolveSiteName(prepared.dir, args.name) : prepared.defaultName;
4001
- const outcome = await deployFolder(c, prepared.dir, {
4163
+ const outcome = await deployFolder(t2, prepared.dir, {
4002
4164
  name,
4003
4165
  nameGiven: args.name !== void 0,
4004
4166
  nameFrom: prepared.kind === "folder" ? "the folder" : "the file name",
@@ -4052,33 +4214,31 @@ ${lines.join("\n")}` : `Nothing found from the last ${args.days ?? 14} days${arg
4052
4214
  searched: searched.map((r) => ({ label: r.label, path: r.path })),
4053
4215
  ...complete ? {} : { truncated: true }
4054
4216
  };
4055
- return {
4056
- content: [{ type: "text", text: `${summary}
4217
+ return textResult(`${summary}
4057
4218
 
4058
- ${JSON.stringify(data, null, 2)}` }]
4059
- };
4219
+ ${JSON.stringify(data, null, 2)}`);
4060
4220
  } catch (error) {
4061
- return { isError: true, content: [{ type: "text", text: describeError(error) }] };
4221
+ return failure(error);
4062
4222
  }
4063
4223
  }
4064
4224
  );
4065
4225
  const publishDef = mcpTools2.publish;
4066
4226
  server.registerTool(
4067
4227
  publishDef.name,
4068
- {
4069
- ...toolDef(publishDef),
4228
+ serverTool(
4229
+ publishDef,
4070
4230
  // This server has no chunked upload tools; big sites go through fling_deploy instead.
4071
- description: publishDef.description.replace(
4231
+ publishDef.description.replace(
4072
4232
  /\(whole site, up to [^;]*; bigger sites: fling_upload_begin \/ fling_upload_file \/ fling_upload_finish\)/,
4073
4233
  "(whole site; for a big site that is already on this computer, use fling_deploy with its folder)"
4074
4234
  )
4075
- },
4076
- (args) => run(async (c) => {
4235
+ ),
4236
+ (args) => run(publishDef.name, args.server, async (t2) => {
4077
4237
  const name = resolveSiteName(".", args.name);
4078
4238
  const tmp = await mkdtemp3(join6(tmpdir3(), "fling-publish-"));
4079
4239
  try {
4080
4240
  const { skippedDotfiles } = await writeInlineFiles(tmp, args.files);
4081
- const outcome = await deployFolder(c, tmp, {
4241
+ return await deployFolder(t2, tmp, {
4082
4242
  skippedDotfiles,
4083
4243
  name,
4084
4244
  nameGiven: true,
@@ -4087,7 +4247,6 @@ ${JSON.stringify(data, null, 2)}` }]
4087
4247
  listHtml: args.listHtml,
4088
4248
  retryWith: "fling_publish"
4089
4249
  });
4090
- return outcome;
4091
4250
  } finally {
4092
4251
  await rm2(tmp, { recursive: true, force: true });
4093
4252
  }
@@ -4096,14 +4255,15 @@ ${JSON.stringify(data, null, 2)}` }]
4096
4255
  server.registerTool(
4097
4256
  "fling_check_name",
4098
4257
  {
4099
- ...toolDef(mcpTools2.checkName),
4100
- description: `${mcpTools2.checkName.description} Pass \`path\` instead of \`name\` to see the name fling_deploy would generate for a file or folder on this computer (same lookup as fling_deploy), so you can confirm it with the user.`,
4258
+ ...serverTool(mcpTools2.checkName),
4259
+ description: `${mcpTools2.checkName.description} Pass \`path\` instead of \`name\` to see the name fling_deploy would generate for a file or folder on this computer (same lookup as fling_deploy), so you can confirm it with the user.${note}`,
4101
4260
  inputSchema: {
4102
4261
  name: z11.string().min(1).max(200).optional().describe("Site name to check"),
4103
- path: z11.string().optional().describe("File or folder whose generated default name to check (path or file name)")
4262
+ path: z11.string().optional().describe("File or folder whose generated default name to check (path or file name)"),
4263
+ server: serverArg
4104
4264
  }
4105
4265
  },
4106
- (args) => run(async (c) => {
4266
+ (args) => run("fling_check_name", args.server, async (t2) => {
4107
4267
  if (args.name === void 0 && args.path === void 0)
4108
4268
  throw new CliError("Pass `name` or `path`");
4109
4269
  let name = args.name;
@@ -4112,8 +4272,8 @@ ${JSON.stringify(data, null, 2)}` }]
4112
4272
  source = await resolveInput(args.path, locate);
4113
4273
  name = defaultNameFor(source.path, source.isDirectory);
4114
4274
  }
4115
- const check2 = await checkName(c, name);
4116
- const base = check2.status === "invalid" ? null : await sitesBase();
4275
+ const check2 = await checkName(t2.client, name);
4276
+ const base = check2.status === "invalid" ? null : sitesBaseOf(await t2.instance());
4117
4277
  const url = base ? siteUrlFor(name, base) : null;
4118
4278
  const suggestions = check2.suggestions.length ? ` Suggestions: ${check2.suggestions.join(", ")}.` : "";
4119
4279
  const summary = {
@@ -4127,59 +4287,77 @@ ${JSON.stringify(data, null, 2)}` }]
4127
4287
  data: {
4128
4288
  ...check2,
4129
4289
  url,
4290
+ server: t2.url,
4130
4291
  ...source ? { generated: true, source: { path: source.path, foundIn: source.foundIn } } : {}
4131
4292
  }
4132
4293
  };
4133
4294
  })
4134
4295
  );
4135
- let pendingLogin;
4296
+ const pendingLogins = /* @__PURE__ */ new Map();
4136
4297
  server.registerTool(
4137
4298
  "fling_login",
4138
4299
  {
4139
4300
  title: "Sign in to Fling",
4140
- description: `Sign the user in to ${wording.name}: opens their browser to sign in with ${wording.signIn} and stores the key on this computer (shared with the fling CLI). Waits up to ~30 seconds; if sign-in is not finished by then, it keeps waiting in the background for up to 5 minutes \u2014 tell the user to finish signing in in the browser, then retry what you were doing. Call it when another tool says the user is not logged in.`,
4141
- inputSchema: {},
4301
+ description: `Sign the user in to ${several ? "a Fling" : wording.name}: opens their browser to sign in with ${wording.signIn} and stores the key on this computer (shared with the fling CLI). Waits up to ~30 seconds; if sign-in is not finished by then, it keeps waiting in the background for up to 5 minutes \u2014 tell the user to finish signing in in the browser, then retry what you were doing. Call it when another tool says the user is not logged in. Pass \`server\` (its address) to sign in to another Fling, including one not set up on this computer yet: it is remembered, and becomes the current one only when none is.` + note,
4302
+ inputSchema: {
4303
+ server: z11.string().min(1).max(500).optional().describe(
4304
+ `The Fling to sign in to: its address (e.g. "https://fling.example.com"), or the host of one set up on this computer. ${several ? `Required while several Flings are set up (${servers.map((s) => serverLabel(s.url)).join(", ")})` : `Omit it to use ${servers[0] ? serverLabel(servers[0].url) : "the one this computer knows"}`}`
4305
+ )
4306
+ },
4142
4307
  annotations: { readOnlyHint: false, openWorldHint: true }
4143
4308
  },
4144
- async () => {
4309
+ async (args) => {
4145
4310
  try {
4146
- const config = await store.resolve({ serverUrl: deps.server });
4147
- const serverUrl = config.serverUrl;
4311
+ let serverUrl;
4312
+ if (args.server !== void 0) {
4313
+ const config2 = await store.load();
4314
+ try {
4315
+ serverUrl = matchServer(Object.keys(config2.servers), args.server, { loose: true }).url;
4316
+ } catch (error) {
4317
+ const known = knownServersOf(config2).map((s) => `- ${describeServer(s)}`);
4318
+ throw new CliError(error.message, {
4319
+ hint: known.length ? `The Flings set up on this computer:
4320
+ ${known.join("\n")}` : ""
4321
+ });
4322
+ }
4323
+ } else {
4324
+ serverUrl = (await choose(void 0, "fling_login")).serverUrl;
4325
+ }
4148
4326
  if (!serverUrl) throw new NoServerError("fling");
4149
- if (config.apiKey && !pendingLogin) {
4327
+ const config = await store.resolve({ serverUrl });
4328
+ let login = pendingLogins.get(serverUrl);
4329
+ if (config.apiKey && !login) {
4150
4330
  const me = await whoami(
4151
- createApiClient({
4152
- serverUrl,
4153
- apiKey: config.apiKey,
4154
- fetch: fetchImpl
4155
- })
4331
+ createApiClient({ serverUrl, apiKey: config.apiKey, fetch: fetchImpl })
4156
4332
  ).catch(() => null);
4157
- if (me) return textResult(`Already signed in to ${config.serverUrl} as ${me.email}.`);
4333
+ if (me) return textResult(`Already signed in to ${serverUrl} as ${me.email}.`);
4158
4334
  }
4159
- if (!pendingLogin) {
4160
- const login = {
4335
+ if (!login) {
4336
+ const started = {
4161
4337
  done: Promise.resolve()
4162
4338
  };
4163
- login.done = loginFlow({
4339
+ started.done = loginFlow({
4164
4340
  serverUrl,
4165
4341
  store,
4166
4342
  log: silentLogger,
4167
4343
  open: deps.open ?? (async (url) => (await import("open")).default(url)),
4168
4344
  fetch: fetchImpl,
4169
4345
  timeoutMs: deps.loginTimeoutMs,
4170
- // Signing in to the extension's server leaves the CLI's current server as it is.
4346
+ // Signing in to another Fling leaves the CLI's current server as it is.
4171
4347
  makeCurrent: !config.current || config.current === serverUrl,
4172
4348
  onAuthUrl: (url) => {
4173
- login.url = url;
4349
+ started.url = url;
4174
4350
  }
4175
4351
  });
4176
- login.done.catch(() => {
4352
+ const key = serverUrl;
4353
+ started.done.catch(() => {
4177
4354
  }).finally(() => {
4178
- if (pendingLogin === login) pendingLogin = void 0;
4355
+ if (pendingLogins.get(key) === started) pendingLogins.delete(key);
4179
4356
  });
4180
- pendingLogin = login;
4357
+ pendingLogins.set(serverUrl, started);
4358
+ login = started;
4181
4359
  }
4182
- const current = pendingLogin;
4360
+ const current = login;
4183
4361
  const waited = await Promise.race([
4184
4362
  current.done.then(
4185
4363
  (r) => ({ ok: true, r }),
@@ -4192,34 +4370,82 @@ ${JSON.stringify(data, null, 2)}` }]
4192
4370
  `Finish signing in in your browser, then try again. (The sign-in page is open; if it did not open, visit: ${current.url ?? "the Fling site"} \u2014 this stays valid for 5 minutes.)`
4193
4371
  );
4194
4372
  if (!waited.ok) throw waited.e;
4195
- return textResult(`Signed in as ${waited.r.user.email} (${waited.r.serverUrl}).`);
4373
+ return textResult(
4374
+ `Signed in as ${waited.r.user.email} (${waited.r.serverUrl}, ${waited.r.instance.name}).`
4375
+ );
4376
+ } catch (error) {
4377
+ return failure(error);
4378
+ }
4379
+ }
4380
+ );
4381
+ server.registerTool(
4382
+ "fling_servers",
4383
+ {
4384
+ title: "List the Flings on this computer",
4385
+ description: `List the Flings set up on this computer, read now: each one's address, name, who is signed in (or "signed out") and which is current, the user's usual one. Call it when unsure which Fling the user means or which exist; the list may have changed since this conversation started. Pass \`use\` to make one the current Fling, the same as running \`${NPX_COMMAND} use <server>\`: it changes the default of the fling CLI too, so only do it when the user asks. It does not replace passing \`server\` to the other tools while several Flings are set up.`,
4386
+ inputSchema: {
4387
+ use: z11.string().min(1).max(500).optional().describe(
4388
+ "Make this Fling current (address, host or unique part of it), like `fling use`"
4389
+ )
4390
+ },
4391
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false }
4392
+ },
4393
+ async (args) => {
4394
+ try {
4395
+ let switched = "";
4396
+ if (args.use !== void 0) {
4397
+ const url = rememberedServer(await store.load(), args.use);
4398
+ await store.setCurrent(url);
4399
+ switched = `Now using ${serverLabel(url)} as the current Fling (as \`fling use\` does).
4400
+ `;
4401
+ }
4402
+ const config = await store.load();
4403
+ const known = knownServersOf(config);
4404
+ const pinned = await store.resolve({ serverUrl: deps.server });
4405
+ const summary = known.length ? `${switched}${known.length} Fling(s) set up on this computer:
4406
+ ${known.map((s) => `- ${describeServer(s)}`).join("\n")}` + (isPinned(pinned) && pinned.serverUrl ? `
4407
+ Calls without \`server\` go to ${serverLabel(pinned.serverUrl)} (set by this server's configuration).` : known.length > 1 ? "\nPass `server` on every call: the Fling settled on for this conversation." : "") : NO_SERVER_HINT;
4408
+ const data = {
4409
+ current: config.current ?? null,
4410
+ servers: known.map((s) => ({
4411
+ url: s.url,
4412
+ host: serverLabel(s.url),
4413
+ instance: s.instance?.name ?? null,
4414
+ signedIn: s.signedIn,
4415
+ email: s.email ?? null,
4416
+ current: s.current
4417
+ }))
4418
+ };
4419
+ return textResult(`${summary}
4420
+
4421
+ ${JSON.stringify(data, null, 2)}`);
4196
4422
  } catch (error) {
4197
- return { isError: true, content: [{ type: "text", text: describeError(error) }] };
4423
+ return failure(error);
4198
4424
  }
4199
4425
  }
4200
4426
  );
4201
4427
  const t = mcpTools2;
4202
4428
  server.registerTool(
4203
4429
  t.list.name,
4204
- toolDef(t.list),
4205
- (args) => run(async (c) => {
4206
- const sites = await listSites(c, { archived: args.archived });
4207
- return { summary: `${sites.length} site(s)`, data: { sites } };
4430
+ serverTool(t.list),
4431
+ (args) => run(t.list.name, args.server, async ({ client, url }) => {
4432
+ const sites = await listSites(client, { archived: args.archived });
4433
+ return { summary: `${sites.length} site(s) on ${serverLabel(url)}`, data: { sites } };
4208
4434
  })
4209
4435
  );
4210
4436
  server.registerTool(
4211
4437
  t.info.name,
4212
- toolDef(t.info),
4213
- (args) => run(async (c) => {
4214
- const site = await getSite(c, args.name);
4438
+ serverTool(t.info),
4439
+ (args) => run(t.info.name, args.server, async ({ client }) => {
4440
+ const site = await getSite(client, args.name);
4215
4441
  return { summary: `${site.name}: ${site.url}`, data: site };
4216
4442
  })
4217
4443
  );
4218
4444
  server.registerTool(
4219
4445
  t.files.name,
4220
- toolDef(t.files),
4221
- (args) => run(async (c) => {
4222
- const result = await versionFiles(c, args.name, args.versionId);
4446
+ serverTool(t.files),
4447
+ (args) => run(t.files.name, args.server, async ({ client }) => {
4448
+ const result = await versionFiles(client, args.name, args.versionId);
4223
4449
  return {
4224
4450
  summary: describeVersionFiles(result.name, result, {
4225
4451
  current: result.current,
@@ -4231,15 +4457,15 @@ ${JSON.stringify(data, null, 2)}` }]
4231
4457
  );
4232
4458
  server.registerTool(
4233
4459
  t.share.name,
4234
- toolDef(t.share),
4235
- (args) => run(async (c) => {
4460
+ serverTool(t.share),
4461
+ (args) => run(t.share.name, args.server, async ({ client }) => {
4236
4462
  if (args.refresh && args.revoke) throw new CliError("Use either refresh or revoke, not both");
4237
- const result = await share(c, args.name, args);
4463
+ const result = await share(client, args.name, args);
4238
4464
  if (result.action === "revoked")
4239
4465
  return { summary: `Share link for ${args.name} revoked`, data: result };
4240
4466
  let edge = { ready: true, note: "" };
4241
4467
  if (result.action !== "existing") {
4242
- const files = await versionFiles(c, args.name).catch(() => null);
4468
+ const files = await versionFiles(client, args.name).catch(() => null);
4243
4469
  edge = await edgeReady(result.shareUrl, files?.files.map((f) => f.path) ?? []);
4244
4470
  }
4245
4471
  return {
@@ -4250,78 +4476,86 @@ ${JSON.stringify(data, null, 2)}` }]
4250
4476
  );
4251
4477
  server.registerTool(
4252
4478
  t.setVisibility.name,
4253
- toolDef(t.setVisibility),
4254
- (args) => run(async (c) => {
4255
- const site = await setVisibility(c, args.name, args.visibility);
4479
+ serverTool(t.setVisibility),
4480
+ (args) => run(t.setVisibility.name, args.server, async ({ client }) => {
4481
+ const site = await setVisibility(client, args.name, args.visibility);
4256
4482
  return { summary: `${site.name} is now ${site.visibility}: ${site.url}`, data: site };
4257
4483
  })
4258
4484
  );
4259
4485
  server.registerTool(
4260
4486
  t.archive.name,
4261
- toolDef(t.archive),
4262
- (args) => run(async (c) => {
4263
- const site = await archiveSite(c, args.name, { unarchive: args.unarchive });
4487
+ serverTool(t.archive),
4488
+ (args) => run(t.archive.name, args.server, async ({ client }) => {
4489
+ const site = await archiveSite(client, args.name, { unarchive: args.unarchive });
4264
4490
  return { summary: `${site.name} ${args.unarchive ? "restored" : "archived"}`, data: site };
4265
4491
  })
4266
4492
  );
4267
4493
  server.registerTool(
4268
4494
  t.delete.name,
4269
- toolDef(t.delete),
4270
- (args) => run(async (c) => {
4271
- const result = await deleteSite(c, args.name);
4272
- return { summary: `${args.name} deleted`, data: result };
4495
+ serverTool(t.delete),
4496
+ (args) => run(t.delete.name, args.server, async ({ client, url }) => {
4497
+ const result = await deleteSite(client, args.name);
4498
+ return { summary: `${args.name} deleted from ${serverLabel(url)}`, data: result };
4273
4499
  })
4274
4500
  );
4275
4501
  server.registerTool(
4276
4502
  t.rollback.name,
4277
- toolDef(t.rollback),
4278
- (args) => run(async (c) => {
4279
- const site = await rollback(c, args.name, args.versionId);
4503
+ serverTool(t.rollback),
4504
+ (args) => run(t.rollback.name, args.server, async ({ client }) => {
4505
+ const site = await rollback(client, args.name, args.versionId);
4280
4506
  return {
4281
4507
  summary: `${site.name} now serves version ${site.currentVersionId}: ${site.url}`,
4282
4508
  data: site
4283
4509
  };
4284
4510
  })
4285
4511
  );
4286
- if (deps.instance && commentsEnabled(deps.instance)) {
4287
- const appOrigin = deps.instance.appOrigin;
4288
- const review = (name) => reviewUrl(appOrigin, name);
4512
+ if (servers.some((s) => commentsEnabled(s.instance))) {
4513
+ async function reviewOn(target2) {
4514
+ const instance = await target2.instance();
4515
+ if (instance?.capabilities.comments === false)
4516
+ throw new CliError(`${instance.name} (${serverLabel(target2.url)}) has comments turned off`);
4517
+ const appOrigin = instance?.appOrigin ?? target2.url;
4518
+ return {
4519
+ review: (name) => reviewUrl(appOrigin, name),
4520
+ fling: onFling(target2.url, instance)
4521
+ };
4522
+ }
4289
4523
  server.registerTool(
4290
4524
  t.commentsList.name,
4291
- toolDef(t.commentsList),
4292
- (args) => run(async (c) => {
4293
- const list = await listComments(c, args.name, { path: args.path });
4525
+ serverTool(t.commentsList),
4526
+ (args) => run(t.commentsList.name, args.server, async (target2) => {
4527
+ const { review, fling } = await reviewOn(target2);
4528
+ const list = await listComments(target2.client, args.name, { path: args.path });
4294
4529
  const reviewPage2 = review(list.site.name);
4295
4530
  return {
4296
- summary: describeComments(list, {
4297
- includeResolved: args.includeResolved,
4298
- reviewUrl: reviewPage2
4299
- }),
4531
+ summary: `${describeComments(list, { includeResolved: args.includeResolved, reviewUrl: reviewPage2 })}${fling}`,
4300
4532
  data: { ...list, reviewUrl: reviewPage2 }
4301
4533
  };
4302
4534
  })
4303
4535
  );
4304
4536
  server.registerTool(
4305
4537
  t.commentAdd.name,
4306
- toolDef(t.commentAdd),
4307
- (args) => run(async (c) => {
4308
- const comment = await addComment(c, args.name, {
4538
+ serverTool(t.commentAdd),
4539
+ (args) => run(t.commentAdd.name, args.server, async (target2) => {
4540
+ const { review, fling } = await reviewOn(target2);
4541
+ const comment = await addComment(target2.client, args.name, {
4309
4542
  body: args.body,
4310
4543
  path: args.path,
4311
4544
  parentId: args.replyTo
4312
4545
  });
4313
4546
  const what = comment.parentId ? `Replied to thread ${comment.parentId}` : "Comment added";
4314
4547
  return {
4315
- summary: `${what} on ${args.name} (${comment.path}, id ${comment.id}). Review page: ${review(args.name)}`,
4548
+ summary: `${what} on ${args.name} (${comment.path}, id ${comment.id}). Review page: ${review(args.name)}${fling}`,
4316
4549
  data: comment
4317
4550
  };
4318
4551
  })
4319
4552
  );
4320
4553
  server.registerTool(
4321
4554
  t.commentResolve.name,
4322
- toolDef(t.commentResolve),
4323
- (args) => run(async (c) => {
4324
- const comment = await resolveComment(c, args.name, args.id, !args.reopen);
4555
+ serverTool(t.commentResolve),
4556
+ (args) => run(t.commentResolve.name, args.server, async (target2) => {
4557
+ await reviewOn(target2);
4558
+ const comment = await resolveComment(target2.client, args.name, args.id, !args.reopen);
4325
4559
  return {
4326
4560
  summary: `Thread ${comment.id} on ${args.name} ${args.reopen ? "reopened" : "resolved"}`,
4327
4561
  data: comment
@@ -4330,22 +4564,24 @@ ${JSON.stringify(data, null, 2)}` }]
4330
4564
  );
4331
4565
  server.registerTool(
4332
4566
  t.commentDelete.name,
4333
- toolDef(t.commentDelete),
4334
- (args) => run(async (c) => {
4335
- const result = await deleteComment(c, args.name, args.id);
4567
+ serverTool(t.commentDelete),
4568
+ (args) => run(t.commentDelete.name, args.server, async (target2) => {
4569
+ await reviewOn(target2);
4570
+ const result = await deleteComment(target2.client, args.name, args.id);
4336
4571
  return { summary: `Comment ${args.id} on ${args.name} deleted`, data: result };
4337
4572
  })
4338
4573
  );
4339
4574
  }
4340
4575
  server.registerTool(
4341
4576
  t.whoami.name,
4342
- toolDef(t.whoami),
4343
- () => run(async (c) => {
4344
- const me = await whoami(c);
4345
- const base = await sitesBase();
4577
+ serverTool(t.whoami),
4578
+ (args) => run(t.whoami.name, args.server, async (target2) => {
4579
+ const me = await whoami(target2.client);
4580
+ const instance = await target2.instance();
4581
+ const base = sitesBaseOf(instance);
4346
4582
  return {
4347
- summary: `Signed in as ${me.email} (sites are served at ${sitesPattern(base)})`,
4348
- data: me
4583
+ summary: `Signed in to ${serverLabel(target2.url)}${instance ? ` (${instance.name})` : ""} as ${me.email} (sites are served at ${sitesPattern(base)})`,
4584
+ data: { ...me, server: target2.url }
4349
4585
  };
4350
4586
  })
4351
4587
  );
@@ -4382,6 +4618,25 @@ async function writeInlineFiles(dir, files) {
4382
4618
  if (secretFiles.length > 0) throw new SecretFilesError(secretFiles);
4383
4619
  return { skippedDotfiles };
4384
4620
  }
4621
+ function failure(error) {
4622
+ if (error instanceof ServerNeededError) {
4623
+ const data = {
4624
+ status: "needs_server",
4625
+ servers: error.servers.map((s) => ({
4626
+ url: s.url,
4627
+ host: serverLabel(s.url),
4628
+ instance: s.instance?.name ?? null,
4629
+ signedIn: s.signedIn,
4630
+ email: s.email ?? null,
4631
+ current: s.current
4632
+ }))
4633
+ };
4634
+ return textResult(`${error.message}
4635
+
4636
+ ${JSON.stringify(data, null, 2)}`);
4637
+ }
4638
+ return { isError: true, content: [{ type: "text", text: describeError(error) }] };
4639
+ }
4385
4640
  function describeError(error) {
4386
4641
  if (error instanceof NoServerError) return NO_SERVER_HINT;
4387
4642
  if (error instanceof CliError && error.exitCode === EXIT_NOT_LOGGED_IN) return LOGIN_HINT;
@@ -4393,8 +4648,11 @@ ${error.hint}` : error.message;
4393
4648
  }
4394
4649
  async function runMcpServer(deps = {}) {
4395
4650
  const store = deps.store ?? createConfigStore();
4396
- const instance = deps.instance ?? await store.resolve({ serverUrl: deps.server }).then((config) => loadInstance(store, config, deps.fetch ?? ((i, init) => fetch(i, init)))).catch(() => null);
4397
- const server = createMcpServer({ ...deps, store, instance });
4651
+ const fetchImpl = deps.fetch ?? ((i, init) => fetch(i, init));
4652
+ await addServerOnStart(store, serverToAdd(), fetchImpl).catch(() => {
4653
+ });
4654
+ const servers = deps.servers ?? await knownServers(store, fetchImpl, deps.server).catch(() => []);
4655
+ const server = createMcpServer({ ...deps, store, servers });
4398
4656
  await server.connect(new StdioServerTransport());
4399
4657
  }
4400
4658