Add gitignor

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

View File

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

View File

@@ -0,0 +1 @@
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;AAYhF,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAUtF;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAczF;AAED,wBAAsB,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,OAAO,CAAC,CAAC,CAAC,CAGtF;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAGxF;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,WAAgB,GACvB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAU/C;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,MAAM,CAAC,CAkBjB"}

View File

@@ -0,0 +1,76 @@
const BASE_URL = 'https://travellermap.com';
const USER_AGENT = 'traveller-map-mcp/1.0 (github.com/shammond42/traveller-map-mcp)';
function buildUrl(path, params) {
const url = new URL(path, BASE_URL);
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) {
url.searchParams.set(key, String(value));
}
}
return url;
}
export async function apiGet(path, params = {}) {
const url = buildUrl(path, params);
const response = await fetch(url.toString(), {
headers: { 'User-Agent': USER_AGENT },
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Traveller Map API error ${response.status} at ${path}: ${body}`);
}
return response;
}
export async function apiGetImage(path, params = {}) {
const url = buildUrl(path, params);
const response = await fetch(url.toString(), {
headers: {
'User-Agent': USER_AGENT,
'Accept': 'image/png',
},
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Traveller Map API error ${response.status} at ${path}: ${body}`);
}
const buffer = await response.arrayBuffer();
return Buffer.from(buffer).toString('base64');
}
export async function apiGetJson(path, params = {}) {
const response = await apiGet(path, { ...params, accept: 'application/json' });
return response.json();
}
export async function apiGetText(path, params = {}) {
const response = await apiGet(path, params);
return response.text();
}
export async function apiGetDataUri(path, params = {}) {
const response = await apiGet(path, { ...params, datauri: 1 });
const text = await response.text();
for (const mimeType of ['image/png', 'image/jpeg']) {
const prefix = `data:${mimeType};base64,`;
if (text.startsWith(prefix)) {
return { base64: text.slice(prefix.length), mimeType };
}
}
throw new Error(`Unexpected data URI format from ${path}: ${text.slice(0, 50)}`);
}
export async function apiPostImage(path, queryParams, formBody) {
const url = buildUrl(path, queryParams);
const body = new URLSearchParams(formBody).toString();
const response = await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT,
'Accept': 'image/png',
},
body,
});
if (!response.ok) {
const responseBody = await response.text();
throw new Error(`Traveller Map API error ${response.status} at POST ${path}: ${responseBody}`);
}
const buffer = await response.arrayBuffer();
return Buffer.from(buffer).toString('base64');
}
//# sourceMappingURL=client.js.map

View File

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

View File

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

View File

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

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

@@ -0,0 +1,12 @@
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createServer } from './server.js';
async function main() {
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}

View File

@@ -0,0 +1,22 @@
import { type DecodedUWP } from './uwp.js';
export interface WorldRecord {
hex: string;
name: string;
uwp: string;
decoded_uwp: DecodedUWP;
bases: string;
remarks: string;
trade_codes: string[];
zone: string;
pbg: string;
allegiance: string;
stars: string;
importance?: string;
economic?: string;
cultural?: string;
nobility?: string;
worlds?: string;
resource_units?: string;
}
export declare function parseSecTabDelimited(text: string): WorldRecord[];
//# sourceMappingURL=sec.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sec.d.ts","sourceRoot":"","sources":["../../src/parsers/sec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,UAAU,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAuBD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CA0FhE"}

View File

@@ -0,0 +1,106 @@
import { parseUWP } from './uwp.js';
const ZONE_MAP = {
R: 'Red',
A: 'Amber',
'': 'Green',
'-': 'Green',
' ': 'Green',
G: 'Green',
};
function normalizeZone(zone) {
return ZONE_MAP[zone?.trim() ?? ''] ?? zone?.trim() ?? 'Green';
}
function parseRemarks(remarks) {
if (!remarks)
return [];
return remarks
.trim()
.split(/\s+/)
.filter((r) => r.length > 0 && r !== '-');
}
export function parseSecTabDelimited(text) {
const lines = text.split('\n');
const worlds = [];
let headerLine = '';
let headerIndex = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('#') || line.trim() === '')
continue;
if (/^[-\s]+$/.test(line))
continue;
if (line.toLowerCase().includes('hex') || line.toLowerCase().includes('name')) {
headerLine = line;
headerIndex = i;
break;
}
}
if (!headerLine) {
return worlds;
}
const headers = headerLine.split('\t').map((h) => h.trim().toLowerCase());
const colIndex = (names) => {
for (const name of names) {
const idx = headers.indexOf(name);
if (idx !== -1)
return idx;
}
return -1;
};
const hexIdx = colIndex(['hex']);
const nameIdx = colIndex(['name', 'world name']);
const uwpIdx = colIndex(['uwp']);
const basesIdx = colIndex(['bases', 'base']);
const remarksIdx = colIndex(['remarks', 'trade codes', 'tradecodes']);
const zoneIdx = colIndex(['zone', 'iz', 'travel zone']);
const pbgIdx = colIndex(['pbg']);
const allegIdx = colIndex(['allegiance', 'alleg', 'a']);
const starsIdx = colIndex(['stars', 'stellar data', 'stellar']);
const importanceIdx = colIndex(['{ix}', 'ix', 'importance', '{importance}']);
const economicIdx = colIndex(['(ex)', 'ex', 'economic', '(economic)']);
const culturalIdx = colIndex(['[cx]', 'cx', 'cultural', '[cultural]']);
const nobilityIdx = colIndex(['nobility', 'nobil', 'n']);
const worldsIdx = colIndex(['w', 'worlds']);
const ruIdx = colIndex(['ru', 'resource units']);
for (let i = headerIndex + 1; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('#') || line.trim() === '')
continue;
if (/^[-\s]+$/.test(line))
continue;
const cols = line.split('\t');
if (cols.length < 3)
continue;
const get = (idx) => (idx >= 0 ? cols[idx]?.trim() ?? '' : '');
const uwpRaw = get(uwpIdx);
let decodedUwp;
try {
decodedUwp = parseUWP(uwpRaw);
}
catch {
decodedUwp = parseUWP('?000000-0');
}
const remarksRaw = get(remarksIdx);
worlds.push({
hex: get(hexIdx),
name: get(nameIdx),
uwp: uwpRaw,
decoded_uwp: decodedUwp,
bases: get(basesIdx),
remarks: remarksRaw,
trade_codes: parseRemarks(remarksRaw),
zone: normalizeZone(get(zoneIdx)),
pbg: get(pbgIdx),
allegiance: get(allegIdx),
stars: get(starsIdx),
importance: importanceIdx >= 0 ? get(importanceIdx) : undefined,
economic: economicIdx >= 0 ? get(economicIdx) : undefined,
cultural: culturalIdx >= 0 ? get(culturalIdx) : undefined,
nobility: nobilityIdx >= 0 ? get(nobilityIdx) : undefined,
worlds: worldsIdx >= 0 ? get(worldsIdx) : undefined,
resource_units: ruIdx >= 0 ? get(ruIdx) : undefined,
});
}
return worlds;
}
//# sourceMappingURL=sec.js.map

View File

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

View File

@@ -0,0 +1,30 @@
export interface DecodedUWPField {
code: string;
value: number;
description: string;
}
export interface DecodedUWP {
raw: string;
starport: {
code: string;
description: string;
};
size: DecodedUWPField & {
diameter_km: string;
};
atmosphere: DecodedUWPField;
hydrographics: DecodedUWPField & {
percent: string;
};
population: DecodedUWPField & {
estimate: string;
};
government: DecodedUWPField;
law_level: DecodedUWPField;
tech_level: DecodedUWPField & {
era: string;
};
}
export declare function eHexValue(char: string): number;
export declare function parseUWP(uwp: string): DecodedUWP;
//# sourceMappingURL=uwp.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"uwp.d.ts","sourceRoot":"","sources":["../../src/parsers/uwp.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,IAAI,EAAE,eAAe,GAAG;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,UAAU,EAAE,eAAe,CAAC;IAC5B,aAAa,EAAE,eAAe,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,UAAU,EAAE,eAAe,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,UAAU,EAAE,eAAe,CAAC;IAC5B,SAAS,EAAE,eAAe,CAAC;IAC3B,UAAU,EAAE,eAAe,GAAG;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C;AAyID,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAsEhD"}

View File

@@ -0,0 +1,203 @@
export function eHexValue(char) {
const c = char.toUpperCase();
if (c >= '0' && c <= '9')
return parseInt(c, 10);
if (c >= 'A' && c <= 'Z')
return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10;
return 0;
}
const STARPORT = {
A: 'Excellent — full repair, refined fuel, shipyard capable',
B: 'Good — full repair, refined fuel available',
C: 'Routine — some repair, unrefined fuel available',
D: 'Poor — limited repair, unrefined fuel only',
E: 'Frontier — no repair, no fuel',
X: 'None — no starport facilities',
F: 'Good — spaceport (non-starship capable)',
G: 'Poor — primitive spaceport',
H: 'Primitive — minimal facilities',
Y: 'None',
};
const SIZE_DESC = {
0: { description: 'Asteroid/planetoid belt or very small body', diameter_km: '<800 km' },
1: { description: 'Small world', diameter_km: '~1,600 km' },
2: { description: 'Small world', diameter_km: '~3,200 km' },
3: { description: 'Small world', diameter_km: '~4,800 km' },
4: { description: 'Small world', diameter_km: '~6,400 km' },
5: { description: 'Medium world', diameter_km: '~8,000 km' },
6: { description: 'Medium world (Earth-like)', diameter_km: '~9,600 km' },
7: { description: 'Medium world', diameter_km: '~11,200 km' },
8: { description: 'Large world', diameter_km: '~12,800 km' },
9: { description: 'Large world', diameter_km: '~14,400 km' },
10: { description: 'Large world', diameter_km: '~16,000 km' },
};
const ATMOSPHERE_DESC = {
0: 'None — vacuum',
1: 'Trace — very thin, requires vacc suit',
2: 'Very Thin, Tainted — requires filter mask and compressor',
3: 'Very Thin — requires compressor',
4: 'Thin, Tainted — requires filter mask',
5: 'Thin — breathable with some discomfort',
6: 'Standard — breathable',
7: 'Standard, Tainted — requires filter mask',
8: 'Dense — breathable with no special equipment',
9: 'Dense, Tainted — requires filter mask',
10: 'Exotic — requires oxygen supply',
11: 'Corrosive — requires vacc suit',
12: 'Insidious — suit penetrating, requires special protection',
13: 'Dense, High — breathable only at high altitudes',
14: 'Thin, Low — breathable only in lowlands',
15: 'Unusual',
};
const HYDROGRAPHICS_DESC = {
0: { description: 'Desert world — no free water', percent: '0%' },
1: { description: 'Dry world — traces of water', percent: '110%' },
2: { description: 'Dry world', percent: '1120%' },
3: { description: 'Dry world', percent: '2130%' },
4: { description: 'Wet world', percent: '3140%' },
5: { description: 'Wet world', percent: '4150%' },
6: { description: 'Wet world', percent: '5160%' },
7: { description: 'Wet world — significant oceans', percent: '6170%' },
8: { description: 'Water world — large oceans', percent: '7180%' },
9: { description: 'Water world — very large oceans', percent: '8190%' },
10: { description: 'Water world — global ocean', percent: '91100%' },
};
const POPULATION_DESC = {
0: { description: 'Unpopulated or tiny outpost', estimate: 'None or a few individuals' },
1: { description: 'Tens of inhabitants', estimate: '~10s' },
2: { description: 'Hundreds of inhabitants', estimate: '~100s' },
3: { description: 'Thousands of inhabitants', estimate: '~1,000s' },
4: { description: 'Tens of thousands', estimate: '~10,000s' },
5: { description: 'Hundreds of thousands', estimate: '~100,000s' },
6: { description: 'Millions of inhabitants', estimate: '~1,000,000s' },
7: { description: 'Tens of millions', estimate: '~10,000,000s' },
8: { description: 'Hundreds of millions', estimate: '~100,000,000s' },
9: { description: 'Billions of inhabitants', estimate: '~1,000,000,000s' },
10: { description: 'Tens of billions', estimate: '~10,000,000,000s' },
11: { description: 'Hundreds of billions', estimate: '~100,000,000,000s' },
12: { description: 'Trillions of inhabitants', estimate: '~1,000,000,000,000s' },
};
const GOVERNMENT_DESC = {
0: 'No Government Structure — family/clan/tribal',
1: 'Company/Corporation — governed by a company',
2: 'Participating Democracy — rule by citizen vote',
3: 'Self-Perpetuating Oligarchy — ruling class maintains power',
4: 'Representative Democracy — elected representatives',
5: 'Feudal Technocracy — controlled by technology owners',
6: 'Captive Government — controlled by outside power',
7: 'Balkanization — no central authority',
8: 'Civil Service Bureaucracy — rule by competence',
9: 'Impersonal Bureaucracy — rule by rigid law',
10: 'Charismatic Dictator — rule by personality',
11: 'Non-Charismatic Leader — rule by position',
12: 'Charismatic Oligarchy — rule by a few personalities',
13: 'Religious Dictatorship — rule by religious doctrine',
14: 'Religious Autocracy — rule by a religious figure',
15: 'Totalitarian Oligarchy — oppressive rule by a few',
};
const LAW_LEVEL_DESC = {
0: 'No prohibitions — no restrictions on weapons or behavior',
1: 'Body pistols, explosives, nuclear weapons prohibited',
2: 'Portable energy weapons prohibited',
3: 'Machine guns, automatic weapons prohibited',
4: 'Light assault weapons prohibited',
5: 'Personal concealable weapons prohibited',
6: 'All firearms except shotguns prohibited',
7: 'Shotguns prohibited',
8: 'Long blades prohibited in public',
9: 'All weapons outside home prohibited',
10: 'Weapon possession prohibited — weapons locked up',
11: 'Rigid control of civilian movement',
12: 'Unrestricted invasion of privacy',
13: 'Paramilitary law enforcement',
14: 'Full-fledged police state',
15: 'Daily life rigidly controlled',
};
const TECH_LEVEL_ERA = {
0: 'Stone Age / Pre-Industrial',
1: 'Bronze Age / Iron Age',
2: 'Renaissance',
3: 'Industrial Revolution',
4: 'Mechanized Age',
5: 'Broadcast Age',
6: 'Atomic Age',
7: 'Space Age',
8: 'Information Age',
9: 'Pre-Stellar',
10: 'Early Stellar',
11: 'Average Stellar',
12: 'Average Interstellar',
13: 'High Interstellar',
14: 'Average Imperial',
15: 'High Imperial / Average Interstellar II',
16: 'Sophont',
17: 'Advanced',
};
export function parseUWP(uwp) {
const clean = uwp.trim().replace(/\s+/g, '');
const starportCode = clean[0] ?? '?';
const sizeCode = clean[1] ?? '0';
const atmCode = clean[2] ?? '0';
const hydroCode = clean[3] ?? '0';
const popCode = clean[4] ?? '0';
const govCode = clean[5] ?? '0';
const lawCode = clean[6] ?? '0';
const tlCode = clean[8] ?? '0';
const sizeVal = eHexValue(sizeCode);
const atmVal = eHexValue(atmCode);
const hydroVal = eHexValue(hydroCode);
const popVal = eHexValue(popCode);
const govVal = eHexValue(govCode);
const lawVal = eHexValue(lawCode);
const tlVal = eHexValue(tlCode);
const sizeInfo = SIZE_DESC[sizeVal] ?? { description: 'Unknown size', diameter_km: 'Unknown' };
const hydroInfo = HYDROGRAPHICS_DESC[hydroVal] ?? { description: 'Unknown hydrographics', percent: 'Unknown' };
const popInfo = POPULATION_DESC[popVal] ?? { description: 'Unknown population', estimate: 'Unknown' };
return {
raw: uwp,
starport: {
code: starportCode,
description: STARPORT[starportCode.toUpperCase()] ?? `Unknown starport code: ${starportCode}`,
},
size: {
code: sizeCode,
value: sizeVal,
description: sizeInfo.description,
diameter_km: sizeInfo.diameter_km,
},
atmosphere: {
code: atmCode,
value: atmVal,
description: ATMOSPHERE_DESC[atmVal] ?? `Unknown atmosphere code: ${atmCode}`,
},
hydrographics: {
code: hydroCode,
value: hydroVal,
description: hydroInfo.description,
percent: hydroInfo.percent,
},
population: {
code: popCode,
value: popVal,
description: popInfo.description,
estimate: popInfo.estimate,
},
government: {
code: govCode,
value: govVal,
description: GOVERNMENT_DESC[govVal] ?? `Unknown government code: ${govCode}`,
},
law_level: {
code: lawCode,
value: lawVal,
description: LAW_LEVEL_DESC[lawVal] ?? `Unknown law level code: ${lawCode}`,
},
tech_level: {
code: tlCode,
value: tlVal,
description: `Tech Level ${tlVal}`,
era: TECH_LEVEL_ERA[tlVal] ?? 'Advanced/Unknown',
},
};
}
//# sourceMappingURL=uwp.js.map

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -0,0 +1 @@
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAcpE,wBAAgB,YAAY,IAAI,SAAS,CAyExC"}

View File

@@ -0,0 +1,31 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as getSubsectorMap from './tools/get_subsector_map.js';
import * as renderCustomMap from './tools/render_custom_map.js';
import * as getWorldInfo from './tools/get_world_info.js';
import * as searchWorlds from './tools/search_worlds.js';
import * as getJumpMap from './tools/get_jump_map.js';
import * as findRoute from './tools/find_route.js';
import * as getWorldsInJumpRange from './tools/get_worlds_in_jump_range.js';
import * as getSectorList from './tools/get_sector_list.js';
import * as getSectorData from './tools/get_sector_data.js';
import * as getSectorMetadata from './tools/get_sector_metadata.js';
import * as getAllegianceList from './tools/get_allegiance_list.js';
export function createServer() {
const server = new McpServer({
name: 'traveller-map',
version: '1.0.0',
});
server.registerTool(getSubsectorMap.name, { description: getSubsectorMap.description, inputSchema: getSubsectorMap.inputSchema.shape }, (args) => getSubsectorMap.handler(args));
server.registerTool(renderCustomMap.name, { description: renderCustomMap.description, inputSchema: renderCustomMap.inputSchema.shape }, (args) => renderCustomMap.handler(args));
server.registerTool(getWorldInfo.name, { description: getWorldInfo.description, inputSchema: getWorldInfo.inputSchema.shape }, (args) => getWorldInfo.handler(args));
server.registerTool(searchWorlds.name, { description: searchWorlds.description, inputSchema: searchWorlds.inputSchema.shape }, (args) => searchWorlds.handler(args));
server.registerTool(getJumpMap.name, { description: getJumpMap.description, inputSchema: getJumpMap.inputSchema.shape }, (args) => getJumpMap.handler(args));
server.registerTool(findRoute.name, { description: findRoute.description, inputSchema: findRoute.inputSchema.shape }, (args) => findRoute.handler(args));
server.registerTool(getWorldsInJumpRange.name, { description: getWorldsInJumpRange.description, inputSchema: getWorldsInJumpRange.inputSchema.shape }, (args) => getWorldsInJumpRange.handler(args));
server.registerTool(getSectorList.name, { description: getSectorList.description, inputSchema: getSectorList.inputSchema.shape }, (args) => getSectorList.handler(args));
server.registerTool(getSectorData.name, { description: getSectorData.description, inputSchema: getSectorData.inputSchema.shape }, (args) => getSectorData.handler(args));
server.registerTool(getSectorMetadata.name, { description: getSectorMetadata.description, inputSchema: getSectorMetadata.inputSchema.shape }, (args) => getSectorMetadata.handler(args));
server.registerTool(getAllegianceList.name, { description: getAllegianceList.description, inputSchema: getAllegianceList.inputSchema.shape }, (args) => getAllegianceList.handler(args));
return server;
}
//# sourceMappingURL=server.js.map

View File

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

View File

@@ -0,0 +1,36 @@
import { z } from 'zod';
export declare const name = "find_route";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{
start: z.ZodString;
end: z.ZodString;
jump: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
avoid_red_zones: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
imperial_only: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
wilderness_refueling: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
milieu: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
jump: number;
start: string;
end: string;
avoid_red_zones: boolean;
imperial_only: boolean;
wilderness_refueling: boolean;
milieu?: string | undefined;
}, {
start: string;
end: string;
milieu?: string | undefined;
jump?: number | undefined;
avoid_red_zones?: boolean | undefined;
imperial_only?: boolean | undefined;
wilderness_refueling?: boolean | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=find_route.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"find_route.d.ts","sourceRoot":"","sources":["../../src/tools/find_route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,IAAI,eAAe,CAAC;AAEjC,eAAO,MAAM,WAAW,QAIsE,CAAC;AAE/F,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;EA8BtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAgBhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAsFxC"}

View File

@@ -0,0 +1,121 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
import { parseUWP } from '../parsers/uwp.js';
export const name = 'find_route';
export const description = 'Finds a jump route between two worlds and returns the sequence of intermediate worlds. ' +
'Locations are specified as "Sector XXYY" (e.g. "Spinward Marches 1910"). ' +
'Returns 404/no-route if no path exists within the jump rating. ' +
'Options: avoid Red zones, require Imperial membership, require wilderness refueling stops.';
export const inputSchema = z.object({
start: z
.string()
.describe('Starting world as "Sector XXYY" (e.g. "Spinward Marches 1910") or T5SS abbreviation format'),
end: z
.string()
.describe('Destination world in the same format (e.g. "Core 2118" for Capital)'),
jump: z
.number()
.min(1)
.max(12)
.optional()
.default(2)
.describe('Maximum jump distance per leg (1-12, default 2)'),
avoid_red_zones: z
.boolean()
.optional()
.default(false)
.describe('If true, the route will not pass through TAS Red Zone worlds'),
imperial_only: z
.boolean()
.optional()
.default(false)
.describe('If true, only stop at Third Imperium member worlds'),
wilderness_refueling: z
.boolean()
.optional()
.default(false)
.describe('If true, stops must have wilderness refueling available (gas giant or ocean)'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
});
const ZONE_LABELS = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' };
export async function handler(args) {
const { start, end, jump, avoid_red_zones, imperial_only, wilderness_refueling, milieu } = args;
let rawData;
try {
rawData = await apiGetJson('/api/route', {
start,
end,
jump,
nored: avoid_red_zones ? 1 : undefined,
im: imperial_only ? 1 : undefined,
wild: wilderness_refueling ? 1 : undefined,
milieu,
});
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes('404')) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
found: false,
start,
end,
jump_rating: jump,
message: `No jump-${jump} route found between "${start}" and "${end}". Try increasing the jump rating or relaxing constraints.`,
}, null, 2),
},
],
};
}
throw err;
}
// The API returns an array of worlds directly (not wrapped in {Worlds: [...]})
const worldArray = Array.isArray(rawData)
? rawData
: (rawData.Worlds ?? []);
const worlds = worldArray.map((w) => {
const zoneCode = (w.Zone ?? '').trim();
const uwpRaw = w.UWP ?? '';
const sectorName = typeof w.Sector === 'string' ? w.Sector : w.Sector?.Name;
let starport = '';
if (uwpRaw && uwpRaw !== '?000000-0') {
try {
starport = parseUWP(uwpRaw).starport.code;
}
catch {
starport = uwpRaw[0] ?? '';
}
}
return {
name: w.Name,
sector: sectorName,
hex: w.Hex,
location: sectorName && w.Hex ? `${sectorName} ${w.Hex}` : undefined,
uwp: uwpRaw,
starport,
zone: (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green',
bases: w.Bases,
allegiance: w.AllegianceName ?? w.Allegiance,
};
});
const result = {
found: true,
start,
end,
jump_rating: jump,
total_jumps: Math.max(0, worlds.length - 1),
route: worlds,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
//# sourceMappingURL=find_route.js.map

View File

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

View File

@@ -0,0 +1,12 @@
import { z } from 'zod';
export declare const name = "get_allegiance_list";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(_args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_allegiance_list.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_allegiance_list.d.ts","sourceRoot":"","sources":["../../src/tools/get_allegiance_list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,wBAAwB,CAAC;AAE1C,eAAO,MAAM,WAAW,QAG6D,CAAC;AAEtF,eAAO,MAAM,WAAW,gDAAe,CAAC;AAExC,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAQhD,wBAAsB,OAAO,CAAC,KAAK,EAAE,KAAK;;;;;GAqBzC"}

View File

@@ -0,0 +1,27 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
export const name = 'get_allegiance_list';
export const description = 'Returns all known allegiance codes and their full names. ' +
'Use this to find the right code before searching with "alleg:" in search_worlds. ' +
'Examples: "ImDd" = "Third Imperium, Domain of Deneb", "Zh" = "Zhodani Consulate".';
export const inputSchema = z.object({});
export async function handler(_args) {
const data = await apiGetJson('/t5ss/allegiances', {});
const allegiances = (Array.isArray(data) ? data : []).map((a) => ({
code: a.Code,
name: a.Name,
}));
const result = {
count: allegiances.length,
allegiances,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
//# sourceMappingURL=get_allegiance_list.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_allegiance_list.js","sourceRoot":"","sources":["../../src/tools/get_allegiance_list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,CAAC,MAAM,IAAI,GAAG,qBAAqB,CAAC;AAE1C,MAAM,CAAC,MAAM,WAAW,GACtB,2DAA2D;IAC3D,mFAAmF;IACnF,mFAAmF,CAAC;AAEtF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAUxC,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,KAAY;IACxC,MAAM,IAAI,GAAG,MAAM,UAAU,CAAoB,mBAAmB,EAAE,EAAE,CAAC,CAAC;IAE1E,MAAM,WAAW,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChE,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,IAAI,EAAE,CAAC,CAAC,IAAI;KACb,CAAC,CAAC,CAAC;IAEJ,MAAM,MAAM,GAAG;QACb,KAAK,EAAE,WAAW,CAAC,MAAM;QACzB,WAAW;KACZ,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC"}

View File

@@ -0,0 +1,50 @@
import { z } from 'zod';
export declare const name = "get_jump_map";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{
sector: z.ZodString;
hex: z.ZodString;
jump: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
scale: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
style: z.ZodDefault<z.ZodOptional<z.ZodEnum<["poster", "print", "atlas", "candy", "draft", "fasa", "terminal", "mongoose"]>>>;
milieu: z.ZodOptional<z.ZodString>;
save_path: z.ZodOptional<z.ZodString>;
filename: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
hex: string;
sector: string;
scale: number;
style: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose";
jump: number;
milieu?: string | undefined;
save_path?: string | undefined;
filename?: string | undefined;
}, {
hex: string;
sector: string;
scale?: number | undefined;
style?: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose" | undefined;
milieu?: string | undefined;
save_path?: string | undefined;
filename?: string | undefined;
jump?: number | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
isError: boolean;
content: {
type: "text";
text: string;
}[];
} | {
content: ({
type: "image";
data: string;
mimeType: string;
} | {
type: "text";
text: string;
})[];
isError?: undefined;
}>;
//# sourceMappingURL=get_jump_map.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_jump_map.d.ts","sourceRoot":"","sources":["../../src/tools/get_jump_map.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,iBAAiB,CAAC;AAEnC,eAAO,MAAM,WAAW,QAI2C,CAAC;AAEpE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsBtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAmBhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;;;;;;;;;;;;GAuDxC"}

View File

@@ -0,0 +1,97 @@
import { stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { z } from 'zod';
import { apiGetDataUri } from '../api/client.js';
export const name = 'get_jump_map';
export const description = 'Returns a PNG image of the hex map centered on a world, showing all worlds within N jump distance. ' +
'Great for visualizing routes and nearby systems during a session. ' +
'Optionally saves the image to a local directory (e.g. an Obsidian vault attachments folder) ' +
'and returns the saved file path so it can be embedded in notes.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'),
hex: z.string().describe('Hex location in XXYY format (e.g. "1910" for Regina)'),
jump: z.number().min(0).max(20).optional().default(2).describe('Jump range in parsecs (0-20, default 2)'),
scale: z.number().optional().default(64).describe('Pixels per parsec (default 64)'),
style: z
.enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose'])
.optional()
.default('poster')
.describe('Visual rendering style. Note: candy style returns JPEG instead of PNG.'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
save_path: z
.string()
.optional()
.describe('Absolute directory path where the image file should be saved'),
filename: z
.string()
.optional()
.describe('Custom filename without extension (e.g. "regina-jump2"). ' +
'Auto-generated as "{sector}-{hex}-jump{jump}" if omitted.'),
});
function extFromMimeType(mimeType) {
return mimeType === 'image/jpeg' ? 'jpg' : 'png';
}
function buildFilename(sector, hex, jump, customName, ext) {
let base;
if (customName) {
base = customName.replace(/\.(png|jpe?g)$/i, '');
}
else {
base = `${sector}-${hex}-jump${jump}`
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9\-_]/g, '');
}
return `${base}.${ext}`;
}
export async function handler(args) {
const { sector, hex, jump, scale, style, milieu, save_path, filename } = args;
const { base64, mimeType } = await apiGetDataUri('/api/jumpmap', {
sector,
hex,
jump,
scale,
style,
milieu,
});
const imageBlock = { type: 'image', data: base64, mimeType };
if (!save_path) {
return { content: [imageBlock] };
}
let dirStat;
try {
dirStat = await stat(save_path);
}
catch {
return {
isError: true,
content: [{ type: 'text', text: `Error: save_path directory does not exist: ${save_path}` }],
};
}
if (!dirStat.isDirectory()) {
return {
isError: true,
content: [{ type: 'text', text: `Error: save_path is not a directory: ${save_path}` }],
};
}
const ext = extFromMimeType(mimeType);
const resolvedFilename = buildFilename(sector, hex, jump, filename, ext);
const fullPath = join(save_path, resolvedFilename);
try {
await writeFile(fullPath, Buffer.from(base64, 'base64'));
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
isError: true,
content: [{ type: 'text', text: `Error: Failed to write file: ${message}` }],
};
}
return {
content: [
imageBlock,
{ type: 'text', text: `Image saved to: ${fullPath}` },
],
};
}
//# sourceMappingURL=get_jump_map.js.map

View File

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

View File

@@ -0,0 +1,24 @@
import { z } from 'zod';
export declare const name = "get_sector_data";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{
sector: z.ZodString;
subsector: z.ZodOptional<z.ZodString>;
milieu: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
sector: string;
subsector?: string | undefined;
milieu?: string | undefined;
}, {
sector: string;
subsector?: string | undefined;
milieu?: string | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_sector_data.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_sector_data.d.ts","sourceRoot":"","sources":["../../src/tools/get_sector_data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,IAAI,oBAAoB,CAAC;AAEtC,eAAO,MAAM,WAAW,QAIwE,CAAC;AAEjG,eAAO,MAAM,WAAW;;;;;;;;;;;;EAOtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GA2BxC"}

View File

@@ -0,0 +1,41 @@
import { z } from 'zod';
import { apiGetText } from '../api/client.js';
import { parseSecTabDelimited } from '../parsers/sec.js';
export const name = 'get_sector_data';
export const description = 'Returns all worlds in a sector (or single subsector) as structured data with decoded UWPs. ' +
'Useful for bulk analysis, e.g. "which worlds in Spinward Marches have Tech Level 15?" ' +
'or "list all Naval Bases in the Regina subsector". ' +
'Returns full world records including trade codes, bases, allegiance, and decoded UWP fields.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'),
subsector: z
.string()
.optional()
.describe('Limit to a single subsector by letter (A-P) or name'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
});
export async function handler(args) {
const { sector, subsector, milieu } = args;
const text = await apiGetText('/api/sec', {
sector,
subsector,
type: 'TabDelimited',
milieu,
});
const worlds = parseSecTabDelimited(text);
const result = {
sector,
subsector: subsector ?? 'all',
count: worlds.length,
worlds,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
//# sourceMappingURL=get_sector_data.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_sector_data.js","sourceRoot":"","sources":["../../src/tools/get_sector_data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,CAAC,MAAM,IAAI,GAAG,iBAAiB,CAAC;AAEtC,MAAM,CAAC,MAAM,WAAW,GACtB,6FAA6F;IAC7F,wFAAwF;IACxF,qDAAqD;IACrD,8FAA8F,CAAC;AAEjG,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sEAAsE,CAAC;IACnG,SAAS,EAAE,CAAC;SACT,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,qDAAqD,CAAC;IAClE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;CACxE,CAAC,CAAC;AAIH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAE3C,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,UAAU,EAAE;QACxC,MAAM;QACN,SAAS;QACT,IAAI,EAAE,cAAc;QACpB,MAAM;KACP,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAE1C,MAAM,MAAM,GAAG;QACb,MAAM;QACN,SAAS,EAAE,SAAS,IAAI,KAAK;QAC7B,KAAK,EAAE,MAAM,CAAC,MAAM;QACpB,MAAM;KACP,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC"}

View File

@@ -0,0 +1,21 @@
import { z } from 'zod';
export declare const name = "get_sector_list";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{
milieu: z.ZodOptional<z.ZodString>;
tag: z.ZodOptional<z.ZodEnum<["Official", "InReview", "Preserve", "Apocryphal"]>>;
}, "strip", z.ZodTypeAny, {
milieu?: string | undefined;
tag?: "Official" | "InReview" | "Preserve" | "Apocryphal" | undefined;
}, {
milieu?: string | undefined;
tag?: "Official" | "InReview" | "Preserve" | "Apocryphal" | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_sector_list.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_sector_list.d.ts","sourceRoot":"","sources":["../../src/tools/get_sector_list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,oBAAoB,CAAC;AAEtC,eAAO,MAAM,WAAW,QAG2E,CAAC;AAEpG,eAAO,MAAM,WAAW;;;;;;;;;EAMtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAgBhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAgCxC"}

View File

@@ -0,0 +1,43 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
export const name = 'get_sector_list';
export const description = 'Returns a list of all known sectors in the Traveller universe with their names, ' +
'T5SS abbreviations, and galactic coordinates. ' +
'Can be filtered by official status (Official, InReview, Preserve, Apocryphal) and campaign era.';
export const inputSchema = z.object({
milieu: z.string().optional().describe('Campaign era filter (e.g. "M1105")'),
tag: z
.enum(['Official', 'InReview', 'Preserve', 'Apocryphal'])
.optional()
.describe('Filter by sector data status'),
});
export async function handler(args) {
const { milieu, tag } = args;
const data = await apiGetJson('/api/universe', {
requireData: 1,
milieu,
tag,
});
const sectors = (data.Sectors ?? [])
.map((s) => ({
name: s.Names?.[0]?.Text ?? 'Unknown',
abbreviation: s.Abbreviation,
x: s.X,
y: s.Y,
tags: s.Tags,
}))
.sort((a, b) => a.name.localeCompare(b.name));
const result = {
count: sectors.length,
sectors,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
//# sourceMappingURL=get_sector_list.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_sector_list.js","sourceRoot":"","sources":["../../src/tools/get_sector_list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,CAAC,MAAM,IAAI,GAAG,iBAAiB,CAAC;AAEtC,MAAM,CAAC,MAAM,WAAW,GACtB,kFAAkF;IAClF,gDAAgD;IAChD,iGAAiG,CAAC;AAEpG,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;IAC5E,GAAG,EAAE,CAAC;SACH,IAAI,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC;SACxD,QAAQ,EAAE;SACV,QAAQ,CAAC,8BAA8B,CAAC;CAC5C,CAAC,CAAC;AAkBH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAE7B,MAAM,IAAI,GAAG,MAAM,UAAU,CAAmB,eAAe,EAAE;QAC/D,WAAW,EAAE,CAAC;QACd,MAAM;QACN,GAAG;KACJ,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;SACjC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACX,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,SAAS;QACrC,YAAY,EAAE,CAAC,CAAC,YAAY;QAC5B,CAAC,EAAE,CAAC,CAAC,CAAC;QACN,CAAC,EAAE,CAAC,CAAC,CAAC;QACN,IAAI,EAAE,CAAC,CAAC,IAAI;KACb,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAEhD,MAAM,MAAM,GAAG;QACb,KAAK,EAAE,OAAO,CAAC,MAAM;QACrB,OAAO;KACR,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC"}

View File

@@ -0,0 +1,21 @@
import { z } from 'zod';
export declare const name = "get_sector_metadata";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{
sector: z.ZodString;
milieu: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
sector: string;
milieu?: string | undefined;
}, {
sector: string;
milieu?: string | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_sector_metadata.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_sector_metadata.d.ts","sourceRoot":"","sources":["../../src/tools/get_sector_metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,wBAAwB,CAAC;AAE1C,eAAO,MAAM,WAAW,QAGiC,CAAC;AAE1D,eAAO,MAAM,WAAW;;;;;;;;;EAGtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAwBhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAoCxC"}

View File

@@ -0,0 +1,43 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
export const name = 'get_sector_metadata';
export const description = 'Returns metadata for a sector: subsector names (A-P), allegiance regions, route overlays, and political borders. ' +
'Use this to discover subsector names before calling get_subsector_map, ' +
'or to understand the political structure of a sector.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
});
export async function handler(args) {
const { sector, milieu } = args;
const data = await apiGetJson('/api/metadata', { sector, milieu });
const subsectorsByIndex = {};
for (const sub of data.Subsectors ?? []) {
if (sub.Index && sub.Name) {
subsectorsByIndex[sub.Index] = sub.Name;
}
}
const subsectors = 'ABCDEFGHIJKLMNOP'.split('').map((letter) => ({
index: letter,
name: subsectorsByIndex[letter] ?? `Subsector ${letter}`,
}));
const result = {
names: data.Names?.map((n) => ({ text: n.Text, lang: n.Lang })),
abbreviation: data.Abbreviation,
coordinates: { x: data.X, y: data.Y },
subsectors,
allegiances: (data.Allegiances ?? []).map((a) => ({
code: a.Code,
name: a.Name,
})),
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
//# sourceMappingURL=get_sector_metadata.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_sector_metadata.js","sourceRoot":"","sources":["../../src/tools/get_sector_metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,CAAC,MAAM,IAAI,GAAG,qBAAqB,CAAC;AAE1C,MAAM,CAAC,MAAM,WAAW,GACtB,mHAAmH;IACnH,yEAAyE;IACzE,uDAAuD,CAAC;AAE1D,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sEAAsE,CAAC;IACnG,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;CACxE,CAAC,CAAC;AA0BH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAEhC,MAAM,IAAI,GAAG,MAAM,UAAU,CAAmB,eAAe,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAErF,MAAM,iBAAiB,GAA2B,EAAE,CAAC;IACrD,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;QACxC,IAAI,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;YAC1B,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC/D,KAAK,EAAE,MAAM;QACb,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,IAAI,aAAa,MAAM,EAAE;KACzD,CAAC,CAAC,CAAC;IAEJ,MAAM,MAAM,GAAG;QACb,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/D,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,WAAW,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE;QACrC,UAAU;QACV,WAAW,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAChD,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;SACb,CAAC,CAAC;KACJ,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC"}

View File

@@ -0,0 +1,47 @@
import { z } from 'zod';
export declare const name = "get_subsector_map";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{
sector: z.ZodString;
subsector: z.ZodString;
scale: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
style: z.ZodDefault<z.ZodOptional<z.ZodEnum<["poster", "print", "atlas", "candy", "draft", "fasa", "terminal", "mongoose"]>>>;
milieu: z.ZodOptional<z.ZodString>;
save_path: z.ZodOptional<z.ZodString>;
filename: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
sector: string;
subsector: string;
scale: number;
style: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose";
milieu?: string | undefined;
save_path?: string | undefined;
filename?: string | undefined;
}, {
sector: string;
subsector: string;
scale?: number | undefined;
style?: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose" | undefined;
milieu?: string | undefined;
save_path?: string | undefined;
filename?: string | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
isError: boolean;
content: {
type: "text";
text: string;
}[];
} | {
content: ({
type: "image";
data: string;
mimeType: string;
} | {
type: "text";
text: string;
})[];
isError?: undefined;
}>;
//# sourceMappingURL=get_subsector_map.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_subsector_map.d.ts","sourceRoot":"","sources":["../../src/tools/get_subsector_map.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,sBAAsB,CAAC;AAExC,eAAO,MAAM,WAAW,QAG2C,CAAC;AAEpE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;EAqBtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAmBhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;;;;;;;;;;;;GAsDxC"}

View File

@@ -0,0 +1,94 @@
import { stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { z } from 'zod';
import { apiGetDataUri } from '../api/client.js';
export const name = 'get_subsector_map';
export const description = 'Returns a PNG image of a named subsector from the official Traveller Map database. ' +
'Optionally saves the image to a local directory (e.g. an Obsidian vault attachments folder) ' +
'and returns the saved file path so it can be embedded in notes.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'),
subsector: z.string().describe('Subsector letter A-P or subsector name (e.g. "A" or "Regina")'),
scale: z.number().optional().default(64).describe('Pixels per parsec (default 64). Higher = larger image.'),
style: z
.enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose'])
.optional()
.default('poster')
.describe('Visual rendering style. Note: candy style returns JPEG instead of PNG.'),
milieu: z.string().optional().describe('Campaign era (e.g. "M1105" for default Third Imperium 1105)'),
save_path: z
.string()
.optional()
.describe('Absolute directory path where the image file should be saved'),
filename: z
.string()
.optional()
.describe('Custom filename without extension (e.g. "regina-map"). ' +
'Auto-generated as "{sector}-{subsector}-subsector" if omitted.'),
});
function extFromMimeType(mimeType) {
return mimeType === 'image/jpeg' ? 'jpg' : 'png';
}
function buildFilename(sector, subsector, customName, ext) {
let base;
if (customName) {
base = customName.replace(/\.(png|jpe?g)$/i, '');
}
else {
base = `${sector}-${subsector}-subsector`
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9\-_]/g, '');
}
return `${base}.${ext}`;
}
export async function handler(args) {
const { sector, subsector, scale, style, milieu, save_path, filename } = args;
const { base64, mimeType } = await apiGetDataUri('/api/poster', {
sector,
subsector,
scale,
style,
milieu,
});
const imageBlock = { type: 'image', data: base64, mimeType };
if (!save_path) {
return { content: [imageBlock] };
}
let dirStat;
try {
dirStat = await stat(save_path);
}
catch {
return {
isError: true,
content: [{ type: 'text', text: `Error: save_path directory does not exist: ${save_path}` }],
};
}
if (!dirStat.isDirectory()) {
return {
isError: true,
content: [{ type: 'text', text: `Error: save_path is not a directory: ${save_path}` }],
};
}
const ext = extFromMimeType(mimeType);
const resolvedFilename = buildFilename(sector, subsector, filename, ext);
const fullPath = join(save_path, resolvedFilename);
try {
await writeFile(fullPath, Buffer.from(base64, 'base64'));
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
isError: true,
content: [{ type: 'text', text: `Error: Failed to write file: ${message}` }],
};
}
return {
content: [
imageBlock,
{ type: 'text', text: `Image saved to: ${fullPath}` },
],
};
}
//# sourceMappingURL=get_subsector_map.js.map

View File

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

View File

@@ -0,0 +1,24 @@
import { z } from 'zod';
export declare const name = "get_world_info";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{
sector: z.ZodString;
hex: z.ZodString;
milieu: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
hex: string;
sector: string;
milieu?: string | undefined;
}, {
hex: string;
sector: string;
milieu?: string | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_world_info.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_world_info.d.ts","sourceRoot":"","sources":["../../src/tools/get_world_info.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,IAAI,mBAAmB,CAAC;AAErC,eAAO,MAAM,WAAW,QAIuD,CAAC;AAEhF,eAAO,MAAM,WAAW;;;;;;;;;;;;EAItB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AA8GhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAwCxC"}

View File

@@ -0,0 +1,134 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
import { parseUWP } from '../parsers/uwp.js';
export const name = 'get_world_info';
export const description = 'Retrieves detailed information about a specific world, with the UWP (Universal World Profile) ' +
'fully decoded into human-readable fields: starport quality, world size, atmosphere type, ' +
'hydrographics percentage, population estimate, government type, law level, and tech level era. ' +
'Also returns trade codes, bases, travel zone, allegiance, and stellar data.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'),
hex: z.string().describe('Hex location in XXYY format (e.g. "1910" for Regina in Spinward Marches)'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
});
const ZONE_LABELS = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' };
const BASE_LABELS = {
N: 'Naval Base',
S: 'Scout Base',
W: 'Scout Waystation',
D: 'Naval Depot',
K: 'Naval Base (Sword Worlds)',
M: 'Military Base',
C: 'Corsair Base',
T: 'TAS Hostel',
R: 'Aslan Clan Base',
F: 'Aslan Tlaukhu Base',
A: 'Naval Base + Scout Base',
B: 'Naval Base + Scout Waystation',
G: 'Scout Base (Vargr)',
H: 'Naval Base + Scout Base (Vargr)',
X: 'Zhodani Relay Station',
Z: 'Zhodani Naval + Scout Base',
};
function decodeBases(bases) {
if (!bases || bases.trim() === '' || bases.trim() === '-')
return [];
return bases
.trim()
.split('')
.filter((c) => c !== ' ' && c !== '-')
.map((c) => BASE_LABELS[c] ?? `Base (${c})`);
}
function decodeTradeCodes(remarks) {
const TRADE_CODES = {
Ag: 'Agricultural',
As: 'Asteroid',
Ba: 'Barren',
De: 'Desert',
Fl: 'Fluid Oceans (non-water)',
Ga: 'Garden World',
Hi: 'High Population',
Ht: 'High Technology',
Ic: 'Ice-Capped',
In: 'Industrial',
Lo: 'Low Population',
Lt: 'Low Technology',
Na: 'Non-Agricultural',
Ni: 'Non-Industrial',
Po: 'Poor',
Ri: 'Rich',
Tr: 'Temperate',
Tu: 'Tundra',
Tz: 'Tidally Locked',
Wa: 'Water World',
Va: 'Vacuum',
Ph: 'Pre-High Population',
Pi: 'Pre-Industrial',
Pa: 'Pre-Agricultural',
Mr: 'Reserve',
Fr: 'Frozen',
Ho: 'Hot',
Co: 'Cold',
Lk: 'Locked',
Tr2: 'Tropic',
Sa: 'Satellite',
Fa: 'Farming',
Mi: 'Mining',
Pz: 'Puzzle',
Cy: 'Cyclopean',
Di: 'Dieback',
Px: 'Prison/Exile Camp',
An: 'Ancient Site',
Rs: 'Research Station',
Cp: 'Subsector Capital',
Cs: 'Sector Capital',
Cx: 'Capital',
Fo: 'Forbidden',
Pn: 'Prison',
Re: 'Reserve',
};
if (!remarks)
return [];
return remarks
.trim()
.split(/\s+/)
.filter((r) => r && r !== '-')
.map((code) => ({ code, meaning: TRADE_CODES[code] ?? code }));
}
export async function handler(args) {
const { sector, hex, milieu } = args;
const data = await apiGetJson('/api/credits', { sector, hex, milieu });
const uwpRaw = data.UWP ?? '?000000-0';
const decoded = parseUWP(uwpRaw);
const zoneCode = (data.Zone ?? '').trim();
const zone = (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green';
const result = {
name: data.Name ?? 'Unknown',
hex: data.Hex ?? hex,
sector: data.Sector ?? sector,
subsector: data.Subsector,
uwp: uwpRaw,
decoded_uwp: decoded,
trade_codes: decodeTradeCodes(data.Remarks ?? ''),
bases: decodeBases(data.Bases ?? ''),
zone,
pbg: data.PBG,
allegiance: data.Allegiance,
stellar: data.Stars,
importance: data.Ix,
economic_extension: data.Ex,
cultural_extension: data.Cx,
nobility: data.Nobility,
worlds_in_system: data.W,
resource_units: data.RU,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
//# sourceMappingURL=get_world_info.js.map

View File

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

View File

@@ -0,0 +1,27 @@
import { z } from 'zod';
export declare const name = "get_worlds_in_jump_range";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{
sector: z.ZodString;
hex: z.ZodString;
jump: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
milieu: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
hex: string;
sector: string;
jump: number;
milieu?: string | undefined;
}, {
hex: string;
sector: string;
milieu?: string | undefined;
jump?: number | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=get_worlds_in_jump_range.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"get_worlds_in_jump_range.d.ts","sourceRoot":"","sources":["../../src/tools/get_worlds_in_jump_range.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,IAAI,6BAA6B,CAAC;AAE/C,eAAO,MAAM,WAAW,QAG0C,CAAC;AAEnE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;EAKtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AA8BhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAiDxC"}

View File

@@ -0,0 +1,70 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
import { parseUWP } from '../parsers/uwp.js';
export const name = 'get_worlds_in_jump_range';
export const description = 'Lists all worlds reachable from a given location within N parsecs. ' +
'Returns structured data for each world including UWP, trade codes, bases, zone, and allegiance. ' +
'Useful for planning routes or finding nearby systems to visit.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation'),
hex: z.string().describe('Hex location in XXYY format'),
jump: z.number().min(0).max(12).optional().default(2).describe('Jump range in parsecs (0-12, default 2)'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
});
const ZONE_LABELS = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' };
function parseTradeCodes(remarks) {
if (!remarks)
return [];
return remarks
.trim()
.split(/\s+/)
.filter((r) => r && r !== '-');
}
export async function handler(args) {
const { sector, hex, jump, milieu } = args;
const data = await apiGetJson('/api/jumpworlds', { sector, hex, jump, milieu });
const worlds = (data.Worlds ?? []).map((w) => {
const zoneCode = (w.Zone ?? '').trim();
const uwpRaw = w.UWP ?? '';
let starport = '';
let techLevel = '';
if (uwpRaw && uwpRaw.length > 1) {
try {
const decoded = parseUWP(uwpRaw);
starport = decoded.starport.code;
techLevel = decoded.tech_level.code;
}
catch {
starport = uwpRaw[0] ?? '';
}
}
return {
name: w.Name,
sector: w.Sector?.Name,
hex: w.Hex,
uwp: uwpRaw,
starport,
tech_level: techLevel,
bases: w.Bases,
trade_codes: parseTradeCodes(w.Remarks),
zone: (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green',
allegiance: w.Allegiance,
distance: w.Distance,
};
});
const result = {
origin: `${hex} in ${sector}`,
jump_range: jump,
count: worlds.length,
worlds,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
//# sourceMappingURL=get_worlds_in_jump_range.js.map

View File

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

View File

@@ -0,0 +1,37 @@
import { z } from 'zod';
export declare const name = "render_custom_map";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{
sec_data: z.ZodString;
metadata: z.ZodOptional<z.ZodString>;
scale: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
style: z.ZodDefault<z.ZodOptional<z.ZodEnum<["poster", "print", "atlas", "candy", "draft", "fasa", "terminal", "mongoose"]>>>;
subsector: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
scale: number;
style: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose";
sec_data: string;
subsector?: string | undefined;
metadata?: string | undefined;
}, {
sec_data: string;
subsector?: string | undefined;
scale?: number | undefined;
style?: "poster" | "print" | "atlas" | "candy" | "draft" | "fasa" | "terminal" | "mongoose" | undefined;
metadata?: string | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: ({
type: "image";
data: string;
mimeType: string;
text?: undefined;
} | {
type: "text";
text: string;
data?: undefined;
mimeType?: undefined;
})[];
}>;
//# sourceMappingURL=render_custom_map.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"render_custom_map.d.ts","sourceRoot":"","sources":["../../src/tools/render_custom_map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,sBAAsB,CAAC;AAExC,eAAO,MAAM,WAAW,QAGoE,CAAC;AAE7F,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;EAkBtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;;;;;;;;GAqBxC"}

View File

@@ -0,0 +1,42 @@
import { z } from 'zod';
import { apiPostImage } from '../api/client.js';
export const name = 'render_custom_map';
export const description = 'Renders a map image from custom SEC-format world data that you provide. ' +
'Useful for homebrew sectors, campaign-specific maps, or previewing modified sector data. ' +
'The sec_data parameter accepts T5 Second Survey, T5 tab-delimited, or legacy SEC format.';
export const inputSchema = z.object({
sec_data: z
.string()
.describe('World data in T5 Second Survey, T5 tab-delimited, or legacy SEC format'),
metadata: z
.string()
.optional()
.describe('Optional sector metadata in XML or MSEC format (defines sector name, subsector names, borders, etc.)'),
scale: z.number().optional().default(64).describe('Pixels per parsec (default 64)'),
style: z
.enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose'])
.optional()
.default('poster')
.describe('Visual rendering style'),
subsector: z
.string()
.optional()
.describe('Render only this subsector (A-P letter) instead of the full sector'),
});
export async function handler(args) {
const { sec_data, metadata, scale, style, subsector } = args;
const formBody = { data: sec_data };
if (metadata)
formBody['metadata'] = metadata;
const base64 = await apiPostImage('/api/poster', { scale, style, subsector }, formBody);
return {
content: [
{ type: 'image', data: base64, mimeType: 'image/png' },
{
type: 'text',
text: `Custom sector map rendered${subsector ? ` (subsector ${subsector})` : ''}`,
},
],
};
}
//# sourceMappingURL=render_custom_map.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"render_custom_map.js","sourceRoot":"","sources":["../../src/tools/render_custom_map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,MAAM,CAAC,MAAM,IAAI,GAAG,mBAAmB,CAAC;AAExC,MAAM,CAAC,MAAM,WAAW,GACtB,0EAA0E;IAC1E,2FAA2F;IAC3F,0FAA0F,CAAC;AAE7F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,QAAQ,CAAC,wEAAwE,CAAC;IACrF,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,sGAAsG,CAAC;IACnH,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,gCAAgC,CAAC;IACnF,KAAK,EAAE,CAAC;SACL,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;SACpF,QAAQ,EAAE;SACV,OAAO,CAAC,QAAQ,CAAC;SACjB,QAAQ,CAAC,wBAAwB,CAAC;IACrC,SAAS,EAAE,CAAC;SACT,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,oEAAoE,CAAC;CAClF,CAAC,CAAC;AAIH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAW;IACvC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAE7D,MAAM,QAAQ,GAA2B,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC5D,IAAI,QAAQ;QAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAC;IAE9C,MAAM,MAAM,GAAG,MAAM,YAAY,CAC/B,aAAa,EACb,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,EAC3B,QAAQ,CACT,CAAC;IAEF,OAAO;QACL,OAAO,EAAE;YACP,EAAE,IAAI,EAAE,OAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE;YAC/D;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,6BAA6B,SAAS,CAAC,CAAC,CAAC,eAAe,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;aAClF;SACF;KACF,CAAC;AACJ,CAAC"}

View File

@@ -0,0 +1,21 @@
import { z } from 'zod';
export declare const name = "search_worlds";
export declare const description: string;
export declare const inputSchema: z.ZodObject<{
query: z.ZodString;
milieu: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
query: string;
milieu?: string | undefined;
}, {
query: string;
milieu?: string | undefined;
}>;
export type Input = z.infer<typeof inputSchema>;
export declare function handler(args: Input): Promise<{
content: {
type: "text";
text: string;
}[];
}>;
//# sourceMappingURL=search_worlds.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"search_worlds.d.ts","sourceRoot":"","sources":["../../src/tools/search_worlds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,IAAI,kBAAkB,CAAC;AAEpC,eAAO,MAAM,WAAW,QAMc,CAAC;AAEvC,eAAO,MAAM,WAAW;;;;;;;;;EAQtB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AA4DhD,wBAAsB,OAAO,CAAC,IAAI,EAAE,KAAK;;;;;GAuDxC"}

View File

@@ -0,0 +1,82 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
export const name = 'search_worlds';
export const description = 'Search for worlds using name patterns, UWP criteria, trade codes, allegiance codes, or travel zones. ' +
'Supports wildcards (* ? %) and multiple filters. ' +
'Examples: "Regina", "Reg*", "uwp:A????[89A]-" (high-pop with excellent starport), ' +
'"remark:Wa" (water worlds), "alleg:Im" (Third Imperium worlds), "zone:R" (Red zones). ' +
'Add a sector name to the query to scope results (e.g. "Wa Tobia" to find water worlds near Tobia). ' +
'Multiple terms are ANDed together.';
export const inputSchema = z.object({
query: z
.string()
.describe('Search query. Examples: "Regina", "Reg*", "uwp:A????[89A]-", "remark:Wa", "alleg:Im", "zone:R", "zone:A". ' +
'Multiple terms are ANDed. Add a sector name to narrow results.'),
milieu: z.string().optional().describe('Campaign era filter (e.g. "M1105")'),
});
const ZONE_LABELS = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' };
function normalizeZone(zone) {
const z = (zone ?? '').trim();
return (ZONE_LABELS[z] ?? z) || 'Green';
}
function parseTradeCodes(remarks) {
if (!remarks)
return [];
return remarks
.trim()
.split(/\s+/)
.filter((r) => r && r !== '-');
}
export async function handler(args) {
const { query, milieu } = args;
const data = await apiGetJson('/api/search', { q: query, milieu });
const items = data?.Results?.Items ?? [];
const worlds = [];
const sectors = [];
const subsectors = [];
for (const item of items) {
if (item.World) {
const w = item.World;
// The API returns Sector as a string, HexX/HexY as ints, Uwp (lowercase p)
const sectorName = typeof w.Sector === 'string' ? w.Sector : w.Sector?.Name;
const hexX = w.HexX !== undefined ? String(w.HexX).padStart(2, '0') : undefined;
const hexY = w.HexY !== undefined ? String(w.HexY).padStart(2, '0') : undefined;
const hex = hexX && hexY ? `${hexX}${hexY}` : w.Hex;
const uwp = w.UWP ?? w.Uwp;
worlds.push({
name: w.Name,
sector: sectorName,
hex,
location: sectorName && hex ? `${sectorName} ${hex}` : undefined,
uwp,
bases: w.Bases,
trade_codes: parseTradeCodes(w.Remarks),
zone: normalizeZone(w.Zone),
pbg: w.PBG,
allegiance: w.Allegiance,
});
}
else if (item.Sector) {
sectors.push({ name: item.Sector.Name, abbreviation: item.Sector.Abbreviation });
}
else if (item.Subsector) {
subsectors.push({ name: item.Subsector.Name, index: item.Subsector.Index });
}
}
const result = {
query,
total_results: items.length,
worlds: { count: worlds.length, results: worlds },
sectors: sectors.length > 0 ? { count: sectors.length, results: sectors } : undefined,
subsectors: subsectors.length > 0 ? { count: subsectors.length, results: subsectors } : undefined,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
//# sourceMappingURL=search_worlds.js.map

View File

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"name": "arioch-traveller-map-mcp",
"version": "1.0.0",
"description": "MCP server for the Traveller Map API — embedded in arioch-assistant",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc --watch"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
}

View File

@@ -0,0 +1,91 @@
const BASE_URL = 'https://travellermap.com';
const USER_AGENT = 'traveller-map-mcp/1.0 (github.com/shammond42/traveller-map-mcp)';
export type QueryParams = Record<string, string | number | boolean | undefined>;
function buildUrl(path: string, params: QueryParams): URL {
const url = new URL(path, BASE_URL);
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) {
url.searchParams.set(key, String(value));
}
}
return url;
}
export async function apiGet(path: string, params: QueryParams = {}): Promise<Response> {
const url = buildUrl(path, params);
const response = await fetch(url.toString(), {
headers: { 'User-Agent': USER_AGENT },
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Traveller Map API error ${response.status} at ${path}: ${body}`);
}
return response;
}
export async function apiGetImage(path: string, params: QueryParams = {}): Promise<string> {
const url = buildUrl(path, params);
const response = await fetch(url.toString(), {
headers: {
'User-Agent': USER_AGENT,
'Accept': 'image/png',
},
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Traveller Map API error ${response.status} at ${path}: ${body}`);
}
const buffer = await response.arrayBuffer();
return Buffer.from(buffer).toString('base64');
}
export async function apiGetJson<T>(path: string, params: QueryParams = {}): Promise<T> {
const response = await apiGet(path, { ...params, accept: 'application/json' });
return response.json() as Promise<T>;
}
export async function apiGetText(path: string, params: QueryParams = {}): Promise<string> {
const response = await apiGet(path, params);
return response.text();
}
export async function apiGetDataUri(
path: string,
params: QueryParams = {},
): Promise<{ base64: string; mimeType: string }> {
const response = await apiGet(path, { ...params, datauri: 1 });
const text = await response.text();
for (const mimeType of ['image/png', 'image/jpeg']) {
const prefix = `data:${mimeType};base64,`;
if (text.startsWith(prefix)) {
return { base64: text.slice(prefix.length), mimeType };
}
}
throw new Error(`Unexpected data URI format from ${path}: ${text.slice(0, 50)}`);
}
export async function apiPostImage(
path: string,
queryParams: QueryParams,
formBody: Record<string, string>,
): Promise<string> {
const url = buildUrl(path, queryParams);
const body = new URLSearchParams(formBody).toString();
const response = await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT,
'Accept': 'image/png',
},
body,
});
if (!response.ok) {
const responseBody = await response.text();
throw new Error(`Traveller Map API error ${response.status} at POST ${path}: ${responseBody}`);
}
const buffer = await response.arrayBuffer();
return Buffer.from(buffer).toString('base64');
}

View File

@@ -0,0 +1,13 @@
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createServer } from './server.js';
async function main() {
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});

View File

@@ -0,0 +1,134 @@
import { parseUWP, type DecodedUWP } from './uwp.js';
export interface WorldRecord {
hex: string;
name: string;
uwp: string;
decoded_uwp: DecodedUWP;
bases: string;
remarks: string;
trade_codes: string[];
zone: string;
pbg: string;
allegiance: string;
stars: string;
importance?: string;
economic?: string;
cultural?: string;
nobility?: string;
worlds?: string;
resource_units?: string;
}
const ZONE_MAP: Record<string, string> = {
R: 'Red',
A: 'Amber',
'': 'Green',
'-': 'Green',
' ': 'Green',
G: 'Green',
};
function normalizeZone(zone: string): string {
return ZONE_MAP[zone?.trim() ?? ''] ?? zone?.trim() ?? 'Green';
}
function parseRemarks(remarks: string): string[] {
if (!remarks) return [];
return remarks
.trim()
.split(/\s+/)
.filter((r) => r.length > 0 && r !== '-');
}
export function parseSecTabDelimited(text: string): WorldRecord[] {
const lines = text.split('\n');
const worlds: WorldRecord[] = [];
let headerLine = '';
let headerIndex = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('#') || line.trim() === '') continue;
if (/^[-\s]+$/.test(line)) continue;
if (line.toLowerCase().includes('hex') || line.toLowerCase().includes('name')) {
headerLine = line;
headerIndex = i;
break;
}
}
if (!headerLine) {
return worlds;
}
const headers = headerLine.split('\t').map((h) => h.trim().toLowerCase());
const colIndex = (names: string[]): number => {
for (const name of names) {
const idx = headers.indexOf(name);
if (idx !== -1) return idx;
}
return -1;
};
const hexIdx = colIndex(['hex']);
const nameIdx = colIndex(['name', 'world name']);
const uwpIdx = colIndex(['uwp']);
const basesIdx = colIndex(['bases', 'base']);
const remarksIdx = colIndex(['remarks', 'trade codes', 'tradecodes']);
const zoneIdx = colIndex(['zone', 'iz', 'travel zone']);
const pbgIdx = colIndex(['pbg']);
const allegIdx = colIndex(['allegiance', 'alleg', 'a']);
const starsIdx = colIndex(['stars', 'stellar data', 'stellar']);
const importanceIdx = colIndex(['{ix}', 'ix', 'importance', '{importance}']);
const economicIdx = colIndex(['(ex)', 'ex', 'economic', '(economic)']);
const culturalIdx = colIndex(['[cx]', 'cx', 'cultural', '[cultural]']);
const nobilityIdx = colIndex(['nobility', 'nobil', 'n']);
const worldsIdx = colIndex(['w', 'worlds']);
const ruIdx = colIndex(['ru', 'resource units']);
for (let i = headerIndex + 1; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('#') || line.trim() === '') continue;
if (/^[-\s]+$/.test(line)) continue;
const cols = line.split('\t');
if (cols.length < 3) continue;
const get = (idx: number): string => (idx >= 0 ? cols[idx]?.trim() ?? '' : '');
const uwpRaw = get(uwpIdx);
let decodedUwp: DecodedUWP;
try {
decodedUwp = parseUWP(uwpRaw);
} catch {
decodedUwp = parseUWP('?000000-0');
}
const remarksRaw = get(remarksIdx);
worlds.push({
hex: get(hexIdx),
name: get(nameIdx),
uwp: uwpRaw,
decoded_uwp: decodedUwp,
bases: get(basesIdx),
remarks: remarksRaw,
trade_codes: parseRemarks(remarksRaw),
zone: normalizeZone(get(zoneIdx)),
pbg: get(pbgIdx),
allegiance: get(allegIdx),
stars: get(starsIdx),
importance: importanceIdx >= 0 ? get(importanceIdx) : undefined,
economic: economicIdx >= 0 ? get(economicIdx) : undefined,
cultural: culturalIdx >= 0 ? get(culturalIdx) : undefined,
nobility: nobilityIdx >= 0 ? get(nobilityIdx) : undefined,
worlds: worldsIdx >= 0 ? get(worldsIdx) : undefined,
resource_units: ruIdx >= 0 ? get(ruIdx) : undefined,
});
}
return worlds;
}

View File

@@ -0,0 +1,231 @@
export interface DecodedUWPField {
code: string;
value: number;
description: string;
}
export interface DecodedUWP {
raw: string;
starport: { code: string; description: string };
size: DecodedUWPField & { diameter_km: string };
atmosphere: DecodedUWPField;
hydrographics: DecodedUWPField & { percent: string };
population: DecodedUWPField & { estimate: string };
government: DecodedUWPField;
law_level: DecodedUWPField;
tech_level: DecodedUWPField & { era: string };
}
export function eHexValue(char: string): number {
const c = char.toUpperCase();
if (c >= '0' && c <= '9') return parseInt(c, 10);
if (c >= 'A' && c <= 'Z') return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10;
return 0;
}
const STARPORT: Record<string, string> = {
A: 'Excellent — full repair, refined fuel, shipyard capable',
B: 'Good — full repair, refined fuel available',
C: 'Routine — some repair, unrefined fuel available',
D: 'Poor — limited repair, unrefined fuel only',
E: 'Frontier — no repair, no fuel',
X: 'None — no starport facilities',
F: 'Good — spaceport (non-starship capable)',
G: 'Poor — primitive spaceport',
H: 'Primitive — minimal facilities',
Y: 'None',
};
const SIZE_DESC: Record<number, { description: string; diameter_km: string }> = {
0: { description: 'Asteroid/planetoid belt or very small body', diameter_km: '<800 km' },
1: { description: 'Small world', diameter_km: '~1,600 km' },
2: { description: 'Small world', diameter_km: '~3,200 km' },
3: { description: 'Small world', diameter_km: '~4,800 km' },
4: { description: 'Small world', diameter_km: '~6,400 km' },
5: { description: 'Medium world', diameter_km: '~8,000 km' },
6: { description: 'Medium world (Earth-like)', diameter_km: '~9,600 km' },
7: { description: 'Medium world', diameter_km: '~11,200 km' },
8: { description: 'Large world', diameter_km: '~12,800 km' },
9: { description: 'Large world', diameter_km: '~14,400 km' },
10: { description: 'Large world', diameter_km: '~16,000 km' },
};
const ATMOSPHERE_DESC: Record<number, string> = {
0: 'None — vacuum',
1: 'Trace — very thin, requires vacc suit',
2: 'Very Thin, Tainted — requires filter mask and compressor',
3: 'Very Thin — requires compressor',
4: 'Thin, Tainted — requires filter mask',
5: 'Thin — breathable with some discomfort',
6: 'Standard — breathable',
7: 'Standard, Tainted — requires filter mask',
8: 'Dense — breathable with no special equipment',
9: 'Dense, Tainted — requires filter mask',
10: 'Exotic — requires oxygen supply',
11: 'Corrosive — requires vacc suit',
12: 'Insidious — suit penetrating, requires special protection',
13: 'Dense, High — breathable only at high altitudes',
14: 'Thin, Low — breathable only in lowlands',
15: 'Unusual',
};
const HYDROGRAPHICS_DESC: Record<number, { description: string; percent: string }> = {
0: { description: 'Desert world — no free water', percent: '0%' },
1: { description: 'Dry world — traces of water', percent: '110%' },
2: { description: 'Dry world', percent: '1120%' },
3: { description: 'Dry world', percent: '2130%' },
4: { description: 'Wet world', percent: '3140%' },
5: { description: 'Wet world', percent: '4150%' },
6: { description: 'Wet world', percent: '5160%' },
7: { description: 'Wet world — significant oceans', percent: '6170%' },
8: { description: 'Water world — large oceans', percent: '7180%' },
9: { description: 'Water world — very large oceans', percent: '8190%' },
10: { description: 'Water world — global ocean', percent: '91100%' },
};
const POPULATION_DESC: Record<number, { description: string; estimate: string }> = {
0: { description: 'Unpopulated or tiny outpost', estimate: 'None or a few individuals' },
1: { description: 'Tens of inhabitants', estimate: '~10s' },
2: { description: 'Hundreds of inhabitants', estimate: '~100s' },
3: { description: 'Thousands of inhabitants', estimate: '~1,000s' },
4: { description: 'Tens of thousands', estimate: '~10,000s' },
5: { description: 'Hundreds of thousands', estimate: '~100,000s' },
6: { description: 'Millions of inhabitants', estimate: '~1,000,000s' },
7: { description: 'Tens of millions', estimate: '~10,000,000s' },
8: { description: 'Hundreds of millions', estimate: '~100,000,000s' },
9: { description: 'Billions of inhabitants', estimate: '~1,000,000,000s' },
10: { description: 'Tens of billions', estimate: '~10,000,000,000s' },
11: { description: 'Hundreds of billions', estimate: '~100,000,000,000s' },
12: { description: 'Trillions of inhabitants', estimate: '~1,000,000,000,000s' },
};
const GOVERNMENT_DESC: Record<number, string> = {
0: 'No Government Structure — family/clan/tribal',
1: 'Company/Corporation — governed by a company',
2: 'Participating Democracy — rule by citizen vote',
3: 'Self-Perpetuating Oligarchy — ruling class maintains power',
4: 'Representative Democracy — elected representatives',
5: 'Feudal Technocracy — controlled by technology owners',
6: 'Captive Government — controlled by outside power',
7: 'Balkanization — no central authority',
8: 'Civil Service Bureaucracy — rule by competence',
9: 'Impersonal Bureaucracy — rule by rigid law',
10: 'Charismatic Dictator — rule by personality',
11: 'Non-Charismatic Leader — rule by position',
12: 'Charismatic Oligarchy — rule by a few personalities',
13: 'Religious Dictatorship — rule by religious doctrine',
14: 'Religious Autocracy — rule by a religious figure',
15: 'Totalitarian Oligarchy — oppressive rule by a few',
};
const LAW_LEVEL_DESC: Record<number, string> = {
0: 'No prohibitions — no restrictions on weapons or behavior',
1: 'Body pistols, explosives, nuclear weapons prohibited',
2: 'Portable energy weapons prohibited',
3: 'Machine guns, automatic weapons prohibited',
4: 'Light assault weapons prohibited',
5: 'Personal concealable weapons prohibited',
6: 'All firearms except shotguns prohibited',
7: 'Shotguns prohibited',
8: 'Long blades prohibited in public',
9: 'All weapons outside home prohibited',
10: 'Weapon possession prohibited — weapons locked up',
11: 'Rigid control of civilian movement',
12: 'Unrestricted invasion of privacy',
13: 'Paramilitary law enforcement',
14: 'Full-fledged police state',
15: 'Daily life rigidly controlled',
};
const TECH_LEVEL_ERA: Record<number, string> = {
0: 'Stone Age / Pre-Industrial',
1: 'Bronze Age / Iron Age',
2: 'Renaissance',
3: 'Industrial Revolution',
4: 'Mechanized Age',
5: 'Broadcast Age',
6: 'Atomic Age',
7: 'Space Age',
8: 'Information Age',
9: 'Pre-Stellar',
10: 'Early Stellar',
11: 'Average Stellar',
12: 'Average Interstellar',
13: 'High Interstellar',
14: 'Average Imperial',
15: 'High Imperial / Average Interstellar II',
16: 'Sophont',
17: 'Advanced',
};
export function parseUWP(uwp: string): DecodedUWP {
const clean = uwp.trim().replace(/\s+/g, '');
const starportCode = clean[0] ?? '?';
const sizeCode = clean[1] ?? '0';
const atmCode = clean[2] ?? '0';
const hydroCode = clean[3] ?? '0';
const popCode = clean[4] ?? '0';
const govCode = clean[5] ?? '0';
const lawCode = clean[6] ?? '0';
const tlCode = clean[8] ?? '0';
const sizeVal = eHexValue(sizeCode);
const atmVal = eHexValue(atmCode);
const hydroVal = eHexValue(hydroCode);
const popVal = eHexValue(popCode);
const govVal = eHexValue(govCode);
const lawVal = eHexValue(lawCode);
const tlVal = eHexValue(tlCode);
const sizeInfo = SIZE_DESC[sizeVal] ?? { description: 'Unknown size', diameter_km: 'Unknown' };
const hydroInfo = HYDROGRAPHICS_DESC[hydroVal] ?? { description: 'Unknown hydrographics', percent: 'Unknown' };
const popInfo = POPULATION_DESC[popVal] ?? { description: 'Unknown population', estimate: 'Unknown' };
return {
raw: uwp,
starport: {
code: starportCode,
description: STARPORT[starportCode.toUpperCase()] ?? `Unknown starport code: ${starportCode}`,
},
size: {
code: sizeCode,
value: sizeVal,
description: sizeInfo.description,
diameter_km: sizeInfo.diameter_km,
},
atmosphere: {
code: atmCode,
value: atmVal,
description: ATMOSPHERE_DESC[atmVal] ?? `Unknown atmosphere code: ${atmCode}`,
},
hydrographics: {
code: hydroCode,
value: hydroVal,
description: hydroInfo.description,
percent: hydroInfo.percent,
},
population: {
code: popCode,
value: popVal,
description: popInfo.description,
estimate: popInfo.estimate,
},
government: {
code: govCode,
value: govVal,
description: GOVERNMENT_DESC[govVal] ?? `Unknown government code: ${govCode}`,
},
law_level: {
code: lawCode,
value: lawVal,
description: LAW_LEVEL_DESC[lawVal] ?? `Unknown law level code: ${lawCode}`,
},
tech_level: {
code: tlCode,
value: tlVal,
description: `Tech Level ${tlVal}`,
era: TECH_LEVEL_ERA[tlVal] ?? 'Advanced/Unknown',
},
};
}

View File

@@ -0,0 +1,88 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as getSubsectorMap from './tools/get_subsector_map.js';
import * as renderCustomMap from './tools/render_custom_map.js';
import * as getWorldInfo from './tools/get_world_info.js';
import * as searchWorlds from './tools/search_worlds.js';
import * as getJumpMap from './tools/get_jump_map.js';
import * as findRoute from './tools/find_route.js';
import * as getWorldsInJumpRange from './tools/get_worlds_in_jump_range.js';
import * as getSectorList from './tools/get_sector_list.js';
import * as getSectorData from './tools/get_sector_data.js';
import * as getSectorMetadata from './tools/get_sector_metadata.js';
import * as getAllegianceList from './tools/get_allegiance_list.js';
export function createServer(): McpServer {
const server = new McpServer({
name: 'traveller-map',
version: '1.0.0',
});
server.registerTool(
getSubsectorMap.name,
{ description: getSubsectorMap.description, inputSchema: getSubsectorMap.inputSchema.shape },
(args) => getSubsectorMap.handler(args as getSubsectorMap.Input),
);
server.registerTool(
renderCustomMap.name,
{ description: renderCustomMap.description, inputSchema: renderCustomMap.inputSchema.shape },
(args) => renderCustomMap.handler(args as renderCustomMap.Input),
);
server.registerTool(
getWorldInfo.name,
{ description: getWorldInfo.description, inputSchema: getWorldInfo.inputSchema.shape },
(args) => getWorldInfo.handler(args as getWorldInfo.Input),
);
server.registerTool(
searchWorlds.name,
{ description: searchWorlds.description, inputSchema: searchWorlds.inputSchema.shape },
(args) => searchWorlds.handler(args as searchWorlds.Input),
);
server.registerTool(
getJumpMap.name,
{ description: getJumpMap.description, inputSchema: getJumpMap.inputSchema.shape },
(args) => getJumpMap.handler(args as getJumpMap.Input),
);
server.registerTool(
findRoute.name,
{ description: findRoute.description, inputSchema: findRoute.inputSchema.shape },
(args) => findRoute.handler(args as findRoute.Input),
);
server.registerTool(
getWorldsInJumpRange.name,
{ description: getWorldsInJumpRange.description, inputSchema: getWorldsInJumpRange.inputSchema.shape },
(args) => getWorldsInJumpRange.handler(args as getWorldsInJumpRange.Input),
);
server.registerTool(
getSectorList.name,
{ description: getSectorList.description, inputSchema: getSectorList.inputSchema.shape },
(args) => getSectorList.handler(args as getSectorList.Input),
);
server.registerTool(
getSectorData.name,
{ description: getSectorData.description, inputSchema: getSectorData.inputSchema.shape },
(args) => getSectorData.handler(args as getSectorData.Input),
);
server.registerTool(
getSectorMetadata.name,
{ description: getSectorMetadata.description, inputSchema: getSectorMetadata.inputSchema.shape },
(args) => getSectorMetadata.handler(args as getSectorMetadata.Input),
);
server.registerTool(
getAllegianceList.name,
{ description: getAllegianceList.description, inputSchema: getAllegianceList.inputSchema.shape },
(args) => getAllegianceList.handler(args as getAllegianceList.Input),
);
return server;
}

View File

@@ -0,0 +1,147 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
import { parseUWP } from '../parsers/uwp.js';
export const name = 'find_route';
export const description =
'Finds a jump route between two worlds and returns the sequence of intermediate worlds. ' +
'Locations are specified as "Sector XXYY" (e.g. "Spinward Marches 1910"). ' +
'Returns 404/no-route if no path exists within the jump rating. ' +
'Options: avoid Red zones, require Imperial membership, require wilderness refueling stops.';
export const inputSchema = z.object({
start: z
.string()
.describe('Starting world as "Sector XXYY" (e.g. "Spinward Marches 1910") or T5SS abbreviation format'),
end: z
.string()
.describe('Destination world in the same format (e.g. "Core 2118" for Capital)'),
jump: z
.number()
.min(1)
.max(12)
.optional()
.default(2)
.describe('Maximum jump distance per leg (1-12, default 2)'),
avoid_red_zones: z
.boolean()
.optional()
.default(false)
.describe('If true, the route will not pass through TAS Red Zone worlds'),
imperial_only: z
.boolean()
.optional()
.default(false)
.describe('If true, only stop at Third Imperium member worlds'),
wilderness_refueling: z
.boolean()
.optional()
.default(false)
.describe('If true, stops must have wilderness refueling available (gas giant or ocean)'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
});
export type Input = z.infer<typeof inputSchema>;
interface RouteWorld {
Name?: string;
Hex?: string;
Sector?: string | { Name?: string };
UWP?: string;
Zone?: string;
Bases?: string;
Allegiance?: string;
AllegianceName?: string;
[key: string]: unknown;
}
const ZONE_LABELS: Record<string, string> = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' };
export async function handler(args: Input) {
const { start, end, jump, avoid_red_zones, imperial_only, wilderness_refueling, milieu } = args;
let rawData: unknown;
try {
rawData = await apiGetJson<unknown>('/api/route', {
start,
end,
jump,
nored: avoid_red_zones ? 1 : undefined,
im: imperial_only ? 1 : undefined,
wild: wilderness_refueling ? 1 : undefined,
milieu,
});
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes('404')) {
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
found: false,
start,
end,
jump_rating: jump,
message: `No jump-${jump} route found between "${start}" and "${end}". Try increasing the jump rating or relaxing constraints.`,
},
null,
2,
),
},
],
};
}
throw err;
}
// The API returns an array of worlds directly (not wrapped in {Worlds: [...]})
const worldArray: RouteWorld[] = Array.isArray(rawData)
? (rawData as RouteWorld[])
: ((rawData as { Worlds?: RouteWorld[] }).Worlds ?? []);
const worlds = worldArray.map((w) => {
const zoneCode = (w.Zone ?? '').trim();
const uwpRaw = w.UWP ?? '';
const sectorName = typeof w.Sector === 'string' ? w.Sector : w.Sector?.Name;
let starport = '';
if (uwpRaw && uwpRaw !== '?000000-0') {
try {
starport = parseUWP(uwpRaw).starport.code;
} catch {
starport = uwpRaw[0] ?? '';
}
}
return {
name: w.Name,
sector: sectorName,
hex: w.Hex,
location: sectorName && w.Hex ? `${sectorName} ${w.Hex}` : undefined,
uwp: uwpRaw,
starport,
zone: (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green',
bases: w.Bases,
allegiance: w.AllegianceName ?? w.Allegiance,
};
});
const result = {
found: true,
start,
end,
jump_rating: jump,
total_jumps: Math.max(0, worlds.length - 1),
route: worlds,
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}

View File

@@ -0,0 +1,42 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
export const name = 'get_allegiance_list';
export const description =
'Returns all known allegiance codes and their full names. ' +
'Use this to find the right code before searching with "alleg:" in search_worlds. ' +
'Examples: "ImDd" = "Third Imperium, Domain of Deneb", "Zh" = "Zhodani Consulate".';
export const inputSchema = z.object({});
export type Input = z.infer<typeof inputSchema>;
interface AllegianceEntry {
Code?: string;
Name?: string;
[key: string]: unknown;
}
export async function handler(_args: Input) {
const data = await apiGetJson<AllegianceEntry[]>('/t5ss/allegiances', {});
const allegiances = (Array.isArray(data) ? data : []).map((a) => ({
code: a.Code,
name: a.Name,
}));
const result = {
count: allegiances.length,
allegiances,
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}

View File

@@ -0,0 +1,112 @@
import { stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { z } from 'zod';
import { apiGetDataUri } from '../api/client.js';
export const name = 'get_jump_map';
export const description =
'Returns a PNG image of the hex map centered on a world, showing all worlds within N jump distance. ' +
'Great for visualizing routes and nearby systems during a session. ' +
'Optionally saves the image to a local directory (e.g. an Obsidian vault attachments folder) ' +
'and returns the saved file path so it can be embedded in notes.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'),
hex: z.string().describe('Hex location in XXYY format (e.g. "1910" for Regina)'),
jump: z.number().min(0).max(20).optional().default(2).describe('Jump range in parsecs (0-20, default 2)'),
scale: z.number().optional().default(64).describe('Pixels per parsec (default 64)'),
style: z
.enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose'])
.optional()
.default('poster')
.describe('Visual rendering style. Note: candy style returns JPEG instead of PNG.'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
save_path: z
.string()
.optional()
.describe('Absolute directory path where the image file should be saved'),
filename: z
.string()
.optional()
.describe(
'Custom filename without extension (e.g. "regina-jump2"). ' +
'Auto-generated as "{sector}-{hex}-jump{jump}" if omitted.',
),
});
export type Input = z.infer<typeof inputSchema>;
function extFromMimeType(mimeType: string): string {
return mimeType === 'image/jpeg' ? 'jpg' : 'png';
}
function buildFilename(sector: string, hex: string, jump: number, customName: string | undefined, ext: string): string {
let base: string;
if (customName) {
base = customName.replace(/\.(png|jpe?g)$/i, '');
} else {
base = `${sector}-${hex}-jump${jump}`
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9\-_]/g, '');
}
return `${base}.${ext}`;
}
export async function handler(args: Input) {
const { sector, hex, jump, scale, style, milieu, save_path, filename } = args;
const { base64, mimeType } = await apiGetDataUri('/api/jumpmap', {
sector,
hex,
jump,
scale,
style,
milieu,
});
const imageBlock = { type: 'image' as const, data: base64, mimeType };
if (!save_path) {
return { content: [imageBlock] };
}
let dirStat;
try {
dirStat = await stat(save_path);
} catch {
return {
isError: true,
content: [{ type: 'text' as const, text: `Error: save_path directory does not exist: ${save_path}` }],
};
}
if (!dirStat.isDirectory()) {
return {
isError: true,
content: [{ type: 'text' as const, text: `Error: save_path is not a directory: ${save_path}` }],
};
}
const ext = extFromMimeType(mimeType);
const resolvedFilename = buildFilename(sector, hex, jump, filename, ext);
const fullPath = join(save_path, resolvedFilename);
try {
await writeFile(fullPath, Buffer.from(base64, 'base64'));
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
return {
isError: true,
content: [{ type: 'text' as const, text: `Error: Failed to write file: ${message}` }],
};
}
return {
content: [
imageBlock,
{ type: 'text' as const, text: `Image saved to: ${fullPath}` },
],
};
}

View File

@@ -0,0 +1,51 @@
import { z } from 'zod';
import { apiGetText } from '../api/client.js';
import { parseSecTabDelimited } from '../parsers/sec.js';
export const name = 'get_sector_data';
export const description =
'Returns all worlds in a sector (or single subsector) as structured data with decoded UWPs. ' +
'Useful for bulk analysis, e.g. "which worlds in Spinward Marches have Tech Level 15?" ' +
'or "list all Naval Bases in the Regina subsector". ' +
'Returns full world records including trade codes, bases, allegiance, and decoded UWP fields.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'),
subsector: z
.string()
.optional()
.describe('Limit to a single subsector by letter (A-P) or name'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
});
export type Input = z.infer<typeof inputSchema>;
export async function handler(args: Input) {
const { sector, subsector, milieu } = args;
const text = await apiGetText('/api/sec', {
sector,
subsector,
type: 'TabDelimited',
milieu,
});
const worlds = parseSecTabDelimited(text);
const result = {
sector,
subsector: subsector ?? 'all',
count: worlds.length,
worlds,
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}

View File

@@ -0,0 +1,67 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
export const name = 'get_sector_list';
export const description =
'Returns a list of all known sectors in the Traveller universe with their names, ' +
'T5SS abbreviations, and galactic coordinates. ' +
'Can be filtered by official status (Official, InReview, Preserve, Apocryphal) and campaign era.';
export const inputSchema = z.object({
milieu: z.string().optional().describe('Campaign era filter (e.g. "M1105")'),
tag: z
.enum(['Official', 'InReview', 'Preserve', 'Apocryphal'])
.optional()
.describe('Filter by sector data status'),
});
export type Input = z.infer<typeof inputSchema>;
interface Sector {
Names?: Array<{ Text?: string }>;
Abbreviation?: string;
X?: number;
Y?: number;
Tags?: string;
[key: string]: unknown;
}
interface UniverseResponse {
Sectors?: Sector[];
[key: string]: unknown;
}
export async function handler(args: Input) {
const { milieu, tag } = args;
const data = await apiGetJson<UniverseResponse>('/api/universe', {
requireData: 1,
milieu,
tag,
});
const sectors = (data.Sectors ?? [])
.map((s) => ({
name: s.Names?.[0]?.Text ?? 'Unknown',
abbreviation: s.Abbreviation,
x: s.X,
y: s.Y,
tags: s.Tags,
}))
.sort((a, b) => a.name.localeCompare(b.name));
const result = {
count: sectors.length,
sectors,
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}

View File

@@ -0,0 +1,76 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
export const name = 'get_sector_metadata';
export const description =
'Returns metadata for a sector: subsector names (A-P), allegiance regions, route overlays, and political borders. ' +
'Use this to discover subsector names before calling get_subsector_map, ' +
'or to understand the political structure of a sector.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
});
export type Input = z.infer<typeof inputSchema>;
interface SubsectorMeta {
Name?: string;
Index?: string;
[key: string]: unknown;
}
interface AllegianceMeta {
Code?: string;
Name?: string;
[key: string]: unknown;
}
interface MetadataResponse {
Names?: Array<{ Text?: string; Lang?: string }>;
Abbreviation?: string;
X?: number;
Y?: number;
Subsectors?: SubsectorMeta[];
Allegiances?: AllegianceMeta[];
[key: string]: unknown;
}
export async function handler(args: Input) {
const { sector, milieu } = args;
const data = await apiGetJson<MetadataResponse>('/api/metadata', { sector, milieu });
const subsectorsByIndex: Record<string, string> = {};
for (const sub of data.Subsectors ?? []) {
if (sub.Index && sub.Name) {
subsectorsByIndex[sub.Index] = sub.Name;
}
}
const subsectors = 'ABCDEFGHIJKLMNOP'.split('').map((letter) => ({
index: letter,
name: subsectorsByIndex[letter] ?? `Subsector ${letter}`,
}));
const result = {
names: data.Names?.map((n) => ({ text: n.Text, lang: n.Lang })),
abbreviation: data.Abbreviation,
coordinates: { x: data.X, y: data.Y },
subsectors,
allegiances: (data.Allegiances ?? []).map((a) => ({
code: a.Code,
name: a.Name,
})),
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}

View File

@@ -0,0 +1,109 @@
import { stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { z } from 'zod';
import { apiGetDataUri } from '../api/client.js';
export const name = 'get_subsector_map';
export const description =
'Returns a PNG image of a named subsector from the official Traveller Map database. ' +
'Optionally saves the image to a local directory (e.g. an Obsidian vault attachments folder) ' +
'and returns the saved file path so it can be embedded in notes.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'),
subsector: z.string().describe('Subsector letter A-P or subsector name (e.g. "A" or "Regina")'),
scale: z.number().optional().default(64).describe('Pixels per parsec (default 64). Higher = larger image.'),
style: z
.enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose'])
.optional()
.default('poster')
.describe('Visual rendering style. Note: candy style returns JPEG instead of PNG.'),
milieu: z.string().optional().describe('Campaign era (e.g. "M1105" for default Third Imperium 1105)'),
save_path: z
.string()
.optional()
.describe('Absolute directory path where the image file should be saved'),
filename: z
.string()
.optional()
.describe(
'Custom filename without extension (e.g. "regina-map"). ' +
'Auto-generated as "{sector}-{subsector}-subsector" if omitted.',
),
});
export type Input = z.infer<typeof inputSchema>;
function extFromMimeType(mimeType: string): string {
return mimeType === 'image/jpeg' ? 'jpg' : 'png';
}
function buildFilename(sector: string, subsector: string, customName: string | undefined, ext: string): string {
let base: string;
if (customName) {
base = customName.replace(/\.(png|jpe?g)$/i, '');
} else {
base = `${sector}-${subsector}-subsector`
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9\-_]/g, '');
}
return `${base}.${ext}`;
}
export async function handler(args: Input) {
const { sector, subsector, scale, style, milieu, save_path, filename } = args;
const { base64, mimeType } = await apiGetDataUri('/api/poster', {
sector,
subsector,
scale,
style,
milieu,
});
const imageBlock = { type: 'image' as const, data: base64, mimeType };
if (!save_path) {
return { content: [imageBlock] };
}
let dirStat;
try {
dirStat = await stat(save_path);
} catch {
return {
isError: true,
content: [{ type: 'text' as const, text: `Error: save_path directory does not exist: ${save_path}` }],
};
}
if (!dirStat.isDirectory()) {
return {
isError: true,
content: [{ type: 'text' as const, text: `Error: save_path is not a directory: ${save_path}` }],
};
}
const ext = extFromMimeType(mimeType);
const resolvedFilename = buildFilename(sector, subsector, filename, ext);
const fullPath = join(save_path, resolvedFilename);
try {
await writeFile(fullPath, Buffer.from(base64, 'base64'));
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
return {
isError: true,
content: [{ type: 'text' as const, text: `Error: Failed to write file: ${message}` }],
};
}
return {
content: [
imageBlock,
{ type: 'text' as const, text: `Image saved to: ${fullPath}` },
],
};
}

View File

@@ -0,0 +1,169 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
import { parseUWP } from '../parsers/uwp.js';
export const name = 'get_world_info';
export const description =
'Retrieves detailed information about a specific world, with the UWP (Universal World Profile) ' +
'fully decoded into human-readable fields: starport quality, world size, atmosphere type, ' +
'hydrographics percentage, population estimate, government type, law level, and tech level era. ' +
'Also returns trade codes, bases, travel zone, allegiance, and stellar data.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation (e.g. "Spinward Marches" or "spin")'),
hex: z.string().describe('Hex location in XXYY format (e.g. "1910" for Regina in Spinward Marches)'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
});
export type Input = z.infer<typeof inputSchema>;
interface CreditsResponse {
Name?: string;
Hex?: string;
Sector?: string;
Subsector?: string;
UWP?: string;
Bases?: string;
Remarks?: string;
Zone?: string;
PBG?: string;
Allegiance?: string;
Stars?: string;
Ix?: string;
Ex?: string;
Cx?: string;
Nobility?: string;
W?: number;
RU?: number;
[key: string]: unknown;
}
const ZONE_LABELS: Record<string, string> = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' };
const BASE_LABELS: Record<string, string> = {
N: 'Naval Base',
S: 'Scout Base',
W: 'Scout Waystation',
D: 'Naval Depot',
K: 'Naval Base (Sword Worlds)',
M: 'Military Base',
C: 'Corsair Base',
T: 'TAS Hostel',
R: 'Aslan Clan Base',
F: 'Aslan Tlaukhu Base',
A: 'Naval Base + Scout Base',
B: 'Naval Base + Scout Waystation',
G: 'Scout Base (Vargr)',
H: 'Naval Base + Scout Base (Vargr)',
X: 'Zhodani Relay Station',
Z: 'Zhodani Naval + Scout Base',
};
function decodeBases(bases: string): string[] {
if (!bases || bases.trim() === '' || bases.trim() === '-') return [];
return bases
.trim()
.split('')
.filter((c) => c !== ' ' && c !== '-')
.map((c) => BASE_LABELS[c] ?? `Base (${c})`);
}
function decodeTradeCodes(remarks: string): Array<{ code: string; meaning: string }> {
const TRADE_CODES: Record<string, string> = {
Ag: 'Agricultural',
As: 'Asteroid',
Ba: 'Barren',
De: 'Desert',
Fl: 'Fluid Oceans (non-water)',
Ga: 'Garden World',
Hi: 'High Population',
Ht: 'High Technology',
Ic: 'Ice-Capped',
In: 'Industrial',
Lo: 'Low Population',
Lt: 'Low Technology',
Na: 'Non-Agricultural',
Ni: 'Non-Industrial',
Po: 'Poor',
Ri: 'Rich',
Tr: 'Temperate',
Tu: 'Tundra',
Tz: 'Tidally Locked',
Wa: 'Water World',
Va: 'Vacuum',
Ph: 'Pre-High Population',
Pi: 'Pre-Industrial',
Pa: 'Pre-Agricultural',
Mr: 'Reserve',
Fr: 'Frozen',
Ho: 'Hot',
Co: 'Cold',
Lk: 'Locked',
Tr2: 'Tropic',
Sa: 'Satellite',
Fa: 'Farming',
Mi: 'Mining',
Pz: 'Puzzle',
Cy: 'Cyclopean',
Di: 'Dieback',
Px: 'Prison/Exile Camp',
An: 'Ancient Site',
Rs: 'Research Station',
Cp: 'Subsector Capital',
Cs: 'Sector Capital',
Cx: 'Capital',
Fo: 'Forbidden',
Pn: 'Prison',
Re: 'Reserve',
};
if (!remarks) return [];
return remarks
.trim()
.split(/\s+/)
.filter((r) => r && r !== '-')
.map((code) => ({ code, meaning: TRADE_CODES[code] ?? code }));
}
export async function handler(args: Input) {
const { sector, hex, milieu } = args;
const data = await apiGetJson<CreditsResponse>('/api/credits', { sector, hex, milieu });
const uwpRaw = data.UWP ?? '?000000-0';
const decoded = parseUWP(uwpRaw);
const zoneCode = (data.Zone ?? '').trim();
const zone = (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green';
const result = {
name: data.Name ?? 'Unknown',
hex: data.Hex ?? hex,
sector: data.Sector ?? sector,
subsector: data.Subsector,
uwp: uwpRaw,
decoded_uwp: decoded,
trade_codes: decodeTradeCodes(data.Remarks ?? ''),
bases: decodeBases(data.Bases ?? ''),
zone,
pbg: data.PBG,
allegiance: data.Allegiance,
stellar: data.Stars,
importance: data.Ix,
economic_extension: data.Ex,
cultural_extension: data.Cx,
nobility: data.Nobility,
worlds_in_system: data.W,
resource_units: data.RU,
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}

View File

@@ -0,0 +1,98 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
import { parseUWP } from '../parsers/uwp.js';
export const name = 'get_worlds_in_jump_range';
export const description =
'Lists all worlds reachable from a given location within N parsecs. ' +
'Returns structured data for each world including UWP, trade codes, bases, zone, and allegiance. ' +
'Useful for planning routes or finding nearby systems to visit.';
export const inputSchema = z.object({
sector: z.string().describe('Sector name or T5SS abbreviation'),
hex: z.string().describe('Hex location in XXYY format'),
jump: z.number().min(0).max(12).optional().default(2).describe('Jump range in parsecs (0-12, default 2)'),
milieu: z.string().optional().describe('Campaign era (default: M1105)'),
});
export type Input = z.infer<typeof inputSchema>;
interface JumpWorld {
Name?: string;
Hex?: string;
Sector?: { Name?: string; Abbreviation?: string };
UWP?: string;
Bases?: string;
Remarks?: string;
Zone?: string;
Allegiance?: string;
Distance?: number;
[key: string]: unknown;
}
interface JumpWorldsResponse {
Worlds?: JumpWorld[];
[key: string]: unknown;
}
const ZONE_LABELS: Record<string, string> = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' };
function parseTradeCodes(remarks: string | undefined): string[] {
if (!remarks) return [];
return remarks
.trim()
.split(/\s+/)
.filter((r) => r && r !== '-');
}
export async function handler(args: Input) {
const { sector, hex, jump, milieu } = args;
const data = await apiGetJson<JumpWorldsResponse>('/api/jumpworlds', { sector, hex, jump, milieu });
const worlds = (data.Worlds ?? []).map((w) => {
const zoneCode = (w.Zone ?? '').trim();
const uwpRaw = w.UWP ?? '';
let starport = '';
let techLevel = '';
if (uwpRaw && uwpRaw.length > 1) {
try {
const decoded = parseUWP(uwpRaw);
starport = decoded.starport.code;
techLevel = decoded.tech_level.code;
} catch {
starport = uwpRaw[0] ?? '';
}
}
return {
name: w.Name,
sector: w.Sector?.Name,
hex: w.Hex,
uwp: uwpRaw,
starport,
tech_level: techLevel,
bases: w.Bases,
trade_codes: parseTradeCodes(w.Remarks),
zone: (ZONE_LABELS[zoneCode] ?? zoneCode) || 'Green',
allegiance: w.Allegiance,
distance: w.Distance,
};
});
const result = {
origin: `${hex} in ${sector}`,
jump_range: jump,
count: worlds.length,
worlds,
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}

View File

@@ -0,0 +1,54 @@
import { z } from 'zod';
import { apiPostImage } from '../api/client.js';
export const name = 'render_custom_map';
export const description =
'Renders a map image from custom SEC-format world data that you provide. ' +
'Useful for homebrew sectors, campaign-specific maps, or previewing modified sector data. ' +
'The sec_data parameter accepts T5 Second Survey, T5 tab-delimited, or legacy SEC format.';
export const inputSchema = z.object({
sec_data: z
.string()
.describe('World data in T5 Second Survey, T5 tab-delimited, or legacy SEC format'),
metadata: z
.string()
.optional()
.describe('Optional sector metadata in XML or MSEC format (defines sector name, subsector names, borders, etc.)'),
scale: z.number().optional().default(64).describe('Pixels per parsec (default 64)'),
style: z
.enum(['poster', 'print', 'atlas', 'candy', 'draft', 'fasa', 'terminal', 'mongoose'])
.optional()
.default('poster')
.describe('Visual rendering style'),
subsector: z
.string()
.optional()
.describe('Render only this subsector (A-P letter) instead of the full sector'),
});
export type Input = z.infer<typeof inputSchema>;
export async function handler(args: Input) {
const { sec_data, metadata, scale, style, subsector } = args;
const formBody: Record<string, string> = { data: sec_data };
if (metadata) formBody['metadata'] = metadata;
const base64 = await apiPostImage(
'/api/poster',
{ scale, style, subsector },
formBody,
);
return {
content: [
{ type: 'image' as const, data: base64, mimeType: 'image/png' },
{
type: 'text' as const,
text: `Custom sector map rendered${subsector ? ` (subsector ${subsector})` : ''}`,
},
],
};
}

View File

@@ -0,0 +1,139 @@
import { z } from 'zod';
import { apiGetJson } from '../api/client.js';
export const name = 'search_worlds';
export const description =
'Search for worlds using name patterns, UWP criteria, trade codes, allegiance codes, or travel zones. ' +
'Supports wildcards (* ? %) and multiple filters. ' +
'Examples: "Regina", "Reg*", "uwp:A????[89A]-" (high-pop with excellent starport), ' +
'"remark:Wa" (water worlds), "alleg:Im" (Third Imperium worlds), "zone:R" (Red zones). ' +
'Add a sector name to the query to scope results (e.g. "Wa Tobia" to find water worlds near Tobia). ' +
'Multiple terms are ANDed together.';
export const inputSchema = z.object({
query: z
.string()
.describe(
'Search query. Examples: "Regina", "Reg*", "uwp:A????[89A]-", "remark:Wa", "alleg:Im", "zone:R", "zone:A". ' +
'Multiple terms are ANDed. Add a sector name to narrow results.',
),
milieu: z.string().optional().describe('Campaign era filter (e.g. "M1105")'),
});
export type Input = z.infer<typeof inputSchema>;
interface SearchResult {
Results?: {
Items?: SearchItem[];
};
[key: string]: unknown;
}
interface SearchItem {
World?: WorldResult;
Sector?: SectorResult;
Subsector?: SubsectorResult;
[key: string]: unknown;
}
interface WorldResult {
Name?: string;
Hex?: string;
HexX?: number;
HexY?: number;
Sector?: string | { Name?: string; Abbreviation?: string };
Subsector?: string;
UWP?: string;
Uwp?: string;
Bases?: string;
Remarks?: string;
Zone?: string;
PBG?: string;
Allegiance?: string;
[key: string]: unknown;
}
interface SectorResult {
Name?: string;
Abbreviation?: string;
[key: string]: unknown;
}
interface SubsectorResult {
Name?: string;
Index?: string;
[key: string]: unknown;
}
const ZONE_LABELS: Record<string, string> = { R: 'Red', A: 'Amber', G: 'Green', '': 'Green' };
function normalizeZone(zone: string | undefined): string {
const z = (zone ?? '').trim();
return (ZONE_LABELS[z] ?? z) || 'Green';
}
function parseTradeCodes(remarks: string | undefined): string[] {
if (!remarks) return [];
return remarks
.trim()
.split(/\s+/)
.filter((r) => r && r !== '-');
}
export async function handler(args: Input) {
const { query, milieu } = args;
const data = await apiGetJson<SearchResult>('/api/search', { q: query, milieu });
const items = data?.Results?.Items ?? [];
const worlds: unknown[] = [];
const sectors: unknown[] = [];
const subsectors: unknown[] = [];
for (const item of items) {
if (item.World) {
const w = item.World;
// The API returns Sector as a string, HexX/HexY as ints, Uwp (lowercase p)
const sectorName = typeof w.Sector === 'string' ? w.Sector : w.Sector?.Name;
const hexX = w.HexX !== undefined ? String(w.HexX).padStart(2, '0') : undefined;
const hexY = w.HexY !== undefined ? String(w.HexY).padStart(2, '0') : undefined;
const hex = hexX && hexY ? `${hexX}${hexY}` : w.Hex;
const uwp = w.UWP ?? w.Uwp;
worlds.push({
name: w.Name,
sector: sectorName,
hex,
location: sectorName && hex ? `${sectorName} ${hex}` : undefined,
uwp,
bases: w.Bases,
trade_codes: parseTradeCodes(w.Remarks),
zone: normalizeZone(w.Zone),
pbg: w.PBG,
allegiance: w.Allegiance,
});
} else if (item.Sector) {
sectors.push({ name: item.Sector.Name, abbreviation: item.Sector.Abbreviation });
} else if (item.Subsector) {
subsectors.push({ name: item.Subsector.Name, index: item.Subsector.Index });
}
}
const result = {
query,
total_results: items.length,
worlds: { count: worlds.length, results: worlds },
sectors: sectors.length > 0 ? { count: sectors.length, results: sectors } : undefined,
subsectors: subsectors.length > 0 ? { count: subsectors.length, results: subsectors } : undefined,
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}