forta-mcp 1.1.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 +32 -6
  2. package/README.md +26 -6
  3. package/index.js +140 -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,12 +113,29 @@ 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 |
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
 
124
+ ### Setting `backchannel_logout_uri` — order matters
125
+
126
+ Forta POSTs a signed `logout_token` to this URL the instant a grant is revoked, so the consumer
127
+ ends the session immediately instead of waiting up to five minutes for its introspection poll.
128
+
129
+ ⚠️ **Only set it once the platform actually serves the endpoint.** Until then every notification
130
+ 404s, is retried six times on a backoff, and is marked `exhausted` — which looks in the logs like
131
+ a broken receiver rather than one that was never built. Deploy first, register second.
132
+
133
+ Pass `null` to clear it. Absent is the normal state for a platform that has not implemented the
134
+ receiver, and it is never an error — such a platform is simply skipped by the sender.
135
+
136
+ `https` only; the emitter refuses anything else at send time and logs
137
+ `FORTA_BACKCHANNEL_BAD_ENDPOINT`.
138
+
122
139
  ### Two behaviours that are easy to get wrong
123
140
 
124
141
  **1. `/oauth/introspect` is a resource-server endpoint, not an admin one.** It requires the
@@ -148,6 +165,12 @@ description says so; do not "fix" it by sending both.
148
165
 
149
166
  - **Published to npm** as `forta-mcp` (public). `npx -y` resolves the latest, so **publishing
150
167
  is deployment**.
168
+ - **Bump the version in TWO places before publishing** — `package.json` *and* the `McpServer({
169
+ version })` literal in `index.js`. npm refuses to republish an existing version, so forgetting
170
+ `package.json` fails loudly (`cannot publish over the previously published versions`), but
171
+ forgetting the one in `index.js` fails SILENTLY: the server then reports a version it is not,
172
+ and a client debugging "which build am I talking to" is misled by the only field that answers
173
+ that question.
151
174
  - **Publishing requires 2FA via passkey** and must run from an interactive terminal — npm's web
152
175
  auth flow needs a browser, and from a non-TTY subprocess it degrades to demanding an OTP a
153
176
  passkey-only account cannot produce.
@@ -172,7 +195,8 @@ description says so; do not "fix" it by sending both.
172
195
  - **`zod` is used but not declared** in `package.json`, resolving transitively through the MCP
173
196
  SDK. Declaring it explicitly is the right fix, as a standalone change.
174
197
  - Publishing is outward-facing and effectively irreversible (npm unpublish is restricted after
175
- 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*).
176
200
 
177
201
  ## Verification — always before "done"
178
202
 
@@ -198,5 +222,7 @@ Update this AGENTS.md in the same change when you:
198
222
  - **Change `api()` or its `opts`** → update *How code is written here*.
199
223
  - **Discover a `forta-api` behaviour that contradicts a tool description** → fix both, and
200
224
  record it under *Two behaviours that are easy to get wrong* if it is non-obvious.
225
+ - **Add/change a tool parameter** → bump the minor version in BOTH places (see *Operations*) in
226
+ the same change, so the commit that adds the capability is the commit that can be published.
201
227
  - **Publish** → note the version and what changed.
202
228
  - Also keep `README.md`'s tool tables in sync.
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.1.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
 
@@ -233,6 +275,7 @@ server.tool("forta_create_platform", "Register a new OAuth platform. Returns the
233
275
  description: z.string().optional().describe("Description"),
234
276
  redirect_uris: z.array(z.string()).optional().describe("Allowed OAuth redirect URIs"),
235
277
  logout_redirect_uris: z.array(z.string()).optional().describe("Allowed post-logout redirect URIs"),
278
+ backchannel_logout_uri: z.string().nullable().optional().describe("OIDC Back-Channel Logout 1.0 endpoint on the platform (https only). Forta POSTs a signed logout_token here the moment a grant is revoked, instead of the consumer waiting up to 5 minutes for its introspection poll. Pass null to clear. ⚠️ Only set this once the platform actually serves the endpoint — until then every notification 404s, retries 6 times and is marked exhausted"),
236
279
  allowed_origins: z.array(z.string()).optional().describe("Allowed CORS origins"),
237
280
  is_first_party: z.boolean().optional().describe("First-party app (skips the consent screen)"),
238
281
  internal_resource: z.boolean().optional().describe("Internal resource, hidden from the app launcher"),
@@ -241,6 +284,7 @@ server.tool("forta_create_platform", "Register a new OAuth platform. Returns the
241
284
  position: z.number().optional().describe("Sort position in the launcher"),
242
285
  category: z.string().optional().describe("Launcher category"),
243
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)"),
244
288
  }, async (args) => {
245
289
  const res = await api("POST", "/admin/platforms", null, body(args));
246
290
  return { content: text(res) };
@@ -254,6 +298,7 @@ server.tool("forta_update_platform", "Update a platform's configuration. Only th
254
298
  platform_type: z.enum(["WEB", "SPA", "SERVICE", "MOBILE"]).optional(),
255
299
  redirect_uris: z.array(z.string()).optional(),
256
300
  logout_redirect_uris: z.array(z.string()).optional(),
301
+ backchannel_logout_uri: z.string().nullable().optional().describe("OIDC Back-Channel Logout 1.0 endpoint on the platform (https only). Forta POSTs a signed logout_token here the moment a grant is revoked, instead of the consumer waiting up to 5 minutes for its introspection poll. Pass null to clear. ⚠️ Only set this once the platform actually serves the endpoint — until then every notification 404s, retries 6 times and is marked exhausted"),
257
302
  allowed_origins: z.array(z.string()).optional(),
258
303
  is_first_party: z.boolean().optional(),
259
304
  internal_resource: z.boolean().optional(),
@@ -262,19 +307,27 @@ server.tool("forta_update_platform", "Update a platform's configuration. Only th
262
307
  position: z.number().optional(),
263
308
  category: z.string().optional(),
264
309
  health_check: z.string().optional(),
310
+ requestable: z.boolean().optional().describe("Let signed-in users without access request it"),
265
311
  status: z.enum(["active", "disabled"]).optional().describe("Disabling blocks all new logins to this platform"),
266
312
  }, async ({ id, ...fields }) => {
267
313
  const res = await api("PUT", `/admin/platforms/${id}`, null, body(fields));
268
314
  return { content: text(res) };
269
315
  });
270
316
 
271
- 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", {
272
318
  id: z.number().describe("Platform ID"),
273
319
  }, async ({ id }) => {
274
320
  const res = await api("DELETE", `/admin/platforms/${id}`);
275
321
  return { content: text(res) };
276
322
  });
277
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
+
278
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", {
279
332
  id: z.number().describe("Platform ID"),
280
333
  }, async ({ id }) => {
@@ -318,7 +371,7 @@ server.tool("forta_update_role", "Update a role's name or permissions. Changes a
318
371
  return { content: text(res) };
319
372
  });
320
373
 
321
- 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", {
322
375
  platform_id: z.number().describe("Platform ID"),
323
376
  role_id: z.number().describe("Role ID"),
324
377
  }, async ({ platform_id, role_id }) => {
@@ -336,12 +389,20 @@ server.tool("forta_list_grants", "List access grants — which users can access
336
389
  return { content: text(res) };
337
390
  });
338
391
 
339
- 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", {
340
393
  user_id: z.number().describe("User ID"),
341
394
  platform_id: z.number().describe("Platform ID"),
342
- 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)"),
343
396
  }, async ({ user_id, platform_id, role_id }) => {
344
- 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 });
345
406
  return { content: text(res) };
346
407
  });
347
408
 
@@ -359,6 +420,72 @@ server.tool("forta_check_grant", "Check whether the token's user holds an active
359
420
  return { content: text(res) };
360
421
  });
361
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
+
362
489
  // --- Token introspection ---
363
490
 
364
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.1.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",