@artifactly/mcp 0.1.7 → 0.1.8
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
|
@@ -57,9 +57,32 @@ export function getDefaultCredentialPath(home = homedir()) {
|
|
|
57
57
|
return join(home, ".artifactly", "credentials.json");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function normalizeCredentials(value: unknown): ArtifactlyCredentials | null {
|
|
61
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const candidate = value as Record<string, unknown>;
|
|
65
|
+
if (typeof candidate.apiBaseUrl !== "string" || !candidate.apiBaseUrl.trim()) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (typeof candidate.apiKey !== "string" || !candidate.apiKey.trim()) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
apiBaseUrl: candidate.apiBaseUrl.trim(),
|
|
73
|
+
apiKey: candidate.apiKey.trim(),
|
|
74
|
+
...(typeof candidate.email === "string" && candidate.email.trim()
|
|
75
|
+
? { email: candidate.email.trim() }
|
|
76
|
+
: {}),
|
|
77
|
+
...(typeof candidate.workspace === "string" && candidate.workspace.trim()
|
|
78
|
+
? { workspace: candidate.workspace.trim() }
|
|
79
|
+
: {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
export function readCredentials(path = getDefaultCredentialPath()): ArtifactlyCredentials | null {
|
|
61
84
|
try {
|
|
62
|
-
return JSON.parse(readFileSync(path, "utf8"))
|
|
85
|
+
return normalizeCredentials(JSON.parse(readFileSync(path, "utf8")));
|
|
63
86
|
} catch {
|
|
64
87
|
return null;
|
|
65
88
|
}
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import packageJson from "../../package.json";
|
|
4
4
|
import {
|
|
5
|
+
createArtifactlyMcpServer,
|
|
5
6
|
createArtifactlyMcpCallResult,
|
|
6
7
|
getArtifactlyMcpInstallConfig,
|
|
7
8
|
listArtifactlyMcpServerTools,
|
|
@@ -37,6 +38,25 @@ describe("Artifactly MCP server packaging", () => {
|
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
describe("Artifactly MCP stdio server helpers", () => {
|
|
41
|
+
it("registers tool schemas and server info from package metadata", () => {
|
|
42
|
+
const server = createArtifactlyMcpServer() as unknown as {
|
|
43
|
+
server: { _serverInfo: { name: string; version: string } };
|
|
44
|
+
_registeredTools: Record<string, { inputSchema: { def: { shape: Record<string, unknown> }; safeParse(value: unknown): { success: boolean } } }>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
expect(server.server._serverInfo).toEqual({
|
|
48
|
+
name: "artifactly",
|
|
49
|
+
version: packageJson.version,
|
|
50
|
+
});
|
|
51
|
+
expect(Object.keys(server._registeredTools.artifactly_auth_status.inputSchema.def.shape)).toContain("validate");
|
|
52
|
+
expect(
|
|
53
|
+
server._registeredTools.artifactly_update_artifact.inputSchema.safeParse({
|
|
54
|
+
artifactId: "artifact_1",
|
|
55
|
+
description: "",
|
|
56
|
+
}).success,
|
|
57
|
+
).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
40
60
|
it("maps Artifactly tools to MCP-compatible tool definitions", () => {
|
|
41
61
|
expect(listArtifactlyMcpServerTools().map((tool) => tool.name)).toEqual([
|
|
42
62
|
"artifactly_auth_status",
|
|
@@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
|
+
import packageJson from "../../package.json";
|
|
5
6
|
import { callArtifactlyMcpTool, listArtifactlyMcpTools } from "./artifactly-mcp.js";
|
|
6
7
|
|
|
7
8
|
export type ArtifactlyMcpInstallConfig = {
|
|
@@ -83,7 +84,9 @@ export async function createArtifactlyMcpCallResult(input: {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
const authStatusSchema = z.object({
|
|
87
|
+
const authStatusSchema = z.object({
|
|
88
|
+
validate: z.boolean().optional(),
|
|
89
|
+
});
|
|
87
90
|
const requestLoginCodeSchema = z.object({
|
|
88
91
|
email: z.string().email(),
|
|
89
92
|
name: z.string().optional(),
|
|
@@ -107,7 +110,7 @@ const updateArtifactSchema = z.object({
|
|
|
107
110
|
title: z.string().min(1).optional(),
|
|
108
111
|
markdown: z.string().min(1).optional(),
|
|
109
112
|
content: z.string().min(1).optional(),
|
|
110
|
-
description: z.string().
|
|
113
|
+
description: z.string().max(500).optional(),
|
|
111
114
|
visibility: z.enum(["public", "unlisted", "private", "gated"]).optional(),
|
|
112
115
|
accessList: z.array(z.string().email()).optional(),
|
|
113
116
|
});
|
|
@@ -137,7 +140,7 @@ const toolSchemas = {
|
|
|
137
140
|
export function createArtifactlyMcpServer() {
|
|
138
141
|
const server = new McpServer({
|
|
139
142
|
name: "artifactly",
|
|
140
|
-
version:
|
|
143
|
+
version: packageJson.version,
|
|
141
144
|
});
|
|
142
145
|
|
|
143
146
|
for (const tool of listArtifactlyMcpTools()) {
|
|
@@ -48,6 +48,21 @@ describe("Artifactly MCP tools", () => {
|
|
|
48
48
|
rmSync(dir, { recursive: true, force: true });
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
it("does not report malformed credential files as authenticated", async () => {
|
|
52
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
53
|
+
const credentialPath = join(dir, "credentials.json");
|
|
54
|
+
writeFileSync(credentialPath, "{}\n");
|
|
55
|
+
|
|
56
|
+
await expect(
|
|
57
|
+
callArtifactlyMcpTool({ name: "artifactly_auth_status", credentialPath }),
|
|
58
|
+
).resolves.toEqual({ authenticated: false, apiBaseUrl: "https://artifactly.io" });
|
|
59
|
+
await expect(
|
|
60
|
+
callArtifactlyMcpTool({ name: "artifactly_list_artifacts", credentialPath }),
|
|
61
|
+
).rejects.toThrow(/artifactly_request_login_code[\s\S]*artifactly_verify_login_code/);
|
|
62
|
+
|
|
63
|
+
rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
51
66
|
it("can validate auth status against the Artifactly API", async () => {
|
|
52
67
|
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
53
68
|
const credentialPath = join(dir, "credentials.json");
|
|
@@ -159,6 +174,34 @@ describe("Artifactly MCP tools", () => {
|
|
|
159
174
|
rmSync(dir, { recursive: true, force: true });
|
|
160
175
|
});
|
|
161
176
|
|
|
177
|
+
it("preserves markdown body whitespace when publishing inline markdown", async () => {
|
|
178
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
179
|
+
const credentialPath = join(dir, "credentials.json");
|
|
180
|
+
let publishedBody: unknown;
|
|
181
|
+
writeCredentials(
|
|
182
|
+
{
|
|
183
|
+
apiBaseUrl: "https://artifactly.example",
|
|
184
|
+
apiKey: "art_t...et",
|
|
185
|
+
email: "agent@example.com",
|
|
186
|
+
workspace: "agent",
|
|
187
|
+
},
|
|
188
|
+
credentialPath,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
await callArtifactlyMcpTool({
|
|
192
|
+
name: "artifactly_publish_markdown",
|
|
193
|
+
credentialPath,
|
|
194
|
+
arguments: { title: "Whitespace", markdown: " # Hello\n\n" },
|
|
195
|
+
postJson: async (_path, body) => {
|
|
196
|
+
publishedBody = body;
|
|
197
|
+
return { id: "artifact_1", url: "https://artifactly.example/share/agent/hello" };
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(publishedBody).toMatchObject({ content: " # Hello\n\n" });
|
|
202
|
+
rmSync(dir, { recursive: true, force: true });
|
|
203
|
+
});
|
|
204
|
+
|
|
162
205
|
it("lists, gets, updates, and deletes artifacts through authenticated API requests", async () => {
|
|
163
206
|
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
164
207
|
const credentialPath = join(dir, "credentials.json");
|
|
@@ -234,6 +277,29 @@ describe("Artifactly MCP tools", () => {
|
|
|
234
277
|
rmSync(dir, { recursive: true, force: true });
|
|
235
278
|
});
|
|
236
279
|
|
|
280
|
+
it("rejects conflicting markdown and content update arguments with a specific error", async () => {
|
|
281
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
282
|
+
const credentialPath = join(dir, "credentials.json");
|
|
283
|
+
writeCredentials(
|
|
284
|
+
{
|
|
285
|
+
apiBaseUrl: "https://artifactly.example",
|
|
286
|
+
apiKey: "art_t...et",
|
|
287
|
+
email: "agent@example.com",
|
|
288
|
+
workspace: "agent",
|
|
289
|
+
},
|
|
290
|
+
credentialPath,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
await expect(
|
|
294
|
+
callArtifactlyMcpTool({
|
|
295
|
+
name: "artifactly_update_artifact",
|
|
296
|
+
credentialPath,
|
|
297
|
+
arguments: { artifactId: "artifact_1", markdown: "# Markdown", content: "Plain" },
|
|
298
|
+
}),
|
|
299
|
+
).rejects.toThrow(/Use either markdown or content, not both/);
|
|
300
|
+
rmSync(dir, { recursive: true, force: true });
|
|
301
|
+
});
|
|
302
|
+
|
|
237
303
|
it("publishes local HTML and PDF files through the artifact API", async () => {
|
|
238
304
|
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
239
305
|
const credentialPath = join(dir, "credentials.json");
|
|
@@ -295,6 +361,39 @@ describe("Artifactly MCP tools", () => {
|
|
|
295
361
|
rmSync(dir, { recursive: true, force: true });
|
|
296
362
|
});
|
|
297
363
|
|
|
364
|
+
it("uses explicit MIME type instead of extension when publishing files", async () => {
|
|
365
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
366
|
+
const credentialPath = join(dir, "credentials.json");
|
|
367
|
+
const markdownPath = join(dir, "report.md");
|
|
368
|
+
let publishedBody: unknown;
|
|
369
|
+
writeFileSync(markdownPath, "# Report");
|
|
370
|
+
writeCredentials(
|
|
371
|
+
{
|
|
372
|
+
apiBaseUrl: "https://artifactly.example",
|
|
373
|
+
apiKey: "art_t...et",
|
|
374
|
+
email: "agent@example.com",
|
|
375
|
+
workspace: "agent",
|
|
376
|
+
},
|
|
377
|
+
credentialPath,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await callArtifactlyMcpTool({
|
|
381
|
+
name: "artifactly_publish_file",
|
|
382
|
+
credentialPath,
|
|
383
|
+
arguments: { path: markdownPath, mimeType: "application/pdf" },
|
|
384
|
+
postJson: async (_path, body) => {
|
|
385
|
+
publishedBody = body;
|
|
386
|
+
return { id: "artifact_1", url: "https://artifactly.example/share/agent/report" };
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(publishedBody).toMatchObject({
|
|
391
|
+
kind: "file",
|
|
392
|
+
storageUrl: "data:application/pdf;base64,IyBSZXBvcnQ=",
|
|
393
|
+
});
|
|
394
|
+
rmSync(dir, { recursive: true, force: true });
|
|
395
|
+
});
|
|
396
|
+
|
|
298
397
|
it("returns a friendly error when publishing a missing file", async () => {
|
|
299
398
|
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
300
399
|
const credentialPath = join(dir, "credentials.json");
|
|
@@ -84,6 +84,17 @@ function requireStringArg(
|
|
|
84
84
|
return value;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function requireRawStringArg(
|
|
88
|
+
args: Record<string, unknown> | undefined,
|
|
89
|
+
key: string,
|
|
90
|
+
): string {
|
|
91
|
+
const value = args?.[key];
|
|
92
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
93
|
+
throw new Error(`Missing required argument: ${key}`);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
function stringArrayArg(
|
|
88
99
|
args: Record<string, unknown> | undefined,
|
|
89
100
|
key: string,
|
|
@@ -139,12 +150,11 @@ function inferMimeType(path: string): string {
|
|
|
139
150
|
return "application/octet-stream";
|
|
140
151
|
}
|
|
141
152
|
|
|
142
|
-
function kindForMime(
|
|
143
|
-
const extension = extname(path).toLowerCase();
|
|
153
|
+
function kindForMime(_path: string, mimeType: string) {
|
|
144
154
|
const normalized = mimeType.toLowerCase();
|
|
145
|
-
if (normalized.includes("html")
|
|
146
|
-
if (normalized.includes("json")
|
|
147
|
-
if (normalized.includes("markdown")
|
|
155
|
+
if (normalized.includes("html")) return "html";
|
|
156
|
+
if (normalized.includes("json")) return "json";
|
|
157
|
+
if (normalized.includes("markdown")) return "markdown";
|
|
148
158
|
return "file";
|
|
149
159
|
}
|
|
150
160
|
|
|
@@ -152,7 +162,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
152
162
|
return [
|
|
153
163
|
{
|
|
154
164
|
name: "artifactly_auth_status",
|
|
155
|
-
description:
|
|
165
|
+
description:
|
|
166
|
+
"Check local Artifactly credentials. Pass validate: true to confirm the saved API key with the Artifactly server.",
|
|
156
167
|
inputSchema: { type: "object", properties: { validate: { type: "boolean" } } },
|
|
157
168
|
},
|
|
158
169
|
{
|
|
@@ -181,7 +192,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
181
192
|
},
|
|
182
193
|
{
|
|
183
194
|
name: "artifactly_publish_markdown",
|
|
184
|
-
description:
|
|
195
|
+
description:
|
|
196
|
+
"Publish markdown through the Artifactly API using stored credentials. Defaults to unlisted visibility; visibility can be public, unlisted, private, or gated.",
|
|
185
197
|
inputSchema: {
|
|
186
198
|
type: "object",
|
|
187
199
|
properties: {
|
|
@@ -208,7 +220,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
208
220
|
},
|
|
209
221
|
{
|
|
210
222
|
name: "artifactly_update_artifact",
|
|
211
|
-
description:
|
|
223
|
+
description:
|
|
224
|
+
"Update an existing Artifactly artifact, including visibility and gated access emails. Use markdown or content, not both; description may be an empty string to clear it.",
|
|
212
225
|
inputSchema: {
|
|
213
226
|
type: "object",
|
|
214
227
|
properties: {
|
|
@@ -234,7 +247,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
234
247
|
},
|
|
235
248
|
{
|
|
236
249
|
name: "artifactly_publish_file",
|
|
237
|
-
description:
|
|
250
|
+
description:
|
|
251
|
+
"Publish a local PDF, HTML, JSON, Markdown, or other file through the Artifactly API. Defaults to unlisted visibility; explicit mimeType takes precedence over file extension.",
|
|
238
252
|
inputSchema: {
|
|
239
253
|
type: "object",
|
|
240
254
|
properties: {
|
|
@@ -328,7 +342,7 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
328
342
|
throw new Error(LOGIN_GUIDANCE);
|
|
329
343
|
}
|
|
330
344
|
const title = requireStringArg(input.arguments, "title");
|
|
331
|
-
const content =
|
|
345
|
+
const content = requireRawStringArg(input.arguments, "markdown");
|
|
332
346
|
const visibility = stringArg(input.arguments, "visibility") ?? "unlisted";
|
|
333
347
|
return objectResult(
|
|
334
348
|
await postJson(
|
|
@@ -360,8 +374,11 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
360
374
|
}
|
|
361
375
|
const artifactId = requireStringArg(input.arguments, "artifactId");
|
|
362
376
|
const title = stringArg(input.arguments, "title");
|
|
363
|
-
const markdown =
|
|
364
|
-
const content =
|
|
377
|
+
const markdown = optionalStringArg(input.arguments, "markdown");
|
|
378
|
+
const content = optionalStringArg(input.arguments, "content");
|
|
379
|
+
if (markdown !== undefined && content !== undefined) {
|
|
380
|
+
throw new Error("Use either markdown or content, not both.");
|
|
381
|
+
}
|
|
365
382
|
const description = optionalStringArg(input.arguments, "description");
|
|
366
383
|
const visibility = stringArg(input.arguments, "visibility");
|
|
367
384
|
const accessList = stringArrayArg(input.arguments, "accessList");
|