Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ inline views, targeted edits, history, and compatibility with older Canvas sourc
Start with the [documentation index](docs/README.md) to find the guide for your task.

- [Authentication, MCP sign-in, and access permissions](docs/authentication.md)
- [Portable MCP plugin and host surfaces](docs/mcp-plugin.md)
- [Host commands and local persistence](docs/daemon.md)
- [Deploy to Cloudflare](docs/cloudflare.md)
- [Deploy celld on your own infrastructure](docs/celld-deployment.md)
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ then open [localhost:4786](http://127.0.0.1:4786).
| --- | --- |
| Run Artifacts on my machine | [Local host](daemon.md) or [Docker](docker.md) |
| Sign in and connect my coding agent | [Authentication and MCP](authentication.md#connect-to-an-existing-deployment) |
| Install the portable agent plugin | [MCP plugin and host surfaces](mcp-plugin.md) |
| Share with my team or publish a link | [Libraries and URL permissions](authentication.md#library-and-url-permissions) |
| Work with source files in a local project | [File-based workspace](local-workspace.md) |

Expand Down
51 changes: 51 additions & 0 deletions docs/mcp-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Artifacts MCP plugin

Artifacts includes a portable [Agent Plugins 1.0.0](https://agent-plugins.org/) package in `plugins/artifacts`. The root `plugin.json` identifies the package, `mcp.json` configures the connection, and `skills/author` and `skills/setup` provide scoped agent instructions. Skills are discovered by directory convention. OpenAI onboarding is isolated under `extensions.com.openai.onboardingSkill`; other clients may ignore it.

## Configure a package

Generate a new package for a local artifact source directory:

```sh
artifacts plugin --out ./artifacts-plugin --dir /absolute/path/to/project/artifacts
```

Or connect it to the MCP endpoint of your own existing deployment:

```sh
artifacts plugin --out ./artifacts-plugin --url "$ARTIFACTS_MCP_URL"
```

Set `ARTIFACTS_MCP_URL` to the actual endpoint provided by your deployment operator. The command does not deploy a service, invent an endpoint, or establish authentication. Remote URLs require HTTPS; an explicit loopback HTTP URL is accepted for local development. URL credentials, query parameters, and fragments are rejected. Authenticate through the host's connection flow rather than adding credentials to the generated files. See [authentication](authentication.md) for deployment access boundaries.

The destination must not exist and its parent directory must exist. Existing output is never overwritten. Choose a fresh directory for a changed configuration. `--url` and `--dir` are mutually exclusive. Without either, the generator captures the CLI's current default artifact directory into the generated stdio arguments. `--dir` names the source directory itself, not its parent project. The generator does not create or modify that workspace.

The repository template uses `bunx --bun @sidequery/artifacts@0.1.0 mcp`. It requires Bun in the host's executable search path and access to that package version. Its unconfigured directory follows the CLI's environment/workspace defaults. Prefer a generated package with an explicit directory when a host's launch directory is unclear. Local stdio stores source files and history locally; it is separate from hosted libraries and does not execute hosted backends. See [local workspace](local-workspace.md) and [app backends](app-backends.md).

Install the resulting directory through a client that supports portable Agent Plugins. Installation and distribution are client-specific; this package is not a marketplace registration. The portable schema uses `type: "streamable-http"` for remote transport. Registered OpenAI app `.app.json` server mappings are a different configuration surface and are not combined with `mcp.json`.

## Host surfaces

The server builds on MCP Apps and advertises optional OpenAI extensions. Actual availability depends on client support, advertised capabilities, deployment permissions, and authentication. Text-only clients retain ordinary tool results and diagnostics.

| Surface | Purpose and boundary |
| --- | --- |
| Global sidebar | Open the Artifacts library through an empty-argument entrypoint; list only the connected identity's accessible artifacts. |
| Conversation view | Browse working artifacts in the connected workspace and keep the selected artifact context available in the host view. There is no stored host conversation identifier or separate conversation membership list. |
| Mentions | Search accessible artifacts for composer selection. Mention results do not grant access. |
| Deep links | Open `/artifact?workspace=WORKSPACE&name=NAME` or `/artifact?workspace=WORKSPACE&version_id=ID`, with optional `route` for internal navigation. Encode query values. Server authorization still applies; a link never bypasses owner/team checks. |
| Model context | Publish selected artifact/revision, route, and filter context without automatically sending a prompt. Prompt actions are deliberate user actions. |
| File editor | Open the owned `.artifact.tsx` format through opaque host resources. Writing requires writable capability and an ETag; conflicts require rereading before retrying. |
| Display and theme | Use the host's supported display modes, theme, styles, locale, and timezone; hide unavailable controls and tolerate rejected requests. |

Tool visibility distinguishes model calls from app calls; it does not replace server authorization. Host context and file resource identifiers are untrusted input. UI selection, attachments, and cached state cannot expand library, owner, or team access.

## Authoring and verification

The author skill reads `artifact_guide`, guards source edits using `source_hash`/`expected_hash`, suppresses intermediate previews with `preview: false` when supported, and deliberately calls `artifact_open` when the result is ready. Validation diagnostics may accompany an applied edit; they are not a rollback guarantee. Compatible preview refreshes should preserve view state.

Setup verifies connection using tool discovery, `artifact_guide`, and `artifact_list`. It does not create example artifacts or alter the user's library. Host file editing requires its own advertised resource and write capabilities; it is separate from hosted artifact file storage described in [files](files.md).

Rich OpenAI forms and migration to multi-round-trip requests (MRTR) are deliberately deferred. Packaging this plugin does not claim support for those protocol extensions or for every surface in every host.

The portable manifest and connection shapes follow the [plugin schema](https://agent-plugins.org/schemas/1.0.0/plugin.schema.json) and [MCP schema](https://agent-plugins.org/schemas/1.0.0/mcp.schema.json). Optional host integration follows the [OpenAI MCP Extensions specification](https://github.com/openai/mcp-extensions/blob/main/docs/spec.md); the installed SDK and capability negotiation determine the implemented contract.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@
"src/gallery/subscription.ts",
"src/gallery/local-subscriptions.ts",
"src/gallery/folders.tsx",
"docs/mcp-plugin.md",
"docs/local-workspace.md",
"src/plugin-package.ts",
"plugins/artifacts/**",
"src/mcp/host-contract.ts",
"src/mcp/workspace-contract.ts",
"src/mcp/file-contract.ts",
Expand Down
10 changes: 10 additions & 0 deletions plugins/artifacts/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"artifacts": {
"type": "stdio",
"command": "bunx",
"args": ["--bun", "@sidequery/artifacts@0.1.0", "mcp"]
}
}
}
10 changes: 10 additions & 0 deletions plugins/artifacts/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "artifacts",
"version": "0.1.0",
"description": "Create, edit, and explore React artifacts in your configured Artifacts workspace.",
"author": { "name": "Sidequery" },
"repository": "https://github.com/sidequery/artifacts",
"license": "MIT",
"extensions": { "com.openai": { "onboardingSkill": "./skills/setup/SKILL.md" } }
}
16 changes: 16 additions & 0 deletions plugins/artifacts/skills/author/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: author
description: Create or edit React artifacts using the configured Artifacts MCP server; inspect existing artifacts, validate changes, and deliberately open the result.
---

Read `artifact_guide` once in the current conversation before authoring. It owns the SDK exports, allowed imports, runtime restrictions, and validation behavior. Use the connected server's current tool schemas rather than assuming every deployment exposes the same tools.

For an existing artifact, read the relevant source with `artifact_read` before editing. Pass its `source_hash` as `expected_hash` to `artifact_edit`; on conflict, reread and reconcile the user's changes. Use exact replacements with unique matches. A failed validation may leave source applied: inspect the result and repair diagnostics instead of assuming rollback.

Use `preview: false` for intermediate writes, edits, and restores when supported by the advertised tool schema. After the requested result passes validation, call `artifact_open` deliberately to show it. Preserve compatible view state and internal navigation. Do not send a user prompt merely to publish selection context; sending a prompt requires the user's action or request.

Respect the connected identity, selected owner/library, team permissions, and workspace. A UI attachment, mention, deep link, or hidden tool is not authorization to access another owner's data. Read the current selection before mutation; ask when the intended target is materially ambiguous. Sharing or moving ownership requires the user's instruction. Never put credentials, transfer URLs, or secret values in source or plugin configuration.

For hosted server, database, or file work, check current runtime capabilities and the SDK guide. File-based local stdio workspaces do not execute hosted backends. Do not replace durable storage with UI state. When editing a host file resource, retain its opaque URI, require writable capability, and use the current ETag; reread and reconcile conflicts.

Finish with the artifact opened when the host supports it, validation status, and any concrete remaining limitation. A successful preview request does not prove the user viewed it.
14 changes: 14 additions & 0 deletions plugins/artifacts/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
name: setup
description: Verify an Artifacts plugin connection and explain the configured local workspace or user-owned remote deployment without creating sample work.
---

Inspect the packaged `../../mcp.json` relative to this skill directory. Determine whether it launches local stdio or connects to a configured remote endpoint. Use the host's supported plugin installation and authentication flow; do not assume one client's installation UI exists in another.

For local stdio, Bun must be available to the host. An explicit `--dir` names the artifact source directory. Without it, the CLI resolves its configured environment or the launching workspace's `artifacts` directory (with legacy compatibility); the host's launch directory can differ from the user's project. If the target is unclear, establish the intended directory before creating work. The local workspace is separate from a hosted library and does not execute hosted backends.

For remote MCP, use the endpoint of the user's existing Artifacts deployment. Require HTTPS except for an explicitly selected loopback HTTP development server. Never invent a public service URL or embed tokens in a manifest, URL, or skill. Complete authentication through normal host tooling and verify the current owner/team context. If authentication is unavailable, report the exact connection problem; do not hunt for credentials.

Verify by discovering the advertised tools and calling `artifact_guide` and `artifact_list` with their current schemas. Do not write, remix, restore, or create a demonstration artifact as a connection check. If tools are unavailable, inspect the host's connection error and configured transport before changing anything.

Report whether the connection succeeded, which workspace or deployment it uses, and which host surfaces are actually available. MCP Apps, sidebar and conversation views, mentions, deep links, and file editing depend on advertised capabilities and client support. Offer the author skill for the user's next artifact request; setup itself is complete after read-only verification.
7 changes: 7 additions & 0 deletions scripts/package.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ try {
`);
await run([process.execPath, "run", mcpSmoke], clientDirectory, isolatedEnv);

const pluginDirectory = join(consumer, "artifacts-plugin");
await run([artifact, "plugin", "--out", pluginDirectory, "--dir", artifacts], consumer, isolatedEnv);
const pluginManifest = JSON.parse(await Bun.file(join(pluginDirectory, "plugin.json")).text());
expect(pluginManifest.name).toBe("artifacts");
const pluginConfig = JSON.parse(await Bun.file(join(pluginDirectory, "mcp.json")).text());
expect(pluginConfig.mcpServers.artifacts.args.slice(-2)).toEqual(["--dir", artifacts]);

if (process.env.CELLD_PACKAGE_INTEGRATION === "1") {
const target = process.platform === "darwin" && process.arch === "arm64"
? "aarch64-apple-darwin"
Expand Down
17 changes: 16 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { createArtifactServer } from "./serve";
import { ArtifactService } from "./service";
import { historyPath } from "./history";
import { PLUGIN_ROOT } from "./paths";
import { writePluginPackage } from "./plugin-package";
import type { ProjectArchive } from "./project-archive-contract";

type ServerManager = Pick<ServerDaemonManager, "start" | "stop" | "status" | "logs" | "uninstall">;
Expand Down Expand Up @@ -117,6 +118,18 @@ async function main(): Promise<void> {
return;
}

if (args.command === "plugin") {
if (args.positionals.length) throw new Error("plugin accepts options only: --out DIRECTORY [--url URL | --dir WORKSPACE]");
for (const [name, value] of Object.entries(args.flags)) {
if (!["out", "url", "dir"].includes(name)) throw new Error(`Unknown plugin option: --${name}`);
if (value === true || !value.trim()) throw new Error(`--${name} requires a value`);
}
const directory = flagString(args.flags, "out");
if (!directory) throw new Error("plugin requires --out DIRECTORY");
printJson(await writePluginPackage({ directory, url: flagString(args.flags, "url"), workspace: flagString(args.flags, "dir") }));
return;
}

if (await runServerCommand(args)) return;

const cwd = process.cwd();
Expand Down Expand Up @@ -320,6 +333,7 @@ Usage:
artifacts serve [--dir PATH] [--port N]
artifacts web [--port 4784] [--dir PATH]
artifacts mcp
artifacts plugin --out DIRECTORY [--url URL | --dir WORKSPACE]
artifacts history [NAME]
artifacts show VERSION_ID [--source]
artifacts open --version VERSION_ID [--event EVENT_ID] [--placement split|tab|zoomed|overlay]
Expand All @@ -338,7 +352,8 @@ Usage:
artifacts server start [--port 4786] [--at-login]
artifacts server stop | status | logs [--lines 100] | uninstall

All commands accept --dir PATH and --history-db PATH.
Workspace commands accept --dir PATH and --history-db PATH.
plugin creates a portable package in a new directory; its parent must exist.
read returns up to 200 lines by default, with a source_hash and next_line.
edit accepts JSON: {"edits":[{"old_text":"exact match","new_text":"replacement"}],"expected_hash":"optional SHA-256"}.
Edits run sequentially in memory; every old_text must match exactly once. Invalid batches
Expand Down
14 changes: 14 additions & 0 deletions src/mcp/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ test("working preview initializes from sidecar without modifying it and rejects
await expect(artifactAppResult(service, { name: "linked" })).rejects.toThrow();
});

test("library previews resolve legacy names and preserve their sidecar state", async () => {
const { dir, call } = fixture();
writeFileSync(join(dir, "legacy.canvas.tsx"), VALID_ARTIFACT);
writeFileSync(join(dir, "legacy.canvas.data.json"), '{"count":7}');
const library = await call("artifacts_library", {});
const name = (library.structuredContent.artifacts as Array<{ name: string }>)[0]!.name;
for (const tool of ["artifacts_preview", "artifact_open"]) {
const preview = await call(tool, { name });
expect(preview.isError).toBe(false);
expect(preview._meta?.artifact?.name).toBe("legacy");
expect(preview._meta?.artifact?.state).toEqual({ count: 7 });
}
});

test("official MCP client can initialize, discover UI, create and show over standard stdio", async () => {
const dir = tempDir();
const client = new Client({ name: "artifact-test", version: "1" }, { capabilities: { extensions: { "io.modelcontextprotocol/ui": { mimeTypes: [ARTIFACTS_APP_MIME] } } } });
Expand Down
5 changes: 3 additions & 2 deletions src/mcp/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export async function artifactAppResult(service: ArtifactService, selection: { n
if (Boolean(selection.name) === Boolean(selection.version_id)) throw new Error("provide name or version_id, but not both");
if (selection.event_id && !selection.version_id) throw new Error("event_id requires version_id");
const saved = selection.version_id ? service.version(selection.version_id) : undefined;
const path = saved?.source_path ?? service.resolve(ensureArtifactFileName(selection.name!));
if (!saved) ensureArtifactFileName(selection.name!);
const path = saved?.source_path ?? service.resolve(selection.name!);
if (!saved) assertRegularArtifact(path);
const source = saved?.source ?? readFileSync(path, "utf8");
const project = saved?.project ?? readLocalProject(path);
Expand All @@ -77,7 +78,7 @@ export async function artifactAppResult(service: ArtifactService, selection: { n
if (event) state = JSON.parse(event.initial_state);
} else {
try {
const value: unknown = JSON.parse(readFileSync(path.replace(/\.artifact\.tsx$/, ".artifact.data.json"), "utf8"));
const value: unknown = JSON.parse(readFileSync(path.replace(/\.(artifact|canvas)\.tsx$/, ".$1.data.json"), "utf8"));
if (value && typeof value === "object" && !Array.isArray(value)) state = value as Record<string, unknown>;
} catch { /* Match gallery previews: malformed sidecars remain untouched. */ }
}
Expand Down
Loading
Loading