@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 CHANGED
@@ -1,29 +1,60 @@
1
- # @skaleagents/swarm (legacy template)
1
+ # @skaleagents/swarm
2
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.
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
- This template talks to the legacy Fastify API with `SKALEAGENTS_API_TOKEN` (personal access token from mock Google login).
6
+ Tools: `review_architecture`, `scan_iac_stub`.
7
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`.
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
- ## Local
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
- # 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"}'
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 platform-api on PLATFORM_API_URL
22
- # Extracted Laravel API:
23
- # PLATFORM_API_URL=http://localhost:8082 PLATFORM_API_PREFIX=/api npm run smoke
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
- ## Cursor / Claude Code
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 from Google callback>",
36
- "PLATFORM_API_URL": "http://localhost:4000",
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
- Or after build: `"command": "node", "args": ["/absolute/path/to/mcp-server/dist/index.js"]`.
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 { getApiEndpoint, getApiToken } from "./config.js";
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(getApiEndpoint("/user"), {
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: "unauthorized" };
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
@@ -1,3 +1,2 @@
1
1
  export declare function getApiUrl(): string;
2
- export declare function getApiEndpoint(path: string): string;
3
2
  export declare function getApiToken(): string;
package/dist/config.js CHANGED
@@ -1,10 +1,5 @@
1
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}`}`;
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 { cannedFindings, countIacResources, fetchPublicBotHints, } from "./review.js";
6
+ import { architectureFindings, countIacResources, fetchPublicBotHints, } from "./review.js";
7
7
  const server = new McpServer({
8
8
  name: "skaleagents-swarm",
9
- version: "0.1.0",
9
+ version: "0.2.0",
10
10
  });
11
11
  server.registerTool("review_architecture", {
12
12
  title: "Review architecture",
13
- description: "Request a high-level architecture / security review of a code or IaC snippet.",
13
+ description: "Review application source or infrastructure text for security, reliability, and cost risks.",
14
14
  inputSchema: {
15
- content: z.string().describe("Source or IaC text to review"),
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 === "security" ||
27
- focus === "reliability" ||
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: "Phase 1 stub architecture review from @skaleagents/swarm",
34
- findings: cannedFindings(content, focusValue),
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 cannedFindings(content: string, focus: string): Finding[];
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 { getApiEndpoint, getApiToken } from "./config.js";
2
- export function cannedFindings(content, focus) {
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: "Stub review",
7
- detail: `Phase 1 canned review (focus=${focus}). Length=${content.length} chars.`,
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 secrets from source; rotate credentials.",
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(getApiEndpoint("/bots"), {
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.0",
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": "./dist/index.js"
11
+ "skaleagents-swarm": "dist/index.js"
8
12
  },
9
13
  "files": [
10
14
  "dist"