@artifactly/mcp 0.1.4 → 0.1.6

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
@@ -23,6 +23,18 @@ npm run build
23
23
 
24
24
  The MCP package is published as `@artifactly/mcp`.
25
25
 
26
+ Codex and other MCP clients can launch the server with either of these forms:
27
+
28
+ ```bash
29
+ npx -y @artifactly/mcp
30
+ ```
31
+
32
+ ```bash
33
+ npx -y --package=@artifactly/mcp artifactly-mcp
34
+ ```
35
+
36
+ The package also exposes a human CLI as `artifactly`.
37
+
26
38
  Manual publish from a logged-in maintainer account:
27
39
 
28
40
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artifactly/mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "files": [
6
6
  "bin",
@@ -10,6 +10,7 @@
10
10
  ],
11
11
  "bin": {
12
12
  "artifactly": "bin/artifactly.mjs",
13
+ "mcp": "bin/artifactly-mcp.mjs",
13
14
  "artifactly-mcp": "bin/artifactly-mcp.mjs"
14
15
  },
15
16
  "scripts": {
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, rmSync } from "node:fs";
1
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -82,6 +82,26 @@ describe("artifactly CLI", () => {
82
82
  rmSync(dir, { recursive: true, force: true });
83
83
  });
84
84
 
85
+ it("logs out by removing stored credentials", async () => {
86
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-cli-"));
87
+ const credentialPath = join(dir, "credentials.json");
88
+ const output: string[] = [];
89
+ const cli = createArtifactlyCli({
90
+ credentialPath,
91
+ stdout: (line) => output.push(line),
92
+ });
93
+
94
+ await cli.run(["login", "--api-key", "art_test_secret", "--email", "agent@example.com"]);
95
+ expect(existsSync(credentialPath)).toBe(true);
96
+
97
+ await cli.run(["logout"]);
98
+
99
+ expect(existsSync(credentialPath)).toBe(false);
100
+ expect(readCredentials(credentialPath)).toBeNull();
101
+ expect(output.at(-1)).toBe("Logged out of Artifactly.");
102
+ rmSync(dir, { recursive: true, force: true });
103
+ });
104
+
85
105
  it("reports the default credential path under the user home", () => {
86
106
  expect(getDefaultCredentialPath("/Users/alice")).toBe(
87
107
  "/Users/alice/.artifactly/credentials.json",
@@ -1,4 +1,4 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
@@ -73,6 +73,10 @@ export function writeCredentials(
73
73
  writeFileSync(path, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 0o600 });
74
74
  }
75
75
 
76
+ export function clearCredentials(path = getDefaultCredentialPath()) {
77
+ rmSync(path, { force: true });
78
+ }
79
+
76
80
  async function defaultPrompt(question: string) {
77
81
  const readline = createInterface({ input, output });
78
82
  try {
@@ -166,6 +170,11 @@ export function createArtifactlyCli(options: ArtifactlyCliOptions = {}) {
166
170
  stdout(`Workspace: ${credentials.workspace ?? "unknown"}`);
167
171
  }
168
172
 
173
+ function logout() {
174
+ clearCredentials(credentialPath);
175
+ stdout("Logged out of Artifactly.");
176
+ }
177
+
169
178
  async function publish(argv: string[]) {
170
179
  const credentials = readCredentials(credentialPath);
171
180
  if (!credentials) {
@@ -206,10 +215,11 @@ export function createArtifactlyCli(options: ArtifactlyCliOptions = {}) {
206
215
  const [command, ...rest] = argv;
207
216
  try {
208
217
  if (command === "login") return await login(rest);
218
+ if (command === "logout") return logout();
209
219
  if (command === "whoami") return whoami();
210
220
  if (command === "publish") return await publish(rest);
211
221
  if (command === "mcp") return await mcp(rest);
212
- stdout("Usage: artifactly <login|whoami|publish|mcp>");
222
+ stdout("Usage: artifactly <login|logout|whoami|publish|mcp>");
213
223
  } catch (error) {
214
224
  const message = error instanceof Error ? error.message : String(error);
215
225
  stderr(message);
@@ -11,6 +11,7 @@ describe("Artifactly MCP server packaging", () => {
11
11
  it("exposes installable CLI bins for humans and MCP clients", () => {
12
12
  expect(packageJson.bin).toEqual({
13
13
  artifactly: "bin/artifactly.mjs",
14
+ mcp: "bin/artifactly-mcp.mjs",
14
15
  "artifactly-mcp": "bin/artifactly-mcp.mjs",
15
16
  });
16
17
  expect(packageJson.dependencies).toHaveProperty("@modelcontextprotocol/sdk");
@@ -41,9 +42,13 @@ describe("Artifactly MCP stdio server helpers", () => {
41
42
  "artifactly_auth_status",
42
43
  "artifactly_request_login_code",
43
44
  "artifactly_verify_login_code",
45
+ "artifactly_logout",
44
46
  "artifactly_publish_markdown",
45
47
  "artifactly_list_artifacts",
46
48
  "artifactly_get_artifact",
49
+ "artifactly_update_artifact",
50
+ "artifactly_delete_artifact",
51
+ "artifactly_publish_file",
47
52
  ]);
48
53
  });
49
54
 
@@ -92,6 +92,7 @@ const verifyLoginCodeSchema = z.object({
92
92
  email: z.string().email(),
93
93
  code: z.string().min(4),
94
94
  });
95
+ const logoutSchema = z.object({});
95
96
  const publishMarkdownSchema = z.object({
96
97
  title: z.string().min(1),
97
98
  markdown: z.string().min(1),
@@ -101,14 +102,36 @@ const listArtifactsSchema = z.object({});
101
102
  const getArtifactSchema = z.object({
102
103
  artifactId: z.string().min(1),
103
104
  });
105
+ const updateArtifactSchema = z.object({
106
+ artifactId: z.string().min(1),
107
+ title: z.string().min(1).optional(),
108
+ markdown: z.string().min(1).optional(),
109
+ content: z.string().min(1).optional(),
110
+ description: z.string().min(1).optional(),
111
+ visibility: z.enum(["public", "unlisted", "private", "gated"]).optional(),
112
+ accessList: z.array(z.string().email()).optional(),
113
+ });
114
+ const deleteArtifactSchema = z.object({
115
+ artifactId: z.string().min(1),
116
+ });
117
+ const publishFileSchema = z.object({
118
+ path: z.string().min(1),
119
+ title: z.string().min(1).optional(),
120
+ mimeType: z.string().min(1).optional(),
121
+ visibility: z.enum(["public", "unlisted", "private", "gated"]).optional(),
122
+ });
104
123
 
105
124
  const toolSchemas = {
106
125
  artifactly_auth_status: authStatusSchema,
107
126
  artifactly_request_login_code: requestLoginCodeSchema,
108
127
  artifactly_verify_login_code: verifyLoginCodeSchema,
128
+ artifactly_logout: logoutSchema,
109
129
  artifactly_publish_markdown: publishMarkdownSchema,
110
130
  artifactly_list_artifacts: listArtifactsSchema,
111
131
  artifactly_get_artifact: getArtifactSchema,
132
+ artifactly_update_artifact: updateArtifactSchema,
133
+ artifactly_delete_artifact: deleteArtifactSchema,
134
+ artifactly_publish_file: publishFileSchema,
112
135
  } as const;
113
136
 
114
137
  export function createArtifactlyMcpServer() {
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, rmSync } from "node:fs";
1
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
 
@@ -13,9 +13,13 @@ describe("Artifactly MCP tools", () => {
13
13
  "artifactly_auth_status",
14
14
  "artifactly_request_login_code",
15
15
  "artifactly_verify_login_code",
16
+ "artifactly_logout",
16
17
  "artifactly_publish_markdown",
17
18
  "artifactly_list_artifacts",
18
19
  "artifactly_get_artifact",
20
+ "artifactly_update_artifact",
21
+ "artifactly_delete_artifact",
22
+ "artifactly_publish_file",
19
23
  ]);
20
24
  });
21
25
 
@@ -44,6 +48,30 @@ describe("Artifactly MCP tools", () => {
44
48
  rmSync(dir, { recursive: true, force: true });
45
49
  });
46
50
 
51
+ it("logs out by clearing the shared credential store", async () => {
52
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
53
+ const credentialPath = join(dir, "credentials.json");
54
+ writeCredentials(
55
+ {
56
+ apiBaseUrl: "https://artifactly.example",
57
+ apiKey: "art_t...et",
58
+ email: "agent@example.com",
59
+ workspace: "agent",
60
+ },
61
+ credentialPath,
62
+ );
63
+
64
+ await expect(
65
+ callArtifactlyMcpTool({ name: "artifactly_logout", credentialPath }),
66
+ ).resolves.toEqual({ ok: true, authenticated: false });
67
+ expect(existsSync(credentialPath)).toBe(false);
68
+ await expect(
69
+ callArtifactlyMcpTool({ name: "artifactly_auth_status", credentialPath }),
70
+ ).resolves.toEqual({ authenticated: false, apiBaseUrl: "https://artifactly.io" });
71
+
72
+ rmSync(dir, { recursive: true, force: true });
73
+ });
74
+
47
75
  it("publishes markdown through the Artifactly API without returning secrets", async () => {
48
76
  const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
49
77
  const credentialPath = join(dir, "credentials.json");
@@ -88,4 +116,140 @@ describe("Artifactly MCP tools", () => {
88
116
 
89
117
  rmSync(dir, { recursive: true, force: true });
90
118
  });
119
+
120
+ it("lists, gets, updates, and deletes artifacts through authenticated API requests", async () => {
121
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
122
+ const credentialPath = join(dir, "credentials.json");
123
+ const requests: Array<{ method: string; path: string; body?: unknown; apiKey?: string }> = [];
124
+ writeCredentials(
125
+ {
126
+ apiBaseUrl: "https://artifactly.example",
127
+ apiKey: "art_t...et",
128
+ email: "agent@example.com",
129
+ workspace: "agent",
130
+ },
131
+ credentialPath,
132
+ );
133
+
134
+ const requestJson = async (method: string, path: string, body?: unknown, options?: { apiKey?: string }) => {
135
+ requests.push({ method, path, body, apiKey: options?.apiKey });
136
+ if (method === "GET" && path === "/api/artifacts") {
137
+ return { artifacts: [{ id: "artifact_1", title: "Hello" }] };
138
+ }
139
+ if (method === "GET" && path === "/api/artifacts/artifact_1") {
140
+ return { artifact: { id: "artifact_1", title: "Hello" } };
141
+ }
142
+ if (method === "PATCH" && path === "/api/artifacts/artifact_1") {
143
+ return { artifact: { id: "artifact_1", title: "Updated", visibility: "private" } };
144
+ }
145
+ return { ok: true, artifactId: "artifact_1" };
146
+ };
147
+
148
+ await expect(
149
+ callArtifactlyMcpTool({ name: "artifactly_list_artifacts", credentialPath, requestJson }),
150
+ ).resolves.toEqual({ artifacts: [{ id: "artifact_1", title: "Hello" }] });
151
+ await expect(
152
+ callArtifactlyMcpTool({
153
+ name: "artifactly_get_artifact",
154
+ credentialPath,
155
+ arguments: { artifactId: "artifact_1" },
156
+ requestJson,
157
+ }),
158
+ ).resolves.toEqual({ artifact: { id: "artifact_1", title: "Hello" } });
159
+ await expect(
160
+ callArtifactlyMcpTool({
161
+ name: "artifactly_update_artifact",
162
+ credentialPath,
163
+ arguments: {
164
+ artifactId: "artifact_1",
165
+ title: "Updated",
166
+ visibility: "private",
167
+ accessList: ["agent@example.com"],
168
+ },
169
+ requestJson,
170
+ }),
171
+ ).resolves.toEqual({ artifact: { id: "artifact_1", title: "Updated", visibility: "private" } });
172
+ await expect(
173
+ callArtifactlyMcpTool({
174
+ name: "artifactly_delete_artifact",
175
+ credentialPath,
176
+ arguments: { artifactId: "artifact_1" },
177
+ requestJson,
178
+ }),
179
+ ).resolves.toEqual({ ok: true, artifactId: "artifact_1" });
180
+
181
+ expect(requests).toEqual([
182
+ { method: "GET", path: "/api/artifacts", apiKey: "art_t...et", body: undefined },
183
+ { method: "GET", path: "/api/artifacts/artifact_1", apiKey: "art_t...et", body: undefined },
184
+ {
185
+ method: "PATCH",
186
+ path: "/api/artifacts/artifact_1",
187
+ apiKey: "art_t...et",
188
+ body: { title: "Updated", visibility: "private", accessList: ["agent@example.com"] },
189
+ },
190
+ { method: "DELETE", path: "/api/artifacts/artifact_1", apiKey: "art_t...et", body: undefined },
191
+ ]);
192
+ rmSync(dir, { recursive: true, force: true });
193
+ });
194
+
195
+ it("publishes local HTML and PDF files through the artifact API", async () => {
196
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
197
+ const credentialPath = join(dir, "credentials.json");
198
+ const htmlPath = join(dir, "hello.html");
199
+ const pdfPath = join(dir, "hello.pdf");
200
+ const requests: Array<{ path: string; body: unknown; apiKey?: string }> = [];
201
+ writeFileSync(htmlPath, "<h1>Hello</h1>");
202
+ writeFileSync(pdfPath, Buffer.from("%PDF-1.4\nhello"));
203
+ writeCredentials(
204
+ {
205
+ apiBaseUrl: "https://artifactly.example",
206
+ apiKey: "art_t...et",
207
+ email: "agent@example.com",
208
+ workspace: "agent",
209
+ },
210
+ credentialPath,
211
+ );
212
+
213
+ const postJson = async (path: string, body: unknown, options?: { apiKey?: string }) => {
214
+ requests.push({ path, body, apiKey: options?.apiKey });
215
+ return { id: "artifact_1", url: "https://artifactly.example/share/agent/hello" };
216
+ };
217
+
218
+ await callArtifactlyMcpTool({
219
+ name: "artifactly_publish_file",
220
+ credentialPath,
221
+ arguments: { path: htmlPath, visibility: "public" },
222
+ postJson,
223
+ });
224
+ await callArtifactlyMcpTool({
225
+ name: "artifactly_publish_file",
226
+ credentialPath,
227
+ arguments: { path: pdfPath, title: "PDF Hello", mimeType: "application/pdf" },
228
+ postJson,
229
+ });
230
+
231
+ expect(requests).toEqual([
232
+ {
233
+ path: "/api/artifacts",
234
+ apiKey: "art_t...et",
235
+ body: {
236
+ title: "hello",
237
+ kind: "html",
238
+ content: "<h1>Hello</h1>",
239
+ visibility: "public",
240
+ },
241
+ },
242
+ {
243
+ path: "/api/artifacts",
244
+ apiKey: "art_t...et",
245
+ body: {
246
+ title: "PDF Hello",
247
+ kind: "file",
248
+ storageUrl: "data:application/pdf;base64,JVBERi0xLjQKaGVsbG8=",
249
+ visibility: "unlisted",
250
+ },
251
+ },
252
+ ]);
253
+ rmSync(dir, { recursive: true, force: true });
254
+ });
91
255
  });
@@ -1,4 +1,8 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename, extname } from "node:path";
3
+
1
4
  import {
5
+ clearCredentials,
2
6
  createDefaultPostJson,
3
7
  getDefaultCredentialPath,
4
8
  readCredentials,
@@ -18,8 +22,41 @@ type CallToolInput = {
18
22
  credentialPath?: string;
19
23
  apiBaseUrl?: string;
20
24
  postJson?: PostJson;
25
+ requestJson?: RequestJson;
21
26
  };
22
27
 
28
+ type RequestJson = (
29
+ method: string,
30
+ path: string,
31
+ body?: unknown,
32
+ options?: { apiKey?: string },
33
+ ) => Promise<unknown>;
34
+
35
+ function createDefaultRequestJson(apiBaseUrl: string): RequestJson {
36
+ return async (method, path, body, options) => {
37
+ const response = await fetch(`${apiBaseUrl}${path}`, {
38
+ method,
39
+ headers: {
40
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
41
+ ...(options?.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {}),
42
+ },
43
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
44
+ });
45
+ const payload = (await response.json().catch(() => ({}))) as unknown;
46
+ if (!response.ok) {
47
+ const message =
48
+ typeof payload === "object" &&
49
+ payload !== null &&
50
+ "error" in payload &&
51
+ typeof payload.error === "string"
52
+ ? payload.error
53
+ : `Artifactly API request failed with ${response.status}`;
54
+ throw new Error(message);
55
+ }
56
+ return payload;
57
+ };
58
+ }
59
+
23
60
  function stringArg(
24
61
  args: Record<string, unknown> | undefined,
25
62
  key: string,
@@ -39,6 +76,17 @@ function requireStringArg(
39
76
  return value;
40
77
  }
41
78
 
79
+ function stringArrayArg(
80
+ args: Record<string, unknown> | undefined,
81
+ key: string,
82
+ ): string[] | undefined {
83
+ const value = args?.[key];
84
+ if (!Array.isArray(value)) {
85
+ return undefined;
86
+ }
87
+ return value.filter((item): item is string => typeof item === "string");
88
+ }
89
+
42
90
  function objectResult(value: unknown): Record<string, unknown> {
43
91
  if (!value || typeof value !== "object" || Array.isArray(value)) {
44
92
  throw new Error("Artifactly API returned an unexpected response.");
@@ -46,6 +94,30 @@ function objectResult(value: unknown): Record<string, unknown> {
46
94
  return value as Record<string, unknown>;
47
95
  }
48
96
 
97
+ function titleFromPath(path: string): string {
98
+ const name = basename(path, extname(path));
99
+ return name || "artifact";
100
+ }
101
+
102
+ function inferMimeType(path: string): string {
103
+ const extension = extname(path).toLowerCase();
104
+ if (extension === ".pdf") return "application/pdf";
105
+ if (extension === ".html" || extension === ".htm") return "text/html";
106
+ if (extension === ".json") return "application/json";
107
+ if (extension === ".md" || extension === ".markdown") return "text/markdown";
108
+ if (extension === ".txt") return "text/plain";
109
+ return "application/octet-stream";
110
+ }
111
+
112
+ function kindForMime(path: string, mimeType: string) {
113
+ const extension = extname(path).toLowerCase();
114
+ const normalized = mimeType.toLowerCase();
115
+ if (normalized.includes("html") || extension === ".html" || extension === ".htm") return "html";
116
+ if (normalized.includes("json") || extension === ".json") return "json";
117
+ if (normalized.includes("markdown") || extension === ".md" || extension === ".markdown") return "markdown";
118
+ return "file";
119
+ }
120
+
49
121
  export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
50
122
  return [
51
123
  {
@@ -72,6 +144,11 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
72
144
  required: ["email", "code"],
73
145
  },
74
146
  },
147
+ {
148
+ name: "artifactly_logout",
149
+ description: "Log out by clearing the local Artifactly credential store.",
150
+ inputSchema: { type: "object", properties: {} },
151
+ },
75
152
  {
76
153
  name: "artifactly_publish_markdown",
77
154
  description: "Publish markdown through the Artifactly API using stored credentials.",
@@ -99,6 +176,46 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
99
176
  required: ["artifactId"],
100
177
  },
101
178
  },
179
+ {
180
+ name: "artifactly_update_artifact",
181
+ description: "Update an existing Artifactly artifact, including visibility and gated access emails.",
182
+ inputSchema: {
183
+ type: "object",
184
+ properties: {
185
+ artifactId: { type: "string" },
186
+ title: { type: "string" },
187
+ markdown: { type: "string" },
188
+ content: { type: "string" },
189
+ description: { type: "string" },
190
+ visibility: { type: "string", enum: ["public", "unlisted", "private", "gated"] },
191
+ accessList: { type: "array", items: { type: "string" } },
192
+ },
193
+ required: ["artifactId"],
194
+ },
195
+ },
196
+ {
197
+ name: "artifactly_delete_artifact",
198
+ description: "Delete an existing Artifactly artifact owned by the authenticated account.",
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: { artifactId: { type: "string" } },
202
+ required: ["artifactId"],
203
+ },
204
+ },
205
+ {
206
+ name: "artifactly_publish_file",
207
+ description: "Publish a local PDF, HTML, JSON, Markdown, or other file through the Artifactly API.",
208
+ inputSchema: {
209
+ type: "object",
210
+ properties: {
211
+ path: { type: "string" },
212
+ title: { type: "string" },
213
+ mimeType: { type: "string" },
214
+ visibility: { type: "string", enum: ["public", "unlisted", "private", "gated"] },
215
+ },
216
+ required: ["path"],
217
+ },
218
+ },
102
219
  ];
103
220
  }
104
221
 
@@ -107,6 +224,7 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
107
224
  const credentials = readCredentials(credentialPath);
108
225
  const apiBaseUrl = input.apiBaseUrl ?? credentials?.apiBaseUrl ?? "https://artifactly.io";
109
226
  const postJson = input.postJson ?? createDefaultPostJson(apiBaseUrl);
227
+ const requestJson = input.requestJson ?? createDefaultRequestJson(apiBaseUrl);
110
228
 
111
229
  if (input.name === "artifactly_auth_status") {
112
230
  return credentials
@@ -155,6 +273,11 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
155
273
  };
156
274
  }
157
275
 
276
+ if (input.name === "artifactly_logout") {
277
+ clearCredentials(credentialPath);
278
+ return { ok: true, authenticated: false };
279
+ }
280
+
158
281
  if (input.name === "artifactly_publish_markdown") {
159
282
  if (!credentials) {
160
283
  throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
@@ -172,17 +295,83 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
172
295
  }
173
296
 
174
297
  if (input.name === "artifactly_list_artifacts") {
175
- return {
176
- error:
177
- "Artifact list API is not available yet. This MCP tool is reserved for the next API expansion.",
178
- };
298
+ if (!credentials) {
299
+ throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
300
+ }
301
+ return objectResult(await requestJson("GET", "/api/artifacts", undefined, { apiKey: credentials.apiKey }));
179
302
  }
180
303
 
181
304
  if (input.name === "artifactly_get_artifact") {
182
- return {
183
- error:
184
- "Artifact lookup API is not available yet. This MCP tool is reserved for the next API expansion.",
185
- };
305
+ if (!credentials) {
306
+ throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
307
+ }
308
+ const artifactId = requireStringArg(input.arguments, "artifactId");
309
+ return objectResult(await requestJson("GET", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
310
+ }
311
+
312
+ if (input.name === "artifactly_update_artifact") {
313
+ if (!credentials) {
314
+ throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
315
+ }
316
+ const artifactId = requireStringArg(input.arguments, "artifactId");
317
+ const title = stringArg(input.arguments, "title");
318
+ const markdown = stringArg(input.arguments, "markdown");
319
+ const content = stringArg(input.arguments, "content");
320
+ const description = stringArg(input.arguments, "description");
321
+ const visibility = stringArg(input.arguments, "visibility");
322
+ const accessList = stringArrayArg(input.arguments, "accessList");
323
+ return objectResult(
324
+ await requestJson(
325
+ "PATCH",
326
+ `/api/artifacts/${artifactId}`,
327
+ {
328
+ ...(title ? { title } : {}),
329
+ ...(markdown ? { markdown } : {}),
330
+ ...(content ? { content } : {}),
331
+ ...(description ? { description } : {}),
332
+ ...(visibility ? { visibility } : {}),
333
+ ...(accessList ? { accessList } : {}),
334
+ },
335
+ { apiKey: credentials.apiKey },
336
+ ),
337
+ );
338
+ }
339
+
340
+ if (input.name === "artifactly_delete_artifact") {
341
+ if (!credentials) {
342
+ throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
343
+ }
344
+ const artifactId = requireStringArg(input.arguments, "artifactId");
345
+ return objectResult(await requestJson("DELETE", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
346
+ }
347
+
348
+ if (input.name === "artifactly_publish_file") {
349
+ if (!credentials) {
350
+ throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
351
+ }
352
+ const path = requireStringArg(input.arguments, "path");
353
+ const title = stringArg(input.arguments, "title") ?? titleFromPath(path);
354
+ const mimeType = stringArg(input.arguments, "mimeType") ?? inferMimeType(path);
355
+ const visibility = stringArg(input.arguments, "visibility") ?? "unlisted";
356
+ const kind = kindForMime(path, mimeType);
357
+ const file = readFileSync(path);
358
+ const payload =
359
+ kind === "file"
360
+ ? {
361
+ title,
362
+ kind,
363
+ storageUrl: `data:${mimeType};base64,${file.toString("base64")}`,
364
+ visibility,
365
+ }
366
+ : {
367
+ title,
368
+ kind,
369
+ content: file.toString("utf8"),
370
+ visibility,
371
+ };
372
+ return objectResult(
373
+ await postJson("/api/artifacts", payload, { apiKey: credentials.apiKey }),
374
+ );
186
375
  }
187
376
 
188
377
  throw new Error(`Unknown Artifactly MCP tool: ${input.name}`);