@artifactly/mcp 0.1.11 → 0.1.12

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/README.md CHANGED
@@ -73,6 +73,12 @@ Artifact workflow notes:
73
73
  - `artifactly_publish_file` infers MIME type from file extension unless `mimeType` is supplied; explicit `mimeType` wins.
74
74
  - Image MIME types such as `image/png` publish as `kind: "image"` and can be listed with `kind: "image"`.
75
75
  - Files larger than 10 MB are rejected before upload to avoid large in-memory base64 payloads.
76
+ - App-hosting MCP tools wrap the public API-key routes for `web_app` artifacts:
77
+ - `artifactly_list_app_env`, `artifactly_set_app_env`, and `artifactly_delete_app_env` manage redacted per-app env/secrets.
78
+ - `artifactly_publish_app_bundle` accepts source files with inline `content`, `contentBase64`, or local `filePath` values and queues a background build job; Docker work does not run in the MCP call.
79
+ - `artifactly_enqueue_app_build` queues an image build for already-materialized source, and `artifactly_get_app_build_status` polls job status.
80
+ - `artifactly_deploy_app` deploys an already-built image reference.
81
+ - `artifactly_list_custom_domains`, `artifactly_add_custom_domain`, and `artifactly_delete_custom_domain` manage custom-domain status and DNS instructions without exposing provider credentials.
76
82
  - Requests time out after 30 seconds.
77
83
  - This package currently exposes MCP tools only; resources, resource templates, and prompts are intentionally unsupported.
78
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artifactly/mcp",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "private": false,
5
5
  "files": [
6
6
  "bin",
@@ -21,6 +21,7 @@
21
21
  "typecheck": "tsc --noEmit",
22
22
  "test": "vitest run",
23
23
  "artifactly": "tsx src/cli/run-artifactly.ts",
24
+ "app-build-worker": "tsx src/lib/apps/run-build-worker.ts",
24
25
  "generate-api-key": "node scripts/generate-api-key.mjs",
25
26
  "supabase:start": "supabase start",
26
27
  "supabase:reset": "supabase db reset",
@@ -13,6 +13,9 @@ describe("Artifactly MCP package documentation", () => {
13
13
  expect(readme).toContain("public`, `unlisted`, `private`, and `gated`");
14
14
  expect(readme).toContain("`artifactly_list_artifacts` supports `limit`, `cursor`, `visibility`, and `kind`");
15
15
  expect(readme).toContain("Files larger than 10 MB are rejected before upload");
16
+ expect(readme).toContain("artifactly_publish_app_bundle");
17
+ expect(readme).toContain("Docker work does not run in the MCP call");
18
+ expect(readme).toContain("artifactly_add_custom_domain");
16
19
  expect(readme).toContain("Requests time out after 30 seconds");
17
20
  expect(readme).toContain("`artifactly_get_artifact` returns full content by default");
18
21
  expect(readme).toContain("`metadataOnly: true`");
@@ -85,6 +85,27 @@ describe("Artifactly MCP stdio server helpers", () => {
85
85
  mimeType: "application/pdf",
86
86
  }).success,
87
87
  ).toBe(true);
88
+ expect(
89
+ server._registeredTools.artifactly_set_app_env.inputSchema.safeParse({
90
+ artifactId: "artifact_1",
91
+ name: "DATABASE_URL",
92
+ value: "postgres://example",
93
+ isSecret: true,
94
+ }).success,
95
+ ).toBe(true);
96
+ expect(
97
+ server._registeredTools.artifactly_publish_app_bundle.inputSchema.safeParse({
98
+ artifactId: "artifact_1",
99
+ files: [{ path: "server.js", filePath: "/tmp/server.js" }],
100
+ sourceKind: "nextjs",
101
+ }).success,
102
+ ).toBe(true);
103
+ expect(
104
+ server._registeredTools.artifactly_add_custom_domain.inputSchema.safeParse({
105
+ artifactId: "artifact_1",
106
+ hostname: "app.example.com",
107
+ }).success,
108
+ ).toBe(true);
88
109
  });
89
110
 
90
111
  it("registers MCP tool annotations and accurate content descriptions", () => {
@@ -101,6 +122,15 @@ describe("Artifactly MCP stdio server helpers", () => {
101
122
  expect(server._registeredTools.artifactly_delete_artifact.annotations).toMatchObject({
102
123
  destructiveHint: true,
103
124
  });
125
+ expect(server._registeredTools.artifactly_delete_app_env.annotations).toMatchObject({
126
+ destructiveHint: true,
127
+ });
128
+ expect(server._registeredTools.artifactly_list_app_env.annotations).toMatchObject({
129
+ readOnlyHint: true,
130
+ });
131
+ expect(server._registeredTools.artifactly_list_custom_domains.annotations).toMatchObject({
132
+ readOnlyHint: true,
133
+ });
104
134
  expect(server._registeredTools.artifactly_get_artifact.description).toMatch(/full content/i);
105
135
  expect(server._registeredTools.artifactly_get_artifact.description).toMatch(/metadataOnly/i);
106
136
  });
@@ -117,6 +147,16 @@ describe("Artifactly MCP stdio server helpers", () => {
117
147
  "artifactly_update_artifact",
118
148
  "artifactly_delete_artifact",
119
149
  "artifactly_publish_file",
150
+ "artifactly_list_app_env",
151
+ "artifactly_set_app_env",
152
+ "artifactly_delete_app_env",
153
+ "artifactly_publish_app_bundle",
154
+ "artifactly_enqueue_app_build",
155
+ "artifactly_get_app_build_status",
156
+ "artifactly_deploy_app",
157
+ "artifactly_list_custom_domains",
158
+ "artifactly_add_custom_domain",
159
+ "artifactly_delete_custom_domain",
120
160
  ]);
121
161
  });
122
162
 
@@ -145,6 +145,50 @@ const publishFileSchema = z.object({
145
145
  visibility: z.enum(["public", "unlisted", "private", "gated"]).optional(),
146
146
  accessList: z.array(z.string().email()).optional(),
147
147
  });
148
+ const appEnvSchema = z.object({
149
+ artifactId: z.string().min(1),
150
+ });
151
+ const setAppEnvSchema = z.object({
152
+ artifactId: z.string().min(1),
153
+ name: z.string().min(1),
154
+ value: z.string(),
155
+ isSecret: z.boolean().optional(),
156
+ });
157
+ const deleteAppEnvSchema = z.object({
158
+ artifactId: z.string().min(1),
159
+ name: z.string().min(1),
160
+ });
161
+ const appBuildSchema = z.object({
162
+ artifactId: z.string().min(1),
163
+ sourceKind: z.enum(["nextjs", "dockerfile"]).optional(),
164
+ tag: z.string().min(1).optional(),
165
+ port: z.number().int().min(1).max(65535).optional(),
166
+ });
167
+ const appBundleFileSchema = z.object({
168
+ path: z.string().min(1),
169
+ content: z.string().optional(),
170
+ contentBase64: z.string().optional(),
171
+ filePath: z.string().min(1).optional(),
172
+ });
173
+ const publishAppBundleSchema = appBuildSchema.extend({
174
+ files: z.array(appBundleFileSchema).min(1),
175
+ });
176
+ const appBuildStatusSchema = z.object({
177
+ artifactId: z.string().min(1),
178
+ jobId: z.string().min(1),
179
+ });
180
+ const deployAppSchema = z.object({
181
+ artifactId: z.string().min(1),
182
+ imageRef: z.string().min(1),
183
+ port: z.number().int().min(1).max(65535).optional(),
184
+ });
185
+ const customDomainSchema = z.object({
186
+ artifactId: z.string().min(1),
187
+ });
188
+ const mutateCustomDomainSchema = z.object({
189
+ artifactId: z.string().min(1),
190
+ hostname: z.string().min(1),
191
+ });
148
192
 
149
193
  const toolSchemas = {
150
194
  artifactly_auth_status: authStatusSchema,
@@ -157,13 +201,28 @@ const toolSchemas = {
157
201
  artifactly_update_artifact: updateArtifactSchema,
158
202
  artifactly_delete_artifact: deleteArtifactSchema,
159
203
  artifactly_publish_file: publishFileSchema,
204
+ artifactly_list_app_env: appEnvSchema,
205
+ artifactly_set_app_env: setAppEnvSchema,
206
+ artifactly_delete_app_env: deleteAppEnvSchema,
207
+ artifactly_publish_app_bundle: publishAppBundleSchema,
208
+ artifactly_enqueue_app_build: appBuildSchema,
209
+ artifactly_get_app_build_status: appBuildStatusSchema,
210
+ artifactly_deploy_app: deployAppSchema,
211
+ artifactly_list_custom_domains: customDomainSchema,
212
+ artifactly_add_custom_domain: mutateCustomDomainSchema,
213
+ artifactly_delete_custom_domain: mutateCustomDomainSchema,
160
214
  } as const;
161
215
 
162
216
  const toolAnnotations: Record<string, Record<string, boolean>> = {
163
217
  artifactly_auth_status: { readOnlyHint: true },
164
218
  artifactly_list_artifacts: { readOnlyHint: true },
165
219
  artifactly_get_artifact: { readOnlyHint: true },
220
+ artifactly_list_app_env: { readOnlyHint: true },
221
+ artifactly_get_app_build_status: { readOnlyHint: true },
222
+ artifactly_list_custom_domains: { readOnlyHint: true },
166
223
  artifactly_delete_artifact: { destructiveHint: true },
224
+ artifactly_delete_app_env: { destructiveHint: true },
225
+ artifactly_delete_custom_domain: { destructiveHint: true },
167
226
  artifactly_logout: { destructiveHint: true },
168
227
  };
169
228
 
@@ -20,6 +20,16 @@ describe("Artifactly MCP tools", () => {
20
20
  "artifactly_update_artifact",
21
21
  "artifactly_delete_artifact",
22
22
  "artifactly_publish_file",
23
+ "artifactly_list_app_env",
24
+ "artifactly_set_app_env",
25
+ "artifactly_delete_app_env",
26
+ "artifactly_publish_app_bundle",
27
+ "artifactly_enqueue_app_build",
28
+ "artifactly_get_app_build_status",
29
+ "artifactly_deploy_app",
30
+ "artifactly_list_custom_domains",
31
+ "artifactly_add_custom_domain",
32
+ "artifactly_delete_custom_domain",
23
33
  ]);
24
34
  });
25
35
 
@@ -639,4 +649,129 @@ describe("Artifactly MCP tools", () => {
639
649
  });
640
650
  rmSync(dir, { recursive: true, force: true });
641
651
  });
652
+
653
+ it("wraps API-key app-hosting endpoints without returning secrets", async () => {
654
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
655
+ const credentialPath = join(dir, "credentials.json");
656
+ const requests: Array<{ method: string; path: string; body?: unknown; apiKey?: string }> = [];
657
+ writeCredentials(
658
+ {
659
+ apiBaseUrl: "https://artifactly.example",
660
+ apiKey: "art_test_secret",
661
+ email: "agent@example.com",
662
+ workspace: "agent",
663
+ },
664
+ credentialPath,
665
+ );
666
+
667
+ const requestJson = async (method: string, path: string, body?: unknown, options?: { apiKey?: string }) => {
668
+ requests.push({ method, path, body, apiKey: options?.apiKey });
669
+ if (path.endsWith("/app/env") && method === "POST") {
670
+ return { env: { name: "DATABASE_URL", value: "[REDACTED]", isSecret: true } };
671
+ }
672
+ if (path.endsWith("/app/build-jobs") && method === "POST") {
673
+ return { job: { id: "job_1", status: "queued", imageRef: "registry.fly.io/app:test" } };
674
+ }
675
+ return { ok: true };
676
+ };
677
+
678
+ await expect(callArtifactlyMcpTool({
679
+ name: "artifactly_list_app_env",
680
+ credentialPath,
681
+ arguments: { artifactId: "artifact_1" },
682
+ requestJson,
683
+ })).resolves.toEqual({ ok: true });
684
+ await expect(callArtifactlyMcpTool({
685
+ name: "artifactly_set_app_env",
686
+ credentialPath,
687
+ arguments: { artifactId: "artifact_1", name: "DATABASE_URL", value: "postgres://secret", isSecret: true },
688
+ requestJson,
689
+ })).resolves.toEqual({ env: { name: "DATABASE_URL", value: "[REDACTED]", isSecret: true } });
690
+ await callArtifactlyMcpTool({
691
+ name: "artifactly_delete_app_env",
692
+ credentialPath,
693
+ arguments: { artifactId: "artifact_1", name: "DATABASE_URL" },
694
+ requestJson,
695
+ });
696
+ await expect(callArtifactlyMcpTool({
697
+ name: "artifactly_enqueue_app_build",
698
+ credentialPath,
699
+ arguments: { artifactId: "artifact_1", sourceKind: "nextjs", tag: "test", port: 3000 },
700
+ requestJson,
701
+ })).resolves.toMatchObject({ job: { id: "job_1", status: "queued" } });
702
+ await callArtifactlyMcpTool({
703
+ name: "artifactly_get_app_build_status",
704
+ credentialPath,
705
+ arguments: { artifactId: "artifact_1", jobId: "job_1" },
706
+ requestJson,
707
+ });
708
+ await callArtifactlyMcpTool({
709
+ name: "artifactly_deploy_app",
710
+ credentialPath,
711
+ arguments: { artifactId: "artifact_1", imageRef: "registry.fly.io/app:test", port: 3000 },
712
+ requestJson,
713
+ });
714
+
715
+ expect(requests).toEqual([
716
+ { method: "GET", path: "/api/artifacts/artifact_1/app/env", apiKey: "art_test_secret" },
717
+ { method: "POST", path: "/api/artifacts/artifact_1/app/env", apiKey: "art_test_secret", body: { name: "DATABASE_URL", value: "postgres://secret", isSecret: true } },
718
+ { method: "DELETE", path: "/api/artifacts/artifact_1/app/env?name=DATABASE_URL", apiKey: "art_test_secret" },
719
+ { method: "POST", path: "/api/artifacts/artifact_1/app/build-jobs", apiKey: "art_test_secret", body: { sourceKind: "nextjs", tag: "test", port: 3000 } },
720
+ { method: "GET", path: "/api/artifacts/artifact_1/app/build-jobs/job_1", apiKey: "art_test_secret" },
721
+ { method: "POST", path: "/api/artifacts/artifact_1/app/deploy", apiKey: "art_test_secret", body: { imageRef: "registry.fly.io/app:test", port: 3000 } },
722
+ ]);
723
+ rmSync(dir, { recursive: true, force: true });
724
+ });
725
+
726
+ it("uploads app bundle files and manages custom domains through public API routes", async () => {
727
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
728
+ const credentialPath = join(dir, "credentials.json");
729
+ const filePath = join(dir, "server.js");
730
+ const requests: Array<{ method: string; path: string; body?: unknown; apiKey?: string }> = [];
731
+ writeFileSync(filePath, "console.log('hi')\n");
732
+ writeCredentials(
733
+ {
734
+ apiBaseUrl: "https://artifactly.example",
735
+ apiKey: "art_test_secret",
736
+ email: "agent@example.com",
737
+ workspace: "agent",
738
+ },
739
+ credentialPath,
740
+ );
741
+
742
+ const requestJson = async (method: string, path: string, body?: unknown, options?: { apiKey?: string }) => {
743
+ requests.push({ method, path, body, apiKey: options?.apiKey });
744
+ return path.includes("custom-domains") ? { domains: [], dns: { target: "app.fly.dev" } } : { job: { id: "job_1" } };
745
+ };
746
+
747
+ await callArtifactlyMcpTool({
748
+ name: "artifactly_publish_app_bundle",
749
+ credentialPath,
750
+ arguments: {
751
+ artifactId: "artifact_1",
752
+ sourceKind: "nextjs",
753
+ files: [{ path: "server.js", filePath }],
754
+ },
755
+ requestJson,
756
+ });
757
+ await callArtifactlyMcpTool({ name: "artifactly_list_custom_domains", credentialPath, arguments: { artifactId: "artifact_1" }, requestJson });
758
+ await callArtifactlyMcpTool({ name: "artifactly_add_custom_domain", credentialPath, arguments: { artifactId: "artifact_1", hostname: "app.example.com" }, requestJson });
759
+ await callArtifactlyMcpTool({ name: "artifactly_delete_custom_domain", credentialPath, arguments: { artifactId: "artifact_1", hostname: "app.example.com" }, requestJson });
760
+
761
+ expect(requests).toEqual([
762
+ {
763
+ method: "POST",
764
+ path: "/api/artifacts/artifact_1/app/publish",
765
+ apiKey: "art_test_secret",
766
+ body: {
767
+ sourceKind: "nextjs",
768
+ files: [{ path: "server.js", contentBase64: "Y29uc29sZS5sb2coJ2hpJykK" }],
769
+ },
770
+ },
771
+ { method: "GET", path: "/api/artifacts/artifact_1/custom-domains", apiKey: "art_test_secret" },
772
+ { method: "POST", path: "/api/artifacts/artifact_1/custom-domains", apiKey: "art_test_secret", body: { hostname: "app.example.com" } },
773
+ { method: "DELETE", path: "/api/artifacts/artifact_1/custom-domains?hostname=app.example.com", apiKey: "art_test_secret" },
774
+ ]);
775
+ rmSync(dir, { recursive: true, force: true });
776
+ });
642
777
  });
@@ -204,6 +204,47 @@ function numberArg(args: Record<string, unknown> | undefined, key: string): numb
204
204
  return undefined;
205
205
  }
206
206
 
207
+ function appBuildPayload(args: Record<string, unknown> | undefined) {
208
+ const sourceKind = stringArg(args, "sourceKind");
209
+ const tag = stringArg(args, "tag");
210
+ const port = numberArg(args, "port");
211
+ return {
212
+ ...(sourceKind ? { sourceKind } : {}),
213
+ ...(tag ? { tag } : {}),
214
+ ...(port !== undefined ? { port } : {}),
215
+ };
216
+ }
217
+
218
+ function appBundleFilesArg(args: Record<string, unknown> | undefined) {
219
+ const value = args?.files;
220
+ if (!Array.isArray(value) || value.length === 0) {
221
+ throw new Error("Missing required argument: files");
222
+ }
223
+ return value.map((item) => {
224
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
225
+ throw new Error("Each bundle file must be an object.");
226
+ }
227
+ const file = item as Record<string, unknown>;
228
+ const path = typeof file.path === "string" && file.path.trim() ? file.path.trim() : "";
229
+ if (!path) {
230
+ throw new Error("Each bundle file requires a path.");
231
+ }
232
+ const content = typeof file.content === "string" ? file.content : undefined;
233
+ const contentBase64 = typeof file.contentBase64 === "string" ? file.contentBase64 : undefined;
234
+ const filePath = typeof file.filePath === "string" && file.filePath.trim() ? file.filePath.trim() : undefined;
235
+ if (filePath) {
236
+ return { path, contentBase64: readFileForPublish(filePath).toString("base64") };
237
+ }
238
+ if (content !== undefined) {
239
+ return { path, content };
240
+ }
241
+ if (contentBase64 !== undefined) {
242
+ return { path, contentBase64 };
243
+ }
244
+ throw new Error(`Bundle file ${path} must include content, contentBase64, or filePath.`);
245
+ });
246
+ }
247
+
207
248
  export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
208
249
  return [
209
250
  {
@@ -319,6 +360,79 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
319
360
  required: ["path"],
320
361
  },
321
362
  },
363
+ {
364
+ name: "artifactly_list_app_env",
365
+ description: "List redacted environment variables for an Artifactly web_app artifact.",
366
+ inputSchema: { type: "object", properties: { artifactId: { type: "string" } }, required: ["artifactId"] },
367
+ },
368
+ {
369
+ name: "artifactly_set_app_env",
370
+ description: "Create or update a web_app environment variable. Secret values are never returned by Artifactly.",
371
+ inputSchema: {
372
+ type: "object",
373
+ properties: {
374
+ artifactId: { type: "string" },
375
+ name: { type: "string" },
376
+ value: { type: "string" },
377
+ isSecret: { type: "boolean" },
378
+ },
379
+ required: ["artifactId", "name", "value"],
380
+ },
381
+ },
382
+ {
383
+ name: "artifactly_delete_app_env",
384
+ description: "Delete a web_app environment variable.",
385
+ inputSchema: { type: "object", properties: { artifactId: { type: "string" }, name: { type: "string" } }, required: ["artifactId", "name"] },
386
+ },
387
+ {
388
+ name: "artifactly_publish_app_bundle",
389
+ description: "Materialize source files for a web_app artifact and enqueue a background image build job. Files can include inline content, contentBase64, or local filePath values; local files larger than 10 MB are rejected before upload.",
390
+ inputSchema: {
391
+ type: "object",
392
+ properties: {
393
+ artifactId: { type: "string" },
394
+ sourceKind: { type: "string", enum: ["nextjs", "dockerfile"] },
395
+ tag: { type: "string" },
396
+ port: { type: "number" },
397
+ files: { type: "array", items: { type: "object" } },
398
+ },
399
+ required: ["artifactId", "files"],
400
+ },
401
+ },
402
+ {
403
+ name: "artifactly_enqueue_app_build",
404
+ description: "Queue a persistent background image build job for an already-materialized web_app source directory.",
405
+ inputSchema: {
406
+ type: "object",
407
+ properties: { artifactId: { type: "string" }, sourceKind: { type: "string", enum: ["nextjs", "dockerfile"] }, tag: { type: "string" }, port: { type: "number" } },
408
+ required: ["artifactId"],
409
+ },
410
+ },
411
+ {
412
+ name: "artifactly_get_app_build_status",
413
+ description: "Get a background app image build job status.",
414
+ inputSchema: { type: "object", properties: { artifactId: { type: "string" }, jobId: { type: "string" } }, required: ["artifactId", "jobId"] },
415
+ },
416
+ {
417
+ name: "artifactly_deploy_app",
418
+ description: "Deploy an already-built app image reference for a web_app artifact.",
419
+ inputSchema: { type: "object", properties: { artifactId: { type: "string" }, imageRef: { type: "string" }, port: { type: "number" } }, required: ["artifactId", "imageRef"] },
420
+ },
421
+ {
422
+ name: "artifactly_list_custom_domains",
423
+ description: "List custom domains and statuses for a web_app artifact.",
424
+ inputSchema: { type: "object", properties: { artifactId: { type: "string" } }, required: ["artifactId"] },
425
+ },
426
+ {
427
+ name: "artifactly_add_custom_domain",
428
+ description: "Add or refresh a custom domain for a web_app artifact and return DNS instructions/status.",
429
+ inputSchema: { type: "object", properties: { artifactId: { type: "string" }, hostname: { type: "string" } }, required: ["artifactId", "hostname"] },
430
+ },
431
+ {
432
+ name: "artifactly_delete_custom_domain",
433
+ description: "Delete a custom domain from a web_app artifact.",
434
+ inputSchema: { type: "object", properties: { artifactId: { type: "string" }, hostname: { type: "string" } }, required: ["artifactId", "hostname"] },
435
+ },
322
436
  ];
323
437
  }
324
438
 
@@ -539,5 +653,75 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
539
653
  );
540
654
  }
541
655
 
656
+ if (input.name === "artifactly_list_app_env") {
657
+ if (!credentials) throw new Error(LOGIN_GUIDANCE);
658
+ const artifactId = requireStringArg(input.arguments, "artifactId");
659
+ return objectResult(await requestJson("GET", `/api/artifacts/${artifactId}/app/env`, undefined, { apiKey: credentials.apiKey }));
660
+ }
661
+
662
+ if (input.name === "artifactly_set_app_env") {
663
+ if (!credentials) throw new Error(LOGIN_GUIDANCE);
664
+ const artifactId = requireStringArg(input.arguments, "artifactId");
665
+ const name = requireStringArg(input.arguments, "name");
666
+ const value = requireRawStringArg(input.arguments, "value");
667
+ const isSecret = booleanArg(input.arguments, "isSecret");
668
+ return objectResult(await requestJson("POST", `/api/artifacts/${artifactId}/app/env`, { name, value, isSecret }, { apiKey: credentials.apiKey }));
669
+ }
670
+
671
+ if (input.name === "artifactly_delete_app_env") {
672
+ if (!credentials) throw new Error(LOGIN_GUIDANCE);
673
+ const artifactId = requireStringArg(input.arguments, "artifactId");
674
+ const name = requireStringArg(input.arguments, "name");
675
+ return objectResult(await requestJson("DELETE", `/api/artifacts/${artifactId}/app/env${queryString({ name })}`, undefined, { apiKey: credentials.apiKey }));
676
+ }
677
+
678
+ if (input.name === "artifactly_publish_app_bundle") {
679
+ if (!credentials) throw new Error(LOGIN_GUIDANCE);
680
+ const artifactId = requireStringArg(input.arguments, "artifactId");
681
+ const files = appBundleFilesArg(input.arguments);
682
+ return objectResult(await requestJson("POST", `/api/artifacts/${artifactId}/app/publish`, { ...appBuildPayload(input.arguments), files }, { apiKey: credentials.apiKey }));
683
+ }
684
+
685
+ if (input.name === "artifactly_enqueue_app_build") {
686
+ if (!credentials) throw new Error(LOGIN_GUIDANCE);
687
+ const artifactId = requireStringArg(input.arguments, "artifactId");
688
+ return objectResult(await requestJson("POST", `/api/artifacts/${artifactId}/app/build-jobs`, appBuildPayload(input.arguments), { apiKey: credentials.apiKey }));
689
+ }
690
+
691
+ if (input.name === "artifactly_get_app_build_status") {
692
+ if (!credentials) throw new Error(LOGIN_GUIDANCE);
693
+ const artifactId = requireStringArg(input.arguments, "artifactId");
694
+ const jobId = requireStringArg(input.arguments, "jobId");
695
+ return objectResult(await requestJson("GET", `/api/artifacts/${artifactId}/app/build-jobs/${jobId}`, undefined, { apiKey: credentials.apiKey }));
696
+ }
697
+
698
+ if (input.name === "artifactly_deploy_app") {
699
+ if (!credentials) throw new Error(LOGIN_GUIDANCE);
700
+ const artifactId = requireStringArg(input.arguments, "artifactId");
701
+ const imageRef = requireStringArg(input.arguments, "imageRef");
702
+ const port = numberArg(input.arguments, "port");
703
+ return objectResult(await requestJson("POST", `/api/artifacts/${artifactId}/app/deploy`, { imageRef, ...(port !== undefined ? { port } : {}) }, { apiKey: credentials.apiKey }));
704
+ }
705
+
706
+ if (input.name === "artifactly_list_custom_domains") {
707
+ if (!credentials) throw new Error(LOGIN_GUIDANCE);
708
+ const artifactId = requireStringArg(input.arguments, "artifactId");
709
+ return objectResult(await requestJson("GET", `/api/artifacts/${artifactId}/custom-domains`, undefined, { apiKey: credentials.apiKey }));
710
+ }
711
+
712
+ if (input.name === "artifactly_add_custom_domain") {
713
+ if (!credentials) throw new Error(LOGIN_GUIDANCE);
714
+ const artifactId = requireStringArg(input.arguments, "artifactId");
715
+ const hostname = requireStringArg(input.arguments, "hostname");
716
+ return objectResult(await requestJson("POST", `/api/artifacts/${artifactId}/custom-domains`, { hostname }, { apiKey: credentials.apiKey }));
717
+ }
718
+
719
+ if (input.name === "artifactly_delete_custom_domain") {
720
+ if (!credentials) throw new Error(LOGIN_GUIDANCE);
721
+ const artifactId = requireStringArg(input.arguments, "artifactId");
722
+ const hostname = requireStringArg(input.arguments, "hostname");
723
+ return objectResult(await requestJson("DELETE", `/api/artifacts/${artifactId}/custom-domains${queryString({ hostname })}`, undefined, { apiKey: credentials.apiKey }));
724
+ }
725
+
542
726
  throw new Error(`Unknown Artifactly MCP tool: ${input.name}`);
543
727
  }