@artifactly/mcp 0.1.6 → 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.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "files": [
6
6
  "bin",
@@ -48,6 +48,48 @@ describe("Artifactly MCP tools", () => {
48
48
  rmSync(dir, { recursive: true, force: true });
49
49
  });
50
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
+
51
93
  it("logs out by clearing the shared credential store", async () => {
52
94
  const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
53
95
  const credentialPath = join(dir, "credentials.json");
@@ -252,4 +294,28 @@ describe("Artifactly MCP tools", () => {
252
294
  ]);
253
295
  rmSync(dir, { recursive: true, force: true });
254
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
+ });
255
321
  });
@@ -65,6 +65,14 @@ function stringArg(
65
65
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
66
66
  }
67
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
+
68
76
  function requireStringArg(
69
77
  args: Record<string, unknown> | undefined,
70
78
  key: string,
@@ -94,6 +102,28 @@ function objectResult(value: unknown): Record<string, unknown> {
94
102
  return value as Record<string, unknown>;
95
103
  }
96
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
+
97
127
  function titleFromPath(path: string): string {
98
128
  const name = basename(path, extname(path));
99
129
  return name || "artifact";
@@ -123,7 +153,7 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
123
153
  {
124
154
  name: "artifactly_auth_status",
125
155
  description: "Check whether local Artifactly credentials are configured.",
126
- inputSchema: { type: "object", properties: {} },
156
+ inputSchema: { type: "object", properties: { validate: { type: "boolean" } } },
127
157
  },
128
158
  {
129
159
  name: "artifactly_request_login_code",
@@ -227,14 +257,29 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
227
257
  const requestJson = input.requestJson ?? createDefaultRequestJson(apiBaseUrl);
228
258
 
229
259
  if (input.name === "artifactly_auth_status") {
230
- return credentials
231
- ? {
232
- authenticated: true,
233
- apiBaseUrl: credentials.apiBaseUrl,
234
- email: credentials.email,
235
- workspace: credentials.workspace,
236
- }
237
- : { 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;
238
283
  }
239
284
 
240
285
  if (input.name === "artifactly_request_login_code") {
@@ -280,7 +325,7 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
280
325
 
281
326
  if (input.name === "artifactly_publish_markdown") {
282
327
  if (!credentials) {
283
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
328
+ throw new Error(LOGIN_GUIDANCE);
284
329
  }
285
330
  const title = requireStringArg(input.arguments, "title");
286
331
  const content = requireStringArg(input.arguments, "markdown");
@@ -296,14 +341,14 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
296
341
 
297
342
  if (input.name === "artifactly_list_artifacts") {
298
343
  if (!credentials) {
299
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
344
+ throw new Error(LOGIN_GUIDANCE);
300
345
  }
301
346
  return objectResult(await requestJson("GET", "/api/artifacts", undefined, { apiKey: credentials.apiKey }));
302
347
  }
303
348
 
304
349
  if (input.name === "artifactly_get_artifact") {
305
350
  if (!credentials) {
306
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
351
+ throw new Error(LOGIN_GUIDANCE);
307
352
  }
308
353
  const artifactId = requireStringArg(input.arguments, "artifactId");
309
354
  return objectResult(await requestJson("GET", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
@@ -311,13 +356,13 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
311
356
 
312
357
  if (input.name === "artifactly_update_artifact") {
313
358
  if (!credentials) {
314
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
359
+ throw new Error(LOGIN_GUIDANCE);
315
360
  }
316
361
  const artifactId = requireStringArg(input.arguments, "artifactId");
317
362
  const title = stringArg(input.arguments, "title");
318
363
  const markdown = stringArg(input.arguments, "markdown");
319
364
  const content = stringArg(input.arguments, "content");
320
- const description = stringArg(input.arguments, "description");
365
+ const description = optionalStringArg(input.arguments, "description");
321
366
  const visibility = stringArg(input.arguments, "visibility");
322
367
  const accessList = stringArrayArg(input.arguments, "accessList");
323
368
  return objectResult(
@@ -328,7 +373,7 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
328
373
  ...(title ? { title } : {}),
329
374
  ...(markdown ? { markdown } : {}),
330
375
  ...(content ? { content } : {}),
331
- ...(description ? { description } : {}),
376
+ ...(description !== undefined ? { description } : {}),
332
377
  ...(visibility ? { visibility } : {}),
333
378
  ...(accessList ? { accessList } : {}),
334
379
  },
@@ -339,7 +384,7 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
339
384
 
340
385
  if (input.name === "artifactly_delete_artifact") {
341
386
  if (!credentials) {
342
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
387
+ throw new Error(LOGIN_GUIDANCE);
343
388
  }
344
389
  const artifactId = requireStringArg(input.arguments, "artifactId");
345
390
  return objectResult(await requestJson("DELETE", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
@@ -347,14 +392,14 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
347
392
 
348
393
  if (input.name === "artifactly_publish_file") {
349
394
  if (!credentials) {
350
- throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
395
+ throw new Error(LOGIN_GUIDANCE);
351
396
  }
352
397
  const path = requireStringArg(input.arguments, "path");
353
398
  const title = stringArg(input.arguments, "title") ?? titleFromPath(path);
354
399
  const mimeType = stringArg(input.arguments, "mimeType") ?? inferMimeType(path);
355
400
  const visibility = stringArg(input.arguments, "visibility") ?? "unlisted";
356
401
  const kind = kindForMime(path, mimeType);
357
- const file = readFileSync(path);
402
+ const file = readFileForPublish(path);
358
403
  const payload =
359
404
  kind === "file"
360
405
  ? {