The CLI/client package is published with changesets + npm trusted
publishing (OIDC — no long-lived NPM_TOKEN). The Release workflow on
main cuts the published versions.
On npmjs.com the package needs a GitHub Actions trusted publisher for:
- Organization:
buildinternet - Repository:
uploads - Workflow:
release.yml - No environment
- Allowed action:
npm publish
Keep maintainer 2FA enabled. The workflow pins npm 11.18.0 (trusted publishing requires npm ≥ 11.5.1 and Node ≥ 22.14).
-
Make user-visible changes under
packages/uploads(and keepskills/uploads-cliin sync when commands change). -
Add a changeset:
pnpm changeset # or write .changeset/<slug>.md by handHeader names the package that actually changed. Two valid targets:
--- "@buildinternet/uploads": minor --- User-facing description.
--- "@uploads/plugin": patch --- Why existing plugin installs must pick this up.
-
Merge the feature PR to
main(with the.changeset/*.mdfile).
Never hand-edit packages/uploads/package.json or
packages/plugin/package.json version — changeset version owns them.
Changesets ignore the other private packages (@uploads/api, @uploads/mcp,
…). They deploy via Workers Builds. @uploads/plugin is the exception: it is
versioned, not published.
- After one or more feature PRs land with pending changesets, the Release
workflow opens or updates a
chore: version packagesPR. That PR runschangeset version: bumps the version, writespackages/uploads/CHANGELOG.md, and removes consumed changeset files. - Review and merge the version PR.
- The same workflow re-runs on
mainwith no pending changesets, then:- tests / builds / pack-checks the package
- runs
changeset publish(OIDC provenance) - creates a GitHub release tagged
uploads-v<version>(same prefix as before) - publishes
server.jsonto the MCP Registry assh.uploads/mcp
Verify the version and provenance on npm after the workflow succeeds. Confirm the MCP listing with:
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=sh.uploads/mcp"The registry stores metadata only. It checks that the published npm package
declares mcpName equal to server.json name, then records how clients
run the server: stdio via uploads mcp, and the hosted remote at
https://agents.uploads.sh/mcp.
The listing name is the reverse-DNS of uploads.sh: sh.uploads/mcp. That
string must match mcpName on @buildinternet/uploads. A registry publish
always follows an npm publish of that version.
changeset version copies the new package version into server.json so the
version PR shows the stamp. The publish job stamps again before
mcp-publisher publish. CI runs pnpm server-json:check on every pull
request so name, mcpName, and the two version fields cannot drift.
Publish authenticates with HTTP domain proof, not GitHub OIDC. The public
record is https://uploads.sh/.well-known/mcp-registry-auth (served from
apps/web/public/.well-known/mcp-registry-auth). Prefer the
MCP_REGISTRY_PRIVATE_KEY_PEM Actions secret (PEM). The job still accepts the
legacy hex secret MCP_PRIVATE_KEY. The publish job runs
mcp-publisher login http --domain uploads.sh. That grant is sh.uploads/*.
changeset version also regenerates
apps/web/public/.well-known/mcp/server-card.json from server.json. The
hosted worker advertises the same CLI version in serverInfo, not
apps/mcp's private package.json.
Do not change server.json name or packages/uploads mcpName without a
matching npm publish. The registry treats those strings as the server's
identity.
The Claude/Codex plugin version is not the CLI version. Claude caches
installs under ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/.
A change that must reach existing installs (manifest, hooks, skills) needs a
changeset for @uploads/plugin:
---
"@uploads/plugin": patch
---
Why existing installs must pick this up.changeset version bumps packages/plugin and copies the number into
plugin.json, plugins/claude/uploads/.claude-plugin/plugin.json,
.claude-plugin/marketplace.json, and .codex-plugin/plugin.json. CI runs
pnpm plugin-version:check so those files cannot drift. A plugin-only
version PR does not publish to npm; the MCP Registry listing still follows
the CLI version via server.json.
After the version PR lands on main, existing installs need
/plugin marketplace update then /plugin update uploads@uploads (or
uninstall and install).
uploads has two listings in the Claude directory. Each updates in its own way. Both are managed at claude.ai/directory/manage under Submissions.
| Listing | What it lists | How it updates |
|---|---|---|
Plugin uploads.sh |
plugins/claude/uploads on main |
From the repo: each new commit to that folder is a new version |
Connector Uploads (public page) |
https://agents.uploads.sh/mcp |
Only in the portal: edit the listing, then submit the edit for review |
The directory follows main and reads only plugins/claude/uploads. It
picks up a new commit to that folder on a schedule, or at once when you
select Check for new commits on the plugin's page. It then validates the
folder and runs a security scan. A version that passes waits for Publish,
and a reviewer then publishes it. The plugin's Versions tab shows each
scanned commit and its result.
Rules that follow from this:
- A merge is not a release. The listing serves the last published version until a newer one is published. A version that fails the scan, or is held for a reviewer, leaves the live version in place. After a failed scan, later versions also wait for a reviewer.
- Raise the version for every release. Use the
@uploads/pluginchangeset described in Plugin version. - Listing text comes from the plugin. The display name and short
description follow
plugin.jsonin the live version. The long description isplugins/claude/uploads/README.md. The portal's Listing tab is read-only. - Describe what the plugin runs. The security scan compares behavior with the README. A new hook or mod that runs a command or calls a service needs a line in the plugin README's data and network access section.
- Expect a reviewer hold for code. The plugin is a subfolder of the
repository, so a hook or mod that runs a non-shell file (such as
hooks/register.tsx) is held as Scripts the validator couldn't follow. A hold delays publishing; it is not a rejection.
Before a change to the plugin folder lands, check it:
claude plugin validate --strict plugins/claude/uploads
claude plugin test plugins/claude/uploads # the mod's testsThe portal runs more checks than the CLI. To run them on a branch before it
merges, start Submit new → Plugin bundle, enter
buildinternet/uploads@<branch> with plugin path plugins/claude/uploads,
select Validate, read the report, and leave without submitting.
Each channel updates on its own. For a short time they can disagree:
- GitHub marketplace: new installs get
mainat once. Existing installs update when the plugin version changes. - npm CLI: CLI changes reach users when the version PR publishes.
- Claude directory: users keep the last published version until the next one is published. Docs that describe a new plugin feature are ahead of directory users until then.
pnpm changeset # add a pending bump
pnpm run changeset:version # apply pending → version + CHANGELOG + server.json (local only)
pnpm run changeset:publish # npm publish packages that need it (needs auth)Do not re-use or move a published version or release tag.
If the MCP Registry step fails after npm already published, re-run Release
with mcp_registry_only (Actions → Release → Run workflow). Republishing the
same version is a no-op. Or publish server.json locally:
brew install mcp-publisher
mcp-publisher login http --domain uploads.sh --private-key "$MCP_PRIVATE_KEY"
pnpm server-json:check
mcp-publisher validate
mcp-publisher publish