@artifactly/mcp 0.1.9 → 0.1.10
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/README.md
CHANGED
|
@@ -66,11 +66,15 @@ Artifact workflow notes:
|
|
|
66
66
|
- Supported visibility values are `public`, `unlisted`, `private`, and `gated`.
|
|
67
67
|
- `gated` artifacts can be published with an `accessList` of allowed email addresses or updated later with `artifactly_update_artifact`.
|
|
68
68
|
- `artifactly_list_artifacts` supports `limit`, `cursor`, `visibility`, and `kind` filters and omits large artifact content by default.
|
|
69
|
-
- `artifactly_get_artifact` returns full
|
|
70
|
-
- `artifactly_update_artifact` accepts either `markdown` or `content`, not both
|
|
69
|
+
- `artifactly_get_artifact` returns full content by default, including base64/data URL file payloads. Pass `metadataOnly: true` to omit `content` and `storageUrl` for bounded metadata reads.
|
|
70
|
+
- `artifactly_update_artifact` accepts either `markdown` or `content`, not both, and preserves text whitespace exactly while rejecting whitespace-only content updates.
|
|
71
|
+
- To replace binary/file content, call `artifactly_update_artifact` with `filePath` and optional `mimeType`; the replacement file becomes the viewer source and clears old inline text content.
|
|
72
|
+
- Pass `description: ""` to clear a description.
|
|
71
73
|
- `artifactly_publish_file` infers MIME type from file extension unless `mimeType` is supplied; explicit `mimeType` wins.
|
|
74
|
+
- Image MIME types such as `image/png` publish as `kind: "image"` and can be listed with `kind: "image"`.
|
|
72
75
|
- Files larger than 10 MB are rejected before upload to avoid large in-memory base64 payloads.
|
|
73
76
|
- Requests time out after 30 seconds.
|
|
77
|
+
- This package currently exposes MCP tools only; resources, resource templates, and prompts are intentionally unsupported.
|
|
74
78
|
|
|
75
79
|
Manual publish from a logged-in maintainer account:
|
|
76
80
|
|
package/package.json
CHANGED
|
@@ -14,5 +14,9 @@ describe("Artifactly MCP package documentation", () => {
|
|
|
14
14
|
expect(readme).toContain("`artifactly_list_artifacts` supports `limit`, `cursor`, `visibility`, and `kind`");
|
|
15
15
|
expect(readme).toContain("Files larger than 10 MB are rejected before upload");
|
|
16
16
|
expect(readme).toContain("Requests time out after 30 seconds");
|
|
17
|
+
expect(readme).toContain("`artifactly_get_artifact` returns full content by default");
|
|
18
|
+
expect(readme).toContain("`metadataOnly: true`");
|
|
19
|
+
expect(readme).toContain("This package currently exposes MCP tools only");
|
|
20
|
+
expect(readme).toContain("resources, resource templates, and prompts are intentionally unsupported");
|
|
17
21
|
});
|
|
18
22
|
});
|
|
@@ -41,7 +41,7 @@ describe("Artifactly MCP stdio server helpers", () => {
|
|
|
41
41
|
it("registers tool schemas and server info from package metadata", () => {
|
|
42
42
|
const server = createArtifactlyMcpServer() as unknown as {
|
|
43
43
|
server: { _serverInfo: { name: string; version: string } };
|
|
44
|
-
_registeredTools: Record<string, { inputSchema: { def: { shape: Record<string, unknown> }; safeParse(value: unknown): { success: boolean } } }>;
|
|
44
|
+
_registeredTools: Record<string, { inputSchema: { def: { shape: Record<string, unknown> }; safeParse(value: unknown): { success: boolean } }; annotations?: Record<string, unknown>; description: string }>;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
expect(server.server._serverInfo).toEqual({
|
|
@@ -71,6 +71,38 @@ describe("Artifactly MCP stdio server helpers", () => {
|
|
|
71
71
|
kind: "html",
|
|
72
72
|
}).success,
|
|
73
73
|
).toBe(true);
|
|
74
|
+
expect(
|
|
75
|
+
server._registeredTools.artifactly_publish_file.inputSchema.safeParse({
|
|
76
|
+
path: "/tmp/chart.png",
|
|
77
|
+
visibility: "gated",
|
|
78
|
+
accessList: ["agent@example.com"],
|
|
79
|
+
}).success,
|
|
80
|
+
).toBe(true);
|
|
81
|
+
expect(
|
|
82
|
+
server._registeredTools.artifactly_update_artifact.inputSchema.safeParse({
|
|
83
|
+
artifactId: "artifact_1",
|
|
84
|
+
filePath: "/tmp/replacement.pdf",
|
|
85
|
+
mimeType: "application/pdf",
|
|
86
|
+
}).success,
|
|
87
|
+
).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("registers MCP tool annotations and accurate content descriptions", () => {
|
|
91
|
+
const server = createArtifactlyMcpServer() as unknown as {
|
|
92
|
+
_registeredTools: Record<string, { annotations?: Record<string, unknown>; description: string }>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
expect(server._registeredTools.artifactly_list_artifacts.annotations).toMatchObject({
|
|
96
|
+
readOnlyHint: true,
|
|
97
|
+
});
|
|
98
|
+
expect(server._registeredTools.artifactly_get_artifact.annotations).toMatchObject({
|
|
99
|
+
readOnlyHint: true,
|
|
100
|
+
});
|
|
101
|
+
expect(server._registeredTools.artifactly_delete_artifact.annotations).toMatchObject({
|
|
102
|
+
destructiveHint: true,
|
|
103
|
+
});
|
|
104
|
+
expect(server._registeredTools.artifactly_get_artifact.description).toMatch(/full content/i);
|
|
105
|
+
expect(server._registeredTools.artifactly_get_artifact.description).toMatch(/metadataOnly/i);
|
|
74
106
|
});
|
|
75
107
|
|
|
76
108
|
it("maps Artifactly tools to MCP-compatible tool definitions", () => {
|
|
@@ -97,6 +129,20 @@ describe("Artifactly MCP stdio server helpers", () => {
|
|
|
97
129
|
expect(result.content).toHaveLength(1);
|
|
98
130
|
expect(result.content[0]).toMatchObject({ type: "text" });
|
|
99
131
|
expect(result.content[0].text).toContain('"authenticated": false');
|
|
132
|
+
expect(result.structuredContent).toEqual({ authenticated: false, apiBaseUrl: "https://artifactly.io" });
|
|
100
133
|
expect(result.content[0].text).not.toContain("art_");
|
|
101
134
|
});
|
|
135
|
+
|
|
136
|
+
it("returns typed MCP error content", async () => {
|
|
137
|
+
const result = await createArtifactlyMcpCallResult({
|
|
138
|
+
name: "artifactly_list_artifacts",
|
|
139
|
+
credentialPath: "/tmp/does-not-exist-artifactly-credentials.json",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.isError).toBe(true);
|
|
143
|
+
expect(result.structuredContent).toMatchObject({
|
|
144
|
+
error: expect.stringContaining("artifactly_request_login_code"),
|
|
145
|
+
errorType: "Error",
|
|
146
|
+
});
|
|
147
|
+
});
|
|
102
148
|
});
|
|
@@ -27,6 +27,7 @@ type McpTextContent = {
|
|
|
27
27
|
|
|
28
28
|
export type ArtifactlyMcpCallResult = {
|
|
29
29
|
content: McpTextContent[];
|
|
30
|
+
structuredContent?: Record<string, unknown>;
|
|
30
31
|
isError?: boolean;
|
|
31
32
|
};
|
|
32
33
|
|
|
@@ -53,7 +54,12 @@ export function listArtifactlyMcpServerTools() {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function resultToText(value: unknown): ArtifactlyMcpCallResult {
|
|
57
|
+
const structuredContent =
|
|
58
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
59
|
+
? (value as Record<string, unknown>)
|
|
60
|
+
: undefined;
|
|
56
61
|
return {
|
|
62
|
+
...(structuredContent ? { structuredContent } : {}),
|
|
57
63
|
content: [
|
|
58
64
|
{
|
|
59
65
|
type: "text",
|
|
@@ -74,6 +80,12 @@ export async function createArtifactlyMcpCallResult(input: {
|
|
|
74
80
|
} catch (error) {
|
|
75
81
|
return {
|
|
76
82
|
isError: true,
|
|
83
|
+
structuredContent: {
|
|
84
|
+
error: error instanceof Error ? error.message : String(error),
|
|
85
|
+
errorType: error instanceof Error ? error.name : "Error",
|
|
86
|
+
...(error instanceof Error && "status" in error ? { status: (error as { status?: unknown }).status } : {}),
|
|
87
|
+
...(error instanceof Error && "details" in error ? { details: (error as { details?: unknown }).details } : {}),
|
|
88
|
+
},
|
|
77
89
|
content: [
|
|
78
90
|
{
|
|
79
91
|
type: "text",
|
|
@@ -110,12 +122,15 @@ const listArtifactsSchema = z.object({
|
|
|
110
122
|
});
|
|
111
123
|
const getArtifactSchema = z.object({
|
|
112
124
|
artifactId: z.string().min(1),
|
|
125
|
+
metadataOnly: z.boolean().optional(),
|
|
113
126
|
});
|
|
114
127
|
const updateArtifactSchema = z.object({
|
|
115
128
|
artifactId: z.string().min(1),
|
|
116
129
|
title: z.string().min(1).optional(),
|
|
117
130
|
markdown: z.string().min(1).optional(),
|
|
118
131
|
content: z.string().min(1).optional(),
|
|
132
|
+
filePath: z.string().min(1).optional(),
|
|
133
|
+
mimeType: z.string().min(1).optional(),
|
|
119
134
|
description: z.string().max(500).optional(),
|
|
120
135
|
visibility: z.enum(["public", "unlisted", "private", "gated"]).optional(),
|
|
121
136
|
accessList: z.array(z.string().email()).optional(),
|
|
@@ -128,6 +143,7 @@ const publishFileSchema = z.object({
|
|
|
128
143
|
title: z.string().min(1).optional(),
|
|
129
144
|
mimeType: z.string().min(1).optional(),
|
|
130
145
|
visibility: z.enum(["public", "unlisted", "private", "gated"]).optional(),
|
|
146
|
+
accessList: z.array(z.string().email()).optional(),
|
|
131
147
|
});
|
|
132
148
|
|
|
133
149
|
const toolSchemas = {
|
|
@@ -143,6 +159,14 @@ const toolSchemas = {
|
|
|
143
159
|
artifactly_publish_file: publishFileSchema,
|
|
144
160
|
} as const;
|
|
145
161
|
|
|
162
|
+
const toolAnnotations: Record<string, Record<string, boolean>> = {
|
|
163
|
+
artifactly_auth_status: { readOnlyHint: true },
|
|
164
|
+
artifactly_list_artifacts: { readOnlyHint: true },
|
|
165
|
+
artifactly_get_artifact: { readOnlyHint: true },
|
|
166
|
+
artifactly_delete_artifact: { destructiveHint: true },
|
|
167
|
+
artifactly_logout: { destructiveHint: true },
|
|
168
|
+
};
|
|
169
|
+
|
|
146
170
|
export function createArtifactlyMcpServer() {
|
|
147
171
|
const server = new McpServer({
|
|
148
172
|
name: "artifactly",
|
|
@@ -157,6 +181,7 @@ export function createArtifactlyMcpServer() {
|
|
|
157
181
|
title: tool.name,
|
|
158
182
|
description: tool.description,
|
|
159
183
|
inputSchema: schema,
|
|
184
|
+
...(toolAnnotations[tool.name] ? { annotations: toolAnnotations[tool.name] } : {}),
|
|
160
185
|
},
|
|
161
186
|
async (args: Record<string, unknown>) =>
|
|
162
187
|
createArtifactlyMcpCallResult({ name: tool.name, arguments: args }),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
@@ -487,7 +487,7 @@ describe("Artifactly MCP tools", () => {
|
|
|
487
487
|
writeCredentials(
|
|
488
488
|
{
|
|
489
489
|
apiBaseUrl: "https://artifactly.example",
|
|
490
|
-
apiKey: "
|
|
490
|
+
apiKey: "art_test_secret",
|
|
491
491
|
email: "agent@example.com",
|
|
492
492
|
workspace: "agent",
|
|
493
493
|
},
|
|
@@ -505,4 +505,138 @@ describe("Artifactly MCP tools", () => {
|
|
|
505
505
|
|
|
506
506
|
rmSync(dir, { recursive: true, force: true });
|
|
507
507
|
});
|
|
508
|
+
|
|
509
|
+
it("rejects directory paths with a friendly file error", async () => {
|
|
510
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
511
|
+
const credentialPath = join(dir, "credentials.json");
|
|
512
|
+
const directoryPath = join(dir, "folder");
|
|
513
|
+
mkdirSync(directoryPath);
|
|
514
|
+
writeCredentials(
|
|
515
|
+
{
|
|
516
|
+
apiBaseUrl: "https://artifactly.example",
|
|
517
|
+
apiKey: "art_test_secret",
|
|
518
|
+
email: "agent@example.com",
|
|
519
|
+
workspace: "agent",
|
|
520
|
+
},
|
|
521
|
+
credentialPath,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
await expect(
|
|
525
|
+
callArtifactlyMcpTool({
|
|
526
|
+
name: "artifactly_publish_file",
|
|
527
|
+
credentialPath,
|
|
528
|
+
arguments: { path: directoryPath },
|
|
529
|
+
}),
|
|
530
|
+
).rejects.toThrow(/Path must point to a file/);
|
|
531
|
+
|
|
532
|
+
rmSync(dir, { recursive: true, force: true });
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("publishes gated image files with access lists and image kind", async () => {
|
|
536
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
537
|
+
const credentialPath = join(dir, "credentials.json");
|
|
538
|
+
const pngPath = join(dir, "chart.png");
|
|
539
|
+
let publishedBody: unknown;
|
|
540
|
+
writeFileSync(pngPath, Buffer.from([0x89, 0x50, 0x4e, 0x47]));
|
|
541
|
+
writeCredentials(
|
|
542
|
+
{
|
|
543
|
+
apiBaseUrl: "https://artifactly.example",
|
|
544
|
+
apiKey: "art_test_secret",
|
|
545
|
+
email: "agent@example.com",
|
|
546
|
+
workspace: "agent",
|
|
547
|
+
},
|
|
548
|
+
credentialPath,
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
await callArtifactlyMcpTool({
|
|
552
|
+
name: "artifactly_publish_file",
|
|
553
|
+
credentialPath,
|
|
554
|
+
arguments: {
|
|
555
|
+
path: pngPath,
|
|
556
|
+
mimeType: "image/png",
|
|
557
|
+
visibility: "gated",
|
|
558
|
+
accessList: ["One@Example.com"],
|
|
559
|
+
},
|
|
560
|
+
postJson: async (_path, body) => {
|
|
561
|
+
publishedBody = body;
|
|
562
|
+
return { id: "artifact_1", accessList: ["one@example.com"] };
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
expect(publishedBody).toMatchObject({
|
|
567
|
+
kind: "image",
|
|
568
|
+
storageUrl: "data:image/png;base64,iVBORw==",
|
|
569
|
+
visibility: "gated",
|
|
570
|
+
accessList: ["One@Example.com"],
|
|
571
|
+
});
|
|
572
|
+
rmSync(dir, { recursive: true, force: true });
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("preserves update whitespace and rejects whitespace-only updates locally", async () => {
|
|
576
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
577
|
+
const credentialPath = join(dir, "credentials.json");
|
|
578
|
+
const requests: unknown[] = [];
|
|
579
|
+
writeCredentials(
|
|
580
|
+
{
|
|
581
|
+
apiBaseUrl: "https://artifactly.example",
|
|
582
|
+
apiKey: "art_test_secret",
|
|
583
|
+
email: "agent@example.com",
|
|
584
|
+
workspace: "agent",
|
|
585
|
+
},
|
|
586
|
+
credentialPath,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
await callArtifactlyMcpTool({
|
|
590
|
+
name: "artifactly_update_artifact",
|
|
591
|
+
credentialPath,
|
|
592
|
+
arguments: { artifactId: "artifact_1", markdown: " # Title\n" },
|
|
593
|
+
requestJson: async (_method, _path, body) => {
|
|
594
|
+
requests.push(body);
|
|
595
|
+
return { artifact: { id: "artifact_1" } };
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
expect(requests).toEqual([{ markdown: " # Title\n" }]);
|
|
599
|
+
await expect(
|
|
600
|
+
callArtifactlyMcpTool({
|
|
601
|
+
name: "artifactly_update_artifact",
|
|
602
|
+
credentialPath,
|
|
603
|
+
arguments: { artifactId: "artifact_1", content: " " },
|
|
604
|
+
}),
|
|
605
|
+
).rejects.toThrow(/Content cannot be whitespace-only/);
|
|
606
|
+
|
|
607
|
+
rmSync(dir, { recursive: true, force: true });
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("replaces artifact files explicitly through update", async () => {
|
|
611
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
612
|
+
const credentialPath = join(dir, "credentials.json");
|
|
613
|
+
const pdfPath = join(dir, "replacement.pdf");
|
|
614
|
+
let bodySent: unknown;
|
|
615
|
+
writeFileSync(pdfPath, Buffer.from("%PDF-1.4"));
|
|
616
|
+
writeCredentials(
|
|
617
|
+
{
|
|
618
|
+
apiBaseUrl: "https://artifactly.example",
|
|
619
|
+
apiKey: "art_test_secret",
|
|
620
|
+
email: "agent@example.com",
|
|
621
|
+
workspace: "agent",
|
|
622
|
+
},
|
|
623
|
+
credentialPath,
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
await callArtifactlyMcpTool({
|
|
627
|
+
name: "artifactly_update_artifact",
|
|
628
|
+
credentialPath,
|
|
629
|
+
arguments: { artifactId: "artifact_1", filePath: pdfPath, mimeType: "application/pdf" },
|
|
630
|
+
requestJson: async (_method, _path, body) => {
|
|
631
|
+
bodySent = body;
|
|
632
|
+
return { artifact: { id: "artifact_1", kind: "file" } };
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
expect(bodySent).toMatchObject({
|
|
637
|
+
kind: "file",
|
|
638
|
+
storageUrl: "data:application/pdf;base64,JVBERi0xLjQ=",
|
|
639
|
+
});
|
|
640
|
+
rmSync(dir, { recursive: true, force: true });
|
|
641
|
+
});
|
|
508
642
|
});
|
|
@@ -35,6 +35,18 @@ type RequestJson = (
|
|
|
35
35
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
36
36
|
const MAX_UPLOAD_BYTES = 10_000_000;
|
|
37
37
|
|
|
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
|
+
|
|
38
50
|
function createDefaultRequestJson(apiBaseUrl: string): RequestJson {
|
|
39
51
|
return async (method, path, body, options) => {
|
|
40
52
|
const response = await fetch(`${apiBaseUrl}${path}`, {
|
|
@@ -55,7 +67,7 @@ function createDefaultRequestJson(apiBaseUrl: string): RequestJson {
|
|
|
55
67
|
typeof payload.error === "string"
|
|
56
68
|
? payload.error
|
|
57
69
|
: `Artifactly API request failed with ${response.status}`;
|
|
58
|
-
throw new
|
|
70
|
+
throw new ArtifactlyApiError(message, response.status, payload);
|
|
59
71
|
}
|
|
60
72
|
return payload;
|
|
61
73
|
};
|
|
@@ -77,6 +89,14 @@ function optionalStringArg(
|
|
|
77
89
|
return typeof value === "string" ? value.trim() : undefined;
|
|
78
90
|
}
|
|
79
91
|
|
|
92
|
+
function optionalRawStringArg(
|
|
93
|
+
args: Record<string, unknown> | undefined,
|
|
94
|
+
key: string,
|
|
95
|
+
): string | undefined {
|
|
96
|
+
const value = args?.[key];
|
|
97
|
+
return typeof value === "string" ? value : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
function requireStringArg(
|
|
81
101
|
args: Record<string, unknown> | undefined,
|
|
82
102
|
key: string,
|
|
@@ -127,6 +147,9 @@ function booleanArg(args: Record<string, unknown> | undefined, key: string): boo
|
|
|
127
147
|
function readFileForPublish(path: string): Buffer {
|
|
128
148
|
try {
|
|
129
149
|
const stats = statSync(path);
|
|
150
|
+
if (!stats.isFile()) {
|
|
151
|
+
throw new Error(`Path must point to a file: ${path}`);
|
|
152
|
+
}
|
|
130
153
|
if (stats.size > MAX_UPLOAD_BYTES) {
|
|
131
154
|
throw new Error(
|
|
132
155
|
`File is too large: ${path}. Maximum supported upload size is 10 MB.`,
|
|
@@ -157,6 +180,10 @@ function inferMimeType(path: string): string {
|
|
|
157
180
|
if (extension === ".json") return "application/json";
|
|
158
181
|
if (extension === ".md" || extension === ".markdown") return "text/markdown";
|
|
159
182
|
if (extension === ".txt") return "text/plain";
|
|
183
|
+
if (extension === ".png") return "image/png";
|
|
184
|
+
if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
|
|
185
|
+
if (extension === ".gif") return "image/gif";
|
|
186
|
+
if (extension === ".webp") return "image/webp";
|
|
160
187
|
return "application/octet-stream";
|
|
161
188
|
}
|
|
162
189
|
|
|
@@ -165,6 +192,7 @@ function kindForMime(_path: string, mimeType: string) {
|
|
|
165
192
|
if (normalized.includes("html")) return "html";
|
|
166
193
|
if (normalized.includes("json")) return "json";
|
|
167
194
|
if (normalized.includes("markdown")) return "markdown";
|
|
195
|
+
if (normalized.startsWith("image/")) return "image";
|
|
168
196
|
return "file";
|
|
169
197
|
}
|
|
170
198
|
|
|
@@ -250,10 +278,10 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
250
278
|
},
|
|
251
279
|
{
|
|
252
280
|
name: "artifactly_get_artifact",
|
|
253
|
-
description: "Get Artifactly artifact
|
|
281
|
+
description: "Get an Artifactly artifact. Returns full content by default, including base64/data URL file payloads; pass metadataOnly: true to omit content and storageUrl.",
|
|
254
282
|
inputSchema: {
|
|
255
283
|
type: "object",
|
|
256
|
-
properties: { artifactId: { type: "string" } },
|
|
284
|
+
properties: { artifactId: { type: "string" }, metadataOnly: { type: "boolean" } },
|
|
257
285
|
required: ["artifactId"],
|
|
258
286
|
},
|
|
259
287
|
},
|
|
@@ -268,6 +296,8 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
268
296
|
title: { type: "string" },
|
|
269
297
|
markdown: { type: "string" },
|
|
270
298
|
content: { type: "string" },
|
|
299
|
+
filePath: { type: "string" },
|
|
300
|
+
mimeType: { type: "string" },
|
|
271
301
|
description: { type: "string" },
|
|
272
302
|
visibility: { type: "string", enum: ["public", "unlisted", "private", "gated"] },
|
|
273
303
|
accessList: { type: "array", items: { type: "string" } },
|
|
@@ -295,6 +325,7 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
295
325
|
title: { type: "string" },
|
|
296
326
|
mimeType: { type: "string" },
|
|
297
327
|
visibility: { type: "string", enum: ["public", "unlisted", "private", "gated"] },
|
|
328
|
+
accessList: { type: "array", items: { type: "string" } },
|
|
298
329
|
},
|
|
299
330
|
required: ["path"],
|
|
300
331
|
},
|
|
@@ -416,7 +447,15 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
416
447
|
throw new Error(LOGIN_GUIDANCE);
|
|
417
448
|
}
|
|
418
449
|
const artifactId = requireStringArg(input.arguments, "artifactId");
|
|
419
|
-
|
|
450
|
+
const metadataOnly = booleanArg(input.arguments, "metadataOnly");
|
|
451
|
+
const result = objectResult(await requestJson("GET", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
|
|
452
|
+
if (metadataOnly && result.artifact && typeof result.artifact === "object" && !Array.isArray(result.artifact)) {
|
|
453
|
+
const { content: _content, storageUrl: _storageUrl, ...metadata } = result.artifact as Record<string, unknown>;
|
|
454
|
+
void _content;
|
|
455
|
+
void _storageUrl;
|
|
456
|
+
return { ...result, artifact: metadata };
|
|
457
|
+
}
|
|
458
|
+
return result;
|
|
420
459
|
}
|
|
421
460
|
|
|
422
461
|
if (input.name === "artifactly_update_artifact") {
|
|
@@ -425,22 +464,41 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
425
464
|
}
|
|
426
465
|
const artifactId = requireStringArg(input.arguments, "artifactId");
|
|
427
466
|
const title = stringArg(input.arguments, "title");
|
|
428
|
-
const markdown =
|
|
429
|
-
const content =
|
|
467
|
+
const markdown = optionalRawStringArg(input.arguments, "markdown");
|
|
468
|
+
const content = optionalRawStringArg(input.arguments, "content");
|
|
469
|
+
const filePath = stringArg(input.arguments, "filePath");
|
|
430
470
|
if (markdown !== undefined && content !== undefined) {
|
|
431
471
|
throw new Error("Use either markdown or content, not both.");
|
|
432
472
|
}
|
|
473
|
+
if ((markdown !== undefined || content !== undefined) && filePath) {
|
|
474
|
+
throw new Error("Use either filePath or text content, not both.");
|
|
475
|
+
}
|
|
476
|
+
const textContent = markdown ?? content;
|
|
477
|
+
if (textContent !== undefined && !textContent.trim()) {
|
|
478
|
+
throw new Error("Content cannot be whitespace-only.");
|
|
479
|
+
}
|
|
433
480
|
const description = optionalStringArg(input.arguments, "description");
|
|
434
481
|
const visibility = stringArg(input.arguments, "visibility");
|
|
435
482
|
const accessList = stringArrayArg(input.arguments, "accessList");
|
|
483
|
+
let replacement: Record<string, unknown> = {};
|
|
484
|
+
if (filePath) {
|
|
485
|
+
const mimeType = stringArg(input.arguments, "mimeType") ?? inferMimeType(filePath);
|
|
486
|
+
const kind = kindForMime(filePath, mimeType);
|
|
487
|
+
const file = readFileForPublish(filePath);
|
|
488
|
+
replacement = {
|
|
489
|
+
kind,
|
|
490
|
+
storageUrl: `data:${mimeType};base64,${file.toString("base64")}`,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
436
493
|
return objectResult(
|
|
437
494
|
await requestJson(
|
|
438
495
|
"PATCH",
|
|
439
496
|
`/api/artifacts/${artifactId}`,
|
|
440
497
|
{
|
|
441
498
|
...(title ? { title } : {}),
|
|
442
|
-
...(markdown ? { markdown } : {}),
|
|
443
|
-
...(content ? { content } : {}),
|
|
499
|
+
...(markdown !== undefined ? { markdown } : {}),
|
|
500
|
+
...(content !== undefined ? { content } : {}),
|
|
501
|
+
...replacement,
|
|
444
502
|
...(description !== undefined ? { description } : {}),
|
|
445
503
|
...(visibility ? { visibility } : {}),
|
|
446
504
|
...(accessList ? { accessList } : {}),
|
|
@@ -466,21 +524,25 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
466
524
|
const title = stringArg(input.arguments, "title") ?? titleFromPath(path);
|
|
467
525
|
const mimeType = stringArg(input.arguments, "mimeType") ?? inferMimeType(path);
|
|
468
526
|
const visibility = stringArg(input.arguments, "visibility") ?? "unlisted";
|
|
527
|
+
const accessList = stringArrayArg(input.arguments, "accessList");
|
|
469
528
|
const kind = kindForMime(path, mimeType);
|
|
470
529
|
const file = readFileForPublish(path);
|
|
530
|
+
const fileLike = kind === "file" || kind === "image";
|
|
471
531
|
const payload =
|
|
472
|
-
|
|
532
|
+
fileLike
|
|
473
533
|
? {
|
|
474
534
|
title,
|
|
475
535
|
kind,
|
|
476
536
|
storageUrl: `data:${mimeType};base64,${file.toString("base64")}`,
|
|
477
537
|
visibility,
|
|
538
|
+
...(accessList ? { accessList } : {}),
|
|
478
539
|
}
|
|
479
540
|
: {
|
|
480
541
|
title,
|
|
481
542
|
kind,
|
|
482
543
|
content: file.toString("utf8"),
|
|
483
544
|
visibility,
|
|
545
|
+
...(accessList ? { accessList } : {}),
|
|
484
546
|
};
|
|
485
547
|
return objectResult(
|
|
486
548
|
await postJson("/api/artifacts", payload, { apiKey: credentials.apiKey }),
|