tdx-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/api.d.ts +8 -0
- package/dist/api.js +99 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +70 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# tdx-mcp
|
|
2
|
+
|
|
3
|
+
Taiwan TDX transport: TRA and THSR stations, live boards, daily timetables. Needs app keys.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
export TDX_CLIENT_ID=...
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Needs free TDX client id + secret (TDX_CLIENT_SECRET) from tdx.transportdata.tw.
|
|
12
|
+
|
|
13
|
+
## Tools
|
|
14
|
+
|
|
15
|
+
- `list_tra_stations` — Taiwan Railway stations with ids, names, coordinates for live boards.
|
|
16
|
+
- `get_tra_liveboard` — Live Taiwan Railway departures and arrivals for a station.
|
|
17
|
+
- `list_thsr_stations` — Taiwan High Speed Rail stations with ids and addresses.
|
|
18
|
+
- `get_tra_timetable` — Taiwan Railway daily timetable for a station on a date.
|
|
19
|
+
|
|
20
|
+
Part of [awesome-mcps](https://github.com/mrfentmen/awesome-mcps).
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class TdxError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare function errorMessage(e: unknown): string;
|
|
5
|
+
export declare function listTraStations(): Promise<string>;
|
|
6
|
+
export declare function getTraLiveboard(stationId: string): Promise<string>;
|
|
7
|
+
export declare function listThsrStations(): Promise<string>;
|
|
8
|
+
export declare function getTraTimetable(stationId: string, date: string): Promise<string>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export class TdxError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "TdxError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function errorMessage(e) {
|
|
8
|
+
return e instanceof Error ? e.message : String(e);
|
|
9
|
+
}
|
|
10
|
+
const UA = { "User-Agent": "awesome-mcps/1.0" };
|
|
11
|
+
const BASE = "https://tdx.transportdata.tw";
|
|
12
|
+
function envStrict(name) {
|
|
13
|
+
const v = process.env[name];
|
|
14
|
+
if (!v)
|
|
15
|
+
throw new TdxError(`Set the ${name} environment variable.`);
|
|
16
|
+
return v;
|
|
17
|
+
}
|
|
18
|
+
let cachedToken = "";
|
|
19
|
+
let tokenExpiresAt = 0;
|
|
20
|
+
async function tdxToken() {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
if (!cachedToken || now >= tokenExpiresAt) {
|
|
23
|
+
const body = new URLSearchParams({
|
|
24
|
+
grant_type: "client_credentials",
|
|
25
|
+
client_id: envStrict("TDX_CLIENT_ID"),
|
|
26
|
+
client_secret: envStrict("TDX_CLIENT_SECRET"),
|
|
27
|
+
});
|
|
28
|
+
const res = await fetch("https://tdx.transportdata.tw/auth/realms/TDXConnect/protocol/openid-connect/token", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { ...UA, "Content-Type": "application/x-www-form-urlencoded" },
|
|
31
|
+
body: body.toString(),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
throw new TdxError(`TDX login failed: HTTP ${res.status}`);
|
|
35
|
+
const data = (await res.json());
|
|
36
|
+
if (!data.access_token)
|
|
37
|
+
throw new TdxError("TDX login returned no access token");
|
|
38
|
+
cachedToken = data.access_token;
|
|
39
|
+
tokenExpiresAt = now + (data.expires_in ?? 86400) * 1000 - 60000;
|
|
40
|
+
}
|
|
41
|
+
return cachedToken;
|
|
42
|
+
}
|
|
43
|
+
async function authHeaders() {
|
|
44
|
+
return { Authorization: "Bearer " + (await tdxToken()) };
|
|
45
|
+
}
|
|
46
|
+
function pretty(data) {
|
|
47
|
+
const t = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
48
|
+
return t.length > 12000 ? t.slice(0, 12000) + "\n…(truncated)" : t;
|
|
49
|
+
}
|
|
50
|
+
async function req(url, init = {}) {
|
|
51
|
+
const res = await fetch(url, { headers: { ...UA, ...(await authHeaders()), ...(init.headers ?? {}) }, ...init });
|
|
52
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
53
|
+
const data = ct.includes("json") ? await res.json() : await res.text();
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const detail = typeof data === "string" ? data.slice(0, 300) : JSON.stringify(data).slice(0, 300);
|
|
56
|
+
throw new TdxError(`HTTP ${res.status}: ${detail}`);
|
|
57
|
+
}
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
export async function listTraStations() {
|
|
61
|
+
let url = BASE + `/api/basic/v2/Rail/TRA/Station`;
|
|
62
|
+
const qs = new URLSearchParams();
|
|
63
|
+
qs.append("$format", "JSON");
|
|
64
|
+
const qstr = qs.toString();
|
|
65
|
+
if (qstr)
|
|
66
|
+
url += (url.includes('?') ? '&' : '?') + qstr;
|
|
67
|
+
const data = await req(url);
|
|
68
|
+
return pretty(data);
|
|
69
|
+
}
|
|
70
|
+
export async function getTraLiveboard(stationId) {
|
|
71
|
+
let url = BASE + `/api/basic/v2/Rail/TRA/LiveBoard/Station/${encodeURIComponent(String(stationId))}`;
|
|
72
|
+
const qs = new URLSearchParams();
|
|
73
|
+
qs.append("$format", "JSON");
|
|
74
|
+
const qstr = qs.toString();
|
|
75
|
+
if (qstr)
|
|
76
|
+
url += (url.includes('?') ? '&' : '?') + qstr;
|
|
77
|
+
const data = await req(url);
|
|
78
|
+
return pretty(data);
|
|
79
|
+
}
|
|
80
|
+
export async function listThsrStations() {
|
|
81
|
+
let url = BASE + `/api/basic/v2/Rail/THSR/Station`;
|
|
82
|
+
const qs = new URLSearchParams();
|
|
83
|
+
qs.append("$format", "JSON");
|
|
84
|
+
const qstr = qs.toString();
|
|
85
|
+
if (qstr)
|
|
86
|
+
url += (url.includes('?') ? '&' : '?') + qstr;
|
|
87
|
+
const data = await req(url);
|
|
88
|
+
return pretty(data);
|
|
89
|
+
}
|
|
90
|
+
export async function getTraTimetable(stationId, date) {
|
|
91
|
+
let url = BASE + `/api/basic/v2/Rail/TRA/DailyTimetable/Station/${encodeURIComponent(String(stationId))}/${encodeURIComponent(String(date))}`;
|
|
92
|
+
const qs = new URLSearchParams();
|
|
93
|
+
qs.append("$format", "JSON");
|
|
94
|
+
const qstr = qs.toString();
|
|
95
|
+
if (qstr)
|
|
96
|
+
url += (url.includes('?') ? '&' : '?') + qstr;
|
|
97
|
+
const data = await req(url);
|
|
98
|
+
return pretty(data);
|
|
99
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
console.error("MCP server running on stdio");
|
|
8
|
+
}
|
|
9
|
+
main().catch((err) => {
|
|
10
|
+
console.error("Fatal error:", err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { errorMessage, listTraStations, getTraLiveboard, listThsrStations, getTraTimetable, } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
|
+
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
7
|
+
export function createServer() {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "tdx-mcp",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
});
|
|
12
|
+
server.registerTool("list_tra_stations", {
|
|
13
|
+
title: "List TRA stations",
|
|
14
|
+
description: "Taiwan Railway stations with ids, names, coordinates for live boards.",
|
|
15
|
+
inputSchema: z.object({}),
|
|
16
|
+
annotations: READ_ONLY,
|
|
17
|
+
}, async ({}) => {
|
|
18
|
+
try {
|
|
19
|
+
return text(await listTraStations());
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return textError(errorMessage(e));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
server.registerTool("get_tra_liveboard", {
|
|
26
|
+
title: "Get TRA live board",
|
|
27
|
+
description: "Live Taiwan Railway departures and arrivals for a station.",
|
|
28
|
+
inputSchema: z.object({
|
|
29
|
+
stationId: z.string().describe("Station id from list_tra_stations"),
|
|
30
|
+
}),
|
|
31
|
+
annotations: READ_ONLY,
|
|
32
|
+
}, async ({ stationId }) => {
|
|
33
|
+
try {
|
|
34
|
+
return text(await getTraLiveboard(stationId));
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
return textError(errorMessage(e));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
server.registerTool("list_thsr_stations", {
|
|
41
|
+
title: "List THSR stations",
|
|
42
|
+
description: "Taiwan High Speed Rail stations with ids and addresses.",
|
|
43
|
+
inputSchema: z.object({}),
|
|
44
|
+
annotations: READ_ONLY,
|
|
45
|
+
}, async ({}) => {
|
|
46
|
+
try {
|
|
47
|
+
return text(await listThsrStations());
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return textError(errorMessage(e));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
server.registerTool("get_tra_timetable", {
|
|
54
|
+
title: "Get TRA timetable",
|
|
55
|
+
description: "Taiwan Railway daily timetable for a station on a date.",
|
|
56
|
+
inputSchema: z.object({
|
|
57
|
+
stationId: z.string().describe("Station id"),
|
|
58
|
+
date: z.string().describe("Date YYYY-MM-DD"),
|
|
59
|
+
}),
|
|
60
|
+
annotations: READ_ONLY,
|
|
61
|
+
}, async ({ stationId, date }) => {
|
|
62
|
+
try {
|
|
63
|
+
return text(await getTraTimetable(stationId, date));
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
return textError(errorMessage(e));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return server;
|
|
70
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tdx-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Taiwan TDX transport: TRA and THSR stations, live boards, daily timetables. Needs app keys.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/tdx-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/mrfentmen/awesome-mcps.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"tdx-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"dev": "rm -rf dist && tsc -p tsconfig.json && node dist/index.js",
|
|
22
|
+
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"taiwan",
|
|
27
|
+
"tdx",
|
|
28
|
+
"rail",
|
|
29
|
+
"tra",
|
|
30
|
+
"thsr",
|
|
31
|
+
"transit"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
36
|
+
"zod": "^3.23.8"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"typescript": "^5.6.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
}
|
|
45
|
+
}
|