#!/usr/bin/env python3 """Run the Bridge against the private local development server without displaying secrets.""" import argparse import json import os from pathlib import Path import re import subprocess ROOT = Path(__file__).resolve().parents[1] def scalar(text, key): """Read only the top-level scalar fields emitted by the Paper configuration writer.""" match = re.search(r'^' + re.escape(key) + r':[ \t]*(.*?)[ \t]*$', text, re.M) if not match: return '' raw = match.group(1).strip() if raw.startswith("'"): quoted = re.fullmatch(r"'((?:[^']|'')*)'[ \t]*(?:#.*)?", raw) if quoted: return quoted.group(1).replace("''", "'") elif raw.startswith('"'): quoted = re.fullmatch(r'("(?:[^"\\]|\\.)*")[ \t]*(?:#.*)?', raw) if quoted: try: return json.loads(quoted.group(1)) except json.JSONDecodeError: pass else: return re.split(r'[ \t]+#', raw, maxsplit=1)[0].strip() raise SystemExit(f'Invalid scalar for {key}; use the Paper-generated configuration format.') def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('mode', choices=['doctor', 'chat', 'mcp', 'login', 'status']) parser.add_argument('action', nargs='?', choices=['status'], help='Only for login: check authentication without starting a login flow') parser.add_argument('--status', action='store_true', help='With login: check authentication without starting login') parser.add_argument('--config', type=Path, default=ROOT / '.runtime/server/plugins/MinecraftBuilderMCP/config.yml') parser.add_argument('--console', action='store_true', help='Use console scope only with allow-local-automation in the disposable fixture') args = parser.parse_args() if (args.action or args.status) and args.mode != 'login': parser.error('The status action is accepted only after login; alternatively use mode status.') # Login/authentication status needs no Paper config or Paper token. Always use the same # default state directory as the chat daemon, including when called from another cwd. env = dict(os.environ) env.setdefault('MCB_STATE_DIR', str(ROOT / '.runtime/bridge-state')) needs_backend = args.mode in ('chat', 'mcp') if needs_backend and not args.config.is_file(): raise SystemExit('Start the Paper plugin once to generate its private config.') if args.mode in ('doctor', 'chat', 'mcp') and args.config.is_file(): text = args.config.read_text() actor = 'console' if args.console else scalar(text, 'owner-uuid') if args.mode == 'mcp' and not actor: raise SystemExit('Bind the owner in game with /ai setup first.') port = scalar(text, 'http-port') if not port.isdigit() or not 1 <= int(port) <= 65535: raise SystemExit('Paper http-port must be an integer in 1..65535.') env.update(MCB_BACKEND_URL='http://127.0.0.1:' + port, MCB_TOKEN=scalar(text, 'admin-token'), MCB_AGENT_TOKEN=scalar(text, 'agent-token'), MCB_PLAYER_ID=actor, MCB_PROJECT_ID=scalar(text, 'project-id')) target = 'dist/' + ('login' if args.mode in ('login', 'status') else args.mode) + '.js' if not (ROOT / 'bridge' / target).is_file(): raise SystemExit('Build the Bridge first: cd bridge && npm ci --ignore-scripts && npm run build') command = ['node', target] if args.mode == 'status' or args.action == 'status' or args.status: command.append('status') return subprocess.call(command, cwd=ROOT / 'bridge', env=env) if __name__ == '__main__': raise SystemExit(main())