Skip to content

feat: introduce mcp commands - #94

Open
bespoyasov wants to merge 22 commits into
mainfrom
feat/mcp-commands
Open

bespoyasov wants to merge 22 commits into
mainfrom
feat/mcp-commands

Conversation

@bespoyasov

Copy link
Copy Markdown
Collaborator
  • Add confidence mcp command with five subcommands: install, list, status, auth, uninstall
  • Add disconnectMcpServer to the IdeIntegration interface with implementations for Claude, Cursor, and Codex
  • Extract shared output helpers (print, message, error, fail, extractFlags) into @output/print.js, replacing all direct console.log/console.error/process.exitCode usage across CLI commands
  • Persist selected IDE to config when prompted via @inquirer/select
  • Add IDE settings scaffolds (writeClaudeSettings, writeCursorMcpConfig, writeCursorCliConfig) to the testing package

Command Description
confidence mcp install Connects all Confidence MCP servers to your AI coding agent, authenticating if needed
confidence mcp uninstall Removes all Confidence MCP servers from your AI coding agent
confidence mcp status Shows connection status of each MCP server (connected, installed, auth expired, not installed)
confidence mcp list Lists available Confidence MCP servers and their URLs
confidence mcp auth Re-authenticates and updates credentials for all connected MCP servers

Authentication flow

When install or auth needs a token, it first checks for a valid persisted token (from confidence login or CONFIDENCE_TOKEN env var). If none is found or auth forces a refresh, it opens the browser for OAuth login. The obtained token is passed to each MCP server connection as a Bearer header.

IDE resolution

Commands that interact with an IDE check confidence config for a saved ide value. If none is set, the user is prompted via an interactive select menu to choose between Claude Code, Cursor, and Codex. The selection is persisted to config so subsequent commands skip the prompt.

@bespoyasov

bespoyasov commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

Opted out from Ink for lighter TUIs, instead, using Ora for spinners and Inquirer for selects. However, if the CLI TUI becomes more complex, we'll fall back to Ink and extract the UI into a separate package.

@filipmyllari filipmyllari left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review notes (AI-assisted). Inline comments below, plus a few that don't map to diff lines:

  • Security — packages/core/src/integrations/claude/mcp.ts (connect): --scope project writes the Authorization: Bearer <token> header into .mcp.json at the repo root, which is normally committed. Pre-existing, but mcp install / mcp auth now make it a one-step action. Consider warning, using --scope local, or checking .gitignore.
  • packages/cli/src/commands/{config,login,logout,whoami}.ts: the move to print/message/fail is unrelated to the MCP feature — consider a separate refactor: commit.
  • packages/core/src/integrations/{claude,codex,cursor}/mcp.ts: comment rewording is fine but adds diff noise.

Comment thread packages/cli/src/commands/mcp.ts Outdated
'not-installed': 'Not installed',
};

function resolveProjectDir(argv: Record<string, unknown>): string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

--project is the hidden global option meaning "Override project (from config)" — a Confidence project name, not a directory. --project my-proj would make MCP config be written to/read from ./my-proj. Use process.cwd() or add a separate --dir option.

Comment thread packages/cli/src/commands/mcp.ts Outdated
refreshMcpAuth,
} from '@features/mcp/index.js';

const STATUS_LABELS: Record<string, string> = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Type as Record<McpServerStatus, string> so a new status is caught at typecheck.

spinner.succeed('MCP server credentials updated');
}

async function resolveAuthToken(opts?: { forceNew?: boolean }): Promise<string | null> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

--profile is ignored here: loadPersistedToken() and authenticate(..., undefined, ...) are called without a profile, unlike login/logout/whoami. Thread the profile through from argv.

const integration = getIntegration(ideId);
const servers = getAvailableMcpServers();

const token = await resolveAuthToken();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

On auth failure this prints an error and returns, but exit code stays 0. Same for the spinner.fail paths. Scripts/CI will see a failed install as success — set process.exitCode = 1 or throw so the command's fail() handles it.


const spinner = ora(`Installing MCP servers for ${integration.name}...`).start();

for (const server of servers) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Install stops at the first failing server; servers already connected stay installed and the user isn't told about the partial state. Either continue and report per-server results, or roll back.

Comment thread packages/cli/package.json
"qa": "pnpm typecheck && pnpm lint && pnpm test"
},
"dependencies": {
"@inquirer/select": "^5.2.5",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adds ora and @inquirer/select while @inkjs/ui already offers spinner/select and the convention is to prefer @inkjs/ui. Fine if intentional for the non-TUI CLI — worth noting in the PR description.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yup, mentioned in this comment: #94 (comment)

}

export async function disconnectMcpServer(opts: McpDisconnectOpts): Promise<void> {
await execFile('codex', ['mcp', 'remove', opts.serverName]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ignores projectDir and removes globally, while status reads both global and project config. A server registered only in the project config would still show after uninstall.


export async function disconnectMcpServer(opts: McpDisconnectOpts): Promise<void> {
removeMcpEntry(mcpConfigPath(opts.projectDir), opts.serverName);
removeMcpEntry(globalConfigPath(), opts.serverName);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Removes the entry from both project and global ~/.cursor/mcp.json. Uninstalling in one repo wipes the user's global setup for every project. Only touch the scope install wrote to.

}

export async function disconnectMcpServer(opts: McpDisconnectOpts): Promise<void> {
await execFile('claude', ['mcp', 'remove', '--scope', 'project', opts.serverName], {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If claude mcp remove throws, removeMcpToolsFromSettings never runs, leaving settings half-cleaned (and the caller hides the error). Clean settings in a finally or before the exec.

Comment thread packages/core/src/integrations/types.ts Outdated
};

export type McpDisconnectOpts = {
serverName: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: serverName could be typed as McpServerName.

bespoyasov and others added 13 commits October 2, 2026 16:00
--project is a global option for overriding the Confidence project name,
not a filesystem path. Replace its misuse in MCP commands with an
explicit --dir option that resolves to an absolute path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ensures new status variants are caught at typecheck time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Positional args with interleaved optionals were hard to read. All call
sites now pass a single object. Also threads --profile through MCP
install/auth commands and extracts argv helpers into argv.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auth failures and connection errors used error() which only prints to
stderr, leaving exitCode at 0. Use fail() which also sets
process.exitCode = 1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of stopping at the first failing server (leaving partial state
unreported), attempt all servers and report per-server results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Real failures (e.g. missing CLI binary) were silently ignored and
reported as success. Now collects per-server errors and reports them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract install, uninstall, status, resolve-token, and report-results
into their own modules for readability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
resolveIde now accepts an explicit IDE override, validates it against
the known set, and throws when no IDE is configured in a non-interactive
session. Adds isInteractive() to core/system and a resolveFlag() argv
helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Empty arrays previously fell through to formatTable, printing only
headers. Empty key-value objects lost their domain-specific message.
Both shapes now go through a unified check with a customizable `empty`
option, defaulting to "No results."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
disconnectMcpServer previously shelled out to `codex mcp remove` which
only touches global config. Servers registered in the project-scoped
config would still show as installed after uninstall.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Uninstalling a server from one project previously also removed it from
the global ~/.cursor/mcp.json, breaking every other project that
relied on the global entry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If `claude mcp remove` threw, removeMcpToolsFromSettings never ran,
leaving stale permissions and enabled-server entries in
settings.local.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bespoyasov

Copy link
Copy Markdown
Collaborator Author

Updated according to the comments. Only this one:

Security — packages/core/src/integrations/claude/mcp.ts (connect): --scope project writes the Authorization: Bearer header into .mcp.json at the repo root, which is normally committed. Pre-existing, but mcp install / mcp auth now make it a one-step action. Consider warning, using --scope local, or checking .gitignore.

...I'll probably tackle separately as a follow-up. There's going to be a lot of checking for regressions.

isInteractive() checks both stdin.isTTY and !isCI(), so stubbing only
the TTY flag was insufficient when CI=true is set in the environment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants