@artifactly/mcp 0.1.6 → 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,63 @@ 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
|
+
|
|
66
|
+
it("can validate auth status against the Artifactly API", async () => {
|
|
67
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
68
|
+
const credentialPath = join(dir, "credentials.json");
|
|
69
|
+
const requests: Array<{ method: string; path: string; apiKey?: string }> = [];
|
|
70
|
+
writeCredentials(
|
|
71
|
+
{
|
|
72
|
+
apiBaseUrl: "https://artifactly.example",
|
|
73
|
+
apiKey: "art_test_secret",
|
|
74
|
+
email: "agent@example.com",
|
|
75
|
+
workspace: "agent",
|
|
76
|
+
},
|
|
77
|
+
credentialPath,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await expect(
|
|
81
|
+
callArtifactlyMcpTool({
|
|
82
|
+
name: "artifactly_auth_status",
|
|
83
|
+
credentialPath,
|
|
84
|
+
arguments: { validate: true },
|
|
85
|
+
requestJson: async (method, path, _body, options) => {
|
|
86
|
+
requests.push({ method, path, apiKey: options?.apiKey });
|
|
87
|
+
return { artifacts: [] };
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
).resolves.toMatchObject({
|
|
91
|
+
authenticated: true,
|
|
92
|
+
serverAuthenticated: true,
|
|
93
|
+
validationError: null,
|
|
94
|
+
});
|
|
95
|
+
expect(requests).toEqual([
|
|
96
|
+
{ method: "GET", path: "/api/artifacts", apiKey: "art_test_secret" },
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
rmSync(dir, { recursive: true, force: true });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("gives complete login guidance when protected tools lack credentials", async () => {
|
|
103
|
+
await expect(
|
|
104
|
+
callArtifactlyMcpTool({ name: "artifactly_list_artifacts", credentialPath: "/tmp/nope.json" }),
|
|
105
|
+
).rejects.toThrow(/artifactly_request_login_code[\s\S]*artifactly_verify_login_code/);
|
|
106
|
+
});
|
|
107
|
+
|
|
51
108
|
it("logs out by clearing the shared credential store", async () => {
|
|
52
109
|
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
53
110
|
const credentialPath = join(dir, "credentials.json");
|
|
@@ -117,6 +174,34 @@ describe("Artifactly MCP tools", () => {
|
|
|
117
174
|
rmSync(dir, { recursive: true, force: true });
|
|
118
175
|
});
|
|
119
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
|
+
|
|
120
205
|
it("lists, gets, updates, and deletes artifacts through authenticated API requests", async () => {
|
|
121
206
|
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
122
207
|
const credentialPath = join(dir, "credentials.json");
|
|
@@ -192,6 +277,29 @@ describe("Artifactly MCP tools", () => {
|
|
|
192
277
|
rmSync(dir, { recursive: true, force: true });
|
|
193
278
|
});
|
|
194
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
|
+
|
|
195
303
|
it("publishes local HTML and PDF files through the artifact API", async () => {
|
|
196
304
|
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
197
305
|
const credentialPath = join(dir, "credentials.json");
|
|
@@ -252,4 +360,61 @@ describe("Artifactly MCP tools", () => {
|
|
|
252
360
|
]);
|
|
253
361
|
rmSync(dir, { recursive: true, force: true });
|
|
254
362
|
});
|
|
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
|
+
|
|
397
|
+
it("returns a friendly error when publishing a missing file", async () => {
|
|
398
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
399
|
+
const credentialPath = join(dir, "credentials.json");
|
|
400
|
+
writeCredentials(
|
|
401
|
+
{
|
|
402
|
+
apiBaseUrl: "https://artifactly.example",
|
|
403
|
+
apiKey: "art_test_secret",
|
|
404
|
+
email: "agent@example.com",
|
|
405
|
+
workspace: "agent",
|
|
406
|
+
},
|
|
407
|
+
credentialPath,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
await expect(
|
|
411
|
+
callArtifactlyMcpTool({
|
|
412
|
+
name: "artifactly_publish_file",
|
|
413
|
+
credentialPath,
|
|
414
|
+
arguments: { path: join(dir, "missing.pdf") },
|
|
415
|
+
}),
|
|
416
|
+
).rejects.toThrow(/File not found.*missing\.pdf/);
|
|
417
|
+
|
|
418
|
+
rmSync(dir, { recursive: true, force: true });
|
|
419
|
+
});
|
|
255
420
|
});
|
|
@@ -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,
|
|
@@ -76,6 +84,17 @@ function requireStringArg(
|
|
|
76
84
|
return value;
|
|
77
85
|
}
|
|
78
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
|
+
|
|
79
98
|
function stringArrayArg(
|
|
80
99
|
args: Record<string, unknown> | undefined,
|
|
81
100
|
key: string,
|
|
@@ -94,6 +113,28 @@ function objectResult(value: unknown): Record<string, unknown> {
|
|
|
94
113
|
return value as Record<string, unknown>;
|
|
95
114
|
}
|
|
96
115
|
|
|
116
|
+
const LOGIN_GUIDANCE =
|
|
117
|
+
"Artifactly credentials are not configured. Run artifactly_request_login_code first, then run artifactly_verify_login_code with the emailed code.";
|
|
118
|
+
|
|
119
|
+
function booleanArg(args: Record<string, unknown> | undefined, key: string): boolean {
|
|
120
|
+
return args?.[key] === true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readFileForPublish(path: string): Buffer {
|
|
124
|
+
try {
|
|
125
|
+
return readFileSync(path);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
|
|
128
|
+
if (code === "ENOENT") {
|
|
129
|
+
throw new Error(`File not found: ${path}`);
|
|
130
|
+
}
|
|
131
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
132
|
+
throw new Error(`File is not readable: ${path}`);
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
function titleFromPath(path: string): string {
|
|
98
139
|
const name = basename(path, extname(path));
|
|
99
140
|
return name || "artifact";
|
|
@@ -109,12 +150,11 @@ function inferMimeType(path: string): string {
|
|
|
109
150
|
return "application/octet-stream";
|
|
110
151
|
}
|
|
111
152
|
|
|
112
|
-
function kindForMime(
|
|
113
|
-
const extension = extname(path).toLowerCase();
|
|
153
|
+
function kindForMime(_path: string, mimeType: string) {
|
|
114
154
|
const normalized = mimeType.toLowerCase();
|
|
115
|
-
if (normalized.includes("html")
|
|
116
|
-
if (normalized.includes("json")
|
|
117
|
-
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";
|
|
118
158
|
return "file";
|
|
119
159
|
}
|
|
120
160
|
|
|
@@ -122,8 +162,9 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
122
162
|
return [
|
|
123
163
|
{
|
|
124
164
|
name: "artifactly_auth_status",
|
|
125
|
-
description:
|
|
126
|
-
|
|
165
|
+
description:
|
|
166
|
+
"Check local Artifactly credentials. Pass validate: true to confirm the saved API key with the Artifactly server.",
|
|
167
|
+
inputSchema: { type: "object", properties: { validate: { type: "boolean" } } },
|
|
127
168
|
},
|
|
128
169
|
{
|
|
129
170
|
name: "artifactly_request_login_code",
|
|
@@ -151,7 +192,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
151
192
|
},
|
|
152
193
|
{
|
|
153
194
|
name: "artifactly_publish_markdown",
|
|
154
|
-
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.",
|
|
155
197
|
inputSchema: {
|
|
156
198
|
type: "object",
|
|
157
199
|
properties: {
|
|
@@ -178,7 +220,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
178
220
|
},
|
|
179
221
|
{
|
|
180
222
|
name: "artifactly_update_artifact",
|
|
181
|
-
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.",
|
|
182
225
|
inputSchema: {
|
|
183
226
|
type: "object",
|
|
184
227
|
properties: {
|
|
@@ -204,7 +247,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
204
247
|
},
|
|
205
248
|
{
|
|
206
249
|
name: "artifactly_publish_file",
|
|
207
|
-
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.",
|
|
208
252
|
inputSchema: {
|
|
209
253
|
type: "object",
|
|
210
254
|
properties: {
|
|
@@ -227,14 +271,29 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
227
271
|
const requestJson = input.requestJson ?? createDefaultRequestJson(apiBaseUrl);
|
|
228
272
|
|
|
229
273
|
if (input.name === "artifactly_auth_status") {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
:
|
|
274
|
+
if (!credentials) {
|
|
275
|
+
return { authenticated: false, apiBaseUrl };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const result: Record<string, unknown> = {
|
|
279
|
+
authenticated: true,
|
|
280
|
+
apiBaseUrl: credentials.apiBaseUrl,
|
|
281
|
+
email: credentials.email,
|
|
282
|
+
workspace: credentials.workspace,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (booleanArg(input.arguments, "validate")) {
|
|
286
|
+
try {
|
|
287
|
+
await requestJson("GET", "/api/artifacts", undefined, { apiKey: credentials.apiKey });
|
|
288
|
+
result.serverAuthenticated = true;
|
|
289
|
+
result.validationError = null;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
result.serverAuthenticated = false;
|
|
292
|
+
result.validationError = error instanceof Error ? error.message : "Validation failed";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return result;
|
|
238
297
|
}
|
|
239
298
|
|
|
240
299
|
if (input.name === "artifactly_request_login_code") {
|
|
@@ -280,10 +339,10 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
280
339
|
|
|
281
340
|
if (input.name === "artifactly_publish_markdown") {
|
|
282
341
|
if (!credentials) {
|
|
283
|
-
throw new Error(
|
|
342
|
+
throw new Error(LOGIN_GUIDANCE);
|
|
284
343
|
}
|
|
285
344
|
const title = requireStringArg(input.arguments, "title");
|
|
286
|
-
const content =
|
|
345
|
+
const content = requireRawStringArg(input.arguments, "markdown");
|
|
287
346
|
const visibility = stringArg(input.arguments, "visibility") ?? "unlisted";
|
|
288
347
|
return objectResult(
|
|
289
348
|
await postJson(
|
|
@@ -296,14 +355,14 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
296
355
|
|
|
297
356
|
if (input.name === "artifactly_list_artifacts") {
|
|
298
357
|
if (!credentials) {
|
|
299
|
-
throw new Error(
|
|
358
|
+
throw new Error(LOGIN_GUIDANCE);
|
|
300
359
|
}
|
|
301
360
|
return objectResult(await requestJson("GET", "/api/artifacts", undefined, { apiKey: credentials.apiKey }));
|
|
302
361
|
}
|
|
303
362
|
|
|
304
363
|
if (input.name === "artifactly_get_artifact") {
|
|
305
364
|
if (!credentials) {
|
|
306
|
-
throw new Error(
|
|
365
|
+
throw new Error(LOGIN_GUIDANCE);
|
|
307
366
|
}
|
|
308
367
|
const artifactId = requireStringArg(input.arguments, "artifactId");
|
|
309
368
|
return objectResult(await requestJson("GET", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
|
|
@@ -311,13 +370,16 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
311
370
|
|
|
312
371
|
if (input.name === "artifactly_update_artifact") {
|
|
313
372
|
if (!credentials) {
|
|
314
|
-
throw new Error(
|
|
373
|
+
throw new Error(LOGIN_GUIDANCE);
|
|
315
374
|
}
|
|
316
375
|
const artifactId = requireStringArg(input.arguments, "artifactId");
|
|
317
376
|
const title = stringArg(input.arguments, "title");
|
|
318
|
-
const markdown =
|
|
319
|
-
const content =
|
|
320
|
-
|
|
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
|
+
}
|
|
382
|
+
const description = optionalStringArg(input.arguments, "description");
|
|
321
383
|
const visibility = stringArg(input.arguments, "visibility");
|
|
322
384
|
const accessList = stringArrayArg(input.arguments, "accessList");
|
|
323
385
|
return objectResult(
|
|
@@ -328,7 +390,7 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
328
390
|
...(title ? { title } : {}),
|
|
329
391
|
...(markdown ? { markdown } : {}),
|
|
330
392
|
...(content ? { content } : {}),
|
|
331
|
-
...(description ? { description } : {}),
|
|
393
|
+
...(description !== undefined ? { description } : {}),
|
|
332
394
|
...(visibility ? { visibility } : {}),
|
|
333
395
|
...(accessList ? { accessList } : {}),
|
|
334
396
|
},
|
|
@@ -339,7 +401,7 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
339
401
|
|
|
340
402
|
if (input.name === "artifactly_delete_artifact") {
|
|
341
403
|
if (!credentials) {
|
|
342
|
-
throw new Error(
|
|
404
|
+
throw new Error(LOGIN_GUIDANCE);
|
|
343
405
|
}
|
|
344
406
|
const artifactId = requireStringArg(input.arguments, "artifactId");
|
|
345
407
|
return objectResult(await requestJson("DELETE", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
|
|
@@ -347,14 +409,14 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
347
409
|
|
|
348
410
|
if (input.name === "artifactly_publish_file") {
|
|
349
411
|
if (!credentials) {
|
|
350
|
-
throw new Error(
|
|
412
|
+
throw new Error(LOGIN_GUIDANCE);
|
|
351
413
|
}
|
|
352
414
|
const path = requireStringArg(input.arguments, "path");
|
|
353
415
|
const title = stringArg(input.arguments, "title") ?? titleFromPath(path);
|
|
354
416
|
const mimeType = stringArg(input.arguments, "mimeType") ?? inferMimeType(path);
|
|
355
417
|
const visibility = stringArg(input.arguments, "visibility") ?? "unlisted";
|
|
356
418
|
const kind = kindForMime(path, mimeType);
|
|
357
|
-
const file =
|
|
419
|
+
const file = readFileForPublish(path);
|
|
358
420
|
const payload =
|
|
359
421
|
kind === "file"
|
|
360
422
|
? {
|