Add JSON API endpoint, CLI tool, and MCP server
- Add /api/finger and /api/finger/<username> JSON endpoints - Remove hardcoded default credentials from config.py; require BASIC_AUTH_USERS env var - Add cli/finger.py: query and plan-upload CLI using the JSON API - Add mcp/server.py: FastMCP server exposing finger_user and upload_plan tools - All credentials and base URL are read from environment variables
This commit is contained in:
@@ -169,25 +169,36 @@ def finger_direct(username):
|
|||||||
|
|
||||||
return render_template('finger.html', title=f'Finger - {username}', result=result, error=error, username=username)
|
return render_template('finger.html', title=f'Finger - {username}', result=result, error=error, username=username)
|
||||||
|
|
||||||
@app.route('/api/hello')
|
@app.route('/api/finger')
|
||||||
def api_hello():
|
@app.route('/api/finger/<path:username>')
|
||||||
"""Simple JSON API endpoint"""
|
def api_finger(username=None):
|
||||||
return jsonify({
|
"""JSON API endpoint for finger queries"""
|
||||||
'message': 'Hello from Flask API!',
|
if username is None:
|
||||||
'status': 'success',
|
username = request.args.get('user', '')
|
||||||
'version': '1.0'
|
|
||||||
})
|
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')
|
@app.route('/api/info')
|
||||||
def api_info():
|
def api_info():
|
||||||
"""API info endpoint"""
|
"""API info endpoint"""
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'app_name': 'Finger Web Flask App',
|
'app_name': 'finger-web',
|
||||||
'routes': [
|
'routes': [
|
||||||
'/',
|
'/',
|
||||||
'/finger',
|
'/finger',
|
||||||
'/finger/<username>',
|
'/finger/<username>',
|
||||||
'/api/hello',
|
'/api/finger',
|
||||||
|
'/api/finger/<username>',
|
||||||
'/api/info',
|
'/api/info',
|
||||||
'/api/upload'
|
'/api/upload'
|
||||||
],
|
],
|
||||||
|
|||||||
+114
@@ -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. [email protected]")
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
requests>=2.31.0
|
||||||
@@ -30,12 +30,12 @@ class Config:
|
|||||||
SCP_ENABLED = os.environ.get('SCP_ENABLED', 'false').lower() == 'true'
|
SCP_ENABLED = os.environ.get('SCP_ENABLED', 'false').lower() == 'true'
|
||||||
|
|
||||||
# Basic authentication credentials - multiple users support
|
# Basic authentication credentials - multiple users support
|
||||||
# Environment variable format: "user1:pass1,user2:pass2,user3:pass3"
|
# Environment variable format: "user1:pass1,user2:pass2"
|
||||||
BASIC_AUTH_USERS_STR = os.environ.get('BASIC_AUTH_USERS') or 'admin:password,uploader:upload123,user:user123'
|
BASIC_AUTH_USERS_STR = os.environ.get('BASIC_AUTH_USERS', '')
|
||||||
|
|
||||||
# Parse users string into dictionary
|
# Parse users string into dictionary
|
||||||
BASIC_AUTH_USERS = {}
|
BASIC_AUTH_USERS = {}
|
||||||
for user_pass in BASIC_AUTH_USERS_STR.split(','):
|
for user_pass in BASIC_AUTH_USERS_STR.split(','):
|
||||||
if ':' in user_pass:
|
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()
|
BASIC_AUTH_USERS[username.strip()] = password.strip()
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
mcp[cli]
|
||||||
|
requests>=2.31.0
|
||||||
@@ -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. '[email protected]'). 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()
|
||||||
Reference in New Issue
Block a user