curatedmcp 2.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.
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/audit/catalog.d.ts +5 -0
- package/dist/audit/catalog.d.ts.map +1 -0
- package/dist/audit/catalog.js +69 -0
- package/dist/audit/index.d.ts +10 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +32 -0
- package/dist/audit/report.d.ts +6 -0
- package/dist/audit/report.d.ts.map +1 -0
- package/dist/audit/report.js +79 -0
- package/dist/audit/risk.d.ts +3 -0
- package/dist/audit/risk.d.ts.map +1 -0
- package/dist/audit/risk.js +68 -0
- package/dist/audit/scanner.d.ts +8 -0
- package/dist/audit/scanner.d.ts.map +1 -0
- package/dist/audit/scanner.js +69 -0
- package/dist/audit/types.d.ts +29 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +2 -0
- package/dist/auth.d.ts +23 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +52 -0
- package/dist/cli/add.d.ts +18 -0
- package/dist/cli/add.d.ts.map +1 -0
- package/dist/cli/add.js +114 -0
- package/dist/cli/audit.d.ts +2 -0
- package/dist/cli/audit.d.ts.map +1 -0
- package/dist/cli/audit.js +58 -0
- package/dist/cli/guard.d.ts +2 -0
- package/dist/cli/guard.d.ts.map +1 -0
- package/dist/cli/guard.js +58 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +44 -0
- package/dist/cli/list.d.ts +5 -0
- package/dist/cli/list.d.ts.map +1 -0
- package/dist/cli/list.js +33 -0
- package/dist/cli/login.d.ts +6 -0
- package/dist/cli/login.d.ts.map +1 -0
- package/dist/cli/login.js +43 -0
- package/dist/cli/remove.d.ts +6 -0
- package/dist/cli/remove.d.ts.map +1 -0
- package/dist/cli/remove.js +15 -0
- package/dist/cli/sync.d.ts +2 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +104 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +132 -0
- package/dist/guard/broker.d.ts +62 -0
- package/dist/guard/broker.d.ts.map +1 -0
- package/dist/guard/broker.js +147 -0
- package/dist/guard/dashboard.d.ts +14 -0
- package/dist/guard/dashboard.d.ts.map +1 -0
- package/dist/guard/dashboard.js +428 -0
- package/dist/guard/default-policy.json +33 -0
- package/dist/guard/index.d.ts +20 -0
- package/dist/guard/index.d.ts.map +1 -0
- package/dist/guard/index.js +61 -0
- package/dist/guard/logger.d.ts +30 -0
- package/dist/guard/logger.d.ts.map +1 -0
- package/dist/guard/logger.js +118 -0
- package/dist/guard/policy.d.ts +19 -0
- package/dist/guard/policy.d.ts.map +1 -0
- package/dist/guard/policy.js +108 -0
- package/dist/guard/proxy.d.ts +29 -0
- package/dist/guard/proxy.d.ts.map +1 -0
- package/dist/guard/proxy.js +109 -0
- package/dist/guard/types.d.ts +70 -0
- package/dist/guard/types.d.ts.map +1 -0
- package/dist/guard/types.js +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +259 -0
- package/dist/proxy.d.ts +122 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +165 -0
- package/dist/stack.d.ts +45 -0
- package/dist/stack.d.ts.map +1 -0
- package/dist/stack.js +93 -0
- package/dist/telemetry.d.ts +15 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +71 -0
- package/dist/tools/get-details.d.ts +14 -0
- package/dist/tools/get-details.d.ts.map +1 -0
- package/dist/tools/get-details.js +27 -0
- package/dist/tools/install.d.ts +2 -0
- package/dist/tools/install.d.ts.map +1 -0
- package/dist/tools/install.js +74 -0
- package/dist/tools/list-categories.d.ts +2 -0
- package/dist/tools/list-categories.d.ts.map +1 -0
- package/dist/tools/list-categories.js +13 -0
- package/dist/tools/search.d.ts +16 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +17 -0
- package/package.json +78 -0
package/dist/auth.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
export const API_URL = process.env.CURATOR_API_URL || "https://www.curatedmcp.com";
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".curatedmcp");
|
|
6
|
+
const AUTH_FILE = join(CONFIG_DIR, "auth.json");
|
|
7
|
+
export function loadAuth() {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function getToken() {
|
|
16
|
+
return process.env.CURATEDMCP_TOKEN || loadAuth()?.token || null;
|
|
17
|
+
}
|
|
18
|
+
export function saveAuth(auth) {
|
|
19
|
+
if (!existsSync(CONFIG_DIR))
|
|
20
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), "utf-8");
|
|
22
|
+
try {
|
|
23
|
+
chmodSync(AUTH_FILE, 0o600);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// chmod may not be supported (e.g. Windows) — non-fatal.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function clearAuth() {
|
|
30
|
+
try {
|
|
31
|
+
writeFileSync(AUTH_FILE, JSON.stringify({ token: "" }), "utf-8");
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// ignore
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Validate a token against the control plane. Returns identity + teams or null. */
|
|
38
|
+
export async function whoami(token) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`${API_URL}/api/auth/cli/whoami`, {
|
|
41
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
42
|
+
signal: AbortSignal.timeout(8000),
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
return null;
|
|
46
|
+
return (await res.json());
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type StackEntry } from "../stack.js";
|
|
2
|
+
export interface AddOptions {
|
|
3
|
+
/** Pre-supplied env vars (used in non-interactive mode). */
|
|
4
|
+
env?: Record<string, string>;
|
|
5
|
+
/** When true, skip prompts. Required env vars must already be in `env` or we error. */
|
|
6
|
+
nonInteractive: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface AddResult {
|
|
9
|
+
entry: StackEntry;
|
|
10
|
+
summary: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function addToStack(slug: string, opts: AddOptions): Promise<AddResult>;
|
|
13
|
+
/**
|
|
14
|
+
* Parse `--env KEY=value` flags from CLI argv tail.
|
|
15
|
+
* Also accepts `--env=KEY=value`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseEnvFlags(args: string[]): Record<string, string>;
|
|
18
|
+
//# sourceMappingURL=add.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/cli/add.ts"],"names":[],"mappings":"AASA,OAAO,EAAe,KAAK,UAAU,EAAa,MAAM,aAAa,CAAC;AAoBtE,MAAM,WAAW,UAAU;IACzB,4DAA4D;IAC5D,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,uFAAuF;IACvF,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,SAAS,CAAC,CA+CpB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAsBpE"}
|
package/dist/cli/add.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `launcher add <slug>` — fetch install config from curatedmcp.com and write to stack.json.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. Interactive (CLI): prompts for required env vars at the terminal
|
|
6
|
+
* 2. Non-interactive (MCP `add_to_stack` tool): env supplied as an argument
|
|
7
|
+
*/
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
10
|
+
import { upsertEntry, stackPath } from "../stack.js";
|
|
11
|
+
const API_URL = process.env.CURATOR_API_URL || "https://www.curatedmcp.com";
|
|
12
|
+
export async function addToStack(slug, opts) {
|
|
13
|
+
const config = await fetchStackConfig(slug);
|
|
14
|
+
// Resolve required env: pre-supplied → prompt → error
|
|
15
|
+
const env = { ...(opts.env || {}) };
|
|
16
|
+
const missing = [];
|
|
17
|
+
for (const v of config.requiredEnv) {
|
|
18
|
+
if (env[v.key] === undefined || env[v.key] === "")
|
|
19
|
+
missing.push(v);
|
|
20
|
+
}
|
|
21
|
+
if (missing.length > 0) {
|
|
22
|
+
if (opts.nonInteractive) {
|
|
23
|
+
throw new Error(`Missing required env vars for "${slug}": ${missing
|
|
24
|
+
.map((v) => v.key)
|
|
25
|
+
.join(", ")}. Re-run with these in the env argument.`);
|
|
26
|
+
}
|
|
27
|
+
const filled = await promptForEnv(missing);
|
|
28
|
+
Object.assign(env, filled);
|
|
29
|
+
}
|
|
30
|
+
// Optional env: prefill defaults but don't prompt
|
|
31
|
+
for (const v of config.optionalEnv) {
|
|
32
|
+
if (env[v.key] === undefined && v.default !== undefined) {
|
|
33
|
+
env[v.key] = v.default;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const entry = {
|
|
37
|
+
slug: config.slug,
|
|
38
|
+
name: config.name,
|
|
39
|
+
command: config.command,
|
|
40
|
+
args: config.args,
|
|
41
|
+
env: Object.keys(env).length > 0 ? env : undefined,
|
|
42
|
+
};
|
|
43
|
+
upsertEntry(entry);
|
|
44
|
+
const summary = `Added \`${config.name}\` (${config.slug}) to stack at ${stackPath()}.\n` +
|
|
45
|
+
`Command: ${config.command} ${config.args.join(" ")}` +
|
|
46
|
+
(Object.keys(env).length > 0
|
|
47
|
+
? `\nEnv: ${Object.keys(env).join(", ")}`
|
|
48
|
+
: "");
|
|
49
|
+
return { entry, summary };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parse `--env KEY=value` flags from CLI argv tail.
|
|
53
|
+
* Also accepts `--env=KEY=value`.
|
|
54
|
+
*/
|
|
55
|
+
export function parseEnvFlags(args) {
|
|
56
|
+
const env = {};
|
|
57
|
+
for (let i = 0; i < args.length; i++) {
|
|
58
|
+
const a = args[i];
|
|
59
|
+
let value;
|
|
60
|
+
if (a === "--env" || a === "-e") {
|
|
61
|
+
value = args[++i];
|
|
62
|
+
}
|
|
63
|
+
else if (a.startsWith("--env=")) {
|
|
64
|
+
value = a.slice("--env=".length);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!value)
|
|
70
|
+
continue;
|
|
71
|
+
const eq = value.indexOf("=");
|
|
72
|
+
if (eq <= 0) {
|
|
73
|
+
throw new Error(`Bad --env flag "${value}". Expected KEY=value (e.g. --env GITHUB_TOKEN=ghp_xxx).`);
|
|
74
|
+
}
|
|
75
|
+
env[value.slice(0, eq)] = value.slice(eq + 1);
|
|
76
|
+
}
|
|
77
|
+
return env;
|
|
78
|
+
}
|
|
79
|
+
async function fetchStackConfig(slug) {
|
|
80
|
+
const url = `${API_URL}/api/launcher/stack-config?slug=${encodeURIComponent(slug)}`;
|
|
81
|
+
const res = await fetch(url, {
|
|
82
|
+
headers: { "User-Agent": "@curatedmcp/launcher/1.0.0" },
|
|
83
|
+
});
|
|
84
|
+
if (res.status === 404) {
|
|
85
|
+
throw new Error(`Server "${slug}" not found in the CuratedMCP catalog. ` +
|
|
86
|
+
`Browse https://curatedmcp.com/marketplace to find the right slug.`);
|
|
87
|
+
}
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
throw new Error(`Failed to fetch install config for "${slug}": ${res.status} ${res.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
return (await res.json());
|
|
92
|
+
}
|
|
93
|
+
async function promptForEnv(vars) {
|
|
94
|
+
const rl = createInterface({ input, output });
|
|
95
|
+
try {
|
|
96
|
+
const out = {};
|
|
97
|
+
console.log(`\nThis server needs ${vars.length} environment variable${vars.length === 1 ? "" : "s"}:`);
|
|
98
|
+
for (const v of vars) {
|
|
99
|
+
if (v.description)
|
|
100
|
+
console.log(` ${v.key} — ${v.description}`);
|
|
101
|
+
const answer = await rl.question(` ${v.key}: `);
|
|
102
|
+
if (!answer.trim()) {
|
|
103
|
+
throw new Error(`${v.key} is required.`);
|
|
104
|
+
}
|
|
105
|
+
out[v.key] = answer.trim();
|
|
106
|
+
}
|
|
107
|
+
console.log("");
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
rl.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=add.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/cli/audit.ts"],"names":[],"mappings":"AA2CA,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBrE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { runAudit } from "../audit/index.js";
|
|
2
|
+
import { printReport, printJson } from "../audit/report.js";
|
|
3
|
+
import { API_URL, getToken } from "../auth.js";
|
|
4
|
+
async function forwardScan(report, token) {
|
|
5
|
+
try {
|
|
6
|
+
const res = await fetch(`${API_URL}/api/auth/cli/scan`, {
|
|
7
|
+
method: "POST",
|
|
8
|
+
headers: {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
Authorization: `Bearer ${token}`,
|
|
11
|
+
"User-Agent": "@curatedmcp/cli",
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
scannedAt: report.scannedAt,
|
|
15
|
+
totalServers: report.totalServers,
|
|
16
|
+
configFiles: report.configFiles,
|
|
17
|
+
high: report.high.map((s) => ({
|
|
18
|
+
name: s.name,
|
|
19
|
+
flags: s.flags,
|
|
20
|
+
command: s.command ?? null,
|
|
21
|
+
sourceFile: s.sourceFile,
|
|
22
|
+
})),
|
|
23
|
+
medium: report.medium.map((s) => ({
|
|
24
|
+
name: s.name,
|
|
25
|
+
flags: s.flags,
|
|
26
|
+
command: s.command ?? null,
|
|
27
|
+
sourceFile: s.sourceFile,
|
|
28
|
+
})),
|
|
29
|
+
unverified: report.unverified.map((s) => ({
|
|
30
|
+
name: s.name,
|
|
31
|
+
sourceFile: s.sourceFile,
|
|
32
|
+
})),
|
|
33
|
+
verified: report.verified.map((s) => ({ name: s.name })),
|
|
34
|
+
}),
|
|
35
|
+
signal: AbortSignal.timeout(8000),
|
|
36
|
+
});
|
|
37
|
+
return res.ok;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function runAuditCommand(args) {
|
|
44
|
+
const jsonMode = args.includes("--json");
|
|
45
|
+
const offline = args.includes("--offline");
|
|
46
|
+
const report = await runAudit({ offline });
|
|
47
|
+
if (jsonMode) {
|
|
48
|
+
printJson(report);
|
|
49
|
+
return report.high.length > 0 ? 1 : 0;
|
|
50
|
+
}
|
|
51
|
+
const token = !offline ? getToken() : null;
|
|
52
|
+
let synced = false;
|
|
53
|
+
if (token)
|
|
54
|
+
synced = await forwardScan(report, token);
|
|
55
|
+
await printReport(report, { synced });
|
|
56
|
+
return report.high.length > 0 ? 1 : 0;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/cli/guard.ts"],"names":[],"mappings":"AAkDA,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAYrE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { runGuard } from "../guard/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse `guard [options] -- <downstream command...>`.
|
|
4
|
+
* Everything after `--` (or the first non-flag token) is the downstream MCP server command.
|
|
5
|
+
*/
|
|
6
|
+
function parse(args) {
|
|
7
|
+
const opts = { command: [] };
|
|
8
|
+
const sep = args.indexOf("--");
|
|
9
|
+
const flagArgs = sep === -1 ? args : args.slice(0, sep);
|
|
10
|
+
let command = sep === -1 ? [] : args.slice(sep + 1);
|
|
11
|
+
for (let i = 0; i < flagArgs.length; i++) {
|
|
12
|
+
const a = flagArgs[i];
|
|
13
|
+
const next = () => flagArgs[++i];
|
|
14
|
+
switch (a) {
|
|
15
|
+
case "--dashboard":
|
|
16
|
+
opts.dashboard = true;
|
|
17
|
+
break;
|
|
18
|
+
case "--port":
|
|
19
|
+
opts.port = parseInt(next(), 10);
|
|
20
|
+
break;
|
|
21
|
+
case "--policy":
|
|
22
|
+
opts.policyPath = next();
|
|
23
|
+
break;
|
|
24
|
+
case "--db":
|
|
25
|
+
opts.dbPath = next();
|
|
26
|
+
break;
|
|
27
|
+
case "--registry-key":
|
|
28
|
+
opts.registryKey = next();
|
|
29
|
+
break;
|
|
30
|
+
case "--registry-slug":
|
|
31
|
+
opts.registrySlug = next();
|
|
32
|
+
break;
|
|
33
|
+
case "--registry-url":
|
|
34
|
+
opts.registryUrl = next();
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
// No `--` separator used: treat the first bare token (and rest) as the command.
|
|
38
|
+
if (sep === -1 && !a.startsWith("-")) {
|
|
39
|
+
command = flagArgs.slice(i);
|
|
40
|
+
i = flagArgs.length;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
opts.command = command;
|
|
45
|
+
return opts;
|
|
46
|
+
}
|
|
47
|
+
export async function runGuardCommand(args) {
|
|
48
|
+
const opts = parse(args);
|
|
49
|
+
if (opts.command.length === 0) {
|
|
50
|
+
console.error("Usage: curatedmcp guard [--dashboard] [--port N] -- <mcp server command>\n" +
|
|
51
|
+
"Example: curatedmcp guard -- npx -y @modelcontextprotocol/server-filesystem /tmp");
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
await runGuard(opts);
|
|
55
|
+
// Proxy + dashboard keep the process alive; resolve only on shutdown.
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AA8CA,wBAAgB,OAAO,IAAI,MAAM,CAGhC"}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `launcher init` — print the one-line config snippet to add Launcher to each AI client.
|
|
3
|
+
*
|
|
4
|
+
* This is the lowest-friction onboarding: a developer who just installed Launcher
|
|
5
|
+
* runs `launcher init`, copy-pastes the snippet that matches their tool, and they're done.
|
|
6
|
+
*/
|
|
7
|
+
import { stackPath } from "../stack.js";
|
|
8
|
+
const CONFIG_SNIPPET = JSON.stringify({
|
|
9
|
+
mcpServers: {
|
|
10
|
+
curatedmcp: {
|
|
11
|
+
command: "npx",
|
|
12
|
+
args: ["-y", "@curatedmcp/launcher"],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}, null, 2);
|
|
16
|
+
const INSTRUCTIONS = `
|
|
17
|
+
@curatedmcp/launcher — The MCP Hub
|
|
18
|
+
Add this entry to your AI client's MCP config:
|
|
19
|
+
|
|
20
|
+
${CONFIG_SNIPPET}
|
|
21
|
+
|
|
22
|
+
Config locations:
|
|
23
|
+
Claude Desktop ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
24
|
+
%APPDATA%\\Claude\\claude_desktop_config.json (Windows)
|
|
25
|
+
Cursor ~/.cursor/mcp.json
|
|
26
|
+
Windsurf ~/.codeium/windsurf/mcp_config.json
|
|
27
|
+
Claude Code ~/.claude/mcp.json (or .claude/mcp.json in your project)
|
|
28
|
+
|
|
29
|
+
Then add servers to your stack:
|
|
30
|
+
launcher add github # interactive — prompts for GITHUB_TOKEN
|
|
31
|
+
launcher add postgres # adds the Postgres server
|
|
32
|
+
launcher list # see your stack
|
|
33
|
+
|
|
34
|
+
Servers in your stack live at:
|
|
35
|
+
${stackPath()}
|
|
36
|
+
|
|
37
|
+
Restart your AI client after adding/removing servers — their tools will appear
|
|
38
|
+
under the prefix "<slug>__" (e.g. "github__create_issue").
|
|
39
|
+
`.trim();
|
|
40
|
+
export function runInit() {
|
|
41
|
+
console.log(INSTRUCTIONS);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/cli/list.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAiClC"}
|
package/dist/cli/list.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readStack, stackPath } from "../stack.js";
|
|
2
|
+
/**
|
|
3
|
+
* `launcher list` — print the user's current stack in human-readable form.
|
|
4
|
+
*/
|
|
5
|
+
export function listStack() {
|
|
6
|
+
const stack = readStack();
|
|
7
|
+
if (stack.entries.length === 0) {
|
|
8
|
+
console.log("Your stack is empty.");
|
|
9
|
+
console.log(`Add a server with \`launcher add <slug>\`.`);
|
|
10
|
+
console.log(`Browse https://curatedmcp.com/marketplace to find one.`);
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
console.log(`Stack (${stackPath()}):`);
|
|
14
|
+
console.log("");
|
|
15
|
+
for (const e of stack.entries) {
|
|
16
|
+
const flag = e.disabled ? " [disabled]" : "";
|
|
17
|
+
console.log(` • ${e.name || e.slug} (${e.slug})${flag}`);
|
|
18
|
+
console.log(` ${e.command} ${e.args.join(" ")}`);
|
|
19
|
+
if (e.env && Object.keys(e.env).length > 0) {
|
|
20
|
+
console.log(` env: ${Object.keys(e.env).join(", ")}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log(`Total: ${stack.entries.length} server(s). Tools appear in your AI client as \`<slug>__<tool>\`.`);
|
|
25
|
+
// One-line upsell — only when the user has 2+ servers, so it's relevant, not spam.
|
|
26
|
+
if (stack.entries.length >= 2) {
|
|
27
|
+
console.log("");
|
|
28
|
+
console.log(`Tip: get continuous security audits for everything in your stack →`);
|
|
29
|
+
console.log(` https://curatedmcp.com/sentinel`);
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/cli/login.ts"],"names":[],"mappings":"AAaA;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqC9D"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
import { API_URL, saveAuth, whoami } from "../auth.js";
|
|
3
|
+
function prompt(question) {
|
|
4
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5
|
+
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
6
|
+
rl.close();
|
|
7
|
+
resolve(answer.trim());
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Authenticate the agent to a CuratedMCP account using a personal token.
|
|
12
|
+
* Token can be supplied as an arg (`login <token>`) or pasted interactively.
|
|
13
|
+
*/
|
|
14
|
+
export async function runLogin(args) {
|
|
15
|
+
const tokenUrl = `${API_URL}/dashboard/registry`;
|
|
16
|
+
let token = args.find((a) => !a.startsWith("-"));
|
|
17
|
+
if (!token) {
|
|
18
|
+
console.log(`\nGenerate an API key under your team registry (Keys tab):\n ${tokenUrl}\n`);
|
|
19
|
+
token = await prompt("Paste your token: ");
|
|
20
|
+
}
|
|
21
|
+
if (!token) {
|
|
22
|
+
console.error("No token provided.");
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
const id = await whoami(token);
|
|
26
|
+
if (!id) {
|
|
27
|
+
console.error("Token is invalid or expired. Generate a new one at " + tokenUrl);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
saveAuth({
|
|
31
|
+
token,
|
|
32
|
+
userId: id.userId,
|
|
33
|
+
email: id.email ?? undefined,
|
|
34
|
+
savedAt: new Date().toISOString(),
|
|
35
|
+
});
|
|
36
|
+
console.log(`\n✓ Signed in${id.email ? ` as ${id.email}` : ""}.`);
|
|
37
|
+
if (id.teams.length > 0) {
|
|
38
|
+
console.log(` Teams: ${id.teams.map((t) => t.slug).join(", ")}`);
|
|
39
|
+
console.log(` Run \`curatedmcp sync\` to pull your team's MCP policy.`);
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remove.d.ts","sourceRoot":"","sources":["../../src/cli/remove.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYpD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { removeEntry, stackPath } from "../stack.js";
|
|
2
|
+
/**
|
|
3
|
+
* `launcher remove <slug>` — drop a server from the stack.
|
|
4
|
+
* Returns 0 on success, 1 if the slug wasn't in the stack.
|
|
5
|
+
*/
|
|
6
|
+
export function removeFromStack(slug) {
|
|
7
|
+
const removed = removeEntry(slug);
|
|
8
|
+
if (!removed) {
|
|
9
|
+
console.error(`"${slug}" is not in your stack. Run \`launcher list\` to see what is.`);
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
console.log(`Removed \`${slug}\` from stack (${stackPath()}). Restart your AI client for the change to take effect.`);
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=remove.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/cli/sync.ts"],"names":[],"mappings":"AAiEA,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyD7D"}
|
package/dist/cli/sync.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { API_URL, getToken, whoami } from "../auth.js";
|
|
2
|
+
import { upsertEntry } from "../stack.js";
|
|
3
|
+
import { runAudit } from "../audit/index.js";
|
|
4
|
+
async function fetchTeamConfig(slug) {
|
|
5
|
+
try {
|
|
6
|
+
const res = await fetch(`${API_URL}/api/teams/${slug}/config`, {
|
|
7
|
+
headers: { "User-Agent": "@curatedmcp/cli" },
|
|
8
|
+
signal: AbortSignal.timeout(8000),
|
|
9
|
+
});
|
|
10
|
+
if (!res.ok)
|
|
11
|
+
return null;
|
|
12
|
+
return (await res.json());
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function pushAudit(token) {
|
|
19
|
+
try {
|
|
20
|
+
const report = await runAudit({});
|
|
21
|
+
const res = await fetch(`${API_URL}/api/auth/cli/scan`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
Authorization: `Bearer ${token}`,
|
|
26
|
+
"User-Agent": "@curatedmcp/cli",
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
scannedAt: report.scannedAt,
|
|
30
|
+
totalServers: report.totalServers,
|
|
31
|
+
configFiles: report.configFiles,
|
|
32
|
+
high: report.high.map((s) => ({
|
|
33
|
+
name: s.name,
|
|
34
|
+
flags: s.flags,
|
|
35
|
+
command: s.command ?? null,
|
|
36
|
+
sourceFile: s.sourceFile,
|
|
37
|
+
})),
|
|
38
|
+
medium: report.medium.map((s) => ({
|
|
39
|
+
name: s.name,
|
|
40
|
+
flags: s.flags,
|
|
41
|
+
command: s.command ?? null,
|
|
42
|
+
sourceFile: s.sourceFile,
|
|
43
|
+
})),
|
|
44
|
+
unverified: report.unverified.map((s) => ({
|
|
45
|
+
name: s.name,
|
|
46
|
+
sourceFile: s.sourceFile,
|
|
47
|
+
})),
|
|
48
|
+
verified: report.verified.map((s) => ({ name: s.name })),
|
|
49
|
+
}),
|
|
50
|
+
signal: AbortSignal.timeout(8000),
|
|
51
|
+
});
|
|
52
|
+
return res.ok;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function runSync(args) {
|
|
59
|
+
const token = getToken();
|
|
60
|
+
if (!token) {
|
|
61
|
+
console.error("Not signed in. Run `curatedmcp login` first.");
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
const id = await whoami(token);
|
|
65
|
+
if (!id) {
|
|
66
|
+
console.error("Session expired. Run `curatedmcp login` again.");
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const flagSlug = (() => {
|
|
70
|
+
const i = args.indexOf("--team");
|
|
71
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
72
|
+
})();
|
|
73
|
+
const slug = flagSlug ?? id.teams[0]?.slug;
|
|
74
|
+
if (!slug) {
|
|
75
|
+
console.error("No team found on your account. Create one at " + `${API_URL}/teams`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
const config = await fetchTeamConfig(slug);
|
|
79
|
+
if (!config) {
|
|
80
|
+
console.error(`Could not fetch config for team "${slug}".`);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
let added = 0;
|
|
84
|
+
for (const [name, def] of Object.entries(config.mcpServers)) {
|
|
85
|
+
if (!def.command)
|
|
86
|
+
continue; // stack requires a spawnable command (skip url/http servers)
|
|
87
|
+
upsertEntry({
|
|
88
|
+
slug: name,
|
|
89
|
+
command: def.command,
|
|
90
|
+
args: def.args ?? [],
|
|
91
|
+
env: def.env,
|
|
92
|
+
note: `team:${slug}`,
|
|
93
|
+
});
|
|
94
|
+
added++;
|
|
95
|
+
}
|
|
96
|
+
console.log(`\n✓ Synced ${added} server(s) from team "${config._meta?.team ?? slug}" into your stack.`);
|
|
97
|
+
const pushed = await pushAudit(token);
|
|
98
|
+
console.log(pushed
|
|
99
|
+
? "✓ Local security audit reported to the team."
|
|
100
|
+
: "ℹ️ Audit not reported (endpoint unavailable or no permission).");
|
|
101
|
+
console.log(" Restart your AI client to load the synced servers.\n");
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=sync.js.map
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True if argv contains a CLI subcommand or top-level flag.
|
|
3
|
+
* False means run as an MCP server (the default mode used by AI clients).
|
|
4
|
+
*
|
|
5
|
+
* NOTE: We deliberately exclude `--no-telemetry` here because Telemetry
|
|
6
|
+
* already handles that flag in MCP server mode.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isCliInvocation(argv: readonly string[]): boolean;
|
|
9
|
+
export declare function runCli(argv: readonly string[]): Promise<number>;
|
|
10
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAyDA;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoBhE;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2DrE"}
|