mcp-travelcode 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 +133 -0
- package/build/auth/cli-auth.d.ts +16 -0
- package/build/auth/cli-auth.js +281 -0
- package/build/auth/token-store.d.ts +39 -0
- package/build/auth/token-store.js +113 -0
- package/build/client/api-client.d.ts +42 -0
- package/build/client/api-client.js +235 -0
- package/build/client/types.d.ts +420 -0
- package/build/client/types.js +3 -0
- package/build/config.d.ts +9 -0
- package/build/config.js +30 -0
- package/build/formatters/aerodatabox-formatter.d.ts +6 -0
- package/build/formatters/aerodatabox-formatter.js +246 -0
- package/build/formatters/airline-formatter.d.ts +3 -0
- package/build/formatters/airline-formatter.js +12 -0
- package/build/formatters/airport-formatter.d.ts +4 -0
- package/build/formatters/airport-formatter.js +28 -0
- package/build/formatters/flight-formatter.d.ts +13 -0
- package/build/formatters/flight-formatter.js +100 -0
- package/build/formatters/hotel-formatter.d.ts +4 -0
- package/build/formatters/hotel-formatter.js +55 -0
- package/build/formatters/order-formatter.d.ts +8 -0
- package/build/formatters/order-formatter.js +115 -0
- package/build/http-server.d.ts +16 -0
- package/build/http-server.js +164 -0
- package/build/index.d.ts +3 -0
- package/build/index.js +16 -0
- package/build/polling/flight-poller.d.ts +11 -0
- package/build/polling/flight-poller.js +60 -0
- package/build/server.d.ts +4 -0
- package/build/server.js +54 -0
- package/build/tools/cancel-order.d.ts +9 -0
- package/build/tools/cancel-order.js +24 -0
- package/build/tools/check-order-cancellation.d.ts +8 -0
- package/build/tools/check-order-cancellation.js +22 -0
- package/build/tools/check-order-modification.d.ts +8 -0
- package/build/tools/check-order-modification.js +22 -0
- package/build/tools/create-order.d.ts +75 -0
- package/build/tools/create-order.js +52 -0
- package/build/tools/get-airport-delay-stats.d.ts +9 -0
- package/build/tools/get-airport-delay-stats.js +31 -0
- package/build/tools/get-airport-flights.d.ts +13 -0
- package/build/tools/get-airport-flights.js +78 -0
- package/build/tools/get-airport.d.ts +8 -0
- package/build/tools/get-airport.js +25 -0
- package/build/tools/get-flight-delay-stats.d.ts +8 -0
- package/build/tools/get-flight-delay-stats.js +28 -0
- package/build/tools/get-flight-results.d.ts +16 -0
- package/build/tools/get-flight-results.js +68 -0
- package/build/tools/get-flight-status.d.ts +11 -0
- package/build/tools/get-flight-status.js +42 -0
- package/build/tools/get-hotel-location.d.ts +8 -0
- package/build/tools/get-hotel-location.js +27 -0
- package/build/tools/get-order.d.ts +8 -0
- package/build/tools/get-order.js +22 -0
- package/build/tools/list-orders.d.ts +16 -0
- package/build/tools/list-orders.js +46 -0
- package/build/tools/modify-order.d.ts +10 -0
- package/build/tools/modify-order.js +33 -0
- package/build/tools/search-airlines.d.ts +9 -0
- package/build/tools/search-airlines.js +29 -0
- package/build/tools/search-airports.d.ts +9 -0
- package/build/tools/search-airports.js +30 -0
- package/build/tools/search-flights.d.ts +17 -0
- package/build/tools/search-flights.js +82 -0
- package/build/tools/search-hotel-locations.d.ts +9 -0
- package/build/tools/search-hotel-locations.js +23 -0
- package/build/tools/search-hotels.d.ts +46 -0
- package/build/tools/search-hotels.js +106 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# MCP TravelCode
|
|
2
|
+
|
|
3
|
+
MCP server for the [TravelCode](https://travel-code.com) corporate travel API. Enables AI assistants (Claude Desktop, Cursor, Claude Code) to search flights & hotels, manage bookings, and track flight status.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. Authenticate (opens browser, one-time)
|
|
9
|
+
npx mcp-travelcode-auth auth
|
|
10
|
+
|
|
11
|
+
# 2. Add to Claude Desktop (claude_desktop_config.json):
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"travelcode": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["mcp-travelcode"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 3. Restart Claude Desktop — done!
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Claude Code
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
claude mcp add travelcode -- npx mcp-travelcode
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Tools (19)
|
|
36
|
+
|
|
37
|
+
### Flight Search & Reference Data
|
|
38
|
+
|
|
39
|
+
| Tool | Description |
|
|
40
|
+
|------|-------------|
|
|
41
|
+
| `search_airports` | Find airports by name, city, or IATA code |
|
|
42
|
+
| `get_airport` | Get details for a specific airport |
|
|
43
|
+
| `search_airlines` | Find airlines by name or IATA code |
|
|
44
|
+
| `search_flights` | Search for flights (handles async polling automatically) |
|
|
45
|
+
| `get_flight_results` | Filter/sort/paginate existing search results |
|
|
46
|
+
|
|
47
|
+
### Flight Statistics (AeroDataBox)
|
|
48
|
+
|
|
49
|
+
| Tool | Description |
|
|
50
|
+
|------|-------------|
|
|
51
|
+
| `get_flight_status` | Real-time flight status (delays, gates, terminals, aircraft) |
|
|
52
|
+
| `get_airport_flights` | Airport departure/arrival board for a time window |
|
|
53
|
+
| `get_flight_delay_stats` | Historical delay statistics for a flight number |
|
|
54
|
+
| `get_airport_delay_stats` | Airport delay and cancellation stats for a date |
|
|
55
|
+
|
|
56
|
+
### Hotel Search
|
|
57
|
+
|
|
58
|
+
| Tool | Description |
|
|
59
|
+
|------|-------------|
|
|
60
|
+
| `search_hotel_locations` | Find cities, regions, or hotels by name (returns location IDs) |
|
|
61
|
+
| `get_hotel_location` | Get location details by ID |
|
|
62
|
+
| `search_hotels` | Search hotels with filters (stars, price, meal plan, refundability) via SSE stream |
|
|
63
|
+
|
|
64
|
+
### Order Management
|
|
65
|
+
|
|
66
|
+
| Tool | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| `list_orders` | List orders with filtering and pagination |
|
|
69
|
+
| `get_order` | Get full order details |
|
|
70
|
+
| `create_order` | Book a flight from search results |
|
|
71
|
+
| `check_order_cancellation` | Check cancellation conditions and refund estimate |
|
|
72
|
+
| `cancel_order` | Cancel an order |
|
|
73
|
+
| `check_order_modification` | Check what modifications are allowed |
|
|
74
|
+
| `modify_order` | Modify an order (contacts, passport, rebook, baggage) |
|
|
75
|
+
|
|
76
|
+
## Authentication
|
|
77
|
+
|
|
78
|
+
MCP TravelCode uses OAuth 2.1 with PKCE. No API keys to manage — just sign in with your TravelCode account.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Sign in (opens browser)
|
|
82
|
+
npx mcp-travelcode-auth auth
|
|
83
|
+
|
|
84
|
+
# Check token status
|
|
85
|
+
npx mcp-travelcode-auth status
|
|
86
|
+
|
|
87
|
+
# Sign out
|
|
88
|
+
npx mcp-travelcode-auth logout
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Tokens are stored in `~/.travelcode/tokens.json` and auto-refresh when expired.
|
|
92
|
+
|
|
93
|
+
**Legacy mode:** You can also set `TRAVELCODE_API_TOKEN` environment variable to use a static API token.
|
|
94
|
+
|
|
95
|
+
## Environment Variables
|
|
96
|
+
|
|
97
|
+
| Variable | Required | Default | Description |
|
|
98
|
+
|----------|----------|---------|-------------|
|
|
99
|
+
| `TRAVELCODE_API_TOKEN` | No | — | Static API token (skips OAuth) |
|
|
100
|
+
| `TRAVELCODE_API_BASE_URL` | No | `https://api.travel-code.com/v1` | API base URL |
|
|
101
|
+
| `TRAVELCODE_POLL_INTERVAL_MS` | No | 2000 | Flight search polling interval (ms) |
|
|
102
|
+
| `TRAVELCODE_POLL_TIMEOUT_MS` | No | 90000 | Flight search timeout (ms) |
|
|
103
|
+
|
|
104
|
+
## Example Conversations
|
|
105
|
+
|
|
106
|
+
> "Find hotels in Dubai for April 15-18, 2 adults, 4-5 stars, all inclusive"
|
|
107
|
+
|
|
108
|
+
Uses `search_hotel_locations` → `search_hotels` with star rating and meal plan filters.
|
|
109
|
+
|
|
110
|
+
> "Search flights from London to Barcelona on March 15, economy, 2 adults"
|
|
111
|
+
|
|
112
|
+
Uses `search_airports` → `search_flights` → returns formatted flight options.
|
|
113
|
+
|
|
114
|
+
> "Show my orders" / "Cancel order 12345"
|
|
115
|
+
|
|
116
|
+
Uses `list_orders`, `check_order_cancellation` → `cancel_order`.
|
|
117
|
+
|
|
118
|
+
> "Is flight LO776 on time today?"
|
|
119
|
+
|
|
120
|
+
Uses `get_flight_status` to check real-time status.
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm run dev # Run with tsx (hot reload)
|
|
126
|
+
npm run build # Compile TypeScript
|
|
127
|
+
npm run inspect # Test with MCP Inspector
|
|
128
|
+
npm run start:http # Run HTTP transport (OAuth for browser clients)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI OAuth flow for obtaining TravelCode API tokens.
|
|
4
|
+
*
|
|
5
|
+
* Usage: npx mcp-travelcode auth
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Register client via DCR (if not already registered)
|
|
9
|
+
* 2. Generate PKCE code_verifier + code_challenge
|
|
10
|
+
* 3. Open browser to /oauth/authorize
|
|
11
|
+
* 4. Start local HTTP server to catch redirect callback
|
|
12
|
+
* 5. Exchange code for tokens
|
|
13
|
+
* 6. Save tokens to ~/.travelcode/tokens.json
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=cli-auth.d.ts.map
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI OAuth flow for obtaining TravelCode API tokens.
|
|
4
|
+
*
|
|
5
|
+
* Usage: npx mcp-travelcode auth
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Register client via DCR (if not already registered)
|
|
9
|
+
* 2. Generate PKCE code_verifier + code_challenge
|
|
10
|
+
* 3. Open browser to /oauth/authorize
|
|
11
|
+
* 4. Start local HTTP server to catch redirect callback
|
|
12
|
+
* 5. Exchange code for tokens
|
|
13
|
+
* 6. Save tokens to ~/.travelcode/tokens.json
|
|
14
|
+
*/
|
|
15
|
+
import { createServer } from "node:http";
|
|
16
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
17
|
+
import { URL, URLSearchParams } from "node:url";
|
|
18
|
+
import { loadClient, saveClient, saveTokens, loadTokens, clearTokens, } from "./token-store.js";
|
|
19
|
+
const DEFAULT_ISSUER = "https://travel-code.com";
|
|
20
|
+
const CALLBACK_PORT = 19284; // random-ish port unlikely to conflict
|
|
21
|
+
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
22
|
+
const SCOPES = [
|
|
23
|
+
"flights:search",
|
|
24
|
+
"flights:status",
|
|
25
|
+
"flights:stats",
|
|
26
|
+
"airports:read",
|
|
27
|
+
"airlines:read",
|
|
28
|
+
"orders:read",
|
|
29
|
+
"orders:write",
|
|
30
|
+
].join(" ");
|
|
31
|
+
// --- PKCE ---
|
|
32
|
+
function generateCodeVerifier() {
|
|
33
|
+
return randomBytes(32).toString("base64url");
|
|
34
|
+
}
|
|
35
|
+
function generateCodeChallenge(verifier) {
|
|
36
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
37
|
+
}
|
|
38
|
+
function generateState() {
|
|
39
|
+
return randomBytes(16).toString("base64url");
|
|
40
|
+
}
|
|
41
|
+
// --- DCR ---
|
|
42
|
+
async function registerClient(issuer) {
|
|
43
|
+
const existing = await loadClient(issuer);
|
|
44
|
+
if (existing) {
|
|
45
|
+
console.log(`Using existing client: ${existing.client_id}`);
|
|
46
|
+
return existing;
|
|
47
|
+
}
|
|
48
|
+
console.log("Registering OAuth client...");
|
|
49
|
+
const response = await fetch(`${issuer}/oauth/register`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
client_name: "MCP TravelCode CLI",
|
|
54
|
+
redirect_uris: [REDIRECT_URI],
|
|
55
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
56
|
+
response_types: ["code"],
|
|
57
|
+
token_endpoint_auth_method: "none",
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const text = await response.text();
|
|
62
|
+
throw new Error(`Client registration failed (${response.status}): ${text}`);
|
|
63
|
+
}
|
|
64
|
+
const data = (await response.json());
|
|
65
|
+
const client = {
|
|
66
|
+
client_id: data.client_id,
|
|
67
|
+
client_name: data.client_name,
|
|
68
|
+
redirect_uris: data.redirect_uris,
|
|
69
|
+
issuer,
|
|
70
|
+
};
|
|
71
|
+
await saveClient(client);
|
|
72
|
+
console.log(`Client registered: ${client.client_id}`);
|
|
73
|
+
return client;
|
|
74
|
+
}
|
|
75
|
+
// --- Token exchange ---
|
|
76
|
+
async function exchangeCodeForTokens(issuer, clientId, code, codeVerifier) {
|
|
77
|
+
const response = await fetch(`${issuer}/oauth/token`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
80
|
+
body: new URLSearchParams({
|
|
81
|
+
grant_type: "authorization_code",
|
|
82
|
+
code,
|
|
83
|
+
redirect_uri: REDIRECT_URI,
|
|
84
|
+
client_id: clientId,
|
|
85
|
+
code_verifier: codeVerifier,
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const text = await response.text();
|
|
90
|
+
throw new Error(`Token exchange failed (${response.status}): ${text}`);
|
|
91
|
+
}
|
|
92
|
+
const data = (await response.json());
|
|
93
|
+
const now = Math.floor(Date.now() / 1000);
|
|
94
|
+
return {
|
|
95
|
+
access_token: data.access_token,
|
|
96
|
+
refresh_token: data.refresh_token,
|
|
97
|
+
expires_at: now + data.expires_in,
|
|
98
|
+
scope: data.scope,
|
|
99
|
+
client_id: clientId,
|
|
100
|
+
issuer,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// --- Open browser ---
|
|
104
|
+
async function openBrowser(url) {
|
|
105
|
+
const { platform } = await import("node:os");
|
|
106
|
+
const { exec } = await import("node:child_process");
|
|
107
|
+
const os = platform();
|
|
108
|
+
let command;
|
|
109
|
+
if (os === "darwin") {
|
|
110
|
+
command = `open "${url}"`;
|
|
111
|
+
}
|
|
112
|
+
else if (os === "win32") {
|
|
113
|
+
command = `start "${url}"`;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
command = `xdg-open "${url}"`;
|
|
117
|
+
}
|
|
118
|
+
exec(command, (error) => {
|
|
119
|
+
if (error) {
|
|
120
|
+
console.log(`\nCould not open browser automatically. Please open this URL manually:\n${url}\n`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// --- Callback server ---
|
|
125
|
+
function waitForCallback(expectedState) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const timeout = setTimeout(() => {
|
|
128
|
+
server.close();
|
|
129
|
+
reject(new Error("Authorization timed out (5 minutes). Please try again."));
|
|
130
|
+
}, 5 * 60 * 1000);
|
|
131
|
+
const server = createServer((req, res) => {
|
|
132
|
+
const url = new URL(req.url || "/", `http://localhost:${CALLBACK_PORT}`);
|
|
133
|
+
if (url.pathname !== "/callback") {
|
|
134
|
+
res.writeHead(404);
|
|
135
|
+
res.end("Not found");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const error = url.searchParams.get("error");
|
|
139
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
140
|
+
const code = url.searchParams.get("code");
|
|
141
|
+
const state = url.searchParams.get("state");
|
|
142
|
+
if (error) {
|
|
143
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
144
|
+
res.end(`
|
|
145
|
+
<html><body style="font-family: sans-serif; text-align: center; margin-top: 50px;">
|
|
146
|
+
<h2>Authorization failed</h2>
|
|
147
|
+
<p>${errorDescription || error}</p>
|
|
148
|
+
<p>You can close this window.</p>
|
|
149
|
+
</body></html>
|
|
150
|
+
`);
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
server.close();
|
|
153
|
+
reject(new Error(`Authorization denied: ${errorDescription || error}`));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!code || state !== expectedState) {
|
|
157
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
158
|
+
res.end(`
|
|
159
|
+
<html><body style="font-family: sans-serif; text-align: center; margin-top: 50px;">
|
|
160
|
+
<h2>Invalid callback</h2>
|
|
161
|
+
<p>Missing code or state mismatch.</p>
|
|
162
|
+
</body></html>
|
|
163
|
+
`);
|
|
164
|
+
clearTimeout(timeout);
|
|
165
|
+
server.close();
|
|
166
|
+
reject(new Error("Invalid callback: missing code or state mismatch"));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
170
|
+
res.end(`
|
|
171
|
+
<html><body style="font-family: sans-serif; text-align: center; margin-top: 50px;">
|
|
172
|
+
<h2>Authorization successful!</h2>
|
|
173
|
+
<p>You can close this window and return to the terminal.</p>
|
|
174
|
+
</body></html>
|
|
175
|
+
`);
|
|
176
|
+
clearTimeout(timeout);
|
|
177
|
+
server.close();
|
|
178
|
+
resolve({ code });
|
|
179
|
+
});
|
|
180
|
+
server.listen(CALLBACK_PORT, () => {
|
|
181
|
+
// server ready
|
|
182
|
+
});
|
|
183
|
+
server.on("error", (err) => {
|
|
184
|
+
clearTimeout(timeout);
|
|
185
|
+
reject(new Error(`Failed to start callback server on port ${CALLBACK_PORT}: ${err.message}`));
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// --- Main ---
|
|
190
|
+
async function authCommand() {
|
|
191
|
+
const issuer = process.env.OAUTH_ISSUER || DEFAULT_ISSUER;
|
|
192
|
+
console.log("TravelCode OAuth Authorization");
|
|
193
|
+
console.log(`Issuer: ${issuer}`);
|
|
194
|
+
console.log("");
|
|
195
|
+
// 1. Register client (or reuse existing)
|
|
196
|
+
const client = await registerClient(issuer);
|
|
197
|
+
// 2. Generate PKCE
|
|
198
|
+
const codeVerifier = generateCodeVerifier();
|
|
199
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
200
|
+
const state = generateState();
|
|
201
|
+
// 3. Build authorization URL
|
|
202
|
+
const authUrl = new URL(`${issuer}/oauth/authorize`);
|
|
203
|
+
authUrl.searchParams.set("response_type", "code");
|
|
204
|
+
authUrl.searchParams.set("client_id", client.client_id);
|
|
205
|
+
authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
206
|
+
authUrl.searchParams.set("scope", SCOPES);
|
|
207
|
+
authUrl.searchParams.set("state", state);
|
|
208
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
209
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
210
|
+
// 4. Start callback server and open browser
|
|
211
|
+
console.log("Opening browser for authorization...");
|
|
212
|
+
const callbackPromise = waitForCallback(state);
|
|
213
|
+
await openBrowser(authUrl.toString());
|
|
214
|
+
console.log("Waiting for authorization...\n");
|
|
215
|
+
// 5. Wait for callback
|
|
216
|
+
const { code } = await callbackPromise;
|
|
217
|
+
console.log("Authorization code received. Exchanging for tokens...");
|
|
218
|
+
// 6. Exchange code for tokens
|
|
219
|
+
const tokens = await exchangeCodeForTokens(issuer, client.client_id, code, codeVerifier);
|
|
220
|
+
await saveTokens(tokens);
|
|
221
|
+
console.log("");
|
|
222
|
+
console.log("Authentication successful!");
|
|
223
|
+
console.log(` Access token expires: ${new Date(tokens.expires_at * 1000).toLocaleString()}`);
|
|
224
|
+
console.log(` Scopes: ${tokens.scope}`);
|
|
225
|
+
console.log(` Tokens saved to: ~/.travelcode/tokens.json`);
|
|
226
|
+
console.log("");
|
|
227
|
+
console.log("You can now use the MCP server. It will automatically use the saved token.");
|
|
228
|
+
}
|
|
229
|
+
async function statusCommand() {
|
|
230
|
+
const tokens = await loadTokens();
|
|
231
|
+
if (!tokens) {
|
|
232
|
+
console.log("Not authenticated. Run: npx mcp-travelcode auth");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const now = Math.floor(Date.now() / 1000);
|
|
236
|
+
const expired = tokens.expires_at <= now;
|
|
237
|
+
const expiresIn = tokens.expires_at - now;
|
|
238
|
+
console.log("TravelCode Auth Status");
|
|
239
|
+
console.log(` Issuer: ${tokens.issuer}`);
|
|
240
|
+
console.log(` Client ID: ${tokens.client_id}`);
|
|
241
|
+
console.log(` Scopes: ${tokens.scope}`);
|
|
242
|
+
console.log(` Expires: ${new Date(tokens.expires_at * 1000).toLocaleString()}`);
|
|
243
|
+
console.log(` Status: ${expired ? "EXPIRED" : `valid (${Math.floor(expiresIn / 60)} min left)`}`);
|
|
244
|
+
console.log(` Has refresh token: ${tokens.refresh_token ? "yes" : "no"}`);
|
|
245
|
+
}
|
|
246
|
+
async function logoutCommand() {
|
|
247
|
+
await clearTokens();
|
|
248
|
+
console.log("Tokens cleared. Run 'npx mcp-travelcode auth' to re-authenticate.");
|
|
249
|
+
}
|
|
250
|
+
// --- Entry point ---
|
|
251
|
+
const command = process.argv[2];
|
|
252
|
+
switch (command) {
|
|
253
|
+
case "auth":
|
|
254
|
+
case "login":
|
|
255
|
+
authCommand().catch((err) => {
|
|
256
|
+
console.error("Auth error:", err.message);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
});
|
|
259
|
+
break;
|
|
260
|
+
case "status":
|
|
261
|
+
statusCommand().catch((err) => {
|
|
262
|
+
console.error("Error:", err.message);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
});
|
|
265
|
+
break;
|
|
266
|
+
case "logout":
|
|
267
|
+
logoutCommand().catch((err) => {
|
|
268
|
+
console.error("Error:", err.message);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
271
|
+
break;
|
|
272
|
+
default:
|
|
273
|
+
console.log("Usage: mcp-travelcode <command>");
|
|
274
|
+
console.log("");
|
|
275
|
+
console.log("Commands:");
|
|
276
|
+
console.log(" auth Authenticate with TravelCode (opens browser)");
|
|
277
|
+
console.log(" status Show current auth status");
|
|
278
|
+
console.log(" logout Clear saved tokens");
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=cli-auth.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token storage for CLI-obtained OAuth tokens.
|
|
3
|
+
*
|
|
4
|
+
* Tokens are stored in ~/.travelcode/tokens.json.
|
|
5
|
+
* Format:
|
|
6
|
+
* {
|
|
7
|
+
* "access_token": "...",
|
|
8
|
+
* "refresh_token": "...",
|
|
9
|
+
* "expires_at": 1234567890,
|
|
10
|
+
* "scope": "flights:search airports:read ...",
|
|
11
|
+
* "client_id": "...",
|
|
12
|
+
* "issuer": "https://api.travel-code.com"
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
export interface StoredTokens {
|
|
16
|
+
access_token: string;
|
|
17
|
+
refresh_token: string;
|
|
18
|
+
expires_at: number;
|
|
19
|
+
scope: string;
|
|
20
|
+
client_id: string;
|
|
21
|
+
issuer: string;
|
|
22
|
+
}
|
|
23
|
+
export interface StoredClient {
|
|
24
|
+
client_id: string;
|
|
25
|
+
client_name: string;
|
|
26
|
+
redirect_uris: string[];
|
|
27
|
+
issuer: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function loadTokens(): Promise<StoredTokens | null>;
|
|
30
|
+
export declare function saveTokens(tokens: StoredTokens): Promise<void>;
|
|
31
|
+
export declare function clearTokens(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Returns a valid access token, refreshing if expired.
|
|
34
|
+
* Returns null if no tokens stored or refresh fails.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getValidToken(issuer: string): Promise<string | null>;
|
|
37
|
+
export declare function loadClient(issuer: string): Promise<StoredClient | null>;
|
|
38
|
+
export declare function saveClient(client: StoredClient): Promise<void>;
|
|
39
|
+
//# sourceMappingURL=token-store.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token storage for CLI-obtained OAuth tokens.
|
|
3
|
+
*
|
|
4
|
+
* Tokens are stored in ~/.travelcode/tokens.json.
|
|
5
|
+
* Format:
|
|
6
|
+
* {
|
|
7
|
+
* "access_token": "...",
|
|
8
|
+
* "refresh_token": "...",
|
|
9
|
+
* "expires_at": 1234567890,
|
|
10
|
+
* "scope": "flights:search airports:read ...",
|
|
11
|
+
* "client_id": "...",
|
|
12
|
+
* "issuer": "https://api.travel-code.com"
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
const TOKEN_DIR = join(homedir(), ".travelcode");
|
|
19
|
+
const TOKEN_FILE = join(TOKEN_DIR, "tokens.json");
|
|
20
|
+
const CLIENT_FILE = join(TOKEN_DIR, "client.json");
|
|
21
|
+
async function ensureDir() {
|
|
22
|
+
await mkdir(TOKEN_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
// --- Tokens ---
|
|
25
|
+
export async function loadTokens() {
|
|
26
|
+
try {
|
|
27
|
+
const data = await readFile(TOKEN_FILE, "utf-8");
|
|
28
|
+
return JSON.parse(data);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function saveTokens(tokens) {
|
|
35
|
+
await ensureDir();
|
|
36
|
+
await writeFile(TOKEN_FILE, JSON.stringify(tokens, null, 2), "utf-8");
|
|
37
|
+
}
|
|
38
|
+
export async function clearTokens() {
|
|
39
|
+
try {
|
|
40
|
+
const { unlink } = await import("node:fs/promises");
|
|
41
|
+
await unlink(TOKEN_FILE);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore if file doesn't exist
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns a valid access token, refreshing if expired.
|
|
49
|
+
* Returns null if no tokens stored or refresh fails.
|
|
50
|
+
*/
|
|
51
|
+
export async function getValidToken(issuer) {
|
|
52
|
+
const tokens = await loadTokens();
|
|
53
|
+
if (!tokens)
|
|
54
|
+
return null;
|
|
55
|
+
if (tokens.issuer !== issuer)
|
|
56
|
+
return null;
|
|
57
|
+
const now = Math.floor(Date.now() / 1000);
|
|
58
|
+
// Token still valid (with 60s buffer)
|
|
59
|
+
if (tokens.expires_at > now + 60) {
|
|
60
|
+
return tokens.access_token;
|
|
61
|
+
}
|
|
62
|
+
// Try to refresh
|
|
63
|
+
if (!tokens.refresh_token || !tokens.client_id)
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(`${issuer}/oauth/token`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
69
|
+
body: new URLSearchParams({
|
|
70
|
+
grant_type: "refresh_token",
|
|
71
|
+
refresh_token: tokens.refresh_token,
|
|
72
|
+
client_id: tokens.client_id,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
console.error("Token refresh failed, please re-authenticate: npx mcp-travelcode auth");
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const data = (await response.json());
|
|
80
|
+
const refreshedTokens = {
|
|
81
|
+
access_token: data.access_token,
|
|
82
|
+
refresh_token: data.refresh_token,
|
|
83
|
+
expires_at: now + data.expires_in,
|
|
84
|
+
scope: data.scope,
|
|
85
|
+
client_id: tokens.client_id,
|
|
86
|
+
issuer: tokens.issuer,
|
|
87
|
+
};
|
|
88
|
+
await saveTokens(refreshedTokens);
|
|
89
|
+
return refreshedTokens.access_token;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error("Token refresh error:", error.message);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// --- Client registration ---
|
|
97
|
+
export async function loadClient(issuer) {
|
|
98
|
+
try {
|
|
99
|
+
const data = await readFile(CLIENT_FILE, "utf-8");
|
|
100
|
+
const client = JSON.parse(data);
|
|
101
|
+
if (client.issuer !== issuer)
|
|
102
|
+
return null;
|
|
103
|
+
return client;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function saveClient(client) {
|
|
110
|
+
await ensureDir();
|
|
111
|
+
await writeFile(CLIENT_FILE, JSON.stringify(client, null, 2), "utf-8");
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TravelCodeConfig } from "../config.js";
|
|
2
|
+
export declare class TravelCodeAuthError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class TravelCodeNotFoundError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class TravelCodeValidationError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class TravelCodeServerError extends Error {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class TravelCodeApiClient {
|
|
15
|
+
private baseUrl;
|
|
16
|
+
private token;
|
|
17
|
+
private issuer;
|
|
18
|
+
constructor(config: TravelCodeConfig);
|
|
19
|
+
/**
|
|
20
|
+
* Ensures the token is still valid, refreshing via OAuth if needed.
|
|
21
|
+
* Falls back to the current token if refresh is not available.
|
|
22
|
+
*/
|
|
23
|
+
private ensureValidToken;
|
|
24
|
+
get<T>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
25
|
+
post<T>(path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
|
|
26
|
+
getAerodatabox<T>(aerodataboxPath: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
27
|
+
/**
|
|
28
|
+
* POST request that returns an SSE stream.
|
|
29
|
+
* Collects events and returns them as an array of {event, data} objects.
|
|
30
|
+
*/
|
|
31
|
+
postSSE(path: string, body: Record<string, unknown>, timeoutMs?: number): Promise<Array<{
|
|
32
|
+
event: string;
|
|
33
|
+
data: unknown;
|
|
34
|
+
}>>;
|
|
35
|
+
/**
|
|
36
|
+
* GET with accessToken as query parameter (used by hotel location endpoints).
|
|
37
|
+
*/
|
|
38
|
+
getWithTokenParam<T>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
39
|
+
private headers;
|
|
40
|
+
private handleResponse;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=api-client.d.ts.map
|