Add gitignor

This commit is contained in:
2026-04-07 22:06:36 +02:00
parent 49802d89d5
commit 5703cf5871
98 changed files with 5329 additions and 72 deletions

101
README.md Normal file
View File

@@ -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 <id>` | 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 23 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=<id_retourné>
```
## 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"]
```

0
assistant/__init__.py Normal file
View File

84
assistant/audio.py Normal file
View File

@@ -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

View File

@@ -1,5 +1,8 @@
import asyncio import asyncio
import queue as _queue
import re
import sys import sys
import threading
from . import llm, tts, audio, config 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}") 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: def _process_message(user_input: str) -> None:
"""Envoie un message au LLM et lit la réponse à voix haute.""" """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
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: try:
audio_bytes = tts.text_to_speech(reply) for chunk in llm.chat_stream(user_input):
audio.play_audio(audio_bytes) 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: 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: 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") print(f"\nTotal : {total} outil(s). Tapez 'mcp tools' pour les lister.\n")
def _list_profiles(profiles: list) -> None:
if not profiles: if not profiles:
print("Aucun profil disponible dans profiles/") print("Aucun profil disponible dans profiles/")
return return

20
assistant/config.py Normal file
View File

@@ -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"))

View File

@@ -1,4 +1,5 @@
import json import json
from typing import Generator
from mistralai.client import Mistral from mistralai.client import Mistral
from . import config from . import config
@@ -13,8 +14,12 @@ def reset_history() -> None:
_history.clear() _history.clear()
def chat(user_message: str) -> str: def chat_stream(user_message: str) -> Generator[str, None, None]:
"""Envoie un message au LLM, gère les appels d'outils MCP et retourne la réponse.""" """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 from . import mcp_client
_history.append({"role": "user", "content": user_message}) _history.append({"role": "user", "content": user_message})
@@ -24,20 +29,30 @@ def chat(user_message: str) -> str:
while True: while True:
messages = [{"role": "system", "content": config.SYSTEM_PROMPT}] + _history messages = [{"role": "system", "content": config.SYSTEM_PROMPT}] + _history
kwargs: dict = {"model": config.LLM_MODEL, "messages": messages} kwargs: dict = {"model": config.LLM_MODEL, "messages": messages}
if tools: if tools:
kwargs["tools"] = tools kwargs["tools"] = tools
response = _client.chat.complete(**kwargs) accumulated_content = ""
choice = response.choices[0] tool_calls_received = None
msg = choice.message
if msg.tool_calls: for event in _client.chat.stream(**kwargs):
# 1. Ajouter le message assistant (avec les appels d'outils) à l'historique 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({ _history.append({
"role": "assistant", "role": "assistant",
"content": msg.content or "", "content": accumulated_content or "",
"tool_calls": [ "tool_calls": [
{ {
"id": tc.id, "id": tc.id,
@@ -47,12 +62,12 @@ def chat(user_message: str) -> str:
"arguments": tc.function.arguments, "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 # Execute each tool and append results
for tc in msg.tool_calls: for tc in tool_calls_received:
tool_name = tc.function.name tool_name = tc.function.name
try: try:
args = ( args = (
@@ -60,7 +75,7 @@ def chat(user_message: str) -> str:
if isinstance(tc.function.arguments, str) if isinstance(tc.function.arguments, str)
else tc.function.arguments 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) result = manager.call_tool(tool_name, args)
except Exception as e: except Exception as e:
result = f"Erreur lors de l'appel à {tool_name} : {e}" result = f"Erreur lors de l'appel à {tool_name} : {e}"
@@ -70,13 +85,17 @@ def chat(user_message: str) -> str:
"content": result, "content": result,
"tool_call_id": tc.id, "tool_call_id": tc.id,
}) })
# Loop to get the next (final) response
# 3. Reboucler pour obtenir la réponse finale
else: else:
reply = msg.content or "" # Pure text response — already yielded chunk by chunk; save to history
_history.append({"role": "assistant", "content": reply}) _history.append({"role": "assistant", "content": accumulated_content})
return reply 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: def _short_args(args: dict) -> str:

View File

@@ -12,12 +12,16 @@ Configure les serveurs dans un profil YAML sous la clé `mcp_servers` :
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import os
import re import re
import threading import threading
from contextlib import AsyncExitStack
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path
from typing import Any from typing import Any
# Racine du projet (le dossier qui contient main.py)
_PROJECT_ROOT = Path(__file__).parent.parent.resolve()
@dataclass @dataclass
class MCPServerConfig: class MCPServerConfig:
@@ -27,6 +31,18 @@ class MCPServerConfig:
env: dict[str, str] | None = None env: dict[str, str] | None = None
url: 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: def _sanitize_name(name: str) -> str:
"""Transforme un nom en identifiant valide pour l'API Mistral (^[a-zA-Z0-9_-]{1,64}$).""" """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: 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: def __init__(self) -> None:
self._loop = asyncio.new_event_loop() self._loop = asyncio.new_event_loop()
@@ -42,9 +63,9 @@ class MCPManager:
self._thread.start() self._thread.start()
self._sessions: dict[str, Any] = {} self._sessions: dict[str, Any] = {}
self._raw_tools: dict[str, list] = {} self._raw_tools: dict[str, list] = {}
# mistral_name -> (server_name, original_tool_name)
self._tool_map: dict[str, tuple[str, str]] = {} 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: def _run_loop(self) -> None:
asyncio.set_event_loop(self._loop) asyncio.set_event_loop(self._loop)
@@ -87,10 +108,18 @@ class MCPManager:
return self._run(self._call_tool_async(server_name, tool_name, arguments)) return self._run(self._call_tool_async(server_name, tool_name, arguments))
def shutdown(self) -> None: 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: try:
self._run(self._shutdown_async(), timeout=10) self._run(_signal_all(), timeout=5)
except Exception: except Exception:
pass pass
self._stop_events.clear()
def summary(self) -> list[tuple[str, int]]: def summary(self) -> list[tuple[str, int]]:
"""Retourne [(server_name, tool_count), ...] pour les serveurs connectés.""" """Retourne [(server_name, tool_count), ...] pour les serveurs connectés."""
@@ -112,38 +141,78 @@ class MCPManager:
print(f"[MCP] ❌ Connexion {cfg.name} impossible : {e}") print(f"[MCP] ❌ Connexion {cfg.name} impossible : {e}")
async def _connect_server(self, cfg: MCPServerConfig) -> None: async def _connect_server(self, cfg: MCPServerConfig) -> None:
from mcp import ClientSession, StdioServerParameters """Lance la keeper task et attend que la connexion soit établie."""
from mcp.client.stdio import stdio_client stop_event = asyncio.Event()
ready_event = asyncio.Event()
error_holder: list[Exception] = []
stack = AsyncExitStack() self._stop_events[cfg.name] = stop_event
if cfg.command: asyncio.create_task(
params = StdioServerParameters( self._run_server(cfg, ready_event, stop_event, error_holder),
command=cfg.command, name=f"mcp-keeper-{cfg.name}",
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))
session = await stack.enter_async_context(ClientSession(read, write)) # Attendre que la connexion soit prête (ou échoue)
await session.initialize() 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 if error_holder:
self._exit_stacks[cfg.name] = stack raise error_holder[0]
tools_resp = await session.list_tools() async def _run_server(
self._raw_tools[cfg.name] = tools_resp.tools 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) if cfg.command:
for tool in tools_resp.tools: from mcp.client.stdio import stdio_client
tool_safe = _sanitize_name(tool.name) transport = stdio_client(StdioServerParameters(
mistral_name = f"{server_safe}__{tool_safe}" command=cfg.command,
self._tool_map[mistral_name] = (cfg.name, tool.name) 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: async def _call_tool_async(self, server_name: str, tool_name: str, arguments: dict) -> str:
session = self._sessions[server_name] session = self._sessions[server_name]
@@ -156,17 +225,6 @@ class MCPManager:
parts.append(str(item)) parts.append(str(item))
return "\n".join(parts) if parts else "(aucun résultat)" 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 _manager: MCPManager | None = None
_lock = threading.Lock() _lock = threading.Lock()
@@ -185,4 +243,6 @@ def reset_manager() -> None:
with _lock: with _lock:
if _manager is not None: if _manager is not None:
_manager.shutdown() _manager.shutdown()
_manager = MCPManager() _manager = MCPManager()

109
assistant/stt.py Normal file
View File

@@ -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()

54
assistant/tts.py Normal file
View File

@@ -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)

4
main.py Normal file
View File

@@ -0,0 +1,4 @@
from assistant.cli import run
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,11 @@
export type QueryParams = Record<string, string | number | boolean | undefined>;
export declare function apiGet(path: string, params?: QueryParams): Promise<Response>;
export declare function apiGetImage(path: string, params?: QueryParams): Promise<string>;
export declare function apiGetJson<T>(path: string, params?: QueryParams): Promise<T>;
export declare function apiGetText(path: string, params?: QueryParams): Promise<string>;
export declare function apiGetDataUri(path: string, params?: QueryParams): Promise<{
base64: string;
mimeType: string;
}>;
export declare function apiPostImage(path: string, queryParams: QueryParams, formBody: Record<string, string>): Promise<string>;
//# sourceMappingURL=client.d.ts.map

View File

@@ -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"}

View File

@@ -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

View File

@@ -0,0 +1 @@
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}

12
mcp_servers/traveller_map/dist/index.js vendored Normal file
View File

@@ -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

View File

@@ -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"}

View File

@@ -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

View File

@@ -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"}

View File

@@ -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

View File

@@ -0,0 +1 @@
{"version":3,"file":"sec.js","sourceRoot":"","sources":["../../src/parsers/sec.ts"],"names":[],"mappings":"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"}

View File

@@ -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

View File

@@ -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"}

View File

@@ -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: '110%' },
2: { description: 'Dry world', percent: '1120%' },
3: { description: 'Dry world', percent: '2130%' },
4: { description: 'Wet world', percent: '3140%' },
5: { description: 'Wet world', percent: '4150%' },
6: { description: 'Wet world', percent: '5160%' },
7: { description: 'Wet world — significant oceans', percent: '6170%' },
8: { description: 'Water world — large oceans', percent: '7180%' },
9: { description: 'Water world — very large oceans', percent: '8190%' },
10: { description: 'Water world — global ocean', percent: '91100%' },
};
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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
export declare function createServer(): McpServer;
//# sourceMappingURL=server.d.ts.map

View File

@@ -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"}

View File

@@ -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

View File

@@ -0,0 +1 @@
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}

View File

@@ -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<z.ZodOptional<z.ZodNumber>>;
avoid_red_zones: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
imperial_only: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
wilderness_refueling: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
milieu: z.ZodOptional<z.ZodString>;
}, "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<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=find_route.d.ts.map

View File

@@ -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"}

View File

@@ -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

View File

@@ -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"}

View File

@@ -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<typeof inputSchema>;
export declare function handler(_args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_allegiance_list.d.ts.map

View File

@@ -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"}

View File

@@ -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

View File

@@ -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"}

View File

@@ -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<z.ZodOptional<z.ZodNumber>>;
scale: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
style: z.ZodDefault<z.ZodOptional<z.ZodEnum<["poster", "print", "atlas", "candy", "draft", "fasa", "terminal", "mongoose"]>>>;
milieu: z.ZodOptional<z.ZodString>;
save_path: z.ZodOptional<z.ZodString>;
filename: z.ZodOptional<z.ZodString>;
}, "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<typeof inputSchema>;
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

View File

@@ -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"}

View File

@@ -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

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_jump_map.js","sourceRoot":"","sources":["../../src/tools/get_jump_map.ts"],"names":[],"mappings":"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"}

View File

@@ -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<z.ZodString>;
milieu: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
sector: string;
subsector?: string | undefined;
milieu?: string | undefined;
}, {
sector: string;
subsector?: string | undefined;
milieu?: string | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_sector_data.d.ts.map

View File

@@ -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"}

View File

@@ -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

View File

@@ -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"}

View File

@@ -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<z.ZodString>;
tag: z.ZodOptional<z.ZodEnum<["Official", "InReview", "Preserve", "Apocryphal"]>>;
}, "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<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_sector_list.d.ts.map

View File

@@ -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"}

View File

@@ -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

View File

@@ -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"}

View File

@@ -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<z.ZodString>;
}, "strip", z.ZodTypeAny, {
sector: string;
milieu?: string | undefined;
}, {
sector: string;
milieu?: string | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_sector_metadata.d.ts.map

View File

@@ -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"}

View File

@@ -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

View File

@@ -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"}

View File

@@ -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<z.ZodOptional<z.ZodNumber>>;
style: z.ZodDefault<z.ZodOptional<z.ZodEnum<["poster", "print", "atlas", "candy", "draft", "fasa", "terminal", "mongoose"]>>>;
milieu: z.ZodOptional<z.ZodString>;
save_path: z.ZodOptional<z.ZodString>;
filename: z.ZodOptional<z.ZodString>;
}, "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<typeof inputSchema>;
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

View File

@@ -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"}

View File

@@ -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

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_subsector_map.js","sourceRoot":"","sources":["../../src/tools/get_subsector_map.ts"],"names":[],"mappings":"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"}

View File

@@ -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<z.ZodString>;
}, "strip", z.ZodTypeAny, {
hex: string;
sector: string;
milieu?: string | undefined;
}, {
hex: string;
sector: string;
milieu?: string | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_world_info.d.ts.map

View File

@@ -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"}

View File

@@ -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

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_world_info.js","sourceRoot":"","sources":["../../src/tools/get_world_info.ts"],"names":[],"mappings":"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"}

View File

@@ -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<z.ZodOptional<z.ZodNumber>>;
milieu: z.ZodOptional<z.ZodString>;
}, "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<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_worlds_in_jump_range.d.ts.map

View File

@@ -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"}

View File

@@ -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

View File

@@ -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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"}

View File

@@ -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<z.ZodString>;
scale: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
style: z.ZodDefault<z.ZodOptional<z.ZodEnum<["poster", "print", "atlas", "candy", "draft", "fasa", "terminal", "mongoose"]>>>;
subsector: z.ZodOptional<z.ZodString>;
}, "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<typeof inputSchema>;
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

View File

@@ -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"}

View File

@@ -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

View File

@@ -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"}

View File

@@ -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<z.ZodString>;
}, "strip", z.ZodTypeAny, {
query: string;
milieu?: string | undefined;
}, {
query: string;
milieu?: string | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=search_worlds.d.ts.map

View File

@@ -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"}

View File

@@ -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

View File

@@ -0,0 +1 @@
{"version":3,"file":"search_worlds.js","sourceRoot":"","sources":["../../src/tools/search_worlds.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,eAAe,CAAC;AAEpC,MAAM,CAAC,MAAM,WAAW,GACtB,uGAAuG;IACvG,mDAAmD;IACnD,oFAAoF;IACpF,wFAAwF;IACxF,qGAAqG;IACrG,oCAAoC,CAAC;AAEvC,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,QAAQ,CACP,4GAA4G;QAC1G,gEAAgE,CACnE;IACH,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;CAC7E,CAAC,CAAC;AA+CH,MAAM,WAAW,GAA2B,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC;AAE9F,SAAS,aAAa,CAAC,IAAwB;IAC7C,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9B,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC;AAC1C,CAAC;AAED,SAAS,eAAe,CAAC,OAA2B;IAClD,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,CAAC;AACnC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAE/B,MAAM,IAAI,GAAG,MAAM,UAAU,CAAe,aAAa,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAEjF,MAAM,KAAK,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC;IAEzC,MAAM,MAAM,GAAc,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAc,EAAE,CAAC;IAC9B,MAAM,UAAU,GAAc,EAAE,CAAC;IAEjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;YACrB,2EAA2E;YAC3E,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC;YAC5E,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAChF,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAChF,MAAM,GAAG,GAAG,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;YACpD,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,MAAM,EAAE,UAAU;gBAClB,GAAG;gBACH,QAAQ,EAAE,UAAU,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,UAAU,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS;gBAChE,GAAG;gBACH,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,WAAW,EAAE,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC;gBACvC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC3B,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,UAAU,EAAE,CAAC,CAAC,UAAU;aACzB,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACvB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC;QACnF,CAAC;aAAM,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC1B,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC;QAC9E,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG;QACb,KAAK;QACL,aAAa,EAAE,KAAK,CAAC,MAAM;QAC3B,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE;QACjD,OAAO,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS;QACrF,UAAU,EAAE,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,SAAS;KAClG,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"}

1174
mcp_servers/traveller_map/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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<string, string | number | boolean | undefined>;
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<Response> {
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<string> {
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<T>(path: string, params: QueryParams = {}): Promise<T> {
const response = await apiGet(path, { ...params, accept: 'application/json' });
return response.json() as Promise<T>;
}
export async function apiGetText(path: string, params: QueryParams = {}): Promise<string> {
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<string, string>,
): Promise<string> {
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');
}

View File

@@ -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);
});

View File

@@ -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<string, string> = {
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;
}

View File

@@ -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<string, string> = {
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<number, { description: string; diameter_km: string }> = {
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<number, string> = {
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<number, { description: string; percent: string }> = {
0: { description: 'Desert world — no free water', percent: '0%' },
1: { description: 'Dry world — traces of water', percent: '110%' },
2: { description: 'Dry world', percent: '1120%' },
3: { description: 'Dry world', percent: '2130%' },
4: { description: 'Wet world', percent: '3140%' },
5: { description: 'Wet world', percent: '4150%' },
6: { description: 'Wet world', percent: '5160%' },
7: { description: 'Wet world — significant oceans', percent: '6170%' },
8: { description: 'Water world — large oceans', percent: '7180%' },
9: { description: 'Water world — very large oceans', percent: '8190%' },
10: { description: 'Water world — global ocean', percent: '91100%' },
};
const POPULATION_DESC: Record<number, { description: string; estimate: string }> = {
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<number, string> = {
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<number, string> = {
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<number, string> = {
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',
},
};
}

View File

@@ -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;
}

View File

@@ -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<typeof inputSchema>;
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<string, string> = { 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<unknown>('/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),
},
],
};
}

View File

@@ -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<typeof inputSchema>;
interface AllegianceEntry {
Code?: string;
Name?: string;
[key: string]: unknown;
}
export async function handler(_args: Input) {
const data = await apiGetJson<AllegianceEntry[]>('/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),
},
],
};
}

View File

@@ -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<typeof inputSchema>;
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}` },
],
};
}

View File

@@ -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<typeof inputSchema>;
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),
},
],
};
}

View File

@@ -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<typeof inputSchema>;
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<UniverseResponse>('/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),
},
],
};
}

View File

@@ -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<typeof inputSchema>;
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<MetadataResponse>('/api/metadata', { sector, milieu });
const subsectorsByIndex: Record<string, string> = {};
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),
},
],
};
}

View File

@@ -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<typeof inputSchema>;
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}` },
],
};
}

View File

@@ -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<typeof inputSchema>;
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<string, string> = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' };
const BASE_LABELS: Record<string, string> = {
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<string, string> = {
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<CreditsResponse>('/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),
},
],
};
}

View File

@@ -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<typeof inputSchema>;
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<string, string> = { 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<JumpWorldsResponse>('/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),
},
],
};
}

View File

@@ -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<typeof inputSchema>;
export async function handler(args: Input) {
const { sec_data, metadata, scale, style, subsector } = args;
const formBody: Record<string, string> = { 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})` : ''}`,
},
],
};
}

View File

@@ -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<typeof inputSchema>;
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<string, string> = { 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<SearchResult>('/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),
},
],
};
}

View File

@@ -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"]
}

View File

@@ -12,3 +12,7 @@ system_prompt: |
# args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] # args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
# - name: brave_search # - name: brave_search
# url: "http://localhost:3000/sse" # url: "http://localhost:3000/sse"
mcp_servers:
- name: traveller-map
command: node
args: [ "/home/morr/work/traveller-map-mcp/dist/server.js" ]

View File

@@ -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)

View File

@@ -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" ]

70
scripts/list_voices.py Normal file
View File

@@ -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=<id>")
if __name__ == "__main__":
main()

57
scripts/register_voice.py Normal file
View File

@@ -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=<id_retourné>
"""
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()