@skaleagents/swarm 0.6.16 → 0.6.18
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 -86
- package/dist/http.js +1 -1
- package/dist/review-edits.d.ts +21 -0
- package/dist/review-edits.js +145 -0
- package/dist/server.js +6 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# @skaleagents/swarm
|
|
2
2
|
|
|
3
|
-
SkaleAgents MCP server
|
|
4
|
-
|
|
3
|
+
SkaleAgents MCP server. The hosted connection is `https://skaleagents.com/mcp`.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
Copy `https://skaleagents.com/mcp` into your client and choose OAuth. Sign in
|
|
8
|
+
with Google and click Allow. Leave client ID and secret blank when the client
|
|
9
|
+
asks. Setup steps for Cursor, Claude Code, Claude Desktop, ChatGPT, and Codex
|
|
10
|
+
are on the [MCP config page](https://skaleagents.com/mcp-config).
|
|
5
11
|
|
|
6
12
|
Tools: `plan_architecture_review`, `review_application_architecture`,
|
|
7
13
|
`review_architecture`, and `scan_iac`. The older `scan_iac_stub` name remains
|
|
@@ -56,7 +62,7 @@ for every review area, and the next questions. The MCP provides static evidence
|
|
|
56
62
|
and the connected assistant's model reasons through the architecture. No
|
|
57
63
|
separate hosted model is invoked, and the tools do not clone repositories or
|
|
58
64
|
run submitted code. A clean static check is not a whole-system correctness
|
|
59
|
-
verdict. The [
|
|
65
|
+
verdict. The [MCP config page](https://skaleagents.com/mcp-config) has an interactive
|
|
60
66
|
review-brief builder.
|
|
61
67
|
|
|
62
68
|
## Infrastructure scanning
|
|
@@ -102,15 +108,9 @@ locations point to resource declarations; property paths identify the setting.
|
|
|
102
108
|
YAML aliases must be expanded before submission. Coverage limits are included
|
|
103
109
|
in every result. An empty finding list is not proof that a system is secure.
|
|
104
110
|
|
|
105
|
-
## Hosted
|
|
111
|
+
## Hosted transport
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
settings. Choose OAuth and leave client ID and secret fields blank. The client
|
|
109
|
-
registers itself, opens Google sign-in, and asks you to approve MCP access.
|
|
110
|
-
See [client setup](https://skaleagents.com/settings) for Cursor, Claude Code,
|
|
111
|
-
Claude Desktop, ChatGPT, and Codex instructions.
|
|
112
|
-
|
|
113
|
-
The web app hosts this endpoint using `handleMcpRequest` from
|
|
113
|
+
The web app hosts `https://skaleagents.com/mcp` using `handleMcpRequest` from
|
|
114
114
|
`@skaleagents/swarm/http`. Audit workers import `scanIac` from
|
|
115
115
|
`@skaleagents/swarm/scan`. It validates each bearer credential with the API's
|
|
116
116
|
`/api/oauth/mcp-token` endpoint before running a tool. Tokens are bound to the
|
|
@@ -120,94 +120,23 @@ forwarded to the public bot directory.
|
|
|
120
120
|
`protectedResourceMetadata` exports the discovery response for
|
|
121
121
|
`/.well-known/oauth-protected-resource/mcp`.
|
|
122
122
|
|
|
123
|
-
## Local stdio prerequisites
|
|
124
|
-
|
|
125
|
-
1. Node.js 20 or newer. The client connects to `https://api.skaleagents.com` by default.
|
|
126
|
-
2. A browser that can open the SkaleAgents sign-in page.
|
|
127
|
-
|
|
128
|
-
The first tool call opens browser sign-in. Approve MCP access there and return
|
|
129
|
-
to your AI client. The package stores the OAuth refresh credential locally and
|
|
130
|
-
refreshes access automatically. You do not need to create or paste an API key.
|
|
131
|
-
Active connections can be revoked from the web app's MCP settings page.
|
|
132
|
-
|
|
133
|
-
Local `review_architecture` calls validate the account and fetch public bot
|
|
134
|
-
hints concurrently. The public directory request carries no credential. Every
|
|
135
|
-
review still requires successful account validation. An anonymous directory
|
|
136
|
-
request may already be in flight when validation rejects a connection.
|
|
137
|
-
|
|
138
123
|
## Local development
|
|
139
124
|
|
|
125
|
+
People working on this repo need Node.js 20 or newer. This is not the way to connect a client.
|
|
126
|
+
|
|
140
127
|
```bash
|
|
141
128
|
git clone https://github.com/SkaleAgents/mcp-server.git
|
|
142
129
|
cd mcp-server
|
|
143
130
|
npm install
|
|
144
|
-
cp .env.example .env
|
|
145
131
|
npm run build
|
|
146
132
|
npm test
|
|
147
|
-
npm run smoke # needs API on :8082
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
## Cursor
|
|
151
|
-
|
|
152
|
-
Add to `.cursor/mcp.json` (project) or Cursor Settings → MCP:
|
|
153
|
-
|
|
154
|
-
### Option A: local clone (recommended while API is local)
|
|
155
|
-
|
|
156
|
-
```json
|
|
157
|
-
{
|
|
158
|
-
"mcpServers": {
|
|
159
|
-
"skaleagents": {
|
|
160
|
-
"command": "node",
|
|
161
|
-
"args": ["/absolute/path/to/mcp-server/dist/index.js"],
|
|
162
|
-
"env": {
|
|
163
|
-
"PLATFORM_API_URL": "http://localhost:8082"
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
133
|
```
|
|
169
134
|
|
|
170
|
-
Dev without build:
|
|
171
|
-
|
|
172
|
-
```json
|
|
173
|
-
{
|
|
174
|
-
"mcpServers": {
|
|
175
|
-
"skaleagents": {
|
|
176
|
-
"command": "npx",
|
|
177
|
-
"args": ["tsx", "/absolute/path/to/mcp-server/src/index.ts"],
|
|
178
|
-
"env": {
|
|
179
|
-
"PLATFORM_API_URL": "http://localhost:8082"
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Option B: published package (hosted API)
|
|
187
|
-
|
|
188
|
-
```json
|
|
189
|
-
{
|
|
190
|
-
"mcpServers": {
|
|
191
|
-
"skaleagents": {
|
|
192
|
-
"command": "npx",
|
|
193
|
-
"args": ["-y", "@skaleagents/swarm@0.6.1"]
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
Reconnect after upgrading to refresh the tool catalog. The consultation tools,
|
|
200
|
-
snippet review, IaC scanner, and compatibility alias should all appear.
|
|
201
|
-
|
|
202
|
-
## Claude Code
|
|
203
|
-
|
|
204
|
-
Use the published package configuration above in `.mcp.json`. OAuth starts on the first tool call. No API URL or keys are needed.
|
|
205
|
-
|
|
206
135
|
## Environment
|
|
207
136
|
|
|
208
137
|
| Variable | Required | Description |
|
|
209
138
|
|----------|----------|-------------|
|
|
210
|
-
| `SKALEAGENTS_API_TOKEN` | No | Legacy
|
|
139
|
+
| `SKALEAGENTS_API_TOKEN` | No | Legacy override for CI only, not the way to connect. |
|
|
211
140
|
| `PLATFORM_API_URL` | No | Defaults to `https://api.skaleagents.com`. Override only for local development or another API deployment. Empty values use the default. |
|
|
212
141
|
| `SKALEAGENTS_OAUTH_CACHE` | No | OAuth cache path. Default `~/.config/skaleagents/oauth.json`. |
|
|
213
142
|
| `SKALEAGENTS_OAUTH_ENABLED` | No | Set to `false` only to disable browser OAuth. |
|
|
@@ -215,7 +144,7 @@ Use the published package configuration above in `.mcp.json`. OAuth starts on th
|
|
|
215
144
|
|
|
216
145
|
## Steady-state tool latency probe
|
|
217
146
|
|
|
218
|
-
|
|
147
|
+
This probe uses the legacy `SKALEAGENTS_API_TOKEN` CI override. It is not how a client connects. With that variable already set, run
|
|
219
148
|
`npm run benchmark:tool`. The script builds this checkout, starts its stdio MCP
|
|
220
149
|
server, makes five warmup calls, then times 50 `review_architecture` calls on a
|
|
221
150
|
fixed synthetic application snippet. Set `BENCHMARK_WARMUP` from 0 to 20 and
|
package/dist/http.js
CHANGED
|
@@ -33,7 +33,7 @@ export function protectedResourceMetadata() {
|
|
|
33
33
|
authorization_servers: [getApiUrl()],
|
|
34
34
|
scopes_supported: ["mcp"],
|
|
35
35
|
bearer_methods_supported: ["header"],
|
|
36
|
-
resource_documentation: "https://skaleagents.com/
|
|
36
|
+
resource_documentation: "https://skaleagents.com/mcp-config",
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
export async function handleMcpRequest(request) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ExactLineEdit = {
|
|
2
|
+
diff: string;
|
|
3
|
+
from: string;
|
|
4
|
+
to: string | null;
|
|
5
|
+
};
|
|
6
|
+
export declare function exactLineEdit(content: string, title: string): ExactLineEdit | null;
|
|
7
|
+
export declare function rankFindings<T extends {
|
|
8
|
+
severity: string;
|
|
9
|
+
}>(findings: T[]): T[];
|
|
10
|
+
export declare function changePhrase(edit: ExactLineEdit): string;
|
|
11
|
+
type RankedFinding = {
|
|
12
|
+
title: string;
|
|
13
|
+
severity: string;
|
|
14
|
+
proposedChange?: ExactLineEdit;
|
|
15
|
+
};
|
|
16
|
+
export declare function presentReview(content: string, output: Record<string, unknown>): {
|
|
17
|
+
findings: any[];
|
|
18
|
+
priority: RankedFinding[];
|
|
19
|
+
summary: string;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const severityOrder = ["critical", "high", "medium", "low", "info"];
|
|
2
|
+
const noFindingsSummary = "No findings. That does not prove the system is secure.";
|
|
3
|
+
const rules = {
|
|
4
|
+
"Dockerfile runs as root": dockerfileUser,
|
|
5
|
+
"Workflow permissions are broad": workflowPermissions,
|
|
6
|
+
"Elevated container privileges": privilegedContainer,
|
|
7
|
+
"Privileged container in a Helm template": privilegedContainer,
|
|
8
|
+
"Database publicly accessible": publicDatabase,
|
|
9
|
+
"Database or storage allows public network access": publicNetworkAccess,
|
|
10
|
+
"Storage encryption disabled": storageEncryption,
|
|
11
|
+
"Pull request target checks out PR code": pullRequestTargetCheckout,
|
|
12
|
+
};
|
|
13
|
+
export function exactLineEdit(content, title) {
|
|
14
|
+
const rule = rules[title];
|
|
15
|
+
if (!rule)
|
|
16
|
+
return null;
|
|
17
|
+
return rule(content.split("\n"));
|
|
18
|
+
}
|
|
19
|
+
export function rankFindings(findings) {
|
|
20
|
+
return findings
|
|
21
|
+
.map((finding, index) => ({ finding, index }))
|
|
22
|
+
.sort((left, right) => {
|
|
23
|
+
const leftRank = severityOrder.indexOf(left.finding.severity);
|
|
24
|
+
const rightRank = severityOrder.indexOf(right.finding.severity);
|
|
25
|
+
return (leftRank === -1 ? severityOrder.length : leftRank) - (rightRank === -1 ? severityOrder.length : rightRank) || left.index - right.index;
|
|
26
|
+
})
|
|
27
|
+
.map((item) => item.finding);
|
|
28
|
+
}
|
|
29
|
+
export function changePhrase(edit) {
|
|
30
|
+
if (edit.to === null)
|
|
31
|
+
return `${edit.from.trim()} removed`;
|
|
32
|
+
return `${edit.from.trim()} becomes ${edit.to.trim()}`;
|
|
33
|
+
}
|
|
34
|
+
function isRankedFinding(value) {
|
|
35
|
+
if (!value || typeof value !== "object")
|
|
36
|
+
return false;
|
|
37
|
+
const finding = value;
|
|
38
|
+
return typeof finding.title === "string" && typeof finding.severity === "string";
|
|
39
|
+
}
|
|
40
|
+
export function presentReview(content, output) {
|
|
41
|
+
const raw = Array.isArray(output.findings) ? output.findings : [];
|
|
42
|
+
const findings = raw.map((finding) => {
|
|
43
|
+
if (!isRankedFinding(finding))
|
|
44
|
+
return finding;
|
|
45
|
+
const edit = exactLineEdit(content, finding.title);
|
|
46
|
+
if (!edit)
|
|
47
|
+
return finding;
|
|
48
|
+
return { ...finding, proposedChange: edit };
|
|
49
|
+
});
|
|
50
|
+
const priority = rankFindings(findings.filter(isRankedFinding)).slice(0, 3);
|
|
51
|
+
return {
|
|
52
|
+
...output,
|
|
53
|
+
findings,
|
|
54
|
+
priority,
|
|
55
|
+
summary: priority.length === 0
|
|
56
|
+
? noFindingsSummary
|
|
57
|
+
: `Fix these first: ${priority.map((finding) => finding.proposedChange ? `${finding.title}: ${changePhrase(finding.proposedChange)}` : finding.title).join(", ")}.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function dockerfileUser(lines) {
|
|
61
|
+
const exact = lines.flatMap((line, index) => line === "USER root" || line === "USER 0" ? [{ line, index }] : []);
|
|
62
|
+
if (exact.length !== 1)
|
|
63
|
+
return null;
|
|
64
|
+
return replacement(exact[0].line, "USER nobody", exact[0].index);
|
|
65
|
+
}
|
|
66
|
+
function workflowPermissions(lines) {
|
|
67
|
+
const exact = lines.flatMap((line, index) => line.trim() === "permissions: write-all" ? [{ line, index }] : []);
|
|
68
|
+
if (exact.length !== 1)
|
|
69
|
+
return null;
|
|
70
|
+
const indent = exact[0].line.match(/^\s*/)?.[0] ?? "";
|
|
71
|
+
return replacement(exact[0].line, `${indent}permissions: contents: read`, exact[0].index);
|
|
72
|
+
}
|
|
73
|
+
function privilegedContainer(lines) {
|
|
74
|
+
return oneExactReplacement(lines, [{ from: "privileged: true", to: "privileged: false" }]);
|
|
75
|
+
}
|
|
76
|
+
function publicDatabase(lines) {
|
|
77
|
+
return oneExactReplacement(lines, [{ from: "publicly_accessible = true", to: "publicly_accessible = false" }]);
|
|
78
|
+
}
|
|
79
|
+
function publicNetworkAccess(lines) {
|
|
80
|
+
return oneExactReplacement(lines, [
|
|
81
|
+
{ from: "publicNetworkAccess: 'Enabled'", to: "publicNetworkAccess: 'Disabled'" },
|
|
82
|
+
{ from: "publicNetworkAccess: \"Enabled\"", to: "publicNetworkAccess: \"Disabled\"" },
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
function storageEncryption(lines) {
|
|
86
|
+
return oneExactReplacement(lines, [
|
|
87
|
+
{ from: "storage_encrypted = false", to: "storage_encrypted = true" },
|
|
88
|
+
{ from: "encrypted: false", to: "encrypted: true" },
|
|
89
|
+
{ from: "encrypted = false", to: "encrypted = true" },
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
function pullRequestTargetCheckout(lines) {
|
|
93
|
+
const exact = lines.flatMap((line, index) => isExactHeadRefLine(line) ? [{ line, index }] : []);
|
|
94
|
+
if (exact.length !== 1)
|
|
95
|
+
return null;
|
|
96
|
+
const match = exact[0];
|
|
97
|
+
if (!isPullRequestTargetWorkflow(lines) || !refBelongsToCheckout(lines, match.index))
|
|
98
|
+
return null;
|
|
99
|
+
return {
|
|
100
|
+
from: match.line,
|
|
101
|
+
to: null,
|
|
102
|
+
diff: [
|
|
103
|
+
`@@ -${match.index + 1},1 +${Math.max(match.index, 0)},0 @@`,
|
|
104
|
+
`-${match.line}`,
|
|
105
|
+
].join("\n"),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function oneExactReplacement(lines, pairs) {
|
|
109
|
+
const matches = lines.flatMap((line, index) => {
|
|
110
|
+
const pair = pairs.find((item) => item.from === line.trim());
|
|
111
|
+
return pair ? [{ line, index, to: pair.to }] : [];
|
|
112
|
+
});
|
|
113
|
+
if (matches.length !== 1)
|
|
114
|
+
return null;
|
|
115
|
+
const indent = matches[0].line.match(/^\s*/)?.[0] ?? "";
|
|
116
|
+
return replacement(matches[0].line, `${indent}${matches[0].to}`, matches[0].index);
|
|
117
|
+
}
|
|
118
|
+
function replacement(from, to, lineIndex) {
|
|
119
|
+
const line = lineIndex + 1;
|
|
120
|
+
return {
|
|
121
|
+
from,
|
|
122
|
+
to,
|
|
123
|
+
diff: [`@@ -${line},1 +${line},1 @@`, `-${from}`, `+${to}`].join("\n"),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function isExactHeadRefLine(line) {
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
return trimmed === "ref: ${{ github.event.pull_request.head.ref }}" || trimmed === "ref: ${{ github.event.pull_request.head.sha }}";
|
|
129
|
+
}
|
|
130
|
+
function isPullRequestTargetWorkflow(lines) {
|
|
131
|
+
return lines.some((line) => {
|
|
132
|
+
const trimmed = line.trim();
|
|
133
|
+
if (trimmed.startsWith("#"))
|
|
134
|
+
return false;
|
|
135
|
+
return trimmed === "on: pull_request_target" || trimmed === "pull_request_target:" || trimmed.startsWith("pull_request_target:");
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function refBelongsToCheckout(lines, index) {
|
|
139
|
+
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
|
|
140
|
+
const trimmed = lines[cursor].trim();
|
|
141
|
+
if (trimmed.startsWith("- uses:") || trimmed.startsWith("uses:"))
|
|
142
|
+
return /actions\/checkout@/.test(trimmed);
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { architectureFindings, fetchPublicBotHints } from "./review.js";
|
|
|
5
5
|
import { looksLikeIac, ScanInputError } from "./iac/parse.js";
|
|
6
6
|
import { filterFindings, scanIac, summarize } from "./iac/scan.js";
|
|
7
7
|
import { registerConsultation } from "./application/tools.js";
|
|
8
|
+
import { presentReview } from "./review-edits.js";
|
|
8
9
|
import { VERSION } from "./version.js";
|
|
9
10
|
const contentSchema = z
|
|
10
11
|
.string()
|
|
@@ -104,18 +105,17 @@ export function createServer(remote = false) {
|
|
|
104
105
|
const options = { focus, minSeverity, maxFindings };
|
|
105
106
|
if (format !== "application" &&
|
|
106
107
|
(format !== "auto" || looksLikeIac(content))) {
|
|
107
|
-
return result({
|
|
108
|
+
return result(presentReview(content, {
|
|
108
109
|
...scanIac(content, { ...options, format }),
|
|
109
110
|
botHints: await botHints,
|
|
110
|
-
});
|
|
111
|
+
}));
|
|
111
112
|
}
|
|
112
113
|
const findings = filterFindings(architectureFindings(content, focus).slice(1), options);
|
|
113
|
-
return result({
|
|
114
|
+
return result(presentReview(content, {
|
|
114
115
|
status: "completed",
|
|
115
116
|
engineVersion: VERSION,
|
|
116
117
|
format: "application",
|
|
117
118
|
focus,
|
|
118
|
-
summary: `Application review completed; ${findings.length} findings match the selected filters.`,
|
|
119
119
|
findings: findings.slice(0, maxFindings),
|
|
120
120
|
totalFindings: findings.length,
|
|
121
121
|
totals: summarize(findings),
|
|
@@ -125,7 +125,7 @@ export function createServer(remote = false) {
|
|
|
125
125
|
"Infrastructure bottleneck checks cover a disabled timeout, a one-connection pool, and an unbounded retry limit.",
|
|
126
126
|
],
|
|
127
127
|
botHints: await botHints,
|
|
128
|
-
});
|
|
128
|
+
}));
|
|
129
129
|
}
|
|
130
130
|
catch (error) {
|
|
131
131
|
return inputError(error);
|
|
@@ -180,7 +180,7 @@ export function createServer(remote = false) {
|
|
|
180
180
|
return unauthorizedContent(auth);
|
|
181
181
|
}
|
|
182
182
|
try {
|
|
183
|
-
return result(scanIac(content, { format, focus, minSeverity, maxFindings }));
|
|
183
|
+
return result(presentReview(content, scanIac(content, { format, focus, minSeverity, maxFindings })));
|
|
184
184
|
}
|
|
185
185
|
catch (error) {
|
|
186
186
|
return inputError(error);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.18";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.18";
|