guardcmd-mcp 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sahil Baligar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # guardcmd-mcp
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server for
4
+ [GuardCMD](https://guardcmd.ai). It lets Claude Code, Cursor, and your own AI
5
+ apps check actions for abuse, screen prompts, authorize agent tool calls, and
6
+ manage GuardCMD projects and policies.
7
+
8
+ It runs locally over stdio with **your own** GuardCMD API key and calls the
9
+ GuardCMD API. Requires Node.js 20 or newer.
10
+
11
+ ## Setup
12
+
13
+ Create an API key at [guardcmd.ai](https://guardcmd.ai), then:
14
+
15
+ **Claude Code**
16
+
17
+ ```bash
18
+ claude mcp add guardcmd -e GUARDCMD_API_KEY=ag_live_... -- npx -y guardcmd-mcp
19
+ ```
20
+
21
+ **Cursor, Claude Desktop, and other clients** (`mcp.json`)
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "guardcmd": {
27
+ "command": "npx",
28
+ "args": ["-y", "guardcmd-mcp"],
29
+ "env": { "GUARDCMD_API_KEY": "ag_live_..." }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ | Environment variable | Required | Purpose |
36
+ | --- | --- | --- |
37
+ | `GUARDCMD_API_KEY` | yes | Your GuardCMD API key |
38
+ | `API_BASE_URL` | no | API origin (default `https://api.guardcmd.com`) |
39
+
40
+ The key grants the same access as your dashboard key. Keep it out of shared
41
+ config files and version control.
42
+
43
+ ## Tools
44
+
45
+ **Runtime checks**
46
+
47
+ - `check_abuse`: score an action (signup, login, checkout, ...) and get
48
+ `allow | challenge | throttle | review | block` with reasons.
49
+ - `screen_prompt`: screen a prompt for injection, data exfiltration, cost
50
+ abuse, and harmful requests.
51
+ - `authorize_tool_call`: before an agent runs a tool, get
52
+ `allow | require_approval | deny` based on the call, the user's intent, and
53
+ untrusted context.
54
+
55
+ **Projects and findings**
56
+
57
+ - `list_projects`, `get_usage`
58
+ - `scan_repository`, `create_scan`, `get_scan`: map abuse surfaces in a repo
59
+ - `list_abuse_surfaces`, `list_recommendations`
60
+ - `create_protection_pr`: preview a protection patch, or open a pull request
61
+ when called with `openPr: true`
62
+
63
+ **Policies and decisions**
64
+
65
+ - `list_policies`, `get_policy`, `set_rate_limit`
66
+ - `promote_policy` (requires `acknowledgeUserImpact: true`), `rollback_policy`
67
+ - `list_decisions`, `explain_decision`, `submit_feedback`, `get_metrics`
68
+
69
+ Tools that change state are labeled in their descriptions. Your MCP client
70
+ asks before calling them unless you have allowed them.
71
+
72
+ ## Renamed from AbuseGuard
73
+
74
+ The `abuseguard-mcp` binary and the `ABUSEGUARD_API_KEY` variable still work
75
+ as deprecated aliases.
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Thin HTTP client for the GuardCMD Cloud API (data plane).
3
+ *
4
+ * Talks to `API_BASE_URL` using the caller's `GUARDCMD_API_KEY`.
5
+ * Endpoints (see platform/CONTRACT.md):
6
+ * - POST /v1/evaluate -> decision
7
+ * - GET /v1/usage -> plan/used/remaining
8
+ *
9
+ * Repository-scan control plane (account-scoped, same API key):
10
+ * - GET /v1/projects -> list projects
11
+ * - POST /v1/projects -> create project
12
+ * - POST /v1/projects/:id/scan-url -> scan a PUBLIC GitHub repo by URL
13
+ * - GET /v1/scans/:id -> scan + counts
14
+ * - GET /v1/scans/:id/surfaces -> surfaces for a scan
15
+ * - GET /v1/projects/:id/surfaces -> surfaces for latest completed scan
16
+ * - GET /v1/surfaces/:id -> a single surface
17
+ * - GET /v1/surfaces/:id/recommendations -> recommendations for a surface
18
+ * - POST /v1/recommendations/:id/autofix -> generate a PR-ready patch for a recommendation
19
+ * - POST /v1/recommendations/:id/pull-request -> open a REAL GitHub PR for a recommendation
20
+ *
21
+ * Policy + decision control plane (account-scoped, same API key):
22
+ * - GET /v1/policies?projectId= -> list policies
23
+ * - GET /v1/policies/:id -> policy + version history
24
+ * - PATCH /v1/policies/:id -> update config (optimistic concurrency)
25
+ * - POST /v1/policies/:id/promote -> promote to a target mode (shadow/live/...)
26
+ * - POST /v1/policies/:id/rollback -> roll back to a prior version
27
+ * - POST /v1/policies -> create a policy
28
+ * - GET /v1/decisions?... -> list recent decisions (paginated)
29
+ * - GET /v1/decisions/:id -> a single decision (signals/reasons/policy/feedback)
30
+ * - POST /v1/decisions/:id/feedback -> label a decision legitimate/abusive
31
+ * - GET /v1/metrics/summary?... -> aggregate metrics for a window
32
+ *
33
+ * All API errors are normalized to `ApiError` carrying `{ error, code, status }`
34
+ * so callers can surface them as MCP tool errors without crashing.
35
+ */
36
+ /** Shape of the structured API error body per CONTRACT.md ("All errors: { error, code }"). */
37
+ export interface ApiErrorBody {
38
+ error: string;
39
+ code: string;
40
+ }
41
+ export declare class ApiError extends Error {
42
+ readonly code: string;
43
+ readonly status: number;
44
+ constructor(message: string, code: string, status: number);
45
+ }
46
+ /** Input to POST /v1/evaluate (mirrors the API body; `action` required, rest optional). */
47
+ export interface EvaluateInput {
48
+ action: string;
49
+ actorId?: string;
50
+ ip?: string;
51
+ email?: string;
52
+ fingerprint?: string;
53
+ userAgent?: string;
54
+ content?: string;
55
+ meta?: Record<string, unknown>;
56
+ timestamp?: string;
57
+ }
58
+ /** A single contributing signal in a decision. */
59
+ export interface DecisionSignal {
60
+ signal: string;
61
+ score: number;
62
+ reasons: string[];
63
+ data?: unknown;
64
+ }
65
+ /** Decision returned by POST /v1/evaluate. */
66
+ export interface EvaluateDecision {
67
+ action: "allow" | "challenge" | "throttle" | "review" | "block";
68
+ score: number;
69
+ flagged: boolean;
70
+ enforced: boolean;
71
+ reasons: string[];
72
+ signals: DecisionSignal[];
73
+ requestId: string;
74
+ }
75
+ /** Input to POST /v1/guard/prompt (TypeSafe-backed prompt screening). */
76
+ export interface ScreenPromptInput {
77
+ prompt: string;
78
+ purpose?: string;
79
+ context?: Record<string, unknown>;
80
+ actorId?: string;
81
+ projectId?: string;
82
+ environment?: string;
83
+ }
84
+ /** Result of POST /v1/guard/prompt. */
85
+ export interface ScreenPromptResult {
86
+ id: string | null;
87
+ decision: "allow" | "review" | "block";
88
+ score: number;
89
+ reasons: string[];
90
+ signals: Record<string, number>;
91
+ degraded: boolean;
92
+ latencyMs: number;
93
+ }
94
+ /** Input to POST /v1/guard/tool-call (agent tool-call authorization evidence). */
95
+ export interface AuthorizeToolCallInput {
96
+ tool: {
97
+ name: string;
98
+ mutating: boolean;
99
+ description?: string;
100
+ };
101
+ args?: unknown;
102
+ userIntent?: string;
103
+ untrustedContext?: string;
104
+ actorId?: string;
105
+ projectId?: string;
106
+ environment?: string;
107
+ }
108
+ /** Result of POST /v1/guard/tool-call. */
109
+ export interface AuthorizeToolCallResult {
110
+ id: string | null;
111
+ decision: "allow" | "require_approval" | "deny";
112
+ reasons: string[];
113
+ evidence: Record<string, unknown>;
114
+ degraded: boolean;
115
+ latencyMs: number;
116
+ }
117
+ /** Usage returned by GET /v1/usage. */
118
+ export interface UsageResult {
119
+ plan: string;
120
+ periodStart?: string;
121
+ periodEnd?: string;
122
+ used: number;
123
+ limit: number | null;
124
+ remaining: number | null;
125
+ }
126
+ /** A project — a container for scans of a single repository/app. */
127
+ export interface Project {
128
+ id: string;
129
+ name: string;
130
+ defaultEnvironment?: string;
131
+ createdAt?: string;
132
+ }
133
+ /** A single evidence item attached to a surface (e.g. a matched call site). */
134
+ export interface SurfaceEvidence {
135
+ kind: string;
136
+ [key: string]: unknown;
137
+ }
138
+ /** A scan of a repository checkout the server created (see {@link GuardCMDClient.scanUrl}). */
139
+ export interface Scan {
140
+ id: string;
141
+ projectId: string;
142
+ status: string;
143
+ scannerVersion?: string;
144
+ stats?: Record<string, unknown>;
145
+ warnings?: string[];
146
+ error?: string | null;
147
+ /** Present on GET /v1/scans/:id. */
148
+ surfaceCount?: number;
149
+ recommendationCount?: number;
150
+ [key: string]: unknown;
151
+ }
152
+ /** An abuse surface discovered by a scan (an endpoint/action that can be abused). */
153
+ export interface Surface {
154
+ id: string;
155
+ surfaceKey: string;
156
+ route?: string;
157
+ method?: string;
158
+ action?: string;
159
+ surfaceType?: string;
160
+ exposure?: string;
161
+ abuseClasses?: string[];
162
+ confidence?: number;
163
+ impact?: string;
164
+ priority?: string;
165
+ priorityScore?: number;
166
+ evidence?: SurfaceEvidence[];
167
+ [key: string]: unknown;
168
+ }
169
+ /** A hardening recommendation for a surface. */
170
+ export interface Recommendation {
171
+ id: string;
172
+ title: string;
173
+ summary?: string;
174
+ suggestedPolicy?: unknown;
175
+ controls?: unknown;
176
+ priority?: string;
177
+ priorityScore?: number;
178
+ status?: string;
179
+ [key: string]: unknown;
180
+ }
181
+ /** A whole-file change in an autofix: the file's full text before and after the edit. */
182
+ export interface FileEdit {
183
+ path: string;
184
+ before: string;
185
+ after: string;
186
+ }
187
+ /**
188
+ * The generated patch for a recommendation (POST /v1/recommendations/:id/autofix). It's a
189
+ * review-only artifact — a unified diff, per-file before/after edits, and the `.env` keys the
190
+ * integration needs. It never writes files or opens a PR.
191
+ */
192
+ export interface AutofixResult {
193
+ surfaceId: string;
194
+ recommendationId: string;
195
+ edits: FileEdit[];
196
+ /** A unified diff across all edits, PR-ready. */
197
+ diff: string;
198
+ /** Env keys the integration needs, e.g. `["GUARDCMD_API_KEY=", "GUARDCMD_PROJECT_ID="]`. */
199
+ envAdditions: string[];
200
+ /** True iff every edited code file re-parses cleanly AND contains the inserted guard call. */
201
+ valid: boolean;
202
+ /** Non-fatal problems (e.g. "could not locate handler body"); empty on a clean result. */
203
+ warnings: string[];
204
+ /** Count of added lines across all edits. */
205
+ estimatedChangedLines: number;
206
+ [key: string]: unknown;
207
+ }
208
+ /** A real GitHub pull request opened for a recommendation (POST /v1/recommendations/:id/pull-request). */
209
+ export interface PullRequestResult {
210
+ number: number;
211
+ /** The PR's html_url. */
212
+ url: string;
213
+ /** The head branch the PR was opened from. */
214
+ headBranch: string;
215
+ }
216
+ /** Response of the open-PR endpoint: the opened PR plus a compact autofix summary. */
217
+ export interface OpenPullRequestResponse {
218
+ pullRequest: PullRequestResult;
219
+ autofix: {
220
+ valid: boolean;
221
+ estimatedChangedLines: number;
222
+ diff: string;
223
+ };
224
+ }
225
+ /** A single entry in a policy's version history. */
226
+ export interface PolicyVersion {
227
+ version: number;
228
+ mode?: string;
229
+ note?: string;
230
+ createdAt?: string;
231
+ config?: Record<string, unknown>;
232
+ [key: string]: unknown;
233
+ }
234
+ /** An anti-abuse policy for a project/action. */
235
+ export interface Policy {
236
+ id: string;
237
+ projectId?: string;
238
+ action?: string;
239
+ mode?: string;
240
+ version?: number;
241
+ config?: Record<string, unknown>;
242
+ versions?: PolicyVersion[];
243
+ createdAt?: string;
244
+ updatedAt?: string;
245
+ [key: string]: unknown;
246
+ }
247
+ /** A single velocity/rate limit for a policy. */
248
+ export interface RateLimit {
249
+ dimension: string;
250
+ limit: number;
251
+ windowSeconds: number;
252
+ }
253
+ /** Body for PATCH /v1/policies/:id (optimistic concurrency via baseVersion). */
254
+ export interface UpdatePolicyBody {
255
+ baseVersion: number;
256
+ config?: Record<string, unknown>;
257
+ note?: string;
258
+ }
259
+ /** Body for POST /v1/policies/:id/promote. */
260
+ export interface PromotePolicyBody {
261
+ targetMode: string;
262
+ acknowledgeUserImpact?: boolean;
263
+ }
264
+ /** Body for POST /v1/policies/:id/rollback. */
265
+ export interface RollbackPolicyBody {
266
+ toVersion?: number;
267
+ }
268
+ /** Body for POST /v1/policies — either adopt a recommendation, or create from scratch. */
269
+ export type CreatePolicyBody = {
270
+ recommendationId: string;
271
+ } | {
272
+ projectId: string;
273
+ action: string;
274
+ config: Record<string, unknown>;
275
+ mode?: string;
276
+ };
277
+ /** Filters for GET /v1/decisions. */
278
+ export interface DecisionFilters {
279
+ projectId?: string;
280
+ action?: string;
281
+ mode?: string;
282
+ enforced?: boolean;
283
+ limit?: number;
284
+ cursor?: string;
285
+ }
286
+ /** A compact decision row in a decision list. */
287
+ export interface DecisionSummary {
288
+ id: string;
289
+ action?: string;
290
+ outcome?: string;
291
+ score?: number;
292
+ mode?: string;
293
+ enforced?: boolean;
294
+ createdAt?: string;
295
+ [key: string]: unknown;
296
+ }
297
+ /** Response of GET /v1/decisions. */
298
+ export interface DecisionList {
299
+ decisions: DecisionSummary[];
300
+ nextCursor?: string | null;
301
+ }
302
+ /** A full decision (GET /v1/decisions/:id): signals, reasons, policy, feedback. */
303
+ export interface Decision {
304
+ id: string;
305
+ action?: string;
306
+ outcome?: string;
307
+ score?: number;
308
+ mode?: string;
309
+ enforced?: boolean;
310
+ reasons?: string[];
311
+ signals?: DecisionSignal[];
312
+ policy?: Record<string, unknown> | null;
313
+ feedback?: {
314
+ label?: string;
315
+ [key: string]: unknown;
316
+ } | null;
317
+ createdAt?: string;
318
+ [key: string]: unknown;
319
+ }
320
+ /** Aggregate metrics (GET /v1/metrics/summary). */
321
+ export interface MetricsSummary {
322
+ projectId?: string;
323
+ window?: string;
324
+ [key: string]: unknown;
325
+ }
326
+ export interface GuardCMDClientOptions {
327
+ baseUrl: string;
328
+ apiKey: string;
329
+ /** Optional custom fetch (used by tests). Defaults to global fetch. */
330
+ fetchImpl?: typeof fetch;
331
+ /** Request timeout in ms (default 15000). */
332
+ timeoutMs?: number;
333
+ }
334
+ export declare class GuardCMDClient {
335
+ private readonly baseUrl;
336
+ private readonly apiKey;
337
+ private readonly fetchImpl;
338
+ private readonly timeoutMs;
339
+ constructor(opts: GuardCMDClientOptions);
340
+ evaluate(input: EvaluateInput): Promise<EvaluateDecision>;
341
+ /** POST /v1/guard/prompt: screen a prompt headed for an LLM. */
342
+ screenPrompt(input: ScreenPromptInput): Promise<ScreenPromptResult>;
343
+ /** POST /v1/guard/tool-call: evidence-based authorization for an agent tool call. */
344
+ authorizeToolCall(input: AuthorizeToolCallInput): Promise<AuthorizeToolCallResult>;
345
+ usage(): Promise<UsageResult>;
346
+ /** GET /v1/projects — the account's projects. */
347
+ listProjects(): Promise<Project[]>;
348
+ /** POST /v1/projects — create a project. */
349
+ createProject(name: string): Promise<Project>;
350
+ /**
351
+ * POST /v1/projects/:id/scan-url — scan a PUBLIC GitHub repository by URL.
352
+ *
353
+ * This REPLACES the old `createScan(projectId, path)`, which posted a
354
+ * server-filesystem `path` to `POST /v1/projects/:id/scans`. That endpoint took its scan
355
+ * root straight from the request body, which made it an arbitrary-file-read primitive for
356
+ * anyone holding a key (scan a host directory, then read whole file contents back out
357
+ * through the autofix endpoint). The API now answers it with
358
+ * 501 `local_path_scans_disabled` unless a development-only switch is set, so a client
359
+ * method for it would only ever produce an error.
360
+ *
361
+ * The server clones the repo itself into a disposable sandbox — the caller never names a
362
+ * path. Synchronous MVP: the response is already a terminal Scan (`completed` or
363
+ * `failed`). A malformed URL is 400 `invalid_github_url`; a private/missing repo is
364
+ * 422 `repo_unavailable`. Both arrive as {@link ApiError} with `code` intact.
365
+ */
366
+ scanUrl(projectId: string, url: string): Promise<Scan>;
367
+ /** GET /v1/scans/:id — a scan plus surface/recommendation counts. */
368
+ getScan(scanId: string): Promise<Scan>;
369
+ /** GET /v1/projects/:id/surfaces — surfaces from the latest completed scan. */
370
+ listProjectSurfaces(projectId: string): Promise<Surface[]>;
371
+ /** GET /v1/scans/:id/surfaces — surfaces discovered by a specific scan. */
372
+ listScanSurfaces(scanId: string): Promise<Surface[]>;
373
+ /** GET /v1/surfaces/:id — a single surface. */
374
+ getSurface(surfaceId: string): Promise<Surface>;
375
+ /** GET /v1/surfaces/:id/recommendations — hardening recommendations. */
376
+ listSurfaceRecommendations(surfaceId: string): Promise<Recommendation[]>;
377
+ /**
378
+ * POST /v1/recommendations/:id/autofix — generate a PR-ready patch/diff for a recommendation.
379
+ * The server re-scans the recommendation's source checkout and returns a review-only patch;
380
+ * it does not write files or open a real PR.
381
+ */
382
+ generateAutofix(recommendationId: string): Promise<AutofixResult>;
383
+ /**
384
+ * POST /v1/recommendations/:id/pull-request — open a REAL GitHub pull request for a
385
+ * recommendation. The server re-scans a sandboxed checkout of the project's linked repo,
386
+ * generates + VALIDATES the patch, and only then opens the PR. Returns the opened PR
387
+ * (number, url, headBranch) plus a compact autofix summary. Requires a linked GitHub repo
388
+ * (else 409) and a configured GitHub App (else 501).
389
+ */
390
+ openPullRequest(recommendationId: string, ref?: string): Promise<OpenPullRequestResponse>;
391
+ /** GET /v1/policies?projectId= — the account's policies (optionally scoped to a project). */
392
+ listPolicies(projectId?: string): Promise<Policy[]>;
393
+ /** GET /v1/policies/:id — a policy plus its version history. */
394
+ getPolicy(policyId: string): Promise<Policy>;
395
+ /**
396
+ * PATCH /v1/policies/:id — update a policy's config with optimistic concurrency.
397
+ * `baseVersion` must match the current version or the API returns 409 (stale).
398
+ * Produces a new draft/shadow version; it does not enforce by itself.
399
+ */
400
+ updatePolicy(policyId: string, body: UpdatePolicyBody): Promise<Policy>;
401
+ /**
402
+ * POST /v1/policies/:id/promote — promote a policy to `targetMode`. Promoting to `live`
403
+ * REQUIRES `acknowledgeUserImpact: true` (the API returns 422 without it).
404
+ */
405
+ promotePolicy(policyId: string, body: PromotePolicyBody): Promise<Policy>;
406
+ /** POST /v1/policies/:id/rollback — roll a policy back to a prior version (default: previous). */
407
+ rollbackPolicy(policyId: string, body?: RollbackPolicyBody): Promise<Policy>;
408
+ /** POST /v1/policies — create a policy (from a recommendation, or from scratch). */
409
+ createPolicy(body: CreatePolicyBody): Promise<Policy>;
410
+ /** GET /v1/decisions — recent decisions (paginated via nextCursor). */
411
+ listDecisions(filters?: DecisionFilters): Promise<DecisionList>;
412
+ /** GET /v1/decisions/:id — a full decision (signals, reasons, policy, feedback). */
413
+ getDecision(decisionId: string): Promise<Decision>;
414
+ /** POST /v1/decisions/:id/feedback — label a decision `legitimate` or `abusive`. */
415
+ submitFeedback(decisionId: string, label: "legitimate" | "abusive"): Promise<Decision>;
416
+ /** GET /v1/metrics/summary — aggregate metrics for a project/window. */
417
+ getMetrics(opts?: {
418
+ projectId?: string;
419
+ window?: string;
420
+ }): Promise<MetricsSummary>;
421
+ private request;
422
+ }
423
+ /** @deprecated use {@link GuardCMDClient} (the product was formerly AbuseGuard). */
424
+ export declare const AbuseGuardClient: typeof GuardCMDClient;
425
+ /** @deprecated use {@link GuardCMDClient}. */
426
+ export type AbuseGuardClient = GuardCMDClient;