gtfs-pro-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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ Copyright (C) Digital Mountaineers
2
+
3
+ This program is free software; you can redistribute it and/or modify it under
4
+ the terms of the GNU General Public License as published by the Free Software
5
+ Foundation; either version 2 of the License, or (at your option) any later
6
+ version.
7
+
8
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY
9
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
10
+ PARTICULAR PURPOSE. See the GNU General Public License for more details.
11
+
12
+ You should have received a copy of the GNU General Public License along with
13
+ this program; if not, see <https://www.gnu.org/licenses/gpl-2.0.html>.
14
+
15
+ SPDX-License-Identifier: GPL-2.0-or-later
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # gtfs-pro-mcp
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that turns any
4
+ [WP GTFS Pro](https://devbydm.com) transit site into a set of tools for AI
5
+ assistants. Point it at an agency's WordPress site and Claude, ChatGPT, or any
6
+ other MCP-compatible client can answer rider questions from the agency's **live**
7
+ GTFS and GTFS-Realtime data — nearest stops, next departures, routes, live bus
8
+ positions, and service alerts.
9
+
10
+ It talks to the site's public REST API (`/wp-json/wp-gtfs-pro/v1/*`) — no plugins,
11
+ no database access, no credentials required. One MCP server covers one transit site
12
+ (which may host multiple agency feeds).
13
+
14
+ > **Why this exists:** the only other transit MCP servers are single-agency,
15
+ > hard-coded hobby projects. This one works with *any* GTFS Pro install, driven
16
+ > entirely by configuration.
17
+
18
+ ## Tools
19
+
20
+ | Tool | What it does |
21
+ |------|--------------|
22
+ | `list_feeds` | List the transit feeds (agencies) configured on the site |
23
+ | `find_nearby_stops` | Nearest stops to a lat/lon, with serving routes and next departures |
24
+ | `search_stops` | Find stops by name, landmark, or stop code |
25
+ | `get_stop_departures` | Next departures from a specific stop, across all routes |
26
+ | `get_route_map` | A route's shape (path geometry) and the stops it serves |
27
+ | `get_system_map` | Every route in the system with its stops |
28
+ | `get_service_alerts` | Active GTFS-Realtime service alerts (empty = no active alerts) |
29
+ | `get_live_vehicles` | Real-time vehicle positions (empty = none reporting right now) |
30
+ | `geocode_address` | Address / place name → coordinates, biased to the service area |
31
+
32
+ The realtime tools (`get_service_alerts`, `get_live_vehicles`) **never error on
33
+ missing data** — an empty result means "nothing active right now," not "no
34
+ service." The tool descriptions tell the AI this explicitly.
35
+
36
+ ## Requirements
37
+
38
+ - Node.js 18 or newer
39
+ - A transit website running WP GTFS Pro 1.5+ with its REST API publicly reachable
40
+
41
+ ## Configuration
42
+
43
+ Provide settings via a JSON config file **or** environment variables (env vars win
44
+ where both are set). Only the site URL is required.
45
+
46
+ ```jsonc
47
+ // gtfs-pro.config.json
48
+ {
49
+ "gtfs_pro_url": "https://your-agency-site.org",
50
+ "feed_id": "default",
51
+ "cache_ttl_seconds": 30,
52
+ "agency_name": "Your Transit Agency",
53
+ "agency_description": "Public transit serving ... (cities, landmarks, region)."
54
+ }
55
+ ```
56
+
57
+ | Setting | Env var | Default | Notes |
58
+ |---------|---------|---------|-------|
59
+ | `gtfs_pro_url` | `GTFS_PRO_URL` | — (required) | Base URL of the WP GTFS Pro site |
60
+ | `feed_id` | `GTFS_PRO_FEED_ID` | `default` | Default feed so the AI needn't pass one each call |
61
+ | `cache_ttl_seconds` | `GTFS_PRO_CACHE_TTL` | `30` | Local response cache; protects the WP site |
62
+ | `agency_name` | `GTFS_PRO_AGENCY_NAME` | — | Shown in server metadata |
63
+ | `agency_description` | `GTFS_PRO_AGENCY_DESCRIPTION` | — | Service-area context for the AI |
64
+
65
+ Pass a non-default config path with `--config /path/to/config.json` or the
66
+ `GTFS_PRO_CONFIG` env var.
67
+
68
+ ## Use with Claude Desktop
69
+
70
+ Edit `claude_desktop_config.json` (Settings → Developer → Edit Config) and add:
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "gtfs-pro-transit": {
76
+ "command": "npx",
77
+ "args": ["-y", "gtfs-pro-mcp"],
78
+ "env": {
79
+ "GTFS_PRO_URL": "https://your-agency-site.org",
80
+ "GTFS_PRO_FEED_ID": "default",
81
+ "GTFS_PRO_AGENCY_NAME": "Your Transit Agency"
82
+ }
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ Restart Claude Desktop, then ask: *"What bus stops are near \<a place in the
89
+ service area\>?"* or *"When's the next bus from \<stop name\>?"*
90
+
91
+ ## Use with ChatGPT / other MCP clients
92
+
93
+ Any client that launches a local stdio MCP server works the same way — run
94
+ `npx -y gtfs-pro-mcp` with the same environment variables, or install globally:
95
+
96
+ ```bash
97
+ npm install -g gtfs-pro-mcp
98
+ GTFS_PRO_URL=https://your-agency-site.org gtfs-pro-mcp
99
+ ```
100
+
101
+ ## Local development
102
+
103
+ ```bash
104
+ npm install
105
+ npm run build # compile TypeScript to dist/
106
+ node test/smoke.mjs # exercise all 9 tools against a live site
107
+ ```
108
+
109
+ ## How it fits together
110
+
111
+ ```
112
+ AI assistant ──MCP/stdio──► gtfs-pro-mcp ──HTTPS──► WP GTFS Pro REST API
113
+ (Claude, GPT) (this package) /wp-json/wp-gtfs-pro/v1/*
114
+ ```
115
+
116
+ A future release adds a Streamable-HTTP transport so the server can run as a
117
+ hosted remote service (one endpoint per agency) instead of a local process.
118
+
119
+ ## License
120
+
121
+ GPL-2.0-or-later © Digital Mountaineers
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Thin HTTP client for the WP GTFS Pro REST API.
3
+ *
4
+ * Responsibilities:
5
+ * - Build namespaced URLs and inject the default feed_id.
6
+ * - Cache GET responses in-process for `cacheTtlSeconds` (spec §1.9: keep it to
7
+ * ~1 call per 30s per unique query so we never hammer the agency's WP site).
8
+ * - Turn failures into human-readable messages (spec §1.6) rather than leaking
9
+ * raw JSON or stack traces to the AI.
10
+ */
11
+ import type { Config } from "./config.js";
12
+ /** A failure we want the AI to read as prose (not a thrown stack trace). */
13
+ export declare class GtfsApiError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ export declare class ApiClient {
17
+ private readonly config;
18
+ private readonly base;
19
+ private readonly cache;
20
+ constructor(config: Config);
21
+ /** The configured default feed, used when a tool call omits feed_id. */
22
+ get defaultFeedId(): string;
23
+ /**
24
+ * GET a REST path with query params. Empty/undefined params are dropped.
25
+ * Results are cached by full URL for the configured TTL.
26
+ */
27
+ get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
28
+ /**
29
+ * Convert a non-2xx response into a readable sentence. WordPress REST errors
30
+ * carry `{ code, message }`; surface the message when present.
31
+ */
32
+ private describeError;
33
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Thin HTTP client for the WP GTFS Pro REST API.
3
+ *
4
+ * Responsibilities:
5
+ * - Build namespaced URLs and inject the default feed_id.
6
+ * - Cache GET responses in-process for `cacheTtlSeconds` (spec §1.9: keep it to
7
+ * ~1 call per 30s per unique query so we never hammer the agency's WP site).
8
+ * - Turn failures into human-readable messages (spec §1.6) rather than leaking
9
+ * raw JSON or stack traces to the AI.
10
+ */
11
+ /** A failure we want the AI to read as prose (not a thrown stack trace). */
12
+ export class GtfsApiError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "GtfsApiError";
16
+ }
17
+ }
18
+ export class ApiClient {
19
+ config;
20
+ base;
21
+ cache = new Map();
22
+ constructor(config) {
23
+ this.config = config;
24
+ this.base = `${config.gtfsProUrl}/wp-json/wp-gtfs-pro/v1`;
25
+ }
26
+ /** The configured default feed, used when a tool call omits feed_id. */
27
+ get defaultFeedId() {
28
+ return this.config.feedId;
29
+ }
30
+ /**
31
+ * GET a REST path with query params. Empty/undefined params are dropped.
32
+ * Results are cached by full URL for the configured TTL.
33
+ */
34
+ async get(path, params = {}) {
35
+ const url = new URL(this.base + path);
36
+ for (const [key, value] of Object.entries(params)) {
37
+ if (value === undefined || value === "")
38
+ continue;
39
+ url.searchParams.set(key, String(value));
40
+ }
41
+ const href = url.toString();
42
+ const now = Date.now();
43
+ const hit = this.cache.get(href);
44
+ if (hit && hit.expires > now) {
45
+ return hit.data;
46
+ }
47
+ let res;
48
+ try {
49
+ res = await fetch(href, {
50
+ headers: { Accept: "application/json", "User-Agent": "gtfs-pro-mcp" },
51
+ });
52
+ }
53
+ catch {
54
+ // Network-level failure: the WP site is unreachable. Spec §1.6 exact intent.
55
+ throw new GtfsApiError(`The transit data service at ${this.config.gtfsProUrl} is currently ` +
56
+ `unavailable. The rider should check the agency website directly.`);
57
+ }
58
+ const text = await res.text();
59
+ let parsed;
60
+ try {
61
+ parsed = text ? JSON.parse(text) : null;
62
+ }
63
+ catch {
64
+ parsed = null;
65
+ }
66
+ if (!res.ok) {
67
+ throw new GtfsApiError(this.describeError(res.status, parsed));
68
+ }
69
+ const ttlMs = this.config.cacheTtlSeconds * 1000;
70
+ if (ttlMs > 0) {
71
+ this.cache.set(href, { expires: now + ttlMs, data: parsed });
72
+ }
73
+ return parsed;
74
+ }
75
+ /**
76
+ * Convert a non-2xx response into a readable sentence. WordPress REST errors
77
+ * carry `{ code, message }`; surface the message when present.
78
+ */
79
+ describeError(status, body) {
80
+ const wpMessage = body && typeof body === "object" && "message" in body
81
+ ? String(body.message)
82
+ : "";
83
+ if (status === 404) {
84
+ return wpMessage
85
+ ? `Not found: ${wpMessage}`
86
+ : "That transit record was not found. Double-check the stop or route id.";
87
+ }
88
+ if (status === 400) {
89
+ return wpMessage
90
+ ? `Invalid request: ${wpMessage}`
91
+ : "The request was invalid. Check the parameters and try again.";
92
+ }
93
+ if (wpMessage) {
94
+ return `The transit data service returned an error (${status}): ${wpMessage}`;
95
+ }
96
+ return `The transit data service returned an unexpected error (HTTP ${status}).`;
97
+ }
98
+ }
99
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Configuration loader for the GTFS Pro MCP server.
3
+ *
4
+ * Resolution order (later wins where they overlap):
5
+ * 1. A JSON config file — path from `--config <path>`, else $GTFS_PRO_CONFIG,
6
+ * else ./gtfs-pro.config.json if it exists.
7
+ * 2. Environment variables (GTFS_PRO_URL, GTFS_PRO_FEED_ID, ...).
8
+ *
9
+ * Only `gtfs_pro_url` is strictly required. Everything else has a sane default
10
+ * so a single-agency setup can run with just the URL.
11
+ */
12
+ export interface Config {
13
+ /** Base URL of the WordPress site running WP GTFS Pro (no trailing slash). */
14
+ gtfsProUrl: string;
15
+ /** Default feed to use when the AI doesn't specify one. */
16
+ feedId: string;
17
+ /** Local response cache lifetime, in seconds (protects the WP site). */
18
+ cacheTtlSeconds: number;
19
+ /** Human label for the agency this server covers (used in server metadata). */
20
+ agencyName: string;
21
+ /** Free-text description of the agency's service area (used in metadata). */
22
+ agencyDescription: string;
23
+ }
24
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Configuration loader for the GTFS Pro MCP server.
3
+ *
4
+ * Resolution order (later wins where they overlap):
5
+ * 1. A JSON config file — path from `--config <path>`, else $GTFS_PRO_CONFIG,
6
+ * else ./gtfs-pro.config.json if it exists.
7
+ * 2. Environment variables (GTFS_PRO_URL, GTFS_PRO_FEED_ID, ...).
8
+ *
9
+ * Only `gtfs_pro_url` is strictly required. Everything else has a sane default
10
+ * so a single-agency setup can run with just the URL.
11
+ */
12
+ import { readFileSync, existsSync } from "node:fs";
13
+ import { resolve } from "node:path";
14
+ function readConfigFile() {
15
+ // --config <path> takes precedence, then $GTFS_PRO_CONFIG, then a local default.
16
+ const argIdx = process.argv.indexOf("--config");
17
+ const fromArg = argIdx !== -1 && process.argv[argIdx + 1] ? process.argv[argIdx + 1] : undefined;
18
+ const candidate = fromArg ||
19
+ process.env.GTFS_PRO_CONFIG ||
20
+ (existsSync("gtfs-pro.config.json") ? "gtfs-pro.config.json" : undefined);
21
+ if (!candidate)
22
+ return {};
23
+ const path = resolve(candidate);
24
+ if (!existsSync(path)) {
25
+ throw new Error(`Config file not found: ${path}`);
26
+ }
27
+ try {
28
+ return JSON.parse(readFileSync(path, "utf8"));
29
+ }
30
+ catch (err) {
31
+ throw new Error(`Could not parse config file ${path}: ${err.message}`);
32
+ }
33
+ }
34
+ export function loadConfig() {
35
+ const file = readConfigFile();
36
+ const gtfsProUrl = (process.env.GTFS_PRO_URL || file.gtfs_pro_url || "").trim();
37
+ if (!gtfsProUrl) {
38
+ throw new Error("Missing GTFS Pro site URL. Set `gtfs_pro_url` in your config file or the " +
39
+ "GTFS_PRO_URL environment variable (e.g. https://your-agency-site.org).");
40
+ }
41
+ const ttlRaw = process.env.GTFS_PRO_CACHE_TTL ?? file.cache_ttl_seconds;
42
+ const cacheTtlSeconds = Number.isFinite(Number(ttlRaw)) ? Number(ttlRaw) : 30;
43
+ return {
44
+ // Strip any trailing slash so we can concatenate the REST namespace cleanly.
45
+ gtfsProUrl: gtfsProUrl.replace(/\/+$/, ""),
46
+ feedId: (process.env.GTFS_PRO_FEED_ID || file.feed_id || "default").trim(),
47
+ cacheTtlSeconds: cacheTtlSeconds >= 0 ? cacheTtlSeconds : 30,
48
+ agencyName: (process.env.GTFS_PRO_AGENCY_NAME ||
49
+ file.agency_name ||
50
+ "this transit agency").trim(),
51
+ agencyDescription: (process.env.GTFS_PRO_AGENCY_DESCRIPTION ||
52
+ file.agency_description ||
53
+ "").trim(),
54
+ };
55
+ }
56
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuBpC,SAAS,cAAc;IACrB,iFAAiF;IACjF,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,OAAO,GACX,MAAM,KAAK,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACnF,MAAM,SAAS,GACb,OAAO;QACP,OAAO,CAAC,GAAG,CAAC,eAAe;QAC3B,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAE5E,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,CAAC;IAE1B,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAkB,CAAC;IACjE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,+BAA+B,IAAI,KAAM,GAAa,CAAC,OAAO,EAAE,CACjE,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAE9B,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChF,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,2EAA2E;YACzE,wEAAwE,CAC3E,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI,CAAC,iBAAiB,CAAC;IACxE,MAAM,eAAe,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE9E,OAAO;QACL,6EAA6E;QAC7E,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QAC1C,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,IAAI,CAAC,OAAO,IAAI,SAAS,CAAC,CAAC,IAAI,EAAE;QAC1E,eAAe,EAAE,eAAe,IAAI,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE;QAC5D,UAAU,EAAE,CACV,OAAO,CAAC,GAAG,CAAC,oBAAoB;YAChC,IAAI,CAAC,WAAW;YAChB,qBAAqB,CACtB,CAAC,IAAI,EAAE;QACR,iBAAiB,EAAE,CACjB,OAAO,CAAC,GAAG,CAAC,2BAA2B;YACvC,IAAI,CAAC,kBAAkB;YACvB,EAAE,CACH,CAAC,IAAI,EAAE;KACT,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GTFS Pro MCP server — entry point.
4
+ *
5
+ * Loads configuration, builds the API client, registers the nine transit tools,
6
+ * and speaks MCP over stdio (the transport Claude Desktop / ChatGPT desktop use
7
+ * to launch a local server). All diagnostics go to stderr so they never corrupt
8
+ * the stdout JSON-RPC stream.
9
+ */
10
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GTFS Pro MCP server — entry point.
4
+ *
5
+ * Loads configuration, builds the API client, registers the nine transit tools,
6
+ * and speaks MCP over stdio (the transport Claude Desktop / ChatGPT desktop use
7
+ * to launch a local server). All diagnostics go to stderr so they never corrupt
8
+ * the stdout JSON-RPC stream.
9
+ */
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { loadConfig } from "./config.js";
13
+ import { ApiClient } from "./api-client.js";
14
+ import { registerTools } from "./tools.js";
15
+ async function main() {
16
+ const config = loadConfig();
17
+ const api = new ApiClient(config);
18
+ const instructions = `This server provides real-time transit information for ${config.agencyName}.` +
19
+ (config.agencyDescription ? ` ${config.agencyDescription}` : "") +
20
+ ` Use the tools to look up bus stops, schedules, routes, live vehicle positions, ` +
21
+ `and service alerts. Always use the tools for schedule and stop data — never guess ` +
22
+ `times. Note: realtime tools (get_service_alerts, get_live_vehicles) returning an ` +
23
+ `empty result is normal and means "nothing active right now," not "no service."`;
24
+ const server = new McpServer({ name: "gtfs-pro-transit", version: "1.0.0" }, { instructions });
25
+ registerTools(server, api);
26
+ const transport = new StdioServerTransport();
27
+ await server.connect(transport);
28
+ // Visible in the client's MCP logs; harmless on stderr.
29
+ console.error(`gtfs-pro-mcp ready — agency: ${config.agencyName}, site: ${config.gtfsProUrl}, ` +
30
+ `default feed: ${config.feedId}`);
31
+ }
32
+ main().catch((err) => {
33
+ console.error(`gtfs-pro-mcp failed to start: ${err.message}`);
34
+ process.exit(1);
35
+ });
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;IAElC,MAAM,YAAY,GAChB,0DAA0D,MAAM,CAAC,UAAU,GAAG;QAC9E,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,kFAAkF;QAClF,oFAAoF;QACpF,mFAAmF;QACnF,gFAAgF,CAAC;IAEnF,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,EAC9C,EAAE,YAAY,EAAE,CACjB,CAAC;IAEF,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAE3B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,wDAAwD;IACxD,OAAO,CAAC,KAAK,CACX,gCAAgC,MAAM,CAAC,UAAU,WAAW,MAAM,CAAC,UAAU,IAAI;QAC/E,iBAAiB,MAAM,CAAC,MAAM,EAAE,CACnC,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,iCAAkC,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IACzE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The nine GTFS Pro MCP tools. Each wraps one REST endpoint, formats the result
3
+ * as readable text (IDs included so the AI can chain calls), and surfaces failures
4
+ * as prose. Tool descriptions are written for the *model* — they spell out when an
5
+ * empty result is normal so the AI never reads "no alerts" as "no service."
6
+ */
7
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { ApiClient } from "./api-client.js";
9
+ export declare function registerTools(server: McpServer, api: ApiClient): void;
package/dist/tools.js ADDED
@@ -0,0 +1,301 @@
1
+ /**
2
+ * The nine GTFS Pro MCP tools. Each wraps one REST endpoint, formats the result
3
+ * as readable text (IDs included so the AI can chain calls), and surfaces failures
4
+ * as prose. Tool descriptions are written for the *model* — they spell out when an
5
+ * empty result is normal so the AI never reads "no alerts" as "no service."
6
+ */
7
+ import { z } from "zod";
8
+ import { GtfsApiError } from "./api-client.js";
9
+ /* ----------------------------- GTFS-RT enum maps ----------------------------- */
10
+ // https://gtfs.org/realtime/reference/ — surface human labels, never raw integers.
11
+ const CAUSE = {
12
+ 1: "Unknown cause", 2: "Other cause", 3: "Technical problem", 4: "Strike",
13
+ 5: "Demonstration", 6: "Accident", 7: "Holiday", 8: "Weather",
14
+ 9: "Maintenance", 10: "Construction", 11: "Police activity", 12: "Medical emergency",
15
+ };
16
+ const EFFECT = {
17
+ 1: "No service", 2: "Reduced service", 3: "Significant delays", 4: "Detour",
18
+ 5: "Additional service", 6: "Modified service", 7: "Other effect",
19
+ 8: "Unknown effect", 9: "Stop moved", 10: "No effect", 11: "Accessibility issue",
20
+ };
21
+ const SEVERITY = {
22
+ 1: "Unknown severity", 2: "Info", 3: "Warning", 4: "Severe",
23
+ };
24
+ /* --------------------------------- helpers ---------------------------------- */
25
+ const text = (s) => ({ content: [{ type: "text", text: s }] });
26
+ const errText = (s) => ({
27
+ content: [{ type: "text", text: s }],
28
+ isError: true,
29
+ });
30
+ /** Run a tool body, converting GtfsApiError into a visible (isError) tool result. */
31
+ async function guard(run) {
32
+ try {
33
+ return await run();
34
+ }
35
+ catch (err) {
36
+ if (err instanceof GtfsApiError)
37
+ return errText(err.message);
38
+ return errText(`Something went wrong reaching the transit data service: ${err.message}`);
39
+ }
40
+ }
41
+ const fmtCoord = (n) => n.toFixed(5);
42
+ /* --------------------------------- tools ------------------------------------ */
43
+ export function registerTools(server, api) {
44
+ const feedArg = {
45
+ feed_id: z
46
+ .string()
47
+ .optional()
48
+ .describe(`Feed identifier for multi-agency sites. Defaults to "${api.defaultFeedId}". ` +
49
+ `Call list_feeds first if unsure which feeds exist.`),
50
+ };
51
+ const feed = (id) => id || api.defaultFeedId;
52
+ /* 1. list_feeds */
53
+ server.registerTool("list_feeds", {
54
+ title: "List transit feeds",
55
+ description: "List all transit feeds (agencies) configured on this GTFS Pro site. Most " +
56
+ "sites have a single feed. Use this to discover feed_id values when a site " +
57
+ "serves more than one agency.",
58
+ inputSchema: {},
59
+ }, () => guard(async () => {
60
+ const feeds = await api.get("/feeds");
61
+ if (!feeds.length)
62
+ return text("No transit feeds are configured on this site.");
63
+ const lines = feeds.map((f) => `• ${f.name} — feed_id: "${f.id}"${f.active ? "" : " (inactive)"}` +
64
+ (f.url ? `\n GTFS source: ${f.url}` : ""));
65
+ return text(`Configured transit feeds:\n${lines.join("\n")}`);
66
+ }));
67
+ /* 2. find_nearby_stops */
68
+ server.registerTool("find_nearby_stops", {
69
+ title: "Find nearby stops",
70
+ description: "Find the nearest transit stops to a latitude/longitude. Returns each stop " +
71
+ "with its distance, the routes that serve it, and the next scheduled " +
72
+ "departure times per route. If you only have an address or place name, call " +
73
+ "geocode_address first to get coordinates.",
74
+ inputSchema: {
75
+ latitude: z.number().min(-90).max(90).describe("Latitude (-90 to 90)."),
76
+ longitude: z.number().min(-180).max(180).describe("Longitude (-180 to 180)."),
77
+ radius_meters: z
78
+ .number()
79
+ .int()
80
+ .positive()
81
+ .max(5000)
82
+ .optional()
83
+ .describe("Search radius in meters (default 800, max 5000)."),
84
+ limit: z
85
+ .number()
86
+ .int()
87
+ .positive()
88
+ .max(10)
89
+ .optional()
90
+ .describe("Max stops to return (default 5, max 10)."),
91
+ ...feedArg,
92
+ },
93
+ }, (args) => guard(async () => {
94
+ const data = await api.get("/stops/nearby", {
95
+ lat: args.latitude,
96
+ lon: args.longitude,
97
+ radius: args.radius_meters ?? 800,
98
+ limit: args.limit ?? 5,
99
+ feed_id: feed(args.feed_id),
100
+ });
101
+ if (!data.stops?.length) {
102
+ return text("No transit stops were found within the search radius. Try a larger " +
103
+ "radius_meters, or confirm the coordinates are inside the agency's service area.");
104
+ }
105
+ const blocks = data.stops.map((s) => {
106
+ const head = `${s.name} (stop_id: ${s.id}${s.code ? `, code ${s.code}` : ""}) — ${s.distance_text}`;
107
+ const routes = s.routes.length
108
+ ? s.routes
109
+ .map((r) => {
110
+ const next = r.next.length ? r.next.join(", ") : "no upcoming scheduled departures";
111
+ return ` - Route ${r.name} (route_id: ${r.route_id}): ${next}`;
112
+ })
113
+ .join("\n")
114
+ : " - No routes currently scheduled here.";
115
+ return `${head}\n${routes}`;
116
+ });
117
+ return text(`Nearest stops:\n${blocks.join("\n")}`);
118
+ }));
119
+ /* 3. search_stops */
120
+ server.registerTool("search_stops", {
121
+ title: "Search stops by name",
122
+ description: "Search for transit stops by name, landmark, or stop code. Use this when the " +
123
+ "rider names a place (\"Big Bear Village\", \"Goodwin's Market\") rather than " +
124
+ "giving coordinates. Returns matching stops with their stop_id, which you can " +
125
+ "pass to get_stop_departures.",
126
+ inputSchema: {
127
+ query: z.string().min(2).describe("Stop name, landmark, or stop code (min 2 chars)."),
128
+ limit: z.number().int().positive().max(12).optional().describe("Max results (default 6, max 12)."),
129
+ ...feedArg,
130
+ },
131
+ }, (args) => guard(async () => {
132
+ const data = await api.get("/stops/search", {
133
+ q: args.query,
134
+ limit: args.limit ?? 6,
135
+ feed_id: feed(args.feed_id),
136
+ });
137
+ if (!data.stops?.length) {
138
+ return text(`No stops matched "${args.query}". Try a shorter or different term, or use ` +
139
+ `find_nearby_stops with coordinates from geocode_address.`);
140
+ }
141
+ const lines = data.stops.map((s) => `• ${s.label || s.name} — stop_id: ${s.id}` +
142
+ `, location: ${fmtCoord(s.lat)}, ${fmtCoord(s.lon)}`);
143
+ return text(`Matching stops:\n${lines.join("\n")}`);
144
+ }));
145
+ /* 4. get_stop_departures */
146
+ server.registerTool("get_stop_departures", {
147
+ title: "Get stop departures",
148
+ description: "Get the next departures from a specific stop, merged across all routes that " +
149
+ "serve it. Use after finding a stop via find_nearby_stops or search_stops. " +
150
+ "Times are in the agency's local timezone; 'minutes' is minutes from now.",
151
+ inputSchema: {
152
+ stop_id: z.string().min(1).describe("GTFS stop_id (from a stop search/nearby result)."),
153
+ limit: z.number().int().positive().max(20).optional().describe("Max departures (default 8, max 20)."),
154
+ ...feedArg,
155
+ },
156
+ }, (args) => guard(async () => {
157
+ const data = await api.get("/stops/departures", {
158
+ stop_id: args.stop_id,
159
+ limit: args.limit ?? 8,
160
+ feed_id: feed(args.feed_id),
161
+ });
162
+ const stopName = data.stop?.name || `stop ${args.stop_id}`;
163
+ if (!data.departures?.length) {
164
+ return text(`No upcoming departures are scheduled from ${stopName} right now. There may ` +
165
+ `be no more service today — suggest checking the agency website for the full schedule.`);
166
+ }
167
+ const lines = data.departures.map((d) => `• ${d.time} (in ${d.minutes} min) — Route ${d.name}` +
168
+ `${d.headsign ? ` toward ${d.headsign}` : ""} (route_id: ${d.route_id})`);
169
+ return text(`Next departures from ${stopName}:\n${lines.join("\n")}`);
170
+ }));
171
+ /* 5. get_route_map */
172
+ server.registerTool("get_route_map", {
173
+ title: "Get route map data",
174
+ description: "Get geographic data for a single route: its shape (an ordered list of " +
175
+ "lat/lon points forming the path) and the stops it serves. Useful for " +
176
+ "describing where a route goes. The shape can be long; the summary reports " +
177
+ "its size and lists the stops.",
178
+ inputSchema: {
179
+ route_id: z.string().min(1).describe("GTFS route_id."),
180
+ ...feedArg,
181
+ },
182
+ }, (args) => guard(async () => {
183
+ const data = await api.get(`/map/route/${encodeURIComponent(args.route_id)}`, { feed_id: feed(args.feed_id) });
184
+ const stops = data.stops || [];
185
+ if (!stops.length && !(data.shapes || []).length) {
186
+ return text(`No map data was found for route_id "${args.route_id}".`);
187
+ }
188
+ const stopLines = stops.map((s) => ` - ${s.name} (stop_id: ${s.id}) at ${fmtCoord(s.lat)}, ${fmtCoord(s.lon)}`);
189
+ return text(`Route ${args.route_id} has ${stops.length} stops and a path of ` +
190
+ `${(data.shapes || []).length} shape points.\nStops:\n${stopLines.join("\n")}`);
191
+ }));
192
+ /* 6. get_system_map */
193
+ server.registerTool("get_system_map", {
194
+ title: "Get system map",
195
+ description: "Get every route in the transit system with its name, color, and the stops it " +
196
+ "serves. Use when the rider asks what routes exist or wants an overview of the " +
197
+ "network. Shape geometry is omitted from the summary to keep it readable.",
198
+ inputSchema: { ...feedArg },
199
+ }, (args) => guard(async () => {
200
+ const data = await api.get("/map/system", {
201
+ feed_id: feed(args.feed_id),
202
+ });
203
+ if (!data.routes?.length)
204
+ return text("No routes are configured for this feed.");
205
+ const lines = data.routes.map((r) => {
206
+ const label = r.long_name && r.long_name !== r.name ? `${r.name} — ${r.long_name}` : r.name;
207
+ return `• Route ${label} (route_id: ${r.route_id}) — ${r.stops?.length ?? 0} stops`;
208
+ });
209
+ return text(`The system has ${data.routes.length} routes:\n${lines.join("\n")}`);
210
+ }));
211
+ /* 7. get_service_alerts */
212
+ server.registerTool("get_service_alerts", {
213
+ title: "Get service alerts",
214
+ description: "Get active service alerts (delays, detours, cancellations) from GTFS-Realtime. " +
215
+ "IMPORTANT: an empty result is normal and means there are currently NO active " +
216
+ "alerts — it does NOT mean buses aren't running. Many agencies have no alerts " +
217
+ "most of the time, and some have no realtime feed at all.",
218
+ inputSchema: {
219
+ route_id: z.string().optional().describe("Optional: filter alerts to one route_id."),
220
+ ...feedArg,
221
+ },
222
+ }, (args) => guard(async () => {
223
+ const data = await api.get("/rt/alerts", {
224
+ feed_id: feed(args.feed_id),
225
+ route_id: args.route_id,
226
+ });
227
+ const alerts = data.alerts || [];
228
+ if (!alerts.length) {
229
+ return text("There are no active service alerts right now. Normal service is in effect " +
230
+ "(no delays, detours, or cancellations are being reported).");
231
+ }
232
+ const blocks = alerts.map((a) => {
233
+ const tags = [
234
+ a.severity != null ? SEVERITY[a.severity] : null,
235
+ a.effect != null ? EFFECT[a.effect] : null,
236
+ a.cause != null ? CAUSE[a.cause] : null,
237
+ ].filter(Boolean);
238
+ const head = `⚠ ${a.header || "Service alert"}${tags.length ? ` [${tags.join(" · ")}]` : ""}`;
239
+ const body = a.description ? `\n ${a.description}` : "";
240
+ const routes = a.routes?.length ? `\n Affected routes: ${a.routes.join(", ")}` : "";
241
+ const more = a.url ? `\n More info: ${a.url}` : "";
242
+ return head + body + routes + more;
243
+ });
244
+ return text(`Active service alerts:\n${blocks.join("\n\n")}`);
245
+ }));
246
+ /* 8. get_live_vehicles */
247
+ server.registerTool("get_live_vehicles", {
248
+ title: "Get live vehicles",
249
+ description: "Get real-time vehicle positions from GTFS-Realtime: each bus's location, " +
250
+ "heading, the route it's on, and its next stop. IMPORTANT: an empty result is " +
251
+ "normal — it means no vehicles are currently reporting (off-hours, or the " +
252
+ "agency has no realtime vehicle feed). It does NOT imply the schedule is wrong.",
253
+ inputSchema: {
254
+ route_id: z.string().optional().describe("Optional: filter to vehicles on one route_id."),
255
+ ...feedArg,
256
+ },
257
+ }, (args) => guard(async () => {
258
+ const data = await api.get("/rt/vehicles", {
259
+ feed_id: feed(args.feed_id),
260
+ route_id: args.route_id,
261
+ });
262
+ const vehicles = data.vehicles || [];
263
+ if (!vehicles.length) {
264
+ return text("No vehicles are reporting their position right now. This is normal outside " +
265
+ "service hours or for agencies without a realtime vehicle feed; it doesn't " +
266
+ "mean scheduled service isn't running. Use get_stop_departures for the schedule.");
267
+ }
268
+ const lines = vehicles.map((v) => {
269
+ const where = v.next_stop
270
+ ? ` — next stop: ${v.next_stop}${v.next_eta != null ? ` (~${v.next_eta} min)` : ""}`
271
+ : "";
272
+ return (`• Route ${v.route_name || v.route_id} bus ${v.id} at ` +
273
+ `${fmtCoord(v.lat)}, ${fmtCoord(v.lon)}` +
274
+ `${v.bearing != null ? `, heading ${Math.round(v.bearing)}°` : ""}${where}`);
275
+ });
276
+ return text(`Live vehicles (${vehicles.length}):\n${lines.join("\n")}`);
277
+ }));
278
+ /* 9. geocode_address */
279
+ server.registerTool("geocode_address", {
280
+ title: "Geocode an address",
281
+ description: "Convert an address, landmark, or place name into coordinates, biased to the " +
282
+ "agency's service area. Use this to get a latitude/longitude you can then pass " +
283
+ "to find_nearby_stops.",
284
+ inputSchema: {
285
+ address: z.string().min(1).describe("Address, landmark, or place name."),
286
+ ...feedArg,
287
+ },
288
+ }, (args) => guard(async () => {
289
+ const data = await api.get("/geocode", {
290
+ q: args.address,
291
+ feed_id: feed(args.feed_id),
292
+ });
293
+ if (data == null || typeof data.lat !== "number" || typeof data.lon !== "number") {
294
+ return text(`Could not find coordinates for "${args.address}". Try a more specific or ` +
295
+ `nearby place name, or search by stop name with search_stops.`);
296
+ }
297
+ return text(`${data.label || args.address}\nCoordinates: ${fmtCoord(data.lat)}, ${fmtCoord(data.lon)}\n` +
298
+ `Pass these to find_nearby_stops to find the closest stops.`);
299
+ }));
300
+ }
301
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Response shapes for the WP GTFS Pro REST API (`/wp-json/wp-gtfs-pro/v1/*`).
3
+ *
4
+ * These mirror the actual JSON returned by a live GTFS Pro site (verified against
5
+ * a production install), not the idealized shapes in the spec — where they differ,
6
+ * reality wins. Notably: `/stops/search` and `/stops/nearby` wrap results in a
7
+ * `stops` array, and RT vehicles use `id`/`next_eta`/`next_stop` (not the
8
+ * `vehicle_id`/`speed` the spec sketches).
9
+ */
10
+ export interface Feed {
11
+ id: string;
12
+ name: string;
13
+ url: string;
14
+ active: boolean;
15
+ logo?: number;
16
+ rt_vehicles?: string;
17
+ rt_trips?: string;
18
+ rt_alerts?: string;
19
+ }
20
+ /** One route serving a stop, with its next scheduled departure times. */
21
+ export interface RouteAtStop {
22
+ route_id: string;
23
+ name: string;
24
+ color: string;
25
+ url?: string;
26
+ next: string[];
27
+ }
28
+ export interface NearbyStop {
29
+ id: string;
30
+ name: string;
31
+ code: string;
32
+ lat: number;
33
+ lon: number;
34
+ distance_m: number;
35
+ distance_text: string;
36
+ is_timepoint: boolean;
37
+ routes: RouteAtStop[];
38
+ }
39
+ export interface NearbyStopsResponse {
40
+ stops: NearbyStop[];
41
+ }
42
+ export interface SearchStop {
43
+ id: string;
44
+ name: string;
45
+ code: string;
46
+ lat: number;
47
+ lon: number;
48
+ label: string;
49
+ type: string;
50
+ }
51
+ export interface SearchStopsResponse {
52
+ stops: SearchStop[];
53
+ }
54
+ export interface Departure {
55
+ route_id: string;
56
+ name: string;
57
+ color: string;
58
+ url?: string;
59
+ headsign: string;
60
+ time: string;
61
+ minutes: number;
62
+ }
63
+ export interface DeparturesResponse {
64
+ stop: {
65
+ id: string;
66
+ name: string;
67
+ code: string;
68
+ };
69
+ departures: Departure[];
70
+ ts: number;
71
+ }
72
+ export interface MapStop {
73
+ id: string;
74
+ name: string;
75
+ code: string;
76
+ lat: number;
77
+ lon: number;
78
+ }
79
+ /** A shape point is a [latitude, longitude] pair. */
80
+ export type ShapePoint = [number, number];
81
+ export interface RouteMapResponse {
82
+ shapes: ShapePoint[];
83
+ stops: MapStop[];
84
+ }
85
+ export interface SystemRoute {
86
+ route_id: string;
87
+ name: string;
88
+ long_name: string;
89
+ color: string;
90
+ shapes: ShapePoint[];
91
+ stops: MapStop[];
92
+ }
93
+ export interface SystemMapResponse {
94
+ routes: SystemRoute[];
95
+ }
96
+ export interface AlertActivePeriod {
97
+ start: number | null;
98
+ end: number | null;
99
+ }
100
+ export interface Alert {
101
+ id: string;
102
+ header: string;
103
+ description: string;
104
+ url: string;
105
+ /** GTFS-RT Cause enum (integer). Translate before showing to a human. */
106
+ cause: number | null;
107
+ /** GTFS-RT Effect enum (integer). */
108
+ effect: number | null;
109
+ /** GTFS-RT SeverityLevel enum (integer). */
110
+ severity: number | null;
111
+ routes: string[];
112
+ stops: string[];
113
+ active_period: AlertActivePeriod[];
114
+ }
115
+ export interface AlertsResponse {
116
+ alerts: Alert[];
117
+ ts?: number;
118
+ }
119
+ export interface Vehicle {
120
+ id: string;
121
+ lat: number;
122
+ lon: number;
123
+ bearing: number | null;
124
+ route_id: string;
125
+ trip_id: string;
126
+ direction_id: number | null;
127
+ stop_id: string | null;
128
+ status: number | null;
129
+ timestamp: number | null;
130
+ route_name: string;
131
+ color: string;
132
+ direction: string | null;
133
+ next_stop: string | null;
134
+ next_eta: number | null;
135
+ }
136
+ export interface VehiclesResponse {
137
+ vehicles: Vehicle[];
138
+ ts?: number;
139
+ }
140
+ export interface GeocodeResponse {
141
+ lat: number;
142
+ lon: number;
143
+ label: string;
144
+ }
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Response shapes for the WP GTFS Pro REST API (`/wp-json/wp-gtfs-pro/v1/*`).
3
+ *
4
+ * These mirror the actual JSON returned by a live GTFS Pro site (verified against
5
+ * a production install), not the idealized shapes in the spec — where they differ,
6
+ * reality wins. Notably: `/stops/search` and `/stops/nearby` wrap results in a
7
+ * `stops` array, and RT vehicles use `id`/`next_eta`/`next_stop` (not the
8
+ * `vehicle_id`/`speed` the spec sketches).
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -0,0 +1,7 @@
1
+ {
2
+ "gtfs_pro_url": "https://your-agency-site.org",
3
+ "feed_id": "default",
4
+ "cache_ttl_seconds": 30,
5
+ "agency_name": "Your Transit Agency",
6
+ "agency_description": "Public transit serving your service area. Describe the cities, landmarks, and regions covered so the AI knows what this server is for."
7
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "gtfs-pro-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Model Context Protocol server for any WP GTFS Pro transit site. Lets AI assistants query bus stops, schedules, routes, live vehicle positions, and service alerts from an agency's live GTFS data.",
5
+ "license": "GPL-2.0-or-later",
6
+ "author": "Digital Mountaineers",
7
+ "homepage": "https://github.com/digital-mountaineers/gtfs-pro-mcp#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/digital-mountaineers/gtfs-pro-mcp.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/digital-mountaineers/gtfs-pro-mcp/issues"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "gtfs",
19
+ "gtfs-realtime",
20
+ "transit",
21
+ "public-transit",
22
+ "bus",
23
+ "wordpress",
24
+ "claude",
25
+ "ai"
26
+ ],
27
+ "type": "module",
28
+ "bin": {
29
+ "gtfs-pro-mcp": "dist/index.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "gtfs-pro.config.example.json",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "watch": "tsc --watch",
43
+ "start": "node dist/index.js",
44
+ "prepublishOnly": "npm run build"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.29.0",
48
+ "zod": "^3.23.8"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.10.0",
52
+ "typescript": "^5.7.0"
53
+ }
54
+ }