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
20 changes: 20 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
20 changes: 20 additions & 0 deletions .claude-plugin/README.md
Original file line number Diff line number Diff line change
@@ -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 .`.
42 changes: 42 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
}
]
}
38 changes: 38 additions & 0 deletions tools/ci/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = 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
Expand Down Expand Up @@ -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.
Expand Down
Loading