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
6 changes: 3 additions & 3 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ Repo-local maintainer skill; it is not shipped to plugin users.

Rerunning after a partial run is safe: an existing tag at HEAD resumes at the push.

`PUBLISH_LOCAL=1 ./scripts/release.sh "Notes"` publishes to npm from this machine's npm login (no provenance) before pushing the tag. It builds, tests and smoke-tests a clean worktree of the tag first. The CI npm job then finds the version on npm and skips it. Use this until an `NPM_TOKEN` secret exists.
`PUBLISH_LOCAL=1 ./scripts/release.sh "Notes"` publishes to npm from this machine's npm login (no provenance) before pushing the tag. It builds, tests and smoke-tests a clean worktree of the tag first. npm processes uploads asynchronously; the CI npm job waits up to 10 minutes for the version and then skips it. Use this until the npm trusted publisher is configured.

`publish.yml` (on `v*.*.*` tags) re-verifies versions against the tag, rebuilds and tests, then:
- `npm publish --provenance` (skipped if the version exists; needs the `NPM_TOKEN` secret in the `npm` environment);
- MCP registry `mcp-publisher validate` + `publish` via GitHub OIDC (fatal on failure);
- `npm publish --provenance` via npm trusted publishing (OIDC, no token; skipped if the version exists). One-time setup on npmjs.com: package Settings → Trusted publisher → GitHub Actions, `PyModel/designer-skill`, workflow `publish.yml`, environment `npm`, with direct publishing allowed (`npm trust github … --allow-publish`). `npm trust github` cannot do it from a 2FA-bypass token. Until then the job falls back to waiting for a `PUBLISH_LOCAL=1` publish;
- MCP registry `mcp-publisher validate` + `publish` via GitHub OIDC (fatal on failure). The namespace is case-sensitive: `io.github.PyModel/*`;
- `gh release create` from the tag notes (skipped if it exists).

A failed publish run is re-run from the Actions tab; each step skips completed work.
Expand Down
20 changes: 10 additions & 10 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
environment: npm
permissions:
contents: read
id-token: write # npm provenance
id-token: write # npm trusted publishing + provenance
steps:
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
Expand All @@ -82,22 +82,22 @@ jobs:
path: pkg
- name: Publish to npm with provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
TAG: ${{ github.ref_name }}
# Trusted publishing (OIDC, no token): npmjs.com → package Settings → Trusted
# publisher → GitHub Actions, PyModel/designer-skill, publish.yml, environment npm.
# Provenance is attached automatically.
run: |
published() { npm view "@pymodel/designer-skill-mcp@${TAG#v}" version >/dev/null 2>&1; }
if [ -n "$NODE_AUTH_TOKEN" ]; then
if published; then echo "already on npm; skipping"; exit 0; fi
npm publish ./pkg/*.tgz --provenance --access public --ignore-scripts
exit 0
fi
# No token: release.sh PUBLISH_LOCAL=1 published it; npm processes uploads
# asynchronously, so wait for the version instead of racing it.
if published; then echo "already on npm; skipping"; exit 0; fi
if npm publish ./pkg/*.tgz --provenance --access public --ignore-scripts; then exit 0; fi
# release.sh PUBLISH_LOCAL=1 may have published it already: npm processes
# uploads asynchronously, so wait for the version instead of failing the race.
echo "::warning::CI publish failed; waiting up to 10 min for a local publish"
for _ in $(seq 60); do
if published; then echo "published locally; skipping"; exit 0; fi
sleep 10
done
echo "::error::${TAG#v} not on npm after 10 min and no NPM_TOKEN secret to publish it"
echo "::error::${TAG#v} is not on npm: configure the trusted publisher or publish with PUBLISH_LOCAL=1"
exit 1

registry:
Expand Down
14 changes: 12 additions & 2 deletions designer-skill-mcp/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,28 @@ export function parseCli(argv: string[]): CliCommand {
const rawPort = values.port ?? process.env.PORT ?? "3017";
const port = parsePort(rawPort);
if (port === null) return { kind: "error", message: `Invalid port "${rawPort}": expected an integer from 0 to 65535.` };
const allowedHosts = values["allowed-host"] ?? [];
const badHost = allowedHosts.find((h) => hostnameOf(h) !== h);
if (badHost !== undefined) {
// The SDK compares the Host header's parsed hostname exactly: a port or upper case never matches.
return { kind: "error", message: `Invalid --allowed-host "${badHost}": expected a lowercase hostname without a port.` };
}
const envRoots = (process.env.DESIGNER_SKILL_ROOTS ?? "").split(",").map((r) => r.trim()).filter(Boolean);
return {
kind: "run",
http: !!values.http,
port,
host: values.host ?? "127.0.0.1",
roots: [...(values.root ?? []), ...envRoots],
allowedHosts: values["allowed-host"] ?? [],
allowedHosts,
notifyUpdates: !values["no-update-notifier"],
};
}

function hostnameOf(value: string): string | null {
try { return new URL(`http://${value}`).hostname; } catch { return null; }
}

export const HELP_TEXT = `designer-skill-mcp — plug-and-play MCP for UI design superpowers

Usage:
Expand All @@ -67,7 +77,7 @@ Flags:
--port <n>, --port=<n> HTTP port (default 3017, or PORT)
--host <addr> HTTP bind address (default 127.0.0.1). Any non-loopback address
requires DESIGNER_SKILL_HTTP_TOKEN and at least one --root.
--allowed-host <name> Host header allowed for a non-loopback bind (repeatable)
--allowed-host <name> Hostname (no port) allowed in the Host header of a non-loopback bind (repeatable)
--version, -v Print version and exit
--check-update Check npm for a newer release and exit
--no-update-notifier Skip update checks (also NO_UPDATE_NOTIFIER=1)
Expand Down
7 changes: 7 additions & 0 deletions designer-skill-mcp/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ describe("parseCli", () => {
expect(run("--http", "--port=4000", "--host=0.0.0.0", "--root=/srv/app", "--allowed-host=design.example")).toEqual(expected);
});

it("rejects --allowed-host values the SDK's hostname match can never accept", () => {
for (const host of ["design.example:3017", "Design.Example", "http://design.example", "a b"]) {
expect(run("--allowed-host", host), host).toMatchObject({ kind: "error" });
}
expect(run("--allowed-host", "[::1]", "--allowed-host", "10.0.0.5")).toMatchObject({ allowedHosts: ["[::1]", "10.0.0.5"] });
});

it("collects repeated and environment roots", () => {
vi.stubEnv("DESIGNER_SKILL_ROOTS", " /c , ,/d");
expect(run("--root", "/a", "--root", "/b")).toMatchObject({ roots: ["/a", "/b", "/c", "/d"] });
Expand Down
Loading
Loading