@kadoa/mcp 0.5.4 → 0.5.7

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 (3) hide show
  1. package/README.md +11 -0
  2. package/dist/index.js +832 -89
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -85,6 +85,17 @@ Claude checks the run status with get_workflow, then calls fetch_data
85
85
  to retrieve the extracted records and display them as a table.
86
86
  ```
87
87
 
88
+ ### Create a workflow from a template
89
+
90
+ ```
91
+ > You: Use my "Product Scraper" template to scrape https://example-shop.com.
92
+
93
+ Claude calls list_templates to find the matching template, then
94
+ create_workflow with `templateId` and `urls` only — the prompt and
95
+ schema are inherited from the template version. Returns the workflow
96
+ ID for follow-up with get_workflow or fetch_data.
97
+ ```
98
+
88
99
  ### Update a workflow and re-run
89
100
 
90
101
  ```
package/dist/index.js CHANGED
@@ -14552,7 +14552,7 @@ function finalize(ctx, schema) {
14552
14552
  result.$schema = "http://json-schema.org/draft-07/schema#";
14553
14553
  } else if (ctx.target === "draft-04") {
14554
14554
  result.$schema = "http://json-schema.org/draft-04/schema#";
14555
- } else if (ctx.target === "openapi-3.0") {} else {}
14555
+ } else if (ctx.target === "openapi-3.0") {}
14556
14556
  if (ctx.external?.uri) {
14557
14557
  const id = ctx.external.registry.get(schema)?.id;
14558
14558
  if (!id)
@@ -14817,7 +14817,7 @@ var formatMap, stringProcessor = (schema, ctx, _json, _params) => {
14817
14817
  if (val === undefined) {
14818
14818
  if (ctx.unrepresentable === "throw") {
14819
14819
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
14820
- } else {}
14820
+ }
14821
14821
  } else if (typeof val === "bigint") {
14822
14822
  if (ctx.unrepresentable === "throw") {
14823
14823
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -44498,9 +44498,9 @@ function createClientDomains(params) {
44498
44498
  const dataFetcherService = new DataFetcherService(client.apis.workflows);
44499
44499
  const channelsService = new NotificationChannelsService(client.apis.notifications, userService);
44500
44500
  const settingsService = new NotificationSettingsService(client.apis.notifications);
44501
- const workflowsCoreService = new WorkflowsCoreService(client.apis.workflows);
44502
44501
  const schemasService = new SchemasService(client);
44503
44502
  const templatesService = new TemplatesService(client);
44503
+ const workflowsCoreService = new WorkflowsCoreService(client.apis.workflows, templatesService);
44504
44504
  const variablesService = new VariablesService(client);
44505
44505
  const channelSetupService = new NotificationSetupService(channelsService, settingsService);
44506
44506
  const coreService = new ValidationCoreService(client);
@@ -48714,7 +48714,7 @@ var import_debug, __require2, ChangeDifferenceType, KadoaErrorCode, _KadoaSdkExc
48714
48714
  }));
48715
48715
  return channels;
48716
48716
  }
48717
- }, PUBLIC_API_URI, WSS_API_URI, REALTIME_API_URI, SDK_VERSION = "0.32.0", SDK_NAME = "kadoa-node-sdk", SDK_LANGUAGE = "node", debug6, isDrainControlMessage = (message) => message.type === "control.draining", isRealtimeEvent = (message) => message.type !== "heartbeat" && message.type !== "control.draining", _Realtime = class _Realtime2 {
48717
+ }, PUBLIC_API_URI, WSS_API_URI, REALTIME_API_URI, SDK_VERSION = "0.33.0", SDK_NAME = "kadoa-node-sdk", SDK_LANGUAGE = "node", debug6, isDrainControlMessage = (message) => message.type === "control.draining", isRealtimeEvent = (message) => message.type !== "heartbeat" && message.type !== "control.draining", _Realtime = class _Realtime2 {
48718
48718
  constructor(config2) {
48719
48719
  this.drainingSockets = /* @__PURE__ */ new Set;
48720
48720
  this.lastHeartbeat = Date.now();
@@ -49443,36 +49443,82 @@ var import_debug, __require2, ChangeDifferenceType, KadoaErrorCode, _KadoaSdkExc
49443
49443
  });
49444
49444
  }
49445
49445
  }, JobStateEnum, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES2, debug9, WorkflowsCoreService = class {
49446
- constructor(workflowsApi) {
49446
+ constructor(workflowsApi, templatesService) {
49447
49447
  this.workflowsApi = workflowsApi;
49448
+ this.templatesService = templatesService;
49448
49449
  }
49449
49450
  async create(input) {
49450
49451
  validateAdditionalData(input.additionalData);
49451
- if (!input.userPrompt) {
49452
+ const isFromTemplate = input.templateId != null;
49453
+ if (isFromTemplate) {
49454
+ const conflicting = [];
49455
+ if (input.userPrompt != null)
49456
+ conflicting.push("userPrompt");
49457
+ if (input.entity != null)
49458
+ conflicting.push("entity");
49459
+ if (input.fields != null)
49460
+ conflicting.push("fields");
49461
+ if (input.schemaId != null)
49462
+ conflicting.push("schemaId");
49463
+ if (input.monitoring != null)
49464
+ conflicting.push("monitoring");
49465
+ if (input.navigationMode != null)
49466
+ conflicting.push("navigationMode");
49467
+ if (conflicting.length > 0) {
49468
+ throw new KadoaSdkException(`Fields are defined by the template and cannot be supplied when creating from a template: ${conflicting.join(", ")}`, {
49469
+ code: "VALIDATION_ERROR",
49470
+ details: { conflicting }
49471
+ });
49472
+ }
49473
+ } else if (!input.userPrompt) {
49452
49474
  throw new KadoaSdkException("userPrompt is required to create a workflow", {
49453
49475
  code: "VALIDATION_ERROR",
49454
49476
  details: { urls: input.urls }
49455
49477
  });
49456
49478
  }
49457
49479
  const domainName = new URL(input.urls[0]).hostname;
49458
- const request = {
49459
- urls: input.urls,
49460
- name: input.name ?? domainName,
49461
- description: input.description,
49462
- userPrompt: input.userPrompt,
49463
- navigationMode: "agentic-navigation",
49464
- schemaId: input.schemaId,
49465
- ...input.entity != null && { entity: input.entity },
49466
- fields: input.fields,
49467
- bypassPreview: input.bypassPreview ?? true,
49468
- tags: input.tags,
49469
- interval: input.interval,
49470
- monitoring: input.monitoring,
49471
- location: input.location,
49472
- schedules: input.schedules,
49473
- additionalData: input.additionalData,
49474
- limit: input.limit
49475
- };
49480
+ let request;
49481
+ if (isFromTemplate) {
49482
+ const templateId = input.templateId;
49483
+ const templateVersion = input.templateVersion ?? await this.resolveLatestVersion(templateId);
49484
+ request = {
49485
+ urls: input.urls,
49486
+ templateId,
49487
+ templateVersion,
49488
+ ...input.name != null && { name: input.name },
49489
+ ...input.description != null && { description: input.description },
49490
+ ...input.tags != null && { tags: input.tags },
49491
+ ...input.interval != null && { interval: input.interval },
49492
+ ...input.schedules != null && { schedules: input.schedules },
49493
+ ...input.location != null && { location: input.location },
49494
+ ...input.bypassPreview != null && {
49495
+ bypassPreview: input.bypassPreview
49496
+ },
49497
+ ...input.additionalData != null && {
49498
+ additionalData: input.additionalData
49499
+ },
49500
+ ...input.limit != null && { limit: input.limit }
49501
+ };
49502
+ } else {
49503
+ request = {
49504
+ urls: input.urls,
49505
+ name: input.name ?? domainName,
49506
+ description: input.description,
49507
+ userPrompt: input.userPrompt,
49508
+ navigationMode: "agentic-navigation",
49509
+ schemaId: input.schemaId,
49510
+ ...input.entity != null && { entity: input.entity },
49511
+ fields: input.fields,
49512
+ bypassPreview: input.bypassPreview ?? true,
49513
+ tags: input.tags,
49514
+ interval: input.interval,
49515
+ monitoring: input.monitoring,
49516
+ location: input.location,
49517
+ schedules: input.schedules,
49518
+ additionalData: input.additionalData,
49519
+ limit: input.limit
49520
+ };
49521
+ }
49476
49522
  const response = await this.workflowsApi.v4WorkflowsPost({
49477
49523
  publicWorkflowCreateRequest: request
49478
49524
  });
@@ -49487,6 +49533,23 @@ var import_debug, __require2, ChangeDifferenceType, KadoaErrorCode, _KadoaSdkExc
49487
49533
  }
49488
49534
  return { id: workflowId };
49489
49535
  }
49536
+ async resolveLatestVersion(templateId) {
49537
+ if (!this.templatesService) {
49538
+ throw new KadoaSdkException("TemplatesService is required to resolve a template's latest version. Pass `templateVersion` explicitly or construct WorkflowsCoreService with a TemplatesService.", {
49539
+ code: "INTERNAL_ERROR",
49540
+ details: { templateId }
49541
+ });
49542
+ }
49543
+ const template = await this.templatesService.get(templateId);
49544
+ const latest = template.latestVersion;
49545
+ if (latest == null) {
49546
+ throw new KadoaSdkException(`Template ${templateId} has no published versions; supply templateVersion explicitly or publish a version first.`, {
49547
+ code: "VALIDATION_ERROR",
49548
+ details: { templateId }
49549
+ });
49550
+ }
49551
+ return latest;
49552
+ }
49490
49553
  async get(id) {
49491
49554
  const response = await this.workflowsApi.v4WorkflowsWorkflowIdGet({
49492
49555
  workflowId: id
@@ -50848,7 +50911,7 @@ function registerTools(server, ctx) {
50848
50911
  urls: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string()).min(1)).optional().describe("Starting URLs for the workflow (array of strings). Also accepts a single URL string.")
50849
50912
  };
50850
50913
  const extractionInputShape = {
50851
- prompt: exports_external.string().describe('Natural language description of what to extract (e.g., "Extract product prices and names")'),
50914
+ prompt: exports_external.string().optional().describe('Natural language description of what to extract (e.g., "Extract product prices and names"). Required unless templateId is provided.'),
50852
50915
  name: exports_external.string().optional().describe("Optional name for the workflow"),
50853
50916
  entity: exports_external.string().optional().describe("Entity name for extraction (e.g., 'Product', 'Job Posting')"),
50854
50917
  schema: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.object(SchemaFieldShape))).optional().describe("Extraction schema fields. If omitted, the AI agent auto-detects the schema.")
@@ -50953,10 +51016,12 @@ function registerTools(server, ctx) {
50953
51016
 
50954
51017
  ` + "Create a data extraction workflow using agentic navigation. Supports one-time or scheduled runs. " + "If entity and schema are provided, they guide the extraction; otherwise the AI agent auto-detects the schema from the page. " + "The workflow runs asynchronously and may take several minutes. Do NOT poll or sleep-wait for completion. " + `Return the workflow ID to the user and let them check back later with get_workflow or fetch_data.
50955
51018
 
50956
- ` + "NOTE: This tool is for one-time or scheduled extraction ONLY. " + "For continuous real-time monitoring (watching a page for changes and alerting), use the create_realtime_monitor tool instead.",
51019
+ ` + "PREFER TEMPLATES: If the user's request matches an existing template, instantiate it via `templateId` instead of writing a fresh prompt/schema. " + "Use `list_templates` to discover available templates and `get_template` to inspect schemas before deciding. " + "When `templateId` is set, only `urls` is required — `prompt`, `entity`, and `schema` must NOT be supplied; they are inherited from the template version.\n\n" + "NOTE: This tool is for one-time or scheduled extraction ONLY. " + "For continuous real-time monitoring (watching a page for changes and alerting), use the create_realtime_monitor tool instead.",
50957
51020
  inputSchema: strictSchema({
50958
51021
  ...extractionInputShape,
50959
51022
  ...urlInputShape,
51023
+ templateId: exports_external.string().optional().describe("Instantiate this workflow from a published template. When set, only 'urls' is required — prompt/entity/schema must NOT be supplied; they are inherited from the template version. Discover templates via list_templates."),
51024
+ templateVersion: exports_external.preprocess(coerceNumber(), exports_external.number()).optional().describe("Specific published template version (integer) to instantiate. Defaults to the latest published version when templateId is set."),
50960
51025
  description: exports_external.string().max(500).optional().describe("Description of what this workflow does (max 500 characters)"),
50961
51026
  tags: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Tags for organizing workflows"),
50962
51027
  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."),
@@ -50991,32 +51056,62 @@ function registerTools(server, ctx) {
50991
51056
  if (args.updateInterval === "CUSTOM" && (!args.schedules || args.schedules.length === 0)) {
50992
51057
  return errorResult("updateInterval='CUSTOM' requires at least one cron expression in the 'schedules' field (e.g. '0 8 * * 2' for Tuesdays at 8am UTC).");
50993
51058
  }
50994
- let builder = ctx.client.extract({
50995
- urls,
50996
- name: args.name || "Untitled Workflow",
50997
- userPrompt: args.prompt,
50998
- extraction: buildExtraction(args),
50999
- interval: args.updateInterval,
51000
- description: args.description,
51001
- schedules: args.schedules
51002
- });
51003
51059
  const n = args.notifications;
51004
- const hasNotifications = n && (n.email || n.webhook || n.slack || n.websocket);
51005
- if (hasNotifications) {
51006
- builder = builder.withNotifications({
51007
- events: ["workflow_data_change"],
51008
- channels: buildNotificationChannels(n)
51060
+ const hasNotifications = !!(n && (n.email || n.webhook || n.slack || n.websocket));
51061
+ let workflowId;
51062
+ if (args.templateId) {
51063
+ if (args.prompt || args.entity || args.schema) {
51064
+ return errorResult("When 'templateId' is set, 'prompt', 'entity', and 'schema' must NOT be supplied — they are inherited from the template version.");
51065
+ }
51066
+ const { id } = await ctx.client.workflow.create({
51067
+ urls,
51068
+ name: args.name || "Untitled Workflow",
51069
+ description: args.description,
51070
+ interval: args.updateInterval,
51071
+ schedules: args.schedules,
51072
+ tags: args.tags && args.tags.length > 0 ? args.tags : undefined,
51073
+ limit: args.limit,
51074
+ templateId: args.templateId,
51075
+ templateVersion: args.templateVersion
51009
51076
  });
51010
- }
51011
- const workflow = await builder.create();
51012
- const needsUpdate = args.limit !== undefined || args.tags && args.tags.length > 0;
51013
- if (needsUpdate) {
51014
- const updates = {};
51015
- if (args.limit !== undefined)
51016
- updates.limit = args.limit;
51017
- if (args.tags && args.tags.length > 0)
51018
- updates.tags = args.tags;
51019
- await ctx.client.workflow.update(workflow.workflowId, updates);
51077
+ workflowId = id;
51078
+ if (hasNotifications) {
51079
+ await ctx.client.notification.configure({
51080
+ workflowId,
51081
+ events: ["workflow_data_change"],
51082
+ channels: buildNotificationChannels(n)
51083
+ });
51084
+ }
51085
+ } else {
51086
+ if (!args.prompt) {
51087
+ return errorResult("'prompt' is required unless 'templateId' is provided.");
51088
+ }
51089
+ let builder = ctx.client.extract({
51090
+ urls,
51091
+ name: args.name || "Untitled Workflow",
51092
+ userPrompt: args.prompt,
51093
+ extraction: buildExtraction(args),
51094
+ interval: args.updateInterval,
51095
+ description: args.description,
51096
+ schedules: args.schedules
51097
+ });
51098
+ if (hasNotifications) {
51099
+ builder = builder.withNotifications({
51100
+ events: ["workflow_data_change"],
51101
+ channels: buildNotificationChannels(n)
51102
+ });
51103
+ }
51104
+ const workflow = await builder.create();
51105
+ workflowId = workflow.workflowId;
51106
+ const needsUpdate = args.limit !== undefined || args.tags && args.tags.length > 0;
51107
+ if (needsUpdate) {
51108
+ const updates = {};
51109
+ if (args.limit !== undefined)
51110
+ updates.limit = args.limit;
51111
+ if (args.tags && args.tags.length > 0)
51112
+ updates.tags = args.tags;
51113
+ await ctx.client.workflow.update(workflowId, updates);
51114
+ }
51020
51115
  }
51021
51116
  const enabledChannels = await describeNotifications(n);
51022
51117
  let message = "Workflow created successfully. The AI agent will start extracting data automatically.";
@@ -51026,8 +51121,8 @@ function registerTools(server, ctx) {
51026
51121
  message += " Use fetch_data to retrieve the latest extracted results.";
51027
51122
  return jsonResult({
51028
51123
  success: true,
51029
- workflowId: workflow.workflowId,
51030
- dashboardUrl: workflowDashboardUrl(workflow.workflowId),
51124
+ workflowId,
51125
+ dashboardUrl: workflowDashboardUrl(workflowId),
51031
51126
  message
51032
51127
  });
51033
51128
  }));
@@ -51987,7 +52082,7 @@ var package_default;
51987
52082
  var init_package = __esm(() => {
51988
52083
  package_default = {
51989
52084
  name: "@kadoa/mcp",
51990
- version: "0.5.4",
52085
+ version: "0.5.7",
51991
52086
  description: "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
51992
52087
  type: "module",
51993
52088
  main: "dist/index.js",
@@ -52011,7 +52106,7 @@ var init_package = __esm(() => {
52011
52106
  prepublishOnly: "bun run check-types && bun run test:unit && bun run build"
52012
52107
  },
52013
52108
  dependencies: {
52014
- "@kadoa/node-sdk": "^0.32.0",
52109
+ "@kadoa/node-sdk": "^0.33.0",
52015
52110
  "@modelcontextprotocol/sdk": "^1.26.0",
52016
52111
  express: "^5.2.1",
52017
52112
  ioredis: "^5.6.1",
@@ -56029,10 +56124,6 @@ function generatePKCE() {
56029
56124
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
56030
56125
  return { verifier, challenge };
56031
56126
  }
56032
- function kadoaAuthUrl() {
56033
- const raw = process.env.KADOA_AUTH_URL || "https://auth.kadoa.com";
56034
- return raw.replace(/\/+$/, "");
56035
- }
56036
56127
  function jwtClaims(jwt2) {
56037
56128
  try {
56038
56129
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
@@ -56045,6 +56136,76 @@ function jwtClaims(jwt2) {
56045
56136
  return {};
56046
56137
  }
56047
56138
  }
56139
+ async function exchangeSupabaseCode(code, codeVerifier) {
56140
+ const supabaseUrl = process.env.SUPABASE_URL;
56141
+ if (!supabaseUrl)
56142
+ throw new Error("SUPABASE_URL is not configured");
56143
+ const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=pkce`, {
56144
+ method: "POST",
56145
+ headers: {
56146
+ "Content-Type": "application/json",
56147
+ apikey: process.env.SUPABASE_ANON_KEY
56148
+ },
56149
+ body: JSON.stringify({ auth_code: code, code_verifier: codeVerifier })
56150
+ });
56151
+ if (!res.ok) {
56152
+ const body = await res.text();
56153
+ throw new Error(`Supabase token exchange failed (${res.status}): ${body}`);
56154
+ }
56155
+ const data = await res.json();
56156
+ return { accessToken: data.access_token, refreshToken: data.refresh_token };
56157
+ }
56158
+ async function fetchUserTeams(supabaseJwt) {
56159
+ const kadoaApiUrl = process.env.KADOA_PUBLIC_API_URI || "https://api.kadoa.com";
56160
+ const userRes = await fetch(`${kadoaApiUrl}/v4/user`, {
56161
+ headers: { Authorization: `Bearer ${supabaseJwt}` }
56162
+ });
56163
+ if (!userRes.ok) {
56164
+ const body = await userRes.text();
56165
+ throw new Error(`Kadoa /v4/user failed (${userRes.status}): ${body}`);
56166
+ }
56167
+ const userData = await userRes.json();
56168
+ if (!userData.teams?.length) {
56169
+ throw new Error("User has no teams");
56170
+ }
56171
+ return userData.teams.map((t) => ({
56172
+ id: t.id,
56173
+ name: t.name,
56174
+ memberRole: t.memberRole
56175
+ }));
56176
+ }
56177
+ async function setActiveTeamAndRefresh(jwt2, refreshToken, teamId) {
56178
+ const kadoaApiUrl = process.env.KADOA_PUBLIC_API_URI || "https://api.kadoa.com";
56179
+ const supabaseUrl = process.env.SUPABASE_URL;
56180
+ if (!supabaseUrl)
56181
+ throw new Error("SUPABASE_URL is not configured");
56182
+ const setRes = await fetch(`${kadoaApiUrl}/v5/auth/active-team`, {
56183
+ method: "POST",
56184
+ headers: {
56185
+ "Content-Type": "application/json",
56186
+ Authorization: `Bearer ${jwt2}`
56187
+ },
56188
+ body: JSON.stringify({ teamId })
56189
+ });
56190
+ if (!setRes.ok) {
56191
+ const body = await setRes.text();
56192
+ throw new Error(`POST /v5/auth/active-team failed (${setRes.status}): ${body}`);
56193
+ }
56194
+ const refreshRes = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
56195
+ method: "POST",
56196
+ headers: {
56197
+ "Content-Type": "application/json",
56198
+ apikey: process.env.SUPABASE_ANON_KEY
56199
+ },
56200
+ body: JSON.stringify({ refresh_token: refreshToken })
56201
+ });
56202
+ if (!refreshRes.ok) {
56203
+ const body = await refreshRes.text();
56204
+ throw new Error(`Supabase token refresh failed (${refreshRes.status}): ${body}`);
56205
+ }
56206
+ const data = await refreshRes.json();
56207
+ return { jwt: data.access_token, refreshToken: data.refresh_token };
56208
+ }
56048
56209
 
56049
56210
  class KadoaOAuthProvider {
56050
56211
  store;
@@ -56070,23 +56231,151 @@ class KadoaOAuthProvider {
56070
56231
  };
56071
56232
  }
56072
56233
  async authorize(client, params, res) {
56234
+ const supabaseUrl = process.env.SUPABASE_URL;
56073
56235
  const serverUrl = process.env.MCP_SERVER_URL;
56074
- if (!serverUrl)
56075
- throw new Error("MCP_SERVER_URL must be configured");
56236
+ if (!supabaseUrl || !serverUrl) {
56237
+ throw new Error("SUPABASE_URL and MCP_SERVER_URL must be configured");
56238
+ }
56076
56239
  const state = randomToken();
56077
56240
  const { verifier, challenge } = generatePKCE();
56078
56241
  await this.store.set("pending_auths", state, {
56079
56242
  client,
56080
56243
  params,
56081
- mcpVerifier: verifier
56244
+ supabaseCodeVerifier: verifier
56082
56245
  }, 600);
56083
- const authUrl = new URL(`${kadoaAuthUrl()}/login`);
56084
- authUrl.searchParams.set("callback_url", `${serverUrl}/auth/callback`);
56085
- authUrl.searchParams.set("state", state);
56086
- authUrl.searchParams.set("code_challenge", challenge);
56246
+ res.type("html").send(renderLoginPage(state));
56247
+ }
56248
+ async handleGoogleLogin(req, res) {
56249
+ const { state } = req.body;
56250
+ const pending = await this.store.get("pending_auths", state);
56251
+ if (!pending) {
56252
+ res.status(400).send("Unknown or expired state parameter");
56253
+ return;
56254
+ }
56255
+ const supabaseUrl = process.env.SUPABASE_URL;
56256
+ const serverUrl = process.env.MCP_SERVER_URL;
56257
+ if (!supabaseUrl || !serverUrl) {
56258
+ res.status(500).send("Server misconfigured");
56259
+ return;
56260
+ }
56261
+ const redirectTo = `${serverUrl}/auth/callback?mcp_state=${state}`;
56262
+ const authUrl = new URL(`${supabaseUrl}/auth/v1/authorize`);
56263
+ authUrl.searchParams.set("provider", "google");
56264
+ authUrl.searchParams.set("redirect_to", redirectTo);
56265
+ authUrl.searchParams.set("code_challenge", pending.supabaseCodeVerifier ? createHash2("sha256").update(pending.supabaseCodeVerifier).digest("base64url") : "");
56087
56266
  authUrl.searchParams.set("code_challenge_method", "S256");
56088
56267
  res.redirect(authUrl.toString());
56089
56268
  }
56269
+ async handleEmailPasswordLogin(req, res) {
56270
+ const { state, email: email3, password } = req.body;
56271
+ if (!state || !email3 || !password) {
56272
+ res.status(400).send("Missing required fields");
56273
+ return;
56274
+ }
56275
+ const pending = await this.store.get("pending_auths", state);
56276
+ if (!pending) {
56277
+ res.status(400).type("html").send(renderLoginPage(state, "Session expired — please try again"));
56278
+ return;
56279
+ }
56280
+ const supabaseUrl = process.env.SUPABASE_URL;
56281
+ if (!supabaseUrl) {
56282
+ res.status(500).send("Server misconfigured");
56283
+ return;
56284
+ }
56285
+ try {
56286
+ const tokenRes = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=password`, {
56287
+ method: "POST",
56288
+ headers: {
56289
+ "Content-Type": "application/json",
56290
+ apikey: process.env.SUPABASE_ANON_KEY
56291
+ },
56292
+ body: JSON.stringify({ email: email3, password })
56293
+ });
56294
+ if (!tokenRes.ok) {
56295
+ const body = await tokenRes.json().catch(() => ({ error_description: "Authentication failed" }));
56296
+ const message = body.error_description || body.msg || "Invalid email or password";
56297
+ res.type("html").send(renderLoginPage(state, message));
56298
+ return;
56299
+ }
56300
+ const data = await tokenRes.json();
56301
+ await this.store.del("pending_auths", state);
56302
+ await this.completeAuthWithTokens(pending, res, data.access_token, data.refresh_token);
56303
+ } catch (error48) {
56304
+ console.error("Email/password login error:", error48);
56305
+ res.type("html").send(renderLoginPage(state, "An unexpected error occurred"));
56306
+ }
56307
+ }
56308
+ async handleSSOLogin(req, res) {
56309
+ const { state, email: email3 } = req.body;
56310
+ if (!state || !email3) {
56311
+ res.status(400).send("Missing required fields");
56312
+ return;
56313
+ }
56314
+ const pending = await this.store.get("pending_auths", state);
56315
+ if (!pending) {
56316
+ res.status(400).type("html").send(renderLoginPage(state, "Session expired — please try again"));
56317
+ return;
56318
+ }
56319
+ const supabaseUrl = process.env.SUPABASE_URL;
56320
+ const serverUrl = process.env.MCP_SERVER_URL;
56321
+ if (!supabaseUrl || !serverUrl) {
56322
+ res.status(500).send("Server misconfigured");
56323
+ return;
56324
+ }
56325
+ const domain2 = email3.includes("@") ? email3.split("@").pop() : email3;
56326
+ try {
56327
+ const ssoRes = await fetch(`${supabaseUrl}/auth/v1/sso`, {
56328
+ method: "POST",
56329
+ headers: {
56330
+ "Content-Type": "application/json",
56331
+ apikey: process.env.SUPABASE_ANON_KEY
56332
+ },
56333
+ body: JSON.stringify({
56334
+ domain: domain2,
56335
+ redirect_to: `${serverUrl}/auth/callback?mcp_state=${state}`,
56336
+ skip_http_redirect: true,
56337
+ code_challenge: createHash2("sha256").update(pending.supabaseCodeVerifier).digest("base64url"),
56338
+ code_challenge_method: "s256"
56339
+ })
56340
+ });
56341
+ if (!ssoRes.ok) {
56342
+ const body = await ssoRes.json().catch(() => ({}));
56343
+ const message = body.error_description || body.msg || body.message || "No SSO provider configured for this domain";
56344
+ res.type("html").send(renderLoginPage(state, message));
56345
+ return;
56346
+ }
56347
+ const data = await ssoRes.json();
56348
+ if (!data.url) {
56349
+ res.type("html").send(renderLoginPage(state, "No SSO provider configured for this domain"));
56350
+ return;
56351
+ }
56352
+ res.redirect(data.url);
56353
+ } catch (error48) {
56354
+ console.error("SSO login error:", error48);
56355
+ res.type("html").send(renderLoginPage(state, "An unexpected error occurred"));
56356
+ }
56357
+ }
56358
+ async completeAuthWithTokens(pending, res, supabaseJwt, supabaseRefreshToken) {
56359
+ const teams = await fetchUserTeams(supabaseJwt);
56360
+ if (teams.length === 1) {
56361
+ const refreshed = await setActiveTeamAndRefresh(supabaseJwt, supabaseRefreshToken, teams[0].id);
56362
+ await this.completeAuthFlow(pending, res, {
56363
+ jwt: refreshed.jwt,
56364
+ refreshToken: refreshed.refreshToken,
56365
+ teamId: teams[0].id
56366
+ });
56367
+ return;
56368
+ }
56369
+ const selectionToken = randomToken();
56370
+ await this.store.set("pending_team_selections", selectionToken, {
56371
+ supabaseJwt,
56372
+ supabaseRefreshToken,
56373
+ teams,
56374
+ pending,
56375
+ expiresAt: Date.now() + TEAM_SELECTION_TTL
56376
+ }, 600);
56377
+ res.type("html").send(renderTeamSelectionPage(teams, selectionToken));
56378
+ }
56090
56379
  async challengeForAuthorizationCode(_client, authorizationCode) {
56091
56380
  const entry = await this.store.get("auth_codes", authorizationCode);
56092
56381
  if (!entry)
@@ -56211,9 +56500,9 @@ class KadoaOAuthProvider {
56211
56500
  };
56212
56501
  }
56213
56502
  async handleAuthCallback(req, res) {
56214
- const { code, state } = req.query;
56503
+ const { code, mcp_state: state } = req.query;
56215
56504
  if (!code || !state) {
56216
- res.status(400).send("Missing code or state parameter");
56505
+ res.status(400).send("Missing code or mcp_state parameter");
56217
56506
  return;
56218
56507
  }
56219
56508
  const pending = await this.store.get("pending_auths", state);
@@ -56223,28 +56512,10 @@ class KadoaOAuthProvider {
56223
56512
  }
56224
56513
  await this.store.del("pending_auths", state);
56225
56514
  try {
56226
- const tokenRes = await fetch(`${kadoaAuthUrl()}/api/token`, {
56227
- method: "POST",
56228
- headers: { "Content-Type": "application/json" },
56229
- body: JSON.stringify({ code, code_verifier: pending.mcpVerifier })
56230
- });
56231
- if (!tokenRes.ok) {
56232
- const body = await tokenRes.text().catch(() => "");
56233
- throw new Error(`auth.kadoa.com /api/token failed (${tokenRes.status}): ${body}`);
56234
- }
56235
- const data = await tokenRes.json();
56236
- if (typeof data?.access_token !== "string" || typeof data?.refresh_token !== "string" || typeof data?.team_id !== "string") {
56237
- throw new Error("auth.kadoa.com /api/token returned malformed response");
56238
- }
56239
- const claims = jwtClaims(data.access_token);
56240
- console.error(`[AUTH] CALLBACK_OK: tokens received (email=${claims.email}, team=${data.team_id})`);
56241
- await this.completeAuthFlow(pending, res, {
56242
- jwt: data.access_token,
56243
- refreshToken: data.refresh_token,
56244
- teamId: data.team_id
56245
- });
56515
+ const supabaseTokens = await exchangeSupabaseCode(code, pending.supabaseCodeVerifier);
56516
+ await this.completeAuthWithTokens(pending, res, supabaseTokens.accessToken, supabaseTokens.refreshToken);
56246
56517
  } catch (error48) {
56247
- console.error("[AUTH] CALLBACK_FAIL:", error48);
56518
+ console.error("Auth callback error:", error48);
56248
56519
  const redirectUrl = new URL(pending.params.redirectUri);
56249
56520
  redirectUrl.searchParams.set("error", "server_error");
56250
56521
  redirectUrl.searchParams.set("error_description", error48 instanceof Error ? error48.message : "Authentication failed");
@@ -56254,6 +56525,45 @@ class KadoaOAuthProvider {
56254
56525
  res.redirect(redirectUrl.toString());
56255
56526
  }
56256
56527
  }
56528
+ async handleTeamSelection(req, res) {
56529
+ const { token, teamId } = req.body;
56530
+ if (!token || !teamId) {
56531
+ res.status(400).send("Missing token or teamId");
56532
+ return;
56533
+ }
56534
+ const entry = await this.store.get("pending_team_selections", token);
56535
+ if (!entry) {
56536
+ res.status(400).send("Unknown or expired team selection token");
56537
+ return;
56538
+ }
56539
+ if (entry.expiresAt < Date.now()) {
56540
+ await this.store.del("pending_team_selections", token);
56541
+ res.status(400).send("Team selection expired — please log in again");
56542
+ return;
56543
+ }
56544
+ if (!entry.teams.some((t) => t.id === teamId)) {
56545
+ res.status(403).send("Invalid team selection");
56546
+ return;
56547
+ }
56548
+ await this.store.del("pending_team_selections", token);
56549
+ try {
56550
+ const refreshed = await setActiveTeamAndRefresh(entry.supabaseJwt, entry.supabaseRefreshToken, teamId);
56551
+ await this.completeAuthFlow(entry.pending, res, {
56552
+ jwt: refreshed.jwt,
56553
+ refreshToken: refreshed.refreshToken,
56554
+ teamId
56555
+ });
56556
+ } catch (error48) {
56557
+ console.error("Team selection error:", error48);
56558
+ const redirectUrl = new URL(entry.pending.params.redirectUri);
56559
+ redirectUrl.searchParams.set("error", "server_error");
56560
+ redirectUrl.searchParams.set("error_description", error48 instanceof Error ? error48.message : "Failed to set active team");
56561
+ if (entry.pending.params.state) {
56562
+ redirectUrl.searchParams.set("state", entry.pending.params.state);
56563
+ }
56564
+ res.redirect(redirectUrl.toString());
56565
+ }
56566
+ }
56257
56567
  async completeAuthFlow(pending, res, credentials) {
56258
56568
  const mcpCode = randomToken();
56259
56569
  await this.store.set("auth_codes", mcpCode, {
@@ -56273,9 +56583,429 @@ class KadoaOAuthProvider {
56273
56583
  res.redirect(redirectUrl.toString());
56274
56584
  }
56275
56585
  }
56276
- var ACCESS_TOKEN_TTL;
56586
+ function renderTeamSelectionPage(teams, selectionToken) {
56587
+ const teamButtons = teams.map((t) => `
56588
+ <button type="submit" name="teamId" value="${t.id}" class="team-btn">
56589
+ <span class="team-name">${escapeHtml(t.name)}</span>
56590
+ ${t.memberRole ? `<span class="team-role">${escapeHtml(t.memberRole.toLowerCase())}</span>` : ""}
56591
+ </button>`).join(`
56592
+ `);
56593
+ return `<!DOCTYPE html>
56594
+ <html lang="en">
56595
+ <head>
56596
+ <meta charset="utf-8" />
56597
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
56598
+ <title>Select Team - Kadoa</title>
56599
+ <style>
56600
+ * { margin: 0; padding: 0; box-sizing: border-box; }
56601
+
56602
+ body {
56603
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
56604
+ background: oklch(1 0 0);
56605
+ color: oklch(0.17 0.02 228);
56606
+ min-height: 100vh;
56607
+ display: flex;
56608
+ align-items: center;
56609
+ justify-content: center;
56610
+ }
56611
+
56612
+ .container {
56613
+ width: 100%;
56614
+ max-width: 420px;
56615
+ padding: 2rem;
56616
+ }
56617
+
56618
+ .logo {
56619
+ text-align: center;
56620
+ margin-bottom: 2rem;
56621
+ }
56622
+
56623
+ h1 {
56624
+ font-size: 1.25rem;
56625
+ font-weight: 600;
56626
+ text-align: center;
56627
+ margin-bottom: 0.5rem;
56628
+ color: oklch(0.17 0.02 228);
56629
+ }
56630
+
56631
+ .subtitle {
56632
+ text-align: center;
56633
+ color: oklch(0.56 0.02 228);
56634
+ font-size: 0.875rem;
56635
+ margin-bottom: 1.5rem;
56636
+ }
56637
+
56638
+ .team-btn {
56639
+ width: 100%;
56640
+ display: flex;
56641
+ align-items: center;
56642
+ justify-content: space-between;
56643
+ padding: 0.875rem 1rem;
56644
+ margin-bottom: 0.5rem;
56645
+ background: oklch(1 0 0);
56646
+ border: 1px solid oklch(0.5 0.02 228 / 0.4);
56647
+ border-radius: 0.3rem;
56648
+ color: oklch(0.17 0.02 228);
56649
+ font-size: 15px;
56650
+ cursor: pointer;
56651
+ transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
56652
+ box-shadow: 0px 1px 1px 0px oklch(0.68 0.01 60.13 / 0.11);
56653
+ }
56654
+
56655
+ .team-btn:hover {
56656
+ background: oklch(0.96 0 286);
56657
+ border-color: oklch(0.7 0.18 42);
56658
+ }
56659
+
56660
+ .team-btn:active {
56661
+ background: oklch(0.72 0.23 54 / 0.13);
56662
+ }
56663
+
56664
+ .team-name { font-weight: 500; }
56665
+
56666
+ .team-role {
56667
+ font-size: 13px;
56668
+ color: oklch(0.56 0.02 228 / 0.67);
56669
+ text-transform: capitalize;
56670
+ }
56671
+ </style>
56672
+ </head>
56673
+ <body>
56674
+ <div class="container">
56675
+ <div class="logo">
56676
+ <svg width="108" height="32" viewBox="0 0 108 32" fill="none" xmlns="http://www.w3.org/2000/svg">
56677
+ <g clip-path="url(#clip0)">
56678
+ <path d="M4.5 27V20.0059C4.49955 18.6288 3.38499 17.5105 2.00781 17.5059L-0.00585938 17.5V14.5L2.00781 14.4941C3.38499 14.4895 4.49955 13.3712 4.5 11.9941V5C4.5 2.51472 6.51472 0.5 9 0.5H12V3.5H9C8.17157 3.5 7.5 4.17157 7.5 5V11.9941C7.49977 13.5757 6.82719 14.9966 5.75781 16C6.82719 17.0034 7.49977 18.4243 7.5 20.0059V27C7.5 27.8284 8.17157 28.5 9 28.5H12V31.5H9C6.51472 31.5 4.5 29.4853 4.5 27Z" fill="#FD7412"/>
56679
+ <path d="M103.5 27V20.0059C103.5 18.6288 104.615 17.5105 105.992 17.5059L108.006 17.5V14.5L105.992 14.4941C104.615 14.4895 103.5 13.3712 103.5 11.9941V5C103.5 2.51472 101.485 0.5 99 0.5H96V3.5H99C99.8284 3.5 100.5 4.17157 100.5 5V11.9941C100.5 13.5757 101.173 14.9966 102.242 16C101.173 17.0034 100.5 18.4243 100.5 20.0059V27C100.5 27.8284 99.8284 28.5 99 28.5H96V31.5H99C101.485 31.5 103.5 29.4853 103.5 27Z" fill="#FD7412"/>
56680
+ <path d="M85.2346 26.308C84.0026 26.308 82.92 26.0093 81.9866 25.412C81.0533 24.8147 80.3253 23.9653 79.8026 22.864C79.28 21.7627 79.0186 20.4373 79.0186 18.888C79.0186 17.3573 79.28 16.0413 79.8026 14.94C80.3253 13.8387 81.0533 12.9987 81.9866 12.42C82.92 11.8227 84.0026 11.524 85.2346 11.524C86.3733 11.524 87.3906 11.804 88.2866 12.364C89.2013 12.9053 89.7986 13.6427 90.0786 14.576H89.7706L90.1066 11.804H94.1666C94.1106 12.42 94.0546 13.0453 93.9986 13.68C93.9613 14.296 93.9426 14.9027 93.9426 15.5V26H89.7426L89.7146 23.34H90.0506C89.752 24.236 89.1546 24.9547 88.2586 25.496C87.3626 26.0373 86.3546 26.308 85.2346 26.308ZM86.5226 23.116C87.4933 23.116 88.2773 22.7707 88.8746 22.08C89.472 21.3893 89.7706 20.3253 89.7706 18.888C89.7706 17.4507 89.472 16.396 88.8746 15.724C88.2773 15.052 87.4933 14.716 86.5226 14.716C85.552 14.716 84.768 15.052 84.1706 15.724C83.5733 16.396 83.2746 17.4507 83.2746 18.888C83.2746 20.3253 83.564 21.3893 84.1426 22.08C84.74 22.7707 85.5333 23.116 86.5226 23.116Z" fill="#18181B"/>
56681
+ <path d="M70.1002 26.308C68.5882 26.308 67.2722 26.0093 66.1522 25.412C65.0509 24.796 64.1922 23.9373 63.5762 22.836C62.9789 21.7347 62.6802 20.4187 62.6802 18.888C62.6802 17.376 62.9789 16.0693 63.5762 14.968C64.1922 13.8667 65.0509 13.0173 66.1522 12.42C67.2722 11.8227 68.5882 11.524 70.1002 11.524C71.6122 11.524 72.9282 11.8227 74.0482 12.42C75.1682 13.0173 76.0269 13.8667 76.6242 14.968C77.2402 16.0693 77.5482 17.376 77.5482 18.888C77.5482 20.4187 77.2402 21.7347 76.6242 22.836C76.0269 23.9373 75.1682 24.796 74.0482 25.412C72.9282 26.0093 71.6122 26.308 70.1002 26.308ZM70.1002 23.116C71.0709 23.116 71.8362 22.7707 72.3962 22.08C72.9749 21.3893 73.2642 20.3253 73.2642 18.888C73.2642 17.4507 72.9749 16.396 72.3962 15.724C71.8362 15.052 71.0709 14.716 70.1002 14.716C69.1295 14.716 68.3549 15.052 67.7762 15.724C67.2162 16.396 66.9362 17.4507 66.9362 18.888C66.9362 20.3253 67.2162 21.3893 67.7762 22.08C68.3549 22.7707 69.1295 23.116 70.1002 23.116Z" fill="#18181B"/>
56682
+ <path d="M51.8208 26.308C50.5888 26.308 49.4968 26.0093 48.5448 25.412C47.6115 24.8147 46.8741 23.9653 46.3328 22.864C45.8101 21.7627 45.5488 20.4373 45.5488 18.888C45.5488 17.3573 45.8101 16.0413 46.3328 14.94C46.8555 13.8387 47.5928 12.9987 48.5448 12.42C49.4968 11.8227 50.5888 11.524 51.8208 11.524C52.9408 11.524 53.9395 11.7947 54.8168 12.336C55.7128 12.8587 56.3101 13.568 56.6088 14.464H56.2448V5.392H60.4728V26H56.3008V23.228H56.6648C56.3661 24.1613 55.7688 24.908 54.8728 25.468C53.9768 26.028 52.9595 26.308 51.8208 26.308ZM53.0808 23.116C54.0515 23.116 54.8355 22.7707 55.4328 22.08C56.0301 21.3893 56.3288 20.3253 56.3288 18.888C56.3288 17.4507 56.0301 16.396 55.4328 15.724C54.8355 15.052 54.0515 14.716 53.0808 14.716C52.1101 14.716 51.3168 15.052 50.7008 15.724C50.1035 16.396 49.8048 17.4507 49.8048 18.888C49.8048 20.3253 50.1035 21.3893 50.7008 22.08C51.3168 22.7707 52.1101 23.116 53.0808 23.116Z" fill="#18181B"/>
56683
+ <path d="M34.6334 26.308C33.4014 26.308 32.3187 26.0093 31.3854 25.412C30.4521 24.8147 29.7241 23.9653 29.2014 22.864C28.6787 21.7627 28.4174 20.4373 28.4174 18.888C28.4174 17.3573 28.6787 16.0413 29.2014 14.94C29.7241 13.8387 30.4521 12.9987 31.3854 12.42C32.3187 11.8227 33.4014 11.524 34.6334 11.524C35.7721 11.524 36.7894 11.804 37.6854 12.364C38.6001 12.9053 39.1974 13.6427 39.4774 14.576H39.1694L39.5054 11.804H43.5654C43.5094 12.42 43.4534 13.0453 43.3974 13.68C43.3601 14.296 43.3414 14.9027 43.3414 15.5V26H39.1414L39.1134 23.34H39.4494C39.1507 24.236 38.5534 24.9547 37.6574 25.496C36.7614 26.0373 35.7534 26.308 34.6334 26.308ZM35.9214 23.116C36.8921 23.116 37.6761 22.7707 38.2734 22.08C38.8707 21.3893 39.1694 20.3253 39.1694 18.888C39.1694 17.4507 38.8707 16.396 38.2734 15.724C37.6761 15.052 36.8921 14.716 35.9214 14.716C34.9507 14.716 34.1667 15.052 33.5694 15.724C32.9721 16.396 32.6734 17.4507 32.6734 18.888C32.6734 20.3253 32.9627 21.3893 33.5414 22.08C34.1387 22.7707 34.9321 23.116 35.9214 23.116Z" fill="#18181B"/>
56684
+ <path d="M13.736 26V5.392H17.964V17.712H18.02L23.284 11.804H28.324L21.52 19.364V17.824L28.688 26H23.508L18.02 19.84H17.964V26H13.736Z" fill="#18181B"/>
56685
+ </g>
56686
+ <defs><clipPath id="clip0"><rect width="108" height="32" fill="white"/></clipPath></defs>
56687
+ </svg>
56688
+ </div>
56689
+ <h1>Select a team</h1>
56690
+ <p class="subtitle">Choose which team to connect with this MCP session</p>
56691
+ <form method="POST" action="/team-select">
56692
+ <input type="hidden" name="token" value="${selectionToken}" />
56693
+ ${teamButtons}
56694
+ </form>
56695
+ </div>
56696
+ </body>
56697
+ </html>`;
56698
+ }
56699
+ function renderLoginPage(state, error48) {
56700
+ const errorHtml = error48 ? `<div class="error">${escapeHtml(error48)}</div>` : "";
56701
+ return `<!DOCTYPE html>
56702
+ <html lang="en">
56703
+ <head>
56704
+ <meta charset="utf-8" />
56705
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
56706
+ <title>Sign In - Kadoa</title>
56707
+ <style>
56708
+ * { margin: 0; padding: 0; box-sizing: border-box; }
56709
+
56710
+ body {
56711
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
56712
+ background: hsl(0 0% 98%);
56713
+ color: #18181b;
56714
+ min-height: 100dvh;
56715
+ display: flex;
56716
+ align-items: center;
56717
+ justify-content: center;
56718
+ background-image: radial-gradient(circle, #d4d4d8 1px, transparent 1px);
56719
+ background-size: 24px 24px;
56720
+ background-position: center top;
56721
+ }
56722
+
56723
+ .card {
56724
+ width: 100%;
56725
+ max-width: 460px;
56726
+ background: #fff;
56727
+ padding: 1rem;
56728
+ display: flex;
56729
+ flex-direction: column;
56730
+ gap: 1rem;
56731
+ min-height: 100dvh;
56732
+ }
56733
+
56734
+ @media (min-width: 768px) {
56735
+ .card { padding: 3rem; min-height: auto; border-left: 1px solid #e0e1e5; border-right: 1px solid #e0e1e5; }
56736
+ }
56737
+
56738
+ .logo { display: grid; place-content: center; }
56739
+
56740
+ h1 {
56741
+ font-size: 20px;
56742
+ font-weight: 600;
56743
+ text-align: center;
56744
+ color: #18181b;
56745
+ }
56746
+
56747
+ .spacer { height: 0; }
56748
+ @media (min-width: 768px) { .spacer { height: 3rem; } }
56749
+
56750
+ .error {
56751
+ background: #fef2f2;
56752
+ color: #991b1b;
56753
+ border: 1px solid #fecaca;
56754
+ border-radius: 4px;
56755
+ padding: 0.6rem 0.875rem;
56756
+ font-size: 15px;
56757
+ }
56758
+
56759
+ /* Buttons — matching KUI default + primary looks */
56760
+ .btn {
56761
+ width: 100%;
56762
+ padding: 0.6em 1em;
56763
+ border-radius: 4px;
56764
+ font-size: 16px;
56765
+ font-weight: 500;
56766
+ cursor: pointer;
56767
+ display: flex;
56768
+ align-items: center;
56769
+ justify-content: center;
56770
+ gap: 0.5rem;
56771
+ transition: background 0.15s, border-color 0.15s;
56772
+ text-decoration: none;
56773
+ }
56774
+
56775
+ .btn-default {
56776
+ background: #fff;
56777
+ color: #18181b;
56778
+ border: 1px solid #d4d4d8;
56779
+ box-shadow: inset 0 -3px 0 0 rgba(0,0,0,0.03), 0 1px 0px 1px rgba(255,255,255,0.5), 0 -1px 0px 1px rgba(0,0,0,0.02);
56780
+ }
56781
+
56782
+ .btn-default:hover {
56783
+ background: rgba(113,113,122,0.1);
56784
+ }
56785
+
56786
+ .btn-primary {
56787
+ background: hsl(212 70% 27%);
56788
+ color: #fff;
56789
+ border: 1px solid hsl(214 70% 23%);
56790
+ box-shadow: inset 0 2px 0 0 rgba(56,189,248,0.2), 0 -1px 0px 1px rgba(0,0,0,0.02);
56791
+ }
56792
+
56793
+ .btn-primary:hover {
56794
+ background: hsl(212 70% 33%);
56795
+ }
56796
+
56797
+ /* OR divider */
56798
+ .line-or {
56799
+ display: flex;
56800
+ align-items: center;
56801
+ gap: 0.5rem;
56802
+ font-weight: 500;
56803
+ color: rgba(24,24,27,0.6);
56804
+ font-size: 14px;
56805
+ margin: 0.5rem 0;
56806
+ }
56807
+
56808
+ .line-or hr {
56809
+ flex: 1;
56810
+ border: none;
56811
+ border-top: 1px solid rgba(113,113,122,0.15);
56812
+ }
56813
+
56814
+ /* Form inputs — matching KUI input style */
56815
+ label {
56816
+ display: block;
56817
+ font-size: 16px;
56818
+ font-weight: 500;
56819
+ margin-bottom: 0.25rem;
56820
+ color: #18181b;
56821
+ }
56822
+
56823
+ input[type="email"], input[type="password"] {
56824
+ width: 100%;
56825
+ padding: 0.35em 0.5em;
56826
+ border: 1px solid #d4d4d8;
56827
+ border-radius: 4px;
56828
+ font-size: 18px;
56829
+ font-family: inherit;
56830
+ color: #18181b;
56831
+ background: #fff;
56832
+ outline: none;
56833
+ box-shadow: inset 0 3px 0 0 rgba(0,0,0,0.025);
56834
+ transition: border-color 0.15s;
56835
+ caret-color: hsl(25 98% 53%);
56836
+ }
56837
+
56838
+ input[type="email"]:hover, input[type="password"]:hover {
56839
+ border-color: hsl(31 99% 72%);
56840
+ }
56841
+
56842
+ input[type="email"]:focus, input[type="password"]:focus {
56843
+ border-color: hsl(25 98% 53%);
56844
+ box-shadow: inset 0 3px 0 0 rgba(0,0,0,0.025), 0 0 0 2px rgba(249,115,22,0.2);
56845
+ }
56846
+
56847
+ .field { margin-bottom: 0.75rem; }
56848
+
56849
+ hr.separator {
56850
+ border: none;
56851
+ border-top: 1px solid rgba(113,113,122,0.15);
56852
+ margin: 0.5rem 0;
56853
+ }
56854
+
56855
+ .google-icon { width: 18px; height: 18px; }
56856
+ .key-icon { width: 16px; height: 16px; }
56857
+
56858
+ /* Tabs for email/SSO — keep simple, same visual weight */
56859
+ .tabs {
56860
+ display: none;
56861
+ }
56862
+
56863
+ .tab-content { display: none; }
56864
+ .tab-content.active { display: block; }
56865
+
56866
+ .tab-switch {
56867
+ text-align: center;
56868
+ margin-top: 0.25rem;
56869
+ }
56870
+
56871
+ .tab-switch a {
56872
+ font-size: 15px;
56873
+ color: #18181b;
56874
+ text-decoration: underline;
56875
+ text-decoration-color: rgba(251,146,60,0.5);
56876
+ text-underline-offset: 2px;
56877
+ cursor: pointer;
56878
+ }
56879
+
56880
+ .tab-switch a:hover {
56881
+ background: rgba(251,146,60,0.1);
56882
+ border-radius: 2px;
56883
+ }
56884
+ </style>
56885
+ </head>
56886
+ <body>
56887
+ <div class="card">
56888
+ <!-- Logo {k} -->
56889
+ <div class="logo">
56890
+ <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
56891
+ <path opacity="0.15" d="M25.3196 6.25H14.6804C14.6804 7.49264 13.6596 8.5 12.4005 8.5C11.3808 8.5 10.8312 8.67478 10.5466 8.82497C10.3001 8.95506 10.147 9.11941 10.0189 9.38005C9.85482 9.7141 9.74438 10.1712 9.68281 10.8152C9.62136 11.458 9.61405 12.2133 9.61405 13.125L9.61416 13.3731C9.61532 14.9118 9.61694 17.0733 8.75235 18.8332C8.55109 19.2428 8.30266 19.6357 8 20C8.30266 20.3643 8.55109 20.7572 8.75235 21.1668C9.61694 22.9267 9.61532 25.0882 9.61416 26.6269L9.61405 26.875C9.61405 27.7867 9.62136 28.542 9.68281 29.1848C9.74438 29.8288 9.85482 30.2859 10.0189 30.6199C10.147 30.8806 10.3001 31.0449 10.5466 31.175C10.8312 31.3252 11.3808 31.5 12.4005 31.5C13.6596 31.5 14.6804 32.5074 14.6804 33.75H25.3196C25.3196 32.5074 26.3404 31.5 27.5995 31.5C28.6192 31.5 29.1688 31.3252 29.4534 31.175C29.6999 31.0449 29.853 30.8806 29.9811 30.6199C30.1452 30.2859 30.2556 29.8288 30.3172 29.1848C30.3786 28.542 30.386 27.7867 30.386 26.875L30.3858 26.6269C30.3847 25.0882 30.3831 22.9267 31.2477 21.1668C31.4489 20.7572 31.6973 20.3643 32 20C31.6973 19.6357 31.4489 19.2428 31.2477 18.8332C30.3831 17.0733 30.3847 14.9118 30.3858 13.3731L30.386 13.125C30.386 12.2133 30.3786 11.458 30.3172 10.8152C30.2556 10.1712 30.1452 9.7141 29.9811 9.38005C29.853 9.11941 29.6999 8.95506 29.4534 8.82497C29.1688 8.67478 28.6192 8.5 27.5995 8.5C26.3404 8.5 25.3196 7.49264 25.3196 6.25Z" fill="#fd7412"/>
56892
+ <path d="M12.5 6.25C2.5 6.25 12.5 20 2.5 20C12.5 20 2.5 33.75 12.5 33.75" stroke="#fd7412" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
56893
+ <path d="M16 10V29" stroke="#18181B" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
56894
+ <path d="M27.5 6.25C37.5 6.25 27.5 20 37.5 20C27.5 20 37.5 33.75 27.5 33.75" stroke="#fd7412" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
56895
+ <path d="M16 23L25 18" stroke="#18181B" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
56896
+ <path d="M25 29L16 23" stroke="#18181B" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/>
56897
+ </svg>
56898
+ </div>
56899
+
56900
+ <!-- Heading -->
56901
+ <h1>Sign in to Kadoa</h1>
56902
+
56903
+ <div class="spacer"></div>
56904
+
56905
+ ${errorHtml}
56906
+
56907
+ <!-- Continue with Google -->
56908
+ <form method="POST" action="/auth/google">
56909
+ <input type="hidden" name="state" value="${escapeHtml(state)}" />
56910
+ <button type="submit" class="btn btn-default">
56911
+ <svg class="google-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
56912
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
56913
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
56914
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
56915
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
56916
+ </svg>
56917
+ Continue with Google
56918
+ </button>
56919
+ </form>
56920
+
56921
+ <!-- Continue with SSO -->
56922
+ <div id="sso-button-wrapper">
56923
+ <form method="POST" action="/auth/sso" id="sso-direct-form" style="display:none">
56924
+ <input type="hidden" name="state" value="${escapeHtml(state)}" />
56925
+ <input type="hidden" name="email" id="sso-email-hidden" />
56926
+ </form>
56927
+ <button type="button" class="btn btn-default" id="sso-toggle-btn">
56928
+ <svg class="key-icon" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
56929
+ <path d="M10 1a5 5 0 0 0-4.546 7.066l-4.161 4.16a.5.5 0 0 0-.146.354V14.5a.5.5 0 0 0 .5.5h2a.5.5 0 0 0 .5-.5V14h1a.5.5 0 0 0 .5-.5v-1h1a.5.5 0 0 0 .354-.146l.94-.94A5 5 0 1 0 10 1zm1.5 4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z" fill="currentColor"/>
56930
+ </svg>
56931
+ Continue with SSO
56932
+ </button>
56933
+ </div>
56934
+
56935
+ <!-- OR divider -->
56936
+ <div class="line-or">
56937
+ <hr />
56938
+ OR
56939
+ <hr />
56940
+ </div>
56941
+
56942
+ <!-- Email + Password form -->
56943
+ <div class="tab-content active" id="tab-email">
56944
+ <form method="POST" action="/auth/login">
56945
+ <input type="hidden" name="state" value="${escapeHtml(state)}" />
56946
+ <div class="field">
56947
+ <label for="email">Sign in with email:</label>
56948
+ <input type="email" id="email" name="email" required autocomplete="email" />
56949
+ </div>
56950
+ <div class="field">
56951
+ <label for="password">Your password:</label>
56952
+ <input type="password" id="password" name="password" required autocomplete="current-password" />
56953
+ </div>
56954
+ <button type="submit" class="btn btn-primary">Continue</button>
56955
+ </form>
56956
+ </div>
56957
+
56958
+ <!-- SSO form (shown when "Continue with SSO" is clicked) -->
56959
+ <div class="tab-content" id="tab-sso">
56960
+ <form method="POST" action="/auth/sso">
56961
+ <input type="hidden" name="state" value="${escapeHtml(state)}" />
56962
+ <div class="field">
56963
+ <label for="sso-email">Work email:</label>
56964
+ <input type="email" id="sso-email" name="email" required autocomplete="email" />
56965
+ </div>
56966
+ <button type="submit" class="btn btn-primary">Continue with SSO</button>
56967
+ </form>
56968
+ <div class="tab-switch">
56969
+ <a id="back-to-email">Sign in with email instead</a>
56970
+ </div>
56971
+ </div>
56972
+
56973
+ <div style="flex:1"></div>
56974
+
56975
+ <script>
56976
+ var ssoBtn = document.getElementById('sso-toggle-btn');
56977
+ var tabEmail = document.getElementById('tab-email');
56978
+ var tabSso = document.getElementById('tab-sso');
56979
+ var ssoWrapper = document.getElementById('sso-button-wrapper');
56980
+ var lineOr = document.querySelector('.line-or');
56981
+ var backLink = document.getElementById('back-to-email');
56982
+
56983
+ ssoBtn.addEventListener('click', function() {
56984
+ tabEmail.classList.remove('active');
56985
+ tabSso.classList.add('active');
56986
+ ssoWrapper.style.display = 'none';
56987
+ lineOr.style.display = 'none';
56988
+ document.getElementById('sso-email').focus();
56989
+ });
56990
+
56991
+ backLink.addEventListener('click', function() {
56992
+ tabSso.classList.remove('active');
56993
+ tabEmail.classList.add('active');
56994
+ ssoWrapper.style.display = '';
56995
+ lineOr.style.display = '';
56996
+ });
56997
+ </script>
56998
+ </div>
56999
+ </body>
57000
+ </html>`;
57001
+ }
57002
+ function escapeHtml(str) {
57003
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
57004
+ }
57005
+ var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
56277
57006
  var init_auth2 = __esm(() => {
56278
57007
  init_errors4();
57008
+ TEAM_SELECTION_TTL = 10 * 60 * 1000;
56279
57009
  ACCESS_TOKEN_TTL = 7 * 24 * 3600;
56280
57010
  });
56281
57011
 
@@ -56380,6 +57110,7 @@ var exports_http = {};
56380
57110
  __export(exports_http, {
56381
57111
  startHttpServer: () => startHttpServer
56382
57112
  });
57113
+ import express8 from "express";
56383
57114
  function jwtClaims2(jwt2) {
56384
57115
  try {
56385
57116
  return JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
@@ -56436,6 +57167,18 @@ async function startHttpServer(options) {
56436
57167
  app.get("/auth/callback", (req, res) => {
56437
57168
  provider.handleAuthCallback(req, res);
56438
57169
  });
57170
+ app.post("/auth/google", express8.urlencoded({ extended: false }), (req, res) => {
57171
+ provider.handleGoogleLogin(req, res);
57172
+ });
57173
+ app.post("/auth/login", express8.urlencoded({ extended: false }), (req, res) => {
57174
+ provider.handleEmailPasswordLogin(req, res);
57175
+ });
57176
+ app.post("/auth/sso", express8.urlencoded({ extended: false }), (req, res) => {
57177
+ provider.handleSSOLogin(req, res);
57178
+ });
57179
+ app.post("/team-select", express8.urlencoded({ extended: false }), (req, res) => {
57180
+ provider.handleTeamSelection(req, res);
57181
+ });
56439
57182
  app.get("/health", (_req, res) => {
56440
57183
  res.json({
56441
57184
  status: "ok",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.5.4",
3
+ "version": "0.5.7",
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.32.0",
27
+ "@kadoa/node-sdk": "^0.33.0",
28
28
  "@modelcontextprotocol/sdk": "^1.26.0",
29
29
  "express": "^5.2.1",
30
30
  "ioredis": "^5.6.1",