@skyelight/mcp 0.2.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
@@ -4,7 +4,7 @@ An MCP server that lets a coding agent read the feedback people left on your
4
4
  running app — the thread, the page, and the element they were pointing at —
5
5
  and report back on it when the work is done.
6
6
 
7
- Works with anything that speaks MCP over stdio: Claude Code, Cursor, Codex.
7
+ Works with anything that speaks MCP over stdio: Claude Code, Cursor, Grok, Codex.
8
8
 
9
9
  ## Setup
10
10
 
@@ -22,8 +22,8 @@ Finds your coding agent, registers the remote server with it, and leaves the
22
22
  sign-in to OAuth — nothing is pasted and no key is stored on disk. It shows
23
23
  what it will change and waits; `--yes` skips that once you have read it.
24
24
 
25
- It knows Claude Code, Cursor, Windsurf and Codex. Name one with `--client
26
- cursor` when more than one is installed, and point at another deployment with
25
+ It knows Claude Code, Cursor, Grok, Codex and Windsurf. Name one with
26
+ `--client cursor` when more than one is installed, and point at another deployment with
27
27
  `SKYELIGHT_URL=https://…`. The deployment tells the installer its own MCP URL
28
28
  and OAuth client over `/mcp/install-config`, so this package holds no
29
29
  environment-specific constants.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyelight/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for Skyelight \u2014 read feedback items from your project as an agent",
5
5
  "type": "module",
6
6
  "bin": {
package/src/install.js CHANGED
@@ -30,7 +30,8 @@ const home = () => homedir();
30
30
  *
31
31
  * Ordered by how likely a given machine is to have one, because the first
32
32
  * match wins when nothing is named. That is a guess, and `--client` exists
33
- * for when it guesses wrong.
33
+ * for when it guesses wrong. The same order the settings card lists them in,
34
+ * so the two never disagree about which agent is the obvious one.
34
35
  */
35
36
  export const CLIENTS = [
36
37
  {
@@ -67,12 +68,40 @@ export const CLIENTS = [
67
68
  detect: () => existsSync(join(home(), ".cursor")),
68
69
  },
69
70
  {
70
- id: "windsurf",
71
- label: "Windsurf",
72
- file: () => join(home(), ".codeium", "windsurf", "mcp_config.json"),
73
- format: "json",
74
- entry: (url) => ({ serverUrl: url }),
75
- detect: () => existsSync(join(home(), ".codeium", "windsurf")),
71
+ /**
72
+ * Grok's own entry, which it needs despite appearing to work without one.
73
+ *
74
+ * It reads `~/.claude.json` as a compatibility source, so on a machine
75
+ * that also has Claude Code it inherits that registration — client id,
76
+ * callback port and all — and looks configured when nothing here ran.
77
+ * On a machine without Claude Code there is nothing to inherit.
78
+ *
79
+ * Two differences from Codex, both from Grok's own config reference:
80
+ * `oauth_client_id` is a flat key rather than a nested table, and there
81
+ * is no callback setting at all — Grok picks the loopback address, which
82
+ * is why both spellings of it are registered with Clerk.
83
+ *
84
+ * `oauth_scopes` is spelled out because Grok sends no `scope` parameter
85
+ * otherwise, and a token minted without `offline_access` carries no
86
+ * refresh: the session works, then quietly stops.
87
+ */
88
+ id: "grok",
89
+ label: "Grok",
90
+ file: () => join(home(), ".grok", "config.toml"),
91
+ format: "toml",
92
+ entry: (url, clientId) =>
93
+ [
94
+ `[mcp_servers.${SERVER_NAME}]`,
95
+ `url = "${url}"`,
96
+ "enabled = true",
97
+ ...(clientId
98
+ ? [
99
+ `oauth_client_id = "${clientId}"`,
100
+ 'oauth_scopes = ["profile", "email", "offline_access"]',
101
+ ]
102
+ : []),
103
+ ].join("\n"),
104
+ detect: () => existsSync(join(home(), ".grok")),
76
105
  },
77
106
  {
78
107
  id: "codex",
@@ -96,6 +125,14 @@ export const CLIENTS = [
96
125
  ].join("\n"),
97
126
  detect: () => existsSync(join(home(), ".codex")),
98
127
  },
128
+ {
129
+ id: "windsurf",
130
+ label: "Windsurf",
131
+ file: () => join(home(), ".codeium", "windsurf", "mcp_config.json"),
132
+ format: "json",
133
+ entry: (url) => ({ serverUrl: url }),
134
+ detect: () => existsSync(join(home(), ".codeium", "windsurf")),
135
+ },
99
136
  ];
100
137
 
101
138
  /** On PATH, without running the thing. */
package/src/tools.js CHANGED
@@ -393,6 +393,18 @@ export function renderItem(item) {
393
393
  if (item.anchor.selectedText) {
394
394
  lines.push(` with "${item.anchor.selectedText}" selected`);
395
395
  }
396
+ // Which row, when the element is one of many the same component rendered.
397
+ // The author's own React key, so it reads as something from their codebase
398
+ // — "the row with key plan:pro" locates a record, which is a different and
399
+ // better question than where on the page it was drawn.
400
+ //
401
+ // `skyId` is deliberately not here. It is an opaque hash: it anchors the
402
+ // pin and tells a reader nothing they can act on, and `source` below
403
+ // already names the file. It stays in the structured response for anything
404
+ // resolving anchors, out of the prose for anything reading one.
405
+ if (item.anchor.skyKey) {
406
+ lines.push(` the item with key ${item.anchor.skyKey}`);
407
+ }
396
408
  lines.push(` selector: ${item.anchor.selector}`);
397
409
  lines.push(
398
410
  ` seen at ${item.anchor.viewport.width}x${item.anchor.viewport.height}`,