Skip to content

Commit 6693e94

Browse files
committed
login fixed
1 parent 0b7a406 commit 6693e94

3 files changed

Lines changed: 124 additions & 4 deletions

File tree

mcpb/manifest.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": "0.3",
33
"name": "codeant",
44
"display_name": "CodeAnt AI",
5-
"version": "0.5.0",
5+
"version": "0.6.0",
66
"description": "Drive CodeAnt AI security scans and code review from Claude — org-wide secret triage, cross-repo SAST/SCA findings, on-demand scans, and local PR review.",
77
"long_description": "CodeAnt AI inside Claude. Ask things like \"how many critical SAST findings do I have across my org?\", \"show every exposed secret in payments-service\", or \"review my staged changes\" — Claude calls the CodeAnt API directly via this MCP server.\n\nIncludes 11 read-only tools (orgs, repos, scan history, scan metadata, findings, dismissed alerts, PRs, comments, comment search, local review) and 2 opt-in write tools (trigger a scan, resolve a PR conversation) gated behind a setting.\n\nRequires a CodeAnt account. Sign up at https://codeant.ai and grab an API key from your settings page.",
88
"author": {
@@ -62,16 +62,18 @@
6262
{ "name": "codeant_pr_comments", "description": "List comments on a PR/MR with optional filters." },
6363
{ "name": "codeant_comments_search", "description": "Search across CodeAnt review comments by free-text query." },
6464
{ "name": "codeant_review_local", "description": "Run a CodeAnt AI review on local working-copy changes." },
65+
{ "name": "codeant_login", "description": "Open app.codeant.ai in the browser and poll until the user completes sign-in; saves the resulting API token." },
6566
{ "name": "codeant_scans_start", "description": "Trigger a new scan run (write — gated behind read_only=false)." },
6667
{ "name": "codeant_pr_resolve", "description": "Resolve a PR conversation thread (write — gated behind read_only=false)." }
6768
],
6869
"user_config": {
6970
"api_token": {
7071
"type": "string",
7172
"title": "CodeAnt API token",
72-
"description": "Find this in your CodeAnt account settings at app.codeant.ai. Required.",
73-
"required": true,
74-
"sensitive": true
73+
"description": "Optional. Leave blank and run the `codeant_login` tool to sign in through your browser instead. Otherwise paste a token from your CodeAnt account settings at app.codeant.ai.",
74+
"required": false,
75+
"sensitive": true,
76+
"default": ""
7577
},
7678
"base_url": {
7779
"type": "string",

src/mcp/server.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { runDismissed } from '../commands/scans/dismissed.js';
1212
import { runStartScan } from '../commands/scans/start-scan.js';
1313
import { runReviewHeadless } from '../reviewHeadless.js';
1414
import * as scm from '../scm/index.js';
15+
import { isAlreadyLoggedIn, runLoginFlow } from '../utils/loginFlow.js';
16+
import { getConfigValue } from '../utils/config.js';
1517

1618
const require = createRequire(import.meta.url);
1719
const pkg = require('../../package.json');
@@ -63,7 +65,26 @@ async function captureStdout(fn) {
6365
return chunks.join('');
6466
}
6567

68+
async function ensureAuthenticated() {
69+
const envToken = process.env.CODEANT_API_TOKEN;
70+
if (envToken && envToken.trim()) return;
71+
if (isAlreadyLoggedIn()) {
72+
process.env.CODEANT_API_TOKEN = getConfigValue('apiKeyV2');
73+
return;
74+
}
75+
76+
console.error('[codeant-mcp] No API token configured — opening browser for sign-in.');
77+
try {
78+
const { loginUrl } = await runLoginFlow();
79+
console.error(`[codeant-mcp] Login complete. (URL was ${loginUrl})`);
80+
} catch (err) {
81+
console.error(`[codeant-mcp] Login failed: ${err.message}. The server will start anyway; call the codeant_login tool to retry.`);
82+
}
83+
}
84+
6685
export async function startMcpServer() {
86+
await ensureAuthenticated();
87+
6788
const server = new McpServer({ name: 'codeant', version: pkg.version });
6889
const readOnly = isReadOnly();
6990

@@ -370,6 +391,29 @@ export async function startMcpServer() {
370391
}
371392
);
372393

394+
// ─── Auth (always registered — login is needed even in read-only mode) ───
395+
server.registerTool(
396+
'codeant_login',
397+
{
398+
title: 'Sign in to CodeAnt AI',
399+
description: 'Opens app.codeant.ai in the user\'s browser and waits up to 10 minutes for them to complete sign-in. Tell the user to check their browser and finish the flow there. On success the API token is saved to ~/.codeant/config.json (apiKeyV2) and set on the running MCP process, so subsequent tool calls are authenticated without restart. Returns { alreadyLoggedIn: true } immediately if a token is already configured, unless `force` is true.',
400+
inputSchema: {
401+
force: z.boolean().optional().describe('Re-authenticate even if a token is already configured. Default false.'),
402+
},
403+
annotations: { ...WRITE_NON_DESTRUCTIVE, idempotentHint: true },
404+
},
405+
async ({ force }) => {
406+
try {
407+
if (!force && isAlreadyLoggedIn()) {
408+
return ok({ alreadyLoggedIn: true });
409+
}
410+
const { token, loginUrl } = await runLoginFlow();
411+
const masked = token ? `${token.slice(0, 8)}…` : null;
412+
return ok({ status: 'success', loginUrl, token: masked });
413+
} catch (err) { return fail(err); }
414+
}
415+
);
416+
373417
// ─── Write-side tools (gated behind CODEANT_READ_ONLY=0) ─────────────────
374418
if (!readOnly) {
375419
server.registerTool(

src/utils/loginFlow.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { randomUUID } from 'crypto';
2+
import { getConfigValue, setConfigValue } from './config.js';
3+
import { getBaseUrl } from './baseUrl.js';
4+
5+
const DEFAULT_POLL_INTERVAL = 10_000;
6+
const DEFAULT_TIMEOUT = 10 * 60 * 1000;
7+
8+
export function isAlreadyLoggedIn() {
9+
return !!getConfigValue('apiKeyV2');
10+
}
11+
12+
export async function startLoginFlow() {
13+
const token = randomUUID();
14+
const baseUrl = getBaseUrl();
15+
const loginUrl = `https://app.codeant.ai?ideLoginToken=${token}`;
16+
const pollUrl = `${baseUrl}/extension/login/status?apiKey=${token}`;
17+
18+
let browserOpened = false;
19+
try {
20+
const { default: open } = await import('open');
21+
await open(loginUrl);
22+
browserOpened = true;
23+
} catch {
24+
// Caller can fall back to printing loginUrl.
25+
}
26+
27+
return { token, loginUrl, pollUrl, browserOpened };
28+
}
29+
30+
export async function awaitLoginCompletion({
31+
token,
32+
pollUrl,
33+
pollIntervalMs = DEFAULT_POLL_INTERVAL,
34+
timeoutMs = DEFAULT_TIMEOUT,
35+
signal,
36+
} = {}) {
37+
if (!token || !pollUrl) {
38+
throw new Error('awaitLoginCompletion requires token and pollUrl');
39+
}
40+
41+
const deadline = Date.now() + timeoutMs;
42+
43+
while (Date.now() < deadline) {
44+
if (signal?.aborted) throw new Error('Login aborted');
45+
46+
try {
47+
const response = await fetch(pollUrl);
48+
const data = await response.json();
49+
if (data.status === 'yes') {
50+
setConfigValue('apiKeyV2', token);
51+
process.env.CODEANT_API_TOKEN = token;
52+
return { ok: true, token };
53+
}
54+
} catch {
55+
// Network blip — keep polling.
56+
}
57+
58+
const remaining = deadline - Date.now();
59+
if (remaining <= 0) break;
60+
await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
61+
}
62+
63+
throw new Error('Login timed out. Please try again.');
64+
}
65+
66+
export async function runLoginFlow({
67+
pollIntervalMs = DEFAULT_POLL_INTERVAL,
68+
timeoutMs = DEFAULT_TIMEOUT,
69+
signal,
70+
} = {}) {
71+
const { token, loginUrl, pollUrl, browserOpened } = await startLoginFlow();
72+
const result = await awaitLoginCompletion({ token, pollUrl, pollIntervalMs, timeoutMs, signal });
73+
return { ...result, loginUrl, browserOpened };
74+
}

0 commit comments

Comments
 (0)