@moor-sh/mcp 0.25.0 → 0.27.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 +208 -3
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -574,7 +574,7 @@ server.registerTool(
|
|
|
574
574
|
{
|
|
575
575
|
title: "Server Stats",
|
|
576
576
|
description:
|
|
577
|
-
"Get server resource usage: load, memory, per-filesystem disk usage (
|
|
577
|
+
"Get server resource usage: load, memory, per-filesystem disk usage (the filesystems the moor container can see, plus any operator-configured monitored host disks via MOOR_MONITORED_DISKS), Docker disk by category (images/containers/volumes/build cache) with reclaimable bytes, and container counts. Note: cpu.percent is load-derived (load avg ÷ cores), not instantaneous CPU; use the `load` field for the same signal with explicit naming.",
|
|
578
578
|
},
|
|
579
579
|
async () => {
|
|
580
580
|
const res = await apiGet("/api/server/stats");
|
|
@@ -587,7 +587,7 @@ server.registerTool(
|
|
|
587
587
|
load?: { one_min: number; cores: number; normalized_percent: number };
|
|
588
588
|
memory: { total: string; used: string; percent: number };
|
|
589
589
|
disk: { total: string; used: string; percent: number };
|
|
590
|
-
disks?: { mount: string; total: string; used: string; percent: number }[];
|
|
590
|
+
disks?: { mount: string; total: string; used: string; percent: number; label?: string }[];
|
|
591
591
|
containers: { running: number; total: number };
|
|
592
592
|
docker?: {
|
|
593
593
|
images: { bytes: number; reclaimable_bytes: number; count: number; unused_count: number };
|
|
@@ -615,7 +615,8 @@ server.registerTool(
|
|
|
615
615
|
lines.push(`Memory: ${s.memory.used} / ${s.memory.total} (${s.memory.percent}%)`);
|
|
616
616
|
const disks = s.disks?.length ? s.disks : [{ mount: "/", ...s.disk }];
|
|
617
617
|
for (const d of disks) {
|
|
618
|
-
|
|
618
|
+
const name = d.label ? `${d.label} (${d.mount})` : `Disk ${d.mount}`;
|
|
619
|
+
lines.push(`${name}: ${d.used} / ${d.total} (${d.percent}%)`);
|
|
619
620
|
}
|
|
620
621
|
lines.push(`Containers: ${s.containers.running} running / ${s.containers.total} total`);
|
|
621
622
|
if (s.docker) {
|
|
@@ -1282,6 +1283,20 @@ server.registerTool(
|
|
|
1282
1283
|
.describe(
|
|
1283
1284
|
"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).",
|
|
1284
1285
|
),
|
|
1286
|
+
command: z
|
|
1287
|
+
.array(z.string())
|
|
1288
|
+
.nullable()
|
|
1289
|
+
.optional()
|
|
1290
|
+
.describe(
|
|
1291
|
+
'Override the image\'s default command (Docker Cmd) as an argv array, e.g. ["tunnel","run"]. Lets a stock image run a custom command with no throwaway Dockerfile. Omit to keep the image default; pass [] or null to clear a previously-set override. Applies on container recreate.',
|
|
1292
|
+
),
|
|
1293
|
+
entrypoint: z
|
|
1294
|
+
.array(z.string())
|
|
1295
|
+
.nullable()
|
|
1296
|
+
.optional()
|
|
1297
|
+
.describe(
|
|
1298
|
+
"Override the image's ENTRYPOINT as an argv array. Omit to keep the image default; pass [] or null to clear. Applies on container recreate.",
|
|
1299
|
+
),
|
|
1285
1300
|
}),
|
|
1286
1301
|
},
|
|
1287
1302
|
async (input) => {
|
|
@@ -1371,6 +1386,20 @@ server.registerTool(
|
|
|
1371
1386
|
.describe(
|
|
1372
1387
|
"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.",
|
|
1373
1388
|
),
|
|
1389
|
+
command: z
|
|
1390
|
+
.array(z.string())
|
|
1391
|
+
.nullable()
|
|
1392
|
+
.optional()
|
|
1393
|
+
.describe(
|
|
1394
|
+
'Override the image\'s default command (Docker Cmd) as an argv array, e.g. ["tunnel","run"]. Pass [] or null to clear the override and return to the image default. Takes effect on container recreate.',
|
|
1395
|
+
),
|
|
1396
|
+
entrypoint: z
|
|
1397
|
+
.array(z.string())
|
|
1398
|
+
.nullable()
|
|
1399
|
+
.optional()
|
|
1400
|
+
.describe(
|
|
1401
|
+
"Override the image's ENTRYPOINT as an argv array. Pass [] or null to clear. Takes effect on container recreate.",
|
|
1402
|
+
),
|
|
1374
1403
|
}),
|
|
1375
1404
|
},
|
|
1376
1405
|
async (input) => {
|
|
@@ -1728,6 +1757,124 @@ server.registerTool(
|
|
|
1728
1757
|
},
|
|
1729
1758
|
);
|
|
1730
1759
|
|
|
1760
|
+
// --- Declarative file injection ---
|
|
1761
|
+
|
|
1762
|
+
server.registerTool(
|
|
1763
|
+
"moor_file_set",
|
|
1764
|
+
{
|
|
1765
|
+
title: "Set Project File",
|
|
1766
|
+
description:
|
|
1767
|
+
"Declare a file to inject into a project's container. moor writes it via a tar archive PUT right before the container starts, on every recreate, honoring the octal mode (e.g. 0600 for a TLS key). Identified by path — setting the same path again updates its content/mode rather than duplicating. Provide exactly one of content (inline) or env_ref (the name of a project env var to source content from at create time, so a secret stays in the env store instead of plaintext here). Takes effect on next container recreate (moor_rebuild / moor_restart / moor_deploy / moor_project run).",
|
|
1768
|
+
inputSchema: z.object({
|
|
1769
|
+
project: z.string().describe("Project name or ID"),
|
|
1770
|
+
path: z
|
|
1771
|
+
.string()
|
|
1772
|
+
.min(1)
|
|
1773
|
+
.describe("Absolute in-container destination path, e.g. /etc/ssl/cert.pem"),
|
|
1774
|
+
content: z
|
|
1775
|
+
.string()
|
|
1776
|
+
.optional()
|
|
1777
|
+
.describe("Inline file contents. Provide exactly one of content or env_ref."),
|
|
1778
|
+
env_ref: z
|
|
1779
|
+
.string()
|
|
1780
|
+
.optional()
|
|
1781
|
+
.describe(
|
|
1782
|
+
"Name of a project env var to source the contents from at create time. Keeps secrets (keys, certs) in the env store instead of plaintext here. Provide exactly one of content or env_ref.",
|
|
1783
|
+
),
|
|
1784
|
+
mode: z
|
|
1785
|
+
.string()
|
|
1786
|
+
.optional()
|
|
1787
|
+
.describe(
|
|
1788
|
+
"Octal permission string applied in the tar header, e.g. '0600'. Default '0644'.",
|
|
1789
|
+
),
|
|
1790
|
+
}),
|
|
1791
|
+
},
|
|
1792
|
+
async ({ project, path, content, env_ref, mode }) => {
|
|
1793
|
+
const p = await resolveProject(project);
|
|
1794
|
+
const body: Record<string, unknown> = { path };
|
|
1795
|
+
if (content !== undefined) body.content = content;
|
|
1796
|
+
if (env_ref !== undefined) body.env_ref = env_ref;
|
|
1797
|
+
if (mode !== undefined) body.mode = mode;
|
|
1798
|
+
const res = await apiPost(`/api/projects/${p.id}/files`, body);
|
|
1799
|
+
if (!res.ok) throw new Error(`Failed: ${await res.text()}`);
|
|
1800
|
+
const saved = (await res.json()) as {
|
|
1801
|
+
id: number;
|
|
1802
|
+
path: string;
|
|
1803
|
+
mode: string;
|
|
1804
|
+
source: string;
|
|
1805
|
+
env_ref: string | null;
|
|
1806
|
+
};
|
|
1807
|
+
const verb = res.status === 201 ? "Added" : "Updated";
|
|
1808
|
+
return {
|
|
1809
|
+
content: [
|
|
1810
|
+
{
|
|
1811
|
+
type: "text",
|
|
1812
|
+
text: `${verb} file on ${p.name}: id=${saved.id}, path=${saved.path}, mode=${saved.mode}, source=${saved.source}${saved.env_ref ? ` (env_ref=${saved.env_ref})` : ""}. Written into the container on next recreate.`,
|
|
1813
|
+
},
|
|
1814
|
+
],
|
|
1815
|
+
};
|
|
1816
|
+
},
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
server.registerTool(
|
|
1820
|
+
"moor_file_list",
|
|
1821
|
+
{
|
|
1822
|
+
title: "List Project Files",
|
|
1823
|
+
description:
|
|
1824
|
+
"List the declarative files configured for a project. Each entry shows the in-container path, octal mode, and how content is sourced (inline or env). Raw inline content is never returned (it may be large, and env-sourced content lives in the env store).",
|
|
1825
|
+
inputSchema: z.object({
|
|
1826
|
+
project: z.string().describe("Project name or ID"),
|
|
1827
|
+
}),
|
|
1828
|
+
},
|
|
1829
|
+
async ({ project }) => {
|
|
1830
|
+
const p = await resolveProject(project);
|
|
1831
|
+
const res = await apiGet(`/api/projects/${p.id}/files`);
|
|
1832
|
+
if (!res.ok) throw new Error(`Failed: ${await res.text()}`);
|
|
1833
|
+
const rows = (await res.json()) as Array<{
|
|
1834
|
+
id: number;
|
|
1835
|
+
path: string;
|
|
1836
|
+
mode: string;
|
|
1837
|
+
source: string;
|
|
1838
|
+
env_ref: string | null;
|
|
1839
|
+
}>;
|
|
1840
|
+
if (rows.length === 0) {
|
|
1841
|
+
return { content: [{ type: "text", text: `No files configured for ${p.name}.` }] };
|
|
1842
|
+
}
|
|
1843
|
+
const lines = rows.map(
|
|
1844
|
+
(f) =>
|
|
1845
|
+
`id=${f.id} path=${f.path} mode=${f.mode} source=${f.source}${f.env_ref ? ` env_ref=${f.env_ref}` : ""}`,
|
|
1846
|
+
);
|
|
1847
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1848
|
+
},
|
|
1849
|
+
);
|
|
1850
|
+
|
|
1851
|
+
server.registerTool(
|
|
1852
|
+
"moor_file_remove",
|
|
1853
|
+
{
|
|
1854
|
+
title: "Remove Project File",
|
|
1855
|
+
description:
|
|
1856
|
+
"Remove a declared file from a project's injection set. The file stops being written on future container recreates; a copy already present in a running container is not deleted until the next recreate. Takes effect on next container recreate.",
|
|
1857
|
+
inputSchema: z.object({
|
|
1858
|
+
project: z.string().describe("Project name or ID"),
|
|
1859
|
+
file_id: z.number().int().positive().describe("File ID from moor_file_list"),
|
|
1860
|
+
}),
|
|
1861
|
+
},
|
|
1862
|
+
async ({ project, file_id }) => {
|
|
1863
|
+
const p = await resolveProject(project);
|
|
1864
|
+
const res = await apiDelete(`/api/projects/${p.id}/files/${file_id}`);
|
|
1865
|
+
if (res.status === 404) throw new Error(`File ${file_id} not found on project ${p.name}`);
|
|
1866
|
+
if (!res.ok) throw new Error(`Failed: ${await res.text()}`);
|
|
1867
|
+
return {
|
|
1868
|
+
content: [
|
|
1869
|
+
{
|
|
1870
|
+
type: "text",
|
|
1871
|
+
text: `Removed file ${file_id} from ${p.name}. Applies on next container recreate.`,
|
|
1872
|
+
},
|
|
1873
|
+
],
|
|
1874
|
+
};
|
|
1875
|
+
},
|
|
1876
|
+
);
|
|
1877
|
+
|
|
1731
1878
|
// --- Runs history (#37) ---
|
|
1732
1879
|
|
|
1733
1880
|
// A runs row can be a cron run, a build/manual run, OR a cron run whose cron
|
|
@@ -2005,6 +2152,47 @@ server.registerTool(
|
|
|
2005
2152
|
.describe(
|
|
2006
2153
|
"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.",
|
|
2007
2154
|
),
|
|
2155
|
+
command: z
|
|
2156
|
+
.array(z.string())
|
|
2157
|
+
.nullable()
|
|
2158
|
+
.optional()
|
|
2159
|
+
.describe(
|
|
2160
|
+
'Override the image default command (Docker Cmd) as an argv array, e.g. ["tunnel","run"]. Lets a stock image (e.g. cloudflare/cloudflared) run a custom command with no throwaway Dockerfile. Omit to keep the image default; pass [] or null to clear. Applies on the recreate the run step performs.',
|
|
2161
|
+
),
|
|
2162
|
+
entrypoint: z
|
|
2163
|
+
.array(z.string())
|
|
2164
|
+
.nullable()
|
|
2165
|
+
.optional()
|
|
2166
|
+
.describe(
|
|
2167
|
+
"Override the image ENTRYPOINT as an argv array. Omit to keep the image default; pass [] or null to clear. Applies on container recreate.",
|
|
2168
|
+
),
|
|
2169
|
+
files: z
|
|
2170
|
+
.array(
|
|
2171
|
+
z.object({
|
|
2172
|
+
path: z
|
|
2173
|
+
.string()
|
|
2174
|
+
.min(1)
|
|
2175
|
+
.describe("Absolute in-container destination path, e.g. /etc/ssl/cert.pem"),
|
|
2176
|
+
content: z
|
|
2177
|
+
.string()
|
|
2178
|
+
.optional()
|
|
2179
|
+
.describe("Inline file contents. Provide exactly one of content or env_ref."),
|
|
2180
|
+
env_ref: z
|
|
2181
|
+
.string()
|
|
2182
|
+
.optional()
|
|
2183
|
+
.describe(
|
|
2184
|
+
"Name of a project env var to source the contents from at create time, so a secret (TLS key, token) lives in the env store rather than in plaintext here. Provide exactly one of content or env_ref.",
|
|
2185
|
+
),
|
|
2186
|
+
mode: z
|
|
2187
|
+
.string()
|
|
2188
|
+
.optional()
|
|
2189
|
+
.describe("Octal permission string for the tar header, e.g. '0600'. Default '0644'."),
|
|
2190
|
+
}),
|
|
2191
|
+
)
|
|
2192
|
+
.optional()
|
|
2193
|
+
.describe(
|
|
2194
|
+
"Declarative files to inject into the container before it starts, written on every recreate (additions/updates only — moor_deploy never removes files; use moor_file_remove). Each file's path identifies it; re-deploying the same path updates its content. Honors the octal mode in the tar header (e.g. 0600 for a key).",
|
|
2195
|
+
),
|
|
2008
2196
|
run: z
|
|
2009
2197
|
.boolean()
|
|
2010
2198
|
.optional()
|
|
@@ -2105,6 +2293,8 @@ server.registerTool(
|
|
|
2105
2293
|
memory_limit_mb: input.memory_limit_mb,
|
|
2106
2294
|
cpus: input.cpus,
|
|
2107
2295
|
source_credential_id: input.source_credential_id,
|
|
2296
|
+
command: input.command,
|
|
2297
|
+
entrypoint: input.entrypoint,
|
|
2108
2298
|
};
|
|
2109
2299
|
const res = await apiPost("/api/projects", createBody);
|
|
2110
2300
|
if (!res.ok) throw new Error(`[create] ${await res.text()}`);
|
|
@@ -2126,6 +2316,8 @@ server.registerTool(
|
|
|
2126
2316
|
if (input.cpus !== undefined) updateBody.cpus = input.cpus;
|
|
2127
2317
|
if (input.source_credential_id !== undefined)
|
|
2128
2318
|
updateBody.source_credential_id = input.source_credential_id;
|
|
2319
|
+
if (input.command !== undefined) updateBody.command = input.command;
|
|
2320
|
+
if (input.entrypoint !== undefined) updateBody.entrypoint = input.entrypoint;
|
|
2129
2321
|
|
|
2130
2322
|
if (Object.keys(updateBody).length > 0) {
|
|
2131
2323
|
const res = await apiPut(`/api/projects/${existing.id}`, updateBody);
|
|
@@ -2180,6 +2372,19 @@ server.registerTool(
|
|
|
2180
2372
|
}
|
|
2181
2373
|
}
|
|
2182
2374
|
|
|
2375
|
+
// Step 1.6: inject declarative files (additions/updates only — deploy never
|
|
2376
|
+
// removes files; use moor_file_remove for that). The route upserts by path,
|
|
2377
|
+
// so re-deploying the same path updates its content. Files are written into
|
|
2378
|
+
// the container right before start on the recreate the run step triggers.
|
|
2379
|
+
if (input.files && input.files.length > 0) {
|
|
2380
|
+
for (const f of input.files) {
|
|
2381
|
+
const fRes = await apiPost(`/api/projects/${projectId}/files`, f);
|
|
2382
|
+
if (!fRes.ok) {
|
|
2383
|
+
throw new Error(`[files] failed to set ${f.path}: ${await fRes.text()}`);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2183
2388
|
// Step 2: merge envs. Omitted env leaves existing untouched; {} is a no-op.
|
|
2184
2389
|
const envEntries = input.env ? Object.entries(input.env) : [];
|
|
2185
2390
|
const envProvided = envEntries.length > 0;
|