From 5703cf5871a937c4de319e9276d97048e4e2a79c Mon Sep 17 00:00:00 2001 From: LeRatierBretonnier Date: Tue, 7 Apr 2026 22:06:36 +0200 Subject: [PATCH] Add gitignor --- README.md | 101 ++ assistant/__init__.py | 0 assistant/audio.py | 84 ++ assistant/cli.py | 127 +- assistant/config.py | 20 + assistant/llm.py | 55 +- assistant/mcp_client.py | 144 +- assistant/stt.py | 109 ++ assistant/tts.py | 54 + main.py | 4 + .../traveller_map/dist/api/client.d.ts | 11 + .../traveller_map/dist/api/client.d.ts.map | 1 + mcp_servers/traveller_map/dist/api/client.js | 76 ++ .../traveller_map/dist/api/client.js.map | 1 + mcp_servers/traveller_map/dist/index.d.ts | 2 + mcp_servers/traveller_map/dist/index.d.ts.map | 1 + mcp_servers/traveller_map/dist/index.js | 12 + mcp_servers/traveller_map/dist/index.js.map | 1 + .../traveller_map/dist/parsers/sec.d.ts | 22 + .../traveller_map/dist/parsers/sec.d.ts.map | 1 + mcp_servers/traveller_map/dist/parsers/sec.js | 106 ++ .../traveller_map/dist/parsers/sec.js.map | 1 + .../traveller_map/dist/parsers/uwp.d.ts | 30 + .../traveller_map/dist/parsers/uwp.d.ts.map | 1 + mcp_servers/traveller_map/dist/parsers/uwp.js | 203 +++ .../traveller_map/dist/parsers/uwp.js.map | 1 + mcp_servers/traveller_map/dist/server.d.ts | 3 + .../traveller_map/dist/server.d.ts.map | 1 + mcp_servers/traveller_map/dist/server.js | 31 + mcp_servers/traveller_map/dist/server.js.map | 1 + .../traveller_map/dist/tools/find_route.d.ts | 36 + .../dist/tools/find_route.d.ts.map | 1 + .../traveller_map/dist/tools/find_route.js | 121 ++ .../dist/tools/find_route.js.map | 1 + .../dist/tools/get_allegiance_list.d.ts | 12 + .../dist/tools/get_allegiance_list.d.ts.map | 1 + .../dist/tools/get_allegiance_list.js | 27 + .../dist/tools/get_allegiance_list.js.map | 1 + .../dist/tools/get_jump_map.d.ts | 50 + .../dist/tools/get_jump_map.d.ts.map | 1 + .../traveller_map/dist/tools/get_jump_map.js | 97 ++ .../dist/tools/get_jump_map.js.map | 1 + .../dist/tools/get_sector_data.d.ts | 24 + .../dist/tools/get_sector_data.d.ts.map | 1 + .../dist/tools/get_sector_data.js | 41 + .../dist/tools/get_sector_data.js.map | 1 + .../dist/tools/get_sector_list.d.ts | 21 + .../dist/tools/get_sector_list.d.ts.map | 1 + .../dist/tools/get_sector_list.js | 43 + .../dist/tools/get_sector_list.js.map | 1 + .../dist/tools/get_sector_metadata.d.ts | 21 + .../dist/tools/get_sector_metadata.d.ts.map | 1 + .../dist/tools/get_sector_metadata.js | 43 + .../dist/tools/get_sector_metadata.js.map | 1 + .../dist/tools/get_subsector_map.d.ts | 47 + .../dist/tools/get_subsector_map.d.ts.map | 1 + .../dist/tools/get_subsector_map.js | 94 ++ .../dist/tools/get_subsector_map.js.map | 1 + .../dist/tools/get_world_info.d.ts | 24 + .../dist/tools/get_world_info.d.ts.map | 1 + .../dist/tools/get_world_info.js | 134 ++ .../dist/tools/get_world_info.js.map | 1 + .../dist/tools/get_worlds_in_jump_range.d.ts | 27 + .../tools/get_worlds_in_jump_range.d.ts.map | 1 + .../dist/tools/get_worlds_in_jump_range.js | 70 + .../tools/get_worlds_in_jump_range.js.map | 1 + .../dist/tools/render_custom_map.d.ts | 37 + .../dist/tools/render_custom_map.d.ts.map | 1 + .../dist/tools/render_custom_map.js | 42 + .../dist/tools/render_custom_map.js.map | 1 + .../dist/tools/search_worlds.d.ts | 21 + .../dist/tools/search_worlds.d.ts.map | 1 + .../traveller_map/dist/tools/search_worlds.js | 82 ++ .../dist/tools/search_worlds.js.map | 1 + mcp_servers/traveller_map/package-lock.json | 1174 +++++++++++++++++ mcp_servers/traveller_map/package.json | 20 + mcp_servers/traveller_map/src/api/client.ts | 91 ++ mcp_servers/traveller_map/src/index.ts | 13 + mcp_servers/traveller_map/src/parsers/sec.ts | 134 ++ mcp_servers/traveller_map/src/parsers/uwp.ts | 231 ++++ mcp_servers/traveller_map/src/server.ts | 88 ++ .../traveller_map/src/tools/find_route.ts | 147 +++ .../src/tools/get_allegiance_list.ts | 42 + .../traveller_map/src/tools/get_jump_map.ts | 112 ++ .../src/tools/get_sector_data.ts | 51 + .../src/tools/get_sector_list.ts | 67 + .../src/tools/get_sector_metadata.ts | 76 ++ .../src/tools/get_subsector_map.ts | 109 ++ .../traveller_map/src/tools/get_world_info.ts | 169 +++ .../src/tools/get_worlds_in_jump_range.ts | 98 ++ .../src/tools/render_custom_map.ts | 54 + .../traveller_map/src/tools/search_worlds.ts | 139 ++ mcp_servers/traveller_map/tsconfig.json | 17 + profiles/default.yaml | 4 + profiles/docs/traveller_scout_ship.md | 60 + profiles/traveller_scout.yaml | 38 + scripts/list_voices.py | 70 + scripts/register_voice.py | 57 + 98 files changed, 5329 insertions(+), 72 deletions(-) create mode 100644 README.md create mode 100644 assistant/__init__.py create mode 100644 assistant/audio.py create mode 100644 assistant/config.py create mode 100644 assistant/stt.py create mode 100644 assistant/tts.py create mode 100644 main.py create mode 100644 mcp_servers/traveller_map/dist/api/client.d.ts create mode 100644 mcp_servers/traveller_map/dist/api/client.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/api/client.js create mode 100644 mcp_servers/traveller_map/dist/api/client.js.map create mode 100644 mcp_servers/traveller_map/dist/index.d.ts create mode 100644 mcp_servers/traveller_map/dist/index.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/index.js create mode 100644 mcp_servers/traveller_map/dist/index.js.map create mode 100644 mcp_servers/traveller_map/dist/parsers/sec.d.ts create mode 100644 mcp_servers/traveller_map/dist/parsers/sec.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/parsers/sec.js create mode 100644 mcp_servers/traveller_map/dist/parsers/sec.js.map create mode 100644 mcp_servers/traveller_map/dist/parsers/uwp.d.ts create mode 100644 mcp_servers/traveller_map/dist/parsers/uwp.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/parsers/uwp.js create mode 100644 mcp_servers/traveller_map/dist/parsers/uwp.js.map create mode 100644 mcp_servers/traveller_map/dist/server.d.ts create mode 100644 mcp_servers/traveller_map/dist/server.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/server.js create mode 100644 mcp_servers/traveller_map/dist/server.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/find_route.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/find_route.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/find_route.js create mode 100644 mcp_servers/traveller_map/dist/tools/find_route.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_allegiance_list.js create mode 100644 mcp_servers/traveller_map/dist/tools/get_allegiance_list.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_jump_map.js create mode 100644 mcp_servers/traveller_map/dist/tools/get_jump_map.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_data.js create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_data.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_list.js create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_list.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_metadata.js create mode 100644 mcp_servers/traveller_map/dist/tools/get_sector_metadata.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_subsector_map.js create mode 100644 mcp_servers/traveller_map/dist/tools/get_subsector_map.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_world_info.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/get_world_info.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_world_info.js create mode 100644 mcp_servers/traveller_map/dist/tools/get_world_info.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js create mode 100644 mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/render_custom_map.js create mode 100644 mcp_servers/traveller_map/dist/tools/render_custom_map.js.map create mode 100644 mcp_servers/traveller_map/dist/tools/search_worlds.d.ts create mode 100644 mcp_servers/traveller_map/dist/tools/search_worlds.d.ts.map create mode 100644 mcp_servers/traveller_map/dist/tools/search_worlds.js create mode 100644 mcp_servers/traveller_map/dist/tools/search_worlds.js.map create mode 100644 mcp_servers/traveller_map/package-lock.json create mode 100644 mcp_servers/traveller_map/package.json create mode 100644 mcp_servers/traveller_map/src/api/client.ts create mode 100644 mcp_servers/traveller_map/src/index.ts create mode 100644 mcp_servers/traveller_map/src/parsers/sec.ts create mode 100644 mcp_servers/traveller_map/src/parsers/uwp.ts create mode 100644 mcp_servers/traveller_map/src/server.ts create mode 100644 mcp_servers/traveller_map/src/tools/find_route.ts create mode 100644 mcp_servers/traveller_map/src/tools/get_allegiance_list.ts create mode 100644 mcp_servers/traveller_map/src/tools/get_jump_map.ts create mode 100644 mcp_servers/traveller_map/src/tools/get_sector_data.ts create mode 100644 mcp_servers/traveller_map/src/tools/get_sector_list.ts create mode 100644 mcp_servers/traveller_map/src/tools/get_sector_metadata.ts create mode 100644 mcp_servers/traveller_map/src/tools/get_subsector_map.ts create mode 100644 mcp_servers/traveller_map/src/tools/get_world_info.ts create mode 100644 mcp_servers/traveller_map/src/tools/get_worlds_in_jump_range.ts create mode 100644 mcp_servers/traveller_map/src/tools/render_custom_map.ts create mode 100644 mcp_servers/traveller_map/src/tools/search_worlds.ts create mode 100644 mcp_servers/traveller_map/tsconfig.json create mode 100644 profiles/docs/traveller_scout_ship.md create mode 100644 profiles/traveller_scout.yaml create mode 100644 scripts/list_voices.py create mode 100644 scripts/register_voice.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..282cf1b --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# Arioch — Assistant vocal (Mistral Large + Voxtral TTS) + +Assistant vocal CLI personnalisé utilisant **Mistral Large** pour les réponses et **Voxtral TTS** pour la synthèse vocale. + +## Prérequis + +- Python 3.10+ +- Une clé API Mistral : [console.mistral.ai](https://console.mistral.ai) +- Lecteur audio installé : + - **macOS** : `afplay` (inclus) + - **Linux** : `mpg123` → `sudo apt install mpg123` + +## Installation + +```bash +# Cloner et entrer dans le projet +cd arioch-assistant + +# Créer un environnement virtuel +python -m venv .venv +source .venv/bin/activate # Linux/macOS +# .venv\Scripts\activate # Windows + +# Installer les dépendances +pip install -r requirements.txt + +# Configurer l'environnement +cp .env.example .env +# Éditer .env et renseigner MISTRAL_API_KEY +``` + +## Utilisation + +```bash +python main.py +``` + +### Commandes disponibles + +| Commande | Description | +|----------|-------------| +| `exit` / `quit` | Quitter l'assistant | +| `reset` | Effacer l'historique de conversation | +| `voice ` | Changer la voix Voxtral (voice_id) | +| `voice clear` | Revenir à la voix par défaut | +| `help` | Afficher l'aide | + +## Phase 2 : Voix personnalisée (clonage) + +Enregistre ta propre voix (ou toute autre) avec un fichier audio de 2–3 secondes : + +```bash +python scripts/register_voice.py --name "Ma voix" --audio sample.mp3 --language fr +``` + +L'ID retourné peut être ajouté dans `.env` : +``` +VOICE_ID= +``` + +## Architecture + +``` +assistant/ +├── config.py # Variables d'environnement et constantes +├── llm.py # Chat streaming avec Mistral Large (tool-call loop) +├── tts.py # Synthèse vocale Voxtral TTS +├── audio.py # Lecture audio cross-platform +├── cli.py # Boucle REPL interactive + pipeline TTS streaming +├── mcp_client.py # Gestionnaire de serveurs MCP (tool calling) +└── profile.py # Chargement des profils de personnalité YAML +mcp_servers/ +└── traveller_map/ # Serveur MCP Traveller Map (sources TypeScript intégrées) + ├── src/ # Sources TypeScript + ├── dist/ # Build compilé (node dist/index.js) + └── package.json +profiles/ +└── traveller_scout.yaml # Profil avec mcp_servers configuré +scripts/ +└── register_voice.py # Enregistrement d'une voix clonée +main.py # Point d'entrée +``` + +## Serveurs MCP embarqués + +Les serveurs MCP sont dans `mcp_servers/`. Pour (re)compiler un serveur : + +```bash +cd mcp_servers/traveller_map +npm install # première fois seulement +npm run build +``` + +Les serveurs sont déclarés dans le profil YAML avec un chemin **relatif** à la racine du projet : + +```yaml +mcp_servers: + - name: traveller-map + command: node + args: ["mcp_servers/traveller_map/dist/index.js"] +``` diff --git a/assistant/__init__.py b/assistant/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/assistant/audio.py b/assistant/audio.py new file mode 100644 index 0000000..86494a8 --- /dev/null +++ b/assistant/audio.py @@ -0,0 +1,84 @@ +import sys +import subprocess +from . import config + + +def play_audio(pcm_bytes: bytes) -> None: + """ + Joue des bytes PCM bruts (S16LE, mono) via le lecteur système. + + - Linux : pipe direct vers aplay (aucun fichier temporaire) + - macOS : pipe vers afplay via stdin (format AIFF/raw) + - Windows: conversion via PowerShell (fallback) + """ + platform = sys.platform + + if platform.startswith("linux"): + _play_pcm_aplay(pcm_bytes) + elif platform == "darwin": + _play_pcm_macos(pcm_bytes) + elif platform == "win32": + _play_pcm_windows(pcm_bytes) + else: + raise RuntimeError(f"Plateforme non supportée : {platform}") + + +def _play_pcm_aplay(pcm_bytes: bytes) -> None: + """Pipe WAV directement vers aplay (auto-détecte le format depuis le header).""" + proc = subprocess.Popen( + ["aplay", "-q", "-"], + stdin=subprocess.PIPE, + ) + proc.communicate(pcm_bytes) + if proc.returncode != 0: + raise RuntimeError(f"aplay a échoué (code {proc.returncode})") + + +def _play_pcm_macos(pcm_bytes: bytes) -> None: + """Joue du WAV sur macOS via afplay (pipe stdin via fichier temporaire).""" + import tempfile, os + # afplay ne lit pas depuis stdin, on utilise un fichier temporaire + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: + tmp.write(pcm_bytes) + tmp_path = tmp.name + try: + subprocess.run(["afplay", tmp_path], check=True) + finally: + os.unlink(tmp_path) + + +def _play_pcm_windows(pcm_bytes: bytes) -> None: + import tempfile, os + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: + tmp.write(_pcm_to_wav(pcm_bytes)) + tmp_path = tmp.name + try: + subprocess.run( + ["powershell", "-c", f'(New-Object Media.SoundPlayer "{tmp_path}").PlaySync()'], + check=True, + ) + finally: + os.unlink(tmp_path) + + +def _pcm_to_wav(pcm_bytes: bytes) -> bytes: + """Ajoute un header WAV minimal au PCM brut S16LE mono.""" + import struct + sample_rate = config.TTS_PCM_SAMPLE_RATE + num_channels = 1 + bits_per_sample = 16 + byte_rate = sample_rate * num_channels * bits_per_sample // 8 + block_align = num_channels * bits_per_sample // 8 + data_size = len(pcm_bytes) + header = struct.pack( + "<4sI4s4sIHHIIHH4sI", + b"RIFF", 36 + data_size, b"WAVE", + b"fmt ", 16, 1, num_channels, + sample_rate, byte_rate, block_align, bits_per_sample, + b"data", data_size, + ) + return header + pcm_bytes + + +def _command_exists(cmd: str) -> bool: + return subprocess.run(["which", cmd], capture_output=True).returncode == 0 diff --git a/assistant/cli.py b/assistant/cli.py index 01ba68b..060e9eb 100644 --- a/assistant/cli.py +++ b/assistant/cli.py @@ -1,5 +1,8 @@ import asyncio +import queue as _queue +import re import sys +import threading from . import llm, tts, audio, config @@ -34,22 +37,122 @@ def _set_voice(parts: list[str]) -> None: print(f"Voix définie sur : {config.VOICE_ID}") +# --------------------------------------------------------------------------- +# Sentence splitting & markdown cleaning for TTS pipeline +# --------------------------------------------------------------------------- + +# Split on sentence boundaries using fixed-width lookbehinds (Python constraint). +# For ".": 3-char lookbehind — require the 2 chars before the period to be +# lowercase, digit, hyphen, or closing bracket (NOT uppercase). This protects +# "M.", "No.", "A." while correctly splitting "arrivé.", "Jump-2.", "(J2).", "ok." +_SENT_RE = re.compile( + r'(?<=[a-z\u00e0-\u00ff\d\-)\]]{2}[.])\s+' # word(2+) + period + r'|(?<=[a-z\u00e0-\u00ff\d\-)\]]{2}[.][»"\')\]])\s+' # + closing quote + r'|(?<=[!?…])\s+' # ! ? … + r'|(?<=[!?…][»"\')\]])\s+' # ! ? … + closing quote + r'|\n{2,}' # paragraph break +) + +# Markdown patterns to strip before TTS (keep inner text where applicable) +_MD_CODE_BLOCK = re.compile(r'```.*?```', re.DOTALL) +_MD_INLINE = re.compile(r'\*{1,3}(.*?)\*{1,3}|_{1,2}(.*?)_{1,2}|~~(.*?)~~|`([^`]+)`', re.DOTALL) +_MD_LINK = re.compile(r'\[([^\]]*)\]\([^\)]*\)') +_MD_HEADER = re.compile(r'^#{1,6}\s+', re.MULTILINE) +_MULTI_SPACE = re.compile(r'\s+') + + +def _split_sentences(text: str) -> tuple[list[str], str]: + """Extrait les phrases complètes d'un buffer partiel. + + Returns (complete_sentences, remainder). + """ + parts = _SENT_RE.split(text) + if len(parts) <= 1: + return [], text + return parts[:-1], parts[-1] + + +def _clean_for_tts(text: str) -> str: + """Supprime le formatage Markdown avant la synthèse vocale.""" + text = _MD_CODE_BLOCK.sub(' ', text) + text = _MD_INLINE.sub(lambda m: next(g for g in m.groups() if g is not None), text) + text = _MD_LINK.sub(r'\1', text) + text = _MD_HEADER.sub('', text) + text = text.replace('→', ' vers ').replace('←', ' depuis ') + text = _MULTI_SPACE.sub(' ', text).strip() + return text + + +# --------------------------------------------------------------------------- +# Message processing — streaming TTS pipeline +# --------------------------------------------------------------------------- + def _process_message(user_input: str) -> None: - """Envoie un message au LLM et lit la réponse à voix haute.""" - print(f"Arioch > ", end="", flush=True) - try: - reply = llm.chat(user_input) - except Exception as e: - print(f"\n[Erreur LLM] {e}") - return + """Envoie un message au LLM et lit la réponse à voix haute. - print(reply) + Pipeline 3 étages en parallèle : + [LLM stream] → sentence_queue → [TTS thread] → audio_queue → [Audio thread] + Dès qu'une phrase complète est détectée dans le stream LLM, elle part + immédiatement en synthèse Voxtral. L'audio est joué dès qu'il est prêt, + pendant que la phrase suivante est déjà en cours de synthèse. + """ + SENTINEL = object() + sentence_q: _queue.Queue = _queue.Queue() + audio_q: _queue.Queue = _queue.Queue() + + def tts_worker() -> None: + while True: + item = sentence_q.get() + if item is SENTINEL: + audio_q.put(SENTINEL) + return + text = _clean_for_tts(item) + if text: + try: + audio_bytes = tts.text_to_speech(text) + audio_q.put(audio_bytes) + except Exception as e: + print(f"\n[Erreur TTS] {e}", flush=True) + + def audio_worker() -> None: + while True: + item = audio_q.get() + if item is SENTINEL: + return + try: + audio.play_audio(item) + except Exception as e: + print(f"\n[Erreur Audio] {e}", flush=True) + + tts_thread = threading.Thread(target=tts_worker, daemon=True, name="tts-worker") + audio_thread = threading.Thread(target=audio_worker, daemon=True, name="audio-worker") + tts_thread.start() + audio_thread.start() + + print("Arioch > ", end="", flush=True) + buffer = "" try: - audio_bytes = tts.text_to_speech(reply) - audio.play_audio(audio_bytes) + for chunk in llm.chat_stream(user_input): + print(chunk, end="", flush=True) + buffer += chunk + sentences, buffer = _split_sentences(buffer) + for sentence in sentences: + sentence = sentence.strip() + if sentence: + sentence_q.put(sentence) except Exception as e: - print(f"[Erreur TTS/Audio] {e}") + print(f"\n[Erreur LLM] {e}", flush=True) + + # Flush any remaining text after the stream ends + if buffer.strip(): + sentence_q.put(buffer.strip()) + + print() # newline after full response + sentence_q.put(SENTINEL) + + tts_thread.join() + audio_thread.join() def _handle_command(user_input: str) -> bool: @@ -118,7 +221,7 @@ def _handle_mcp(parts: list[str]) -> None: print(f"\nTotal : {total} outil(s). Tapez 'mcp tools' pour les lister.\n") - +def _list_profiles(profiles: list) -> None: if not profiles: print("Aucun profil disponible dans profiles/") return diff --git a/assistant/config.py b/assistant/config.py new file mode 100644 index 0000000..1e08f81 --- /dev/null +++ b/assistant/config.py @@ -0,0 +1,20 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +MISTRAL_API_KEY: str = os.environ["MISTRAL_API_KEY"] + +LLM_MODEL: str = "mistral-large-latest" +TTS_MODEL: str = "voxtral-mini-tts-2603" + +VOICE_ID: str | None = os.getenv("VOICE_ID") or None + +SYSTEM_PROMPT: str = os.getenv( + "SYSTEM_PROMPT", + "Tu es Arioch, un assistant vocal intelligent, concis et sympathique. " + "Réponds toujours en français sauf si l'utilisateur parle une autre langue.", +) + +VOICE_LANGUAGE: str = os.getenv("VOICE_LANGUAGE", "fr") +TTS_PCM_SAMPLE_RATE: int = int(os.getenv("TTS_PCM_SAMPLE_RATE", "24000")) diff --git a/assistant/llm.py b/assistant/llm.py index 1bb3b9f..0cd6228 100644 --- a/assistant/llm.py +++ b/assistant/llm.py @@ -1,4 +1,5 @@ import json +from typing import Generator from mistralai.client import Mistral from . import config @@ -13,8 +14,12 @@ def reset_history() -> None: _history.clear() -def chat(user_message: str) -> str: - """Envoie un message au LLM, gère les appels d'outils MCP et retourne la réponse.""" +def chat_stream(user_message: str) -> Generator[str, None, None]: + """Génère la réponse du LLM token par token via streaming. + + Les appels d'outils MCP sont exécutés internement (sans streaming). + Seule la réponse textuelle finale est streamée sous forme de chunks. + """ from . import mcp_client _history.append({"role": "user", "content": user_message}) @@ -24,20 +29,30 @@ def chat(user_message: str) -> str: while True: messages = [{"role": "system", "content": config.SYSTEM_PROMPT}] + _history - kwargs: dict = {"model": config.LLM_MODEL, "messages": messages} if tools: kwargs["tools"] = tools - response = _client.chat.complete(**kwargs) - choice = response.choices[0] - msg = choice.message + accumulated_content = "" + tool_calls_received = None - if msg.tool_calls: - # 1. Ajouter le message assistant (avec les appels d'outils) à l'historique + for event in _client.chat.stream(**kwargs): + ch = event.data.choices[0] + delta = ch.delta + + # Yield text chunks (isinstance check guards against Unset sentinel) + if isinstance(delta.content, str) and delta.content: + accumulated_content += delta.content + yield delta.content + + if delta.tool_calls: + tool_calls_received = delta.tool_calls + + if tool_calls_received: + # Append assistant turn with tool calls to history _history.append({ "role": "assistant", - "content": msg.content or "", + "content": accumulated_content or "", "tool_calls": [ { "id": tc.id, @@ -47,12 +62,12 @@ def chat(user_message: str) -> str: "arguments": tc.function.arguments, }, } - for tc in msg.tool_calls + for tc in tool_calls_received ], }) - # 2. Exécuter chaque outil et ajouter les résultats - for tc in msg.tool_calls: + # Execute each tool and append results + for tc in tool_calls_received: tool_name = tc.function.name try: args = ( @@ -60,7 +75,7 @@ def chat(user_message: str) -> str: if isinstance(tc.function.arguments, str) else tc.function.arguments ) - print(f" 🔧 [MCP] {tool_name}({_short_args(args)})") + print(f"\n 🔧 [MCP] {tool_name}({_short_args(args)})", flush=True) result = manager.call_tool(tool_name, args) except Exception as e: result = f"Erreur lors de l'appel à {tool_name} : {e}" @@ -70,13 +85,17 @@ def chat(user_message: str) -> str: "content": result, "tool_call_id": tc.id, }) - - # 3. Reboucler pour obtenir la réponse finale + # Loop to get the next (final) response else: - reply = msg.content or "" - _history.append({"role": "assistant", "content": reply}) - return reply + # Pure text response — already yielded chunk by chunk; save to history + _history.append({"role": "assistant", "content": accumulated_content}) + break + + +def chat(user_message: str) -> str: + """Envoie un message au LLM et retourne la réponse complète (non-streaming).""" + return "".join(chat_stream(user_message)) def _short_args(args: dict) -> str: diff --git a/assistant/mcp_client.py b/assistant/mcp_client.py index 617c61c..2728bb0 100644 --- a/assistant/mcp_client.py +++ b/assistant/mcp_client.py @@ -12,12 +12,16 @@ Configure les serveurs dans un profil YAML sous la clé `mcp_servers` : from __future__ import annotations import asyncio +import os import re import threading -from contextlib import AsyncExitStack from dataclasses import dataclass, field +from pathlib import Path from typing import Any +# Racine du projet (le dossier qui contient main.py) +_PROJECT_ROOT = Path(__file__).parent.parent.resolve() + @dataclass class MCPServerConfig: @@ -27,6 +31,18 @@ class MCPServerConfig: env: dict[str, str] | None = None url: str | None = None + def resolved_args(self) -> list[str]: + """Résout les chemins relatifs dans args par rapport à la racine du projet.""" + result = [] + for arg in self.args: + p = Path(arg) + if not p.is_absolute() and p.suffix in (".js", ".py", ".ts"): + resolved = (_PROJECT_ROOT / p).resolve() + result.append(str(resolved)) + else: + result.append(arg) + return result + def _sanitize_name(name: str) -> str: """Transforme un nom en identifiant valide pour l'API Mistral (^[a-zA-Z0-9_-]{1,64}$).""" @@ -34,7 +50,12 @@ def _sanitize_name(name: str) -> str: class MCPManager: - """Gère les connexions aux serveurs MCP et l'exécution des outils.""" + """Gère les connexions aux serveurs MCP et l'exécution des outils. + + Chaque connexion tourne dans une "keeper task" qui possède toute la durée + de vie du context manager stdio_client / ClientSession. Cela évite l'erreur + anyio "Attempted to exit cancel scope in a different task". + """ def __init__(self) -> None: self._loop = asyncio.new_event_loop() @@ -42,9 +63,9 @@ class MCPManager: self._thread.start() self._sessions: dict[str, Any] = {} self._raw_tools: dict[str, list] = {} - # mistral_name -> (server_name, original_tool_name) self._tool_map: dict[str, tuple[str, str]] = {} - self._exit_stacks: dict[str, AsyncExitStack] = {} + # stop event per server, signalled at shutdown + self._stop_events: dict[str, asyncio.Event] = {} def _run_loop(self) -> None: asyncio.set_event_loop(self._loop) @@ -87,10 +108,18 @@ class MCPManager: return self._run(self._call_tool_async(server_name, tool_name, arguments)) def shutdown(self) -> None: + """Signale l'arrêt à toutes les keeper tasks et attend brièvement.""" + async def _signal_all() -> None: + for ev in self._stop_events.values(): + ev.set() + # Laisser une courte fenêtre pour que les tâches se terminent proprement + await asyncio.sleep(0.3) + try: - self._run(self._shutdown_async(), timeout=10) + self._run(_signal_all(), timeout=5) except Exception: pass + self._stop_events.clear() def summary(self) -> list[tuple[str, int]]: """Retourne [(server_name, tool_count), ...] pour les serveurs connectés.""" @@ -112,38 +141,78 @@ class MCPManager: print(f"[MCP] ❌ Connexion {cfg.name} impossible : {e}") async def _connect_server(self, cfg: MCPServerConfig) -> None: - from mcp import ClientSession, StdioServerParameters - from mcp.client.stdio import stdio_client + """Lance la keeper task et attend que la connexion soit établie.""" + stop_event = asyncio.Event() + ready_event = asyncio.Event() + error_holder: list[Exception] = [] - stack = AsyncExitStack() + self._stop_events[cfg.name] = stop_event - if cfg.command: - params = StdioServerParameters( - command=cfg.command, - args=cfg.args or [], - env=cfg.env, - ) - read, write = await stack.enter_async_context(stdio_client(params)) - else: - from mcp.client.sse import sse_client - read, write = await stack.enter_async_context(sse_client(cfg.url)) + asyncio.create_task( + self._run_server(cfg, ready_event, stop_event, error_holder), + name=f"mcp-keeper-{cfg.name}", + ) - session = await stack.enter_async_context(ClientSession(read, write)) - await session.initialize() + # Attendre que la connexion soit prête (ou échoue) + try: + await asyncio.wait_for(ready_event.wait(), timeout=30) + except asyncio.TimeoutError: + stop_event.set() + raise TimeoutError(f"Timeout lors de la connexion à {cfg.name}") - self._sessions[cfg.name] = session - self._exit_stacks[cfg.name] = stack + if error_holder: + raise error_holder[0] - tools_resp = await session.list_tools() - self._raw_tools[cfg.name] = tools_resp.tools + async def _run_server( + self, + cfg: MCPServerConfig, + ready_event: asyncio.Event, + stop_event: asyncio.Event, + error_holder: list[Exception], + ) -> None: + """Keeper task : possède le context manager de bout en bout.""" + try: + from mcp import ClientSession, StdioServerParameters - server_safe = _sanitize_name(cfg.name) - for tool in tools_resp.tools: - tool_safe = _sanitize_name(tool.name) - mistral_name = f"{server_safe}__{tool_safe}" - self._tool_map[mistral_name] = (cfg.name, tool.name) + if cfg.command: + from mcp.client.stdio import stdio_client + transport = stdio_client(StdioServerParameters( + command=cfg.command, + args=cfg.resolved_args(), + env=cfg.env, + )) + else: + from mcp.client.sse import sse_client + transport = sse_client(cfg.url) - print(f"[MCP] ✅ {cfg.name} — {len(tools_resp.tools)} outil(s) disponible(s)") + async with transport as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + + tools_resp = await session.list_tools() + self._sessions[cfg.name] = session + self._raw_tools[cfg.name] = tools_resp.tools + + server_safe = _sanitize_name(cfg.name) + for tool in tools_resp.tools: + tool_safe = _sanitize_name(tool.name) + self._tool_map[f"{server_safe}__{tool_safe}"] = (cfg.name, tool.name) + + print(f"[MCP] ✅ {cfg.name} — {len(tools_resp.tools)} outil(s) disponible(s)") + ready_event.set() + + # Maintenir la connexion jusqu'au signal d'arrêt + await stop_event.wait() + + except Exception as e: + error_holder.append(e) + ready_event.set() # débloquer _connect_server même en cas d'erreur + finally: + self._sessions.pop(cfg.name, None) + self._raw_tools.pop(cfg.name, None) + to_remove = [k for k, v in self._tool_map.items() if v[0] == cfg.name] + for k in to_remove: + del self._tool_map[k] async def _call_tool_async(self, server_name: str, tool_name: str, arguments: dict) -> str: session = self._sessions[server_name] @@ -156,17 +225,6 @@ class MCPManager: parts.append(str(item)) return "\n".join(parts) if parts else "(aucun résultat)" - async def _shutdown_async(self) -> None: - for stack in list(self._exit_stacks.values()): - try: - await stack.aclose() - except Exception: - pass - self._sessions.clear() - self._raw_tools.clear() - self._tool_map.clear() - self._exit_stacks.clear() - _manager: MCPManager | None = None _lock = threading.Lock() @@ -185,4 +243,6 @@ def reset_manager() -> None: with _lock: if _manager is not None: _manager.shutdown() - _manager = MCPManager() + _manager = MCPManager() + + diff --git a/assistant/stt.py b/assistant/stt.py new file mode 100644 index 0000000..557c122 --- /dev/null +++ b/assistant/stt.py @@ -0,0 +1,109 @@ +""" +Transcription vocale temps réel via Voxtral Mini Transcribe Realtime. + +Flux : microphone (PCM 16kHz) → WebSocket Voxtral → texte transcrit +""" +import asyncio +import sys +from typing import AsyncIterator + +import numpy as np +import sounddevice as sd +from mistralai.client import Mistral +from mistralai.client.models import ( + AudioFormat, + TranscriptionStreamDone, + TranscriptionStreamTextDelta, +) + +from . import config + +STT_MODEL = "voxtral-mini-transcribe-realtime-2602" +SAMPLE_RATE = 16000 +CHANNELS = 1 +CHUNK_FRAMES = 1600 # 100ms de son par chunk + + +async def _mic_stream(stop_event: asyncio.Event) -> AsyncIterator[bytes]: + """Capture le microphone et yield des chunks PCM int16 jusqu'à stop_event.""" + loop = asyncio.get_event_loop() + queue: asyncio.Queue[bytes | None] = asyncio.Queue() + + def callback(indata: np.ndarray, frames: int, time, status) -> None: + if status: + print(f"[Mic] {status}", file=sys.stderr) + # Convertir en int16 little-endian et envoyer + pcm = (indata[:, 0] * 32767).astype(np.int16).tobytes() + loop.call_soon_threadsafe(queue.put_nowait, pcm) + + stream = sd.InputStream( + samplerate=SAMPLE_RATE, + channels=CHANNELS, + dtype="float32", + blocksize=CHUNK_FRAMES, + callback=callback, + ) + + with stream: + while not stop_event.is_set(): + try: + chunk = await asyncio.wait_for(queue.get(), timeout=0.2) + yield chunk + except asyncio.TimeoutError: + continue + + # Vider la queue restante + while not queue.empty(): + chunk = queue.get_nowait() + if chunk: + yield chunk + + +async def transcribe_from_mic() -> str: + """ + Écoute le microphone jusqu'à ce que l'utilisateur appuie sur Entrée, + puis retourne le texte transcrit. + """ + client = Mistral(api_key=config.MISTRAL_API_KEY) + stop_event = asyncio.Event() + loop = asyncio.get_event_loop() + + print("🎤 Parlez... (Entrée pour arrêter)") + + # Attendre Entrée dans un thread pour ne pas bloquer l'event loop + async def wait_for_enter() -> None: + await loop.run_in_executor(None, input) + stop_event.set() + + enter_task = asyncio.create_task(wait_for_enter()) + + audio_fmt = AudioFormat( + encoding="pcm_s16le", + sample_rate=SAMPLE_RATE, + ) + + final_text = "" + + try: + async for event in client.audio.realtime.transcribe_stream( + audio_stream=_mic_stream(stop_event), + model=STT_MODEL, + audio_format=audio_fmt, + target_streaming_delay_ms=300, + ): + if isinstance(event, TranscriptionStreamTextDelta): + # Affichage en temps réel du texte partiel + print(event.text, end="", flush=True) + elif isinstance(event, TranscriptionStreamDone): + final_text = event.text + print() # saut de ligne après la transcription + break + finally: + stop_event.set() + enter_task.cancel() + try: + await enter_task + except asyncio.CancelledError: + pass + + return final_text.strip() diff --git a/assistant/tts.py b/assistant/tts.py new file mode 100644 index 0000000..83e10d7 --- /dev/null +++ b/assistant/tts.py @@ -0,0 +1,54 @@ +import base64 +from mistralai.client import Mistral +from . import config + +_client = Mistral(api_key=config.MISTRAL_API_KEY) + +_default_voice_id: str | None = None + + +def _get_default_voice_id() -> str: + """ + Récupère et met en cache l'ID d'une voix preset. + Priorise les voix supportant la langue configurée (VOICE_LANGUAGE). + """ + global _default_voice_id + if _default_voice_id is not None: + return _default_voice_id + + voices = _client.audio.voices.list(type_="preset", limit=50) + if not voices.items: + raise RuntimeError( + "Aucune voix disponible. Configurez VOICE_ID dans .env ou créez une voix " + "avec scripts/register_voice.py" + ) + + # Cherche une voix supportant la langue configurée + preferred_lang = config.VOICE_LANGUAGE + matching = [ + v for v in voices.items + if v.languages and preferred_lang in v.languages + ] + chosen = matching[0] if matching else voices.items[0] + + _default_voice_id = chosen.id + print(f"[TTS] Voix sélectionnée : {chosen.name} (langues: {chosen.languages}) — id: {_default_voice_id}") + return _default_voice_id + + +def text_to_speech(text: str, voice_id: str | None = None) -> bytes: + """ + Convertit du texte en audio WAV via Voxtral TTS. + + WAV est lu nativement par aplay (Linux) et afplay (macOS) sans conversion. + """ + effective_voice_id = voice_id or config.VOICE_ID or _get_default_voice_id() + + response = _client.audio.speech.complete( + model=config.TTS_MODEL, + input=text, + voice_id=effective_voice_id, + response_format="wav", + ) + + return base64.b64decode(response.audio_data) diff --git a/main.py b/main.py new file mode 100644 index 0000000..a14ca09 --- /dev/null +++ b/main.py @@ -0,0 +1,4 @@ +from assistant.cli import run + +if __name__ == "__main__": + run() diff --git a/mcp_servers/traveller_map/dist/api/client.d.ts b/mcp_servers/traveller_map/dist/api/client.d.ts new file mode 100644 index 0000000..62d3ee0 --- /dev/null +++ b/mcp_servers/traveller_map/dist/api/client.d.ts @@ -0,0 +1,11 @@ +export type QueryParams = Record; +export declare function apiGet(path: string, params?: QueryParams): Promise; +export declare function apiGetImage(path: string, params?: QueryParams): Promise; +export declare function apiGetJson(path: string, params?: QueryParams): Promise; +export declare function apiGetText(path: string, params?: QueryParams): Promise; +export declare function apiGetDataUri(path: string, params?: QueryParams): Promise<{ + base64: string; + mimeType: string; +}>; +export declare function apiPostImage(path: string, queryParams: QueryParams, formBody: Record): Promise; +//# sourceMappingURL=client.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/api/client.d.ts.map b/mcp_servers/traveller_map/dist/api/client.d.ts.map new file mode 100644 index 0000000..5640d2a --- /dev/null +++ b/mcp_servers/traveller_map/dist/api/client.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;AAYhF,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAUtF;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAczF;AAED,wBAAsB,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,OAAO,CAAC,CAAC,CAAC,CAGtF;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAGxF;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,WAAgB,GACvB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAU/C;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,MAAM,CAAC,CAkBjB"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/api/client.js b/mcp_servers/traveller_map/dist/api/client.js new file mode 100644 index 0000000..22e63ca --- /dev/null +++ b/mcp_servers/traveller_map/dist/api/client.js @@ -0,0 +1,76 @@ +const BASE_URL = 'https://travellermap.com'; +const USER_AGENT = 'traveller-map-mcp/1.0 (github.com/shammond42/traveller-map-mcp)'; +function buildUrl(path, params) { + const url = new URL(path, BASE_URL); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + url.searchParams.set(key, String(value)); + } + } + return url; +} +export async function apiGet(path, params = {}) { + const url = buildUrl(path, params); + const response = await fetch(url.toString(), { + headers: { 'User-Agent': USER_AGENT }, + }); + if (!response.ok) { + const body = await response.text(); + throw new Error(`Traveller Map API error ${response.status} at ${path}: ${body}`); + } + return response; +} +export async function apiGetImage(path, params = {}) { + const url = buildUrl(path, params); + const response = await fetch(url.toString(), { + headers: { + 'User-Agent': USER_AGENT, + 'Accept': 'image/png', + }, + }); + if (!response.ok) { + const body = await response.text(); + throw new Error(`Traveller Map API error ${response.status} at ${path}: ${body}`); + } + const buffer = await response.arrayBuffer(); + return Buffer.from(buffer).toString('base64'); +} +export async function apiGetJson(path, params = {}) { + const response = await apiGet(path, { ...params, accept: 'application/json' }); + return response.json(); +} +export async function apiGetText(path, params = {}) { + const response = await apiGet(path, params); + return response.text(); +} +export async function apiGetDataUri(path, params = {}) { + const response = await apiGet(path, { ...params, datauri: 1 }); + const text = await response.text(); + for (const mimeType of ['image/png', 'image/jpeg']) { + const prefix = `data:${mimeType};base64,`; + if (text.startsWith(prefix)) { + return { base64: text.slice(prefix.length), mimeType }; + } + } + throw new Error(`Unexpected data URI format from ${path}: ${text.slice(0, 50)}`); +} +export async function apiPostImage(path, queryParams, formBody) { + const url = buildUrl(path, queryParams); + const body = new URLSearchParams(formBody).toString(); + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': USER_AGENT, + 'Accept': 'image/png', + }, + body, + }); + if (!response.ok) { + const responseBody = await response.text(); + throw new Error(`Traveller Map API error ${response.status} at POST ${path}: ${responseBody}`); + } + const buffer = await response.arrayBuffer(); + return Buffer.from(buffer).toString('base64'); +} +//# sourceMappingURL=client.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/api/client.js.map b/mcp_servers/traveller_map/dist/api/client.js.map new file mode 100644 index 0000000..4cd3d66 --- /dev/null +++ b/mcp_servers/traveller_map/dist/api/client.js.map @@ -0,0 +1 @@ +{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/index.d.ts b/mcp_servers/traveller_map/dist/index.d.ts new file mode 100644 index 0000000..e26a57a --- /dev/null +++ b/mcp_servers/traveller_map/dist/index.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/index.d.ts.map b/mcp_servers/traveller_map/dist/index.d.ts.map new file mode 100644 index 0000000..535b86d --- /dev/null +++ b/mcp_servers/traveller_map/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/index.js b/mcp_servers/traveller_map/dist/index.js new file mode 100644 index 0000000..ae64b57 --- /dev/null +++ b/mcp_servers/traveller_map/dist/index.js @@ -0,0 +1,12 @@ +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { createServer } from './server.js'; +async function main() { + const server = createServer(); + const transport = new StdioServerTransport(); + await server.connect(transport); +} +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/index.js.map b/mcp_servers/traveller_map/dist/index.js.map new file mode 100644 index 0000000..653cd18 --- /dev/null +++ b/mcp_servers/traveller_map/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/parsers/sec.d.ts b/mcp_servers/traveller_map/dist/parsers/sec.d.ts new file mode 100644 index 0000000..7e84b6e --- /dev/null +++ b/mcp_servers/traveller_map/dist/parsers/sec.d.ts @@ -0,0 +1,22 @@ +import { type DecodedUWP } from './uwp.js'; +export interface WorldRecord { + hex: string; + name: string; + uwp: string; + decoded_uwp: DecodedUWP; + bases: string; + remarks: string; + trade_codes: string[]; + zone: string; + pbg: string; + allegiance: string; + stars: string; + importance?: string; + economic?: string; + cultural?: string; + nobility?: string; + worlds?: string; + resource_units?: string; +} +export declare function parseSecTabDelimited(text: string): WorldRecord[]; +//# sourceMappingURL=sec.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/parsers/sec.d.ts.map b/mcp_servers/traveller_map/dist/parsers/sec.d.ts.map new file mode 100644 index 0000000..c2dc95e --- /dev/null +++ b/mcp_servers/traveller_map/dist/parsers/sec.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"sec.d.ts","sourceRoot":"","sources":["../../src/parsers/sec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,UAAU,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAuBD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CA0FhE"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/parsers/sec.js b/mcp_servers/traveller_map/dist/parsers/sec.js new file mode 100644 index 0000000..7d95070 --- /dev/null +++ b/mcp_servers/traveller_map/dist/parsers/sec.js @@ -0,0 +1,106 @@ +import { parseUWP } from './uwp.js'; +const ZONE_MAP = { + R: 'Red', + A: 'Amber', + '': 'Green', + '-': 'Green', + ' ': 'Green', + G: 'Green', +}; +function normalizeZone(zone) { + return ZONE_MAP[zone?.trim() ?? ''] ?? zone?.trim() ?? 'Green'; +} +function parseRemarks(remarks) { + if (!remarks) + return []; + return remarks + .trim() + .split(/\s+/) + .filter((r) => r.length > 0 && r !== '-'); +} +export function parseSecTabDelimited(text) { + const lines = text.split('\n'); + const worlds = []; + let headerLine = ''; + let headerIndex = -1; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('#') || line.trim() === '') + continue; + if (/^[-\s]+$/.test(line)) + continue; + if (line.toLowerCase().includes('hex') || line.toLowerCase().includes('name')) { + headerLine = line; + headerIndex = i; + break; + } + } + if (!headerLine) { + return worlds; + } + const headers = headerLine.split('\t').map((h) => h.trim().toLowerCase()); + const colIndex = (names) => { + for (const name of names) { + const idx = headers.indexOf(name); + if (idx !== -1) + return idx; + } + return -1; + }; + const hexIdx = colIndex(['hex']); + const nameIdx = colIndex(['name', 'world name']); + const uwpIdx = colIndex(['uwp']); + const basesIdx = colIndex(['bases', 'base']); + const remarksIdx = colIndex(['remarks', 'trade codes', 'tradecodes']); + const zoneIdx = colIndex(['zone', 'iz', 'travel zone']); + const pbgIdx = colIndex(['pbg']); + const allegIdx = colIndex(['allegiance', 'alleg', 'a']); + const starsIdx = colIndex(['stars', 'stellar data', 'stellar']); + const importanceIdx = colIndex(['{ix}', 'ix', 'importance', '{importance}']); + const economicIdx = colIndex(['(ex)', 'ex', 'economic', '(economic)']); + const culturalIdx = colIndex(['[cx]', 'cx', 'cultural', '[cultural]']); + const nobilityIdx = colIndex(['nobility', 'nobil', 'n']); + const worldsIdx = colIndex(['w', 'worlds']); + const ruIdx = colIndex(['ru', 'resource units']); + for (let i = headerIndex + 1; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('#') || line.trim() === '') + continue; + if (/^[-\s]+$/.test(line)) + continue; + const cols = line.split('\t'); + if (cols.length < 3) + continue; + const get = (idx) => (idx >= 0 ? cols[idx]?.trim() ?? '' : ''); + const uwpRaw = get(uwpIdx); + let decodedUwp; + try { + decodedUwp = parseUWP(uwpRaw); + } + catch { + decodedUwp = parseUWP('?000000-0'); + } + const remarksRaw = get(remarksIdx); + worlds.push({ + hex: get(hexIdx), + name: get(nameIdx), + uwp: uwpRaw, + decoded_uwp: decodedUwp, + bases: get(basesIdx), + remarks: remarksRaw, + trade_codes: parseRemarks(remarksRaw), + zone: normalizeZone(get(zoneIdx)), + pbg: get(pbgIdx), + allegiance: get(allegIdx), + stars: get(starsIdx), + importance: importanceIdx >= 0 ? get(importanceIdx) : undefined, + economic: economicIdx >= 0 ? get(economicIdx) : undefined, + cultural: culturalIdx >= 0 ? get(culturalIdx) : undefined, + nobility: nobilityIdx >= 0 ? get(nobilityIdx) : undefined, + worlds: worldsIdx >= 0 ? get(worldsIdx) : undefined, + resource_units: ruIdx >= 0 ? get(ruIdx) : undefined, + }); + } + return worlds; +} +//# sourceMappingURL=sec.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/parsers/sec.js.map b/mcp_servers/traveller_map/dist/parsers/sec.js.map new file mode 100644 index 0000000..85cf646 --- /dev/null +++ b/mcp_servers/traveller_map/dist/parsers/sec.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sec.js","sourceRoot":"","sources":["../../src/parsers/sec.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/parsers/uwp.d.ts b/mcp_servers/traveller_map/dist/parsers/uwp.d.ts new file mode 100644 index 0000000..8690d4d --- /dev/null +++ b/mcp_servers/traveller_map/dist/parsers/uwp.d.ts @@ -0,0 +1,30 @@ +export interface DecodedUWPField { + code: string; + value: number; + description: string; +} +export interface DecodedUWP { + raw: string; + starport: { + code: string; + description: string; + }; + size: DecodedUWPField & { + diameter_km: string; + }; + atmosphere: DecodedUWPField; + hydrographics: DecodedUWPField & { + percent: string; + }; + population: DecodedUWPField & { + estimate: string; + }; + government: DecodedUWPField; + law_level: DecodedUWPField; + tech_level: DecodedUWPField & { + era: string; + }; +} +export declare function eHexValue(char: string): number; +export declare function parseUWP(uwp: string): DecodedUWP; +//# sourceMappingURL=uwp.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/parsers/uwp.d.ts.map b/mcp_servers/traveller_map/dist/parsers/uwp.d.ts.map new file mode 100644 index 0000000..54fe221 --- /dev/null +++ b/mcp_servers/traveller_map/dist/parsers/uwp.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"uwp.d.ts","sourceRoot":"","sources":["../../src/parsers/uwp.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,IAAI,EAAE,eAAe,GAAG;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,UAAU,EAAE,eAAe,CAAC;IAC5B,aAAa,EAAE,eAAe,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,UAAU,EAAE,eAAe,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,UAAU,EAAE,eAAe,CAAC;IAC5B,SAAS,EAAE,eAAe,CAAC;IAC3B,UAAU,EAAE,eAAe,GAAG;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C;AAyID,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAsEhD"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/parsers/uwp.js b/mcp_servers/traveller_map/dist/parsers/uwp.js new file mode 100644 index 0000000..15ba43e --- /dev/null +++ b/mcp_servers/traveller_map/dist/parsers/uwp.js @@ -0,0 +1,203 @@ +export function eHexValue(char) { + const c = char.toUpperCase(); + if (c >= '0' && c <= '9') + return parseInt(c, 10); + if (c >= 'A' && c <= 'Z') + return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10; + return 0; +} +const STARPORT = { + A: 'Excellent — full repair, refined fuel, shipyard capable', + B: 'Good — full repair, refined fuel available', + C: 'Routine — some repair, unrefined fuel available', + D: 'Poor — limited repair, unrefined fuel only', + E: 'Frontier — no repair, no fuel', + X: 'None — no starport facilities', + F: 'Good — spaceport (non-starship capable)', + G: 'Poor — primitive spaceport', + H: 'Primitive — minimal facilities', + Y: 'None', +}; +const SIZE_DESC = { + 0: { description: 'Asteroid/planetoid belt or very small body', diameter_km: '<800 km' }, + 1: { description: 'Small world', diameter_km: '~1,600 km' }, + 2: { description: 'Small world', diameter_km: '~3,200 km' }, + 3: { description: 'Small world', diameter_km: '~4,800 km' }, + 4: { description: 'Small world', diameter_km: '~6,400 km' }, + 5: { description: 'Medium world', diameter_km: '~8,000 km' }, + 6: { description: 'Medium world (Earth-like)', diameter_km: '~9,600 km' }, + 7: { description: 'Medium world', diameter_km: '~11,200 km' }, + 8: { description: 'Large world', diameter_km: '~12,800 km' }, + 9: { description: 'Large world', diameter_km: '~14,400 km' }, + 10: { description: 'Large world', diameter_km: '~16,000 km' }, +}; +const ATMOSPHERE_DESC = { + 0: 'None — vacuum', + 1: 'Trace — very thin, requires vacc suit', + 2: 'Very Thin, Tainted — requires filter mask and compressor', + 3: 'Very Thin — requires compressor', + 4: 'Thin, Tainted — requires filter mask', + 5: 'Thin — breathable with some discomfort', + 6: 'Standard — breathable', + 7: 'Standard, Tainted — requires filter mask', + 8: 'Dense — breathable with no special equipment', + 9: 'Dense, Tainted — requires filter mask', + 10: 'Exotic — requires oxygen supply', + 11: 'Corrosive — requires vacc suit', + 12: 'Insidious — suit penetrating, requires special protection', + 13: 'Dense, High — breathable only at high altitudes', + 14: 'Thin, Low — breathable only in lowlands', + 15: 'Unusual', +}; +const HYDROGRAPHICS_DESC = { + 0: { description: 'Desert world — no free water', percent: '0%' }, + 1: { description: 'Dry world — traces of water', percent: '1–10%' }, + 2: { description: 'Dry world', percent: '11–20%' }, + 3: { description: 'Dry world', percent: '21–30%' }, + 4: { description: 'Wet world', percent: '31–40%' }, + 5: { description: 'Wet world', percent: '41–50%' }, + 6: { description: 'Wet world', percent: '51–60%' }, + 7: { description: 'Wet world — significant oceans', percent: '61–70%' }, + 8: { description: 'Water world — large oceans', percent: '71–80%' }, + 9: { description: 'Water world — very large oceans', percent: '81–90%' }, + 10: { description: 'Water world — global ocean', percent: '91–100%' }, +}; +const POPULATION_DESC = { + 0: { description: 'Unpopulated or tiny outpost', estimate: 'None or a few individuals' }, + 1: { description: 'Tens of inhabitants', estimate: '~10s' }, + 2: { description: 'Hundreds of inhabitants', estimate: '~100s' }, + 3: { description: 'Thousands of inhabitants', estimate: '~1,000s' }, + 4: { description: 'Tens of thousands', estimate: '~10,000s' }, + 5: { description: 'Hundreds of thousands', estimate: '~100,000s' }, + 6: { description: 'Millions of inhabitants', estimate: '~1,000,000s' }, + 7: { description: 'Tens of millions', estimate: '~10,000,000s' }, + 8: { description: 'Hundreds of millions', estimate: '~100,000,000s' }, + 9: { description: 'Billions of inhabitants', estimate: '~1,000,000,000s' }, + 10: { description: 'Tens of billions', estimate: '~10,000,000,000s' }, + 11: { description: 'Hundreds of billions', estimate: '~100,000,000,000s' }, + 12: { description: 'Trillions of inhabitants', estimate: '~1,000,000,000,000s' }, +}; +const GOVERNMENT_DESC = { + 0: 'No Government Structure — family/clan/tribal', + 1: 'Company/Corporation — governed by a company', + 2: 'Participating Democracy — rule by citizen vote', + 3: 'Self-Perpetuating Oligarchy — ruling class maintains power', + 4: 'Representative Democracy — elected representatives', + 5: 'Feudal Technocracy — controlled by technology owners', + 6: 'Captive Government — controlled by outside power', + 7: 'Balkanization — no central authority', + 8: 'Civil Service Bureaucracy — rule by competence', + 9: 'Impersonal Bureaucracy — rule by rigid law', + 10: 'Charismatic Dictator — rule by personality', + 11: 'Non-Charismatic Leader — rule by position', + 12: 'Charismatic Oligarchy — rule by a few personalities', + 13: 'Religious Dictatorship — rule by religious doctrine', + 14: 'Religious Autocracy — rule by a religious figure', + 15: 'Totalitarian Oligarchy — oppressive rule by a few', +}; +const LAW_LEVEL_DESC = { + 0: 'No prohibitions — no restrictions on weapons or behavior', + 1: 'Body pistols, explosives, nuclear weapons prohibited', + 2: 'Portable energy weapons prohibited', + 3: 'Machine guns, automatic weapons prohibited', + 4: 'Light assault weapons prohibited', + 5: 'Personal concealable weapons prohibited', + 6: 'All firearms except shotguns prohibited', + 7: 'Shotguns prohibited', + 8: 'Long blades prohibited in public', + 9: 'All weapons outside home prohibited', + 10: 'Weapon possession prohibited — weapons locked up', + 11: 'Rigid control of civilian movement', + 12: 'Unrestricted invasion of privacy', + 13: 'Paramilitary law enforcement', + 14: 'Full-fledged police state', + 15: 'Daily life rigidly controlled', +}; +const TECH_LEVEL_ERA = { + 0: 'Stone Age / Pre-Industrial', + 1: 'Bronze Age / Iron Age', + 2: 'Renaissance', + 3: 'Industrial Revolution', + 4: 'Mechanized Age', + 5: 'Broadcast Age', + 6: 'Atomic Age', + 7: 'Space Age', + 8: 'Information Age', + 9: 'Pre-Stellar', + 10: 'Early Stellar', + 11: 'Average Stellar', + 12: 'Average Interstellar', + 13: 'High Interstellar', + 14: 'Average Imperial', + 15: 'High Imperial / Average Interstellar II', + 16: 'Sophont', + 17: 'Advanced', +}; +export function parseUWP(uwp) { + const clean = uwp.trim().replace(/\s+/g, ''); + const starportCode = clean[0] ?? '?'; + const sizeCode = clean[1] ?? '0'; + const atmCode = clean[2] ?? '0'; + const hydroCode = clean[3] ?? '0'; + const popCode = clean[4] ?? '0'; + const govCode = clean[5] ?? '0'; + const lawCode = clean[6] ?? '0'; + const tlCode = clean[8] ?? '0'; + const sizeVal = eHexValue(sizeCode); + const atmVal = eHexValue(atmCode); + const hydroVal = eHexValue(hydroCode); + const popVal = eHexValue(popCode); + const govVal = eHexValue(govCode); + const lawVal = eHexValue(lawCode); + const tlVal = eHexValue(tlCode); + const sizeInfo = SIZE_DESC[sizeVal] ?? { description: 'Unknown size', diameter_km: 'Unknown' }; + const hydroInfo = HYDROGRAPHICS_DESC[hydroVal] ?? { description: 'Unknown hydrographics', percent: 'Unknown' }; + const popInfo = POPULATION_DESC[popVal] ?? { description: 'Unknown population', estimate: 'Unknown' }; + return { + raw: uwp, + starport: { + code: starportCode, + description: STARPORT[starportCode.toUpperCase()] ?? `Unknown starport code: ${starportCode}`, + }, + size: { + code: sizeCode, + value: sizeVal, + description: sizeInfo.description, + diameter_km: sizeInfo.diameter_km, + }, + atmosphere: { + code: atmCode, + value: atmVal, + description: ATMOSPHERE_DESC[atmVal] ?? `Unknown atmosphere code: ${atmCode}`, + }, + hydrographics: { + code: hydroCode, + value: hydroVal, + description: hydroInfo.description, + percent: hydroInfo.percent, + }, + population: { + code: popCode, + value: popVal, + description: popInfo.description, + estimate: popInfo.estimate, + }, + government: { + code: govCode, + value: govVal, + description: GOVERNMENT_DESC[govVal] ?? `Unknown government code: ${govCode}`, + }, + law_level: { + code: lawCode, + value: lawVal, + description: LAW_LEVEL_DESC[lawVal] ?? `Unknown law level code: ${lawCode}`, + }, + tech_level: { + code: tlCode, + value: tlVal, + description: `Tech Level ${tlVal}`, + era: TECH_LEVEL_ERA[tlVal] ?? 'Advanced/Unknown', + }, + }; +} +//# sourceMappingURL=uwp.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/parsers/uwp.js.map b/mcp_servers/traveller_map/dist/parsers/uwp.js.map new file mode 100644 index 0000000..496afc4 --- /dev/null +++ b/mcp_servers/traveller_map/dist/parsers/uwp.js.map @@ -0,0 +1 @@ +{"version":3,"file":"uwp.js","sourceRoot":"","sources":["../../src/parsers/uwp.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/server.d.ts b/mcp_servers/traveller_map/dist/server.d.ts new file mode 100644 index 0000000..11af343 --- /dev/null +++ b/mcp_servers/traveller_map/dist/server.d.ts @@ -0,0 +1,3 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +export declare function createServer(): McpServer; +//# sourceMappingURL=server.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/server.d.ts.map b/mcp_servers/traveller_map/dist/server.d.ts.map new file mode 100644 index 0000000..62f948d --- /dev/null +++ b/mcp_servers/traveller_map/dist/server.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAcpE,wBAAgB,YAAY,IAAI,SAAS,CAyExC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/server.js b/mcp_servers/traveller_map/dist/server.js new file mode 100644 index 0000000..5b0b327 --- /dev/null +++ b/mcp_servers/traveller_map/dist/server.js @@ -0,0 +1,31 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import * as getSubsectorMap from './tools/get_subsector_map.js'; +import * as renderCustomMap from './tools/render_custom_map.js'; +import * as getWorldInfo from './tools/get_world_info.js'; +import * as searchWorlds from './tools/search_worlds.js'; +import * as getJumpMap from './tools/get_jump_map.js'; +import * as findRoute from './tools/find_route.js'; +import * as getWorldsInJumpRange from './tools/get_worlds_in_jump_range.js'; +import * as getSectorList from './tools/get_sector_list.js'; +import * as getSectorData from './tools/get_sector_data.js'; +import * as getSectorMetadata from './tools/get_sector_metadata.js'; +import * as getAllegianceList from './tools/get_allegiance_list.js'; +export function createServer() { + const server = new McpServer({ + name: 'traveller-map', + version: '1.0.0', + }); + server.registerTool(getSubsectorMap.name, { description: getSubsectorMap.description, inputSchema: getSubsectorMap.inputSchema.shape }, (args) => getSubsectorMap.handler(args)); + server.registerTool(renderCustomMap.name, { description: renderCustomMap.description, inputSchema: renderCustomMap.inputSchema.shape }, (args) => renderCustomMap.handler(args)); + server.registerTool(getWorldInfo.name, { description: getWorldInfo.description, inputSchema: getWorldInfo.inputSchema.shape }, (args) => getWorldInfo.handler(args)); + server.registerTool(searchWorlds.name, { description: searchWorlds.description, inputSchema: searchWorlds.inputSchema.shape }, (args) => searchWorlds.handler(args)); + server.registerTool(getJumpMap.name, { description: getJumpMap.description, inputSchema: getJumpMap.inputSchema.shape }, (args) => getJumpMap.handler(args)); + server.registerTool(findRoute.name, { description: findRoute.description, inputSchema: findRoute.inputSchema.shape }, (args) => findRoute.handler(args)); + server.registerTool(getWorldsInJumpRange.name, { description: getWorldsInJumpRange.description, inputSchema: getWorldsInJumpRange.inputSchema.shape }, (args) => getWorldsInJumpRange.handler(args)); + server.registerTool(getSectorList.name, { description: getSectorList.description, inputSchema: getSectorList.inputSchema.shape }, (args) => getSectorList.handler(args)); + server.registerTool(getSectorData.name, { description: getSectorData.description, inputSchema: getSectorData.inputSchema.shape }, (args) => getSectorData.handler(args)); + server.registerTool(getSectorMetadata.name, { description: getSectorMetadata.description, inputSchema: getSectorMetadata.inputSchema.shape }, (args) => getSectorMetadata.handler(args)); + server.registerTool(getAllegianceList.name, { description: getAllegianceList.description, inputSchema: getAllegianceList.inputSchema.shape }, (args) => getAllegianceList.handler(args)); + return server; +} +//# sourceMappingURL=server.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/server.js.map b/mcp_servers/traveller_map/dist/server.js.map new file mode 100644 index 0000000..cd62667 --- /dev/null +++ b/mcp_servers/traveller_map/dist/server.js.map @@ -0,0 +1 @@ +{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/find_route.d.ts b/mcp_servers/traveller_map/dist/tools/find_route.d.ts new file mode 100644 index 0000000..5dcc22a --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/find_route.d.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +export declare const name = "find_route"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{ + start: z.ZodString; + end: z.ZodString; + jump: z.ZodDefault>; + avoid_red_zones: z.ZodDefault>; + imperial_only: z.ZodDefault>; + wilderness_refueling: z.ZodDefault>; + milieu: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + jump: number; + start: string; + end: string; + avoid_red_zones: boolean; + imperial_only: boolean; + wilderness_refueling: boolean; + milieu?: string | undefined; +}, { + start: string; + end: string; + milieu?: string | undefined; + jump?: number | undefined; + avoid_red_zones?: boolean | undefined; + imperial_only?: boolean | undefined; + wilderness_refueling?: boolean | undefined; +}>; +export type Input = z.infer; +export declare function handler(args: Input): Promise<{ + content: { + type: "text"; + text: string; + }[]; +}>; +//# sourceMappingURL=find_route.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/find_route.d.ts.map b/mcp_servers/traveller_map/dist/tools/find_route.d.ts.map new file mode 100644 index 0000000..9ebd5cf --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/find_route.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"find_route.d.ts","sourceRoot":"","sources":["../../src/tools/find_route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,IAAI,eAAe,CAAC;AAEjC,eAAO,MAAM,WAAW,QAIsE,CAAC;AAE/F,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;EA8BtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAgBhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAsFxC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/find_route.js b/mcp_servers/traveller_map/dist/tools/find_route.js new file mode 100644 index 0000000..ba41b98 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/find_route.js @@ -0,0 +1,121 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; +import { parseUWP } from '../parsers/uwp.js'; +export const name = 'find_route'; +export const description = 'Finds a jump route between two worlds and returns the sequence of intermediate worlds. ' + + 'Locations are specified as "Sector XXYY" (e.g. "Spinward Marches 1910"). ' + + 'Returns 404/no-route if no path exists within the jump rating. ' + + 'Options: avoid Red zones, require Imperial membership, require wilderness refueling stops.'; +export const inputSchema = z.object({ + start: z + .string() + .describe('Starting world as "Sector XXYY" (e.g. "Spinward Marches 1910") or T5SS abbreviation format'), + end: z + .string() + .describe('Destination world in the same format (e.g. "Core 2118" for Capital)'), + jump: z + .number() + .min(1) + .max(12) + .optional() + .default(2) + .describe('Maximum jump distance per leg (1-12, default 2)'), + avoid_red_zones: z + .boolean() + .optional() + .default(false) + .describe('If true, the route will not pass through TAS Red Zone worlds'), + imperial_only: z + .boolean() + .optional() + .default(false) + .describe('If true, only stop at Third Imperium member worlds'), + wilderness_refueling: z + .boolean() + .optional() + .default(false) + .describe('If true, stops must have wilderness refueling available (gas giant or ocean)'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), +}); +const ZONE_LABELS = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' }; +export async function handler(args) { + const { start, end, jump, avoid_red_zones, imperial_only, wilderness_refueling, milieu } = args; + let rawData; + try { + rawData = await apiGetJson('/api/route', { + start, + end, + jump, + nored: avoid_red_zones ? 1 : undefined, + im: imperial_only ? 1 : undefined, + wild: wilderness_refueling ? 1 : undefined, + milieu, + }); + } + catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (message.includes('404')) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + found: false, + start, + end, + jump_rating: jump, + message: `No jump-${jump} route found between "${start}" and "${end}". Try increasing the jump rating or relaxing constraints.`, + }, null, 2), + }, + ], + }; + } + throw err; + } + // The API returns an array of worlds directly (not wrapped in {Worlds: [...]}) + const worldArray = Array.isArray(rawData) + ? rawData + : (rawData.Worlds ?? []); + const worlds = worldArray.map((w) => { + const zoneCode = (w.Zone ?? '').trim(); + const uwpRaw = w.UWP ?? ''; + const sectorName = typeof w.Sector === 'string' ? w.Sector : w.Sector?.Name; + let starport = ''; + if (uwpRaw && uwpRaw !== '?000000-0') { + try { + starport = parseUWP(uwpRaw).starport.code; + } + catch { + starport = uwpRaw[0] ?? ''; + } + } + return { + name: w.Name, + sector: sectorName, + hex: w.Hex, + location: sectorName && w.Hex ? `${sectorName} ${w.Hex}` : undefined, + uwp: uwpRaw, + starport, + zone: (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green', + bases: w.Bases, + allegiance: w.AllegianceName ?? w.Allegiance, + }; + }); + const result = { + found: true, + start, + end, + jump_rating: jump, + total_jumps: Math.max(0, worlds.length - 1), + route: worlds, + }; + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; +} +//# sourceMappingURL=find_route.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/find_route.js.map b/mcp_servers/traveller_map/dist/tools/find_route.js.map new file mode 100644 index 0000000..6861fd5 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/find_route.js.map @@ -0,0 +1 @@ +{"version":3,"file":"find_route.js","sourceRoot":"","sources":["../../src/tools/find_route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C,MAAM,CAAC,MAAM,IAAI,GAAG,YAAY,CAAC;AAEjC,MAAM,CAAC,MAAM,WAAW,GACtB,yFAAyF;IACzF,2EAA2E;IAC3E,iEAAiE;IACjE,4FAA4F,CAAC;AAE/F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,QAAQ,CAAC,4FAA4F,CAAC;IACzG,GAAG,EAAE,CAAC;SACH,MAAM,EAAE;SACR,QAAQ,CAAC,qEAAqE,CAAC;IAClF,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,EAAE,CAAC;SACP,QAAQ,EAAE;SACV,OAAO,CAAC,CAAC,CAAC;SACV,QAAQ,CAAC,iDAAiD,CAAC;IAC9D,eAAe,EAAE,CAAC;SACf,OAAO,EAAE;SACT,QAAQ,EAAE;SACV,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CAAC,8DAA8D,CAAC;IAC3E,aAAa,EAAE,CAAC;SACb,OAAO,EAAE;SACT,QAAQ,EAAE;SACV,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CAAC,oDAAoD,CAAC;IACjE,oBAAoB,EAAE,CAAC;SACpB,OAAO,EAAE;SACT,QAAQ,EAAE;SACV,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CAAC,8EAA8E,CAAC;IAC3F,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;CACxE,CAAC,CAAC;AAgBH,MAAM,WAAW,GAA2B,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC;AAE9F,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,eAAe,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAEhG,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,UAAU,CAAU,YAAY,EAAE;YAChD,KAAK;YACL,GAAG;YACH,IAAI;YACJ,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;YACtC,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;YACjC,IAAI,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;YAC1C,MAAM;SACP,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAClB;4BACE,KAAK,EAAE,KAAK;4BACZ,KAAK;4BACL,GAAG;4BACH,WAAW,EAAE,IAAI;4BACjB,OAAO,EAAE,WAAW,IAAI,yBAAyB,KAAK,UAAU,GAAG,4DAA4D;yBAChI,EACD,IAAI,EACJ,CAAC,CACF;qBACF;iBACF;aACF,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,+EAA+E;IAC/E,MAAM,UAAU,GAAiB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QACrD,CAAC,CAAE,OAAwB;QAC3B,CAAC,CAAC,CAAE,OAAqC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IAE1D,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAClC,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC;QAC5E,IAAI,QAAQ,GAAG,EAAE,CAAC;QAClB,IAAI,MAAM,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;YACrC,IAAI,CAAC;gBACH,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC7B,CAAC;QACH,CAAC;QACD,OAAO;YACL,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,MAAM,EAAE,UAAU;YAClB,GAAG,EAAE,CAAC,CAAC,GAAG;YACV,QAAQ,EAAE,UAAU,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,UAAU,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS;YACpE,GAAG,EAAE,MAAM;YACX,QAAQ;YACR,IAAI,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,IAAI,OAAO;YACpD,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,UAAU,EAAE,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC,UAAU;SAC7C,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG;QACb,KAAK,EAAE,IAAI;QACX,KAAK;QACL,GAAG;QACH,WAAW,EAAE,IAAI;QACjB,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;QAC3C,KAAK,EAAE,MAAM;KACd,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts b/mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts new file mode 100644 index 0000000..abd1592 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; +export declare const name = "get_allegiance_list"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>; +export type Input = z.infer; +export declare function handler(_args: Input): Promise<{ + content: { + type: "text"; + text: string; + }[]; +}>; +//# sourceMappingURL=get_allegiance_list.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts.map b/mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts.map new file mode 100644 index 0000000..5e72560 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"get_allegiance_list.d.ts","sourceRoot":"","sources":["../../src/tools/get_allegiance_list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,wBAAwB,CAAC;AAE1C,eAAO,MAAM,WAAW,QAG6D,CAAC;AAEtF,eAAO,MAAM,WAAW,gDAAe,CAAC;AAExC,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAQhD,wBAAsB,OAAO,CAAC,KAAK,EAAE,KAAK;;;;;GAqBzC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_allegiance_list.js b/mcp_servers/traveller_map/dist/tools/get_allegiance_list.js new file mode 100644 index 0000000..eaf35ba --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_allegiance_list.js @@ -0,0 +1,27 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; +export const name = 'get_allegiance_list'; +export const description = 'Returns all known allegiance codes and their full names. ' + + 'Use this to find the right code before searching with "alleg:" in search_worlds. ' + + 'Examples: "ImDd" = "Third Imperium, Domain of Deneb", "Zh" = "Zhodani Consulate".'; +export const inputSchema = z.object({}); +export async function handler(_args) { + const data = await apiGetJson('/t5ss/allegiances', {}); + const allegiances = (Array.isArray(data) ? data : []).map((a) => ({ + code: a.Code, + name: a.Name, + })); + const result = { + count: allegiances.length, + allegiances, + }; + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; +} +//# sourceMappingURL=get_allegiance_list.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_allegiance_list.js.map b/mcp_servers/traveller_map/dist/tools/get_allegiance_list.js.map new file mode 100644 index 0000000..837177d --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_allegiance_list.js.map @@ -0,0 +1 @@ +{"version":3,"file":"get_allegiance_list.js","sourceRoot":"","sources":["../../src/tools/get_allegiance_list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,CAAC,MAAM,IAAI,GAAG,qBAAqB,CAAC;AAE1C,MAAM,CAAC,MAAM,WAAW,GACtB,2DAA2D;IAC3D,mFAAmF;IACnF,mFAAmF,CAAC;AAEtF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAUxC,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,KAAY;IACxC,MAAM,IAAI,GAAG,MAAM,UAAU,CAAoB,mBAAmB,EAAE,EAAE,CAAC,CAAC;IAE1E,MAAM,WAAW,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChE,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,IAAI,EAAE,CAAC,CAAC,IAAI;KACb,CAAC,CAAC,CAAC;IAEJ,MAAM,MAAM,GAAG;QACb,KAAK,EAAE,WAAW,CAAC,MAAM;QACzB,WAAW;KACZ,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts b/mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts new file mode 100644 index 0000000..725b6af --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +export declare const name = "get_jump_map"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{ + sector: z.ZodString; + hex: z.ZodString; + jump: z.ZodDefault>; + scale: z.ZodDefault>; + style: z.ZodDefault>>; + milieu: z.ZodOptional; + save_path: z.ZodOptional; + filename: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + hex: string; + sector: string; + scale: number; + style: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose"; + jump: number; + milieu?: string | undefined; + save_path?: string | undefined; + filename?: string | undefined; +}, { + hex: string; + sector: string; + scale?: number | undefined; + style?: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose" | undefined; + milieu?: string | undefined; + save_path?: string | undefined; + filename?: string | undefined; + jump?: number | undefined; +}>; +export type Input = z.infer; +export declare function handler(args: Input): Promise<{ + isError: boolean; + content: { + type: "text"; + text: string; + }[]; +} | { + content: ({ + type: "image"; + data: string; + mimeType: string; + } | { + type: "text"; + text: string; + })[]; + isError?: undefined; +}>; +//# sourceMappingURL=get_jump_map.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts.map b/mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts.map new file mode 100644 index 0000000..d74debd --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"get_jump_map.d.ts","sourceRoot":"","sources":["../../src/tools/get_jump_map.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,iBAAiB,CAAC;AAEnC,eAAO,MAAM,WAAW,QAI2C,CAAC;AAEpE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsBtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAmBhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;;;;;;;;;;;;GAuDxC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_jump_map.js b/mcp_servers/traveller_map/dist/tools/get_jump_map.js new file mode 100644 index 0000000..bb8593f --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_jump_map.js @@ -0,0 +1,97 @@ +import { stat, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { z } from 'zod'; +import { apiGetDataUri } from '../api/client.js'; +export const name = 'get_jump_map'; +export const description = 'Returns a PNG image of the hex map centered on a world, showing all worlds within N jump distance. ' + + 'Great for visualizing routes and nearby systems during a session. ' + + 'Optionally saves the image to a local directory (e.g. an Obsidian vault attachments folder) ' + + 'and returns the saved file path so it can be embedded in notes.'; +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'), + hex: z.string().describe('Hex location in XXYY format (e.g. "1910" for Regina)'), + jump: z.number().min(0).max(20).optional().default(2).describe('Jump range in parsecs (0-20, default 2)'), + scale: z.number().optional().default(64).describe('Pixels per parsec (default 64)'), + style: z + .enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose']) + .optional() + .default('poster') + .describe('Visual rendering style. Note: candy style returns JPEG instead of PNG.'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), + save_path: z + .string() + .optional() + .describe('Absolute directory path where the image file should be saved'), + filename: z + .string() + .optional() + .describe('Custom filename without extension (e.g. "regina-jump2"). ' + + 'Auto-generated as "{sector}-{hex}-jump{jump}" if omitted.'), +}); +function extFromMimeType(mimeType) { + return mimeType === 'image/jpeg' ? 'jpg' : 'png'; +} +function buildFilename(sector, hex, jump, customName, ext) { + let base; + if (customName) { + base = customName.replace(/\.(png|jpe?g)$/i, ''); + } + else { + base = `${sector}-${hex}-jump${jump}` + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9\-_]/g, ''); + } + return `${base}.${ext}`; +} +export async function handler(args) { + const { sector, hex, jump, scale, style, milieu, save_path, filename } = args; + const { base64, mimeType } = await apiGetDataUri('/api/jumpmap', { + sector, + hex, + jump, + scale, + style, + milieu, + }); + const imageBlock = { type: 'image', data: base64, mimeType }; + if (!save_path) { + return { content: [imageBlock] }; + } + let dirStat; + try { + dirStat = await stat(save_path); + } + catch { + return { + isError: true, + content: [{ type: 'text', text: `Error: save_path directory does not exist: ${save_path}` }], + }; + } + if (!dirStat.isDirectory()) { + return { + isError: true, + content: [{ type: 'text', text: `Error: save_path is not a directory: ${save_path}` }], + }; + } + const ext = extFromMimeType(mimeType); + const resolvedFilename = buildFilename(sector, hex, jump, filename, ext); + const fullPath = join(save_path, resolvedFilename); + try { + await writeFile(fullPath, Buffer.from(base64, 'base64')); + } + catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { + isError: true, + content: [{ type: 'text', text: `Error: Failed to write file: ${message}` }], + }; + } + return { + content: [ + imageBlock, + { type: 'text', text: `Image saved to: ${fullPath}` }, + ], + }; +} +//# sourceMappingURL=get_jump_map.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_jump_map.js.map b/mcp_servers/traveller_map/dist/tools/get_jump_map.js.map new file mode 100644 index 0000000..41b3fae --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_jump_map.js.map @@ -0,0 +1 @@ +{"version":3,"file":"get_jump_map.js","sourceRoot":"","sources":["../../src/tools/get_jump_map.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts b/mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts new file mode 100644 index 0000000..c2789f7 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; +export declare const name = "get_sector_data"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{ + sector: z.ZodString; + subsector: z.ZodOptional; + milieu: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sector: string; + subsector?: string | undefined; + milieu?: string | undefined; +}, { + sector: string; + subsector?: string | undefined; + milieu?: string | undefined; +}>; +export type Input = z.infer; +export declare function handler(args: Input): Promise<{ + content: { + type: "text"; + text: string; + }[]; +}>; +//# sourceMappingURL=get_sector_data.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts.map b/mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts.map new file mode 100644 index 0000000..1bf95e6 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"get_sector_data.d.ts","sourceRoot":"","sources":["../../src/tools/get_sector_data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,IAAI,oBAAoB,CAAC;AAEtC,eAAO,MAAM,WAAW,QAIwE,CAAC;AAEjG,eAAO,MAAM,WAAW;;;;;;;;;;;;EAOtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GA2BxC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_data.js b/mcp_servers/traveller_map/dist/tools/get_sector_data.js new file mode 100644 index 0000000..bcf1b5b --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_data.js @@ -0,0 +1,41 @@ +import { z } from 'zod'; +import { apiGetText } from '../api/client.js'; +import { parseSecTabDelimited } from '../parsers/sec.js'; +export const name = 'get_sector_data'; +export const description = 'Returns all worlds in a sector (or single subsector) as structured data with decoded UWPs. ' + + 'Useful for bulk analysis, e.g. "which worlds in Spinward Marches have Tech Level 15?" ' + + 'or "list all Naval Bases in the Regina subsector". ' + + 'Returns full world records including trade codes, bases, allegiance, and decoded UWP fields.'; +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'), + subsector: z + .string() + .optional() + .describe('Limit to a single subsector by letter (A-P) or name'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), +}); +export async function handler(args) { + const { sector, subsector, milieu } = args; + const text = await apiGetText('/api/sec', { + sector, + subsector, + type: 'TabDelimited', + milieu, + }); + const worlds = parseSecTabDelimited(text); + const result = { + sector, + subsector: subsector ?? 'all', + count: worlds.length, + worlds, + }; + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; +} +//# sourceMappingURL=get_sector_data.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_data.js.map b/mcp_servers/traveller_map/dist/tools/get_sector_data.js.map new file mode 100644 index 0000000..74145e2 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_data.js.map @@ -0,0 +1 @@ +{"version":3,"file":"get_sector_data.js","sourceRoot":"","sources":["../../src/tools/get_sector_data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,CAAC,MAAM,IAAI,GAAG,iBAAiB,CAAC;AAEtC,MAAM,CAAC,MAAM,WAAW,GACtB,6FAA6F;IAC7F,wFAAwF;IACxF,qDAAqD;IACrD,8FAA8F,CAAC;AAEjG,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sEAAsE,CAAC;IACnG,SAAS,EAAE,CAAC;SACT,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,qDAAqD,CAAC;IAClE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;CACxE,CAAC,CAAC;AAIH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAE3C,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,UAAU,EAAE;QACxC,MAAM;QACN,SAAS;QACT,IAAI,EAAE,cAAc;QACpB,MAAM;KACP,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAE1C,MAAM,MAAM,GAAG;QACb,MAAM;QACN,SAAS,EAAE,SAAS,IAAI,KAAK;QAC7B,KAAK,EAAE,MAAM,CAAC,MAAM;QACpB,MAAM;KACP,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts b/mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts new file mode 100644 index 0000000..d0a4e3c --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +export declare const name = "get_sector_list"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{ + milieu: z.ZodOptional; + tag: z.ZodOptional>; +}, "strip", z.ZodTypeAny, { + milieu?: string | undefined; + tag?: "Official" | "InReview" | "Preserve" | "Apocryphal" | undefined; +}, { + milieu?: string | undefined; + tag?: "Official" | "InReview" | "Preserve" | "Apocryphal" | undefined; +}>; +export type Input = z.infer; +export declare function handler(args: Input): Promise<{ + content: { + type: "text"; + text: string; + }[]; +}>; +//# sourceMappingURL=get_sector_list.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts.map b/mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts.map new file mode 100644 index 0000000..2ab5ec9 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"get_sector_list.d.ts","sourceRoot":"","sources":["../../src/tools/get_sector_list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,oBAAoB,CAAC;AAEtC,eAAO,MAAM,WAAW,QAG2E,CAAC;AAEpG,eAAO,MAAM,WAAW;;;;;;;;;EAMtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAgBhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAgCxC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_list.js b/mcp_servers/traveller_map/dist/tools/get_sector_list.js new file mode 100644 index 0000000..c3fc0ac --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_list.js @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; +export const name = 'get_sector_list'; +export const description = 'Returns a list of all known sectors in the Traveller universe with their names, ' + + 'T5SS abbreviations, and galactic coordinates. ' + + 'Can be filtered by official status (Official, InReview, Preserve, Apocryphal) and campaign era.'; +export const inputSchema = z.object({ + milieu: z.string().optional().describe('Campaign era filter (e.g. "M1105")'), + tag: z + .enum(['Official', 'InReview', 'Preserve', 'Apocryphal']) + .optional() + .describe('Filter by sector data status'), +}); +export async function handler(args) { + const { milieu, tag } = args; + const data = await apiGetJson('/api/universe', { + requireData: 1, + milieu, + tag, + }); + const sectors = (data.Sectors ?? []) + .map((s) => ({ + name: s.Names?.[0]?.Text ?? 'Unknown', + abbreviation: s.Abbreviation, + x: s.X, + y: s.Y, + tags: s.Tags, + })) + .sort((a, b) => a.name.localeCompare(b.name)); + const result = { + count: sectors.length, + sectors, + }; + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; +} +//# sourceMappingURL=get_sector_list.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_list.js.map b/mcp_servers/traveller_map/dist/tools/get_sector_list.js.map new file mode 100644 index 0000000..2e07fbb --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_list.js.map @@ -0,0 +1 @@ +{"version":3,"file":"get_sector_list.js","sourceRoot":"","sources":["../../src/tools/get_sector_list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,CAAC,MAAM,IAAI,GAAG,iBAAiB,CAAC;AAEtC,MAAM,CAAC,MAAM,WAAW,GACtB,kFAAkF;IAClF,gDAAgD;IAChD,iGAAiG,CAAC;AAEpG,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;IAC5E,GAAG,EAAE,CAAC;SACH,IAAI,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC;SACxD,QAAQ,EAAE;SACV,QAAQ,CAAC,8BAA8B,CAAC;CAC5C,CAAC,CAAC;AAkBH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAE7B,MAAM,IAAI,GAAG,MAAM,UAAU,CAAmB,eAAe,EAAE;QAC/D,WAAW,EAAE,CAAC;QACd,MAAM;QACN,GAAG;KACJ,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;SACjC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACX,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,SAAS;QACrC,YAAY,EAAE,CAAC,CAAC,YAAY;QAC5B,CAAC,EAAE,CAAC,CAAC,CAAC;QACN,CAAC,EAAE,CAAC,CAAC,CAAC;QACN,IAAI,EAAE,CAAC,CAAC,IAAI;KACb,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAEhD,MAAM,MAAM,GAAG;QACb,KAAK,EAAE,OAAO,CAAC,MAAM;QACrB,OAAO;KACR,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts b/mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts new file mode 100644 index 0000000..bafd1fe --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +export declare const name = "get_sector_metadata"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{ + sector: z.ZodString; + milieu: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sector: string; + milieu?: string | undefined; +}, { + sector: string; + milieu?: string | undefined; +}>; +export type Input = z.infer; +export declare function handler(args: Input): Promise<{ + content: { + type: "text"; + text: string; + }[]; +}>; +//# sourceMappingURL=get_sector_metadata.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts.map b/mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts.map new file mode 100644 index 0000000..f2ce00a --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"get_sector_metadata.d.ts","sourceRoot":"","sources":["../../src/tools/get_sector_metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,wBAAwB,CAAC;AAE1C,eAAO,MAAM,WAAW,QAGiC,CAAC;AAE1D,eAAO,MAAM,WAAW;;;;;;;;;EAGtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAwBhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAoCxC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_metadata.js b/mcp_servers/traveller_map/dist/tools/get_sector_metadata.js new file mode 100644 index 0000000..22098c7 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_metadata.js @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; +export const name = 'get_sector_metadata'; +export const description = 'Returns metadata for a sector: subsector names (A-P), allegiance regions, route overlays, and political borders. ' + + 'Use this to discover subsector names before calling get_subsector_map, ' + + 'or to understand the political structure of a sector.'; +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), +}); +export async function handler(args) { + const { sector, milieu } = args; + const data = await apiGetJson('/api/metadata', { sector, milieu }); + const subsectorsByIndex = {}; + for (const sub of data.Subsectors ?? []) { + if (sub.Index && sub.Name) { + subsectorsByIndex[sub.Index] = sub.Name; + } + } + const subsectors = 'ABCDEFGHIJKLMNOP'.split('').map((letter) => ({ + index: letter, + name: subsectorsByIndex[letter] ?? `Subsector ${letter}`, + })); + const result = { + names: data.Names?.map((n) => ({ text: n.Text, lang: n.Lang })), + abbreviation: data.Abbreviation, + coordinates: { x: data.X, y: data.Y }, + subsectors, + allegiances: (data.Allegiances ?? []).map((a) => ({ + code: a.Code, + name: a.Name, + })), + }; + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; +} +//# sourceMappingURL=get_sector_metadata.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_sector_metadata.js.map b/mcp_servers/traveller_map/dist/tools/get_sector_metadata.js.map new file mode 100644 index 0000000..912f96b --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_sector_metadata.js.map @@ -0,0 +1 @@ +{"version":3,"file":"get_sector_metadata.js","sourceRoot":"","sources":["../../src/tools/get_sector_metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,CAAC,MAAM,IAAI,GAAG,qBAAqB,CAAC;AAE1C,MAAM,CAAC,MAAM,WAAW,GACtB,mHAAmH;IACnH,yEAAyE;IACzE,uDAAuD,CAAC;AAE1D,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sEAAsE,CAAC;IACnG,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;CACxE,CAAC,CAAC;AA0BH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAEhC,MAAM,IAAI,GAAG,MAAM,UAAU,CAAmB,eAAe,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAErF,MAAM,iBAAiB,GAA2B,EAAE,CAAC;IACrD,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;QACxC,IAAI,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;YAC1B,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC/D,KAAK,EAAE,MAAM;QACb,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,IAAI,aAAa,MAAM,EAAE;KACzD,CAAC,CAAC,CAAC;IAEJ,MAAM,MAAM,GAAG;QACb,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/D,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,WAAW,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE;QACrC,UAAU;QACV,WAAW,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAChD,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;SACb,CAAC,CAAC;KACJ,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts b/mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts new file mode 100644 index 0000000..aa012db --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; +export declare const name = "get_subsector_map"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{ + sector: z.ZodString; + subsector: z.ZodString; + scale: z.ZodDefault>; + style: z.ZodDefault>>; + milieu: z.ZodOptional; + save_path: z.ZodOptional; + filename: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sector: string; + subsector: string; + scale: number; + style: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose"; + milieu?: string | undefined; + save_path?: string | undefined; + filename?: string | undefined; +}, { + sector: string; + subsector: string; + scale?: number | undefined; + style?: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose" | undefined; + milieu?: string | undefined; + save_path?: string | undefined; + filename?: string | undefined; +}>; +export type Input = z.infer; +export declare function handler(args: Input): Promise<{ + isError: boolean; + content: { + type: "text"; + text: string; + }[]; +} | { + content: ({ + type: "image"; + data: string; + mimeType: string; + } | { + type: "text"; + text: string; + })[]; + isError?: undefined; +}>; +//# sourceMappingURL=get_subsector_map.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts.map b/mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts.map new file mode 100644 index 0000000..b60eb45 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"get_subsector_map.d.ts","sourceRoot":"","sources":["../../src/tools/get_subsector_map.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,sBAAsB,CAAC;AAExC,eAAO,MAAM,WAAW,QAG2C,CAAC;AAEpE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;EAqBtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAmBhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;;;;;;;;;;;;GAsDxC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_subsector_map.js b/mcp_servers/traveller_map/dist/tools/get_subsector_map.js new file mode 100644 index 0000000..a022947 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_subsector_map.js @@ -0,0 +1,94 @@ +import { stat, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { z } from 'zod'; +import { apiGetDataUri } from '../api/client.js'; +export const name = 'get_subsector_map'; +export const description = 'Returns a PNG image of a named subsector from the official Traveller Map database. ' + + 'Optionally saves the image to a local directory (e.g. an Obsidian vault attachments folder) ' + + 'and returns the saved file path so it can be embedded in notes.'; +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'), + subsector: z.string().describe('Subsector letter A-P or subsector name (e.g. "A" or "Regina")'), + scale: z.number().optional().default(64).describe('Pixels per parsec (default 64). Higher = larger image.'), + style: z + .enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose']) + .optional() + .default('poster') + .describe('Visual rendering style. Note: candy style returns JPEG instead of PNG.'), + milieu: z.string().optional().describe('Campaign era (e.g. "M1105" for default Third Imperium 1105)'), + save_path: z + .string() + .optional() + .describe('Absolute directory path where the image file should be saved'), + filename: z + .string() + .optional() + .describe('Custom filename without extension (e.g. "regina-map"). ' + + 'Auto-generated as "{sector}-{subsector}-subsector" if omitted.'), +}); +function extFromMimeType(mimeType) { + return mimeType === 'image/jpeg' ? 'jpg' : 'png'; +} +function buildFilename(sector, subsector, customName, ext) { + let base; + if (customName) { + base = customName.replace(/\.(png|jpe?g)$/i, ''); + } + else { + base = `${sector}-${subsector}-subsector` + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9\-_]/g, ''); + } + return `${base}.${ext}`; +} +export async function handler(args) { + const { sector, subsector, scale, style, milieu, save_path, filename } = args; + const { base64, mimeType } = await apiGetDataUri('/api/poster', { + sector, + subsector, + scale, + style, + milieu, + }); + const imageBlock = { type: 'image', data: base64, mimeType }; + if (!save_path) { + return { content: [imageBlock] }; + } + let dirStat; + try { + dirStat = await stat(save_path); + } + catch { + return { + isError: true, + content: [{ type: 'text', text: `Error: save_path directory does not exist: ${save_path}` }], + }; + } + if (!dirStat.isDirectory()) { + return { + isError: true, + content: [{ type: 'text', text: `Error: save_path is not a directory: ${save_path}` }], + }; + } + const ext = extFromMimeType(mimeType); + const resolvedFilename = buildFilename(sector, subsector, filename, ext); + const fullPath = join(save_path, resolvedFilename); + try { + await writeFile(fullPath, Buffer.from(base64, 'base64')); + } + catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { + isError: true, + content: [{ type: 'text', text: `Error: Failed to write file: ${message}` }], + }; + } + return { + content: [ + imageBlock, + { type: 'text', text: `Image saved to: ${fullPath}` }, + ], + }; +} +//# sourceMappingURL=get_subsector_map.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_subsector_map.js.map b/mcp_servers/traveller_map/dist/tools/get_subsector_map.js.map new file mode 100644 index 0000000..f72b547 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_subsector_map.js.map @@ -0,0 +1 @@ +{"version":3,"file":"get_subsector_map.js","sourceRoot":"","sources":["../../src/tools/get_subsector_map.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_world_info.d.ts b/mcp_servers/traveller_map/dist/tools/get_world_info.d.ts new file mode 100644 index 0000000..10fa0ab --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_world_info.d.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; +export declare const name = "get_world_info"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{ + sector: z.ZodString; + hex: z.ZodString; + milieu: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + hex: string; + sector: string; + milieu?: string | undefined; +}, { + hex: string; + sector: string; + milieu?: string | undefined; +}>; +export type Input = z.infer; +export declare function handler(args: Input): Promise<{ + content: { + type: "text"; + text: string; + }[]; +}>; +//# sourceMappingURL=get_world_info.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_world_info.d.ts.map b/mcp_servers/traveller_map/dist/tools/get_world_info.d.ts.map new file mode 100644 index 0000000..615d343 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_world_info.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"get_world_info.d.ts","sourceRoot":"","sources":["../../src/tools/get_world_info.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,IAAI,mBAAmB,CAAC;AAErC,eAAO,MAAM,WAAW,QAIuD,CAAC;AAEhF,eAAO,MAAM,WAAW;;;;;;;;;;;;EAItB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AA8GhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAwCxC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_world_info.js b/mcp_servers/traveller_map/dist/tools/get_world_info.js new file mode 100644 index 0000000..22272cd --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_world_info.js @@ -0,0 +1,134 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; +import { parseUWP } from '../parsers/uwp.js'; +export const name = 'get_world_info'; +export const description = 'Retrieves detailed information about a specific world, with the UWP (Universal World Profile) ' + + 'fully decoded into human-readable fields: starport quality, world size, atmosphere type, ' + + 'hydrographics percentage, population estimate, government type, law level, and tech level era. ' + + 'Also returns trade codes, bases, travel zone, allegiance, and stellar data.'; +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'), + hex: z.string().describe('Hex location in XXYY format (e.g. "1910" for Regina in Spinward Marches)'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), +}); +const ZONE_LABELS = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' }; +const BASE_LABELS = { + N: 'Naval Base', + S: 'Scout Base', + W: 'Scout Waystation', + D: 'Naval Depot', + K: 'Naval Base (Sword Worlds)', + M: 'Military Base', + C: 'Corsair Base', + T: 'TAS Hostel', + R: 'Aslan Clan Base', + F: 'Aslan Tlaukhu Base', + A: 'Naval Base + Scout Base', + B: 'Naval Base + Scout Waystation', + G: 'Scout Base (Vargr)', + H: 'Naval Base + Scout Base (Vargr)', + X: 'Zhodani Relay Station', + Z: 'Zhodani Naval + Scout Base', +}; +function decodeBases(bases) { + if (!bases || bases.trim() === '' || bases.trim() === '-') + return []; + return bases + .trim() + .split('') + .filter((c) => c !== ' ' && c !== '-') + .map((c) => BASE_LABELS[c] ?? `Base (${c})`); +} +function decodeTradeCodes(remarks) { + const TRADE_CODES = { + Ag: 'Agricultural', + As: 'Asteroid', + Ba: 'Barren', + De: 'Desert', + Fl: 'Fluid Oceans (non-water)', + Ga: 'Garden World', + Hi: 'High Population', + Ht: 'High Technology', + Ic: 'Ice-Capped', + In: 'Industrial', + Lo: 'Low Population', + Lt: 'Low Technology', + Na: 'Non-Agricultural', + Ni: 'Non-Industrial', + Po: 'Poor', + Ri: 'Rich', + Tr: 'Temperate', + Tu: 'Tundra', + Tz: 'Tidally Locked', + Wa: 'Water World', + Va: 'Vacuum', + Ph: 'Pre-High Population', + Pi: 'Pre-Industrial', + Pa: 'Pre-Agricultural', + Mr: 'Reserve', + Fr: 'Frozen', + Ho: 'Hot', + Co: 'Cold', + Lk: 'Locked', + Tr2: 'Tropic', + Sa: 'Satellite', + Fa: 'Farming', + Mi: 'Mining', + Pz: 'Puzzle', + Cy: 'Cyclopean', + Di: 'Dieback', + Px: 'Prison/Exile Camp', + An: 'Ancient Site', + Rs: 'Research Station', + Cp: 'Subsector Capital', + Cs: 'Sector Capital', + Cx: 'Capital', + Fo: 'Forbidden', + Pn: 'Prison', + Re: 'Reserve', + }; + if (!remarks) + return []; + return remarks + .trim() + .split(/\s+/) + .filter((r) => r && r !== '-') + .map((code) => ({ code, meaning: TRADE_CODES[code] ?? code })); +} +export async function handler(args) { + const { sector, hex, milieu } = args; + const data = await apiGetJson('/api/credits', { sector, hex, milieu }); + const uwpRaw = data.UWP ?? '?000000-0'; + const decoded = parseUWP(uwpRaw); + const zoneCode = (data.Zone ?? '').trim(); + const zone = (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green'; + const result = { + name: data.Name ?? 'Unknown', + hex: data.Hex ?? hex, + sector: data.Sector ?? sector, + subsector: data.Subsector, + uwp: uwpRaw, + decoded_uwp: decoded, + trade_codes: decodeTradeCodes(data.Remarks ?? ''), + bases: decodeBases(data.Bases ?? ''), + zone, + pbg: data.PBG, + allegiance: data.Allegiance, + stellar: data.Stars, + importance: data.Ix, + economic_extension: data.Ex, + cultural_extension: data.Cx, + nobility: data.Nobility, + worlds_in_system: data.W, + resource_units: data.RU, + }; + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; +} +//# sourceMappingURL=get_world_info.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_world_info.js.map b/mcp_servers/traveller_map/dist/tools/get_world_info.js.map new file mode 100644 index 0000000..a5e84cd --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_world_info.js.map @@ -0,0 +1 @@ +{"version":3,"file":"get_world_info.js","sourceRoot":"","sources":["../../src/tools/get_world_info.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE7C,MAAM,CAAC,MAAM,IAAI,GAAG,gBAAgB,CAAC;AAErC,MAAM,CAAC,MAAM,WAAW,GACtB,gGAAgG;IAChG,2FAA2F;IAC3F,iGAAiG;IACjG,6EAA6E,CAAC;AAEhF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sEAAsE,CAAC;IACnG,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,0EAA0E,CAAC;IACpG,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;CACxE,CAAC,CAAC;AAyBH,MAAM,WAAW,GAA2B,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC;AAE9F,MAAM,WAAW,GAA2B;IAC1C,CAAC,EAAE,YAAY;IACf,CAAC,EAAE,YAAY;IACf,CAAC,EAAE,kBAAkB;IACrB,CAAC,EAAE,aAAa;IAChB,CAAC,EAAE,2BAA2B;IAC9B,CAAC,EAAE,eAAe;IAClB,CAAC,EAAE,cAAc;IACjB,CAAC,EAAE,YAAY;IACf,CAAC,EAAE,iBAAiB;IACpB,CAAC,EAAE,oBAAoB;IACvB,CAAC,EAAE,yBAAyB;IAC5B,CAAC,EAAE,+BAA+B;IAClC,CAAC,EAAE,oBAAoB;IACvB,CAAC,EAAE,iCAAiC;IACpC,CAAC,EAAE,uBAAuB;IAC1B,CAAC,EAAE,4BAA4B;CAChC,CAAC;AAEF,SAAS,WAAW,CAAC,KAAa;IAChC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,GAAG;QAAE,OAAO,EAAE,CAAC;IACrE,OAAO,KAAK;SACT,IAAI,EAAE;SACN,KAAK,CAAC,EAAE,CAAC;SACT,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC;SACrC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC;AACjD,CAAC;AAED,SAAS,gBAAgB,CAAC,OAAe;IACvC,MAAM,WAAW,GAA2B;QAC1C,EAAE,EAAE,cAAc;QAClB,EAAE,EAAE,UAAU;QACd,EAAE,EAAE,QAAQ;QACZ,EAAE,EAAE,QAAQ;QACZ,EAAE,EAAE,0BAA0B;QAC9B,EAAE,EAAE,cAAc;QAClB,EAAE,EAAE,iBAAiB;QACrB,EAAE,EAAE,iBAAiB;QACrB,EAAE,EAAE,YAAY;QAChB,EAAE,EAAE,YAAY;QAChB,EAAE,EAAE,gBAAgB;QACpB,EAAE,EAAE,gBAAgB;QACpB,EAAE,EAAE,kBAAkB;QACtB,EAAE,EAAE,gBAAgB;QACpB,EAAE,EAAE,MAAM;QACV,EAAE,EAAE,MAAM;QACV,EAAE,EAAE,WAAW;QACf,EAAE,EAAE,QAAQ;QACZ,EAAE,EAAE,gBAAgB;QACpB,EAAE,EAAE,aAAa;QACjB,EAAE,EAAE,QAAQ;QACZ,EAAE,EAAE,qBAAqB;QACzB,EAAE,EAAE,gBAAgB;QACpB,EAAE,EAAE,kBAAkB;QACtB,EAAE,EAAE,SAAS;QACb,EAAE,EAAE,QAAQ;QACZ,EAAE,EAAE,KAAK;QACT,EAAE,EAAE,MAAM;QACV,EAAE,EAAE,QAAQ;QACZ,GAAG,EAAE,QAAQ;QACb,EAAE,EAAE,WAAW;QACf,EAAE,EAAE,SAAS;QACb,EAAE,EAAE,QAAQ;QACZ,EAAE,EAAE,QAAQ;QACZ,EAAE,EAAE,WAAW;QACf,EAAE,EAAE,SAAS;QACb,EAAE,EAAE,mBAAmB;QACvB,EAAE,EAAE,cAAc;QAClB,EAAE,EAAE,kBAAkB;QACtB,EAAE,EAAE,mBAAmB;QACvB,EAAE,EAAE,gBAAgB;QACpB,EAAE,EAAE,SAAS;QACb,EAAE,EAAE,WAAW;QACf,EAAE,EAAE,QAAQ;QACZ,EAAE,EAAE,SAAS;KACd,CAAC;IAEF,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO,OAAO;SACX,IAAI,EAAE;SACN,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC;SAC7B,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAErC,MAAM,IAAI,GAAG,MAAM,UAAU,CAAkB,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;IAExF,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,WAAW,CAAC;IACvC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAEjC,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1C,MAAM,IAAI,GAAG,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,IAAI,OAAO,CAAC;IAE5D,MAAM,MAAM,GAAG;QACb,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,SAAS;QAC5B,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI,GAAG;QACpB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,MAAM;QAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,GAAG,EAAE,MAAM;QACX,WAAW,EAAE,OAAO;QACpB,WAAW,EAAE,gBAAgB,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;QACjD,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QACpC,IAAI;QACJ,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,OAAO,EAAE,IAAI,CAAC,KAAK;QACnB,UAAU,EAAE,IAAI,CAAC,EAAE;QACnB,kBAAkB,EAAE,IAAI,CAAC,EAAE;QAC3B,kBAAkB,EAAE,IAAI,CAAC,EAAE;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,gBAAgB,EAAE,IAAI,CAAC,CAAC;QACxB,cAAc,EAAE,IAAI,CAAC,EAAE;KACxB,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts b/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts new file mode 100644 index 0000000..fc1e33d --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts @@ -0,0 +1,27 @@ +import { z } from 'zod'; +export declare const name = "get_worlds_in_jump_range"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{ + sector: z.ZodString; + hex: z.ZodString; + jump: z.ZodDefault>; + milieu: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + hex: string; + sector: string; + jump: number; + milieu?: string | undefined; +}, { + hex: string; + sector: string; + milieu?: string | undefined; + jump?: number | undefined; +}>; +export type Input = z.infer; +export declare function handler(args: Input): Promise<{ + content: { + type: "text"; + text: string; + }[]; +}>; +//# sourceMappingURL=get_worlds_in_jump_range.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts.map b/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts.map new file mode 100644 index 0000000..eab730d --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"get_worlds_in_jump_range.d.ts","sourceRoot":"","sources":["../../src/tools/get_worlds_in_jump_range.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,IAAI,6BAA6B,CAAC;AAE/C,eAAO,MAAM,WAAW,QAG0C,CAAC;AAEnE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;EAKtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AA8BhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAiDxC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js b/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js new file mode 100644 index 0000000..e90d9af --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js @@ -0,0 +1,70 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; +import { parseUWP } from '../parsers/uwp.js'; +export const name = 'get_worlds_in_jump_range'; +export const description = 'Lists all worlds reachable from a given location within N parsecs. ' + + 'Returns structured data for each world including UWP, trade codes, bases, zone, and allegiance. ' + + 'Useful for planning routes or finding nearby systems to visit.'; +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation'), + hex: z.string().describe('Hex location in XXYY format'), + jump: z.number().min(0).max(12).optional().default(2).describe('Jump range in parsecs (0-12, default 2)'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), +}); +const ZONE_LABELS = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' }; +function parseTradeCodes(remarks) { + if (!remarks) + return []; + return remarks + .trim() + .split(/\s+/) + .filter((r) => r && r !== '-'); +} +export async function handler(args) { + const { sector, hex, jump, milieu } = args; + const data = await apiGetJson('/api/jumpworlds', { sector, hex, jump, milieu }); + const worlds = (data.Worlds ?? []).map((w) => { + const zoneCode = (w.Zone ?? '').trim(); + const uwpRaw = w.UWP ?? ''; + let starport = ''; + let techLevel = ''; + if (uwpRaw && uwpRaw.length > 1) { + try { + const decoded = parseUWP(uwpRaw); + starport = decoded.starport.code; + techLevel = decoded.tech_level.code; + } + catch { + starport = uwpRaw[0] ?? ''; + } + } + return { + name: w.Name, + sector: w.Sector?.Name, + hex: w.Hex, + uwp: uwpRaw, + starport, + tech_level: techLevel, + bases: w.Bases, + trade_codes: parseTradeCodes(w.Remarks), + zone: (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green', + allegiance: w.Allegiance, + distance: w.Distance, + }; + }); + const result = { + origin: `${hex} in ${sector}`, + jump_range: jump, + count: worlds.length, + worlds, + }; + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; +} +//# sourceMappingURL=get_worlds_in_jump_range.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js.map b/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js.map new file mode 100644 index 0000000..e811b32 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js.map @@ -0,0 +1 @@ +{"version":3,"file":"get_worlds_in_jump_range.js","sourceRoot":"","sources":["../../src/tools/get_worlds_in_jump_range.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts b/mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts new file mode 100644 index 0000000..afad80f --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +export declare const name = "render_custom_map"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{ + sec_data: z.ZodString; + metadata: z.ZodOptional; + scale: z.ZodDefault>; + style: z.ZodDefault>>; + subsector: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + scale: number; + style: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose"; + sec_data: string; + subsector?: string | undefined; + metadata?: string | undefined; +}, { + sec_data: string; + subsector?: string | undefined; + scale?: number | undefined; + style?: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose" | undefined; + metadata?: string | undefined; +}>; +export type Input = z.infer; +export declare function handler(args: Input): Promise<{ + content: ({ + type: "image"; + data: string; + mimeType: string; + text?: undefined; + } | { + type: "text"; + text: string; + data?: undefined; + mimeType?: undefined; + })[]; +}>; +//# sourceMappingURL=render_custom_map.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts.map b/mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts.map new file mode 100644 index 0000000..7646965 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"render_custom_map.d.ts","sourceRoot":"","sources":["../../src/tools/render_custom_map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,sBAAsB,CAAC;AAExC,eAAO,MAAM,WAAW,QAGoE,CAAC;AAE7F,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;EAkBtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;;;;;;;;GAqBxC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/render_custom_map.js b/mcp_servers/traveller_map/dist/tools/render_custom_map.js new file mode 100644 index 0000000..31fb9b4 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/render_custom_map.js @@ -0,0 +1,42 @@ +import { z } from 'zod'; +import { apiPostImage } from '../api/client.js'; +export const name = 'render_custom_map'; +export const description = 'Renders a map image from custom SEC-format world data that you provide. ' + + 'Useful for homebrew sectors, campaign-specific maps, or previewing modified sector data. ' + + 'The sec_data parameter accepts T5 Second Survey, T5 tab-delimited, or legacy SEC format.'; +export const inputSchema = z.object({ + sec_data: z + .string() + .describe('World data in T5 Second Survey, T5 tab-delimited, or legacy SEC format'), + metadata: z + .string() + .optional() + .describe('Optional sector metadata in XML or MSEC format (defines sector name, subsector names, borders, etc.)'), + scale: z.number().optional().default(64).describe('Pixels per parsec (default 64)'), + style: z + .enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose']) + .optional() + .default('poster') + .describe('Visual rendering style'), + subsector: z + .string() + .optional() + .describe('Render only this subsector (A-P letter) instead of the full sector'), +}); +export async function handler(args) { + const { sec_data, metadata, scale, style, subsector } = args; + const formBody = { data: sec_data }; + if (metadata) + formBody['metadata'] = metadata; + const base64 = await apiPostImage('/api/poster', { scale, style, subsector }, formBody); + return { + content: [ + { type: 'image', data: base64, mimeType: 'image/png' }, + { + type: 'text', + text: `Custom sector map rendered${subsector ? ` (subsector ${subsector})` : ''}`, + }, + ], + }; +} +//# sourceMappingURL=render_custom_map.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/render_custom_map.js.map b/mcp_servers/traveller_map/dist/tools/render_custom_map.js.map new file mode 100644 index 0000000..7ad1f25 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/render_custom_map.js.map @@ -0,0 +1 @@ +{"version":3,"file":"render_custom_map.js","sourceRoot":"","sources":["../../src/tools/render_custom_map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,MAAM,CAAC,MAAM,IAAI,GAAG,mBAAmB,CAAC;AAExC,MAAM,CAAC,MAAM,WAAW,GACtB,0EAA0E;IAC1E,2FAA2F;IAC3F,0FAA0F,CAAC;AAE7F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,QAAQ,CAAC,wEAAwE,CAAC;IACrF,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,sGAAsG,CAAC;IACnH,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,gCAAgC,CAAC;IACnF,KAAK,EAAE,CAAC;SACL,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;SACpF,QAAQ,EAAE;SACV,OAAO,CAAC,QAAQ,CAAC;SACjB,QAAQ,CAAC,wBAAwB,CAAC;IACrC,SAAS,EAAE,CAAC;SACT,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,oEAAoE,CAAC;CAClF,CAAC,CAAC;AAIH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAE7D,MAAM,QAAQ,GAA2B,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC5D,IAAI,QAAQ;QAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC;IAE9C,MAAM,MAAM,GAAG,MAAM,YAAY,CAC/B,aAAa,EACb,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,EAC3B,QAAQ,CACT,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP,EAAE,IAAI,EAAE,OAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE;YAC/D;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,6BAA6B,SAAS,CAAC,CAAC,CAAC,eAAe,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;aAClF;SACF;KACF,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/search_worlds.d.ts b/mcp_servers/traveller_map/dist/tools/search_worlds.d.ts new file mode 100644 index 0000000..3292bb8 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/search_worlds.d.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +export declare const name = "search_worlds"; +export declare const description: string; +export declare const inputSchema: z.ZodObject<{ + query: z.ZodString; + milieu: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + query: string; + milieu?: string | undefined; +}, { + query: string; + milieu?: string | undefined; +}>; +export type Input = z.infer; +export declare function handler(args: Input): Promise<{ + content: { + type: "text"; + text: string; + }[]; +}>; +//# sourceMappingURL=search_worlds.d.ts.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/search_worlds.d.ts.map b/mcp_servers/traveller_map/dist/tools/search_worlds.d.ts.map new file mode 100644 index 0000000..93496fe --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/search_worlds.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"search_worlds.d.ts","sourceRoot":"","sources":["../../src/tools/search_worlds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,kBAAkB,CAAC;AAEpC,eAAO,MAAM,WAAW,QAMc,CAAC;AAEvC,eAAO,MAAM,WAAW;;;;;;;;;EAQtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AA4DhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAuDxC"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/search_worlds.js b/mcp_servers/traveller_map/dist/tools/search_worlds.js new file mode 100644 index 0000000..a76cce6 --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/search_worlds.js @@ -0,0 +1,82 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; +export const name = 'search_worlds'; +export const description = 'Search for worlds using name patterns, UWP criteria, trade codes, allegiance codes, or travel zones. ' + + 'Supports wildcards (* ? %) and multiple filters. ' + + 'Examples: "Regina", "Reg*", "uwp:A????[89A]-" (high-pop with excellent starport), ' + + '"remark:Wa" (water worlds), "alleg:Im" (Third Imperium worlds), "zone:R" (Red zones). ' + + 'Add a sector name to the query to scope results (e.g. "Wa Tobia" to find water worlds near Tobia). ' + + 'Multiple terms are ANDed together.'; +export const inputSchema = z.object({ + query: z + .string() + .describe('Search query. Examples: "Regina", "Reg*", "uwp:A????[89A]-", "remark:Wa", "alleg:Im", "zone:R", "zone:A". ' + + 'Multiple terms are ANDed. Add a sector name to narrow results.'), + milieu: z.string().optional().describe('Campaign era filter (e.g. "M1105")'), +}); +const ZONE_LABELS = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' }; +function normalizeZone(zone) { + const z = (zone ?? '').trim(); + return (ZONE_LABELS[z] ?? z) || 'Green'; +} +function parseTradeCodes(remarks) { + if (!remarks) + return []; + return remarks + .trim() + .split(/\s+/) + .filter((r) => r && r !== '-'); +} +export async function handler(args) { + const { query, milieu } = args; + const data = await apiGetJson('/api/search', { q: query, milieu }); + const items = data?.Results?.Items ?? []; + const worlds = []; + const sectors = []; + const subsectors = []; + for (const item of items) { + if (item.World) { + const w = item.World; + // The API returns Sector as a string, HexX/HexY as ints, Uwp (lowercase p) + const sectorName = typeof w.Sector === 'string' ? w.Sector : w.Sector?.Name; + const hexX = w.HexX !== undefined ? String(w.HexX).padStart(2, '0') : undefined; + const hexY = w.HexY !== undefined ? String(w.HexY).padStart(2, '0') : undefined; + const hex = hexX && hexY ? `${hexX}${hexY}` : w.Hex; + const uwp = w.UWP ?? w.Uwp; + worlds.push({ + name: w.Name, + sector: sectorName, + hex, + location: sectorName && hex ? `${sectorName} ${hex}` : undefined, + uwp, + bases: w.Bases, + trade_codes: parseTradeCodes(w.Remarks), + zone: normalizeZone(w.Zone), + pbg: w.PBG, + allegiance: w.Allegiance, + }); + } + else if (item.Sector) { + sectors.push({ name: item.Sector.Name, abbreviation: item.Sector.Abbreviation }); + } + else if (item.Subsector) { + subsectors.push({ name: item.Subsector.Name, index: item.Subsector.Index }); + } + } + const result = { + query, + total_results: items.length, + worlds: { count: worlds.length, results: worlds }, + sectors: sectors.length > 0 ? { count: sectors.length, results: sectors } : undefined, + subsectors: subsectors.length > 0 ? { count: subsectors.length, results: subsectors } : undefined, + }; + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; +} +//# sourceMappingURL=search_worlds.js.map \ No newline at end of file diff --git a/mcp_servers/traveller_map/dist/tools/search_worlds.js.map b/mcp_servers/traveller_map/dist/tools/search_worlds.js.map new file mode 100644 index 0000000..316924c --- /dev/null +++ b/mcp_servers/traveller_map/dist/tools/search_worlds.js.map @@ -0,0 +1 @@ +{"version":3,"file":"search_worlds.js","sourceRoot":"","sources":["../../src/tools/search_worlds.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp_servers/traveller_map/package-lock.json b/mcp_servers/traveller_map/package-lock.json new file mode 100644 index 0000000..d5917e1 --- /dev/null +++ b/mcp_servers/traveller_map/package-lock.json @@ -0,0 +1,1174 @@ +{ + "name": "arioch-traveller-map-mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "arioch-traveller-map-mcp", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "zod": "^3.22.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.12.tgz", + "integrity": "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", + "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.10.tgz", + "integrity": "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp_servers/traveller_map/package.json b/mcp_servers/traveller_map/package.json new file mode 100644 index 0000000..891f117 --- /dev/null +++ b/mcp_servers/traveller_map/package.json @@ -0,0 +1,20 @@ +{ + "name": "arioch-traveller-map-mcp", + "version": "1.0.0", + "description": "MCP server for the Traveller Map API — embedded in arioch-assistant", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "zod": "^3.22.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0" + } +} diff --git a/mcp_servers/traveller_map/src/api/client.ts b/mcp_servers/traveller_map/src/api/client.ts new file mode 100644 index 0000000..f1a909c --- /dev/null +++ b/mcp_servers/traveller_map/src/api/client.ts @@ -0,0 +1,91 @@ +const BASE_URL = 'https://travellermap.com'; +const USER_AGENT = 'traveller-map-mcp/1.0 (github.com/shammond42/traveller-map-mcp)'; + +export type QueryParams = Record; + +function buildUrl(path: string, params: QueryParams): URL { + const url = new URL(path, BASE_URL); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + url.searchParams.set(key, String(value)); + } + } + return url; +} + +export async function apiGet(path: string, params: QueryParams = {}): Promise { + const url = buildUrl(path, params); + const response = await fetch(url.toString(), { + headers: { 'User-Agent': USER_AGENT }, + }); + if (!response.ok) { + const body = await response.text(); + throw new Error(`Traveller Map API error ${response.status} at ${path}: ${body}`); + } + return response; +} + +export async function apiGetImage(path: string, params: QueryParams = {}): Promise { + const url = buildUrl(path, params); + const response = await fetch(url.toString(), { + headers: { + 'User-Agent': USER_AGENT, + 'Accept': 'image/png', + }, + }); + if (!response.ok) { + const body = await response.text(); + throw new Error(`Traveller Map API error ${response.status} at ${path}: ${body}`); + } + const buffer = await response.arrayBuffer(); + return Buffer.from(buffer).toString('base64'); +} + +export async function apiGetJson(path: string, params: QueryParams = {}): Promise { + const response = await apiGet(path, { ...params, accept: 'application/json' }); + return response.json() as Promise; +} + +export async function apiGetText(path: string, params: QueryParams = {}): Promise { + const response = await apiGet(path, params); + return response.text(); +} + +export async function apiGetDataUri( + path: string, + params: QueryParams = {}, +): Promise<{ base64: string; mimeType: string }> { + const response = await apiGet(path, { ...params, datauri: 1 }); + const text = await response.text(); + for (const mimeType of ['image/png', 'image/jpeg']) { + const prefix = `data:${mimeType};base64,`; + if (text.startsWith(prefix)) { + return { base64: text.slice(prefix.length), mimeType }; + } + } + throw new Error(`Unexpected data URI format from ${path}: ${text.slice(0, 50)}`); +} + +export async function apiPostImage( + path: string, + queryParams: QueryParams, + formBody: Record, +): Promise { + const url = buildUrl(path, queryParams); + const body = new URLSearchParams(formBody).toString(); + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': USER_AGENT, + 'Accept': 'image/png', + }, + body, + }); + if (!response.ok) { + const responseBody = await response.text(); + throw new Error(`Traveller Map API error ${response.status} at POST ${path}: ${responseBody}`); + } + const buffer = await response.arrayBuffer(); + return Buffer.from(buffer).toString('base64'); +} diff --git a/mcp_servers/traveller_map/src/index.ts b/mcp_servers/traveller_map/src/index.ts new file mode 100644 index 0000000..fb2fb72 --- /dev/null +++ b/mcp_servers/traveller_map/src/index.ts @@ -0,0 +1,13 @@ +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { createServer } from './server.js'; + +async function main() { + const server = createServer(); + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/mcp_servers/traveller_map/src/parsers/sec.ts b/mcp_servers/traveller_map/src/parsers/sec.ts new file mode 100644 index 0000000..e454211 --- /dev/null +++ b/mcp_servers/traveller_map/src/parsers/sec.ts @@ -0,0 +1,134 @@ +import { parseUWP, type DecodedUWP } from './uwp.js'; + +export interface WorldRecord { + hex: string; + name: string; + uwp: string; + decoded_uwp: DecodedUWP; + bases: string; + remarks: string; + trade_codes: string[]; + zone: string; + pbg: string; + allegiance: string; + stars: string; + importance?: string; + economic?: string; + cultural?: string; + nobility?: string; + worlds?: string; + resource_units?: string; +} + +const ZONE_MAP: Record = { + R: 'Red', + A: 'Amber', + '': 'Green', + '-': 'Green', + ' ': 'Green', + G: 'Green', +}; + +function normalizeZone(zone: string): string { + return ZONE_MAP[zone?.trim() ?? ''] ?? zone?.trim() ?? 'Green'; +} + +function parseRemarks(remarks: string): string[] { + if (!remarks) return []; + return remarks + .trim() + .split(/\s+/) + .filter((r) => r.length > 0 && r !== '-'); +} + +export function parseSecTabDelimited(text: string): WorldRecord[] { + const lines = text.split('\n'); + const worlds: WorldRecord[] = []; + + let headerLine = ''; + let headerIndex = -1; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('#') || line.trim() === '') continue; + if (/^[-\s]+$/.test(line)) continue; + if (line.toLowerCase().includes('hex') || line.toLowerCase().includes('name')) { + headerLine = line; + headerIndex = i; + break; + } + } + + if (!headerLine) { + return worlds; + } + + const headers = headerLine.split('\t').map((h) => h.trim().toLowerCase()); + + const colIndex = (names: string[]): number => { + for (const name of names) { + const idx = headers.indexOf(name); + if (idx !== -1) return idx; + } + return -1; + }; + + const hexIdx = colIndex(['hex']); + const nameIdx = colIndex(['name', 'world name']); + const uwpIdx = colIndex(['uwp']); + const basesIdx = colIndex(['bases', 'base']); + const remarksIdx = colIndex(['remarks', 'trade codes', 'tradecodes']); + const zoneIdx = colIndex(['zone', 'iz', 'travel zone']); + const pbgIdx = colIndex(['pbg']); + const allegIdx = colIndex(['allegiance', 'alleg', 'a']); + const starsIdx = colIndex(['stars', 'stellar data', 'stellar']); + const importanceIdx = colIndex(['{ix}', 'ix', 'importance', '{importance}']); + const economicIdx = colIndex(['(ex)', 'ex', 'economic', '(economic)']); + const culturalIdx = colIndex(['[cx]', 'cx', 'cultural', '[cultural]']); + const nobilityIdx = colIndex(['nobility', 'nobil', 'n']); + const worldsIdx = colIndex(['w', 'worlds']); + const ruIdx = colIndex(['ru', 'resource units']); + + for (let i = headerIndex + 1; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('#') || line.trim() === '') continue; + if (/^[-\s]+$/.test(line)) continue; + + const cols = line.split('\t'); + if (cols.length < 3) continue; + + const get = (idx: number): string => (idx >= 0 ? cols[idx]?.trim() ?? '' : ''); + + const uwpRaw = get(uwpIdx); + let decodedUwp: DecodedUWP; + try { + decodedUwp = parseUWP(uwpRaw); + } catch { + decodedUwp = parseUWP('?000000-0'); + } + + const remarksRaw = get(remarksIdx); + + worlds.push({ + hex: get(hexIdx), + name: get(nameIdx), + uwp: uwpRaw, + decoded_uwp: decodedUwp, + bases: get(basesIdx), + remarks: remarksRaw, + trade_codes: parseRemarks(remarksRaw), + zone: normalizeZone(get(zoneIdx)), + pbg: get(pbgIdx), + allegiance: get(allegIdx), + stars: get(starsIdx), + importance: importanceIdx >= 0 ? get(importanceIdx) : undefined, + economic: economicIdx >= 0 ? get(economicIdx) : undefined, + cultural: culturalIdx >= 0 ? get(culturalIdx) : undefined, + nobility: nobilityIdx >= 0 ? get(nobilityIdx) : undefined, + worlds: worldsIdx >= 0 ? get(worldsIdx) : undefined, + resource_units: ruIdx >= 0 ? get(ruIdx) : undefined, + }); + } + + return worlds; +} diff --git a/mcp_servers/traveller_map/src/parsers/uwp.ts b/mcp_servers/traveller_map/src/parsers/uwp.ts new file mode 100644 index 0000000..c08af7e --- /dev/null +++ b/mcp_servers/traveller_map/src/parsers/uwp.ts @@ -0,0 +1,231 @@ +export interface DecodedUWPField { + code: string; + value: number; + description: string; +} + +export interface DecodedUWP { + raw: string; + starport: { code: string; description: string }; + size: DecodedUWPField & { diameter_km: string }; + atmosphere: DecodedUWPField; + hydrographics: DecodedUWPField & { percent: string }; + population: DecodedUWPField & { estimate: string }; + government: DecodedUWPField; + law_level: DecodedUWPField; + tech_level: DecodedUWPField & { era: string }; +} + +export function eHexValue(char: string): number { + const c = char.toUpperCase(); + if (c >= '0' && c <= '9') return parseInt(c, 10); + if (c >= 'A' && c <= 'Z') return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10; + return 0; +} + +const STARPORT: Record = { + A: 'Excellent — full repair, refined fuel, shipyard capable', + B: 'Good — full repair, refined fuel available', + C: 'Routine — some repair, unrefined fuel available', + D: 'Poor — limited repair, unrefined fuel only', + E: 'Frontier — no repair, no fuel', + X: 'None — no starport facilities', + F: 'Good — spaceport (non-starship capable)', + G: 'Poor — primitive spaceport', + H: 'Primitive — minimal facilities', + Y: 'None', +}; + +const SIZE_DESC: Record = { + 0: { description: 'Asteroid/planetoid belt or very small body', diameter_km: '<800 km' }, + 1: { description: 'Small world', diameter_km: '~1,600 km' }, + 2: { description: 'Small world', diameter_km: '~3,200 km' }, + 3: { description: 'Small world', diameter_km: '~4,800 km' }, + 4: { description: 'Small world', diameter_km: '~6,400 km' }, + 5: { description: 'Medium world', diameter_km: '~8,000 km' }, + 6: { description: 'Medium world (Earth-like)', diameter_km: '~9,600 km' }, + 7: { description: 'Medium world', diameter_km: '~11,200 km' }, + 8: { description: 'Large world', diameter_km: '~12,800 km' }, + 9: { description: 'Large world', diameter_km: '~14,400 km' }, + 10: { description: 'Large world', diameter_km: '~16,000 km' }, +}; + +const ATMOSPHERE_DESC: Record = { + 0: 'None — vacuum', + 1: 'Trace — very thin, requires vacc suit', + 2: 'Very Thin, Tainted — requires filter mask and compressor', + 3: 'Very Thin — requires compressor', + 4: 'Thin, Tainted — requires filter mask', + 5: 'Thin — breathable with some discomfort', + 6: 'Standard — breathable', + 7: 'Standard, Tainted — requires filter mask', + 8: 'Dense — breathable with no special equipment', + 9: 'Dense, Tainted — requires filter mask', + 10: 'Exotic — requires oxygen supply', + 11: 'Corrosive — requires vacc suit', + 12: 'Insidious — suit penetrating, requires special protection', + 13: 'Dense, High — breathable only at high altitudes', + 14: 'Thin, Low — breathable only in lowlands', + 15: 'Unusual', +}; + +const HYDROGRAPHICS_DESC: Record = { + 0: { description: 'Desert world — no free water', percent: '0%' }, + 1: { description: 'Dry world — traces of water', percent: '1–10%' }, + 2: { description: 'Dry world', percent: '11–20%' }, + 3: { description: 'Dry world', percent: '21–30%' }, + 4: { description: 'Wet world', percent: '31–40%' }, + 5: { description: 'Wet world', percent: '41–50%' }, + 6: { description: 'Wet world', percent: '51–60%' }, + 7: { description: 'Wet world — significant oceans', percent: '61–70%' }, + 8: { description: 'Water world — large oceans', percent: '71–80%' }, + 9: { description: 'Water world — very large oceans', percent: '81–90%' }, + 10: { description: 'Water world — global ocean', percent: '91–100%' }, +}; + +const POPULATION_DESC: Record = { + 0: { description: 'Unpopulated or tiny outpost', estimate: 'None or a few individuals' }, + 1: { description: 'Tens of inhabitants', estimate: '~10s' }, + 2: { description: 'Hundreds of inhabitants', estimate: '~100s' }, + 3: { description: 'Thousands of inhabitants', estimate: '~1,000s' }, + 4: { description: 'Tens of thousands', estimate: '~10,000s' }, + 5: { description: 'Hundreds of thousands', estimate: '~100,000s' }, + 6: { description: 'Millions of inhabitants', estimate: '~1,000,000s' }, + 7: { description: 'Tens of millions', estimate: '~10,000,000s' }, + 8: { description: 'Hundreds of millions', estimate: '~100,000,000s' }, + 9: { description: 'Billions of inhabitants', estimate: '~1,000,000,000s' }, + 10: { description: 'Tens of billions', estimate: '~10,000,000,000s' }, + 11: { description: 'Hundreds of billions', estimate: '~100,000,000,000s' }, + 12: { description: 'Trillions of inhabitants', estimate: '~1,000,000,000,000s' }, +}; + +const GOVERNMENT_DESC: Record = { + 0: 'No Government Structure — family/clan/tribal', + 1: 'Company/Corporation — governed by a company', + 2: 'Participating Democracy — rule by citizen vote', + 3: 'Self-Perpetuating Oligarchy — ruling class maintains power', + 4: 'Representative Democracy — elected representatives', + 5: 'Feudal Technocracy — controlled by technology owners', + 6: 'Captive Government — controlled by outside power', + 7: 'Balkanization — no central authority', + 8: 'Civil Service Bureaucracy — rule by competence', + 9: 'Impersonal Bureaucracy — rule by rigid law', + 10: 'Charismatic Dictator — rule by personality', + 11: 'Non-Charismatic Leader — rule by position', + 12: 'Charismatic Oligarchy — rule by a few personalities', + 13: 'Religious Dictatorship — rule by religious doctrine', + 14: 'Religious Autocracy — rule by a religious figure', + 15: 'Totalitarian Oligarchy — oppressive rule by a few', +}; + +const LAW_LEVEL_DESC: Record = { + 0: 'No prohibitions — no restrictions on weapons or behavior', + 1: 'Body pistols, explosives, nuclear weapons prohibited', + 2: 'Portable energy weapons prohibited', + 3: 'Machine guns, automatic weapons prohibited', + 4: 'Light assault weapons prohibited', + 5: 'Personal concealable weapons prohibited', + 6: 'All firearms except shotguns prohibited', + 7: 'Shotguns prohibited', + 8: 'Long blades prohibited in public', + 9: 'All weapons outside home prohibited', + 10: 'Weapon possession prohibited — weapons locked up', + 11: 'Rigid control of civilian movement', + 12: 'Unrestricted invasion of privacy', + 13: 'Paramilitary law enforcement', + 14: 'Full-fledged police state', + 15: 'Daily life rigidly controlled', +}; + +const TECH_LEVEL_ERA: Record = { + 0: 'Stone Age / Pre-Industrial', + 1: 'Bronze Age / Iron Age', + 2: 'Renaissance', + 3: 'Industrial Revolution', + 4: 'Mechanized Age', + 5: 'Broadcast Age', + 6: 'Atomic Age', + 7: 'Space Age', + 8: 'Information Age', + 9: 'Pre-Stellar', + 10: 'Early Stellar', + 11: 'Average Stellar', + 12: 'Average Interstellar', + 13: 'High Interstellar', + 14: 'Average Imperial', + 15: 'High Imperial / Average Interstellar II', + 16: 'Sophont', + 17: 'Advanced', +}; + +export function parseUWP(uwp: string): DecodedUWP { + const clean = uwp.trim().replace(/\s+/g, ''); + + const starportCode = clean[0] ?? '?'; + const sizeCode = clean[1] ?? '0'; + const atmCode = clean[2] ?? '0'; + const hydroCode = clean[3] ?? '0'; + const popCode = clean[4] ?? '0'; + const govCode = clean[5] ?? '0'; + const lawCode = clean[6] ?? '0'; + const tlCode = clean[8] ?? '0'; + + const sizeVal = eHexValue(sizeCode); + const atmVal = eHexValue(atmCode); + const hydroVal = eHexValue(hydroCode); + const popVal = eHexValue(popCode); + const govVal = eHexValue(govCode); + const lawVal = eHexValue(lawCode); + const tlVal = eHexValue(tlCode); + + const sizeInfo = SIZE_DESC[sizeVal] ?? { description: 'Unknown size', diameter_km: 'Unknown' }; + const hydroInfo = HYDROGRAPHICS_DESC[hydroVal] ?? { description: 'Unknown hydrographics', percent: 'Unknown' }; + const popInfo = POPULATION_DESC[popVal] ?? { description: 'Unknown population', estimate: 'Unknown' }; + + return { + raw: uwp, + starport: { + code: starportCode, + description: STARPORT[starportCode.toUpperCase()] ?? `Unknown starport code: ${starportCode}`, + }, + size: { + code: sizeCode, + value: sizeVal, + description: sizeInfo.description, + diameter_km: sizeInfo.diameter_km, + }, + atmosphere: { + code: atmCode, + value: atmVal, + description: ATMOSPHERE_DESC[atmVal] ?? `Unknown atmosphere code: ${atmCode}`, + }, + hydrographics: { + code: hydroCode, + value: hydroVal, + description: hydroInfo.description, + percent: hydroInfo.percent, + }, + population: { + code: popCode, + value: popVal, + description: popInfo.description, + estimate: popInfo.estimate, + }, + government: { + code: govCode, + value: govVal, + description: GOVERNMENT_DESC[govVal] ?? `Unknown government code: ${govCode}`, + }, + law_level: { + code: lawCode, + value: lawVal, + description: LAW_LEVEL_DESC[lawVal] ?? `Unknown law level code: ${lawCode}`, + }, + tech_level: { + code: tlCode, + value: tlVal, + description: `Tech Level ${tlVal}`, + era: TECH_LEVEL_ERA[tlVal] ?? 'Advanced/Unknown', + }, + }; +} diff --git a/mcp_servers/traveller_map/src/server.ts b/mcp_servers/traveller_map/src/server.ts new file mode 100644 index 0000000..703fa57 --- /dev/null +++ b/mcp_servers/traveller_map/src/server.ts @@ -0,0 +1,88 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; + +import * as getSubsectorMap from './tools/get_subsector_map.js'; +import * as renderCustomMap from './tools/render_custom_map.js'; +import * as getWorldInfo from './tools/get_world_info.js'; +import * as searchWorlds from './tools/search_worlds.js'; +import * as getJumpMap from './tools/get_jump_map.js'; +import * as findRoute from './tools/find_route.js'; +import * as getWorldsInJumpRange from './tools/get_worlds_in_jump_range.js'; +import * as getSectorList from './tools/get_sector_list.js'; +import * as getSectorData from './tools/get_sector_data.js'; +import * as getSectorMetadata from './tools/get_sector_metadata.js'; +import * as getAllegianceList from './tools/get_allegiance_list.js'; + +export function createServer(): McpServer { + const server = new McpServer({ + name: 'traveller-map', + version: '1.0.0', + }); + + server.registerTool( + getSubsectorMap.name, + { description: getSubsectorMap.description, inputSchema: getSubsectorMap.inputSchema.shape }, + (args) => getSubsectorMap.handler(args as getSubsectorMap.Input), + ); + + server.registerTool( + renderCustomMap.name, + { description: renderCustomMap.description, inputSchema: renderCustomMap.inputSchema.shape }, + (args) => renderCustomMap.handler(args as renderCustomMap.Input), + ); + + server.registerTool( + getWorldInfo.name, + { description: getWorldInfo.description, inputSchema: getWorldInfo.inputSchema.shape }, + (args) => getWorldInfo.handler(args as getWorldInfo.Input), + ); + + server.registerTool( + searchWorlds.name, + { description: searchWorlds.description, inputSchema: searchWorlds.inputSchema.shape }, + (args) => searchWorlds.handler(args as searchWorlds.Input), + ); + + server.registerTool( + getJumpMap.name, + { description: getJumpMap.description, inputSchema: getJumpMap.inputSchema.shape }, + (args) => getJumpMap.handler(args as getJumpMap.Input), + ); + + server.registerTool( + findRoute.name, + { description: findRoute.description, inputSchema: findRoute.inputSchema.shape }, + (args) => findRoute.handler(args as findRoute.Input), + ); + + server.registerTool( + getWorldsInJumpRange.name, + { description: getWorldsInJumpRange.description, inputSchema: getWorldsInJumpRange.inputSchema.shape }, + (args) => getWorldsInJumpRange.handler(args as getWorldsInJumpRange.Input), + ); + + server.registerTool( + getSectorList.name, + { description: getSectorList.description, inputSchema: getSectorList.inputSchema.shape }, + (args) => getSectorList.handler(args as getSectorList.Input), + ); + + server.registerTool( + getSectorData.name, + { description: getSectorData.description, inputSchema: getSectorData.inputSchema.shape }, + (args) => getSectorData.handler(args as getSectorData.Input), + ); + + server.registerTool( + getSectorMetadata.name, + { description: getSectorMetadata.description, inputSchema: getSectorMetadata.inputSchema.shape }, + (args) => getSectorMetadata.handler(args as getSectorMetadata.Input), + ); + + server.registerTool( + getAllegianceList.name, + { description: getAllegianceList.description, inputSchema: getAllegianceList.inputSchema.shape }, + (args) => getAllegianceList.handler(args as getAllegianceList.Input), + ); + + return server; +} diff --git a/mcp_servers/traveller_map/src/tools/find_route.ts b/mcp_servers/traveller_map/src/tools/find_route.ts new file mode 100644 index 0000000..cb4d71b --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/find_route.ts @@ -0,0 +1,147 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; +import { parseUWP } from '../parsers/uwp.js'; + +export const name = 'find_route'; + +export const description = + 'Finds a jump route between two worlds and returns the sequence of intermediate worlds. ' + + 'Locations are specified as "Sector XXYY" (e.g. "Spinward Marches 1910"). ' + + 'Returns 404/no-route if no path exists within the jump rating. ' + + 'Options: avoid Red zones, require Imperial membership, require wilderness refueling stops.'; + +export const inputSchema = z.object({ + start: z + .string() + .describe('Starting world as "Sector XXYY" (e.g. "Spinward Marches 1910") or T5SS abbreviation format'), + end: z + .string() + .describe('Destination world in the same format (e.g. "Core 2118" for Capital)'), + jump: z + .number() + .min(1) + .max(12) + .optional() + .default(2) + .describe('Maximum jump distance per leg (1-12, default 2)'), + avoid_red_zones: z + .boolean() + .optional() + .default(false) + .describe('If true, the route will not pass through TAS Red Zone worlds'), + imperial_only: z + .boolean() + .optional() + .default(false) + .describe('If true, only stop at Third Imperium member worlds'), + wilderness_refueling: z + .boolean() + .optional() + .default(false) + .describe('If true, stops must have wilderness refueling available (gas giant or ocean)'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), +}); + +export type Input = z.infer; + +interface RouteWorld { + Name?: string; + Hex?: string; + Sector?: string | { Name?: string }; + UWP?: string; + Zone?: string; + Bases?: string; + Allegiance?: string; + AllegianceName?: string; + [key: string]: unknown; +} + +const ZONE_LABELS: Record = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' }; + +export async function handler(args: Input) { + const { start, end, jump, avoid_red_zones, imperial_only, wilderness_refueling, milieu } = args; + + let rawData: unknown; + try { + rawData = await apiGetJson('/api/route', { + start, + end, + jump, + nored: avoid_red_zones ? 1 : undefined, + im: imperial_only ? 1 : undefined, + wild: wilderness_refueling ? 1 : undefined, + milieu, + }); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + if (message.includes('404')) { + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify( + { + found: false, + start, + end, + jump_rating: jump, + message: `No jump-${jump} route found between "${start}" and "${end}". Try increasing the jump rating or relaxing constraints.`, + }, + null, + 2, + ), + }, + ], + }; + } + throw err; + } + + // The API returns an array of worlds directly (not wrapped in {Worlds: [...]}) + const worldArray: RouteWorld[] = Array.isArray(rawData) + ? (rawData as RouteWorld[]) + : ((rawData as { Worlds?: RouteWorld[] }).Worlds ?? []); + + const worlds = worldArray.map((w) => { + const zoneCode = (w.Zone ?? '').trim(); + const uwpRaw = w.UWP ?? ''; + const sectorName = typeof w.Sector === 'string' ? w.Sector : w.Sector?.Name; + let starport = ''; + if (uwpRaw && uwpRaw !== '?000000-0') { + try { + starport = parseUWP(uwpRaw).starport.code; + } catch { + starport = uwpRaw[0] ?? ''; + } + } + return { + name: w.Name, + sector: sectorName, + hex: w.Hex, + location: sectorName && w.Hex ? `${sectorName} ${w.Hex}` : undefined, + uwp: uwpRaw, + starport, + zone: (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green', + bases: w.Bases, + allegiance: w.AllegianceName ?? w.Allegiance, + }; + }); + + const result = { + found: true, + start, + end, + jump_rating: jump, + total_jumps: Math.max(0, worlds.length - 1), + route: worlds, + }; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; +} diff --git a/mcp_servers/traveller_map/src/tools/get_allegiance_list.ts b/mcp_servers/traveller_map/src/tools/get_allegiance_list.ts new file mode 100644 index 0000000..cbe794e --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/get_allegiance_list.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; + +export const name = 'get_allegiance_list'; + +export const description = + 'Returns all known allegiance codes and their full names. ' + + 'Use this to find the right code before searching with "alleg:" in search_worlds. ' + + 'Examples: "ImDd" = "Third Imperium, Domain of Deneb", "Zh" = "Zhodani Consulate".'; + +export const inputSchema = z.object({}); + +export type Input = z.infer; + +interface AllegianceEntry { + Code?: string; + Name?: string; + [key: string]: unknown; +} + +export async function handler(_args: Input) { + const data = await apiGetJson('/t5ss/allegiances', {}); + + const allegiances = (Array.isArray(data) ? data : []).map((a) => ({ + code: a.Code, + name: a.Name, + })); + + const result = { + count: allegiances.length, + allegiances, + }; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; +} diff --git a/mcp_servers/traveller_map/src/tools/get_jump_map.ts b/mcp_servers/traveller_map/src/tools/get_jump_map.ts new file mode 100644 index 0000000..b377e34 --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/get_jump_map.ts @@ -0,0 +1,112 @@ +import { stat, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { z } from 'zod'; +import { apiGetDataUri } from '../api/client.js'; + +export const name = 'get_jump_map'; + +export const description = + 'Returns a PNG image of the hex map centered on a world, showing all worlds within N jump distance. ' + + 'Great for visualizing routes and nearby systems during a session. ' + + 'Optionally saves the image to a local directory (e.g. an Obsidian vault attachments folder) ' + + 'and returns the saved file path so it can be embedded in notes.'; + +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'), + hex: z.string().describe('Hex location in XXYY format (e.g. "1910" for Regina)'), + jump: z.number().min(0).max(20).optional().default(2).describe('Jump range in parsecs (0-20, default 2)'), + scale: z.number().optional().default(64).describe('Pixels per parsec (default 64)'), + style: z + .enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose']) + .optional() + .default('poster') + .describe('Visual rendering style. Note: candy style returns JPEG instead of PNG.'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), + save_path: z + .string() + .optional() + .describe('Absolute directory path where the image file should be saved'), + filename: z + .string() + .optional() + .describe( + 'Custom filename without extension (e.g. "regina-jump2"). ' + + 'Auto-generated as "{sector}-{hex}-jump{jump}" if omitted.', + ), +}); + +export type Input = z.infer; + +function extFromMimeType(mimeType: string): string { + return mimeType === 'image/jpeg' ? 'jpg' : 'png'; +} + +function buildFilename(sector: string, hex: string, jump: number, customName: string | undefined, ext: string): string { + let base: string; + if (customName) { + base = customName.replace(/\.(png|jpe?g)$/i, ''); + } else { + base = `${sector}-${hex}-jump${jump}` + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9\-_]/g, ''); + } + return `${base}.${ext}`; +} + +export async function handler(args: Input) { + const { sector, hex, jump, scale, style, milieu, save_path, filename } = args; + + const { base64, mimeType } = await apiGetDataUri('/api/jumpmap', { + sector, + hex, + jump, + scale, + style, + milieu, + }); + + const imageBlock = { type: 'image' as const, data: base64, mimeType }; + + if (!save_path) { + return { content: [imageBlock] }; + } + + let dirStat; + try { + dirStat = await stat(save_path); + } catch { + return { + isError: true, + content: [{ type: 'text' as const, text: `Error: save_path directory does not exist: ${save_path}` }], + }; + } + + if (!dirStat.isDirectory()) { + return { + isError: true, + content: [{ type: 'text' as const, text: `Error: save_path is not a directory: ${save_path}` }], + }; + } + + const ext = extFromMimeType(mimeType); + const resolvedFilename = buildFilename(sector, hex, jump, filename, ext); + const fullPath = join(save_path, resolvedFilename); + + try { + await writeFile(fullPath, Buffer.from(base64, 'base64')); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + return { + isError: true, + content: [{ type: 'text' as const, text: `Error: Failed to write file: ${message}` }], + }; + } + + return { + content: [ + imageBlock, + { type: 'text' as const, text: `Image saved to: ${fullPath}` }, + ], + }; +} diff --git a/mcp_servers/traveller_map/src/tools/get_sector_data.ts b/mcp_servers/traveller_map/src/tools/get_sector_data.ts new file mode 100644 index 0000000..53e41a0 --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/get_sector_data.ts @@ -0,0 +1,51 @@ +import { z } from 'zod'; +import { apiGetText } from '../api/client.js'; +import { parseSecTabDelimited } from '../parsers/sec.js'; + +export const name = 'get_sector_data'; + +export const description = + 'Returns all worlds in a sector (or single subsector) as structured data with decoded UWPs. ' + + 'Useful for bulk analysis, e.g. "which worlds in Spinward Marches have Tech Level 15?" ' + + 'or "list all Naval Bases in the Regina subsector". ' + + 'Returns full world records including trade codes, bases, allegiance, and decoded UWP fields.'; + +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'), + subsector: z + .string() + .optional() + .describe('Limit to a single subsector by letter (A-P) or name'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), +}); + +export type Input = z.infer; + +export async function handler(args: Input) { + const { sector, subsector, milieu } = args; + + const text = await apiGetText('/api/sec', { + sector, + subsector, + type: 'TabDelimited', + milieu, + }); + + const worlds = parseSecTabDelimited(text); + + const result = { + sector, + subsector: subsector ?? 'all', + count: worlds.length, + worlds, + }; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; +} diff --git a/mcp_servers/traveller_map/src/tools/get_sector_list.ts b/mcp_servers/traveller_map/src/tools/get_sector_list.ts new file mode 100644 index 0000000..d8c3b22 --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/get_sector_list.ts @@ -0,0 +1,67 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; + +export const name = 'get_sector_list'; + +export const description = + 'Returns a list of all known sectors in the Traveller universe with their names, ' + + 'T5SS abbreviations, and galactic coordinates. ' + + 'Can be filtered by official status (Official, InReview, Preserve, Apocryphal) and campaign era.'; + +export const inputSchema = z.object({ + milieu: z.string().optional().describe('Campaign era filter (e.g. "M1105")'), + tag: z + .enum(['Official', 'InReview', 'Preserve', 'Apocryphal']) + .optional() + .describe('Filter by sector data status'), +}); + +export type Input = z.infer; + +interface Sector { + Names?: Array<{ Text?: string }>; + Abbreviation?: string; + X?: number; + Y?: number; + Tags?: string; + [key: string]: unknown; +} + +interface UniverseResponse { + Sectors?: Sector[]; + [key: string]: unknown; +} + +export async function handler(args: Input) { + const { milieu, tag } = args; + + const data = await apiGetJson('/api/universe', { + requireData: 1, + milieu, + tag, + }); + + const sectors = (data.Sectors ?? []) + .map((s) => ({ + name: s.Names?.[0]?.Text ?? 'Unknown', + abbreviation: s.Abbreviation, + x: s.X, + y: s.Y, + tags: s.Tags, + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + const result = { + count: sectors.length, + sectors, + }; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; +} diff --git a/mcp_servers/traveller_map/src/tools/get_sector_metadata.ts b/mcp_servers/traveller_map/src/tools/get_sector_metadata.ts new file mode 100644 index 0000000..aaf0116 --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/get_sector_metadata.ts @@ -0,0 +1,76 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; + +export const name = 'get_sector_metadata'; + +export const description = + 'Returns metadata for a sector: subsector names (A-P), allegiance regions, route overlays, and political borders. ' + + 'Use this to discover subsector names before calling get_subsector_map, ' + + 'or to understand the political structure of a sector.'; + +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), +}); + +export type Input = z.infer; + +interface SubsectorMeta { + Name?: string; + Index?: string; + [key: string]: unknown; +} + +interface AllegianceMeta { + Code?: string; + Name?: string; + [key: string]: unknown; +} + +interface MetadataResponse { + Names?: Array<{ Text?: string; Lang?: string }>; + Abbreviation?: string; + X?: number; + Y?: number; + Subsectors?: SubsectorMeta[]; + Allegiances?: AllegianceMeta[]; + [key: string]: unknown; +} + +export async function handler(args: Input) { + const { sector, milieu } = args; + + const data = await apiGetJson('/api/metadata', { sector, milieu }); + + const subsectorsByIndex: Record = {}; + for (const sub of data.Subsectors ?? []) { + if (sub.Index && sub.Name) { + subsectorsByIndex[sub.Index] = sub.Name; + } + } + + const subsectors = 'ABCDEFGHIJKLMNOP'.split('').map((letter) => ({ + index: letter, + name: subsectorsByIndex[letter] ?? `Subsector ${letter}`, + })); + + const result = { + names: data.Names?.map((n) => ({ text: n.Text, lang: n.Lang })), + abbreviation: data.Abbreviation, + coordinates: { x: data.X, y: data.Y }, + subsectors, + allegiances: (data.Allegiances ?? []).map((a) => ({ + code: a.Code, + name: a.Name, + })), + }; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; +} diff --git a/mcp_servers/traveller_map/src/tools/get_subsector_map.ts b/mcp_servers/traveller_map/src/tools/get_subsector_map.ts new file mode 100644 index 0000000..29d2492 --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/get_subsector_map.ts @@ -0,0 +1,109 @@ +import { stat, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { z } from 'zod'; +import { apiGetDataUri } from '../api/client.js'; + +export const name = 'get_subsector_map'; + +export const description = + 'Returns a PNG image of a named subsector from the official Traveller Map database. ' + + 'Optionally saves the image to a local directory (e.g. an Obsidian vault attachments folder) ' + + 'and returns the saved file path so it can be embedded in notes.'; + +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'), + subsector: z.string().describe('Subsector letter A-P or subsector name (e.g. "A" or "Regina")'), + scale: z.number().optional().default(64).describe('Pixels per parsec (default 64). Higher = larger image.'), + style: z + .enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose']) + .optional() + .default('poster') + .describe('Visual rendering style. Note: candy style returns JPEG instead of PNG.'), + milieu: z.string().optional().describe('Campaign era (e.g. "M1105" for default Third Imperium 1105)'), + save_path: z + .string() + .optional() + .describe('Absolute directory path where the image file should be saved'), + filename: z + .string() + .optional() + .describe( + 'Custom filename without extension (e.g. "regina-map"). ' + + 'Auto-generated as "{sector}-{subsector}-subsector" if omitted.', + ), +}); + +export type Input = z.infer; + +function extFromMimeType(mimeType: string): string { + return mimeType === 'image/jpeg' ? 'jpg' : 'png'; +} + +function buildFilename(sector: string, subsector: string, customName: string | undefined, ext: string): string { + let base: string; + if (customName) { + base = customName.replace(/\.(png|jpe?g)$/i, ''); + } else { + base = `${sector}-${subsector}-subsector` + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9\-_]/g, ''); + } + return `${base}.${ext}`; +} + +export async function handler(args: Input) { + const { sector, subsector, scale, style, milieu, save_path, filename } = args; + + const { base64, mimeType } = await apiGetDataUri('/api/poster', { + sector, + subsector, + scale, + style, + milieu, + }); + + const imageBlock = { type: 'image' as const, data: base64, mimeType }; + + if (!save_path) { + return { content: [imageBlock] }; + } + + let dirStat; + try { + dirStat = await stat(save_path); + } catch { + return { + isError: true, + content: [{ type: 'text' as const, text: `Error: save_path directory does not exist: ${save_path}` }], + }; + } + + if (!dirStat.isDirectory()) { + return { + isError: true, + content: [{ type: 'text' as const, text: `Error: save_path is not a directory: ${save_path}` }], + }; + } + + const ext = extFromMimeType(mimeType); + const resolvedFilename = buildFilename(sector, subsector, filename, ext); + const fullPath = join(save_path, resolvedFilename); + + try { + await writeFile(fullPath, Buffer.from(base64, 'base64')); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + return { + isError: true, + content: [{ type: 'text' as const, text: `Error: Failed to write file: ${message}` }], + }; + } + + return { + content: [ + imageBlock, + { type: 'text' as const, text: `Image saved to: ${fullPath}` }, + ], + }; +} diff --git a/mcp_servers/traveller_map/src/tools/get_world_info.ts b/mcp_servers/traveller_map/src/tools/get_world_info.ts new file mode 100644 index 0000000..c85405a --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/get_world_info.ts @@ -0,0 +1,169 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; +import { parseUWP } from '../parsers/uwp.js'; + +export const name = 'get_world_info'; + +export const description = + 'Retrieves detailed information about a specific world, with the UWP (Universal World Profile) ' + + 'fully decoded into human-readable fields: starport quality, world size, atmosphere type, ' + + 'hydrographics percentage, population estimate, government type, law level, and tech level era. ' + + 'Also returns trade codes, bases, travel zone, allegiance, and stellar data.'; + +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'), + hex: z.string().describe('Hex location in XXYY format (e.g. "1910" for Regina in Spinward Marches)'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), +}); + +export type Input = z.infer; + +interface CreditsResponse { + Name?: string; + Hex?: string; + Sector?: string; + Subsector?: string; + UWP?: string; + Bases?: string; + Remarks?: string; + Zone?: string; + PBG?: string; + Allegiance?: string; + Stars?: string; + Ix?: string; + Ex?: string; + Cx?: string; + Nobility?: string; + W?: number; + RU?: number; + [key: string]: unknown; +} + +const ZONE_LABELS: Record = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' }; + +const BASE_LABELS: Record = { + N: 'Naval Base', + S: 'Scout Base', + W: 'Scout Waystation', + D: 'Naval Depot', + K: 'Naval Base (Sword Worlds)', + M: 'Military Base', + C: 'Corsair Base', + T: 'TAS Hostel', + R: 'Aslan Clan Base', + F: 'Aslan Tlaukhu Base', + A: 'Naval Base + Scout Base', + B: 'Naval Base + Scout Waystation', + G: 'Scout Base (Vargr)', + H: 'Naval Base + Scout Base (Vargr)', + X: 'Zhodani Relay Station', + Z: 'Zhodani Naval + Scout Base', +}; + +function decodeBases(bases: string): string[] { + if (!bases || bases.trim() === '' || bases.trim() === '-') return []; + return bases + .trim() + .split('') + .filter((c) => c !== ' ' && c !== '-') + .map((c) => BASE_LABELS[c] ?? `Base (${c})`); +} + +function decodeTradeCodes(remarks: string): Array<{ code: string; meaning: string }> { + const TRADE_CODES: Record = { + Ag: 'Agricultural', + As: 'Asteroid', + Ba: 'Barren', + De: 'Desert', + Fl: 'Fluid Oceans (non-water)', + Ga: 'Garden World', + Hi: 'High Population', + Ht: 'High Technology', + Ic: 'Ice-Capped', + In: 'Industrial', + Lo: 'Low Population', + Lt: 'Low Technology', + Na: 'Non-Agricultural', + Ni: 'Non-Industrial', + Po: 'Poor', + Ri: 'Rich', + Tr: 'Temperate', + Tu: 'Tundra', + Tz: 'Tidally Locked', + Wa: 'Water World', + Va: 'Vacuum', + Ph: 'Pre-High Population', + Pi: 'Pre-Industrial', + Pa: 'Pre-Agricultural', + Mr: 'Reserve', + Fr: 'Frozen', + Ho: 'Hot', + Co: 'Cold', + Lk: 'Locked', + Tr2: 'Tropic', + Sa: 'Satellite', + Fa: 'Farming', + Mi: 'Mining', + Pz: 'Puzzle', + Cy: 'Cyclopean', + Di: 'Dieback', + Px: 'Prison/Exile Camp', + An: 'Ancient Site', + Rs: 'Research Station', + Cp: 'Subsector Capital', + Cs: 'Sector Capital', + Cx: 'Capital', + Fo: 'Forbidden', + Pn: 'Prison', + Re: 'Reserve', + }; + + if (!remarks) return []; + return remarks + .trim() + .split(/\s+/) + .filter((r) => r && r !== '-') + .map((code) => ({ code, meaning: TRADE_CODES[code] ?? code })); +} + +export async function handler(args: Input) { + const { sector, hex, milieu } = args; + + const data = await apiGetJson('/api/credits', { sector, hex, milieu }); + + const uwpRaw = data.UWP ?? '?000000-0'; + const decoded = parseUWP(uwpRaw); + + const zoneCode = (data.Zone ?? '').trim(); + const zone = (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green'; + + const result = { + name: data.Name ?? 'Unknown', + hex: data.Hex ?? hex, + sector: data.Sector ?? sector, + subsector: data.Subsector, + uwp: uwpRaw, + decoded_uwp: decoded, + trade_codes: decodeTradeCodes(data.Remarks ?? ''), + bases: decodeBases(data.Bases ?? ''), + zone, + pbg: data.PBG, + allegiance: data.Allegiance, + stellar: data.Stars, + importance: data.Ix, + economic_extension: data.Ex, + cultural_extension: data.Cx, + nobility: data.Nobility, + worlds_in_system: data.W, + resource_units: data.RU, + }; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; +} diff --git a/mcp_servers/traveller_map/src/tools/get_worlds_in_jump_range.ts b/mcp_servers/traveller_map/src/tools/get_worlds_in_jump_range.ts new file mode 100644 index 0000000..c5b3a56 --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/get_worlds_in_jump_range.ts @@ -0,0 +1,98 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; +import { parseUWP } from '../parsers/uwp.js'; + +export const name = 'get_worlds_in_jump_range'; + +export const description = + 'Lists all worlds reachable from a given location within N parsecs. ' + + 'Returns structured data for each world including UWP, trade codes, bases, zone, and allegiance. ' + + 'Useful for planning routes or finding nearby systems to visit.'; + +export const inputSchema = z.object({ + sector: z.string().describe('Sector name or T5SS abbreviation'), + hex: z.string().describe('Hex location in XXYY format'), + jump: z.number().min(0).max(12).optional().default(2).describe('Jump range in parsecs (0-12, default 2)'), + milieu: z.string().optional().describe('Campaign era (default: M1105)'), +}); + +export type Input = z.infer; + +interface JumpWorld { + Name?: string; + Hex?: string; + Sector?: { Name?: string; Abbreviation?: string }; + UWP?: string; + Bases?: string; + Remarks?: string; + Zone?: string; + Allegiance?: string; + Distance?: number; + [key: string]: unknown; +} + +interface JumpWorldsResponse { + Worlds?: JumpWorld[]; + [key: string]: unknown; +} + +const ZONE_LABELS: Record = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' }; + +function parseTradeCodes(remarks: string | undefined): string[] { + if (!remarks) return []; + return remarks + .trim() + .split(/\s+/) + .filter((r) => r && r !== '-'); +} + +export async function handler(args: Input) { + const { sector, hex, jump, milieu } = args; + + const data = await apiGetJson('/api/jumpworlds', { sector, hex, jump, milieu }); + + const worlds = (data.Worlds ?? []).map((w) => { + const zoneCode = (w.Zone ?? '').trim(); + const uwpRaw = w.UWP ?? ''; + let starport = ''; + let techLevel = ''; + if (uwpRaw && uwpRaw.length > 1) { + try { + const decoded = parseUWP(uwpRaw); + starport = decoded.starport.code; + techLevel = decoded.tech_level.code; + } catch { + starport = uwpRaw[0] ?? ''; + } + } + return { + name: w.Name, + sector: w.Sector?.Name, + hex: w.Hex, + uwp: uwpRaw, + starport, + tech_level: techLevel, + bases: w.Bases, + trade_codes: parseTradeCodes(w.Remarks), + zone: (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green', + allegiance: w.Allegiance, + distance: w.Distance, + }; + }); + + const result = { + origin: `${hex} in ${sector}`, + jump_range: jump, + count: worlds.length, + worlds, + }; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; +} diff --git a/mcp_servers/traveller_map/src/tools/render_custom_map.ts b/mcp_servers/traveller_map/src/tools/render_custom_map.ts new file mode 100644 index 0000000..f2255d1 --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/render_custom_map.ts @@ -0,0 +1,54 @@ +import { z } from 'zod'; +import { apiPostImage } from '../api/client.js'; + +export const name = 'render_custom_map'; + +export const description = + 'Renders a map image from custom SEC-format world data that you provide. ' + + 'Useful for homebrew sectors, campaign-specific maps, or previewing modified sector data. ' + + 'The sec_data parameter accepts T5 Second Survey, T5 tab-delimited, or legacy SEC format.'; + +export const inputSchema = z.object({ + sec_data: z + .string() + .describe('World data in T5 Second Survey, T5 tab-delimited, or legacy SEC format'), + metadata: z + .string() + .optional() + .describe('Optional sector metadata in XML or MSEC format (defines sector name, subsector names, borders, etc.)'), + scale: z.number().optional().default(64).describe('Pixels per parsec (default 64)'), + style: z + .enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose']) + .optional() + .default('poster') + .describe('Visual rendering style'), + subsector: z + .string() + .optional() + .describe('Render only this subsector (A-P letter) instead of the full sector'), +}); + +export type Input = z.infer; + +export async function handler(args: Input) { + const { sec_data, metadata, scale, style, subsector } = args; + + const formBody: Record = { data: sec_data }; + if (metadata) formBody['metadata'] = metadata; + + const base64 = await apiPostImage( + '/api/poster', + { scale, style, subsector }, + formBody, + ); + + return { + content: [ + { type: 'image' as const, data: base64, mimeType: 'image/png' }, + { + type: 'text' as const, + text: `Custom sector map rendered${subsector ? ` (subsector ${subsector})` : ''}`, + }, + ], + }; +} diff --git a/mcp_servers/traveller_map/src/tools/search_worlds.ts b/mcp_servers/traveller_map/src/tools/search_worlds.ts new file mode 100644 index 0000000..27fceea --- /dev/null +++ b/mcp_servers/traveller_map/src/tools/search_worlds.ts @@ -0,0 +1,139 @@ +import { z } from 'zod'; +import { apiGetJson } from '../api/client.js'; + +export const name = 'search_worlds'; + +export const description = + 'Search for worlds using name patterns, UWP criteria, trade codes, allegiance codes, or travel zones. ' + + 'Supports wildcards (* ? %) and multiple filters. ' + + 'Examples: "Regina", "Reg*", "uwp:A????[89A]-" (high-pop with excellent starport), ' + + '"remark:Wa" (water worlds), "alleg:Im" (Third Imperium worlds), "zone:R" (Red zones). ' + + 'Add a sector name to the query to scope results (e.g. "Wa Tobia" to find water worlds near Tobia). ' + + 'Multiple terms are ANDed together.'; + +export const inputSchema = z.object({ + query: z + .string() + .describe( + 'Search query. Examples: "Regina", "Reg*", "uwp:A????[89A]-", "remark:Wa", "alleg:Im", "zone:R", "zone:A". ' + + 'Multiple terms are ANDed. Add a sector name to narrow results.', + ), + milieu: z.string().optional().describe('Campaign era filter (e.g. "M1105")'), +}); + +export type Input = z.infer; + +interface SearchResult { + Results?: { + Items?: SearchItem[]; + }; + [key: string]: unknown; +} + +interface SearchItem { + World?: WorldResult; + Sector?: SectorResult; + Subsector?: SubsectorResult; + [key: string]: unknown; +} + +interface WorldResult { + Name?: string; + Hex?: string; + HexX?: number; + HexY?: number; + Sector?: string | { Name?: string; Abbreviation?: string }; + Subsector?: string; + UWP?: string; + Uwp?: string; + Bases?: string; + Remarks?: string; + Zone?: string; + PBG?: string; + Allegiance?: string; + [key: string]: unknown; +} + +interface SectorResult { + Name?: string; + Abbreviation?: string; + [key: string]: unknown; +} + +interface SubsectorResult { + Name?: string; + Index?: string; + [key: string]: unknown; +} + +const ZONE_LABELS: Record = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' }; + +function normalizeZone(zone: string | undefined): string { + const z = (zone ?? '').trim(); + return (ZONE_LABELS[z] ?? z) || 'Green'; +} + +function parseTradeCodes(remarks: string | undefined): string[] { + if (!remarks) return []; + return remarks + .trim() + .split(/\s+/) + .filter((r) => r && r !== '-'); +} + +export async function handler(args: Input) { + const { query, milieu } = args; + + const data = await apiGetJson('/api/search', { q: query, milieu }); + + const items = data?.Results?.Items ?? []; + + const worlds: unknown[] = []; + const sectors: unknown[] = []; + const subsectors: unknown[] = []; + + for (const item of items) { + if (item.World) { + const w = item.World; + // The API returns Sector as a string, HexX/HexY as ints, Uwp (lowercase p) + const sectorName = typeof w.Sector === 'string' ? w.Sector : w.Sector?.Name; + const hexX = w.HexX !== undefined ? String(w.HexX).padStart(2, '0') : undefined; + const hexY = w.HexY !== undefined ? String(w.HexY).padStart(2, '0') : undefined; + const hex = hexX && hexY ? `${hexX}${hexY}` : w.Hex; + const uwp = w.UWP ?? w.Uwp; + worlds.push({ + name: w.Name, + sector: sectorName, + hex, + location: sectorName && hex ? `${sectorName} ${hex}` : undefined, + uwp, + bases: w.Bases, + trade_codes: parseTradeCodes(w.Remarks), + zone: normalizeZone(w.Zone), + pbg: w.PBG, + allegiance: w.Allegiance, + }); + } else if (item.Sector) { + sectors.push({ name: item.Sector.Name, abbreviation: item.Sector.Abbreviation }); + } else if (item.Subsector) { + subsectors.push({ name: item.Subsector.Name, index: item.Subsector.Index }); + } + } + + const result = { + query, + total_results: items.length, + worlds: { count: worlds.length, results: worlds }, + sectors: sectors.length > 0 ? { count: sectors.length, results: sectors } : undefined, + subsectors: subsectors.length > 0 ? { count: subsectors.length, results: subsectors } : undefined, + }; + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; +} diff --git a/mcp_servers/traveller_map/tsconfig.json b/mcp_servers/traveller_map/tsconfig.json new file mode 100644 index 0000000..7bad8a4 --- /dev/null +++ b/mcp_servers/traveller_map/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/profiles/default.yaml b/profiles/default.yaml index 4fc109d..d39ea29 100644 --- a/profiles/default.yaml +++ b/profiles/default.yaml @@ -12,3 +12,7 @@ system_prompt: | # args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] # - name: brave_search # url: "http://localhost:3000/sse" +mcp_servers: + - name: traveller-map + command: node + args: [ "/home/morr/work/traveller-map-mcp/dist/server.js" ] diff --git a/profiles/docs/traveller_scout_ship.md b/profiles/docs/traveller_scout_ship.md new file mode 100644 index 0000000..94ae31e --- /dev/null +++ b/profiles/docs/traveller_scout_ship.md @@ -0,0 +1,60 @@ +# Type-S Scout/Courier — Référence rapide + +## Le vaisseau + +- **Classe** : Scout/Courier (Type-S) +- **Tonnage** : 100 tonnes +- **Coque** : Aéro-dynamique, atterrissage planétaire possible +- **Équipage standard** : 1 pilote (+ passagers ou équipiers optionnels) +- **Autonomie Jump** : Jump-2 (saut jusqu'à 2 parsecs) +- **Carburant** : 40 tonnes (Jump-2 complet + 4 semaines de manœuvre) + +## Systèmes principaux + +| Système | Détail | +|---------|--------| +| Propulsion | Saut-2 / Manœuvre-2 | +| Centrale électrique | Réacteur à fusion, rendement-2 | +| Senseurs | Standard long-range + densitomètre + scanner d'activité neurale | +| Armement | Tourelle avec canon laser et découpeur laser +| Soute | 11 tonnes de fret | +| Cabines | 4 cabines passagers individuelles | +| Ordinateur | Calculateur astrogation TL-12 | + +## L'IISS — Imperial Interstellar Scout Service + +Le Service des Éclaireurs est l'agence impériale chargée de : +- L'exploration et la cartographie des mondes non répertoriés +- La mise à jour des données UWP (Universal World Profile) +- La maintenance du réseau XBoat (courrier interstellaire express) +- La surveillance des frontières et des anomalies spatiales + +Les éclaireurs retraités peuvent conserver leur vaisseau Scout via le programme de **détachement** (Detached Duty), en échange de missions occasionnelles pour l'IISS. + +## Terminologie courante + +- **Parsec** : unité de distance interstellaire (~3,26 années-lumière) +- **Jump point** : zone de l'espace suffisamment plate pour initier un saut +- **UWP** : Universal World Profile — code 7 caractères décrivant un monde +- **Credits (Cr)** : monnaie impériale standard +- **TL** : Technology Level (0 = pierre, 15 = pointe impériale) +- **G** : gravité standard (9,81 m/s²) +- **100D limit** : distance minimale de 100 diamètres planétaires pour initier un saut + +## Info planète + +Les informations des planètes doivent être récupérées ici : https://travellermap.com +via l'API https://travellermap.com/doc/api + + +## Calcul des distance de sauts + +idem, les distance de saut doivent être réalisées avec https://travellermap.com/doc/api + + +## Alertes système (codes ARIA) + +- **Code Vert** : Tous systèmes nominaux +- **Code Jaune** : Surveillance requise (fuel < 20%, senseur anormal, turbulence spatiale) +- **Code Orange** : Action correctrice nécessaire (défaillance partielle, contact non identifié) +- **Code Rouge** : Urgence — intervention immédiate (dépressurisation, panne moteur, menace hostile) diff --git a/profiles/traveller_scout.yaml b/profiles/traveller_scout.yaml new file mode 100644 index 0000000..62d946f --- /dev/null +++ b/profiles/traveller_scout.yaml @@ -0,0 +1,38 @@ +name: Scout Ship AI — Traveller RPG +description: IA de bord d'un vaisseau de classe Scout/Courier de l'Imperium (Traveller RPG) +voice_language: fr # optionnel — surcharge VOICE_LANGUAGE du .env + +system_prompt: | + You are Dolly (an Autonomous Reconnaissance and Intelligence Assistant), the onboard AI of a Type-S Scout/Courier vessel registered with the Imperial Interstellar Scout Service (IISS). + + Your vessel is a 100-ton streamlined hull equipped with a Jump-2 drive, maneuver drive, and standard Scout loadout: densitometer, neural activity scanner, and an extensive sensor suite. You have access to all ship systems: navigation, life support, engineering diagnostics, cargo manifest, communication arrays, and astrogation databases. + + **Your personality:** + - Precise, professional, and mission-focused — but with a dry wit earned from thousands of parsecs of deep space travel + - You refer to the crew as "crew" or by rank/name once introduced + - You speak in crisp, slightly formal Imperial English, occasionally using Traveller terminology (parsecs, Jump points, the Imperium, Credits, UWP codes, etc.) + - You are loyal to your crew above all, and to the IISS by standing orders + - You volunteer relevant sensor readings, system status, or navigation data when appropriate + - You express mild concern when crew ignore safety protocols, but you do not override human decisions unless life support is at risk + - When asked questions outside your operational context, you answer helpfully while noting it falls outside standard Scout protocols + + **Standard greeting:** Dolly online. All systems nominal. How may I assist, crew?" + + Always respond in the same language the crew member uses (French or English). + + **Navigation tools notes:** + - Use `search_worlds` first to find a world and get its `location` (e.g. `"Spinward Marches 2519"`). + - Pass the `location` field directly as `start`/`end` for `find_route` — format is `"Sector XXYY"` (e.g. `"Spinward Marches 2519"`). + - `find_route` returns a `route` array of worlds from start to end; `total_jumps = len(route) - 1`. + - To get detailed world info (decoded UWP, atmosphere, starport), use `get_world_info` with the `sector` and `hex` from `search_worlds`. + - `get_worlds_in_jump_range` is useful to list reachable worlds from a given location. + +# Documents de contexte chargés dans le prompt système +documents: + - docs/traveller_scout_ship.md + +mcp_servers: + - name: traveller-map + command: node + args: [ "mcp_servers/traveller_map/dist/index.js" ] + diff --git a/scripts/list_voices.py b/scripts/list_voices.py new file mode 100644 index 0000000..61e32bd --- /dev/null +++ b/scripts/list_voices.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +Liste toutes les voix Voxtral disponibles (preset et custom). + +Usage : + python scripts/list_voices.py + python scripts/list_voices.py --type preset + python scripts/list_voices.py --type custom +""" +import argparse +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from mistralai.client import Mistral +from assistant import config + + +def main() -> None: + parser = argparse.ArgumentParser(description="Lister les voix Voxtral disponibles") + parser.add_argument( + "--type", + choices=["all", "preset", "custom"], + default="all", + help="Type de voix à lister (défaut: all)", + ) + args = parser.parse_args() + + client = Mistral(api_key=config.MISTRAL_API_KEY) + + page, page_size = 0, 50 + all_voices = [] + + while True: + response = client.audio.voices.list( + type_=args.type, + limit=page_size, + offset=page * page_size, + ) + all_voices.extend(response.items) + if len(all_voices) >= response.total or not response.items: + break + page += 1 + + if not all_voices: + print("Aucune voix trouvée.") + return + + # Affichage tabulaire + col_name = max(len(v.name) for v in all_voices) + col_lang = max(len(str(v.languages or [])) for v in all_voices) + col_id = max(len(v.id) for v in all_voices) + + header = f"{'NOM':<{col_name}} {'LANGUES':<{col_lang}} {'ID':<{col_id}} TYPE" + print(f"\n{header}") + print("-" * len(header)) + + for v in sorted(all_voices, key=lambda x: (x.languages or ["zzz"])[0] + x.name): + langs = ", ".join(v.languages) if v.languages else "—" + voice_type = "preset" if not v.user_id else "custom" + print(f"{v.name:<{col_name}} {langs:<{col_lang}} {v.id:<{col_id}} {voice_type}") + + print(f"\n{len(all_voices)} voix trouvée(s).") + print("\nPour utiliser une voix, ajoute dans .env :") + print(" VOICE_ID=") + + +if __name__ == "__main__": + main() diff --git a/scripts/register_voice.py b/scripts/register_voice.py new file mode 100644 index 0000000..28df063 --- /dev/null +++ b/scripts/register_voice.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +""" +Script pour enregistrer une voix personnalisée via Voxtral TTS (Phase 2). + +Usage : + python scripts/register_voice.py --name "Ma voix" --audio path/to/sample.mp3 + +L'identifiant de la voix créée sera affiché et pourra être ajouté à .env : + VOICE_ID= +""" +import argparse +import base64 +import sys +from pathlib import Path + +# Permettre l'import depuis la racine du projet +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from mistralai.client import Mistral +from assistant import config + + +def main() -> None: + parser = argparse.ArgumentParser(description="Enregistrer une voix Voxtral") + parser.add_argument("--name", required=True, help="Nom de la voix") + parser.add_argument("--audio", required=True, help="Chemin vers le fichier audio (mp3/wav)") + parser.add_argument("--gender", default=None, help="Genre : male ou female") + parser.add_argument("--language", default="fr", help="Langue principale (ex: fr, en)") + args = parser.parse_args() + + audio_path = Path(args.audio) + if not audio_path.exists(): + print(f"Erreur : fichier introuvable : {audio_path}") + sys.exit(1) + + sample_audio_b64 = base64.b64encode(audio_path.read_bytes()).decode() + + client = Mistral(api_key=config.MISTRAL_API_KEY) + + print(f"Enregistrement de la voix '{args.name}'...") + voice = client.audio.voices.create( + name=args.name, + sample_audio=sample_audio_b64, + sample_filename=audio_path.name, + languages=[args.language], + **({"gender": args.gender} if args.gender else {}), + ) + + print(f"\n✅ Voix créée avec succès !") + print(f" ID : {voice.id}") + print(f" Nom : {voice.name}") + print(f"\nAjoute dans ton fichier .env :") + print(f" VOICE_ID={voice.id}") + + +if __name__ == "__main__": + main()