@artifactly/mcp 0.1.5 → 0.1.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artifactly/mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "files": [
6
6
  "bin",
@@ -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);
@@ -42,9 +42,13 @@ describe("Artifactly MCP stdio server helpers", () => {
42
42
  "artifactly_auth_status",
43
43
  "artifactly_request_login_code",
44
44
  "artifactly_verify_login_code",
45
+ "artifactly_logout",
45
46
  "artifactly_publish_markdown",
46
47
  "artifactly_list_artifacts",
47
48
  "artifactly_get_artifact",
49
+ "artifactly_update_artifact",
50
+ "artifactly_delete_artifact",
51
+ "artifactly_publish_file",
48
52
  ]);
49
53
  });
50
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,72 @@ describe("Artifactly MCP tools", () => {
44
48
  rmSync(dir, { recursive: true, force: true });
45
49
  });
46
50
 
51
+ it("can validate auth status against the Artifactly API", async () => {
52
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
53
+ const credentialPath = join(dir, "credentials.json");
54
+ const requests: Array<{ method: string; path: string; apiKey?: string }> = [];
55
+ writeCredentials(
56
+ {
57
+ apiBaseUrl: "https://artifactly.example",
58
+ apiKey: "art_test_secret",
59
+ email: "agent@example.com",
60
+ workspace: "agent",
61
+ },
62
+ credentialPath,
63
+ );
64
+
65
+ await expect(
66
+ callArtifactlyMcpTool({
67
+ name: "artifactly_auth_status",
68
+ credentialPath,
69
+ arguments: { validate: true },
70
+ requestJson: async (method, path, _body, options) => {
71
+ requests.push({ method, path, apiKey: options?.apiKey });
72
+ return { artifacts: [] };
73
+ },
74
+ }),
75
+ ).resolves.toMatchObject({
76
+ authenticated: true,
77
+ serverAuthenticated: true,
78
+ validationError: null,
79
+ });
80
+ expect(requests).toEqual([
81
+ { method: "GET", path: "/api/artifacts", apiKey: "art_test_secret" },
82
+ ]);
83
+
84
+ rmSync(dir, { recursive: true, force: true });
85
+ });
86
+
87
+ it("gives complete login guidance when protected tools lack credentials", async () => {
88
+ await expect(
89
+ callArtifactlyMcpTool({ name: "artifactly_list_artifacts", credentialPath: "/tmp/nope.json" }),
90
+ ).rejects.toThrow(/artifactly_request_login_code[\s\S]*artifactly_verify_login_code/);
91
+ });
92
+
93
+ it("logs out by clearing the shared credential store", async () => {
94
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
95
+ const credentialPath = join(dir, "credentials.json");
96
+ writeCredentials(
97
+ {
98
+ apiBaseUrl: "https://artifactly.example",
99
+ apiKey: "art_t...et",
100
+ email: "agent@example.com",
101
+ workspace: "agent",
102
+ },
103
+ credentialPath,
104
+ );
105
+
106
+ await expect(
107
+ callArtifactlyMcpTool({ name: "artifactly_logout", credentialPath }),
108
+ ).resolves.toEqual({ ok: true, authenticated: false });
109
+ expect(existsSync(credentialPath)).toBe(false);
110
+ await expect(
111
+ callArtifactlyMcpTool({ name: "artifactly_auth_status", credentialPath }),
112
+ ).resolves.toEqual({ authenticated: false, apiBaseUrl: "https://artifactly.io" });
113
+
114
+ rmSync(dir, { recursive: true, force: true });
115
+ });
116
+
47
117
  it("publishes markdown through the Artifactly API without returning secrets", async () => {
48
118
  const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
49
119
  const credentialPath = join(dir, "credentials.json");
@@ -88,4 +158,164 @@ describe("Artifactly MCP tools", () => {
88
158
 
89
159
  rmSync(dir, { recursive: true, force: true });
90
160
  });
161
+
162
+ it("lists, gets, updates, and deletes artifacts through authenticated API requests", async () => {
163
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
164
+ const credentialPath = join(dir, "credentials.json");
165
+ const requests: Array<{ method: string; path: string; body?: unknown; apiKey?: string }> = [];
166
+ writeCredentials(
167
+ {
168
+ apiBaseUrl: "https://artifactly.example",
169
+ apiKey: "art_t...et",
170
+ email: "agent@example.com",
171
+ workspace: "agent",
172
+ },
173
+ credentialPath,
174
+ );
175
+
176
+ const requestJson = async (method: string, path: string, body?: unknown, options?: { apiKey?: string }) => {
177
+ requests.push({ method, path, body, apiKey: options?.apiKey });
178
+ if (method === "GET" && path === "/api/artifacts") {
179
+ return { artifacts: [{ id: "artifact_1", title: "Hello" }] };
180
+ }
181
+ if (method === "GET" && path === "/api/artifacts/artifact_1") {
182
+ return { artifact: { id: "artifact_1", title: "Hello" } };
183
+ }
184
+ if (method === "PATCH" && path === "/api/artifacts/artifact_1") {
185
+ return { artifact: { id: "artifact_1", title: "Updated", visibility: "private" } };
186
+ }
187
+ return { ok: true, artifactId: "artifact_1" };
188
+ };
189
+
190
+ await expect(
191
+ callArtifactlyMcpTool({ name: "artifactly_list_artifacts", credentialPath, requestJson }),
192
+ ).resolves.toEqual({ artifacts: [{ id: "artifact_1", title: "Hello" }] });
193
+ await expect(
194
+ callArtifactlyMcpTool({
195
+ name: "artifactly_get_artifact",
196
+ credentialPath,
197
+ arguments: { artifactId: "artifact_1" },
198
+ requestJson,
199
+ }),
200
+ ).resolves.toEqual({ artifact: { id: "artifact_1", title: "Hello" } });
201
+ await expect(
202
+ callArtifactlyMcpTool({
203
+ name: "artifactly_update_artifact",
204
+ credentialPath,
205
+ arguments: {
206
+ artifactId: "artifact_1",
207
+ title: "Updated",
208
+ visibility: "private",
209
+ accessList: ["agent@example.com"],
210
+ },
211
+ requestJson,
212
+ }),
213
+ ).resolves.toEqual({ artifact: { id: "artifact_1", title: "Updated", visibility: "private" } });
214
+ await expect(
215
+ callArtifactlyMcpTool({
216
+ name: "artifactly_delete_artifact",
217
+ credentialPath,
218
+ arguments: { artifactId: "artifact_1" },
219
+ requestJson,
220
+ }),
221
+ ).resolves.toEqual({ ok: true, artifactId: "artifact_1" });
222
+
223
+ expect(requests).toEqual([
224
+ { method: "GET", path: "/api/artifacts", apiKey: "art_t...et", body: undefined },
225
+ { method: "GET", path: "/api/artifacts/artifact_1", apiKey: "art_t...et", body: undefined },
226
+ {
227
+ method: "PATCH",
228
+ path: "/api/artifacts/artifact_1",
229
+ apiKey: "art_t...et",
230
+ body: { title: "Updated", visibility: "private", accessList: ["agent@example.com"] },
231
+ },
232
+ { method: "DELETE", path: "/api/artifacts/artifact_1", apiKey: "art_t...et", body: undefined },
233
+ ]);
234
+ rmSync(dir, { recursive: true, force: true });
235
+ });
236
+
237
+ it("publishes local HTML and PDF files through the artifact API", async () => {
238
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
239
+ const credentialPath = join(dir, "credentials.json");
240
+ const htmlPath = join(dir, "hello.html");
241
+ const pdfPath = join(dir, "hello.pdf");
242
+ const requests: Array<{ path: string; body: unknown; apiKey?: string }> = [];
243
+ writeFileSync(htmlPath, "<h1>Hello</h1>");
244
+ writeFileSync(pdfPath, Buffer.from("%PDF-1.4\nhello"));
245
+ writeCredentials(
246
+ {
247
+ apiBaseUrl: "https://artifactly.example",
248
+ apiKey: "art_t...et",
249
+ email: "agent@example.com",
250
+ workspace: "agent",
251
+ },
252
+ credentialPath,
253
+ );
254
+
255
+ const postJson = async (path: string, body: unknown, options?: { apiKey?: string }) => {
256
+ requests.push({ path, body, apiKey: options?.apiKey });
257
+ return { id: "artifact_1", url: "https://artifactly.example/share/agent/hello" };
258
+ };
259
+
260
+ await callArtifactlyMcpTool({
261
+ name: "artifactly_publish_file",
262
+ credentialPath,
263
+ arguments: { path: htmlPath, visibility: "public" },
264
+ postJson,
265
+ });
266
+ await callArtifactlyMcpTool({
267
+ name: "artifactly_publish_file",
268
+ credentialPath,
269
+ arguments: { path: pdfPath, title: "PDF Hello", mimeType: "application/pdf" },
270
+ postJson,
271
+ });
272
+
273
+ expect(requests).toEqual([
274
+ {
275
+ path: "/api/artifacts",
276
+ apiKey: "art_t...et",
277
+ body: {
278
+ title: "hello",
279
+ kind: "html",
280
+ content: "<h1>Hello</h1>",
281
+ visibility: "public",
282
+ },
283
+ },
284
+ {
285
+ path: "/api/artifacts",
286
+ apiKey: "art_t...et",
287
+ body: {
288
+ title: "PDF Hello",
289
+ kind: "file",
290
+ storageUrl: "data:application/pdf;base64,JVBERi0xLjQKaGVsbG8=",
291
+ visibility: "unlisted",
292
+ },
293
+ },
294
+ ]);
295
+ rmSync(dir, { recursive: true, force: true });
296
+ });
297
+
298
+ it("returns a friendly error when publishing a missing file", async () => {
299
+ const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
300
+ const credentialPath = join(dir, "credentials.json");
301
+ writeCredentials(
302
+ {
303
+ apiBaseUrl: "https://artifactly.example",
304
+ apiKey: "art_test_secret",
305
+ email: "agent@example.com",
306
+ workspace: "agent",
307
+ },
308
+ credentialPath,
309
+ );
310
+
311
+ await expect(
312
+ callArtifactlyMcpTool({
313
+ name: "artifactly_publish_file",
314
+ credentialPath,
315
+ arguments: { path: join(dir, "missing.pdf") },
316
+ }),
317
+ ).rejects.toThrow(/File not found.*missing\.pdf/);
318
+
319
+ rmSync(dir, { recursive: true, force: true });
320
+ });
91
321
  });
@@ -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,
@@ -28,6 +65,14 @@ function stringArg(
28
65
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
29
66
  }
30
67
 
68
+ function optionalStringArg(
69
+ args: Record<string, unknown> | undefined,
70
+ key: string,
71
+ ): string | undefined {
72
+ const value = args?.[key];
73
+ return typeof value === "string" ? value.trim() : undefined;
74
+ }
75
+
31
76
  function requireStringArg(
32
77
  args: Record<string, unknown> | undefined,
33
78
  key: string,
@@ -39,6 +84,17 @@ function requireStringArg(
39
84
  return value;
40
85
  }
41
86
 
87
+ function stringArrayArg(
88
+ args: Record<string, unknown> | undefined,
89
+ key: string,
90
+ ): string[] | undefined {
91
+ const value = args?.[key];
92
+ if (!Array.isArray(value)) {
93
+ return undefined;
94
+ }
95
+ return value.filter((item): item is string => typeof item === "string");
96
+ }
97
+
42
98
  function objectResult(value: unknown): Record<string, unknown> {
43
99
  if (!value || typeof value !== "object" || Array.isArray(value)) {
44
100
  throw new Error("Artifactly API returned an unexpected response.");
@@ -46,12 +102,58 @@ function objectResult(value: unknown): Record<string, unknown> {
46
102
  return value as Record<string, unknown>;
47
103
  }
48
104
 
105
+ const LOGIN_GUIDANCE =
106
+ "Artifactly credentials are not configured. Run artifactly_request_login_code first, then run artifactly_verify_login_code with the emailed code.";
107
+
108
+ function booleanArg(args: Record<string, unknown> | undefined, key: string): boolean {
109
+ return args?.[key] === true;
110
+ }
111
+
112
+ function readFileForPublish(path: string): Buffer {
113
+ try {
114
+ return readFileSync(path);
115
+ } catch (error) {
116
+ const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
117
+ if (code === "ENOENT") {
118
+ throw new Error(`File not found: ${path}`);
119
+ }
120
+ if (code === "EACCES" || code === "EPERM") {
121
+ throw new Error(`File is not readable: ${path}`);
122
+ }
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ function titleFromPath(path: string): string {
128
+ const name = basename(path, extname(path));
129
+ return name || "artifact";
130
+ }
131
+
132
+ function inferMimeType(path: string): string {
133
+ const extension = extname(path).toLowerCase();
134
+ if (extension === ".pdf") return "application/pdf";
135
+ if (extension === ".html" || extension === ".htm") return "text/html";
136
+ if (extension === ".json") return "application/json";
137
+ if (extension === ".md" || extension === ".markdown") return "text/markdown";
138
+ if (extension === ".txt") return "text/plain";
139
+ return "application/octet-stream";
140
+ }
141
+
142
+ function kindForMime(path: string, mimeType: string) {
143
+ const extension = extname(path).toLowerCase();
144
+ const normalized = mimeType.toLowerCase();
145
+ if (normalized.includes("html") || extension === ".html" || extension === ".htm") return "html";
146
+ if (normalized.includes("json") || extension === ".json") return "json";
147
+ if (normalized.includes("markdown") || extension === ".md" || extension === ".markdown") return "markdown";
148
+ return "file";
149
+ }
150
+
49
151
  export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
50
152
  return [
51
153
  {
52
154
  name: "artifactly_auth_status",
53
155
  description: "Check whether local Artifactly credentials are configured.",
54
- inputSchema: { type: "object", properties: {} },
156
+ inputSchema: { type: "object", properties: { validate: { type: "boolean" } } },
55
157
  },
56
158
  {
57
159
  name: "artifactly_request_login_code",
@@ -72,6 +174,11 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
72
174
  required: ["email", "code"],
73
175
  },
74
176
  },
177
+ {
178
+ name: "artifactly_logout",
179
+ description: "Log out by clearing the local Artifactly credential store.",
180
+ inputSchema: { type: "object", properties: {} },
181
+ },
75
182
  {
76
183
  name: "artifactly_publish_markdown",
77
184
  description: "Publish markdown through the Artifactly API using stored credentials.",
@@ -99,6 +206,46 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
99
206
  required: ["artifactId"],
100
207
  },
101
208
  },
209
+ {
210
+ name: "artifactly_update_artifact",
211
+ description: "Update an existing Artifactly artifact, including visibility and gated access emails.",
212
+ inputSchema: {
213
+ type: "object",
214
+ properties: {
215
+ artifactId: { type: "string" },
216
+ title: { type: "string" },
217
+ markdown: { type: "string" },
218
+ content: { type: "string" },
219
+ description: { type: "string" },
220
+ visibility: { type: "string", enum: ["public", "unlisted", "private", "gated"] },
221
+ accessList: { type: "array", items: { type: "string" } },
222
+ },
223
+ required: ["artifactId"],
224
+ },
225
+ },
226
+ {
227
+ name: "artifactly_delete_artifact",
228
+ description: "Delete an existing Artifactly artifact owned by the authenticated account.",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: { artifactId: { type: "string" } },
232
+ required: ["artifactId"],
233
+ },
234
+ },
235
+ {
236
+ name: "artifactly_publish_file",
237
+ description: "Publish a local PDF, HTML, JSON, Markdown, or other file through the Artifactly API.",
238
+ inputSchema: {
239
+ type: "object",
240
+ properties: {
241
+ path: { type: "string" },
242
+ title: { type: "string" },
243
+ mimeType: { type: "string" },
244
+ visibility: { type: "string", enum: ["public", "unlisted", "private", "gated"] },
245
+ },
246
+ required: ["path"],
247
+ },
248
+ },
102
249
  ];
103
250
  }
104
251
 
@@ -107,16 +254,32 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
107
254
  const credentials = readCredentials(credentialPath);
108
255
  const apiBaseUrl = input.apiBaseUrl ?? credentials?.apiBaseUrl ?? "https://artifactly.io";
109
256
  const postJson = input.postJson ?? createDefaultPostJson(apiBaseUrl);
257
+ const requestJson = input.requestJson ?? createDefaultRequestJson(apiBaseUrl);
110
258
 
111
259
  if (input.name === "artifactly_auth_status") {
112
- return credentials
113
- ? {
114
- authenticated: true,
115
- apiBaseUrl: credentials.apiBaseUrl,
116
- email: credentials.email,
117
- workspace: credentials.workspace,
118
- }
119
- : { authenticated: false, apiBaseUrl };
260
+ if (!credentials) {
261
+ return { authenticated: false, apiBaseUrl };
262
+ }
263
+
264
+ const result: Record<string, unknown> = {
265
+ authenticated: true,
266
+ apiBaseUrl: credentials.apiBaseUrl,
267
+ email: credentials.email,
268
+ workspace: credentials.workspace,
269
+ };
270
+
271
+ if (booleanArg(input.arguments, "validate")) {
272
+ try {
273
+ await requestJson("GET", "/api/artifacts", undefined, { apiKey: credentials.apiKey });
274
+ result.serverAuthenticated = true;
275
+ result.validationError = null;
276
+ } catch (error) {
277
+ result.serverAuthenticated = false;
278
+ result.validationError = error instanceof Error ? error.message : "Validation failed";
279
+ }
280
+ }
281
+
282
+ return result;
120
283
  }
121
284
 
122
285
  if (input.name === "artifactly_request_login_code") {
@@ -155,9 +318,14 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
155
318
  };
156
319
  }
157
320
 
321
+ if (input.name === "artifactly_logout") {
322
+ clearCredentials(credentialPath);
323
+ return { ok: true, authenticated: false };
324
+ }
325
+
158
326
  if (input.name === "artifactly_publish_markdown") {
159
327
  if (!credentials) {
160
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
328
+ throw new Error(LOGIN_GUIDANCE);
161
329
  }
162
330
  const title = requireStringArg(input.arguments, "title");
163
331
  const content = requireStringArg(input.arguments, "markdown");
@@ -172,17 +340,83 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
172
340
  }
173
341
 
174
342
  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
- };
343
+ if (!credentials) {
344
+ throw new Error(LOGIN_GUIDANCE);
345
+ }
346
+ return objectResult(await requestJson("GET", "/api/artifacts", undefined, { apiKey: credentials.apiKey }));
179
347
  }
180
348
 
181
349
  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
- };
350
+ if (!credentials) {
351
+ throw new Error(LOGIN_GUIDANCE);
352
+ }
353
+ const artifactId = requireStringArg(input.arguments, "artifactId");
354
+ return objectResult(await requestJson("GET", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
355
+ }
356
+
357
+ if (input.name === "artifactly_update_artifact") {
358
+ if (!credentials) {
359
+ throw new Error(LOGIN_GUIDANCE);
360
+ }
361
+ const artifactId = requireStringArg(input.arguments, "artifactId");
362
+ const title = stringArg(input.arguments, "title");
363
+ const markdown = stringArg(input.arguments, "markdown");
364
+ const content = stringArg(input.arguments, "content");
365
+ const description = optionalStringArg(input.arguments, "description");
366
+ const visibility = stringArg(input.arguments, "visibility");
367
+ const accessList = stringArrayArg(input.arguments, "accessList");
368
+ return objectResult(
369
+ await requestJson(
370
+ "PATCH",
371
+ `/api/artifacts/${artifactId}`,
372
+ {
373
+ ...(title ? { title } : {}),
374
+ ...(markdown ? { markdown } : {}),
375
+ ...(content ? { content } : {}),
376
+ ...(description !== undefined ? { description } : {}),
377
+ ...(visibility ? { visibility } : {}),
378
+ ...(accessList ? { accessList } : {}),
379
+ },
380
+ { apiKey: credentials.apiKey },
381
+ ),
382
+ );
383
+ }
384
+
385
+ if (input.name === "artifactly_delete_artifact") {
386
+ if (!credentials) {
387
+ throw new Error(LOGIN_GUIDANCE);
388
+ }
389
+ const artifactId = requireStringArg(input.arguments, "artifactId");
390
+ return objectResult(await requestJson("DELETE", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
391
+ }
392
+
393
+ if (input.name === "artifactly_publish_file") {
394
+ if (!credentials) {
395
+ throw new Error(LOGIN_GUIDANCE);
396
+ }
397
+ const path = requireStringArg(input.arguments, "path");
398
+ const title = stringArg(input.arguments, "title") ?? titleFromPath(path);
399
+ const mimeType = stringArg(input.arguments, "mimeType") ?? inferMimeType(path);
400
+ const visibility = stringArg(input.arguments, "visibility") ?? "unlisted";
401
+ const kind = kindForMime(path, mimeType);
402
+ const file = readFileForPublish(path);
403
+ const payload =
404
+ kind === "file"
405
+ ? {
406
+ title,
407
+ kind,
408
+ storageUrl: `data:${mimeType};base64,${file.toString("base64")}`,
409
+ visibility,
410
+ }
411
+ : {
412
+ title,
413
+ kind,
414
+ content: file.toString("utf8"),
415
+ visibility,
416
+ };
417
+ return objectResult(
418
+ await postJson("/api/artifacts", payload, { apiKey: credentials.apiKey }),
419
+ );
186
420
  }
187
421
 
188
422
  throw new Error(`Unknown Artifactly MCP tool: ${input.name}`);