feat: introduce mcp commands - #94
bespoyasov wants to merge 22 commits into
Conversation
|
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
left a comment
There was a problem hiding this comment.
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 projectwrites theAuthorization: Bearer <token>header into.mcp.jsonat the repo root, which is normally committed. Pre-existing, butmcp install/mcp authnow 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 toprint/message/failis unrelated to the MCP feature — consider a separaterefactor:commit.packages/core/src/integrations/{claude,codex,cursor}/mcp.ts: comment rewording is fine but adds diff noise.
| 'not-installed': 'Not installed', | ||
| }; | ||
|
|
||
| function resolveProjectDir(argv: Record<string, unknown>): string { |
There was a problem hiding this comment.
--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.
| refreshMcpAuth, | ||
| } from '@features/mcp/index.js'; | ||
|
|
||
| const STATUS_LABELS: Record<string, string> = { |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
--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(); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
| "qa": "pnpm typecheck && pnpm lint && pnpm test" | ||
| }, | ||
| "dependencies": { | ||
| "@inquirer/select": "^5.2.5", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yup, mentioned in this comment: #94 (comment)
| } | ||
|
|
||
| export async function disconnectMcpServer(opts: McpDisconnectOpts): Promise<void> { | ||
| await execFile('codex', ['mcp', 'remove', opts.serverName]); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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], { |
There was a problem hiding this comment.
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.
| }; | ||
|
|
||
| export type McpDisconnectOpts = { | ||
| serverName: string; |
There was a problem hiding this comment.
Nit: serverName could be typed as McpServerName.
--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>
|
Updated according to the comments. Only this one:
...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>
confidence mcpcommand with five subcommands:install,list,status,auth,uninstalldisconnectMcpServerto theIdeIntegrationinterface with implementations for Claude, Cursor, and Codexprint,message,error,fail,extractFlags) into@output/print.js, replacing all directconsole.log/console.error/process.exitCodeusage across CLI commands@inquirer/selectwriteClaudeSettings,writeCursorMcpConfig,writeCursorCliConfig) to the testing packageconfidence mcp installconfidence mcp uninstallconfidence mcp statusconfidence mcp listconfidence mcp authAuthentication flow
When
installorauthneeds a token, it first checks for a valid persisted token (fromconfidence loginorCONFIDENCE_TOKENenv var). If none is found orauthforces 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 configfor a savedidevalue. 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.