@moor-sh/mcp 0.20.0 → 0.22.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/package.json +1 -1
- package/src/index.ts +314 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1174,6 +1174,15 @@ server.registerTool(
|
|
|
1174
1174
|
.describe(
|
|
1175
1175
|
"Named Docker volumes to attach. Each entry creates a per-project volume (stored as moor-<project>-<name>) and mounts it at the given target on next container recreate. Data survives container/project rebuilds unless explicitly purged via project delete with purge_volumes=true.",
|
|
1176
1176
|
),
|
|
1177
|
+
source_credential_id: z
|
|
1178
|
+
.number()
|
|
1179
|
+
.int()
|
|
1180
|
+
.positive()
|
|
1181
|
+
.nullable()
|
|
1182
|
+
.optional()
|
|
1183
|
+
.describe(
|
|
1184
|
+
"For github_url projects: pin the source credential row (from moor_source_credential_add) the build path should use. Build synthesizes the credentialed clone URL in memory; the secret is never stored on the project. Ignored when docker_image is set; save-time validation is structural only (id exists).",
|
|
1185
|
+
),
|
|
1177
1186
|
}),
|
|
1178
1187
|
},
|
|
1179
1188
|
async (input) => {
|
|
@@ -1254,6 +1263,15 @@ server.registerTool(
|
|
|
1254
1263
|
.describe(
|
|
1255
1264
|
"Max CPU cores (fractional OK; min 0.001). Pass null to clear. Max host core count. Takes effect on container recreate.",
|
|
1256
1265
|
),
|
|
1266
|
+
source_credential_id: z
|
|
1267
|
+
.number()
|
|
1268
|
+
.int()
|
|
1269
|
+
.positive()
|
|
1270
|
+
.nullable()
|
|
1271
|
+
.optional()
|
|
1272
|
+
.describe(
|
|
1273
|
+
"Pin (or unlink, by passing null) the source credential the build path should use for this github_url project. Switching to docker_image force-clears the id regardless of input. Save-time validation is structural only; host-mismatch / not-active is enforced at build time.",
|
|
1274
|
+
),
|
|
1257
1275
|
}),
|
|
1258
1276
|
},
|
|
1259
1277
|
async (input) => {
|
|
@@ -1879,6 +1897,15 @@ server.registerTool(
|
|
|
1879
1897
|
.describe(
|
|
1880
1898
|
"Env vars to MERGE into existing project envs. Omit to leave envs untouched. Pass {} for an explicit no-op. Use moor_env_delete to remove keys.",
|
|
1881
1899
|
),
|
|
1900
|
+
source_credential_id: z
|
|
1901
|
+
.number()
|
|
1902
|
+
.int()
|
|
1903
|
+
.positive()
|
|
1904
|
+
.nullable()
|
|
1905
|
+
.optional()
|
|
1906
|
+
.describe(
|
|
1907
|
+
"For github_url projects: pin the source credential row (created via moor_source_credential_add). Build path synthesizes the credentialed clone URL in memory; secret never gets stored on the project row. Pass null to detach without switching source type. Ignored when docker_image is set. Save-time validation is structural only (id exists); host-mismatch / not-active is enforced at build time so configuration can survive transient credential outages.",
|
|
1908
|
+
),
|
|
1882
1909
|
run: z
|
|
1883
1910
|
.boolean()
|
|
1884
1911
|
.optional()
|
|
@@ -1978,6 +2005,7 @@ server.registerTool(
|
|
|
1978
2005
|
restart_policy: input.restart_policy,
|
|
1979
2006
|
memory_limit_mb: input.memory_limit_mb,
|
|
1980
2007
|
cpus: input.cpus,
|
|
2008
|
+
source_credential_id: input.source_credential_id,
|
|
1981
2009
|
};
|
|
1982
2010
|
const res = await apiPost("/api/projects", createBody);
|
|
1983
2011
|
if (!res.ok) throw new Error(`[create] ${await res.text()}`);
|
|
@@ -1997,6 +2025,8 @@ server.registerTool(
|
|
|
1997
2025
|
if (input.restart_policy !== undefined) updateBody.restart_policy = input.restart_policy;
|
|
1998
2026
|
if (input.memory_limit_mb !== undefined) updateBody.memory_limit_mb = input.memory_limit_mb;
|
|
1999
2027
|
if (input.cpus !== undefined) updateBody.cpus = input.cpus;
|
|
2028
|
+
if (input.source_credential_id !== undefined)
|
|
2029
|
+
updateBody.source_credential_id = input.source_credential_id;
|
|
2000
2030
|
|
|
2001
2031
|
if (Object.keys(updateBody).length > 0) {
|
|
2002
2032
|
const res = await apiPut(`/api/projects/${existing.id}`, updateBody);
|
|
@@ -2442,6 +2472,290 @@ server.registerTool(
|
|
|
2442
2472
|
},
|
|
2443
2473
|
);
|
|
2444
2474
|
|
|
2475
|
+
// --- Source credentials (HTTPS PATs for private Git repos) ---
|
|
2476
|
+
//
|
|
2477
|
+
// v1 ships HTTPS PATs only. Read paths return metadata + secret.kind;
|
|
2478
|
+
// the raw token only crosses MCP on `add` and `update`. Delete uses
|
|
2479
|
+
// confirm_label since (hostname, label) is the disambiguation key
|
|
2480
|
+
// (multiple github.com rows coexist by design). The check tool runs a
|
|
2481
|
+
// real `git ls-remote` inside moor to validate access before any
|
|
2482
|
+
// deploy commits state.
|
|
2483
|
+
|
|
2484
|
+
type SourceCredentialMetadata = {
|
|
2485
|
+
id: number;
|
|
2486
|
+
hostname: string;
|
|
2487
|
+
label: string;
|
|
2488
|
+
username: string;
|
|
2489
|
+
secret: {
|
|
2490
|
+
configured: true;
|
|
2491
|
+
kind: "github_classic_pat" | "github_fine_grained_pat" | "unknown";
|
|
2492
|
+
};
|
|
2493
|
+
state: "active" | "failed";
|
|
2494
|
+
expires_at: string | null;
|
|
2495
|
+
last_checked_at: string | null;
|
|
2496
|
+
last_check_status: string | null;
|
|
2497
|
+
created_at: string;
|
|
2498
|
+
updated_at: string;
|
|
2499
|
+
};
|
|
2500
|
+
|
|
2501
|
+
function renderSourceCredentialLine(c: SourceCredentialMetadata): string {
|
|
2502
|
+
const checked = c.last_check_status ? ` last_check=${c.last_check_status}` : "";
|
|
2503
|
+
return `id=${c.id} ${c.hostname} label=${c.label} user=${c.username} kind=${c.secret.kind} state=${c.state}${checked}`;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
server.registerTool(
|
|
2507
|
+
"moor_source_credentials_list",
|
|
2508
|
+
{
|
|
2509
|
+
title: "List Source Credentials",
|
|
2510
|
+
description:
|
|
2511
|
+
"List all stored Git source credentials (HTTPS PATs). Returns metadata only - the raw secret value is never returned by any read path. Multiple credentials can share a hostname (e.g. two github.com rows for different orgs); use label to disambiguate.",
|
|
2512
|
+
},
|
|
2513
|
+
async () => {
|
|
2514
|
+
const res = await apiGet("/api/server/source-credentials");
|
|
2515
|
+
if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
|
|
2516
|
+
const data = (await res.json()) as { rows: SourceCredentialMetadata[] };
|
|
2517
|
+
const text =
|
|
2518
|
+
data.rows.length === 0
|
|
2519
|
+
? "No source credentials configured. Public repos work anonymously."
|
|
2520
|
+
: data.rows.map(renderSourceCredentialLine).join("\n");
|
|
2521
|
+
return {
|
|
2522
|
+
content: [{ type: "text", text }],
|
|
2523
|
+
structuredContent: { rows: data.rows },
|
|
2524
|
+
};
|
|
2525
|
+
},
|
|
2526
|
+
);
|
|
2527
|
+
|
|
2528
|
+
server.registerTool(
|
|
2529
|
+
"moor_source_credential_get",
|
|
2530
|
+
{
|
|
2531
|
+
title: "Get Source Credential",
|
|
2532
|
+
description:
|
|
2533
|
+
"Get a single source credential by id. Returns metadata only. Use this before moor_source_credential_delete to confirm the label you intend to delete.",
|
|
2534
|
+
inputSchema: z.object({
|
|
2535
|
+
id: z.number().int().positive().describe("Credential id (from moor_source_credentials_list)"),
|
|
2536
|
+
}),
|
|
2537
|
+
},
|
|
2538
|
+
async ({ id }) => {
|
|
2539
|
+
const res = await apiGet(`/api/server/source-credentials/${id}`);
|
|
2540
|
+
if (res.status === 404) throw new Error(`source credential id=${id} not found`);
|
|
2541
|
+
if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
|
|
2542
|
+
const row = (await res.json()) as SourceCredentialMetadata;
|
|
2543
|
+
return {
|
|
2544
|
+
content: [{ type: "text", text: renderSourceCredentialLine(row) }],
|
|
2545
|
+
structuredContent: row as unknown as Record<string, unknown>,
|
|
2546
|
+
};
|
|
2547
|
+
},
|
|
2548
|
+
);
|
|
2549
|
+
|
|
2550
|
+
server.registerTool(
|
|
2551
|
+
"moor_source_credential_add",
|
|
2552
|
+
{
|
|
2553
|
+
title: "Add Source Credential",
|
|
2554
|
+
description:
|
|
2555
|
+
"Store a Git source credential (HTTPS PAT) for a private repo host. v1 supports HTTPS PATs only; SSH deploy keys may be added in a future version. Hostname must be the bare host as parsed from a Git URL (github.com, gitlab.com, etc.) - no scheme, no path. Multiple credentials can share a hostname; the (hostname, label) pair is unique. For GitHub: use a fine-grained PAT with `Contents: read` (username `x-access-token`), or a classic PAT with `repo` scope. Note: the secret value passes through the MCP client and tool-call transport on input, same security model as moor_env_set.",
|
|
2556
|
+
inputSchema: z.object({
|
|
2557
|
+
hostname: z
|
|
2558
|
+
.string()
|
|
2559
|
+
.describe("Bare Git host: github.com, gitlab.com, etc. No scheme, no path."),
|
|
2560
|
+
label: z
|
|
2561
|
+
.string()
|
|
2562
|
+
.describe(
|
|
2563
|
+
"Operator-supplied label for disambiguation when multiple credentials share a host (e.g. 'personal', 'work-org', 'acme-clients'). Trimmed at storage.",
|
|
2564
|
+
),
|
|
2565
|
+
username: z
|
|
2566
|
+
.string()
|
|
2567
|
+
.describe(
|
|
2568
|
+
"Git username. For GitHub fine-grained PATs, use 'x-access-token'. For classic PATs, your GitHub username works too.",
|
|
2569
|
+
),
|
|
2570
|
+
secret: z
|
|
2571
|
+
.string()
|
|
2572
|
+
.describe(
|
|
2573
|
+
"Git token (PAT). Visible to the MCP client on input; rotate via moor_source_credential_update if exposed.",
|
|
2574
|
+
),
|
|
2575
|
+
expires_at: z
|
|
2576
|
+
.string()
|
|
2577
|
+
.nullable()
|
|
2578
|
+
.optional()
|
|
2579
|
+
.describe(
|
|
2580
|
+
"Operator-supplied expiry timestamp (PAT expiry from GitHub). Optional; helps rotation reminders.",
|
|
2581
|
+
),
|
|
2582
|
+
}),
|
|
2583
|
+
},
|
|
2584
|
+
async ({ hostname, label, username, secret, expires_at }) => {
|
|
2585
|
+
const body: Record<string, unknown> = { hostname, label, username, secret };
|
|
2586
|
+
if (expires_at !== undefined) body.expires_at = expires_at;
|
|
2587
|
+
const res = await apiPost("/api/server/source-credentials", body);
|
|
2588
|
+
if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
|
|
2589
|
+
const row = (await res.json()) as SourceCredentialMetadata;
|
|
2590
|
+
return {
|
|
2591
|
+
content: [
|
|
2592
|
+
{
|
|
2593
|
+
type: "text",
|
|
2594
|
+
text: `Added source credential for ${row.hostname} label="${row.label}" (id=${row.id}, kind=${row.secret.kind}).`,
|
|
2595
|
+
},
|
|
2596
|
+
],
|
|
2597
|
+
structuredContent: row as unknown as Record<string, unknown>,
|
|
2598
|
+
};
|
|
2599
|
+
},
|
|
2600
|
+
);
|
|
2601
|
+
|
|
2602
|
+
server.registerTool(
|
|
2603
|
+
"moor_source_credential_update",
|
|
2604
|
+
{
|
|
2605
|
+
title: "Update Source Credential",
|
|
2606
|
+
description:
|
|
2607
|
+
"Rotate username, secret, label, or expires_at on an existing source credential. Hostname is intentionally not patchable - changing the lookup key on an existing row would silently break in-flight builds. To change hostname, delete and recreate. Requires at least one of username, secret, label, or expires_at.",
|
|
2608
|
+
inputSchema: z.object({
|
|
2609
|
+
id: z.number().int().positive().describe("Credential id to update"),
|
|
2610
|
+
username: z.string().optional().describe("New username (optional)"),
|
|
2611
|
+
secret: z
|
|
2612
|
+
.string()
|
|
2613
|
+
.optional()
|
|
2614
|
+
.describe("New secret (optional). Visible to the MCP client on input."),
|
|
2615
|
+
label: z.string().optional().describe("New label (optional). Trimmed at storage."),
|
|
2616
|
+
expires_at: z.string().nullable().optional().describe("New expiry; null to clear"),
|
|
2617
|
+
}),
|
|
2618
|
+
},
|
|
2619
|
+
async ({ id, username, secret, label, expires_at }) => {
|
|
2620
|
+
if (
|
|
2621
|
+
username === undefined &&
|
|
2622
|
+
secret === undefined &&
|
|
2623
|
+
label === undefined &&
|
|
2624
|
+
expires_at === undefined
|
|
2625
|
+
) {
|
|
2626
|
+
throw new Error("must provide at least one of username, secret, label, or expires_at");
|
|
2627
|
+
}
|
|
2628
|
+
const patch: Record<string, unknown> = {};
|
|
2629
|
+
if (username !== undefined) patch.username = username;
|
|
2630
|
+
if (secret !== undefined) patch.secret = secret;
|
|
2631
|
+
if (label !== undefined) patch.label = label;
|
|
2632
|
+
if (expires_at !== undefined) patch.expires_at = expires_at;
|
|
2633
|
+
const res = await apiPut(`/api/server/source-credentials/${id}`, patch);
|
|
2634
|
+
if (res.status === 404) throw new Error(`source credential id=${id} not found`);
|
|
2635
|
+
if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
|
|
2636
|
+
const row = (await res.json()) as SourceCredentialMetadata;
|
|
2637
|
+
const fields: string[] = [];
|
|
2638
|
+
if (username !== undefined) fields.push("username");
|
|
2639
|
+
if (secret !== undefined) fields.push("secret");
|
|
2640
|
+
if (label !== undefined) fields.push("label");
|
|
2641
|
+
if (expires_at !== undefined) fields.push("expires_at");
|
|
2642
|
+
return {
|
|
2643
|
+
content: [
|
|
2644
|
+
{
|
|
2645
|
+
type: "text",
|
|
2646
|
+
text: `Updated source credential id=${id} (${row.hostname}, ${row.label}): rotated ${fields.join(" + ")}. kind=${row.secret.kind}.`,
|
|
2647
|
+
},
|
|
2648
|
+
],
|
|
2649
|
+
structuredContent: row as unknown as Record<string, unknown>,
|
|
2650
|
+
};
|
|
2651
|
+
},
|
|
2652
|
+
);
|
|
2653
|
+
|
|
2654
|
+
server.registerTool(
|
|
2655
|
+
"moor_source_credential_delete",
|
|
2656
|
+
{
|
|
2657
|
+
title: "Delete Source Credential",
|
|
2658
|
+
description:
|
|
2659
|
+
"Delete a stored source credential. Requires confirm_label to match the resolved row's label exactly - protects against deleting the wrong credential on a host that has several (e.g. two github.com rows). Refused with credential_in_use if any project still references this credential. Irreversible.",
|
|
2660
|
+
inputSchema: z.object({
|
|
2661
|
+
id: z.number().int().positive().describe("Credential id to delete"),
|
|
2662
|
+
confirm_label: z
|
|
2663
|
+
.string()
|
|
2664
|
+
.describe(
|
|
2665
|
+
"Must equal the credential row's label exactly. Resolved via moor_source_credential_get and compared before deletion.",
|
|
2666
|
+
),
|
|
2667
|
+
}),
|
|
2668
|
+
},
|
|
2669
|
+
async ({ id, confirm_label }) => {
|
|
2670
|
+
const getRes = await apiGet(`/api/server/source-credentials/${id}`);
|
|
2671
|
+
if (getRes.status === 404) throw new Error(`source credential id=${id} not found`);
|
|
2672
|
+
if (!getRes.ok)
|
|
2673
|
+
throw new Error(`Failed to fetch credential: ${getRes.status} ${await getRes.text()}`);
|
|
2674
|
+
const row = (await getRes.json()) as SourceCredentialMetadata;
|
|
2675
|
+
if (confirm_label !== row.label) {
|
|
2676
|
+
throw new Error(
|
|
2677
|
+
`confirm_label "${confirm_label}" does not match resolved label "${row.label}". Refusing to delete.`,
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
const delRes = await apiDelete(
|
|
2681
|
+
`/api/server/source-credentials/${id}?confirm_label=${encodeURIComponent(row.label)}`,
|
|
2682
|
+
);
|
|
2683
|
+
if (delRes.status === 409) {
|
|
2684
|
+
const body = (await delRes.json()) as { projects: string[] };
|
|
2685
|
+
throw new Error(
|
|
2686
|
+
`credential_in_use: ${body.projects.length} project(s) still reference id=${id}: ${body.projects.join(", ")}`,
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
if (!delRes.ok) throw new Error(`Failed to delete: ${delRes.status} ${await delRes.text()}`);
|
|
2690
|
+
return {
|
|
2691
|
+
content: [
|
|
2692
|
+
{
|
|
2693
|
+
type: "text",
|
|
2694
|
+
text: `Deleted source credential ${row.hostname}/${row.label} (id=${id}).`,
|
|
2695
|
+
},
|
|
2696
|
+
],
|
|
2697
|
+
structuredContent: { deleted: { id, hostname: row.hostname, label: row.label } },
|
|
2698
|
+
};
|
|
2699
|
+
},
|
|
2700
|
+
);
|
|
2701
|
+
|
|
2702
|
+
server.registerTool(
|
|
2703
|
+
"moor_source_credential_check",
|
|
2704
|
+
{
|
|
2705
|
+
title: "Check Source Credential Access",
|
|
2706
|
+
description:
|
|
2707
|
+
"Run a real `git ls-remote` against the repo URL to verify access. Without source_credential_id, probes anonymously first; if private and exactly one credential matches the host, auto-selects it. With source_credential_id, tests that exact credential. With branch, tests the specific branch (branch_not_found is distinct from auth failure). Discovers default_branch via HEAD symref when no branch is provided. Side effect: updates last_checked_at and last_check_status on the credential row; flips state to failed on a credentialed rejection.",
|
|
2708
|
+
inputSchema: z.object({
|
|
2709
|
+
github_url: z
|
|
2710
|
+
.string()
|
|
2711
|
+
.describe(
|
|
2712
|
+
"HTTPS Git repo URL: https://host/owner/repo (optional .git). No query, no fragment, no embedded credentials.",
|
|
2713
|
+
),
|
|
2714
|
+
branch: z
|
|
2715
|
+
.string()
|
|
2716
|
+
.optional()
|
|
2717
|
+
.describe("Specific branch to verify. Omit to discover the default branch."),
|
|
2718
|
+
source_credential_id: z
|
|
2719
|
+
.number()
|
|
2720
|
+
.int()
|
|
2721
|
+
.positive()
|
|
2722
|
+
.optional()
|
|
2723
|
+
.describe("Pin a specific credential. Required when multiple credentials match the host."),
|
|
2724
|
+
}),
|
|
2725
|
+
},
|
|
2726
|
+
async ({ github_url, branch, source_credential_id }) => {
|
|
2727
|
+
const body: Record<string, unknown> = { github_url };
|
|
2728
|
+
if (branch !== undefined) body.branch = branch;
|
|
2729
|
+
if (source_credential_id !== undefined) body.source_credential_id = source_credential_id;
|
|
2730
|
+
const res = await apiPost("/api/server/source-credentials/check", body);
|
|
2731
|
+
const json = (await res.json()) as Record<string, unknown>;
|
|
2732
|
+
if (res.ok) {
|
|
2733
|
+
const def = json.default_branch ? ` default_branch=${json.default_branch}` : "";
|
|
2734
|
+
const head = json.head_sha ? ` head_sha=${String(json.head_sha).slice(0, 12)}` : "";
|
|
2735
|
+
const auto = json.auto_selected_credential_id
|
|
2736
|
+
? ` auto_selected=${json.auto_selected_credential_id}`
|
|
2737
|
+
: "";
|
|
2738
|
+
return {
|
|
2739
|
+
content: [{ type: "text", text: `reachable${def}${head}${auto}` }],
|
|
2740
|
+
structuredContent: json,
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
// Non-OK: surface the structured failure shape directly. The agent
|
|
2744
|
+
// can branch on `code` to decide what to do next (ask for a PAT,
|
|
2745
|
+
// pick from candidates, etc.).
|
|
2746
|
+
return {
|
|
2747
|
+
content: [
|
|
2748
|
+
{
|
|
2749
|
+
type: "text",
|
|
2750
|
+
text: `check failed: code=${json.code}${json.reason ? ` reason=${json.reason}` : ""}`,
|
|
2751
|
+
},
|
|
2752
|
+
],
|
|
2753
|
+
structuredContent: json,
|
|
2754
|
+
isError: true,
|
|
2755
|
+
};
|
|
2756
|
+
},
|
|
2757
|
+
);
|
|
2758
|
+
|
|
2445
2759
|
function formatMs(ms: number): string {
|
|
2446
2760
|
if (ms < 1000) return `${ms}ms`;
|
|
2447
2761
|
const s = Math.floor(ms / 1000);
|