diff --git a/app.py b/app.py index a6e7b49..a6949cc 100644 --- a/app.py +++ b/app.py @@ -169,25 +169,36 @@ def finger_direct(username): return render_template('finger.html', title=f'Finger - {username}', result=result, error=error, username=username) -@app.route('/api/hello') -def api_hello(): - """Simple JSON API endpoint""" - return jsonify({ - 'message': 'Hello from Flask API!', - 'status': 'success', - 'version': '1.0' - }) +@app.route('/api/finger') +@app.route('/api/finger/') +def api_finger(username=None): + """JSON API endpoint for finger queries""" + if username is None: + username = request.args.get('user', '') + + if username: + if not all(c.isalnum() or c in '.-_@' for c in username): + return jsonify({'error': 'Invalid username', 'status': 'error'}), 400 + cmd = ['finger', username] + else: + cmd = ['finger'] + + success, output, is_error = run_finger_command(cmd) + if success: + return jsonify({'result': output, 'username': username, 'status': 'success'}) + return jsonify({'error': output, 'status': 'error'}), 500 @app.route('/api/info') def api_info(): """API info endpoint""" return jsonify({ - 'app_name': 'Finger Web Flask App', + 'app_name': 'finger-web', 'routes': [ '/', '/finger', '/finger/', - '/api/hello', + '/api/finger', + '/api/finger/', '/api/info', '/api/upload' ], diff --git a/cli/finger.py b/cli/finger.py new file mode 100644 index 0000000..8b979ff --- /dev/null +++ b/cli/finger.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""finger-web CLI — query finger users and manage plan files. + +Configuration via environment variables: + FINGER_WEB_URL Base URL of the finger-web instance (default: http://localhost:5000) + FINGER_USER Username for authenticated endpoints (upload) + FINGER_PASS Password for authenticated endpoints (upload) +""" + +import argparse +import os +import sys + +try: + import requests +except ImportError: + print("requests is required: pip install requests", file=sys.stderr) + sys.exit(1) + +BASE_URL = os.environ.get("FINGER_WEB_URL", "http://localhost:5000").rstrip("/") + + +def _get_auth(): + user = os.environ.get("FINGER_USER", "") + passwd = os.environ.get("FINGER_PASS", "") + return (user, passwd) if user and passwd else None + + +def cmd_query(args): + target = args.username + url = f"{BASE_URL}/api/finger/{target}" if target else f"{BASE_URL}/api/finger" + try: + resp = requests.get(url, timeout=10) + resp.raise_for_status() + data = resp.json() + if data.get("status") == "success": + print(data.get("result", "").rstrip()) + else: + print(data.get("error", "Unknown error"), file=sys.stderr) + sys.exit(1) + except requests.exceptions.ConnectionError: + print(f"Cannot connect to {BASE_URL}", file=sys.stderr) + sys.exit(1) + except requests.exceptions.HTTPError as e: + print(f"HTTP error: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_plan(args): + auth = _get_auth() + if not auth: + print( + "Set FINGER_USER and FINGER_PASS environment variables to authenticate.", + file=sys.stderr, + ) + sys.exit(1) + + plan_file = args.file + if not os.path.exists(plan_file): + print(f"File not found: {plan_file}", file=sys.stderr) + sys.exit(1) + + with open(plan_file, "rb") as f: + try: + resp = requests.post( + f"{BASE_URL}/api/upload", + files={"file": (os.path.basename(plan_file), f, "text/plain")}, + auth=auth, + timeout=30, + ) + resp.raise_for_status() + data = resp.json() + print(data.get("message", "Upload successful.")) + except requests.exceptions.ConnectionError: + print(f"Cannot connect to {BASE_URL}", file=sys.stderr) + sys.exit(1) + except requests.exceptions.HTTPError as e: + print(f"HTTP error: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser( + prog="finger-web", + description="Query finger users and manage plan files via finger-web.", + ) + sub = parser.add_subparsers(dest="command", metavar="COMMAND") + + q = sub.add_parser("query", aliases=["q"], help="Finger a user") + q.add_argument("username", nargs="?", default="", help="User to finger, e.g. pete@peteftw.com") + + sub.add_parser("plan", help="Upload your plan file").add_argument( + "file", help="Path to the plan text file" + ) + + args = parser.parse_args() + + if args.command in ("query", "q"): + cmd_query(args) + elif args.command == "plan": + cmd_plan(args) + else: + parser.print_help() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/cli/requirements.txt b/cli/requirements.txt new file mode 100644 index 0000000..0eb8cae --- /dev/null +++ b/cli/requirements.txt @@ -0,0 +1 @@ +requests>=2.31.0 diff --git a/config.py b/config.py index e790466..7a08b50 100644 --- a/config.py +++ b/config.py @@ -30,12 +30,12 @@ class Config: SCP_ENABLED = os.environ.get('SCP_ENABLED', 'false').lower() == 'true' # Basic authentication credentials - multiple users support - # Environment variable format: "user1:pass1,user2:pass2,user3:pass3" - BASIC_AUTH_USERS_STR = os.environ.get('BASIC_AUTH_USERS') or 'admin:password,uploader:upload123,user:user123' - + # Environment variable format: "user1:pass1,user2:pass2" + BASIC_AUTH_USERS_STR = os.environ.get('BASIC_AUTH_USERS', '') + # Parse users string into dictionary BASIC_AUTH_USERS = {} for user_pass in BASIC_AUTH_USERS_STR.split(','): if ':' in user_pass: - username, password = user_pass.strip().split(':', 1) # Split only on first colon + username, password = user_pass.strip().split(':', 1) BASIC_AUTH_USERS[username.strip()] = password.strip() diff --git a/mcp/requirements.txt b/mcp/requirements.txt new file mode 100644 index 0000000..c129cfe --- /dev/null +++ b/mcp/requirements.txt @@ -0,0 +1,2 @@ +mcp[cli] +requests>=2.31.0 diff --git a/mcp/server.py b/mcp/server.py new file mode 100644 index 0000000..e0a9c11 --- /dev/null +++ b/mcp/server.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""MCP server for finger-web. + +Exposes finger query and plan-upload operations as Claude tools. + +Environment variables: + FINGER_WEB_URL Base URL of the finger-web instance (default: http://localhost:5000) + FINGER_USER Username for authenticated endpoints + FINGER_PASS Password for authenticated endpoints +""" + +import os + +import requests +from mcp.server.fastmcp import FastMCP + +BASE_URL = os.environ.get("FINGER_WEB_URL", "http://localhost:5000").rstrip("/") + +mcp = FastMCP("finger") + + +def _auth(): + user = os.environ.get("FINGER_USER", "") + passwd = os.environ.get("FINGER_PASS", "") + return (user, passwd) if user and passwd else None + + +@mcp.tool() +def finger_user(username: str = "") -> str: + """Query finger information for a user via finger-web. + + Pass a bare username (e.g. 'pete') or a user@host address + (e.g. 'pete@peteftw.com'). Leave username empty to list currently + logged-in users. + """ + if username and not all(c.isalnum() or c in ".-_@" for c in username): + return f"Invalid username: {username!r}" + + url = f"{BASE_URL}/api/finger/{username}" if username else f"{BASE_URL}/api/finger" + try: + resp = requests.get(url, timeout=10) + resp.raise_for_status() + data = resp.json() + if data.get("status") == "success": + return data.get("result", "No information available.").strip() + return data.get("error", "Unknown error.") + except requests.exceptions.ConnectionError: + return f"Cannot connect to {BASE_URL}" + except Exception as e: + return f"Error: {e}" + + +@mcp.tool() +def upload_plan(filename: str, content: str) -> str: + """Upload or update a finger plan file via finger-web. + + filename The plan filename (typically your username, e.g. 'pete'). + content Plain-text content to publish as your .plan. + + Requires FINGER_USER and FINGER_PASS environment variables to be set. + """ + auth = _auth() + if not auth: + return "Set FINGER_USER and FINGER_PASS environment variables to authenticate." + + try: + resp = requests.post( + f"{BASE_URL}/api/upload", + files={"file": (filename, content.encode(), "text/plain")}, + auth=auth, + timeout=30, + ) + resp.raise_for_status() + data = resp.json() + return data.get("message", "Upload successful.") + except requests.exceptions.ConnectionError: + return f"Cannot connect to {BASE_URL}" + except requests.exceptions.HTTPError as e: + return f"HTTP error: {e}" + except Exception as e: + return f"Error: {e}" + + +if __name__ == "__main__": + mcp.run()