@moor-sh/mcp 0.20.0 → 0.21.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 +284 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2442,6 +2442,290 @@ server.registerTool(
|
|
|
2442
2442
|
},
|
|
2443
2443
|
);
|
|
2444
2444
|
|
|
2445
|
+
// --- Source credentials (HTTPS PATs for private Git repos) ---
|
|
2446
|
+
//
|
|
2447
|
+
// v1 ships HTTPS PATs only. Read paths return metadata + secret.kind;
|
|
2448
|
+
// the raw token only crosses MCP on `add` and `update`. Delete uses
|
|
2449
|
+
// confirm_label since (hostname, label) is the disambiguation key
|
|
2450
|
+
// (multiple github.com rows coexist by design). The check tool runs a
|
|
2451
|
+
// real `git ls-remote` inside moor to validate access before any
|
|
2452
|
+
// deploy commits state.
|
|
2453
|
+
|
|
2454
|
+
type SourceCredentialMetadata = {
|
|
2455
|
+
id: number;
|
|
2456
|
+
hostname: string;
|
|
2457
|
+
label: string;
|
|
2458
|
+
username: string;
|
|
2459
|
+
secret: {
|
|
2460
|
+
configured: true;
|
|
2461
|
+
kind: "github_classic_pat" | "github_fine_grained_pat" | "unknown";
|
|
2462
|
+
};
|
|
2463
|
+
state: "active" | "failed";
|
|
2464
|
+
expires_at: string | null;
|
|
2465
|
+
last_checked_at: string | null;
|
|
2466
|
+
last_check_status: string | null;
|
|
2467
|
+
created_at: string;
|
|
2468
|
+
updated_at: string;
|
|
2469
|
+
};
|
|
2470
|
+
|
|
2471
|
+
function renderSourceCredentialLine(c: SourceCredentialMetadata): string {
|
|
2472
|
+
const checked = c.last_check_status ? ` last_check=${c.last_check_status}` : "";
|
|
2473
|
+
return `id=${c.id} ${c.hostname} label=${c.label} user=${c.username} kind=${c.secret.kind} state=${c.state}${checked}`;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
server.registerTool(
|
|
2477
|
+
"moor_source_credentials_list",
|
|
2478
|
+
{
|
|
2479
|
+
title: "List Source Credentials",
|
|
2480
|
+
description:
|
|
2481
|
+
"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.",
|
|
2482
|
+
},
|
|
2483
|
+
async () => {
|
|
2484
|
+
const res = await apiGet("/api/server/source-credentials");
|
|
2485
|
+
if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
|
|
2486
|
+
const data = (await res.json()) as { rows: SourceCredentialMetadata[] };
|
|
2487
|
+
const text =
|
|
2488
|
+
data.rows.length === 0
|
|
2489
|
+
? "No source credentials configured. Public repos work anonymously."
|
|
2490
|
+
: data.rows.map(renderSourceCredentialLine).join("\n");
|
|
2491
|
+
return {
|
|
2492
|
+
content: [{ type: "text", text }],
|
|
2493
|
+
structuredContent: { rows: data.rows },
|
|
2494
|
+
};
|
|
2495
|
+
},
|
|
2496
|
+
);
|
|
2497
|
+
|
|
2498
|
+
server.registerTool(
|
|
2499
|
+
"moor_source_credential_get",
|
|
2500
|
+
{
|
|
2501
|
+
title: "Get Source Credential",
|
|
2502
|
+
description:
|
|
2503
|
+
"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.",
|
|
2504
|
+
inputSchema: z.object({
|
|
2505
|
+
id: z.number().int().positive().describe("Credential id (from moor_source_credentials_list)"),
|
|
2506
|
+
}),
|
|
2507
|
+
},
|
|
2508
|
+
async ({ id }) => {
|
|
2509
|
+
const res = await apiGet(`/api/server/source-credentials/${id}`);
|
|
2510
|
+
if (res.status === 404) throw new Error(`source credential id=${id} not found`);
|
|
2511
|
+
if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
|
|
2512
|
+
const row = (await res.json()) as SourceCredentialMetadata;
|
|
2513
|
+
return {
|
|
2514
|
+
content: [{ type: "text", text: renderSourceCredentialLine(row) }],
|
|
2515
|
+
structuredContent: row as unknown as Record<string, unknown>,
|
|
2516
|
+
};
|
|
2517
|
+
},
|
|
2518
|
+
);
|
|
2519
|
+
|
|
2520
|
+
server.registerTool(
|
|
2521
|
+
"moor_source_credential_add",
|
|
2522
|
+
{
|
|
2523
|
+
title: "Add Source Credential",
|
|
2524
|
+
description:
|
|
2525
|
+
"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.",
|
|
2526
|
+
inputSchema: z.object({
|
|
2527
|
+
hostname: z
|
|
2528
|
+
.string()
|
|
2529
|
+
.describe("Bare Git host: github.com, gitlab.com, etc. No scheme, no path."),
|
|
2530
|
+
label: z
|
|
2531
|
+
.string()
|
|
2532
|
+
.describe(
|
|
2533
|
+
"Operator-supplied label for disambiguation when multiple credentials share a host (e.g. 'personal', 'work-org', 'acme-clients'). Trimmed at storage.",
|
|
2534
|
+
),
|
|
2535
|
+
username: z
|
|
2536
|
+
.string()
|
|
2537
|
+
.describe(
|
|
2538
|
+
"Git username. For GitHub fine-grained PATs, use 'x-access-token'. For classic PATs, your GitHub username works too.",
|
|
2539
|
+
),
|
|
2540
|
+
secret: z
|
|
2541
|
+
.string()
|
|
2542
|
+
.describe(
|
|
2543
|
+
"Git token (PAT). Visible to the MCP client on input; rotate via moor_source_credential_update if exposed.",
|
|
2544
|
+
),
|
|
2545
|
+
expires_at: z
|
|
2546
|
+
.string()
|
|
2547
|
+
.nullable()
|
|
2548
|
+
.optional()
|
|
2549
|
+
.describe(
|
|
2550
|
+
"Operator-supplied expiry timestamp (PAT expiry from GitHub). Optional; helps rotation reminders.",
|
|
2551
|
+
),
|
|
2552
|
+
}),
|
|
2553
|
+
},
|
|
2554
|
+
async ({ hostname, label, username, secret, expires_at }) => {
|
|
2555
|
+
const body: Record<string, unknown> = { hostname, label, username, secret };
|
|
2556
|
+
if (expires_at !== undefined) body.expires_at = expires_at;
|
|
2557
|
+
const res = await apiPost("/api/server/source-credentials", body);
|
|
2558
|
+
if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
|
|
2559
|
+
const row = (await res.json()) as SourceCredentialMetadata;
|
|
2560
|
+
return {
|
|
2561
|
+
content: [
|
|
2562
|
+
{
|
|
2563
|
+
type: "text",
|
|
2564
|
+
text: `Added source credential for ${row.hostname} label="${row.label}" (id=${row.id}, kind=${row.secret.kind}).`,
|
|
2565
|
+
},
|
|
2566
|
+
],
|
|
2567
|
+
structuredContent: row as unknown as Record<string, unknown>,
|
|
2568
|
+
};
|
|
2569
|
+
},
|
|
2570
|
+
);
|
|
2571
|
+
|
|
2572
|
+
server.registerTool(
|
|
2573
|
+
"moor_source_credential_update",
|
|
2574
|
+
{
|
|
2575
|
+
title: "Update Source Credential",
|
|
2576
|
+
description:
|
|
2577
|
+
"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.",
|
|
2578
|
+
inputSchema: z.object({
|
|
2579
|
+
id: z.number().int().positive().describe("Credential id to update"),
|
|
2580
|
+
username: z.string().optional().describe("New username (optional)"),
|
|
2581
|
+
secret: z
|
|
2582
|
+
.string()
|
|
2583
|
+
.optional()
|
|
2584
|
+
.describe("New secret (optional). Visible to the MCP client on input."),
|
|
2585
|
+
label: z.string().optional().describe("New label (optional). Trimmed at storage."),
|
|
2586
|
+
expires_at: z.string().nullable().optional().describe("New expiry; null to clear"),
|
|
2587
|
+
}),
|
|
2588
|
+
},
|
|
2589
|
+
async ({ id, username, secret, label, expires_at }) => {
|
|
2590
|
+
if (
|
|
2591
|
+
username === undefined &&
|
|
2592
|
+
secret === undefined &&
|
|
2593
|
+
label === undefined &&
|
|
2594
|
+
expires_at === undefined
|
|
2595
|
+
) {
|
|
2596
|
+
throw new Error("must provide at least one of username, secret, label, or expires_at");
|
|
2597
|
+
}
|
|
2598
|
+
const patch: Record<string, unknown> = {};
|
|
2599
|
+
if (username !== undefined) patch.username = username;
|
|
2600
|
+
if (secret !== undefined) patch.secret = secret;
|
|
2601
|
+
if (label !== undefined) patch.label = label;
|
|
2602
|
+
if (expires_at !== undefined) patch.expires_at = expires_at;
|
|
2603
|
+
const res = await apiPut(`/api/server/source-credentials/${id}`, patch);
|
|
2604
|
+
if (res.status === 404) throw new Error(`source credential id=${id} not found`);
|
|
2605
|
+
if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
|
|
2606
|
+
const row = (await res.json()) as SourceCredentialMetadata;
|
|
2607
|
+
const fields: string[] = [];
|
|
2608
|
+
if (username !== undefined) fields.push("username");
|
|
2609
|
+
if (secret !== undefined) fields.push("secret");
|
|
2610
|
+
if (label !== undefined) fields.push("label");
|
|
2611
|
+
if (expires_at !== undefined) fields.push("expires_at");
|
|
2612
|
+
return {
|
|
2613
|
+
content: [
|
|
2614
|
+
{
|
|
2615
|
+
type: "text",
|
|
2616
|
+
text: `Updated source credential id=${id} (${row.hostname}, ${row.label}): rotated ${fields.join(" + ")}. kind=${row.secret.kind}.`,
|
|
2617
|
+
},
|
|
2618
|
+
],
|
|
2619
|
+
structuredContent: row as unknown as Record<string, unknown>,
|
|
2620
|
+
};
|
|
2621
|
+
},
|
|
2622
|
+
);
|
|
2623
|
+
|
|
2624
|
+
server.registerTool(
|
|
2625
|
+
"moor_source_credential_delete",
|
|
2626
|
+
{
|
|
2627
|
+
title: "Delete Source Credential",
|
|
2628
|
+
description:
|
|
2629
|
+
"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.",
|
|
2630
|
+
inputSchema: z.object({
|
|
2631
|
+
id: z.number().int().positive().describe("Credential id to delete"),
|
|
2632
|
+
confirm_label: z
|
|
2633
|
+
.string()
|
|
2634
|
+
.describe(
|
|
2635
|
+
"Must equal the credential row's label exactly. Resolved via moor_source_credential_get and compared before deletion.",
|
|
2636
|
+
),
|
|
2637
|
+
}),
|
|
2638
|
+
},
|
|
2639
|
+
async ({ id, confirm_label }) => {
|
|
2640
|
+
const getRes = await apiGet(`/api/server/source-credentials/${id}`);
|
|
2641
|
+
if (getRes.status === 404) throw new Error(`source credential id=${id} not found`);
|
|
2642
|
+
if (!getRes.ok)
|
|
2643
|
+
throw new Error(`Failed to fetch credential: ${getRes.status} ${await getRes.text()}`);
|
|
2644
|
+
const row = (await getRes.json()) as SourceCredentialMetadata;
|
|
2645
|
+
if (confirm_label !== row.label) {
|
|
2646
|
+
throw new Error(
|
|
2647
|
+
`confirm_label "${confirm_label}" does not match resolved label "${row.label}". Refusing to delete.`,
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
const delRes = await apiDelete(
|
|
2651
|
+
`/api/server/source-credentials/${id}?confirm_label=${encodeURIComponent(row.label)}`,
|
|
2652
|
+
);
|
|
2653
|
+
if (delRes.status === 409) {
|
|
2654
|
+
const body = (await delRes.json()) as { projects: string[] };
|
|
2655
|
+
throw new Error(
|
|
2656
|
+
`credential_in_use: ${body.projects.length} project(s) still reference id=${id}: ${body.projects.join(", ")}`,
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
if (!delRes.ok) throw new Error(`Failed to delete: ${delRes.status} ${await delRes.text()}`);
|
|
2660
|
+
return {
|
|
2661
|
+
content: [
|
|
2662
|
+
{
|
|
2663
|
+
type: "text",
|
|
2664
|
+
text: `Deleted source credential ${row.hostname}/${row.label} (id=${id}).`,
|
|
2665
|
+
},
|
|
2666
|
+
],
|
|
2667
|
+
structuredContent: { deleted: { id, hostname: row.hostname, label: row.label } },
|
|
2668
|
+
};
|
|
2669
|
+
},
|
|
2670
|
+
);
|
|
2671
|
+
|
|
2672
|
+
server.registerTool(
|
|
2673
|
+
"moor_source_credential_check",
|
|
2674
|
+
{
|
|
2675
|
+
title: "Check Source Credential Access",
|
|
2676
|
+
description:
|
|
2677
|
+
"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.",
|
|
2678
|
+
inputSchema: z.object({
|
|
2679
|
+
github_url: z
|
|
2680
|
+
.string()
|
|
2681
|
+
.describe(
|
|
2682
|
+
"HTTPS Git repo URL: https://host/owner/repo (optional .git). No query, no fragment, no embedded credentials.",
|
|
2683
|
+
),
|
|
2684
|
+
branch: z
|
|
2685
|
+
.string()
|
|
2686
|
+
.optional()
|
|
2687
|
+
.describe("Specific branch to verify. Omit to discover the default branch."),
|
|
2688
|
+
source_credential_id: z
|
|
2689
|
+
.number()
|
|
2690
|
+
.int()
|
|
2691
|
+
.positive()
|
|
2692
|
+
.optional()
|
|
2693
|
+
.describe("Pin a specific credential. Required when multiple credentials match the host."),
|
|
2694
|
+
}),
|
|
2695
|
+
},
|
|
2696
|
+
async ({ github_url, branch, source_credential_id }) => {
|
|
2697
|
+
const body: Record<string, unknown> = { github_url };
|
|
2698
|
+
if (branch !== undefined) body.branch = branch;
|
|
2699
|
+
if (source_credential_id !== undefined) body.source_credential_id = source_credential_id;
|
|
2700
|
+
const res = await apiPost("/api/server/source-credentials/check", body);
|
|
2701
|
+
const json = (await res.json()) as Record<string, unknown>;
|
|
2702
|
+
if (res.ok) {
|
|
2703
|
+
const def = json.default_branch ? ` default_branch=${json.default_branch}` : "";
|
|
2704
|
+
const head = json.head_sha ? ` head_sha=${String(json.head_sha).slice(0, 12)}` : "";
|
|
2705
|
+
const auto = json.auto_selected_credential_id
|
|
2706
|
+
? ` auto_selected=${json.auto_selected_credential_id}`
|
|
2707
|
+
: "";
|
|
2708
|
+
return {
|
|
2709
|
+
content: [{ type: "text", text: `reachable${def}${head}${auto}` }],
|
|
2710
|
+
structuredContent: json,
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
// Non-OK: surface the structured failure shape directly. The agent
|
|
2714
|
+
// can branch on `code` to decide what to do next (ask for a PAT,
|
|
2715
|
+
// pick from candidates, etc.).
|
|
2716
|
+
return {
|
|
2717
|
+
content: [
|
|
2718
|
+
{
|
|
2719
|
+
type: "text",
|
|
2720
|
+
text: `check failed: code=${json.code}${json.reason ? ` reason=${json.reason}` : ""}`,
|
|
2721
|
+
},
|
|
2722
|
+
],
|
|
2723
|
+
structuredContent: json,
|
|
2724
|
+
isError: true,
|
|
2725
|
+
};
|
|
2726
|
+
},
|
|
2727
|
+
);
|
|
2728
|
+
|
|
2445
2729
|
function formatMs(ms: number): string {
|
|
2446
2730
|
if (ms < 1000) return `${ms}ms`;
|
|
2447
2731
|
const s = Math.floor(ms / 1000);
|