feat: Gruppe 1 – Projektgerüst, Auth, CLI (v0.1.0)
- pyproject.toml: hatchling build, mcp-familywall entry-point, deps - src/mcp_familywall/__init__.py: version 0.1.0 - src/mcp_familywall/config.py: YAML config loader (schema_version: 1) - src/mcp_familywall/auth.py: keyring credential resolution (FW_EMAIL/FW_PASSWORD → keyring) - src/mcp_familywall/fw_client.py: httpx client, login/logout/call, FW_DEBUG logging - src/mcp_familywall/cli.py: click CLI with setup / check / serve commands - .gitignore, README.md, CLAUDE.md, SPEC.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"""CLI entry point for mcp-familywall.
|
||||
|
||||
Commands:
|
||||
- setup : Interactive credential setup; stores email+password in OS keyring.
|
||||
- check : Validate stored credentials against the Family Wall API.
|
||||
- serve : Start the MCP server (launched by Claude Desktop).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
from mcp_familywall import __version__
|
||||
|
||||
|
||||
@click.group(context_settings={"help_option_names": ["-h", "--help"]}, invoke_without_command=True)
|
||||
@click.version_option(__version__, "-v", "--version", prog_name="mcp-familywall")
|
||||
@click.pass_context
|
||||
def app(ctx: click.Context) -> None:
|
||||
"""mcp-familywall — MCP server for Family Wall (read-only)."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
click.echo(ctx.get_help())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# setup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@app.command()
|
||||
def setup() -> None:
|
||||
"""Interactive credential setup.
|
||||
|
||||
Prompts for email and password, verifies them against the Family Wall API,
|
||||
stores them in the OS keyring, and prints a Claude Desktop config snippet.
|
||||
"""
|
||||
from mcp_familywall.auth import store_credentials
|
||||
from mcp_familywall.config import save_config
|
||||
from mcp_familywall.fw_client import FamilyWallClient, FamilyWallError
|
||||
|
||||
click.echo("mcp-familywall setup\n")
|
||||
|
||||
email = click.prompt("Family Wall email")
|
||||
password = click.prompt("Family Wall password", hide_input=True)
|
||||
|
||||
# Verify credentials
|
||||
click.echo("\nVerifying credentials...")
|
||||
try:
|
||||
with FamilyWallClient() as client:
|
||||
client.login(email, password)
|
||||
client.logout()
|
||||
except FamilyWallError as exc:
|
||||
click.echo(click.style(f"Login failed: {exc}", fg="red"), err=True)
|
||||
sys.exit(1)
|
||||
except Exception as exc:
|
||||
click.echo(click.style(f"Connection error: {exc}", fg="red"), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
click.echo(click.style("Login successful!", fg="green"))
|
||||
|
||||
# Store credentials
|
||||
ok = store_credentials(email, password)
|
||||
if ok:
|
||||
click.echo("Credentials stored in OS keyring.")
|
||||
else:
|
||||
click.echo(
|
||||
click.style("Keyring not available.", fg="yellow")
|
||||
+ " Set environment variables instead:\n"
|
||||
f" FW_EMAIL={email}\n"
|
||||
" FW_PASSWORD=<your-password>"
|
||||
)
|
||||
|
||||
# Ensure config file exists
|
||||
save_config()
|
||||
|
||||
# Print Claude Desktop snippet
|
||||
_emit_claude_desktop_snippet()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@app.command()
|
||||
def check() -> None:
|
||||
"""Validate stored credentials against the Family Wall API."""
|
||||
from mcp_familywall.auth import get_credentials
|
||||
from mcp_familywall.fw_client import FamilyWallClient, FamilyWallError
|
||||
|
||||
click.echo("Checking Family Wall credentials...")
|
||||
|
||||
try:
|
||||
email, password = get_credentials()
|
||||
except RuntimeError as exc:
|
||||
click.echo(click.style(f"Error: {exc}", fg="red"), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
click.echo(f"Email: {email}")
|
||||
|
||||
try:
|
||||
with FamilyWallClient() as client:
|
||||
client.login(email, password)
|
||||
client.logout()
|
||||
except FamilyWallError as exc:
|
||||
click.echo(click.style(f"Authentication failed: {exc}", fg="red"), err=True)
|
||||
sys.exit(1)
|
||||
except Exception as exc:
|
||||
click.echo(click.style(f"Connection error: {exc}", fg="red"), err=True)
|
||||
sys.exit(1)
|
||||
|
||||
click.echo(click.style("Authentication successful!", fg="green"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# serve
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@app.command()
|
||||
def serve() -> None:
|
||||
"""Start the MCP server (launched by Claude Desktop)."""
|
||||
from mcp_familywall.server import create_server
|
||||
|
||||
server = create_server()
|
||||
server.run(transport="stdio")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _emit_claude_desktop_snippet() -> None:
|
||||
"""Print a Claude Desktop JSON config snippet."""
|
||||
uvx_path = shutil.which("uvx") or "<path-to-uvx>"
|
||||
|
||||
snippet = {
|
||||
"mcpServers": {
|
||||
"familywall": {
|
||||
"command": uvx_path,
|
||||
"args": ["mcp-familywall", "serve"],
|
||||
}
|
||||
}
|
||||
}
|
||||
click.echo("\nAdd this to your Claude Desktop config (claude_desktop_config.json):\n")
|
||||
click.echo(json.dumps(snippet, indent=2))
|
||||
Reference in New Issue
Block a user