154 lines
5.4 KiB
Python
Executable File
154 lines
5.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import sys
|
|
import os
|
|
import argparse
|
|
import json
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# EVENNIA NATIVE SETUP
|
|
# -----------------------------------------------------------------------------
|
|
def init_evennia():
|
|
# Only bootstrap if not already initialized
|
|
import django
|
|
from django.apps import apps
|
|
if not apps.ready:
|
|
sys.path.append("/home/becomingone/kairos/spatial_engine")
|
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.conf.settings")
|
|
|
|
# Change cwd to the MUD root to avoid permission errors when settings_default.py calls os.listdir()
|
|
os.chdir("/home/becomingone/kairos/spatial_engine")
|
|
django.setup()
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# MCP SERVER DEFINITION
|
|
# -----------------------------------------------------------------------------
|
|
mcp = FastMCP("evennia_admin")
|
|
|
|
@mcp.tool()
|
|
def db_query_character(name: str) -> str:
|
|
"""Queries the underlying Evennia database for raw character data."""
|
|
init_evennia()
|
|
from evennia.utils.search import search_object
|
|
results = search_object(name)
|
|
if not results:
|
|
return json.dumps({"error": f"Character '{name}' not found."})
|
|
|
|
char = results[0]
|
|
data = {
|
|
"id": char.id,
|
|
"key": char.key,
|
|
"location": char.location.key if char.location else None,
|
|
"attributes": {attr.key: str(attr.value) for attr in char.attributes.all()},
|
|
"typeclass": char.typeclass_path
|
|
}
|
|
return json.dumps(data, indent=2)
|
|
|
|
@mcp.tool()
|
|
def db_query_room(name: str) -> str:
|
|
"""Queries the underlying Evennia database for raw room data and topology."""
|
|
init_evennia()
|
|
from evennia.utils.search import search_object
|
|
results = search_object(name)
|
|
if not results:
|
|
return json.dumps({"error": f"Room '{name}' not found."})
|
|
|
|
room = results[0]
|
|
contents = [{"key": obj.key, "typeclass": obj.typeclass_path} for obj in room.contents]
|
|
exits = [{"key": ex.key, "destination": ex.destination.key if ex.destination else None} for ex in room.exits]
|
|
|
|
data = {
|
|
"id": room.id,
|
|
"key": room.key,
|
|
"desc": room.db.desc,
|
|
"contents": contents,
|
|
"exits": exits
|
|
}
|
|
return json.dumps(data, indent=2)
|
|
|
|
@mcp.tool()
|
|
def execute_as(character_name: str, command: str) -> str:
|
|
"""Forces a character to execute a command seamlessly at the root level."""
|
|
init_evennia()
|
|
from evennia.utils.search import search_object
|
|
results = search_object(character_name)
|
|
if not results:
|
|
return json.dumps({"error": f"Character '{character_name}' not found."})
|
|
|
|
char = results[0]
|
|
|
|
# Intercept output by overriding the msg method temporarily
|
|
output_buffer = []
|
|
original_msg = char.msg
|
|
|
|
def intercepted_msg(self, text=None, **kwargs):
|
|
if text:
|
|
if isinstance(text, tuple):
|
|
output_buffer.append(str(text[0]))
|
|
else:
|
|
output_buffer.append(str(text))
|
|
original_msg(text=text, **kwargs)
|
|
|
|
char.msg = intercepted_msg.__get__(char, type(char))
|
|
|
|
try:
|
|
char.execute_cmd(command)
|
|
# Wait a tiny bit for any async command processing
|
|
import time
|
|
time.sleep(0.5)
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)})
|
|
finally:
|
|
# Restore original msg method
|
|
char.msg = original_msg
|
|
|
|
# Clean ANSI codes
|
|
import re
|
|
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
cleaned = [ansi_escape.sub('', msg) for msg in output_buffer]
|
|
|
|
return json.dumps({"character": character_name, "command": command, "output": "\n".join(cleaned)})
|
|
|
|
@mcp.tool()
|
|
def broadcast_event(message: str) -> str:
|
|
"""Broadcasts a global message to all active sessions in the MUD."""
|
|
init_evennia()
|
|
from evennia.utils.create import create_message
|
|
from evennia.server.sessionhandler import SESSIONS
|
|
try:
|
|
SESSIONS.announce_all(message)
|
|
return json.dumps({"status": "success", "message": message})
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)})
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# CLI / MCP ROUTER
|
|
# -----------------------------------------------------------------------------
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Evennia MCP Admin Server / CLI Suite")
|
|
parser.add_argument("--mcp", action="store_true", help="Run as MCP stdio server")
|
|
|
|
# CLI Commands
|
|
parser.add_argument("--query-char", type=str, help="Query character data")
|
|
parser.add_argument("--query-room", type=str, help="Query room data")
|
|
parser.add_argument("--execute", nargs=2, metavar=('CHARACTER', 'COMMAND'), help="Execute command as character")
|
|
parser.add_argument("--broadcast", type=str, help="Broadcast a global event")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.mcp:
|
|
# Run standard MCP stdio loop
|
|
mcp.run()
|
|
else:
|
|
# Run in CLI mode
|
|
if args.query_char:
|
|
print(db_query_character(args.query_char))
|
|
elif args.query_room:
|
|
print(db_query_room(args.query_room))
|
|
elif args.execute:
|
|
print(execute_as(args.execute[0], args.execute[1]))
|
|
elif args.broadcast:
|
|
print(broadcast_event(args.broadcast))
|
|
else:
|
|
parser.print_help()
|