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
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@
"agents": "agents",
"rules": "rules",
"mcpServers": "mcp.json",
"hooks": "hooks/cursor.json"
"hooks": "hooks/hooks.json"
}
9 changes: 9 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
27 changes: 12 additions & 15 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
15 changes: 12 additions & 3 deletions docs/PLATFORMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion hooks/claude-code.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\""
}
]
}
Expand Down
1 change: 1 addition & 0 deletions hooks/cursor.json → hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"version": 1,
"hooks": {
"afterFileEdit": [
{
Expand Down
49 changes: 49 additions & 0 deletions patches/0003-scrub-internal-name-and-competitors.patch
Original file line number Diff line number Diff line change
@@ -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."

---
12 changes: 12 additions & 0 deletions patches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 40 additions & 1 deletion scripts/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
6 changes: 3 additions & 3 deletions skills/ship-it-nexlayer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand All @@ -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)
Expand Down Expand Up @@ -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 |
2 changes: 1 addition & 1 deletion skills/ship-it-nexlayer/references/ANTIPATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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."

---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."

---
Expand Down
Loading