(input: {
readonly code: string;
readonly title?: string;
readonly description?: string;
readonly connections?: Readonly<Record<string, string>>;
readonly artifactId?: string;
})
| 1673 | * what is stored, so a pure code tweak doesn't have to restate them. |
| 1674 | */ |
| 1675 | const createArtifact = (input: { |
| 1676 | readonly code: string; |
| 1677 | readonly title?: string; |
| 1678 | readonly description?: string; |
| 1679 | readonly connections?: Readonly<Record<string, string>>; |
| 1680 | readonly artifactId?: string; |
| 1681 | }): Effect.Effect<McpToolResult, unknown> => |
| 1682 | Effect.gen(function* () { |
| 1683 | // An update reads the existing row FIRST, both to carry its title and |
| 1684 | // description forward and to refuse a foreign id before any work. The |
| 1685 | // refusal is `artifact_unavailable` — the same answer `execute-action` |
| 1686 | // gives — so create-artifact cannot be used to probe which ids exist. |
| 1687 | const existing = |
| 1688 | input.artifactId === undefined ? null : yield* loadArtifact(input.artifactId); |
| 1689 | if (input.artifactId !== undefined && !existing) return actionArtifactUnavailableResult(); |
| 1690 | |
| 1691 | const title = input.title ?? existing?.title; |
| 1692 | if (title === undefined) { |
| 1693 | return renderRejectedResult( |
| 1694 | "title is required when creating an artifact. Give it a short human-readable name.", |
| 1695 | ); |
| 1696 | } |
| 1697 | // Only an update inherits; a create with no description stores none. |
| 1698 | const description = input.description ?? existing?.description ?? undefined; |
| 1699 | |
| 1700 | const rejection = validateArtifactCode(input.code); |
| 1701 | if (rejection) return renderRejectedResult(rejection); |
| 1702 | |
| 1703 | // Static checks first, then the real one: render it. See |
| 1704 | // `smokeRenderRejection` for what the model is told. |
| 1705 | // |
| 1706 | // FAIL OPEN. The renderer is injected, runs on three different hosts, |
| 1707 | // and is the newest thing in this path — if IT breaks (a missing |
| 1708 | // module, an environment gap on some host), the right outcome is a |
| 1709 | // saved artifact and a logged warning, never a refused create of code |
| 1710 | // that is perfectly good. Only a definite `failed` blocks a save. |
| 1711 | const smoke = config.smokeRenderArtifact; |
| 1712 | // The render that validates the artifact is also the render that |
| 1713 | // previews it: the same pass produces the loading-state markup the |
| 1714 | // gallery draws, so a preview costs nothing beyond sanitizing it. |
| 1715 | let preview: string | null = null; |
| 1716 | if (smoke) { |
| 1717 | const smokeResult: ArtifactSmokeRenderResult = yield* Effect.tryPromise(() => |
| 1718 | smoke(input.code), |
| 1719 | ).pipe( |
| 1720 | Effect.catchCause((cause) => |
| 1721 | Effect.as(Effect.logWarning("create-artifact smoke render was unavailable", cause), { |
| 1722 | status: "ok", |
| 1723 | } satisfies ArtifactSmokeRenderResult), |
| 1724 | ), |
| 1725 | ); |
| 1726 | const renderRejection = smokeRenderRejection(smokeResult); |
| 1727 | if (renderRejection) { |
| 1728 | yield* Effect.annotateCurrentSpan({ "mcp.artifact.smoke_render": "failed" }); |
| 1729 | return renderRejectedResult(renderRejection); |
| 1730 | } |
| 1731 | // Fail open, exactly as the verdict does: a preview that cannot be |
| 1732 | // produced or cannot be sanitized is a card that falls back to its |
no test coverage detected