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 asyncio
|
||||||
|
import queue as _queue
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
from . import llm, tts, audio, config
|
from . import llm, tts, audio, config
|
||||||
|
|
||||||
|
|
||||||
@@ -34,22 +37,122 @@ def _set_voice(parts: list[str]) -> None:
|
|||||||
print(f"Voix définie sur : {config.VOICE_ID}")
|
print(f"Voix définie sur : {config.VOICE_ID}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sentence splitting & markdown cleaning for TTS pipeline
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Split on sentence boundaries using fixed-width lookbehinds (Python constraint).
|
||||||
|
# For ".": 3-char lookbehind — require the 2 chars before the period to be
|
||||||
|
# lowercase, digit, hyphen, or closing bracket (NOT uppercase). This protects
|
||||||
|
# "M.", "No.", "A." while correctly splitting "arrivé.", "Jump-2.", "(J2).", "ok."
|
||||||
|
_SENT_RE = re.compile(
|
||||||
|
r'(?<=[a-z\u00e0-\u00ff\d\-)\]]{2}[.])\s+' # word(2+) + period
|
||||||
|
r'|(?<=[a-z\u00e0-\u00ff\d\-)\]]{2}[.][»"\')\]])\s+' # + closing quote
|
||||||
|
r'|(?<=[!?…])\s+' # ! ? …
|
||||||
|
r'|(?<=[!?…][»"\')\]])\s+' # ! ? … + closing quote
|
||||||
|
r'|\n{2,}' # paragraph break
|
||||||
|
)
|
||||||
|
|
||||||
|
# Markdown patterns to strip before TTS (keep inner text where applicable)
|
||||||
|
_MD_CODE_BLOCK = re.compile(r'```.*?```', re.DOTALL)
|
||||||
|
_MD_INLINE = re.compile(r'\*{1,3}(.*?)\*{1,3}|_{1,2}(.*?)_{1,2}|~~(.*?)~~|`([^`]+)`', re.DOTALL)
|
||||||
|
_MD_LINK = re.compile(r'\[([^\]]*)\]\([^\)]*\)')
|
||||||
|
_MD_HEADER = re.compile(r'^#{1,6}\s+', re.MULTILINE)
|
||||||
|
_MULTI_SPACE = re.compile(r'\s+')
|
||||||
|
|
||||||
|
|
||||||
|
def _split_sentences(text: str) -> tuple[list[str], str]:
|
||||||
|
"""Extrait les phrases complètes d'un buffer partiel.
|
||||||
|
|
||||||
|
Returns (complete_sentences, remainder).
|
||||||
|
"""
|
||||||
|
parts = _SENT_RE.split(text)
|
||||||
|
if len(parts) <= 1:
|
||||||
|
return [], text
|
||||||
|
return parts[:-1], parts[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_for_tts(text: str) -> str:
|
||||||
|
"""Supprime le formatage Markdown avant la synthèse vocale."""
|
||||||
|
text = _MD_CODE_BLOCK.sub(' ', text)
|
||||||
|
text = _MD_INLINE.sub(lambda m: next(g for g in m.groups() if g is not None), text)
|
||||||
|
text = _MD_LINK.sub(r'\1', text)
|
||||||
|
text = _MD_HEADER.sub('', text)
|
||||||
|
text = text.replace('→', ' vers ').replace('←', ' depuis ')
|
||||||
|
text = _MULTI_SPACE.sub(' ', text).strip()
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Message processing — streaming TTS pipeline
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _process_message(user_input: str) -> None:
|
def _process_message(user_input: str) -> None:
|
||||||
"""Envoie un message au LLM et lit la réponse à voix haute."""
|
"""Envoie un message au LLM et lit la réponse à voix haute.
|
||||||
print(f"Arioch > ", end="", flush=True)
|
|
||||||
try:
|
|
||||||
reply = llm.chat(user_input)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n[Erreur LLM] {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(reply)
|
Pipeline 3 étages en parallèle :
|
||||||
|
[LLM stream] → sentence_queue → [TTS thread] → audio_queue → [Audio thread]
|
||||||
|
|
||||||
|
Dès qu'une phrase complète est détectée dans le stream LLM, elle part
|
||||||
|
immédiatement en synthèse Voxtral. L'audio est joué dès qu'il est prêt,
|
||||||
|
pendant que la phrase suivante est déjà en cours de synthèse.
|
||||||
|
"""
|
||||||
|
SENTINEL = object()
|
||||||
|
sentence_q: _queue.Queue = _queue.Queue()
|
||||||
|
audio_q: _queue.Queue = _queue.Queue()
|
||||||
|
|
||||||
|
def tts_worker() -> None:
|
||||||
|
while True:
|
||||||
|
item = sentence_q.get()
|
||||||
|
if item is SENTINEL:
|
||||||
|
audio_q.put(SENTINEL)
|
||||||
|
return
|
||||||
|
text = _clean_for_tts(item)
|
||||||
|
if text:
|
||||||
|
try:
|
||||||
|
audio_bytes = tts.text_to_speech(text)
|
||||||
|
audio_q.put(audio_bytes)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n[Erreur TTS] {e}", flush=True)
|
||||||
|
|
||||||
|
def audio_worker() -> None:
|
||||||
|
while True:
|
||||||
|
item = audio_q.get()
|
||||||
|
if item is SENTINEL:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
audio.play_audio(item)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n[Erreur Audio] {e}", flush=True)
|
||||||
|
|
||||||
|
tts_thread = threading.Thread(target=tts_worker, daemon=True, name="tts-worker")
|
||||||
|
audio_thread = threading.Thread(target=audio_worker, daemon=True, name="audio-worker")
|
||||||
|
tts_thread.start()
|
||||||
|
audio_thread.start()
|
||||||
|
|
||||||
|
print("Arioch > ", end="", flush=True)
|
||||||
|
buffer = ""
|
||||||
try:
|
try:
|
||||||
audio_bytes = tts.text_to_speech(reply)
|
for chunk in llm.chat_stream(user_input):
|
||||||
audio.play_audio(audio_bytes)
|
print(chunk, end="", flush=True)
|
||||||
|
buffer += chunk
|
||||||
|
sentences, buffer = _split_sentences(buffer)
|
||||||
|
for sentence in sentences:
|
||||||
|
sentence = sentence.strip()
|
||||||
|
if sentence:
|
||||||
|
sentence_q.put(sentence)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Erreur TTS/Audio] {e}")
|
print(f"\n[Erreur LLM] {e}", flush=True)
|
||||||
|
|
||||||
|
# Flush any remaining text after the stream ends
|
||||||
|
if buffer.strip():
|
||||||
|
sentence_q.put(buffer.strip())
|
||||||
|
|
||||||
|
print() # newline after full response
|
||||||
|
sentence_q.put(SENTINEL)
|
||||||
|
|
||||||
|
tts_thread.join()
|
||||||
|
audio_thread.join()
|
||||||
|
|
||||||
|
|
||||||
def _handle_command(user_input: str) -> bool:
|
def _handle_command(user_input: str) -> bool:
|
||||||
@@ -118,7 +221,7 @@ def _handle_mcp(parts: list[str]) -> None:
|
|||||||
print(f"\nTotal : {total} outil(s). Tapez 'mcp tools' pour les lister.\n")
|
print(f"\nTotal : {total} outil(s). Tapez 'mcp tools' pour les lister.\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _list_profiles(profiles: list) -> None:
|
||||||
if not profiles:
|
if not profiles:
|
||||||
print("Aucun profil disponible dans profiles/")
|
print("Aucun profil disponible dans profiles/")
|
||||||
return
|
return
|
||||||
|
|||||||
20
assistant/config.py
Normal file
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
|
import json
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
from mistralai.client import Mistral
|
from mistralai.client import Mistral
|
||||||
from . import config
|
from . import config
|
||||||
@@ -13,8 +14,12 @@ def reset_history() -> None:
|
|||||||
_history.clear()
|
_history.clear()
|
||||||
|
|
||||||
|
|
||||||
def chat(user_message: str) -> str:
|
def chat_stream(user_message: str) -> Generator[str, None, None]:
|
||||||
"""Envoie un message au LLM, gère les appels d'outils MCP et retourne la réponse."""
|
"""Génère la réponse du LLM token par token via streaming.
|
||||||
|
|
||||||
|
Les appels d'outils MCP sont exécutés internement (sans streaming).
|
||||||
|
Seule la réponse textuelle finale est streamée sous forme de chunks.
|
||||||
|
"""
|
||||||
from . import mcp_client
|
from . import mcp_client
|
||||||
|
|
||||||
_history.append({"role": "user", "content": user_message})
|
_history.append({"role": "user", "content": user_message})
|
||||||
@@ -24,20 +29,30 @@ def chat(user_message: str) -> str:
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = [{"role": "system", "content": config.SYSTEM_PROMPT}] + _history
|
messages = [{"role": "system", "content": config.SYSTEM_PROMPT}] + _history
|
||||||
|
|
||||||
kwargs: dict = {"model": config.LLM_MODEL, "messages": messages}
|
kwargs: dict = {"model": config.LLM_MODEL, "messages": messages}
|
||||||
if tools:
|
if tools:
|
||||||
kwargs["tools"] = tools
|
kwargs["tools"] = tools
|
||||||
|
|
||||||
response = _client.chat.complete(**kwargs)
|
accumulated_content = ""
|
||||||
choice = response.choices[0]
|
tool_calls_received = None
|
||||||
msg = choice.message
|
|
||||||
|
|
||||||
if msg.tool_calls:
|
for event in _client.chat.stream(**kwargs):
|
||||||
# 1. Ajouter le message assistant (avec les appels d'outils) à l'historique
|
ch = event.data.choices[0]
|
||||||
|
delta = ch.delta
|
||||||
|
|
||||||
|
# Yield text chunks (isinstance check guards against Unset sentinel)
|
||||||
|
if isinstance(delta.content, str) and delta.content:
|
||||||
|
accumulated_content += delta.content
|
||||||
|
yield delta.content
|
||||||
|
|
||||||
|
if delta.tool_calls:
|
||||||
|
tool_calls_received = delta.tool_calls
|
||||||
|
|
||||||
|
if tool_calls_received:
|
||||||
|
# Append assistant turn with tool calls to history
|
||||||
_history.append({
|
_history.append({
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": msg.content or "",
|
"content": accumulated_content or "",
|
||||||
"tool_calls": [
|
"tool_calls": [
|
||||||
{
|
{
|
||||||
"id": tc.id,
|
"id": tc.id,
|
||||||
@@ -47,12 +62,12 @@ def chat(user_message: str) -> str:
|
|||||||
"arguments": tc.function.arguments,
|
"arguments": tc.function.arguments,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for tc in msg.tool_calls
|
for tc in tool_calls_received
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
# 2. Exécuter chaque outil et ajouter les résultats
|
# Execute each tool and append results
|
||||||
for tc in msg.tool_calls:
|
for tc in tool_calls_received:
|
||||||
tool_name = tc.function.name
|
tool_name = tc.function.name
|
||||||
try:
|
try:
|
||||||
args = (
|
args = (
|
||||||
@@ -60,7 +75,7 @@ def chat(user_message: str) -> str:
|
|||||||
if isinstance(tc.function.arguments, str)
|
if isinstance(tc.function.arguments, str)
|
||||||
else tc.function.arguments
|
else tc.function.arguments
|
||||||
)
|
)
|
||||||
print(f" 🔧 [MCP] {tool_name}({_short_args(args)})")
|
print(f"\n 🔧 [MCP] {tool_name}({_short_args(args)})", flush=True)
|
||||||
result = manager.call_tool(tool_name, args)
|
result = manager.call_tool(tool_name, args)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
result = f"Erreur lors de l'appel à {tool_name} : {e}"
|
result = f"Erreur lors de l'appel à {tool_name} : {e}"
|
||||||
@@ -70,13 +85,17 @@ def chat(user_message: str) -> str:
|
|||||||
"content": result,
|
"content": result,
|
||||||
"tool_call_id": tc.id,
|
"tool_call_id": tc.id,
|
||||||
})
|
})
|
||||||
|
# Loop to get the next (final) response
|
||||||
# 3. Reboucler pour obtenir la réponse finale
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
reply = msg.content or ""
|
# Pure text response — already yielded chunk by chunk; save to history
|
||||||
_history.append({"role": "assistant", "content": reply})
|
_history.append({"role": "assistant", "content": accumulated_content})
|
||||||
return reply
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def chat(user_message: str) -> str:
|
||||||
|
"""Envoie un message au LLM et retourne la réponse complète (non-streaming)."""
|
||||||
|
return "".join(chat_stream(user_message))
|
||||||
|
|
||||||
|
|
||||||
def _short_args(args: dict) -> str:
|
def _short_args(args: dict) -> str:
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ Configure les serveurs dans un profil YAML sous la clé `mcp_servers` :
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
from contextlib import AsyncExitStack
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
# Racine du projet (le dossier qui contient main.py)
|
||||||
|
_PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MCPServerConfig:
|
class MCPServerConfig:
|
||||||
@@ -27,6 +31,18 @@ class MCPServerConfig:
|
|||||||
env: dict[str, str] | None = None
|
env: dict[str, str] | None = None
|
||||||
url: str | None = None
|
url: str | None = None
|
||||||
|
|
||||||
|
def resolved_args(self) -> list[str]:
|
||||||
|
"""Résout les chemins relatifs dans args par rapport à la racine du projet."""
|
||||||
|
result = []
|
||||||
|
for arg in self.args:
|
||||||
|
p = Path(arg)
|
||||||
|
if not p.is_absolute() and p.suffix in (".js", ".py", ".ts"):
|
||||||
|
resolved = (_PROJECT_ROOT / p).resolve()
|
||||||
|
result.append(str(resolved))
|
||||||
|
else:
|
||||||
|
result.append(arg)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_name(name: str) -> str:
|
def _sanitize_name(name: str) -> str:
|
||||||
"""Transforme un nom en identifiant valide pour l'API Mistral (^[a-zA-Z0-9_-]{1,64}$)."""
|
"""Transforme un nom en identifiant valide pour l'API Mistral (^[a-zA-Z0-9_-]{1,64}$)."""
|
||||||
@@ -34,7 +50,12 @@ def _sanitize_name(name: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class MCPManager:
|
class MCPManager:
|
||||||
"""Gère les connexions aux serveurs MCP et l'exécution des outils."""
|
"""Gère les connexions aux serveurs MCP et l'exécution des outils.
|
||||||
|
|
||||||
|
Chaque connexion tourne dans une "keeper task" qui possède toute la durée
|
||||||
|
de vie du context manager stdio_client / ClientSession. Cela évite l'erreur
|
||||||
|
anyio "Attempted to exit cancel scope in a different task".
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._loop = asyncio.new_event_loop()
|
self._loop = asyncio.new_event_loop()
|
||||||
@@ -42,9 +63,9 @@ class MCPManager:
|
|||||||
self._thread.start()
|
self._thread.start()
|
||||||
self._sessions: dict[str, Any] = {}
|
self._sessions: dict[str, Any] = {}
|
||||||
self._raw_tools: dict[str, list] = {}
|
self._raw_tools: dict[str, list] = {}
|
||||||
# mistral_name -> (server_name, original_tool_name)
|
|
||||||
self._tool_map: dict[str, tuple[str, str]] = {}
|
self._tool_map: dict[str, tuple[str, str]] = {}
|
||||||
self._exit_stacks: dict[str, AsyncExitStack] = {}
|
# stop event per server, signalled at shutdown
|
||||||
|
self._stop_events: dict[str, asyncio.Event] = {}
|
||||||
|
|
||||||
def _run_loop(self) -> None:
|
def _run_loop(self) -> None:
|
||||||
asyncio.set_event_loop(self._loop)
|
asyncio.set_event_loop(self._loop)
|
||||||
@@ -87,10 +108,18 @@ class MCPManager:
|
|||||||
return self._run(self._call_tool_async(server_name, tool_name, arguments))
|
return self._run(self._call_tool_async(server_name, tool_name, arguments))
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
|
"""Signale l'arrêt à toutes les keeper tasks et attend brièvement."""
|
||||||
|
async def _signal_all() -> None:
|
||||||
|
for ev in self._stop_events.values():
|
||||||
|
ev.set()
|
||||||
|
# Laisser une courte fenêtre pour que les tâches se terminent proprement
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._run(self._shutdown_async(), timeout=10)
|
self._run(_signal_all(), timeout=5)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
self._stop_events.clear()
|
||||||
|
|
||||||
def summary(self) -> list[tuple[str, int]]:
|
def summary(self) -> list[tuple[str, int]]:
|
||||||
"""Retourne [(server_name, tool_count), ...] pour les serveurs connectés."""
|
"""Retourne [(server_name, tool_count), ...] pour les serveurs connectés."""
|
||||||
@@ -112,38 +141,78 @@ class MCPManager:
|
|||||||
print(f"[MCP] ❌ Connexion {cfg.name} impossible : {e}")
|
print(f"[MCP] ❌ Connexion {cfg.name} impossible : {e}")
|
||||||
|
|
||||||
async def _connect_server(self, cfg: MCPServerConfig) -> None:
|
async def _connect_server(self, cfg: MCPServerConfig) -> None:
|
||||||
from mcp import ClientSession, StdioServerParameters
|
"""Lance la keeper task et attend que la connexion soit établie."""
|
||||||
from mcp.client.stdio import stdio_client
|
stop_event = asyncio.Event()
|
||||||
|
ready_event = asyncio.Event()
|
||||||
|
error_holder: list[Exception] = []
|
||||||
|
|
||||||
stack = AsyncExitStack()
|
self._stop_events[cfg.name] = stop_event
|
||||||
|
|
||||||
if cfg.command:
|
asyncio.create_task(
|
||||||
params = StdioServerParameters(
|
self._run_server(cfg, ready_event, stop_event, error_holder),
|
||||||
command=cfg.command,
|
name=f"mcp-keeper-{cfg.name}",
|
||||||
args=cfg.args or [],
|
)
|
||||||
env=cfg.env,
|
|
||||||
)
|
|
||||||
read, write = await stack.enter_async_context(stdio_client(params))
|
|
||||||
else:
|
|
||||||
from mcp.client.sse import sse_client
|
|
||||||
read, write = await stack.enter_async_context(sse_client(cfg.url))
|
|
||||||
|
|
||||||
session = await stack.enter_async_context(ClientSession(read, write))
|
# Attendre que la connexion soit prête (ou échoue)
|
||||||
await session.initialize()
|
try:
|
||||||
|
await asyncio.wait_for(ready_event.wait(), timeout=30)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
stop_event.set()
|
||||||
|
raise TimeoutError(f"Timeout lors de la connexion à {cfg.name}")
|
||||||
|
|
||||||
self._sessions[cfg.name] = session
|
if error_holder:
|
||||||
self._exit_stacks[cfg.name] = stack
|
raise error_holder[0]
|
||||||
|
|
||||||
tools_resp = await session.list_tools()
|
async def _run_server(
|
||||||
self._raw_tools[cfg.name] = tools_resp.tools
|
self,
|
||||||
|
cfg: MCPServerConfig,
|
||||||
|
ready_event: asyncio.Event,
|
||||||
|
stop_event: asyncio.Event,
|
||||||
|
error_holder: list[Exception],
|
||||||
|
) -> None:
|
||||||
|
"""Keeper task : possède le context manager de bout en bout."""
|
||||||
|
try:
|
||||||
|
from mcp import ClientSession, StdioServerParameters
|
||||||
|
|
||||||
server_safe = _sanitize_name(cfg.name)
|
if cfg.command:
|
||||||
for tool in tools_resp.tools:
|
from mcp.client.stdio import stdio_client
|
||||||
tool_safe = _sanitize_name(tool.name)
|
transport = stdio_client(StdioServerParameters(
|
||||||
mistral_name = f"{server_safe}__{tool_safe}"
|
command=cfg.command,
|
||||||
self._tool_map[mistral_name] = (cfg.name, tool.name)
|
args=cfg.resolved_args(),
|
||||||
|
env=cfg.env,
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
from mcp.client.sse import sse_client
|
||||||
|
transport = sse_client(cfg.url)
|
||||||
|
|
||||||
print(f"[MCP] ✅ {cfg.name} — {len(tools_resp.tools)} outil(s) disponible(s)")
|
async with transport as (read, write):
|
||||||
|
async with ClientSession(read, write) as session:
|
||||||
|
await session.initialize()
|
||||||
|
|
||||||
|
tools_resp = await session.list_tools()
|
||||||
|
self._sessions[cfg.name] = session
|
||||||
|
self._raw_tools[cfg.name] = tools_resp.tools
|
||||||
|
|
||||||
|
server_safe = _sanitize_name(cfg.name)
|
||||||
|
for tool in tools_resp.tools:
|
||||||
|
tool_safe = _sanitize_name(tool.name)
|
||||||
|
self._tool_map[f"{server_safe}__{tool_safe}"] = (cfg.name, tool.name)
|
||||||
|
|
||||||
|
print(f"[MCP] ✅ {cfg.name} — {len(tools_resp.tools)} outil(s) disponible(s)")
|
||||||
|
ready_event.set()
|
||||||
|
|
||||||
|
# Maintenir la connexion jusqu'au signal d'arrêt
|
||||||
|
await stop_event.wait()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_holder.append(e)
|
||||||
|
ready_event.set() # débloquer _connect_server même en cas d'erreur
|
||||||
|
finally:
|
||||||
|
self._sessions.pop(cfg.name, None)
|
||||||
|
self._raw_tools.pop(cfg.name, None)
|
||||||
|
to_remove = [k for k, v in self._tool_map.items() if v[0] == cfg.name]
|
||||||
|
for k in to_remove:
|
||||||
|
del self._tool_map[k]
|
||||||
|
|
||||||
async def _call_tool_async(self, server_name: str, tool_name: str, arguments: dict) -> str:
|
async def _call_tool_async(self, server_name: str, tool_name: str, arguments: dict) -> str:
|
||||||
session = self._sessions[server_name]
|
session = self._sessions[server_name]
|
||||||
@@ -156,17 +225,6 @@ class MCPManager:
|
|||||||
parts.append(str(item))
|
parts.append(str(item))
|
||||||
return "\n".join(parts) if parts else "(aucun résultat)"
|
return "\n".join(parts) if parts else "(aucun résultat)"
|
||||||
|
|
||||||
async def _shutdown_async(self) -> None:
|
|
||||||
for stack in list(self._exit_stacks.values()):
|
|
||||||
try:
|
|
||||||
await stack.aclose()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._sessions.clear()
|
|
||||||
self._raw_tools.clear()
|
|
||||||
self._tool_map.clear()
|
|
||||||
self._exit_stacks.clear()
|
|
||||||
|
|
||||||
|
|
||||||
_manager: MCPManager | None = None
|
_manager: MCPManager | None = None
|
||||||
_lock = threading.Lock()
|
_lock = threading.Lock()
|
||||||
@@ -185,4 +243,6 @@ def reset_manager() -> None:
|
|||||||
with _lock:
|
with _lock:
|
||||||
if _manager is not None:
|
if _manager is not None:
|
||||||
_manager.shutdown()
|
_manager.shutdown()
|
||||||
_manager = MCPManager()
|
_manager = MCPManager()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
109
assistant/stt.py
Normal file
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":"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"}
|
||||||
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"]
|
# args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
||||||
# - name: brave_search
|
# - name: brave_search
|
||||||
# url: "http://localhost:3000/sse"
|
# url: "http://localhost:3000/sse"
|
||||||
|
mcp_servers:
|
||||||
|
- name: traveller-map
|
||||||
|
command: node
|
||||||
|
args: [ "/home/morr/work/traveller-map-mcp/dist/server.js" ]
|
||||||
|
|||||||
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