rayonne-mcp 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rayonne_mcp-0.2.0/.gitignore +5 -0
- rayonne_mcp-0.2.0/PKG-INFO +76 -0
- rayonne_mcp-0.2.0/README.md +60 -0
- rayonne_mcp-0.2.0/build/build_mcpb.sh +70 -0
- rayonne_mcp-0.2.0/build/build_plugin.sh +45 -0
- rayonne_mcp-0.2.0/build/build_wheel.sh +24 -0
- rayonne_mcp-0.2.0/build/publish_pypi.sh +47 -0
- rayonne_mcp-0.2.0/claude_code_plugin/.claude-plugin/plugin.json +34 -0
- rayonne_mcp-0.2.0/claude_code_plugin/.mcp.json +13 -0
- rayonne_mcp-0.2.0/claude_code_plugin/README.md +30 -0
- rayonne_mcp-0.2.0/claude_code_plugin/commands/rayonne-audit.md +19 -0
- rayonne_mcp-0.2.0/claude_code_plugin/commands/rayonne-status.md +13 -0
- rayonne_mcp-0.2.0/mcpb/README.md +17 -0
- rayonne_mcp-0.2.0/mcpb/icon.png +0 -0
- rayonne_mcp-0.2.0/mcpb/manifest.json +54 -0
- rayonne_mcp-0.2.0/mcpb/server/bootstrap.py +151 -0
- rayonne_mcp-0.2.0/pyproject.toml +36 -0
- rayonne_mcp-0.2.0/src/rayonne_mcp/__init__.py +3 -0
- rayonne_mcp-0.2.0/src/rayonne_mcp/client.py +122 -0
- rayonne_mcp-0.2.0/src/rayonne_mcp/server.py +183 -0
- rayonne_mcp-0.2.0/tests/test_client.py +106 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: rayonne-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Rayonne MCP server — pilote le workspace Rayonne (audit marketing, analyse produit, contenus, vidéos, métriques AARRR) depuis tout agent compatible MCP.
|
|
5
|
+
Author-email: Sponge Theory <contact@sponge-theory.ai>
|
|
6
|
+
License: SPONGE THEORY PROPRIETARY LICENSE
|
|
7
|
+
Keywords: claude,marketing,mcp,rayonne,sponge-theory
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: httpx<1,>=0.27
|
|
10
|
+
Requires-Dist: mcp[cli]<2,>=1.2.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
14
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Rayonne MCP
|
|
18
|
+
|
|
19
|
+
Serveur MCP (stdio) exposant le workspace Rayonne — contenus, soumissions,
|
|
20
|
+
métriques AARRR — à tout agent compatible MCP (Claude Code, Claude Desktop…),
|
|
21
|
+
via l'**API publique v1 en lecture** (`docs/API.md` du repo).
|
|
22
|
+
|
|
23
|
+
## Prérequis
|
|
24
|
+
|
|
25
|
+
Une clé API workspace (`rk_…`) : Rayonne → **Réglages → Clés API**
|
|
26
|
+
(plan **Business**, entitlement `api_access`).
|
|
27
|
+
|
|
28
|
+
## Claude Code
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
claude mcp add rayonne \
|
|
32
|
+
--env RAYONNE_API_KEY=rk_xxxxxxxx \
|
|
33
|
+
--env RAYONNE_API_URL=https://rayonne.ai \
|
|
34
|
+
-- uv run --project /chemin/vers/rayonne/mcp rayonne-mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Claude Desktop (`claude_desktop_config.json`)
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"rayonne": {
|
|
43
|
+
"command": "uv",
|
|
44
|
+
"args": ["run", "--project", "/chemin/vers/rayonne/mcp", "rayonne-mcp"],
|
|
45
|
+
"env": {
|
|
46
|
+
"RAYONNE_API_KEY": "rk_xxxxxxxx",
|
|
47
|
+
"RAYONNE_API_URL": "https://rayonne.ai"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Le serveur démarre **sans** clé (non fatal) : chaque outil renvoie alors un
|
|
55
|
+
message d'installation clair au lieu d'un crash.
|
|
56
|
+
|
|
57
|
+
## Outils
|
|
58
|
+
|
|
59
|
+
| Outil | Rend |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `rayonne_overview` | nom, URL produit, autopilote, compteurs contenus/soumissions par statut |
|
|
62
|
+
| `rayonne_list_content` | derniers contenus (+ `predicted_engagement` 0-100) |
|
|
63
|
+
| `rayonne_list_submissions` | statut de publication par plateforme (+ `external_url`) |
|
|
64
|
+
| `rayonne_metrics_aarrr` | snapshot AARRR (stages, geo, by_channel, timeseries, deltas) |
|
|
65
|
+
|
|
66
|
+
## Tests
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
cd mcp && uv run --extra dev pytest -q
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Publication marketplace
|
|
73
|
+
|
|
74
|
+
À faire via le flux commun Sponge Theory (même patron qu'`audigeo-mcp` :
|
|
75
|
+
stdio + PyPI, vendored dans `ezeeFlop/claude-plugins`) — voir l'agent
|
|
76
|
+
`mcp-plugin-publisher`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Rayonne MCP
|
|
2
|
+
|
|
3
|
+
Serveur MCP (stdio) exposant le workspace Rayonne — contenus, soumissions,
|
|
4
|
+
métriques AARRR — à tout agent compatible MCP (Claude Code, Claude Desktop…),
|
|
5
|
+
via l'**API publique v1 en lecture** (`docs/API.md` du repo).
|
|
6
|
+
|
|
7
|
+
## Prérequis
|
|
8
|
+
|
|
9
|
+
Une clé API workspace (`rk_…`) : Rayonne → **Réglages → Clés API**
|
|
10
|
+
(plan **Business**, entitlement `api_access`).
|
|
11
|
+
|
|
12
|
+
## Claude Code
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
claude mcp add rayonne \
|
|
16
|
+
--env RAYONNE_API_KEY=rk_xxxxxxxx \
|
|
17
|
+
--env RAYONNE_API_URL=https://rayonne.ai \
|
|
18
|
+
-- uv run --project /chemin/vers/rayonne/mcp rayonne-mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Claude Desktop (`claude_desktop_config.json`)
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"rayonne": {
|
|
27
|
+
"command": "uv",
|
|
28
|
+
"args": ["run", "--project", "/chemin/vers/rayonne/mcp", "rayonne-mcp"],
|
|
29
|
+
"env": {
|
|
30
|
+
"RAYONNE_API_KEY": "rk_xxxxxxxx",
|
|
31
|
+
"RAYONNE_API_URL": "https://rayonne.ai"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Le serveur démarre **sans** clé (non fatal) : chaque outil renvoie alors un
|
|
39
|
+
message d'installation clair au lieu d'un crash.
|
|
40
|
+
|
|
41
|
+
## Outils
|
|
42
|
+
|
|
43
|
+
| Outil | Rend |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `rayonne_overview` | nom, URL produit, autopilote, compteurs contenus/soumissions par statut |
|
|
46
|
+
| `rayonne_list_content` | derniers contenus (+ `predicted_engagement` 0-100) |
|
|
47
|
+
| `rayonne_list_submissions` | statut de publication par plateforme (+ `external_url`) |
|
|
48
|
+
| `rayonne_metrics_aarrr` | snapshot AARRR (stages, geo, by_channel, timeseries, deltas) |
|
|
49
|
+
|
|
50
|
+
## Tests
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
cd mcp && uv run --extra dev pytest -q
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Publication marketplace
|
|
57
|
+
|
|
58
|
+
À faire via le flux commun Sponge Theory (même patron qu'`audigeo-mcp` :
|
|
59
|
+
stdio + PyPI, vendored dans `ezeeFlop/claude-plugins`) — voir l'agent
|
|
60
|
+
`mcp-plugin-publisher`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the Claude Desktop MCPB bundle from mcp/mcpb/.
|
|
3
|
+
# Output: mcp/dist/rayonne-<VERSION>.mcpb (zip file with renamed extension)
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./build_mcpb.sh
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
MCP_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
cd "$MCP_DIR"
|
|
11
|
+
|
|
12
|
+
VERSION=$(grep -E "^version" pyproject.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
|
|
13
|
+
if [ -z "$VERSION" ]; then
|
|
14
|
+
echo "ERROR: cannot parse version from pyproject.toml" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
mkdir -p dist
|
|
19
|
+
OUT="${MCP_DIR}/dist/rayonne-${VERSION}.mcpb"
|
|
20
|
+
|
|
21
|
+
if [ ! -d "mcpb" ]; then
|
|
22
|
+
echo "ERROR: mcpb/ template missing" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
STAGE=$(mktemp -d)
|
|
27
|
+
trap 'rm -rf "$STAGE"' EXIT
|
|
28
|
+
|
|
29
|
+
cp -R mcpb/* "$STAGE/"
|
|
30
|
+
|
|
31
|
+
# Embed the server source so the bundle is self-contained
|
|
32
|
+
mkdir -p "$STAGE/server"
|
|
33
|
+
cp -R src "$STAGE/server/src"
|
|
34
|
+
cp pyproject.toml "$STAGE/server/pyproject.toml"
|
|
35
|
+
cp README.md "$STAGE/server/README.md"
|
|
36
|
+
|
|
37
|
+
# Bundle the wheel so bootstrap.py can provision dependencies on the user's
|
|
38
|
+
# machine at first run (pip resolves the correct per-platform binaries —
|
|
39
|
+
# vendoring compiled deps like pydantic-core into one cross-platform bundle is
|
|
40
|
+
# not possible). Build the wheel first if it isn't in dist/ yet.
|
|
41
|
+
mkdir -p "$STAGE/server/wheels"
|
|
42
|
+
WHEEL=$(ls -t dist/rayonne_mcp-*.whl 2>/dev/null | head -1 || true)
|
|
43
|
+
if [ -z "${WHEEL:-}" ]; then
|
|
44
|
+
echo "No wheel in dist/ — building it first…"
|
|
45
|
+
"$(dirname "$0")/build_wheel.sh" >/dev/null
|
|
46
|
+
WHEEL=$(ls -t dist/rayonne_mcp-*.whl 2>/dev/null | head -1 || true)
|
|
47
|
+
fi
|
|
48
|
+
if [ -z "${WHEEL:-}" ]; then
|
|
49
|
+
echo "ERROR: could not build/find the rayonne_mcp wheel" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
cp "$WHEEL" "$STAGE/server/wheels/"
|
|
53
|
+
echo "Bundled wheel: $(basename "$WHEEL")"
|
|
54
|
+
|
|
55
|
+
# Inject version into manifest.json
|
|
56
|
+
MANIFEST="$STAGE/manifest.json"
|
|
57
|
+
if [ -f "$MANIFEST" ]; then
|
|
58
|
+
/usr/bin/sed -i.bak -E "s/\"version\": *\"[^\"]+\"/\"version\": \"${VERSION}\"/" "$MANIFEST"
|
|
59
|
+
rm -f "${MANIFEST}.bak"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Strip macOS metadata
|
|
63
|
+
find "$STAGE" -name '.DS_Store' -delete 2>/dev/null || true
|
|
64
|
+
find "$STAGE" -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true
|
|
65
|
+
|
|
66
|
+
# .mcpb is just a zip
|
|
67
|
+
rm -f "$OUT"
|
|
68
|
+
(cd "$STAGE" && zip -qr "$OUT" .)
|
|
69
|
+
|
|
70
|
+
echo "Built: $OUT ($(du -h "$OUT" | awk '{print $1}'))"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the Claude Code plugin tar.gz from mcp/claude_code_plugin/.
|
|
3
|
+
# Output: mcp/dist/rayonne-claude-code-<VERSION>.tar.gz
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./build_plugin.sh
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
MCP_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
cd "$MCP_DIR"
|
|
11
|
+
|
|
12
|
+
# The plugin is versioned independently of the PyPI server package (the plugin
|
|
13
|
+
# just references `uvx rayonne-mcp`). Read the version from plugin.json.
|
|
14
|
+
VERSION=$(grep -E '"version"' claude_code_plugin/.claude-plugin/plugin.json | head -1 | sed -E 's/.*"version": *"([^"]+)".*/\1/')
|
|
15
|
+
if [ -z "$VERSION" ]; then
|
|
16
|
+
echo "ERROR: cannot parse version from plugin.json" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
mkdir -p dist
|
|
21
|
+
OUT="dist/rayonne-claude-code-${VERSION}.tar.gz"
|
|
22
|
+
|
|
23
|
+
if [ ! -d "claude_code_plugin" ]; then
|
|
24
|
+
echo "ERROR: claude_code_plugin/ template missing" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Stage to a temp dir so the tarball has a clean top-level directory
|
|
29
|
+
STAGE=$(mktemp -d)
|
|
30
|
+
trap 'rm -rf "$STAGE"' EXIT
|
|
31
|
+
cp -R claude_code_plugin "${STAGE}/rayonne"
|
|
32
|
+
|
|
33
|
+
# No server source is bundled: the plugin's .mcp.json launches the MCP server
|
|
34
|
+
# via `uvx rayonne-mcp`, which fetches the published package from PyPI and
|
|
35
|
+
# caches an isolated environment on first run. This keeps the plugin tiny and
|
|
36
|
+
# lets it be distributed from a PUBLIC marketplace without shipping the
|
|
37
|
+
# proprietary server source. (The self-contained path lives in the .mcpb
|
|
38
|
+
# bundle for Claude Desktop, built by build_mcpb.sh.)
|
|
39
|
+
|
|
40
|
+
# Strip macOS metadata and caches
|
|
41
|
+
find "$STAGE" -name '.DS_Store' -delete 2>/dev/null || true
|
|
42
|
+
find "$STAGE" -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true
|
|
43
|
+
|
|
44
|
+
tar -czf "$OUT" -C "$STAGE" rayonne
|
|
45
|
+
echo "Built: $OUT ($(du -h "$OUT" | awk '{print $1}'))"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the rayonne-mcp Python wheel.
|
|
3
|
+
# Output: mcp/dist/rayonne_mcp-<VERSION>-py3-none-any.whl + .tar.gz
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./build_wheel.sh
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
MCP_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
cd "$MCP_DIR"
|
|
11
|
+
|
|
12
|
+
# Don't rm dist/ — other build scripts have already populated it
|
|
13
|
+
rm -f dist/rayonne_mcp-*.whl dist/rayonne_mcp-*.tar.gz 2>/dev/null || true
|
|
14
|
+
|
|
15
|
+
# Prefer uv (already used in the project), fall back to python -m build
|
|
16
|
+
if command -v uv >/dev/null 2>&1; then
|
|
17
|
+
uv build --wheel --sdist --out-dir dist 2>&1 | tail -5
|
|
18
|
+
else
|
|
19
|
+
"${PYTHON:-python}" -m build --wheel --sdist --outdir dist . 2>&1 | tail -5
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
echo ""
|
|
23
|
+
echo "Built artifacts:"
|
|
24
|
+
ls -lh dist/rayonne_mcp-*.whl dist/rayonne_mcp-*.tar.gz 2>/dev/null
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Publish the rayonne-mcp package to PyPI.
|
|
3
|
+
#
|
|
4
|
+
# Builds a fresh wheel + sdist for the current version and uploads them.
|
|
5
|
+
# Already-published versions are skipped (idempotent) via --check-url, so a
|
|
6
|
+
# re-run after a failed upload is safe.
|
|
7
|
+
#
|
|
8
|
+
# Token resolution order (first hit wins):
|
|
9
|
+
# 1. $UV_PUBLISH_TOKEN (CI / one-off overrides)
|
|
10
|
+
# 2. ~/.pypirc [pypi] password (the global PyPI token, chmod 600)
|
|
11
|
+
#
|
|
12
|
+
# Usage: ./build/publish_pypi.sh
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
MCP_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
16
|
+
cd "$MCP_DIR"
|
|
17
|
+
|
|
18
|
+
VERSION=$(grep -E "^version" pyproject.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
|
|
19
|
+
echo "Publishing rayonne-mcp ${VERSION} to PyPI…"
|
|
20
|
+
|
|
21
|
+
# --- Resolve token ---------------------------------------------------------
|
|
22
|
+
TOKEN="${UV_PUBLISH_TOKEN:-}"
|
|
23
|
+
if [ -z "$TOKEN" ] && [ -f "${HOME}/.pypirc" ]; then
|
|
24
|
+
TOKEN="$(awk '/^\[pypi\]/{f=1} f&&/password/{sub(/^[^=]*=[[:space:]]*/,"");print;exit}' "${HOME}/.pypirc")"
|
|
25
|
+
fi
|
|
26
|
+
if [ -z "$TOKEN" ]; then
|
|
27
|
+
echo "ERROR: no PyPI token found. Set UV_PUBLISH_TOKEN, or create ${TOKEN_FILE}." >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# --- Build fresh artifacts -------------------------------------------------
|
|
32
|
+
"$(dirname "$0")/build_wheel.sh" >/dev/null
|
|
33
|
+
WHEEL=$(ls -t dist/rayonne_mcp-"${VERSION}"-*.whl 2>/dev/null | head -1)
|
|
34
|
+
SDIST=$(ls -t dist/rayonne_mcp-"${VERSION}".tar.gz 2>/dev/null | head -1)
|
|
35
|
+
if [ -z "${WHEEL:-}" ] || [ -z "${SDIST:-}" ]; then
|
|
36
|
+
echo "ERROR: could not find built artifacts for ${VERSION} in dist/." >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# --- Upload (idempotent: skips versions already on the index) --------------
|
|
41
|
+
uv publish \
|
|
42
|
+
--token "$TOKEN" \
|
|
43
|
+
--check-url https://pypi.org/simple/rayonne-mcp/ \
|
|
44
|
+
"$WHEEL" "$SDIST"
|
|
45
|
+
|
|
46
|
+
echo ""
|
|
47
|
+
echo "Done. Verify: https://pypi.org/project/rayonne-mcp/${VERSION}/"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rayonne",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Rayonne — l'autopilote marketing des SaaS : audit marketing de ta page produit, analyse, contenus, vidéos, métriques AARRR, depuis Claude Code.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Sponge Theory",
|
|
7
|
+
"email": "contact@sponge-theory.ai",
|
|
8
|
+
"url": "https://rayonne.ai"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://rayonne.ai",
|
|
11
|
+
"license": "Sponge Theory Proprietary",
|
|
12
|
+
"keywords": ["marketing", "saas", "rayonne", "audit", "sponge-theory"],
|
|
13
|
+
"userConfig": {
|
|
14
|
+
"api_key": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"title": "Clé API Rayonne",
|
|
17
|
+
"description": "Générée dans Rayonne → Réglages → Clés API (format rk_…, plan Business requis).",
|
|
18
|
+
"sensitive": true,
|
|
19
|
+
"required": true
|
|
20
|
+
},
|
|
21
|
+
"api_url": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"title": "URL de l'API",
|
|
24
|
+
"description": "Laisser par défaut sauf instance auto-hébergée.",
|
|
25
|
+
"default": "https://rayonne.ai/api/public/v1"
|
|
26
|
+
},
|
|
27
|
+
"read_only": {
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"title": "Mode lecture seule",
|
|
30
|
+
"description": "Désactive les outils qui déclenchent un travail (analyse, audit, génération vidéo — la vidéo est facturée). À activer pour explorer sans rien consommer.",
|
|
31
|
+
"default": false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"rayonne": {
|
|
4
|
+
"command": "uvx",
|
|
5
|
+
"args": ["rayonne-mcp@0.2.0"],
|
|
6
|
+
"env": {
|
|
7
|
+
"RAYONNE_API_KEY": "${user_config.api_key}",
|
|
8
|
+
"RAYONNE_API_URL": "${user_config.api_url}",
|
|
9
|
+
"RAYONNE_READ_ONLY": "${user_config.read_only}"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Rayonne — plugin Claude Code
|
|
2
|
+
|
|
3
|
+
Pilote ton workspace [Rayonne](https://rayonne.ai) depuis Claude Code :
|
|
4
|
+
audit marketing de ta page produit, analyse produit, génération de contenus
|
|
5
|
+
et de vidéos, métriques AARRR.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/plugin marketplace add ezeeFlop/claude-plugins
|
|
11
|
+
/plugin install rayonne
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Claude Code demande alors ta **clé API** (Rayonne → Réglages → Clés API,
|
|
15
|
+
format `rk_…`, plan Business).
|
|
16
|
+
|
|
17
|
+
## Commandes
|
|
18
|
+
|
|
19
|
+
- `/rayonne-audit` — audite le message de ta page produit et dit ce qui
|
|
20
|
+
plafonne tout ce que Rayonne génère pour toi.
|
|
21
|
+
- `/rayonne-status` — état du workspace en quelques lignes.
|
|
22
|
+
|
|
23
|
+
## Outils
|
|
24
|
+
|
|
25
|
+
Lecture : `rayonne_overview`, `rayonne_list_content`,
|
|
26
|
+
`rayonne_list_submissions`, `rayonne_metrics_aarrr`, `rayonne_list_audits`.
|
|
27
|
+
|
|
28
|
+
Écriture : `rayonne_analyze_product`, `rayonne_marketing_audit`,
|
|
29
|
+
`rayonne_create_video`. Chacun consomme du quota ; la génération vidéo est
|
|
30
|
+
facturée. Coche **Mode lecture seule** à l'installation pour les désactiver.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Audite le message de ta page produit et dit ce qui plafonne tes contenus.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Lance un audit marketing de la page produit du workspace Rayonne, puis
|
|
6
|
+
présente le résultat.
|
|
7
|
+
|
|
8
|
+
1. Appelle `rayonne_marketing_audit` (laisse `refresh` à true : il faut noter
|
|
9
|
+
la page **telle qu'elle est**, pas la dernière analyse).
|
|
10
|
+
2. L'audit tourne en fond. Attends une minute environ, puis relis-le avec
|
|
11
|
+
`rayonne_list_audits`, en reprenant le plus récent.
|
|
12
|
+
3. Présente dans cet ordre, sans jargon :
|
|
13
|
+
- le **verdict** en premier, tel quel — c'est la phrase qui doit suffire ;
|
|
14
|
+
- la note globale et les axes, en signalant celui qui est le plus bas ;
|
|
15
|
+
- les recommandations, avec leurs phrases de remplacement prêtes à coller.
|
|
16
|
+
4. Si un audit précédent existe, compare les notes et dis franchement si la
|
|
17
|
+
dernière modification de la page a payé ou non.
|
|
18
|
+
|
|
19
|
+
Ne reformule jamais le verdict et ne traduis pas les citations de la page.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: État du workspace Rayonne — contenus, soumissions, métriques.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Donne un état des lieux court du workspace Rayonne.
|
|
6
|
+
|
|
7
|
+
1. `rayonne_overview` pour le workspace et ses compteurs.
|
|
8
|
+
2. `rayonne_list_content` (10 derniers) et `rayonne_list_submissions`.
|
|
9
|
+
3. `rayonne_metrics_aarrr` pour les chiffres d'acquisition.
|
|
10
|
+
|
|
11
|
+
Résume en quelques lignes : ce qui a été généré, ce qui est publié, ce qui
|
|
12
|
+
attend une revue, et le chiffre AARRR qui bouge le plus. Signale tout
|
|
13
|
+
contenu bloqué en revue depuis plus d'une semaine.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Rayonne — bundle Claude Desktop
|
|
2
|
+
|
|
3
|
+
Installe ce fichier `.mcpb` dans Claude Desktop (Réglages → Extensions →
|
|
4
|
+
installer depuis un fichier), puis colle ta **clé API Rayonne**
|
|
5
|
+
(Réglages → Clés API dans Rayonne, format `rk_…`, plan Business).
|
|
6
|
+
|
|
7
|
+
Aucun prérequis autre qu'un Python 3.10+ : le bundle provisionne son propre
|
|
8
|
+
environnement au premier lancement.
|
|
9
|
+
|
|
10
|
+
## À quoi ça sert
|
|
11
|
+
|
|
12
|
+
Demande par exemple : « audite le message de ma page produit ». Rayonne note
|
|
13
|
+
sept axes et te dit lequel plafonne tout le reste — avec les phrases de
|
|
14
|
+
remplacement à coller.
|
|
15
|
+
|
|
16
|
+
Coche **Mode lecture seule** si tu veux explorer sans rien déclencher : la
|
|
17
|
+
génération vidéo consomme du quota et est facturée.
|
|
Binary file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": "0.2",
|
|
3
|
+
"name": "rayonne",
|
|
4
|
+
"display_name": "Rayonne",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"description": "Audit marketing de ta page produit, analyse, contenus, vidéos et métriques AARRR — directement dans Claude Desktop.",
|
|
7
|
+
"long_description": "Rayonne est l'autopilote marketing des SaaS. Ce bundle expose ton workspace à Claude Desktop : l'AUDIT MARKETING note le message de ta page produit sur sept axes (hiérarchie du message, clarté, audience, différenciation, douleur, preuve, offre) et dit lequel plafonne tout ce que la plateforme génère pour toi, avec des phrases de remplacement prêtes à coller. S'y ajoutent l'analyse produit, la génération de vidéos, l'état des contenus et des soumissions, et le tableau AARRR. Plan Business requis (entitlement accès API).",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Sponge Theory",
|
|
10
|
+
"email": "contact@sponge-theory.ai",
|
|
11
|
+
"url": "https://rayonne.ai"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://rayonne.ai",
|
|
14
|
+
"documentation": "https://rayonne.ai/docs/mcp",
|
|
15
|
+
"support": "https://rayonne.ai",
|
|
16
|
+
"server": {
|
|
17
|
+
"type": "python",
|
|
18
|
+
"entry_point": "server/bootstrap.py",
|
|
19
|
+
"mcp_config": {
|
|
20
|
+
"command": "python",
|
|
21
|
+
"args": ["${__dirname}/server/bootstrap.py"],
|
|
22
|
+
"cwd": "${__dirname}/server",
|
|
23
|
+
"env": {
|
|
24
|
+
"PYTHONPATH": "${__dirname}/server/src",
|
|
25
|
+
"RAYONNE_API_KEY": "${user_config.api_key}",
|
|
26
|
+
"RAYONNE_API_URL": "${user_config.api_url}",
|
|
27
|
+
"RAYONNE_READ_ONLY": "${user_config.read_only}"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"user_config": {
|
|
32
|
+
"api_key": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"title": "Clé API Rayonne",
|
|
35
|
+
"description": "Générée dans Rayonne → Réglages → Clés API (format rk_…, plan Business requis).",
|
|
36
|
+
"sensitive": true,
|
|
37
|
+
"required": true
|
|
38
|
+
},
|
|
39
|
+
"api_url": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"title": "URL de l'API",
|
|
42
|
+
"description": "Laisser par défaut sauf instance auto-hébergée.",
|
|
43
|
+
"default": "https://rayonne.ai/api/public/v1",
|
|
44
|
+
"required": false
|
|
45
|
+
},
|
|
46
|
+
"read_only": {
|
|
47
|
+
"type": "boolean",
|
|
48
|
+
"title": "Mode lecture seule",
|
|
49
|
+
"description": "Désactive les outils qui déclenchent un travail (analyse, audit, génération vidéo — la vidéo est facturée). Recommandé pour explorer sans rien consommer.",
|
|
50
|
+
"default": false,
|
|
51
|
+
"required": false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bootstrap launcher for the Rayonne Claude Desktop (.mcpb) bundle.
|
|
3
|
+
|
|
4
|
+
Claude Desktop starts this file with a system Python (manifest
|
|
5
|
+
``server.type = "python"``). A single ``.mcpb`` must run on macOS, Windows and
|
|
6
|
+
Linux, but the MCP SDK depends on compiled wheels (notably ``pydantic-core``)
|
|
7
|
+
whose binaries differ per OS/architecture — they cannot be vendored portably
|
|
8
|
+
into one bundle. So rather than ship binaries, we provision them on the user's
|
|
9
|
+
own machine on first launch:
|
|
10
|
+
|
|
11
|
+
1. Fast path — if ``rayonne_mcp`` and its dependencies already import in the
|
|
12
|
+
current interpreter (developer machines, or an already-provisioned
|
|
13
|
+
Python), run the server in-process.
|
|
14
|
+
2. Otherwise, create a private per-user virtual environment and ``pip
|
|
15
|
+
install`` the wheel bundled next to this script under ``wheels/`` (pip
|
|
16
|
+
resolves the correct per-platform binaries), then exec the installed
|
|
17
|
+
``rayonne-mcp`` console script. The venv is keyed by the wheel filename,
|
|
18
|
+
so a bundle upgrade provisions a fresh environment automatically.
|
|
19
|
+
|
|
20
|
+
This makes a single cross-platform bundle work with no prerequisite beyond a
|
|
21
|
+
Python 3.10+ interpreter — exactly what the bundle README promises. All
|
|
22
|
+
diagnostics go to stderr; stdout is reserved for the MCP stdio protocol.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
HERE = Path(__file__).resolve().parent # .../server
|
|
31
|
+
SRC = HERE / "src"
|
|
32
|
+
WHEELS = HERE / "wheels"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _log(msg: str) -> None:
|
|
36
|
+
print(f"[rayonne-mcp bootstrap] {msg}", file=sys.stderr, flush=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run_in_process() -> bool:
|
|
40
|
+
"""Run the server with the current interpreter if deps are importable.
|
|
41
|
+
|
|
42
|
+
Returns False (without side effects beyond a sys.path insert) when the
|
|
43
|
+
dependencies are not present, so the caller can fall back to provisioning.
|
|
44
|
+
"""
|
|
45
|
+
if SRC.is_dir():
|
|
46
|
+
sys.path.insert(0, str(SRC))
|
|
47
|
+
try:
|
|
48
|
+
import mcp # noqa: F401
|
|
49
|
+
|
|
50
|
+
from rayonne_mcp.server import run
|
|
51
|
+
except Exception:
|
|
52
|
+
return False
|
|
53
|
+
_log("dependencies already present — running in-process")
|
|
54
|
+
run()
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _bundled_wheel() -> Path | None:
|
|
59
|
+
if not WHEELS.is_dir():
|
|
60
|
+
return None
|
|
61
|
+
wheels = sorted(WHEELS.glob("rayonne_mcp-*.whl"))
|
|
62
|
+
return wheels[-1] if wheels else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _data_root() -> Path:
|
|
66
|
+
if os.name == "nt":
|
|
67
|
+
return Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
68
|
+
if sys.platform == "darwin":
|
|
69
|
+
return Path.home() / "Library" / "Application Support"
|
|
70
|
+
# Linux / other POSIX — honour XDG
|
|
71
|
+
return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _venv_dir(wheel: Path) -> Path:
|
|
75
|
+
# Key by wheel filename so a new bundle version provisions a fresh venv.
|
|
76
|
+
return _data_root() / "rayonne-mcp" / f"venv-{wheel.stem}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _venv_python(vdir: Path) -> Path:
|
|
80
|
+
if os.name == "nt":
|
|
81
|
+
return vdir / "Scripts" / "python.exe"
|
|
82
|
+
return vdir / "bin" / "python"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _venv_console(vdir: Path) -> Path:
|
|
86
|
+
if os.name == "nt":
|
|
87
|
+
return vdir / "Scripts" / "rayonne-mcp.exe"
|
|
88
|
+
return vdir / "bin" / "rayonne-mcp"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _provision(wheel: Path) -> Path:
|
|
92
|
+
import subprocess
|
|
93
|
+
import venv
|
|
94
|
+
|
|
95
|
+
vdir = _venv_dir(wheel)
|
|
96
|
+
marker = vdir / ".provisioned"
|
|
97
|
+
py = _venv_python(vdir)
|
|
98
|
+
|
|
99
|
+
# Fast-path: only skip provisioning when the sentinel records the *exact*
|
|
100
|
+
# wheel filename — a partial install (crash mid-pip) or a different wheel
|
|
101
|
+
# will have a missing or mismatched sentinel and triggers re-provisioning.
|
|
102
|
+
sentinel_ok = marker.exists() and marker.read_text().strip() == wheel.name
|
|
103
|
+
if not sentinel_ok:
|
|
104
|
+
_log(f"provisioning environment in {vdir} (wheel={wheel.name})…")
|
|
105
|
+
if not py.exists():
|
|
106
|
+
venv.EnvBuilder(with_pip=True).create(vdir)
|
|
107
|
+
# Remove a stale sentinel before installing so a crash mid-pip leaves
|
|
108
|
+
# no sentinel (not a stale one that would fool the next launch).
|
|
109
|
+
if marker.exists():
|
|
110
|
+
marker.unlink()
|
|
111
|
+
subprocess.check_call(
|
|
112
|
+
[
|
|
113
|
+
str(py),
|
|
114
|
+
"-m",
|
|
115
|
+
"pip",
|
|
116
|
+
"install",
|
|
117
|
+
"--quiet",
|
|
118
|
+
"--disable-pip-version-check",
|
|
119
|
+
str(wheel),
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
marker.write_text(wheel.name)
|
|
123
|
+
_log("environment ready")
|
|
124
|
+
return vdir
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main() -> None:
|
|
128
|
+
if _run_in_process():
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
wheel = _bundled_wheel()
|
|
132
|
+
if wheel is None:
|
|
133
|
+
_log(
|
|
134
|
+
"FATAL: dependencies are not installed and no bundled wheel was "
|
|
135
|
+
"found. Reinstall the bundle, or run 'uv tool install rayonne-mcp'."
|
|
136
|
+
)
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
|
|
139
|
+
vdir = _provision(wheel)
|
|
140
|
+
console = _venv_console(vdir)
|
|
141
|
+
if console.exists():
|
|
142
|
+
exe, argv = str(console), [str(console)]
|
|
143
|
+
else: # fall back to the module entry point
|
|
144
|
+
py = str(_venv_python(vdir))
|
|
145
|
+
exe, argv = py, [py, "-m", "rayonne_mcp"]
|
|
146
|
+
_log(f"launching {argv[0]}")
|
|
147
|
+
os.execv(exe, argv)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rayonne-mcp"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Rayonne MCP server — pilote le workspace Rayonne (audit marketing, analyse produit, contenus, vidéos, métriques AARRR) depuis tout agent compatible MCP."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "SPONGE THEORY PROPRIETARY LICENSE" }
|
|
8
|
+
authors = [{ name = "Sponge Theory", email = "contact@sponge-theory.ai" }]
|
|
9
|
+
keywords = ["mcp", "marketing", "rayonne", "claude", "sponge-theory"]
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
# <2 : la 2.0 a déplacé FastMCP hors de mcp.server.fastmcp ; la série 1.x
|
|
13
|
+
# est celle qu'audigeo-mcp fait tourner en production.
|
|
14
|
+
"mcp[cli]>=1.2.0,<2",
|
|
15
|
+
"httpx>=0.27,<1",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8",
|
|
21
|
+
"pytest-asyncio>=0.24",
|
|
22
|
+
"respx>=0.21",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
rayonne-mcp = "rayonne_mcp.server:main"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/rayonne_mcp"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""HTTP client for the Rayonne public API v1 (lecture ET écriture).
|
|
2
|
+
|
|
3
|
+
Pure httpx, no MCP imports — testable seule (respx), réutilisable par un
|
|
4
|
+
futur connecteur Zapier. Contrat serveur : ``docs/API.md`` du repo Rayonne.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
DEFAULT_API_URL = "https://rayonne.ai/api/public/v1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RayonneConfigError(RuntimeError):
|
|
18
|
+
"""Raised on the first call when no API key is configured."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RayonneClient:
|
|
22
|
+
"""Thin async client. ``async with RayonneClient() as c: ...``"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self, *, api_url: str | None = None, api_key: str | None = None
|
|
26
|
+
) -> None:
|
|
27
|
+
raw_url = api_url or os.environ.get("RAYONNE_API_URL") or DEFAULT_API_URL
|
|
28
|
+
self.api_url = raw_url.rstrip("/")
|
|
29
|
+
# Tolère l'URL racine du déploiement : on complète le chemin public.
|
|
30
|
+
if not self.api_url.endswith("/api/public/v1"):
|
|
31
|
+
self.api_url = f"{self.api_url}/api/public/v1"
|
|
32
|
+
self.api_key = api_key or os.environ.get("RAYONNE_API_KEY") or ""
|
|
33
|
+
self._http: httpx.AsyncClient | None = None
|
|
34
|
+
|
|
35
|
+
async def __aenter__(self) -> "RayonneClient":
|
|
36
|
+
self._http = httpx.AsyncClient(timeout=30)
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
40
|
+
if self._http is not None:
|
|
41
|
+
await self._http.aclose()
|
|
42
|
+
self._http = None
|
|
43
|
+
|
|
44
|
+
def _headers(self) -> dict[str, str]:
|
|
45
|
+
if not self.api_key:
|
|
46
|
+
raise RayonneConfigError(
|
|
47
|
+
"No Rayonne API key configured. Create one in Rayonne → "
|
|
48
|
+
"Réglages → Clés API (rk_…, Business plan required) and set "
|
|
49
|
+
"RAYONNE_API_KEY."
|
|
50
|
+
)
|
|
51
|
+
return {"Authorization": f"Bearer {self.api_key}"}
|
|
52
|
+
|
|
53
|
+
async def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
54
|
+
assert self._http is not None, "use `async with RayonneClient()`"
|
|
55
|
+
resp = await self._http.get(
|
|
56
|
+
f"{self.api_url}{path}", params=params, headers=self._headers()
|
|
57
|
+
)
|
|
58
|
+
resp.raise_for_status()
|
|
59
|
+
return resp.json()
|
|
60
|
+
|
|
61
|
+
async def overview(self) -> dict[str, Any]:
|
|
62
|
+
return await self._get("/overview") # type: ignore[no-any-return]
|
|
63
|
+
|
|
64
|
+
async def list_content(
|
|
65
|
+
self, *, status: str | None = None, limit: int = 50
|
|
66
|
+
) -> list[dict[str, Any]]:
|
|
67
|
+
params: dict[str, Any] = {"limit": limit}
|
|
68
|
+
if status:
|
|
69
|
+
params["status"] = status
|
|
70
|
+
return await self._get("/content", params) # type: ignore[no-any-return]
|
|
71
|
+
|
|
72
|
+
async def list_submissions(
|
|
73
|
+
self, *, status: str | None = None, limit: int = 50
|
|
74
|
+
) -> list[dict[str, Any]]:
|
|
75
|
+
params: dict[str, Any] = {"limit": limit}
|
|
76
|
+
if status:
|
|
77
|
+
params["status"] = status
|
|
78
|
+
return await self._get("/submissions", params) # type: ignore[no-any-return]
|
|
79
|
+
|
|
80
|
+
async def metrics_aarrr(self, *, span_days: int = 14) -> dict[str, Any]:
|
|
81
|
+
return await self._get( # type: ignore[no-any-return]
|
|
82
|
+
"/metrics/aarrr", {"span_days": span_days}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# ── Écriture ────────────────────────────────────────────────────────
|
|
86
|
+
#
|
|
87
|
+
# Le serveur applique les mêmes gardes que l'interface (entitlement,
|
|
88
|
+
# quota facturé, garde SSRF) et déduit TOUJOURS le workspace de la clé.
|
|
89
|
+
# Côté client il n'y a donc rien à valider : on transmet, et une erreur
|
|
90
|
+
# métier revient telle quelle au modèle, qui saura la lire.
|
|
91
|
+
|
|
92
|
+
async def _post(self, path: str, body: dict[str, Any]) -> Any:
|
|
93
|
+
assert self._http is not None, "use `async with RayonneClient()`"
|
|
94
|
+
resp = await self._http.post(
|
|
95
|
+
f"{self.api_url}{path}", json=body, headers=self._headers()
|
|
96
|
+
)
|
|
97
|
+
resp.raise_for_status()
|
|
98
|
+
return resp.json()
|
|
99
|
+
|
|
100
|
+
async def create_brief(self, *, product_url: str) -> dict[str, Any]:
|
|
101
|
+
return await self._post( # type: ignore[no-any-return]
|
|
102
|
+
"/briefs", {"product_url": product_url}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def create_video(
|
|
106
|
+
self, *, locale: str = "fr", video_format: str = "9x16"
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
return await self._post( # type: ignore[no-any-return]
|
|
109
|
+
"/video", {"locale": locale, "format": video_format}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def run_marketing_audit(
|
|
113
|
+
self, *, locale: str | None = None, refresh: bool = True
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
return await self._post( # type: ignore[no-any-return]
|
|
116
|
+
"/marketing-audit", {"locale": locale, "refresh": refresh}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
async def list_marketing_audits(self, *, limit: int = 10) -> list[dict[str, Any]]:
|
|
120
|
+
return await self._get( # type: ignore[no-any-return]
|
|
121
|
+
"/marketing-audit", {"limit": limit}
|
|
122
|
+
)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""FastMCP wiring — lecture ET écriture sur l'API publique Rayonne v1.
|
|
2
|
+
|
|
3
|
+
Démarrage TOUJOURS non fatal (même patron que le MCP audigeo) : le serveur
|
|
4
|
+
monte sans clé, chaque outil rend alors un message d'installation clair —
|
|
5
|
+
jamais un crash au boot de Claude.
|
|
6
|
+
|
|
7
|
+
**Écriture** : les outils qui déclenchent un travail (analyse de produit,
|
|
8
|
+
audit marketing, génération vidéo) consomment du quota et, pour la vidéo,
|
|
9
|
+
de la facturation. Ils sont donc désactivables d'un seul réglage,
|
|
10
|
+
``RAYONNE_READ_ONLY=true``, que l'utilisateur pose à l'installation quand il
|
|
11
|
+
veut explorer sans rien déclencher. Le serveur ne réimplémente aucune garde :
|
|
12
|
+
l'API publique applique déjà l'entitlement, le quota et la garde SSRF, et
|
|
13
|
+
déduit le workspace de la clé. Ce drapeau est un cran de sûreté côté client,
|
|
14
|
+
pas une politique de sécurité.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from mcp.server.fastmcp import FastMCP
|
|
24
|
+
|
|
25
|
+
from rayonne_mcp.client import RayonneClient, RayonneConfigError
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
INSTRUCTIONS = """\
|
|
30
|
+
Rayonne MCP — pilot the marketing-autopilot workspace.
|
|
31
|
+
|
|
32
|
+
READ: `rayonne_overview` (workspace + counts), `rayonne_list_content`
|
|
33
|
+
(latest generated pieces, with predicted_engagement 0-100),
|
|
34
|
+
`rayonne_list_submissions` (per-platform publication status, external_url
|
|
35
|
+
when live), `rayonne_metrics_aarrr` (the AARRR dashboard snapshot),
|
|
36
|
+
`rayonne_list_audits` (past marketing audits of the product page).
|
|
37
|
+
|
|
38
|
+
WRITE (each consumes quota; video generation is billed):
|
|
39
|
+
`rayonne_analyze_product` (crawl the product page into a fresh brief),
|
|
40
|
+
`rayonne_marketing_audit` (grade the product page's MESSAGE — what it
|
|
41
|
+
promises, to whom, against which alternative, at what price — and say which
|
|
42
|
+
axis caps everything generated downstream), `rayonne_create_video`.
|
|
43
|
+
|
|
44
|
+
START HERE when a user asks why their generated content is off-message:
|
|
45
|
+
`rayonne_marketing_audit`. A page that leads with its architecture instead
|
|
46
|
+
of the job it does caps every asset built from it — the audit names that
|
|
47
|
+
and returns ready-to-paste rewrites.
|
|
48
|
+
|
|
49
|
+
Auth: set RAYONNE_API_KEY (an `rk_` key from Rayonne → Réglages → Clés API;
|
|
50
|
+
Business plan) and optionally RAYONNE_API_URL. Set RAYONNE_READ_ONLY=true to
|
|
51
|
+
disable every write tool.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
mcp = FastMCP("rayonne", instructions=INSTRUCTIONS)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _call(fn: str, /, **kwargs: Any) -> Any:
|
|
58
|
+
try:
|
|
59
|
+
async with RayonneClient() as client:
|
|
60
|
+
return await getattr(client, fn)(**kwargs)
|
|
61
|
+
except RayonneConfigError as exc:
|
|
62
|
+
return {"error": "not_configured", "message": str(exc)}
|
|
63
|
+
except Exception as exc: # noqa: BLE001 — le message vaut mieux qu'un crash MCP
|
|
64
|
+
return {"error": "request_failed", "message": str(exc)[:300]}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@mcp.tool()
|
|
68
|
+
async def rayonne_overview() -> Any:
|
|
69
|
+
"""Workspace summary: name, product URL, autopilot state, and content /
|
|
70
|
+
submission counts by status. Call this first."""
|
|
71
|
+
return await _call("overview")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
async def rayonne_list_content(status: str | None = None, limit: int = 50) -> Any:
|
|
76
|
+
"""Latest content pieces (max 100). `status` filters on
|
|
77
|
+
pending/generating/review/approved/rejected/scheduled/published/failed/
|
|
78
|
+
cancelled. Each row carries predicted_engagement (0-100) when scored."""
|
|
79
|
+
return await _call("list_content", status=status, limit=limit)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@mcp.tool()
|
|
83
|
+
async def rayonne_list_submissions(status: str | None = None, limit: int = 50) -> Any:
|
|
84
|
+
"""Latest submissions (max 100): platform slug, mode (api/browser_local/
|
|
85
|
+
semi_auto), status, and external_url once published."""
|
|
86
|
+
return await _call("list_submissions", status=status, limit=limit)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _read_only() -> bool:
|
|
90
|
+
return os.environ.get("RAYONNE_READ_ONLY", "").strip().lower() in {
|
|
91
|
+
"1",
|
|
92
|
+
"true",
|
|
93
|
+
"yes",
|
|
94
|
+
"on",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _write(fn: str, /, **kwargs: Any) -> Any:
|
|
99
|
+
"""Comme ``_call``, avec le cran de sûreté lecture seule.
|
|
100
|
+
|
|
101
|
+
Le refus est une RÉPONSE, pas une exception : le modèle doit pouvoir la
|
|
102
|
+
lire et l'expliquer à l'utilisateur plutôt que de voir l'outil planter.
|
|
103
|
+
"""
|
|
104
|
+
if _read_only():
|
|
105
|
+
return {
|
|
106
|
+
"error": "read_only",
|
|
107
|
+
"message": (
|
|
108
|
+
"Ce serveur Rayonne est en lecture seule (RAYONNE_READ_ONLY). "
|
|
109
|
+
"Retire ce réglage dans la configuration du plugin pour "
|
|
110
|
+
"autoriser les actions qui consomment du quota."
|
|
111
|
+
),
|
|
112
|
+
}
|
|
113
|
+
return await _call(fn, **kwargs)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@mcp.tool()
|
|
117
|
+
async def rayonne_metrics_aarrr(span_days: int = 14) -> Any:
|
|
118
|
+
"""The AARRR snapshot (stages, geo, by_channel, timeseries, deltas) over
|
|
119
|
+
a sliding window of 1-90 days."""
|
|
120
|
+
return await _call("metrics_aarrr", span_days=span_days)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main() -> None:
|
|
124
|
+
logging.basicConfig(level=logging.INFO)
|
|
125
|
+
mcp.run()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mcp.tool()
|
|
133
|
+
async def rayonne_list_audits(limit: int = 10) -> Any:
|
|
134
|
+
"""Les audits marketing passés de la page produit (verdict, note, axes).
|
|
135
|
+
|
|
136
|
+
L'historique montre si une réécriture de page a payé : chaque audit
|
|
137
|
+
porte sa date et sa note.
|
|
138
|
+
"""
|
|
139
|
+
return await _call("list_marketing_audits", limit=limit)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@mcp.tool()
|
|
143
|
+
async def rayonne_marketing_audit(
|
|
144
|
+
locale: str | None = None, refresh: bool = True
|
|
145
|
+
) -> Any:
|
|
146
|
+
"""Audite le MESSAGE de la page produit et rend un verdict actionnable.
|
|
147
|
+
|
|
148
|
+
Note sept axes — hiérarchie du message, clarté, audience nommée,
|
|
149
|
+
différenciation, douleur vécue, preuve, offre — et nomme celui qui
|
|
150
|
+
plafonne tout le reste. Une page qui mène avec son architecture au lieu
|
|
151
|
+
du métier qu'elle rend bride tout ce que la plateforme génère ensuite :
|
|
152
|
+
c'est ce que l'audit dit, avec des phrases de remplacement prêtes à
|
|
153
|
+
coller.
|
|
154
|
+
|
|
155
|
+
Distinct d'un audit GEO (visibilité DANS les IA) : ici c'est le message.
|
|
156
|
+
|
|
157
|
+
``refresh`` relit la page avant de noter — laisse-le à True après une
|
|
158
|
+
modification de la page, sinon tu noterais l'analyse précédente.
|
|
159
|
+
``locale`` suit le sélecteur de langue du produit (fr/en).
|
|
160
|
+
|
|
161
|
+
202 : l'audit tourne en fond, relis-le avec `rayonne_list_audits`.
|
|
162
|
+
"""
|
|
163
|
+
return await _write("run_marketing_audit", locale=locale, refresh=refresh)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@mcp.tool()
|
|
167
|
+
async def rayonne_analyze_product(product_url: str) -> Any:
|
|
168
|
+
"""Analyse la page produit et en tire un brief frais (crawl + extraction).
|
|
169
|
+
|
|
170
|
+
C'est la matière première de tout ce que la plateforme génère. 202 :
|
|
171
|
+
l'analyse tourne en fond.
|
|
172
|
+
"""
|
|
173
|
+
return await _write("create_brief", product_url=product_url)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@mcp.tool()
|
|
177
|
+
async def rayonne_create_video(locale: str = "fr", video_format: str = "9x16") -> Any:
|
|
178
|
+
"""Génère une vidéo courte à partir du brief courant.
|
|
179
|
+
|
|
180
|
+
ATTENTION : consomme du quota vidéo et est FACTURÉE. Formats : 9x16
|
|
181
|
+
(vertical), 16x9 (paysage), 1x1 (carré). 202 : le rendu tourne en fond.
|
|
182
|
+
"""
|
|
183
|
+
return await _write("create_video", locale=locale, video_format=video_format)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""RayonneClient contre le contrat public v1 (respx, aucun réseau)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
import respx
|
|
10
|
+
from httpx import Response
|
|
11
|
+
|
|
12
|
+
from rayonne_mcp.client import RayonneClient, RayonneConfigError
|
|
13
|
+
|
|
14
|
+
BASE = "https://rayonne.test/api/public/v1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@respx.mock
|
|
18
|
+
async def test_overview_hits_the_public_path_with_bearer() -> None:
|
|
19
|
+
route = respx.get(f"{BASE}/overview").mock(
|
|
20
|
+
return_value=Response(200, json={"name": "WS", "autopilot": True})
|
|
21
|
+
)
|
|
22
|
+
async with RayonneClient(api_url="https://rayonne.test", api_key="rk_x") as c:
|
|
23
|
+
out = await c.overview()
|
|
24
|
+
assert out["name"] == "WS"
|
|
25
|
+
req = route.calls.last.request
|
|
26
|
+
assert req.headers["authorization"] == "Bearer rk_x"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@respx.mock
|
|
30
|
+
async def test_list_content_passes_status_and_limit() -> None:
|
|
31
|
+
route = respx.get(f"{BASE}/content").mock(return_value=Response(200, json=[]))
|
|
32
|
+
async with RayonneClient(api_url=BASE, api_key="rk_x") as c:
|
|
33
|
+
await c.list_content(status="approved", limit=10)
|
|
34
|
+
url = str(route.calls.last.request.url)
|
|
35
|
+
assert "status=approved" in url and "limit=10" in url
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def test_missing_key_raises_config_error(monkeypatch) -> None:
|
|
39
|
+
monkeypatch.delenv("RAYONNE_API_KEY", raising=False)
|
|
40
|
+
async with RayonneClient(api_url=BASE, api_key="") as c:
|
|
41
|
+
with pytest.raises(RayonneConfigError):
|
|
42
|
+
await c.overview()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@respx.mock
|
|
46
|
+
async def test_metrics_span_days() -> None:
|
|
47
|
+
route = respx.get(f"{BASE}/metrics/aarrr").mock(
|
|
48
|
+
return_value=Response(200, json={"stages": {}})
|
|
49
|
+
)
|
|
50
|
+
async with RayonneClient(api_url=BASE, api_key="rk_x") as c:
|
|
51
|
+
await c.metrics_aarrr(span_days=30)
|
|
52
|
+
assert "span_days=30" in str(route.calls.last.request.url)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── Écriture (2026-09-01) ────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_running_an_audit_posts_to_the_public_api(respx_mock):
|
|
60
|
+
route = respx_mock.post("https://x.io/api/public/v1/marketing-audit").mock(
|
|
61
|
+
return_value=httpx.Response(202, json={"job_id": "j", "resource_id": None})
|
|
62
|
+
)
|
|
63
|
+
async with RayonneClient(api_url="https://x.io", api_key="rk_x") as c:
|
|
64
|
+
out = await c.run_marketing_audit(locale="en", refresh=True)
|
|
65
|
+
assert out["job_id"] == "j"
|
|
66
|
+
body = json.loads(route.calls[0].request.content)
|
|
67
|
+
assert body == {"locale": "en", "refresh": True}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_a_write_without_a_key_raises_the_config_error():
|
|
72
|
+
"""Même message d'installation que pour la lecture : jamais un crash
|
|
73
|
+
opaque au premier appel."""
|
|
74
|
+
async with RayonneClient(api_url="https://x.io", api_key="") as c:
|
|
75
|
+
with pytest.raises(RayonneConfigError):
|
|
76
|
+
await c.create_brief(product_url="https://y.io")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_read_only_disables_every_write_tool(monkeypatch):
|
|
80
|
+
"""Le cran de sûreté est une RÉPONSE lisible, pas une exception : le
|
|
81
|
+
modèle doit pouvoir l'expliquer à l'utilisateur."""
|
|
82
|
+
import asyncio
|
|
83
|
+
|
|
84
|
+
from rayonne_mcp import server
|
|
85
|
+
|
|
86
|
+
monkeypatch.setenv("RAYONNE_READ_ONLY", "true")
|
|
87
|
+
out = asyncio.run(server._write("create_video"))
|
|
88
|
+
assert out["error"] == "read_only"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_read_only_off_lets_writes_through(monkeypatch):
|
|
92
|
+
import asyncio
|
|
93
|
+
|
|
94
|
+
from rayonne_mcp import server
|
|
95
|
+
|
|
96
|
+
monkeypatch.delenv("RAYONNE_READ_ONLY", raising=False)
|
|
97
|
+
called: list = []
|
|
98
|
+
|
|
99
|
+
async def fake_call(fn, /, **kw):
|
|
100
|
+
called.append(fn)
|
|
101
|
+
return {"ok": True}
|
|
102
|
+
|
|
103
|
+
monkeypatch.setattr(server, "_call", fake_call)
|
|
104
|
+
out = asyncio.run(server._write("create_video", locale="fr"))
|
|
105
|
+
assert out == {"ok": True}
|
|
106
|
+
assert called == ["create_video"]
|