@artifactly/mcp 0.1.4 → 0.1.6
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 +12 -0
- package/package.json +2 -1
- package/src/cli/artifactly-cli.test.ts +21 -1
- package/src/cli/artifactly-cli.ts +12 -2
- package/src/mcp/artifactly-mcp-server.test.ts +5 -0
- package/src/mcp/artifactly-mcp-server.ts +23 -0
- package/src/mcp/artifactly-mcp.test.ts +165 -1
- package/src/mcp/artifactly-mcp.ts +197 -8
package/README.md
CHANGED
|
@@ -23,6 +23,18 @@ npm run build
|
|
|
23
23
|
|
|
24
24
|
The MCP package is published as `@artifactly/mcp`.
|
|
25
25
|
|
|
26
|
+
Codex and other MCP clients can launch the server with either of these forms:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx -y @artifactly/mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx -y --package=@artifactly/mcp artifactly-mcp
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The package also exposes a human CLI as `artifactly`.
|
|
37
|
+
|
|
26
38
|
Manual publish from a logged-in maintainer account:
|
|
27
39
|
|
|
28
40
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artifactly/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"bin",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"bin": {
|
|
12
12
|
"artifactly": "bin/artifactly.mjs",
|
|
13
|
+
"mcp": "bin/artifactly-mcp.mjs",
|
|
13
14
|
"artifactly-mcp": "bin/artifactly-mcp.mjs"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
@@ -82,6 +82,26 @@ describe("artifactly CLI", () => {
|
|
|
82
82
|
rmSync(dir, { recursive: true, force: true });
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
it("logs out by removing stored credentials", async () => {
|
|
86
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-cli-"));
|
|
87
|
+
const credentialPath = join(dir, "credentials.json");
|
|
88
|
+
const output: string[] = [];
|
|
89
|
+
const cli = createArtifactlyCli({
|
|
90
|
+
credentialPath,
|
|
91
|
+
stdout: (line) => output.push(line),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await cli.run(["login", "--api-key", "art_test_secret", "--email", "agent@example.com"]);
|
|
95
|
+
expect(existsSync(credentialPath)).toBe(true);
|
|
96
|
+
|
|
97
|
+
await cli.run(["logout"]);
|
|
98
|
+
|
|
99
|
+
expect(existsSync(credentialPath)).toBe(false);
|
|
100
|
+
expect(readCredentials(credentialPath)).toBeNull();
|
|
101
|
+
expect(output.at(-1)).toBe("Logged out of Artifactly.");
|
|
102
|
+
rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
});
|
|
104
|
+
|
|
85
105
|
it("reports the default credential path under the user home", () => {
|
|
86
106
|
expect(getDefaultCredentialPath("/Users/alice")).toBe(
|
|
87
107
|
"/Users/alice/.artifactly/credentials.json",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { 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";
|
|
@@ -73,6 +73,10 @@ export function writeCredentials(
|
|
|
73
73
|
writeFileSync(path, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 0o600 });
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export function clearCredentials(path = getDefaultCredentialPath()) {
|
|
77
|
+
rmSync(path, { force: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
76
80
|
async function defaultPrompt(question: string) {
|
|
77
81
|
const readline = createInterface({ input, output });
|
|
78
82
|
try {
|
|
@@ -166,6 +170,11 @@ export function createArtifactlyCli(options: ArtifactlyCliOptions = {}) {
|
|
|
166
170
|
stdout(`Workspace: ${credentials.workspace ?? "unknown"}`);
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
function logout() {
|
|
174
|
+
clearCredentials(credentialPath);
|
|
175
|
+
stdout("Logged out of Artifactly.");
|
|
176
|
+
}
|
|
177
|
+
|
|
169
178
|
async function publish(argv: string[]) {
|
|
170
179
|
const credentials = readCredentials(credentialPath);
|
|
171
180
|
if (!credentials) {
|
|
@@ -206,10 +215,11 @@ export function createArtifactlyCli(options: ArtifactlyCliOptions = {}) {
|
|
|
206
215
|
const [command, ...rest] = argv;
|
|
207
216
|
try {
|
|
208
217
|
if (command === "login") return await login(rest);
|
|
218
|
+
if (command === "logout") return logout();
|
|
209
219
|
if (command === "whoami") return whoami();
|
|
210
220
|
if (command === "publish") return await publish(rest);
|
|
211
221
|
if (command === "mcp") return await mcp(rest);
|
|
212
|
-
stdout("Usage: artifactly <login|whoami|publish|mcp>");
|
|
222
|
+
stdout("Usage: artifactly <login|logout|whoami|publish|mcp>");
|
|
213
223
|
} catch (error) {
|
|
214
224
|
const message = error instanceof Error ? error.message : String(error);
|
|
215
225
|
stderr(message);
|
|
@@ -11,6 +11,7 @@ describe("Artifactly MCP server packaging", () => {
|
|
|
11
11
|
it("exposes installable CLI bins for humans and MCP clients", () => {
|
|
12
12
|
expect(packageJson.bin).toEqual({
|
|
13
13
|
artifactly: "bin/artifactly.mjs",
|
|
14
|
+
mcp: "bin/artifactly-mcp.mjs",
|
|
14
15
|
"artifactly-mcp": "bin/artifactly-mcp.mjs",
|
|
15
16
|
});
|
|
16
17
|
expect(packageJson.dependencies).toHaveProperty("@modelcontextprotocol/sdk");
|
|
@@ -41,9 +42,13 @@ describe("Artifactly MCP stdio server helpers", () => {
|
|
|
41
42
|
"artifactly_auth_status",
|
|
42
43
|
"artifactly_request_login_code",
|
|
43
44
|
"artifactly_verify_login_code",
|
|
45
|
+
"artifactly_logout",
|
|
44
46
|
"artifactly_publish_markdown",
|
|
45
47
|
"artifactly_list_artifacts",
|
|
46
48
|
"artifactly_get_artifact",
|
|
49
|
+
"artifactly_update_artifact",
|
|
50
|
+
"artifactly_delete_artifact",
|
|
51
|
+
"artifactly_publish_file",
|
|
47
52
|
]);
|
|
48
53
|
});
|
|
49
54
|
|
|
@@ -92,6 +92,7 @@ const verifyLoginCodeSchema = z.object({
|
|
|
92
92
|
email: z.string().email(),
|
|
93
93
|
code: z.string().min(4),
|
|
94
94
|
});
|
|
95
|
+
const logoutSchema = z.object({});
|
|
95
96
|
const publishMarkdownSchema = z.object({
|
|
96
97
|
title: z.string().min(1),
|
|
97
98
|
markdown: z.string().min(1),
|
|
@@ -101,14 +102,36 @@ const listArtifactsSchema = z.object({});
|
|
|
101
102
|
const getArtifactSchema = z.object({
|
|
102
103
|
artifactId: z.string().min(1),
|
|
103
104
|
});
|
|
105
|
+
const updateArtifactSchema = z.object({
|
|
106
|
+
artifactId: z.string().min(1),
|
|
107
|
+
title: z.string().min(1).optional(),
|
|
108
|
+
markdown: z.string().min(1).optional(),
|
|
109
|
+
content: z.string().min(1).optional(),
|
|
110
|
+
description: z.string().min(1).optional(),
|
|
111
|
+
visibility: z.enum(["public", "unlisted", "private", "gated"]).optional(),
|
|
112
|
+
accessList: z.array(z.string().email()).optional(),
|
|
113
|
+
});
|
|
114
|
+
const deleteArtifactSchema = z.object({
|
|
115
|
+
artifactId: z.string().min(1),
|
|
116
|
+
});
|
|
117
|
+
const publishFileSchema = z.object({
|
|
118
|
+
path: z.string().min(1),
|
|
119
|
+
title: z.string().min(1).optional(),
|
|
120
|
+
mimeType: z.string().min(1).optional(),
|
|
121
|
+
visibility: z.enum(["public", "unlisted", "private", "gated"]).optional(),
|
|
122
|
+
});
|
|
104
123
|
|
|
105
124
|
const toolSchemas = {
|
|
106
125
|
artifactly_auth_status: authStatusSchema,
|
|
107
126
|
artifactly_request_login_code: requestLoginCodeSchema,
|
|
108
127
|
artifactly_verify_login_code: verifyLoginCodeSchema,
|
|
128
|
+
artifactly_logout: logoutSchema,
|
|
109
129
|
artifactly_publish_markdown: publishMarkdownSchema,
|
|
110
130
|
artifactly_list_artifacts: listArtifactsSchema,
|
|
111
131
|
artifactly_get_artifact: getArtifactSchema,
|
|
132
|
+
artifactly_update_artifact: updateArtifactSchema,
|
|
133
|
+
artifactly_delete_artifact: deleteArtifactSchema,
|
|
134
|
+
artifactly_publish_file: publishFileSchema,
|
|
112
135
|
} as const;
|
|
113
136
|
|
|
114
137
|
export function createArtifactlyMcpServer() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
@@ -13,9 +13,13 @@ describe("Artifactly MCP tools", () => {
|
|
|
13
13
|
"artifactly_auth_status",
|
|
14
14
|
"artifactly_request_login_code",
|
|
15
15
|
"artifactly_verify_login_code",
|
|
16
|
+
"artifactly_logout",
|
|
16
17
|
"artifactly_publish_markdown",
|
|
17
18
|
"artifactly_list_artifacts",
|
|
18
19
|
"artifactly_get_artifact",
|
|
20
|
+
"artifactly_update_artifact",
|
|
21
|
+
"artifactly_delete_artifact",
|
|
22
|
+
"artifactly_publish_file",
|
|
19
23
|
]);
|
|
20
24
|
});
|
|
21
25
|
|
|
@@ -44,6 +48,30 @@ describe("Artifactly MCP tools", () => {
|
|
|
44
48
|
rmSync(dir, { recursive: true, force: true });
|
|
45
49
|
});
|
|
46
50
|
|
|
51
|
+
it("logs out by clearing the shared credential store", async () => {
|
|
52
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
53
|
+
const credentialPath = join(dir, "credentials.json");
|
|
54
|
+
writeCredentials(
|
|
55
|
+
{
|
|
56
|
+
apiBaseUrl: "https://artifactly.example",
|
|
57
|
+
apiKey: "art_t...et",
|
|
58
|
+
email: "agent@example.com",
|
|
59
|
+
workspace: "agent",
|
|
60
|
+
},
|
|
61
|
+
credentialPath,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
await expect(
|
|
65
|
+
callArtifactlyMcpTool({ name: "artifactly_logout", credentialPath }),
|
|
66
|
+
).resolves.toEqual({ ok: true, authenticated: false });
|
|
67
|
+
expect(existsSync(credentialPath)).toBe(false);
|
|
68
|
+
await expect(
|
|
69
|
+
callArtifactlyMcpTool({ name: "artifactly_auth_status", credentialPath }),
|
|
70
|
+
).resolves.toEqual({ authenticated: false, apiBaseUrl: "https://artifactly.io" });
|
|
71
|
+
|
|
72
|
+
rmSync(dir, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
47
75
|
it("publishes markdown through the Artifactly API without returning secrets", async () => {
|
|
48
76
|
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
49
77
|
const credentialPath = join(dir, "credentials.json");
|
|
@@ -88,4 +116,140 @@ describe("Artifactly MCP tools", () => {
|
|
|
88
116
|
|
|
89
117
|
rmSync(dir, { recursive: true, force: true });
|
|
90
118
|
});
|
|
119
|
+
|
|
120
|
+
it("lists, gets, updates, and deletes artifacts through authenticated API requests", async () => {
|
|
121
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
122
|
+
const credentialPath = join(dir, "credentials.json");
|
|
123
|
+
const requests: Array<{ method: string; path: string; body?: unknown; apiKey?: string }> = [];
|
|
124
|
+
writeCredentials(
|
|
125
|
+
{
|
|
126
|
+
apiBaseUrl: "https://artifactly.example",
|
|
127
|
+
apiKey: "art_t...et",
|
|
128
|
+
email: "agent@example.com",
|
|
129
|
+
workspace: "agent",
|
|
130
|
+
},
|
|
131
|
+
credentialPath,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const requestJson = async (method: string, path: string, body?: unknown, options?: { apiKey?: string }) => {
|
|
135
|
+
requests.push({ method, path, body, apiKey: options?.apiKey });
|
|
136
|
+
if (method === "GET" && path === "/api/artifacts") {
|
|
137
|
+
return { artifacts: [{ id: "artifact_1", title: "Hello" }] };
|
|
138
|
+
}
|
|
139
|
+
if (method === "GET" && path === "/api/artifacts/artifact_1") {
|
|
140
|
+
return { artifact: { id: "artifact_1", title: "Hello" } };
|
|
141
|
+
}
|
|
142
|
+
if (method === "PATCH" && path === "/api/artifacts/artifact_1") {
|
|
143
|
+
return { artifact: { id: "artifact_1", title: "Updated", visibility: "private" } };
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, artifactId: "artifact_1" };
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
await expect(
|
|
149
|
+
callArtifactlyMcpTool({ name: "artifactly_list_artifacts", credentialPath, requestJson }),
|
|
150
|
+
).resolves.toEqual({ artifacts: [{ id: "artifact_1", title: "Hello" }] });
|
|
151
|
+
await expect(
|
|
152
|
+
callArtifactlyMcpTool({
|
|
153
|
+
name: "artifactly_get_artifact",
|
|
154
|
+
credentialPath,
|
|
155
|
+
arguments: { artifactId: "artifact_1" },
|
|
156
|
+
requestJson,
|
|
157
|
+
}),
|
|
158
|
+
).resolves.toEqual({ artifact: { id: "artifact_1", title: "Hello" } });
|
|
159
|
+
await expect(
|
|
160
|
+
callArtifactlyMcpTool({
|
|
161
|
+
name: "artifactly_update_artifact",
|
|
162
|
+
credentialPath,
|
|
163
|
+
arguments: {
|
|
164
|
+
artifactId: "artifact_1",
|
|
165
|
+
title: "Updated",
|
|
166
|
+
visibility: "private",
|
|
167
|
+
accessList: ["agent@example.com"],
|
|
168
|
+
},
|
|
169
|
+
requestJson,
|
|
170
|
+
}),
|
|
171
|
+
).resolves.toEqual({ artifact: { id: "artifact_1", title: "Updated", visibility: "private" } });
|
|
172
|
+
await expect(
|
|
173
|
+
callArtifactlyMcpTool({
|
|
174
|
+
name: "artifactly_delete_artifact",
|
|
175
|
+
credentialPath,
|
|
176
|
+
arguments: { artifactId: "artifact_1" },
|
|
177
|
+
requestJson,
|
|
178
|
+
}),
|
|
179
|
+
).resolves.toEqual({ ok: true, artifactId: "artifact_1" });
|
|
180
|
+
|
|
181
|
+
expect(requests).toEqual([
|
|
182
|
+
{ method: "GET", path: "/api/artifacts", apiKey: "art_t...et", body: undefined },
|
|
183
|
+
{ method: "GET", path: "/api/artifacts/artifact_1", apiKey: "art_t...et", body: undefined },
|
|
184
|
+
{
|
|
185
|
+
method: "PATCH",
|
|
186
|
+
path: "/api/artifacts/artifact_1",
|
|
187
|
+
apiKey: "art_t...et",
|
|
188
|
+
body: { title: "Updated", visibility: "private", accessList: ["agent@example.com"] },
|
|
189
|
+
},
|
|
190
|
+
{ method: "DELETE", path: "/api/artifacts/artifact_1", apiKey: "art_t...et", body: undefined },
|
|
191
|
+
]);
|
|
192
|
+
rmSync(dir, { recursive: true, force: true });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("publishes local HTML and PDF files through the artifact API", async () => {
|
|
196
|
+
const dir = mkdtempSync(join(tmpdir(), "artifactly-mcp-"));
|
|
197
|
+
const credentialPath = join(dir, "credentials.json");
|
|
198
|
+
const htmlPath = join(dir, "hello.html");
|
|
199
|
+
const pdfPath = join(dir, "hello.pdf");
|
|
200
|
+
const requests: Array<{ path: string; body: unknown; apiKey?: string }> = [];
|
|
201
|
+
writeFileSync(htmlPath, "<h1>Hello</h1>");
|
|
202
|
+
writeFileSync(pdfPath, Buffer.from("%PDF-1.4\nhello"));
|
|
203
|
+
writeCredentials(
|
|
204
|
+
{
|
|
205
|
+
apiBaseUrl: "https://artifactly.example",
|
|
206
|
+
apiKey: "art_t...et",
|
|
207
|
+
email: "agent@example.com",
|
|
208
|
+
workspace: "agent",
|
|
209
|
+
},
|
|
210
|
+
credentialPath,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const postJson = async (path: string, body: unknown, options?: { apiKey?: string }) => {
|
|
214
|
+
requests.push({ path, body, apiKey: options?.apiKey });
|
|
215
|
+
return { id: "artifact_1", url: "https://artifactly.example/share/agent/hello" };
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
await callArtifactlyMcpTool({
|
|
219
|
+
name: "artifactly_publish_file",
|
|
220
|
+
credentialPath,
|
|
221
|
+
arguments: { path: htmlPath, visibility: "public" },
|
|
222
|
+
postJson,
|
|
223
|
+
});
|
|
224
|
+
await callArtifactlyMcpTool({
|
|
225
|
+
name: "artifactly_publish_file",
|
|
226
|
+
credentialPath,
|
|
227
|
+
arguments: { path: pdfPath, title: "PDF Hello", mimeType: "application/pdf" },
|
|
228
|
+
postJson,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(requests).toEqual([
|
|
232
|
+
{
|
|
233
|
+
path: "/api/artifacts",
|
|
234
|
+
apiKey: "art_t...et",
|
|
235
|
+
body: {
|
|
236
|
+
title: "hello",
|
|
237
|
+
kind: "html",
|
|
238
|
+
content: "<h1>Hello</h1>",
|
|
239
|
+
visibility: "public",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
path: "/api/artifacts",
|
|
244
|
+
apiKey: "art_t...et",
|
|
245
|
+
body: {
|
|
246
|
+
title: "PDF Hello",
|
|
247
|
+
kind: "file",
|
|
248
|
+
storageUrl: "data:application/pdf;base64,JVBERi0xLjQKaGVsbG8=",
|
|
249
|
+
visibility: "unlisted",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
rmSync(dir, { recursive: true, force: true });
|
|
254
|
+
});
|
|
91
255
|
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
|
|
1
4
|
import {
|
|
5
|
+
clearCredentials,
|
|
2
6
|
createDefaultPostJson,
|
|
3
7
|
getDefaultCredentialPath,
|
|
4
8
|
readCredentials,
|
|
@@ -18,8 +22,41 @@ type CallToolInput = {
|
|
|
18
22
|
credentialPath?: string;
|
|
19
23
|
apiBaseUrl?: string;
|
|
20
24
|
postJson?: PostJson;
|
|
25
|
+
requestJson?: RequestJson;
|
|
21
26
|
};
|
|
22
27
|
|
|
28
|
+
type RequestJson = (
|
|
29
|
+
method: string,
|
|
30
|
+
path: string,
|
|
31
|
+
body?: unknown,
|
|
32
|
+
options?: { apiKey?: string },
|
|
33
|
+
) => Promise<unknown>;
|
|
34
|
+
|
|
35
|
+
function createDefaultRequestJson(apiBaseUrl: string): RequestJson {
|
|
36
|
+
return async (method, path, body, options) => {
|
|
37
|
+
const response = await fetch(`${apiBaseUrl}${path}`, {
|
|
38
|
+
method,
|
|
39
|
+
headers: {
|
|
40
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
41
|
+
...(options?.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {}),
|
|
42
|
+
},
|
|
43
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
44
|
+
});
|
|
45
|
+
const payload = (await response.json().catch(() => ({}))) as unknown;
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const message =
|
|
48
|
+
typeof payload === "object" &&
|
|
49
|
+
payload !== null &&
|
|
50
|
+
"error" in payload &&
|
|
51
|
+
typeof payload.error === "string"
|
|
52
|
+
? payload.error
|
|
53
|
+
: `Artifactly API request failed with ${response.status}`;
|
|
54
|
+
throw new Error(message);
|
|
55
|
+
}
|
|
56
|
+
return payload;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
23
60
|
function stringArg(
|
|
24
61
|
args: Record<string, unknown> | undefined,
|
|
25
62
|
key: string,
|
|
@@ -39,6 +76,17 @@ function requireStringArg(
|
|
|
39
76
|
return value;
|
|
40
77
|
}
|
|
41
78
|
|
|
79
|
+
function stringArrayArg(
|
|
80
|
+
args: Record<string, unknown> | undefined,
|
|
81
|
+
key: string,
|
|
82
|
+
): string[] | undefined {
|
|
83
|
+
const value = args?.[key];
|
|
84
|
+
if (!Array.isArray(value)) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return value.filter((item): item is string => typeof item === "string");
|
|
88
|
+
}
|
|
89
|
+
|
|
42
90
|
function objectResult(value: unknown): Record<string, unknown> {
|
|
43
91
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
44
92
|
throw new Error("Artifactly API returned an unexpected response.");
|
|
@@ -46,6 +94,30 @@ function objectResult(value: unknown): Record<string, unknown> {
|
|
|
46
94
|
return value as Record<string, unknown>;
|
|
47
95
|
}
|
|
48
96
|
|
|
97
|
+
function titleFromPath(path: string): string {
|
|
98
|
+
const name = basename(path, extname(path));
|
|
99
|
+
return name || "artifact";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function inferMimeType(path: string): string {
|
|
103
|
+
const extension = extname(path).toLowerCase();
|
|
104
|
+
if (extension === ".pdf") return "application/pdf";
|
|
105
|
+
if (extension === ".html" || extension === ".htm") return "text/html";
|
|
106
|
+
if (extension === ".json") return "application/json";
|
|
107
|
+
if (extension === ".md" || extension === ".markdown") return "text/markdown";
|
|
108
|
+
if (extension === ".txt") return "text/plain";
|
|
109
|
+
return "application/octet-stream";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function kindForMime(path: string, mimeType: string) {
|
|
113
|
+
const extension = extname(path).toLowerCase();
|
|
114
|
+
const normalized = mimeType.toLowerCase();
|
|
115
|
+
if (normalized.includes("html") || extension === ".html" || extension === ".htm") return "html";
|
|
116
|
+
if (normalized.includes("json") || extension === ".json") return "json";
|
|
117
|
+
if (normalized.includes("markdown") || extension === ".md" || extension === ".markdown") return "markdown";
|
|
118
|
+
return "file";
|
|
119
|
+
}
|
|
120
|
+
|
|
49
121
|
export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
50
122
|
return [
|
|
51
123
|
{
|
|
@@ -72,6 +144,11 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
72
144
|
required: ["email", "code"],
|
|
73
145
|
},
|
|
74
146
|
},
|
|
147
|
+
{
|
|
148
|
+
name: "artifactly_logout",
|
|
149
|
+
description: "Log out by clearing the local Artifactly credential store.",
|
|
150
|
+
inputSchema: { type: "object", properties: {} },
|
|
151
|
+
},
|
|
75
152
|
{
|
|
76
153
|
name: "artifactly_publish_markdown",
|
|
77
154
|
description: "Publish markdown through the Artifactly API using stored credentials.",
|
|
@@ -99,6 +176,46 @@ export function listArtifactlyMcpTools(): ArtifactlyMcpTool[] {
|
|
|
99
176
|
required: ["artifactId"],
|
|
100
177
|
},
|
|
101
178
|
},
|
|
179
|
+
{
|
|
180
|
+
name: "artifactly_update_artifact",
|
|
181
|
+
description: "Update an existing Artifactly artifact, including visibility and gated access emails.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
artifactId: { type: "string" },
|
|
186
|
+
title: { type: "string" },
|
|
187
|
+
markdown: { type: "string" },
|
|
188
|
+
content: { type: "string" },
|
|
189
|
+
description: { type: "string" },
|
|
190
|
+
visibility: { type: "string", enum: ["public", "unlisted", "private", "gated"] },
|
|
191
|
+
accessList: { type: "array", items: { type: "string" } },
|
|
192
|
+
},
|
|
193
|
+
required: ["artifactId"],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "artifactly_delete_artifact",
|
|
198
|
+
description: "Delete an existing Artifactly artifact owned by the authenticated account.",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: { artifactId: { type: "string" } },
|
|
202
|
+
required: ["artifactId"],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "artifactly_publish_file",
|
|
207
|
+
description: "Publish a local PDF, HTML, JSON, Markdown, or other file through the Artifactly API.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
path: { type: "string" },
|
|
212
|
+
title: { type: "string" },
|
|
213
|
+
mimeType: { type: "string" },
|
|
214
|
+
visibility: { type: "string", enum: ["public", "unlisted", "private", "gated"] },
|
|
215
|
+
},
|
|
216
|
+
required: ["path"],
|
|
217
|
+
},
|
|
218
|
+
},
|
|
102
219
|
];
|
|
103
220
|
}
|
|
104
221
|
|
|
@@ -107,6 +224,7 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
107
224
|
const credentials = readCredentials(credentialPath);
|
|
108
225
|
const apiBaseUrl = input.apiBaseUrl ?? credentials?.apiBaseUrl ?? "https://artifactly.io";
|
|
109
226
|
const postJson = input.postJson ?? createDefaultPostJson(apiBaseUrl);
|
|
227
|
+
const requestJson = input.requestJson ?? createDefaultRequestJson(apiBaseUrl);
|
|
110
228
|
|
|
111
229
|
if (input.name === "artifactly_auth_status") {
|
|
112
230
|
return credentials
|
|
@@ -155,6 +273,11 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
155
273
|
};
|
|
156
274
|
}
|
|
157
275
|
|
|
276
|
+
if (input.name === "artifactly_logout") {
|
|
277
|
+
clearCredentials(credentialPath);
|
|
278
|
+
return { ok: true, authenticated: false };
|
|
279
|
+
}
|
|
280
|
+
|
|
158
281
|
if (input.name === "artifactly_publish_markdown") {
|
|
159
282
|
if (!credentials) {
|
|
160
283
|
throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
|
|
@@ -172,17 +295,83 @@ export async function callArtifactlyMcpTool(input: CallToolInput) {
|
|
|
172
295
|
}
|
|
173
296
|
|
|
174
297
|
if (input.name === "artifactly_list_artifacts") {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
};
|
|
298
|
+
if (!credentials) {
|
|
299
|
+
throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
|
|
300
|
+
}
|
|
301
|
+
return objectResult(await requestJson("GET", "/api/artifacts", undefined, { apiKey: credentials.apiKey }));
|
|
179
302
|
}
|
|
180
303
|
|
|
181
304
|
if (input.name === "artifactly_get_artifact") {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
305
|
+
if (!credentials) {
|
|
306
|
+
throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
|
|
307
|
+
}
|
|
308
|
+
const artifactId = requireStringArg(input.arguments, "artifactId");
|
|
309
|
+
return objectResult(await requestJson("GET", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (input.name === "artifactly_update_artifact") {
|
|
313
|
+
if (!credentials) {
|
|
314
|
+
throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
|
|
315
|
+
}
|
|
316
|
+
const artifactId = requireStringArg(input.arguments, "artifactId");
|
|
317
|
+
const title = stringArg(input.arguments, "title");
|
|
318
|
+
const markdown = stringArg(input.arguments, "markdown");
|
|
319
|
+
const content = stringArg(input.arguments, "content");
|
|
320
|
+
const description = stringArg(input.arguments, "description");
|
|
321
|
+
const visibility = stringArg(input.arguments, "visibility");
|
|
322
|
+
const accessList = stringArrayArg(input.arguments, "accessList");
|
|
323
|
+
return objectResult(
|
|
324
|
+
await requestJson(
|
|
325
|
+
"PATCH",
|
|
326
|
+
`/api/artifacts/${artifactId}`,
|
|
327
|
+
{
|
|
328
|
+
...(title ? { title } : {}),
|
|
329
|
+
...(markdown ? { markdown } : {}),
|
|
330
|
+
...(content ? { content } : {}),
|
|
331
|
+
...(description ? { description } : {}),
|
|
332
|
+
...(visibility ? { visibility } : {}),
|
|
333
|
+
...(accessList ? { accessList } : {}),
|
|
334
|
+
},
|
|
335
|
+
{ apiKey: credentials.apiKey },
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (input.name === "artifactly_delete_artifact") {
|
|
341
|
+
if (!credentials) {
|
|
342
|
+
throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
|
|
343
|
+
}
|
|
344
|
+
const artifactId = requireStringArg(input.arguments, "artifactId");
|
|
345
|
+
return objectResult(await requestJson("DELETE", `/api/artifacts/${artifactId}`, undefined, { apiKey: credentials.apiKey }));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (input.name === "artifactly_publish_file") {
|
|
349
|
+
if (!credentials) {
|
|
350
|
+
throw new Error("Artifactly credentials are not configured. Run artifactly_request_login_code first.");
|
|
351
|
+
}
|
|
352
|
+
const path = requireStringArg(input.arguments, "path");
|
|
353
|
+
const title = stringArg(input.arguments, "title") ?? titleFromPath(path);
|
|
354
|
+
const mimeType = stringArg(input.arguments, "mimeType") ?? inferMimeType(path);
|
|
355
|
+
const visibility = stringArg(input.arguments, "visibility") ?? "unlisted";
|
|
356
|
+
const kind = kindForMime(path, mimeType);
|
|
357
|
+
const file = readFileSync(path);
|
|
358
|
+
const payload =
|
|
359
|
+
kind === "file"
|
|
360
|
+
? {
|
|
361
|
+
title,
|
|
362
|
+
kind,
|
|
363
|
+
storageUrl: `data:${mimeType};base64,${file.toString("base64")}`,
|
|
364
|
+
visibility,
|
|
365
|
+
}
|
|
366
|
+
: {
|
|
367
|
+
title,
|
|
368
|
+
kind,
|
|
369
|
+
content: file.toString("utf8"),
|
|
370
|
+
visibility,
|
|
371
|
+
};
|
|
372
|
+
return objectResult(
|
|
373
|
+
await postJson("/api/artifacts", payload, { apiKey: credentials.apiKey }),
|
|
374
|
+
);
|
|
186
375
|
}
|
|
187
376
|
|
|
188
377
|
throw new Error(`Unknown Artifactly MCP tool: ${input.name}`);
|