@skyelight/mcp 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -11,7 +11,25 @@ keeps working. A **patch** is a fix or a wording change. A **major** would be
11
11
  removing a tool or an argument, or changing what an existing field means, and
12
12
  there has not been one.
13
13
 
14
- ## 0.6.0 — unreleased
14
+ ## 0.7.0 — unreleased
15
+
16
+ ### Changed
17
+
18
+ - **`publish_prototype` is now `publish_review_link`, and takes `code` or
19
+ `url`.** With `code`, as before: Skyelight hosts one file at its own link.
20
+ With `url`, a deployed app (Lovable, v0, Bolt, Vercel) goes on the
21
+ project's Review Links, and the answer carries the script tag to add to
22
+ the app's `<head>`. Code that is part of a larger app is no longer hosted;
23
+ the answer says to send the app's `url`. The server still answers the old
24
+ name, so 0.6.0 keeps working.
25
+ - **`source`:** where a hosted prototype was made (`claude`, `openai`, `v0`,
26
+ `lovable` and others), shown as its logo on the project's Review Links.
27
+
28
+ - **`list_workspaces` says what an agent can't do in a workspace:** a
29
+ reviewer seat, a plan without agent tools, or Free, whose MCP publishes
30
+ prototypes.
31
+
32
+ ## 0.6.0 — 2026-10-05
15
33
 
16
34
  ### Added
17
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyelight/mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "MCP server for Skyelight — read feedback items from your project as an agent",
5
5
  "type": "module",
6
6
  "bin": {
package/src/brand.js CHANGED
@@ -61,8 +61,5 @@ export const STATUS = {
61
61
  merge_items: ["Merging duplicates", "Merged the duplicates"],
62
62
  decision_log: ["Reading the decisions", "Read the decisions"],
63
63
  save_rule: ["Saving the rule", "Saved the rule"],
64
- publish_prototype: [
65
- "Publishing the prototype to Skyelight",
66
- "Published the prototype",
67
- ],
64
+ publish_review_link: ["Publishing to Skyelight", "Published to Skyelight"],
68
65
  };
package/src/client.js CHANGED
@@ -110,6 +110,6 @@ export function createClient({ apiUrl, token, fetchImpl = fetch }) {
110
110
  mergeItems: (body) => post("/api/v1/items/merge", body),
111
111
  decisionLog: (params) => request("/api/v1/decisions", params),
112
112
  saveRule: (body) => post("/api/v1/rules", body),
113
- publishPrototype: (body) => post("/api/v1/prototypes/publish", body),
113
+ publishReviewLink: (body) => post("/api/v1/review-links", body),
114
114
  };
115
115
  }
package/src/tools.d.ts CHANGED
@@ -42,6 +42,7 @@ export declare function renderMembers(result: unknown): string;
42
42
  export declare function renderAssigned(result: unknown): string;
43
43
 
44
44
  export declare function renderPublished(result: unknown): string;
45
+ export declare function renderReviewLink(result: unknown): string;
45
46
  export declare function renderPosted(
46
47
  result: unknown,
47
48
  opts: { kind: "update" | "item" },
package/src/tools.js CHANGED
@@ -204,31 +204,42 @@ export function toolDefinitions(opts = {}) {
204
204
  },
205
205
  },
206
206
  {
207
- name: "publish_prototype",
207
+ name: "publish_review_link",
208
208
  description:
209
- "Host a prototype on Skyelight so people can review it: Skyelight serves the code at " +
210
- "its own link with the review badge on it, and their pins land in a Skyelight project. " +
211
- "Pass the complete code of one file: a whole HTML page, or one React component with " +
212
- "an export default (Tailwind classes and npm imports work; local imports such as " +
213
- "@/components do not). Returns the share link and the project link; give both to the " +
214
- "person. Publishing again with the same prototypeId, or the same name in the same " +
215
- "project, makes a new version at the same link. The link is public to anyone who has it.",
209
+ "Share something with Skyelight for review, and get a link people can pin feedback on. " +
210
+ "Two ways:\n" +
211
+ "- code: one file Skyelight hosts at its own link with the review badge on it. A whole HTML page, " +
212
+ "or one React component with an export default (Tailwind classes and npm imports work; local " +
213
+ "imports such as @/components do not). For Claude prototypes, v0 snippets and single pages. " +
214
+ "Publishing again with the same prototypeId, or the same name in the same project, makes a new " +
215
+ "version at the same link.\n" +
216
+ "- url: an app already deployed (a Lovable, v0, Bolt or Vercel project: anything with more than " +
217
+ "one file). Adds its address to the project's Review Links and returns a script tag to add to the " +
218
+ "app's <head> (index.html, or the root layout); add it and redeploy. Owners and admins.\n" +
219
+ "Returns the share link and the project link; give both to the person. A hosted link is public " +
220
+ "to anyone who has it.",
216
221
  inputSchema: {
217
222
  type: "object",
218
223
  properties: {
219
- name: {
224
+ code: {
220
225
  type: "string",
221
226
  description:
222
- "What it is, in a few words. Names the link and, when no project is given, a new project.",
227
+ "For Skyelight to host: the complete file, never a summary or an excerpt. Or give url.",
223
228
  },
224
- code: {
229
+ url: {
225
230
  type: "string",
226
- description: "The complete file. Never a summary or an excerpt.",
231
+ description:
232
+ "Where the app is deployed, e.g. https://my-app.lovable.app. Or give code.",
233
+ },
234
+ name: {
235
+ type: "string",
236
+ description:
237
+ "What it is, in a few words. Names the hosted link and, when no project is given, a new project.",
227
238
  },
228
239
  kind: {
229
240
  type: "string",
230
241
  enum: ["html", "react"],
231
- description: "Optional; worked out from the code when left out.",
242
+ description: "With code; worked out from it when left out.",
232
243
  },
233
244
  projectId: {
234
245
  type: "string",
@@ -238,15 +249,19 @@ export function toolDefinitions(opts = {}) {
238
249
  prototypeId: {
239
250
  type: "string",
240
251
  description:
241
- "To publish a new version of a prototype published before.",
252
+ "With code: to publish a new version of a prototype published before.",
242
253
  },
243
254
  workspaceId: {
244
255
  type: "string",
245
256
  description:
246
257
  "When you are in more than one workspace and give no project, from list_workspaces.",
247
258
  },
259
+ source: {
260
+ type: "string",
261
+ description:
262
+ 'With code: where it was made, shown as its logo in Skyelight: "claude", "claude-code", "openai" (ChatGPT), "cursor", "windsurf", "v0", "lovable" or "other". Name yourself.',
263
+ },
248
264
  },
249
- required: ["code"],
250
265
  },
251
266
  },
252
267
  {
@@ -386,8 +401,9 @@ const TOOL_ANNOTATIONS = {
386
401
  save_rule: { ...WRITE, idempotentHint: true },
387
402
  // Hides items from every list (reversible in the app, but not here).
388
403
  merge_items: { ...WRITE, destructiveHint: true },
389
- // Puts code at a public link: something the person should agree to.
390
- publish_prototype: { ...WRITE, openWorldHint: true },
404
+ // Puts code at a public link, or lets reviewers sign in on another site:
405
+ // either way, something the person should agree to.
406
+ publish_review_link: { ...WRITE, openWorldHint: true },
391
407
  };
392
408
 
393
409
  function plural(n, one, many) {
@@ -433,6 +449,9 @@ export function renderWorkspaces(result) {
433
449
  const projects = plural(w.projectCount, "project", "projects");
434
450
  lines.push(` ${w.name} — ${w.role}, ${projects}`);
435
451
  lines.push(` ${w.id}`);
452
+ // What an agent cannot do here, and why: a seat to ask for, a plan to
453
+ // upgrade, or Free's MCP, which publishes prototypes.
454
+ if (w.unavailableReason) lines.push(` ${w.unavailableReason}`);
436
455
  }
437
456
  return lines.join("\n");
438
457
  }
@@ -810,6 +829,19 @@ export function renderPublished(result) {
810
829
  return lines.join("\n");
811
830
  }
812
831
 
832
+ export function renderReviewLink(result) {
833
+ return [
834
+ result.alreadyAdded
835
+ ? `${result.origin} was already on ${result.projectName}'s Review Links.`
836
+ : `Added ${result.origin} to ${result.projectName}'s Review Links${result.createdProject ? " (new project)" : ""}.`,
837
+ "Add this to the app's <head> (index.html, or the root layout), then redeploy:",
838
+ result.scriptTag,
839
+ `Share link: ${result.shareUrl}`,
840
+ `Project: ${result.projectUrl}`,
841
+ "Reviewers sign in to Skyelight from the badge to leave pins.",
842
+ ].join("\n");
843
+ }
844
+
813
845
  export function renderMembers(result) {
814
846
  const members = result?.members ?? [];
815
847
  const where = result?.workspace?.name ? ` in ${result.workspace.name}` : "";
@@ -897,24 +929,38 @@ export async function callTool(name, args, { client, config }) {
897
929
  return { text: renderPosted(result, { kind: "item" }), data: result };
898
930
  }
899
931
 
900
- if (name === "publish_prototype") {
901
- if (!args.code?.trim()) {
932
+ if (name === "publish_review_link" || name === "publish_prototype") {
933
+ const hasCode = !!args.code?.trim();
934
+ const hasUrl = !!args.url?.trim();
935
+ if (hasCode === hasUrl) {
902
936
  throw new Error(
903
- "publish_prototype needs code: the whole page, or one React component",
937
+ "publish_review_link takes code (one file for Skyelight to host) or url (where the app is deployed)",
904
938
  );
905
939
  }
906
- if (!args.prototypeId && !args.name?.trim()) {
907
- throw new Error("publish_prototype needs a name");
940
+ if (hasCode && !args.prototypeId && !args.name?.trim()) {
941
+ throw new Error("publish_review_link needs a name");
908
942
  }
909
- const result = await client.publishPrototype({
910
- name: args.name ?? "",
911
- code: args.code,
912
- ...(args.kind ? { kind: args.kind } : {}),
913
- ...(args.projectId ? { projectId: args.projectId } : {}),
914
- ...(args.prototypeId ? { prototypeId: args.prototypeId } : {}),
915
- ...(args.workspaceId ? { workspaceId: args.workspaceId } : {}),
916
- });
917
- return { text: renderPublished(result), data: result };
943
+ const pick = (keys) =>
944
+ Object.fromEntries(keys.filter((k) => args[k]).map((k) => [k, args[k]]));
945
+ const result = await client.publishReviewLink(
946
+ hasUrl
947
+ ? pick(["url", "projectId", "name", "workspaceId"])
948
+ : {
949
+ name: args.name ?? "",
950
+ ...pick([
951
+ "code",
952
+ "kind",
953
+ "projectId",
954
+ "prototypeId",
955
+ "workspaceId",
956
+ "source",
957
+ ]),
958
+ },
959
+ );
960
+ return {
961
+ text: result?.linked ? renderReviewLink(result) : renderPublished(result),
962
+ data: result,
963
+ };
918
964
  }
919
965
 
920
966
  if (name === "list_members") {
package/src/version.js CHANGED
@@ -12,4 +12,4 @@
12
12
  * filesystem there. `test/server.test.js` asserts the two agree, so the pair
13
13
  * cannot drift again without a test going red.
14
14
  */
15
- export const VERSION = "0.6.0";
15
+ export const VERSION = "0.7.0";