@oomol-lab/open-connector 1.6.3 → 1.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oomol-lab/open-connector",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "Embeddable Open Connector runtime for Node.js and Bun.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -209,6 +209,24 @@ async function getMetadata(input, accessToken, fetcher) {
209
209
  metadata: mapDropboxMetadata(payload),
210
210
  };
211
211
  }
212
+ /**
213
+ * Serialize a `Dropbox-API-Arg` header value.
214
+ *
215
+ * Dropbox requires this header to be ASCII: its own docs say `0x7F` and every
216
+ * non-ASCII character must be escaped as `\uXXXX`, which is also the range the
217
+ * official JS SDK escapes. `JSON.stringify` does not escape, and Node's `Headers`
218
+ * refuses a value outside ByteString, so a path containing any non-ASCII
219
+ * character threw `TypeError: Cannot convert argument to a ByteString` inside
220
+ * the runtime BEFORE the request was made. `provider-runtime.ts`'s fallback
221
+ * then reported it as a bare `internal_error`, discarding the cause.
222
+ *
223
+ * Measured 2026-09-18 against a real account: `/kkndme 天涯.pdf` failed in 1ms
224
+ * with no network call, while ASCII-path siblings downloaded in ~400ms. Dropbox
225
+ * itself accepts either spelling on the wire; only the header build was broken.
226
+ */
227
+ function dropboxApiArg(arg) {
228
+ return JSON.stringify(arg).replace(/[\u007f-\uffff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
229
+ }
212
230
  async function downloadFile(input, context) {
213
231
  const { accessToken, fetcher, transitFiles } = context;
214
232
  if (!transitFiles) {
@@ -218,7 +236,7 @@ async function downloadFile(input, context) {
218
236
  method: "POST",
219
237
  headers: {
220
238
  ...dropboxAuthHeaders(accessToken),
221
- "Dropbox-API-Arg": JSON.stringify({
239
+ "Dropbox-API-Arg": dropboxApiArg({
222
240
  path: requireString(input.path, "dropbox download path"),
223
241
  }),
224
242
  },
@@ -251,7 +269,7 @@ async function uploadFile(input, accessToken, fetcher) {
251
269
  headers: {
252
270
  ...dropboxAuthHeaders(accessToken),
253
271
  "Content-Type": source.mimeType || "application/octet-stream",
254
- "Dropbox-API-Arg": JSON.stringify(arg),
272
+ "Dropbox-API-Arg": dropboxApiArg(arg),
255
273
  },
256
274
  body: Buffer.from(source.bytes),
257
275
  });
@@ -491,7 +509,7 @@ async function getSharedLinkFile(input, context) {
491
509
  method: "POST",
492
510
  headers: {
493
511
  ...dropboxAuthHeaders(accessToken),
494
- "Dropbox-API-Arg": JSON.stringify(arg),
512
+ "Dropbox-API-Arg": dropboxApiArg(arg),
495
513
  },
496
514
  signal: context.signal,
497
515
  });
@@ -15,6 +15,6 @@ export const feishuAppBotScopes = {
15
15
  groupAtReadonly: "im:message.group_at_msg:readonly",
16
16
  groupAtWithBotReadonly: "im:message.group_at_msg.include_bot:readonly",
17
17
  groupReadonly: "im:message.group_msg:readonly",
18
- applicationRead: "application:application:readonly",
18
+ applicationRead: "application:application:self_manage",
19
19
  applicationVersionRead: "application:application.app_version:readonly",
20
20
  };
@@ -10,6 +10,7 @@ export interface McpClientOptions {
10
10
  signal?: AbortSignal;
11
11
  protocolVersion?: McpProtocolVersion;
12
12
  terminateSession?: boolean;
13
+ retryOnSessionNotFound?: boolean;
13
14
  mapError?: (error: unknown) => unknown;
14
15
  }
15
16
  export declare function withMcpClient<T>(options: McpClientOptions, run: (client: Client) => Promise<T>): Promise<T>;
@@ -1,4 +1,4 @@
1
- import { Client } from "@modelcontextprotocol/client";
1
+ import { Client, SdkHttpError } from "@modelcontextprotocol/client";
2
2
  import { SSEClientTransport } from "@modelcontextprotocol/client";
3
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
4
4
  import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/client/validators/cf-worker";
@@ -6,33 +6,47 @@ const mcpConnectTimeoutMs = 60_000;
6
6
  const modernMcpProtocolVersion = "2026-07-28";
7
7
  const mcpJsonSchemaValidator = new CfWorkerJsonSchemaValidator();
8
8
  export async function withMcpClient(options, run) {
9
- const transportOptions = {
10
- fetch: options.fetcher,
11
- requestInit: {
12
- headers: options.headers,
13
- redirect: options.redirect,
14
- signal: options.signal,
15
- },
16
- };
17
- const transport = options.transport === "sse"
18
- ? new SSEClientTransport(options.endpoint, transportOptions)
19
- : new StreamableHTTPClientTransport(options.endpoint, transportOptions);
20
- const client = new Client({ name: "open-connector", version: "1.0.0" }, {
21
- jsonSchemaValidator: mcpJsonSchemaValidator,
22
- versionNegotiation: { mode: resolveVersionNegotiationMode(options.protocolVersion) },
23
- });
24
- try {
25
- await client.connect(transport, { timeout: mcpConnectTimeoutMs, signal: options.signal });
26
- return await run(client);
27
- }
28
- catch (error) {
29
- throw options.mapError ? options.mapError(error) : error;
30
- }
31
- finally {
32
- if (options.terminateSession && transport instanceof StreamableHTTPClientTransport) {
33
- await terminateMcpSession(transport);
9
+ let retriedSession = false;
10
+ while (true) {
11
+ const transportOptions = {
12
+ fetch: options.fetcher,
13
+ requestInit: {
14
+ headers: options.headers,
15
+ redirect: options.redirect,
16
+ signal: options.signal,
17
+ },
18
+ };
19
+ const transport = options.transport === "sse"
20
+ ? new SSEClientTransport(options.endpoint, transportOptions)
21
+ : new StreamableHTTPClientTransport(options.endpoint, transportOptions);
22
+ const client = new Client({ name: "open-connector", version: "1.0.0" }, {
23
+ jsonSchemaValidator: mcpJsonSchemaValidator,
24
+ versionNegotiation: { mode: resolveVersionNegotiationMode(options.protocolVersion) },
25
+ });
26
+ let connected = false;
27
+ try {
28
+ await client.connect(transport, { timeout: mcpConnectTimeoutMs, signal: options.signal });
29
+ connected = true;
30
+ return await run(client);
31
+ }
32
+ catch (error) {
33
+ if (connected &&
34
+ options.transport === "streamable_http" &&
35
+ options.retryOnSessionNotFound === true &&
36
+ !retriedSession &&
37
+ error instanceof SdkHttpError &&
38
+ error.status === 404) {
39
+ retriedSession = true;
40
+ continue;
41
+ }
42
+ throw options.mapError ? options.mapError(error) : error;
43
+ }
44
+ finally {
45
+ if (options.terminateSession && transport instanceof StreamableHTTPClientTransport) {
46
+ await terminateMcpSession(transport);
47
+ }
48
+ await client.close().catch(() => undefined);
34
49
  }
35
- await client.close().catch(() => undefined);
36
50
  }
37
51
  }
38
52
  async function terminateMcpSession(transport) {
@@ -87,8 +87,9 @@ function normalize(result) {
87
87
  const message = envelope.message ?? `SellerSprite MCP tool failed with ${envelope.code}`;
88
88
  if (["ERROR_UNAUTHORIZED", "ERROR_SECRET_KEY", "ERROR_SECRET_KEY_INVALID", "ERROR_SECRET_KEY_OVERDUE"].includes(envelope.code))
89
89
  throw new ProviderRequestError(401, message, envelope);
90
- if (envelope.code === "ERROR_VISIT_MAX")
90
+ if (envelope.code === "ERROR_VISIT_MAX" || envelope.code === "ERROR_MAXIMUM_ACCESS_PER_MINUTE") {
91
91
  throw new ProviderRequestError(429, message, envelope);
92
+ }
92
93
  if (envelope.code === "ERROR_AUTH_ERROR")
93
94
  throw new ProviderRequestError(403, message, envelope);
94
95
  if (envelope.code === "ERROR_PARAM")
@@ -688,10 +688,10 @@ function readSlackTokenKind(accessToken, metadata) {
688
688
  if (rawTokenType == "user") {
689
689
  return "user";
690
690
  }
691
- if (accessToken.startsWith("xoxb-")) {
691
+ if (accessToken.startsWith("xoxb-") || accessToken.startsWith("xoxe.xoxb-")) {
692
692
  return "bot";
693
693
  }
694
- if (accessToken.startsWith("xoxp-")) {
694
+ if (accessToken.startsWith("xoxp-") || accessToken.startsWith("xoxe.xoxp-")) {
695
695
  return "user";
696
696
  }
697
697
  return undefined;
@@ -20,7 +20,7 @@ const handlers = {
20
20
  if (!argumentsValue)
21
21
  throw new ProviderRequestError(400, "arguments must be a JSON object");
22
22
  assertToolArgumentsSize(argumentsValue);
23
- const result = await withTongdaxinClient(context, "execute", (client) => client.callTool({ name: toolName, arguments: argumentsValue }, { timeout: requestTimeoutMs, signal: context.signal }));
23
+ const result = await withTongdaxinClient(context, "execute", false, (client) => client.callTool({ name: toolName, arguments: argumentsValue }, { timeout: requestTimeoutMs, signal: context.signal }));
24
24
  if (!("toolResult" in result) && result.isError) {
25
25
  throw new ProviderRequestError(502, `Tongdaxin MCP tool ${toolName} returned an error`, result);
26
26
  }
@@ -48,7 +48,7 @@ for (const actionName of Object.keys(resolveNamedHandlers())) {
48
48
  tool.annotations.destructiveHint === true)) {
49
49
  throw new ProviderRequestError(403, `Tongdaxin MCP does not currently affirm ${tool.name} as a non-destructive read-only tool`);
50
50
  }
51
- const result = await withTongdaxinClient(context, "execute", (client) => client.callTool({ name: toolCall.toolName, arguments: toolCall.arguments }, { timeout: requestTimeoutMs, signal: context.signal }));
51
+ const result = await withTongdaxinClient(context, "execute", true, (client) => client.callTool({ name: toolCall.toolName, arguments: toolCall.arguments }, { timeout: requestTimeoutMs, signal: context.signal }));
52
52
  if (!("toolResult" in result) && result.isError) {
53
53
  throw new ProviderRequestError(502, `Tongdaxin MCP tool ${toolCall.toolName} returned an error`, result);
54
54
  }
@@ -78,7 +78,7 @@ export const credentialValidators = {
78
78
  },
79
79
  };
80
80
  async function discoverSupportedTools(context, phase) {
81
- const result = await withTongdaxinClient(context, phase, (client) => client.listTools({}, { timeout: requestTimeoutMs, signal: context.signal }));
81
+ const result = await withTongdaxinClient(context, phase, true, (client) => client.listTools({}, { timeout: requestTimeoutMs, signal: context.signal }));
82
82
  return result.tools.map((tool) => ({
83
83
  name: tool.name,
84
84
  description: tool.description,
@@ -86,7 +86,7 @@ async function discoverSupportedTools(context, phase) {
86
86
  inputSchema: tool.inputSchema,
87
87
  }));
88
88
  }
89
- async function withTongdaxinClient(context, phase, run) {
89
+ async function withTongdaxinClient(context, phase, retryOnSessionNotFound, run) {
90
90
  return withMcpClient({
91
91
  endpoint,
92
92
  transport: "streamable_http",
@@ -97,6 +97,7 @@ async function withTongdaxinClient(context, phase, run) {
97
97
  },
98
98
  redirect: "error",
99
99
  signal: context.signal,
100
+ retryOnSessionNotFound,
100
101
  mapError: (error) => mapTongdaxinError(error, phase),
101
102
  }, run);
102
103
  }
@@ -40,7 +40,7 @@ export function normalizeTongdaxinNamedActionOutput(actionName, result) {
40
40
  case "get_board_and_industry_data":
41
41
  case "get_hot_topics":
42
42
  case "get_hk_financials":
43
- return normalizeF10(resultObject, result);
43
+ return normalizeF10(resultObject);
44
44
  default:
45
45
  return assertNever(actionName);
46
46
  }
@@ -103,15 +103,9 @@ function normalizeSearch(resultObject, result) {
103
103
  result,
104
104
  };
105
105
  }
106
- function normalizeF10(resultObject, result) {
106
+ function normalizeF10(resultObject) {
107
107
  const response = requiredResponseRecord(resultObject.response, "Tongdaxin F10 response");
108
- const transformed = requiredResponseRecord(response.transformed, "Tongdaxin F10 transformed data");
109
- return {
110
- ok: optionalBoolean(resultObject.ok) ?? optionalBoolean(response.ok) ?? null,
111
- summary: asStringOrNull(transformed.summary ?? resultObject.summary),
112
- tables: requireResponseObjectArray(transformed.tables, "Tongdaxin F10 tables"),
113
- result,
114
- };
108
+ return response.transformed ?? null;
115
109
  }
116
110
  function assertNever(value) {
117
111
  throw providerResponseError(`unhandled Tongdaxin action output: ${value}`);
@@ -295,7 +295,7 @@ function capitalAndTradingArguments(input) {
295
295
  extra: input.date,
296
296
  };
297
297
  case "BLOCK_TRADE_INTENTION":
298
- return dataSection("TdxSharePCCW.tdxf10_gg_iyds", code, "yxsbxx");
298
+ return dataSection("TdxSharePCCW.tdxf10_gg_jyds", code, "yxsbxx");
299
299
  case "MARGIN_TRADING":
300
300
  return dataSection("TdxSharePCCW.tdxf10_gg_jyds", code, "rzrq");
301
301
  case "REFINANCING":
@@ -62,12 +62,7 @@ const searchOutputSchema = s.object("A normalized Tongdaxin current-information
62
62
  data: s.nullable(s.unknown("The provider-defined search rows returned by Wenda, or null when unavailable.")),
63
63
  result: rawResultSchema,
64
64
  });
65
- const f10OutputSchema = s.object("A normalized Tongdaxin F10 result.", {
66
- ok: s.nullableBoolean("Whether the underlying F10 request reported success."),
67
- summary: s.nullableString("The structured result summary, or null when unavailable."),
68
- tables: s.array("The formatted F10 result tables.", looseRowSchema),
69
- result: rawResultSchema,
70
- });
65
+ const f10OutputSchema = s.unknown("The transformed F10 data returned by Tongdaxin, preserved as any JSON value.");
71
66
  function defineReadAction(name, description, inputSchema, outputSchema = f10OutputSchema) {
72
67
  return defineProviderAction(service, {
73
68
  name,
@@ -57,9 +57,10 @@ async function listMetadataObjects(context, phase = "execute") {
57
57
  };
58
58
  }
59
59
  async function listRecords(input, context) {
60
+ const objectNamePlural = readRequiredString(input.objectNamePlural, "objectNamePlural");
60
61
  const payload = await requestTwentyCrmJson({
61
62
  method: "GET",
62
- path: `/rest/${encodeObjectName(readRequiredString(input.objectNamePlural, "objectNamePlural"))}`,
63
+ path: `/rest/${encodeObjectName(objectNamePlural)}`,
63
64
  query: compactObject({
64
65
  limit: optionalInteger(input.limit),
65
66
  starting_after: readOptionalString(input.startingAfter),
@@ -70,8 +71,9 @@ async function listRecords(input, context) {
70
71
  }),
71
72
  }, context);
72
73
  const record = requireProviderObject(payload, "Twenty CRM list records response");
74
+ const data = requireProviderObject(record.data, "Twenty CRM data");
73
75
  return {
74
- records: readArray(record, "data").map((item) => requireProviderObject(item, "Twenty CRM record")),
76
+ records: readArray(data, objectNamePlural).map((item) => requireProviderObject(item, "Twenty CRM record")),
75
77
  pageInfo: optionalRecord(record.pageInfo) ?? {},
76
78
  raw: record,
77
79
  };