diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 786f2a8..f11c97b 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -25,5 +25,5 @@ "agents": "agents", "rules": "rules", "mcpServers": "mcp.json", - "hooks": "hooks/cursor.json" + "hooks": "hooks/hooks.json" } diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..25e3d16 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "nexlayer-mcp": { + "type": "streamable-http", + "url": "https://mcp.nexlayer.ai/api/mcp" + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 68d1687..c77f96a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,23 +5,20 @@ First release. - Agent Plugins 1.0 conformant `plugin.json` — installs in Cursor, Codex, VS Code, and Copilot with no per-host packaging -- Nexlayer MCP server over streamable HTTP (59 tools) +- Nexlayer MCP server over streamable HTTP at `https://mcp.nexlayer.ai/api/mcp`, registered as `nexlayer-mcp` — the same name the public docs and the skill use. 55 tools reach a connected client; `scripts/mcp-tools.txt` lists 59 because four billing tools are registered server-side but runtime-gated - `ship-it-nexlayer` skill v3.0.0 and `debug-nexlayer` skill v1.1.0, copied verbatim from `Nexlayer/claudecode-mcp-go` (commit 0f5cc32) with their references, examples, and schema - Cursor manifest adding two commands, a `nexlayer-deploy` subagent, and a `nexlayer.yaml` rule -- Claude Code manifest and marketplace entry -- `scripts/sync-from-mcp.sh` — resync or drift-check skills and the tool list against the MCP repo -- `scripts/validate.py` — spec schemas, skill frontmatter, links, MCP tool names, manifest agreement - -## Unreleased - -- `references/MCP-SETUP.md` corrected via `patches/0001`: dashboard is `app.nexlayer.com` (there is no `app.nexlayer.io`), transport is `http` not the deprecated `sse`, Cursor config path is `~/.cursor/mcp.json`, and Claude Code leads with `npx @nexlayer/mcp-install` — all matching nexlayer.com/docs/mcp. Upstream: claudecode-mcp-go#45 -- MCP server registered as `nexlayer-mcp`, the same name the public docs and the skill use -- `patches/` layer added: `sync-from-mcp.sh` reapplies patches with `git apply` after every sync and exits 3 if one goes stale, so a local fix can neither rot nor be silently reverted +- Claude Code manifest and marketplace entry, with `category: deployment` matching the vocabulary in Anthropic's official catalog - Codex support: `.codex-plugin/plugin.json` with listing metadata and `.agents/plugins/marketplace.json`, so `codex plugin marketplace add Nexlayer/nexlayer-plugin` works -- `assets/logo.png` (512×512) for hosts whose listings want a raster logo -- `hooks/nexlayer-yaml-check.py`: advisory `nexlayer.yaml` check on file edit, covering the five hard constraints the server-side validator returns VALID for, plus the `version: 2.0` gate, `.pod` in browser-facing vars, loopback addresses, volume-size units, and the Postgres `PGDATA` trap. Wired for Cursor (`afterFileEdit`) and Claude Code (`PostToolUse`), which read the same filename with different schemas -- `com.github.copilot/agents/nexlayer-deploy.agent.md` so VS Code and Copilot get the subagent, generated by `scripts/gen-host-components.py` and drift-checked - `.devin-plugin/plugin.json` — Devin CLI shipped plugins; it also honors the root Agent Plugins manifest -- `.claude-plugin/marketplace.json` entries carry `category: deployment`, matching the vocabulary in Anthropic's official catalog +- `com.github.copilot/agents/nexlayer-deploy.agent.md` so VS Code and Copilot get the subagent, generated by `scripts/gen-host-components.py` and drift-checked +- `hooks/nexlayer-yaml-check.py`: advisory `nexlayer.yaml` check on file edit, covering the five hard constraints the server-side validator returns VALID for, plus the `version: 2.0` gate, `.pod` in browser-facing vars, loopback addresses, volume-size units, and the Postgres `PGDATA` trap. Wired for Cursor (`hooks/hooks.json`, `afterFileEdit`) and Claude Code (`hooks/claude-code.json`, `PostToolUse`) +- `.mcp.json` alongside `mcp.json` — Claude Code discovers MCP servers only from the dot-prefixed name; the manifest `mcpServers` key is ignored. Verified by real install +- `assets/logo.png` (512×512) for hosts whose listings want a raster logo +- `scripts/sync-from-mcp.sh` — resync or drift-check skills and the tool list against the MCP repo +- `scripts/validate.py` — spec schemas, skill frontmatter, links, MCP tool names, manifest agreement, hook scripts +- `patches/` layer: `sync-from-mcp.sh` reapplies patches with `git apply` after every sync and exits 3 if one goes stale, so a local fix can neither rot nor be silently reverted + - `0001` — `references/MCP-SETUP.md`: dashboard is `app.nexlayer.com` (there is no `app.nexlayer.io`), transport is `http` not the deprecated `sse`, Cursor config path is `~/.cursor/mcp.json`, and Claude Code leads with `npx @nexlayer/mcp-install` — all matching nexlayer.com/docs/mcp. Upstream: claudecode-mcp-go#45 + - `0002` — scrubbed a real-shaped account identifier from the registry example and removed a dead internal-tool link + - `0003` — scrubbed the remaining internal tool name from `SKILL.md` frontmatter and both antipattern references, and replaced named third-party platforms in `SKILL.md`'s decision tree and reference table with neutral wording - Pre-publication security review (`docs/SECURITY-REVIEW.md`): no credentials in the tree or in git history, registry rejects anonymous access, workflow token scoped read-only with actions pinned to SHAs, `SECURITY.md` added -- `patches/0002`: scrubbed a real-shaped account identifier from the registry example and removed a dead internal-tool link diff --git a/README.md b/README.md index 4cbde91..d82c57d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ One plugin, every agent. It follows the [Agent Plugins 1.0](https://agent-plugin | Component | What it does | |-----------|--------------| -| `nexlayer` MCP server | 59 tools: build and push images, validate and deploy YAML, read status/logs/events, exec into pods, edit files, query databases, restart and scale, manage domains, keys, and billing | +| `nexlayer` MCP server | 55 tools: build and push images, validate and deploy YAML, read status/logs/events, exec into pods, edit files, query databases, restart and scale, manage domains, keys, and billing | | `ship-it-nexlayer` skill | The deployment contract — decision tree, hard constraints, `.pod` vs `<% URL %>` rules, steps 0-10, plus 9 deep references, 18 worked YAML examples, and the machine-readable schema | | `debug-nexlayer` skill | Live-deployment debugging — proxy session rules, symptom decision tree, and the order to call the debug tools in | diff --git a/docs/PLATFORMS.md b/docs/PLATFORMS.md index dacbfce..18b1971 100644 --- a/docs/PLATFORMS.md +++ b/docs/PLATFORMS.md @@ -41,13 +41,22 @@ A `nexlayer-cursor` / `nexlayer-codex` / `nexlayer-claude-code` split would fork **Hooks.** `hooks/nexlayer-yaml-check.py` runs on every file edit and checks `nexlayer.yaml` for the things the server-side validator lets through — untagged image, empty `servicePorts`, no pod with `path`, invalid pod name, unknown fields — plus the `version: 2.0` gate, `.pod` in browser-facing vars, loopback addresses, volume-size units, and the Postgres `PGDATA` trap. It is advisory: findings go to stdout, exit code is always 0, and it stays silent on a clean file or an unrelated edit. -Cursor and Claude Code both read `hooks/hooks.json` by default but with **different schemas**, so each gets its own file (`hooks/cursor.json` with `afterFileEdit`, `hooks/claude-code.json` with `PostToolUse` and `${CLAUDE_PLUGIN_ROOT}`) and each manifest points at its own. Codex and Copilot support hooks too; their schemas are not documented well enough to write blind, so they are left off rather than guessed. +Cursor and Claude Code both read `hooks/hooks.json` by default but with **different schemas**, so Cursor keeps the default name (`hooks/hooks.json`, `"version": 1` plus `afterFileEdit`) and Claude Code gets an explicitly-named file (`hooks/claude-code.json` with `PostToolUse` and `${CLAUDE_PLUGIN_ROOT}`) that `.claude-plugin/plugin.json` points at. Marketplace scanners look for the default name, so a non-default filename means the hook is never detected. + +Two Claude Code loading rules were found by installing the plugin and reading `claude plugin details`, not from a spec — both fail silently and both pass every schema check: + +| Rule | Wrong form | Symptom | +|------|-----------|---------| +| MCP is discovered only from a dot-prefixed `.mcp.json` at the plugin root | `mcp.json` + `"mcpServers": "./mcp.json"`, or an inline object | `MCP servers (0)` — the whole point of the plugin, absent | +| A hook `command` must be one shell string | `["python3", "..."]` | `Hooks (0)` | + +So the repo ships **both** `mcp.json` (Agent Plugins 1.0 and Cursor) and an identical `.mcp.json` (Claude Code). `scripts/validate.py` fails if they diverge or if a hook command is a list. Codex and Copilot support hooks too; their schemas are not documented well enough to write blind, so they are left off rather than guessed. **Copilot.** VS Code reads portable `skills/` and `mcp.json` from the root manifest, but custom agents only from `com.github.copilot/agents/*.agent.md`. That file is generated from `agents/` by `scripts/gen-host-components.py`, and `validate.py` fails if it drifts. Copilot CLI's own plugin reference lists agents, skills, hooks, MCP, and LSP — no commands or rules — so nothing is mirrored for those. **Devin.** Devin CLI shipped plugins (closed beta). It reads `.devin-plugin/plugin.json`, falls back to `.claude-plugin/plugin.json` or the root `plugin.json`, and honors Agent Plugins 1.0 including `${PLUGIN_ROOT}`. It also reads `rules/` and `agents/`, so Devin gets more of this plugin than Codex does. -Devin has an MCP marketplace, not a plugin format — there is nothing to package for it, and the skills do not transfer. Same for any MCP-only client: they get the tools and none of the judgment. +A client with an MCP marketplace but no plugin format has nothing here to package: the skills do not transfer, so it gets the tools and none of the judgment. That is the whole Windsurf / Cline / Roo / Kilo row above. ## Naming @@ -134,5 +143,5 @@ Root `plugin.json` is enough to install: **Chat: Install Plugin From Source** wi ## Open items before submission - **Docs parity.** The install snippets in the shipped skill now match nexlayer.com/docs/mcp — see docs/VALIDATION.md §8 for what was reconciled and what is still inconsistent on the website side. -- **Public repo is public.** The skills carry migration guidance that names other hosting platforms (`references/MIGRATION.md`, and one line of `SKILL.md`), and a few references mention the underlying orchestration layer. Both are fine internally; decide whether they should ship on a public marketplace page before submitting. +- **Public repo is public.** Resolved for the listing surface: marketplace scanners inline `SKILL.md` verbatim into the listing body, so `patches/0003` neutralizes the named platforms there. `references/MIGRATION.md` keeps them deliberately — a migration guide has to name what you migrate from, and references are not inlined. Still open: a few references describe resource limits in the underlying orchestration layer's units, which is factually required by the field but reads as implementation detail. - `Nexlayer/nexlayer-claude-skills` holds an older copy of the deploy skill. Point it here and freeze it. diff --git a/hooks/claude-code.json b/hooks/claude-code.json index c5b9750..8630648 100644 --- a/hooks/claude-code.json +++ b/hooks/claude-code.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": ["python3", "${CLAUDE_PLUGIN_ROOT}/hooks/nexlayer-yaml-check.py"] + "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/nexlayer-yaml-check.py\"" } ] } diff --git a/hooks/cursor.json b/hooks/hooks.json similarity index 88% rename from hooks/cursor.json rename to hooks/hooks.json index e059078..bc77883 100644 --- a/hooks/cursor.json +++ b/hooks/hooks.json @@ -1,4 +1,5 @@ { + "version": 1, "hooks": { "afterFileEdit": [ { diff --git a/patches/0003-scrub-internal-name-and-competitors.patch b/patches/0003-scrub-internal-name-and-competitors.patch new file mode 100644 index 0000000..46ce288 --- /dev/null +++ b/patches/0003-scrub-internal-name-and-competitors.patch @@ -0,0 +1,49 @@ +--- a/skills/ship-it-nexlayer/SKILL.md ++++ b/skills/ship-it-nexlayer/SKILL.md +@@ -5,7 +5,7 @@ + metadata: + author: nexlayer + version: "3.0.0" +- validated: "MCP + Liz verified" ++ validated: "MCP verified" + allowed-tools: Bash(npx:* docker:* git:*) Read Write Edit + --- + +@@ -21,7 +21,7 @@ + │ └─► Has nexlayer.yaml? ──► YES ──► Validate & Deploy + │ └─► NO ──► Generate & Deploy + ├─► "Fix deployment" / "Not working" ──► Reference: TROUBLESHOOTING +- └─► "Migrate from Vercel/Railway/etc" ──► Reference: MIGRATION ++ └─► "Migrate from another platform" ──► Reference: MIGRATION + ``` + + ## Hard Constraints (NEVER violate) +@@ -180,5 +180,5 @@ + | `ANTIPATTERNS` | Common YAML mistakes and fixes | + | `ARCHITECTURE-ANTIPATTERNS` | Infrastructure design mistakes | + | `TROUBLESHOOTING` | Debugging failed deployments | +-| `MIGRATION` | Migrate from Vercel, Railway, Render, Fly.io, etc. | ++| `MIGRATION` | Migrate an app from another hosting platform | + | `MCP-SETUP` | IDE configuration for MCP | +--- a/skills/ship-it-nexlayer/references/ANTIPATTERNS.md ++++ b/skills/ship-it-nexlayer/references/ANTIPATTERNS.md +@@ -6,7 +6,7 @@ + + --- + +-**Key insight from Liz:** ++**Key principle:** + > ".pod DNS is used for internal communication between pods within your application, while `<% URL %>` is designed to dynamically generate URLs that are accessible from outside the pod network, such as through a browser. If you mistakenly use `.pod` DNS in a browser-facing variable like `REACT_APP_API_URL`, this will not work as expected." + + --- +--- a/skills/ship-it-nexlayer/references/ARCHITECTURE-ANTIPATTERNS.md ++++ b/skills/ship-it-nexlayer/references/ARCHITECTURE-ANTIPATTERNS.md +@@ -6,7 +6,7 @@ + + --- + +-**Key insight from Liz:** ++**Key principle:** + > "Terraform is designed for infrastructure provisioning, not application orchestration. Use `nexlayer.yaml` for deploying applications. Evaluate trade-offs between self-hosting and managed services like Supabase, Neon, or Pinecone. Managed services often provide better scalability, reliability, and ease of use." + + --- diff --git a/patches/README.md b/patches/README.md index 7971c4a..0d5dafd 100644 --- a/patches/README.md +++ b/patches/README.md @@ -37,6 +37,18 @@ Reason: pre-publication review of a repo that is going public. Both should also land upstream so the next sync does not need this patch. +### `0003-scrub-internal-name-and-competitors.patch` + +Targets: `SKILL.md`, `references/ANTIPATTERNS.md`, `references/ARCHITECTURE-ANTIPATTERNS.md` +Reason: `SKILL.md` is inlined verbatim into marketplace listings, so its text is public copy, not just agent instructions. + +| Change | Why | +|--------|-----| +| `validated: "MCP + Liz verified"` → `"MCP verified"`, and `**Key insight from Liz:**` → `**Key principle:**` in both `ANTIPATTERNS.md` and `ARCHITECTURE-ANTIPATTERNS.md` | `0002` removed the linked form of this attribution; these two unlinked instances survived. Naming an internal tool in public documentation. Matches the `debug-nexlayer` skill, which already says `"MCP verified"`. | +| Decision-tree branch and reference table: named third-party platforms → "another platform" / "another hosting platform" | Cursor and cursor.directory inline `SKILL.md` into the listing body, so these lines become public Nexlayer marketing copy. `references/MIGRATION.md` keeps the names — a migration guide has to name what you are migrating from, and it is not inlined. | + +Both should also land upstream so the next sync does not need this patch. + ## Adding a patch ```bash diff --git a/plugin.json b/plugin.json index db466bc..95ff870 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "nexlayer", "version": "1.0.0", - "description": "Deploy any containerized application to Nexlayer and get a live URL, from inside your agent. Bundles the Nexlayer MCP server (59 tools) with the official ship-it-nexlayer and debug-nexlayer skills.", + "description": "Deploy any containerized application to Nexlayer and get a live URL, from inside your agent. Bundles the Nexlayer MCP server (55 tools) with the official ship-it-nexlayer and debug-nexlayer skills.", "author": { "name": "Nexlayer", "email": "support@nexlayer.com", diff --git a/scripts/validate.py b/scripts/validate.py index 4557696..0e9b36d 100755 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -230,7 +230,7 @@ def check_hooks() -> None: Host hook schemas differ (Cursor's afterFileEdit vs Claude Code's PostToolUse), so each host gets its own file; what they must share is a working script. """ - for rel in ("hooks/cursor.json", "hooks/claude-code.json"): + for rel in ("hooks/hooks.json", "hooks/claude-code.json"): doc = load_json(ROOT / rel) if not doc: continue @@ -245,8 +245,47 @@ def check_hooks() -> None: fail(f"{rel}: hook script hooks/{name} is not executable") +# Verified by installing the plugin and running `claude plugin details`, not read +# off a spec: Claude Code discovers MCP only from a dot-prefixed `.mcp.json` at +# the plugin root. A `mcpServers` path string or inline object in the manifest is +# ignored, and the plugin loads with zero servers while every schema check passes. +# Agent Plugins 1.0 and Cursor want the undotted `mcp.json`, so both must exist +# and agree. +def check_mcp_discovery() -> None: + dotted, plain = ROOT / ".mcp.json", ROOT / "mcp.json" + if not dotted.is_file(): + fail(".mcp.json missing — Claude Code loads no MCP server without it") + return + if plain.read_text() != dotted.read_text(): + fail(".mcp.json and mcp.json disagree — copy mcp.json to .mcp.json") + + +# Same method, same surprise: Claude Code silently drops a hook whose `command` +# is a list. Every working plugin in the official marketplace uses one shell +# string. Cursor's file additionally requires a top-level `version`. +def check_hook_schemas() -> None: + doc = load_json(ROOT / "hooks/claude-code.json") or {} + for event, entries in doc.get("hooks", {}).items(): + for entry in entries: + for hook in entry.get("hooks", []): + if not isinstance(hook.get("command"), str): + fail( + f"hooks/claude-code.json: {event} command must be one " + f"shell string; a list is silently ignored" + ) + + cursor = load_json(ROOT / "hooks/hooks.json") or {} + if cursor.get("version") != 1: + fail('hooks/hooks.json: Cursor requires a top-level "version": 1') + stray = set(cursor.get("hooks", {})) & {"PostToolUse", "PreToolUse", "Stop"} + if stray: + fail(f"hooks/hooks.json: Claude Code event(s) {sorted(stray)} in Cursor's file") + + def main() -> int: check_schemas() + check_mcp_discovery() + check_hook_schemas() check_generated_mirrors() check_hooks() check_skills() diff --git a/skills/ship-it-nexlayer/SKILL.md b/skills/ship-it-nexlayer/SKILL.md index 376e71a..1c7fd8f 100644 --- a/skills/ship-it-nexlayer/SKILL.md +++ b/skills/ship-it-nexlayer/SKILL.md @@ -5,7 +5,7 @@ license: MIT metadata: author: nexlayer version: "3.0.0" - validated: "MCP + Liz verified" + validated: "MCP verified" allowed-tools: Bash(npx:* docker:* git:*) Read Write Edit --- @@ -21,7 +21,7 @@ USER REQUEST │ └─► Has nexlayer.yaml? ──► YES ──► Validate & Deploy │ └─► NO ──► Generate & Deploy ├─► "Fix deployment" / "Not working" ──► Reference: TROUBLESHOOTING - └─► "Migrate from Vercel/Railway/etc" ──► Reference: MIGRATION + └─► "Migrate from another platform" ──► Reference: MIGRATION ``` ## Hard Constraints (NEVER violate) @@ -180,5 +180,5 @@ Use `nexlayer_get_skill_content` with the `reference` parameter to access: | `ANTIPATTERNS` | Common YAML mistakes and fixes | | `ARCHITECTURE-ANTIPATTERNS` | Infrastructure design mistakes | | `TROUBLESHOOTING` | Debugging failed deployments | -| `MIGRATION` | Migrate from Vercel, Railway, Render, Fly.io, etc. | +| `MIGRATION` | Migrate an app from another hosting platform | | `MCP-SETUP` | IDE configuration for MCP | diff --git a/skills/ship-it-nexlayer/references/ANTIPATTERNS.md b/skills/ship-it-nexlayer/references/ANTIPATTERNS.md index e87b743..61b99c7 100644 --- a/skills/ship-it-nexlayer/references/ANTIPATTERNS.md +++ b/skills/ship-it-nexlayer/references/ANTIPATTERNS.md @@ -6,7 +6,7 @@ --- -**Key insight from Liz:** +**Key principle:** > ".pod DNS is used for internal communication between pods within your application, while `<% URL %>` is designed to dynamically generate URLs that are accessible from outside the pod network, such as through a browser. If you mistakenly use `.pod` DNS in a browser-facing variable like `REACT_APP_API_URL`, this will not work as expected." --- diff --git a/skills/ship-it-nexlayer/references/ARCHITECTURE-ANTIPATTERNS.md b/skills/ship-it-nexlayer/references/ARCHITECTURE-ANTIPATTERNS.md index be502fb..8c88f9e 100644 --- a/skills/ship-it-nexlayer/references/ARCHITECTURE-ANTIPATTERNS.md +++ b/skills/ship-it-nexlayer/references/ARCHITECTURE-ANTIPATTERNS.md @@ -6,7 +6,7 @@ --- -**Key insight from Liz:** +**Key principle:** > "Terraform is designed for infrastructure provisioning, not application orchestration. Use `nexlayer.yaml` for deploying applications. Evaluate trade-offs between self-hosting and managed services like Supabase, Neon, or Pinecone. Managed services often provide better scalability, reliability, and ease of use." ---