feat: add 'agent-reach configure xhs-cookies' command (#108) (#113)

Adds a proper way for AI agents to import XiaoHongShu cookies into
the xiaohongshu-mcp Docker container. Previously agents had no clear
command to use, leading to confusion loops (issue #108).

Supports two input formats:
- Cookie-Editor JSON export (array of cookie objects)
- Header String format (key1=val1; key2=val2; ...)

The command:
1. Parses and validates the cookie input
2. Finds the running xiaohongshu-mcp container
3. Reads COOKIES_PATH from container env
4. Writes cookies via docker cp
5. Verifies login status via mcporter

Also updates install.md with the new command.

Closes #108

Co-authored-by: Panniantong <panniantong@users.noreply.github.com>
This commit is contained in:
Pnant 2026-03-08 21:19:33 +08:00 committed by GitHub
parent eda80b89b5
commit 4b7d55111f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 180 additions and 3 deletions

View file

@ -73,7 +73,8 @@ def main():
p_conf = sub.add_parser("configure", help="Set a config value or auto-extract from browser")
p_conf.add_argument("key", nargs="?", default=None,
choices=["proxy", "github-token", "groq-key",
"twitter-cookies", "youtube-cookies"],
"twitter-cookies", "youtube-cookies",
"xhs-cookies"],
help="What to configure (omit if using --from-browser)")
p_conf.add_argument("value", nargs="*", help="The value(s) to set")
p_conf.add_argument("--from-browser", metavar="BROWSER",
@ -879,6 +880,9 @@ def _cmd_configure(args):
print(f"✅ YouTube cookie source configured: {value}")
print(" yt-dlp will use cookies from this browser for age-restricted/member videos.")
elif args.key == "xhs-cookies":
_configure_xhs_cookies(value)
elif args.key == "github-token":
config.set("github_token", value)
print(f"✅ GitHub token configured!")
@ -888,6 +892,171 @@ def _cmd_configure(args):
print(f"✅ Groq key configured!")
def _configure_xhs_cookies(value):
"""Import cookies into xiaohongshu-mcp Docker container.
Accepts two formats:
1. Cookie-Editor JSON export (array of cookie objects)
2. Header String: "name1=value1; name2=value2; ..."
The xiaohongshu-mcp container stores cookies at $COOKIES_PATH
(default: /app/data/cookies.json or cookies.json in workdir).
Format: JSON array of {name, value, domain, path, expires, httpOnly, secure, sameSite}.
"""
import json
import shutil
import subprocess
value = value.strip()
if not value:
print("[X] Missing cookie value.")
print(" Usage: agent-reach configure xhs-cookies '<cookie JSON or header string>'")
return
# Detect format and parse
cookies_json = None
# Try JSON format first (Cookie-Editor JSON export)
if value.startswith("["):
try:
parsed = json.loads(value)
if isinstance(parsed, list) and parsed:
# Validate it looks like cookie objects
first = parsed[0]
if isinstance(first, dict) and "name" in first and "value" in first:
cookies_json = json.dumps(parsed)
print(f" Parsed {len(parsed)} cookies from JSON format")
else:
print("[X] JSON array doesn't contain cookie objects (need name/value fields)")
return
else:
print("[X] Empty or invalid JSON array")
return
except json.JSONDecodeError as e:
print(f"[X] Invalid JSON: {e}")
return
# Header String format: "key1=val1; key2=val2; ..."
if cookies_json is None and "=" in value:
cookies = []
for part in value.split(";"):
part = part.strip()
if "=" not in part:
continue
name, val = part.split("=", 1)
name = name.strip()
val = val.strip()
if name:
cookies.append({
"name": name,
"value": val,
"domain": ".xiaohongshu.com",
"path": "/",
"expires": -1,
"size": len(name) + len(val),
"httpOnly": False,
"secure": False,
"session": True,
"sameSite": "Lax",
})
if cookies:
cookies_json = json.dumps(cookies)
print(f" Parsed {len(cookies)} cookies from Header String format")
else:
print("[X] Could not parse any cookies from input")
return
if not cookies_json:
print("[X] Could not parse cookies. Accepted formats:")
print(' 1. JSON array: \'[{"name":"x","value":"y","domain":".xiaohongshu.com",...}]\'')
print(' 2. Header String: "key1=val1; key2=val2; ..."')
return
# Find the container
docker = shutil.which("docker")
if not docker:
# No Docker - write to a local file for manual import
cookie_path = os.path.expanduser("~/.agent-reach/xhs-cookies.json")
with open(cookie_path, "w") as f:
f.write(cookies_json)
os.chmod(cookie_path, 0o600)
print(f" Cookies saved to {cookie_path}")
print(" Docker not found. Copy manually:")
print(f" docker cp {cookie_path} xiaohongshu-mcp:/app/data/cookies.json")
return
# Check if xiaohongshu-mcp container is running
try:
result = subprocess.run(
[docker, "ps", "--filter", "name=xiaohongshu-mcp", "--format", "{{.Names}}"],
capture_output=True, encoding="utf-8", timeout=5,
)
container_name = result.stdout.strip()
if not container_name:
print("[X] xiaohongshu-mcp container is not running.")
print(" Start it first:")
print(" docker run -d --name xiaohongshu-mcp -p 18060:18060 xpzouying/xiaohongshu-mcp")
return
except Exception as e:
print(f"[X] Could not check Docker: {e}")
return
# Find the cookies path inside the container
try:
result = subprocess.run(
[docker, "exec", container_name, "printenv", "COOKIES_PATH"],
capture_output=True, encoding="utf-8", timeout=5,
)
cookie_path_in_container = result.stdout.strip()
if not cookie_path_in_container:
cookie_path_in_container = "cookies.json" # fallback to workdir
except Exception:
cookie_path_in_container = "cookies.json"
# Write cookies into the container
try:
# Write to temp file then docker cp
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
f.write(cookies_json)
tmp_path = f.name
result = subprocess.run(
[docker, "cp", tmp_path, f"{container_name}:{cookie_path_in_container}"],
capture_output=True, encoding="utf-8", timeout=10,
)
os.unlink(tmp_path)
if result.returncode != 0:
print(f"[X] Failed to copy cookies: {result.stderr}")
return
print(f"✅ Cookies written to {container_name}:{cookie_path_in_container}")
except Exception as e:
print(f"[X] Failed to write cookies: {e}")
return
# Verify login status via mcporter
mcporter = shutil.which("mcporter")
if mcporter:
print(" Verifying login status...", end=" ")
try:
result = subprocess.run(
[mcporter, "call", "xiaohongshu.check_login_status()"],
capture_output=True, encoding="utf-8", errors="replace", timeout=15,
)
if "已登录" in result.stdout or "logged" in result.stdout.lower():
print("✅ Login verified!")
else:
print("[!] Login check returned unexpected result:")
print(f" {result.stdout.strip()[:200]}")
print(" Cookies were written but login might not be valid. Try fresh cookies.")
except Exception as e:
print(f"[!] Could not verify: {e}")
else:
print(" (mcporter not found, skipping verification)")
def _cmd_uninstall(args):
"""Remove all Agent Reach config, tokens, and skill files."""
import shutil

View file

@ -148,9 +148,17 @@ mcporter config add xiaohongshu http://localhost:18060/mcp
>
> **登录方式(优先用 Cookie-Editor最简单**
> 1. 用户在自己的浏览器登录小红书 (xiaohongshu.com)
> 2. 用 [Cookie-Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) 插件导出 CookieHeader String 格式)
> 2. 用 [Cookie-Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) 插件导出 CookieJSON 或 Header String 格式均可
> 3. 把 Cookie 字符串发给 Agent
> 4. Agent 将 Cookie 写入 MCP 服务的 cookie 文件完成登录
> 4. Agent 运行命令完成登录:
>
> ```bash
> # JSON 格式Cookie-Editor → Export → JSON
> agent-reach configure xhs-cookies '[{"name":"web_session","value":"xxx","domain":".xiaohongshu.com",...}]'
>
> # 或 Header String 格式Cookie-Editor → Export → Header String
> agent-reach configure xhs-cookies "key1=val1; key2=val2; ..."
> ```
>
> **备选:** 本地电脑如果有浏览器,也可以打开 http://localhost:18060 扫码登录。