fortrip 1.0.0 → 2.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 +202 -21
- package/NOTICE +4 -0
- package/README.md +56 -33
- package/THIRD-PARTY-NOTICES +30 -0
- package/dist/{chunk-OBK2KTLC.js → chunk-53656LB2.js} +192 -39
- package/dist/cli.js +115 -53
- package/dist/{mcp-bridge-VEJ33LYD.js → mcp-bridge-LPQBHWLD.js} +158 -10
- package/package.json +6 -4
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import {
|
|
3
3
|
API,
|
|
4
4
|
COMMANDS,
|
|
5
|
+
COMMAND_GROUPS,
|
|
5
6
|
CliError,
|
|
6
7
|
EXIT,
|
|
7
8
|
GLOBAL_FLAGS,
|
|
8
9
|
VERSION,
|
|
9
10
|
authorizedRequest,
|
|
11
|
+
commandInvocation,
|
|
10
12
|
login,
|
|
11
13
|
logout,
|
|
12
14
|
notices,
|
|
@@ -15,7 +17,7 @@ import {
|
|
|
15
17
|
unwrap,
|
|
16
18
|
usageError,
|
|
17
19
|
whoami
|
|
18
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-53656LB2.js";
|
|
19
21
|
|
|
20
22
|
// src/commands.ts
|
|
21
23
|
import { readFileSync } from "fs";
|
|
@@ -39,14 +41,16 @@ function wrap(text, indent) {
|
|
|
39
41
|
if (line) lines.push(pad + line);
|
|
40
42
|
return lines;
|
|
41
43
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
+
function positionals(spec) {
|
|
45
|
+
return (spec.positionals ?? []).map(
|
|
44
46
|
(name) => OPTIONAL_POSITIONALS.has(name) ? `[${name}]` : `<${name}>`
|
|
45
47
|
);
|
|
46
|
-
|
|
48
|
+
}
|
|
49
|
+
function label(spec) {
|
|
50
|
+
return [spec.name, ...positionals(spec)].join(" ");
|
|
47
51
|
}
|
|
48
52
|
function usageLine(spec) {
|
|
49
|
-
return
|
|
53
|
+
return [commandInvocation(spec), ...positionals(spec), "[options]"].join(" ");
|
|
50
54
|
}
|
|
51
55
|
function names(option, aligned) {
|
|
52
56
|
const short = option.short ? `-${option.short}, ` : aligned ? " " : "";
|
|
@@ -71,23 +75,45 @@ Options:
|
|
|
71
75
|
${optionList(options)}
|
|
72
76
|
`;
|
|
73
77
|
}
|
|
74
|
-
function commandList() {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const label = ` ${labels[n]}`.padEnd(column);
|
|
80
|
-
return [label + first.slice(column), ...rest].join("\n");
|
|
78
|
+
function commandList(rows) {
|
|
79
|
+
const column = 2 + Math.max(...rows.map(([name]) => name.length)) + 2;
|
|
80
|
+
return rows.map(([name, summary]) => {
|
|
81
|
+
const [first = "", ...rest] = wrap(summary, column);
|
|
82
|
+
return [` ${name}`.padEnd(column) + first.slice(column), ...rest].join("\n");
|
|
81
83
|
}).join("\n");
|
|
82
84
|
}
|
|
85
|
+
function groupHelp(group) {
|
|
86
|
+
const usage = `fortrip ${group.name} <command>`;
|
|
87
|
+
const commands = COMMANDS.filter((spec) => spec.group === group.name);
|
|
88
|
+
return `Usage: ${usage} [options]
|
|
89
|
+
${usage} --help
|
|
90
|
+
|
|
91
|
+
${wrap(group.summary, 0).join("\n")}
|
|
92
|
+
|
|
93
|
+
Commands:
|
|
94
|
+
${commandList(commands.map((spec) => [label(spec), spec.summary]))}
|
|
95
|
+
|
|
96
|
+
Options:
|
|
97
|
+
${optionList(HELP_FLAG)}
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
var CURRENCY_COMMANDS = COMMANDS.filter(
|
|
101
|
+
(spec) => spec.flags.some((flag) => flag.field === "currency")
|
|
102
|
+
).map((spec) => commandInvocation(spec).slice("fortrip ".length)).join(" and ");
|
|
83
103
|
function overallHelp() {
|
|
104
|
+
const rows = [
|
|
105
|
+
...COMMAND_GROUPS.map((group) => [`${group.name} <command>`, group.summary]),
|
|
106
|
+
...COMMANDS.filter((spec) => spec.group === null).map(
|
|
107
|
+
(spec) => [label(spec), spec.summary]
|
|
108
|
+
)
|
|
109
|
+
];
|
|
84
110
|
return `fortrip ${VERSION} \u2014 search, price and book Fortrip hotels
|
|
85
111
|
|
|
86
112
|
Usage: fortrip <command> [options]
|
|
87
113
|
fortrip <command> --help
|
|
88
114
|
|
|
89
115
|
Commands:
|
|
90
|
-
${commandList()}
|
|
116
|
+
${commandList(rows)}
|
|
91
117
|
|
|
92
118
|
Options:
|
|
93
119
|
${optionList(GLOBAL_FLAGS)}
|
|
@@ -99,7 +125,7 @@ Exit codes: 0 ok, 1 error (retry once when stderr has "retryable": true),
|
|
|
99
125
|
Environment:
|
|
100
126
|
FORTRIP_TOKEN Use this token instead of the saved login (CI, sandboxes)
|
|
101
127
|
FORTRIP_API_URL API base URL (default https://fortrip.ai)
|
|
102
|
-
FORTRIP_CURRENCY Default --currency for
|
|
128
|
+
FORTRIP_CURRENCY Default --currency for ${CURRENCY_COMMANDS}
|
|
103
129
|
`;
|
|
104
130
|
}
|
|
105
131
|
|
|
@@ -108,9 +134,9 @@ var COMMA_LISTS = /* @__PURE__ */ new Set(["hotelIds", "childrenAges", "boardCod
|
|
|
108
134
|
var UPPER_CASE = /* @__PURE__ */ new Set(["nationality", "boardCodes", "currency"]);
|
|
109
135
|
var TURNS_OFF = /* @__PURE__ */ new Set(["bookableOnly"]);
|
|
110
136
|
var POSTS = {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
search_hotels: API.search,
|
|
138
|
+
get_hotel_rates: API.rates,
|
|
139
|
+
book_hotel: API.bookings
|
|
114
140
|
};
|
|
115
141
|
function parseOptions(spec, argv) {
|
|
116
142
|
const options = {};
|
|
@@ -143,24 +169,24 @@ function parseOptions(spec, argv) {
|
|
|
143
169
|
}
|
|
144
170
|
if (!spec.positionals && parsed.positionals.length) {
|
|
145
171
|
throw usageError(
|
|
146
|
-
`${spec
|
|
172
|
+
`${commandInvocation(spec)} takes no arguments, got "${parsed.positionals[0]}"`
|
|
147
173
|
);
|
|
148
174
|
}
|
|
149
175
|
return { values: parsed.values, positionals: parsed.positionals };
|
|
150
176
|
}
|
|
151
177
|
function parseCommand(spec, argv, env) {
|
|
152
|
-
const post = POSTS[spec.
|
|
153
|
-
if (!post && spec.
|
|
154
|
-
throw new Error(`${spec
|
|
178
|
+
const post = spec.tool ? POSTS[spec.tool] : void 0;
|
|
179
|
+
if (!post && spec.tool !== "get_hotel") {
|
|
180
|
+
throw new Error(`${commandInvocation(spec)} sends no request`);
|
|
155
181
|
}
|
|
156
|
-
const { values, positionals } = parseOptions(spec, argv);
|
|
182
|
+
const { values, positionals: positionals2 } = parseOptions(spec, argv);
|
|
157
183
|
const input = baseBody(values.body);
|
|
158
184
|
for (const flag of spec.flags) {
|
|
159
185
|
const raw = values[flag.flag];
|
|
160
186
|
if (raw !== void 0) setField(input, flag.field, readFlag(flag, raw));
|
|
161
187
|
}
|
|
162
|
-
if (spec.
|
|
163
|
-
const [hotelId, ...extra] =
|
|
188
|
+
if (spec.tool === "get_hotel") {
|
|
189
|
+
const [hotelId, ...extra] = positionals2;
|
|
164
190
|
const parsed = Number(hotelId);
|
|
165
191
|
if (!hotelId || extra.length || !/^\d+$/.test(hotelId) || !Number.isSafeInteger(parsed)) {
|
|
166
192
|
throw usageError(`Usage: ${usageLine(spec)}`);
|
|
@@ -177,7 +203,7 @@ function parseCommand(spec, argv, env) {
|
|
|
177
203
|
const missing = occupancy.filter((flag) => input[flag.field] === void 0);
|
|
178
204
|
if (occupancy.length === 2 && missing.length) {
|
|
179
205
|
throw usageError(
|
|
180
|
-
`${spec
|
|
206
|
+
`${commandInvocation(spec)} needs ${missing.map((flag) => `--${flag.flag}`).join(" and ")}, per room and the same for every room (2 rooms for 4 adults is --rooms 2 --adults 2)`
|
|
181
207
|
);
|
|
182
208
|
}
|
|
183
209
|
return { body: input, path: post };
|
|
@@ -277,8 +303,42 @@ function globalFlag(name) {
|
|
|
277
303
|
}
|
|
278
304
|
var HELP_FLAGS = globalFlag("help");
|
|
279
305
|
var VERSION_FLAGS = globalFlag("version");
|
|
306
|
+
var RENAMED = /* @__PURE__ */ new Map([
|
|
307
|
+
["hotel", { group: "hotels", name: "get" }]
|
|
308
|
+
]);
|
|
309
|
+
function findCommand(argv) {
|
|
310
|
+
const [word, ...afterWord] = argv;
|
|
311
|
+
const group = COMMAND_GROUPS.find((candidate) => candidate.name === word);
|
|
312
|
+
if (group) {
|
|
313
|
+
const [name, ...rest] = afterWord;
|
|
314
|
+
if (!name || name === "help" || HELP_FLAGS.includes(name)) {
|
|
315
|
+
return { groupHelp: group };
|
|
316
|
+
}
|
|
317
|
+
const spec2 = COMMANDS.find(
|
|
318
|
+
(candidate) => candidate.group === group.name && candidate.name === name
|
|
319
|
+
);
|
|
320
|
+
if (!spec2) {
|
|
321
|
+
throw usageError(
|
|
322
|
+
`Unknown command "${group.name} ${name}". Run \`fortrip ${group.name} --help\`.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
return { spec: spec2, rest };
|
|
326
|
+
}
|
|
327
|
+
const spec = COMMANDS.find(
|
|
328
|
+
(candidate) => candidate.group === null && candidate.name === word
|
|
329
|
+
);
|
|
330
|
+
if (spec) return { spec, rest: afterWord };
|
|
331
|
+
const renamed = RENAMED.get(word ?? "");
|
|
332
|
+
const meant = [
|
|
333
|
+
...COMMANDS.filter((candidate) => candidate.group !== null && candidate.name === word),
|
|
334
|
+
...renamed ? [renamed] : []
|
|
335
|
+
].map((candidate) => `\`${commandInvocation(candidate)}\``);
|
|
336
|
+
throw usageError(
|
|
337
|
+
meant.length ? `Unknown command "${word}". Did you mean ${meant.join(" or ")}?` : `Unknown command "${word}". Run \`fortrip --help\`.`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
280
340
|
async function run(argv) {
|
|
281
|
-
const [command
|
|
341
|
+
const [command] = argv;
|
|
282
342
|
if (!command || command === "help" || HELP_FLAGS.includes(command)) {
|
|
283
343
|
process.stdout.write(overallHelp());
|
|
284
344
|
return void 0;
|
|
@@ -288,17 +348,19 @@ async function run(argv) {
|
|
|
288
348
|
`);
|
|
289
349
|
return void 0;
|
|
290
350
|
}
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
|
|
351
|
+
const found = findCommand(argv);
|
|
352
|
+
if ("groupHelp" in found) {
|
|
353
|
+
process.stdout.write(groupHelp(found.groupHelp));
|
|
354
|
+
return void 0;
|
|
294
355
|
}
|
|
356
|
+
const { spec, rest } = found;
|
|
295
357
|
if (wantsHelp(rest)) {
|
|
296
358
|
process.stdout.write(commandHelp(spec));
|
|
297
359
|
return void 0;
|
|
298
360
|
}
|
|
299
|
-
if (spec.name === "guide") {
|
|
300
|
-
const { positionals } = parseOptions(spec, rest);
|
|
301
|
-
const topic =
|
|
361
|
+
if (spec.group === null && spec.name === "guide") {
|
|
362
|
+
const { positionals: positionals2 } = parseOptions(spec, rest);
|
|
363
|
+
const topic = positionals2.join(" ");
|
|
302
364
|
if (topic !== "" && topic !== "planning") {
|
|
303
365
|
throw usageError("Usage: fortrip guide [planning]");
|
|
304
366
|
}
|
|
@@ -308,30 +370,30 @@ async function run(argv) {
|
|
|
308
370
|
const apiUrl = resolveApiUrl();
|
|
309
371
|
const log = (line) => process.stderr.write(`${line}
|
|
310
372
|
`);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
373
|
+
if (spec.group === null) {
|
|
374
|
+
switch (spec.name) {
|
|
375
|
+
case "login": {
|
|
376
|
+
const { values } = parseOptions(spec, rest);
|
|
377
|
+
if (process.env.FORTRIP_TOKEN) {
|
|
378
|
+
log("Note: FORTRIP_TOKEN is set and takes precedence over this login.");
|
|
379
|
+
}
|
|
380
|
+
return login({ apiUrl, openBrowser: !values["no-browser"], log });
|
|
381
|
+
}
|
|
382
|
+
case "logout":
|
|
383
|
+
parseOptions(spec, rest);
|
|
384
|
+
return logout(apiUrl);
|
|
385
|
+
case "whoami":
|
|
386
|
+
parseOptions(spec, rest);
|
|
387
|
+
return whoami(apiUrl);
|
|
388
|
+
case "mcp": {
|
|
389
|
+
parseOptions(spec, rest);
|
|
390
|
+
const { runMcpBridge } = await import("./mcp-bridge-LPQBHWLD.js");
|
|
391
|
+
return runMcpBridge(apiUrl);
|
|
316
392
|
}
|
|
317
|
-
return login({ apiUrl, openBrowser: !values["no-browser"], log });
|
|
318
|
-
}
|
|
319
|
-
case "logout":
|
|
320
|
-
parseOptions(spec, rest);
|
|
321
|
-
return logout(apiUrl);
|
|
322
|
-
case "whoami":
|
|
323
|
-
parseOptions(spec, rest);
|
|
324
|
-
return whoami(apiUrl);
|
|
325
|
-
case "mcp": {
|
|
326
|
-
parseOptions(spec, rest);
|
|
327
|
-
const { runMcpBridge } = await import("./mcp-bridge-VEJ33LYD.js");
|
|
328
|
-
return runMcpBridge(apiUrl);
|
|
329
|
-
}
|
|
330
|
-
default: {
|
|
331
|
-
const { body, path } = parseCommand(spec, rest, process.env);
|
|
332
|
-
return call(apiUrl, body ? "POST" : "GET", path, body);
|
|
333
393
|
}
|
|
334
394
|
}
|
|
395
|
+
const { body, path } = parseCommand(spec, rest, process.env);
|
|
396
|
+
return call(apiUrl, body ? "POST" : "GET", path, body);
|
|
335
397
|
}
|
|
336
398
|
async function call(apiUrl, method, path, body) {
|
|
337
399
|
return unwrap(await authorizedRequest(apiUrl, path, { method, body }));
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
API,
|
|
4
|
+
CONTRACT_VERSION,
|
|
4
5
|
CliError,
|
|
5
6
|
EXIT,
|
|
7
|
+
TOOLS,
|
|
8
|
+
loadSession,
|
|
6
9
|
refreshSession,
|
|
10
|
+
renderGuide,
|
|
7
11
|
requireToken
|
|
8
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-53656LB2.js";
|
|
9
13
|
|
|
10
14
|
// src/mcp-bridge.ts
|
|
11
15
|
import {
|
|
@@ -15,6 +19,10 @@ import {
|
|
|
15
19
|
SdkHttpError,
|
|
16
20
|
StreamableHTTPClientTransport
|
|
17
21
|
} from "@modelcontextprotocol/client";
|
|
22
|
+
import {
|
|
23
|
+
InMemoryTransport,
|
|
24
|
+
McpServer
|
|
25
|
+
} from "@modelcontextprotocol/server";
|
|
18
26
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
19
27
|
var SIGN_IN_REQUIRED = -32001;
|
|
20
28
|
var INTERNAL_ERROR = -32603;
|
|
@@ -121,27 +129,74 @@ var CONNECTION_LOST = {
|
|
|
121
129
|
message: "The connection to Fortrip ended before it answered.",
|
|
122
130
|
data: { retryable: true }
|
|
123
131
|
};
|
|
132
|
+
var SWITCH_TIMEOUT_MS = 6e4;
|
|
133
|
+
async function hasSignIn(apiUrl) {
|
|
134
|
+
if (process.env.FORTRIP_TOKEN?.trim()) return true;
|
|
135
|
+
return await loadSession(apiUrl).catch(() => null) !== null;
|
|
136
|
+
}
|
|
137
|
+
function localServer() {
|
|
138
|
+
const server = new McpServer(
|
|
139
|
+
{ name: "fortrip", version: CONTRACT_VERSION },
|
|
140
|
+
{ instructions: renderGuide("mcp", "main") }
|
|
141
|
+
);
|
|
142
|
+
const handler = () => {
|
|
143
|
+
throw new Error("Fortrip's tools run on the hosted server.");
|
|
144
|
+
};
|
|
145
|
+
for (const spec of TOOLS) {
|
|
146
|
+
const config = {
|
|
147
|
+
title: spec.title,
|
|
148
|
+
description: spec.description,
|
|
149
|
+
annotations: spec.annotations,
|
|
150
|
+
outputSchema: spec.output ?? void 0
|
|
151
|
+
};
|
|
152
|
+
if (spec.input === null) {
|
|
153
|
+
server.registerTool(spec.name, config, handler);
|
|
154
|
+
} else {
|
|
155
|
+
server.registerTool(
|
|
156
|
+
spec.name,
|
|
157
|
+
{ ...config, inputSchema: spec.input },
|
|
158
|
+
handler
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return server;
|
|
163
|
+
}
|
|
124
164
|
async function runMcpBridge(apiUrl, io = {
|
|
125
165
|
stdin: process.stdin,
|
|
126
166
|
stdout: process.stdout
|
|
127
|
-
}) {
|
|
167
|
+
}, { switchTimeoutMs = SWITCH_TIMEOUT_MS } = {}) {
|
|
128
168
|
const stderr = io.stderr ?? process.stderr;
|
|
129
169
|
const stdio = new StdioServerTransport(io.stdin, io.stdout);
|
|
130
170
|
const http = new StreamableHTTPClientTransport(new URL(API.mcp, apiUrl), {
|
|
131
171
|
fetch: authorizedFetch(apiUrl)
|
|
132
172
|
});
|
|
173
|
+
const server = localServer();
|
|
174
|
+
const [local, localServerEnd] = InMemoryTransport.createLinkedPair();
|
|
175
|
+
const signInSaved = await hasSignIn(apiUrl);
|
|
133
176
|
let closed = false;
|
|
177
|
+
let remote = false;
|
|
178
|
+
let clientInitialize;
|
|
179
|
+
let listChanged = false;
|
|
180
|
+
let received = 0;
|
|
181
|
+
let abandonedThrough = 0;
|
|
134
182
|
const pending = /* @__PURE__ */ new Set();
|
|
135
183
|
let initializeId;
|
|
184
|
+
const asked = /* @__PURE__ */ new Map();
|
|
136
185
|
const log = (message) => {
|
|
137
186
|
if (!closed) stderr.write(`fortrip mcp: ${message}
|
|
138
187
|
`);
|
|
139
188
|
};
|
|
189
|
+
const toClient = (message) => {
|
|
190
|
+
stdio.send(message).catch((cause) => log(cause.message));
|
|
191
|
+
};
|
|
140
192
|
const fail = (id, error) => {
|
|
141
193
|
if (closed || !pending.delete(id)) return;
|
|
142
|
-
|
|
194
|
+
toClient({ jsonrpc: "2.0", id, error });
|
|
143
195
|
};
|
|
144
|
-
|
|
196
|
+
const adopt = ({ result }) => {
|
|
197
|
+
if (typeof result.protocolVersion === "string") http.setProtocolVersion(result.protocolVersion);
|
|
198
|
+
};
|
|
199
|
+
const forward = async (message) => {
|
|
145
200
|
if (!isJSONRPCRequest(message)) {
|
|
146
201
|
await http.send(message).catch(() => {
|
|
147
202
|
});
|
|
@@ -155,15 +210,107 @@ async function runMcpBridge(apiUrl, io = {
|
|
|
155
210
|
fail(message.id, rpcErrorOf(error));
|
|
156
211
|
}
|
|
157
212
|
};
|
|
213
|
+
const ask = (request, signal) => {
|
|
214
|
+
let waiter;
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
waiter = resolve;
|
|
217
|
+
asked.set(request.id, resolve);
|
|
218
|
+
http.send(request, {
|
|
219
|
+
requestSignal: signal,
|
|
220
|
+
onRequestStreamEnd: () => resolve({ jsonrpc: "2.0", id: request.id, error: CONNECTION_LOST })
|
|
221
|
+
}).catch(reject);
|
|
222
|
+
}).finally(() => {
|
|
223
|
+
if (asked.get(request.id) === waiter) asked.delete(request.id);
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
const initialize = async (request) => {
|
|
227
|
+
clientInitialize = request;
|
|
228
|
+
if (signInSaved) {
|
|
229
|
+
try {
|
|
230
|
+
const answer = await ask(request);
|
|
231
|
+
if (isJSONRPCResultResponse(answer)) {
|
|
232
|
+
adopt(answer);
|
|
233
|
+
remote = true;
|
|
234
|
+
}
|
|
235
|
+
return toClient(answer);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (!(error instanceof SignInRequired)) {
|
|
238
|
+
return toClient({ jsonrpc: "2.0", id: request.id, error: rpcErrorOf(error) });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
await local.send(request);
|
|
243
|
+
};
|
|
244
|
+
const handshake = async (initialize2) => {
|
|
245
|
+
const deadline = new AbortController();
|
|
246
|
+
const timer = setTimeout(() => deadline.abort(), switchTimeoutMs);
|
|
247
|
+
const late = new Promise(
|
|
248
|
+
(resolve) => deadline.signal.addEventListener("abort", () => resolve(void 0))
|
|
249
|
+
);
|
|
250
|
+
try {
|
|
251
|
+
let answer;
|
|
252
|
+
try {
|
|
253
|
+
answer = await Promise.race([ask(initialize2, deadline.signal), late]);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return rpcErrorOf(error);
|
|
256
|
+
}
|
|
257
|
+
if (answer && !isJSONRPCResultResponse(answer)) return answer.error;
|
|
258
|
+
if (answer) {
|
|
259
|
+
adopt(answer);
|
|
260
|
+
await http.send(
|
|
261
|
+
{ jsonrpc: "2.0", method: "notifications/initialized" },
|
|
262
|
+
{ requestSignal: deadline.signal }
|
|
263
|
+
).catch(() => {
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (!deadline.signal.aborted) return void 0;
|
|
267
|
+
abandonedThrough = received;
|
|
268
|
+
return CONNECTION_LOST;
|
|
269
|
+
} finally {
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const callTool = async (call) => {
|
|
274
|
+
if (clientInitialize) {
|
|
275
|
+
const error = await handshake(clientInitialize);
|
|
276
|
+
if (error) return toClient({ jsonrpc: "2.0", id: call.id, error });
|
|
277
|
+
}
|
|
278
|
+
remote = true;
|
|
279
|
+
void forward(call);
|
|
280
|
+
if (listChanged) toClient({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
281
|
+
};
|
|
282
|
+
const handle = async (message, seq) => {
|
|
283
|
+
if (closed) return;
|
|
284
|
+
if (remote) return void forward(message);
|
|
285
|
+
if (isJSONRPCRequest(message) && message.method === "initialize") return initialize(message);
|
|
286
|
+
if (isJSONRPCRequest(message) && message.method === "tools/call") {
|
|
287
|
+
if (seq <= abandonedThrough) {
|
|
288
|
+
return toClient({ jsonrpc: "2.0", id: message.id, error: CONNECTION_LOST });
|
|
289
|
+
}
|
|
290
|
+
return callTool(message);
|
|
291
|
+
}
|
|
292
|
+
await local.send(message);
|
|
293
|
+
};
|
|
294
|
+
let inbox = Promise.resolve();
|
|
295
|
+
stdio.onmessage = (message) => {
|
|
296
|
+
const seq = ++received;
|
|
297
|
+
inbox = inbox.then(() => handle(message, seq)).catch((error) => log(error.message));
|
|
298
|
+
};
|
|
299
|
+
local.onmessage = (message) => {
|
|
300
|
+
if (isJSONRPCResultResponse(message) && message.id === clientInitialize?.id) {
|
|
301
|
+
const { capabilities } = message.result;
|
|
302
|
+
listChanged = capabilities?.tools?.listChanged === true;
|
|
303
|
+
}
|
|
304
|
+
toClient(message);
|
|
305
|
+
};
|
|
158
306
|
http.onmessage = (message) => {
|
|
159
307
|
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
|
|
308
|
+
const waiting = message.id === void 0 ? void 0 : asked.get(message.id);
|
|
309
|
+
if (waiting) return waiting(message);
|
|
160
310
|
if (message.id !== void 0) pending.delete(message.id);
|
|
161
|
-
if (isJSONRPCResultResponse(message) && message.id === initializeId)
|
|
162
|
-
const { protocolVersion } = message.result;
|
|
163
|
-
if (typeof protocolVersion === "string") http.setProtocolVersion(protocolVersion);
|
|
164
|
-
}
|
|
311
|
+
if (isJSONRPCResultResponse(message) && message.id === initializeId) adopt(message);
|
|
165
312
|
}
|
|
166
|
-
|
|
313
|
+
toClient(message);
|
|
167
314
|
};
|
|
168
315
|
http.onerror = (error) => log(error.message);
|
|
169
316
|
stdio.onerror = (error) => log(error.message);
|
|
@@ -176,13 +323,14 @@ async function runMcpBridge(apiUrl, io = {
|
|
|
176
323
|
closed = true;
|
|
177
324
|
process.off("SIGINT", shutdown);
|
|
178
325
|
process.off("SIGTERM", shutdown);
|
|
179
|
-
await Promise.allSettled([http.close(), stdio.close()]);
|
|
326
|
+
await Promise.allSettled([http.close(), stdio.close(), server.close()]);
|
|
180
327
|
stopped();
|
|
181
328
|
};
|
|
182
329
|
stdio.onclose = shutdown;
|
|
183
330
|
process.on("SIGINT", shutdown);
|
|
184
331
|
process.on("SIGTERM", shutdown);
|
|
185
332
|
await http.start();
|
|
333
|
+
await server.connect(localServerEnd);
|
|
186
334
|
await stdio.start();
|
|
187
335
|
return done;
|
|
188
336
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fortrip",
|
|
3
|
-
"version": "
|
|
4
|
-
"license": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
5
|
"description": "Search, price and book Fortrip hotels from the terminal or an AI agent.",
|
|
6
6
|
"mcpName": "io.github.FortripEngineering/fortrip",
|
|
7
7
|
"type": "module",
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"NOTICE",
|
|
20
|
+
"THIRD-PARTY-NOTICES"
|
|
19
21
|
],
|
|
20
22
|
"engines": {
|
|
21
23
|
"node": ">=22"
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
"tsx": "^4.20.0",
|
|
31
33
|
"typescript": "^5.9.0",
|
|
32
34
|
"zod": "4.4.3",
|
|
33
|
-
"@fortrip/agent-contract": "
|
|
35
|
+
"@fortrip/agent-contract": "2.0.0"
|
|
34
36
|
},
|
|
35
37
|
"scripts": {
|
|
36
38
|
"build": "tsup",
|