@sproobo/mcp 0.1.0 → 0.1.2
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/dist/index.js +0 -0
- package/package.json +12 -10
- package/dist/__tests__/client.test.d.ts +0 -2
- package/dist/__tests__/client.test.d.ts.map +0 -1
- package/dist/__tests__/client.test.js +0 -81
- package/dist/__tests__/redact.test.d.ts +0 -2
- package/dist/__tests__/redact.test.d.ts.map +0 -1
- package/dist/__tests__/redact.test.js +0 -74
- package/dist/__tests__/startup.test.d.ts +0 -2
- package/dist/__tests__/startup.test.d.ts.map +0 -1
- package/dist/__tests__/startup.test.js +0 -29
package/dist/index.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproobo/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Sproobo MCP server — deploy sites, manage servers, and configure infrastructure from AI agents",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
|
-
"files": ["dist"],
|
|
9
8
|
"types": "./dist/index.d.ts",
|
|
10
9
|
"bin": {
|
|
11
10
|
"sproobo-mcp": "./dist/index.js"
|
|
@@ -16,12 +15,9 @@
|
|
|
16
15
|
"default": "./dist/index.js"
|
|
17
16
|
}
|
|
18
17
|
},
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"test": "vitest run",
|
|
23
|
-
"test:watch": "vitest"
|
|
24
|
-
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
25
21
|
"dependencies": {
|
|
26
22
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
27
23
|
"zod": "^3.25.76"
|
|
@@ -38,5 +34,11 @@
|
|
|
38
34
|
"server-management",
|
|
39
35
|
"ai-agent"
|
|
40
36
|
],
|
|
41
|
-
"license": "MIT"
|
|
42
|
-
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/client.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { SprooboClient } from "../client.js";
|
|
3
|
-
describe("SprooboClient", () => {
|
|
4
|
-
let client;
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
client = new SprooboClient("spb_testapikey123", "https://test.sproobo.com");
|
|
7
|
-
vi.restoreAllMocks();
|
|
8
|
-
});
|
|
9
|
-
it("sends Authorization header with API key", async () => {
|
|
10
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
11
|
-
ok: true,
|
|
12
|
-
json: () => Promise.resolve({ servers: [] }),
|
|
13
|
-
});
|
|
14
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
15
|
-
await client.get("/api/servers");
|
|
16
|
-
expect(mockFetch).toHaveBeenCalledWith("https://test.sproobo.com/api/servers", expect.objectContaining({
|
|
17
|
-
headers: expect.objectContaining({
|
|
18
|
-
Authorization: "Bearer spb_testapikey123",
|
|
19
|
-
}),
|
|
20
|
-
}));
|
|
21
|
-
});
|
|
22
|
-
it("strips trailing slash from base URL", async () => {
|
|
23
|
-
const clientWithSlash = new SprooboClient("spb_key", "https://test.sproobo.com/");
|
|
24
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
25
|
-
ok: true,
|
|
26
|
-
json: () => Promise.resolve({}),
|
|
27
|
-
});
|
|
28
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
29
|
-
await clientWithSlash.get("/api/servers");
|
|
30
|
-
expect(mockFetch).toHaveBeenCalledWith("https://test.sproobo.com/api/servers", expect.anything());
|
|
31
|
-
});
|
|
32
|
-
it("throws on 401 with helpful message", async () => {
|
|
33
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
34
|
-
ok: false,
|
|
35
|
-
status: 401,
|
|
36
|
-
statusText: "Unauthorized",
|
|
37
|
-
json: () => Promise.resolve({ error: "Unauthorized" }),
|
|
38
|
-
}));
|
|
39
|
-
await expect(client.get("/api/servers")).rejects.toThrow("Invalid or revoked API key");
|
|
40
|
-
});
|
|
41
|
-
it("throws on 403 with permission message", async () => {
|
|
42
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
43
|
-
ok: false,
|
|
44
|
-
status: 403,
|
|
45
|
-
statusText: "Forbidden",
|
|
46
|
-
json: () => Promise.resolve({ error: "You don't have access to this server" }),
|
|
47
|
-
}));
|
|
48
|
-
await expect(client.get("/api/servers/abc")).rejects.toThrow("Insufficient permissions");
|
|
49
|
-
});
|
|
50
|
-
it("throws on 429 with retry-after", async () => {
|
|
51
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
52
|
-
ok: false,
|
|
53
|
-
status: 429,
|
|
54
|
-
statusText: "Too Many Requests",
|
|
55
|
-
headers: new Headers({ "Retry-After": "30" }),
|
|
56
|
-
json: () => Promise.resolve({ error: "Rate limited" }),
|
|
57
|
-
}));
|
|
58
|
-
await expect(client.get("/api/servers")).rejects.toThrow("Rate limit exceeded. Retry after 30 seconds");
|
|
59
|
-
});
|
|
60
|
-
it("sends POST body as JSON", async () => {
|
|
61
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
62
|
-
ok: true,
|
|
63
|
-
json: () => Promise.resolve({ success: true }),
|
|
64
|
-
});
|
|
65
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
66
|
-
await client.post("/api/sites/abc/deployments", { commitSha: "abc123" });
|
|
67
|
-
expect(mockFetch).toHaveBeenCalledWith("https://test.sproobo.com/api/sites/abc/deployments", expect.objectContaining({
|
|
68
|
-
method: "POST",
|
|
69
|
-
body: JSON.stringify({ commitSha: "abc123" }),
|
|
70
|
-
}));
|
|
71
|
-
});
|
|
72
|
-
it("handles 404 errors", async () => {
|
|
73
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
74
|
-
ok: false,
|
|
75
|
-
status: 404,
|
|
76
|
-
statusText: "Not Found",
|
|
77
|
-
json: () => Promise.resolve({ error: "Server not found" }),
|
|
78
|
-
}));
|
|
79
|
-
await expect(client.get("/api/servers/nonexistent")).rejects.toThrow("Not found: Server not found");
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redact.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/redact.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { redact, safeJson } from "../redact.js";
|
|
3
|
-
describe("redact", () => {
|
|
4
|
-
it("redacts known sensitive keys", () => {
|
|
5
|
-
const input = {
|
|
6
|
-
name: "my-site",
|
|
7
|
-
envVars: { DB_URL: "postgres://...", API_KEY: "secret" },
|
|
8
|
-
status: "healthy",
|
|
9
|
-
};
|
|
10
|
-
const result = redact(input);
|
|
11
|
-
expect(result.name).toBe("my-site");
|
|
12
|
-
expect(result.envVars).toBe("[REDACTED]");
|
|
13
|
-
expect(result.status).toBe("healthy");
|
|
14
|
-
});
|
|
15
|
-
it("redacts keys ending with _KEY, _SECRET, _TOKEN, _PASSWORD", () => {
|
|
16
|
-
const input = {
|
|
17
|
-
STRIPE_SECRET_KEY: "sk_test_xxx",
|
|
18
|
-
database_password: "hunter2",
|
|
19
|
-
auth_token: "tok_xxx",
|
|
20
|
-
slack_credentials: "xoxb-xxx",
|
|
21
|
-
};
|
|
22
|
-
const result = redact(input);
|
|
23
|
-
expect(result.STRIPE_SECRET_KEY).toBe("[REDACTED]");
|
|
24
|
-
expect(result.database_password).toBe("[REDACTED]");
|
|
25
|
-
expect(result.auth_token).toBe("[REDACTED]");
|
|
26
|
-
expect(result.slack_credentials).toBe("[REDACTED]");
|
|
27
|
-
});
|
|
28
|
-
it("redacts nested objects", () => {
|
|
29
|
-
const input = {
|
|
30
|
-
site: {
|
|
31
|
-
name: "app",
|
|
32
|
-
envVars: { SECRET: "xxx" },
|
|
33
|
-
config: {
|
|
34
|
-
password: "hunter2",
|
|
35
|
-
port: 3000,
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
const result = redact(input);
|
|
40
|
-
expect(result.site.name).toBe("app");
|
|
41
|
-
expect(result.site.envVars).toBe("[REDACTED]");
|
|
42
|
-
const config = result.site.config;
|
|
43
|
-
expect(config.password).toBe("[REDACTED]");
|
|
44
|
-
expect(config.port).toBe(3000);
|
|
45
|
-
});
|
|
46
|
-
it("redacts items in arrays", () => {
|
|
47
|
-
const input = [
|
|
48
|
-
{ name: "site-a", envVars: { KEY: "val" } },
|
|
49
|
-
{ name: "site-b", token: "xxx" },
|
|
50
|
-
];
|
|
51
|
-
const result = redact(input);
|
|
52
|
-
expect(result[0].name).toBe("site-a");
|
|
53
|
-
expect(result[0].envVars).toBe("[REDACTED]");
|
|
54
|
-
expect(result[1].token).toBe("[REDACTED]");
|
|
55
|
-
});
|
|
56
|
-
it("leaves non-sensitive data untouched", () => {
|
|
57
|
-
const input = { id: "abc", name: "test", status: "healthy", port: 3000 };
|
|
58
|
-
expect(redact(input)).toEqual(input);
|
|
59
|
-
});
|
|
60
|
-
it("handles null and primitives", () => {
|
|
61
|
-
expect(redact(null)).toBeNull();
|
|
62
|
-
expect(redact(undefined)).toBeUndefined();
|
|
63
|
-
expect(redact("hello")).toBe("hello");
|
|
64
|
-
expect(redact(42)).toBe(42);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe("safeJson", () => {
|
|
68
|
-
it("returns redacted JSON string", () => {
|
|
69
|
-
const input = { name: "site", envVars: { SECRET: "xxx" } };
|
|
70
|
-
const result = JSON.parse(safeJson(input));
|
|
71
|
-
expect(result.name).toBe("site");
|
|
72
|
-
expect(result.envVars).toBe("[REDACTED]");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"startup.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/startup.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { execFile } from "child_process";
|
|
3
|
-
import { resolve } from "path";
|
|
4
|
-
describe("MCP server startup", () => {
|
|
5
|
-
const originalEnv = process.env;
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
process.env = { ...originalEnv };
|
|
8
|
-
});
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
process.env = originalEnv;
|
|
11
|
-
vi.restoreAllMocks();
|
|
12
|
-
});
|
|
13
|
-
it("exits with error when SPROOBO_API_KEY is missing", async () => {
|
|
14
|
-
const entryPoint = resolve(__dirname, "../../dist/index.js");
|
|
15
|
-
const result = await new Promise((resolve) => {
|
|
16
|
-
const child = execFile("node", [entryPoint], {
|
|
17
|
-
env: { ...process.env, SPROOBO_API_KEY: "" },
|
|
18
|
-
timeout: 5000,
|
|
19
|
-
}, (error, _stdout, stderr) => {
|
|
20
|
-
resolve({
|
|
21
|
-
code: error?.code !== undefined ? Number(error.code) : child.exitCode,
|
|
22
|
-
stderr,
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
expect(result.code).toBe(1);
|
|
27
|
-
expect(result.stderr).toContain("SPROOBO_API_KEY");
|
|
28
|
-
});
|
|
29
|
-
});
|