artifacty 0.2.0 → 0.4.0

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/src/mcp-server.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
2
4
  import readline from "node:readline";
5
+ import { fileURLToPath } from "node:url";
3
6
  import {
4
7
  ARTIFACT_FORMATS,
5
8
  ARTIFACT_TYPES,
@@ -8,7 +11,7 @@ import {
8
11
  createStore,
9
12
  getArtifact,
10
13
  listAuditEvents,
11
- listArtifacts,
14
+ listArtifactsPage,
12
15
  restoreArtifact,
13
16
  updateArtifact
14
17
  } from "./lib/storage.js";
@@ -16,6 +19,7 @@ import { convertAgentArtifact } from "./lib/converters.js";
16
19
  import { resolvePublicBaseUrl } from "./lib/server-state.js";
17
20
 
18
21
  const PROTOCOL_VERSION = "2025-06-18";
22
+ const PACKAGE_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
19
23
  const store = createStore();
20
24
 
21
25
  const nativeArtifactInputSchema = {
@@ -86,7 +90,8 @@ const tools = [
86
90
  tag: { type: "string" },
87
91
  sourceAgent: { type: "string" },
88
92
  includeArchived: { type: "boolean" },
89
- limit: { type: "number" }
93
+ limit: { type: "number" },
94
+ offset: { type: "number" }
90
95
  }
91
96
  },
92
97
  annotations: {
@@ -254,6 +259,71 @@ const tools = [
254
259
  }
255
260
  ];
256
261
 
262
+ const resourceTemplates = [
263
+ {
264
+ uriTemplate: "artifacty://artifacts/{id}",
265
+ name: "artifact-by-id",
266
+ title: "Artifact by ID",
267
+ description: "Read an Artifacty artifact with metadata, latest version, content, and browser URLs.",
268
+ mimeType: "application/json"
269
+ },
270
+ {
271
+ uriTemplate: "artifacty://artifacts/{id}/raw{?version}",
272
+ name: "artifact-raw-content",
273
+ title: "Artifact Raw Content",
274
+ description: "Read raw artifact content by ID and optional version.",
275
+ mimeType: "text/plain"
276
+ }
277
+ ];
278
+
279
+ const prompts = [
280
+ {
281
+ name: "artifacty_handoff",
282
+ title: "Create Artifact Handoff",
283
+ description: "Prepare a concise continuation artifact for another agent.",
284
+ arguments: [
285
+ { name: "goal", description: "Current goal or handoff objective.", required: false },
286
+ { name: "artifactId", description: "Existing Artifacty artifact to continue from.", required: false }
287
+ ]
288
+ },
289
+ {
290
+ name: "artifacty_review",
291
+ title: "Create Review Artifact",
292
+ description: "Capture code review findings as a shareable Artifacty artifact.",
293
+ arguments: [
294
+ { name: "scope", description: "Files, branch, PR, or behavior under review.", required: false },
295
+ { name: "artifactId", description: "Existing artifact with review context.", required: false }
296
+ ]
297
+ },
298
+ {
299
+ name: "artifacty_test_report",
300
+ title: "Create Test Report Artifact",
301
+ description: "Summarize verification commands, status, failures, and residual risk.",
302
+ arguments: [
303
+ { name: "goal", description: "Feature or release being verified.", required: false },
304
+ { name: "artifactId", description: "Existing artifact with implementation context.", required: false }
305
+ ]
306
+ },
307
+ {
308
+ name: "artifacty_visual_qa",
309
+ title: "Create Visual QA Artifact",
310
+ description: "Record browser, screenshot, media, or visual regression evidence.",
311
+ arguments: [
312
+ { name: "target", description: "URL, artifact ID, or UI surface under visual review.", required: false },
313
+ { name: "artifactId", description: "Existing visual evidence artifact.", required: false }
314
+ ]
315
+ },
316
+ {
317
+ name: "artifacty_release_notes",
318
+ title: "Create Release Notes Artifact",
319
+ description: "Draft release notes from changed artifacts, tests, and known risks.",
320
+ arguments: [
321
+ { name: "version", description: "Release version.", required: false },
322
+ { name: "artifactId", description: "Existing roadmap, checklist, or handoff artifact.", required: false }
323
+ ]
324
+ }
325
+ ];
326
+
257
327
  const rl = readline.createInterface({
258
328
  input: process.stdin,
259
329
  crlfDelay: Infinity
@@ -310,14 +380,21 @@ async function handleRequest(message) {
310
380
  capabilities: {
311
381
  tools: {
312
382
  listChanged: false
383
+ },
384
+ resources: {
385
+ subscribe: false,
386
+ listChanged: false
387
+ },
388
+ prompts: {
389
+ listChanged: false
313
390
  }
314
391
  },
315
392
  serverInfo: {
316
393
  name: "artifacty",
317
394
  title: "Artifacty",
318
- version: "0.1.0"
395
+ version: "0.4.0"
319
396
  },
320
- instructions: "Use Artifacty to create, import, list, read, and update local artifacts that other agents can reuse."
397
+ instructions: "Use Artifacty to create, import, list, read, update, and resource-read local artifacts that other agents can reuse."
321
398
  };
322
399
  }
323
400
 
@@ -334,6 +411,27 @@ async function handleRequest(message) {
334
411
  return callTool(params.name, params.arguments || {});
335
412
  }
336
413
 
414
+ if (message.method === "resources/list") {
415
+ return listResources();
416
+ }
417
+
418
+ if (message.method === "resources/templates/list") {
419
+ return { resourceTemplates };
420
+ }
421
+
422
+ if (message.method === "resources/read") {
423
+ return readResource(requireParam(message.params, "uri"));
424
+ }
425
+
426
+ if (message.method === "prompts/list") {
427
+ return { prompts };
428
+ }
429
+
430
+ if (message.method === "prompts/get") {
431
+ const params = message.params || {};
432
+ return getPrompt(requireParam(params, "name"), params.arguments || {});
433
+ }
434
+
337
435
  throw Object.assign(new Error(`Method not found: ${message.method}`), {
338
436
  jsonRpcCode: -32601
339
437
  });
@@ -346,12 +444,21 @@ async function callTool(name, args) {
346
444
 
347
445
  if (name === "artifacty_list") {
348
446
  const publicBaseUrl = await resolvePublicBaseUrl(store);
349
- const artifacts = await listArtifacts(store, args);
447
+ const page = await listArtifactsPage(store, args);
350
448
  return toolResult({
351
- artifacts: artifacts.map((artifact) => ({
449
+ artifacts: page.artifacts.map((artifact) => ({
352
450
  ...artifact,
353
451
  url: `${publicBaseUrl}/artifacts/${encodeURIComponent(artifact.id)}`
354
- }))
452
+ })),
453
+ pagination: {
454
+ total: page.total,
455
+ limit: page.limit,
456
+ offset: page.offset,
457
+ hasMore: page.hasMore,
458
+ nextOffset: page.nextOffset,
459
+ previousOffset: page.previousOffset
460
+ },
461
+ search: page.search
355
462
  });
356
463
  }
357
464
 
@@ -430,6 +537,177 @@ async function callTool(name, args) {
430
537
  });
431
538
  }
432
539
 
540
+ async function listResources() {
541
+ const page = await listArtifactsPage(store, { limit: 10 });
542
+ const artifactResources = page.artifacts.flatMap((artifact) => [
543
+ {
544
+ uri: artifactResourceUri(artifact.id),
545
+ name: `artifact:${artifact.id}`,
546
+ title: artifact.title,
547
+ description: `${artifact.sourceAgent} ${artifact.artifactType} artifact, v${artifact.latestVersion}`,
548
+ mimeType: "application/json"
549
+ },
550
+ {
551
+ uri: artifactRawResourceUri(artifact.id),
552
+ name: `artifact-raw:${artifact.id}`,
553
+ title: `${artifact.title} raw`,
554
+ description: `Raw latest ${artifact.format || "text"} content for ${artifact.id}`,
555
+ mimeType: artifact.contentType || "text/plain"
556
+ }
557
+ ]);
558
+
559
+ return {
560
+ resources: [
561
+ {
562
+ uri: "artifacty://recent",
563
+ name: "recent-artifacts",
564
+ title: "Recent Artifacts",
565
+ description: "Recent Artifacty artifacts with pagination metadata and browser URLs.",
566
+ mimeType: "application/json"
567
+ },
568
+ {
569
+ uri: "artifacty://schema/v1",
570
+ name: "artifact-schema-v1",
571
+ title: "Artifact Schema v1",
572
+ description: "Artifacty schema v1 reference document.",
573
+ mimeType: "text/markdown"
574
+ },
575
+ ...artifactResources
576
+ ]
577
+ };
578
+ }
579
+
580
+ async function readResource(uri) {
581
+ if (uri === "artifacty://recent") {
582
+ const publicBaseUrl = await resolvePublicBaseUrl(store);
583
+ const page = await listArtifactsPage(store, { limit: 20 });
584
+ return resourceText(uri, "application/json", {
585
+ artifacts: page.artifacts.map((artifact) => ({
586
+ ...artifact,
587
+ url: `${publicBaseUrl}/artifacts/${encodeURIComponent(artifact.id)}`
588
+ })),
589
+ pagination: {
590
+ total: page.total,
591
+ limit: page.limit,
592
+ offset: page.offset,
593
+ hasMore: page.hasMore,
594
+ nextOffset: page.nextOffset,
595
+ previousOffset: page.previousOffset
596
+ },
597
+ search: page.search
598
+ });
599
+ }
600
+
601
+ if (uri === "artifacty://schema/v1") {
602
+ return {
603
+ contents: [
604
+ {
605
+ uri,
606
+ mimeType: "text/markdown",
607
+ text: await readFile(path.join(PACKAGE_ROOT, "docs", "artifact-schema-v1.md"), "utf8")
608
+ }
609
+ ]
610
+ };
611
+ }
612
+
613
+ const parsed = parseArtifactResourceUri(uri);
614
+ if (parsed) {
615
+ const artifact = await getArtifact(store, parsed.id, {
616
+ version: parsed.version,
617
+ audit: mcpAuditContext()
618
+ });
619
+ if (parsed.raw) {
620
+ return {
621
+ contents: [
622
+ {
623
+ uri,
624
+ mimeType: artifact.version.contentType || "text/plain",
625
+ text: artifact.content
626
+ }
627
+ ]
628
+ };
629
+ }
630
+ return resourceText(uri, "application/json", await withUrls(artifact));
631
+ }
632
+
633
+ throw Object.assign(new Error(`Unknown resource: ${uri}`), {
634
+ jsonRpcCode: -32602
635
+ });
636
+ }
637
+
638
+ function getPrompt(name, args) {
639
+ const definition = prompts.find((prompt) => prompt.name === name);
640
+ if (!definition) {
641
+ throw Object.assign(new Error(`Unknown prompt: ${name}`), {
642
+ jsonRpcCode: -32602
643
+ });
644
+ }
645
+
646
+ return {
647
+ description: definition.description,
648
+ messages: [
649
+ {
650
+ role: "user",
651
+ content: {
652
+ type: "text",
653
+ text: promptText(name, args)
654
+ }
655
+ }
656
+ ]
657
+ };
658
+ }
659
+
660
+ function promptText(name, args = {}) {
661
+ const context = args.artifactId
662
+ ? `First read Artifacty resource ${artifactResourceUri(args.artifactId)} and use it as context.`
663
+ : "Use the current session context and any relevant Artifacty resources.";
664
+ const common = `${context}
665
+
666
+ Create or update an Artifacty artifact through artifacty_create, artifacty_import, or artifacty_update. Use concise Markdown unless another format is clearly better. Include sourceAgent, artifactType, and tags so another agent can discover it.`;
667
+
668
+ if (name === "artifacty_handoff") {
669
+ return `${common}
670
+
671
+ Goal: ${args.goal || "Prepare a continuation handoff for the next agent."}
672
+
673
+ Capture: current state, changed files or artifacts, commands run, decisions, blockers, residual risk, and next steps.
674
+ Recommended artifactType: handoff. Recommended tags: handoff, continuation.`;
675
+ }
676
+ if (name === "artifacty_review") {
677
+ return `${common}
678
+
679
+ Review scope: ${args.scope || "Review the current implementation or linked artifact."}
680
+
681
+ Capture findings first, ordered by severity, with file/line references when available. Include open questions and verification gaps.
682
+ Recommended artifactType: code-review. Recommended tags: review.`;
683
+ }
684
+ if (name === "artifacty_test_report") {
685
+ return `${common}
686
+
687
+ Verification goal: ${args.goal || "Summarize test and smoke evidence."}
688
+
689
+ Capture commands, status, failures, environment, manual checks, and what remains untested.
690
+ Recommended artifactType: test-report. Recommended tags: verification, test-report.`;
691
+ }
692
+ if (name === "artifacty_visual_qa") {
693
+ return `${common}
694
+
695
+ Visual target: ${args.target || "Inspect the UI or visual artifact under review."}
696
+
697
+ Capture screenshots/media references, viewport, expected behavior, observed issues, and pass/fail verdict.
698
+ Recommended artifactType: design-option or bundle. Recommended tags: visual, qa.`;
699
+ }
700
+ if (name === "artifacty_release_notes") {
701
+ return `${common}
702
+
703
+ Release version: ${args.version || "next"}
704
+
705
+ Capture highlights, breaking changes, migration notes, tests, known limitations, and publish evidence.
706
+ Recommended artifactType: document. Recommended tags: release-notes.`;
707
+ }
708
+ return common;
709
+ }
710
+
433
711
  async function createNativeArtifact(args) {
434
712
  return createArtifact(store, {
435
713
  title: args.title,
@@ -461,6 +739,49 @@ async function withUrls(artifact) {
461
739
  };
462
740
  }
463
741
 
742
+ function resourceText(uri, mimeType, data) {
743
+ return {
744
+ contents: [
745
+ {
746
+ uri,
747
+ mimeType,
748
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
749
+ }
750
+ ]
751
+ };
752
+ }
753
+
754
+ function artifactResourceUri(id) {
755
+ return `artifacty://artifacts/${encodeURIComponent(id)}`;
756
+ }
757
+
758
+ function artifactRawResourceUri(id, version) {
759
+ const suffix = version ? `?version=${encodeURIComponent(String(version))}` : "";
760
+ return `artifacty://artifacts/${encodeURIComponent(id)}/raw${suffix}`;
761
+ }
762
+
763
+ function parseArtifactResourceUri(uri) {
764
+ let parsed;
765
+ try {
766
+ parsed = new URL(uri);
767
+ } catch {
768
+ return null;
769
+ }
770
+
771
+ if (parsed.protocol !== "artifacty:" || parsed.hostname !== "artifacts") {
772
+ return null;
773
+ }
774
+ const parts = parsed.pathname.split("/").filter(Boolean);
775
+ if (parts.length !== 1 && !(parts.length === 2 && parts[1] === "raw")) {
776
+ return null;
777
+ }
778
+ return {
779
+ id: decodeURIComponent(parts[0]),
780
+ raw: parts[1] === "raw",
781
+ version: parsed.searchParams.get("version") || undefined
782
+ };
783
+ }
784
+
464
785
  function toolResult(data) {
465
786
  return {
466
787
  content: [
@@ -483,6 +804,15 @@ function requireArg(args, name) {
483
804
  return args[name];
484
805
  }
485
806
 
807
+ function requireParam(params = {}, name) {
808
+ if (!params[name]) {
809
+ throw Object.assign(new Error(`Missing required parameter: ${name}`), {
810
+ jsonRpcCode: -32602
811
+ });
812
+ }
813
+ return params[name];
814
+ }
815
+
486
816
  function writeResponse(id, result, error) {
487
817
  const response = {
488
818
  jsonrpc: "2.0",
package/src/server.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  createArtifact,
10
10
  createStore,
11
11
  getArtifact,
12
- listArtifacts,
12
+ listArtifactsPage,
13
13
  listAuditEvents,
14
14
  MAX_ARTIFACT_BYTES,
15
15
  restoreArtifact,
@@ -177,15 +177,19 @@ export async function handleRequest({ request, response, store, host, port, secu
177
177
  query: url.searchParams.get("q") || "",
178
178
  tag: url.searchParams.get("tag") || "",
179
179
  sourceAgent: url.searchParams.get("sourceAgent") || "",
180
- includeArchived: url.searchParams.get("includeArchived") === "true"
180
+ includeArchived: url.searchParams.get("includeArchived") === "true",
181
+ limit: url.searchParams.get("limit") || undefined,
182
+ offset: url.searchParams.get("offset") || undefined
181
183
  };
182
- const artifacts = await listArtifacts(store, {
184
+ const page = await listArtifactsPage(store, {
183
185
  query: filters.query || undefined,
184
186
  tag: filters.tag || undefined,
185
187
  sourceAgent: filters.sourceAgent || undefined,
186
- includeArchived: filters.includeArchived
188
+ includeArchived: filters.includeArchived,
189
+ limit: filters.limit,
190
+ offset: filters.offset
187
191
  });
188
- return sendHtml(response, renderDashboard({ artifacts, baseUrl, filters, locale, currentPath }), 200, headOnly);
192
+ return sendHtml(response, renderDashboard({ artifacts: page.artifacts, baseUrl, filters, pagination: page, locale, currentPath }), 200, headOnly);
189
193
  }
190
194
 
191
195
  if (method === "GET" && pathname === "/new") {
@@ -240,14 +244,19 @@ export async function handleRequest({ request, response, store, host, port, secu
240
244
  }
241
245
 
242
246
  if (method === "GET" && pathname === "/api/artifacts") {
243
- const artifacts = await listArtifacts(store, {
247
+ const page = await listArtifactsPage(store, {
244
248
  query: url.searchParams.get("q") || undefined,
245
249
  tag: url.searchParams.get("tag") || undefined,
246
250
  sourceAgent: url.searchParams.get("sourceAgent") || undefined,
247
251
  includeArchived: url.searchParams.get("includeArchived") === "true",
248
- limit: url.searchParams.get("limit") || undefined
252
+ limit: url.searchParams.get("limit") || undefined,
253
+ offset: url.searchParams.get("offset") || undefined
249
254
  });
250
- return sendJson(response, { artifacts }, 200, headOnly);
255
+ return sendJson(response, {
256
+ artifacts: page.artifacts,
257
+ pagination: paginationJson(page),
258
+ search: page.search
259
+ }, 200, headOnly);
251
260
  }
252
261
 
253
262
  if (method === "GET" && pathname === "/api/audit") {
@@ -442,6 +451,17 @@ export function decorateArtifactUrls(artifact, baseUrl) {
442
451
  };
443
452
  }
444
453
 
454
+ function paginationJson(page) {
455
+ return {
456
+ total: page.total,
457
+ limit: page.limit,
458
+ offset: page.offset,
459
+ hasMore: page.hasMore,
460
+ nextOffset: page.nextOffset,
461
+ previousOffset: page.previousOffset
462
+ };
463
+ }
464
+
445
465
  export async function readJsonBody(request) {
446
466
  const raw = await readBody(request, MAX_ARTIFACT_BYTES + 1024);
447
467
  if (!raw.trim()) {