@skaleagents/swarm 0.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/README.md +46 -0
- package/dist/auth.d.ts +20 -0
- package/dist/auth.js +40 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +80 -0
- package/dist/review.d.ts +9 -0
- package/dist/review.js +48 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @skaleagents/swarm (legacy template)
|
|
2
|
+
|
|
3
|
+
Historical stdio MCP starter for SkaleAgents Phase 1 (`review_architecture`, `scan_iac_stub`).
|
|
4
|
+
The extracted [`mcp-server`](https://github.com/SkaleAgents/mcp-server) repo is canonical.
|
|
5
|
+
|
|
6
|
+
This template talks to the legacy Fastify API with `SKALEAGENTS_API_TOKEN` (personal access token from mock Google login).
|
|
7
|
+
|
|
8
|
+
The default points at the Fastify template on `:4000`. For the extracted Laravel API, use `PLATFORM_API_URL=http://localhost:8082` and `PLATFORM_API_PREFIX=/api`.
|
|
9
|
+
|
|
10
|
+
## Local
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install
|
|
14
|
+
cp .env.example .env
|
|
15
|
+
# Get a token:
|
|
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"}'
|
|
19
|
+
npm run build
|
|
20
|
+
npm test
|
|
21
|
+
npm run smoke # needs platform-api on PLATFORM_API_URL
|
|
22
|
+
# Extracted Laravel API:
|
|
23
|
+
# PLATFORM_API_URL=http://localhost:8082 PLATFORM_API_PREFIX=/api npm run smoke
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Cursor / Claude Code
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"skaleagents": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["tsx", "/absolute/path/to/mcp-server/src/index.ts"],
|
|
34
|
+
"env": {
|
|
35
|
+
"SKALEAGENTS_API_TOKEN": "<token from Google callback>",
|
|
36
|
+
"PLATFORM_API_URL": "http://localhost:4000",
|
|
37
|
+
"PLATFORM_API_PREFIX": ""
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or after build: `"command": "node", "args": ["/absolute/path/to/mcp-server/dist/index.js"]`.
|
|
45
|
+
|
|
46
|
+
Hub contract: [docs/contracts/mcp/tools.md](https://github.com/SkaleAgents/workspace/blob/main/docs/contracts/mcp/tools.md)
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type AuthResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
userId?: string;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
reason: "missing_token" | "unauthorized" | "api_unavailable";
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Require the API to validate the bearer token before running any tool.
|
|
10
|
+
*/
|
|
11
|
+
export declare function requireApiAuth(): Promise<AuthResult>;
|
|
12
|
+
export declare function unauthorizedContent(reason: AuthResult & {
|
|
13
|
+
ok: false;
|
|
14
|
+
}): {
|
|
15
|
+
isError: true;
|
|
16
|
+
content: {
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
}[];
|
|
20
|
+
};
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getApiEndpoint, getApiToken } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Require the API to validate the bearer token before running any tool.
|
|
4
|
+
*/
|
|
5
|
+
export async function requireApiAuth() {
|
|
6
|
+
const token = getApiToken();
|
|
7
|
+
if (!token) {
|
|
8
|
+
return { ok: false, reason: "missing_token" };
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(getApiEndpoint("/user"), {
|
|
12
|
+
headers: {
|
|
13
|
+
Accept: "application/json",
|
|
14
|
+
Authorization: `Bearer ${token}`,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
if (res.status === 401 || res.status === 403) {
|
|
18
|
+
return { ok: false, reason: "unauthorized" };
|
|
19
|
+
}
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
return { ok: false, reason: "unauthorized" };
|
|
22
|
+
}
|
|
23
|
+
const user = (await res.json());
|
|
24
|
+
return { ok: true, userId: user.id };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { ok: false, reason: "api_unavailable" };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function unauthorizedContent(reason) {
|
|
31
|
+
const text = reason.reason === "missing_token"
|
|
32
|
+
? "unauthorized: set SKALEAGENTS_API_TOKEN"
|
|
33
|
+
: reason.reason === "unauthorized"
|
|
34
|
+
? "unauthorized: invalid SKALEAGENTS_API_TOKEN"
|
|
35
|
+
: "api unavailable: could not validate SKALEAGENTS_API_TOKEN";
|
|
36
|
+
return {
|
|
37
|
+
isError: true,
|
|
38
|
+
content: [{ type: "text", text }],
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function getApiUrl() {
|
|
2
|
+
return (process.env.PLATFORM_API_URL?.replace(/\/$/, "") ?? "http://localhost:4000");
|
|
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}`}`;
|
|
8
|
+
}
|
|
9
|
+
export function getApiToken() {
|
|
10
|
+
return process.env.SKALEAGENTS_API_TOKEN?.trim() ?? "";
|
|
11
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { requireApiAuth, unauthorizedContent } from "./auth.js";
|
|
6
|
+
import { cannedFindings, countIacResources, fetchPublicBotHints, } from "./review.js";
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "skaleagents-swarm",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
});
|
|
11
|
+
server.registerTool("review_architecture", {
|
|
12
|
+
title: "Review architecture",
|
|
13
|
+
description: "Request a high-level architecture / security review of a code or IaC snippet.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
content: z.string().describe("Source or IaC text to review"),
|
|
16
|
+
focus: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Review focus: security, reliability, cost, general"),
|
|
20
|
+
},
|
|
21
|
+
}, async ({ content, focus }) => {
|
|
22
|
+
const auth = await requireApiAuth();
|
|
23
|
+
if (!auth.ok)
|
|
24
|
+
return unauthorizedContent(auth);
|
|
25
|
+
const botHints = await fetchPublicBotHints();
|
|
26
|
+
const focusValue = focus === "security" ||
|
|
27
|
+
focus === "reliability" ||
|
|
28
|
+
focus === "cost" ||
|
|
29
|
+
focus === "general"
|
|
30
|
+
? focus
|
|
31
|
+
: "general";
|
|
32
|
+
const output = {
|
|
33
|
+
summary: "Phase 1 stub architecture review from @skaleagents/swarm",
|
|
34
|
+
findings: cannedFindings(content, focusValue),
|
|
35
|
+
botHints,
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
server.registerTool("scan_iac_stub", {
|
|
42
|
+
title: "Scan IaC (stub)",
|
|
43
|
+
description: "Stub seam for Phase 2 DevSecOps IaC scanning.",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
content: z
|
|
46
|
+
.string()
|
|
47
|
+
.describe("Terraform / CloudFormation / Kubernetes YAML"),
|
|
48
|
+
format: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("terraform, cloudformation, kubernetes, auto"),
|
|
52
|
+
},
|
|
53
|
+
}, async ({ content, format }) => {
|
|
54
|
+
const auth = await requireApiAuth();
|
|
55
|
+
if (!auth.ok)
|
|
56
|
+
return unauthorizedContent(auth);
|
|
57
|
+
const formatValue = format === "terraform" ||
|
|
58
|
+
format === "cloudformation" ||
|
|
59
|
+
format === "kubernetes" ||
|
|
60
|
+
format === "auto"
|
|
61
|
+
? format
|
|
62
|
+
: "auto";
|
|
63
|
+
const output = {
|
|
64
|
+
status: "stub",
|
|
65
|
+
message: "Full IaC scanning lands in Phase 2 agent-swarm",
|
|
66
|
+
format: formatValue,
|
|
67
|
+
parsedResourceCount: countIacResources(content),
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
async function main() {
|
|
74
|
+
const transport = new StdioServerTransport();
|
|
75
|
+
await server.connect(transport);
|
|
76
|
+
}
|
|
77
|
+
main().catch((err) => {
|
|
78
|
+
console.error(err);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
package/dist/review.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type FindingSeverity = "info" | "low" | "medium" | "high" | "critical";
|
|
2
|
+
export type Finding = {
|
|
3
|
+
severity: FindingSeverity;
|
|
4
|
+
title: string;
|
|
5
|
+
detail: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function cannedFindings(content: string, focus: string): Finding[];
|
|
8
|
+
export declare function fetchPublicBotHints(): Promise<string[]>;
|
|
9
|
+
export declare function countIacResources(content: string): number;
|
package/dist/review.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getApiEndpoint, getApiToken } from "./config.js";
|
|
2
|
+
export function cannedFindings(content, focus) {
|
|
3
|
+
const findings = [
|
|
4
|
+
{
|
|
5
|
+
severity: "info",
|
|
6
|
+
title: "Stub review",
|
|
7
|
+
detail: `Phase 1 canned review (focus=${focus}). Length=${content.length} chars.`,
|
|
8
|
+
},
|
|
9
|
+
];
|
|
10
|
+
if (/0\.0\.0\.0(?:\/0)?/.test(content)) {
|
|
11
|
+
findings.push({
|
|
12
|
+
severity: "high",
|
|
13
|
+
title: "Broad network exposure",
|
|
14
|
+
detail: "Detected a possible wide-open CIDR or bind address.",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (/AKIA[0-9A-Z]{16}/.test(content)) {
|
|
18
|
+
findings.push({
|
|
19
|
+
severity: "critical",
|
|
20
|
+
title: "Possible AWS access key",
|
|
21
|
+
detail: "Remove secrets from source; rotate credentials.",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return findings;
|
|
25
|
+
}
|
|
26
|
+
export async function fetchPublicBotHints() {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(getApiEndpoint("/bots"), {
|
|
29
|
+
headers: {
|
|
30
|
+
Accept: "application/json",
|
|
31
|
+
Authorization: `Bearer ${getApiToken()}`,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
return [];
|
|
36
|
+
const data = (await res.json());
|
|
37
|
+
return data.bots
|
|
38
|
+
.filter((b) => b.status === "published" && b.visibility === "public")
|
|
39
|
+
.map((b) => b.name)
|
|
40
|
+
.slice(0, 5);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function countIacResources(content) {
|
|
47
|
+
return (content.match(/\bresource\b|\bkind:\s*\w+/gi) ?? []).length;
|
|
48
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skaleagents/swarm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "SkaleAgents MCP server — stdio tools against the Laravel api",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skaleagents-swarm": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "tsx --test src/**/*.test.ts",
|
|
18
|
+
"smoke": "tsx scripts/smoke-tools.mjs"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
25
|
+
"zod": "^4.4.3"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.20.1",
|
|
29
|
+
"tsx": "^4.23.12",
|
|
30
|
+
"typescript": "^5.7.3"
|
|
31
|
+
}
|
|
32
|
+
}
|