@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@artifactly/mcp",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "files": [
6
6
  "bin",
@@ -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 Error(message);
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: "art_test_secret",
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 === "/api/artifacts/artifact_1") {
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: "art_t...et", body: undefined },
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: "art_t...et",
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: "art_t...et", body: undefined },
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 result = objectResult(await requestJson("GET", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
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;