Skip to content

Commit 1ef9ed7

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/skills-update
# Conflicts: # CHANGELOG.md
2 parents 2d6e5e7 + 51af010 commit 1ef9ed7

9 files changed

Lines changed: 306 additions & 15 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
},
77
"metadata": {
88
"description": "Official Claude Code plugin marketplace from SE Ranking. Plugins built on top of the SE Ranking SEO Data API MCP server.",
9-
"version": "2.10.3"
9+
"version": "2.10.4"
1010
},
1111
"plugins": [
1212
{
1313
"name": "seo-skills",
1414
"source": "./",
1515
"description": "Claude SEO Skills for the SE Ranking MCP server. Keyword research, content briefing, page intelligence, technical and content audits, structured data, drift monitoring, SXO diagnostics, competitive analysis, paid search, AI-search optimization, sitemap analysis, subdomain mapping, content opportunity mining, strategic roadmap planning, local SEO (GBP / NAP / local pack), international SEO (hreflang validation), Google APIs (GSC, PSI, CrUX, Indexing, GA4, YouTube, NLP), and SE Ranking API integration recipes (Data API + Project API).",
16-
"version": "2.10.3",
16+
"version": "2.10.4",
1717
"author": {
1818
"name": "SE Ranking",
1919
"email": "api@seranking.com"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "seo-skills",
33
"description": "Claude SEO Skills for the SE Ranking MCP server. Turns raw API data into finished deliverables across keyword research, content briefing, page intelligence, technical and content audits, structured data, drift monitoring, SXO diagnostics, competitive analysis, paid search, AI-search optimization, sitemap analysis, subdomain mapping, content opportunity mining, strategic roadmap planning, Google APIs (GSC, PSI, CrUX, Indexing, GA4, YouTube, NLP), and SE Ranking API integration recipes (Data API + Project API).",
4-
"version": "2.10.3",
4+
"version": "2.10.4",
55
"author": {
66
"name": "SE Ranking",
77
"email": "api@seranking.com",

.cursor-plugin/plugin.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@
22
"name": "se-ranking",
33
"displayName": "SE Ranking",
44
"description": "Plugin that includes the SE Ranking MCP server and Skills for SEO and AI-search workflows.",
5-
"version": "2.10.3",
5+
"version": "2.10.4",
66
"author": {
77
"name": "SE Ranking",
8-
"email": "api@seranking.com",
9-
"url": "https://seranking.com"
8+
"email": "api@seranking.com"
109
},
1110
"license": "MIT",
12-
"category": "Productivity",
11+
"category": "developer-tools",
1312
"homepage": "https://seranking.com/api/integrations/mcp/",
1413
"repository": "https://github.com/seranking/seo-skills",
15-
"icon": "icon.png",
14+
"logo": "icon.png",
1615
"keywords": [
1716
"SEO",
1817
"GEO",
@@ -21,5 +20,6 @@
2120
"AI results tracking",
2221
"competitive research",
2322
"AI visibility"
24-
]
23+
],
24+
"skills": "./skills/"
2525
}

.github/workflows/release.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ jobs:
4646
meta=$(jq -r '.metadata.version' .claude-plugin/marketplace.json)
4747
entry=$(jq -r '.plugins[0].version' .claude-plugin/marketplace.json)
4848
pj=$(jq -r '.version' .claude-plugin/plugin.json)
49-
echo "tag=$ver metadata=$meta entry=$entry plugin.json=$pj"
50-
if [ "$ver" != "$meta" ] || [ "$ver" != "$entry" ] || [ "$ver" != "$pj" ]; then
51-
echo "::error::tag $tag does not match manifest versions (metadata=$meta entry=$entry plugin.json=$pj)"
49+
cursor=$(jq -r '.version' .cursor-plugin/plugin.json)
50+
echo "tag=$ver metadata=$meta entry=$entry plugin.json=$pj cursor=$cursor"
51+
if [ "$ver" != "$meta" ] || [ "$ver" != "$entry" ] || [ "$ver" != "$pj" ] || [ "$ver" != "$cursor" ]; then
52+
echo "::error::tag $tag does not match manifest versions (metadata=$meta entry=$entry plugin.json=$pj cursor=$cursor)"
5253
exit 1
5354
fi
5455

.github/workflows/validate.yml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ jobs:
5151
done < <(find . -path '*/.claude-plugin/plugin.json' -not -path '*/node_modules/*' | sort)
5252
exit "$status"
5353
54+
- name: Set up Python
55+
uses: actions/setup-python@v5
56+
with:
57+
python-version: '3.12'
58+
59+
- name: Validate Cursor plugin manifest (schema + assets)
60+
# The Claude CLI only validates Claude manifests; this guards the Cursor
61+
# manifest against schemas/cursor-plugin.schema.json and checks its assets.
62+
shell: bash
63+
run: |
64+
set -euo pipefail
65+
python -m pip install --quiet jsonschema
66+
python scripts/validate_cursor_manifest.py
67+
5468
- name: Check manifest versions agree
5569
shell: bash
5670
# Assumes the current single-plugin marketplace (plugins[0], plugin.json
@@ -60,8 +74,9 @@ jobs:
6074
meta=$(jq -r '.metadata.version' .claude-plugin/marketplace.json)
6175
entry=$(jq -r '.plugins[0].version' .claude-plugin/marketplace.json)
6276
pj=$(jq -r '.version' .claude-plugin/plugin.json)
63-
echo "metadata=$meta marketplace-entry=$entry plugin.json=$pj"
64-
if [ "$meta" != "$entry" ] || [ "$meta" != "$pj" ]; then
65-
echo "::error::version drift across manifests: metadata=$meta entry=$entry plugin.json=$pj"
77+
cursor=$(jq -r '.version' .cursor-plugin/plugin.json)
78+
echo "metadata=$meta marketplace-entry=$entry plugin.json=$pj cursor=$cursor"
79+
if [ "$meta" != "$entry" ] || [ "$meta" != "$pj" ] || [ "$meta" != "$cursor" ]; then
80+
echo "::error::version drift across manifests: metadata=$meta entry=$entry plugin.json=$pj cursor=$cursor"
6681
exit 1
6782
fi

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,23 @@ All notable changes to this project will be documented in this file. Format base
44

55
## [2.10.4] — 2026-06-17
66

7+
Adds Cursor marketplace support. The plugin can now be listed on cursor.com/marketplace alongside the existing Claude Code listing — it ships a Cursor manifest, declares the hosted SE Ranking MCP server for Cursor, carries a plugin logo, and is guarded in CI against the same class of schema and version errors as the Claude manifests.
8+
9+
### Added
10+
- **Cursor plugin packaging**`.cursor-plugin/plugin.json` (Cursor manifest) and `mcp.json` (declares the hosted SE Ranking MCP server at `https://api.seranking.com/mcp`, OAuth, auto-detected by Cursor). The repo is a single self-contained plugin, so `skills/`, `README.md`, `CHANGELOG.md`, and `LICENSE` are shared with the Claude listing.
11+
- **`icon.png`** — 512×512 plugin logo (SE Ranking brand mark), referenced by the Cursor manifest's `logo` field. Required for the marketplace listing.
12+
- **`schemas/cursor-plugin.schema.json`** + **`scripts/validate_cursor_manifest.py`** — a CI guard for the Cursor manifest. `claude plugin validate` only understands Claude manifests, so this validates `.cursor-plugin/plugin.json` against Cursor's published draft-07 schema (vendored, so there is no network dependency at validation time) and confirms the `logo`, `skills`, and `mcp.json` it relies on exist.
13+
714
### Fixed
15+
- **Cursor manifest failed Cursor's schema (`additionalProperties: false`).** Renamed `icon``logo` (the schema has no `icon` property) and removed `author.url` (the `author` object permits only `name` and `email`) — either would have rejected the plugin at submission.
816
- **All skills — removed the phantom `DATA_getCreditBalance` MCP tool.** Verified against the live SE Ranking MCP: the only account/system tool is `DATA_getSubscription` (returns `units_left` at 0 credits) — there is no `DATA_getCreditBalance`. Every reference across `skills/`, `README.md`, `CONTRIBUTING.md`, and the `seo-api` example now uses `DATA_getSubscription`. This supersedes the v2.10.1 entry below: the "not an alias / ~8.6M gap" finding came from the smoke-test calling a tool that does not exist in the MCP and is not reproducible.
917

18+
### Changed
19+
- **Cursor manifest**: `category` `Productivity``developer-tools` (the convention used across the official marketplace); added an explicit `skills: ./skills/` declaration.
20+
- **CI**: `validate.yml` runs the Cursor-manifest validator on every push and PR, and both `validate.yml` and `release.yml` now assert `.cursor-plugin/plugin.json`'s version agrees with the three Claude manifest versions.
21+
- **`README.md`**: added an "Install in Cursor" path under Install.
22+
- Version bumped to **2.10.4** across all four manifests (Claude `marketplace.json` ×2, Claude `plugin.json`, Cursor `plugin.json`).
23+
1024
## [2.10.3] — 2026-06-03
1125

1226
Patch release. Adds release automation and ships the first signed (Verified) release.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ curl -fsSL https://raw.githubusercontent.com/seranking/seo-skills/main/install.s
5454

5555
The manual install only wires your *environment* (extensions, Python deps, `~/.config/seo-skills/`). You still need Option 1 or 2 above so Claude can find the skill files.
5656

57+
### 4. Cursor
58+
59+
SE Ranking is also packaged as a [Cursor plugin](.cursor-plugin/plugin.json). Once it is listed on [cursor.com/marketplace](https://cursor.com/marketplace), install it from the marketplace, or add it by name in Cursor:
60+
61+
```
62+
/add-plugin se-ranking
63+
```
64+
65+
The hosted SE Ranking MCP server is declared in [`mcp.json`](mcp.json) and is auto-detected when the plugin is installed — open Cursor's MCP settings and sign in via OAuth (no API token to manage). The same skills ship to the Cursor agent.
66+
5767
### Connect SE Ranking (any install path)
5868

5969
The SE Ranking remote MCP auto-registers when you install the plugin — there's no separate `claude mcp add` step. On your first session, run `/mcp` and sign in via OAuth. No API token to manage. If you don't have an SE Ranking account yet, [sign up](https://seranking.com/api.html) — API access is required.

schemas/cursor-plugin.schema.json

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "https://cursor.com/schemas/cursor-plugin/plugin.json",
4+
"title": "Cursor Plugin Manifest",
5+
"description": "Schema for .cursor-plugin/plugin.json — defines a single Cursor plugin's metadata, components, and configuration.",
6+
"type": "object",
7+
"required": ["name"],
8+
"additionalProperties": false,
9+
"properties": {
10+
"name": {
11+
"type": "string",
12+
"minLength": 1,
13+
"pattern": "^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$",
14+
"description": "Unique plugin identifier in kebab-case (lowercase alphanumeric with hyphens and periods)."
15+
},
16+
"displayName": {
17+
"type": "string",
18+
"description": "Human-readable display name for the plugin."
19+
},
20+
"description": {
21+
"type": "string",
22+
"description": "Short description of what the plugin does."
23+
},
24+
"version": {
25+
"type": "string",
26+
"description": "Semantic version of the plugin (e.g. \"1.2.3\")."
27+
},
28+
"author": {
29+
"$ref": "#/$defs/author",
30+
"description": "The plugin author."
31+
},
32+
"publisher": {
33+
"type": "string",
34+
"minLength": 1,
35+
"description": "Publisher or organisation name."
36+
},
37+
"homepage": {
38+
"type": "string",
39+
"format": "uri",
40+
"description": "URL to the plugin's homepage."
41+
},
42+
"repository": {
43+
"type": "string",
44+
"format": "uri",
45+
"description": "URL to the plugin's source code repository."
46+
},
47+
"license": {
48+
"type": "string",
49+
"description": "SPDX license identifier (e.g. \"MIT\", \"Apache-2.0\")."
50+
},
51+
"logo": {
52+
"type": "string",
53+
"description": "Path to a logo image (relative to the plugin root) or an absolute URL."
54+
},
55+
"keywords": {
56+
"type": "array",
57+
"items": { "type": "string" },
58+
"description": "Keywords for discovery and search."
59+
},
60+
"category": {
61+
"type": "string",
62+
"description": "Plugin category for marketplace classification."
63+
},
64+
"tags": {
65+
"type": "array",
66+
"items": { "type": "string" },
67+
"description": "Tags for filtering and discovery."
68+
},
69+
"commands": {
70+
"$ref": "#/$defs/stringOrStringArray",
71+
"description": "Glob pattern(s) or path(s) to command files."
72+
},
73+
"agents": {
74+
"$ref": "#/$defs/stringOrStringArray",
75+
"description": "Glob pattern(s) or path(s) to agent definition files."
76+
},
77+
"skills": {
78+
"$ref": "#/$defs/stringOrStringArray",
79+
"description": "Glob pattern(s) or path(s) to skill files."
80+
},
81+
"rules": {
82+
"$ref": "#/$defs/stringOrStringArray",
83+
"description": "Glob pattern(s) or path(s) to rule files."
84+
},
85+
"hooks": {
86+
"oneOf": [
87+
{ "type": "string" },
88+
{ "type": "object" }
89+
],
90+
"description": "Path to a hooks configuration file, or an inline hooks object."
91+
},
92+
"mcpServers": {
93+
"$ref": "#/$defs/mcpServers",
94+
"description": "MCP server configuration — a path, an inline config object, or an array of either."
95+
}
96+
},
97+
"$defs": {
98+
"author": {
99+
"type": "object",
100+
"required": ["name"],
101+
"additionalProperties": false,
102+
"properties": {
103+
"name": {
104+
"type": "string",
105+
"minLength": 1,
106+
"description": "Author name."
107+
},
108+
"email": {
109+
"type": "string",
110+
"format": "email",
111+
"description": "Author email address."
112+
}
113+
}
114+
},
115+
"stringOrStringArray": {
116+
"oneOf": [
117+
{ "type": "string" },
118+
{
119+
"type": "array",
120+
"items": { "type": "string" }
121+
}
122+
]
123+
},
124+
"mcpServers": {
125+
"oneOf": [
126+
{ "type": "string" },
127+
{ "type": "object" },
128+
{
129+
"type": "array",
130+
"items": {
131+
"oneOf": [
132+
{ "type": "string" },
133+
{ "type": "object" }
134+
]
135+
}
136+
}
137+
]
138+
}
139+
}
140+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
"""Validate .cursor-plugin/plugin.json against the vendored Cursor plugin schema.
3+
4+
The Claude Code CLI (`claude plugin validate`) only understands Claude manifests,
5+
so the Cursor manifest needs its own guard. This validates it against
6+
schemas/cursor-plugin.schema.json (Cursor's published draft-07 schema, which is
7+
additionalProperties:false — unknown fields fail, not just warn) and confirms the
8+
assets it declares (logo, skills) and the auto-detected mcp.json actually exist.
9+
10+
Exits non-zero on any failure so CI goes red. Requires: jsonschema.
11+
"""
12+
from __future__ import annotations
13+
14+
import json
15+
import struct
16+
import sys
17+
from pathlib import Path
18+
19+
ROOT = Path(__file__).resolve().parent.parent
20+
SCHEMA = ROOT / "schemas" / "cursor-plugin.schema.json"
21+
MANIFEST = ROOT / ".cursor-plugin" / "plugin.json"
22+
23+
PNG_SIG = bytes.fromhex("89504e470d0a1a0a")
24+
25+
26+
def main() -> int:
27+
from jsonschema import Draft7Validator
28+
29+
problems = 0
30+
31+
def ok(msg: str) -> None:
32+
print(f" [PASS] {msg}")
33+
34+
def fail(msg: str) -> None:
35+
nonlocal problems
36+
problems += 1
37+
print(f" [FAIL] {msg}")
38+
39+
print("Cursor plugin manifest validation")
40+
41+
if not MANIFEST.is_file():
42+
print(f" [FAIL] {MANIFEST.relative_to(ROOT)} not found")
43+
return 1
44+
if not SCHEMA.is_file():
45+
print(f" [FAIL] {SCHEMA.relative_to(ROOT)} not found")
46+
return 1
47+
48+
manifest = json.loads(MANIFEST.read_text())
49+
schema = json.loads(SCHEMA.read_text())
50+
51+
# 1) Schema conformance (catches the icon/author.url class of errors).
52+
errs = sorted(Draft7Validator(schema).iter_errors(manifest), key=lambda e: list(e.path))
53+
if errs:
54+
fail(f"schema: {len(errs)} violation(s)")
55+
for e in errs:
56+
loc = "/".join(map(str, e.path)) or "(root)"
57+
print(f" - at {loc}: {e.message}")
58+
else:
59+
ok("schema: valid against schemas/cursor-plugin.schema.json")
60+
61+
# 2) Declared logo exists, is relative, and is a square PNG.
62+
logo = manifest.get("logo")
63+
if not logo:
64+
fail("logo: not declared")
65+
elif logo.startswith(("/", "http://", "https://")):
66+
ok(f"logo: external/absolute ({logo}) — file check skipped")
67+
else:
68+
p = ROOT / logo
69+
if not p.is_file():
70+
fail(f"logo: {logo} does not exist")
71+
else:
72+
b = p.read_bytes()
73+
if b[:8] != PNG_SIG:
74+
fail(f"logo: {logo} is not a valid PNG")
75+
else:
76+
w, h = struct.unpack(">II", b[16:24])
77+
if w == h:
78+
ok(f"logo: {logo} is a {w}x{h} square PNG")
79+
else:
80+
fail(f"logo: {logo} is {w}x{h}, not square")
81+
82+
# 3) Declared skills path resolves to a directory.
83+
skills = manifest.get("skills")
84+
if skills:
85+
sp = ROOT / str(skills).lstrip("./")
86+
if sp.is_dir():
87+
ok(f"skills: {skills} exists")
88+
else:
89+
fail(f"skills: {skills} is missing")
90+
91+
# 4) Auto-detected mcp.json at plugin root (optional, but if present must be sane).
92+
mcp = ROOT / "mcp.json"
93+
if mcp.is_file():
94+
try:
95+
servers = json.loads(mcp.read_text()).get("mcpServers", {})
96+
except json.JSONDecodeError as ex:
97+
fail(f"mcp.json: invalid JSON ({ex})")
98+
else:
99+
if isinstance(servers, dict) and servers:
100+
ok(f"mcp.json: present with {len(servers)} server(s)")
101+
else:
102+
fail("mcp.json: no mcpServers entries")
103+
else:
104+
ok("mcp.json: not present (no bundled MCP server)")
105+
106+
print("OK" if problems == 0 else f"{problems} problem(s) — see above")
107+
return 0 if problems == 0 else 1
108+
109+
110+
if __name__ == "__main__":
111+
sys.exit(main())

0 commit comments

Comments
 (0)