@skaleagents/swarm 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # @skaleagents/swarm
2
2
 
3
- Public stdio MCP server for SkaleAgents Phase 1. Talks to the Laravel **api**
4
- over JSON and uses browser OAuth for sign-in.
3
+ SkaleAgents MCP server with local stdio and hosted Streamable HTTP transports.
4
+ Uses browser OAuth for sign-in.
5
5
 
6
6
  Tools: `review_architecture`, `scan_iac_stub`.
7
7
 
@@ -9,9 +9,26 @@ Tools: `review_architecture`, `scan_iac_stub`.
9
9
  client reads the files in its workspace and sends the relevant content through
10
10
  the MCP tool for a structured review.
11
11
 
12
- ## Prerequisites
12
+ ## Hosted connection
13
13
 
14
- 1. **API running:** Sail on `http://localhost:8082` (or your hosted API URL later).
14
+ Use `https://skaleagents.com/mcp` in Claude Desktop or ChatGPT's custom connector
15
+ settings. Choose OAuth and leave client ID and secret fields blank. The client
16
+ registers itself, opens Google sign-in, and asks you to approve MCP access.
17
+ See [client setup](https://skaleagents.com/settings) for Cursor, Claude Code,
18
+ Claude Desktop, ChatGPT, and Codex instructions.
19
+
20
+ The web app hosts this endpoint using `handleMcpRequest` from
21
+ `@skaleagents/swarm/http`. It validates each bearer credential with the API's
22
+ `/api/oauth/mcp-token` endpoint before running a tool. Tokens are bound to the
23
+ MCP resource and cannot access unrelated API routes. Credentials are not
24
+ forwarded to the public bot directory.
25
+
26
+ `protectedResourceMetadata` exports the discovery response for
27
+ `/.well-known/oauth-protected-resource/mcp`.
28
+
29
+ ## Local stdio prerequisites
30
+
31
+ 1. Node.js 20 or newer. The client connects to `https://api.skaleagents.com` by default.
15
32
  2. A browser that can open the SkaleAgents sign-in page.
16
33
 
17
34
  The first tool call opens browser sign-in. Approve MCP access there and return
@@ -67,17 +84,14 @@ Dev without build:
67
84
  }
68
85
  ```
69
86
 
70
- ### Option B: after npm publish (hosted API)
87
+ ### Option B: published package (hosted API)
71
88
 
72
89
  ```json
73
90
  {
74
91
  "mcpServers": {
75
92
  "skaleagents": {
76
93
  "command": "npx",
77
- "args": ["-y", "@skaleagents/swarm"],
78
- "env": {
79
- "PLATFORM_API_URL": "https://api.skaleagents.com"
80
- }
94
+ "args": ["-y", "@skaleagents/swarm@0.4.0"]
81
95
  }
82
96
  }
83
97
  }
@@ -87,16 +101,17 @@ Restart Cursor after saving. In Agent/Chat, tools should appear as `review_archi
87
101
 
88
102
  ## Claude Code
89
103
 
90
- Point `command`/`args` at `node .../dist/index.js` or `npx @skaleagents/swarm` once published. OAuth starts on the first tool call.
104
+ Use the published package configuration above in `.mcp.json`. OAuth starts on the first tool call. No API URL or keys are needed.
91
105
 
92
106
  ## Environment
93
107
 
94
108
  | Variable | Required | Description |
95
109
  |----------|----------|-------------|
96
110
  | `SKALEAGENTS_API_TOKEN` | No | Legacy Sanctum bearer-token override. OAuth is used when empty. |
97
- | `PLATFORM_API_URL` | No | Default `http://localhost:8082` |
111
+ | `PLATFORM_API_URL` | No | Defaults to `https://api.skaleagents.com`. Override only for local development or another API deployment. Empty values use the default. |
98
112
  | `SKALEAGENTS_OAUTH_CACHE` | No | OAuth cache path. Default `~/.config/skaleagents/oauth.json`. |
99
113
  | `SKALEAGENTS_OAUTH_ENABLED` | No | Set to `false` only to disable browser OAuth. |
114
+ | `MCP_RESOURCE_URL` | No | Hosted transport audience. Defaults to `https://skaleagents.com/mcp`; must match the API OAuth configuration. |
100
115
 
101
116
  ## Auth behavior
102
117
 
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  export function getApiUrl() {
4
- return (process.env.PLATFORM_API_URL?.replace(/\/$/, "") ?? "http://localhost:8082");
4
+ return (process.env.PLATFORM_API_URL?.trim().replace(/\/+$/, "") || "https://api.skaleagents.com");
5
5
  }
6
6
  export function getApiToken() {
7
7
  return process.env.SKALEAGENTS_API_TOKEN?.trim() ?? "";
package/dist/http.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function protectedResourceMetadata(): Response;
2
+ export declare function handleMcpRequest(request: Request): Promise<Response>;
package/dist/http.js ADDED
@@ -0,0 +1,134 @@
1
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
2
+ import { createServer } from "./server.js";
3
+ import { getApiUrl } from "./config.js";
4
+ const resource = process.env.MCP_RESOURCE_URL ?? "https://skaleagents.com/mcp";
5
+ const metadataUrl = `${new URL(resource).origin}/.well-known/oauth-protected-resource/mcp`;
6
+ const allowedOrigins = new Set([
7
+ "https://claude.ai",
8
+ "https://chatgpt.com",
9
+ "https://platform.openai.com",
10
+ new URL(resource).origin,
11
+ ]);
12
+ const headers = {
13
+ "Cache-Control": "no-store",
14
+ "Access-Control-Allow-Origin": "*",
15
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
16
+ "Access-Control-Allow-Headers": "Authorization, Content-Type, Accept, MCP-Protocol-Version, MCP-Session-Id, Last-Event-ID",
17
+ "Access-Control-Expose-Headers": "WWW-Authenticate, MCP-Session-Id, MCP-Protocol-Version",
18
+ };
19
+ function json(status, body, extraHeaders = {}) {
20
+ return Response.json(body, {
21
+ status,
22
+ headers: { ...headers, ...extraHeaders },
23
+ });
24
+ }
25
+ function challenge() {
26
+ return json(401, { error: "unauthorized" }, {
27
+ "WWW-Authenticate": `Bearer resource_metadata="${metadataUrl}", scope="mcp"`,
28
+ });
29
+ }
30
+ export function protectedResourceMetadata() {
31
+ return json(200, {
32
+ resource,
33
+ authorization_servers: [getApiUrl()],
34
+ scopes_supported: ["mcp"],
35
+ bearer_methods_supported: ["header"],
36
+ resource_documentation: "https://skaleagents.com/settings",
37
+ });
38
+ }
39
+ export async function handleMcpRequest(request) {
40
+ const origin = request.headers.get("origin");
41
+ if (origin && !allowedOrigins.has(origin))
42
+ return json(403, { error: "origin_not_allowed" });
43
+ if (request.method === "OPTIONS")
44
+ return new Response(null, { status: 204, headers });
45
+ const authorization = request.headers.get("authorization");
46
+ if (!authorization || !/^Bearer [^\s]+$/i.test(authorization))
47
+ return challenge();
48
+ try {
49
+ // Validate the opaque token with its issuer, including the MCP audience.
50
+ const validation = await fetch(`${getApiUrl()}/api/oauth/mcp-token`, {
51
+ headers: { Authorization: authorization, Accept: "application/json" },
52
+ signal: AbortSignal.timeout(10_000),
53
+ redirect: "error",
54
+ cache: "no-store",
55
+ });
56
+ if (validation.status === 401 || validation.status === 403)
57
+ return challenge();
58
+ if (!validation.ok)
59
+ return json(503, { error: "authorization_unavailable" });
60
+ const token = (await validation.json());
61
+ if (!token.active ||
62
+ token.resource !== resource ||
63
+ token.scope !== "mcp" ||
64
+ !token.expiresAt ||
65
+ token.expiresAt * 1000 <= Date.now()) {
66
+ return challenge();
67
+ }
68
+ }
69
+ catch {
70
+ return json(503, { error: "authorization_unavailable" });
71
+ }
72
+ if (request.method !== "POST")
73
+ return json(405, { error: "method_not_allowed" }, { Allow: "POST, OPTIONS" });
74
+ if (!request.headers.get("content-type")?.startsWith("application/json"))
75
+ return json(415, { error: "expected_json" });
76
+ let body;
77
+ try {
78
+ const reader = request.body?.getReader();
79
+ if (!reader)
80
+ return json(400, { error: "missing_body" });
81
+ const chunks = [];
82
+ let size = 0;
83
+ try {
84
+ for (;;) {
85
+ const { value, done } = await reader.read();
86
+ if (done)
87
+ break;
88
+ size += value.byteLength;
89
+ if (size > 2_100_000) {
90
+ await reader.cancel();
91
+ return json(413, { error: "request_too_large" });
92
+ }
93
+ chunks.push(value);
94
+ }
95
+ }
96
+ finally {
97
+ reader.releaseLock();
98
+ }
99
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
100
+ }
101
+ catch {
102
+ return json(400, {
103
+ jsonrpc: "2.0",
104
+ id: null,
105
+ error: { code: -32700, message: "Invalid JSON" },
106
+ });
107
+ }
108
+ const server = createServer(true);
109
+ const transport = new WebStandardStreamableHTTPServerTransport({
110
+ sessionIdGenerator: undefined,
111
+ enableJsonResponse: true,
112
+ });
113
+ try {
114
+ await server.connect(transport);
115
+ const response = await transport.handleRequest(request, {
116
+ parsedBody: body,
117
+ });
118
+ // Finish the JSON response before closing this request-scoped transport.
119
+ const responseBody = await response.arrayBuffer();
120
+ const responseHeaders = new Headers(response.headers);
121
+ for (const [key, value] of Object.entries(headers))
122
+ responseHeaders.set(key, value);
123
+ return new Response(responseBody.byteLength ? responseBody : null, {
124
+ status: response.status,
125
+ headers: responseHeaders,
126
+ });
127
+ }
128
+ catch {
129
+ return json(500, { error: "mcp_request_failed" });
130
+ }
131
+ finally {
132
+ await server.close();
133
+ }
134
+ }
package/dist/index.js CHANGED
@@ -1,91 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { z } from "zod";
5
- import { getApiAccessToken, requireApiAuth, unauthorizedContent, } from "./auth.js";
6
- import { architectureFindings, countIacResources, fetchPublicBotHints, } from "./review.js";
7
- const server = new McpServer({
8
- name: "skaleagents-swarm",
9
- version: "0.3.0",
10
- });
11
- server.registerTool("review_architecture", {
12
- title: "Review architecture",
13
- description: "Review application source or infrastructure text for security, reliability, and cost risks.",
14
- inputSchema: {
15
- content: z
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"])
22
- .optional()
23
- .default("general")
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"),
30
- },
31
- }, async ({ content, focus, format }) => {
32
- const auth = await requireApiAuth();
33
- if (!auth.ok)
34
- return unauthorizedContent(auth);
35
- const token = await getApiAccessToken();
36
- if (!token)
37
- return unauthorizedContent({ ok: false, reason: "oauth_failed" });
38
- const botHints = await fetchPublicBotHints(token);
39
- const focusValue = focus ?? "general";
40
- const formatValue = format ?? "auto";
41
- const output = {
42
- summary: `Structured architecture review from @skaleagents/swarm (focus=${focusValue}, format=${formatValue})`,
43
- findings: architectureFindings(content, focusValue),
44
- botHints,
45
- };
46
- return {
47
- content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
48
- };
49
- });
50
- server.registerTool("scan_iac_stub", {
51
- title: "Scan IaC (stub)",
52
- description: "Stub seam for Phase 2 DevSecOps IaC scanning.",
53
- inputSchema: {
54
- content: z
55
- .string()
56
- .min(1)
57
- .max(500_000)
58
- .describe("Terraform / CloudFormation / Kubernetes YAML"),
59
- format: z
60
- .string()
61
- .optional()
62
- .describe("terraform, cloudformation, kubernetes, auto"),
63
- },
64
- }, async ({ content, format }) => {
65
- const auth = await requireApiAuth();
66
- if (!auth.ok)
67
- return unauthorizedContent(auth);
68
- const formatValue = format === "terraform" ||
69
- format === "cloudformation" ||
70
- format === "kubernetes" ||
71
- format === "auto"
72
- ? format
73
- : "auto";
74
- const output = {
75
- status: "stub",
76
- message: "Full IaC scanning lands in Phase 2 agent-swarm",
77
- format: formatValue,
78
- parsedResourceCount: countIacResources(content),
79
- };
80
- return {
81
- content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
82
- };
83
- });
84
- async function main() {
85
- const transport = new StdioServerTransport();
86
- await server.connect(transport);
87
- }
88
- main().catch((err) => {
89
- console.error(err);
3
+ import { createServer } from "./server.js";
4
+ createServer()
5
+ .connect(new StdioServerTransport())
6
+ .catch((error) => {
7
+ console.error(error);
90
8
  process.exit(1);
91
9
  });
package/dist/review.d.ts CHANGED
@@ -5,5 +5,5 @@ export type Finding = {
5
5
  detail: string;
6
6
  };
7
7
  export declare function architectureFindings(content: string, focus: string): Finding[];
8
- export declare function fetchPublicBotHints(token: string): Promise<string[]>;
8
+ export declare function fetchPublicBotHints(token?: string): Promise<string[]>;
9
9
  export declare function countIacResources(content: string): number;
package/dist/review.js CHANGED
@@ -107,9 +107,10 @@ export function architectureFindings(content, focus) {
107
107
  export async function fetchPublicBotHints(token) {
108
108
  try {
109
109
  const res = await fetch(`${getApiUrl()}/api/bots`, {
110
+ signal: AbortSignal.timeout(10_000),
110
111
  headers: {
111
112
  Accept: "application/json",
112
- Authorization: `Bearer ${token}`,
113
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
113
114
  },
114
115
  });
115
116
  if (!res.ok)
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(remote?: boolean): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,107 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getApiAccessToken, requireApiAuth, unauthorizedContent, } from "./auth.js";
4
+ import { architectureFindings, countIacResources, fetchPublicBotHints, } from "./review.js";
5
+ export function createServer(remote = false) {
6
+ const server = new McpServer({
7
+ name: "skaleagents-swarm",
8
+ version: "0.4.0",
9
+ });
10
+ server.registerTool("review_architecture", {
11
+ title: "Review architecture",
12
+ description: "Review application source or infrastructure text for security, reliability, and cost risks.",
13
+ annotations: {
14
+ readOnlyHint: true,
15
+ destructiveHint: false,
16
+ openWorldHint: false,
17
+ },
18
+ _meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
19
+ inputSchema: {
20
+ content: z
21
+ .string()
22
+ .min(1)
23
+ .max(500_000)
24
+ .describe("Application source or IaC text to review"),
25
+ focus: z
26
+ .enum(["security", "reliability", "cost", "general"])
27
+ .optional()
28
+ .default("general")
29
+ .describe("Review focus: security, reliability, cost, general"),
30
+ format: z
31
+ .enum([
32
+ "terraform",
33
+ "cloudformation",
34
+ "kubernetes",
35
+ "application",
36
+ "auto",
37
+ ])
38
+ .optional()
39
+ .default("auto")
40
+ .describe("Content format: terraform, cloudformation, kubernetes, application, auto"),
41
+ },
42
+ }, async ({ content, focus, format }) => {
43
+ let token;
44
+ if (!remote) {
45
+ const auth = await requireApiAuth();
46
+ if (!auth.ok)
47
+ return unauthorizedContent(auth);
48
+ token = (await getApiAccessToken()) ?? undefined;
49
+ if (!token)
50
+ return unauthorizedContent({ ok: false, reason: "oauth_failed" });
51
+ }
52
+ const botHints = await fetchPublicBotHints(token);
53
+ const focusValue = focus ?? "general";
54
+ const formatValue = format ?? "auto";
55
+ const output = {
56
+ summary: `Structured architecture review from @skaleagents/swarm (focus=${focusValue}, format=${formatValue})`,
57
+ findings: architectureFindings(content, focusValue),
58
+ botHints,
59
+ };
60
+ return {
61
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
62
+ };
63
+ });
64
+ server.registerTool("scan_iac_stub", {
65
+ title: "Scan IaC (stub)",
66
+ description: "Stub seam for Phase 2 DevSecOps IaC scanning.",
67
+ annotations: {
68
+ readOnlyHint: true,
69
+ destructiveHint: false,
70
+ openWorldHint: false,
71
+ },
72
+ _meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
73
+ inputSchema: {
74
+ content: z
75
+ .string()
76
+ .min(1)
77
+ .max(500_000)
78
+ .describe("Terraform / CloudFormation / Kubernetes YAML"),
79
+ format: z
80
+ .string()
81
+ .optional()
82
+ .describe("terraform, cloudformation, kubernetes, auto"),
83
+ },
84
+ }, async ({ content, format }) => {
85
+ if (!remote) {
86
+ const auth = await requireApiAuth();
87
+ if (!auth.ok)
88
+ return unauthorizedContent(auth);
89
+ }
90
+ const formatValue = format === "terraform" ||
91
+ format === "cloudformation" ||
92
+ format === "kubernetes" ||
93
+ format === "auto"
94
+ ? format
95
+ : "auto";
96
+ const output = {
97
+ status: "stub",
98
+ message: "Full IaC scanning lands in Phase 2 agent-swarm",
99
+ format: formatValue,
100
+ parsedResourceCount: countIacResources(content),
101
+ };
102
+ return {
103
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
104
+ };
105
+ });
106
+ return server;
107
+ }
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "@skaleagents/swarm",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "type": "module",
9
- "description": "SkaleAgents MCP server — stdio tools against the Laravel api",
9
+ "description": "SkaleAgents review tools with OAuth for local and hosted MCP clients",
10
+ "exports": {
11
+ "./http": {
12
+ "types": "./dist/http.d.ts",
13
+ "import": "./dist/http.js"
14
+ }
15
+ },
10
16
  "bin": {
11
17
  "skaleagents-swarm": "dist/index.js"
12
18
  },