Skip to content

Commit a06c92a

Browse files
Create Claude Plugin (#5672)
**Based on: #5582 # Description of Changes Add a Claude plugin for the SpacetimeDB skills and MCP server, alongside the existing Codex plugin. The entry uses `strict: false`, so it reads `skills/` directly and declares the MCP server inline. Usage: ```bash claude plugin marketplace add clockworklabs/SpacetimeDB claude plugin install spacetimedb@spacetimedb-plugins ``` <!-- Please describe your change, mention any related tickets, and so on here. --> # API and ABI breaking changes None. <!-- If this is an API or ABI breaking change, please apply the corresponding GitHub label. --> # Expected complexity level and risk 1 <!-- How complicated do you think these changes are? Grade on a scale from 1 to 5, where 1 is a trivial change, and 5 is a deep-reaching and complex change. This complexity rating applies not only to the complexity apparent in the diff, but also to its interactions with existing and future code. If you answered more than a 2, explain what is complex about the PR, and what other components it interacts with in potentially concerning ways. --> # Testing <!-- Describe any testing you've done, and any testing you'd like your reviewers to do, so that you're confident that all the changes work as expected! --> - [x] I tested the Claude plugin, `claude plugin validate .` passes, then install and `claude plugin details spacetimedb` reports the skills and MCP server. For testing, from repo root: ```bash claude plugin validate . claude plugin marketplace add ./ claude plugin install spacetimedb@spacetimedb-plugins claude plugin details spacetimedb ``` Then restart Claude
1 parent 047645e commit a06c92a

4 files changed

Lines changed: 120 additions & 0 deletions

File tree

.agents/plugins/marketplace.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "spacetimedb-plugins",
3+
"interface": {
4+
"displayName": "SpacetimeDB"
5+
},
6+
"plugins": [
7+
{
8+
"name": "spacetimedb",
9+
"source": {
10+
"source": "local",
11+
"path": "./codex-plugin/plugins/spacetimedb"
12+
},
13+
"policy": {
14+
"installation": "AVAILABLE",
15+
"authentication": "ON_USE"
16+
},
17+
"category": "Developer Tools"
18+
}
19+
]
20+
}

.claude-plugin/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# SpacetimeDB for Claude Code
2+
3+
`marketplace.json` here installs the repository's `skills/` directory and the SpacetimeDB MCP
4+
server into Claude Code:
5+
6+
```bash
7+
claude plugin marketplace add clockworklabs/SpacetimeDB
8+
claude plugin install spacetimedb@spacetimedb-plugins
9+
```
10+
11+
From a local checkout, use `./` as the source. Confirm with `claude plugin details spacetimedb`,
12+
which lists the skills and the MCP server. The MCP server runs `spacetime mcp`, which bridges
13+
stdio to the HTTP endpoint on whichever server your CLI is configured for.
14+
15+
## Maintaining
16+
17+
The entry uses `strict: false`, so it reads `skills/` directly and needs no manifest inside a
18+
plugin payload. It lists each skill explicitly, so adding one under `skills/` means adding it
19+
here too. `cargo ci lint` fails when the list and the directory disagree. Validate the catalog
20+
with `claude plugin validate .`.

.claude-plugin/marketplace.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3+
"name": "spacetimedb-plugins",
4+
"description": "SpacetimeDB plugins for Claude Code",
5+
"owner": {
6+
"name": "Clockwork Labs",
7+
"url": "https://spacetimedb.com"
8+
},
9+
"plugins": [
10+
{
11+
"name": "spacetimedb",
12+
"displayName": "SpacetimeDB",
13+
"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.",
14+
"author": {
15+
"name": "Clockwork Labs"
16+
},
17+
"category": "database",
18+
"homepage": "https://spacetimedb.com",
19+
"source": "./skills",
20+
"strict": false,
21+
"skills": [
22+
"./cli",
23+
"./concepts",
24+
"./cpp-server",
25+
"./csharp-client",
26+
"./csharp-server",
27+
"./mcp",
28+
"./rust-server",
29+
"./typescript-client",
30+
"./typescript-server",
31+
"./unity",
32+
"./unreal"
33+
],
34+
"mcpServers": {
35+
"spacetimedb": {
36+
"command": "spacetime",
37+
"args": ["mcp"]
38+
}
39+
}
40+
}
41+
]
42+
}

tools/ci/src/main.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,43 @@ fn check_codex_plugin_skills_sync() -> Result<()> {
359359
Ok(())
360360
}
361361

362+
fn check_claude_marketplace_skills() -> Result<()> {
363+
// Claude catalog lists each skill explicitly, so a new skill under `skills/` is
364+
// invisible to Claude until it is added there
365+
let catalog_path = Path::new(".claude-plugin/marketplace.json");
366+
let contents = fs::read_to_string(catalog_path).with_context(|| format!("reading {}", catalog_path.display()))?;
367+
let catalog: Value =
368+
serde_json::from_str(&contents).with_context(|| format!("parsing {}", catalog_path.display()))?;
369+
let listed: BTreeSet<String> = catalog["plugins"][0]["skills"]
370+
.as_array()
371+
.ok_or_else(|| anyhow::anyhow!("{} plugins[0].skills must be an array", catalog_path.display()))?
372+
.iter()
373+
.filter_map(|value| value.as_str())
374+
.map(|entry| entry.trim_start_matches("./").to_owned())
375+
.collect();
376+
377+
let mut present = BTreeSet::new();
378+
for entry in fs::read_dir("skills").with_context(|| "reading skills")? {
379+
let entry = entry?;
380+
if entry.path().join("SKILL.md").is_file() {
381+
present.insert(entry.file_name().to_string_lossy().into_owned());
382+
}
383+
}
384+
385+
if listed != present {
386+
let missing: Vec<_> = present.difference(&listed).cloned().collect();
387+
let extraneous: Vec<_> = listed.difference(&present).cloned().collect();
388+
bail!(
389+
"{} skills list does not match skills/:\n missing: {:?}\n extraneous: {:?}\nUpdate the skills list in {}",
390+
catalog_path.display(),
391+
missing,
392+
extraneous,
393+
catalog_path.display()
394+
);
395+
}
396+
Ok(())
397+
}
398+
362399
#[derive(Subcommand)]
363400
enum CiCmd {
364401
/// Runs tests
@@ -683,6 +720,7 @@ fn main() -> Result<()> {
683720
ensure_repo_root()?;
684721
check_pnpm_release_age_policy()?;
685722
check_codex_plugin_skills_sync()?;
723+
check_claude_marketplace_skills()?;
686724
// `cargo fmt --all` only checks files that Cargo discovers through workspace/package targets.
687725
// However, we also keep Rust sources in a locations that are tracked but not part of our workspace,
688726
// so this approach properly catches all the files, where `cargo fmt` does not.

0 commit comments

Comments
 (0)