@cantinasecurity/apex-cli 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/apex-cli/SKILL.md +3 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.claude.json +6 -2
- package/.mcp.codex.json +6 -2
- package/MARKETPLACE.md +1 -1
- package/README.md +80 -7
- package/dist/apex.js +21 -3
- package/dist/api-client.js +5 -0
- package/dist/commands.js +36 -0
- package/dist/config.js +4 -0
- package/dist/help.js +6 -0
- package/dist/mcp.js +101 -24
- package/dist/setup.js +52 -5
- package/dist/shell.js +27 -2
- package/dist/telemetry.js +755 -0
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +3 -0
|
@@ -7,6 +7,8 @@ description: Use when a user wants to run Apex scans, inspect findings, export r
|
|
|
7
7
|
|
|
8
8
|
This skill is bundled with Apex CLI and can be installed into the current repository with `apex setup claude`.
|
|
9
9
|
|
|
10
|
+
`apex setup claude` configures the MCP server with client attribution so Apex can distinguish Claude MCP usage from direct CLI usage. Users can inspect or disable anonymous local usage telemetry with `apex telemetry status` and `apex telemetry disable`.
|
|
11
|
+
|
|
10
12
|
## Instructions
|
|
11
13
|
|
|
12
14
|
Use Apex through the MCP server when the `apex-*` tools are available.
|
|
@@ -39,6 +41,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
|
|
|
39
41
|
- Use `sourceMode: "remote"` only when the user explicitly wants to forbid local snapshot fallbacks.
|
|
40
42
|
- When checking a scan that is not the workspace binding's latest scan, pass `scanId` to `apex-status`; use `apex scans` or `apex-scans` first if you need to discover scan IDs.
|
|
41
43
|
- Finding comments, feedback, and fix review scan starts use the same Apex device-login credentials as read tools. If a write tool reports missing auth, re-run `apex-auth-status` and complete `apex-auth-start` / `apex-auth-wait` instead of asking for browser cookies or auth tokens.
|
|
44
|
+
- Anonymous telemetry records only sanitized command/tool metadata such as command names, enum modes, counts, durations, success/failure categories, CLI version, and client integration. It must not include raw repository paths, scan IDs, finding IDs, comments, file paths, PR URLs, or tokens.
|
|
42
45
|
- Invalid finding feedback requires `dismissalReason`; valid feedback can include `suggestedSeverity`, including `extreme`.
|
|
43
46
|
- Fix PR callback feedback requires valid feedback with `labels: ["fixed"]` and `fixPrUrls`; start the fix review scan with `apex-finding-fix-review` after saving that feedback.
|
|
44
47
|
- Finding identifiers such as `KERN2-25` resolve against the selected or latest scan for the current workspace binding. Pass an explicit `scanId` when needed, or use the finding UUID directly.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Cantina agent plugins for security review workflows.",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.11"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"source": {
|
|
15
15
|
"source": "npm",
|
|
16
16
|
"package": "@cantinasecurity/apex-cli",
|
|
17
|
-
"version": "0.1.
|
|
17
|
+
"version": "0.1.11"
|
|
18
18
|
},
|
|
19
19
|
"description": "Run Apex security scans and review findings from Claude Code.",
|
|
20
|
-
"version": "0.1.
|
|
20
|
+
"version": "0.1.11",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "Cantina",
|
|
23
23
|
"email": "support@cantina.xyz"
|
package/.mcp.claude.json
CHANGED
package/.mcp.codex.json
CHANGED
package/MARKETPLACE.md
CHANGED
|
@@ -13,7 +13,7 @@ This package is prepared as both a Claude Code plugin and a Codex plugin. The pu
|
|
|
13
13
|
The plugin MCP configs launch the pinned npm CLI package with:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
16
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.11 apex-mcp
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
That keeps marketplace installs independent of a user's global `apex` install.
|
package/README.md
CHANGED
|
@@ -149,6 +149,7 @@ Supported shell commands:
|
|
|
149
149
|
- `apex login`
|
|
150
150
|
- `apex logout`
|
|
151
151
|
- `apex setup [all|codex|claude|copilot]`
|
|
152
|
+
- `apex telemetry [status|enable|disable]`
|
|
152
153
|
- `apex update`
|
|
153
154
|
- `apex connect github`
|
|
154
155
|
- `apex connect gitlab`
|
|
@@ -221,18 +222,24 @@ If Apex is installed globally, prefer:
|
|
|
221
222
|
apex setup
|
|
222
223
|
```
|
|
223
224
|
|
|
224
|
-
That registers Apex for installed Codex CLI, Claude Code, and GitHub Copilot CLI clients automatically.
|
|
225
|
+
That registers Apex for installed Codex CLI, Claude Code, and GitHub Copilot CLI clients automatically. Codex and Claude registrations also set `APEX_MCP_CLIENT` and `APEX_CLIENT_INTEGRATION` so Apex can distinguish agent-driven MCP usage from direct CLI usage.
|
|
225
226
|
|
|
226
227
|
If you want to wire clients manually instead, Apex ships a stable `apex-mcp` binary. For Codex:
|
|
227
228
|
|
|
228
229
|
```bash
|
|
229
|
-
codex mcp add apex
|
|
230
|
+
codex mcp add apex \
|
|
231
|
+
--env APEX_MCP_CLIENT=codex \
|
|
232
|
+
--env APEX_CLIENT_INTEGRATION=codex \
|
|
233
|
+
-- apex-mcp
|
|
230
234
|
```
|
|
231
235
|
|
|
232
236
|
For Claude Code:
|
|
233
237
|
|
|
234
238
|
```bash
|
|
235
|
-
claude mcp add --scope user
|
|
239
|
+
claude mcp add --scope user \
|
|
240
|
+
-e APEX_MCP_CLIENT=claude \
|
|
241
|
+
-e APEX_CLIENT_INTEGRATION=claude \
|
|
242
|
+
apex -- apex-mcp
|
|
236
243
|
```
|
|
237
244
|
|
|
238
245
|
For GitHub Copilot CLI:
|
|
@@ -247,7 +254,11 @@ For any other MCP client, configure it to launch:
|
|
|
247
254
|
{
|
|
248
255
|
"mcpServers": {
|
|
249
256
|
"apex": {
|
|
250
|
-
"command": "apex-mcp"
|
|
257
|
+
"command": "apex-mcp",
|
|
258
|
+
"env": {
|
|
259
|
+
"APEX_MCP_CLIENT": "custom-mcp-client",
|
|
260
|
+
"APEX_CLIENT_INTEGRATION": "custom-mcp-client"
|
|
261
|
+
}
|
|
251
262
|
}
|
|
252
263
|
}
|
|
253
264
|
}
|
|
@@ -259,7 +270,11 @@ From a local checkout during development, prefer the repo-local binary so the MC
|
|
|
259
270
|
{
|
|
260
271
|
"mcpServers": {
|
|
261
272
|
"apex": {
|
|
262
|
-
"command": "/path/to/apex-cli/bin/apex-mcp"
|
|
273
|
+
"command": "/path/to/apex-cli/bin/apex-mcp",
|
|
274
|
+
"env": {
|
|
275
|
+
"APEX_MCP_CLIENT": "local-dev",
|
|
276
|
+
"APEX_CLIENT_INTEGRATION": "local-dev"
|
|
277
|
+
}
|
|
263
278
|
}
|
|
264
279
|
}
|
|
265
280
|
}
|
|
@@ -273,7 +288,11 @@ If you need to launch through `pnpm`, use `--silent`:
|
|
|
273
288
|
"apex": {
|
|
274
289
|
"command": "pnpm",
|
|
275
290
|
"args": ["--silent", "mcp"],
|
|
276
|
-
"cwd": "/path/to/apex-cli"
|
|
291
|
+
"cwd": "/path/to/apex-cli",
|
|
292
|
+
"env": {
|
|
293
|
+
"APEX_MCP_CLIENT": "local-dev",
|
|
294
|
+
"APEX_CLIENT_INTEGRATION": "local-dev"
|
|
295
|
+
}
|
|
277
296
|
}
|
|
278
297
|
}
|
|
279
298
|
}
|
|
@@ -297,6 +316,60 @@ For Codex-style clients, the packaged skill can be installed with `apex setup co
|
|
|
297
316
|
|
|
298
317
|
For Claude Code, the packaged project skill can be installed into the current repository with `apex setup claude`. The repo-local source lives at `.claude/skills/apex-cli/SKILL.md`. Anthropic documents project skills as filesystem directories under `.claude/skills/<name>/SKILL.md`, and the Claude Agent SDK uses the same location when the `Skill` tool is enabled.
|
|
299
318
|
|
|
319
|
+
## Usage Telemetry
|
|
320
|
+
|
|
321
|
+
Apex CLI emits first-party, privacy-preserving usage telemetry so Cantina can understand how people use the direct CLI, the interactive shell, and MCP tools in Codex, Claude, GitHub Copilot CLI, or other clients.
|
|
322
|
+
|
|
323
|
+
Telemetry is enabled by default and can be disabled locally:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
apex telemetry status
|
|
327
|
+
apex telemetry disable
|
|
328
|
+
apex telemetry enable
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Environment opt-outs override local config:
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
APEX_TELEMETRY_DISABLED=1 apex scan
|
|
335
|
+
# also honored: APEX_DISABLE_TELEMETRY=1 or DO_NOT_TRACK=1
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Telemetry records lifecycle events such as command/tool start and completion, duration, success/failure category, CLI version, Node/platform basics, anonymous install/session IDs, and sanitized command metadata. It also adds attribution headers to Apex API requests, including surface (`cli`, `interactive_shell`, or `mcp`), client integration, invocation ID, command/tool name, and CLI version.
|
|
339
|
+
|
|
340
|
+
Telemetry does not send raw cwd paths, repository URLs, finding IDs, scan IDs, PR URLs, comments, file paths, tokens, or raw flag values. Sensitive inputs are reduced to booleans, counts, enum values, or length buckets.
|
|
341
|
+
|
|
342
|
+
Telemetry event posts do not include bearer tokens. The telemetry endpoint is `POST /api/cli/v1/telemetry/events` with a batch payload:
|
|
343
|
+
|
|
344
|
+
```json
|
|
345
|
+
{
|
|
346
|
+
"events": [
|
|
347
|
+
{
|
|
348
|
+
"schemaVersion": 1,
|
|
349
|
+
"event": "apex.invocation.completed",
|
|
350
|
+
"cliVersion": "0.1.11",
|
|
351
|
+
"invocation": {
|
|
352
|
+
"surface": "mcp",
|
|
353
|
+
"command": "apex-scan",
|
|
354
|
+
"mcpTool": "apex-scan",
|
|
355
|
+
"metadata": {
|
|
356
|
+
"mode": "pr",
|
|
357
|
+
"pullRequestCount": 1,
|
|
358
|
+
"cwdProvided": true
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
"client": {
|
|
362
|
+
"integration": "codex"
|
|
363
|
+
},
|
|
364
|
+
"outcome": {
|
|
365
|
+
"success": true,
|
|
366
|
+
"durationMs": 1234
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
300
373
|
## Plugin And Marketplace Packaging
|
|
301
374
|
|
|
302
375
|
The npm package also includes marketplace-ready plugin artifacts:
|
|
@@ -305,7 +378,7 @@ The npm package also includes marketplace-ready plugin artifacts:
|
|
|
305
378
|
- `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
|
|
306
379
|
- `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
|
|
307
380
|
|
|
308
|
-
These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
381
|
+
These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.11 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
|
|
309
382
|
|
|
310
383
|
The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
|
|
311
384
|
|
package/dist/apex.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ApexApiClient, formatApiError } from "./api-client.js";
|
|
2
2
|
import { parseArgs } from "./args.js";
|
|
3
|
-
import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandLogin, commandLogout, commandScan, commandScans, commandSetup, commandStatus, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, } from "./commands.js";
|
|
3
|
+
import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandLogin, commandLogout, commandScan, commandScans, commandSetup, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, } from "./commands.js";
|
|
4
4
|
import { CLI_HELP_TEXT } from "./help.js";
|
|
5
5
|
import { runMcpServer } from "./mcp.js";
|
|
6
6
|
import { runInteractiveShell } from "./shell.js";
|
|
7
|
+
import { createCliTelemetryInvocation, emitInvocationCompleted, emitInvocationStarted, withTelemetryContext, } from "./telemetry.js";
|
|
7
8
|
import { maybePromptForUpdate } from "./update.js";
|
|
8
|
-
async function
|
|
9
|
-
const parsed = parseArgs(process.argv.slice(2));
|
|
9
|
+
async function dispatch(parsed) {
|
|
10
10
|
if (parsed.flags.help === true || parsed.command === "help") {
|
|
11
11
|
process.stdout.write(CLI_HELP_TEXT);
|
|
12
12
|
return;
|
|
@@ -45,6 +45,9 @@ async function main() {
|
|
|
45
45
|
case "setup":
|
|
46
46
|
await commandSetup(cwd, parsed.flags, parsed.subcommand);
|
|
47
47
|
return;
|
|
48
|
+
case "telemetry":
|
|
49
|
+
await commandTelemetry(parsed.flags, parsed.subcommand);
|
|
50
|
+
return;
|
|
48
51
|
case "doctor":
|
|
49
52
|
await commandDoctor(client, cwd, parsed.flags);
|
|
50
53
|
return;
|
|
@@ -117,6 +120,21 @@ async function main() {
|
|
|
117
120
|
throw new Error(`Unknown command: ${parsed.command}`);
|
|
118
121
|
}
|
|
119
122
|
}
|
|
123
|
+
async function main() {
|
|
124
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
125
|
+
const invocation = createCliTelemetryInvocation(parsed);
|
|
126
|
+
emitInvocationStarted(invocation);
|
|
127
|
+
await withTelemetryContext(invocation, async () => {
|
|
128
|
+
try {
|
|
129
|
+
await dispatch(parsed);
|
|
130
|
+
emitInvocationCompleted(invocation);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
emitInvocationCompleted(invocation, error);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
120
138
|
function getFlagValue(value) {
|
|
121
139
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
122
140
|
}
|
package/dist/api-client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { clearCredentials, loadConfig, loadCredentials, saveCredentials } from "./config.js";
|
|
2
|
+
import { getTelemetryRequestHeaders } from "./telemetry.js";
|
|
2
3
|
export class ApiError extends Error {
|
|
3
4
|
status;
|
|
4
5
|
body;
|
|
@@ -84,7 +85,11 @@ export class ApexApiClient {
|
|
|
84
85
|
async request(path, options = {}) {
|
|
85
86
|
const baseUrl = await this.getBaseUrl();
|
|
86
87
|
const headers = new Headers(options.headers ?? {});
|
|
88
|
+
const telemetryHeaders = await getTelemetryRequestHeaders();
|
|
87
89
|
let credentials = options.auth === false ? null : await this.refreshIfNeeded().catch(() => null);
|
|
90
|
+
for (const [key, value] of Object.entries(telemetryHeaders)) {
|
|
91
|
+
headers.set(key, value);
|
|
92
|
+
}
|
|
88
93
|
if (options.json !== undefined) {
|
|
89
94
|
headers.set("Content-Type", "application/json");
|
|
90
95
|
}
|
package/dist/commands.js
CHANGED
|
@@ -12,6 +12,7 @@ import { chooseCompany, createWorkspaceBinding, ensureAuthenticated, remediateMi
|
|
|
12
12
|
import { createWorkspaceBindingFromSummary, fetchCompanyCredits, fetchCompanyWorkspaces, findWorkspaceByRef, } from "./workspaces.js";
|
|
13
13
|
import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
|
|
14
14
|
import { commandSetup as runCliSetup } from "./setup.js";
|
|
15
|
+
import { getTelemetryStatus, setTelemetryEnabled, } from "./telemetry.js";
|
|
15
16
|
import { commandUpdate as runCliUpdate } from "./update.js";
|
|
16
17
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
17
18
|
import path from "node:path";
|
|
@@ -496,6 +497,41 @@ export async function commandUpdate(flags) {
|
|
|
496
497
|
export async function commandSetup(cwd, flags, target) {
|
|
497
498
|
return runCliSetup(cwd, flags, target);
|
|
498
499
|
}
|
|
500
|
+
function formatTelemetryDisabledBy(disabledBy) {
|
|
501
|
+
if (!disabledBy)
|
|
502
|
+
return "not disabled";
|
|
503
|
+
if (disabledBy === "config")
|
|
504
|
+
return "local config";
|
|
505
|
+
return disabledBy.replace(/^env:/, "environment variable ");
|
|
506
|
+
}
|
|
507
|
+
export async function commandTelemetry(flags, target) {
|
|
508
|
+
let payload;
|
|
509
|
+
switch (target ?? "status") {
|
|
510
|
+
case "status":
|
|
511
|
+
payload = await getTelemetryStatus("cli");
|
|
512
|
+
break;
|
|
513
|
+
case "enable":
|
|
514
|
+
payload = await setTelemetryEnabled(true);
|
|
515
|
+
break;
|
|
516
|
+
case "disable":
|
|
517
|
+
payload = await setTelemetryEnabled(false);
|
|
518
|
+
break;
|
|
519
|
+
default:
|
|
520
|
+
throw new Error("Usage: apex telemetry [status|enable|disable]");
|
|
521
|
+
}
|
|
522
|
+
if (isJsonMode(flags)) {
|
|
523
|
+
printJson(payload);
|
|
524
|
+
return payload;
|
|
525
|
+
}
|
|
526
|
+
logLine(`Telemetry: ${payload.enabled ? "enabled" : "disabled"}`, flags);
|
|
527
|
+
logLine(`Disabled by: ${formatTelemetryDisabledBy(payload.disabledBy)}`, flags);
|
|
528
|
+
logLine(`Endpoint: ${payload.endpointPath}`, flags);
|
|
529
|
+
logLine(`Client integration: ${payload.client.integration}`, flags);
|
|
530
|
+
if (payload.installId) {
|
|
531
|
+
logLine(`Install ID: ${payload.installId}`, flags);
|
|
532
|
+
}
|
|
533
|
+
return payload;
|
|
534
|
+
}
|
|
499
535
|
export async function commandDoctor(client, cwd, flags) {
|
|
500
536
|
const me = await ensureAuthenticated(client, flags);
|
|
501
537
|
try {
|
package/dist/config.js
CHANGED
|
@@ -32,12 +32,16 @@ export async function loadConfig() {
|
|
|
32
32
|
version: 1,
|
|
33
33
|
baseUrl: DEFAULT_BASE_URL,
|
|
34
34
|
defaultCompanyId: null,
|
|
35
|
+
telemetryDisabled: config?.telemetryDisabled ?? null,
|
|
36
|
+
telemetryInstallId: config?.telemetryInstallId ?? null,
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
return {
|
|
38
40
|
version: 1,
|
|
39
41
|
baseUrl: config.baseUrl,
|
|
40
42
|
defaultCompanyId: config.defaultCompanyId ?? null,
|
|
43
|
+
telemetryDisabled: config.telemetryDisabled ?? null,
|
|
44
|
+
telemetryInstallId: config.telemetryInstallId ?? null,
|
|
41
45
|
};
|
|
42
46
|
}
|
|
43
47
|
export async function saveConfig(config) {
|
package/dist/help.js
CHANGED
|
@@ -23,6 +23,8 @@ export const CLI_HELP_TEXT = `Usage:
|
|
|
23
23
|
apex mcp Start the Apex MCP server over stdio
|
|
24
24
|
apex setup [all|codex|claude|copilot]
|
|
25
25
|
Configure Apex for Codex, Claude Code, and GitHub Copilot CLI
|
|
26
|
+
apex telemetry [status|enable|disable]
|
|
27
|
+
Show or change local anonymous usage telemetry settings
|
|
26
28
|
apex update Update the local Apex CLI install
|
|
27
29
|
apex connect github Open the GitHub connection flow
|
|
28
30
|
apex connect gitlab Open the GitLab connection flow
|
|
@@ -66,6 +68,7 @@ Tips:
|
|
|
66
68
|
Invalid finding feedback requires --dismissal-reason.
|
|
67
69
|
Fix review scans require valid feedback with --label fixed and at least one --fix-pr-url, then apex findings fix-review.
|
|
68
70
|
Finding identifiers such as KERN2-25 resolve against the selected scan; pass --scan or use the finding UUID directly when needed.
|
|
71
|
+
Anonymous usage telemetry helps Apex understand CLI, MCP, Codex, Claude, and Copilot usage. Disable it with apex telemetry disable or APEX_TELEMETRY_DISABLED=1.
|
|
69
72
|
Quote workspace names that contain spaces:
|
|
70
73
|
apex workspace use "Core Platform"
|
|
71
74
|
`;
|
|
@@ -90,6 +93,8 @@ Commands:
|
|
|
90
93
|
/cancel-scan [scan-id] Cancel a running or most recent scan
|
|
91
94
|
/status [scan-id] Show progress for the most recent or selected scan
|
|
92
95
|
/doctor Validate auth, repos, connections, and workspace binding
|
|
96
|
+
/telemetry [status|enable|disable]
|
|
97
|
+
Show or change local anonymous usage telemetry settings
|
|
93
98
|
/update Update the local Apex CLI install and exit the shell
|
|
94
99
|
/logout Sign out locally and exit the shell
|
|
95
100
|
/repos List detected repositories
|
|
@@ -112,5 +117,6 @@ Tips:
|
|
|
112
117
|
Invalid finding feedback requires a dismissal reason.
|
|
113
118
|
Fix review scans require fixed valid feedback with a Fix PR URL. Use scripted CLI or MCP for attaching Fix PR URLs.
|
|
114
119
|
Use scripted CLI flags for advanced feedback options such as suggested severity or dismissal reason.
|
|
120
|
+
Anonymous usage telemetry can be disabled with /telemetry disable or APEX_TELEMETRY_DISABLED=1.
|
|
115
121
|
Quote workspace names that contain spaces: /workspace use "Core Platform"
|
|
116
122
|
`;
|
package/dist/mcp.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getMe, logout, startDeviceLogin, waitForDeviceLoginApproval } from "./a
|
|
|
6
6
|
import { ApexApiClient, formatApiError } from "./api-client.js";
|
|
7
7
|
import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandScan, commandScans, commandStatus, commandWorkspace, commandWorkspaceUse, commandWorkspaces, } from "./commands.js";
|
|
8
8
|
import { CLI_HELP_TEXT } from "./help.js";
|
|
9
|
+
import { createMcpServerTelemetryInvocation, createMcpTelemetryInvocation, emitInvocationCompleted, emitInvocationStarted, emitMcpServerStarted, withTelemetryContext, } from "./telemetry.js";
|
|
9
10
|
import { APEX_CLI_VERSION } from "./version.js";
|
|
10
11
|
const MCP_WORKFLOW_GUIDE = `Use Apex through these tools instead of shelling out to the CLI.
|
|
11
12
|
|
|
@@ -139,14 +140,20 @@ function errorResult(toolName, error) {
|
|
|
139
140
|
isError: true,
|
|
140
141
|
};
|
|
141
142
|
}
|
|
142
|
-
async function runTool(toolName, action, summary) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
async function runTool(toolName, action, summary, input = {}) {
|
|
144
|
+
const invocation = createMcpTelemetryInvocation(toolName, input);
|
|
145
|
+
emitInvocationStarted(invocation);
|
|
146
|
+
return withTelemetryContext(invocation, async () => {
|
|
147
|
+
try {
|
|
148
|
+
const value = await action();
|
|
149
|
+
emitInvocationCompleted(invocation);
|
|
150
|
+
return successResult(summary(value), value);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
emitInvocationCompleted(invocation, error);
|
|
154
|
+
return errorResult(toolName, error);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
150
157
|
}
|
|
151
158
|
async function requireAuthenticated(client) {
|
|
152
159
|
const me = await getMe(client);
|
|
@@ -235,7 +242,7 @@ function registerTools(server) {
|
|
|
235
242
|
return "Device login expired before approval.";
|
|
236
243
|
}
|
|
237
244
|
return "Device login is still pending.";
|
|
238
|
-
}));
|
|
245
|
+
}, { deviceCode, intervalSeconds, expiresAt, timeoutSeconds }));
|
|
239
246
|
server.registerTool("apex-logout", {
|
|
240
247
|
title: "Log Out Of Apex",
|
|
241
248
|
description: "Clear the locally stored Apex session.",
|
|
@@ -268,7 +275,13 @@ function registerTools(server) {
|
|
|
268
275
|
cwd: targetCwd,
|
|
269
276
|
...payload,
|
|
270
277
|
};
|
|
271
|
-
}, (value) => `Apex doctor completed for ${String(value.cwd)}
|
|
278
|
+
}, (value) => `Apex doctor completed for ${String(value.cwd)}.`, {
|
|
279
|
+
cwd,
|
|
280
|
+
company,
|
|
281
|
+
workspaceName,
|
|
282
|
+
repoPaths,
|
|
283
|
+
sourceMode,
|
|
284
|
+
}));
|
|
272
285
|
server.registerTool("apex-credits", {
|
|
273
286
|
title: "Get Apex Credits",
|
|
274
287
|
description: "Show scan credits plus audit and fix review scan entitlements for the active or selected company.",
|
|
@@ -287,7 +300,10 @@ function registerTools(server) {
|
|
|
287
300
|
cwd: targetCwd,
|
|
288
301
|
...payload,
|
|
289
302
|
};
|
|
290
|
-
}, (value) => `Fetched Apex credits for ${String(value.cwd)}
|
|
303
|
+
}, (value) => `Fetched Apex credits for ${String(value.cwd)}.`, {
|
|
304
|
+
cwd,
|
|
305
|
+
company,
|
|
306
|
+
}));
|
|
291
307
|
server.registerTool("apex-workspace", {
|
|
292
308
|
title: "Get Current Apex Workspace Binding",
|
|
293
309
|
description: "Show the current Apex workspace bound to a directory, if any.",
|
|
@@ -301,7 +317,9 @@ function registerTools(server) {
|
|
|
301
317
|
cwd: targetCwd,
|
|
302
318
|
...payload,
|
|
303
319
|
};
|
|
304
|
-
}, (value) => `Loaded workspace binding for ${String(value.cwd)}
|
|
320
|
+
}, (value) => `Loaded workspace binding for ${String(value.cwd)}.`, {
|
|
321
|
+
cwd,
|
|
322
|
+
}));
|
|
305
323
|
server.registerTool("apex-workspaces", {
|
|
306
324
|
title: "List Apex Workspaces",
|
|
307
325
|
description: "List workspaces available to the active or selected company.",
|
|
@@ -320,7 +338,10 @@ function registerTools(server) {
|
|
|
320
338
|
cwd: targetCwd,
|
|
321
339
|
...payload,
|
|
322
340
|
};
|
|
323
|
-
}, (value) => `Listed Apex workspaces for ${String(value.cwd)}
|
|
341
|
+
}, (value) => `Listed Apex workspaces for ${String(value.cwd)}.`, {
|
|
342
|
+
cwd,
|
|
343
|
+
company,
|
|
344
|
+
}));
|
|
324
345
|
server.registerTool("apex-workspace-use", {
|
|
325
346
|
title: "Bind Directory To Apex Workspace",
|
|
326
347
|
description: "Bind a directory to an existing Apex workspace by id, prefix, or name.",
|
|
@@ -340,7 +361,11 @@ function registerTools(server) {
|
|
|
340
361
|
cwd: targetCwd,
|
|
341
362
|
...payload,
|
|
342
363
|
};
|
|
343
|
-
}, (value) => `Bound ${String(value.cwd)} to an Apex workspace
|
|
364
|
+
}, (value) => `Bound ${String(value.cwd)} to an Apex workspace.`, {
|
|
365
|
+
cwd,
|
|
366
|
+
company,
|
|
367
|
+
workspaceRef,
|
|
368
|
+
}));
|
|
344
369
|
server.registerTool("apex-scan", {
|
|
345
370
|
title: "Start Apex Scan",
|
|
346
371
|
description: "Start a new Apex scan for the provided cwd by default. Pass repoPaths only to scan explicit alternate local roots. Use mode audit for audit scans and mode pr for GitHub pull request scans.",
|
|
@@ -378,7 +403,16 @@ function registerTools(server) {
|
|
|
378
403
|
cwd: targetCwd,
|
|
379
404
|
...payload,
|
|
380
405
|
};
|
|
381
|
-
}, (value) => `Started an Apex scan for ${String(value.cwd)}
|
|
406
|
+
}, (value) => `Started an Apex scan for ${String(value.cwd)}.`, {
|
|
407
|
+
cwd,
|
|
408
|
+
company,
|
|
409
|
+
workspaceName,
|
|
410
|
+
repoPaths,
|
|
411
|
+
mode,
|
|
412
|
+
pullRequests,
|
|
413
|
+
sourceMode,
|
|
414
|
+
force,
|
|
415
|
+
}));
|
|
382
416
|
server.registerTool("apex-status", {
|
|
383
417
|
title: "Get Apex Scan Status",
|
|
384
418
|
description: "Show progress for the most recent or selected Apex scan in a directory.",
|
|
@@ -395,7 +429,10 @@ function registerTools(server) {
|
|
|
395
429
|
cwd: targetCwd,
|
|
396
430
|
...payload,
|
|
397
431
|
};
|
|
398
|
-
}, (value) => `Fetched scan status for ${String(value.cwd)}
|
|
432
|
+
}, (value) => `Fetched scan status for ${String(value.cwd)}.`, {
|
|
433
|
+
cwd,
|
|
434
|
+
scanId,
|
|
435
|
+
}));
|
|
399
436
|
server.registerTool("apex-scans", {
|
|
400
437
|
title: "List Apex Scans",
|
|
401
438
|
description: "List scans for the Apex workspace bound to a directory.",
|
|
@@ -411,7 +448,9 @@ function registerTools(server) {
|
|
|
411
448
|
cwd: targetCwd,
|
|
412
449
|
...payload,
|
|
413
450
|
};
|
|
414
|
-
}, (value) => `Listed scans for ${String(value.cwd)}
|
|
451
|
+
}, (value) => `Listed scans for ${String(value.cwd)}.`, {
|
|
452
|
+
cwd,
|
|
453
|
+
}));
|
|
415
454
|
server.registerTool("apex-cancel-scan", {
|
|
416
455
|
title: "Cancel Apex Scan",
|
|
417
456
|
description: "Cancel a running Apex scan in the bound workspace.",
|
|
@@ -428,7 +467,10 @@ function registerTools(server) {
|
|
|
428
467
|
cwd: targetCwd,
|
|
429
468
|
...payload,
|
|
430
469
|
};
|
|
431
|
-
}, (value) => `Cancelled the selected Apex scan for ${String(value.cwd)}
|
|
470
|
+
}, (value) => `Cancelled the selected Apex scan for ${String(value.cwd)}.`, {
|
|
471
|
+
cwd,
|
|
472
|
+
scanId,
|
|
473
|
+
}));
|
|
432
474
|
server.registerTool("apex-findings", {
|
|
433
475
|
title: "List Apex Findings",
|
|
434
476
|
description: "List findings for the latest or selected Apex scan in a directory.",
|
|
@@ -449,7 +491,11 @@ function registerTools(server) {
|
|
|
449
491
|
cwd: targetCwd,
|
|
450
492
|
...payload,
|
|
451
493
|
};
|
|
452
|
-
}, (value) => `Fetched Apex findings for ${String(value.cwd)}
|
|
494
|
+
}, (value) => `Fetched Apex findings for ${String(value.cwd)}.`, {
|
|
495
|
+
cwd,
|
|
496
|
+
scanId,
|
|
497
|
+
limit,
|
|
498
|
+
}));
|
|
453
499
|
server.registerTool("apex-finding-comment", {
|
|
454
500
|
title: "Add Apex Finding Comment",
|
|
455
501
|
description: "Add a comment or note to an Apex finding using the current Apex login.",
|
|
@@ -470,7 +516,13 @@ function registerTools(server) {
|
|
|
470
516
|
cwd: targetCwd,
|
|
471
517
|
...payload,
|
|
472
518
|
};
|
|
473
|
-
}, (value) => `Added a finding comment for ${String(value.findingRef)}
|
|
519
|
+
}, (value) => `Added a finding comment for ${String(value.findingRef)}.`, {
|
|
520
|
+
cwd,
|
|
521
|
+
findingRef,
|
|
522
|
+
content,
|
|
523
|
+
parentCommentId,
|
|
524
|
+
scanId,
|
|
525
|
+
}));
|
|
474
526
|
server.registerTool("apex-finding-feedback", {
|
|
475
527
|
title: "Leave Apex Finding Feedback",
|
|
476
528
|
description: "Leave valid or invalid feedback on an Apex finding using the current Apex login. To attach a fix PR, send status valid with label fixed and fixPrUrls.",
|
|
@@ -508,7 +560,17 @@ function registerTools(server) {
|
|
|
508
560
|
...payload,
|
|
509
561
|
};
|
|
510
562
|
}, (value) => `Submitted ${String((value.feedback?.feedbackType ??
|
|
511
|
-
"finding"))} feedback for ${String(value.findingRef)}
|
|
563
|
+
"finding"))} feedback for ${String(value.findingRef)}.`, {
|
|
564
|
+
cwd,
|
|
565
|
+
findingRef,
|
|
566
|
+
status,
|
|
567
|
+
comment,
|
|
568
|
+
labels,
|
|
569
|
+
fixPrUrls,
|
|
570
|
+
suggestedSeverity,
|
|
571
|
+
dismissalReason,
|
|
572
|
+
scanId,
|
|
573
|
+
}));
|
|
512
574
|
server.registerTool("apex-finding-fix-review", {
|
|
513
575
|
title: "Start Apex Finding Fix Review Scan",
|
|
514
576
|
description: "Start a fix review scan for a finding after fixed feedback with one or more Fix PR URLs has been saved.",
|
|
@@ -527,7 +589,11 @@ function registerTools(server) {
|
|
|
527
589
|
cwd: targetCwd,
|
|
528
590
|
...payload,
|
|
529
591
|
};
|
|
530
|
-
}, (value) => `Started fix review scan for ${String(value.findingRef)}
|
|
592
|
+
}, (value) => `Started fix review scan for ${String(value.findingRef)}.`, {
|
|
593
|
+
cwd,
|
|
594
|
+
findingRef,
|
|
595
|
+
scanId,
|
|
596
|
+
}));
|
|
531
597
|
server.registerTool("apex-export-findings", {
|
|
532
598
|
title: "Export Apex Findings",
|
|
533
599
|
description: "Export findings for the latest or selected Apex scan to a file on disk.",
|
|
@@ -556,7 +622,12 @@ function registerTools(server) {
|
|
|
556
622
|
scan: payload.scan,
|
|
557
623
|
outputPath: payload.outputPath,
|
|
558
624
|
};
|
|
559
|
-
}, (value) => `Exported Apex findings for ${String(value.cwd)}
|
|
625
|
+
}, (value) => `Exported Apex findings for ${String(value.cwd)}.`, {
|
|
626
|
+
cwd,
|
|
627
|
+
scanId,
|
|
628
|
+
format,
|
|
629
|
+
output,
|
|
630
|
+
}));
|
|
560
631
|
server.registerTool("apex-connect-provider", {
|
|
561
632
|
title: "Get Apex Provider Connection URL",
|
|
562
633
|
description: "Return the browser URL a user needs to connect GitHub or GitLab access for Apex. This tool never opens a browser.",
|
|
@@ -576,16 +647,22 @@ function registerTools(server) {
|
|
|
576
647
|
cwd: targetCwd,
|
|
577
648
|
...payload,
|
|
578
649
|
};
|
|
579
|
-
}, (value) => `Fetched the ${String(value.provider)} connection URL
|
|
650
|
+
}, (value) => `Fetched the ${String(value.provider)} connection URL.`, {
|
|
651
|
+
cwd,
|
|
652
|
+
company,
|
|
653
|
+
provider,
|
|
654
|
+
}));
|
|
580
655
|
}
|
|
581
656
|
export async function runMcpServer() {
|
|
582
657
|
const server = new McpServer({
|
|
583
658
|
name: "apex-cli",
|
|
584
659
|
version: APEX_CLI_VERSION,
|
|
585
660
|
});
|
|
661
|
+
const invocation = createMcpServerTelemetryInvocation();
|
|
586
662
|
registerResources(server);
|
|
587
663
|
registerTools(server);
|
|
588
664
|
const transport = new StdioServerTransport();
|
|
665
|
+
emitMcpServerStarted(invocation);
|
|
589
666
|
await server.connect(transport);
|
|
590
667
|
}
|
|
591
668
|
export const testing = {
|