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.
@@ -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-53656LB2.js";
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 ${apiUrl}: ${cause?.message ?? String(error)}`);
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
- let body;
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 ?? void 0
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(API.mcp, apiUrl), {
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 Set();
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
- if (closed || !pending.delete(id)) return;
194
- toClient({ jsonrpc: "2.0", id, error });
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.add(message.id);
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, rpcErrorOf(error));
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 rpcErrorOf(error);
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 error = await handshake(clientInitialize);
276
- if (error) return toClient({ jsonrpc: "2.0", id: call.id, error });
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": "2.0.0",
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": "2.0.0"
35
+ "@fortrip/agent-contract": "3.0.0"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsup",