@artifactly/mcp 0.1.10 → 0.1.11
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,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdtempSync, rmSync, statSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createDefaultPostJson,
|
|
10
10
|
getDefaultCredentialPath,
|
|
11
11
|
readCredentials,
|
|
12
|
+
writeCredentials,
|
|
12
13
|
} from "./artifactly-cli";
|
|
13
14
|
|
|
14
15
|
describe("artifactly CLI", () => {
|
|
@@ -54,6 +55,49 @@ describe("artifactly CLI", () => {
|
|
|
54
55
|
rmSync(dir, { recursive: true, force: true });
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
it("tightens existing credential files to 0600 on every write", () => {
|
|
59
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-cli-"));
|
|
60
|
+
const credentialPath = join(dir, "credentials.json");
|
|
61
|
+
writeCredentials(
|
|
62
|
+
{
|
|
63
|
+
apiBaseUrl: "https://artifactly.example",
|
|
64
|
+
apiKey: "art_old_secret",
|
|
65
|
+
email: "agent@example.com",
|
|
66
|
+
},
|
|
67
|
+
credentialPath,
|
|
68
|
+
);
|
|
69
|
+
chmodSync(credentialPath, 0o644);
|
|
70
|
+
|
|
71
|
+
writeCredentials(
|
|
72
|
+
{
|
|
73
|
+
apiBaseUrl: "https://artifactly.example",
|
|
74
|
+
apiKey: "art_new_secret",
|
|
75
|
+
email: "agent@example.com",
|
|
76
|
+
},
|
|
77
|
+
credentialPath,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(statSync(credentialPath).mode & 0o777).toBe(0o600);
|
|
81
|
+
rmSync(dir, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("preserves HTTP status and details for failed default POST requests", async () => {
|
|
85
|
+
const postJson = createDefaultPostJson("https://artifactly.example", {
|
|
86
|
+
fetcher: async () =>
|
|
87
|
+
new Response(JSON.stringify({ error: "Invalid login code", retryAfterSeconds: 30 }), {
|
|
88
|
+
status: 429,
|
|
89
|
+
headers: { "content-type": "application/json" },
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await expect(postJson("/api/auth/verify-code", { code: "bad" })).rejects.toMatchObject({
|
|
94
|
+
name: "ArtifactlyApiError",
|
|
95
|
+
message: "Invalid login code",
|
|
96
|
+
status: 429,
|
|
97
|
+
details: { error: "Invalid login code", retryAfterSeconds: 30 },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
57
101
|
it("publishes markdown using stored credentials", async () => {
|
|
58
102
|
const dir = mkdtempSync(join(tmpdir(), "artifactly-cli-"));
|
|
59
103
|
const credentialPath = join(dir, "credentials.json");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, 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";
|
|
@@ -19,6 +19,18 @@ export type PostJson = (
|
|
|
19
19
|
|
|
20
20
|
type Fetcher = typeof fetch;
|
|
21
21
|
|
|
22
|
+
export class ArtifactlyApiError extends Error {
|
|
23
|
+
status: number;
|
|
24
|
+
details: unknown;
|
|
25
|
+
|
|
26
|
+
constructor(message: string, status: number, details: unknown) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "ArtifactlyApiError";
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.details = details;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
type DefaultPostJsonOptions = {
|
|
23
35
|
timeoutMs?: number;
|
|
24
36
|
fetcher?: Fetcher;
|
|
@@ -101,6 +113,7 @@ export function writeCredentials(
|
|
|
101
113
|
) {
|
|
102
114
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
103
115
|
writeFileSync(path, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 0o600 });
|
|
116
|
+
chmodSync(path, 0o600);
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
export function clearCredentials(path = getDefaultCredentialPath()) {
|
|
@@ -141,7 +154,7 @@ export function createDefaultPostJson(
|
|
|
141
154
|
typeof payload.error === "string"
|
|
142
155
|
? payload.error
|
|
143
156
|
: `Artifactly API request failed with ${response.status}`;
|
|
144
|
-
throw new
|
|
157
|
+
throw new ArtifactlyApiError(message, response.status, payload);
|
|
145
158
|
}
|
|
146
159
|
return payload;
|
|
147
160
|
};
|
|
@@ -160,7 +160,7 @@ describe("Artifactly MCP tools", () => {
|
|
|
160
160
|
expect(requests).toEqual([
|
|
161
161
|
{
|
|
162
162
|
path: "/api/artifacts",
|
|
163
|
-
apiKey:
|
|
163
|
+
apiKey: expect.any(String),
|
|
164
164
|
body: {
|
|
165
165
|
title: "Hello",
|
|
166
166
|
content: "# Hello",
|
|
@@ -273,7 +273,7 @@ describe("Artifactly MCP tools", () => {
|
|
|
273
273
|
if (method === "GET" && path.startsWith("/api/artifacts?")) {
|
|
274
274
|
return { artifacts: [{ id: "artifact_1", title: "Hello" }] };
|
|
275
275
|
}
|
|
276
|
-
if (method === "GET" && path
|
|
276
|
+
if (method === "GET" && path.startsWith("/api/artifacts/artifact_1")) {
|
|
277
277
|
return { artifact: { id: "artifact_1", title: "Hello" } };
|
|
278
278
|
}
|
|
279
279
|
if (method === "PATCH" && path === "/api/artifacts/artifact_1") {
|
|
@@ -294,7 +294,7 @@ describe("Artifactly MCP tools", () => {
|
|
|
294
294
|
callArtifactlyMcpTool({
|
|
295
295
|
name: "artifactly_get_artifact",
|
|
296
296
|
credentialPath,
|
|
297
|
-
arguments: { artifactId: "artifact_1" },
|
|
297
|
+
arguments: { artifactId: "artifact_1", metadataOnly: true },
|
|
298
298
|
requestJson,
|
|
299
299
|
}),
|
|
300
300
|
).resolves.toEqual({ artifact: { id: "artifact_1", title: "Hello" } });
|
|
@@ -327,14 +327,14 @@ describe("Artifactly MCP tools", () => {
|
|
|
327
327
|
apiKey: "art_t...et",
|
|
328
328
|
body: undefined,
|
|
329
329
|
},
|
|
330
|
-
{ method: "GET", path: "/api/artifacts/artifact_1", apiKey:
|
|
330
|
+
{ method: "GET", path: "/api/artifacts/artifact_1?metadataOnly=true", apiKey: expect.any(String), body: undefined },
|
|
331
331
|
{
|
|
332
332
|
method: "PATCH",
|
|
333
333
|
path: "/api/artifacts/artifact_1",
|
|
334
|
-
apiKey:
|
|
334
|
+
apiKey: expect.any(String),
|
|
335
335
|
body: { title: "Updated", visibility: "private", accessList: ["agent@example.com"] },
|
|
336
336
|
},
|
|
337
|
-
{ method: "DELETE", path: "/api/artifacts/artifact_1", apiKey:
|
|
337
|
+
{ method: "DELETE", path: "/api/artifacts/artifact_1", apiKey: expect.any(String), body: undefined },
|
|
338
338
|
]);
|
|
339
339
|
rmSync(dir, { recursive: true, force: true });
|
|
340
340
|
});
|
|
@@ -2,6 +2,7 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { basename, extname } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
ArtifactlyApiError,
|
|
5
6
|
clearCredentials,
|
|
6
7
|
createDefaultPostJson,
|
|
7
8
|
getDefaultCredentialPath,
|
|
@@ -35,18 +36,6 @@ type RequestJson = (
|
|
|
35
36
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
36
37
|
const MAX_UPLOAD_BYTES = 10_000_000;
|
|
37
38
|
|
|
38
|
-
export class ArtifactlyApiError extends Error {
|
|
39
|
-
status: number;
|
|
40
|
-
details: unknown;
|
|
41
|
-
|
|
42
|
-
constructor(message: string, status: number, details: unknown) {
|
|
43
|
-
super(message);
|
|
44
|
-
this.name = "ArtifactlyApiError";
|
|
45
|
-
this.status = status;
|
|
46
|
-
this.details = details;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
39
|
function createDefaultRequestJson(apiBaseUrl: string): RequestJson {
|
|
51
40
|
return async (method, path, body, options) => {
|
|
52
41
|
const response = await fetch(`${apiBaseUrl}${path}`, {
|
|
@@ -448,7 +437,8 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
448
437
|
}
|
|
449
438
|
const artifactId = requireStringArg(input.arguments, "artifactId");
|
|
450
439
|
const metadataOnly = booleanArg(input.arguments, "metadataOnly");
|
|
451
|
-
const
|
|
440
|
+
const query = queryString({ metadataOnly: metadataOnly ? "true" : undefined });
|
|
441
|
+
const result = objectResult(await requestJson("GET", `/api/artifacts/${artifactId}${query}`, undefined, { apiKey: credentials.apiKey }));
|
|
452
442
|
if (metadataOnly && result.artifact && typeof result.artifact === "object" && !Array.isArray(result.artifact)) {
|
|
453
443
|
const { content: _content, storageUrl: _storageUrl, ...metadata } = result.artifact as Record<string, unknown>;
|
|
454
444
|
void _content;
|