@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 +1 -1
- package/src/mcp/artifactly-mcp.test.ts +66 -0
- package/src/mcp/artifactly-mcp.ts +63 -18
package/package.json
CHANGED
|
@@ -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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
402
|
+
const file = readFileForPublish(path);
|
|
358
403
|
const payload =
|
|
359
404
|
kind === "file"
|
|
360
405
|
? {
|