@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.
Files changed (83) hide show
  1. package/README.md +107 -0
  2. package/dist/client.d.ts +38 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +121 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/helpers.d.ts +14 -0
  7. package/dist/helpers.d.ts.map +1 -0
  8. package/dist/helpers.js +44 -0
  9. package/dist/helpers.js.map +1 -0
  10. package/dist/index.d.ts +24 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +59 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/tools/access_reviews.d.ts +3 -0
  15. package/dist/tools/access_reviews.d.ts.map +1 -0
  16. package/dist/tools/access_reviews.js +131 -0
  17. package/dist/tools/access_reviews.js.map +1 -0
  18. package/dist/tools/ai.d.ts +3 -0
  19. package/dist/tools/ai.d.ts.map +1 -0
  20. package/dist/tools/ai.js +443 -0
  21. package/dist/tools/ai.js.map +1 -0
  22. package/dist/tools/ciba.d.ts +3 -0
  23. package/dist/tools/ciba.d.ts.map +1 -0
  24. package/dist/tools/ciba.js +85 -0
  25. package/dist/tools/ciba.js.map +1 -0
  26. package/dist/tools/clients.d.ts +3 -0
  27. package/dist/tools/clients.d.ts.map +1 -0
  28. package/dist/tools/clients.js +124 -0
  29. package/dist/tools/clients.js.map +1 -0
  30. package/dist/tools/developer.d.ts +3 -0
  31. package/dist/tools/developer.d.ts.map +1 -0
  32. package/dist/tools/developer.js +580 -0
  33. package/dist/tools/developer.js.map +1 -0
  34. package/dist/tools/fga.d.ts +3 -0
  35. package/dist/tools/fga.d.ts.map +1 -0
  36. package/dist/tools/fga.js +126 -0
  37. package/dist/tools/fga.js.map +1 -0
  38. package/dist/tools/groups.d.ts +3 -0
  39. package/dist/tools/groups.d.ts.map +1 -0
  40. package/dist/tools/groups.js +135 -0
  41. package/dist/tools/groups.js.map +1 -0
  42. package/dist/tools/idps.d.ts +3 -0
  43. package/dist/tools/idps.d.ts.map +1 -0
  44. package/dist/tools/idps.js +98 -0
  45. package/dist/tools/idps.js.map +1 -0
  46. package/dist/tools/orgs.d.ts +3 -0
  47. package/dist/tools/orgs.d.ts.map +1 -0
  48. package/dist/tools/orgs.js +90 -0
  49. package/dist/tools/orgs.js.map +1 -0
  50. package/dist/tools/pam.d.ts +3 -0
  51. package/dist/tools/pam.d.ts.map +1 -0
  52. package/dist/tools/pam.js +238 -0
  53. package/dist/tools/pam.js.map +1 -0
  54. package/dist/tools/policies.d.ts +3 -0
  55. package/dist/tools/policies.d.ts.map +1 -0
  56. package/dist/tools/policies.js +173 -0
  57. package/dist/tools/policies.js.map +1 -0
  58. package/dist/tools/ssf.d.ts +3 -0
  59. package/dist/tools/ssf.d.ts.map +1 -0
  60. package/dist/tools/ssf.js +65 -0
  61. package/dist/tools/ssf.js.map +1 -0
  62. package/dist/tools/users.d.ts +3 -0
  63. package/dist/tools/users.d.ts.map +1 -0
  64. package/dist/tools/users.js +144 -0
  65. package/dist/tools/users.js.map +1 -0
  66. package/package.json +48 -0
  67. package/src/client.ts +148 -0
  68. package/src/helpers.ts +45 -0
  69. package/src/index.ts +63 -0
  70. package/src/tools/access_reviews.ts +163 -0
  71. package/src/tools/ai.ts +581 -0
  72. package/src/tools/ciba.ts +109 -0
  73. package/src/tools/clients.ts +168 -0
  74. package/src/tools/developer.ts +661 -0
  75. package/src/tools/fga.ts +148 -0
  76. package/src/tools/groups.ts +200 -0
  77. package/src/tools/idps.ts +137 -0
  78. package/src/tools/orgs.ts +119 -0
  79. package/src/tools/pam.ts +285 -0
  80. package/src/tools/policies.ts +233 -0
  81. package/src/tools/ssf.ts +82 -0
  82. package/src/tools/users.ts +202 -0
  83. package/tsconfig.json +18 -0
@@ -0,0 +1,285 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getClient } from "../client.js";
4
+ import { handleError, mdTable } from "../helpers.js";
5
+
6
+ export function registerPAMTools(server: McpServer): void {
7
+ // ── List pending PAM access requests ──────────────────────────────────────
8
+ server.registerTool(
9
+ "clavex_pam_list_pending",
10
+ {
11
+ title: "List Pending PAM Access Requests",
12
+ description: `List all pending Privileged Access Management (PAM) JIT access requests for an organization.
13
+
14
+ Returns: id, resource_name, resource_type, justification, requested_duration (minutes), created_at.
15
+
16
+ A security engineer approves or denies these using clavex_pam_approve.
17
+
18
+ Use when:
19
+ "show pending PAM requests"
20
+ "chi ha richiesto accesso privilegiato?"
21
+ "which JIT access requests need approval?"
22
+ "list all open privileged access requests"`,
23
+ inputSchema: {
24
+ org_id: z.string().uuid().describe("Organization UUID"),
25
+ },
26
+ annotations: { readOnlyHint: true, destructiveHint: false },
27
+ },
28
+ async ({ org_id }) =>
29
+ handleError(async () => {
30
+ const resp = await getClient().get<{ data: Array<Record<string, unknown>> }>(
31
+ getClient().orgPath(org_id, "/pam/access-requests?status=pending"),
32
+ );
33
+ const items = resp.data ?? [];
34
+ if (!Array.isArray(items) || items.length === 0) {
35
+ return "_No pending PAM access requests._";
36
+ }
37
+ return mdTable(items, [
38
+ "id",
39
+ "resource_name",
40
+ "resource_type",
41
+ "justification",
42
+ "requested_duration",
43
+ "created_at",
44
+ ]);
45
+ }),
46
+ );
47
+
48
+ // ── Submit a JIT access request ────────────────────────────────────────────
49
+ server.registerTool(
50
+ "clavex_pam_request",
51
+ {
52
+ title: "Request Privileged Access (PAM JIT)",
53
+ description: `Submit a Just-In-Time (JIT) privileged access request to the PAM system.
54
+
55
+ The request is routed to the security team for approval.
56
+ Once approved, access is active for the requested duration and automatically revoked when it expires.
57
+
58
+ Args:
59
+ - org_id: Organization UUID
60
+ - resource_name: Human-readable name of the resource (e.g. "prod-db", "k8s-prod-cluster")
61
+ - resource_type: Category — "database" | "server" | "kubernetes" | "api" | "generic"
62
+ - resource_id: Unique identifier (hostname, ARN, UUID, etc.)
63
+ - justification: Why access is needed — be specific (e.g. "deploy hotfix for incident #1234")
64
+ - duration_minutes: Requested access window in minutes (5–480, default 60)
65
+
66
+ Returns: Created access request JSON — save the id to approve/track it.
67
+
68
+ Use when:
69
+ "richiedi accesso privilegiato al database prod per 2 ore"
70
+ "request 120-minute access to prod-db — hotfix deploy"
71
+ "submit a PAM JIT request for the Kubernetes cluster"`,
72
+ inputSchema: {
73
+ org_id: z.string().uuid().describe("Organization UUID"),
74
+ resource_name: z.string().describe("Human-readable resource name, e.g. 'prod-db'"),
75
+ resource_type: z
76
+ .enum(["database", "server", "kubernetes", "api", "generic"])
77
+ .default("generic")
78
+ .describe("Resource category"),
79
+ resource_id: z.string().describe("Unique resource identifier (hostname, ARN, UUID, etc.)"),
80
+ justification: z
81
+ .string()
82
+ .min(10)
83
+ .describe("Reason for access — be specific, e.g. 'deploy hotfix for incident #1234'"),
84
+ duration_minutes: z
85
+ .number()
86
+ .int()
87
+ .min(5)
88
+ .max(480)
89
+ .default(60)
90
+ .describe("Requested access window in minutes (5–480, default 60)"),
91
+ },
92
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
93
+ },
94
+ async ({ org_id, resource_name, resource_type, resource_id, justification, duration_minutes }) =>
95
+ handleError(async () => {
96
+ const req = await getClient().post<Record<string, unknown>>(
97
+ getClient().orgPath(org_id, "/pam/access-requests"),
98
+ {
99
+ resource_name,
100
+ resource_type,
101
+ resource_id,
102
+ justification,
103
+ requested_duration: duration_minutes,
104
+ },
105
+ );
106
+ return [
107
+ `PAM access request submitted — status: **pending**`,
108
+ ``,
109
+ `\`\`\`json`,
110
+ JSON.stringify(req, null, 2),
111
+ `\`\`\``,
112
+ ``,
113
+ `Awaiting security team approval.`,
114
+ `Use \`clavex_pam_approve\` with \`request_id: "${req["id"]}"\` to approve or deny.`,
115
+ ].join("\n");
116
+ }),
117
+ );
118
+
119
+ // ── Approve or deny a PAM access request ──────────────────────────────────
120
+ server.registerTool(
121
+ "clavex_pam_approve",
122
+ {
123
+ title: "Approve or Deny PAM Access Request",
124
+ description: `Approve or deny a pending Privileged Access Management (PAM) JIT access request.
125
+
126
+ Approving activates access for the originally requested duration, then auto-revokes it.
127
+ Denying rejects the request; the requester cannot use that access window.
128
+
129
+ Use clavex_pam_list_pending first to get the request IDs awaiting decision.
130
+
131
+ Args:
132
+ - org_id: Organization UUID
133
+ - request_id: UUID of the PAM access request to decide on
134
+ - decision: "approve" or "deny"
135
+ - note (optional): Message visible to the requester (reason for approval or denial)
136
+
137
+ Returns: Updated access request JSON — status becomes "active" (approved) or "denied".
138
+
139
+ Use when:
140
+ "approva la richiesta PAM <id>"
141
+ "approve prod-db request for Alice — confirmed maintenance window"
142
+ "deny PAM request — not an approved change window"
143
+ "nega la richiesta di accesso privilegiato"`,
144
+ inputSchema: {
145
+ org_id: z.string().uuid().describe("Organization UUID"),
146
+ request_id: z.string().uuid().describe("PAM access request UUID"),
147
+ decision: z.enum(["approve", "deny"]).describe('"approve" = grant access, "deny" = reject'),
148
+ note: z.string().optional().describe("Optional note to the requester"),
149
+ },
150
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
151
+ },
152
+ async ({ org_id, request_id, decision, note }) =>
153
+ handleError(async () => {
154
+ const endpoint = decision === "approve" ? "approve" : "deny";
155
+ const result = await getClient().post<Record<string, unknown>>(
156
+ getClient().orgPath(org_id, `/pam/access-requests/${request_id}/${endpoint}`),
157
+ { note: note ?? "" },
158
+ );
159
+ const emoji = decision === "approve" ? "✅" : "❌";
160
+ const label = decision === "approve" ? "APPROVED — access is now active" : "DENIED";
161
+ return [
162
+ `${emoji} Request \`${request_id}\`: **${label}**`,
163
+ ``,
164
+ `\`\`\`json`,
165
+ JSON.stringify(result, null, 2),
166
+ `\`\`\``,
167
+ ].join("\n");
168
+ }),
169
+ );
170
+
171
+ // ── Vault: list credentials ────────────────────────────────────────────────
172
+ server.registerTool(
173
+ "clavex_vault_list_credentials",
174
+ {
175
+ title: "Vault: List Credentials",
176
+ description: `List all credentials stored in the PAM vault for an organization.
177
+
178
+ Returns: id, name, credential_type, username, target_host, checkout_duration, require_access_request, is_active.
179
+ Note: plaintext secrets are never returned in list responses. Use clavex_vault_checkout to retrieve a secret.
180
+
181
+ Use when:
182
+ "show vault credentials"
183
+ "which credentials are in the PAM vault?"
184
+ "quali credenziali ci sono nel vault?"
185
+ "list SSH keys in the vault"`,
186
+ inputSchema: {
187
+ org_id: z.string().uuid().describe("Organization UUID"),
188
+ },
189
+ annotations: { readOnlyHint: true, destructiveHint: false },
190
+ },
191
+ async ({ org_id }) =>
192
+ handleError(async () => {
193
+ const resp = await getClient().get<{ data: Array<Record<string, unknown>> }>(
194
+ getClient().orgPath(org_id, "/pam/credentials"),
195
+ );
196
+ const items = resp.data ?? [];
197
+ if (!Array.isArray(items) || items.length === 0) {
198
+ return "_No vault credentials found._";
199
+ }
200
+ return mdTable(items, [
201
+ "id",
202
+ "name",
203
+ "credential_type",
204
+ "username",
205
+ "target_host",
206
+ "checkout_duration",
207
+ "require_access_request",
208
+ "is_active",
209
+ ]);
210
+ }),
211
+ );
212
+
213
+ // ── Vault: checkout credential ─────────────────────────────────────────────
214
+ server.registerTool(
215
+ "clavex_vault_checkout",
216
+ {
217
+ title: "Vault: Checkout Credential",
218
+ description: `Check out a credential from the Clavex PAM vault. Returns the plaintext secret once.
219
+
220
+ The credential is locked for the checkout duration and automatically released when it expires.
221
+ Pass an approved access_request_id when the credential policy requires one.
222
+
223
+ ⚠️ The secret is returned ONCE — store it immediately.
224
+ It will NOT be shown again and access is revoked after the checkout window closes.
225
+
226
+ Args:
227
+ - org_id: Organization UUID
228
+ - cred_id: UUID of the vault credential (from clavex_vault_list_credentials)
229
+ - reason (optional): Reason for checkout (goes to audit trail)
230
+ - access_request_id (optional): UUID of an approved PAM access request (required if credential policy demands it)
231
+
232
+ Returns:
233
+ - secret: plaintext credential — save immediately
234
+ - checkout: metadata including expires_at and checkout_id
235
+
236
+ Use when:
237
+ "checkout prod-db password from vault"
238
+ "ritira le credenziali SSH dalla vault"
239
+ "get the API key for prod deployment — linked to request <id>"`,
240
+ inputSchema: {
241
+ org_id: z.string().uuid().describe("Organization UUID"),
242
+ cred_id: z
243
+ .string()
244
+ .uuid()
245
+ .describe("Vault credential UUID (use clavex_vault_list_credentials to find)"),
246
+ reason: z
247
+ .string()
248
+ .optional()
249
+ .describe("Reason for checkout — recorded in the audit trail"),
250
+ access_request_id: z
251
+ .string()
252
+ .uuid()
253
+ .optional()
254
+ .describe("Approved PAM access request UUID (required when credential policy demands it)"),
255
+ },
256
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
257
+ },
258
+ async ({ org_id, cred_id, reason, access_request_id }) =>
259
+ handleError(async () => {
260
+ const body: Record<string, unknown> = {};
261
+ if (reason) body["reason"] = reason;
262
+ if (access_request_id) body["access_request_id"] = access_request_id;
263
+
264
+ const result = await getClient().post<{
265
+ checkout: Record<string, unknown>;
266
+ secret: string;
267
+ warning: string;
268
+ }>(
269
+ getClient().orgPath(org_id, `/pam/credentials/${cred_id}/checkout`),
270
+ body,
271
+ );
272
+
273
+ return [
274
+ `⚠️ **${result.warning}**`,
275
+ ``,
276
+ `**Secret:** \`${result.secret}\``,
277
+ ``,
278
+ `**Checkout details:**`,
279
+ `\`\`\`json`,
280
+ JSON.stringify(result.checkout, null, 2),
281
+ `\`\`\``,
282
+ ].join("\n");
283
+ }),
284
+ );
285
+ }
@@ -0,0 +1,233 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getClient } from "../client.js";
4
+ import { handleError, mdTable } from "../helpers.js";
5
+
6
+ export function registerPolicyTools(server: McpServer): void {
7
+ // ── List policies ──────────────────────────────────────────────────────────
8
+ server.registerTool(
9
+ "clavex_list_policies",
10
+ {
11
+ title: "List Auth Policies",
12
+ description: `List all auth-flow policy rules for an organization.
13
+
14
+ Rules are evaluated in priority order (lowest number first) on every login attempt.
15
+ Each rule matches conditions (country, IP range, device, time) and applies an action (allow, deny, require_mfa).
16
+
17
+ Returns: id, name, priority, action, conditions summary, is_active.
18
+
19
+ Use when: "show auth policies for org <id>", "what geo-blocking rules are active?".`,
20
+ inputSchema: {
21
+ org_id: z.string().uuid().describe("Organization UUID"),
22
+ },
23
+ annotations: { readOnlyHint: true, destructiveHint: false },
24
+ },
25
+ async ({ org_id }) =>
26
+ handleError(async () => {
27
+ const rules = await getClient().get<Array<Record<string, unknown>>>(
28
+ getClient().orgPath(org_id, "/auth-policies"),
29
+ );
30
+ return mdTable(rules, ["id", "name", "priority", "action", "is_active"]);
31
+ }),
32
+ );
33
+
34
+ // ── Create policy ──────────────────────────────────────────────────────────
35
+ server.registerTool(
36
+ "clavex_create_policy",
37
+ {
38
+ title: "Create Auth Policy Rule",
39
+ description: `Create a new auth-flow policy rule for an organization.
40
+
41
+ Rules control login behavior: blocking countries, requiring MFA, restricting IP ranges.
42
+ Rules are evaluated in priority order — lower number = evaluated first.
43
+
44
+ Args:
45
+ - org_id: Organization UUID
46
+ - name: Rule name (e.g. "Block Russia")
47
+ - priority: Evaluation order (1 = first). Rules with same priority are evaluated in creation order.
48
+ - action: What to do when rule matches:
49
+ "allow" — permit login (use to create allow-list exceptions before a deny rule)
50
+ "deny" — block login
51
+ "require_mfa" — force MFA step
52
+ - conditions: JSON object describing match criteria. Supported fields:
53
+ { "country": ["US", "DE"] } — match these countries
54
+ { "country_not": ["RU", "CN"] } — block these countries
55
+ { "ip_cidr": ["10.0.0.0/8"] } — match IP ranges
56
+ { "ip_cidr_not": ["..."] } — exclude IP ranges
57
+ { "user_group": ["<group_id>"] } — match users in group
58
+ Conditions are ANDed together.
59
+ - description (optional): Human-readable explanation
60
+
61
+ Returns: Created policy rule JSON.
62
+
63
+ Examples:
64
+ - "block all logins from Russia" → action=deny, conditions={"country":["RU"]}
65
+ - "require MFA for everyone" → action=require_mfa, conditions={}`,
66
+ inputSchema: {
67
+ org_id: z.string().uuid().describe("Organization UUID"),
68
+ name: z.string().describe("Rule name"),
69
+ priority: z.number().int().min(1).describe("Evaluation order (lower = first)"),
70
+ action: z.enum(["allow", "deny", "require_mfa"]).describe("Action when rule matches"),
71
+ conditions: z.record(z.unknown()).describe("Match conditions JSON object"),
72
+ description: z.string().optional().describe("Human-readable explanation"),
73
+ },
74
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
75
+ },
76
+ async ({ org_id, name, priority, action, conditions, description }) =>
77
+ handleError(async () => {
78
+ const rule = await getClient().post<Record<string, unknown>>(
79
+ getClient().orgPath(org_id, "/auth-policies"),
80
+ { name, priority, action, conditions, description },
81
+ );
82
+ return `Policy rule created:\n\n${JSON.stringify(rule, null, 2)}`;
83
+ }),
84
+ );
85
+
86
+ // ── Delete policy ──────────────────────────────────────────────────────────
87
+ server.registerTool(
88
+ "clavex_delete_policy",
89
+ {
90
+ title: "Delete Auth Policy Rule",
91
+ description: `Delete an auth-flow policy rule. Logins previously affected by this rule will no longer be restricted by it.
92
+
93
+ Use when: "remove policy rule <id>", "delete the block-russia rule".`,
94
+ inputSchema: {
95
+ org_id: z.string().uuid().describe("Organization UUID"),
96
+ rule_id: z.string().uuid().describe("Policy rule UUID"),
97
+ },
98
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
99
+ },
100
+ async ({ org_id, rule_id }) =>
101
+ handleError(async () => {
102
+ await getClient().del(getClient().orgPath(org_id, `/auth-policies/${rule_id}`));
103
+ return `Policy rule ${rule_id} deleted.`;
104
+ }),
105
+ );
106
+
107
+ // ── Simulate policy ────────────────────────────────────────────────────────
108
+ server.registerTool(
109
+ "clavex_simulate_policy",
110
+ {
111
+ title: "Simulate Auth Policy Evaluation",
112
+ description: `Dry-run the auth policy evaluation for a hypothetical login — no actual login occurs.
113
+
114
+ Useful for testing policy rules: "would this user be blocked if logging in from Russia?"
115
+
116
+ Args:
117
+ - org_id: Organization UUID
118
+ - user_id (optional): UUID of the user whose groups/attributes to consider
119
+ - ip_address (optional): Simulated IP address
120
+ - country (optional): ISO 3166-1 alpha-2 country code (e.g. "RU", "US")
121
+ - device_type (optional): "mobile" | "desktop"
122
+
123
+ Returns:
124
+ - outcome.action: "allow" | "deny" | "require_mfa"
125
+ - matched_rule: the first rule that matched (or null if no match → default allow)
126
+ - trace: evaluation trace of all rules checked`,
127
+ inputSchema: {
128
+ org_id: z.string().uuid().describe("Organization UUID"),
129
+ user_id: z.string().uuid().optional().describe("User UUID to simulate (considers group memberships)"),
130
+ ip_address: z.string().optional().describe("Simulated IP address, e.g. 1.2.3.4"),
131
+ country: z.string().length(2).optional().describe("ISO 3166-1 alpha-2 country code, e.g. RU"),
132
+ device_type: z.enum(["mobile", "desktop"]).optional().describe("Device type"),
133
+ },
134
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
135
+ },
136
+ async ({ org_id, user_id, ip_address, country, device_type }) =>
137
+ handleError(async () => {
138
+ const body = Object.fromEntries(
139
+ Object.entries({ user_id, ip_address, country, device_type }).filter(([, v]) => v !== undefined),
140
+ );
141
+ const result = await getClient().post<Record<string, unknown>>(
142
+ getClient().orgPath(org_id, "/auth-policies/simulate"),
143
+ body,
144
+ );
145
+ return JSON.stringify(result, null, 2);
146
+ }),
147
+ );
148
+
149
+ // ── API Keys ───────────────────────────────────────────────────────────────
150
+ server.registerTool(
151
+ "clavex_list_api_keys",
152
+ {
153
+ title: "List API Keys",
154
+ description: `List all superadmin API keys. API keys are used instead of JWT tokens for machine-to-machine integrations.
155
+
156
+ Returns: id, name, last_used_at, created_at.
157
+ Note: The plaintext secret is never returned after creation.
158
+
159
+ Use when: "show all API keys", "list service accounts".`,
160
+ inputSchema: {},
161
+ annotations: { readOnlyHint: true, destructiveHint: false },
162
+ },
163
+ async () =>
164
+ handleError(async () => {
165
+ const keys = await getClient().get<Array<Record<string, unknown>>>("/api/v1/admin/api-keys");
166
+ return mdTable(keys, ["id", "name", "last_used_at", "created_at"]);
167
+ }),
168
+ );
169
+
170
+ server.registerTool(
171
+ "clavex_create_api_key",
172
+ {
173
+ title: "Create API Key",
174
+ description: `Create a new superadmin API key for machine-to-machine access.
175
+
176
+ Returns: API key JSON including the one-time plaintext secret.
177
+ IMPORTANT: Save the secret immediately — it will never be shown again.
178
+
179
+ Use when: "create API key for Terraform", "generate service account credentials".`,
180
+ inputSchema: {
181
+ name: z.string().describe("API key name / description, e.g. 'terraform-provisioner'"),
182
+ },
183
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
184
+ },
185
+ async ({ name }) =>
186
+ handleError(async () => {
187
+ const result = await getClient().post<Record<string, unknown>>("/api/v1/admin/api-keys", { name });
188
+ const secret = result.secret ?? result.api_key;
189
+ return `API key created.\n\n⚠️ SAVE THIS SECRET:\nsecret: ${secret}\n\n${JSON.stringify(result, null, 2)}`;
190
+ }),
191
+ );
192
+
193
+ server.registerTool(
194
+ "clavex_delete_api_key",
195
+ {
196
+ title: "Delete API Key",
197
+ description: `Delete a superadmin API key. Any service using it will immediately lose access.
198
+
199
+ Use when: "revoke API key <id>", "delete the terraform API key".`,
200
+ inputSchema: {
201
+ key_id: z.string().uuid().describe("API key UUID"),
202
+ },
203
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
204
+ },
205
+ async ({ key_id }) =>
206
+ handleError(async () => {
207
+ await getClient().del(`/api/v1/admin/api-keys/${key_id}`);
208
+ return `API key ${key_id} deleted.`;
209
+ }),
210
+ );
211
+
212
+ // ── Usage ──────────────────────────────────────────────────────────────────
213
+ server.registerTool(
214
+ "clavex_get_usage",
215
+ {
216
+ title: "Get Organization Usage",
217
+ description: `Get current usage metrics for an organization: active users, sessions, API calls.
218
+
219
+ Use when: "how many users does org <id> have?", "what's the monthly auth volume?".`,
220
+ inputSchema: {
221
+ org_id: z.string().uuid().describe("Organization UUID"),
222
+ },
223
+ annotations: { readOnlyHint: true, destructiveHint: false },
224
+ },
225
+ async ({ org_id }) =>
226
+ handleError(async () => {
227
+ const usage = await getClient().get<Record<string, unknown>>(
228
+ getClient().orgPath(org_id, "/usage"),
229
+ );
230
+ return JSON.stringify(usage, null, 2);
231
+ }),
232
+ );
233
+ }
@@ -0,0 +1,82 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getClient } from "../client.js";
4
+ import { handleError, mdTable } from "../helpers.js";
5
+
6
+ export function registerSSFTools(server: McpServer): void {
7
+ // ── List SSF streams ───────────────────────────────────────────────────────
8
+ server.registerTool(
9
+ "clavex_ssf_list_streams",
10
+ {
11
+ title: "List Shared Signals Framework (SSF) Streams",
12
+ description: `List all Shared Signals Framework (SSF) event delivery streams for an organization.
13
+
14
+ SSF (OpenID Shared Signals Framework) enables real-time propagation of security events
15
+ (CAEP / RISC) to relying parties — e.g. revoking sessions across all connected apps when
16
+ a user's credentials are compromised.
17
+
18
+ Stream delivery methods:
19
+ "push" — Clavex POSTs Security Event Tokens (SETs) to the receiver's endpoint
20
+ "poll" — The receiver polls Clavex to retrieve queued SETs
21
+
22
+ Returns: stream_id, delivery_method, endpoint_url, enabled, event_types, last_delivery_status.
23
+
24
+ Use when:
25
+ "list SSF streams for org <id>"
26
+ "which relying parties receive security events?"
27
+ "mostra i canali di segnalazione eventi di sicurezza"`,
28
+ inputSchema: {
29
+ org_id: z.string().uuid().describe("Organization UUID"),
30
+ },
31
+ annotations: { readOnlyHint: true, destructiveHint: false },
32
+ },
33
+ async ({ org_id }) =>
34
+ handleError(async () => {
35
+ const streams = await getClient().get<Array<Record<string, unknown>>>(
36
+ getClient().orgPath(org_id, "/ssf/streams"),
37
+ );
38
+ if (!Array.isArray(streams) || streams.length === 0) {
39
+ return "_No SSF streams configured for this organization._";
40
+ }
41
+ return mdTable(streams, ["id", "delivery_method", "endpoint_url", "enabled", "event_types_requested"]);
42
+ }),
43
+ );
44
+
45
+ // ── Disable SSF stream ────────────────────────────────────────────────────
46
+ server.registerTool(
47
+ "clavex_ssf_disable_stream",
48
+ {
49
+ title: "Disable SSF Stream",
50
+ description: `Disable an SSF event delivery stream so no more SETs are pushed or queued.
51
+
52
+ Use this to temporarily pause security event delivery without deleting the stream configuration.
53
+ Re-enable by calling the PATCH stream endpoint with enabled=true.
54
+
55
+ Args:
56
+ - org_id: Organization UUID
57
+ - stream_id: UUID of the SSF stream to disable
58
+
59
+ Use when:
60
+ "disable SSF stream <id>"
61
+ "stop sending security events to <endpoint>"
62
+ "pause event delivery for this relying party"`,
63
+ inputSchema: {
64
+ org_id: z.string().uuid().describe("Organization UUID"),
65
+ stream_id: z.string().uuid().describe("SSF stream UUID to disable"),
66
+ },
67
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
68
+ },
69
+ async ({ org_id, stream_id }) =>
70
+ handleError(async () => {
71
+ // The SSF stream is identified by the client (stream is scoped to client credentials).
72
+ // Admin streams list endpoint returns streams with their IDs.
73
+ // We use the admin PATCH stream endpoint — stream is identified by the client's stream config.
74
+ // Since the admin endpoint manages streams by org, we patch using the standard SSF stream API.
75
+ const result = await getClient().patch<Record<string, unknown>>(
76
+ getClient().orgPath(org_id, `/ssf/streams/${stream_id}`),
77
+ { status: "paused" },
78
+ );
79
+ return `SSF stream ${stream_id} disabled.\n\n${JSON.stringify(result, null, 2)}`;
80
+ }),
81
+ );
82
+ }