artifacty 0.7.0 → 0.9.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
@@ -20,7 +20,7 @@ import { resolvePublicBaseUrl } from "./lib/server-state.js";
20
20
 
21
21
  const PROTOCOL_VERSION = "2025-06-18";
22
22
  const PACKAGE_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
23
- const store = createStore();
23
+ const DEFAULT_MCP_HTTP_TIMEOUT_MS = 30000;
24
24
 
25
25
  const nativeArtifactInputSchema = {
26
26
  type: "object",
@@ -82,7 +82,7 @@ const tools = [
82
82
  {
83
83
  name: "artifacty_list",
84
84
  title: "List Artifacts",
85
- description: "List artifacts from the local Artifacty store.",
85
+ description: "List artifacts from the Artifacty store.",
86
86
  inputSchema: {
87
87
  type: "object",
88
88
  properties: {
@@ -324,45 +324,122 @@ const prompts = [
324
324
  }
325
325
  ];
326
326
 
327
- const rl = readline.createInterface({
328
- input: process.stdin,
329
- crlfDelay: Infinity
330
- });
327
+ export function createMcpJsonRpcHandler(options = {}) {
328
+ const context = createMcpContext(options);
329
+ const requestHandler = createMcpRequestHandler(context);
330
+ return (message) => handleJsonRpcMessage(message, requestHandler);
331
+ }
331
332
 
332
- rl.on("line", (line) => {
333
- if (!line.trim()) {
334
- return;
335
- }
336
- handleLine(line).catch((error) => {
337
- process.stderr.write(`${error.stack || error.message}\n`);
338
- });
339
- });
333
+ export function createMcpRequestHandler(options = {}) {
334
+ const context = options.store ? createMcpContext(options) : options;
335
+ return (message) => handleMcpRequest(context, message);
336
+ }
340
337
 
341
- async function handleLine(line) {
342
- let message;
343
- try {
344
- message = JSON.parse(line);
345
- } catch {
346
- writeResponse(null, undefined, {
347
- code: -32700,
348
- message: "Parse error"
349
- });
350
- return;
338
+ export function createMcpContext(options = {}) {
339
+ const store = options.store || createStore();
340
+ return {
341
+ store,
342
+ transport: options.transport || "stdio",
343
+ auditContext: options.auditContext || (() => ({
344
+ surface: options.auditSurface || "mcp",
345
+ actor: options.actor || "mcp-client"
346
+ })),
347
+ resolvePublicBaseUrl: options.resolvePublicBaseUrl || (() => resolvePublicBaseUrl(store, {
348
+ url: options.publicBaseUrl
349
+ })),
350
+ serverCommand: options.serverCommand || "node src/mcp-server.js",
351
+ browserCommand: options.browserCommand || "npm start"
352
+ };
353
+ }
354
+
355
+ export async function handleJsonRpcMessage(message, requestHandler) {
356
+ if (!message || typeof message !== "object") {
357
+ return jsonRpcError(null, -32600, "Invalid Request");
351
358
  }
352
359
 
353
360
  if (!message.id && message.id !== 0) {
354
361
  await handleNotification(message);
355
- return;
362
+ return null;
356
363
  }
357
364
 
358
365
  try {
359
- const result = await handleRequest(message);
360
- writeResponse(message.id, result);
366
+ const result = await requestHandler(message);
367
+ return {
368
+ jsonrpc: "2.0",
369
+ id: message.id,
370
+ result
371
+ };
361
372
  } catch (error) {
362
- writeResponse(message.id, undefined, {
363
- code: error.jsonRpcCode || -32603,
364
- message: error.message
373
+ return jsonRpcError(message.id, error.jsonRpcCode || -32603, error.message);
374
+ }
375
+ }
376
+
377
+ export function createRemoteMcpJsonRpcHandler(options = {}) {
378
+ const endpoint = normalizeMcpUrl(options.url || process.env.ARTIFACTY_MCP_URL);
379
+ if (!endpoint) {
380
+ throw new Error("ARTIFACTY_MCP_URL is required when ARTIFACTY_MCP_MODE=bridge");
381
+ }
382
+ const token = options.apiToken || process.env.ARTIFACTY_API_TOKEN || "";
383
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs || process.env.ARTIFACTY_MCP_TIMEOUT_MS);
384
+
385
+ return async (message) => {
386
+ try {
387
+ return await postMcpJsonRpc({
388
+ url: endpoint,
389
+ token,
390
+ message,
391
+ timeoutMs
392
+ });
393
+ } catch (error) {
394
+ if (!message?.id && message?.id !== 0) {
395
+ process.stderr.write(`Remote MCP notification failed: ${error.message}\n`);
396
+ return null;
397
+ }
398
+ return jsonRpcError(message?.id ?? null, -32603, error.message);
399
+ }
400
+ };
401
+ }
402
+
403
+ async function runStdioServer(options = {}) {
404
+ const jsonRpcHandler = createStdioJsonRpcHandler(options);
405
+ const rl = readline.createInterface({
406
+ input: process.stdin,
407
+ crlfDelay: Infinity
408
+ });
409
+
410
+ rl.on("line", (line) => {
411
+ if (!line.trim()) {
412
+ return;
413
+ }
414
+ handleLine(line, jsonRpcHandler).catch((error) => {
415
+ process.stderr.write(`${error.stack || error.message}\n`);
365
416
  });
417
+ });
418
+ }
419
+
420
+ function createStdioJsonRpcHandler(options = {}) {
421
+ const mode = String(options.mode || process.env.ARTIFACTY_MCP_MODE || "local").toLowerCase();
422
+ if (mode === "bridge" || mode === "remote") {
423
+ return createRemoteMcpJsonRpcHandler(options);
424
+ }
425
+ return createMcpJsonRpcHandler({
426
+ ...options,
427
+ transport: "stdio"
428
+ });
429
+ }
430
+
431
+ async function handleLine(line, jsonRpcHandler) {
432
+ let message;
433
+ try {
434
+ message = JSON.parse(line);
435
+ } catch {
436
+ writeJsonRpcResponse(jsonRpcError(null, -32700, "Parse error"));
437
+ return;
438
+ }
439
+
440
+ const response = await jsonRpcHandler(message);
441
+ if (response) {
442
+ writeJsonRpcResponse(response);
366
443
  }
367
444
  }
368
445
 
@@ -373,7 +450,7 @@ async function handleNotification(message) {
373
450
  process.stderr.write(`Ignoring MCP notification: ${message.method}\n`);
374
451
  }
375
452
 
376
- async function handleRequest(message) {
453
+ export async function handleMcpRequest(context, message) {
377
454
  if (message.method === "initialize") {
378
455
  return {
379
456
  protocolVersion: PROTOCOL_VERSION,
@@ -394,7 +471,7 @@ async function handleRequest(message) {
394
471
  title: "Artifacty",
395
472
  version: "0.4.0"
396
473
  },
397
- instructions: "Use Artifacty to create, import, list, read, update, and resource-read local artifacts that other agents can reuse."
474
+ instructions: "Use Artifacty to create, import, list, read, update, and resource-read artifacts that other agents can reuse."
398
475
  };
399
476
  }
400
477
 
@@ -408,11 +485,11 @@ async function handleRequest(message) {
408
485
 
409
486
  if (message.method === "tools/call") {
410
487
  const params = message.params || {};
411
- return callTool(params.name, params.arguments || {});
488
+ return callTool(context, params.name, params.arguments || {});
412
489
  }
413
490
 
414
491
  if (message.method === "resources/list") {
415
- return listResources();
492
+ return listResources(context);
416
493
  }
417
494
 
418
495
  if (message.method === "resources/templates/list") {
@@ -420,7 +497,7 @@ async function handleRequest(message) {
420
497
  }
421
498
 
422
499
  if (message.method === "resources/read") {
423
- return readResource(requireParam(message.params, "uri"));
500
+ return readResource(context, requireParam(message.params, "uri"));
424
501
  }
425
502
 
426
503
  if (message.method === "prompts/list") {
@@ -437,14 +514,14 @@ async function handleRequest(message) {
437
514
  });
438
515
  }
439
516
 
440
- async function callTool(name, args) {
517
+ async function callTool(context, name, args) {
441
518
  if (name === "artifacty_create" || name === "artifacty_publish") {
442
- return toolResult(await withUrls(await createNativeArtifact(args)));
519
+ return toolResult(await withUrls(context, await createNativeArtifact(context, args)));
443
520
  }
444
521
 
445
522
  if (name === "artifacty_list") {
446
- const publicBaseUrl = await resolvePublicBaseUrl(store);
447
- const page = await listArtifactsPage(store, args);
523
+ const publicBaseUrl = await context.resolvePublicBaseUrl();
524
+ const page = await listArtifactsPage(context.store, args);
448
525
  return toolResult({
449
526
  artifacts: page.artifacts.map((artifact) => ({
450
527
  ...artifact,
@@ -464,24 +541,24 @@ async function callTool(name, args) {
464
541
 
465
542
  if (name === "artifacty_import") {
466
543
  const converted = convertAgentArtifact(args);
467
- const artifact = await createArtifact(store, {
544
+ const artifact = await createArtifact(context.store, {
468
545
  ...converted,
469
546
  allowSecrets: args.allowSecrets,
470
547
  auditAction: "import",
471
- audit: mcpAuditContext()
548
+ audit: mcpAuditContext(context)
472
549
  });
473
550
  return toolResult({
474
- ...await withUrls(artifact),
551
+ ...await withUrls(context, artifact),
475
552
  converted
476
553
  });
477
554
  }
478
555
 
479
556
  if (name === "artifacty_get") {
480
- const artifact = await getArtifact(store, requireArg(args, "id"), {
557
+ const artifact = await getArtifact(context.store, requireArg(args, "id"), {
481
558
  version: args.version,
482
- audit: mcpAuditContext()
559
+ audit: mcpAuditContext(context)
483
560
  });
484
- const decorated = await withUrls(artifact);
561
+ const decorated = await withUrls(context, artifact);
485
562
  if (args.includeContent === false) {
486
563
  delete decorated.content;
487
564
  }
@@ -489,7 +566,7 @@ async function callTool(name, args) {
489
566
  }
490
567
 
491
568
  if (name === "artifacty_update") {
492
- const artifact = await updateArtifact(store, requireArg(args, "id"), {
569
+ const artifact = await updateArtifact(context.store, requireArg(args, "id"), {
493
570
  title: args.title,
494
571
  content: args.content,
495
572
  format: args.format,
@@ -499,21 +576,21 @@ async function callTool(name, args) {
499
576
  tags: args.tags || [],
500
577
  metadata: args.metadata || {},
501
578
  allowSecrets: args.allowSecrets,
502
- audit: mcpAuditContext()
579
+ audit: mcpAuditContext(context)
503
580
  });
504
- return toolResult(await withUrls(artifact));
581
+ return toolResult(await withUrls(context, artifact));
505
582
  }
506
583
 
507
584
  if (name === "artifacty_archive" || name === "artifacty_restore") {
508
585
  const artifact = name === "artifacty_archive"
509
- ? await archiveArtifact(store, requireArg(args, "id"), { audit: mcpAuditContext() })
510
- : await restoreArtifact(store, requireArg(args, "id"), { audit: mcpAuditContext() });
511
- return toolResult(await withUrls(artifact));
586
+ ? await archiveArtifact(context.store, requireArg(args, "id"), { audit: mcpAuditContext(context) })
587
+ : await restoreArtifact(context.store, requireArg(args, "id"), { audit: mcpAuditContext(context) });
588
+ return toolResult(await withUrls(context, artifact));
512
589
  }
513
590
 
514
591
  if (name === "artifacty_audit") {
515
592
  return toolResult({
516
- events: await listAuditEvents(store, {
593
+ events: await listAuditEvents(context.store, {
517
594
  artifactId: args.artifactId,
518
595
  limit: args.limit
519
596
  })
@@ -521,14 +598,15 @@ async function callTool(name, args) {
521
598
  }
522
599
 
523
600
  if (name === "artifacty_info") {
524
- const publicBaseUrl = await resolvePublicBaseUrl(store);
601
+ const publicBaseUrl = await context.resolvePublicBaseUrl();
525
602
  return toolResult({
526
603
  name: "artifacty",
527
- store: store.home,
604
+ store: context.store.home,
528
605
  url: publicBaseUrl,
606
+ transport: context.transport,
529
607
  mcpProtocolVersion: PROTOCOL_VERSION,
530
- serverCommand: "node src/mcp-server.js",
531
- browserCommand: "npm start"
608
+ serverCommand: context.serverCommand,
609
+ browserCommand: context.browserCommand
532
610
  });
533
611
  }
534
612
 
@@ -537,8 +615,8 @@ async function callTool(name, args) {
537
615
  });
538
616
  }
539
617
 
540
- async function listResources() {
541
- const page = await listArtifactsPage(store, { limit: 10 });
618
+ async function listResources(context) {
619
+ const page = await listArtifactsPage(context.store, { limit: 10 });
542
620
  const artifactResources = page.artifacts.flatMap((artifact) => [
543
621
  {
544
622
  uri: artifactResourceUri(artifact.id),
@@ -577,10 +655,10 @@ async function listResources() {
577
655
  };
578
656
  }
579
657
 
580
- async function readResource(uri) {
658
+ async function readResource(context, uri) {
581
659
  if (uri === "artifacty://recent") {
582
- const publicBaseUrl = await resolvePublicBaseUrl(store);
583
- const page = await listArtifactsPage(store, { limit: 20 });
660
+ const publicBaseUrl = await context.resolvePublicBaseUrl();
661
+ const page = await listArtifactsPage(context.store, { limit: 20 });
584
662
  return resourceText(uri, "application/json", {
585
663
  artifacts: page.artifacts.map((artifact) => ({
586
664
  ...artifact,
@@ -612,9 +690,9 @@ async function readResource(uri) {
612
690
 
613
691
  const parsed = parseArtifactResourceUri(uri);
614
692
  if (parsed) {
615
- const artifact = await getArtifact(store, parsed.id, {
693
+ const artifact = await getArtifact(context.store, parsed.id, {
616
694
  version: parsed.version,
617
- audit: mcpAuditContext()
695
+ audit: mcpAuditContext(context)
618
696
  });
619
697
  if (parsed.raw) {
620
698
  return {
@@ -627,7 +705,7 @@ async function readResource(uri) {
627
705
  ]
628
706
  };
629
707
  }
630
- return resourceText(uri, "application/json", await withUrls(artifact));
708
+ return resourceText(uri, "application/json", await withUrls(context, artifact));
631
709
  }
632
710
 
633
711
  throw Object.assign(new Error(`Unknown resource: ${uri}`), {
@@ -708,8 +786,8 @@ Recommended artifactType: document. Recommended tags: release-notes.`;
708
786
  return common;
709
787
  }
710
788
 
711
- async function createNativeArtifact(args) {
712
- return createArtifact(store, {
789
+ async function createNativeArtifact(context, args) {
790
+ return createArtifact(context.store, {
713
791
  title: args.title,
714
792
  content: args.content,
715
793
  format: args.format,
@@ -719,19 +797,16 @@ async function createNativeArtifact(args) {
719
797
  tags: args.tags || [],
720
798
  metadata: args.metadata || {},
721
799
  allowSecrets: args.allowSecrets,
722
- audit: mcpAuditContext()
800
+ audit: mcpAuditContext(context)
723
801
  });
724
802
  }
725
803
 
726
- function mcpAuditContext() {
727
- return {
728
- surface: "mcp",
729
- actor: "mcp-client"
730
- };
804
+ function mcpAuditContext(context) {
805
+ return context.auditContext();
731
806
  }
732
807
 
733
- async function withUrls(artifact) {
734
- const publicBaseUrl = await resolvePublicBaseUrl(store);
808
+ async function withUrls(context, artifact) {
809
+ const publicBaseUrl = await context.resolvePublicBaseUrl();
735
810
  return {
736
811
  ...artifact,
737
812
  url: `${publicBaseUrl}/artifacts/${encodeURIComponent(artifact.id)}`,
@@ -813,15 +888,84 @@ function requireParam(params = {}, name) {
813
888
  return params[name];
814
889
  }
815
890
 
816
- function writeResponse(id, result, error) {
817
- const response = {
891
+ function jsonRpcError(id, code, message) {
892
+ return {
818
893
  jsonrpc: "2.0",
819
- id
894
+ id,
895
+ error: {
896
+ code,
897
+ message
898
+ }
820
899
  };
821
- if (error) {
822
- response.error = error;
900
+ }
901
+
902
+ function writeJsonRpcResponse(response) {
903
+ process.stdout.write(`${JSON.stringify(response)}\n`);
904
+ }
905
+
906
+ async function postMcpJsonRpc({ url, token, message, timeoutMs }) {
907
+ const controller = new AbortController();
908
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
909
+ try {
910
+ const response = await fetch(url, {
911
+ method: "POST",
912
+ headers: {
913
+ "content-type": "application/json",
914
+ accept: "application/json, text/event-stream",
915
+ ...(token ? { authorization: `Bearer ${token}` } : {})
916
+ },
917
+ body: JSON.stringify(message),
918
+ signal: controller.signal
919
+ });
920
+ const text = await response.text();
921
+ if (response.status === 202 && !text.trim()) {
922
+ return null;
923
+ }
924
+ if (!response.ok) {
925
+ throw new Error(`Remote MCP ${response.status}: ${text.trim() || response.statusText}`);
926
+ }
927
+ if (!text.trim()) {
928
+ return null;
929
+ }
930
+ return JSON.parse(text);
931
+ } catch (error) {
932
+ if (error.name === "AbortError") {
933
+ throw new Error(`Remote MCP request timed out after ${timeoutMs}ms`);
934
+ }
935
+ throw error;
936
+ } finally {
937
+ clearTimeout(timeout);
938
+ }
939
+ }
940
+
941
+ function normalizeMcpUrl(value) {
942
+ if (!value) {
943
+ return "";
944
+ }
945
+ const parsed = new URL(value);
946
+ const pathname = parsed.pathname.replace(/\/+$/, "");
947
+ if (!pathname) {
948
+ parsed.pathname = "/mcp";
823
949
  } else {
824
- response.result = result;
950
+ parsed.pathname = pathname;
825
951
  }
826
- process.stdout.write(`${JSON.stringify(response)}\n`);
952
+ parsed.search = "";
953
+ parsed.hash = "";
954
+ return parsed.toString();
955
+ }
956
+
957
+ function normalizeTimeoutMs(value) {
958
+ const timeout = Number(value ?? DEFAULT_MCP_HTTP_TIMEOUT_MS);
959
+ return Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_MCP_HTTP_TIMEOUT_MS;
960
+ }
961
+
962
+ function isMain(metaUrl) {
963
+ return process.argv[1] && metaUrl === new URL(`file://${process.argv[1]}`).href;
964
+ }
965
+
966
+ if (isMain(import.meta.url)) {
967
+ runStdioServer().catch((error) => {
968
+ process.stderr.write(`${error.stack || error.message}\n`);
969
+ process.exitCode = 1;
970
+ });
827
971
  }