Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
src/**
.github/**

# AI tool session data (never ship)
.opencode/**
.claude/**
.codenomad/**

# Config files
.gitignore
.yarnrc
Expand All @@ -12,6 +17,9 @@ src/**
tsconfig.json
esbuild.js
vsc-extension-quickstart.md
build-package.js
release-please-config.json
.release-please-manifest.json

# Build artifacts - exclude all TypeScript files including .d.ts
**/*.map
Expand All @@ -35,6 +43,15 @@ package-lock.json
**/*.test.js
**/*.spec.js

# Development-only docs and specs
docs/**
specs/**
AGENTS.md
PR.md

# Build tooling (pre-bundled CLI entry for Antigravity ovsx only)
bin/**

# Redundant dist files (tsc output, keep only extension.js from esbuild)
dist/localization.js
dist/mcpServer.js
269 changes: 245 additions & 24 deletions bin/seamless-agent-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,52 @@

const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const { z } = require('zod');
// Use zod/v3 API so the MCP SDK routes schema conversion through zod-to-json-schema
// (rather than z4mini.toJSONSchema) — avoids a bundled-duplicate-core conflict.
const { z } = require('zod/v3');
const os = require('os');
const fs = require('fs');
const path = require('path');

const STATE_FILE = path.join(os.homedir(), '.antigravity', 'seamless-agent-state.json');

/**
* Reads the state file and returns the best (most recently active) instance.
* Supports both the new registry format:
* { "uuid": { port, token, lastActive, startedAt }, ... }
* and the legacy flat format:
* { port, token }
*/
function readBestInstance() {
try {
const raw = fs.readFileSync(STATE_FILE, 'utf8');
const registry = JSON.parse(raw);
// Backward compatibility: old format has a direct `port` field
if (registry && typeof registry.port === 'number' && typeof registry.token === 'string') {
return { port: registry.port, token: registry.token };
}
const entries = Object.values(registry);
if (!Array.isArray(entries) || entries.length === 0) return null;
// Sort by lastActive descending, fall back to startedAt
entries.sort((a, b) => {
const aTime = (a.lastActive ?? a.startedAt ?? 0);
const bTime = (b.lastActive ?? b.startedAt ?? 0);
return bTime - aTime;
});
const best = entries[0];
if (!best || !best.port || !best.token) return null;
return { port: best.port, token: best.token };
} catch {
return null;
}
}

// Parse command line arguments
// --port and --token are optional; if absent, routing relies entirely on the registry.
function parseArgs() {
const args = process.argv.slice(2);
let port = null;
let token = null;
let port = 0;
let token = '';

for (let i = 0; i < args.length; i++) {
if (args[i] === '--port' && args[i + 1]) {
Expand All @@ -30,21 +69,14 @@ function parseArgs() {
}
}

if (!port || isNaN(port) || !token) {
console.error('Usage: seamless-agent-mcp --port <api-port> --token <api-token>');
console.error(' --port The port where the VS Code extension API is running');
console.error(' --token Authentication token for the local API service');
process.exit(1);
}

return { port, token };
return { port: isNaN(port) ? 0 : port, token };
}

// Make HTTP request to VS Code extension API
async function callExtensionApi(port, token, endpoint, data) {
const url = `http://localhost:${port}${endpoint}`;

try {
// state is a mutable object { port, token } — updated on ECONNREFUSED from state file
async function callExtensionApi(state, endpoint, data) {
async function attempt(port, token) {
const url = `http://localhost:${port}${endpoint}`;
const response = await fetch(url, {
method: 'POST',
headers: {
Expand All @@ -53,17 +85,36 @@ async function callExtensionApi(port, token, endpoint, data) {
},
body: JSON.stringify(data),
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return await response.json();
}

// Re-read registry on every call to route to the most-recently-focused IDE window
const proactive = readBestInstance();
if (proactive && (proactive.port !== state.port || proactive.token !== state.token)) {
state.port = proactive.port;
state.token = proactive.token;
}

try {
return await attempt(state.port, state.token);
} catch (error) {
// Check if extension API is available
// If the extension was restarted with a new port, recover from the registry and retry once
if (error.cause && error.cause.code === 'ECONNREFUSED') {
const fresh = readBestInstance();
if (fresh && (fresh.port !== state.port || fresh.token !== state.token)) {
state.port = fresh.port;
state.token = fresh.token;
try {
return await attempt(state.port, state.token);
} catch (_) {
// Fall through to the error below
}
}
throw new Error(
`Cannot connect to Seamless Agent extension API at port ${port}. ` +
`Cannot connect to Seamless Agent extension API at port ${state.port}. ` +
`Please ensure the VS Code extension is running and the API service has started.`
);
}
Expand All @@ -72,7 +123,10 @@ async function callExtensionApi(port, token, endpoint, data) {
}

async function main() {
const { port, token } = parseArgs();
const args = parseArgs();

// Mutable state — port and token may be refreshed from state file on ECONNREFUSED
const state = { port: args.port, token: args.token };

// Create MCP server
const server = new McpServer({
Expand All @@ -93,7 +147,7 @@ async function main() {
},
async (args) => {
try {
const result = await callExtensionApi(port, token, '/ask_user', {
const result = await callExtensionApi(state, '/ask_user', {
question: args.question,
title: args.title,
agentName: args.agentName,
Expand Down Expand Up @@ -138,7 +192,7 @@ async function main() {
},
async (args) => {
try {
const result = await callExtensionApi(port, token, '/plan_review', {
const result = await callExtensionApi(state, '/plan_review', {
plan: args.plan,
title: args.title,
mode: 'review',
Expand Down Expand Up @@ -186,7 +240,7 @@ async function main() {
},
async (args) => {
try {
const result = await callExtensionApi(port, token, '/plan_review', {
const result = await callExtensionApi(state, '/plan_review', {
plan: args.plan,
title: args.title,
mode: 'walkthrough',
Expand Down Expand Up @@ -220,14 +274,181 @@ async function main() {
}
);

// Register open_whiteboard tool
server.registerTool(
'open_whiteboard',
{
description: 'Open an interactive whiteboard panel for sketching, drawing, or annotating visuals. Returns exported images as data URIs.',
inputSchema: z.object({
context: z.string().optional().describe('Instructions for the user about what to draw or annotate'),
title: z.string().optional().describe('Title for the whiteboard panel'),
blankCanvas: z.boolean().optional().describe('Open a blank canvas. Defaults to true.'),
importImages: z.array(z.object({
uri: z.string().describe('File URI of an image to import'),
label: z.string().optional().describe('Optional label for the image'),
})).optional().describe('Optional images to pre-load onto the canvas'),
})
},
async (args) => {
try {
const result = await callExtensionApi(state, '/open_whiteboard', args);
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
} catch (error) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: `Error: ${error.message}` }) }],
isError: true,
};
}
}
);

// Register render_ui tool
server.registerTool(
'render_ui',
{
description: 'Render a structured UI panel in a dedicated VS Code webview using a flat component list. Use for dashboards, forms, data displays, reports, or any rich structured UI. This tool creates the surface — call it FIRST before using append_ui, update_ui, or close_ui on the same surfaceId.',
inputSchema: z.object({
surfaceId: z.string().optional().describe('Optional unique surface identifier. Re-using the same surfaceId will update an existing panel.'),
title: z.string().optional().describe('Optional panel title displayed in the webview header.'),
components: z.array(z.object({
id: z.string(),
component: z.object({
type: z.enum(['Row', 'Column', 'Card', 'Divider', 'Text', 'Heading', 'Image', 'Markdown', 'CodeBlock', 'Button', 'TextField', 'Checkbox', 'Select']),
props: z.record(z.any()).optional(),
}),
})).optional(),
waitForAction: z.boolean().optional().describe('If true, block until the user clicks a Button'),
})
},
async (args) => {
try {
const result = await callExtensionApi(state, '/render_ui', args);
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
} catch (error) {
return {
content: [{ type: 'text', text: JSON.stringify({ surfaceId: '', rendered: false, error: `Error: ${error.message}` }) }],
isError: true,
};
}
}
);

// Register update_ui tool
server.registerTool(
'update_ui',
{
description: 'Update the dataModel and/or title of an existing surface.',
inputSchema: z.object({
surfaceId: z.string().describe('The surface identifier of the panel to update'),
title: z.string().optional().describe('Optional new panel title'),
dataModel: z.record(z.any()).optional().describe('Replacement data model'),
})
},
async (args) => {
try {
const result = await callExtensionApi(state, '/update_ui', args);
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
} catch (error) {
return {
content: [{ type: 'text', text: JSON.stringify({ surfaceId: args.surfaceId ?? '', applied: false, error: `Error: ${error.message}` }) }],
isError: true,
};
}
}
);

// Register append_ui tool
server.registerTool(
'append_ui',
{
description: 'Append components onto an existing surface.',
inputSchema: z.object({
surfaceId: z.string().describe('The surface identifier of the panel to append onto'),
title: z.string().optional().describe('Optional new panel title'),
components: z.array(z.object({
id: z.string(),
component: z.object({
type: z.enum(['Row', 'Column', 'Card', 'Divider', 'Text', 'Heading', 'Image', 'Markdown', 'CodeBlock', 'Button', 'TextField', 'Checkbox', 'Select']),
props: z.record(z.any()).optional(),
}),
})).describe('Non-empty list of components to append'),
})
},
async (args) => {
try {
const result = await callExtensionApi(state, '/append_ui', args);
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
} catch (error) {
return {
content: [{ type: 'text', text: JSON.stringify({ surfaceId: args.surfaceId ?? '', applied: false, error: `Error: ${error.message}` }) }],
isError: true,
};
}
}
);

// Register close_ui tool
server.registerTool(
'close_ui',
{
description: 'Close an active surface panel by surfaceId.',
inputSchema: z.object({
surfaceId: z.string().describe('The surface identifier of the panel to close'),
})
},
async (args) => {
try {
const result = await callExtensionApi(state, '/close_ui', args);
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
} catch (error) {
return {
content: [{ type: 'text', text: JSON.stringify({ surfaceId: args.surfaceId ?? '', closed: false, error: `Error: ${error.message}` }) }],
isError: true,
};
}
}
);

// Register list_surfaces tool
server.registerTool(
'list_surfaces',
{
description: 'List all currently active UI surface panels with their IDs, titles, and timestamps.',
inputSchema: z.object({}).describe('No parameters required'),
},
async (args) => {
try {
const result = await callExtensionApi(state, '/list_surfaces', {});
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
} catch (error) {
return {
content: [{ type: 'text', text: JSON.stringify({ surfaces: [], error: `Error: ${error.message}` }) }],
isError: true,
};
}
}
);

// Create stdio transport
const transport = new StdioServerTransport();

// Connect server to transport
await server.connect(transport);

// Log to stderr (stdout is used for MCP protocol)
console.error(`Seamless Agent MCP server started, connecting to API at port ${port}`);
console.error(`Seamless Agent MCP server started, connecting to API at port ${state.port}`);
}

main().catch((error) => {
Expand Down
Loading