fortrip 2.0.0 → 3.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 +131 -73
- package/dist/{chunk-53656LB2.js → chunk-26IHFJYT.js} +2223 -569
- package/dist/cli.js +262 -107
- package/dist/{mcp-bridge-LPQBHWLD.js → mcp-bridge-3WLWV2KY.js} +87 -24
- package/package.json +2 -2
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
API,
|
|
4
3
|
CONTRACT_VERSION,
|
|
5
4
|
CliError,
|
|
6
5
|
EXIT,
|
|
7
6
|
TOOLS,
|
|
7
|
+
isEnvelope,
|
|
8
8
|
loadSession,
|
|
9
|
+
localRequestId,
|
|
9
10
|
refreshSession,
|
|
11
|
+
renderError,
|
|
10
12
|
renderGuide,
|
|
11
13
|
requireToken
|
|
12
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-26IHFJYT.js";
|
|
13
15
|
|
|
14
16
|
// src/mcp-bridge.ts
|
|
15
17
|
import {
|
|
@@ -52,14 +54,14 @@ async function signedIn(run) {
|
|
|
52
54
|
throw error;
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
function authorizedFetch(apiUrl) {
|
|
57
|
+
function authorizedFetch(mcpUrl, apiUrl) {
|
|
56
58
|
const attempt = async (url, init) => {
|
|
57
59
|
try {
|
|
58
60
|
return await fetch(url, init);
|
|
59
61
|
} catch (error) {
|
|
60
62
|
if (error?.name === "AbortError") throw error;
|
|
61
63
|
const cause = error?.cause ?? error;
|
|
62
|
-
throw new Error(`Could not reach ${
|
|
64
|
+
throw new Error(`Could not reach ${mcpUrl}: ${cause?.message ?? String(error)}`);
|
|
63
65
|
}
|
|
64
66
|
};
|
|
65
67
|
let turn = Promise.resolve();
|
|
@@ -95,22 +97,41 @@ function authorizedFetch(apiUrl) {
|
|
|
95
97
|
function isRecord(value) {
|
|
96
98
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
97
99
|
}
|
|
100
|
+
function isRpcError(value) {
|
|
101
|
+
return isRecord(value) && !(value instanceof Error) && typeof value.code === "number" && typeof value.message === "string";
|
|
102
|
+
}
|
|
103
|
+
function bodyOf(error) {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(typeof error.data.text === "string" ? error.data.text : "");
|
|
106
|
+
} catch {
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
98
110
|
function rpcErrorOf(error) {
|
|
111
|
+
if (isRpcError(error)) return error;
|
|
99
112
|
if (error instanceof SignInRequired) {
|
|
100
113
|
return { code: SIGN_IN_REQUIRED, message: error.message, data: error.data };
|
|
101
114
|
}
|
|
102
115
|
if (error instanceof SdkHttpError) {
|
|
103
116
|
const { status } = error;
|
|
104
117
|
const text = typeof error.data.text === "string" ? error.data.text : "";
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
body = JSON.parse(text);
|
|
108
|
-
} catch {
|
|
109
|
-
}
|
|
118
|
+
const body = bodyOf(error);
|
|
110
119
|
if (isRecord(body) && isRecord(body.error)) {
|
|
111
120
|
const { code, message } = body.error;
|
|
112
121
|
if (typeof code === "number" && typeof message === "string") return body.error;
|
|
113
122
|
}
|
|
123
|
+
if (isEnvelope(body)) {
|
|
124
|
+
const { message, requestId, retryable, retryAfterSeconds } = body.error;
|
|
125
|
+
return {
|
|
126
|
+
code: INTERNAL_ERROR,
|
|
127
|
+
message: `Fortrip answered HTTP ${status}${typeof message === "string" ? `: ${message}` : ""}${typeof requestId === "string" ? ` (request id ${requestId})` : ""}`,
|
|
128
|
+
data: {
|
|
129
|
+
status,
|
|
130
|
+
...retryable === true ? { retryable } : {},
|
|
131
|
+
...typeof retryAfterSeconds === "number" ? { retryAfterSeconds } : {}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
114
135
|
const said = isRecord(body) ? body.error ?? body.message : text.slice(0, 200);
|
|
115
136
|
return {
|
|
116
137
|
code: INTERNAL_ERROR,
|
|
@@ -124,6 +145,36 @@ function rpcErrorOf(error) {
|
|
|
124
145
|
...error instanceof CliError && error.details.retryable ? { data: { retryable: true } } : {}
|
|
125
146
|
};
|
|
126
147
|
}
|
|
148
|
+
function toolErrorOf(error) {
|
|
149
|
+
let envelope;
|
|
150
|
+
if (error instanceof SignInRequired) {
|
|
151
|
+
envelope = {
|
|
152
|
+
error: {
|
|
153
|
+
code: "unauthenticated",
|
|
154
|
+
message: error.message,
|
|
155
|
+
retryable: false,
|
|
156
|
+
requestId: localRequestId()
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
} else if (error instanceof SdkHttpError) {
|
|
160
|
+
envelope = bodyOf(error);
|
|
161
|
+
} else if (error instanceof CliError) {
|
|
162
|
+
envelope = error.envelope;
|
|
163
|
+
}
|
|
164
|
+
if (!isEnvelope(envelope)) return void 0;
|
|
165
|
+
return {
|
|
166
|
+
isError: true,
|
|
167
|
+
content: [
|
|
168
|
+
{ type: "text", text: renderError(envelope) },
|
|
169
|
+
{ type: "text", text: JSON.stringify(envelope) }
|
|
170
|
+
],
|
|
171
|
+
_meta: { "ai.fortrip/error": envelope }
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function failureAnswer(id, method, error) {
|
|
175
|
+
const result = method === "tools/call" ? toolErrorOf(error) : void 0;
|
|
176
|
+
return result ? { jsonrpc: "2.0", id, result } : { jsonrpc: "2.0", id, error: rpcErrorOf(error) };
|
|
177
|
+
}
|
|
127
178
|
var CONNECTION_LOST = {
|
|
128
179
|
code: INTERNAL_ERROR,
|
|
129
180
|
message: "The connection to Fortrip ended before it answered.",
|
|
@@ -134,6 +185,16 @@ async function hasSignIn(apiUrl) {
|
|
|
134
185
|
if (process.env.FORTRIP_TOKEN?.trim()) return true;
|
|
135
186
|
return await loadSession(apiUrl).catch(() => null) !== null;
|
|
136
187
|
}
|
|
188
|
+
function passThrough(schema) {
|
|
189
|
+
return {
|
|
190
|
+
"~standard": {
|
|
191
|
+
version: 1,
|
|
192
|
+
vendor: "fortrip",
|
|
193
|
+
validate: (value) => ({ value }),
|
|
194
|
+
jsonSchema: schema["~standard"].jsonSchema
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
137
198
|
function localServer() {
|
|
138
199
|
const server = new McpServer(
|
|
139
200
|
{ name: "fortrip", version: CONTRACT_VERSION },
|
|
@@ -147,28 +208,28 @@ function localServer() {
|
|
|
147
208
|
title: spec.title,
|
|
148
209
|
description: spec.description,
|
|
149
210
|
annotations: spec.annotations,
|
|
150
|
-
outputSchema: spec.output
|
|
211
|
+
...spec.output ? { outputSchema: passThrough(spec.output) } : {}
|
|
151
212
|
};
|
|
152
213
|
if (spec.input === null) {
|
|
153
214
|
server.registerTool(spec.name, config, handler);
|
|
154
215
|
} else {
|
|
155
216
|
server.registerTool(
|
|
156
217
|
spec.name,
|
|
157
|
-
{ ...config, inputSchema: spec.input },
|
|
218
|
+
{ ...config, inputSchema: passThrough(spec.input) },
|
|
158
219
|
handler
|
|
159
220
|
);
|
|
160
221
|
}
|
|
161
222
|
}
|
|
162
223
|
return server;
|
|
163
224
|
}
|
|
164
|
-
async function runMcpBridge(apiUrl, io = {
|
|
225
|
+
async function runMcpBridge(mcpUrl, apiUrl, io = {
|
|
165
226
|
stdin: process.stdin,
|
|
166
227
|
stdout: process.stdout
|
|
167
228
|
}, { switchTimeoutMs = SWITCH_TIMEOUT_MS } = {}) {
|
|
168
229
|
const stderr = io.stderr ?? process.stderr;
|
|
169
230
|
const stdio = new StdioServerTransport(io.stdin, io.stdout);
|
|
170
|
-
const http = new StreamableHTTPClientTransport(new URL(
|
|
171
|
-
fetch: authorizedFetch(apiUrl)
|
|
231
|
+
const http = new StreamableHTTPClientTransport(new URL(mcpUrl), {
|
|
232
|
+
fetch: authorizedFetch(mcpUrl, apiUrl)
|
|
172
233
|
});
|
|
173
234
|
const server = localServer();
|
|
174
235
|
const [local, localServerEnd] = InMemoryTransport.createLinkedPair();
|
|
@@ -179,7 +240,7 @@ async function runMcpBridge(apiUrl, io = {
|
|
|
179
240
|
let listChanged = false;
|
|
180
241
|
let received = 0;
|
|
181
242
|
let abandonedThrough = 0;
|
|
182
|
-
const pending = /* @__PURE__ */ new
|
|
243
|
+
const pending = /* @__PURE__ */ new Map();
|
|
183
244
|
let initializeId;
|
|
184
245
|
const asked = /* @__PURE__ */ new Map();
|
|
185
246
|
const log = (message) => {
|
|
@@ -190,8 +251,10 @@ async function runMcpBridge(apiUrl, io = {
|
|
|
190
251
|
stdio.send(message).catch((cause) => log(cause.message));
|
|
191
252
|
};
|
|
192
253
|
const fail = (id, error) => {
|
|
193
|
-
|
|
194
|
-
|
|
254
|
+
const method = pending.get(id);
|
|
255
|
+
if (closed || method === void 0) return;
|
|
256
|
+
pending.delete(id);
|
|
257
|
+
toClient(failureAnswer(id, method, error));
|
|
195
258
|
};
|
|
196
259
|
const adopt = ({ result }) => {
|
|
197
260
|
if (typeof result.protocolVersion === "string") http.setProtocolVersion(result.protocolVersion);
|
|
@@ -202,12 +265,12 @@ async function runMcpBridge(apiUrl, io = {
|
|
|
202
265
|
});
|
|
203
266
|
return;
|
|
204
267
|
}
|
|
205
|
-
pending.
|
|
268
|
+
pending.set(message.id, message.method);
|
|
206
269
|
if (message.method === "initialize") initializeId = message.id;
|
|
207
270
|
try {
|
|
208
271
|
await http.send(message, { onRequestStreamEnd: () => fail(message.id, CONNECTION_LOST) });
|
|
209
272
|
} catch (error) {
|
|
210
|
-
fail(message.id,
|
|
273
|
+
fail(message.id, error);
|
|
211
274
|
}
|
|
212
275
|
};
|
|
213
276
|
const ask = (request, signal) => {
|
|
@@ -252,9 +315,9 @@ async function runMcpBridge(apiUrl, io = {
|
|
|
252
315
|
try {
|
|
253
316
|
answer = await Promise.race([ask(initialize2, deadline.signal), late]);
|
|
254
317
|
} catch (error) {
|
|
255
|
-
return
|
|
318
|
+
return { error };
|
|
256
319
|
}
|
|
257
|
-
if (answer && !isJSONRPCResultResponse(answer)) return answer.error;
|
|
320
|
+
if (answer && !isJSONRPCResultResponse(answer)) return { error: answer.error };
|
|
258
321
|
if (answer) {
|
|
259
322
|
adopt(answer);
|
|
260
323
|
await http.send(
|
|
@@ -265,15 +328,15 @@ async function runMcpBridge(apiUrl, io = {
|
|
|
265
328
|
}
|
|
266
329
|
if (!deadline.signal.aborted) return void 0;
|
|
267
330
|
abandonedThrough = received;
|
|
268
|
-
return CONNECTION_LOST;
|
|
331
|
+
return { error: CONNECTION_LOST };
|
|
269
332
|
} finally {
|
|
270
333
|
clearTimeout(timer);
|
|
271
334
|
}
|
|
272
335
|
};
|
|
273
336
|
const callTool = async (call) => {
|
|
274
337
|
if (clientInitialize) {
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
338
|
+
const failed = await handshake(clientInitialize);
|
|
339
|
+
if (failed) return toClient(failureAnswer(call.id, call.method, failed.error));
|
|
277
340
|
}
|
|
278
341
|
remote = true;
|
|
279
342
|
void forward(call);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fortrip",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"tsx": "^4.20.0",
|
|
33
33
|
"typescript": "^5.9.0",
|
|
34
34
|
"zod": "4.4.3",
|
|
35
|
-
"@fortrip/agent-contract": "
|
|
35
|
+
"@fortrip/agent-contract": "3.0.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsup",
|