@skaleagents/swarm 0.1.0 → 0.2.1
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 +81 -18
- package/dist/auth.js +3 -3
- package/dist/config.d.ts +0 -1
- package/dist/config.js +1 -6
- package/dist/index.js +22 -14
- package/dist/review.d.ts +1 -1
- package/dist/review.js +90 -9
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,29 +1,60 @@
|
|
|
1
|
-
# @skaleagents/swarm
|
|
1
|
+
# @skaleagents/swarm
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Public stdio MCP server for SkaleAgents Phase 1. Talks to the Laravel **api**
|
|
4
|
+
(JSON only, no web UI) with a Sanctum bearer token.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Tools: `review_architecture`, `scan_iac_stub`.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
`review_architecture` accepts application source or infrastructure text. Your AI
|
|
9
|
+
client reads the files in its workspace and sends the relevant content through
|
|
10
|
+
the MCP tool for a structured review.
|
|
9
11
|
|
|
10
|
-
##
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
1. **API running:** Sail on `http://localhost:8082` (or your hosted API URL later).
|
|
15
|
+
2. **Bearer token:** mint one in the web app: sign in → **MCP** → Create token.
|
|
16
|
+
Or for local-only testing:
|
|
17
|
+
```bash
|
|
18
|
+
curl -s -X POST http://localhost:8082/api/auth/google/callback \
|
|
19
|
+
-H 'Content-Type: application/json' \
|
|
20
|
+
-d '{"code":"mcp","displayName":"MCP User","email":"mcp@example.com"}' | jq -r .token
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Local development
|
|
11
24
|
|
|
12
25
|
```bash
|
|
26
|
+
git clone https://github.com/SkaleAgents/mcp-server.git
|
|
27
|
+
cd mcp-server
|
|
13
28
|
npm install
|
|
14
29
|
cp .env.example .env
|
|
15
|
-
#
|
|
16
|
-
# curl -s -X POST http://localhost:4000/auth/google/callback \
|
|
17
|
-
# -H 'Content-Type: application/json' \
|
|
18
|
-
# -d '{"code":"mcp","displayName":"MCP","email":"mcp@example.com"}'
|
|
30
|
+
# Set SKALEAGENTS_API_TOKEN=<token from web app or curl above>
|
|
19
31
|
npm run build
|
|
20
32
|
npm test
|
|
21
|
-
npm run smoke # needs
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
npm run smoke # needs API on :8082
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Cursor
|
|
37
|
+
|
|
38
|
+
Add to `.cursor/mcp.json` (project) or Cursor Settings → MCP:
|
|
39
|
+
|
|
40
|
+
### Option A: local clone (recommended while API is local)
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"skaleagents": {
|
|
46
|
+
"command": "node",
|
|
47
|
+
"args": ["/absolute/path/to/mcp-server/dist/index.js"],
|
|
48
|
+
"env": {
|
|
49
|
+
"SKALEAGENTS_API_TOKEN": "<paste token from web app → API tokens>",
|
|
50
|
+
"PLATFORM_API_URL": "http://localhost:8082"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
24
55
|
```
|
|
25
56
|
|
|
26
|
-
|
|
57
|
+
Dev without build:
|
|
27
58
|
|
|
28
59
|
```json
|
|
29
60
|
{
|
|
@@ -32,15 +63,47 @@ npm run smoke # needs platform-api on PLATFORM_API_URL
|
|
|
32
63
|
"command": "npx",
|
|
33
64
|
"args": ["tsx", "/absolute/path/to/mcp-server/src/index.ts"],
|
|
34
65
|
"env": {
|
|
35
|
-
"SKALEAGENTS_API_TOKEN": "<token
|
|
36
|
-
"PLATFORM_API_URL": "http://localhost:
|
|
37
|
-
"PLATFORM_API_PREFIX": ""
|
|
66
|
+
"SKALEAGENTS_API_TOKEN": "<token>",
|
|
67
|
+
"PLATFORM_API_URL": "http://localhost:8082"
|
|
38
68
|
}
|
|
39
69
|
}
|
|
40
70
|
}
|
|
41
71
|
}
|
|
42
72
|
```
|
|
43
73
|
|
|
44
|
-
|
|
74
|
+
### Option B: after npm publish (hosted API)
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"skaleagents": {
|
|
80
|
+
"command": "npx",
|
|
81
|
+
"args": ["-y", "@skaleagents/swarm"],
|
|
82
|
+
"env": {
|
|
83
|
+
"SKALEAGENTS_API_TOKEN": "<token>",
|
|
84
|
+
"PLATFORM_API_URL": "https://api.skaleagents.com"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Restart Cursor after saving. In Agent/Chat, tools should appear as `review_architecture` and `scan_iac_stub`.
|
|
92
|
+
|
|
93
|
+
## Claude Code
|
|
94
|
+
|
|
95
|
+
Same env vars; point `command`/`args` at `node …/dist/index.js` or `npx @skaleagents/swarm` once published.
|
|
96
|
+
|
|
97
|
+
## Environment
|
|
98
|
+
|
|
99
|
+
| Variable | Required | Description |
|
|
100
|
+
|----------|----------|-------------|
|
|
101
|
+
| `SKALEAGENTS_API_TOKEN` | Yes | Sanctum bearer token (from the web **MCP** page) |
|
|
102
|
+
| `PLATFORM_API_URL` | No | Default `http://localhost:8082` |
|
|
103
|
+
|
|
104
|
+
## Auth behavior
|
|
105
|
+
|
|
106
|
+
- Missing/invalid token → tools return an **unauthorized** error (fail closed).
|
|
107
|
+
- Token is user-scoped; bot visibility follows `api` RBAC.
|
|
45
108
|
|
|
46
109
|
Hub contract: [docs/contracts/mcp/tools.md](https://github.com/SkaleAgents/workspace/blob/main/docs/contracts/mcp/tools.md)
|
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getApiToken, getApiUrl } from "./config.js";
|
|
2
2
|
/**
|
|
3
3
|
* Require the API to validate the bearer token before running any tool.
|
|
4
4
|
*/
|
|
@@ -8,7 +8,7 @@ export async function requireApiAuth() {
|
|
|
8
8
|
return { ok: false, reason: "missing_token" };
|
|
9
9
|
}
|
|
10
10
|
try {
|
|
11
|
-
const res = await fetch(
|
|
11
|
+
const res = await fetch(`${getApiUrl()}/api/user`, {
|
|
12
12
|
headers: {
|
|
13
13
|
Accept: "application/json",
|
|
14
14
|
Authorization: `Bearer ${token}`,
|
|
@@ -18,7 +18,7 @@ export async function requireApiAuth() {
|
|
|
18
18
|
return { ok: false, reason: "unauthorized" };
|
|
19
19
|
}
|
|
20
20
|
if (!res.ok) {
|
|
21
|
-
return { ok: false, reason: "
|
|
21
|
+
return { ok: false, reason: "api_unavailable" };
|
|
22
22
|
}
|
|
23
23
|
const user = (await res.json());
|
|
24
24
|
return { ok: true, userId: user.id };
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
export function getApiUrl() {
|
|
2
|
-
return (process.env.PLATFORM_API_URL?.replace(/\/$/, "") ?? "http://localhost:
|
|
3
|
-
}
|
|
4
|
-
export function getApiEndpoint(path) {
|
|
5
|
-
const rawPrefix = (process.env.PLATFORM_API_PREFIX ?? "").replace(/\/$/, "");
|
|
6
|
-
const prefix = rawPrefix && !rawPrefix.startsWith("/") ? `/${rawPrefix}` : rawPrefix;
|
|
7
|
-
return `${getApiUrl()}${prefix}${path.startsWith("/") ? path : `/${path}`}`;
|
|
2
|
+
return (process.env.PLATFORM_API_URL?.replace(/\/$/, "") ?? "http://localhost:8082");
|
|
8
3
|
}
|
|
9
4
|
export function getApiToken() {
|
|
10
5
|
return process.env.SKALEAGENTS_API_TOKEN?.trim() ?? "";
|
package/dist/index.js
CHANGED
|
@@ -3,35 +3,41 @@ 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
5
|
import { requireApiAuth, unauthorizedContent } from "./auth.js";
|
|
6
|
-
import {
|
|
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.2.0",
|
|
10
10
|
});
|
|
11
11
|
server.registerTool("review_architecture", {
|
|
12
12
|
title: "Review architecture",
|
|
13
|
-
description: "
|
|
13
|
+
description: "Review application source or infrastructure text for security, reliability, and cost risks.",
|
|
14
14
|
inputSchema: {
|
|
15
|
-
content: z
|
|
16
|
-
focus: z
|
|
15
|
+
content: z
|
|
17
16
|
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.max(500_000)
|
|
19
|
+
.describe("Application source or IaC text to review"),
|
|
20
|
+
focus: z
|
|
21
|
+
.enum(["security", "reliability", "cost", "general"])
|
|
18
22
|
.optional()
|
|
23
|
+
.default("general")
|
|
19
24
|
.describe("Review focus: security, reliability, cost, general"),
|
|
25
|
+
format: z
|
|
26
|
+
.enum(["terraform", "cloudformation", "kubernetes", "application", "auto"])
|
|
27
|
+
.optional()
|
|
28
|
+
.default("auto")
|
|
29
|
+
.describe("Content format: terraform, cloudformation, kubernetes, application, auto"),
|
|
20
30
|
},
|
|
21
|
-
}, async ({ content, focus }) => {
|
|
31
|
+
}, async ({ content, focus, format }) => {
|
|
22
32
|
const auth = await requireApiAuth();
|
|
23
33
|
if (!auth.ok)
|
|
24
34
|
return unauthorizedContent(auth);
|
|
25
35
|
const botHints = await fetchPublicBotHints();
|
|
26
|
-
const focusValue = focus
|
|
27
|
-
|
|
28
|
-
focus === "cost" ||
|
|
29
|
-
focus === "general"
|
|
30
|
-
? focus
|
|
31
|
-
: "general";
|
|
36
|
+
const focusValue = focus ?? "general";
|
|
37
|
+
const formatValue = format ?? "auto";
|
|
32
38
|
const output = {
|
|
33
|
-
summary:
|
|
34
|
-
findings:
|
|
39
|
+
summary: `Structured architecture review from @skaleagents/swarm (focus=${focusValue}, format=${formatValue})`,
|
|
40
|
+
findings: architectureFindings(content, focusValue),
|
|
35
41
|
botHints,
|
|
36
42
|
};
|
|
37
43
|
return {
|
|
@@ -44,6 +50,8 @@ server.registerTool("scan_iac_stub", {
|
|
|
44
50
|
inputSchema: {
|
|
45
51
|
content: z
|
|
46
52
|
.string()
|
|
53
|
+
.min(1)
|
|
54
|
+
.max(500_000)
|
|
47
55
|
.describe("Terraform / CloudFormation / Kubernetes YAML"),
|
|
48
56
|
format: z
|
|
49
57
|
.string()
|
package/dist/review.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export type Finding = {
|
|
|
4
4
|
title: string;
|
|
5
5
|
detail: string;
|
|
6
6
|
};
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function architectureFindings(content: string, focus: string): Finding[];
|
|
8
8
|
export declare function fetchPublicBotHints(): Promise<string[]>;
|
|
9
9
|
export declare function countIacResources(content: string): number;
|
package/dist/review.js
CHANGED
|
@@ -1,31 +1,112 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { getApiToken, getApiUrl } from "./config.js";
|
|
2
|
+
function shouldInclude(focus, category) {
|
|
3
|
+
return focus === "general" || focus === category;
|
|
4
|
+
}
|
|
5
|
+
export function architectureFindings(content, focus) {
|
|
6
|
+
const focusValue = focus === "security" ||
|
|
7
|
+
focus === "reliability" ||
|
|
8
|
+
focus === "cost" ||
|
|
9
|
+
focus === "general"
|
|
10
|
+
? focus
|
|
11
|
+
: "general";
|
|
3
12
|
const findings = [
|
|
4
13
|
{
|
|
5
14
|
severity: "info",
|
|
6
|
-
title: "
|
|
7
|
-
detail: `
|
|
15
|
+
title: "Structured review completed",
|
|
16
|
+
detail: `Checked ${content.length} characters with the ${focusValue} ruleset. Findings are pattern-based and should be validated against the running system.`,
|
|
8
17
|
},
|
|
9
18
|
];
|
|
10
|
-
if (/0\.0\.0\.0(?:\/0)?/.test(content)) {
|
|
19
|
+
if (shouldInclude(focusValue, "security") && /0\.0\.0\.0(?:\/0)?/.test(content)) {
|
|
11
20
|
findings.push({
|
|
12
21
|
severity: "high",
|
|
13
22
|
title: "Broad network exposure",
|
|
14
|
-
detail: "Detected a possible wide-open CIDR or bind address.",
|
|
23
|
+
detail: "Detected a possible wide-open CIDR or bind address. Restrict ingress to approved sources and keep public listeners behind the intended edge control.",
|
|
15
24
|
});
|
|
16
25
|
}
|
|
17
|
-
if (/AKIA[0-9A-Z]{16}/.test(content)) {
|
|
26
|
+
if (shouldInclude(focusValue, "security") && /AKIA[0-9A-Z]{16}/.test(content)) {
|
|
18
27
|
findings.push({
|
|
19
28
|
severity: "critical",
|
|
20
29
|
title: "Possible AWS access key",
|
|
21
|
-
detail: "Remove
|
|
30
|
+
detail: "A value matches an AWS access-key pattern. Remove it from source, rotate the credential, and check repository history.",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (shouldInclude(focusValue, "security") &&
|
|
34
|
+
/-----BEGIN (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----/.test(content)) {
|
|
35
|
+
findings.push({
|
|
36
|
+
severity: "critical",
|
|
37
|
+
title: "Private key material in source",
|
|
38
|
+
detail: "Detected a private-key block. Revoke or rotate the key, remove it from source and history, and load replacement credentials through a secret manager.",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (shouldInclude(focusValue, "security") &&
|
|
42
|
+
/(?:password|passwd|secret|api[_-]?key|auth[_-]?token)\s*[:=]\s*["'][^"']{8,}["']/i.test(content) &&
|
|
43
|
+
!/(?:process\.env|secrets?\.|vault|parameter|<[^>]+>|\$\{)/i.test(content)) {
|
|
44
|
+
findings.push({
|
|
45
|
+
severity: "high",
|
|
46
|
+
title: "Hardcoded credential-like value",
|
|
47
|
+
detail: "A credential-like assignment contains a literal value. Move it to a secret manager or runtime environment and rotate the exposed value.",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (shouldInclude(focusValue, "security") &&
|
|
51
|
+
/(?:Action|actions?)\s*[:=][^\n]*["']?\*["']?/i.test(content)) {
|
|
52
|
+
findings.push({
|
|
53
|
+
severity: "high",
|
|
54
|
+
title: "Wildcard permission detected",
|
|
55
|
+
detail: "An IAM or policy action uses a wildcard. Replace it with the smallest permission set required by the workload.",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (shouldInclude(focusValue, "security") &&
|
|
59
|
+
/(?:privileged\s*:\s*true|hostNetwork\s*:\s*true|allowPrivilegeEscalation\s*:\s*true)/i.test(content)) {
|
|
60
|
+
findings.push({
|
|
61
|
+
severity: "critical",
|
|
62
|
+
title: "Elevated container privileges",
|
|
63
|
+
detail: "The content enables a privileged container setting. Remove it unless the workload requires it, then isolate and monitor that workload.",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (shouldInclude(focusValue, "security") &&
|
|
67
|
+
/(?:^|[\s"'])https?:\/\/(?!localhost\b|127\.0\.0\.1\b)/i.test(content)) {
|
|
68
|
+
findings.push({
|
|
69
|
+
severity: "medium",
|
|
70
|
+
title: "Unencrypted HTTP endpoint",
|
|
71
|
+
detail: "Detected an HTTP URL outside localhost. Use HTTPS for service and dependency traffic, and verify certificate validation is enabled.",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if ((shouldInclude(focusValue, "security") || shouldInclude(focusValue, "reliability")) &&
|
|
75
|
+
/(?:^|[\s:=])(?:[\w./-]+:)?latest(?:[\s"']|$)/im.test(content)) {
|
|
76
|
+
findings.push({
|
|
77
|
+
severity: "medium",
|
|
78
|
+
title: "Unpinned container image",
|
|
79
|
+
detail: "An image uses the mutable latest tag. Pin a version or digest so deployments are repeatable and rollback targets remain known.",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (shouldInclude(focusValue, "security") &&
|
|
83
|
+
/(?:public-read|publicRead|allUsers)/i.test(content)) {
|
|
84
|
+
findings.push({
|
|
85
|
+
severity: "high",
|
|
86
|
+
title: "Public data access pattern",
|
|
87
|
+
detail: "Detected a public access setting. Confirm the resource is intended to be public and restrict access when it is not.",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (shouldInclude(focusValue, "reliability") &&
|
|
91
|
+
/(?:debug|app_debug)\s*[:=]\s*["']?(?:true|1|yes)["']?/i.test(content)) {
|
|
92
|
+
findings.push({
|
|
93
|
+
severity: "medium",
|
|
94
|
+
title: "Debug mode enabled",
|
|
95
|
+
detail: "The content enables debug mode. Disable it in production to avoid noisy behavior and accidental disclosure of internal details.",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (shouldInclude(focusValue, "cost") && /(?:instance_type|machine_type|vm_size)\s*[:=]/i.test(content)) {
|
|
99
|
+
findings.push({
|
|
100
|
+
severity: "info",
|
|
101
|
+
title: "Compute sizing needs review",
|
|
102
|
+
detail: "Detected an explicit compute size. Compare the selected size with observed utilization and set a review point for scale-up and scale-down decisions.",
|
|
22
103
|
});
|
|
23
104
|
}
|
|
24
105
|
return findings;
|
|
25
106
|
}
|
|
26
107
|
export async function fetchPublicBotHints() {
|
|
27
108
|
try {
|
|
28
|
-
const res = await fetch(
|
|
109
|
+
const res = await fetch(`${getApiUrl()}/api/bots`, {
|
|
29
110
|
headers: {
|
|
30
111
|
Accept: "application/json",
|
|
31
112
|
Authorization: `Bearer ${getApiToken()}`,
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaleagents/swarm",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
4
8
|
"type": "module",
|
|
5
9
|
"description": "SkaleAgents MCP server — stdio tools against the Laravel api",
|
|
6
10
|
"bin": {
|
|
7
|
-
"skaleagents-swarm": "
|
|
11
|
+
"skaleagents-swarm": "dist/index.js"
|
|
8
12
|
},
|
|
9
13
|
"files": [
|
|
10
14
|
"dist"
|