Skip to content
Merged
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
16 changes: 5 additions & 11 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'

- id: version
run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"

- run: npm ci

- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- run: npm pack
# produces codeant-cli-<version>.tgz in the working directory

- uses: actions/upload-artifact@v4
with:
Expand All @@ -43,13 +47,3 @@ jobs:

Commit: ${{ github.sha }}
Message: ${{ github.event.head_commit.message }}

## Publish to npm

Download the `.tgz` and run:

```
npm publish codeant-cli-${{ steps.version.outputs.version }}.tgz --access public
```

(Requires npm auth with publish rights on the `codeant-cli` package.)
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Thumbs.db
# Build output
dist/
build/
tmp/

# Yarn lock (if using npm)
yarn.lock
Expand Down
79 changes: 74 additions & 5 deletions mcpb/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"manifest_version": "0.3",
"name": "codeant",
"display_name": "CodeAnt AI",
"version": "0.5.0",
"version": "0.5.1",
"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.",
"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.",
"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. To authenticate, call the `codeant_login` tool — it opens the CodeAnt sign-in page in your browser and saves the token automatically.\n\nCollects anonymous usage telemetry via PostHog by default; set CODEANT_TELEMETRY_DISABLED=1 to opt out.",
"author": {
"name": "CodeAnt AI",
"email": "support@codeant.ai",
Expand All @@ -28,7 +28,7 @@
"pull-requests"
],
"privacy_policies": [
"https://codeant.ai/privacy"
"https://www.codeant.ai/privacy-policy"
],
"icon": "icon.png",
"server": {
Expand All @@ -40,7 +40,15 @@
"env": {
"CODEANT_API_TOKEN": "${user_config.api_token}",
"CODEANT_API_URL": "${user_config.base_url}",
"CODEANT_READ_ONLY": "${user_config.read_only}"
"CODEANT_READ_ONLY": "${user_config.read_only}",
"GITHUB_TOKEN": "${user_config.github_token}",
"GITLAB_TOKEN": "${user_config.gitlab_token}",
"BITBUCKET_TOKEN": "${user_config.bitbucket_token}",
"AZURE_DEVOPS_TOKEN": "${user_config.azure_devops_token}",
"GITHUB_API_URL": "${user_config.github_api_url}",
"GITLAB_URL": "${user_config.gitlab_url}",
"BITBUCKET_URL": "${user_config.bitbucket_url}",
"AZURE_DEVOPS_ORG_URL": "${user_config.azure_devops_org_url}"
}
}
},
Expand All @@ -63,14 +71,15 @@
{ "name": "codeant_comments_search", "description": "Search across CodeAnt review comments by free-text query." },
{ "name": "codeant_review_local", "description": "Run a CodeAnt AI review on local working-copy changes." },
{ "name": "codeant_login", "description": "Open app.codeant.ai in the browser and poll until the user completes sign-in; saves the resulting API token." },
{ "name": "codeant_logout", "description": "Clear the saved API token and sign out of CodeAnt AI." },
{ "name": "codeant_scans_start", "description": "Trigger a new scan run (write — gated behind read_only=false)." },
{ "name": "codeant_pr_resolve", "description": "Resolve a PR conversation thread (write — gated behind read_only=false)." }
],
"user_config": {
"api_token": {
"type": "string",
"title": "CodeAnt API token",
"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.",
"description": "Optional. Leave blank and call the `codeant_login` tool to sign in through your browser.",
"required": false,
"sensitive": true,
"default": ""
Expand All @@ -88,6 +97,66 @@
"description": "When enabled, write tools (trigger scan, resolve PR thread) are hidden. Recommended.",
"required": false,
"default": true
},
"github_token": {
"type": "string",
"title": "GitHub token",
"description": "Personal access token for GitHub. Also accepts the GH_TOKEN environment variable. Leave blank to fall back to the gh CLI.",
"required": false,
"sensitive": true,
"default": ""
},
"gitlab_token": {
"type": "string",
"title": "GitLab token",
"description": "Personal access token for GitLab. Leave blank to fall back to the glab CLI.",
"required": false,
"sensitive": true,
"default": ""
},
"bitbucket_token": {
"type": "string",
"title": "Bitbucket token",
"description": "App password or access token for Bitbucket.",
"required": false,
"sensitive": true,
"default": ""
},
"azure_devops_token": {
"type": "string",
"title": "Azure DevOps token",
"description": "Personal access token for Azure DevOps. Also accepts AZURE_DEVOPS_PAT.",
"required": false,
"sensitive": true,
"default": ""
},
"github_api_url": {
"type": "string",
"title": "GitHub API URL",
"description": "Override for GitHub Enterprise Server (e.g. https://github.example.com/api/v3). Leave blank for GitHub.com.",
"required": false,
"default": ""
},
"gitlab_url": {
"type": "string",
"title": "GitLab URL",
"description": "Override for a self-hosted GitLab instance (e.g. https://gitlab.example.com). Leave blank for GitLab.com.",
"required": false,
"default": ""
},
"bitbucket_url": {
"type": "string",
"title": "Bitbucket URL",
"description": "Override for a self-hosted Bitbucket Server instance. Leave blank for Bitbucket Cloud.",
"required": false,
"default": ""
},
"azure_devops_org_url": {
"type": "string",
"title": "Azure DevOps organization URL",
"description": "Full URL to your Azure DevOps organization (e.g. https://dev.azure.com/myorg).",
"required": false,
"default": ""
}
}
}
2 changes: 1 addition & 1 deletion src/components/ScanCenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export default function ScanCenter({ filterDismissed = false, includeFalsePositi

// ── Step 3: repo selected → fetch scan history ──
const handleSelectRepo = (item) =>
_handleSelectRepo({ STEPS, item, setSelectedRepo, setStep, setLoadingMsg, setError, setScanHistory });
_handleSelectRepo({ STEPS, item, selectedConnection, setSelectedRepo, setStep, setLoadingMsg, setError, setScanHistory });

// ── Step 4: scan selected → show result type menu ──
const handleSelectScan = (item) =>
Expand Down
28 changes: 20 additions & 8 deletions src/mcp/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { runStartScan } from '../commands/scans/start-scan.js';
import { runReviewHeadless } from '../reviewHeadless.js';
import * as scm from '../scm/index.js';
import { isAlreadyLoggedIn, runLoginFlow } from '../utils/loginFlow.js';
import { getConfigValue } from '../utils/config.js';
import { getConfigValue, setConfigValue } from '../utils/config.js';

const require = createRequire(import.meta.url);
const pkg = require('../../package.json');
Expand Down Expand Up @@ -73,13 +73,7 @@ async function ensureAuthenticated() {
return;
}

console.error('[codeant-mcp] No API token configured — opening browser for sign-in.');
try {
await runLoginFlow();
console.error('[codeant-mcp] Login complete.');
} catch (err) {
console.error(`[codeant-mcp] Login failed: ${err.message}. The server will start anyway; call the codeant_login tool to retry.`);
}
console.error('[codeant-mcp] No API token configured. Call the codeant_login tool to sign in, or set CODEANT_API_TOKEN.');
}

export async function startMcpServer() {
Expand Down Expand Up @@ -415,6 +409,24 @@ export async function startMcpServer() {
}
);

server.registerTool(
'codeant_logout',
{
title: 'Sign out of CodeAnt AI',
description: 'Clears the saved API token from ~/.codeant/config.json and unsets CODEANT_API_TOKEN on the running MCP process. Returns { wasLoggedIn: false } immediately if no token was configured.',
inputSchema: {},
annotations: { ...WRITE_NON_DESTRUCTIVE, idempotentHint: true },
},
async () => {
try {
const wasLoggedIn = !!(process.env.CODEANT_API_TOKEN?.trim() || getConfigValue('apiKeyV2'));
setConfigValue('apiKeyV2', null);
delete process.env.CODEANT_API_TOKEN;
return ok({ wasLoggedIn, status: wasLoggedIn ? 'logged_out' : 'not_logged_in' });
} catch (err) { return fail(err); }
}
);

// ─── Write-side tools (gated behind CODEANT_READ_ONLY=0) ─────────────────
if (!readOnly) {
server.registerTool(
Expand Down
10 changes: 0 additions & 10 deletions src/rules/secrets.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4071,16 +4071,6 @@ regex = '''(?i)\b(phx_[a-zA-Z0-9_\-]{47})(?:\\?['"\x60]|[\s;]|\\[nr]|$)'''
entropy = 3
keywords = ["phx_"]

# ──────────────────────────────────────────────────────────────────────────────
# posthog-project-api-key
# ──────────────────────────────────────────────────────────────────────────────
[[rules]]
id = "posthog-project-api-key"
description = "Detected a PostHog Project API Key, which may expose product analytics data and event tracking to unauthorized access."
regex = '''(?i)\b(phc_[a-zA-Z0-9_\-]{43})(?:\\?['"\x60]|[\s;]|\\[nr]|$)'''
entropy = 3
keywords = ["phc_"]

# ──────────────────────────────────────────────────────────────────────────────
# postman-api-token
# ──────────────────────────────────────────────────────────────────────────────
Expand Down
10 changes: 8 additions & 2 deletions src/scanCenter/handleSelectRepo.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { getScanHistory } from '../scans/getScanHistory.js';

export async function handleSelectRepo({ STEPS, item, setSelectedRepo, setStep, setLoadingMsg, setError, setScanHistory }) {
export async function handleSelectRepo({ STEPS, item, selectedConnection, setSelectedRepo, setStep, setLoadingMsg, setError, setScanHistory }) {
setSelectedRepo(item.value);
setStep(STEPS.LOADING);
const repoFullName = item.value.full_name || item.value.name;
const orgName = selectedConnection?.organizationName;
const repoName = item.value.name;
const repoFullName = item.value.full_name || (orgName && repoName ? `${orgName}/${repoName}` : repoName);
if (!repoFullName || !repoFullName.includes('/')) {
setError(`Cannot resolve repository in org/repo form (got "${repoFullName}")`, STEPS.SELECT_REPO);
return;
}
setLoadingMsg(`Loading scan history for ${repoFullName}…`);
const res = await getScanHistory(repoFullName);
if (!res.success) {
Expand Down
2 changes: 2 additions & 0 deletions src/tools/globTool.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import path from 'path';
import { assertInsideCwd } from './pathUtils.js';

export async function globTool(args, cwd) {
const { globSync } = await import('glob');
const pattern = path.resolve(cwd, args.pattern);
assertInsideCwd(pattern.replace(/[*?{[\\].*$/, '') || cwd, cwd);
const matches = globSync(pattern);
if (!matches.length) return 'No files found';
return matches.map(m => path.relative(cwd, m)).join('\n');
Expand Down
2 changes: 2 additions & 0 deletions src/tools/grepTool.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { spawn } from 'child_process';
import path from 'path';
import { assertInsideCwd } from './pathUtils.js';

export async function grepTool(args, cwd) {
const target = args.path ? path.resolve(cwd, args.path) : cwd;
assertInsideCwd(target, cwd);
const result = await new Promise((resolve) => {
const proc = spawn('grep', ['-rn', args.pattern, target], {
cwd,
Expand Down
2 changes: 2 additions & 0 deletions src/tools/lsTool.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import fs from 'fs';
import path from 'path';
import { assertInsideCwd } from './pathUtils.js';

export async function lsTool(args, cwd) {
const dirPath = args.path ? path.resolve(cwd, args.path) : cwd;
assertInsideCwd(dirPath, cwd);
return fs.readdirSync(dirPath).sort().join('\n');
}
9 changes: 9 additions & 0 deletions src/tools/pathUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import path from 'path';

export function assertInsideCwd(resolved, cwd) {
const base = path.resolve(cwd);
const target = path.resolve(resolved);
if (target !== base && !target.startsWith(base + path.sep)) {
throw new Error(`Access denied: path is outside the working directory`);
}
}
2 changes: 2 additions & 0 deletions src/tools/readTool.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import fs from 'fs';
import path from 'path';
import { assertInsideCwd } from './pathUtils.js';

export async function readTool(args, cwd) {
const filePath = path.resolve(cwd, args.file_path);
assertInsideCwd(filePath, cwd);
const content = await fs.promises.readFile(filePath, 'utf8');
const lines = content.split('\n');
const offset = args.offset || 1;
Expand Down
2 changes: 1 addition & 1 deletion src/utils/fetchApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const fetchApi = async (endpoint, method = 'GET', body = null) => {
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
const response = await fetch(url, options);
console.error('API Response Status:', response.status);
// console.error('API Response Status:', response.status);

if (response.status === 403) {
throw new Error('Access denied (403). Please run `codeant logout` and then `codeant login` to re-authenticate.');
Expand Down