Skip to content
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
3 changes: 3 additions & 0 deletions .antigravity/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Provider MCP config file .antigravity/mcp.json is committed in-repo and will be mutated by connect with sensitive API credentials, creating accidental commit risk.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .antigravity/mcp.json:

<comment>Provider MCP config file `.antigravity/mcp.json` is committed in-repo and will be mutated by `connect` with sensitive API credentials, creating accidental commit risk.</comment>

<file context>
@@ -0,0 +1,3 @@
+{
+  "mcpServers": {}
+}
</file context>

"mcpServers": {}
}
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,66 @@ Skill files are written to per-agent directories (e.g. `.claude/`, `.cursor/`, `

For bare agent harnesses that follow the open [agents.md](https://agents.md) standard (a single `AGENTS.md` at the project root) rather than the per-agent skill directories, the CLI also writes an `AGENTS.md` into your project. It contains a delimited `<!-- INSFORGE:START -->…<!-- INSFORGE:END -->` block with InsForge context (where credentials live, when to reach for the SDK vs. the CLI, and a few correctness patterns). If you already have an `AGENTS.md`, the block is appended once and refreshed in place on subsequent runs, leaving your own content untouched. Unlike the per-agent skill files, `AGENTS.md` is **not** gitignored, so you can commit and share it.

### MCP Connection

After linking a project, connect or disconnect a local MCP provider config.

#### `npx @insforge/cli connect [provider]`

Writes the InsForge MCP server configuration into the provider's local config file, then marks the backend MCP status as `connected`.

```bash
# Connect the default provider (cursor)
npx @insforge/cli connect

# Connect a specific provider
npx @insforge/cli connect cursor
npx @insforge/cli connect claude-code
npx @insforge/cli connect windsurf
npx @insforge/cli connect cline
npx @insforge/cli connect roo
npx @insforge/cli connect codex
npx @insforge/cli connect antigravity

# Output as JSON
npx @insforge/cli connect cursor --json
```

The `connect` command:
1. Reads the linked project config from `.insforge/project.json`
2. Writes an `insforge` MCP server entry into the provider's config file (e.g. `.cursor/mcp.json` for Cursor, `.mcp.json` for Claude Code)
3. Reports the backend MCP status as `connected`

Config files written per provider:

| Provider | Config file written |
| ------------- | -------------------------------- |
| `cursor` | `.cursor/mcp.json` |
| `claude-code` | `.mcp.json` |
| `windsurf` | `.windsurf/mcp_config.json` |
| `cline` | `.cline/mcp.json` |
| `roo` | `.roo/mcp.json` |
| `codex` | `.codex/mcp.json` |
| `antigravity` | `.antigravity/mcp.json` |

#### `npx @insforge/cli disconnect [provider]`

Removes the InsForge MCP server entry from the provider's local config file, then marks the backend MCP status as `disconnected`.

```bash
# Disconnect from all known local provider configs
npx @insforge/cli disconnect

# Disconnect from a specific provider
npx @insforge/cli disconnect cursor
npx @insforge/cli disconnect claude-code

# Output as JSON
npx @insforge/cli disconnect cursor --json
```

The `disconnect` command removes only the `insforge` key from `mcpServers` — all other MCP server entries in the config file are left untouched.

## Analytics

The CLI reports anonymous usage events to [PostHog](https://posthog.com) so we can understand which features are being used and prioritize improvements.
Expand Down
78 changes: 78 additions & 0 deletions src/commands/mcp/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { Command } from 'commander';
import * as clack from '@clack/prompts';
import { updateMcpConnectionStatus } from '../../lib/api/oss.js';
import { getProjectConfig } from '../../lib/config.js';
import { handleError, getRootOpts, ProjectNotLinkedError, CLIError } from '../../lib/errors.js';
import { connectMcpProvider, displayMcpConfigPath, parseMcpProvider } from '../../lib/mcp-config.js';
import { outputJson, outputSuccess } from '../../lib/output.js';
import { reportCliUsage } from '../../lib/skills.js';
import { captureEvent, shutdownAnalytics } from '../../lib/analytics.js';
import type { ProjectConfig } from '../../types.js';

export function registerMcpConnectCommand(program: Command): void {
program
.command('connect [provider]')
.description('Connect an MCP provider to the linked InsForge project')
.option('--api-key <apiKey>', 'API key for InsForge MCP')
.option('--api-base-url <apiBaseUrl>', 'Base URL of the InsForge backend')
.action(async (providerArg: string | undefined, options: { apiKey?: string; apiBaseUrl?: string }, cmd) => {
const { json } = getRootOpts(cmd);
try {
const { apiKey, apiBaseUrl } = options;
let connectionConfig: { apiKey: string; apiBaseUrl: string } | ProjectConfig;
let projectId: string | undefined;

if (apiKey || apiBaseUrl) {
if (!apiKey || !apiBaseUrl) {
throw new CLIError('Both --api-key and --api-base-url must be provided if not using a linked project.');
}
connectionConfig = { apiKey, apiBaseUrl };
} else {
const project = getProjectConfig();
if (!project) throw new ProjectNotLinkedError();
connectionConfig = project;
projectId = project.project_id;
}

const provider = parseMcpProvider(providerArg ?? 'cursor');
const result = connectMcpProvider(provider, connectionConfig);
if (apiKey && apiBaseUrl) {
await updateMcpConnectionStatus('connected', { apiKey, apiBaseUrl });
} else {
await updateMcpConnectionStatus('connected');
}

if (projectId && 'project_id' in connectionConfig) {
captureEvent(connectionConfig.project_id, 'cli_mcp_connect', {
provider,
project_id: connectionConfig.project_id,
project_name: connectionConfig.project_name,
org_id: connectionConfig.org_id,
region: connectionConfig.region,
changed: result.changed,
});
}
await reportCliUsage('cli.mcp.connect', true);

if (json) {
outputJson({
success: true,
status: 'connected',
provider,
config_path: result.path,
changed: result.changed,
});
} else {
outputSuccess(`Connected ${provider} to InsForge MCP in ${displayMcpConfigPath(result.path)}.`);
if (!result.changed) {
clack.log.info('The existing InsForge MCP entry was already up to date.');
}
}
} catch (err) {
await reportCliUsage('cli.mcp.connect', false);
handleError(err, json);
} finally {
await shutdownAnalytics();
}
});
}
89 changes: 89 additions & 0 deletions src/commands/mcp/disconnect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { Command } from 'commander';
import * as clack from '@clack/prompts';
import { updateMcpConnectionStatus } from '../../lib/api/oss.js';
import { getProjectConfig } from '../../lib/config.js';
import { handleError, getRootOpts, ProjectNotLinkedError, CLIError } from '../../lib/errors.js';
import { MCP_PROVIDERS, disconnectMcpProvider, displayMcpConfigPath, parseMcpProvider } from '../../lib/mcp-config.js';
import { outputJson, outputSuccess } from '../../lib/output.js';
import { reportCliUsage } from '../../lib/skills.js';
import { captureEvent, shutdownAnalytics } from '../../lib/analytics.js';

export function registerMcpDisconnectCommand(program: Command): void {
program
.command('disconnect [provider]')
.description('Disconnect an MCP provider from the linked InsForge project')
.option('--api-key <apiKey>', 'API key for InsForge MCP')
.option('--api-base-url <apiBaseUrl>', 'Base URL of the InsForge backend')
.action(async (providerArg: string | undefined, options: { apiKey?: string; apiBaseUrl?: string }, cmd) => {
const { json } = getRootOpts(cmd);
try {
const { apiKey, apiBaseUrl } = options;
let projectId: string | undefined;
let projectConfig: ReturnType<typeof getProjectConfig> = null;

if (apiKey || apiBaseUrl) {
if (!apiKey || !apiBaseUrl) {
throw new CLIError('Both --api-key and --api-base-url must be provided if not using a linked project.');
}
} else {
projectConfig = getProjectConfig();
if (!projectConfig) throw new ProjectNotLinkedError();
projectId = projectConfig.project_id;
}

const providers = providerArg ? [parseMcpProvider(providerArg)] : MCP_PROVIDERS;
const results = providers.map((provider) => disconnectMcpProvider(provider));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Bulk disconnect is not fault-tolerant: a single malformed or unreadable provider config file aborts the entire cleanup and leaves remaining providers untouched. Each provider operation should be isolated in a try/catch so that one bad config doesn't prevent cleanup of the others.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/mcp/disconnect.ts, line 22:

<comment>Bulk disconnect is not fault-tolerant: a single malformed or unreadable provider config file aborts the entire cleanup and leaves remaining providers untouched. Each provider operation should be isolated in a try/catch so that one bad config doesn't prevent cleanup of the others.</comment>

<file context>
@@ -0,0 +1,69 @@
+        if (!project) throw new ProjectNotLinkedError();
+
+        const providers = providerArg ? [parseMcpProvider(providerArg)] : MCP_PROVIDERS;
+        const results = providers.map((provider) => disconnectMcpProvider(provider));
+        await updateMcpConnectionStatus('disconnected');
+        const changed = results.some((result) => result.changed);
</file context>

if (apiKey && apiBaseUrl) {
await updateMcpConnectionStatus('disconnected', { apiKey, apiBaseUrl });
} else {
await updateMcpConnectionStatus('disconnected');
}
const changed = results.some((result) => result.changed);

if (projectId && projectConfig) {
captureEvent(projectConfig.project_id, 'cli_mcp_disconnect', {
provider: providerArg ? results[0].provider : 'all',
project_id: projectConfig.project_id,
project_name: projectConfig.project_name,
org_id: projectConfig.org_id,
region: projectConfig.region,
changed,
});
}
await reportCliUsage('cli.mcp.disconnect', true);

if (json) {
outputJson({
success: true,
status: 'disconnected',
provider: providerArg ? results[0].provider : 'all',
results: results.map((result) => ({
provider: result.provider,
config_path: result.path,
changed: result.changed,
})),
changed,
});
} else {
if (providerArg) {
const result = results[0];
outputSuccess(`Disconnected ${result.provider} from InsForge MCP in ${displayMcpConfigPath(result.path)}.`);
} else {
outputSuccess('Disconnected InsForge MCP from all known local provider configs.');
const updated = results.filter((result) => result.changed);
if (updated.length > 0) {
clack.log.info(`Updated: ${updated.map((result) => displayMcpConfigPath(result.path)).join(', ')}`);
}
}
if (!changed) {
clack.log.info('No InsForge MCP entries were present; backend status was still marked disconnected.');
}
}
} catch (err) {
await reportCliUsage('cli.mcp.disconnect', false);
handleError(err, json);
} finally {
await shutdownAnalytics();
}
});
}
8 changes: 8 additions & 0 deletions src/commands/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Command } from 'commander';
import { registerMcpConnectCommand } from './connect.js';
import { registerMcpDisconnectCommand } from './disconnect.js';

export function registerMcpCommands(program: Command): void {
registerMcpConnectCommand(program);
registerMcpDisconnectCommand(program);
}
Loading