supergravity-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rice
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # supergravity-mcp
2
+
3
+ An MCP server that lets Claude Code (or any MCP client) delegate tasks to the
4
+ **Google Antigravity** desktop app — by driving its real, already-logged-in UI.
5
+ No reverse engineering, no extracted credentials, no unofficial API calls.
6
+
7
+ ## Why this exists
8
+
9
+ Antigravity ships a CLI (`agy`) for scripting it headlessly — but on older
10
+ Intel Macs, that CLI isn't available even though the desktop **app** runs fine
11
+ (confirmed working on a 2015 MacBook Pro, macOS Monterey, Intel). This project
12
+ is the missing bridge for anyone in that situation: it automates the app the
13
+ same way a human would use it, so it works anywhere the app itself works.
14
+
15
+ ## How it works
16
+
17
+ 1. Antigravity is relaunched with Chrome's remote-debugging flag
18
+ (`--remote-debugging-port`), the same mechanism browser test tools like
19
+ Playwright use to drive Electron apps.
20
+ 2. A script connects to that debug port and: types your task into the chat
21
+ box (via a simulated paste event — Antigravity's input is a rich-text
22
+ editor, not a plain textarea, so it needs a real paste, not just poking
23
+ text into the DOM), clicks send, and reads back the reply once it's done
24
+ streaming.
25
+ 3. Everything happens through your own existing login. This tool never
26
+ touches Antigravity's stored credentials, internal code, or private APIs.
27
+
28
+ ## Setup
29
+
30
+ ```bash
31
+ npm install
32
+ npm run build
33
+ ```
34
+
35
+ Antigravity must be installed and already signed in. By default the tool
36
+ looks for it at `/Applications/Antigravity.app` or `~/Desktop/Antigravity.app`;
37
+ set `ANTIGRAVITY_APP_PATH` if it lives somewhere else.
38
+
39
+ Register it with Claude Code:
40
+
41
+ ```bash
42
+ claude mcp add supergravity -- node /absolute/path/to/dist/mcp-server.js
43
+ ```
44
+
45
+ ## The tools
46
+
47
+ ### `delegate_to_antigravity(task, model?, timeoutMs?)`
48
+
49
+ - `task` — what to ask Antigravity to do.
50
+ - `model` — optional. One of the models in Antigravity's own selector
51
+ (see `AVAILABLE_MODELS` in `src/antigravity-client.ts`).
52
+ - `timeoutMs` — optional, default 120000 (2 minutes).
53
+
54
+ ### `get_antigravity_quota()`
55
+
56
+ Reads Antigravity's Settings > Models panel and returns remaining quota —
57
+ Gemini models and Claude+GPT models draw from **separate pools**, each with a
58
+ weekly limit and a 5-hour limit. Useful to check before delegating, or to
59
+ explain a `send button is disabled` error (usually means that pool is
60
+ exhausted, not that the model is unavailable).
61
+
62
+ ### Picking a model
63
+
64
+ Antigravity doesn't show per-message token cost, only pooled quota %. The
65
+ `model` parameter's description carries a rough cost/capability cheat sheet
66
+ (`MODEL_GUIDE` in `src/antigravity-client.ts`) built from third-party API
67
+ list prices and public benchmarks, researched mid-2026 — newer than this
68
+ tool's own training data, so treat it as a starting point, not gospel:
69
+
70
+ | Model | ~Price (in/out per 1M tokens) | Known for |
71
+ |---|---|---|
72
+ | Gemini 3.5 Flash | $1.50 / $9 | Fastest, cheapest frontier-tier option; strong coding/agentic benchmarks |
73
+ | Gemini 3.1 Pro | $2 / $12 | "Thinking" mode for harder reasoning (ARC-AGI-2 ~77%) |
74
+ | Claude Sonnet 4.6 | $3 / $15 | Coding quality, computer-use, agent workflows (SWE-bench ~80%) |
75
+ | Claude Opus 4.6 | $5 / $25 | Strongest complex/ambiguous reasoning; legal/financial/research-grade analysis |
76
+ | GPT-OSS 120B | ~$0.03–0.09 / $0.10–0.40 | Open-weight, near-o4-mini reasoning, far cheaper than the rest |
77
+
78
+ These are what the underlying models cost via their own APIs elsewhere —
79
+ Antigravity itself doesn't bill you per token, it draws down the pooled
80
+ quota above instead.
81
+
82
+ ## Known limitations
83
+
84
+ - **A model's quota can run out.** If a non-default model (e.g. Claude Opus)
85
+ has hit its usage limit, Antigravity's send button stays disabled after
86
+ selecting it, even though it's still pickable in the dropdown. The tool
87
+ fails fast with a clear error in that case instead of hanging until
88
+ timeout — just retry later or pick a different model.
89
+ - **UI automation, not an API.** If Google changes Antigravity's screen
90
+ layout, the CSS selectors this relies on (`data-testid="send-button"`, the
91
+ message input's `aria-label`, etc.) may need updating. That's the tradeoff
92
+ for staying entirely inside Antigravity's terms of use — see below.
93
+ - **Reply extraction is a text heuristic**, not a structured read. It looks
94
+ for the "Thought for Ns" marker Antigravity renders before its answer and
95
+ takes what follows. Works reliably in testing but is the most likely thing
96
+ to break on a UI redesign.
97
+ - **Single-window assumption.** Only tested with one Antigravity window open.
98
+
99
+ ## What this deliberately does NOT do
100
+
101
+ No unpacking of Antigravity's `app.asar`, no reading of stored auth tokens,
102
+ no calling of internal/undocumented endpoints. Everything here operates
103
+ through the same interface a human uses, which is why it doesn't carry the
104
+ legal risk that reverse-engineering approaches do.
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,404 @@
1
+ import { execSync, spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ const CDP_PORT = Number(process.env.ANTIGRAVITY_CDP_PORT || 9222);
6
+ // All of these are selectable in Antigravity's model dropdown. If a model's
7
+ // quota is exhausted, the send button stays disabled after selecting it —
8
+ // selectModel() / pasteAndSend() fail fast with a clear error in that case
9
+ // rather than hanging until timeout. Retry later or pick a different model.
10
+ export const AVAILABLE_MODELS = [
11
+ "Gemini 3.5 Flash (Low)",
12
+ "Gemini 3.5 Flash (Medium)",
13
+ "Gemini 3.5 Flash (High)",
14
+ "Gemini 3.1 Pro (Low)",
15
+ "Gemini 3.1 Pro (High)",
16
+ "Claude Sonnet 4.6 (Thinking)",
17
+ "Claude Opus 4.6 (Thinking)",
18
+ "GPT-OSS 120B (Medium)",
19
+ ];
20
+ // Antigravity itself bills via a pooled weekly/5-hour quota, not per token —
21
+ // see getAntigravityQuota(). These are third-party API list prices (per 1M
22
+ // tokens, researched mid-2026, newer than this tool author's model training
23
+ // data) included only as a relative cost/capability signal for routing
24
+ // decisions, not what you're actually billed inside Antigravity. Verify
25
+ // independently before using this for real budgeting.
26
+ export const MODEL_GUIDE = `
27
+ Gemini 3.5 Flash ($1.50/$9 per 1M in/out) — fastest and cheapest of the group; strong coding/agentic benchmarks. Good default for quick, well-scoped tasks.
28
+ Gemini 3.1 Pro ($2/$12) — adds a "thinking" mode for harder reasoning (ARC-AGI-2 ~77%); costlier for long (>200K token) context.
29
+ Claude Sonnet 4.6 ($3/$15) — best-regarded for coding quality, computer-use, and multi-step agent workflows (SWE-bench ~80%).
30
+ Claude Opus 4.6 ($5/$25, higher in fast mode) — strongest at complex, ambiguous, multi-step reasoning; the pick for legal/financial/research-grade analysis.
31
+ GPT-OSS 120B (~$0.03-0.09/$0.10-0.40 — far cheaper than the rest) — open-weight, near-o4-mini reasoning, solid coding/tool-use. Good for high-volume or low-stakes tasks.
32
+
33
+ Gemini models and Claude+GPT models draw from SEPARATE Antigravity quota pools — call getAntigravityQuota() first if unsure which pool has room.
34
+ `.trim();
35
+ export async function delegateToAntigravity(opts) {
36
+ const { task, model, timeoutMs = 120_000 } = opts;
37
+ await ensureAntigravityReady();
38
+ const target = await getMainPageTarget();
39
+ const session = new CdpSession(target.webSocketDebuggerUrl);
40
+ try {
41
+ await startNewConversation(session);
42
+ if (model)
43
+ await selectModel(session, model);
44
+ await pasteAndSend(session, task);
45
+ return await waitForReply(session, task, timeoutMs);
46
+ }
47
+ finally {
48
+ session.close();
49
+ }
50
+ }
51
+ export async function getAntigravityQuota() {
52
+ await ensureAntigravityReady();
53
+ const target = await getMainPageTarget();
54
+ const session = new CdpSession(target.webSocketDebuggerUrl);
55
+ try {
56
+ const raw = await openSettingsModelsTab(session);
57
+ return parseQuotaText(raw);
58
+ }
59
+ finally {
60
+ await closeSettings(session);
61
+ session.close();
62
+ }
63
+ }
64
+ async function openSettingsModelsTab(session) {
65
+ await session.evaluate(`
66
+ (() => {
67
+ const b = Array.from(document.querySelectorAll('button')).find(x => x.innerText && x.innerText.trim() === 'Settings');
68
+ if (b) b.click();
69
+ return true;
70
+ })()
71
+ `);
72
+ await sleep(1000);
73
+ await session.evaluate(`
74
+ (() => {
75
+ const b = Array.from(document.querySelectorAll('button, [role="tab"]')).find(x => (x.innerText||'').trim() === 'Models');
76
+ if (b) b.click();
77
+ return true;
78
+ })()
79
+ `);
80
+ await sleep(800);
81
+ const raw = await session.evaluate(`
82
+ (() => {
83
+ const all = Array.from(document.querySelectorAll('*'));
84
+ const hit = all.find(el => el.innerText && el.innerText.includes('Model Quota') && el.innerText.includes('Weekly Limit'));
85
+ return hit ? hit.innerText : null;
86
+ })()
87
+ `);
88
+ if (!raw)
89
+ throw new Error("Could not find the Model Quota panel in Antigravity's Settings.");
90
+ return raw;
91
+ }
92
+ async function closeSettings(session) {
93
+ await session.evaluate(`
94
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true, cancelable: true }))
95
+ `);
96
+ await sleep(400);
97
+ }
98
+ function parseQuotaText(raw) {
99
+ const lines = raw
100
+ .split("\n")
101
+ .map((l) => l.trim())
102
+ .filter((l) => l.length > 0);
103
+ const planLine = lines.find((l) => l.startsWith("Your Plan:"));
104
+ const plan = planLine ? planLine.replace("Your Plan:", "").trim() : null;
105
+ const groups = [];
106
+ for (let i = 1; i < lines.length; i++) {
107
+ if (lines[i] !== "Weekly Limit")
108
+ continue;
109
+ const group = lines[i - 1];
110
+ let j = i + 1;
111
+ const weeklyStatusText = lines[j] ?? null;
112
+ j++;
113
+ const weeklyMatch = (lines[j] ?? "").match(/^(\d+)%$/);
114
+ const weeklyPercentRemaining = weeklyMatch ? Number(weeklyMatch[1]) : null;
115
+ if (weeklyMatch)
116
+ j++;
117
+ let fiveHourStatusText = null;
118
+ let fiveHourPercentRemaining = null;
119
+ if (lines[j] === "Five Hour Limit") {
120
+ j++;
121
+ fiveHourStatusText = lines[j] ?? null;
122
+ j++;
123
+ const fiveHourMatch = (lines[j] ?? "").match(/^(\d+)%$/);
124
+ if (fiveHourMatch)
125
+ fiveHourPercentRemaining = Number(fiveHourMatch[1]);
126
+ }
127
+ groups.push({ group, weeklyPercentRemaining, weeklyStatusText, fiveHourPercentRemaining, fiveHourStatusText });
128
+ }
129
+ return { plan, groups, raw };
130
+ }
131
+ // ---------- App lifecycle ----------
132
+ function resolveAppPath() {
133
+ const candidates = [
134
+ process.env.ANTIGRAVITY_APP_PATH,
135
+ "/Applications/Antigravity.app",
136
+ path.join(os.homedir(), "Desktop", "Antigravity.app"),
137
+ ].filter((p) => !!p);
138
+ for (const c of candidates) {
139
+ if (fs.existsSync(c))
140
+ return c;
141
+ }
142
+ throw new Error("Could not find Antigravity.app. Set the ANTIGRAVITY_APP_PATH environment variable to its full path.");
143
+ }
144
+ async function isCdpAvailable() {
145
+ try {
146
+ const res = await fetch(`http://localhost:${CDP_PORT}/json/version`);
147
+ return res.ok;
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ }
153
+ function isAppRunningWithoutDebugPort() {
154
+ try {
155
+ execSync('pgrep -f "Antigravity.app/Contents/MacOS/Antigravity"', { stdio: "ignore" });
156
+ return true;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ }
162
+ function quitApp() {
163
+ try {
164
+ execSync('osascript -e \'quit app "Antigravity"\'');
165
+ }
166
+ catch {
167
+ /* app may not have been running via a scriptable process */
168
+ }
169
+ try {
170
+ execSync('pkill -f "Antigravity.app/Contents/MacOS/Antigravity"');
171
+ }
172
+ catch {
173
+ /* already gone */
174
+ }
175
+ }
176
+ function launchWithDebugPort(appPath) {
177
+ const child = spawn("open", ["-a", appPath, "--args", `--remote-debugging-port=${CDP_PORT}`], {
178
+ detached: true,
179
+ stdio: "ignore",
180
+ });
181
+ child.unref();
182
+ }
183
+ async function waitFor(condition, timeoutMs, intervalMs = 500) {
184
+ const start = Date.now();
185
+ while (Date.now() - start < timeoutMs) {
186
+ if (await condition())
187
+ return;
188
+ await sleep(intervalMs);
189
+ }
190
+ throw new Error("Timed out waiting for Antigravity to become ready");
191
+ }
192
+ export async function ensureAntigravityReady() {
193
+ if (await isCdpAvailable())
194
+ return;
195
+ // Antigravity only opens the debug port if launched with the flag. If it's
196
+ // already running without it, we have to restart it to attach.
197
+ if (isAppRunningWithoutDebugPort()) {
198
+ quitApp();
199
+ await sleep(1500);
200
+ }
201
+ const appPath = resolveAppPath();
202
+ launchWithDebugPort(appPath);
203
+ await waitFor(isCdpAvailable, 30_000);
204
+ }
205
+ async function fetchTargets() {
206
+ const res = await fetch(`http://localhost:${CDP_PORT}/json`);
207
+ return (await res.json());
208
+ }
209
+ function isMainPage(t) {
210
+ // The loading splash is a data: URL; the real UI is served locally by
211
+ // Antigravity's own language-server process over https://127.0.0.1:<port>.
212
+ return t.type === "page" && t.url.startsWith("https://127.0.0.1:");
213
+ }
214
+ async function getMainPageTarget() {
215
+ await waitFor(async () => (await fetchTargets()).some(isMainPage), 20_000);
216
+ const targets = await fetchTargets();
217
+ const target = targets.find(isMainPage);
218
+ if (!target)
219
+ throw new Error("Antigravity's main window did not appear in time.");
220
+ return target;
221
+ }
222
+ class CdpSession {
223
+ ws;
224
+ nextId = 1;
225
+ pending = new Map();
226
+ ready;
227
+ constructor(wsUrl) {
228
+ this.ws = new WebSocket(wsUrl);
229
+ this.ready = new Promise((resolve, reject) => {
230
+ this.ws.addEventListener("open", () => resolve());
231
+ this.ws.addEventListener("error", () => reject(new Error("CDP WebSocket connection failed")));
232
+ });
233
+ this.ws.addEventListener("message", (event) => {
234
+ const msg = JSON.parse(event.data);
235
+ if (msg.id && this.pending.has(msg.id)) {
236
+ this.pending.get(msg.id)(msg.result);
237
+ this.pending.delete(msg.id);
238
+ }
239
+ });
240
+ }
241
+ async send(method, params = {}) {
242
+ await this.ready;
243
+ const id = this.nextId++;
244
+ return new Promise((resolve) => {
245
+ this.pending.set(id, resolve);
246
+ this.ws.send(JSON.stringify({ id, method, params }));
247
+ });
248
+ }
249
+ async evaluate(expression) {
250
+ const result = await this.send("Runtime.evaluate", { expression, returnByValue: true });
251
+ if (result?.exceptionDetails) {
252
+ throw new Error(`Antigravity page script error: ${JSON.stringify(result.exceptionDetails)}`);
253
+ }
254
+ return result?.result?.value;
255
+ }
256
+ close() {
257
+ this.ws.close();
258
+ }
259
+ }
260
+ // ---------- UI actions (selectors verified against Antigravity 2.2.1) ----------
261
+ async function startNewConversation(session) {
262
+ await session.evaluate(`
263
+ (() => {
264
+ const btns = Array.from(document.querySelectorAll('button'));
265
+ const b = btns.find(x => x.innerText && x.innerText.trim() === 'New Conversation');
266
+ if (b) b.click();
267
+ return true;
268
+ })()
269
+ `);
270
+ await sleep(1000);
271
+ }
272
+ async function selectModel(session, modelName) {
273
+ await session.evaluate(`
274
+ (() => {
275
+ const btns = Array.from(document.querySelectorAll('button'));
276
+ const b = btns.find(x => (x.getAttribute('aria-label')||'').startsWith('Select model'));
277
+ if (b) b.click();
278
+ return true;
279
+ })()
280
+ `);
281
+ await sleep(600);
282
+ const result = await session.evaluate(`
283
+ (() => {
284
+ const btns = Array.from(document.querySelectorAll('button'));
285
+ const target = btns.find(b => b.innerText && b.innerText.includes(${JSON.stringify(modelName)}) && b.className.includes('popover-item'));
286
+ if (!target) return { error: 'model option not found in dropdown' };
287
+ target.click();
288
+ return { clicked: target.innerText.slice(0, 60) };
289
+ })()
290
+ `);
291
+ if (result?.error) {
292
+ throw new Error(`Could not select model "${modelName}": ${result.error}`);
293
+ }
294
+ await sleep(500);
295
+ }
296
+ async function pasteAndSend(session, taskText) {
297
+ // Antigravity's message box is a Lexical rich-text editor, not a plain
298
+ // textarea. Setting its text directly doesn't update Lexical's internal
299
+ // state, so we simulate a real paste event instead — same as a human
300
+ // pasting text, which the editor is built to handle correctly.
301
+ //
302
+ // Paste inserts at the current selection rather than replacing the whole
303
+ // box, and "New Conversation" does NOT clear a pre-existing unsent draft
304
+ // (confirmed the hard way: a leftover draft silently glued itself onto a
305
+ // later task). So we explicitly select all existing content first —
306
+ // that makes the paste replace it instead of appending to it.
307
+ const pasteResult = await session.evaluate(`
308
+ (() => {
309
+ const editable = document.querySelector('[contenteditable="true"][aria-label="Message input"]');
310
+ if (!editable) return { error: 'message input not found' };
311
+ editable.focus();
312
+ const range = document.createRange();
313
+ range.selectNodeContents(editable);
314
+ const selection = window.getSelection();
315
+ selection.removeAllRanges();
316
+ selection.addRange(range);
317
+ const dt = new DataTransfer();
318
+ dt.setData('text/plain', ${JSON.stringify(taskText)});
319
+ const pasteEvent = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true });
320
+ editable.dispatchEvent(pasteEvent);
321
+ return { dispatched: true };
322
+ })()
323
+ `);
324
+ if (pasteResult?.error)
325
+ throw new Error(pasteResult.error);
326
+ await sleep(1000);
327
+ // Safety net for the exact bug above: if the box holds far more text than
328
+ // we just sent, something didn't get replaced. Refuse to send a message
329
+ // that isn't the one we were asked to send.
330
+ const landedLength = await session.evaluate(`
331
+ (() => {
332
+ const editable = document.querySelector('[contenteditable="true"][aria-label="Message input"]');
333
+ return editable ? editable.innerText.length : -1;
334
+ })()
335
+ `);
336
+ if (landedLength < 0)
337
+ throw new Error("message input disappeared before send");
338
+ if (landedLength > taskText.length + 1000) {
339
+ throw new Error(`Refusing to send: message box has ${landedLength} chars but the task was only ${taskText.length}. ` +
340
+ "The box likely still has leftover content that wasn't replaced.");
341
+ }
342
+ const clickResult = await session.evaluate(`
343
+ (() => {
344
+ const btn = document.querySelector('[data-testid="send-button"]');
345
+ if (!btn) return { error: 'send button not found' };
346
+ if (btn.disabled) return { error: 'send button is disabled' };
347
+ btn.click();
348
+ return { clicked: true };
349
+ })()
350
+ `);
351
+ if (clickResult?.error) {
352
+ // A disabled send button right after switching models usually means that
353
+ // model's quota is temporarily exhausted (observed with Claude Opus after
354
+ // heavy use — selectable in the dropdown, but send stays disabled until
355
+ // quota resets or you pick a different model).
356
+ throw new Error(`${clickResult.error}. If you just switched models, its quota may be temporarily exhausted — try again later or pick a different model.`);
357
+ }
358
+ }
359
+ async function waitForReply(session, taskText, timeoutMs) {
360
+ const needle = JSON.stringify(taskText.slice(0, 200));
361
+ const start = Date.now();
362
+ let lastText = "";
363
+ let stableCount = 0;
364
+ while (Date.now() - start < timeoutMs) {
365
+ await sleep(1200);
366
+ const threadText = await session.evaluate(`
367
+ (() => {
368
+ const all = Array.from(document.querySelectorAll('*'));
369
+ const hit = all.findLast(el => el.children.length === 0 && el.innerText && el.innerText.includes(${needle}));
370
+ if (!hit) return '';
371
+ let container = hit;
372
+ for (let i = 0; i < 14 && container.parentElement; i++) container = container.parentElement;
373
+ return container.innerText;
374
+ })()
375
+ `);
376
+ if (!threadText)
377
+ continue;
378
+ if (threadText === lastText) {
379
+ stableCount++;
380
+ // Require the "Thought for Ns" marker so we don't return early on the
381
+ // brief window right after sending, before Antigravity starts replying.
382
+ if (stableCount >= 2 && /Thought for/.test(threadText)) {
383
+ return extractReply(threadText, taskText);
384
+ }
385
+ }
386
+ else {
387
+ stableCount = 0;
388
+ lastText = threadText;
389
+ }
390
+ }
391
+ throw new Error(`Timed out after ${timeoutMs}ms waiting for Antigravity's reply.`);
392
+ }
393
+ function extractReply(threadText, taskText) {
394
+ const needle = taskText.slice(0, 200);
395
+ const idx = threadText.lastIndexOf(needle);
396
+ const after = idx >= 0 ? threadText.slice(idx) : threadText;
397
+ const thoughtMatch = after.match(/Thought for[^\n]*\n+/);
398
+ let rest = thoughtMatch ? after.slice(after.indexOf(thoughtMatch[0]) + thoughtMatch[0].length) : after;
399
+ rest = rest.replace(/\n+\d{1,2}:\d{2}\s*$/, "").trim();
400
+ return rest;
401
+ }
402
+ function sleep(ms) {
403
+ return new Promise((resolve) => setTimeout(resolve, ms));
404
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { AVAILABLE_MODELS, MODEL_GUIDE, delegateToAntigravity, getAntigravityQuota } from "./antigravity-client.js";
6
+ const server = new McpServer({
7
+ name: "supergravity-mcp",
8
+ version: "0.1.0",
9
+ });
10
+ server.registerTool("delegate_to_antigravity", {
11
+ title: "Delegate to Antigravity",
12
+ description: "Send a task to the Google Antigravity desktop app and return its reply. " +
13
+ "Drives the real, already-logged-in app UI (types into its chat box and reads the response) — " +
14
+ "does not touch Antigravity's internals or credentials. Requires Antigravity.app to be installed " +
15
+ "and signed in on this machine.",
16
+ inputSchema: {
17
+ task: z.string().describe("The task, question, or instruction to send to Antigravity."),
18
+ model: z
19
+ .enum(AVAILABLE_MODELS)
20
+ .optional()
21
+ .describe("Which model Antigravity should use. If omitted, uses whatever model is currently selected in the app.\n\n" +
22
+ MODEL_GUIDE),
23
+ timeoutMs: z
24
+ .number()
25
+ .int()
26
+ .positive()
27
+ .optional()
28
+ .describe("Max time to wait for a reply, in milliseconds. Default 120000 (2 minutes)."),
29
+ },
30
+ }, async ({ task, model, timeoutMs }) => {
31
+ try {
32
+ const reply = await delegateToAntigravity({ task, model, timeoutMs });
33
+ return { content: [{ type: "text", text: reply }] };
34
+ }
35
+ catch (err) {
36
+ return {
37
+ content: [{ type: "text", text: `Antigravity delegation failed: ${err.message}` }],
38
+ isError: true,
39
+ };
40
+ }
41
+ });
42
+ server.registerTool("get_antigravity_quota", {
43
+ title: "Get Antigravity quota",
44
+ description: "Read Antigravity's remaining model quota from its Settings > Models panel. " +
45
+ "Gemini models and Claude+GPT models draw from separate pools, each with a weekly limit and a " +
46
+ "5-hour limit. Check this before delegating to Claude/GPT-OSS models if a prior call failed with " +
47
+ "a 'send button is disabled' error, or to decide which model family has room before delegating.",
48
+ inputSchema: {},
49
+ }, async () => {
50
+ try {
51
+ const status = await getAntigravityQuota();
52
+ return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
53
+ }
54
+ catch (err) {
55
+ return {
56
+ content: [{ type: "text", text: `Could not read Antigravity quota: ${err.message}` }],
57
+ isError: true,
58
+ };
59
+ }
60
+ });
61
+ const transport = new StdioServerTransport();
62
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "supergravity-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server that lets Claude (or any MCP client) delegate tasks to the Google Antigravity desktop app by driving its real UI — no CLI, no reverse engineering. Built for Intel Macs Antigravity's own CLI doesn't support.",
5
+ "type": "module",
6
+ "bin": {
7
+ "supergravity-mcp": "dist/mcp-server.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "start": "node dist/mcp-server.js"
15
+ },
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/presidentrice/supergravity-mcp.git"
20
+ },
21
+ "homepage": "https://github.com/presidentrice/supergravity-mcp",
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.29.0",
24
+ "zod": "^4.4.3"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.6.0",
28
+ "@types/node": "^22.0.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=22"
32
+ }
33
+ }