supergravity-mcp 0.1.5 → 0.2.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
@@ -70,6 +70,38 @@ npm run build
70
70
  (see `AVAILABLE_MODELS` in `src/antigravity-client.ts`).
71
71
  - `timeoutMs` — optional, default 120000 (2 minutes).
72
72
 
73
+ If the task generates files (images, documents, etc.), the result also
74
+ includes a real filesystem path — Antigravity stores each conversation's
75
+ files at `~/.gemini/antigravity/brain/<conversation-id>/` on disk, confirmed
76
+ directly (not documented anywhere by Google). No need to fetch anything
77
+ through Antigravity's local server; just read the file.
78
+
79
+ ### `delegate_to_antigravity_batch(tasks)`
80
+
81
+ Runs several independent tasks at once, each in its own Antigravity window —
82
+ genuinely concurrent, not queued. Confirmed with two simultaneous
83
+ ~200-word generations that both completed correctly in the same ~16s window.
84
+ Opens extra windows automatically as needed (macOS's "New Window" menu
85
+ command via System Events, with a fallback for when zero windows are open).
86
+
87
+ Requires Accessibility permission for whatever process runs this (System
88
+ Settings > Privacy & Security > Accessibility) — macOS only. Don't run this
89
+ at the same time as a separate `delegate_to_antigravity` call; window
90
+ allocation between the two isn't coordinated.
91
+
92
+ ```ts
93
+ delegateToAntigravityBatch([
94
+ { task: "..." },
95
+ { task: "...", model: "Claude Sonnet 4.6 (Thinking)" },
96
+ ]);
97
+ ```
98
+
99
+ ### `list_antigravity_files(limit?)`
100
+
101
+ Every file Antigravity has generated, across every conversation, most recent
102
+ first — not scoped to a single delegate call. Useful for finding something
103
+ generated earlier in this session or a past one.
104
+
73
105
  ### `get_antigravity_quota()`
74
106
 
75
107
  Reads Antigravity's Settings > Models panel and returns remaining quota —
@@ -1,4 +1,4 @@
1
- import { execSync, spawn } from "node:child_process";
1
+ import { execFileSync, execSync, spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -44,26 +44,160 @@ function withLock(fn) {
44
44
  }
45
45
  export async function delegateToAntigravity(opts) {
46
46
  return withLock(async () => {
47
- const { task, model, timeoutMs = 120_000 } = opts;
48
47
  await ensureAntigravityReady();
49
48
  const target = await getMainPageTarget();
49
+ return runOnTarget(target, opts);
50
+ });
51
+ }
52
+ /**
53
+ * Run several tasks at once, each in its own Antigravity window - confirmed
54
+ * these genuinely run concurrently (not just dispatched together then
55
+ * queued), verified with two simultaneous ~200-word generations that both
56
+ * completed correctly in the same ~16s window.
57
+ *
58
+ * macOS only: opening additional windows uses the "New Window" menu command
59
+ * via System Events, which requires Accessibility permission (System
60
+ * Settings > Privacy & Security > Accessibility) for whatever process is
61
+ * running this. Don't mix this with a concurrent single delegateToAntigravity
62
+ * call - window allocation between the two isn't coordinated, so they could
63
+ * both grab the same window.
64
+ */
65
+ export async function delegateToAntigravityBatch(tasks) {
66
+ if (tasks.length === 0)
67
+ return [];
68
+ await ensureAntigravityReady();
69
+ const targets = await ensureWindowCount(tasks.length);
70
+ return Promise.all(tasks.map((opts, i) => runOnTarget(targets[i], opts)));
71
+ }
72
+ async function runOnTarget(target, opts) {
73
+ const { task, model, timeoutMs = 120_000 } = opts;
74
+ const session = new CdpSession(target.webSocketDebuggerUrl);
75
+ try {
76
+ // The CDP debug port opens before Antigravity's own UI finishes mounting
77
+ // (its window loads content from a local server that itself needs a
78
+ // moment to boot). On a cold start this can leave the chat input
79
+ // missing for several seconds after we've already connected.
80
+ await waitForChatReady(session);
81
+ await startNewConversation(session);
82
+ if (model)
83
+ await selectModel(session, model);
84
+ await pasteAndSend(session, task);
85
+ const reply = await waitForReply(session, task, timeoutMs);
86
+ const conversationId = await getConversationId(session);
87
+ const localFilesDir = conversationId ? resolveBrainDir(conversationId) : null;
88
+ return { reply, conversationId, localFilesDir };
89
+ }
90
+ finally {
91
+ session.close();
92
+ }
93
+ }
94
+ function openNewAntigravityWindow() {
95
+ try {
96
+ execFileSync("osascript", [
97
+ "-e",
98
+ 'tell application "System Events" to tell process "Antigravity" to click menu item "New Window" of menu "File" of menu bar 1',
99
+ ]);
100
+ return;
101
+ }
102
+ catch (err) {
103
+ // With zero windows open, System Events can't find "Antigravity" as an
104
+ // addressable process at all (confirmed directly - error -600, "app is
105
+ // not open" - even though the underlying process is genuinely running).
106
+ // macOS's normal app-reopen convention handles this: relaunching an
107
+ // already-running, windowless app creates a fresh window instead of
108
+ // just focusing an existing one.
109
+ try {
110
+ execFileSync("open", ["-a", resolveAppPath()]);
111
+ return;
112
+ }
113
+ catch {
114
+ throw new Error(`Could not open a new Antigravity window (needed to run tasks in parallel): ${err.message}. ` +
115
+ "If Antigravity has at least one window open, this needs Accessibility permission - grant it in " +
116
+ "System Settings > Privacy & Security > Accessibility for whatever is running this process. macOS only.");
117
+ }
118
+ }
119
+ }
120
+ async function ensureWindowCount(n) {
121
+ // No initial wait-for-a-window here: unlike getMainPageTarget() (used right
122
+ // after launching the app, where a window is guaranteed to appear on its
123
+ // own), this can run against an already-running app with zero windows open
124
+ // (user closed them all) - nothing creates that first window except us.
125
+ let pageTargets = (await fetchTargets()).filter(isMainPage);
126
+ while (pageTargets.length < n) {
127
+ const beforeCount = pageTargets.length;
128
+ openNewAntigravityWindow();
129
+ // wait for a genuinely new target to register, not a fixed guess at timing
130
+ await waitFor(async () => (await fetchTargets()).filter(isMainPage).length > beforeCount, 15_000);
131
+ pageTargets = (await fetchTargets()).filter(isMainPage);
132
+ }
133
+ const targets = pageTargets.slice(0, n);
134
+ // A freshly opened window can take longer to mount its chat UI than any
135
+ // fixed delay reliably covers (this is exactly what broke on the first
136
+ // real test - the 3rd window still wasn't ready 45s in). Confirm every
137
+ // window individually before handing any of them out.
138
+ await Promise.all(targets.map(async (target) => {
50
139
  const session = new CdpSession(target.webSocketDebuggerUrl);
51
140
  try {
52
- // The CDP debug port opens before Antigravity's own UI finishes mounting
53
- // (its window loads content from a local server that itself needs a
54
- // moment to boot). On a cold start this can leave the chat input
55
- // missing for several seconds after we've already connected.
56
141
  await waitForChatReady(session);
57
- await startNewConversation(session);
58
- if (model)
59
- await selectModel(session, model);
60
- await pasteAndSend(session, task);
61
- return await waitForReply(session, task, timeoutMs);
62
142
  }
63
143
  finally {
64
144
  session.close();
65
145
  }
66
- });
146
+ }));
147
+ return targets;
148
+ }
149
+ async function getConversationId(session) {
150
+ const href = await session.evaluate("location.href");
151
+ const match = href.match(/\/c\/([a-f0-9-]{36})/i);
152
+ return match ? match[1] : null;
153
+ }
154
+ // Antigravity (built on the Gemini CLI/Windsurf lineage) stores every
155
+ // conversation's generated files under ~/.gemini/antigravity/brain/<id>/ on
156
+ // disk - confirmed directly, not documented anywhere. This is real
157
+ // filesystem storage, separate from the local HTTPS server that serves it to
158
+ // the UI, so it's readable without going through that server at all.
159
+ // os.homedir() resolves per-user at runtime - not specific to any one machine.
160
+ export function getAntigravityBrainDir() {
161
+ return path.join(os.homedir(), ".gemini", "antigravity", "brain");
162
+ }
163
+ function resolveBrainDir(conversationId) {
164
+ const dir = path.join(getAntigravityBrainDir(), conversationId);
165
+ return fs.existsSync(dir) ? dir : null;
166
+ }
167
+ /**
168
+ * Every file Antigravity has ever generated, across every conversation, most
169
+ * recent first - not scoped to any single delegate_to_antigravity call.
170
+ */
171
+ export function listAntigravityFiles(limit = 20) {
172
+ const brainDir = getAntigravityBrainDir();
173
+ if (!fs.existsSync(brainDir))
174
+ return [];
175
+ const results = [];
176
+ for (const conversationId of fs.readdirSync(brainDir)) {
177
+ const convDir = path.join(brainDir, conversationId);
178
+ let entries;
179
+ try {
180
+ entries = fs.readdirSync(convDir, { withFileTypes: true });
181
+ }
182
+ catch {
183
+ continue;
184
+ }
185
+ for (const entry of entries) {
186
+ // skip Antigravity's own internal bookkeeping (.system_generated, etc.)
187
+ if (entry.name.startsWith(".") || !entry.isFile())
188
+ continue;
189
+ const filePath = path.join(convDir, entry.name);
190
+ const mtimeMs = fs.statSync(filePath).mtimeMs;
191
+ results.push({
192
+ path: filePath,
193
+ conversationId,
194
+ filename: entry.name,
195
+ modifiedAt: new Date(mtimeMs).toISOString(),
196
+ });
197
+ }
198
+ }
199
+ results.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
200
+ return results.slice(0, limit);
67
201
  }
68
202
  export async function getAntigravityQuota() {
69
203
  return withLock(async () => {
@@ -2,7 +2,7 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { AVAILABLE_MODELS, MODEL_GUIDE, delegateToAntigravity, getAntigravityQuota } from "./antigravity-client.js";
5
+ import { AVAILABLE_MODELS, MODEL_GUIDE, delegateToAntigravity, delegateToAntigravityBatch, getAntigravityQuota, listAntigravityFiles, } from "./antigravity-client.js";
6
6
  const server = new McpServer({
7
7
  name: "supergravity-mcp",
8
8
  version: "0.1.0",
@@ -12,7 +12,9 @@ server.registerTool("delegate_to_antigravity", {
12
12
  description: "Send a task to the Google Antigravity desktop app and return its reply. " +
13
13
  "Drives the real, already-logged-in app UI (types into its chat box and reads the response) — " +
14
14
  "does not touch Antigravity's internals or credentials. Requires Antigravity.app to be installed " +
15
- "and signed in on this machine.",
15
+ "and signed in on this machine. If the task produces files (generated images, edited code, etc.), " +
16
+ "the response includes a local filesystem path where they actually live — check that path directly " +
17
+ "instead of trying to fetch anything through Antigravity's UI or local server.",
16
18
  inputSchema: {
17
19
  task: z.string().describe("The task, question, or instruction to send to Antigravity."),
18
20
  model: z
@@ -29,8 +31,11 @@ server.registerTool("delegate_to_antigravity", {
29
31
  },
30
32
  }, async ({ task, model, timeoutMs }) => {
31
33
  try {
32
- const reply = await delegateToAntigravity({ task, model, timeoutMs });
33
- return { content: [{ type: "text", text: reply }] };
34
+ const result = await delegateToAntigravity({ task, model, timeoutMs });
35
+ const text = result.localFilesDir
36
+ ? `${result.reply}\n\n(Any files this produced are on disk at: ${result.localFilesDir})`
37
+ : result.reply;
38
+ return { content: [{ type: "text", text }] };
34
39
  }
35
40
  catch (err) {
36
41
  return {
@@ -39,6 +44,46 @@ server.registerTool("delegate_to_antigravity", {
39
44
  };
40
45
  }
41
46
  });
47
+ server.registerTool("delegate_to_antigravity_batch", {
48
+ title: "Delegate multiple tasks to Antigravity in parallel",
49
+ description: "Run several tasks at once, each in its own Antigravity window - genuinely concurrent, not queued " +
50
+ "one after another (verified: two simultaneous ~200-word generations both completed correctly in the " +
51
+ "same ~16s window). Use this instead of multiple delegate_to_antigravity calls when tasks are " +
52
+ "independent of each other (e.g. separate branches, unrelated questions). macOS only: opening extra " +
53
+ "windows needs Accessibility permission (System Settings > Privacy & Security > Accessibility) for " +
54
+ "whatever is running this. Don't call this at the same time as a separate delegate_to_antigravity call - " +
55
+ "window allocation between them isn't coordinated.",
56
+ inputSchema: {
57
+ tasks: z
58
+ .array(z.object({
59
+ task: z.string().describe("The task, question, or instruction to send to Antigravity."),
60
+ model: z
61
+ .enum(AVAILABLE_MODELS)
62
+ .optional()
63
+ .describe("Which model this specific task should use. If omitted, uses whatever is currently selected."),
64
+ timeoutMs: z.number().int().positive().optional().describe("Max time to wait for this task's reply."),
65
+ }))
66
+ .min(1)
67
+ .describe("One entry per task, each running in its own window at the same time."),
68
+ },
69
+ }, async ({ tasks }) => {
70
+ try {
71
+ const results = await delegateToAntigravityBatch(tasks);
72
+ const text = results
73
+ .map((r, i) => {
74
+ const files = r.localFilesDir ? `\n(Files on disk at: ${r.localFilesDir})` : "";
75
+ return `--- Task ${i + 1} ---\n${r.reply}${files}`;
76
+ })
77
+ .join("\n\n");
78
+ return { content: [{ type: "text", text }] };
79
+ }
80
+ catch (err) {
81
+ return {
82
+ content: [{ type: "text", text: `Batch delegation failed: ${err.message}` }],
83
+ isError: true,
84
+ };
85
+ }
86
+ });
42
87
  server.registerTool("get_antigravity_quota", {
43
88
  title: "Get Antigravity quota",
44
89
  description: "Read Antigravity's remaining model quota from its Settings > Models panel. " +
@@ -58,5 +103,25 @@ server.registerTool("get_antigravity_quota", {
58
103
  };
59
104
  }
60
105
  });
106
+ server.registerTool("list_antigravity_files", {
107
+ title: "List Antigravity-generated files",
108
+ description: "List files Antigravity has generated (images, documents, etc.), most recent first, across ALL " +
109
+ "conversations - not just the most recent delegate_to_antigravity call. Use this to find something " +
110
+ "generated earlier in this session or in a past one. Returns real filesystem paths, readable directly.",
111
+ inputSchema: {
112
+ limit: z.number().int().positive().optional().describe("Max number of files to return. Default 20."),
113
+ },
114
+ }, async ({ limit }) => {
115
+ try {
116
+ const files = listAntigravityFiles(limit);
117
+ return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
118
+ }
119
+ catch (err) {
120
+ return {
121
+ content: [{ type: "text", text: `Could not list Antigravity files: ${err.message}` }],
122
+ isError: true,
123
+ };
124
+ }
125
+ });
61
126
  const transport = new StdioServerTransport();
62
127
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supergravity-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "🚀 MCP server that lets Claude (or any MCP client) delegate tasks to Google's Antigravity desktop app 🌌. Built for Intel Macs 💻 that Antigravity's own CLI doesn't support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,15 +1,14 @@
1
1
  ---
2
2
  name: delegate
3
- description: Use when the user wants to delegate a task to Google's Antigravity desktop app — phrases like "use gemini", "ask antigravity", "have claude in antigravity do this", "offload this", or "check antigravity's quota". Explains which model to pick and how to handle a disabled-send-button error. Requires the supergravity MCP server (delegate_to_antigravity, get_antigravity_quota tools) to be connected.
3
+ description: Use when the user wants to delegate a task to Google's Antigravity desktop app — phrases like "use gemini", "ask antigravity", "have claude in antigravity do this", "offload this", "run N agents/tasks in parallel", or "check antigravity's quota". Explains which model to pick, when to use the batch/parallel tool, and how to handle a disabled-send-button error. Requires the supergravity MCP server (delegate_to_antigravity, delegate_to_antigravity_batch, get_antigravity_quota, list_antigravity_files tools) to be connected.
4
4
  ---
5
5
 
6
6
  # delegate
7
7
 
8
8
  Antigravity is a separate AI desktop app (Google's). This skill runs tasks
9
- through it via the `supergravity` MCP server's `delegate_to_antigravity` /
10
- `get_antigravity_quota` tools, instead of answering yourself useful when
11
- the user explicitly asks to use it, wants a second opinion from a different
12
- model family, or wants to offload work.
9
+ through it via the `supergravity` MCP server's tools, instead of answering
10
+ yourself useful when the user explicitly asks to use it, wants a second
11
+ opinion from a different model family, or wants to offload work.
13
12
 
14
13
  ## When to trigger
15
14
 
@@ -76,6 +75,38 @@ trusting prose. Only fall back to the reply text for context Antigravity
76
75
  wouldn't have written to disk (its reasoning, or a plain Q&A task with no
77
76
  files involved).
78
77
 
78
+ ## Generated files (images, artifacts) - same idea
79
+
80
+ If the reply mentions it made an image or other file, the response also
81
+ includes a real filesystem path (`~/.gemini/antigravity/brain/<conversation
82
+ id>/`) — read the file directly from there rather than trying to fetch it
83
+ through Antigravity's UI or local server. That directory can occasionally
84
+ hold files from an earlier conversation too (Antigravity sometimes reuses a
85
+ conversation ID) — if more than one file is present, each filename ends in
86
+ a millisecond timestamp, so sort by that (or file mtime) to find the one
87
+ this task actually produced.
88
+
89
+ ## Running multiple tasks at once
90
+
91
+ If the user wants several independent things done at the same time ("spin up
92
+ 3 agents to work on different branches", "ask it these 5 questions"), use
93
+ `delegate_to_antigravity_batch` with one entry per task instead of calling
94
+ `delegate_to_antigravity` several times. This genuinely runs them
95
+ concurrently, each in its own Antigravity window - confirmed with real
96
+ simultaneous multi-paragraph generations that both completed correctly in
97
+ the same ~16s window, not queued one after another.
98
+
99
+ Only use batch when the tasks are actually independent of each other (they
100
+ can't see or depend on each other's output, since they're separate
101
+ conversations). For a multi-step task where step 2 needs step 1's result,
102
+ use sequential `delegate_to_antigravity` calls instead.
103
+
104
+ macOS only - opening extra windows needs Accessibility permission for
105
+ whatever process is running this (System Settings > Privacy & Security >
106
+ Accessibility). If that's missing, the error says so directly. Don't run a
107
+ batch call and a single `delegate_to_antigravity` call at the same time -
108
+ they don't coordinate which window they're using.
109
+
79
110
  ## Other failure modes
80
111
 
81
112
  - Antigravity not installed / not found: the error will say so — tell the