From c394d8136707309355e9663f84ffe8245f10ed59 Mon Sep 17 00:00:00 2001 From: Tien Pham Date: Mon, 10 Aug 2026 10:37:41 +0300 Subject: [PATCH] feat: claude plugin --- .agents/plugins/marketplace.json | 20 +++++++++++++++ .claude-plugin/README.md | 20 +++++++++++++++ .claude-plugin/marketplace.json | 42 ++++++++++++++++++++++++++++++++ tools/ci/src/main.rs | 38 +++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/README.md create mode 100644 .claude-plugin/marketplace.json diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 00000000000..250383ed50d --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "spacetimedb-plugins", + "interface": { + "displayName": "SpacetimeDB" + }, + "plugins": [ + { + "name": "spacetimedb", + "source": { + "source": "local", + "path": "./codex-plugin/plugins/spacetimedb" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_USE" + }, + "category": "Developer Tools" + } + ] +} diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md new file mode 100644 index 00000000000..ba1a64fa01d --- /dev/null +++ b/.claude-plugin/README.md @@ -0,0 +1,20 @@ +# SpacetimeDB for Claude Code + +`marketplace.json` here installs the repository's `skills/` directory and the SpacetimeDB MCP +server into Claude Code: + +```bash +claude plugin marketplace add clockworklabs/SpacetimeDB +claude plugin install spacetimedb@spacetimedb-plugins +``` + +From a local checkout, use `./` as the source. Confirm with `claude plugin details spacetimedb`, +which lists the skills and the MCP server. The MCP server runs `spacetime mcp`, which bridges +stdio to the HTTP endpoint on whichever server your CLI is configured for. + +## Maintaining + +The entry uses `strict: false`, so it reads `skills/` directly and needs no manifest inside a +plugin payload. It lists each skill explicitly, so adding one under `skills/` means adding it +here too. `cargo ci lint` fails when the list and the directory disagree. Validate the catalog +with `claude plugin validate .`. diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 00000000000..4f8953b92f1 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", + "name": "spacetimedb-plugins", + "description": "SpacetimeDB plugins for Claude Code", + "owner": { + "name": "Clockwork Labs", + "url": "https://spacetimedb.com" + }, + "plugins": [ + { + "name": "spacetimedb", + "displayName": "SpacetimeDB", + "description": "SpacetimeDB skills for building modules and clients in Rust, C#, TypeScript, C++, Unity, and Unreal, plus an MCP server that lists your databases, reads schemas, runs SQL, and calls reducers with your CLI login.", + "author": { + "name": "Clockwork Labs" + }, + "category": "database", + "homepage": "https://spacetimedb.com", + "source": "./skills", + "strict": false, + "skills": [ + "./cli", + "./concepts", + "./cpp-server", + "./csharp-client", + "./csharp-server", + "./mcp", + "./rust-server", + "./typescript-client", + "./typescript-server", + "./unity", + "./unreal" + ], + "mcpServers": { + "spacetimedb": { + "command": "spacetime", + "args": ["mcp"] + } + } + } + ] +} diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 469d085bf5e..247361aff9b 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -359,6 +359,43 @@ fn check_codex_plugin_skills_sync() -> Result<()> { Ok(()) } +fn check_claude_marketplace_skills() -> Result<()> { + // Claude catalog lists each skill explicitly, so a new skill under `skills/` is + // invisible to Claude until it is added there + let catalog_path = Path::new(".claude-plugin/marketplace.json"); + let contents = fs::read_to_string(catalog_path).with_context(|| format!("reading {}", catalog_path.display()))?; + let catalog: Value = + serde_json::from_str(&contents).with_context(|| format!("parsing {}", catalog_path.display()))?; + let listed: BTreeSet = catalog["plugins"][0]["skills"] + .as_array() + .ok_or_else(|| anyhow::anyhow!("{} plugins[0].skills must be an array", catalog_path.display()))? + .iter() + .filter_map(|value| value.as_str()) + .map(|entry| entry.trim_start_matches("./").to_owned()) + .collect(); + + let mut present = BTreeSet::new(); + for entry in fs::read_dir("skills").with_context(|| "reading skills")? { + let entry = entry?; + if entry.path().join("SKILL.md").is_file() { + present.insert(entry.file_name().to_string_lossy().into_owned()); + } + } + + if listed != present { + let missing: Vec<_> = present.difference(&listed).cloned().collect(); + let extraneous: Vec<_> = listed.difference(&present).cloned().collect(); + bail!( + "{} skills list does not match skills/:\n missing: {:?}\n extraneous: {:?}\nUpdate the skills list in {}", + catalog_path.display(), + missing, + extraneous, + catalog_path.display() + ); + } + Ok(()) +} + #[derive(Subcommand)] enum CiCmd { /// Runs tests @@ -683,6 +720,7 @@ fn main() -> Result<()> { ensure_repo_root()?; check_pnpm_release_age_policy()?; check_codex_plugin_skills_sync()?; + check_claude_marketplace_skills()?; // `cargo fmt --all` only checks files that Cargo discovers through workspace/package targets. // However, we also keep Rust sources in a locations that are tracked but not part of our workspace, // so this approach properly catches all the files, where `cargo fmt` does not.