Add gitignor
This commit is contained in:
101
README.md
Normal file
101
README.md
Normal 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 2–3 secondes :
|
||||
|
||||
```bash
|
||||
python scripts/register_voice.py --name "Ma voix" --audio sample.mp3 --language fr
|
||||
```
|
||||
|
||||
L'ID retourné peut être ajouté dans `.env` :
|
||||
```
|
||||
VOICE_ID=<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
0
assistant/__init__.py
Normal file
84
assistant/audio.py
Normal file
84
assistant/audio.py
Normal 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
|
||||
127
assistant/cli.py
127
assistant/cli.py
@@ -1,5 +1,8 @@
|
||||
import asyncio
|
||||
import queue as _queue
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
from . import llm, tts, audio, config
|
||||
|
||||
|
||||
@@ -34,22 +37,122 @@ def _set_voice(parts: list[str]) -> None:
|
||||
print(f"Voix définie sur : {config.VOICE_ID}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sentence splitting & markdown cleaning for TTS pipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Split on sentence boundaries using fixed-width lookbehinds (Python constraint).
|
||||
# For ".": 3-char lookbehind — require the 2 chars before the period to be
|
||||
# lowercase, digit, hyphen, or closing bracket (NOT uppercase). This protects
|
||||
# "M.", "No.", "A." while correctly splitting "arrivé.", "Jump-2.", "(J2).", "ok."
|
||||
_SENT_RE = re.compile(
|
||||
r'(?<=[a-z\u00e0-\u00ff\d\-)\]]{2}[.])\s+' # word(2+) + period
|
||||
r'|(?<=[a-z\u00e0-\u00ff\d\-)\]]{2}[.][»"\')\]])\s+' # + closing quote
|
||||
r'|(?<=[!?…])\s+' # ! ? …
|
||||
r'|(?<=[!?…][»"\')\]])\s+' # ! ? … + closing quote
|
||||
r'|\n{2,}' # paragraph break
|
||||
)
|
||||
|
||||
# Markdown patterns to strip before TTS (keep inner text where applicable)
|
||||
_MD_CODE_BLOCK = re.compile(r'```.*?```', re.DOTALL)
|
||||
_MD_INLINE = re.compile(r'\*{1,3}(.*?)\*{1,3}|_{1,2}(.*?)_{1,2}|~~(.*?)~~|`([^`]+)`', re.DOTALL)
|
||||
_MD_LINK = re.compile(r'\[([^\]]*)\]\([^\)]*\)')
|
||||
_MD_HEADER = re.compile(r'^#{1,6}\s+', re.MULTILINE)
|
||||
_MULTI_SPACE = re.compile(r'\s+')
|
||||
|
||||
|
||||
def _split_sentences(text: str) -> tuple[list[str], str]:
|
||||
"""Extrait les phrases complètes d'un buffer partiel.
|
||||
|
||||
Returns (complete_sentences, remainder).
|
||||
"""
|
||||
parts = _SENT_RE.split(text)
|
||||
if len(parts) <= 1:
|
||||
return [], text
|
||||
return parts[:-1], parts[-1]
|
||||
|
||||
|
||||
def _clean_for_tts(text: str) -> str:
|
||||
"""Supprime le formatage Markdown avant la synthèse vocale."""
|
||||
text = _MD_CODE_BLOCK.sub(' ', text)
|
||||
text = _MD_INLINE.sub(lambda m: next(g for g in m.groups() if g is not None), text)
|
||||
text = _MD_LINK.sub(r'\1', text)
|
||||
text = _MD_HEADER.sub('', text)
|
||||
text = text.replace('→', ' vers ').replace('←', ' depuis ')
|
||||
text = _MULTI_SPACE.sub(' ', text).strip()
|
||||
return text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Message processing — streaming TTS pipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _process_message(user_input: str) -> None:
|
||||
"""Envoie un message au LLM et lit la réponse à voix haute."""
|
||||
print(f"Arioch > ", end="", flush=True)
|
||||
try:
|
||||
reply = llm.chat(user_input)
|
||||
except Exception as e:
|
||||
print(f"\n[Erreur LLM] {e}")
|
||||
return
|
||||
"""Envoie un message au LLM et lit la réponse à voix haute.
|
||||
|
||||
print(reply)
|
||||
Pipeline 3 étages en parallèle :
|
||||
[LLM stream] → sentence_queue → [TTS thread] → audio_queue → [Audio thread]
|
||||
|
||||
Dès qu'une phrase complète est détectée dans le stream LLM, elle part
|
||||
immédiatement en synthèse Voxtral. L'audio est joué dès qu'il est prêt,
|
||||
pendant que la phrase suivante est déjà en cours de synthèse.
|
||||
"""
|
||||
SENTINEL = object()
|
||||
sentence_q: _queue.Queue = _queue.Queue()
|
||||
audio_q: _queue.Queue = _queue.Queue()
|
||||
|
||||
def tts_worker() -> None:
|
||||
while True:
|
||||
item = sentence_q.get()
|
||||
if item is SENTINEL:
|
||||
audio_q.put(SENTINEL)
|
||||
return
|
||||
text = _clean_for_tts(item)
|
||||
if text:
|
||||
try:
|
||||
audio_bytes = tts.text_to_speech(text)
|
||||
audio_q.put(audio_bytes)
|
||||
except Exception as e:
|
||||
print(f"\n[Erreur TTS] {e}", flush=True)
|
||||
|
||||
def audio_worker() -> None:
|
||||
while True:
|
||||
item = audio_q.get()
|
||||
if item is SENTINEL:
|
||||
return
|
||||
try:
|
||||
audio.play_audio(item)
|
||||
except Exception as e:
|
||||
print(f"\n[Erreur Audio] {e}", flush=True)
|
||||
|
||||
tts_thread = threading.Thread(target=tts_worker, daemon=True, name="tts-worker")
|
||||
audio_thread = threading.Thread(target=audio_worker, daemon=True, name="audio-worker")
|
||||
tts_thread.start()
|
||||
audio_thread.start()
|
||||
|
||||
print("Arioch > ", end="", flush=True)
|
||||
buffer = ""
|
||||
try:
|
||||
audio_bytes = tts.text_to_speech(reply)
|
||||
audio.play_audio(audio_bytes)
|
||||
for chunk in llm.chat_stream(user_input):
|
||||
print(chunk, end="", flush=True)
|
||||
buffer += chunk
|
||||
sentences, buffer = _split_sentences(buffer)
|
||||
for sentence in sentences:
|
||||
sentence = sentence.strip()
|
||||
if sentence:
|
||||
sentence_q.put(sentence)
|
||||
except Exception as e:
|
||||
print(f"[Erreur TTS/Audio] {e}")
|
||||
print(f"\n[Erreur LLM] {e}", flush=True)
|
||||
|
||||
# Flush any remaining text after the stream ends
|
||||
if buffer.strip():
|
||||
sentence_q.put(buffer.strip())
|
||||
|
||||
print() # newline after full response
|
||||
sentence_q.put(SENTINEL)
|
||||
|
||||
tts_thread.join()
|
||||
audio_thread.join()
|
||||
|
||||
|
||||
def _handle_command(user_input: str) -> bool:
|
||||
@@ -118,7 +221,7 @@ def _handle_mcp(parts: list[str]) -> None:
|
||||
print(f"\nTotal : {total} outil(s). Tapez 'mcp tools' pour les lister.\n")
|
||||
|
||||
|
||||
|
||||
def _list_profiles(profiles: list) -> None:
|
||||
if not profiles:
|
||||
print("Aucun profil disponible dans profiles/")
|
||||
return
|
||||
|
||||
20
assistant/config.py
Normal file
20
assistant/config.py
Normal 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"))
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Generator
|
||||
|
||||
from mistralai.client import Mistral
|
||||
from . import config
|
||||
@@ -13,8 +14,12 @@ def reset_history() -> None:
|
||||
_history.clear()
|
||||
|
||||
|
||||
def chat(user_message: str) -> str:
|
||||
"""Envoie un message au LLM, gère les appels d'outils MCP et retourne la réponse."""
|
||||
def chat_stream(user_message: str) -> Generator[str, None, None]:
|
||||
"""Génère la réponse du LLM token par token via streaming.
|
||||
|
||||
Les appels d'outils MCP sont exécutés internement (sans streaming).
|
||||
Seule la réponse textuelle finale est streamée sous forme de chunks.
|
||||
"""
|
||||
from . import mcp_client
|
||||
|
||||
_history.append({"role": "user", "content": user_message})
|
||||
@@ -24,20 +29,30 @@ def chat(user_message: str) -> str:
|
||||
|
||||
while True:
|
||||
messages = [{"role": "system", "content": config.SYSTEM_PROMPT}] + _history
|
||||
|
||||
kwargs: dict = {"model": config.LLM_MODEL, "messages": messages}
|
||||
if tools:
|
||||
kwargs["tools"] = tools
|
||||
|
||||
response = _client.chat.complete(**kwargs)
|
||||
choice = response.choices[0]
|
||||
msg = choice.message
|
||||
accumulated_content = ""
|
||||
tool_calls_received = None
|
||||
|
||||
if msg.tool_calls:
|
||||
# 1. Ajouter le message assistant (avec les appels d'outils) à l'historique
|
||||
for event in _client.chat.stream(**kwargs):
|
||||
ch = event.data.choices[0]
|
||||
delta = ch.delta
|
||||
|
||||
# Yield text chunks (isinstance check guards against Unset sentinel)
|
||||
if isinstance(delta.content, str) and delta.content:
|
||||
accumulated_content += delta.content
|
||||
yield delta.content
|
||||
|
||||
if delta.tool_calls:
|
||||
tool_calls_received = delta.tool_calls
|
||||
|
||||
if tool_calls_received:
|
||||
# Append assistant turn with tool calls to history
|
||||
_history.append({
|
||||
"role": "assistant",
|
||||
"content": msg.content or "",
|
||||
"content": accumulated_content or "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tc.id,
|
||||
@@ -47,12 +62,12 @@ def chat(user_message: str) -> str:
|
||||
"arguments": tc.function.arguments,
|
||||
},
|
||||
}
|
||||
for tc in msg.tool_calls
|
||||
for tc in tool_calls_received
|
||||
],
|
||||
})
|
||||
|
||||
# 2. Exécuter chaque outil et ajouter les résultats
|
||||
for tc in msg.tool_calls:
|
||||
# Execute each tool and append results
|
||||
for tc in tool_calls_received:
|
||||
tool_name = tc.function.name
|
||||
try:
|
||||
args = (
|
||||
@@ -60,7 +75,7 @@ def chat(user_message: str) -> str:
|
||||
if isinstance(tc.function.arguments, str)
|
||||
else tc.function.arguments
|
||||
)
|
||||
print(f" 🔧 [MCP] {tool_name}({_short_args(args)})")
|
||||
print(f"\n 🔧 [MCP] {tool_name}({_short_args(args)})", flush=True)
|
||||
result = manager.call_tool(tool_name, args)
|
||||
except Exception as e:
|
||||
result = f"Erreur lors de l'appel à {tool_name} : {e}"
|
||||
@@ -70,13 +85,17 @@ def chat(user_message: str) -> str:
|
||||
"content": result,
|
||||
"tool_call_id": tc.id,
|
||||
})
|
||||
|
||||
# 3. Reboucler pour obtenir la réponse finale
|
||||
# Loop to get the next (final) response
|
||||
|
||||
else:
|
||||
reply = msg.content or ""
|
||||
_history.append({"role": "assistant", "content": reply})
|
||||
return reply
|
||||
# Pure text response — already yielded chunk by chunk; save to history
|
||||
_history.append({"role": "assistant", "content": accumulated_content})
|
||||
break
|
||||
|
||||
|
||||
def chat(user_message: str) -> str:
|
||||
"""Envoie un message au LLM et retourne la réponse complète (non-streaming)."""
|
||||
return "".join(chat_stream(user_message))
|
||||
|
||||
|
||||
def _short_args(args: dict) -> str:
|
||||
|
||||
@@ -12,12 +12,16 @@ Configure les serveurs dans un profil YAML sous la clé `mcp_servers` :
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
from contextlib import AsyncExitStack
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# Racine du projet (le dossier qui contient main.py)
|
||||
_PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPServerConfig:
|
||||
@@ -27,6 +31,18 @@ class MCPServerConfig:
|
||||
env: dict[str, str] | None = None
|
||||
url: str | None = None
|
||||
|
||||
def resolved_args(self) -> list[str]:
|
||||
"""Résout les chemins relatifs dans args par rapport à la racine du projet."""
|
||||
result = []
|
||||
for arg in self.args:
|
||||
p = Path(arg)
|
||||
if not p.is_absolute() and p.suffix in (".js", ".py", ".ts"):
|
||||
resolved = (_PROJECT_ROOT / p).resolve()
|
||||
result.append(str(resolved))
|
||||
else:
|
||||
result.append(arg)
|
||||
return result
|
||||
|
||||
|
||||
def _sanitize_name(name: str) -> str:
|
||||
"""Transforme un nom en identifiant valide pour l'API Mistral (^[a-zA-Z0-9_-]{1,64}$)."""
|
||||
@@ -34,7 +50,12 @@ def _sanitize_name(name: str) -> str:
|
||||
|
||||
|
||||
class MCPManager:
|
||||
"""Gère les connexions aux serveurs MCP et l'exécution des outils."""
|
||||
"""Gère les connexions aux serveurs MCP et l'exécution des outils.
|
||||
|
||||
Chaque connexion tourne dans une "keeper task" qui possède toute la durée
|
||||
de vie du context manager stdio_client / ClientSession. Cela évite l'erreur
|
||||
anyio "Attempted to exit cancel scope in a different task".
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._loop = asyncio.new_event_loop()
|
||||
@@ -42,9 +63,9 @@ class MCPManager:
|
||||
self._thread.start()
|
||||
self._sessions: dict[str, Any] = {}
|
||||
self._raw_tools: dict[str, list] = {}
|
||||
# mistral_name -> (server_name, original_tool_name)
|
||||
self._tool_map: dict[str, tuple[str, str]] = {}
|
||||
self._exit_stacks: dict[str, AsyncExitStack] = {}
|
||||
# stop event per server, signalled at shutdown
|
||||
self._stop_events: dict[str, asyncio.Event] = {}
|
||||
|
||||
def _run_loop(self) -> None:
|
||||
asyncio.set_event_loop(self._loop)
|
||||
@@ -87,10 +108,18 @@ class MCPManager:
|
||||
return self._run(self._call_tool_async(server_name, tool_name, arguments))
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Signale l'arrêt à toutes les keeper tasks et attend brièvement."""
|
||||
async def _signal_all() -> None:
|
||||
for ev in self._stop_events.values():
|
||||
ev.set()
|
||||
# Laisser une courte fenêtre pour que les tâches se terminent proprement
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
try:
|
||||
self._run(self._shutdown_async(), timeout=10)
|
||||
self._run(_signal_all(), timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
self._stop_events.clear()
|
||||
|
||||
def summary(self) -> list[tuple[str, int]]:
|
||||
"""Retourne [(server_name, tool_count), ...] pour les serveurs connectés."""
|
||||
@@ -112,38 +141,78 @@ class MCPManager:
|
||||
print(f"[MCP] ❌ Connexion {cfg.name} impossible : {e}")
|
||||
|
||||
async def _connect_server(self, cfg: MCPServerConfig) -> None:
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
"""Lance la keeper task et attend que la connexion soit établie."""
|
||||
stop_event = asyncio.Event()
|
||||
ready_event = asyncio.Event()
|
||||
error_holder: list[Exception] = []
|
||||
|
||||
stack = AsyncExitStack()
|
||||
self._stop_events[cfg.name] = stop_event
|
||||
|
||||
if cfg.command:
|
||||
params = StdioServerParameters(
|
||||
command=cfg.command,
|
||||
args=cfg.args or [],
|
||||
env=cfg.env,
|
||||
)
|
||||
read, write = await stack.enter_async_context(stdio_client(params))
|
||||
else:
|
||||
from mcp.client.sse import sse_client
|
||||
read, write = await stack.enter_async_context(sse_client(cfg.url))
|
||||
asyncio.create_task(
|
||||
self._run_server(cfg, ready_event, stop_event, error_holder),
|
||||
name=f"mcp-keeper-{cfg.name}",
|
||||
)
|
||||
|
||||
session = await stack.enter_async_context(ClientSession(read, write))
|
||||
await session.initialize()
|
||||
# Attendre que la connexion soit prête (ou échoue)
|
||||
try:
|
||||
await asyncio.wait_for(ready_event.wait(), timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
stop_event.set()
|
||||
raise TimeoutError(f"Timeout lors de la connexion à {cfg.name}")
|
||||
|
||||
self._sessions[cfg.name] = session
|
||||
self._exit_stacks[cfg.name] = stack
|
||||
if error_holder:
|
||||
raise error_holder[0]
|
||||
|
||||
tools_resp = await session.list_tools()
|
||||
self._raw_tools[cfg.name] = tools_resp.tools
|
||||
async def _run_server(
|
||||
self,
|
||||
cfg: MCPServerConfig,
|
||||
ready_event: asyncio.Event,
|
||||
stop_event: asyncio.Event,
|
||||
error_holder: list[Exception],
|
||||
) -> None:
|
||||
"""Keeper task : possède le context manager de bout en bout."""
|
||||
try:
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
|
||||
server_safe = _sanitize_name(cfg.name)
|
||||
for tool in tools_resp.tools:
|
||||
tool_safe = _sanitize_name(tool.name)
|
||||
mistral_name = f"{server_safe}__{tool_safe}"
|
||||
self._tool_map[mistral_name] = (cfg.name, tool.name)
|
||||
if cfg.command:
|
||||
from mcp.client.stdio import stdio_client
|
||||
transport = stdio_client(StdioServerParameters(
|
||||
command=cfg.command,
|
||||
args=cfg.resolved_args(),
|
||||
env=cfg.env,
|
||||
))
|
||||
else:
|
||||
from mcp.client.sse import sse_client
|
||||
transport = sse_client(cfg.url)
|
||||
|
||||
print(f"[MCP] ✅ {cfg.name} — {len(tools_resp.tools)} outil(s) disponible(s)")
|
||||
async with transport as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
await session.initialize()
|
||||
|
||||
tools_resp = await session.list_tools()
|
||||
self._sessions[cfg.name] = session
|
||||
self._raw_tools[cfg.name] = tools_resp.tools
|
||||
|
||||
server_safe = _sanitize_name(cfg.name)
|
||||
for tool in tools_resp.tools:
|
||||
tool_safe = _sanitize_name(tool.name)
|
||||
self._tool_map[f"{server_safe}__{tool_safe}"] = (cfg.name, tool.name)
|
||||
|
||||
print(f"[MCP] ✅ {cfg.name} — {len(tools_resp.tools)} outil(s) disponible(s)")
|
||||
ready_event.set()
|
||||
|
||||
# Maintenir la connexion jusqu'au signal d'arrêt
|
||||
await stop_event.wait()
|
||||
|
||||
except Exception as e:
|
||||
error_holder.append(e)
|
||||
ready_event.set() # débloquer _connect_server même en cas d'erreur
|
||||
finally:
|
||||
self._sessions.pop(cfg.name, None)
|
||||
self._raw_tools.pop(cfg.name, None)
|
||||
to_remove = [k for k, v in self._tool_map.items() if v[0] == cfg.name]
|
||||
for k in to_remove:
|
||||
del self._tool_map[k]
|
||||
|
||||
async def _call_tool_async(self, server_name: str, tool_name: str, arguments: dict) -> str:
|
||||
session = self._sessions[server_name]
|
||||
@@ -156,17 +225,6 @@ class MCPManager:
|
||||
parts.append(str(item))
|
||||
return "\n".join(parts) if parts else "(aucun résultat)"
|
||||
|
||||
async def _shutdown_async(self) -> None:
|
||||
for stack in list(self._exit_stacks.values()):
|
||||
try:
|
||||
await stack.aclose()
|
||||
except Exception:
|
||||
pass
|
||||
self._sessions.clear()
|
||||
self._raw_tools.clear()
|
||||
self._tool_map.clear()
|
||||
self._exit_stacks.clear()
|
||||
|
||||
|
||||
_manager: MCPManager | None = None
|
||||
_lock = threading.Lock()
|
||||
@@ -185,4 +243,6 @@ def reset_manager() -> None:
|
||||
with _lock:
|
||||
if _manager is not None:
|
||||
_manager.shutdown()
|
||||
_manager = MCPManager()
|
||||
_manager = MCPManager()
|
||||
|
||||
|
||||
|
||||
109
assistant/stt.py
Normal file
109
assistant/stt.py
Normal 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
54
assistant/tts.py
Normal 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
4
main.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from assistant.cli import run
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
11
mcp_servers/traveller_map/dist/api/client.d.ts
vendored
Normal file
11
mcp_servers/traveller_map/dist/api/client.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/api/client.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/api/client.d.ts.map
vendored
Normal 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"}
|
||||
76
mcp_servers/traveller_map/dist/api/client.js
vendored
Normal file
76
mcp_servers/traveller_map/dist/api/client.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/api/client.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/api/client.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
|
||||
2
mcp_servers/traveller_map/dist/index.d.ts
vendored
Normal file
2
mcp_servers/traveller_map/dist/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
mcp_servers/traveller_map/dist/index.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/index.d.ts.map
vendored
Normal 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
12
mcp_servers/traveller_map/dist/index.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/index.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/index.js.map
vendored
Normal 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"}
|
||||
22
mcp_servers/traveller_map/dist/parsers/sec.d.ts
vendored
Normal file
22
mcp_servers/traveller_map/dist/parsers/sec.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/parsers/sec.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/parsers/sec.d.ts.map
vendored
Normal 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"}
|
||||
106
mcp_servers/traveller_map/dist/parsers/sec.js
vendored
Normal file
106
mcp_servers/traveller_map/dist/parsers/sec.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/parsers/sec.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/parsers/sec.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sec.js","sourceRoot":"","sources":["../../src/parsers/sec.ts"],"names":[],"mappings":"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"}
|
||||
30
mcp_servers/traveller_map/dist/parsers/uwp.d.ts
vendored
Normal file
30
mcp_servers/traveller_map/dist/parsers/uwp.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/parsers/uwp.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/parsers/uwp.d.ts.map
vendored
Normal 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"}
|
||||
203
mcp_servers/traveller_map/dist/parsers/uwp.js
vendored
Normal file
203
mcp_servers/traveller_map/dist/parsers/uwp.js
vendored
Normal 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: '1–10%' },
|
||||
2: { description: 'Dry world', percent: '11–20%' },
|
||||
3: { description: 'Dry world', percent: '21–30%' },
|
||||
4: { description: 'Wet world', percent: '31–40%' },
|
||||
5: { description: 'Wet world', percent: '41–50%' },
|
||||
6: { description: 'Wet world', percent: '51–60%' },
|
||||
7: { description: 'Wet world — significant oceans', percent: '61–70%' },
|
||||
8: { description: 'Water world — large oceans', percent: '71–80%' },
|
||||
9: { description: 'Water world — very large oceans', percent: '81–90%' },
|
||||
10: { description: 'Water world — global ocean', percent: '91–100%' },
|
||||
};
|
||||
const POPULATION_DESC = {
|
||||
0: { description: 'Unpopulated or tiny outpost', estimate: 'None or a few individuals' },
|
||||
1: { description: 'Tens of inhabitants', estimate: '~10s' },
|
||||
2: { description: 'Hundreds of inhabitants', estimate: '~100s' },
|
||||
3: { description: 'Thousands of inhabitants', estimate: '~1,000s' },
|
||||
4: { description: 'Tens of thousands', estimate: '~10,000s' },
|
||||
5: { description: 'Hundreds of thousands', estimate: '~100,000s' },
|
||||
6: { description: 'Millions of inhabitants', estimate: '~1,000,000s' },
|
||||
7: { description: 'Tens of millions', estimate: '~10,000,000s' },
|
||||
8: { description: 'Hundreds of millions', estimate: '~100,000,000s' },
|
||||
9: { description: 'Billions of inhabitants', estimate: '~1,000,000,000s' },
|
||||
10: { description: 'Tens of billions', estimate: '~10,000,000,000s' },
|
||||
11: { description: 'Hundreds of billions', estimate: '~100,000,000,000s' },
|
||||
12: { description: 'Trillions of inhabitants', estimate: '~1,000,000,000,000s' },
|
||||
};
|
||||
const GOVERNMENT_DESC = {
|
||||
0: 'No Government Structure — family/clan/tribal',
|
||||
1: 'Company/Corporation — governed by a company',
|
||||
2: 'Participating Democracy — rule by citizen vote',
|
||||
3: 'Self-Perpetuating Oligarchy — ruling class maintains power',
|
||||
4: 'Representative Democracy — elected representatives',
|
||||
5: 'Feudal Technocracy — controlled by technology owners',
|
||||
6: 'Captive Government — controlled by outside power',
|
||||
7: 'Balkanization — no central authority',
|
||||
8: 'Civil Service Bureaucracy — rule by competence',
|
||||
9: 'Impersonal Bureaucracy — rule by rigid law',
|
||||
10: 'Charismatic Dictator — rule by personality',
|
||||
11: 'Non-Charismatic Leader — rule by position',
|
||||
12: 'Charismatic Oligarchy — rule by a few personalities',
|
||||
13: 'Religious Dictatorship — rule by religious doctrine',
|
||||
14: 'Religious Autocracy — rule by a religious figure',
|
||||
15: 'Totalitarian Oligarchy — oppressive rule by a few',
|
||||
};
|
||||
const LAW_LEVEL_DESC = {
|
||||
0: 'No prohibitions — no restrictions on weapons or behavior',
|
||||
1: 'Body pistols, explosives, nuclear weapons prohibited',
|
||||
2: 'Portable energy weapons prohibited',
|
||||
3: 'Machine guns, automatic weapons prohibited',
|
||||
4: 'Light assault weapons prohibited',
|
||||
5: 'Personal concealable weapons prohibited',
|
||||
6: 'All firearms except shotguns prohibited',
|
||||
7: 'Shotguns prohibited',
|
||||
8: 'Long blades prohibited in public',
|
||||
9: 'All weapons outside home prohibited',
|
||||
10: 'Weapon possession prohibited — weapons locked up',
|
||||
11: 'Rigid control of civilian movement',
|
||||
12: 'Unrestricted invasion of privacy',
|
||||
13: 'Paramilitary law enforcement',
|
||||
14: 'Full-fledged police state',
|
||||
15: 'Daily life rigidly controlled',
|
||||
};
|
||||
const TECH_LEVEL_ERA = {
|
||||
0: 'Stone Age / Pre-Industrial',
|
||||
1: 'Bronze Age / Iron Age',
|
||||
2: 'Renaissance',
|
||||
3: 'Industrial Revolution',
|
||||
4: 'Mechanized Age',
|
||||
5: 'Broadcast Age',
|
||||
6: 'Atomic Age',
|
||||
7: 'Space Age',
|
||||
8: 'Information Age',
|
||||
9: 'Pre-Stellar',
|
||||
10: 'Early Stellar',
|
||||
11: 'Average Stellar',
|
||||
12: 'Average Interstellar',
|
||||
13: 'High Interstellar',
|
||||
14: 'Average Imperial',
|
||||
15: 'High Imperial / Average Interstellar II',
|
||||
16: 'Sophont',
|
||||
17: 'Advanced',
|
||||
};
|
||||
export function parseUWP(uwp) {
|
||||
const clean = uwp.trim().replace(/\s+/g, '');
|
||||
const starportCode = clean[0] ?? '?';
|
||||
const sizeCode = clean[1] ?? '0';
|
||||
const atmCode = clean[2] ?? '0';
|
||||
const hydroCode = clean[3] ?? '0';
|
||||
const popCode = clean[4] ?? '0';
|
||||
const govCode = clean[5] ?? '0';
|
||||
const lawCode = clean[6] ?? '0';
|
||||
const tlCode = clean[8] ?? '0';
|
||||
const sizeVal = eHexValue(sizeCode);
|
||||
const atmVal = eHexValue(atmCode);
|
||||
const hydroVal = eHexValue(hydroCode);
|
||||
const popVal = eHexValue(popCode);
|
||||
const govVal = eHexValue(govCode);
|
||||
const lawVal = eHexValue(lawCode);
|
||||
const tlVal = eHexValue(tlCode);
|
||||
const sizeInfo = SIZE_DESC[sizeVal] ?? { description: 'Unknown size', diameter_km: 'Unknown' };
|
||||
const hydroInfo = HYDROGRAPHICS_DESC[hydroVal] ?? { description: 'Unknown hydrographics', percent: 'Unknown' };
|
||||
const popInfo = POPULATION_DESC[popVal] ?? { description: 'Unknown population', estimate: 'Unknown' };
|
||||
return {
|
||||
raw: uwp,
|
||||
starport: {
|
||||
code: starportCode,
|
||||
description: STARPORT[starportCode.toUpperCase()] ?? `Unknown starport code: ${starportCode}`,
|
||||
},
|
||||
size: {
|
||||
code: sizeCode,
|
||||
value: sizeVal,
|
||||
description: sizeInfo.description,
|
||||
diameter_km: sizeInfo.diameter_km,
|
||||
},
|
||||
atmosphere: {
|
||||
code: atmCode,
|
||||
value: atmVal,
|
||||
description: ATMOSPHERE_DESC[atmVal] ?? `Unknown atmosphere code: ${atmCode}`,
|
||||
},
|
||||
hydrographics: {
|
||||
code: hydroCode,
|
||||
value: hydroVal,
|
||||
description: hydroInfo.description,
|
||||
percent: hydroInfo.percent,
|
||||
},
|
||||
population: {
|
||||
code: popCode,
|
||||
value: popVal,
|
||||
description: popInfo.description,
|
||||
estimate: popInfo.estimate,
|
||||
},
|
||||
government: {
|
||||
code: govCode,
|
||||
value: govVal,
|
||||
description: GOVERNMENT_DESC[govVal] ?? `Unknown government code: ${govCode}`,
|
||||
},
|
||||
law_level: {
|
||||
code: lawCode,
|
||||
value: lawVal,
|
||||
description: LAW_LEVEL_DESC[lawVal] ?? `Unknown law level code: ${lawCode}`,
|
||||
},
|
||||
tech_level: {
|
||||
code: tlCode,
|
||||
value: tlVal,
|
||||
description: `Tech Level ${tlVal}`,
|
||||
era: TECH_LEVEL_ERA[tlVal] ?? 'Advanced/Unknown',
|
||||
},
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=uwp.js.map
|
||||
1
mcp_servers/traveller_map/dist/parsers/uwp.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/parsers/uwp.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
mcp_servers/traveller_map/dist/server.d.ts
vendored
Normal file
3
mcp_servers/traveller_map/dist/server.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
export declare function createServer(): McpServer;
|
||||
//# sourceMappingURL=server.d.ts.map
|
||||
1
mcp_servers/traveller_map/dist/server.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/server.d.ts.map
vendored
Normal 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"}
|
||||
31
mcp_servers/traveller_map/dist/server.js
vendored
Normal file
31
mcp_servers/traveller_map/dist/server.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/server.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/server.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
||||
36
mcp_servers/traveller_map/dist/tools/find_route.d.ts
vendored
Normal file
36
mcp_servers/traveller_map/dist/tools/find_route.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/find_route.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/find_route.d.ts.map
vendored
Normal 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"}
|
||||
121
mcp_servers/traveller_map/dist/tools/find_route.js
vendored
Normal file
121
mcp_servers/traveller_map/dist/tools/find_route.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/find_route.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/find_route.js.map
vendored
Normal 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"}
|
||||
12
mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts
vendored
Normal file
12
mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_allegiance_list.d.ts.map
vendored
Normal 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"}
|
||||
27
mcp_servers/traveller_map/dist/tools/get_allegiance_list.js
vendored
Normal file
27
mcp_servers/traveller_map/dist/tools/get_allegiance_list.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_allegiance_list.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_allegiance_list.js.map
vendored
Normal 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"}
|
||||
50
mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts
vendored
Normal file
50
mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_jump_map.d.ts.map
vendored
Normal 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"}
|
||||
97
mcp_servers/traveller_map/dist/tools/get_jump_map.js
vendored
Normal file
97
mcp_servers/traveller_map/dist/tools/get_jump_map.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_jump_map.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_jump_map.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"get_jump_map.js","sourceRoot":"","sources":["../../src/tools/get_jump_map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,CAAC,MAAM,IAAI,GAAG,cAAc,CAAC;AAEnC,MAAM,CAAC,MAAM,WAAW,GACtB,qGAAqG;IACrG,oEAAoE;IACpE,8FAA8F;IAC9F,iEAAiE,CAAC;AAEpE,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sEAAsE,CAAC;IACnG,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sDAAsD,CAAC;IAChF,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,yCAAyC,CAAC;IACzG,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,wEAAwE,CAAC;IACrF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;IACvE,SAAS,EAAE,CAAC;SACT,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,8DAA8D,CAAC;IAC3E,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,2DAA2D;QACzD,2DAA2D,CAC9D;CACJ,CAAC,CAAC;AAIH,SAAS,eAAe,CAAC,QAAgB;IACvC,OAAO,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AACnD,CAAC;AAED,SAAS,aAAa,CAAC,MAAc,EAAE,GAAW,EAAE,IAAY,EAAE,UAA8B,EAAE,GAAW;IAC3G,IAAI,IAAY,CAAC;IACjB,IAAI,UAAU,EAAE,CAAC;QACf,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;IACnD,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,GAAG,MAAM,IAAI,GAAG,QAAQ,IAAI,EAAE;aAClC,WAAW,EAAE;aACb,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;aACpB,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,GAAG,IAAI,IAAI,GAAG,EAAE,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IAE9E,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,aAAa,CAAC,cAAc,EAAE;QAC/D,MAAM;QACN,GAAG;QACH,IAAI;QACJ,KAAK;QACL,KAAK;QACL,MAAM;KACP,CAAC,CAAC;IAEH,MAAM,UAAU,GAAG,EAAE,IAAI,EAAE,OAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAEtE,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,EAAE,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;IACnC,CAAC;IAED,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,8CAA8C,SAAS,EAAE,EAAE,CAAC;SACtG,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;QAC3B,OAAO;YACL,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,wCAAwC,SAAS,EAAE,EAAE,CAAC;SAChG,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IACtC,MAAM,gBAAgB,GAAG,aAAa,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;IAEnD,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC3D,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,OAAO;YACL,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,gCAAgC,OAAO,EAAE,EAAE,CAAC;SACtF,CAAC;IACJ,CAAC;IAED,OAAO;QACL,OAAO,EAAE;YACP,UAAU;YACV,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,mBAAmB,QAAQ,EAAE,EAAE;SAC/D;KACF,CAAC;AACJ,CAAC"}
|
||||
24
mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts
vendored
Normal file
24
mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_sector_data.d.ts.map
vendored
Normal 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"}
|
||||
41
mcp_servers/traveller_map/dist/tools/get_sector_data.js
vendored
Normal file
41
mcp_servers/traveller_map/dist/tools/get_sector_data.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_sector_data.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_sector_data.js.map
vendored
Normal 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"}
|
||||
21
mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts
vendored
Normal file
21
mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_sector_list.d.ts.map
vendored
Normal 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"}
|
||||
43
mcp_servers/traveller_map/dist/tools/get_sector_list.js
vendored
Normal file
43
mcp_servers/traveller_map/dist/tools/get_sector_list.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_sector_list.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_sector_list.js.map
vendored
Normal 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"}
|
||||
21
mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts
vendored
Normal file
21
mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_sector_metadata.d.ts.map
vendored
Normal 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"}
|
||||
43
mcp_servers/traveller_map/dist/tools/get_sector_metadata.js
vendored
Normal file
43
mcp_servers/traveller_map/dist/tools/get_sector_metadata.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_sector_metadata.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_sector_metadata.js.map
vendored
Normal 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"}
|
||||
47
mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts
vendored
Normal file
47
mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_subsector_map.d.ts.map
vendored
Normal 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"}
|
||||
94
mcp_servers/traveller_map/dist/tools/get_subsector_map.js
vendored
Normal file
94
mcp_servers/traveller_map/dist/tools/get_subsector_map.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_subsector_map.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_subsector_map.js.map
vendored
Normal 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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"}
|
||||
24
mcp_servers/traveller_map/dist/tools/get_world_info.d.ts
vendored
Normal file
24
mcp_servers/traveller_map/dist/tools/get_world_info.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_world_info.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_world_info.d.ts.map
vendored
Normal 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"}
|
||||
134
mcp_servers/traveller_map/dist/tools/get_world_info.js
vendored
Normal file
134
mcp_servers/traveller_map/dist/tools/get_world_info.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_world_info.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_world_info.js.map
vendored
Normal 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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"}
|
||||
27
mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts
vendored
Normal file
27
mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.d.ts.map
vendored
Normal 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"}
|
||||
70
mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js
vendored
Normal file
70
mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/get_worlds_in_jump_range.js.map
vendored
Normal 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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"}
|
||||
37
mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts
vendored
Normal file
37
mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/render_custom_map.d.ts.map
vendored
Normal 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"}
|
||||
42
mcp_servers/traveller_map/dist/tools/render_custom_map.js
vendored
Normal file
42
mcp_servers/traveller_map/dist/tools/render_custom_map.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/render_custom_map.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/render_custom_map.js.map
vendored
Normal 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"}
|
||||
21
mcp_servers/traveller_map/dist/tools/search_worlds.d.ts
vendored
Normal file
21
mcp_servers/traveller_map/dist/tools/search_worlds.d.ts
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/search_worlds.d.ts.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/search_worlds.d.ts.map
vendored
Normal 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"}
|
||||
82
mcp_servers/traveller_map/dist/tools/search_worlds.js
vendored
Normal file
82
mcp_servers/traveller_map/dist/tools/search_worlds.js
vendored
Normal 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
|
||||
1
mcp_servers/traveller_map/dist/tools/search_worlds.js.map
vendored
Normal file
1
mcp_servers/traveller_map/dist/tools/search_worlds.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"search_worlds.js","sourceRoot":"","sources":["../../src/tools/search_worlds.ts"],"names":[],"mappings":"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"}
|
||||
1174
mcp_servers/traveller_map/package-lock.json
generated
Normal file
1174
mcp_servers/traveller_map/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
mcp_servers/traveller_map/package.json
Normal file
20
mcp_servers/traveller_map/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
91
mcp_servers/traveller_map/src/api/client.ts
Normal file
91
mcp_servers/traveller_map/src/api/client.ts
Normal 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');
|
||||
}
|
||||
13
mcp_servers/traveller_map/src/index.ts
Normal file
13
mcp_servers/traveller_map/src/index.ts
Normal 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);
|
||||
});
|
||||
134
mcp_servers/traveller_map/src/parsers/sec.ts
Normal file
134
mcp_servers/traveller_map/src/parsers/sec.ts
Normal 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;
|
||||
}
|
||||
231
mcp_servers/traveller_map/src/parsers/uwp.ts
Normal file
231
mcp_servers/traveller_map/src/parsers/uwp.ts
Normal 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: '1–10%' },
|
||||
2: { description: 'Dry world', percent: '11–20%' },
|
||||
3: { description: 'Dry world', percent: '21–30%' },
|
||||
4: { description: 'Wet world', percent: '31–40%' },
|
||||
5: { description: 'Wet world', percent: '41–50%' },
|
||||
6: { description: 'Wet world', percent: '51–60%' },
|
||||
7: { description: 'Wet world — significant oceans', percent: '61–70%' },
|
||||
8: { description: 'Water world — large oceans', percent: '71–80%' },
|
||||
9: { description: 'Water world — very large oceans', percent: '81–90%' },
|
||||
10: { description: 'Water world — global ocean', percent: '91–100%' },
|
||||
};
|
||||
|
||||
const POPULATION_DESC: Record<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',
|
||||
},
|
||||
};
|
||||
}
|
||||
88
mcp_servers/traveller_map/src/server.ts
Normal file
88
mcp_servers/traveller_map/src/server.ts
Normal 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;
|
||||
}
|
||||
147
mcp_servers/traveller_map/src/tools/find_route.ts
Normal file
147
mcp_servers/traveller_map/src/tools/find_route.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
42
mcp_servers/traveller_map/src/tools/get_allegiance_list.ts
Normal file
42
mcp_servers/traveller_map/src/tools/get_allegiance_list.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
112
mcp_servers/traveller_map/src/tools/get_jump_map.ts
Normal file
112
mcp_servers/traveller_map/src/tools/get_jump_map.ts
Normal 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}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
51
mcp_servers/traveller_map/src/tools/get_sector_data.ts
Normal file
51
mcp_servers/traveller_map/src/tools/get_sector_data.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
67
mcp_servers/traveller_map/src/tools/get_sector_list.ts
Normal file
67
mcp_servers/traveller_map/src/tools/get_sector_list.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
76
mcp_servers/traveller_map/src/tools/get_sector_metadata.ts
Normal file
76
mcp_servers/traveller_map/src/tools/get_sector_metadata.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
109
mcp_servers/traveller_map/src/tools/get_subsector_map.ts
Normal file
109
mcp_servers/traveller_map/src/tools/get_subsector_map.ts
Normal 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}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
169
mcp_servers/traveller_map/src/tools/get_world_info.ts
Normal file
169
mcp_servers/traveller_map/src/tools/get_world_info.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
54
mcp_servers/traveller_map/src/tools/render_custom_map.ts
Normal file
54
mcp_servers/traveller_map/src/tools/render_custom_map.ts
Normal 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})` : ''}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
139
mcp_servers/traveller_map/src/tools/search_worlds.ts
Normal file
139
mcp_servers/traveller_map/src/tools/search_worlds.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
17
mcp_servers/traveller_map/tsconfig.json
Normal file
17
mcp_servers/traveller_map/tsconfig.json
Normal 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"]
|
||||
}
|
||||
@@ -12,3 +12,7 @@ system_prompt: |
|
||||
# args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
||||
# - name: brave_search
|
||||
# url: "http://localhost:3000/sse"
|
||||
mcp_servers:
|
||||
- name: traveller-map
|
||||
command: node
|
||||
args: [ "/home/morr/work/traveller-map-mcp/dist/server.js" ]
|
||||
|
||||
60
profiles/docs/traveller_scout_ship.md
Normal file
60
profiles/docs/traveller_scout_ship.md
Normal 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)
|
||||
38
profiles/traveller_scout.yaml
Normal file
38
profiles/traveller_scout.yaml
Normal 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
70
scripts/list_voices.py
Normal 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
57
scripts/register_voice.py
Normal 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()
|
||||
Reference in New Issue
Block a user