mcp-server-cloud-agent 1.0.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.
Files changed (4) hide show
  1. package/README.md +96 -0
  2. package/index.js +341 -0
  3. package/package.json +58 -0
  4. package/server.json +36 -0
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # mcp-server-cloud-agent
2
+
3
+ MCP server for **Cloud Agent** — an AI software engineer that writes code, opens PRs, reviews code, generates tests, runs security scans, and answers codebase questions.
4
+
5
+ Connect from any MCP client (Claude Code, Cursor, Windsurf, or your own agents) and delegate engineering tasks.
6
+
7
+ ## Tools (9)
8
+
9
+ | Tool | Description |
10
+ |------|-------------|
11
+ | `run_task` | Write code, fix bugs, add features — returns result + PR URL |
12
+ | `review_pr` | Review a GitHub PR with structured feedback, optionally post to GitHub |
13
+ | `ask_codebase` | Ask questions about any GitHub repo (auto-indexes on first use) |
14
+ | `generate_tests` | Generate tests for a file or feature, opens a PR |
15
+ | `security_scan` | Security + dependency scan across one or more repos |
16
+ | `list_sessions` | List recent sessions with status, cost, duration, PR URLs |
17
+ | `list_playbooks` | List available workflow templates (bug-triage, test-coverage, etc.) |
18
+ | `run_playbook` | Run a playbook against a repo |
19
+ | `get_usage` | Usage stats — sessions, cost, time saved, breakdowns |
20
+
21
+ ## Setup
22
+
23
+ ### 1. Get an API key
24
+
25
+ Sign in to your Cloud Agent web workspace and generate an API key at `/auth/api-key`.
26
+
27
+ ### 2. Configure your MCP client
28
+
29
+ **Claude Code** (`~/.claude.json`):
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "cloud-agent": {
34
+ "command": "npx",
35
+ "args": ["-y", "mcp-server-cloud-agent"],
36
+ "env": {
37
+ "CLOUD_AGENT_API_KEY": "ca_your_key_here"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ **Cursor / Windsurf** (MCP settings):
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "cloud-agent": {
49
+ "command": "npx",
50
+ "args": ["-y", "mcp-server-cloud-agent"],
51
+ "env": {
52
+ "CLOUD_AGENT_API_KEY": "ca_your_key_here"
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### Environment Variables
60
+
61
+ | Variable | Required | Description |
62
+ |----------|----------|-------------|
63
+ | `CLOUD_AGENT_API_KEY` | Yes | API key (`ca_*` prefix) from your Cloud Agent workspace |
64
+ | `CLOUD_AGENT_URL` | No | Backend URL (defaults to `https://cloudagent.metaltorque.dev`) |
65
+
66
+ ## Usage Examples
67
+
68
+ Once configured, your MCP client can call these tools directly:
69
+
70
+ **Fix a bug:**
71
+ > "Use cloud-agent to fix the broken login flow in myorg/myapp"
72
+
73
+ **Review a PR:**
74
+ > "Use cloud-agent to review https://github.com/myorg/myapp/pull/42"
75
+
76
+ **Ask about code:**
77
+ > "Use cloud-agent to explain how authentication works in myorg/myapp"
78
+
79
+ **Generate tests:**
80
+ > "Use cloud-agent to generate tests for src/auth.ts in myorg/myapp"
81
+
82
+ **Security scan:**
83
+ > "Use cloud-agent to scan myorg/myapp and myorg/api for vulnerabilities"
84
+
85
+ **Run a playbook:**
86
+ > "Use cloud-agent to run the bug-triage playbook on myorg/myapp"
87
+
88
+ ## What is Cloud Agent?
89
+
90
+ Cloud Agent is a Devin-alternative that puts an AI software engineer where your team already works — Slack, Teams, Jira, Linear, and 12 more platforms. It writes code, opens PRs, reviews code, generates tests, runs security scans, and answers codebase questions.
91
+
92
+ This MCP server gives any MCP-compatible AI client the same capabilities.
93
+
94
+ ## License
95
+
96
+ MIT
package/index.js ADDED
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
4
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ const { z } = require("zod");
6
+ const https = require("https");
7
+ const http = require("http");
8
+ const { version } = require("./package.json");
9
+
10
+ // ── Config ──────────────────────────────────────────────────────────
11
+
12
+ const API_KEY = process.env.CLOUD_AGENT_API_KEY || "";
13
+ const BASE_URL = (process.env.CLOUD_AGENT_URL || "https://cloudagent.metaltorque.dev").replace(/\/$/, "");
14
+
15
+ // ── HTTP helper ─────────────────────────────────────────────────────
16
+
17
+ const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
18
+
19
+ function request(method, urlPath, body, timeout = 600_000) {
20
+ return new Promise((resolve, reject) => {
21
+ const fullUrl = `${BASE_URL}${urlPath}`;
22
+ const parsed = new URL(fullUrl);
23
+ const isHttps = parsed.protocol === "https:";
24
+
25
+ if (!isHttps && API_KEY) {
26
+ return reject(new Error("Refusing to send API key over insecure HTTP. Use HTTPS."));
27
+ }
28
+
29
+ const mod = isHttps ? https : http;
30
+
31
+ const headers = {
32
+ "Content-Type": "application/json",
33
+ "User-Agent": `mcp-server-cloud-agent/${version}`,
34
+ };
35
+ if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`;
36
+
37
+ const opts = {
38
+ hostname: parsed.hostname,
39
+ port: parsed.port || (isHttps ? 443 : 80),
40
+ path: parsed.pathname + parsed.search,
41
+ method,
42
+ headers,
43
+ timeout,
44
+ };
45
+
46
+ const req = mod.request(opts, (res) => {
47
+ let data = "";
48
+ let size = 0;
49
+ res.on("data", (c) => {
50
+ size += c.length;
51
+ if (size > MAX_RESPONSE_SIZE) { req.destroy(); return reject(new Error("Response too large")); }
52
+ data += c;
53
+ });
54
+ res.on("end", () => {
55
+ try {
56
+ const json = JSON.parse(data);
57
+ if (res.statusCode >= 400) return reject(new Error(json.error || `HTTP ${res.statusCode}`));
58
+ resolve(json);
59
+ } catch {
60
+ if (res.statusCode >= 400) return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 300)}`));
61
+ resolve(data);
62
+ }
63
+ });
64
+ });
65
+
66
+ req.on("error", reject);
67
+ req.on("timeout", () => { req.destroy(); reject(new Error("Request timed out")); });
68
+ if (body) req.write(JSON.stringify(body));
69
+ req.end();
70
+ });
71
+ }
72
+
73
+ function noKeyError() {
74
+ return {
75
+ content: [{
76
+ type: "text",
77
+ text: "Error: CLOUD_AGENT_API_KEY environment variable is required.\n\nGet an API key from your Cloud Agent web workspace at /auth/api-key.\nAPI keys use the ca_* prefix.",
78
+ }],
79
+ };
80
+ }
81
+
82
+ // ── MCP Server ──────────────────────────────────────────────────────
83
+
84
+ const server = new McpServer({
85
+ name: "cloud-agent",
86
+ version,
87
+ });
88
+
89
+ // ── Tool: run_task ──────────────────────────────────────────────────
90
+
91
+ server.tool(
92
+ "run_task",
93
+ "Run a coding task: write code, fix bugs, add features, refactor. The AI agent clones the repo, makes changes, and opens a PR. Returns the result and PR URL when complete.",
94
+ {
95
+ prompt: z.string().describe("Task description, e.g. 'Fix the login bug in owner/repo' or 'Add dark mode to owner/repo'"),
96
+ },
97
+ async ({ prompt }) => {
98
+ if (!API_KEY) return noKeyError();
99
+ try {
100
+ const result = await request("POST", "/query", { prompt });
101
+ return {
102
+ content: [{
103
+ type: "text",
104
+ text: JSON.stringify({
105
+ response: result.response,
106
+ cost_usd: result.cost_usd,
107
+ duration_ms: result.duration_ms,
108
+ pr_url: result.pr_url || null,
109
+ }, null, 2),
110
+ }],
111
+ };
112
+ } catch (e) {
113
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
114
+ }
115
+ }
116
+ );
117
+
118
+ // ── Tool: review_pr ─────────────────────────────────────────────────
119
+
120
+ server.tool(
121
+ "review_pr",
122
+ "Review a GitHub pull request. Returns structured feedback with issues, verdict, and suggestions. Optionally posts review comments directly to GitHub.",
123
+ {
124
+ pr_url: z.string().describe("Full GitHub PR URL, e.g. https://github.com/owner/repo/pull/123"),
125
+ post_comments: z.boolean().optional().describe("Post review comments directly to GitHub (default: false)"),
126
+ },
127
+ async ({ pr_url, post_comments }) => {
128
+ if (!API_KEY) return noKeyError();
129
+ try {
130
+ const result = await request("POST", "/review", {
131
+ pr_url,
132
+ post_review: post_comments === true,
133
+ });
134
+ return {
135
+ content: [{
136
+ type: "text",
137
+ text: result.review || JSON.stringify(result, null, 2),
138
+ }],
139
+ };
140
+ } catch (e) {
141
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
142
+ }
143
+ }
144
+ );
145
+
146
+ // ── Tool: ask_codebase ──────────────────────────────────────────────
147
+
148
+ server.tool(
149
+ "ask_codebase",
150
+ "Ask a question about any GitHub repository's codebase. Auto-indexes the repo on first use. Returns an answer with file references.",
151
+ {
152
+ question: z.string().describe("Question about the codebase, e.g. 'How does authentication work?'"),
153
+ repo: z.string().describe("GitHub repo in owner/repo format, e.g. 'facebook/react'"),
154
+ },
155
+ async ({ question, repo }) => {
156
+ if (!API_KEY) return noKeyError();
157
+ try {
158
+ const result = await request("POST", "/ask", { question, repo });
159
+ return {
160
+ content: [{
161
+ type: "text",
162
+ text: result.answer || JSON.stringify(result, null, 2),
163
+ }],
164
+ };
165
+ } catch (e) {
166
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
167
+ }
168
+ }
169
+ );
170
+
171
+ // ── Tool: generate_tests ────────────────────────────────────────────
172
+
173
+ server.tool(
174
+ "generate_tests",
175
+ "Generate tests for a file or feature in a GitHub repository. Creates test files and opens a PR with them.",
176
+ {
177
+ repo: z.string().describe("GitHub repo in owner/repo format"),
178
+ file: z.string().optional().describe("Specific file to test, e.g. 'src/auth.ts'"),
179
+ feature: z.string().optional().describe("Feature to test, e.g. 'user authentication'"),
180
+ },
181
+ async ({ repo, file, feature }) => {
182
+ if (!API_KEY) return noKeyError();
183
+ const target = file || feature;
184
+ if (!target) {
185
+ return { content: [{ type: "text", text: "Error: Provide either 'file' or 'feature' to test." }] };
186
+ }
187
+ try {
188
+ const result = await request("POST", "/test", { file, feature, repo });
189
+ return {
190
+ content: [{
191
+ type: "text",
192
+ text: JSON.stringify({
193
+ response: result.response,
194
+ cost_usd: result.cost_usd,
195
+ duration_ms: result.duration_ms,
196
+ pr_url: result.pr_url || null,
197
+ }, null, 2),
198
+ }],
199
+ };
200
+ } catch (e) {
201
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
202
+ }
203
+ }
204
+ );
205
+
206
+ // ── Tool: security_scan ─────────────────────────────────────────────
207
+
208
+ server.tool(
209
+ "security_scan",
210
+ "Run a security and dependency scan on one or more GitHub repositories. Checks for vulnerabilities, secret exposure, and security anti-patterns.",
211
+ {
212
+ repos: z.array(z.string()).describe("Array of repos in owner/repo format, e.g. ['owner/repo1', 'owner/repo2']"),
213
+ type: z.enum(["all", "dependencies", "secrets", "code"]).optional().describe("Scan type (default: all)"),
214
+ },
215
+ async ({ repos, type }) => {
216
+ if (!API_KEY) return noKeyError();
217
+ try {
218
+ const result = await request("POST", "/scan", { repos, type: type || "all" });
219
+ return {
220
+ content: [{
221
+ type: "text",
222
+ text: JSON.stringify(result, null, 2),
223
+ }],
224
+ };
225
+ } catch (e) {
226
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
227
+ }
228
+ }
229
+ );
230
+
231
+ // ── Tool: list_sessions ─────────────────────────────────────────────
232
+
233
+ server.tool(
234
+ "list_sessions",
235
+ "List recent agent sessions with status, cost, duration, and PR URLs. Use to check on past or running tasks.",
236
+ {
237
+ limit: z.number().int().min(1).max(100).optional().describe("Max sessions to return (default: 20)"),
238
+ status: z.enum(["running", "completed", "error"]).optional().describe("Filter by session status"),
239
+ },
240
+ async ({ limit, status }) => {
241
+ if (!API_KEY) return noKeyError();
242
+ try {
243
+ let path = `/api/sessions?limit=${limit || 20}`;
244
+ if (status) path += `&status=${status}`;
245
+ const result = await request("GET", path);
246
+ return {
247
+ content: [{
248
+ type: "text",
249
+ text: JSON.stringify(result.sessions || result, null, 2),
250
+ }],
251
+ };
252
+ } catch (e) {
253
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
254
+ }
255
+ }
256
+ );
257
+
258
+ // ── Tool: list_playbooks ────────────────────────────────────────────
259
+
260
+ server.tool(
261
+ "list_playbooks",
262
+ "List available playbooks — reusable workflow templates for common engineering tasks like bug triage, security remediation, test coverage, docs sync, and more.",
263
+ {},
264
+ async () => {
265
+ if (!API_KEY) return noKeyError();
266
+ try {
267
+ const result = await request("GET", "/api/playbooks");
268
+ return {
269
+ content: [{
270
+ type: "text",
271
+ text: JSON.stringify(result, null, 2),
272
+ }],
273
+ };
274
+ } catch (e) {
275
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
276
+ }
277
+ }
278
+ );
279
+
280
+ // ── Tool: run_playbook ──────────────────────────────────────────────
281
+
282
+ server.tool(
283
+ "run_playbook",
284
+ "Run a playbook (reusable workflow template) against a repository. Use list_playbooks to see available options. Built-in playbooks include: bug-triage, security-remediation, dependency-upgrade, docs-sync, test-coverage, code-migration, pr-review-cycle.",
285
+ {
286
+ slug: z.string().describe("Playbook slug, e.g. 'bug-triage', 'security-remediation', 'test-coverage'"),
287
+ repo: z.string().describe("GitHub repo in owner/repo format"),
288
+ inputs: z.record(z.string()).optional().describe("Additional inputs for the playbook template variables"),
289
+ },
290
+ async ({ slug, repo, inputs }) => {
291
+ if (!API_KEY) return noKeyError();
292
+ try {
293
+ const result = await request("POST", `/api/playbooks/${encodeURIComponent(slug)}/run`, { repo, inputs });
294
+ return {
295
+ content: [{
296
+ type: "text",
297
+ text: JSON.stringify(result, null, 2),
298
+ }],
299
+ };
300
+ } catch (e) {
301
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
302
+ }
303
+ }
304
+ );
305
+
306
+ // ── Tool: get_usage ─────────────────────────────────────────────────
307
+
308
+ server.tool(
309
+ "get_usage",
310
+ "Get usage statistics: total sessions, cost, estimated time saved, breakdowns by source, repo, and user. Useful for tracking ROI.",
311
+ {
312
+ days: z.number().int().min(1).max(365).optional().describe("Number of days to look back (default: all time)"),
313
+ },
314
+ async ({ days }) => {
315
+ if (!API_KEY) return noKeyError();
316
+ try {
317
+ const path = days ? `/api/usage?days=${days}` : "/api/usage";
318
+ const result = await request("GET", path);
319
+ return {
320
+ content: [{
321
+ type: "text",
322
+ text: JSON.stringify(result, null, 2),
323
+ }],
324
+ };
325
+ } catch (e) {
326
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
327
+ }
328
+ }
329
+ );
330
+
331
+ // ── Start ───────────────────────────────────────────────────────────
332
+
333
+ async function main() {
334
+ const transport = new StdioServerTransport();
335
+ await server.connect(transport);
336
+ }
337
+
338
+ main().catch((e) => {
339
+ console.error("MCP server error:", e);
340
+ process.exit(1);
341
+ });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "mcp-server-cloud-agent",
3
+ "version": "1.0.0",
4
+ "mcpName": "io.github.joepangallo/cloud-agent",
5
+ "description": "MCP server for Cloud Agent — an AI software engineer that writes code, opens PRs, reviews code, generates tests, runs security scans, and answers codebase questions. Connect from any MCP client (Claude Code, Cursor, Windsurf) and delegate engineering tasks.",
6
+ "bin": {
7
+ "mcp-server-cloud-agent": "index.js"
8
+ },
9
+ "keywords": [
10
+ "mcp",
11
+ "mcp-server",
12
+ "model-context-protocol",
13
+ "ai-agent",
14
+ "ai-engineer",
15
+ "cloud-agent",
16
+ "code-generation",
17
+ "code-review",
18
+ "pull-request",
19
+ "github",
20
+ "test-generation",
21
+ "security-scan",
22
+ "codebase-qa",
23
+ "playbooks",
24
+ "devin-alternative",
25
+ "autonomous-agent",
26
+ "ai-tools",
27
+ "claude",
28
+ "claude-code",
29
+ "llm",
30
+ "llm-tools",
31
+ "software-engineer",
32
+ "ci-cd",
33
+ "slack-integration",
34
+ "dev-tools"
35
+ ],
36
+ "license": "MIT",
37
+ "author": "MetalTorque",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/joepangallo/mcp-server-cloud-agent"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.27.0",
44
+ "zod": "^3.23.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=18"
48
+ },
49
+ "files": [
50
+ "index.js",
51
+ "README.md",
52
+ "server.json",
53
+ "package.json"
54
+ ],
55
+ "devDependencies": {
56
+ "vitest": "^4.0.18"
57
+ }
58
+ }
package/server.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.joepangallo/cloud-agent",
4
+ "description": "AI software engineer — writes code, opens PRs, reviews code, generates tests, runs security scans, and answers codebase questions.",
5
+ "repository": {
6
+ "url": "https://github.com/joepangallo/mcp-server-cloud-agent",
7
+ "source": "github"
8
+ },
9
+ "version": "1.0.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "mcp-server-cloud-agent",
14
+ "version": "1.0.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ },
18
+ "environmentVariables": [
19
+ {
20
+ "description": "API key for authenticating with Cloud Agent (ca_* prefix). Get one from your Cloud Agent web workspace.",
21
+ "isRequired": true,
22
+ "format": "string",
23
+ "isSecret": true,
24
+ "name": "CLOUD_AGENT_API_KEY"
25
+ },
26
+ {
27
+ "description": "Cloud Agent backend URL. Defaults to https://cloudagent.metaltorque.dev",
28
+ "isRequired": false,
29
+ "format": "string",
30
+ "isSecret": false,
31
+ "name": "CLOUD_AGENT_URL"
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ }