@kadoa/mcp 0.5.10 → 0.5.12

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 +2 -2
  2. package/dist/index.js +313 -102
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -72,8 +72,8 @@ Once the MCP server is configured, you can manage the full workflow lifecycle th
72
72
  > You: Fetch https://example.com as markdown.
73
73
 
74
74
  Claude calls scrape with smart routing and returns compact markdown.
75
- If the page reports bot detection, Claude can retry explicitly with
76
- the stealth proxy tier. Ask for HTML when you need the raw source.
75
+ Routing is fully automatic it escalates through the stealth tiers on
76
+ its own when a site blocks bots. Ask for HTML when you need the raw source.
77
77
  ```
78
78
 
79
79
  The `scrape` tool is available only to workspaces enabled for the Scrape API. Use a workflow instead for structured extraction, recurring runs, monitoring, or multi-page navigation.
package/dist/index.js CHANGED
@@ -51861,13 +51861,23 @@ This is the main page content.`
51861
51861
  });
51862
51862
 
51863
51863
  // src/client.ts
51864
+ function resolveServiceAccountApiKey(credentialRef) {
51865
+ const configuredRef = process.env.MCP_SERVICE_ACCOUNT_CREDENTIAL_REF?.trim();
51866
+ if (!configuredRef || credentialRef !== configuredRef) {
51867
+ throw new Error("Unknown service-account credential reference");
51868
+ }
51869
+ const apiKey = process.env.MCP_SERVICE_ACCOUNT_KADOA_API_KEY?.trim();
51870
+ if (!apiKey) {
51871
+ throw new Error("Service-account API key is not configured");
51872
+ }
51873
+ return apiKey;
51874
+ }
51864
51875
  function createKadoaClient(auth) {
51865
- const client = new KadoaClient({ bearerToken: auth.jwt });
51876
+ const { principal } = auth;
51877
+ const client = principal.type === "user" ? new KadoaClient({ bearerToken: principal.supabaseJwt }) : new KadoaClient({ apiKey: auth.apiKey ?? resolveServiceAccountApiKey(principal.credentialRef) });
51866
51878
  client.axiosInstance.interceptors.request.use((config2) => {
51867
51879
  config2.headers["x-kadoa-source"] = "mcp";
51868
- if (auth.teamId) {
51869
- config2.headers["x-team-id"] = auth.teamId;
51870
- }
51880
+ config2.headers["x-team-id"] = principal.teamId;
51871
51881
  return config2;
51872
51882
  });
51873
51883
  return client;
@@ -51882,6 +51892,7 @@ var init_client = __esm(() => {
51882
51892
  // src/client.ts
51883
51893
  var exports_client = {};
51884
51894
  __export(exports_client, {
51895
+ resolveServiceAccountApiKey: () => resolveServiceAccountApiKey2,
51885
51896
  refreshSupabaseJwtRaw: () => refreshSupabaseJwtRaw,
51886
51897
  refreshSupabaseJwt: () => refreshSupabaseJwt,
51887
51898
  isJwtExpired: () => isJwtExpired,
@@ -51892,13 +51903,23 @@ __export(exports_client, {
51892
51903
  KadoaSdkException: () => KadoaSdkException,
51893
51904
  KadoaClient: () => KadoaClient
51894
51905
  });
51906
+ function resolveServiceAccountApiKey2(credentialRef) {
51907
+ const configuredRef = process.env.MCP_SERVICE_ACCOUNT_CREDENTIAL_REF?.trim();
51908
+ if (!configuredRef || credentialRef !== configuredRef) {
51909
+ throw new Error("Unknown service-account credential reference");
51910
+ }
51911
+ const apiKey = process.env.MCP_SERVICE_ACCOUNT_KADOA_API_KEY?.trim();
51912
+ if (!apiKey) {
51913
+ throw new Error("Service-account API key is not configured");
51914
+ }
51915
+ return apiKey;
51916
+ }
51895
51917
  function createKadoaClient2(auth) {
51896
- const client = new KadoaClient({ bearerToken: auth.jwt });
51918
+ const { principal } = auth;
51919
+ const client = principal.type === "user" ? new KadoaClient({ bearerToken: principal.supabaseJwt }) : new KadoaClient({ apiKey: auth.apiKey ?? resolveServiceAccountApiKey2(principal.credentialRef) });
51897
51920
  client.axiosInstance.interceptors.request.use((config2) => {
51898
51921
  config2.headers["x-kadoa-source"] = "mcp";
51899
- if (auth.teamId) {
51900
- config2.headers["x-team-id"] = auth.teamId;
51901
- }
51922
+ config2.headers["x-team-id"] = principal.teamId;
51902
51923
  return config2;
51903
51924
  });
51904
51925
  return client;
@@ -51955,7 +51976,7 @@ async function _doRefreshRaw(supabaseRefreshToken) {
51955
51976
  return { jwt: data.access_token, refreshToken: data.refresh_token };
51956
51977
  }
51957
51978
  const body = await res.text().catch(() => "");
51958
- console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status} (refreshToken=${supabaseRefreshToken.slice(0, 12)}...): ${body}`);
51979
+ console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status}: ${body}`);
51959
51980
  if (body.includes("session_expired") || body.includes("refresh_token_not_found") || body.includes("refresh_token_already_used")) {
51960
51981
  throw new SessionExpiredError("Your Kadoa session has expired due to inactivity. Please reconnect to re-authenticate.");
51961
51982
  }
@@ -51968,7 +51989,7 @@ async function refreshSupabaseJwt(ctx) {
51968
51989
  }
51969
51990
  try {
51970
51991
  const refreshToken = ctx.supabaseRefreshToken;
51971
- console.error(`[JWT_REFRESH] Refreshing Supabase JWT (refreshToken=${refreshToken.slice(0, 12)}..., team=${ctx.teamId ?? "unknown"})`);
51992
+ console.error(`[JWT_REFRESH] Refreshing Supabase JWT (team=${ctx.teamId ?? "unknown"})`);
51972
51993
  const result = await refreshSupabaseJwtRaw(refreshToken);
51973
51994
  if (!result)
51974
51995
  return;
@@ -51984,7 +52005,7 @@ async function refreshSupabaseJwt(ctx) {
51984
52005
  } catch (e) {
51985
52006
  console.error("[JWT_REFRESH] WARN: persist failed, tokens updated in-memory only:", e);
51986
52007
  }
51987
- console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${result.refreshToken.slice(0, 12)}...)`);
52008
+ console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"})`);
51988
52009
  return result.jwt;
51989
52010
  } catch (error48) {
51990
52011
  if (error48 instanceof SessionExpiredError)
@@ -52276,15 +52297,13 @@ function registerTools(server, ctx, capabilities) {
52276
52297
  }
52277
52298
  if (capabilities.scrape) {
52278
52299
  server.registerTool("scrape", {
52279
- description: "Immediately fetch one URL as LLM-friendly markdown or raw HTML without creating a workflow. " + "Markdown is the default. Auto routing is normally best; if bot detection reports a stealth retry hint, call again with proxy='stealth'. " + "Use create_workflow instead for structured extraction, recurring runs, monitoring, or navigation-heavy jobs.",
52300
+ description: "Immediately fetch one URL as LLM-friendly markdown or raw HTML without creating a workflow. " + "Markdown is the default. Routing is fully automatic: it starts cheap and escalates through stealth tiers on its own when a site blocks bots, and remembers the working method per host. " + "Use create_workflow instead for structured extraction, recurring runs, monitoring, or navigation-heavy jobs.",
52280
52301
  inputSchema: strictSchema({
52281
52302
  url: exports_external.url().refine((value) => {
52282
52303
  const protocol = new URL(value).protocol;
52283
52304
  return protocol === "http:" || protocol === "https:";
52284
52305
  }, "URL must use HTTP or HTTPS").describe("HTTP or HTTPS URL to fetch"),
52285
- format: exports_external.enum(["html", "markdown"]).optional().describe("Output format. Defaults to markdown for compact LLM consumption; use html for raw source."),
52286
- fetchMode: exports_external.enum(["auto", "http", "browser"]).optional().describe("Fetch mode. Omit or use auto for smart routing; browser renders JavaScript."),
52287
- proxy: exports_external.enum(["auto", "dc", "isp", "residential", "stealth"]).optional().describe("Proxy tier. Use stealth only when normal routing is bot-blocked.")
52306
+ format: exports_external.enum(["html", "markdown"]).optional().describe("Output format. Defaults to markdown for compact LLM consumption; use html for raw source.")
52288
52307
  }),
52289
52308
  annotations: {
52290
52309
  readOnlyHint: true,
@@ -52295,9 +52314,7 @@ function registerTools(server, ctx, capabilities) {
52295
52314
  }, withErrorHandling("scrape", async (args) => {
52296
52315
  const response = await ctx.client.scrape.fetch({
52297
52316
  url: args.url,
52298
- format: args.format ?? "markdown",
52299
- ...args.fetchMode != null && { fetchMode: args.fetchMode },
52300
- ...args.proxy != null && { proxy: args.proxy }
52317
+ format: args.format ?? "markdown"
52301
52318
  });
52302
52319
  return jsonResult(response);
52303
52320
  }));
@@ -52307,6 +52324,12 @@ function registerTools(server, ctx, capabilities) {
52307
52324
  inputSchema: {},
52308
52325
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
52309
52326
  }, withErrorHandling("whoami", async () => {
52327
+ if (ctx.principal.type === "serviceAccount") {
52328
+ return jsonResult({
52329
+ authMethod: "Shared organization service account",
52330
+ teamId: ctx.principal.teamId
52331
+ });
52332
+ }
52310
52333
  const jwt2 = await getValidJwt(ctx);
52311
52334
  const user = await ctx.client.user.getCurrentUser();
52312
52335
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
@@ -53181,6 +53204,12 @@ function registerTools(server, ctx, capabilities) {
53181
53204
  inputSchema: {},
53182
53205
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
53183
53206
  }, withErrorHandling("team_list", async () => {
53207
+ if (ctx.principal.type === "serviceAccount") {
53208
+ return jsonResult({
53209
+ teams: [{ id: ctx.principal.teamId, active: true, sharedServiceAccount: true }],
53210
+ activeTeamId: ctx.principal.teamId
53211
+ });
53212
+ }
53184
53213
  const jwt2 = await getValidJwt(ctx);
53185
53214
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
53186
53215
  const activeTeamId = ctx.teamId ?? teams[0]?.id;
@@ -53202,6 +53231,9 @@ function registerTools(server, ctx, capabilities) {
53202
53231
  },
53203
53232
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
53204
53233
  }, withErrorHandling("team_switch", async (args) => {
53234
+ if (ctx.principal.type === "serviceAccount") {
53235
+ return errorResult("This shared organization connector is fixed to one team and cannot switch teams.");
53236
+ }
53205
53237
  const jwt2 = await getValidJwt(ctx);
53206
53238
  const teams = await ctx.client.listTeams({ bearerToken: jwt2 });
53207
53239
  const identifier = args.teamIdentifier;
@@ -53589,8 +53621,8 @@ function registerTools(server, ctx, capabilities) {
53589
53621
  }
53590
53622
  var SchemaFieldShape, DASHBOARD_BASE_URL = "https://www.kadoa.com", WORKFLOW_AUDIT_WATCHED_KEYS;
53591
53623
  var init_tools = __esm(() => {
53592
- init_zod();
53593
53624
  init_dist2();
53625
+ init_zod();
53594
53626
  init_client2();
53595
53627
  SchemaFieldShape = {
53596
53628
  name: exports_external.string().describe("Field name"),
@@ -53619,7 +53651,7 @@ var package_default;
53619
53651
  var init_package = __esm(() => {
53620
53652
  package_default = {
53621
53653
  name: "@kadoa/mcp",
53622
- version: "0.5.10",
53654
+ version: "0.5.12",
53623
53655
  description: "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
53624
53656
  type: "module",
53625
53657
  main: "dist/index.js",
@@ -57651,11 +57683,87 @@ var init_bearerAuth = __esm(() => {
57651
57683
  init_errors4();
57652
57684
  });
57653
57685
 
57686
+ // src/service-account-config.ts
57687
+ function getServiceAccountOAuthConfig() {
57688
+ const values = Object.fromEntries(Object.entries(ENV_KEYS).map(([field, envKey]) => [field, process.env[envKey]?.trim()]));
57689
+ if (Object.values(values).every((value) => !value))
57690
+ return;
57691
+ const missing = Object.entries(values).filter(([, value]) => !value).map(([field]) => ENV_KEYS[field]);
57692
+ if (missing.length > 0) {
57693
+ throw new Error(`Incomplete service-account OAuth configuration; missing ${missing.join(", ")}`);
57694
+ }
57695
+ const redirectUri = new URL(values.redirectUri);
57696
+ if (redirectUri.protocol !== "https:" && redirectUri.hostname !== "localhost") {
57697
+ throw new Error("MCP_SERVICE_ACCOUNT_OAUTH_REDIRECT_URI must use HTTPS");
57698
+ }
57699
+ return {
57700
+ clientId: values.clientId,
57701
+ clientSecret: values.clientSecret,
57702
+ redirectUri: redirectUri.toString(),
57703
+ credentialRef: values.credentialRef,
57704
+ teamId: values.teamId,
57705
+ label: values.label
57706
+ };
57707
+ }
57708
+ function getConfiguredServiceAccountClient(clientId) {
57709
+ const configuredClientId = process.env.MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_ID?.trim();
57710
+ if (!configuredClientId || configuredClientId !== clientId)
57711
+ return;
57712
+ const config2 = getServiceAccountOAuthConfig();
57713
+ if (!config2)
57714
+ return;
57715
+ return {
57716
+ client_id: config2.clientId,
57717
+ client_secret: config2.clientSecret,
57718
+ client_id_issued_at: 0,
57719
+ client_name: config2.label,
57720
+ redirect_uris: [config2.redirectUri],
57721
+ token_endpoint_auth_method: "client_secret_post",
57722
+ grant_types: ["authorization_code", "refresh_token"],
57723
+ response_types: ["code"]
57724
+ };
57725
+ }
57726
+ var ENV_KEYS;
57727
+ var init_service_account_config = __esm(() => {
57728
+ ENV_KEYS = {
57729
+ clientId: "MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_ID",
57730
+ clientSecret: "MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_SECRET",
57731
+ redirectUri: "MCP_SERVICE_ACCOUNT_OAUTH_REDIRECT_URI",
57732
+ credentialRef: "MCP_SERVICE_ACCOUNT_CREDENTIAL_REF",
57733
+ teamId: "MCP_SERVICE_ACCOUNT_TEAM_ID",
57734
+ label: "MCP_SERVICE_ACCOUNT_LABEL"
57735
+ };
57736
+ });
57737
+
57654
57738
  // src/auth.ts
57655
57739
  import { randomBytes, createHash as createHash2 } from "node:crypto";
57656
57740
  function randomToken(bytes = 32) {
57657
57741
  return randomBytes(bytes).toString("hex");
57658
57742
  }
57743
+ function readPrincipal(entry) {
57744
+ if (entry.principal)
57745
+ return entry.principal;
57746
+ if (entry.supabaseJwt && entry.supabaseRefreshToken) {
57747
+ return {
57748
+ type: "user",
57749
+ supabaseJwt: entry.supabaseJwt,
57750
+ supabaseRefreshToken: entry.supabaseRefreshToken,
57751
+ teamId: entry.teamId
57752
+ };
57753
+ }
57754
+ throw new InvalidTokenError("Stored token has no valid principal");
57755
+ }
57756
+ function persistPrincipal(principal) {
57757
+ if (principal.type === "serviceAccount") {
57758
+ return { principal, teamId: principal.teamId };
57759
+ }
57760
+ return {
57761
+ principal,
57762
+ supabaseJwt: principal.supabaseJwt,
57763
+ supabaseRefreshToken: principal.supabaseRefreshToken,
57764
+ teamId: principal.teamId
57765
+ };
57766
+ }
57659
57767
  function generatePKCE() {
57660
57768
  const verifier = randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "").slice(0, 128);
57661
57769
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
@@ -57753,7 +57861,7 @@ class KadoaOAuthProvider {
57753
57861
  const store = this.store;
57754
57862
  return {
57755
57863
  async getClient(clientId) {
57756
- return store.get("clients", clientId);
57864
+ return getConfiguredServiceAccountClient(clientId) ?? store.get("clients", clientId);
57757
57865
  },
57758
57866
  async registerClient(client) {
57759
57867
  const clientId = randomToken(16);
@@ -57768,10 +57876,9 @@ class KadoaOAuthProvider {
57768
57876
  };
57769
57877
  }
57770
57878
  async authorize(client, params, res) {
57771
- const supabaseUrl = process.env.SUPABASE_URL;
57772
57879
  const serverUrl = process.env.MCP_SERVER_URL;
57773
- if (!supabaseUrl || !serverUrl) {
57774
- throw new Error("SUPABASE_URL and MCP_SERVER_URL must be configured");
57880
+ if (!serverUrl) {
57881
+ throw new Error("MCP_SERVER_URL must be configured");
57775
57882
  }
57776
57883
  const state = randomToken();
57777
57884
  const { verifier, challenge } = generatePKCE();
@@ -57780,6 +57887,14 @@ class KadoaOAuthProvider {
57780
57887
  params,
57781
57888
  supabaseCodeVerifier: verifier
57782
57889
  }, 600);
57890
+ const configuredServiceClientId = process.env.MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_ID?.trim();
57891
+ if (configuredServiceClientId === client.client_id) {
57892
+ const serviceConfig = getServiceAccountOAuthConfig();
57893
+ if (!serviceConfig)
57894
+ throw new Error("Service-account OAuth configuration is unavailable");
57895
+ res.type("html").send(renderServiceAccountConsentPage(state, serviceConfig.label));
57896
+ return;
57897
+ }
57783
57898
  res.type("html").send(renderLoginPage(state));
57784
57899
  }
57785
57900
  async handleGoogleLogin(req, res) {
@@ -57897,8 +58012,9 @@ class KadoaOAuthProvider {
57897
58012
  if (teams.length === 1) {
57898
58013
  const refreshed = await setActiveTeamAndRefresh(supabaseJwt, supabaseRefreshToken, teams[0].id);
57899
58014
  await this.completeAuthFlow(pending, res, {
57900
- jwt: refreshed.jwt,
57901
- refreshToken: refreshed.refreshToken,
58015
+ type: "user",
58016
+ supabaseJwt: refreshed.jwt,
58017
+ supabaseRefreshToken: refreshed.refreshToken,
57902
58018
  teamId: teams[0].id
57903
58019
  });
57904
58020
  return;
@@ -57927,29 +58043,28 @@ class KadoaOAuthProvider {
57927
58043
  await this.store.del("auth_codes", authorizationCode);
57928
58044
  throw new Error("Authorization code expired");
57929
58045
  }
58046
+ if (entry.clientId !== _client.client_id) {
58047
+ throw new Error("OAuth client mismatch for authorization code");
58048
+ }
57930
58049
  if (redirectUri && redirectUri !== entry.redirectUri) {
57931
58050
  throw new Error("redirect_uri mismatch");
57932
58051
  }
57933
58052
  const accessToken = randomToken();
57934
58053
  const refreshToken = randomToken();
57935
58054
  const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
58055
+ const principal = readPrincipal(entry);
57936
58056
  await this.store.set("access_tokens", accessToken, {
57937
- supabaseJwt: entry.supabaseJwt,
57938
- supabaseRefreshToken: entry.supabaseRefreshToken,
57939
- teamId: entry.teamId,
58057
+ ...persistPrincipal(principal),
57940
58058
  clientId: entry.clientId,
57941
58059
  expiresAt
57942
58060
  }, ACCESS_TOKEN_TTL);
57943
58061
  await this.store.set("refresh_tokens", refreshToken, {
57944
- supabaseJwt: entry.supabaseJwt,
57945
- supabaseRefreshToken: entry.supabaseRefreshToken,
57946
- teamId: entry.teamId,
58062
+ ...persistPrincipal(principal),
57947
58063
  clientId: entry.clientId
57948
58064
  }, 2592000);
57949
58065
  await this.store.del("auth_codes", authorizationCode);
57950
- const claims = jwtClaims(entry.supabaseJwt);
57951
58066
  const sessionCount = await this.store.size("access_tokens");
57952
- console.log(`[AUTH] LOGIN: tokens issued (email=${claims.email}, team=${entry.teamId}, token=${accessToken.slice(0, 12)}..., ttl=${ACCESS_TOKEN_TTL}s, active_sessions=${sessionCount})`);
58067
+ console.log(`[AUTH] LOGIN: tokens issued (mode=${principal.type}, team=${principal.teamId}, ttl=${ACCESS_TOKEN_TTL}s, active_sessions=${sessionCount})`);
57953
58068
  return {
57954
58069
  access_token: accessToken,
57955
58070
  token_type: "bearer",
@@ -57961,12 +58076,36 @@ class KadoaOAuthProvider {
57961
58076
  const entry = await this.store.get("refresh_tokens", refreshToken);
57962
58077
  if (!entry) {
57963
58078
  const sessionCount = await this.store.size("refresh_tokens");
57964
- console.error(`[AUTH] REFRESH_FAIL: unknown refresh token (token=${refreshToken.slice(0, 12)}..., active_sessions=${sessionCount})`);
58079
+ console.error(`[AUTH] REFRESH_FAIL: unknown refresh token (active_sessions=${sessionCount})`);
57965
58080
  throw new InvalidTokenError("Unknown or expired refresh token");
57966
58081
  }
58082
+ if (entry.clientId !== _client.client_id) {
58083
+ throw new Error("OAuth client mismatch for refresh token");
58084
+ }
57967
58085
  await this.store.del("refresh_tokens", refreshToken);
57968
- let { supabaseJwt, supabaseRefreshToken } = entry;
57969
- const claims = jwtClaims(entry.supabaseJwt);
58086
+ const storedPrincipal = readPrincipal(entry);
58087
+ if (storedPrincipal.type === "serviceAccount") {
58088
+ const newAccessToken2 = randomToken();
58089
+ const newRefreshToken2 = randomToken();
58090
+ const expiresAt2 = Date.now() + ACCESS_TOKEN_TTL * 1000;
58091
+ await this.store.set("access_tokens", newAccessToken2, {
58092
+ ...persistPrincipal(storedPrincipal),
58093
+ clientId: entry.clientId,
58094
+ expiresAt: expiresAt2
58095
+ }, ACCESS_TOKEN_TTL);
58096
+ await this.store.set("refresh_tokens", newRefreshToken2, {
58097
+ ...persistPrincipal(storedPrincipal),
58098
+ clientId: entry.clientId
58099
+ }, 2592000);
58100
+ return {
58101
+ access_token: newAccessToken2,
58102
+ token_type: "bearer",
58103
+ expires_in: ACCESS_TOKEN_TTL,
58104
+ refresh_token: newRefreshToken2
58105
+ };
58106
+ }
58107
+ let { supabaseJwt, supabaseRefreshToken } = storedPrincipal;
58108
+ const claims = jwtClaims(storedPrincipal.supabaseJwt);
57970
58109
  const context = `email=${claims.email}, team=${entry.teamId}`;
57971
58110
  try {
57972
58111
  const { refreshSupabaseJwtRaw: refreshSupabaseJwtRaw2, SessionExpiredError: SessionExpiredError2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
@@ -57990,17 +58129,19 @@ class KadoaOAuthProvider {
57990
58129
  const newAccessToken = randomToken();
57991
58130
  const newRefreshToken = randomToken();
57992
58131
  const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
57993
- await this.store.set("access_tokens", newAccessToken, {
58132
+ const refreshedPrincipal = {
58133
+ type: "user",
57994
58134
  supabaseJwt,
57995
58135
  supabaseRefreshToken,
57996
- teamId,
58136
+ teamId
58137
+ };
58138
+ await this.store.set("access_tokens", newAccessToken, {
58139
+ ...persistPrincipal(refreshedPrincipal),
57997
58140
  clientId: entry.clientId,
57998
58141
  expiresAt
57999
58142
  }, ACCESS_TOKEN_TTL);
58000
58143
  await this.store.set("refresh_tokens", newRefreshToken, {
58001
- supabaseJwt,
58002
- supabaseRefreshToken,
58003
- teamId,
58144
+ ...persistPrincipal(refreshedPrincipal),
58004
58145
  clientId: entry.clientId
58005
58146
  }, 2592000);
58006
58147
  return {
@@ -58014,25 +58155,28 @@ class KadoaOAuthProvider {
58014
58155
  const entry = await this.store.get("access_tokens", token);
58015
58156
  if (!entry) {
58016
58157
  const sessionCount = await this.store.size("access_tokens");
58017
- console.error(`[AUTH] VERIFY_FAIL: unknown token (token=${token.slice(0, 12)}..., active_sessions=${sessionCount})`);
58158
+ console.error(`[AUTH] VERIFY_FAIL: unknown token (active_sessions=${sessionCount})`);
58018
58159
  throw new InvalidTokenError("Unknown or expired access token");
58019
58160
  }
58020
58161
  if (entry.expiresAt < Date.now()) {
58021
58162
  const expiredAgo = Math.round((Date.now() - entry.expiresAt) / 1000);
58022
- const claims = jwtClaims(entry.supabaseJwt);
58023
- console.error(`[AUTH] VERIFY_FAIL: token expired ${expiredAgo}s ago (email=${claims.email}, team=${entry.teamId}, token=${token.slice(0, 12)}...)`);
58163
+ console.error(`[AUTH] VERIFY_FAIL: token expired ${expiredAgo}s ago (team=${entry.teamId})`);
58024
58164
  await this.store.del("access_tokens", token);
58025
58165
  throw new InvalidTokenError("Access token expired");
58026
58166
  }
58167
+ const principal = readPrincipal(entry);
58027
58168
  return {
58028
58169
  token,
58029
58170
  clientId: entry.clientId,
58030
58171
  scopes: [],
58031
58172
  expiresAt: Math.floor(entry.expiresAt / 1000),
58032
58173
  extra: {
58033
- supabaseJwt: entry.supabaseJwt,
58034
- supabaseRefreshToken: entry.supabaseRefreshToken,
58035
- teamId: entry.teamId
58174
+ principal,
58175
+ ...principal.type === "user" ? {
58176
+ supabaseJwt: principal.supabaseJwt,
58177
+ supabaseRefreshToken: principal.supabaseRefreshToken
58178
+ } : {},
58179
+ teamId: principal.teamId
58036
58180
  }
58037
58181
  };
58038
58182
  }
@@ -58062,6 +58206,29 @@ class KadoaOAuthProvider {
58062
58206
  res.redirect(redirectUrl.toString());
58063
58207
  }
58064
58208
  }
58209
+ async handleServiceAccountConsent(req, res) {
58210
+ const { state } = req.body;
58211
+ if (!state) {
58212
+ res.status(400).send("Missing state parameter");
58213
+ return;
58214
+ }
58215
+ const pending = await this.store.get("pending_auths", state);
58216
+ if (!pending) {
58217
+ res.status(400).send("Unknown or expired state parameter");
58218
+ return;
58219
+ }
58220
+ const config2 = getServiceAccountOAuthConfig();
58221
+ if (!config2 || pending.client.client_id !== config2.clientId) {
58222
+ res.status(403).send("Service-account authorization is not available for this client");
58223
+ return;
58224
+ }
58225
+ await this.store.del("pending_auths", state);
58226
+ await this.completeAuthFlow(pending, res, {
58227
+ type: "serviceAccount",
58228
+ credentialRef: config2.credentialRef,
58229
+ teamId: config2.teamId
58230
+ });
58231
+ }
58065
58232
  async handleTeamSelection(req, res) {
58066
58233
  const { token, teamId } = req.body;
58067
58234
  if (!token || !teamId) {
@@ -58086,8 +58253,9 @@ class KadoaOAuthProvider {
58086
58253
  try {
58087
58254
  const refreshed = await setActiveTeamAndRefresh(entry.supabaseJwt, entry.supabaseRefreshToken, teamId);
58088
58255
  await this.completeAuthFlow(entry.pending, res, {
58089
- jwt: refreshed.jwt,
58090
- refreshToken: refreshed.refreshToken,
58256
+ type: "user",
58257
+ supabaseJwt: refreshed.jwt,
58258
+ supabaseRefreshToken: refreshed.refreshToken,
58091
58259
  teamId
58092
58260
  });
58093
58261
  } catch (error48) {
@@ -58101,12 +58269,10 @@ class KadoaOAuthProvider {
58101
58269
  res.redirect(redirectUrl.toString());
58102
58270
  }
58103
58271
  }
58104
- async completeAuthFlow(pending, res, credentials) {
58272
+ async completeAuthFlow(pending, res, principal) {
58105
58273
  const mcpCode = randomToken();
58106
58274
  await this.store.set("auth_codes", mcpCode, {
58107
- supabaseJwt: credentials.jwt,
58108
- supabaseRefreshToken: credentials.refreshToken,
58109
- teamId: credentials.teamId,
58275
+ ...persistPrincipal(principal),
58110
58276
  codeChallenge: pending.params.codeChallenge,
58111
58277
  clientId: pending.client.client_id,
58112
58278
  redirectUri: pending.params.redirectUri,
@@ -58120,6 +58286,33 @@ class KadoaOAuthProvider {
58120
58286
  res.redirect(redirectUrl.toString());
58121
58287
  }
58122
58288
  }
58289
+ function renderServiceAccountConsentPage(state, label) {
58290
+ return `<!DOCTYPE html>
58291
+ <html lang="en">
58292
+ <head>
58293
+ <meta charset="utf-8" />
58294
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
58295
+ <title>Connect shared Kadoa workspace</title>
58296
+ <style>
58297
+ body { font-family: ui-sans-serif, system-ui, sans-serif; background: #fafafa; color: #18181b; min-height: 100vh; display: grid; place-items: center; margin: 0; }
58298
+ main { width: min(420px, calc(100% - 2rem)); background: white; border: 1px solid #e4e4e7; border-radius: 8px; padding: 2rem; box-sizing: border-box; }
58299
+ h1 { font-size: 1.25rem; margin: 0 0 0.75rem; }
58300
+ p { color: #52525b; line-height: 1.5; }
58301
+ button { width: 100%; border: 1px solid #9a3412; border-radius: 4px; padding: 0.7rem 1rem; color: white; background: #c2410c; font: inherit; font-weight: 600; cursor: pointer; }
58302
+ </style>
58303
+ </head>
58304
+ <body>
58305
+ <main>
58306
+ <h1>Connect ${escapeHtml(label)}</h1>
58307
+ <p>Claude will use the shared Kadoa service account for this organization. Activity will not be attributed to your individual Kadoa identity.</p>
58308
+ <form method="POST" action="/auth/service-account">
58309
+ <input type="hidden" name="state" value="${escapeHtml(state)}" />
58310
+ <button type="submit">Continue with shared workspace</button>
58311
+ </form>
58312
+ </main>
58313
+ </body>
58314
+ </html>`;
58315
+ }
58123
58316
  function renderTeamSelectionPage(teams, selectionToken) {
58124
58317
  const teamButtons = teams.map((t) => `
58125
58318
  <button type="submit" name="teamId" value="${t.id}" class="team-btn">
@@ -58542,6 +58735,7 @@ function escapeHtml(str) {
58542
58735
  var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
58543
58736
  var init_auth2 = __esm(() => {
58544
58737
  init_errors4();
58738
+ init_service_account_config();
58545
58739
  TEAM_SELECTION_TTL = 10 * 60 * 1000;
58546
58740
  ACCESS_TOKEN_TTL = 7 * 24 * 3600;
58547
58741
  });
@@ -58657,14 +58851,21 @@ function jwtClaims2(jwt2) {
58657
58851
  }
58658
58852
  function resolveAuth(req) {
58659
58853
  const extra = req.auth?.extra;
58660
- if (!extra) {
58661
- console.error("[AUTH_RESOLVE] FAIL: req.auth.extra is missing");
58854
+ if (!extra || !extra.principal || typeof extra.principal !== "object") {
58855
+ console.error("[AUTH_RESOLVE] FAIL: req.auth.extra.principal is missing");
58662
58856
  return;
58663
58857
  }
58664
- if (typeof extra.supabaseJwt === "string") {
58665
- const claims = jwtClaims2(extra.supabaseJwt);
58666
- const userId = claims.sub;
58667
- if (!userId) {
58858
+ const principal = extra.principal;
58859
+ if (principal.type === "serviceAccount") {
58860
+ if (!principal.credentialRef || !principal.teamId) {
58861
+ console.error("[AUTH_RESOLVE] FAIL: service-account principal is incomplete");
58862
+ return;
58863
+ }
58864
+ return { principal, mcpToken: req.auth.token };
58865
+ }
58866
+ if (principal.type === "user") {
58867
+ const claims = jwtClaims2(principal.supabaseJwt);
58868
+ if (!claims.sub) {
58668
58869
  console.error(`[AUTH_RESOLVE] FAIL: JWT missing sub claim (email=${claims.email ?? "unknown"})`);
58669
58870
  return;
58670
58871
  }
@@ -58672,20 +58873,14 @@ function resolveAuth(req) {
58672
58873
  if (exp) {
58673
58874
  const remainingSec = exp - Math.floor(Date.now() / 1000);
58674
58875
  if (remainingSec <= 0) {
58675
- console.error(`[AUTH_RESOLVE] WARN: JWT already expired ${-remainingSec}s ago (email=${claims.email}, team=${extra.teamId})`);
58876
+ console.error(`[AUTH_RESOLVE] WARN: JWT already expired ${-remainingSec}s ago (team=${principal.teamId})`);
58676
58877
  } else if (remainingSec < 300) {
58677
- console.error(`[AUTH_RESOLVE] WARN: JWT expires in ${remainingSec}s (email=${claims.email}, team=${extra.teamId})`);
58878
+ console.error(`[AUTH_RESOLVE] WARN: JWT expires in ${remainingSec}s (team=${principal.teamId})`);
58678
58879
  }
58679
58880
  }
58680
- return {
58681
- jwt: extra.supabaseJwt,
58682
- refreshToken: extra.supabaseRefreshToken ?? "",
58683
- teamId: extra.teamId ?? "",
58684
- userId,
58685
- mcpToken: req.auth.token
58686
- };
58881
+ return { principal, mcpToken: req.auth.token };
58687
58882
  }
58688
- console.error(`[AUTH_RESOLVE] FAIL: no supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
58883
+ console.error("[AUTH_RESOLVE] FAIL: unknown principal type");
58689
58884
  return;
58690
58885
  }
58691
58886
  async function startHttpServer(options) {
@@ -58710,6 +58905,9 @@ async function startHttpServer(options) {
58710
58905
  app.post("/auth/login", express8.urlencoded({ extended: false }), (req, res) => {
58711
58906
  provider.handleEmailPasswordLogin(req, res);
58712
58907
  });
58908
+ app.post("/auth/service-account", express8.urlencoded({ extended: false }), (req, res) => {
58909
+ provider.handleServiceAccountConsent(req, res);
58910
+ });
58713
58911
  app.post("/auth/sso", express8.urlencoded({ extended: false }), (req, res) => {
58714
58912
  provider.handleSSOLogin(req, res);
58715
58913
  });
@@ -58735,19 +58933,24 @@ async function startHttpServer(options) {
58735
58933
  });
58736
58934
  return;
58737
58935
  }
58738
- const identity2 = `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...`;
58739
- if (isJwtExpired(auth.jwt)) {
58936
+ let principal = auth.principal;
58937
+ const identity2 = `mode=${principal.type}:team=${principal.teamId.slice(0, 8)}...`;
58938
+ if (principal.type === "user" && isJwtExpired(principal.supabaseJwt)) {
58740
58939
  try {
58741
- const refreshed = await refreshSupabaseJwtRaw(auth.refreshToken);
58940
+ const refreshed = await refreshSupabaseJwtRaw(principal.supabaseRefreshToken);
58742
58941
  if (refreshed) {
58743
- auth.jwt = refreshed.jwt;
58744
- auth.refreshToken = refreshed.refreshToken;
58942
+ principal = {
58943
+ ...principal,
58944
+ supabaseJwt: refreshed.jwt,
58945
+ supabaseRefreshToken: refreshed.refreshToken
58946
+ };
58745
58947
  const entry = await store.get("access_tokens", auth.mcpToken);
58746
58948
  if (entry) {
58747
58949
  const remainingMs = entry.expiresAt - Date.now();
58748
58950
  if (remainingMs > 0) {
58749
58951
  await store.set("access_tokens", auth.mcpToken, {
58750
58952
  ...entry,
58953
+ principal,
58751
58954
  supabaseJwt: refreshed.jwt,
58752
58955
  supabaseRefreshToken: refreshed.refreshToken
58753
58956
  }, Math.ceil(remainingMs / 1000));
@@ -58774,31 +58977,32 @@ async function startHttpServer(options) {
58774
58977
  const transport = new StreamableHTTPServerTransport({
58775
58978
  sessionIdGenerator: undefined
58776
58979
  });
58777
- const server = await createServer({
58778
- jwt: auth.jwt,
58779
- refreshToken: auth.refreshToken,
58780
- teamId: auth.teamId,
58781
- persist: async (state) => {
58782
- const entry = await store.get("access_tokens", auth.mcpToken);
58783
- if (!entry) {
58784
- console.error(`[PERSIST] WARN: access token gone from store (token=${auth.mcpToken.slice(0, 12)}...)`);
58785
- return;
58786
- }
58787
- const remainingMs = entry.expiresAt - Date.now();
58788
- if (remainingMs <= 0) {
58789
- console.error(`[PERSIST] WARN: access token already expired, skipping persist`);
58790
- return;
58791
- }
58792
- const ttlSeconds = Math.ceil(remainingMs / 1000);
58793
- await store.set("access_tokens", auth.mcpToken, {
58794
- ...entry,
58795
- supabaseJwt: state.supabaseJwt ?? entry.supabaseJwt,
58796
- supabaseRefreshToken: state.supabaseRefreshToken ?? entry.supabaseRefreshToken,
58797
- teamId: state.teamId ?? entry.teamId
58798
- }, ttlSeconds);
58799
- console.error(`[PERSIST] OK: updated access token in store (token=${auth.mcpToken.slice(0, 12)}..., team=${state.teamId ?? "unchanged"}, ttl=${ttlSeconds}s)`);
58980
+ const persist = principal.type === "user" ? async (state) => {
58981
+ const entry = await store.get("access_tokens", auth.mcpToken);
58982
+ if (!entry) {
58983
+ console.error("[PERSIST] WARN: access token gone from store");
58984
+ return;
58800
58985
  }
58801
- });
58986
+ const remainingMs = entry.expiresAt - Date.now();
58987
+ if (remainingMs <= 0) {
58988
+ console.error("[PERSIST] WARN: access token already expired, skipping persist");
58989
+ return;
58990
+ }
58991
+ const updatedPrincipal = {
58992
+ type: "user",
58993
+ supabaseJwt: state.supabaseJwt ?? principal.supabaseJwt,
58994
+ supabaseRefreshToken: state.supabaseRefreshToken ?? principal.supabaseRefreshToken,
58995
+ teamId: state.teamId ?? principal.teamId
58996
+ };
58997
+ await store.set("access_tokens", auth.mcpToken, {
58998
+ ...entry,
58999
+ principal: updatedPrincipal,
59000
+ supabaseJwt: updatedPrincipal.supabaseJwt,
59001
+ supabaseRefreshToken: updatedPrincipal.supabaseRefreshToken,
59002
+ teamId: updatedPrincipal.teamId
59003
+ }, Math.ceil(remainingMs / 1000));
59004
+ } : undefined;
59005
+ const server = await createServer({ principal, persist });
58802
59006
  await server.connect(transport);
58803
59007
  await transport.handleRequest(req, res, req.body);
58804
59008
  } catch (error48) {
@@ -58852,11 +59056,18 @@ var init_http2 = __esm(async () => {
58852
59056
 
58853
59057
  // src/index.ts
58854
59058
  async function createServer(auth, options) {
58855
- const ctx = {
58856
- client: createKadoaClient({ jwt: auth.jwt, teamId: auth.teamId }),
59059
+ const principal = "principal" in auth ? auth.principal : {
59060
+ type: "user",
58857
59061
  supabaseJwt: auth.jwt,
58858
59062
  supabaseRefreshToken: auth.refreshToken,
58859
- teamId: auth.teamId,
59063
+ teamId: auth.teamId
59064
+ };
59065
+ const ctx = {
59066
+ client: createKadoaClient({ principal }),
59067
+ principal,
59068
+ supabaseJwt: principal.type === "user" ? principal.supabaseJwt : undefined,
59069
+ supabaseRefreshToken: principal.type === "user" ? principal.supabaseRefreshToken : undefined,
59070
+ teamId: principal.teamId,
58860
59071
  persist: auth.persist
58861
59072
  };
58862
59073
  const server = new McpServer({ name: "kadoa", version: package_default.version }, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
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",