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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ jobs:
- run: bun install

- run: bun run build

- run: bun ./scripts/validate-mcpb-artifact.mjs
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ node_modules
tmp/
temp/

# Built assets
*.mcpb
# Built assets (keep committed MCPB; un-ignore dist/ first so children can match)
**/dist/**
!**/dist/
!**/dist/paper.mcpb
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,13 @@ Are we missing one, you have feedback for us, or just want to chat? Join us on o
```sh
/plugin install paper-desktop@paper
```

## Claude Desktop

Pack the config-only MCPB extension, then open or drag `claude-extensions/paper-desktop/dist/paper.mcpb` into Claude Desktop → Extensions:

```sh
bun run pack:mcpb
```

Requires Paper Desktop installed and opened once (so `~/.paper/bin/paper` exists). See [`claude-extensions/paper-desktop`](claude-extensions/paper-desktop).
107 changes: 107 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions claude-extensions/paper-desktop/README.md
1 change: 1 addition & 0 deletions claude-extensions/paper-desktop/assets
Binary file added claude-extensions/paper-desktop/dist/paper.mcpb
Binary file not shown.
53 changes: 53 additions & 0 deletions claude-extensions/paper-desktop/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
"manifest_version": "0.4",
"name": "paper",
"display_name": "Paper",
"version": "0.1.0",
"description": "Design on a canvas that Claude can read and write to — built on web standards.",
"author": {
"name": "Paper",
"email": "team@paper.design",
"url": "https://paper.design"
},
"homepage": "https://paper.design",
"documentation": "https://paper.design/docs/mcp",
"support": "mailto:team@paper.design",
"repository": {
"type": "git",
"url": "https://github.com/paper-design/agent-plugins"
},
"license": "MIT",
"privacy_policies": ["https://paper.design/privacy"],
"icon": "assets/logo@512w.png",
"screenshots": [
"assets/barley-layout.png",
"assets/barley-feedback.png",
"assets/barley-responsive-frames.png"
],
"keywords": [
"paper",
"design",
"ui",
"canvas",
"html",
"css",
"design-to-code",
"code-to-design",
"mcp",
"design-system"
],
"server": {
"type": "binary",
"entry_point": "${HOME}/.paper/bin/paper",
"mcp_config": {
"command": "${HOME}/.paper/bin/paper",
"args": ["mcp"]
}
},
"compatibility": {
"claude_desktop": ">=0.1.0",
"platforms": ["darwin", "win32", "linux"]
},
"tools_generated": true
}
8 changes: 8 additions & 0 deletions claude-extensions/paper-desktop/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@paper/claude-desktop-mcpb",
"private": true,
"type": "module",
"scripts": {
"pack": "bun ./scripts/pack.ts"
}
}
101 changes: 101 additions & 0 deletions claude-extensions/paper-desktop/scripts/pack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env bun
/**
* Pack a config-only MCPB (zip with manifest.json at archive root).
*
* Injects README.md as long_description at pack time — the source
* manifest.json is left unchanged.
*
* Bundles icon/screenshot paths referenced by the built manifest (resolving
* symlinks into the archive).
*
* Uses a plain zip instead of `mcpb pack` because the binary entry_point is an
* external `${HOME}/.paper/bin/paper` path installed by Paper Desktop — not a
* file inside the bundle.
*/
import {
chmod,
cp,
mkdir,
readdir,
rm,
stat,
utimes,
writeFile,
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { $ } from "bun";
import { buildMcpbManifest } from "../../../scripts/mcpb-manifest.mjs";

/** DOS-era zip epoch — fixed so rebuilds are bit-identical across runs. */
const ZIP_EPOCH = new Date(Date.UTC(1980, 0, 1, 0, 0, 0));
/** Fixed Unix modes so Info-ZIP external attributes ignore umask / cp defaults. */
const FILE_MODE = 0o644;
const DIR_MODE = 0o755;

async function normalizeStaging(dir: string) {
await chmod(dir, DIR_MODE);
await utimes(dir, ZIP_EPOCH, ZIP_EPOCH);
for (const name of await readdir(dir)) {
const path = join(dir, name);
if ((await stat(path)).isDirectory()) {
await normalizeStaging(path);
} else {
await chmod(path, FILE_MODE);
await utimes(path, ZIP_EPOCH, ZIP_EPOCH);
}
}
}

const extensionDir = join(dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = join(extensionDir, "../..");
const distDir = join(extensionDir, "dist");
const stageDir = join(distDir, "stage");
const outFile = join(distDir, "paper.mcpb");
const builtManifestPath = join(stageDir, "manifest.json");

const { manifest, readmeLabel } = buildMcpbManifest(extensionDir, {
root: repoRoot,
});

const assetPaths = [
...(typeof manifest.icon === "string" ? [manifest.icon] : []),
...(Array.isArray(manifest.screenshots) ? manifest.screenshots : []),
];

await rm(distDir, { recursive: true, force: true });
await mkdir(stageDir, { recursive: true });
await writeFile(builtManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);

for (const rel of assetPaths) {
if (typeof rel !== "string" || rel.includes("..") || rel.startsWith("/")) {
console.error(`Refusing to pack unsafe asset path: ${rel}`);
process.exit(1);
}
const src = join(extensionDir, rel);
const dest = join(stageDir, rel);
await mkdir(dirname(dest), { recursive: true });
await cp(src, dest);
}

await normalizeStaging(stageDir);

// TZ=UTC so DOS timestamps in the zip are timezone-independent (CI is UTC).
const result = await $`zip -r -X ${outFile} manifest.json ${assetPaths}`
.cwd(stageDir)
.env({ ...process.env, TZ: "UTC" })
.nothrow();
Comment thread
cursor[bot] marked this conversation as resolved.

if (result.exitCode !== 0) {
console.error(result.stderr.toString() || result.stdout.toString());
process.exit(result.exitCode ?? 1);
}

// Keep a copy of the built manifest next to the .mcpb for inspection
await cp(builtManifestPath, join(distDir, "manifest.json"));
await rm(stageDir, { recursive: true, force: true });

const listing = await $`unzip -l ${outFile}`.text();
console.log(listing.trimEnd());
console.log(`\nInjected long_description from ${readmeLabel}`);
console.log(`Wrote ${outFile}`);
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
"type": "module",
"private": true,
"scripts": {
"build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs"
"build": "bun ./scripts/validate-cursor-schema.mjs && bun ./scripts/validate-cursor-structure.mjs && bun ./scripts/validate-mcpb.mjs && bun run pack:mcpb",
"pack:mcpb": "bun ./claude-extensions/paper-desktop/scripts/pack.ts",
"test": "bun run build"
},
"devDependencies": {
"@anthropic-ai/mcpb": "^2.1.2",
"@types/bun": "latest",
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1"
Expand Down
Loading
Loading