@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +208 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moor-sh/mcp",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "MCP server for moor - lets AI agents (Claude Code, Cursor, etc.) manage your moor projects via the moor HTTP API.",
5
5
  "license": "MIT",
6
6
  "repository": {
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 (every real mount, not just root so a separate data volume can't hide), 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.",
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
- lines.push(`Disk ${d.mount}: ${d.used} / ${d.total} (${d.percent}%)`);
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;