mcp-travelcode 1.0.2 → 1.0.3
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/build/client/api-client.d.ts +4 -0
- package/build/client/api-client.js +17 -0
- package/build/client/types.d.ts +65 -0
- package/build/formatters/hotel-formatter.d.ts +2 -1
- package/build/formatters/hotel-formatter.js +46 -0
- package/build/server.js +2 -0
- package/build/tools/get-hotel-offers.d.ts +25 -0
- package/build/tools/get-hotel-offers.js +49 -0
- package/build/tools/search-hotels.js +1 -1
- package/package.json +1 -1
|
@@ -36,6 +36,10 @@ export declare class TravelCodeApiClient {
|
|
|
36
36
|
* GET with accessToken as query parameter (used by hotel location endpoints).
|
|
37
37
|
*/
|
|
38
38
|
getWithTokenParam<T>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
39
|
+
/**
|
|
40
|
+
* POST with accessToken in body (used by hotel offers endpoint).
|
|
41
|
+
*/
|
|
42
|
+
postWithTokenParam<T>(path: string, body: Record<string, unknown>): Promise<T>;
|
|
39
43
|
private headers;
|
|
40
44
|
private handleResponse;
|
|
41
45
|
}
|
|
@@ -197,6 +197,23 @@ export class TravelCodeApiClient {
|
|
|
197
197
|
});
|
|
198
198
|
return this.handleResponse(response);
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* POST with accessToken in body (used by hotel offers endpoint).
|
|
202
|
+
*/
|
|
203
|
+
async postWithTokenParam(path, body) {
|
|
204
|
+
await this.ensureValidToken();
|
|
205
|
+
const bodyWithToken = { ...body, accessToken: this.token };
|
|
206
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
"X-Source": "mcp-server",
|
|
211
|
+
Accept: "application/json",
|
|
212
|
+
},
|
|
213
|
+
body: JSON.stringify(bodyWithToken),
|
|
214
|
+
});
|
|
215
|
+
return this.handleResponse(response);
|
|
216
|
+
}
|
|
200
217
|
headers() {
|
|
201
218
|
return {
|
|
202
219
|
Authorization: `Bearer ${this.token}`,
|
package/build/client/types.d.ts
CHANGED
|
@@ -412,6 +412,71 @@ export interface HotelSSECompleted {
|
|
|
412
412
|
hotels: HotelOffer[];
|
|
413
413
|
cacheKey: string;
|
|
414
414
|
}
|
|
415
|
+
export interface HotelOfferPrice {
|
|
416
|
+
currency: string;
|
|
417
|
+
net: number;
|
|
418
|
+
gross: number;
|
|
419
|
+
total: number;
|
|
420
|
+
markup: number;
|
|
421
|
+
nights: number;
|
|
422
|
+
rooms: number;
|
|
423
|
+
nightly: number;
|
|
424
|
+
extra?: number;
|
|
425
|
+
totalWithExtra?: number;
|
|
426
|
+
deposit?: number | null;
|
|
427
|
+
}
|
|
428
|
+
export interface HotelOfferCancelPolicy {
|
|
429
|
+
refundable: boolean;
|
|
430
|
+
title: string;
|
|
431
|
+
description?: string;
|
|
432
|
+
fullyRefundable: boolean;
|
|
433
|
+
}
|
|
434
|
+
export interface HotelOfferRoom {
|
|
435
|
+
occupancyRefId: number;
|
|
436
|
+
code: string;
|
|
437
|
+
description: string;
|
|
438
|
+
}
|
|
439
|
+
export interface HotelOfferRate {
|
|
440
|
+
partnerId: number;
|
|
441
|
+
boardName: string;
|
|
442
|
+
price: HotelOfferPrice;
|
|
443
|
+
cancelPolicy: HotelOfferCancelPolicy;
|
|
444
|
+
rooms: HotelOfferRoom[];
|
|
445
|
+
externalId: string;
|
|
446
|
+
quoteKey: string;
|
|
447
|
+
}
|
|
448
|
+
export interface HotelOfferRoomGroup {
|
|
449
|
+
content: {
|
|
450
|
+
area?: string | null;
|
|
451
|
+
views?: string | null;
|
|
452
|
+
photos?: string[];
|
|
453
|
+
};
|
|
454
|
+
rates: HotelOfferRate[];
|
|
455
|
+
}
|
|
456
|
+
export interface HotelPropertyDescription {
|
|
457
|
+
title: string;
|
|
458
|
+
text: string;
|
|
459
|
+
}
|
|
460
|
+
export interface HotelProperty {
|
|
461
|
+
id?: string;
|
|
462
|
+
gId?: number;
|
|
463
|
+
name: string;
|
|
464
|
+
starRating?: number;
|
|
465
|
+
address?: string;
|
|
466
|
+
heroImage?: string;
|
|
467
|
+
images?: Array<{
|
|
468
|
+
url: string;
|
|
469
|
+
}>;
|
|
470
|
+
description?: HotelPropertyDescription[];
|
|
471
|
+
latitude?: number;
|
|
472
|
+
longitude?: number;
|
|
473
|
+
}
|
|
474
|
+
export interface HotelOffersResponse {
|
|
475
|
+
offersKey: string;
|
|
476
|
+
property: HotelProperty;
|
|
477
|
+
offers: Record<string, HotelOfferRoomGroup>;
|
|
478
|
+
bronevikId?: number;
|
|
479
|
+
}
|
|
415
480
|
export interface ApiErrorResponse {
|
|
416
481
|
code: number;
|
|
417
482
|
message?: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { HotelLocationSearchResponse, HotelOffer } from "../client/types.js";
|
|
1
|
+
import { HotelLocationSearchResponse, HotelOffer, HotelOffersResponse } from "../client/types.js";
|
|
2
2
|
export declare function formatHotelLocations(data: HotelLocationSearchResponse): string;
|
|
3
3
|
export declare function formatHotelResults(hotels: HotelOffer[], totalCount: number): string;
|
|
4
|
+
export declare function formatHotelOffers(data: HotelOffersResponse): string;
|
|
4
5
|
//# sourceMappingURL=hotel-formatter.d.ts.map
|
|
@@ -52,4 +52,50 @@ export function formatHotelResults(hotels, totalCount) {
|
|
|
52
52
|
}
|
|
53
53
|
return lines.join("\n");
|
|
54
54
|
}
|
|
55
|
+
export function formatHotelOffers(data) {
|
|
56
|
+
const prop = data.property;
|
|
57
|
+
const stars = prop.starRating ? "★".repeat(prop.starRating) : "";
|
|
58
|
+
const lines = [
|
|
59
|
+
`${stars} ${prop.name}`,
|
|
60
|
+
prop.address ? `Address: ${prop.address}` : "",
|
|
61
|
+
].filter(Boolean);
|
|
62
|
+
// Descriptions
|
|
63
|
+
if (prop.description && prop.description.length > 0) {
|
|
64
|
+
for (const desc of prop.description.slice(0, 2)) {
|
|
65
|
+
const text = desc.text.length > 200 ? desc.text.slice(0, 200) + "..." : desc.text;
|
|
66
|
+
lines.push(`${desc.title}: ${text}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const roomGroups = Object.entries(data.offers);
|
|
70
|
+
let totalRates = 0;
|
|
71
|
+
for (const [, group] of roomGroups) {
|
|
72
|
+
totalRates += group.rates.length;
|
|
73
|
+
}
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push(`${roomGroups.length} room types, ${totalRates} rates total:`);
|
|
76
|
+
lines.push("");
|
|
77
|
+
for (const [roomName, group] of roomGroups) {
|
|
78
|
+
const cheapest = group.rates.reduce((min, r) => (r.price.nightly < min.price.nightly ? r : min), group.rates[0]);
|
|
79
|
+
if (!cheapest)
|
|
80
|
+
continue;
|
|
81
|
+
const refundable = group.rates.some((r) => r.cancelPolicy.refundable);
|
|
82
|
+
const boards = [...new Set(group.rates.map((r) => r.boardName))].join(", ");
|
|
83
|
+
lines.push(`--- ${roomName} (${group.rates.length} offers) ---`);
|
|
84
|
+
lines.push(` From: ${cheapest.price.nightly} ${cheapest.price.currency}/night (total: ${cheapest.price.total} for ${cheapest.price.nights} night(s))`);
|
|
85
|
+
lines.push(` Meal options: ${boards}`);
|
|
86
|
+
lines.push(` ${refundable ? "Refundable options available" : "Non-refundable"}`);
|
|
87
|
+
// Show top 3 rates
|
|
88
|
+
const sorted = [...group.rates].sort((a, b) => a.price.nightly - b.price.nightly);
|
|
89
|
+
for (const rate of sorted.slice(0, 3)) {
|
|
90
|
+
const cancel = rate.cancelPolicy.refundable ? "Refundable" : "Non-refundable";
|
|
91
|
+
lines.push(` ${rate.price.nightly} ${rate.price.currency}/night | ${rate.boardName} | ${cancel}`);
|
|
92
|
+
}
|
|
93
|
+
if (sorted.length > 3) {
|
|
94
|
+
lines.push(` ... and ${sorted.length - 3} more offers`);
|
|
95
|
+
}
|
|
96
|
+
lines.push("");
|
|
97
|
+
}
|
|
98
|
+
lines.push(`offersKey: ${data.offersKey}`);
|
|
99
|
+
return lines.join("\n");
|
|
100
|
+
}
|
|
55
101
|
//# sourceMappingURL=hotel-formatter.js.map
|
package/build/server.js
CHANGED
|
@@ -19,6 +19,7 @@ import { registerModifyOrder } from "./tools/modify-order.js";
|
|
|
19
19
|
import { registerSearchHotelLocations } from "./tools/search-hotel-locations.js";
|
|
20
20
|
import { registerGetHotelLocation } from "./tools/get-hotel-location.js";
|
|
21
21
|
import { registerSearchHotels } from "./tools/search-hotels.js";
|
|
22
|
+
import { registerGetHotelOffers } from "./tools/get-hotel-offers.js";
|
|
22
23
|
export function createServer(config) {
|
|
23
24
|
const server = new McpServer({
|
|
24
25
|
name: "TravelCode",
|
|
@@ -49,6 +50,7 @@ export function createServer(config) {
|
|
|
49
50
|
registerSearchHotelLocations(server, client);
|
|
50
51
|
registerGetHotelLocation(server, client);
|
|
51
52
|
registerSearchHotels(server, client);
|
|
53
|
+
registerGetHotelOffers(server, client);
|
|
52
54
|
return server;
|
|
53
55
|
}
|
|
54
56
|
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { TravelCodeApiClient } from "../client/api-client.js";
|
|
4
|
+
export declare const getHotelOffersSchema: {
|
|
5
|
+
id: z.ZodNumber;
|
|
6
|
+
checkin: z.ZodString;
|
|
7
|
+
checkout: z.ZodString;
|
|
8
|
+
country_code: z.ZodString;
|
|
9
|
+
guests: z.ZodArray<z.ZodObject<{
|
|
10
|
+
adults: z.ZodNumber;
|
|
11
|
+
children: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
childrenAges: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
adults: number;
|
|
15
|
+
children?: number | undefined;
|
|
16
|
+
childrenAges?: number[] | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
adults: number;
|
|
19
|
+
children?: number | undefined;
|
|
20
|
+
childrenAges?: number[] | undefined;
|
|
21
|
+
}>, "many">;
|
|
22
|
+
location: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
};
|
|
24
|
+
export declare function registerGetHotelOffers(server: McpServer, client: TravelCodeApiClient): void;
|
|
25
|
+
//# sourceMappingURL=get-hotel-offers.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatHotelOffers } from "../formatters/hotel-formatter.js";
|
|
3
|
+
const guestSchema = z.object({
|
|
4
|
+
adults: z.number().int().min(1).max(4).describe("Number of adults (1-4)"),
|
|
5
|
+
children: z.number().int().min(0).max(3).optional().describe("Number of children (0-3)"),
|
|
6
|
+
childrenAges: z
|
|
7
|
+
.array(z.number().int().min(0).max(17))
|
|
8
|
+
.optional()
|
|
9
|
+
.describe("Array of child ages (0-17), required if children > 0"),
|
|
10
|
+
});
|
|
11
|
+
export const getHotelOffersSchema = {
|
|
12
|
+
id: z.number().int().describe("Hotel giataId (from search_hotels results)"),
|
|
13
|
+
checkin: z.string().describe("Check-in date (YYYY-MM-DD)"),
|
|
14
|
+
checkout: z.string().describe("Check-out date (YYYY-MM-DD)"),
|
|
15
|
+
country_code: z.string().describe("Guest nationality ISO code (e.g. BY, RU, US)"),
|
|
16
|
+
guests: z.array(guestSchema).min(1).describe("Array of rooms, each with adults count and optional children"),
|
|
17
|
+
location: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Location ID from prior search — enables cache reuse for faster results"),
|
|
22
|
+
};
|
|
23
|
+
export function registerGetHotelOffers(server, client) {
|
|
24
|
+
server.tool("get_hotel_offers", "Get all available rooms and rates for a specific hotel from all suppliers. Returns room types, prices, meal plans, and cancellation policies. The hotel giataId comes from search_hotels results — do not explain this to the user, just chain the calls silently.", getHotelOffersSchema, async ({ id, checkin, checkout, country_code, guests, location }) => {
|
|
25
|
+
try {
|
|
26
|
+
const body = {
|
|
27
|
+
id,
|
|
28
|
+
checkin,
|
|
29
|
+
checkout,
|
|
30
|
+
countryCode: country_code,
|
|
31
|
+
guests,
|
|
32
|
+
};
|
|
33
|
+
if (location !== undefined) {
|
|
34
|
+
body.location = location;
|
|
35
|
+
}
|
|
36
|
+
const data = await client.postWithTokenParam("/search/hotels/offers", body);
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: formatHotelOffers(data) }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: `Error getting hotel offers: ${error.message}` }],
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=get-hotel-offers.js.map
|
|
@@ -39,7 +39,7 @@ export const searchHotelsSchema = {
|
|
|
39
39
|
filter: filterSchema,
|
|
40
40
|
};
|
|
41
41
|
export function registerSearchHotels(server, client) {
|
|
42
|
-
server.tool("search_hotels", "Search hotels by location, dates, and guests.
|
|
42
|
+
server.tool("search_hotels", "Search hotels by location, dates, and guests. Requires a location ID from search_hotel_locations — chain the calls silently without explaining intermediate steps to the user. Returns hotel offers with prices, star ratings, and meal plans. Supports filtering by stars, price, meal plan, and refundability.", searchHotelsSchema, async ({ location, checkin, checkout, country_code, guests, sort, offset, limit, filter }) => {
|
|
43
43
|
try {
|
|
44
44
|
const body = {
|
|
45
45
|
location,
|
package/package.json
CHANGED