@shipeasy/mcp
v2.4.0
Published
Feature flags, A/B experiments, kill switches, dynamic config & i18n — Shipeasy MCP server.
Maintainers
Keywords
Readme
@shipeasy/mcp — unified Model Context Protocol server
Replaces packages/mcp-server/. One MCP server, one npm package, covers both subsystems:
- Experimentation — gates, configs, experiments, metrics, events (from experiment-platform/10-mcp-server.md).
- String manager / i18n — label profiles, keys, drafts, translations, publish, codemods (from string-manager-platform/plan.md § MCP server).
AI assistants (Claude Code, Cursor, Copilot, Windsurf, Claude Desktop, Continue) talk to one server and get the full platform.
📖 Docs: docs.shipeasy.ai/get-started/mcp — setup guide · full tool reference (auto-generated from the tool catalog).
Status / migration
- New location:
packages/mcp/(this doc). - Old location:
packages/mcp-server/— will be deleted once this ships. The npm package rename (@shipeasy/mcp-server→@shipeasy/mcp) is a breaking change; the old name publishes one final version that re-exports the new package as a deprecation shim. - Binary rename:
shipeasy-mcp(unchanged — the old binary name is preserved so existing.claude/settings.jsonentries keep working).
Install / run
Two steps: (1) register the server with your AI assistant, (2) authenticate once with shipeasy-mcp install. Step 2 is required before any mutating tool will work — it runs a browser-based PKCE device flow against /auth/device/* and writes the token to ~/.config/shipeasy/config.json (shared with @shipeasy/cli).
Step 1 — register
npx (recommended for AI assistant configs)
// ~/.claude/settings.json | .cursor/mcp.json | .windsurf/mcp.json | .mcp.json (project-local)
{
"mcpServers": {
"shipeasy": {
"command": "npx",
"args": ["-y", "@shipeasy/mcp@latest"]
}
}
}Global install
npm i -g @shipeasy/mcp
# or
pnpm add -g @shipeasy/mcp{ "mcpServers": { "shipeasy": { "command": "shipeasy-mcp" } } }Through the CLI (if @shipeasy/cli is already installed)
shipeasy mcp install # auto-writes ~/.claude/settings.json + .cursor/mcp.json
shipeasy mcp start # run stdio server (same binary, different entry)Configuration (env)
The server is configured through environment variables, so every setting flows
straight from your mcp.json / settings.json env block:
{
"mcpServers": {
"shipeasy": {
"command": "npx",
"args": ["-y", "@shipeasy/mcp@latest"],
"env": {
"SHIPEASY_MCP_LIST_GUARD": "on",
"SHIPEASY_MCP_LIST_GUARD_WINDOW_MINUTES": "10"
}
}
}
}| Variable | Default | Effect |
| ---------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| XDG_CONFIG_HOME | — | Overrides the token/config file location. |
| SHIPEASY_MCP_LIST_GUARD | on | List-before-create guard. When on, every *_create that has a *_list sibling refuses to run unless it carries a fresh listToken that the sibling *_list just returned — a forcing-function so the assistant checks for an existing resource before creating a duplicate. Set off to disable. |
| SHIPEASY_MCP_LIST_GUARD_WINDOW_MINUTES | 10 | How long a minted listToken stays valid (effective validity is 1×–2× this, since the previous window is also accepted). A token also dies with the server process, so it can never be replayed across sessions. |
Step 2 — authenticate
Run this once per machine:
shipeasy-mcp installWhat happens:
- The CLI generates a PKCE verifier + challenge.
- It calls
POST {api_base}/auth/device/startto open a session; the worker returns astate. - Your default browser opens at
{app_base}/cli-auth?state=…&code_challenge=…&source=mcp. Sign in with your existing Shipeasy account (GitHub, Google, or magic link — same as the dashboard). - The UI page calls
POST {api_base}/auth/device/completewithproject_id+ PKCE verifier. - The CLI polls
GET {api_base}/auth/device/poll?state=…(headerX-Code-Verifier) every ~2 s until it receives{ token, project_id }. - Token is written to
~/.config/shipeasy/config.jsonwithchmod 600. The directory is createdchmod 700.
Flags:
| Flag | Effect |
| ---------------- | ----------------------------------------------------------------------------------------- |
| --force | Overwrite an existing session instead of aborting. |
| --no-browser | Print the auth URL; useful on remote / headless machines (paste it into a local browser). |
Other subcommands:
shipeasy-mcp whoami # prints { project_id, user_email, config_path }
shipeasy-mcp logout # deletes ~/.config/shipeasy/config.json
shipeasy-mcp --help # usage
shipeasy-mcp --versionWhy is this a CLI subcommand instead of an MCP tool?
The MCP stdio transport runs inside the AI assistant — it can't block for a browser round-trip, spawn new windows, or receive a browser-delivered callback. Browser-based auth has to run in a terminal the user owns. Once the token is written, every MCP server instance on the machine (Claude Code, Cursor, Windsurf, MCP Inspector, etc.) reads the same ~/.config/shipeasy/config.json — one install, many clients.
The in-process auth_login MCP tool therefore always returns a pointer back to the CLI command rather than trying to launch a browser itself.
Manual stdio invocation (for testing)
npx -y @shipeasy/mcp
# or pipe it through MCP Inspector:
npx @modelcontextprotocol/inspector npx -y @shipeasy/mcpProtocol surface
Transport: stdio (JSON-RPC 2.0 framed by Content-Length headers per MCP spec). Capabilities advertised on initialize:
{
"tools": { "listChanged": true },
"prompts": { "listChanged": false },
"resources": { "subscribe": true, "listChanged": true },
"logging": {}
}- Tools — actions the assistant can invoke (the full catalog below).
- Prompts — named, parameterized workflow playbooks (see Prompts section).
- Resources — read-only views of project state the assistant can pull in as context (see Resources section).
- Logging — structured progress and error notifications via
notifications/message.
Authentication
Every mutating tool requires a Shipeasy session. Credentials live in ~/.config/shipeasy/config.json and are shared between @shipeasy/mcp and @shipeasy/cli — whichever tool the user authenticates in first, both pick up the same session.
shipeasy-mcp install(terminal) completes the PKCE device flow and writes the config file (see Step 2 — authenticate above).- The MCP stdio server's
auth_checktool reads the file on every call — no cached state in the server process. auth_logininvoked over MCP always returns an actionable error asking the human to runshipeasy-mcp installin a terminal (stdio can't open a browser safely).auth_logoutremoves the file; the CLI equivalent works too.
~/.config/shipeasy/config.json (mode 0600, parent dir 0700)
{
"project_id": "proj_…",
"cli_token": "sdk_admin_…", ← scoped to admin Route Handlers; 90-day rotation
"api_base_url": "https://api.shipeasy.ai",
"app_base_url": "https://shipeasy.ai",
"user_email": "[email protected]",
"created_at": "2026-04-16T…Z"
}Tool-level auth policy:
| Category | Requires session | Notes |
| ----------------------------------------------- | ---------------- | ------------------------------------------------------ |
| auth_* | — / triggers it | |
| list_*, get_* | Yes | Read-only GETs against apps/ui admin Route Handlers. |
| create_*, update_*, delete_*, publish_* | Yes | Mutations — the CLI enforces checkLimit server-side. |
Filesystem / AST tools live in the
shipeasyCLI, not this server. Project detection and the i18n source scanners / codemods / loader-install / file-based push / local-Anthropic translation were removed from MCP — the server only makes admin-API calls now (plus the auth +.shipeasybind primitives). The authoritative, generated tool reference isdocs/mcp-reference.md; the catalog below is design-era narrative and has drifted.
Tool catalog
Tools are namespaced by subsystem: exp_* (experimentation), i18n_* (string manager), unprefixed (shared: auth, resource listing, SDK snippets). Project detection has moved to the shipeasy CLI.
Shared tools
auth_check
// input
{}
// output
{ "authenticated": true, "project_id": "…", "base_url": "…", "user_email": "…" }auth_login
Spawns shipeasy login, which opens the browser and blocks until the device-auth flow completes. Uses spawn (not execSync) so the MCP event loop stays responsive. The AI assistant should surface a "waiting for browser…" message — the CLI session polls for up to 5 minutes.
// input
{}
// output — same shape as auth_check after successauth_logout
Deletes ~/.config/shipeasy/config.json. No network call.
list_resources
Unified listing across both subsystems.
// input
{
"kind": "gates|configs|experiments|events|metrics|universes|attributes|profiles|chunks|keys|drafts|sdk_keys|all",
"limit": 50,
"search": "checkout" // optional name filter
}Hits the matching apps/ui admin Route Handler (e.g. /api/admin/gates, /api/admin/i18n/profiles) and returns a normalized list:
{
"kind": "experiments",
"items": [
{ "id": "…", "name": "checkout_button_color", "universe": "checkout", "status": "running", "allocation": 10 },
...
],
"next_cursor": null
}get_resource
Fetches a single resource by { kind, name_or_id }. Same routing as list_resources.
get_sdk_snippet
Returns ready-to-paste code for the detected language + framework, for either subsystem.
// input
{
"domain": "experiment | i18n",
"language": "typescript | python | ruby | go | java | php | swift | kotlin",
"framework": "nextjs | react | remix | vue | svelte | angular | nuxt | django | rails | laravel | spring | swiftui | compose | ... | null",
"type": "gate | experiment | config | label_load | label_render | loader_script | provider_setup",
"name": "new_checkout",
"params": { "color": "string" },
"success_event": "purchase_completed",
"success_value": true
}Output:
{
"install": "pnpm add shipeasy",
"env_vars": ["SHIPEASY_SERVER_KEY", "NEXT_PUBLIC_SHIPEASY_CLIENT_KEY"],
"init": "…code block…",
"usage": "…code block…",
"tracking": "…code block (only for experiments)…",
"validate_command": "pnpm tsc --noEmit",
"docs_url": "https://docs.shipeasy.ai/sdk/typescript/next"
}Templates are loaded from the installed SDK package (shipeasy/templates/<language>.js), not from this MCP bundle — so they track the customer's SDK version. Falls back to bundled templates if the SDK has no templates/ export or isn't installed yet. See packages/language_sdks/README.md for the source-of-truth template files per language.
list_prompts / get_prompt
Standard MCP — see Prompts section below. These are built into @modelcontextprotocol/sdk.
Experimentation tools (exp_*)
All mutations shell out to @shipeasy/cli via execFile (never exec with shell interpolation) with validated, slugified arguments. Names are auto-slugged before validation (SAFE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/) and a warning is logged if auto-slugging changed the input.
| Tool | What it does | Shells to |
| ------------------------- | ----------------------------------------------------------------- | ------------------------------------------ |
| exp_create_gate | Create a feature gate with targeting rules and rollout percentage | shipeasy gates create |
| exp_update_gate | Update rules, rollout, killswitch | shipeasy gates update |
| exp_delete_gate | | shipeasy gates delete |
| exp_create_config | Create a static config (sitevar) | shipeasy configs create |
| exp_update_config_value | Update the live value | shipeasy configs set |
| exp_create_universe | Create a universe with holdout % | shipeasy universes create |
| exp_create_experiment | Create experiment draft with groups, params, targeting gate | shipeasy experiments create |
| exp_start_experiment | Transition draft → running | shipeasy experiments start |
| exp_stop_experiment | Transition running → stopped, promote winning group | shipeasy experiments stop |
| exp_add_metric | Attach a metric as goal/guardrail | shipeasy experiments metric add |
| exp_create_event | Register an event schema | shipeasy events create |
| exp_create_metric | Create a metric from an event | shipeasy metrics create |
| exp_experiment_status | Current results + ship/hold/wait verdict | GET /api/admin/experiments/:name/results |
| exp_cleanup_winner | AST-drop losing branches after shipping | Local codemod via jscodeshift/ast-grep |
Representative input — exp_create_experiment:
{
"name": "checkout_button_color",
"description": "Test green vs. gray on new checkout",
"universe": "checkout",
"allocation": 10,
"groups": [
{ "name": "control", "weight": 5000, "params": { "color": "gray" } },
{ "name": "test", "weight": 5000, "params": { "color": "green" } }
],
"params_schema": { "color": "string" },
"targeting_gate": "new_checkout",
"success_event": "purchase_completed",
"success_aggregation": "count_users"
}Output:
{
"experiment": {
"name": "checkout_button_color",
"id": "exp_…",
"status": "draft",
"universe": "checkout"
},
"metric": { "name": "checkout_button_color_purchase_completed", "status": "created" },
"snippet": {
/* same shape as get_sdk_snippet */
},
"docs_url": "https://docs.shipeasy.ai/experiments/create"
}String manager tools (i18n_*)
The i18n admin API surface is projected from the spec (tag chain i18n ▸ Profiles | Keys | Drafts + x-cli verb), exactly like every other resource. The exact, authoritative list is the generated docs/mcp-reference.md; the current tools are:
| Tool | What it does | Endpoint |
| ---------------------- | --------------------------------------------------------------------------- | ------------------------------------------------- |
| i18n_profiles_list | List locale profiles | GET /api/admin/i18n/profiles |
| i18n_profiles_create | Create a locale profile (e.g. fr:prod) | POST /api/admin/i18n/profiles |
| i18n_profiles_publish| Publish a profile live (profile-wide KV rebuild + CDN purge) | POST /api/admin/i18n/profiles/{profileId}/publish |
| i18n_keys_list | List keys for a profile (filter by prefix / free-text) | GET /api/admin/i18n/keys |
| i18n_keys_push | Push new keys (insert-only — existing keys are skipped) | POST /api/admin/i18n/keys |
| i18n_keys_update | Overwrite one existing key's value (by key id) | PUT /api/admin/i18n/keys/{id} |
| i18n_keys_set | Set one key by profile name (omit → default) and publish it live | POST /api/admin/i18n/set |
| i18n_drafts_list | List staged machine-translation drafts | GET /api/admin/i18n/drafts |
The filesystem / AST i18n tooling — source scanning, codemods, loader install, key validation, site discovery, local-Anthropic draft translation, and file-based bulk push — is not part of the MCP surface; it lives in the shipeasy CLI (the fs-having consumer). It never hit the admin API, so it carries no spec op.
Prompts (workflow skills)
MCP prompts expose named, parameterized playbooks the AI can get_prompt() to load as context. Mirrors the skills bundle in experiment-platform/11-skills.md and string-manager-platform/plan.md § Skills.
| Prompt name | Purpose |
| ----------------------- | ------------------------------------------------------------------------- |
| setup_experimentation | Install the SDK, add env keys, wire a provider, verify with a sample gate |
| create_experiment | Propose → create → inject code → start → monitor |
| analyze_experiment | Pull results, compute lift + significance, emit ship/hold/wait verdict |
| cleanup_winner | Remove losing branches + dead gate code after shipping |
| rotate_sdk_keys | Revoke + re-issue client/server keys and update env vars |
(The i18n playbooks setup_i18n / translate_site / i18n_health were removed — they orchestrated the fs/AST tools that now live in the shipeasy CLI.)
Each prompt's body is a short markdown playbook embedded in the server bundle. The assistant fetches it once per conversation with get_prompt({ name }) and follows the steps.
Resources
Read-only project context streamed to the assistant via MCP's resources/read. The server advertises resource templates (URI patterns) so the assistant can pull context on demand without the user needing to paste files.
| URI template | Returns |
| -------------------------------------------- | ------------------------------------------------------------------- |
| shipeasy://project | Cached detect_project() output + auth_check() output. |
| shipeasy://experiments/{name} | Experiment config + latest stats JSON. |
| shipeasy://gates/{name} | Gate config + rollout state. |
| shipeasy://configs/{name} | Config value + history. |
| shipeasy://i18n/profiles/{profile} | Profile metadata + chunk list + coverage %. |
| shipeasy://i18n/profiles/{profile}/{chunk} | Published strings for one chunk. |
| shipeasy://i18n/drafts/{draft_id} | Draft metadata + per-key diff vs. source profile. |
| shipeasy://plans/current | Plan tier + current-month usage + remaining quota. |
| shipeasy://docs/{slug} | Pre-rendered markdown page from docs.shipeasy.ai — AI-consumable. |
resources/subscribe is supported on shipeasy://experiments/{name} — the server pushes notifications/resources/updated when cron finishes a new analysis run (detected via long-poll on /api/admin/experiments/:name/results?since=ts).
Package structure
packages/mcp/
package.json ← name: "@shipeasy/mcp", bin: "shipeasy-mcp"
tsconfig.json
tsup.config.ts ← esm output, single bundle
bin/
mcp.js ← shebang → runs dist/index.js
src/
index.ts ← Server setup, capability advertise, tool routing
rpc/
list-tools.ts
call-tool.ts
list-prompts.ts
get-prompt.ts
list-resources.ts
read-resource.ts
subscribe-resource.ts
tools/
schema.ts ← TOOLS array — MCP tool definitions
shared/
detect.ts ← detect_project (with realpath sandbox)
auth.ts ← auth_check, auth_login, auth_logout
list-resource.ts ← list_resources, get_resource
snippets.ts ← get_sdk_snippet + template loader
exp/
gates.ts
configs.ts
universes.ts
experiments.ts
events.ts
metrics.ts
status.ts
cleanup.ts
i18n/
scan.ts ← i18n_scan_code (ast-grep driver)
discover.ts ← i18n_discover_site
profiles.ts
chunks.ts
keys.ts
drafts.ts
translate.ts ← i18n_translate_draft (Anthropic shell-out)
publish.ts
usage.ts
codemods/
nextjs.ts
react.ts
vue.ts
svelte.ts
angular.ts
rails.ts
django.ts
index.ts ← dispatcher for i18n_codemod_preview/apply
validate.ts
loader.ts ← i18n_install_loader
prompts/
schema.ts ← PROMPTS array
setup_experimentation.md
create_experiment.md
analyze_experiment.md
cleanup_winner.md
setup_i18n.md
translate_site.md
i18n_health.md
rotate_sdk_keys.md
resources/
schema.ts ← RESOURCE_TEMPLATES
project.ts
experiments.ts
gates.ts
configs.ts
i18n.ts
plans.ts
docs.ts
util/
cli.ts ← execFile wrapper with shared error decoding
http.ts ← fetch wrapper w/ cli_token header
slug.ts ← autoSlug + SAFE_NAME_RE
safe-read.ts ← realpath-sandboxed fs reads
progress.ts ← notifications/progress helper
logger.ts ← notifications/message helper (respects client log level)
compat.ts ← semver compatibility check per language
templates/ ← fallback snippets when SDK has no templates/ export
typescript.ts
python.ts
ruby.ts
go.ts
java.ts
php.ts
swift.ts
kotlin.ts
test/
rpc/*.test.ts ← each request handler covered
tools/exp/*.test.ts
tools/i18n/*.test.ts
tools/shared/detect.test.ts ← realpath sandbox edge cases
fixtures/
projects/
nextjs-with-sdk/
django-clean/
rails-with-i18n/
astro-plain/package.json
{
"name": "@shipeasy/mcp",
"version": "1.0.0",
"description": "Model Context Protocol server for the Shipeasy platform (experimentation + i18n)",
"keywords": [
"mcp",
"model-context-protocol",
"shipeasy",
"feature-flags",
"experimentation",
"i18n",
"ai"
],
"type": "module",
"bin": { "shipeasy-mcp": "./bin/mcp.js" },
"files": ["bin/", "dist/", "src/prompts/*.md"],
"engines": { "node": ">=20" },
"scripts": {
"build": "tsup src/index.ts --format esm --dts --clean",
"type-check": "tsc --noEmit",
"test": "vitest",
"prepublishOnly": "pnpm build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"@shipeasy/sdk": "workspace:*",
"semver": "^7.6.0",
"zod": "^3.23.0",
"conf": "^13.0.0",
"@ast-grep/napi": "^0.22.0"
},
"devDependencies": {
"@types/node": "^20.19.0",
"@types/semver": "^7.5.0",
"tsup": "^8.3.0",
"typescript": "^5.7.0",
"vitest": "^2.0.0"
}
}Why @ast-grep/napi? A single, fast, multi-language AST engine used by every codemod (JS/TS/Vue/Svelte/Python/Ruby) instead of per-framework parsers. Keeps the install footprint small — one native dep, prebuilt binaries for common platforms.
Error handling
Every tool returns one of three shapes:
// Success
{ content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }
// Known validation / domain error (isError: true)
{ content: [{ type: "text", text: "Error: gate 'new_checkout' already exists" }], isError: true }
// Protocol error — JSON-RPC level
{ error: { code: -32602, message: "Invalid params: 'name' required" } }Rules:
- Never crash the stdio transport. Any unexpected exception is converted into an
isError: truetool result with a redacted message; a structurednotifications/messagelog is emitted aterrorlevel for the operator. - Never include the SDK key, CLI token, or Anthropic key in any response, error, or log line. The logger has a deny-list of env var names that it redacts before emitting.
- Never shell out with
exec+ string interpolation. All subprocess calls go throughutil/cli.tswhich usesexecFilewith an argument array and a 60 s default timeout. - Validate every path input via
util/safe-read.ts— path traversal attempts throw a specific error that's shown to the user, not silently followed.
Logging / progress
- Structured logs are sent via
notifications/message. The assistant honours the client-set log level (debug,info,warning,error) advertised at initialize time. - Long-running tools (
i18n_translate_draft,i18n_codemod_applyon large repos,i18n_push_keys) emitnotifications/progresswith{ progressToken, progress, total, message }so the assistant can render a spinner/bar. - Every tool call is logged with
{ tool, duration_ms, result_status, error_code }locally to~/.cache/shipeasy/mcp.log(rolling 10 MB, 3 files). No payloads. No secrets.
SDK / template compatibility
Each SDK language declares a compatible template range; the MCP server bundles the template authority and resolves at call time.
const COMPATIBLE_VERSIONS: Record<Lang, string> = {
typescript: ">=1.0.0 <3.0.0",
python: ">=1.0.0",
ruby: ">=1.0.0",
go: ">=1.0.0",
java: ">=1.0.0",
php: ">=1.0.0",
swift: ">=1.0.0",
kotlin: ">=1.0.0",
};detect_project sets template_warning when the installed SDK is outside the range. get_sdk_snippet first tries to import templates from the customer's installed shipeasy/templates/<lang>.js and falls back to this MCP server's bundled templates only if the SDK is not present (e.g. during fresh project setup).
Testing
- Unit tests — every tool handler in isolation with a mock
fetch+ mockedexecFile. Target ≥90% coverage onsrc/tools/**. - Fixture projects —
test/fixtures/projects/contains minimal Next.js / Django / Rails / Astro projects.detect_projectruns against each one and asserts the returned shape. - MCP Inspector smoke test — CI runs
npx @modelcontextprotocol/inspector npx -y ./dist/index.jsand exerciseslist_tools,list_prompts,list_resources, and one success/one error tool call per domain. - End-to-end — a Playwright suite under apps/ui/e2e/auth/mcp/ stands up a real worker + UI, spawns the MCP server as a child process, and verifies the full
create_experiment→get_sdk_snippet→experiment_statusloop writes to D1 and KV correctly. Required per CLAUDE.md. - Cross-SDK snippet verification — for each language × framework × type tuple, the generated snippet is compiled/parsed (TypeScript →
tsc --noEmit, Python →py_compile, etc.) in a tiny scratch project. Regressions in template strings fail CI instantly.
Versioning
Independent of the CLI and the SDK:
- Major — tool rename/removal, input-schema breaking change, prompt rename.
- Minor — new tool, new prompt, new resource template, additive optional input field.
- Patch — bug fix, template refresh, copy tweak.
initialize advertises serverInfo.version. The assistant may show a nudge when a newer version is available on npm. @shipeasy/cli re-exports @shipeasy/mcp at the matching major so shipeasy mcp start never runs a mismatched server.
Publish to the MCP Registry
This server is listed in the official MCP Registry as ai.shipeasy/mcp, which is what most MCP directories (Smithery, Glama, PulseMCP, mcp.so, the VS Code / Cursor galleries) ingest from. The registry hosts only metadata — the actual artifact is the @shipeasy/mcp npm package.
Single source of truth: package.json. Its mcpName is the registry server name and the npm name/version/description/repository feed the rest. server.json is generated — never hand-edit it:
pnpm --filter @shipeasy/mcp registry:gen # package.json → server.jsonThis is automated. .github/workflows/publish.yml publishes to npm on a GitHub Release, then a publish-registry job lists the new version on the MCP Registry (it waits for npm to expose the version, regenerates server.json, and authenticates via DNS using the MCP_PRIVATE_KEY repo secret — the Ed25519 key behind the shipeasy.ai TXT proof). GitHub-OIDC auth is not usable here: it only grants io.github.* namespaces, not our ai.shipeasy domain namespace. So the day-to-day flow is just: bump version, regen server.json, commit, tag a release — CI does the rest.
The manual flow below is the fallback (first-time setup, or republishing out of band).
The registry verifies ownership by checking that the published npm package's package.json carries mcpName: "ai.shipeasy/mcp". So the order matters — ship npm first, then the registry:
- Release the npm version the registry will verify (it must already include
mcpName). Bumpversion, tag a release onshipeasy-ai/mcp, let CI publish via OIDC. Manualnpm publishis forbidden (see## Non-negotiables). - Authenticate under the
ai.shipeasynamespace (DNS-based, one-time TXT record onshipeasy.ai):openssl genpkey -algorithm Ed25519 -out key.pem PUBLIC_KEY="$(openssl pkey -in key.pem -pubout -outform DER | tail -c 32 | base64)" # add TXT on shipeasy.ai: v=MCPv1; k=ed25519; p=$PUBLIC_KEY PRIVATE_KEY="$(openssl pkey -in key.pem -noout -text | grep -A3 'priv:' | tail -n +2 | tr -d ' :\n')" mcp-publisher login dns --domain shipeasy.ai --private-key "$PRIVATE_KEY" - Regenerate + publish:
pnpm --filter @shipeasy/mcp registry:gen cd packages/mcp && mcp-publisher publish
Install mcp-publisher with brew install mcp-publisher (or the release binary). Verify after: curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=ai.shipeasy/mcp".
Why one MCP server and not two
- One auth flow. The CLI token unlocks both
/api/admin/experiments/*and/api/admin/i18n/*. Two servers would each prompt for login. - One project detection.
detect_projectneeds to report both experimentation and i18n status; splitting doubles the filesystem walks for every conversation. - Cross-subsystem prompts.
translate_sitewants to read experiment config (does the site have anenvariant gated by anew_language_pickerflag?) — a single server canget_resourcefrom both domains without a cross-server handshake. - One AI install step. Customers add one entry to
.claude/settings.json, not two. - One shared bundle of templates. Language + framework templates are co-located; an
i18n_install_loadercall can consult the same detected stackexp_create_experimentjust used.
Non-negotiables
- Every new tool ships with: input schema in
tools/schema.ts, Zod validator at call-time, unit test, and an entry in this README's catalog table. - Every new prompt ships with: entry in
prompts/schema.ts+ a markdown file underprompts/*.md+ a one-line description forlist_prompts. - Every mutating tool (
create_*,update_*,delete_*,publish_*) requires an authenticated session and re-validates limits by letting theapps/uihandler callcheckLimit()— the MCP server never hand-rolls plan enforcement. - All subprocess invocations use
execFile+ argument arrays. - All filesystem reads go through
util/safe-read.ts. - Secrets (SDK keys, CLI token, Anthropic API key) never appear in tool output, prompt content, resource content, or log lines.
- Stdio transport stays healthy: unhandled rejections are captured in
index.tsand converted tonotifications/messageerrors — the process never exits on a per-request failure. - Template updates are co-versioned with the SDK (
shipeasy/templates/) and validated in CI withtsc --noEmit/py_compile/ equivalent.
