forta-mcp 1.2.0 → 1.3.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 (4) hide show
  1. package/AGENTS.md +9 -6
  2. package/README.md +26 -6
  3. package/index.js +138 -13
  4. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -37,7 +37,7 @@ from a manual database dig into one call.
37
37
 
38
38
  | Path | Role |
39
39
  |------|------|
40
- | `index.js` | `--setup` flow, config guard, `api()`/`text()`/`body()` helpers, all 25 tools, transport. |
40
+ | `index.js` | `--setup` flow, config guard, `api()`/`text()`/`body()` helpers, all 38 tools, transport. |
41
41
  | `package.json` | `bin.forta-mcp` → `index.js`. |
42
42
  | `README.md` | User-facing setup + tool tables. |
43
43
  | `AGENTS.md` | This file. |
@@ -113,10 +113,12 @@ be a Forta **super-admin** — the token does not bypass that.
113
113
  | Group | Tools |
114
114
  |-------|-------|
115
115
  | Health & identity | `forta_health`, `forta_get_self` |
116
- | Users | list, get, update, delete |
117
- | Platforms | list, get, create, update, delete, rotate credentials. `create`/`update` carry **`backchannel_logout_uri`** — see the warning below |
118
- | Roles | list, get, create, update, delete |
119
- | Grants | list, create, revoke, check |
116
+ | Users | list (`status`, `q` filters), get, **create**, update, delete, **revoke sessions**, **activity**, **platform usage** |
117
+ | Platforms | list (`include_deleted`), get, create, update, delete (also revokes its grants), **restore**, rotate credentials. `create`/`update` carry **`backchannel_logout_uri`** — see the warning below — and **`requestable`** |
118
+ | Roles | list, get, create, update, delete (refused while anyone holds the role) |
119
+ | Grants | list, create (`role_id` optional → platform default), **update role**, revoke, check |
120
+ | Access requests | **list**, **decide** (approve/deny + `role_id`/`note`) |
121
+ | Bulk / audit / settings | **`forta_bulk`** (`POST /admin/bulk`, atomic by default — the op list is in the tool description and forta-api's AGENTS.md), **list activity**, **overview**, **get/update settings** (`signup_mode`) |
120
122
  | Tokens | introspect, list/create/revoke API tokens |
121
123
 
122
124
  ### Setting `backchannel_logout_uri` — order matters
@@ -193,7 +195,8 @@ description says so; do not "fix" it by sending both.
193
195
  - **`zod` is used but not declared** in `package.json`, resolving transitively through the MCP
194
196
  SDK. Declaring it explicitly is the right fix, as a standalone change.
195
197
  - Publishing is outward-facing and effectively irreversible (npm unpublish is restricted after
196
- 72 hours) — do not publish without explicit instruction.
198
+ 72 hours) — publish **only after asking Aiden and getting an explicit yes** for that specific
199
+ version (see `~/CLAUDE.md` *Ask-first actions*).
197
200
 
198
201
  ## Verification — always before "done"
199
202
 
package/README.md CHANGED
@@ -75,19 +75,24 @@ Set `FORTA_ALLOW_SECRET_VALUES=1` to turn masking off if you genuinely need a wo
75
75
  ### Users
76
76
  | Tool | Description |
77
77
  |------|-------------|
78
- | `forta_list_users` | List users with status and super-admin flag |
79
- | `forta_get_user` | Single user with metadata and linked identities |
80
- | `forta_update_user` | Update name, email, status, super-admin, password |
78
+ | `forta_list_users` | List users with status and super-admin flag (`status`, `q` filters) |
79
+ | `forta_get_user` | Single user with metadata and sign-in methods |
80
+ | `forta_create_user` | Create an account (no grants) |
81
+ | `forta_update_user` | Update name, email, status (`pending` = awaiting approval), super-admin, password |
82
+ | `forta_revoke_user_sessions` | Sign a user out of every platform ⚠️ |
83
+ | `forta_get_user_activity` | Their trail across every platform: sign-ins, apps opened, refusals, admin changes |
84
+ | `forta_get_user_platform_usage` | When they last opened each platform |
81
85
  | `forta_delete_user` | Soft-delete a user ⚠️ |
82
86
 
83
87
  ### Platforms
84
88
  | Tool | Description |
85
89
  |------|-------------|
86
- | `forta_list_platforms` | All registered OAuth platforms |
90
+ | `forta_list_platforms` | All registered OAuth platforms (`include_deleted` for soft-deleted ones) |
87
91
  | `forta_get_platform` | Full platform config |
88
92
  | `forta_create_platform` | Register a platform (returns `client_secret` once) |
89
93
  | `forta_update_platform` | Update platform config |
90
- | `forta_delete_platform` | Delete a platform ⚠️ |
94
+ | `forta_delete_platform` | Soft-delete a platform and revoke its grants ⚠️ |
95
+ | `forta_restore_platform` | Restore a deleted platform (grants stay revoked) |
91
96
  | `forta_rotate_platform_credentials` | Rotate `client_secret` ⚠️ |
92
97
 
93
98
  ### Roles
@@ -103,10 +108,25 @@ Set `FORTA_ALLOW_SECRET_VALUES=1` to turn masking off if you genuinely need a wo
103
108
  | Tool | Description |
104
109
  |------|-------------|
105
110
  | `forta_list_grants` | Who can access what, with which role — **start here for 403s** |
106
- | `forta_create_grant` | Grant a user access to a platform |
111
+ | `forta_create_grant` | Grant a user access to a platform (role optional → default) |
112
+ | `forta_update_grant_role` | Move a grant to another role |
107
113
  | `forta_revoke_grant` | Revoke a grant ⚠️ |
108
114
  | `forta_check_grant` | Check a grant by `client_id`, exactly as a service sees it |
109
115
 
116
+ ### Access requests
117
+ | Tool | Description |
118
+ |------|-------------|
119
+ | `forta_list_access_requests` | People asking for platforms marked requestable |
120
+ | `forta_decide_access_request` | Approve (creates the grant) or deny |
121
+
122
+ ### Bulk, audit & settings
123
+ | Tool | Description |
124
+ |------|-------------|
125
+ | `forta_bulk` | Many admin changes in one request, all-or-nothing by default ⚠️ |
126
+ | `forta_list_activity` | Audit trail across every user and platform |
127
+ | `forta_get_overview` | Users by status, pending requests, sign-up mode, failed sign-ins |
128
+ | `forta_get_settings` / `forta_update_settings` | `signup_mode`: open, approval or closed |
129
+
110
130
  ### Tokens
111
131
  | Tool | Description |
112
132
  |------|-------------|
package/index.js CHANGED
@@ -158,7 +158,7 @@ function body(obj) {
158
158
 
159
159
  const server = new McpServer({
160
160
  name: "forta",
161
- version: "1.2.0",
161
+ version: "1.3.0",
162
162
  });
163
163
 
164
164
  // --- Health & identity ---
@@ -175,11 +175,51 @@ server.tool("forta_get_self", "Get the user this API token authenticates as —
175
175
 
176
176
  // --- Users ---
177
177
 
178
- server.tool("forta_list_users", "List all Forta users (id, uuid, email, status, is_super_admin, last_login_at)", {
178
+ server.tool("forta_list_users", "List Forta users (id, uuid, email, status, is_super_admin, last_login_at). Filter with status (e.g. 'pending' for accounts awaiting approval) and q (name/email substring)", {
179
179
  limit: z.number().optional().describe("Max users to return"),
180
180
  offset: z.number().optional().describe("Pagination offset"),
181
- }, async ({ limit, offset }) => {
182
- const res = await api("GET", "/admin/users", { limit, offset });
181
+ status: z.enum(["active", "pending", "invited", "suspended", "disabled"]).optional().describe("Only users in this status"),
182
+ q: z.string().optional().describe("Substring of name, display name or email"),
183
+ }, async ({ limit, offset, status, q }) => {
184
+ const res = await api("GET", "/admin/users", { limit, offset, status, q });
185
+ return { content: text(res) };
186
+ });
187
+
188
+ server.tool("forta_create_user", "Create an account directly. New accounts have no grants. Without a password the user can sign in only via Google/Apple with this (verified) address", {
189
+ name: z.string().describe("Full name"),
190
+ email: z.string().describe("Email address"),
191
+ password: z.string().optional().describe("Initial password, 8–72 characters"),
192
+ status: z.enum(["active", "pending", "invited", "suspended", "disabled"]).optional().describe("Default active"),
193
+ email_verified: z.boolean().optional().describe("Mark the email verified"),
194
+ is_super_admin: z.boolean().optional().describe("Make a Forta super admin"),
195
+ }, async (args) => {
196
+ const res = await api("POST", "/admin/users", null, body(args));
197
+ return { content: text(res) };
198
+ });
199
+
200
+ server.tool("forta_revoke_user_sessions", "Sign a user out of every platform now (revokes refresh-token families, denylists their access tokens, queues back-channel logouts). The account and its grants are unchanged", {
201
+ id: z.number().describe("User ID"),
202
+ }, async ({ id }) => {
203
+ const res = await api("POST", `/admin/users/${id}/revoke-sessions`);
204
+ return { content: text(res) };
205
+ });
206
+
207
+ server.tool("forta_get_user_activity", "A user's activity trail across every platform: sign-ins (and failures), apps opened (access.sign_in, by platform_id), refusals, admin changes to them, and events platforms reported. Newest first; page with before_id", {
208
+ id: z.number().describe("User ID"),
209
+ category: z.enum(["auth", "access", "admin", "platform"]).optional(),
210
+ outcome: z.enum(["success", "failure", "denied"]).optional(),
211
+ platform_id: z.number().optional(),
212
+ limit: z.number().optional().describe("Max events (default 50, max 500)"),
213
+ before_id: z.number().optional().describe("next_before_id from the previous page"),
214
+ }, async ({ id, ...params }) => {
215
+ const res = await api("GET", `/admin/users/${id}/activity`, params);
216
+ return { content: text(res) };
217
+ });
218
+
219
+ server.tool("forta_get_user_platform_usage", "When a user last signed in to each platform, with a count — answers 'does this person still use X?' before revoking", {
220
+ id: z.number().describe("User ID"),
221
+ }, async ({ id }) => {
222
+ const res = await api("GET", `/admin/users/${id}/platform-usage`);
183
223
  return { content: text(res) };
184
224
  });
185
225
 
@@ -197,8 +237,8 @@ server.tool("forta_update_user", "Update a user. Only the fields you pass are ch
197
237
  email: z.string().optional().describe("Email address (normalized to lowercase)"),
198
238
  email_verified: z.boolean().optional().describe("Mark the email verified"),
199
239
  profile_image_url: z.string().optional().describe("Profile image URL"),
200
- status: z.enum(["active", "invited", "suspended", "disabled", "deleted"]).optional().describe("Account status"),
201
- is_super_admin: z.boolean().optional().describe("Grant or revoke Forta super-admin"),
240
+ status: z.enum(["active", "pending", "invited", "suspended", "disabled"]).optional().describe("Account status. 'pending' = awaiting approval. Leaving 'active' ends the user's sessions. Use forta_delete_user to delete. An administrator cannot change their own status"),
241
+ is_super_admin: z.boolean().optional().describe("Grant or revoke Forta super-admin (you cannot revoke your own)"),
202
242
  password: z.string().optional().describe("Set a new password for the user's local identity"),
203
243
  }, async ({ id, ...fields }) => {
204
244
  const res = await api("PUT", `/admin/users/${id}`, null, body(fields));
@@ -214,8 +254,10 @@ server.tool("forta_delete_user", "Soft-delete a user (sets deleted_at). The user
214
254
 
215
255
  // --- Platforms ---
216
256
 
217
- server.tool("forta_list_platforms", "List all registered OAuth platforms (client_id, base_url, type, status)", {}, async () => {
218
- const res = await api("GET", "/admin/platforms");
257
+ server.tool("forta_list_platforms", "List registered OAuth platforms (client_id, base_url, type, status, requestable). include_deleted adds soft-deleted ones (deleted: true), which can be restored", {
258
+ include_deleted: z.boolean().optional().describe("Include soft-deleted platforms"),
259
+ }, async ({ include_deleted }) => {
260
+ const res = await api("GET", "/admin/platforms", include_deleted ? { include_deleted: "true" } : null);
219
261
  return { content: text(res) };
220
262
  });
221
263
 
@@ -242,6 +284,7 @@ server.tool("forta_create_platform", "Register a new OAuth platform. Returns the
242
284
  position: z.number().optional().describe("Sort position in the launcher"),
243
285
  category: z.string().optional().describe("Launcher category"),
244
286
  health_check: z.string().optional().describe("Health check URL"),
287
+ requestable: z.boolean().optional().describe("Let signed-in users without access see it and request access (default false)"),
245
288
  }, async (args) => {
246
289
  const res = await api("POST", "/admin/platforms", null, body(args));
247
290
  return { content: text(res) };
@@ -264,19 +307,27 @@ server.tool("forta_update_platform", "Update a platform's configuration. Only th
264
307
  position: z.number().optional(),
265
308
  category: z.string().optional(),
266
309
  health_check: z.string().optional(),
310
+ requestable: z.boolean().optional().describe("Let signed-in users without access request it"),
267
311
  status: z.enum(["active", "disabled"]).optional().describe("Disabling blocks all new logins to this platform"),
268
312
  }, async ({ id, ...fields }) => {
269
313
  const res = await api("PUT", `/admin/platforms/${id}`, null, body(fields));
270
314
  return { content: text(res) };
271
315
  });
272
316
 
273
- server.tool("forta_delete_platform", "Delete a platform. Every grant and role attached to it stops working and all its users lose access. Destructive — confirm with forta_get_platform first", {
317
+ server.tool("forta_delete_platform", "Soft-delete a platform and revoke every grant on it, in one transaction. All its users lose access. Restorable with forta_restore_platform (grants stay revoked). Destructive — confirm with forta_get_platform first", {
274
318
  id: z.number().describe("Platform ID"),
275
319
  }, async ({ id }) => {
276
320
  const res = await api("DELETE", `/admin/platforms/${id}`);
277
321
  return { content: text(res) };
278
322
  });
279
323
 
324
+ server.tool("forta_restore_platform", "Restore a soft-deleted platform (status back to active). Its grants stay revoked — re-grant deliberately", {
325
+ id: z.number().describe("Platform ID (from forta_list_platforms with include_deleted)"),
326
+ }, async ({ id }) => {
327
+ const res = await api("POST", `/admin/platforms/${id}/restore`);
328
+ return { content: text(res) };
329
+ });
330
+
280
331
  server.tool("forta_rotate_platform_credentials", "Rotate a platform's client_secret. The old secret stops working immediately — the platform is down until its deployed config is updated. Returns the new secret once", {
281
332
  id: z.number().describe("Platform ID"),
282
333
  }, async ({ id }) => {
@@ -320,7 +371,7 @@ server.tool("forta_update_role", "Update a role's name or permissions. Changes a
320
371
  return { content: text(res) };
321
372
  });
322
373
 
323
- server.tool("forta_delete_role", "Delete a role. Users holding it lose its permissions. Destructive", {
374
+ server.tool("forta_delete_role", "Delete a role. Refused (409) while anyone actively holds it — move them first with forta_update_grant_role. Revoked grants pointing at it are moved to another of the platform's roles", {
324
375
  platform_id: z.number().describe("Platform ID"),
325
376
  role_id: z.number().describe("Role ID"),
326
377
  }, async ({ platform_id, role_id }) => {
@@ -338,12 +389,20 @@ server.tool("forta_list_grants", "List access grants — which users can access
338
389
  return { content: text(res) };
339
390
  });
340
391
 
341
- server.tool("forta_create_grant", "Grant a user access to a platform with a specific role", {
392
+ server.tool("forta_create_grant", "Grant a user access to a platform. role_id omitted or 0 = the platform's default (lowest-id) role. Reactivates a previously revoked grant for the same pair", {
342
393
  user_id: z.number().describe("User ID"),
343
394
  platform_id: z.number().describe("Platform ID"),
344
- role_id: z.number().describe("Role ID to assign"),
395
+ role_id: z.number().optional().describe("Role ID to assign (default: the platform's default role)"),
345
396
  }, async ({ user_id, platform_id, role_id }) => {
346
- const res = await api("POST", "/admin/grants", null, { user_id, platform_id, role_id });
397
+ const res = await api("POST", "/admin/grants", null, { user_id, platform_id, role_id: role_id ?? 0 });
398
+ return { content: text(res) };
399
+ });
400
+
401
+ server.tool("forta_update_grant_role", "Move an active grant to another role on the same platform. Access tokens already issued keep the old role until they expire (10 min)", {
402
+ id: z.number().describe("Grant ID (from forta_list_grants)"),
403
+ role_id: z.number().describe("New role ID (from forta_list_roles)"),
404
+ }, async ({ id, role_id }) => {
405
+ const res = await api("PUT", `/admin/grants/${id}`, null, { role_id });
347
406
  return { content: text(res) };
348
407
  });
349
408
 
@@ -361,6 +420,72 @@ server.tool("forta_check_grant", "Check whether the token's user holds an active
361
420
  return { content: text(res) };
362
421
  });
363
422
 
423
+ // --- Access requests ---
424
+
425
+ server.tool("forta_list_access_requests", "List access requests — users asking for a grant on a platform marked requestable. Pending first", {
426
+ status: z.enum(["pending", "approved", "denied", "cancelled"]).optional(),
427
+ user_id: z.number().optional(),
428
+ platform_id: z.number().optional(),
429
+ }, async (params) => {
430
+ const res = await api("GET", "/admin/access-requests", params);
431
+ return { content: text(res) };
432
+ });
433
+
434
+ server.tool("forta_decide_access_request", "Approve (creates the grant) or deny a pending access request. The requester sees the decision and note on their dashboard", {
435
+ id: z.number().describe("Access request ID"),
436
+ decision: z.enum(["approve", "deny"]),
437
+ role_id: z.number().optional().describe("Approve only: role to grant (default: the requested role, else the platform default)"),
438
+ note: z.string().optional().describe("Shown to the requester"),
439
+ }, async ({ id, decision, role_id, note }) => {
440
+ const res = await api("POST", `/admin/access-requests/${id}/${decision}`, null, body({ role_id, note }));
441
+ return { content: text(res) };
442
+ });
443
+
444
+ // --- Bulk, activity, settings ---
445
+
446
+ server.tool("forta_bulk", "Run many admin changes in one request — the dashboard's action cart. atomic (default true) runs them in one transaction: the first failure rolls back everything. Each op goes through the same logic as its single endpoint (session revocation, back-channel logout, audit). Ops: grant.create {user_id, platform_id, role_id?} · grant.revoke {grant_id} · grant.update_role {grant_id, role_id} · user.update {user_id, ...fields} · user.delete {user_id} · user.revoke_sessions {user_id} · platform.update {platform_id, ...fields} · platform.delete {platform_id} · platform.restore {platform_id} · access_request.approve {request_id, role_id?, note?} · access_request.deny {request_id, note?}. Max 250 operations", {
447
+ operations: z.array(z.object({
448
+ op: z.enum(["grant.create", "grant.revoke", "grant.update_role", "user.update", "user.delete", "user.revoke_sessions", "platform.update", "platform.delete", "platform.restore", "access_request.approve", "access_request.deny"]),
449
+ args: z.record(z.any()),
450
+ })).min(1).max(250),
451
+ atomic: z.boolean().optional().describe("All or nothing (default true)"),
452
+ }, async ({ operations, atomic }) => {
453
+ const res = await api("POST", "/admin/bulk", null, { operations, atomic: atomic ?? true });
454
+ return { content: text(res) };
455
+ });
456
+
457
+ server.tool("forta_list_activity", "The audit trail across every user and platform: sign-ins and failures, apps opened, refusals, admin changes, platform-reported events. Persisted in Forta (survives Monitor's 30-day retention). Newest first; page with before_id", {
458
+ user_id: z.number().optional().describe("Subject or actor"),
459
+ platform_id: z.number().optional(),
460
+ category: z.enum(["auth", "access", "admin", "platform"]).optional(),
461
+ action: z.string().optional().describe("Action prefix, e.g. 'grant.' or 'auth.login'"),
462
+ outcome: z.enum(["success", "failure", "denied"]).optional(),
463
+ from: z.string().optional().describe("RFC 3339"),
464
+ to: z.string().optional().describe("RFC 3339"),
465
+ limit: z.number().optional().describe("Default 50, max 500"),
466
+ before_id: z.number().optional(),
467
+ }, async (params) => {
468
+ const res = await api("GET", "/admin/activity", params);
469
+ return { content: text(res) };
470
+ });
471
+
472
+ server.tool("forta_get_overview", "Admin headline counts: users by status, pending access requests, sign-up mode, failed sign-ins in the last 24h", {}, async () => {
473
+ const res = await api("GET", "/admin/overview");
474
+ return { content: text(res) };
475
+ });
476
+
477
+ server.tool("forta_get_settings", "Service-wide settings (signup_mode: open | approval | closed)", {}, async () => {
478
+ const res = await api("GET", "/admin/settings");
479
+ return { content: text(res) };
480
+ });
481
+
482
+ server.tool("forta_update_settings", "Change service-wide settings. signup_mode: open (new accounts active, no grants), approval (new accounts pending until approved), closed (no new accounts; first-time Google/Apple sign-ins refused)", {
483
+ signup_mode: z.enum(["open", "approval", "closed"]).optional(),
484
+ }, async (args) => {
485
+ const res = await api("PUT", "/admin/settings", null, body(args));
486
+ return { content: text(res) };
487
+ });
488
+
364
489
  // --- Token introspection ---
365
490
 
366
491
  server.tool("forta_introspect_token", "Introspect an access JWT (RFC 7662) — returns active, sub, exp, iat, token_type. Answers 'is the token this service rejected expired, malformed, or lacking a grant?'. Requires the calling platform's client credentials: introspection is scoped to that platform, and a token valid elsewhere returns active=false here. Only works for access JWTs — opaque frt_ API tokens always report active=false, so use forta_list_api_tokens to inspect those instead", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forta-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for the Forta OAuth2 identity provider",
5
5
  "type": "module",
6
6
  "main": "index.js",