@skaleagents/swarm 0.2.2 → 0.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.
- package/README.md +15 -16
- package/dist/auth.d.ts +4 -2
- package/dist/auth.js +260 -15
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/index.js +6 -3
- package/dist/review.d.ts +1 -1
- package/dist/review.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @skaleagents/swarm
|
|
2
2
|
|
|
3
3
|
Public stdio MCP server for SkaleAgents Phase 1. Talks to the Laravel **api**
|
|
4
|
-
|
|
4
|
+
over JSON and uses browser OAuth for sign-in.
|
|
5
5
|
|
|
6
6
|
Tools: `review_architecture`, `scan_iac_stub`.
|
|
7
7
|
|
|
@@ -12,13 +12,12 @@ the MCP tool for a structured review.
|
|
|
12
12
|
## Prerequisites
|
|
13
13
|
|
|
14
14
|
1. **API running:** Sail on `http://localhost:8082` (or your hosted API URL later).
|
|
15
|
-
2.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```
|
|
15
|
+
2. A browser that can open the SkaleAgents sign-in page.
|
|
16
|
+
|
|
17
|
+
The first tool call opens browser sign-in. Approve MCP access there and return
|
|
18
|
+
to your AI client. The package stores the OAuth refresh credential locally and
|
|
19
|
+
refreshes access automatically. You do not need to create or paste an API key.
|
|
20
|
+
Active connections can be revoked from the web app's MCP settings page.
|
|
22
21
|
|
|
23
22
|
## Local development
|
|
24
23
|
|
|
@@ -27,7 +26,6 @@ git clone https://github.com/SkaleAgents/mcp-server.git
|
|
|
27
26
|
cd mcp-server
|
|
28
27
|
npm install
|
|
29
28
|
cp .env.example .env
|
|
30
|
-
# Set SKALEAGENTS_API_TOKEN=<token from web app or curl above>
|
|
31
29
|
npm run build
|
|
32
30
|
npm test
|
|
33
31
|
npm run smoke # needs API on :8082
|
|
@@ -46,7 +44,6 @@ Add to `.cursor/mcp.json` (project) or Cursor Settings → MCP:
|
|
|
46
44
|
"command": "node",
|
|
47
45
|
"args": ["/absolute/path/to/mcp-server/dist/index.js"],
|
|
48
46
|
"env": {
|
|
49
|
-
"SKALEAGENTS_API_TOKEN": "<paste token from web app → API tokens>",
|
|
50
47
|
"PLATFORM_API_URL": "http://localhost:8082"
|
|
51
48
|
}
|
|
52
49
|
}
|
|
@@ -63,7 +60,6 @@ Dev without build:
|
|
|
63
60
|
"command": "npx",
|
|
64
61
|
"args": ["tsx", "/absolute/path/to/mcp-server/src/index.ts"],
|
|
65
62
|
"env": {
|
|
66
|
-
"SKALEAGENTS_API_TOKEN": "<token>",
|
|
67
63
|
"PLATFORM_API_URL": "http://localhost:8082"
|
|
68
64
|
}
|
|
69
65
|
}
|
|
@@ -80,7 +76,6 @@ Dev without build:
|
|
|
80
76
|
"command": "npx",
|
|
81
77
|
"args": ["-y", "@skaleagents/swarm"],
|
|
82
78
|
"env": {
|
|
83
|
-
"SKALEAGENTS_API_TOKEN": "<token>",
|
|
84
79
|
"PLATFORM_API_URL": "https://api.skaleagents.com"
|
|
85
80
|
}
|
|
86
81
|
}
|
|
@@ -92,18 +87,22 @@ Restart Cursor after saving. In Agent/Chat, tools should appear as `review_archi
|
|
|
92
87
|
|
|
93
88
|
## Claude Code
|
|
94
89
|
|
|
95
|
-
|
|
90
|
+
Point `command`/`args` at `node .../dist/index.js` or `npx @skaleagents/swarm` once published. OAuth starts on the first tool call.
|
|
96
91
|
|
|
97
92
|
## Environment
|
|
98
93
|
|
|
99
94
|
| Variable | Required | Description |
|
|
100
95
|
|----------|----------|-------------|
|
|
101
|
-
| `SKALEAGENTS_API_TOKEN` |
|
|
96
|
+
| `SKALEAGENTS_API_TOKEN` | No | Legacy Sanctum bearer-token override. OAuth is used when empty. |
|
|
102
97
|
| `PLATFORM_API_URL` | No | Default `http://localhost:8082` |
|
|
98
|
+
| `SKALEAGENTS_OAUTH_CACHE` | No | OAuth cache path. Default `~/.config/skaleagents/oauth.json`. |
|
|
99
|
+
| `SKALEAGENTS_OAUTH_ENABLED` | No | Set to `false` only to disable browser OAuth. |
|
|
103
100
|
|
|
104
101
|
## Auth behavior
|
|
105
102
|
|
|
106
|
-
- Missing
|
|
107
|
-
-
|
|
103
|
+
- Missing bearer override → browser OAuth starts automatically.
|
|
104
|
+
- OAuth access and refresh credentials are stored with local-user-only file permissions.
|
|
105
|
+
- A rejected or revoked connection returns an auth error and does not run a tool.
|
|
106
|
+
- The connection is user-scoped; bot visibility follows `api` RBAC.
|
|
108
107
|
|
|
109
108
|
Hub contract: [docs/contracts/mcp/tools.md](https://github.com/SkaleAgents/workspace/blob/main/docs/contracts/mcp/tools.md)
|
package/dist/auth.d.ts
CHANGED
|
@@ -3,12 +3,14 @@ export type AuthResult = {
|
|
|
3
3
|
userId?: string;
|
|
4
4
|
} | {
|
|
5
5
|
ok: false;
|
|
6
|
-
reason: "
|
|
6
|
+
reason: "oauth_required" | "oauth_failed" | "unauthorized" | "api_unavailable";
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Validate the API bearer token before running any tool. OAuth starts in the
|
|
10
|
+
* user's browser when no legacy SKALEAGENTS_API_TOKEN is configured.
|
|
10
11
|
*/
|
|
11
12
|
export declare function requireApiAuth(): Promise<AuthResult>;
|
|
13
|
+
export declare function getApiAccessToken(): Promise<string | null>;
|
|
12
14
|
export declare function unauthorizedContent(reason: AuthResult & {
|
|
13
15
|
ok: false;
|
|
14
16
|
}): {
|
package/dist/auth.js
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import { getApiToken, getApiUrl, getOAuthCachePath, isOAuthEnabled } from "./config.js";
|
|
7
|
+
let cachedCredentials = null;
|
|
8
|
+
let authenticationInFlight = null;
|
|
2
9
|
/**
|
|
3
|
-
*
|
|
10
|
+
* Validate the API bearer token before running any tool. OAuth starts in the
|
|
11
|
+
* user's browser when no legacy SKALEAGENTS_API_TOKEN is configured.
|
|
4
12
|
*/
|
|
5
13
|
export async function requireApiAuth() {
|
|
6
|
-
const token =
|
|
14
|
+
const token = await getAccessToken();
|
|
7
15
|
if (!token) {
|
|
8
|
-
return {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
reason: isOAuthEnabled() ? "oauth_failed" : "oauth_required",
|
|
19
|
+
};
|
|
9
20
|
}
|
|
10
21
|
try {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
let res = await fetchApiUser(token);
|
|
23
|
+
if ((res.status === 401 || res.status === 403) && !getApiToken()) {
|
|
24
|
+
const replacementToken = await reauthorizeAfterRejection();
|
|
25
|
+
if (replacementToken) {
|
|
26
|
+
res = await fetchApiUser(replacementToken);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
17
29
|
if (res.status === 401 || res.status === 403) {
|
|
18
30
|
return { ok: false, reason: "unauthorized" };
|
|
19
31
|
}
|
|
@@ -27,14 +39,247 @@ export async function requireApiAuth() {
|
|
|
27
39
|
return { ok: false, reason: "api_unavailable" };
|
|
28
40
|
}
|
|
29
41
|
}
|
|
42
|
+
export async function getApiAccessToken() {
|
|
43
|
+
return getAccessToken();
|
|
44
|
+
}
|
|
30
45
|
export function unauthorizedContent(reason) {
|
|
31
|
-
const text = reason.reason === "
|
|
32
|
-
? "
|
|
33
|
-
: reason.reason === "
|
|
34
|
-
? "
|
|
35
|
-
:
|
|
46
|
+
const text = reason.reason === "oauth_required"
|
|
47
|
+
? "MCP OAuth is disabled. Set SKALEAGENTS_API_TOKEN or enable OAuth."
|
|
48
|
+
: reason.reason === "oauth_failed"
|
|
49
|
+
? "MCP sign-in could not be completed. Check the browser window and try again."
|
|
50
|
+
: reason.reason === "unauthorized"
|
|
51
|
+
? "unauthorized: the SkaleAgents connection was rejected"
|
|
52
|
+
: "api unavailable: could not validate the SkaleAgents connection";
|
|
36
53
|
return {
|
|
37
54
|
isError: true,
|
|
38
55
|
content: [{ type: "text", text }],
|
|
39
56
|
};
|
|
40
57
|
}
|
|
58
|
+
async function getAccessToken() {
|
|
59
|
+
const legacyToken = getApiToken();
|
|
60
|
+
if (legacyToken)
|
|
61
|
+
return legacyToken;
|
|
62
|
+
if (!isOAuthEnabled())
|
|
63
|
+
return null;
|
|
64
|
+
if (!authenticationInFlight) {
|
|
65
|
+
authenticationInFlight = getOAuthAccessToken().finally(() => {
|
|
66
|
+
authenticationInFlight = null;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return await authenticationInFlight;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function fetchApiUser(token) {
|
|
77
|
+
return fetch(`${getApiUrl()}/api/user`, {
|
|
78
|
+
headers: {
|
|
79
|
+
Accept: "application/json",
|
|
80
|
+
Authorization: `Bearer ${token}`,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function reauthorizeAfterRejection() {
|
|
85
|
+
cachedCredentials = null;
|
|
86
|
+
await removeCredentials();
|
|
87
|
+
return getAccessToken();
|
|
88
|
+
}
|
|
89
|
+
async function getOAuthAccessToken() {
|
|
90
|
+
const apiUrl = getApiUrl();
|
|
91
|
+
const metadata = await getMetadata(apiUrl);
|
|
92
|
+
const stored = await readCredentials(apiUrl);
|
|
93
|
+
if (stored && stored.expiresAt > Date.now() + 30_000) {
|
|
94
|
+
cachedCredentials = stored;
|
|
95
|
+
return stored.accessToken;
|
|
96
|
+
}
|
|
97
|
+
if (stored?.refreshToken) {
|
|
98
|
+
try {
|
|
99
|
+
const refreshed = await exchangeRefreshToken(metadata.token_endpoint, stored.refreshToken);
|
|
100
|
+
await saveCredentials(apiUrl, refreshed);
|
|
101
|
+
return refreshed.accessToken;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
cachedCredentials = null;
|
|
105
|
+
await removeCredentials();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const authorizationCode = await authorizeInBrowser(metadata.authorization_endpoint);
|
|
109
|
+
const exchanged = await exchangeAuthorizationCode(metadata.token_endpoint, authorizationCode.code, authorizationCode.redirectUri, authorizationCode.verifier);
|
|
110
|
+
await saveCredentials(apiUrl, exchanged);
|
|
111
|
+
return exchanged.accessToken;
|
|
112
|
+
}
|
|
113
|
+
async function getMetadata(apiUrl) {
|
|
114
|
+
const response = await fetch(`${apiUrl}/.well-known/oauth-authorization-server`, {
|
|
115
|
+
headers: { Accept: "application/json" },
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`OAuth metadata request failed with status ${response.status}`);
|
|
119
|
+
}
|
|
120
|
+
const metadata = (await response.json());
|
|
121
|
+
if (typeof metadata.authorization_endpoint !== "string" ||
|
|
122
|
+
typeof metadata.token_endpoint !== "string") {
|
|
123
|
+
throw new Error("OAuth metadata is incomplete");
|
|
124
|
+
}
|
|
125
|
+
return metadata;
|
|
126
|
+
}
|
|
127
|
+
async function authorizeInBrowser(authorizationEndpoint) {
|
|
128
|
+
const verifier = randomBytes(48).toString("base64url");
|
|
129
|
+
const challenge = createCodeChallenge(verifier);
|
|
130
|
+
const state = randomBytes(32).toString("base64url");
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
let settled = false;
|
|
133
|
+
let redirectUri = "";
|
|
134
|
+
const server = createServer((request, response) => {
|
|
135
|
+
const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
136
|
+
if (requestUrl.pathname !== "/oauth/callback") {
|
|
137
|
+
response.writeHead(404);
|
|
138
|
+
response.end("Not found");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const receivedState = requestUrl.searchParams.get("state");
|
|
142
|
+
const error = requestUrl.searchParams.get("error");
|
|
143
|
+
const code = requestUrl.searchParams.get("code");
|
|
144
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
145
|
+
response.end("<!doctype html><title>SkaleAgents connected</title>" +
|
|
146
|
+
"<p>You can close this window and return to your AI client.</p>");
|
|
147
|
+
if (settled)
|
|
148
|
+
return;
|
|
149
|
+
settled = true;
|
|
150
|
+
server.close();
|
|
151
|
+
if (receivedState !== state) {
|
|
152
|
+
reject(new Error("OAuth state validation failed"));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (error || !code) {
|
|
156
|
+
reject(new Error("OAuth authorization was denied"));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
resolve({
|
|
160
|
+
code,
|
|
161
|
+
redirectUri,
|
|
162
|
+
verifier,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
server.once("error", reject);
|
|
166
|
+
server.listen(0, "127.0.0.1", () => {
|
|
167
|
+
const port = addressPort(server);
|
|
168
|
+
redirectUri = `http://127.0.0.1:${port}/oauth/callback`;
|
|
169
|
+
const url = new URL(authorizationEndpoint);
|
|
170
|
+
url.search = new URLSearchParams({
|
|
171
|
+
client_id: "skaleagents-mcp",
|
|
172
|
+
response_type: "code",
|
|
173
|
+
redirect_uri: redirectUri,
|
|
174
|
+
scope: "mcp",
|
|
175
|
+
state,
|
|
176
|
+
code_challenge: challenge,
|
|
177
|
+
code_challenge_method: "S256",
|
|
178
|
+
}).toString();
|
|
179
|
+
console.error("Opening SkaleAgents sign-in in your browser...");
|
|
180
|
+
console.error(url.toString());
|
|
181
|
+
openBrowser(url.toString());
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, verifier) {
|
|
186
|
+
const response = await fetchToken(tokenEndpoint, {
|
|
187
|
+
grant_type: "authorization_code",
|
|
188
|
+
client_id: "skaleagents-mcp",
|
|
189
|
+
code,
|
|
190
|
+
redirect_uri: redirectUri,
|
|
191
|
+
code_verifier: verifier,
|
|
192
|
+
});
|
|
193
|
+
return parseTokenResponse(response);
|
|
194
|
+
}
|
|
195
|
+
async function exchangeRefreshToken(tokenEndpoint, refreshToken) {
|
|
196
|
+
const response = await fetchToken(tokenEndpoint, {
|
|
197
|
+
grant_type: "refresh_token",
|
|
198
|
+
client_id: "skaleagents-mcp",
|
|
199
|
+
refresh_token: refreshToken,
|
|
200
|
+
});
|
|
201
|
+
return parseTokenResponse(response);
|
|
202
|
+
}
|
|
203
|
+
async function fetchToken(tokenEndpoint, values) {
|
|
204
|
+
const response = await fetch(tokenEndpoint, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: {
|
|
207
|
+
Accept: "application/json",
|
|
208
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
209
|
+
},
|
|
210
|
+
body: new URLSearchParams(values),
|
|
211
|
+
});
|
|
212
|
+
const payload = (await response.json().catch(() => ({})));
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
throw new Error(`OAuth token request failed with status ${response.status}`);
|
|
215
|
+
}
|
|
216
|
+
return payload;
|
|
217
|
+
}
|
|
218
|
+
function parseTokenResponse(response) {
|
|
219
|
+
if (typeof response.access_token !== "string" ||
|
|
220
|
+
typeof response.refresh_token !== "string" ||
|
|
221
|
+
typeof response.expires_in !== "number") {
|
|
222
|
+
throw new Error("OAuth token response is incomplete");
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
apiUrl: getApiUrl(),
|
|
226
|
+
accessToken: response.access_token,
|
|
227
|
+
refreshToken: response.refresh_token,
|
|
228
|
+
expiresAt: Date.now() + response.expires_in * 1000,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function readCredentials(apiUrl) {
|
|
232
|
+
if (cachedCredentials?.apiUrl === apiUrl)
|
|
233
|
+
return cachedCredentials;
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(await readFile(getOAuthCachePath(), "utf8"));
|
|
236
|
+
if (parsed.apiUrl !== apiUrl ||
|
|
237
|
+
typeof parsed.accessToken !== "string" ||
|
|
238
|
+
typeof parsed.refreshToken !== "string" ||
|
|
239
|
+
typeof parsed.expiresAt !== "number") {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return parsed;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function saveCredentials(apiUrl, credentials) {
|
|
249
|
+
cachedCredentials = { ...credentials, apiUrl };
|
|
250
|
+
const path = getOAuthCachePath();
|
|
251
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
252
|
+
await writeFile(path, JSON.stringify(cachedCredentials), { encoding: "utf8", mode: 0o600 });
|
|
253
|
+
await chmod(path, 0o600);
|
|
254
|
+
}
|
|
255
|
+
async function removeCredentials() {
|
|
256
|
+
try {
|
|
257
|
+
await unlink(getOAuthCachePath());
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
/* Cache may not exist. */
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function createCodeChallenge(verifier) {
|
|
264
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
265
|
+
}
|
|
266
|
+
function addressPort(server) {
|
|
267
|
+
const address = server.address();
|
|
268
|
+
if (address === null || typeof address === "string") {
|
|
269
|
+
throw new Error("OAuth callback server did not receive a port");
|
|
270
|
+
}
|
|
271
|
+
return address.port;
|
|
272
|
+
}
|
|
273
|
+
function openBrowser(url) {
|
|
274
|
+
const command = process.platform === "darwin"
|
|
275
|
+
? "open"
|
|
276
|
+
: process.platform === "win32"
|
|
277
|
+
? "cmd"
|
|
278
|
+
: "xdg-open";
|
|
279
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
280
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
281
|
+
child.once("error", () => {
|
|
282
|
+
console.error("Could not open a browser automatically. Open the URL printed above.");
|
|
283
|
+
});
|
|
284
|
+
child.unref();
|
|
285
|
+
}
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
export function getApiUrl() {
|
|
2
4
|
return (process.env.PLATFORM_API_URL?.replace(/\/$/, "") ?? "http://localhost:8082");
|
|
3
5
|
}
|
|
4
6
|
export function getApiToken() {
|
|
5
7
|
return process.env.SKALEAGENTS_API_TOKEN?.trim() ?? "";
|
|
6
8
|
}
|
|
9
|
+
export function getOAuthCachePath() {
|
|
10
|
+
return (process.env.SKALEAGENTS_OAUTH_CACHE?.trim() ??
|
|
11
|
+
join(homedir(), ".config", "skaleagents", "oauth.json"));
|
|
12
|
+
}
|
|
13
|
+
export function isOAuthEnabled() {
|
|
14
|
+
return process.env.SKALEAGENTS_OAUTH_ENABLED !== "false";
|
|
15
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { requireApiAuth, unauthorizedContent } from "./auth.js";
|
|
5
|
+
import { getApiAccessToken, requireApiAuth, unauthorizedContent, } from "./auth.js";
|
|
6
6
|
import { architectureFindings, countIacResources, fetchPublicBotHints, } from "./review.js";
|
|
7
7
|
const server = new McpServer({
|
|
8
8
|
name: "skaleagents-swarm",
|
|
9
|
-
version: "0.
|
|
9
|
+
version: "0.3.0",
|
|
10
10
|
});
|
|
11
11
|
server.registerTool("review_architecture", {
|
|
12
12
|
title: "Review architecture",
|
|
@@ -32,7 +32,10 @@ server.registerTool("review_architecture", {
|
|
|
32
32
|
const auth = await requireApiAuth();
|
|
33
33
|
if (!auth.ok)
|
|
34
34
|
return unauthorizedContent(auth);
|
|
35
|
-
const
|
|
35
|
+
const token = await getApiAccessToken();
|
|
36
|
+
if (!token)
|
|
37
|
+
return unauthorizedContent({ ok: false, reason: "oauth_failed" });
|
|
38
|
+
const botHints = await fetchPublicBotHints(token);
|
|
36
39
|
const focusValue = focus ?? "general";
|
|
37
40
|
const formatValue = format ?? "auto";
|
|
38
41
|
const output = {
|
package/dist/review.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export type Finding = {
|
|
|
5
5
|
detail: string;
|
|
6
6
|
};
|
|
7
7
|
export declare function architectureFindings(content: string, focus: string): Finding[];
|
|
8
|
-
export declare function fetchPublicBotHints(): Promise<string[]>;
|
|
8
|
+
export declare function fetchPublicBotHints(token: string): Promise<string[]>;
|
|
9
9
|
export declare function countIacResources(content: string): number;
|
package/dist/review.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getApiUrl } from "./config.js";
|
|
2
2
|
function shouldInclude(focus, category) {
|
|
3
3
|
return focus === "general" || focus === category;
|
|
4
4
|
}
|
|
@@ -104,12 +104,12 @@ export function architectureFindings(content, focus) {
|
|
|
104
104
|
}
|
|
105
105
|
return findings;
|
|
106
106
|
}
|
|
107
|
-
export async function fetchPublicBotHints() {
|
|
107
|
+
export async function fetchPublicBotHints(token) {
|
|
108
108
|
try {
|
|
109
109
|
const res = await fetch(`${getApiUrl()}/api/bots`, {
|
|
110
110
|
headers: {
|
|
111
111
|
Accept: "application/json",
|
|
112
|
-
Authorization: `Bearer ${
|
|
112
|
+
Authorization: `Bearer ${token}`,
|
|
113
113
|
},
|
|
114
114
|
});
|
|
115
115
|
if (!res.ok)
|