@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 CHANGED
@@ -1,7 +1,13 @@
1
1
  # @skaleagents/swarm
2
2
 
3
- SkaleAgents MCP server with local stdio and hosted Streamable HTTP transports.
4
- Uses browser OAuth for sign-in.
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 [settings page](https://skaleagents.com/settings) has an interactive
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 connection
111
+ ## Hosted transport
106
112
 
107
- Use `https://skaleagents.com/mcp` in Claude Desktop or ChatGPT's custom connector
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 Sanctum bearer-token override. OAuth is used when empty. |
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
- With an existing, authorized `SKALEAGENTS_API_TOKEN` in the environment, run
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/settings",
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.16";
1
+ export declare const VERSION = "0.6.18";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.6.16";
1
+ export const VERSION = "0.6.18";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaleagents/swarm",
3
- "version": "0.6.16",
3
+ "version": "0.6.18",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"