Sign In

@shipeasy/mcp

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@shipeasy/mcp

Feature flags, A/B experiments, kill switches, dynamic config & i18n — Shipeasy MCP server.

Source
npmnpm
Version
1.5.0
Version published
Weekly downloads
289
-7.37%
Maintainers
1
Weekly downloads
 
Created
Source

@shipeasy/mcp — unified Model Context Protocol server

Replaces packages/mcp-server/. One MCP server, one npm package, covers both subsystems:

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.json entries 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)

Step 2 — authenticate

Run this once per machine:

shipeasy-mcp install

What happens:

  • The CLI generates a PKCE verifier + challenge.
  • It calls POST {api_base}/auth/device/start to open a session; the worker returns a state.
  • 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/complete with project_id + PKCE verifier.
  • The CLI polls GET {api_base}/auth/device/poll?state=… (header X-Code-Verifier) every ~2 s until it receives { token, project_id }.
  • Token is written to ~/.config/shipeasy/config.json with chmod 600. The directory is created chmod 700.

Flags:

FlagEffect
--forceOverwrite an existing session instead of aborting.
--no-browserPrint 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 --version

Why 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/mcp

Protocol 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_check tool reads the file on every call — no cached state in the server process.
  • auth_login invoked over MCP always returns an actionable error asking the human to run shipeasy-mcp install in a terminal (stdio can't open a browser safely).
  • auth_logout removes 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":   "you@example.com",
    "created_at":   "2026-04-16T…Z"
  }

Tool-level auth policy:

CategoryRequires sessionNotes
detect_*NoPure filesystem inspection, no network.
auth_*— / triggers it
list_*, get_*YesRead-only GETs against apps/ui admin Route Handlers.
create_*, update_*, delete_*, publish_*YesMutations — the CLI enforces checkLimit server-side.
i18n_scan_code, i18n_codemod_*NoLocal-only AST tools.

Tool catalog

Tools are namespaced by subsystem: exp_* (experimentation), i18n_* (string manager), unprefixed (shared: auth, project detection, resource listing, SDK snippets).

Shared tools

detect_project

Inspects the working directory and returns the language, framework, package manager, installed shipeasy packages, and i18n presence signals.

Input (all optional):

{ "path": "string (defaults to cwd; sandboxed via realpath to refuse escapes)" }

Output:

{
  "language":       "typescript | javascript | python | ruby | go | java | php | swift | kotlin | unknown",
  "frameworks":     ["nextjs","react","tailwind", ...],
  "package_manager":"npm | pnpm | yarn | bun | pip | poetry | bundler | go | maven | gradle | composer | swiftpm",
  "entry_points":   ["src/app/layout.tsx", "src/main.tsx"],
  "test_files":     ["src/app/page.test.tsx"],

  "shipeasy": {
    "experimentation_sdk": { "installed": true, "version": "^1.3.0", "configured": true, "subentry": "shipeasy/react" },
    "i18n_sdk":            { "installed": true, "version": "^1.1.0", "configured": true, "profile": "en:prod" },
    "loader_script_tag":   { "present": true, "data_key": "sdk_client_…", "data_profile": "en:prod" },
    "env_keys_detected":   ["SHIPEASY_SERVER_KEY","NEXT_PUBLIC_SHIPEASY_CLIENT_KEY"],
    "template_warning":    "Installed SDK version 0.9.x is incompatible with MCP templates >=1.0.0."
  }
}

Security: realpath sandbox (see 10-mcp-server.md § detect_project). All reads go through safeRead() which refuses symlinks pointing outside the requested root.

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 success

auth_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.

ToolWhat it doesShells to
exp_create_gateCreate a feature gate with targeting rules and rollout percentageshipeasy gates create
exp_update_gateUpdate rules, rollout, killswitchshipeasy gates update
exp_delete_gateshipeasy gates delete
exp_create_configCreate a static config (sitevar)shipeasy configs create
exp_update_config_valueUpdate the live valueshipeasy configs set
exp_create_universeCreate a universe with holdout %shipeasy universes create
exp_create_experimentCreate experiment draft with groups, params, targeting gateshipeasy experiments create
exp_start_experimentTransition draft → runningshipeasy experiments start
exp_stop_experimentTransition running → stopped, promote winning groupshipeasy experiments stop
exp_add_metricAttach a metric as goal/guardrailshipeasy experiments metric add
exp_create_eventRegister an event schemashipeasy events create
exp_create_metricCreate a metric from an eventshipeasy metrics create
exp_experiment_statusCurrent results + ship/hold/wait verdictGET /api/admin/experiments/:name/results
exp_cleanup_winnerAST-drop losing branches after shippingLocal 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_*)

Absorbs the packages/mcp-server/src/tools/i18n/ sketch from string-manager-platform/plan.md.

ToolWhat it doesBacked by
i18n_scan_codeWalk the repo AST, return candidate JSX text / string literals / template strings that look translatableLocal (jscodeshift / ast-grep / Python ast)
i18n_discover_siteFetch a URL, parse <link rel="i18n-config"> + /.well-known/i18n.json, return profiles + glossaryfetch
i18n_list_profilesList profiles, chunks, coverageGET /api/admin/i18n/profiles
i18n_create_profileCreate a new locale profile, e.g. fr:prodshipeasy i18n profiles create
i18n_create_chunkCreate a chunk inside a profileshipeasy i18n chunks create
i18n_create_keyCreate/update a single label keyshipeasy i18n keys set
i18n_push_keysBulk upload a JSON file of keys to a chunkshipeasy i18n push
i18n_pull_keysDownload published strings to local diskshipeasy i18n pull
i18n_create_draftClone a source profile into a draft (optionally pre-translated)shipeasy i18n drafts create
i18n_translate_draftRun Anthropic translation on a draft (operator's API key, zero shipeasy-side AI)shipeasy i18n translate
i18n_update_draft_keyEdit a single key in a draftPATCH /api/admin/i18n/drafts/:id/keys/:key
i18n_publish_profilePublish a chunk or whole profile — rebuilds KV manifest, purges CDNshipeasy i18n publish
i18n_usage_summaryMonthly loader request count + per-chunk breakdownGET /api/admin/i18n/usage
i18n_codemod_previewAST-transform framework code (Next.js / React / Vue / Rails / Django) to wrap strings in ShipeasyString, return a diff without writingLocal codemods
i18n_codemod_applySame as preview, but writes the diff (requires confirm: true)Local codemods
i18n_validate_keysPre-commit check: every code-side key exists server-sideshipeasy i18n validate
i18n_install_loaderEmit the <script src="https://api.shipeasy.ai/sdk/i18n/loader.js" data-key=…> tag for the detected frameworkLocal

Representative input — i18n_translate_draft:

{
  "draft_id": "draft_…",
  "source_profile": "en:prod",
  "target_profile": "fr:prod",
  "glossary": [
    { "term": "ShipEasy", "policy": "keep" },
    { "term": "Patient", "policy": "translate_as", "fr": "Patient" }
  ],
  "anthropic_api_key_env": "ANTHROPIC_API_KEY",
  "max_parallel": 4
}

Calls Anthropic from the user's machine (never from shipeasy servers). Writes progress via MCP notifications/progress. Returns { draft_id, translated_key_count, failed_key_count, review_url }.

Representative input — i18n_codemod_preview:

{
  "framework": "nextjs",
  "files": ["src/app/dashboard/page.tsx"],
  "strategy": "jsx_text_literals",
  "key_prefix": "dashboard."
}

Output is a diff list; the AI shows it to the user and then calls i18n_codemod_apply with confirm: true only once the user approves.

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 namePurpose
setup_experimentationInstall the SDK, add env keys, wire a provider, verify with a sample gate
create_experimentPropose → create → inject code → start → monitor
analyze_experimentPull results, compute lift + significance, emit ship/hold/wait verdict
cleanup_winnerRemove losing branches + dead gate code after shipping
setup_i18nInstall SDK + loader script, create en:prod, run codemod, validate
translate_siteGiven a URL, discover, add target locale, translate, review, publish
i18n_healthReport missing keys, unused keys, drift between profiles
rotate_sdk_keysRevoke + re-issue client/server keys and update env vars

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 templateReturns
shipeasy://projectCached 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/currentPlan 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: true tool result with a redacted message; a structured notifications/message log is emitted at error level 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 through util/cli.ts which uses execFile with 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_apply on large repos, i18n_push_keys) emit notifications/progress with { 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 + mocked execFile. Target ≥90% coverage on src/tools/**.
  • Fixture projectstest/fixtures/projects/ contains minimal Next.js / Django / Rails / Astro projects. detect_project runs against each one and asserts the returned shape.
  • MCP Inspector smoke test — CI runs npx @modelcontextprotocol/inspector npx -y ./dist/index.js and exercises list_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_experimentget_sdk_snippetexperiment_status loop 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.json

This 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). Bump version, tag a release on shipeasy-ai/mcp, let CI publish via OIDC. Manual npm publish is forbidden (see ## Non-negotiables).
  • Authenticate under the ai.shipeasy namespace (DNS-based, one-time TXT record on shipeasy.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_project needs to report both experimentation and i18n status; splitting doubles the filesystem walks for every conversation.
  • Cross-subsystem prompts. translate_site wants to read experiment config (does the site have an en variant gated by a new_language_picker flag?) — a single server can get_resource from 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_loader call can consult the same detected stack exp_create_experiment just 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 under prompts/*.md + a one-line description for list_prompts.
  • Every mutating tool (create_*, update_*, delete_*, publish_*) requires an authenticated session and re-validates limits by letting the apps/ui handler call checkLimit() — 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.ts and converted to notifications/message errors — the process never exits on a per-request failure.
  • Template updates are co-versioned with the SDK (shipeasy/templates/) and validated in CI with tsc --noEmit / py_compile / equivalent.

Keywords

mcp

FAQs

Package last updated on 24 Jun 2026

Related posts