artifacty 0.3.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/README.md CHANGED
@@ -175,6 +175,10 @@ artifacty-mcp
175
175
  ```
176
176
 
177
177
  MCP clients can create artifacts with `artifacty_create`. `artifacty_publish` remains as a backwards-compatible alias.
178
+ MCP clients that support resources or prompts can also read `artifacty://recent`,
179
+ `artifacty://artifacts/{id}`, `artifacty://artifacts/{id}/raw{?version}`, and
180
+ `artifacty://schema/v1`, or use prompt templates such as `artifacty_handoff`,
181
+ `artifacty_review`, and `artifacty_release_notes`.
178
182
 
179
183
  Operational commands:
180
184
 
@@ -275,6 +279,7 @@ Schema and storage:
275
279
  - Diagram, component, source snippet, analysis report, table, and media assets use `diagram`, `component`, `snippet`, `analysis-report`, `table`, and `asset` artifact types.
276
280
  - Copilot/Cursor examples cover PR reviews, screenshots, demo recordings, and visual evidence bundles.
277
281
  - See [docs/artifact-schema-v1.md](docs/artifact-schema-v1.md).
282
+ - See [docs/mcp-public-api.md](docs/mcp-public-api.md) for MCP tools, resources, prompts, and compatibility notes.
278
283
  - See [docs/sarif-csv-artifact-plan.md](docs/sarif-csv-artifact-plan.md) for the SARIF/CSV output artifact roadmap.
279
284
 
280
285
  ## Security Model
@@ -83,7 +83,21 @@ node src/cli.js check
83
83
  - Cursor: writes project `.cursor/mcp.json` using the Cursor `mcpServers` shape. Pass `--config ~/.cursor/mcp.json` for global Cursor setup.
84
84
  - `--dry-run` returns the generated config without writing it.
85
85
  - `--timeout <ms>` adjusts Codex `startup_timeout_sec` and Gemini `timeout`. It does not change Claude Code startup behavior; set `MCP_TIMEOUT` before launching Claude Code if you need a larger value there.
86
- - `check` starts the local MCP server and verifies required tools through `initialize` and `tools/list`.
86
+ - `check` starts the local MCP server and verifies required tools, resources, and prompts through MCP discovery methods.
87
+
88
+ ## Client Compatibility Matrix
89
+
90
+ | Client | Config shape | Scope | Timeout behavior | Restart requirement | Notes |
91
+ | --- | --- | --- | --- | --- | --- |
92
+ | Claude Code | `.mcp.json` `mcpServers` | project | parent `MCP_TIMEOUT`, default 30s | restart Claude Code | no per-server startup timeout field |
93
+ | Codex | TOML `[mcp_servers.artifacty]` | user or explicit config | `startup_timeout_sec = 30.0` | restart Codex session | Windows cold starts may need the 30s default |
94
+ | Gemini CLI | JSON `mcpServers` | user or project | `timeout: 30000` | reload `/mcp` or restart | `trust` is preserved from installer options |
95
+ | GitHub Copilot in VS Code | `.vscode/mcp.json` `servers` | workspace or user profile | client-managed | reload VS Code MCP server | tools are the primary tested surface |
96
+ | Cursor | `.cursor/mcp.json` `mcpServers` | workspace or global | client-managed | restart/reload Cursor MCP | tools are the primary tested surface |
97
+
98
+ All clients should support MCP tools. Resource and prompt display varies by
99
+ client, so Artifacty's core workflows continue to work through tools even when a
100
+ client does not expose resources or prompts in its UI.
87
101
 
88
102
  Generate a token for protected HTTP routes:
89
103
 
@@ -196,6 +210,25 @@ Then run `/mcp` inside Gemini CLI to confirm that the Artifacty tools are connec
196
210
 
197
211
  Mutating MCP tools scan content for common API keys and private keys before storage. Pass `allowSecrets: true` only for intentional exceptions.
198
212
 
213
+ ## MCP Resources and Prompts
214
+
215
+ Artifacty also exposes read-only MCP resources:
216
+
217
+ - `artifacty://recent`: recent artifacts with pagination and browser URLs.
218
+ - `artifacty://schema/v1`: Artifact schema v1 Markdown.
219
+ - `artifacty://artifacts/{id}`: artifact JSON with metadata, content, and URLs.
220
+ - `artifacty://artifacts/{id}/raw{?version}`: raw latest or versioned content.
221
+
222
+ Prompt templates:
223
+
224
+ - `artifacty_handoff`: continuation handoff.
225
+ - `artifacty_review`: findings-first code review artifact.
226
+ - `artifacty_test_report`: verification evidence.
227
+ - `artifacty_visual_qa`: screenshot/media/browser evidence.
228
+ - `artifacty_release_notes`: release notes with publish evidence and risks.
229
+
230
+ See [mcp-public-api.md](mcp-public-api.md) for the public MCP surface.
231
+
199
232
  ## Importing Agent Artifacts
200
233
 
201
234
  Use `artifacty_import` or the CLI `import` command when the artifact was produced by another agent and needs normalization before sharing.
@@ -0,0 +1,68 @@
1
+ # MCP Public API
2
+
3
+ Artifacty's MCP stdio server is the primary agent-to-agent integration surface.
4
+ The server currently targets MCP protocol `2025-06-18`.
5
+
6
+ ## Capabilities
7
+
8
+ `initialize` advertises:
9
+
10
+ - `tools`: Artifact create, import, list, get, update, archive, restore, audit, and info.
11
+ - `resources`: static and dynamic read-only artifact resources.
12
+ - `prompts`: reusable workflow prompt templates.
13
+
14
+ ## Tools
15
+
16
+ Stable tool names:
17
+
18
+ - `artifacty_create`: create a native Artifacty artifact.
19
+ - `artifacty_publish`: backwards-compatible alias for `artifacty_create`.
20
+ - `artifacty_import`: convert external Claude, Codex, Gemini, Copilot, Cursor, Artifacty, or generic payloads.
21
+ - `artifacty_list`: list artifacts with `query`, `tag`, `sourceAgent`, `includeArchived`, `limit`, and `offset`.
22
+ - `artifacty_get`: read one artifact by `id` and optional `version`.
23
+ - `artifacty_update`: append an immutable version.
24
+ - `artifacty_archive` / `artifacty_restore`: toggle archive state.
25
+ - `artifacty_audit`: list audit events.
26
+ - `artifacty_info`: return local store and browser URL information.
27
+
28
+ Tool schemas use Artifacty schema v1 formats and artifact types. New optional
29
+ properties may be added during 0.x releases; existing names should not be
30
+ renamed without a documented migration.
31
+
32
+ ## Resources
33
+
34
+ Static resources:
35
+
36
+ - `artifacty://recent`: JSON list of recent artifacts with pagination and browser URLs.
37
+ - `artifacty://schema/v1`: Markdown reference for Artifacty schema v1.
38
+
39
+ Resource templates:
40
+
41
+ - `artifacty://artifacts/{id}`: JSON artifact metadata, selected version, content, and URLs.
42
+ - `artifacty://artifacts/{id}/raw{?version}`: raw artifact content for latest or specified version.
43
+
44
+ Resources are read-only and may record an audit `read` event for artifact content.
45
+
46
+ ## Prompts
47
+
48
+ Prompt names:
49
+
50
+ - `artifacty_handoff`
51
+ - `artifacty_review`
52
+ - `artifacty_test_report`
53
+ - `artifacty_visual_qa`
54
+ - `artifacty_release_notes`
55
+
56
+ Each prompt returns one user message that instructs an agent to create or update
57
+ an Artifacty artifact with discoverable `artifactType`, `sourceAgent`, and tags.
58
+ Prompts accept optional context arguments such as `artifactId`, `goal`, `scope`,
59
+ `target`, or `version`.
60
+
61
+ ## Compatibility Notes
62
+
63
+ - The MCP server is local stdio only. Remote MCP auth/OAuth is out of scope.
64
+ - Binary media resources return stored base64 text through MCP; browser `/raw`
65
+ decodes first-class `image` and `video` artifacts into bytes.
66
+ - Clients may display resources and prompts differently. Tools remain the most
67
+ widely supported integration path across Claude, Codex, Gemini, Copilot, and
68
+ Cursor.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifacty",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -37,6 +37,7 @@
37
37
  "docs/artifact-schema-v1.md",
38
38
  "docs/assets/artifacty.png",
39
39
  "docs/integrations.md",
40
+ "docs/mcp-public-api.md",
40
41
  "docs/network-sharing.md",
41
42
  "docs/release-checklist.md",
42
43
  "docs/sarif-csv-artifact-plan.md",
package/src/lib/check.js CHANGED
@@ -14,10 +14,25 @@ export const REQUIRED_MCP_TOOLS = [
14
14
  "artifacty_info"
15
15
  ];
16
16
 
17
+ export const REQUIRED_MCP_RESOURCES = [
18
+ "artifacty://recent",
19
+ "artifacty://schema/v1"
20
+ ];
21
+
22
+ export const REQUIRED_MCP_PROMPTS = [
23
+ "artifacty_handoff",
24
+ "artifacty_review",
25
+ "artifacty_test_report",
26
+ "artifacty_visual_qa",
27
+ "artifacty_release_notes"
28
+ ];
29
+
17
30
  export async function checkMcpTools(options = {}) {
18
31
  const projectDir = path.resolve(options.projectDir || process.cwd());
19
32
  const serverPath = path.resolve(options.serverPath || path.join(projectDir, "src", "mcp-server.js"));
20
33
  const requiredTools = options.requiredTools || REQUIRED_MCP_TOOLS;
34
+ const requiredResources = options.requiredResources || REQUIRED_MCP_RESOURCES;
35
+ const requiredPrompts = options.requiredPrompts || REQUIRED_MCP_PROMPTS;
21
36
  const timeoutMs = Number(options.timeout || 5000);
22
37
  const client = spawnMcpClient({
23
38
  serverPath,
@@ -39,14 +54,26 @@ export async function checkMcpTools(options = {}) {
39
54
  const listed = await client.request("tools/list", {});
40
55
  const toolNames = (listed.tools || []).map((tool) => tool.name).sort();
41
56
  const missingTools = requiredTools.filter((tool) => !toolNames.includes(tool));
57
+ const listedResources = await client.request("resources/list", {});
58
+ const resourceUris = (listedResources.resources || []).map((resource) => resource.uri).sort();
59
+ const missingResources = requiredResources.filter((resource) => !resourceUris.includes(resource));
60
+ const listedPrompts = await client.request("prompts/list", {});
61
+ const promptNames = (listedPrompts.prompts || []).map((prompt) => prompt.name).sort();
62
+ const missingPrompts = requiredPrompts.filter((prompt) => !promptNames.includes(prompt));
42
63
 
43
64
  return {
44
- ok: missingTools.length === 0,
65
+ ok: missingTools.length === 0 && missingResources.length === 0 && missingPrompts.length === 0,
45
66
  serverPath,
46
67
  protocolVersion: initialized.protocolVersion,
47
68
  toolCount: toolNames.length,
69
+ resourceCount: resourceUris.length,
70
+ promptCount: promptNames.length,
48
71
  tools: toolNames,
49
- missingTools
72
+ resources: resourceUris,
73
+ prompts: promptNames,
74
+ missingTools,
75
+ missingResources,
76
+ missingPrompts
50
77
  };
51
78
  } finally {
52
79
  client.close();
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,
@@ -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 = {
@@ -255,6 +259,71 @@ const tools = [
255
259
  }
256
260
  ];
257
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
+
258
327
  const rl = readline.createInterface({
259
328
  input: process.stdin,
260
329
  crlfDelay: Infinity
@@ -311,14 +380,21 @@ async function handleRequest(message) {
311
380
  capabilities: {
312
381
  tools: {
313
382
  listChanged: false
383
+ },
384
+ resources: {
385
+ subscribe: false,
386
+ listChanged: false
387
+ },
388
+ prompts: {
389
+ listChanged: false
314
390
  }
315
391
  },
316
392
  serverInfo: {
317
393
  name: "artifacty",
318
394
  title: "Artifacty",
319
- version: "0.3.0"
395
+ version: "0.4.0"
320
396
  },
321
- 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."
322
398
  };
323
399
  }
324
400
 
@@ -335,6 +411,27 @@ async function handleRequest(message) {
335
411
  return callTool(params.name, params.arguments || {});
336
412
  }
337
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
+
338
435
  throw Object.assign(new Error(`Method not found: ${message.method}`), {
339
436
  jsonRpcCode: -32601
340
437
  });
@@ -440,6 +537,177 @@ async function callTool(name, args) {
440
537
  });
441
538
  }
442
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
+
443
711
  async function createNativeArtifact(args) {
444
712
  return createArtifact(store, {
445
713
  title: args.title,
@@ -471,6 +739,49 @@ async function withUrls(artifact) {
471
739
  };
472
740
  }
473
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
+
474
785
  function toolResult(data) {
475
786
  return {
476
787
  content: [
@@ -493,6 +804,15 @@ function requireArg(args, name) {
493
804
  return args[name];
494
805
  }
495
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
+
496
816
  function writeResponse(id, result, error) {
497
817
  const response = {
498
818
  jsonrpc: "2.0",