supergravity-mcp 0.1.3 → 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 +32 -0
- package/dist/antigravity-client.js +213 -22
- package/dist/mcp-server.js +69 -4
- package/package.json +1 -1
- package/skills/delegate/SKILL.md +65 -2
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";
|
|
@@ -32,34 +32,187 @@ GPT-OSS 120B (~$0.03-0.09/$0.10-0.40 — far cheaper than the rest) — open-wei
|
|
|
32
32
|
|
|
33
33
|
Gemini models and Claude+GPT models draw from SEPARATE Antigravity quota pools — call getAntigravityQuota() first if unsure which pool has room.
|
|
34
34
|
`.trim();
|
|
35
|
+
// Antigravity is a single window shared by every call. Two delegations
|
|
36
|
+
// running at once would type into and read from the same conversation,
|
|
37
|
+
// corrupting both. Every entry point below is serialized through this lock
|
|
38
|
+
// so calls queue up instead of racing each other.
|
|
39
|
+
let lock = Promise.resolve();
|
|
40
|
+
function withLock(fn) {
|
|
41
|
+
const run = lock.then(fn, fn);
|
|
42
|
+
lock = run.then(() => undefined, () => undefined);
|
|
43
|
+
return run;
|
|
44
|
+
}
|
|
35
45
|
export async function delegateToAntigravity(opts) {
|
|
36
|
-
|
|
46
|
+
return withLock(async () => {
|
|
47
|
+
await ensureAntigravityReady();
|
|
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 [];
|
|
37
68
|
await ensureAntigravityReady();
|
|
38
|
-
const
|
|
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;
|
|
39
74
|
const session = new CdpSession(target.webSocketDebuggerUrl);
|
|
40
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);
|
|
41
81
|
await startNewConversation(session);
|
|
42
82
|
if (model)
|
|
43
83
|
await selectModel(session, model);
|
|
44
84
|
await pasteAndSend(session, task);
|
|
45
|
-
|
|
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 };
|
|
46
89
|
}
|
|
47
90
|
finally {
|
|
48
91
|
session.close();
|
|
49
92
|
}
|
|
50
93
|
}
|
|
51
|
-
|
|
52
|
-
await ensureAntigravityReady();
|
|
53
|
-
const target = await getMainPageTarget();
|
|
54
|
-
const session = new CdpSession(target.webSocketDebuggerUrl);
|
|
94
|
+
function openNewAntigravityWindow() {
|
|
55
95
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
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;
|
|
58
101
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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) => {
|
|
139
|
+
const session = new CdpSession(target.webSocketDebuggerUrl);
|
|
140
|
+
try {
|
|
141
|
+
await waitForChatReady(session);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
session.close();
|
|
145
|
+
}
|
|
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
|
+
}
|
|
62
198
|
}
|
|
199
|
+
results.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
|
|
200
|
+
return results.slice(0, limit);
|
|
201
|
+
}
|
|
202
|
+
export async function getAntigravityQuota() {
|
|
203
|
+
return withLock(async () => {
|
|
204
|
+
await ensureAntigravityReady();
|
|
205
|
+
const target = await getMainPageTarget();
|
|
206
|
+
const session = new CdpSession(target.webSocketDebuggerUrl);
|
|
207
|
+
try {
|
|
208
|
+
const raw = await openSettingsModelsTab(session);
|
|
209
|
+
return parseQuotaText(raw);
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
await closeSettings(session);
|
|
213
|
+
session.close();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
63
216
|
}
|
|
64
217
|
async function openSettingsModelsTab(session) {
|
|
65
218
|
await session.evaluate(`
|
|
@@ -258,6 +411,26 @@ class CdpSession {
|
|
|
258
411
|
}
|
|
259
412
|
}
|
|
260
413
|
// ---------- UI actions (selectors verified against Antigravity 2.2.1) ----------
|
|
414
|
+
async function waitForChatReady(session, timeoutMs = 45_000) {
|
|
415
|
+
const start = Date.now();
|
|
416
|
+
while (Date.now() - start < timeoutMs) {
|
|
417
|
+
const state = await session.evaluate(`
|
|
418
|
+
(() => {
|
|
419
|
+
if (document.querySelector('[contenteditable="true"][aria-label="Message input"]')) return 'ready';
|
|
420
|
+
const text = document.body.innerText || '';
|
|
421
|
+
if (text.includes('Continue with Google') || text.includes('Welcome to Antigravity')) return 'needs-signin';
|
|
422
|
+
return 'loading';
|
|
423
|
+
})()
|
|
424
|
+
`);
|
|
425
|
+
if (state === "ready")
|
|
426
|
+
return;
|
|
427
|
+
if (state === "needs-signin") {
|
|
428
|
+
throw new Error("Antigravity needs you to sign in. Open the app and click \"Continue with Google\", then try again — this can't be automated (it's your Google login).");
|
|
429
|
+
}
|
|
430
|
+
await sleep(500);
|
|
431
|
+
}
|
|
432
|
+
throw new Error(`Antigravity's chat UI didn't finish loading within ${timeoutMs}ms of launch. It may still be starting up — try again in a few seconds.`);
|
|
433
|
+
}
|
|
261
434
|
async function startNewConversation(session) {
|
|
262
435
|
await session.evaluate(`
|
|
263
436
|
(() => {
|
|
@@ -377,10 +550,15 @@ async function waitForReply(session, taskText, timeoutMs) {
|
|
|
377
550
|
continue;
|
|
378
551
|
if (threadText === lastText) {
|
|
379
552
|
stableCount++;
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
553
|
+
// Two consecutive stable reads plus actual reply content (not just our
|
|
554
|
+
// own sent message sitting there) means it's done. "Thought for Ns" is
|
|
555
|
+
// NOT a reliable signal — Antigravity skips it entirely for fast/simple
|
|
556
|
+
// answers, so requiring it caused real replies to be missed until
|
|
557
|
+
// timeout even though the answer was already on screen.
|
|
558
|
+
if (stableCount >= 2) {
|
|
559
|
+
const candidate = extractReply(threadText, taskText);
|
|
560
|
+
if (candidate)
|
|
561
|
+
return candidate;
|
|
384
562
|
}
|
|
385
563
|
}
|
|
386
564
|
else {
|
|
@@ -391,13 +569,26 @@ async function waitForReply(session, taskText, timeoutMs) {
|
|
|
391
569
|
throw new Error(`Timed out after ${timeoutMs}ms waiting for Antigravity's reply.`);
|
|
392
570
|
}
|
|
393
571
|
function extractReply(threadText, taskText) {
|
|
572
|
+
// We only search on the first 200 chars (waitForReply's DOM lookup does the
|
|
573
|
+
// same, to keep every poll's evaluate() call cheap on huge tasks) - but
|
|
574
|
+
// slicing by that same truncated length left a tail of our own message
|
|
575
|
+
// behind for anything longer than 200 chars. Instead, find where OUR
|
|
576
|
+
// message actually ends by locating the timestamp Antigravity renders
|
|
577
|
+
// immediately after it, which appears regardless of message length.
|
|
394
578
|
const needle = taskText.slice(0, 200);
|
|
395
579
|
const idx = threadText.lastIndexOf(needle);
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
let
|
|
399
|
-
|
|
400
|
-
|
|
580
|
+
const searchFrom = idx >= 0 ? idx + needle.length : 0;
|
|
581
|
+
const closingTimestamp = threadText.slice(searchFrom).match(/\d{1,2}:\d{2}/);
|
|
582
|
+
let after = closingTimestamp
|
|
583
|
+
? threadText.slice(searchFrom + closingTimestamp.index + closingTimestamp[0].length)
|
|
584
|
+
: threadText.slice(searchFrom);
|
|
585
|
+
// strip the "Thought for Ns" line when present - not always there
|
|
586
|
+
const thoughtMatch = after.match(/^\n*Thought for[^\n]*\n+/);
|
|
587
|
+
if (thoughtMatch)
|
|
588
|
+
after = after.slice(thoughtMatch[0].length);
|
|
589
|
+
// strip the trailing timestamp under the reply itself
|
|
590
|
+
after = after.replace(/\n+\d{1,2}:\d{2}\s*$/, "");
|
|
591
|
+
return after.trim();
|
|
401
592
|
}
|
|
402
593
|
function sleep(ms) {
|
|
403
594
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
|
33
|
-
|
|
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.
|
|
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": {
|
package/skills/delegate/SKILL.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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
|
|
9
|
+
through it via the `supergravity` MCP server's tools, instead of answering
|
|
10
10
|
yourself — useful when the user explicitly asks to use it, wants a second
|
|
11
11
|
opinion from a different model family, or wants to offload work.
|
|
12
12
|
|
|
@@ -18,6 +18,21 @@ Any explicit ask to use Antigravity or one of the models it hosts:
|
|
|
18
18
|
generic requests that don't name Antigravity or one of its models — just
|
|
19
19
|
answer normally.
|
|
20
20
|
|
|
21
|
+
Load this skill *before* calling either tool the first time in a
|
|
22
|
+
conversation, not after something already went wrong — the model-picking and
|
|
23
|
+
quota-pool guidance below changes which tool call you should even make.
|
|
24
|
+
|
|
25
|
+
## You don't need to check if Antigravity is running
|
|
26
|
+
|
|
27
|
+
`delegate_to_antigravity` launches Antigravity itself if it isn't already
|
|
28
|
+
open, and waits for its chat UI to finish loading before typing anything —
|
|
29
|
+
you don't need to check first or tell the user to open it. Just call the
|
|
30
|
+
tool. The only implication: the very first call in a session can take
|
|
31
|
+
15-30+ seconds longer than normal (cold app launch) before anything visible
|
|
32
|
+
happens. If a call fails specifically because the app couldn't be found at
|
|
33
|
+
all, the error names the exact env var (`ANTIGRAVITY_APP_PATH`) to fix it —
|
|
34
|
+
that's a real install problem, not something to retry.
|
|
35
|
+
|
|
21
36
|
## Picking a model
|
|
22
37
|
|
|
23
38
|
If the user names a specific model ("use claude opus", "use gemini flash"),
|
|
@@ -44,6 +59,54 @@ until it refreshes — don't retry the same model. Instead:
|
|
|
44
59
|
3. Otherwise, tell the user which pool is out and when it refreshes — don't
|
|
45
60
|
silently fall back without saying so if they asked for a specific model.
|
|
46
61
|
|
|
62
|
+
## Code-editing tasks: verify with the filesystem, not the reply text
|
|
63
|
+
|
|
64
|
+
If the task involves editing code, Antigravity works inside a real project
|
|
65
|
+
folder on disk (its "Projects" sidebar is literal directories, same
|
|
66
|
+
filesystem you already have access to) — it actually writes the files, it
|
|
67
|
+
doesn't just describe changes in chat. Its reply text is a narrative summary
|
|
68
|
+
written by the model, not the deliverable, and can be vague or incomplete
|
|
69
|
+
("Updated the function as requested").
|
|
70
|
+
|
|
71
|
+
So after a code-editing delegation, don't parse the reply for specifics —
|
|
72
|
+
go check the real files yourself: `git diff`, `git status`, or just read the
|
|
73
|
+
file. You have direct filesystem access on this machine; use it instead of
|
|
74
|
+
trusting prose. Only fall back to the reply text for context Antigravity
|
|
75
|
+
wouldn't have written to disk (its reasoning, or a plain Q&A task with no
|
|
76
|
+
files involved).
|
|
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
|
+
|
|
47
110
|
## Other failure modes
|
|
48
111
|
|
|
49
112
|
- Antigravity not installed / not found: the error will say so — tell the
|