@clavex/mcp-server 1.0.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/README.md +107 -0
- package/dist/client.d.ts +38 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +121 -0
- package/dist/client.js.map +1 -0
- package/dist/helpers.d.ts +14 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +44 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/access_reviews.d.ts +3 -0
- package/dist/tools/access_reviews.d.ts.map +1 -0
- package/dist/tools/access_reviews.js +131 -0
- package/dist/tools/access_reviews.js.map +1 -0
- package/dist/tools/ai.d.ts +3 -0
- package/dist/tools/ai.d.ts.map +1 -0
- package/dist/tools/ai.js +443 -0
- package/dist/tools/ai.js.map +1 -0
- package/dist/tools/ciba.d.ts +3 -0
- package/dist/tools/ciba.d.ts.map +1 -0
- package/dist/tools/ciba.js +85 -0
- package/dist/tools/ciba.js.map +1 -0
- package/dist/tools/clients.d.ts +3 -0
- package/dist/tools/clients.d.ts.map +1 -0
- package/dist/tools/clients.js +124 -0
- package/dist/tools/clients.js.map +1 -0
- package/dist/tools/developer.d.ts +3 -0
- package/dist/tools/developer.d.ts.map +1 -0
- package/dist/tools/developer.js +580 -0
- package/dist/tools/developer.js.map +1 -0
- package/dist/tools/fga.d.ts +3 -0
- package/dist/tools/fga.d.ts.map +1 -0
- package/dist/tools/fga.js +126 -0
- package/dist/tools/fga.js.map +1 -0
- package/dist/tools/groups.d.ts +3 -0
- package/dist/tools/groups.d.ts.map +1 -0
- package/dist/tools/groups.js +135 -0
- package/dist/tools/groups.js.map +1 -0
- package/dist/tools/idps.d.ts +3 -0
- package/dist/tools/idps.d.ts.map +1 -0
- package/dist/tools/idps.js +98 -0
- package/dist/tools/idps.js.map +1 -0
- package/dist/tools/orgs.d.ts +3 -0
- package/dist/tools/orgs.d.ts.map +1 -0
- package/dist/tools/orgs.js +90 -0
- package/dist/tools/orgs.js.map +1 -0
- package/dist/tools/pam.d.ts +3 -0
- package/dist/tools/pam.d.ts.map +1 -0
- package/dist/tools/pam.js +238 -0
- package/dist/tools/pam.js.map +1 -0
- package/dist/tools/policies.d.ts +3 -0
- package/dist/tools/policies.d.ts.map +1 -0
- package/dist/tools/policies.js +173 -0
- package/dist/tools/policies.js.map +1 -0
- package/dist/tools/ssf.d.ts +3 -0
- package/dist/tools/ssf.d.ts.map +1 -0
- package/dist/tools/ssf.js +65 -0
- package/dist/tools/ssf.js.map +1 -0
- package/dist/tools/users.d.ts +3 -0
- package/dist/tools/users.d.ts.map +1 -0
- package/dist/tools/users.js +144 -0
- package/dist/tools/users.js.map +1 -0
- package/package.json +48 -0
- package/src/client.ts +148 -0
- package/src/helpers.ts +45 -0
- package/src/index.ts +63 -0
- package/src/tools/access_reviews.ts +163 -0
- package/src/tools/ai.ts +581 -0
- package/src/tools/ciba.ts +109 -0
- package/src/tools/clients.ts +168 -0
- package/src/tools/developer.ts +661 -0
- package/src/tools/fga.ts +148 -0
- package/src/tools/groups.ts +200 -0
- package/src/tools/idps.ts +137 -0
- package/src/tools/orgs.ts +119 -0
- package/src/tools/pam.ts +285 -0
- package/src/tools/policies.ts +233 -0
- package/src/tools/ssf.ts +82 -0
- package/src/tools/users.ts +202 -0
- package/tsconfig.json +18 -0
package/src/tools/ai.ts
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getClient } from "../client.js";
|
|
4
|
+
import { handleError } from "../helpers.js";
|
|
5
|
+
|
|
6
|
+
export function registerAITools(server: McpServer): void {
|
|
7
|
+
// ── AI config ──────────────────────────────────────────────────────────────
|
|
8
|
+
server.registerTool(
|
|
9
|
+
"clavex_ai_configure",
|
|
10
|
+
{
|
|
11
|
+
title: "Configure Anthropic API Key",
|
|
12
|
+
description: `Set or clear the Anthropic API key for AI-assisted admin features in an organization.
|
|
13
|
+
|
|
14
|
+
AI features require a valid Anthropic API key (starts with 'sk-ant-').
|
|
15
|
+
The key is stored securely and used only for server-side AI calls (never exposed to end-users).
|
|
16
|
+
|
|
17
|
+
Once configured, these AI features become available:
|
|
18
|
+
- clavex_ai_suggest_policy — NL → auth-flow policy JSON
|
|
19
|
+
- clavex_ai_suggest_fga_model — NL → OpenFGA authorization model
|
|
20
|
+
- clavex_ai_explain_anomaly — risk signals → NL explanation (NIS2)
|
|
21
|
+
- clavex_ai_audit_query — natural language → audit log search
|
|
22
|
+
- clavex_ai_suggest_access_review — pre-fill keep/revoke decisions
|
|
23
|
+
- clavex_ai_suggest_lifecycle_rule — NL → JML lifecycle rule JSON
|
|
24
|
+
- clavex_ai_suggest_dpia — processing activity → GDPR Art.35 DPIA
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
- org_id: Organization UUID
|
|
28
|
+
- anthropic_api_key: The key (sk-ant-...). Pass null or "" to remove the key.
|
|
29
|
+
|
|
30
|
+
Use when: "configure Anthropic key for org <id>", "set AI key to sk-ant-..."`,
|
|
31
|
+
inputSchema: {
|
|
32
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
33
|
+
anthropic_api_key: z.string().nullable().describe("Anthropic API key (sk-ant-...) — null to clear"),
|
|
34
|
+
},
|
|
35
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
36
|
+
},
|
|
37
|
+
async ({ org_id, anthropic_api_key }) =>
|
|
38
|
+
handleError(async () => {
|
|
39
|
+
const result = await getClient().put<{ configured: boolean }>(
|
|
40
|
+
getClient().orgPath(org_id, "/ai/config"),
|
|
41
|
+
{ anthropic_api_key },
|
|
42
|
+
);
|
|
43
|
+
return result.configured
|
|
44
|
+
? "Anthropic API key configured successfully. AI features are now enabled."
|
|
45
|
+
: "Anthropic API key cleared. AI features are disabled.";
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// ── Suggest policy ─────────────────────────────────────────────────────────
|
|
50
|
+
server.registerTool(
|
|
51
|
+
"clavex_ai_suggest_policy",
|
|
52
|
+
{
|
|
53
|
+
title: "AI: Suggest Auth-Flow Policy",
|
|
54
|
+
description: `Convert a natural language access control requirement into a Clavex auth-flow policy JSON.
|
|
55
|
+
|
|
56
|
+
The AI (Claude Opus 4.7) produces a structured policy with rules, conditions, and actions.
|
|
57
|
+
You can review the result and optionally save it via clavex_create_policy.
|
|
58
|
+
|
|
59
|
+
Requires an Anthropic API key — configure via clavex_ai_configure.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
"block logins from Russia and China"
|
|
63
|
+
"require MFA for all admin users outside office hours"
|
|
64
|
+
"allow only users from Italy, Germany and France; block everyone else"
|
|
65
|
+
"deny logins from IP ranges 185.0.0.0/8 and 45.0.0.0/8"
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
- org_id: Organization UUID
|
|
69
|
+
- description: Natural language description of the access control requirement`,
|
|
70
|
+
inputSchema: {
|
|
71
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
72
|
+
description: z.string().describe("Natural language description of the policy requirement"),
|
|
73
|
+
},
|
|
74
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
75
|
+
},
|
|
76
|
+
async ({ org_id, description }) =>
|
|
77
|
+
handleError(async () => {
|
|
78
|
+
const result = await getClient().post<{ policy?: unknown; raw?: string; error?: string }>(
|
|
79
|
+
getClient().orgPath(org_id, "/ai/suggest-policy"),
|
|
80
|
+
{ description },
|
|
81
|
+
);
|
|
82
|
+
if (result.error) {
|
|
83
|
+
return `⚠️ AI returned non-JSON output. Review and adjust the description.\n\nRaw output:\n${result.raw}`;
|
|
84
|
+
}
|
|
85
|
+
return `Suggested policy:\n\n\`\`\`json\n${JSON.stringify(result.policy, null, 2)}\n\`\`\`\n\nUse \`clavex_create_policy\` to save individual rules from this policy.`;
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// ── Suggest FGA model ──────────────────────────────────────────────────────
|
|
90
|
+
server.registerTool(
|
|
91
|
+
"clavex_ai_suggest_fga_model",
|
|
92
|
+
{
|
|
93
|
+
title: "AI: Generate OpenFGA Authorization Model",
|
|
94
|
+
description: `Convert a natural language description of access control requirements into an OpenFGA 1.1 authorization model JSON.
|
|
95
|
+
|
|
96
|
+
Use this to bootstrap a Zanzibar-style ReBAC model without learning the OpenFGA schema syntax.
|
|
97
|
+
The result can be imported via clavex_fga_write_tuple after uploading the model.
|
|
98
|
+
|
|
99
|
+
Requires an Anthropic API key — configure via clavex_ai_configure.
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
"A document management system: owners can read and write; editors can write; viewers can only read"
|
|
103
|
+
"A multi-tenant SaaS: org admins manage members; members access resources in their org only"
|
|
104
|
+
"GitHub-style: repo owners grant write/read to teams; teams inherit from org membership"
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
- org_id: Organization UUID
|
|
108
|
+
- description: Natural language description of the access control structure`,
|
|
109
|
+
inputSchema: {
|
|
110
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
111
|
+
description: z.string().describe("Natural language description of the authorization model"),
|
|
112
|
+
},
|
|
113
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
114
|
+
},
|
|
115
|
+
async ({ org_id, description }) =>
|
|
116
|
+
handleError(async () => {
|
|
117
|
+
const result = await getClient().post<{ model?: unknown; raw?: string; error?: string }>(
|
|
118
|
+
getClient().orgPath(org_id, "/ai/suggest-fga-model"),
|
|
119
|
+
{ description },
|
|
120
|
+
);
|
|
121
|
+
if (result.error) {
|
|
122
|
+
return `⚠️ AI returned non-JSON output.\n\nRaw output:\n${result.raw}`;
|
|
123
|
+
}
|
|
124
|
+
return `Generated OpenFGA authorization model:\n\n\`\`\`json\n${JSON.stringify(result.model, null, 2)}\n\`\`\`\n\nUse \`PUT /fga/model\` or the FGA console to upload this model.`;
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// ── Explain anomaly ────────────────────────────────────────────────────────
|
|
129
|
+
server.registerTool(
|
|
130
|
+
"clavex_ai_explain_anomaly",
|
|
131
|
+
{
|
|
132
|
+
title: "AI: Explain Risk Anomaly (NIS2)",
|
|
133
|
+
description: `Explain a login risk anomaly in natural language, suitable for NIS2 Art.21 security reporting.
|
|
134
|
+
|
|
135
|
+
Provide the risk signals from a login event; the AI produces a human-readable summary
|
|
136
|
+
and detailed explanation for security teams, plus a suggested action.
|
|
137
|
+
|
|
138
|
+
Requires an Anthropic API key — configure via clavex_ai_configure.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
- org_id: Organization UUID
|
|
142
|
+
- signals: JSON object with risk signals, e.g.:
|
|
143
|
+
{ "risk_score": 85, "country": "RU", "new_country": true, "user_email": "alice@example.com",
|
|
144
|
+
"ip": "185.22.1.1", "user_agent": "...", "is_tor": false, "abuseipdb_score": 67 }`,
|
|
145
|
+
inputSchema: {
|
|
146
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
147
|
+
signals: z.record(z.unknown()).describe("Risk signals object from a login event"),
|
|
148
|
+
},
|
|
149
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
150
|
+
},
|
|
151
|
+
async ({ org_id, signals }) =>
|
|
152
|
+
handleError(async () => {
|
|
153
|
+
const result = await getClient().post<{
|
|
154
|
+
summary?: string;
|
|
155
|
+
explanation?: string;
|
|
156
|
+
suggested_action?: string;
|
|
157
|
+
confidence?: string;
|
|
158
|
+
}>(
|
|
159
|
+
getClient().orgPath(org_id, "/ai/explain-anomaly"),
|
|
160
|
+
{ signals },
|
|
161
|
+
);
|
|
162
|
+
const lines = [
|
|
163
|
+
`**Summary:** ${result.summary ?? "—"}`,
|
|
164
|
+
`**Suggested action:** \`${result.suggested_action ?? "—"}\` (confidence: ${result.confidence ?? "—"})`,
|
|
165
|
+
`\n**Explanation:**\n${result.explanation ?? "—"}`,
|
|
166
|
+
];
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// ── NL audit query ─────────────────────────────────────────────────────────
|
|
172
|
+
server.registerTool(
|
|
173
|
+
"clavex_ai_audit_query",
|
|
174
|
+
{
|
|
175
|
+
title: "AI: Natural Language Audit Log Query",
|
|
176
|
+
description: `Search the audit log using plain natural language. The AI translates your question into structured query filters and executes the search.
|
|
177
|
+
|
|
178
|
+
No need to know the exact filter parameters — just describe what you're looking for.
|
|
179
|
+
|
|
180
|
+
Requires an Anthropic API key — configure via clavex_ai_configure.
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
"show all failed logins in the last 24 hours"
|
|
184
|
+
"which admin created the most users this week?"
|
|
185
|
+
"find all MFA enrollment events for alice@example.com"
|
|
186
|
+
"show me password resets from last month"
|
|
187
|
+
"list all SCIM provisioning failures"
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
- org_id: Organization UUID
|
|
191
|
+
- query: Natural language question about the audit log`,
|
|
192
|
+
inputSchema: {
|
|
193
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
194
|
+
query: z.string().describe("Natural language audit question, e.g. 'show failed logins today'"),
|
|
195
|
+
},
|
|
196
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
197
|
+
},
|
|
198
|
+
async ({ org_id, query }) =>
|
|
199
|
+
handleError(async () => {
|
|
200
|
+
const result = await getClient().post<{
|
|
201
|
+
nl_query: string;
|
|
202
|
+
applied_filter: Record<string, unknown>;
|
|
203
|
+
results: { events: Array<Record<string, unknown>>; next_cursor?: number };
|
|
204
|
+
}>(
|
|
205
|
+
getClient().orgPath(org_id, "/ai/nl-audit-query"),
|
|
206
|
+
{ query },
|
|
207
|
+
);
|
|
208
|
+
const events = result.results?.events ?? [];
|
|
209
|
+
const filterStr = JSON.stringify(result.applied_filter, null, 2);
|
|
210
|
+
if (events.length === 0) {
|
|
211
|
+
return `No audit events found.\n\nApplied filter:\n\`\`\`json\n${filterStr}\n\`\`\``;
|
|
212
|
+
}
|
|
213
|
+
const rows = events
|
|
214
|
+
.map((e) => `| ${e.action ?? "—"} | ${e.actor_email ?? "—"} | ${e.resource_type ?? "—"} | ${e.status ?? "—"} | ${String(e.created_at ?? "—").substring(0, 19)} |`)
|
|
215
|
+
.join("\n");
|
|
216
|
+
return `Found **${events.length}** events.\n\nApplied filter: \`${JSON.stringify(result.applied_filter)}\`\n\n| Action | Actor | Resource | Status | Timestamp |\n| --- | --- | --- | --- | --- |\n${rows}`;
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// ── Suggest access review ──────────────────────────────────────────────────
|
|
221
|
+
server.registerTool(
|
|
222
|
+
"clavex_ai_suggest_access_review",
|
|
223
|
+
{
|
|
224
|
+
title: "AI: Pre-fill Access Review Decisions",
|
|
225
|
+
description: `Analyze pending access review items and suggest keep/revoke decisions with reasoning.
|
|
226
|
+
|
|
227
|
+
The AI evaluates each (user, role) pair and suggests whether access should be retained or revoked,
|
|
228
|
+
based on role sensitivity and other context. This pre-fills reviewer decisions to speed up campaigns.
|
|
229
|
+
|
|
230
|
+
Requires an Anthropic API key — configure via clavex_ai_configure.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
- org_id: Organization UUID
|
|
234
|
+
- campaign_id: Access review campaign UUID (use clavex_list_access_reviews to find it)
|
|
235
|
+
|
|
236
|
+
Returns: JSON array of suggestions with item_id, suggestion (keep/revoke), and reason.`,
|
|
237
|
+
inputSchema: {
|
|
238
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
239
|
+
campaign_id: z.string().uuid().describe("Campaign UUID"),
|
|
240
|
+
},
|
|
241
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
242
|
+
},
|
|
243
|
+
async ({ org_id, campaign_id }) =>
|
|
244
|
+
handleError(async () => {
|
|
245
|
+
const result = await getClient().post<{
|
|
246
|
+
campaign_id: string;
|
|
247
|
+
item_count: number;
|
|
248
|
+
suggestions: Array<{ item_id: string; suggestion: string; reason: string }>;
|
|
249
|
+
}>(
|
|
250
|
+
getClient().orgPath(org_id, "/ai/suggest-access-review"),
|
|
251
|
+
{ campaign_id },
|
|
252
|
+
);
|
|
253
|
+
const suggestions = result.suggestions ?? [];
|
|
254
|
+
if (suggestions.length === 0) {
|
|
255
|
+
return "No pending items to review in this campaign.";
|
|
256
|
+
}
|
|
257
|
+
const lines = suggestions.map(
|
|
258
|
+
(s) => `- **${s.item_id}** → \`${s.suggestion.toUpperCase()}\`: ${s.reason}`,
|
|
259
|
+
);
|
|
260
|
+
return `AI suggestions for **${result.item_count}** pending items:\n\n${lines.join("\n")}`;
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// ── Suggest lifecycle rule ─────────────────────────────────────────────────
|
|
265
|
+
server.registerTool(
|
|
266
|
+
"clavex_ai_suggest_lifecycle_rule",
|
|
267
|
+
{
|
|
268
|
+
title: "AI: Generate JML Lifecycle Rule",
|
|
269
|
+
description: `Convert a natural language description into a Clavex JML (Joiner/Mover/Leaver) lifecycle rule JSON.
|
|
270
|
+
|
|
271
|
+
Lowers the barrier for HR/IT admins to automate provisioning without knowing the rule syntax.
|
|
272
|
+
|
|
273
|
+
Requires an Anthropic API key — configure via clavex_ai_configure.
|
|
274
|
+
|
|
275
|
+
Examples:
|
|
276
|
+
"When a new user joins Engineering, give them the Developer role and add them to the Dev-Tools group"
|
|
277
|
+
"When someone's job title changes to Manager, assign them the Team-Lead role"
|
|
278
|
+
"When a user is deactivated, revoke all their roles immediately"
|
|
279
|
+
"When SCIM deprovisions a user, remove them from all groups"
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
- org_id: Organization UUID
|
|
283
|
+
- description: Natural language description of the automation rule`,
|
|
284
|
+
inputSchema: {
|
|
285
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
286
|
+
description: z.string().describe("Natural language description of the JML automation"),
|
|
287
|
+
},
|
|
288
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
289
|
+
},
|
|
290
|
+
async ({ org_id, description }) =>
|
|
291
|
+
handleError(async () => {
|
|
292
|
+
const result = await getClient().post<{ rule?: unknown; raw?: string; error?: string }>(
|
|
293
|
+
getClient().orgPath(org_id, "/ai/suggest-lifecycle-rule"),
|
|
294
|
+
{ description },
|
|
295
|
+
);
|
|
296
|
+
if (result.error) {
|
|
297
|
+
return `⚠️ AI returned non-JSON output.\n\nRaw output:\n${result.raw}`;
|
|
298
|
+
}
|
|
299
|
+
return `Generated lifecycle rule:\n\n\`\`\`json\n${JSON.stringify(result.rule, null, 2)}\n\`\`\`\n\nUse \`POST /lifecycle-rules\` or the admin console to save this rule.`;
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// ── Suggest DPIA ───────────────────────────────────────────────────────────
|
|
304
|
+
server.registerTool(
|
|
305
|
+
"clavex_ai_suggest_dpia",
|
|
306
|
+
{
|
|
307
|
+
title: "AI: Generate GDPR Art.35 DPIA Draft",
|
|
308
|
+
description: `Generate a GDPR Article 35 Data Protection Impact Assessment (DPIA) draft from a processing activity description.
|
|
309
|
+
|
|
310
|
+
The AI produces a structured DPIA covering: purposes, legal basis, data categories, risks, mitigations,
|
|
311
|
+
technical/organisational measures, and DPO consultation requirements.
|
|
312
|
+
|
|
313
|
+
Particularly useful for DPOs who need to quickly draft DPIAs for new processing activities.
|
|
314
|
+
The output JSON can be rendered as a formatted document or reviewed interactively.
|
|
315
|
+
|
|
316
|
+
Requires an Anthropic API key — configure via clavex_ai_configure.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
- org_id: Organization UUID
|
|
320
|
+
- description: Description of the data processing activity to assess`,
|
|
321
|
+
inputSchema: {
|
|
322
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
323
|
+
description: z.string().describe("Description of the data processing activity (who, what, why, how)"),
|
|
324
|
+
},
|
|
325
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
326
|
+
},
|
|
327
|
+
async ({ org_id, description }) =>
|
|
328
|
+
handleError(async () => {
|
|
329
|
+
const result = await getClient().post<{ dpia?: Record<string, unknown>; raw?: string }>(
|
|
330
|
+
getClient().orgPath(org_id, "/ai/suggest-dpia"),
|
|
331
|
+
{ description },
|
|
332
|
+
);
|
|
333
|
+
if (!result.dpia) {
|
|
334
|
+
return `Raw DPIA output:\n${result.raw}`;
|
|
335
|
+
}
|
|
336
|
+
const d = result.dpia;
|
|
337
|
+
const risks = (d.risks as Array<Record<string, string>> ?? [])
|
|
338
|
+
.map((r) => ` - ${r.risk} (likelihood: ${r.likelihood}, severity: ${r.severity}) → ${r.mitigation}`)
|
|
339
|
+
.join("\n");
|
|
340
|
+
return [
|
|
341
|
+
`# ${d.title}`,
|
|
342
|
+
`**Status:** ${d.status} · **Residual risk:** \`${d.residual_risk}\``,
|
|
343
|
+
`**DPO consultation required:** ${d.dpo_consultation_required ? "Yes ⚠️" : "No"}`,
|
|
344
|
+
"",
|
|
345
|
+
`## Processing Activity\n${d.processing_activity}`,
|
|
346
|
+
`## Legal Basis\n${d.legal_basis}`,
|
|
347
|
+
`## Data Categories\n${(d.data_categories as string[] ?? []).join(", ")}`,
|
|
348
|
+
`## Risks\n${risks}`,
|
|
349
|
+
"",
|
|
350
|
+
`\`\`\`json\n${JSON.stringify(d, null, 2)}\n\`\`\``,
|
|
351
|
+
].join("\n");
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// ── Explain error ──────────────────────────────────────────────────────────
|
|
356
|
+
server.registerTool(
|
|
357
|
+
"clavex_ai_explain_error",
|
|
358
|
+
{
|
|
359
|
+
title: "AI: Explain OAuth2/OIDC Error",
|
|
360
|
+
description: `Explain an OAuth2/OIDC/identity protocol error code in plain language.
|
|
361
|
+
|
|
362
|
+
The AI decodes cryptic error codes (invalid_request, invalid_grant, access_denied, interaction_required, etc.)
|
|
363
|
+
with context-aware explanations, likely root causes, fix steps, and RFC/spec references.
|
|
364
|
+
|
|
365
|
+
Perfect for developers integrating OIDC/OAuth2 who receive opaque errors from the token endpoint,
|
|
366
|
+
PAR endpoint, OID4VCI credential endpoint, or other identity flows.
|
|
367
|
+
|
|
368
|
+
Requires an Anthropic API key — configure via clavex_ai_configure.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
- org_id: Organization UUID
|
|
372
|
+
- code: OAuth2/OIDC error code (e.g. invalid_request, invalid_grant, access_denied)
|
|
373
|
+
- context: Where the error occurred (e.g. "PAR endpoint", "refresh token exchange", "FAPI 2.0")
|
|
374
|
+
- description: Optional raw error_description string from the server response
|
|
375
|
+
- lang: Optional BCP-47 language code for the response`,
|
|
376
|
+
inputSchema: {
|
|
377
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
378
|
+
code: z.string().describe("OAuth2/OIDC error code, e.g. invalid_request"),
|
|
379
|
+
context: z.string().optional().describe("Context where the error occurred, e.g. 'PAR endpoint'"),
|
|
380
|
+
description: z.string().optional().describe("Raw error_description string from the response"),
|
|
381
|
+
lang: z.string().optional().describe("Response language BCP-47 code, e.g. it, fr, de"),
|
|
382
|
+
},
|
|
383
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
384
|
+
},
|
|
385
|
+
async ({ org_id, code, context, description, lang }) =>
|
|
386
|
+
handleError(async () => {
|
|
387
|
+
const result = await getClient().post<{
|
|
388
|
+
code?: string;
|
|
389
|
+
explanation?: string;
|
|
390
|
+
likely_cause?: string;
|
|
391
|
+
how_to_fix?: string[];
|
|
392
|
+
references?: string[];
|
|
393
|
+
raw?: string;
|
|
394
|
+
}>(
|
|
395
|
+
getClient().orgPath(org_id, "/ai/explain-error"),
|
|
396
|
+
{ code, context, description, lang },
|
|
397
|
+
);
|
|
398
|
+
if (result.raw) return result.raw;
|
|
399
|
+
const lines = [
|
|
400
|
+
`## Error: \`${result.code ?? code}\``,
|
|
401
|
+
...(context ? [`**Context:** ${context}`] : []),
|
|
402
|
+
"",
|
|
403
|
+
`**Explanation:** ${result.explanation ?? "—"}`,
|
|
404
|
+
"",
|
|
405
|
+
`**Likely cause:** ${result.likely_cause ?? "—"}`,
|
|
406
|
+
];
|
|
407
|
+
if (result.how_to_fix?.length) {
|
|
408
|
+
lines.push("", "**How to fix:**");
|
|
409
|
+
result.how_to_fix.forEach((step, i) => lines.push(`${i + 1}. ${step}`));
|
|
410
|
+
}
|
|
411
|
+
if (result.references?.length) {
|
|
412
|
+
lines.push("", `**References:** ${result.references.join(" · ")}`);
|
|
413
|
+
}
|
|
414
|
+
return lines.join("\n");
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// ── Audit Copilot ──────────────────────────────────────────────────────────
|
|
419
|
+
server.registerTool(
|
|
420
|
+
"clavex_audit_copilot",
|
|
421
|
+
{
|
|
422
|
+
title: "AI Audit Copilot — Compliance Query",
|
|
423
|
+
description: `Query the Clavex audit log in plain natural language. The AI generates SQL, executes it read-only, and returns results with a compliance interpretation.
|
|
424
|
+
|
|
425
|
+
Unlike the basic audit query tool, the Audit Copilot:
|
|
426
|
+
- Generates full SQL (JOINs, aggregations, GROUP BY, complex filters)
|
|
427
|
+
- Has access to PAM resources, session tables, and user data
|
|
428
|
+
- Returns an AI compliance interpretation of the results
|
|
429
|
+
- Accepts additional context (approved user lists, resource names, policies)
|
|
430
|
+
|
|
431
|
+
Designed for auditors and CISOs who need compliance answers without writing SQL.
|
|
432
|
+
|
|
433
|
+
Requires an Anthropic API key — configure via clavex_ai_configure.
|
|
434
|
+
|
|
435
|
+
Examples:
|
|
436
|
+
"PAM accesses to production resources in the last 48h from non-approved users"
|
|
437
|
+
"failed login attempts per country this week"
|
|
438
|
+
"users who enrolled MFA today but also had a failed login"
|
|
439
|
+
"break-glass activations last 30 days with approver names"
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
- org_id: Organization UUID
|
|
443
|
+
- query: Natural language compliance question
|
|
444
|
+
- context: Optional extra context (approved users, production resource names, policies)
|
|
445
|
+
- lang: Optional BCP-47 language code for the interpretation`,
|
|
446
|
+
inputSchema: {
|
|
447
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
448
|
+
query: z.string().describe("Natural language compliance question about the audit log"),
|
|
449
|
+
context: z.string().optional().describe("Additional context: approved users, resource names, etc."),
|
|
450
|
+
lang: z.string().optional().describe("Response language BCP-47 code, e.g. it, fr, de"),
|
|
451
|
+
},
|
|
452
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
453
|
+
},
|
|
454
|
+
async ({ org_id, query, context, lang }) =>
|
|
455
|
+
handleError(async () => {
|
|
456
|
+
const result = await getClient().post<{
|
|
457
|
+
query: string;
|
|
458
|
+
generated_sql: string;
|
|
459
|
+
columns: string[];
|
|
460
|
+
results: Array<Record<string, unknown>>;
|
|
461
|
+
row_count: number;
|
|
462
|
+
interpretation: string;
|
|
463
|
+
error?: string;
|
|
464
|
+
detail?: string;
|
|
465
|
+
}>(
|
|
466
|
+
getClient().orgPath(org_id, "/ai/audit-copilot"),
|
|
467
|
+
{ query, context, lang },
|
|
468
|
+
);
|
|
469
|
+
if (result.error) {
|
|
470
|
+
return `**Error:** ${result.error}\n${result.detail ? `Detail: ${result.detail}\n` : ""}${result.generated_sql ? `\n\`\`\`sql\n${result.generated_sql}\n\`\`\`` : ""}`;
|
|
471
|
+
}
|
|
472
|
+
const lines: string[] = [
|
|
473
|
+
`**Query:** ${result.query}`,
|
|
474
|
+
`\`\`\`sql\n${result.generated_sql}\n\`\`\``,
|
|
475
|
+
];
|
|
476
|
+
if (result.row_count === 0) {
|
|
477
|
+
lines.push("No results found.");
|
|
478
|
+
} else {
|
|
479
|
+
const cols = result.columns ?? [];
|
|
480
|
+
lines.push(
|
|
481
|
+
`| ${cols.join(" | ")} |`,
|
|
482
|
+
`| ${cols.map(() => "---").join(" | ")} |`,
|
|
483
|
+
...result.results.map((row) => `| ${cols.map((c) => String(row[c] ?? "—")).join(" | ")} |`),
|
|
484
|
+
`\n**${result.row_count} row(s)**`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
if (result.interpretation) {
|
|
488
|
+
lines.push(`\n**Analysis:** ${result.interpretation}`);
|
|
489
|
+
}
|
|
490
|
+
return lines.join("\n");
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
// ── Generate DCQL ──────────────────────────────────────────────────────────
|
|
495
|
+
server.registerTool(
|
|
496
|
+
"clavex_generate_dcql",
|
|
497
|
+
{
|
|
498
|
+
title: "AI: Generate OID4VP DCQL Query from Natural Language",
|
|
499
|
+
description: `Convert a natural language verifier requirement into a valid OID4VP DCQL (Digital Credentials Query Language) query.
|
|
500
|
+
|
|
501
|
+
DCQL (OID4VP §6) is the eIDAS 2 / EUDIW ARF 1.4 standard for expressing what credentials and claims
|
|
502
|
+
a verifier wants to receive from a holder wallet. It replaces the verbose Presentation Exchange format.
|
|
503
|
+
|
|
504
|
+
This tool lets verifier developers describe what they want to check in plain Italian, English, or any
|
|
505
|
+
language — without needing to know the DCQL schema, VCT URIs, mDL namespaces, or claim paths.
|
|
506
|
+
|
|
507
|
+
Supports EU credential types:
|
|
508
|
+
- EU PID (Person Identification Data — all member states)
|
|
509
|
+
- Italian PID (SPID / CIE — urn:eu.europa.ec.eudi.pid.1)
|
|
510
|
+
- ISO 18013-5 mDL (Mobile Driving Licence — org.iso.18013.5.1.mDL)
|
|
511
|
+
- EUDI QEAA (Qualified Electronic Attestation of Attributes)
|
|
512
|
+
|
|
513
|
+
Handles age constraints (age_over_18, age_over_21, age_in_years), nationality/country filters,
|
|
514
|
+
document expiry, credential_sets for OR-based alternatives, and multi-credential queries.
|
|
515
|
+
|
|
516
|
+
Requires an Anthropic API key — configure via clavex_ai_configure.
|
|
517
|
+
|
|
518
|
+
Examples:
|
|
519
|
+
"voglio accettare solo patenti italiane valide con età > 21"
|
|
520
|
+
"verifica che l'utente sia maggiorenne (≥18) e cittadino EU"
|
|
521
|
+
"accetta PID italiano, voglio nome, cognome e codice fiscale"
|
|
522
|
+
"driving licence OR EU PID, minimum age 18, for a car rental"
|
|
523
|
+
"accetta solo CIE italiane valide, nome, cognome, età ≥ 18"
|
|
524
|
+
"verify the user has a valid Italian driving licence issued after 2020 with privilege category B"
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
- org_id: Organization UUID (required — the AI key is stored per-org)
|
|
528
|
+
- description: Natural language description of what to verify (Italian, English, or any language)
|
|
529
|
+
- lang: Optional BCP-47 language for error/explanation messages (default: same as description)
|
|
530
|
+
|
|
531
|
+
Returns: { dcql_query: { credentials: [...], credential_sets?: [...] } } ready to use in an OID4VP request.
|
|
532
|
+
Use clavex_create_presentation_session or PATCH /oid4vci/configs/:config_id to attach it to a flow.`,
|
|
533
|
+
inputSchema: {
|
|
534
|
+
org_id: z.string().uuid().describe("Organization UUID"),
|
|
535
|
+
description: z.string().describe("Natural language description of what to verify, e.g. 'patente italiana valida con età > 21'"),
|
|
536
|
+
lang: z.string().optional().describe("Optional BCP-47 language code for explanation messages, e.g. it, en, fr"),
|
|
537
|
+
},
|
|
538
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
539
|
+
},
|
|
540
|
+
async ({ org_id, description, lang }) =>
|
|
541
|
+
handleError(async () => {
|
|
542
|
+
const result = await getClient().post<{
|
|
543
|
+
dcql_query?: Record<string, unknown>;
|
|
544
|
+
raw?: string;
|
|
545
|
+
error?: string;
|
|
546
|
+
}>(
|
|
547
|
+
getClient().orgPath(org_id, "/ai/suggest-dcql"),
|
|
548
|
+
{ description, lang },
|
|
549
|
+
);
|
|
550
|
+
if (result.error || !result.dcql_query) {
|
|
551
|
+
return `⚠️ Could not generate a valid DCQL query. Adjust the description and try again.\n\nRaw output:\n${result.raw ?? "(empty)"}`;
|
|
552
|
+
}
|
|
553
|
+
const dcql = result.dcql_query;
|
|
554
|
+
const creds = (dcql.credentials as Array<Record<string, unknown>> ?? []);
|
|
555
|
+
const summary = creds.map((c) => {
|
|
556
|
+
const fmt = c.format as string ?? "?";
|
|
557
|
+
const id = c.id as string ?? "?";
|
|
558
|
+
const meta = c.meta as Record<string, unknown> ?? {};
|
|
559
|
+
const type = (meta.vct_values as string[] ?? [meta.doctype_value as string]).filter(Boolean).join(", ") || "—";
|
|
560
|
+
const claims = (c.claims as Array<Record<string, unknown>> ?? []).map((cl) => {
|
|
561
|
+
const path = (cl.path as string[] ?? []).join(".");
|
|
562
|
+
const vals = cl.values ? ` = ${JSON.stringify(cl.values)}` : "";
|
|
563
|
+
return `\`${path}${vals}\``;
|
|
564
|
+
});
|
|
565
|
+
return `**${id}** (${fmt}, ${type})\n Claims: ${claims.join(", ") || "—"}`;
|
|
566
|
+
}).join("\n");
|
|
567
|
+
return [
|
|
568
|
+
"Generated DCQL query:",
|
|
569
|
+
"",
|
|
570
|
+
summary,
|
|
571
|
+
"",
|
|
572
|
+
"```json",
|
|
573
|
+
JSON.stringify(dcql, null, 2),
|
|
574
|
+
"```",
|
|
575
|
+
"",
|
|
576
|
+
"Use this as the `dcql_query` field in an OID4VP authorization request or via `PATCH /oid4vci/configs/:config_id` to attach it to a credential issuance flow.",
|
|
577
|
+
].join("\n");
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|