forta-mcp 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +202 -0
- package/README.md +18 -0
- package/index.js +66 -14
- package/package.json +5 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# AGENTS.md — forta-mcp
|
|
2
|
+
|
|
3
|
+
> `forta-mcp` is the **Model Context Protocol server for Forta**, the OAuth2 identity provider
|
|
4
|
+
> for `appleby.cloud`. It exposes the `forta-api` admin surface to Claude Code as **25 typed
|
|
5
|
+
> tools** — users, platforms, roles, grants, token introspection and API tokens. This file
|
|
6
|
+
> orients any agent/worker before touching code in this repo.
|
|
7
|
+
>
|
|
8
|
+
> **⚠️ Golden rule — keep this file current:** any change that adds, removes or retypes a tool,
|
|
9
|
+
> or drifts from `forta-api`'s routes, MUST update this AGENTS.md in the SAME change.
|
|
10
|
+
>
|
|
11
|
+
> **⚠️ Blast radius:** these tools can delete users, delete platforms and rotate client
|
|
12
|
+
> secrets. `forta_rotate_platform_credentials` takes a platform *down* until its deployed config
|
|
13
|
+
> is updated. Tool descriptions are the only warning an agent gets — keep them honest.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What this repo is
|
|
18
|
+
|
|
19
|
+
A single-file Node ESM program (`index.js`) speaking MCP over stdio, translating tool calls into
|
|
20
|
+
HTTP requests against `forta-api` at **`auth.appleby.cloud`**. Published to npm as `forta-mcp`,
|
|
21
|
+
consumed via `npx -y forta-mcp`.
|
|
22
|
+
|
|
23
|
+
It owns only the translation layer — tool names, argument schemas, descriptions, URL
|
|
24
|
+
construction. All behaviour comes from `forta-api`.
|
|
25
|
+
|
|
26
|
+
Its reason to exist: Forta is the identity provider for the whole ecosystem, so nearly every
|
|
27
|
+
auth bug starts there. `forta_list_grants` turns "why is this user getting a 403 from Keyring"
|
|
28
|
+
from a manual database dig into one call.
|
|
29
|
+
|
|
30
|
+
## Stack & dependencies
|
|
31
|
+
|
|
32
|
+
- **Node ≥18** (global `fetch`, `AbortSignal.timeout`). `"type": "module"`, top-level `await`.
|
|
33
|
+
- **`@modelcontextprotocol/sdk` ^1.29.0**, **`zod`** (transitive via the SDK — not declared).
|
|
34
|
+
- No build step, no tests, no linter. `node --check index.js` is the only static gate.
|
|
35
|
+
|
|
36
|
+
## Project structure
|
|
37
|
+
|
|
38
|
+
| Path | Role |
|
|
39
|
+
|------|------|
|
|
40
|
+
| `index.js` | `--setup` flow, config guard, `api()`/`text()`/`body()` helpers, all 25 tools, transport. |
|
|
41
|
+
| `package.json` | `bin.forta-mcp` → `index.js`. |
|
|
42
|
+
| `README.md` | User-facing setup + tool tables. |
|
|
43
|
+
| `AGENTS.md` | This file. |
|
|
44
|
+
|
|
45
|
+
## Running, building & testing
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install
|
|
49
|
+
node --check index.js
|
|
50
|
+
node index.js --setup # writes ~/.mcp.json
|
|
51
|
+
FORTA_API_URL=... FORTA_API_TOKEN=frt_... node index.js # run on stdio
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Smoke test without a client (the `sleep`s are required — requests otherwise race the handshake):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
{ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'
|
|
58
|
+
sleep 2
|
|
59
|
+
echo '{"jsonrpc":"2.0","method":"notifications/initialized"}'
|
|
60
|
+
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
|
|
61
|
+
sleep 2; } | FORTA_API_URL=x FORTA_API_TOKEN=frt_x node index.js
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## How code is written here
|
|
65
|
+
|
|
66
|
+
- **Every tool follows one shape** — `server.tool(name, description, zodSchema, handler)`, and
|
|
67
|
+
the handler ends `return { content: text(res) }`.
|
|
68
|
+
- **Naming:** `forta_` prefix, then verb, then noun.
|
|
69
|
+
- **`api(method, path, params, body, opts)`** — `params` become query entries (undefined/null
|
|
70
|
+
dropped); `body` is JSON by default. `opts.form` switches to
|
|
71
|
+
`application/x-www-form-urlencoded`; `opts.noAuth` omits the `Authorization` header. **Both
|
|
72
|
+
exist solely for `/oauth/introspect`** — see below.
|
|
73
|
+
- **`body(obj)` strips `undefined`.** Always use it on PUT tools: `forta-api` treats a
|
|
74
|
+
present-but-null field as an explicit clear, so forwarding raw `{...fields}` would wipe every
|
|
75
|
+
field the caller omitted.
|
|
76
|
+
- **Descriptions carry the blast radius.** Anything destructive must say so and name the safer
|
|
77
|
+
alternative.
|
|
78
|
+
|
|
79
|
+
### Sensitive value masking
|
|
80
|
+
|
|
81
|
+
`api()` passes every decoded JSON response through **`sanitise()`** before returning it. This is
|
|
82
|
+
central, not per-tool, so a newly added tool is safe by default rather than by remembering.
|
|
83
|
+
|
|
84
|
+
`mask()` keeps a value's **first two characters** and appends a **fixed-width tail** —
|
|
85
|
+
`"supersecret"` → `"su**********"`. The prefix is what makes the mask useful rather than merely
|
|
86
|
+
safe: you can still tell one credential from another, or confirm a rotation actually changed a
|
|
87
|
+
value. The tail is fixed width so the mask does not disclose the real length. Values under
|
|
88
|
+
three characters are masked whole, so a two-character secret cannot round-trip as itself.
|
|
89
|
+
|
|
90
|
+
The same shape is implemented in all five `*-mcp` servers.
|
|
91
|
+
|
|
92
|
+
`SECRET_FIELDS` covers `client_secret`, `secret`, `password`, `token`, `api_token`,
|
|
93
|
+
`access_token`, `refresh_token`, `id_token`, `oauth_request_token`, `plaintext`, `private_key`
|
|
94
|
+
and `signing_key`, masked at **any nesting depth**, in objects and arrays alike.
|
|
95
|
+
|
|
96
|
+
This matters most on the three tools that mint a credential and return it exactly once:
|
|
97
|
+
`forta_create_platform` and `forta_rotate_platform_credentials` (a `client_secret`) and
|
|
98
|
+
`forta_create_api_token` (a plaintext `frt_` token). Those are keys to the whole SSO estate.
|
|
99
|
+
Introspection metadata — `active`, `sub`, `exp`, `token_type` — is deliberately left readable,
|
|
100
|
+
since that is the entire point of `forta_introspect_token`.
|
|
101
|
+
|
|
102
|
+
`FORTA_ALLOW_SECRET_VALUES=1` disables masking. It is off by default and should stay that way;
|
|
103
|
+
read a real credential from the Forta dashboard instead.
|
|
104
|
+
|
|
105
|
+
## Domain & architecture
|
|
106
|
+
|
|
107
|
+
**Auth.** One long-lived `frt_` API token in `FORTA_API_TOKEN`, sent as
|
|
108
|
+
`Authorization: Bearer` on every request. `/admin/*` additionally requires the owning user to
|
|
109
|
+
be a Forta **super-admin** — the token does not bypass that.
|
|
110
|
+
|
|
111
|
+
**Tool groups:**
|
|
112
|
+
|
|
113
|
+
| Group | Tools |
|
|
114
|
+
|-------|-------|
|
|
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 |
|
|
120
|
+
| Tokens | introspect, list/create/revoke API tokens |
|
|
121
|
+
|
|
122
|
+
### Two behaviours that are easy to get wrong
|
|
123
|
+
|
|
124
|
+
**1. `/oauth/introspect` is a resource-server endpoint, not an admin one.** It requires the
|
|
125
|
+
*calling platform's* `client_id` and `client_secret`, reads **form-encoded** parameters, and
|
|
126
|
+
ignores the `Authorization` header entirely. It also only understands access JWTs — an opaque
|
|
127
|
+
`frt_` token reports `active: false`, which reads as "revoked" but means "wrong tool"
|
|
128
|
+
(`forta_list_api_tokens` is the right one). Introspection is scoped to the calling platform, so
|
|
129
|
+
a token valid elsewhere returns `active: false`.
|
|
130
|
+
|
|
131
|
+
The original implementation sent JSON with no credentials and **returned `invalid_client` on
|
|
132
|
+
every call** — it never worked. `opts.form` and `opts.noAuth` exist because of this.
|
|
133
|
+
|
|
134
|
+
**2. `/admin/grants` filters are mutually exclusive.** `HandleAdminListGrants` selects with a
|
|
135
|
+
`switch`, so passing both `platform_id` and `user_id` silently drops `user_id`. The tool
|
|
136
|
+
description says so; do not "fix" it by sending both.
|
|
137
|
+
|
|
138
|
+
## Ecosystem & related repos
|
|
139
|
+
|
|
140
|
+
| Repo | Relationship |
|
|
141
|
+
|------|--------------|
|
|
142
|
+
| [`forta-api`](https://github.com/aidenappl/forta-api) | The API this wraps. Its `main.go` is the source of truth for coverage. |
|
|
143
|
+
| [`go-forta`](https://github.com/aidenappl/go-forta) | v1.3.0+ accepts the `frt_` tokens this server uses. |
|
|
144
|
+
| [`keyring-mcp`](https://github.com/aidenappl/keyring-mcp) | **Uses the same `frt_` token** — Keyring has no admin credential of its own. |
|
|
145
|
+
| `lattice-mcp`, `monitor-mcp`, `openbucket-mcp` | Sibling MCPs; keep the single-file structure aligned. |
|
|
146
|
+
|
|
147
|
+
## Operations
|
|
148
|
+
|
|
149
|
+
- **Published to npm** as `forta-mcp` (public). `npx -y` resolves the latest, so **publishing
|
|
150
|
+
is deployment**.
|
|
151
|
+
- **Publishing requires 2FA via passkey** and must run from an interactive terminal — npm's web
|
|
152
|
+
auth flow needs a browser, and from a non-TTY subprocess it degrades to demanding an OTP a
|
|
153
|
+
passkey-only account cannot produce.
|
|
154
|
+
- **A running MCP server does not pick up a new npm version** — the client must restart.
|
|
155
|
+
- **Common failure modes:**
|
|
156
|
+
- *All tools `fetch failed`* — wrong `FORTA_API_URL`. The API is **`auth.appleby.cloud`**;
|
|
157
|
+
`forta.appleby.cloud` is the dashboard and returns 404 on `/healthcheck`.
|
|
158
|
+
- *All tools 401* — token revoked or expired.
|
|
159
|
+
- *`/admin` tools 403 but `forta_get_self` works* — the owning user is not super-admin.
|
|
160
|
+
|
|
161
|
+
## Rules & guardrails
|
|
162
|
+
|
|
163
|
+
- **Never hardcode a token, URL or hostname.**
|
|
164
|
+
- **Never log request or response bodies.** `forta_create_api_token` and
|
|
165
|
+
`forta_create_platform` return live secrets exactly once.
|
|
166
|
+
- **Never weaken `sanitise()`.** It must stay recursive, applied centrally in `api()`, and on by
|
|
167
|
+
default. If something genuinely needs a real value, the answer is
|
|
168
|
+
`FORTA_ALLOW_SECRET_VALUES=1` in that server's env, not an exemption in the code.
|
|
169
|
+
- **Never add a tool without reading its handler in `forta-api`.** Struct fields are not the
|
|
170
|
+
request contract — that assumption is exactly what shipped the broken introspect tool.
|
|
171
|
+
- **Never rename an existing tool.** Names are a public contract; add and deprecate instead.
|
|
172
|
+
- **`zod` is used but not declared** in `package.json`, resolving transitively through the MCP
|
|
173
|
+
SDK. Declaring it explicitly is the right fix, as a standalone change.
|
|
174
|
+
- Publishing is outward-facing and effectively irreversible (npm unpublish is restricted after
|
|
175
|
+
72 hours) — do not publish without explicit instruction.
|
|
176
|
+
|
|
177
|
+
## Verification — always before "done"
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
node --check index.js
|
|
181
|
+
grep -c 'server.tool(' index.js # expect 25
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Then the stdio handshake above, asserting the server starts with **no stderr**, the tool count
|
|
185
|
+
is right, and there are **no duplicate names** — `server.tool` accepts duplicates silently, the
|
|
186
|
+
last registration wins, and the earlier tool vanishes without an error.
|
|
187
|
+
|
|
188
|
+
For any tool added or changed, make **one real call against the live API**. Schema checks catch
|
|
189
|
+
typos; they do not catch wrong units, wrong enum values, wrong content types, or parameters the
|
|
190
|
+
handler ignores — every bug this repo has shipped was in that second category.
|
|
191
|
+
|
|
192
|
+
**Never report work complete on the strength of `tools/list` alone.**
|
|
193
|
+
|
|
194
|
+
## Keeping this file updated
|
|
195
|
+
|
|
196
|
+
Update this AGENTS.md in the same change when you:
|
|
197
|
+
- **Add/remove/rename a tool** → update the group table and the count in the header.
|
|
198
|
+
- **Change `api()` or its `opts`** → update *How code is written here*.
|
|
199
|
+
- **Discover a `forta-api` behaviour that contradicts a tool description** → fix both, and
|
|
200
|
+
record it under *Two behaviours that are easy to get wrong* if it is non-obvious.
|
|
201
|
+
- **Publish** → note the version and what changed.
|
|
202
|
+
- Also keep `README.md`'s tool tables in sync.
|
package/README.md
CHANGED
|
@@ -46,6 +46,24 @@ curl -X POST https://auth.appleby.cloud/admin/api-tokens \
|
|
|
46
46
|
|
|
47
47
|
Tokens are `frt_`-prefixed, owned by the creating user, and require that user to be a Forta super-admin for the `/admin` tools. The plaintext is returned once and stored only as a SHA-256 hash.
|
|
48
48
|
|
|
49
|
+
## Secret values are masked
|
|
50
|
+
|
|
51
|
+
Every response is passed through a masking step before it reaches the model. Anything that looks
|
|
52
|
+
like a credential keeps its **first two characters** and loses the rest to a fixed-width tail —
|
|
53
|
+
`supersecret` becomes `su**********`.
|
|
54
|
+
|
|
55
|
+
That is enough to tell two credentials apart, or to confirm a rotation actually changed
|
|
56
|
+
something, and not enough to use. The tail is a fixed width so the mask does not reveal the real
|
|
57
|
+
length.
|
|
58
|
+
|
|
59
|
+
This matters most on the tools that mint a credential and return it exactly once —
|
|
60
|
+
`forta_create_platform` and `forta_rotate_platform_credentials` (a `client_secret`) and
|
|
61
|
+
`forta_create_api_token` (a plaintext `frt_` token). Read the real value from the Forta
|
|
62
|
+
dashboard instead. Introspection metadata (`active`, `sub`, `exp`, `token_type`) is left
|
|
63
|
+
readable.
|
|
64
|
+
|
|
65
|
+
Set `FORTA_ALLOW_SECRET_VALUES=1` to turn masking off if you genuinely need a working value.
|
|
66
|
+
|
|
49
67
|
## Tools
|
|
50
68
|
|
|
51
69
|
### Health & identity
|
package/index.js
CHANGED
|
@@ -62,9 +62,54 @@ if (!API_URL || !API_TOKEN) {
|
|
|
62
62
|
process.exit(1);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// --- Sensitive value masking ---
|
|
66
|
+
//
|
|
67
|
+
// Several Forta endpoints return a live credential exactly once, at the moment
|
|
68
|
+
// it is minted: forta_create_platform and forta_rotate_platform_credentials
|
|
69
|
+
// return a client_secret, forta_create_api_token returns a plaintext frt_
|
|
70
|
+
// token. Those are the keys to the whole SSO estate, and anything that reaches
|
|
71
|
+
// a model's context is in a transcript forever.
|
|
72
|
+
//
|
|
73
|
+
// Sensitive values are masked to their first two characters plus a
|
|
74
|
+
// fixed-width tail: "frt_abc123" -> "fr**********". The prefix keeps a value
|
|
75
|
+
// identifiable and comparable while the fixed tail avoids disclosing length.
|
|
76
|
+
// Read the usable value from the Forta dashboard instead.
|
|
77
|
+
//
|
|
78
|
+
// Set FORTA_ALLOW_SECRET_VALUES=1 to pass values through unmasked.
|
|
79
|
+
|
|
80
|
+
const ALLOW_SECRETS = process.env.FORTA_ALLOW_SECRET_VALUES === "1";
|
|
81
|
+
|
|
82
|
+
function mask(value) {
|
|
83
|
+
if (typeof value !== "string" || value === "") return value;
|
|
84
|
+
// Under three characters there is no prefix worth keeping — a two-char
|
|
85
|
+
// secret would otherwise round-trip as itself.
|
|
86
|
+
if (value.length < 3) return "**********";
|
|
87
|
+
return value.slice(0, 2) + "**********";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Response fields that are a credential wherever they appear. `id_token` and
|
|
91
|
+
// `access_token` are included because the OAuth endpoints return real, usable
|
|
92
|
+
// session tokens.
|
|
93
|
+
const SECRET_FIELDS = new Set([
|
|
94
|
+
"client_secret", "secret", "password", "token", "api_token", "access_token",
|
|
95
|
+
"refresh_token", "id_token", "oauth_request_token", "plaintext",
|
|
96
|
+
"private_key", "signing_key",
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
// Walks a decoded response and masks every sensitive value in place.
|
|
100
|
+
function sanitise(node) {
|
|
101
|
+
if (ALLOW_SECRETS || node === null || typeof node !== "object") return node;
|
|
102
|
+
if (Array.isArray(node)) return node.map(sanitise);
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const [k, v] of Object.entries(node)) {
|
|
105
|
+
out[k] = SECRET_FIELDS.has(k) && typeof v === "string" ? mask(v) : sanitise(v);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
65
110
|
// --- HTTP helper ---
|
|
66
111
|
|
|
67
|
-
async function api(method, path, params, body) {
|
|
112
|
+
async function api(method, path, params, body, opts_ = {}) {
|
|
68
113
|
const url = new URL(path, API_URL);
|
|
69
114
|
if (params) {
|
|
70
115
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -73,12 +118,16 @@ async function api(method, path, params, body) {
|
|
|
73
118
|
}
|
|
74
119
|
const opts = {
|
|
75
120
|
method,
|
|
76
|
-
headers: {
|
|
77
|
-
Authorization: `Bearer ${API_TOKEN}`,
|
|
78
|
-
},
|
|
121
|
+
headers: {},
|
|
79
122
|
signal: AbortSignal.timeout(30000),
|
|
80
123
|
};
|
|
81
|
-
|
|
124
|
+
// The OAuth endpoints authenticate with client credentials, not the API
|
|
125
|
+
// token, and reject a stray Authorization header.
|
|
126
|
+
if (!opts_.noAuth) opts.headers.Authorization = `Bearer ${API_TOKEN}`;
|
|
127
|
+
if (body && opts_.form) {
|
|
128
|
+
opts.headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
129
|
+
opts.body = new URLSearchParams(body).toString();
|
|
130
|
+
} else if (body) {
|
|
82
131
|
opts.headers["Content-Type"] = "application/json";
|
|
83
132
|
opts.body = JSON.stringify(body);
|
|
84
133
|
}
|
|
@@ -87,7 +136,7 @@ async function api(method, path, params, body) {
|
|
|
87
136
|
const text = await res.text();
|
|
88
137
|
if (!text) return { success: res.ok, status: res.status };
|
|
89
138
|
try {
|
|
90
|
-
return JSON.parse(text);
|
|
139
|
+
return sanitise(JSON.parse(text));
|
|
91
140
|
} catch {
|
|
92
141
|
// /healthcheck and the OAuth endpoints return bare text, not the envelope.
|
|
93
142
|
return { success: res.ok, status: res.status, body: text };
|
|
@@ -109,7 +158,7 @@ function body(obj) {
|
|
|
109
158
|
|
|
110
159
|
const server = new McpServer({
|
|
111
160
|
name: "forta",
|
|
112
|
-
version: "1.
|
|
161
|
+
version: "1.1.0",
|
|
113
162
|
});
|
|
114
163
|
|
|
115
164
|
// --- Health & identity ---
|
|
@@ -279,9 +328,9 @@ server.tool("forta_delete_role", "Delete a role. Users holding it lose its permi
|
|
|
279
328
|
|
|
280
329
|
// --- Grants ---
|
|
281
330
|
|
|
282
|
-
server.tool("forta_list_grants", "List access grants — which users can access which platforms, and with what role.
|
|
283
|
-
platform_id: z.number().optional().describe("Filter to one platform"),
|
|
284
|
-
user_id: z.number().optional().describe("Filter to one user"),
|
|
331
|
+
server.tool("forta_list_grants", "List access grants — which users can access which platforms, and with what role. The first place to look when a user gets a 403 from an appleby.cloud service. Filters are mutually exclusive, not combined: pass platform_id OR user_id, or neither to list all. Passing both silently ignores user_id", {
|
|
332
|
+
platform_id: z.number().optional().describe("Filter to one platform (takes precedence over user_id)"),
|
|
333
|
+
user_id: z.number().optional().describe("Filter to one user (ignored if platform_id is also given)"),
|
|
285
334
|
}, async ({ platform_id, user_id }) => {
|
|
286
335
|
const res = await api("GET", "/admin/grants", { platform_id, user_id });
|
|
287
336
|
return { content: text(res) };
|
|
@@ -312,10 +361,13 @@ server.tool("forta_check_grant", "Check whether the token's user holds an active
|
|
|
312
361
|
|
|
313
362
|
// --- Token introspection ---
|
|
314
363
|
|
|
315
|
-
server.tool("forta_introspect_token", "Introspect an access
|
|
316
|
-
token: z.string().describe("The access
|
|
317
|
-
|
|
318
|
-
|
|
364
|
+
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", {
|
|
365
|
+
token: z.string().describe("The access JWT to introspect"),
|
|
366
|
+
client_id: z.string().describe("Calling platform's client_id (from forta_list_platforms)"),
|
|
367
|
+
client_secret: z.string().describe("Calling platform's client_secret — shown only when the platform is created or its credentials are rotated"),
|
|
368
|
+
}, async ({ token, client_id, client_secret }) => {
|
|
369
|
+
const res = await api("POST", "/oauth/introspect", null,
|
|
370
|
+
{ token, client_id, client_secret }, { form: true, noAuth: true });
|
|
319
371
|
return { content: text(res) };
|
|
320
372
|
});
|
|
321
373
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forta-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for the Forta OAuth2 identity provider",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
"keywords": [
|
|
14
14
|
"mcp",
|
|
15
15
|
"forta",
|
|
16
|
-
"auth",
|
|
16
|
+
"auth",
|
|
17
|
+
"oauth",
|
|
18
|
+
"identity",
|
|
19
|
+
"sso",
|
|
17
20
|
"model-context-protocol"
|
|
18
21
|
],
|
|
19
22
|
"author": "Aiden Appleby",
|