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:
2026-04-15 12:18:37 +02:00
parent a58c776298
commit 38da31b0cb
10 changed files with 948 additions and 0 deletions
+151
View File
@@ -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))