@kadoa/mcp 0.3.10-rc.1 → 0.3.10-rc.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.
Files changed (2) hide show
  1. package/dist/index.js +205 -89
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -49167,13 +49167,37 @@ function createKadoaClient(auth) {
49167
49167
  });
49168
49168
  return client;
49169
49169
  }
49170
- var ctxRefreshMutex;
49170
+ var refreshRawMutex, ctxRefreshMutex;
49171
49171
  var init_client = __esm(() => {
49172
49172
  init_dist2();
49173
+ refreshRawMutex = new Map;
49173
49174
  ctxRefreshMutex = new WeakMap;
49174
49175
  });
49175
49176
 
49176
49177
  // src/client.ts
49178
+ var exports_client = {};
49179
+ __export(exports_client, {
49180
+ refreshSupabaseJwtRaw: () => refreshSupabaseJwtRaw,
49181
+ refreshSupabaseJwt: () => refreshSupabaseJwt,
49182
+ isJwtExpired: () => isJwtExpired,
49183
+ getValidJwt: () => getValidJwt,
49184
+ decodeJwtClaims: () => decodeJwtClaims,
49185
+ createKadoaClient: () => createKadoaClient2,
49186
+ SessionExpiredError: () => SessionExpiredError,
49187
+ KadoaSdkException: () => KadoaSdkException,
49188
+ KadoaClient: () => KadoaClient
49189
+ });
49190
+ function createKadoaClient2(auth) {
49191
+ const client = new KadoaClient({ bearerToken: auth.jwt });
49192
+ client.axiosInstance.interceptors.request.use((config2) => {
49193
+ config2.headers["x-kadoa-source"] = "mcp";
49194
+ if (auth.teamId) {
49195
+ config2.headers["x-team-id"] = auth.teamId;
49196
+ }
49197
+ return config2;
49198
+ });
49199
+ return client;
49200
+ }
49177
49201
  function decodeJwtClaims(jwt2) {
49178
49202
  try {
49179
49203
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
@@ -49195,36 +49219,57 @@ function isJwtExpired(jwt2) {
49195
49219
  return true;
49196
49220
  }
49197
49221
  }
49198
- async function refreshSupabaseJwt(ctx) {
49199
- if (!ctx.supabaseRefreshToken) {
49200
- console.error("[JWT_REFRESH] No refresh token available, cannot refresh");
49201
- return;
49222
+ async function refreshSupabaseJwtRaw(supabaseRefreshToken) {
49223
+ const inflight = refreshRawMutex2.get(supabaseRefreshToken);
49224
+ if (inflight) {
49225
+ console.error(`[JWT_REFRESH] DEDUP: reusing in-flight raw refresh`);
49226
+ return inflight;
49202
49227
  }
49228
+ const promise3 = _doRefreshRaw(supabaseRefreshToken).finally(() => {
49229
+ refreshRawMutex2.delete(supabaseRefreshToken);
49230
+ });
49231
+ refreshRawMutex2.set(supabaseRefreshToken, promise3);
49232
+ return promise3;
49233
+ }
49234
+ async function _doRefreshRaw(supabaseRefreshToken) {
49203
49235
  const supabaseUrl = process.env.SUPABASE_URL;
49204
49236
  if (!supabaseUrl) {
49205
49237
  console.error("[JWT_REFRESH] SUPABASE_URL not set, cannot refresh");
49238
+ return null;
49239
+ }
49240
+ const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
49241
+ method: "POST",
49242
+ headers: {
49243
+ "Content-Type": "application/json",
49244
+ apikey: process.env.SUPABASE_ANON_KEY
49245
+ },
49246
+ body: JSON.stringify({ refresh_token: supabaseRefreshToken })
49247
+ });
49248
+ if (res.ok) {
49249
+ const data = await res.json();
49250
+ return { jwt: data.access_token, refreshToken: data.refresh_token };
49251
+ }
49252
+ const body = await res.text().catch(() => "");
49253
+ console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status} (refreshToken=${supabaseRefreshToken.slice(0, 12)}...): ${body}`);
49254
+ if (body.includes("session_expired") || body.includes("refresh_token_not_found") || body.includes("refresh_token_already_used")) {
49255
+ throw new SessionExpiredError("Your Kadoa session has expired due to inactivity. Please reconnect to re-authenticate.");
49256
+ }
49257
+ return null;
49258
+ }
49259
+ async function refreshSupabaseJwt(ctx) {
49260
+ if (!ctx.supabaseRefreshToken) {
49261
+ console.error("[JWT_REFRESH] No refresh token available, cannot refresh");
49206
49262
  return;
49207
49263
  }
49208
49264
  try {
49209
49265
  const refreshToken = ctx.supabaseRefreshToken;
49210
49266
  console.error(`[JWT_REFRESH] Refreshing Supabase JWT (refreshToken=${refreshToken.slice(0, 12)}..., team=${ctx.teamId ?? "unknown"})`);
49211
- const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
49212
- method: "POST",
49213
- headers: {
49214
- "Content-Type": "application/json",
49215
- apikey: process.env.SUPABASE_ANON_KEY
49216
- },
49217
- body: JSON.stringify({ refresh_token: refreshToken })
49218
- });
49219
- if (!res.ok) {
49220
- const body = await res.text().catch(() => "");
49221
- console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status} (refreshToken=${refreshToken.slice(0, 12)}...): ${body}`);
49267
+ const result = await refreshSupabaseJwtRaw(refreshToken);
49268
+ if (!result)
49222
49269
  return;
49223
- }
49224
- const data = await res.json();
49225
- ctx.supabaseJwt = data.access_token;
49226
- ctx.supabaseRefreshToken = data.refresh_token;
49227
- ctx.client.setBearerToken(data.access_token);
49270
+ ctx.supabaseJwt = result.jwt;
49271
+ ctx.supabaseRefreshToken = result.refreshToken;
49272
+ ctx.client.setBearerToken(result.jwt);
49228
49273
  try {
49229
49274
  await ctx.persist?.({
49230
49275
  supabaseJwt: ctx.supabaseJwt,
@@ -49234,9 +49279,11 @@ async function refreshSupabaseJwt(ctx) {
49234
49279
  } catch (e) {
49235
49280
  console.error("[JWT_REFRESH] WARN: persist failed, tokens updated in-memory only:", e);
49236
49281
  }
49237
- console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${data.refresh_token.slice(0, 12)}...)`);
49238
- return data.access_token;
49282
+ console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${result.refreshToken.slice(0, 12)}...)`);
49283
+ return result.jwt;
49239
49284
  } catch (error48) {
49285
+ if (error48 instanceof SessionExpiredError)
49286
+ throw error48;
49240
49287
  console.error("[JWT_REFRESH] FAIL: threw", error48);
49241
49288
  return;
49242
49289
  }
@@ -49257,9 +49304,16 @@ async function getValidJwt(ctx) {
49257
49304
  ctxRefreshMutex2.set(ctx, promise3);
49258
49305
  return promise3;
49259
49306
  }
49260
- var ctxRefreshMutex2;
49307
+ var SessionExpiredError, refreshRawMutex2, ctxRefreshMutex2;
49261
49308
  var init_client2 = __esm(() => {
49262
49309
  init_dist2();
49310
+ SessionExpiredError = class SessionExpiredError extends Error {
49311
+ constructor(message) {
49312
+ super(message ?? "Supabase session expired. Please re-authenticate.");
49313
+ this.name = "SessionExpiredError";
49314
+ }
49315
+ };
49316
+ refreshRawMutex2 = new Map;
49263
49317
  ctxRefreshMutex2 = new WeakMap;
49264
49318
  });
49265
49319
 
@@ -49281,6 +49335,32 @@ function coerceArray(wrapPlainString = false) {
49281
49335
  return val;
49282
49336
  };
49283
49337
  }
49338
+ function coerceNumber() {
49339
+ return (val) => typeof val === "string" ? Number(val) : val;
49340
+ }
49341
+ function coerceBoolean() {
49342
+ return (val) => {
49343
+ if (typeof val === "string") {
49344
+ if (val === "true")
49345
+ return true;
49346
+ if (val === "false")
49347
+ return false;
49348
+ }
49349
+ return val;
49350
+ };
49351
+ }
49352
+ function coerceJson() {
49353
+ return (val) => {
49354
+ if (typeof val === "string") {
49355
+ try {
49356
+ const parsed = JSON.parse(val);
49357
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
49358
+ return parsed;
49359
+ } catch {}
49360
+ }
49361
+ return val;
49362
+ };
49363
+ }
49284
49364
  function isRealTimeInterval(interval) {
49285
49365
  return interval?.toUpperCase().replace("-", "_") === "REAL_TIME";
49286
49366
  }
@@ -49380,6 +49460,10 @@ function registerTools(server, ctx) {
49380
49460
  }
49381
49461
  return await handler(...args);
49382
49462
  } catch (error48) {
49463
+ if (error48 instanceof SessionExpiredError) {
49464
+ console.error(`[Tool Error] ${name}: session expired, user must re-authenticate`);
49465
+ return errorResult("Your session has expired. Please reconnect the MCP server to re-authenticate.");
49466
+ }
49383
49467
  let message = classifyError(error48);
49384
49468
  if (KadoaHttpException.isInstance(error48) && error48.httpStatus === 403) {
49385
49469
  try {
@@ -49435,7 +49519,7 @@ function registerTools(server, ctx) {
49435
49519
  headers: exports_external.object({}).passthrough().optional().describe("Custom headers as key-value pairs (required for 'header' type)")
49436
49520
  }).optional().describe("Authentication configuration for the webhook endpoint");
49437
49521
  const notificationsInputShape = {
49438
- notifications: exports_external.object({
49522
+ notifications: exports_external.preprocess(coerceJson(), exports_external.object({
49439
49523
  email: exports_external.object({
49440
49524
  recipients: exports_external.array(exports_external.string().email()).optional().describe("Email addresses to notify. Omit to use account default email.")
49441
49525
  }).optional().describe("Send email notifications. Pass {} for account default email, or {recipients: [...]} for custom addresses."),
@@ -49450,7 +49534,7 @@ function registerTools(server, ctx) {
49450
49534
  slackChannelName: exports_external.string().optional().describe("Slack channel name (e.g. #alerts)")
49451
49535
  }).optional().describe("Send notifications to a Slack channel. Use slackChannelId/slackChannelName for OAuth integration, or webhookUrl for legacy incoming webhooks."),
49452
49536
  websocket: exports_external.boolean().optional().describe("Enable WebSocket notifications for programmatic real-time consumption")
49453
- }).optional().describe("Notification channels to alert when data changes.")
49537
+ }).optional().describe("Notification channels to alert when data changes."))
49454
49538
  };
49455
49539
  function resolveUrls(args) {
49456
49540
  const urls = args.urls ?? (args.url ? [args.url] : []);
@@ -49529,7 +49613,10 @@ function registerTools(server, ctx) {
49529
49613
  inputSchema: strictSchema({
49530
49614
  ...extractionInputShape,
49531
49615
  ...urlInputShape,
49532
- interval: exports_external.enum([
49616
+ description: exports_external.string().max(500).optional().describe("Description of what this workflow does (max 500 characters)"),
49617
+ tags: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Tags for organizing workflows"),
49618
+ limit: exports_external.preprocess(coerceNumber(), exports_external.number()).optional().describe("Maximum number of records to extract per run. Useful for limiting scope or cost control."),
49619
+ updateInterval: exports_external.enum([
49533
49620
  "ONLY_ONCE",
49534
49621
  "EVERY_10_MINUTES",
49535
49622
  "HALF_HOURLY",
@@ -49545,8 +49632,10 @@ function registerTools(server, ctx) {
49545
49632
  "BIWEEKLY",
49546
49633
  "TRIWEEKLY",
49547
49634
  "FOUR_WEEKS",
49548
- "MONTHLY"
49549
- ]).optional().describe("How often the workflow runs. Defaults to ONLY_ONCE."),
49635
+ "MONTHLY",
49636
+ "CUSTOM"
49637
+ ]).optional().describe("How often the workflow runs. Defaults to ONLY_ONCE. Use CUSTOM with the 'schedules' field for cron-based scheduling."),
49638
+ schedules: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Cron expressions for CUSTOM updateInterval (e.g. '0 8 * * 2' for Tuesdays at 8am UTC). Requires updateInterval='CUSTOM'."),
49550
49639
  ...notificationsInputShape
49551
49640
  }),
49552
49641
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
@@ -49555,13 +49644,18 @@ function registerTools(server, ctx) {
49555
49644
  if (!urls) {
49556
49645
  return errorResult("At least one URL is required. Provide 'urls' (array) or 'url' (string).");
49557
49646
  }
49647
+ if (args.updateInterval === "CUSTOM" && (!args.schedules || args.schedules.length === 0)) {
49648
+ return errorResult("updateInterval='CUSTOM' requires at least one cron expression in the 'schedules' field (e.g. '0 8 * * 2' for Tuesdays at 8am UTC).");
49649
+ }
49558
49650
  let builder = ctx.client.extract({
49559
49651
  urls,
49560
49652
  name: args.name || "Untitled Workflow",
49561
49653
  navigationMode: "agentic-navigation",
49562
49654
  userPrompt: args.prompt,
49563
49655
  extraction: buildExtraction(args),
49564
- interval: args.interval
49656
+ interval: args.updateInterval,
49657
+ description: args.description,
49658
+ schedules: args.schedules
49565
49659
  });
49566
49660
  const n = args.notifications;
49567
49661
  const hasNotifications = n && (n.email || n.webhook || n.slack || n.websocket);
@@ -49572,6 +49666,15 @@ function registerTools(server, ctx) {
49572
49666
  });
49573
49667
  }
49574
49668
  const workflow = await builder.create();
49669
+ const needsUpdate = args.limit !== undefined || args.tags && args.tags.length > 0;
49670
+ if (needsUpdate) {
49671
+ const updates = {};
49672
+ if (args.limit !== undefined)
49673
+ updates.limit = args.limit;
49674
+ if (args.tags && args.tags.length > 0)
49675
+ updates.tags = args.tags;
49676
+ await ctx.client.workflow.update(workflow.workflowId, updates);
49677
+ }
49575
49678
  const enabledChannels = await describeNotifications(n);
49576
49679
  let message = "Workflow created successfully. The AI agent will start extracting data automatically.";
49577
49680
  if (enabledChannels.length > 0) {
@@ -49592,6 +49695,8 @@ function registerTools(server, ctx) {
49592
49695
  inputSchema: strictSchema({
49593
49696
  ...extractionInputShape,
49594
49697
  ...urlInputShape,
49698
+ description: exports_external.string().max(500).optional().describe("Description of what this monitor watches (max 500 characters)"),
49699
+ tags: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Tags for organizing monitors"),
49595
49700
  ...notificationsInputShape
49596
49701
  }),
49597
49702
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
@@ -49610,13 +49715,17 @@ function registerTools(server, ctx) {
49610
49715
  name: args.name || "Untitled Monitor",
49611
49716
  userPrompt: args.prompt,
49612
49717
  extraction: buildExtraction(args),
49613
- interval: "REAL_TIME"
49718
+ interval: "REAL_TIME",
49719
+ description: args.description
49614
49720
  });
49615
49721
  builder = builder.withNotifications({
49616
49722
  events: ["workflow_data_change"],
49617
49723
  channels: buildNotificationChannels(n)
49618
49724
  });
49619
49725
  const workflow = await builder.create();
49726
+ if (args.tags && args.tags.length > 0) {
49727
+ await ctx.client.workflow.update(workflow.workflowId, { tags: args.tags });
49728
+ }
49620
49729
  const enabledChannels = await describeNotifications(n);
49621
49730
  let message = "Real-time monitor created successfully. It will continuously watch for changes — no manual runs needed.";
49622
49731
  if (enabledChannels.length > 0) {
@@ -49633,7 +49742,7 @@ function registerTools(server, ctx) {
49633
49742
  server.registerTool("list_workflows", {
49634
49743
  description: "List all workflows with their current status",
49635
49744
  inputSchema: {
49636
- limit: exports_external.number().optional().describe("Maximum number of workflows to return"),
49745
+ limit: exports_external.preprocess(coerceNumber(), exports_external.number()).optional().describe("Maximum number of workflows to return"),
49637
49746
  state: exports_external.enum(["ACTIVE", "FAILED", "PAUSED", "PREVIEW"]).optional().describe("Filter by state (ACTIVE, FAILED, PAUSED, PREVIEW)")
49638
49747
  },
49639
49748
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
@@ -49694,7 +49803,7 @@ function registerTools(server, ctx) {
49694
49803
  description: "Run a workflow to extract fresh data. The run is asynchronous and may take several minutes. Do NOT poll or sleep-wait for completion. Return the workflow ID to the user and let them check status with get_workflow or fetch results later with fetch_data.",
49695
49804
  inputSchema: {
49696
49805
  workflowId: exports_external.string().describe("The workflow ID to run"),
49697
- limit: exports_external.number().optional().describe("Maximum number of records to extract (default: 1000)")
49806
+ limit: exports_external.preprocess(coerceNumber(), exports_external.number()).optional().describe("Maximum number of records to extract (default: 1000)")
49698
49807
  },
49699
49808
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
49700
49809
  }, withErrorHandling("run_workflow", async (args) => {
@@ -49715,8 +49824,8 @@ function registerTools(server, ctx) {
49715
49824
  description: "Get extracted data from a workflow. Data is only available after the workflow run has completed (displayState is no longer RUNNING). Do NOT poll or sleep-wait for completion.",
49716
49825
  inputSchema: {
49717
49826
  workflowId: exports_external.string().describe("The workflow ID"),
49718
- limit: exports_external.number().optional().describe("Maximum number of records to return"),
49719
- page: exports_external.number().optional().describe("Page number for pagination")
49827
+ limit: exports_external.preprocess(coerceNumber(), exports_external.number()).optional().describe("Maximum number of records to return"),
49828
+ page: exports_external.preprocess(coerceNumber(), exports_external.number()).optional().describe("Page number for pagination")
49720
49829
  },
49721
49830
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
49722
49831
  }, withErrorHandling("fetch_data", async (args) => {
@@ -49735,7 +49844,7 @@ function registerTools(server, ctx) {
49735
49844
  description: "Delete a workflow permanently. This is irreversible. " + "You MUST first call this tool without confirmed=true to preview what will be deleted, " + "then show the user the workflow name and ask for confirmation, " + "then call again with confirmed=true only after the user explicitly agrees.",
49736
49845
  inputSchema: {
49737
49846
  workflowId: exports_external.string().describe("The workflow ID to delete"),
49738
- confirmed: exports_external.boolean().optional().describe("Set to true only after the user has explicitly confirmed deletion. Omit or set to false for the initial preview call.")
49847
+ confirmed: exports_external.preprocess(coerceBoolean(), exports_external.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion. Omit or set to false for the initial preview call.")
49739
49848
  },
49740
49849
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true }
49741
49850
  }, withErrorHandling("delete_workflow", async (args) => {
@@ -49785,7 +49894,7 @@ function registerTools(server, ctx) {
49785
49894
  urls: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string()).min(1)).optional().describe("New target URLs for the workflow (array of strings). Also accepts a single URL string."),
49786
49895
  entity: exports_external.string().optional().describe("Entity name for extraction (e.g., 'Product', 'Job Posting')"),
49787
49896
  schema: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.object(SchemaFieldShape))).optional().describe("New extraction schema fields"),
49788
- description: exports_external.string().optional().describe("Workflow description"),
49897
+ description: exports_external.string().max(500).optional().describe("Workflow description (max 500 characters)"),
49789
49898
  tags: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Tags for organizing workflows"),
49790
49899
  userPrompt: exports_external.string().optional().describe("Navigation prompt for agentic-navigation mode (10-5000 characters)"),
49791
49900
  updateInterval: exports_external.enum([
@@ -49808,7 +49917,7 @@ function registerTools(server, ctx) {
49808
49917
  "CUSTOM"
49809
49918
  ]).optional().describe("How often the workflow should run. CUSTOM requires schedules. Note: REAL_TIME is NOT allowed here — real-time workflows must be created via create_workflow."),
49810
49919
  schedules: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Cron expressions for CUSTOM update interval"),
49811
- limit: exports_external.number().optional().describe("Maximum number of records to extract per run")
49920
+ limit: exports_external.preprocess(coerceNumber(), exports_external.number()).optional().describe("Maximum number of records to extract per run")
49812
49921
  }),
49813
49922
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
49814
49923
  }, withErrorHandling("update_workflow", async (args) => {
@@ -49824,6 +49933,9 @@ function registerTools(server, ctx) {
49824
49933
  return errorResult("Cannot change a real-time workflow's interval to a scheduled interval. " + "Real-time workflows are architecturally different and cannot be converted to regular workflows. " + "Delete this workflow and create a new one with the desired interval using create_workflow.");
49825
49934
  }
49826
49935
  }
49936
+ if (updates.updateInterval === "CUSTOM" && (!updates.schedules || updates.schedules.length === 0)) {
49937
+ return errorResult("updateInterval='CUSTOM' requires at least one cron expression in the 'schedules' field (e.g. '0 8 * * 2' for Tuesdays at 8am UTC).");
49938
+ }
49827
49939
  const result = await ctx.client.workflow.update(workflowId, updates);
49828
49940
  return jsonResult({
49829
49941
  success: true,
@@ -49894,7 +50006,7 @@ function registerTools(server, ctx) {
49894
50006
  description: "Delete a notification channel by ID. " + "Call first without confirmed=true to preview, then with confirmed=true after user agrees.",
49895
50007
  inputSchema: {
49896
50008
  channelId: exports_external.string().describe("The channel ID to delete"),
49897
- confirmed: exports_external.boolean().optional().describe("Set to true only after the user has explicitly confirmed deletion.")
50009
+ confirmed: exports_external.preprocess(coerceBoolean(), exports_external.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion.")
49898
50010
  },
49899
50011
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true }
49900
50012
  }, withErrorHandling("delete_notification_channel", async (args) => {
@@ -50032,7 +50144,7 @@ function registerTools(server, ctx) {
50032
50144
  description: "Delete a notification setting by ID. This removes the event-to-channel mapping but does not delete the channels themselves. " + "Call first without confirmed=true to preview, then with confirmed=true after user agrees.",
50033
50145
  inputSchema: {
50034
50146
  settingsId: exports_external.string().describe("The notification settings ID to delete"),
50035
- confirmed: exports_external.boolean().optional().describe("Set to true only after the user has explicitly confirmed deletion.")
50147
+ confirmed: exports_external.preprocess(coerceBoolean(), exports_external.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion.")
50036
50148
  },
50037
50149
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true }
50038
50150
  }, withErrorHandling("delete_notification_setting", async (args) => {
@@ -50097,7 +50209,7 @@ function registerTools(server, ctx) {
50097
50209
  });
50098
50210
  }));
50099
50211
  }
50100
- var SchemaFieldShape, DASHBOARD_BASE_URL = "https://app.kadoa.com";
50212
+ var SchemaFieldShape, DASHBOARD_BASE_URL = "https://www.kadoa.com";
50101
50213
  var init_tools = __esm(() => {
50102
50214
  init_zod();
50103
50215
  init_dist2();
@@ -54108,45 +54220,6 @@ function generatePKCE() {
54108
54220
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
54109
54221
  return { verifier, challenge };
54110
54222
  }
54111
- async function refreshSupabaseToken(supabaseRefreshToken, context) {
54112
- const inflight = supabaseRefreshMutex.get(supabaseRefreshToken);
54113
- if (inflight) {
54114
- console.error(`[AUTH] REFRESH_DEDUP: reusing in-flight refresh (${context})`);
54115
- return inflight;
54116
- }
54117
- const promise3 = (async () => {
54118
- const supabaseUrl = process.env.SUPABASE_URL;
54119
- if (!supabaseUrl || !supabaseRefreshToken)
54120
- return null;
54121
- try {
54122
- const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
54123
- method: "POST",
54124
- headers: {
54125
- "Content-Type": "application/json",
54126
- apikey: process.env.SUPABASE_ANON_KEY
54127
- },
54128
- body: JSON.stringify({ refresh_token: supabaseRefreshToken })
54129
- });
54130
- if (res.ok) {
54131
- const data = await res.json();
54132
- const newClaims = jwtClaims(data.access_token);
54133
- console.log(`[AUTH] REFRESH_OK: Supabase JWT refreshed (${context}, newEmail=${newClaims.email})`);
54134
- return { jwt: data.access_token, refreshToken: data.refresh_token };
54135
- }
54136
- const body = await res.text().catch(() => "");
54137
- console.error(`[AUTH] REFRESH_FAIL: Supabase returned ${res.status} (${context}): ${body.slice(0, 200)}`);
54138
- return null;
54139
- } catch (err) {
54140
- console.error(`[AUTH] REFRESH_FAIL: Supabase refresh threw (${context}):`, err);
54141
- return null;
54142
- }
54143
- })();
54144
- supabaseRefreshMutex.set(supabaseRefreshToken, promise3);
54145
- promise3.finally(() => {
54146
- supabaseRefreshMutex.delete(supabaseRefreshToken);
54147
- });
54148
- return promise3;
54149
- }
54150
54223
  function jwtClaims(jwt2) {
54151
54224
  try {
54152
54225
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
@@ -54454,12 +54527,22 @@ class KadoaOAuthProvider {
54454
54527
  let { supabaseJwt, supabaseRefreshToken } = entry;
54455
54528
  const claims = jwtClaims(entry.supabaseJwt);
54456
54529
  const context = `email=${claims.email}, team=${entry.teamId}`;
54457
- const refreshed = await refreshSupabaseToken(supabaseRefreshToken, context);
54458
- if (refreshed) {
54459
- supabaseJwt = refreshed.jwt;
54460
- supabaseRefreshToken = refreshed.refreshToken;
54461
- } else {
54462
- console.error(`[AUTH] REFRESH_WARN: using stale Supabase JWT as fallback (${context})`);
54530
+ try {
54531
+ const { refreshSupabaseJwtRaw: refreshSupabaseJwtRaw2, SessionExpiredError: SessionExpiredError2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
54532
+ const refreshed = await refreshSupabaseJwtRaw2(supabaseRefreshToken);
54533
+ if (refreshed) {
54534
+ supabaseJwt = refreshed.jwt;
54535
+ supabaseRefreshToken = refreshed.refreshToken;
54536
+ console.error(`[AUTH] REFRESH_OK: Supabase JWT refreshed (${context})`);
54537
+ } else {
54538
+ console.error(`[AUTH] REFRESH_WARN: using stale Supabase JWT as fallback (${context})`);
54539
+ }
54540
+ } catch (error48) {
54541
+ if (error48 instanceof Error && error48.name === "SessionExpiredError") {
54542
+ console.error(`[AUTH] REFRESH_DEAD: session permanently expired (${context}): ${error48.message}`);
54543
+ throw new InvalidTokenError("Supabase session expired. Please re-authenticate.");
54544
+ }
54545
+ console.error(`[AUTH] REFRESH_WARN: unexpected error, using stale JWT (${context}):`, error48);
54463
54546
  }
54464
54547
  const freshClaims = jwtClaims(supabaseJwt);
54465
54548
  const teamId = freshClaims.activeTeamId ?? entry.teamId;
@@ -54586,7 +54669,7 @@ class KadoaOAuthProvider {
54586
54669
  codeChallenge: pending.params.codeChallenge,
54587
54670
  clientId: pending.client.client_id,
54588
54671
  redirectUri: pending.params.redirectUri,
54589
- expiresAt: Date.now() + 10 * 60 * 1000
54672
+ expiresAt: Date.now() + 600000
54590
54673
  }, 600);
54591
54674
  const redirectUrl = new URL(pending.params.redirectUri);
54592
54675
  redirectUrl.searchParams.set("code", mcpCode);
@@ -55015,12 +55098,11 @@ function renderLoginPage(state, error48) {
55015
55098
  function escapeHtml(str) {
55016
55099
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
55017
55100
  }
55018
- var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL, supabaseRefreshMutex;
55101
+ var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
55019
55102
  var init_auth2 = __esm(() => {
55020
55103
  init_errors4();
55021
55104
  TEAM_SELECTION_TTL = 10 * 60 * 1000;
55022
55105
  ACCESS_TOKEN_TTL = 7 * 24 * 3600;
55023
- supabaseRefreshMutex = new Map;
55024
55106
  });
55025
55107
 
55026
55108
  // src/redis-store.ts
@@ -55213,6 +55295,39 @@ async function startHttpServer(options) {
55213
55295
  return;
55214
55296
  }
55215
55297
  const identity = `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...`;
55298
+ if (isJwtExpired(auth.jwt)) {
55299
+ try {
55300
+ const refreshed = await refreshSupabaseJwtRaw(auth.refreshToken);
55301
+ if (refreshed) {
55302
+ auth.jwt = refreshed.jwt;
55303
+ auth.refreshToken = refreshed.refreshToken;
55304
+ const entry = await store.get("access_tokens", auth.mcpToken);
55305
+ if (entry) {
55306
+ const remainingMs = entry.expiresAt - Date.now();
55307
+ if (remainingMs > 0) {
55308
+ await store.set("access_tokens", auth.mcpToken, {
55309
+ ...entry,
55310
+ supabaseJwt: refreshed.jwt,
55311
+ supabaseRefreshToken: refreshed.refreshToken
55312
+ }, Math.ceil(remainingMs / 1000));
55313
+ console.error(`[PROACTIVE_REFRESH] OK: JWT refreshed on ${method} (${identity})`);
55314
+ }
55315
+ }
55316
+ }
55317
+ } catch (error48) {
55318
+ if (error48 instanceof SessionExpiredError) {
55319
+ console.error(`[PROACTIVE_REFRESH] Session dead on ${method} (${identity}): ${error48.message}`);
55320
+ await store.del("access_tokens", auth.mcpToken);
55321
+ res.status(401).json({
55322
+ jsonrpc: "2.0",
55323
+ error: { code: -32001, message: error48.message },
55324
+ id: req.body?.id ?? null
55325
+ });
55326
+ return;
55327
+ }
55328
+ console.error(`[PROACTIVE_REFRESH] WARN: refresh failed on ${method}, continuing with stale JWT`, error48);
55329
+ }
55330
+ }
55216
55331
  try {
55217
55332
  console.error(`[MCP] POST method=${method} auth=${identity}`);
55218
55333
  const transport = new StreamableHTTPServerTransport({
@@ -55290,6 +55405,7 @@ var init_http2 = __esm(async () => {
55290
55405
  init_bearerAuth();
55291
55406
  init_auth2();
55292
55407
  init_redis_store();
55408
+ init_client2();
55293
55409
  await init_src();
55294
55410
  });
55295
55411
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.3.10-rc.1",
3
+ "version": "0.3.10-rc.3",
4
4
  "description": "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,7 +24,7 @@
24
24
  "prepublishOnly": "bun run check-types && bun run test:unit && bun run build"
25
25
  },
26
26
  "dependencies": {
27
- "@kadoa/node-sdk": "^0.24.2",
27
+ "@kadoa/node-sdk": "^0.23.0",
28
28
  "@modelcontextprotocol/sdk": "^1.26.0",
29
29
  "express": "^5.2.1",
30
30
  "ioredis": "^5.6.1",