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
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ jobs:
with:
node-version: 22
cache: npm
cache-dependency-path: scripts/package-lock.json
# The tooling lives in scripts/ so plugin installs don't run npm ci on it.
- run: npm ci
working-directory: scripts
- run: npm test
working-directory: scripts
- run: npm run check
working-directory: scripts
- name: Validate Claude Code plugin
run: npx -y @anthropic-ai/claude-code@2.1.293 plugin validate .
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ The key gives full access to its workspace. `-g` writes it to your user config i

## Developing

Run `npm install`, then `npm test` and `npm run check`. CI runs the same checks.
In `scripts/`, run `npm install`, then `npm test` and `npm run check`. CI runs the same checks. The tooling lives there so plugin installs don't install its dependencies.

The Claude Code plugin declares its server in `.claude-plugin/plugin.json`, so it can use the URL you enter. `.mcp.json` holds the baserow.io URL for other tools that read Claude-format plugins, such as GitHub Copilot CLI and Cursor.

Expand Down
14 changes: 0 additions & 14 deletions package.json

This file was deleted.

19 changes: 13 additions & 6 deletions scripts/check-manifests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url";
import Ajv from "ajv";
import Ajv2020 from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import { parse as parseYaml } from "yaml";

export const SERVER_NAME = "baserow";
const CLAUDE_URL = "${user_config.mcp_url}";
Expand Down Expand Up @@ -230,11 +231,14 @@ export async function checkSkills(root) {
errors.push(`${file}: front matter missing`);
continue;
}
const field = (k) =>
front[1]
.match(new RegExp(`^${k}:[ \\t]*(.*)$`, "m"))?.[1]
.trim()
.replace(/^(["'])(.*)\1$/, "$2");
let meta;
try {
meta = parseYaml(front[1]);
} catch (e) {
errors.push(`${file}: front matter isn't valid YAML (${e.message.split("\n")[0]})`);
continue;
}
const field = (k) => (typeof meta?.[k] === "string" ? meta[k].trim() : undefined);
const name = field("name");
if (!name) errors.push(`${file}: name missing`);
else if (name !== dir) errors.push(`${file}: name "${name}" must match folder "${dir}"`);
Expand All @@ -243,12 +247,15 @@ export async function checkSkills(root) {
return errors;
}

// Every logo and file a listing points at must exist in the repo.
// Every logo, file and plugin folder a manifest points at must exist in the repo.
export async function checkAssets(m, root) {
const ui = m.agentPlugin?.extensions?.["com.openai"]?.interface ?? {};
const refs = [
...["logo", "logoDark", "composerIcon", "composerIconDark"].map((k) => [FILES.agentPlugin, `interface.${k}`, ui[k]]),
...["logo", "mcpServers", "skills"].map((k) => [FILES.cursorPlugin, k, m.cursorPlugin?.[k]]),
[FILES.gemini, "contextFileName", m.gemini?.contextFileName],
[FILES.claudeMarketplace, "plugins[0].source", m.claudeMarketplace?.plugins?.[0]?.source],
[FILES.codexMarketplace, "plugins[0].source.path", m.codexMarketplace?.plugins?.[0]?.source?.path],
];
const errors = [];
for (const [file, key, rel] of refs) {
Expand Down
18 changes: 18 additions & 0 deletions scripts/check-manifests.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
checkSchemas,
checkSkills,
loadManifests,
runAll,
} from "./check-manifests.mjs";

const SCHEMA_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "schemas");
Expand Down Expand Up @@ -290,3 +291,20 @@ test("registry URL variables must match the URL", () => {
assert.match(errors, /uses \{baserow_host\}, which isn't in its variables/);
assert.match(errors, /declares host, which its URL doesn't use/);
});

test("front matter that isn't valid YAML fails", async (t) => {
const root = await repoWithSkill(t, "baserow-basics", "---\nname: baserow-basics\ndescription: Use when: the user asks\n---\n");
assert.match((await checkSkills(root)).join("\n"), /front matter isn't valid YAML/);
});

test("a marketplace source that doesn't exist is reported", async (t) => {
const root = await tempDir(t, "source-");
const m = validManifests();
m.claudeMarketplace.plugins[0].source = "./plugins/baserow";
assert.ok((await checkAssets(m, root)).includes(".claude-plugin/marketplace.json: plugins[0].source ./plugins/baserow not found"));
});

test("the repo itself passes every check", async () => {
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
assert.deepEqual(await runAll(root), []);
});
19 changes: 18 additions & 1 deletion package-lock.json → scripts/package-lock.json

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

17 changes: 17 additions & 0 deletions scripts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "baserow-mcp",
"private": true,
"type": "module",
"scripts": {
"test": "node --test *.test.mjs",
"check": "node check-manifests.mjs .."
},
"devDependencies": {
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"yaml": "^2.9.1"
},
"engines": {
"node": ">=22"
}
}
Loading