@kadoa/mcp 0.5.10 → 0.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +309 -94
  2. package/package.json +1 -1
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)
@@ -52307,6 +52328,12 @@ function registerTools(server, ctx, capabilities) {
52307
52328
  inputSchema: {},
52308
52329
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
52309
52330
  }, withErrorHandling("whoami", async () => {
52331
+ if (ctx.principal.type === "serviceAccount") {
52332
+ return jsonResult({
52333
+ authMethod: "Shared organization service account",
52334
+ teamId: ctx.principal.teamId
52335
+ });
52336
+ }
52310
52337
  const jwt2 = await getValidJwt(ctx);
52311
52338
  const user = await ctx.client.user.getCurrentUser();
52312
52339
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
@@ -53181,6 +53208,12 @@ function registerTools(server, ctx, capabilities) {
53181
53208
  inputSchema: {},
53182
53209
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
53183
53210
  }, withErrorHandling("team_list", async () => {
53211
+ if (ctx.principal.type === "serviceAccount") {
53212
+ return jsonResult({
53213
+ teams: [{ id: ctx.principal.teamId, active: true, sharedServiceAccount: true }],
53214
+ activeTeamId: ctx.principal.teamId
53215
+ });
53216
+ }
53184
53217
  const jwt2 = await getValidJwt(ctx);
53185
53218
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
53186
53219
  const activeTeamId = ctx.teamId ?? teams[0]?.id;
@@ -53202,6 +53235,9 @@ function registerTools(server, ctx, capabilities) {
53202
53235
  },
53203
53236
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
53204
53237
  }, withErrorHandling("team_switch", async (args) => {
53238
+ if (ctx.principal.type === "serviceAccount") {
53239
+ return errorResult("This shared organization connector is fixed to one team and cannot switch teams.");
53240
+ }
53205
53241
  const jwt2 = await getValidJwt(ctx);
53206
53242
  const teams = await ctx.client.listTeams({ bearerToken: jwt2 });
53207
53243
  const identifier = args.teamIdentifier;
@@ -53619,7 +53655,7 @@ var package_default;
53619
53655
  var init_package = __esm(() => {
53620
53656
  package_default = {
53621
53657
  name: "@kadoa/mcp",
53622
- version: "0.5.10",
53658
+ version: "0.5.11",
53623
53659
  description: "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
53624
53660
  type: "module",
53625
53661
  main: "dist/index.js",
@@ -57651,11 +57687,87 @@ var init_bearerAuth = __esm(() => {
57651
57687
  init_errors4();
57652
57688
  });
57653
57689
 
57690
+ // src/service-account-config.ts
57691
+ function getServiceAccountOAuthConfig() {
57692
+ const values = Object.fromEntries(Object.entries(ENV_KEYS).map(([field, envKey]) => [field, process.env[envKey]?.trim()]));
57693
+ if (Object.values(values).every((value) => !value))
57694
+ return;
57695
+ const missing = Object.entries(values).filter(([, value]) => !value).map(([field]) => ENV_KEYS[field]);
57696
+ if (missing.length > 0) {
57697
+ throw new Error(`Incomplete service-account OAuth configuration; missing ${missing.join(", ")}`);
57698
+ }
57699
+ const redirectUri = new URL(values.redirectUri);
57700
+ if (redirectUri.protocol !== "https:" && redirectUri.hostname !== "localhost") {
57701
+ throw new Error("MCP_SERVICE_ACCOUNT_OAUTH_REDIRECT_URI must use HTTPS");
57702
+ }
57703
+ return {
57704
+ clientId: values.clientId,
57705
+ clientSecret: values.clientSecret,
57706
+ redirectUri: redirectUri.toString(),
57707
+ credentialRef: values.credentialRef,
57708
+ teamId: values.teamId,
57709
+ label: values.label
57710
+ };
57711
+ }
57712
+ function getConfiguredServiceAccountClient(clientId) {
57713
+ const configuredClientId = process.env.MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_ID?.trim();
57714
+ if (!configuredClientId || configuredClientId !== clientId)
57715
+ return;
57716
+ const config2 = getServiceAccountOAuthConfig();
57717
+ if (!config2)
57718
+ return;
57719
+ return {
57720
+ client_id: config2.clientId,
57721
+ client_secret: config2.clientSecret,
57722
+ client_id_issued_at: 0,
57723
+ client_name: config2.label,
57724
+ redirect_uris: [config2.redirectUri],
57725
+ token_endpoint_auth_method: "client_secret_post",
57726
+ grant_types: ["authorization_code", "refresh_token"],
57727
+ response_types: ["code"]
57728
+ };
57729
+ }
57730
+ var ENV_KEYS;
57731
+ var init_service_account_config = __esm(() => {
57732
+ ENV_KEYS = {
57733
+ clientId: "MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_ID",
57734
+ clientSecret: "MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_SECRET",
57735
+ redirectUri: "MCP_SERVICE_ACCOUNT_OAUTH_REDIRECT_URI",
57736
+ credentialRef: "MCP_SERVICE_ACCOUNT_CREDENTIAL_REF",
57737
+ teamId: "MCP_SERVICE_ACCOUNT_TEAM_ID",
57738
+ label: "MCP_SERVICE_ACCOUNT_LABEL"
57739
+ };
57740
+ });
57741
+
57654
57742
  // src/auth.ts
57655
57743
  import { randomBytes, createHash as createHash2 } from "node:crypto";
57656
57744
  function randomToken(bytes = 32) {
57657
57745
  return randomBytes(bytes).toString("hex");
57658
57746
  }
57747
+ function readPrincipal(entry) {
57748
+ if (entry.principal)
57749
+ return entry.principal;
57750
+ if (entry.supabaseJwt && entry.supabaseRefreshToken) {
57751
+ return {
57752
+ type: "user",
57753
+ supabaseJwt: entry.supabaseJwt,
57754
+ supabaseRefreshToken: entry.supabaseRefreshToken,
57755
+ teamId: entry.teamId
57756
+ };
57757
+ }
57758
+ throw new InvalidTokenError("Stored token has no valid principal");
57759
+ }
57760
+ function persistPrincipal(principal) {
57761
+ if (principal.type === "serviceAccount") {
57762
+ return { principal, teamId: principal.teamId };
57763
+ }
57764
+ return {
57765
+ principal,
57766
+ supabaseJwt: principal.supabaseJwt,
57767
+ supabaseRefreshToken: principal.supabaseRefreshToken,
57768
+ teamId: principal.teamId
57769
+ };
57770
+ }
57659
57771
  function generatePKCE() {
57660
57772
  const verifier = randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9\-._~]/g, "").slice(0, 128);
57661
57773
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
@@ -57753,7 +57865,7 @@ class KadoaOAuthProvider {
57753
57865
  const store = this.store;
57754
57866
  return {
57755
57867
  async getClient(clientId) {
57756
- return store.get("clients", clientId);
57868
+ return getConfiguredServiceAccountClient(clientId) ?? store.get("clients", clientId);
57757
57869
  },
57758
57870
  async registerClient(client) {
57759
57871
  const clientId = randomToken(16);
@@ -57768,10 +57880,9 @@ class KadoaOAuthProvider {
57768
57880
  };
57769
57881
  }
57770
57882
  async authorize(client, params, res) {
57771
- const supabaseUrl = process.env.SUPABASE_URL;
57772
57883
  const serverUrl = process.env.MCP_SERVER_URL;
57773
- if (!supabaseUrl || !serverUrl) {
57774
- throw new Error("SUPABASE_URL and MCP_SERVER_URL must be configured");
57884
+ if (!serverUrl) {
57885
+ throw new Error("MCP_SERVER_URL must be configured");
57775
57886
  }
57776
57887
  const state = randomToken();
57777
57888
  const { verifier, challenge } = generatePKCE();
@@ -57780,6 +57891,14 @@ class KadoaOAuthProvider {
57780
57891
  params,
57781
57892
  supabaseCodeVerifier: verifier
57782
57893
  }, 600);
57894
+ const configuredServiceClientId = process.env.MCP_SERVICE_ACCOUNT_OAUTH_CLIENT_ID?.trim();
57895
+ if (configuredServiceClientId === client.client_id) {
57896
+ const serviceConfig = getServiceAccountOAuthConfig();
57897
+ if (!serviceConfig)
57898
+ throw new Error("Service-account OAuth configuration is unavailable");
57899
+ res.type("html").send(renderServiceAccountConsentPage(state, serviceConfig.label));
57900
+ return;
57901
+ }
57783
57902
  res.type("html").send(renderLoginPage(state));
57784
57903
  }
57785
57904
  async handleGoogleLogin(req, res) {
@@ -57897,8 +58016,9 @@ class KadoaOAuthProvider {
57897
58016
  if (teams.length === 1) {
57898
58017
  const refreshed = await setActiveTeamAndRefresh(supabaseJwt, supabaseRefreshToken, teams[0].id);
57899
58018
  await this.completeAuthFlow(pending, res, {
57900
- jwt: refreshed.jwt,
57901
- refreshToken: refreshed.refreshToken,
58019
+ type: "user",
58020
+ supabaseJwt: refreshed.jwt,
58021
+ supabaseRefreshToken: refreshed.refreshToken,
57902
58022
  teamId: teams[0].id
57903
58023
  });
57904
58024
  return;
@@ -57927,29 +58047,28 @@ class KadoaOAuthProvider {
57927
58047
  await this.store.del("auth_codes", authorizationCode);
57928
58048
  throw new Error("Authorization code expired");
57929
58049
  }
58050
+ if (entry.clientId !== _client.client_id) {
58051
+ throw new Error("OAuth client mismatch for authorization code");
58052
+ }
57930
58053
  if (redirectUri && redirectUri !== entry.redirectUri) {
57931
58054
  throw new Error("redirect_uri mismatch");
57932
58055
  }
57933
58056
  const accessToken = randomToken();
57934
58057
  const refreshToken = randomToken();
57935
58058
  const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
58059
+ const principal = readPrincipal(entry);
57936
58060
  await this.store.set("access_tokens", accessToken, {
57937
- supabaseJwt: entry.supabaseJwt,
57938
- supabaseRefreshToken: entry.supabaseRefreshToken,
57939
- teamId: entry.teamId,
58061
+ ...persistPrincipal(principal),
57940
58062
  clientId: entry.clientId,
57941
58063
  expiresAt
57942
58064
  }, ACCESS_TOKEN_TTL);
57943
58065
  await this.store.set("refresh_tokens", refreshToken, {
57944
- supabaseJwt: entry.supabaseJwt,
57945
- supabaseRefreshToken: entry.supabaseRefreshToken,
57946
- teamId: entry.teamId,
58066
+ ...persistPrincipal(principal),
57947
58067
  clientId: entry.clientId
57948
58068
  }, 2592000);
57949
58069
  await this.store.del("auth_codes", authorizationCode);
57950
- const claims = jwtClaims(entry.supabaseJwt);
57951
58070
  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})`);
58071
+ console.log(`[AUTH] LOGIN: tokens issued (mode=${principal.type}, team=${principal.teamId}, ttl=${ACCESS_TOKEN_TTL}s, active_sessions=${sessionCount})`);
57953
58072
  return {
57954
58073
  access_token: accessToken,
57955
58074
  token_type: "bearer",
@@ -57961,12 +58080,36 @@ class KadoaOAuthProvider {
57961
58080
  const entry = await this.store.get("refresh_tokens", refreshToken);
57962
58081
  if (!entry) {
57963
58082
  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})`);
58083
+ console.error(`[AUTH] REFRESH_FAIL: unknown refresh token (active_sessions=${sessionCount})`);
57965
58084
  throw new InvalidTokenError("Unknown or expired refresh token");
57966
58085
  }
58086
+ if (entry.clientId !== _client.client_id) {
58087
+ throw new Error("OAuth client mismatch for refresh token");
58088
+ }
57967
58089
  await this.store.del("refresh_tokens", refreshToken);
57968
- let { supabaseJwt, supabaseRefreshToken } = entry;
57969
- const claims = jwtClaims(entry.supabaseJwt);
58090
+ const storedPrincipal = readPrincipal(entry);
58091
+ if (storedPrincipal.type === "serviceAccount") {
58092
+ const newAccessToken2 = randomToken();
58093
+ const newRefreshToken2 = randomToken();
58094
+ const expiresAt2 = Date.now() + ACCESS_TOKEN_TTL * 1000;
58095
+ await this.store.set("access_tokens", newAccessToken2, {
58096
+ ...persistPrincipal(storedPrincipal),
58097
+ clientId: entry.clientId,
58098
+ expiresAt: expiresAt2
58099
+ }, ACCESS_TOKEN_TTL);
58100
+ await this.store.set("refresh_tokens", newRefreshToken2, {
58101
+ ...persistPrincipal(storedPrincipal),
58102
+ clientId: entry.clientId
58103
+ }, 2592000);
58104
+ return {
58105
+ access_token: newAccessToken2,
58106
+ token_type: "bearer",
58107
+ expires_in: ACCESS_TOKEN_TTL,
58108
+ refresh_token: newRefreshToken2
58109
+ };
58110
+ }
58111
+ let { supabaseJwt, supabaseRefreshToken } = storedPrincipal;
58112
+ const claims = jwtClaims(storedPrincipal.supabaseJwt);
57970
58113
  const context = `email=${claims.email}, team=${entry.teamId}`;
57971
58114
  try {
57972
58115
  const { refreshSupabaseJwtRaw: refreshSupabaseJwtRaw2, SessionExpiredError: SessionExpiredError2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
@@ -57990,17 +58133,19 @@ class KadoaOAuthProvider {
57990
58133
  const newAccessToken = randomToken();
57991
58134
  const newRefreshToken = randomToken();
57992
58135
  const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
57993
- await this.store.set("access_tokens", newAccessToken, {
58136
+ const refreshedPrincipal = {
58137
+ type: "user",
57994
58138
  supabaseJwt,
57995
58139
  supabaseRefreshToken,
57996
- teamId,
58140
+ teamId
58141
+ };
58142
+ await this.store.set("access_tokens", newAccessToken, {
58143
+ ...persistPrincipal(refreshedPrincipal),
57997
58144
  clientId: entry.clientId,
57998
58145
  expiresAt
57999
58146
  }, ACCESS_TOKEN_TTL);
58000
58147
  await this.store.set("refresh_tokens", newRefreshToken, {
58001
- supabaseJwt,
58002
- supabaseRefreshToken,
58003
- teamId,
58148
+ ...persistPrincipal(refreshedPrincipal),
58004
58149
  clientId: entry.clientId
58005
58150
  }, 2592000);
58006
58151
  return {
@@ -58014,25 +58159,28 @@ class KadoaOAuthProvider {
58014
58159
  const entry = await this.store.get("access_tokens", token);
58015
58160
  if (!entry) {
58016
58161
  const sessionCount = await this.store.size("access_tokens");
58017
- console.error(`[AUTH] VERIFY_FAIL: unknown token (token=${token.slice(0, 12)}..., active_sessions=${sessionCount})`);
58162
+ console.error(`[AUTH] VERIFY_FAIL: unknown token (active_sessions=${sessionCount})`);
58018
58163
  throw new InvalidTokenError("Unknown or expired access token");
58019
58164
  }
58020
58165
  if (entry.expiresAt < Date.now()) {
58021
58166
  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)}...)`);
58167
+ console.error(`[AUTH] VERIFY_FAIL: token expired ${expiredAgo}s ago (team=${entry.teamId})`);
58024
58168
  await this.store.del("access_tokens", token);
58025
58169
  throw new InvalidTokenError("Access token expired");
58026
58170
  }
58171
+ const principal = readPrincipal(entry);
58027
58172
  return {
58028
58173
  token,
58029
58174
  clientId: entry.clientId,
58030
58175
  scopes: [],
58031
58176
  expiresAt: Math.floor(entry.expiresAt / 1000),
58032
58177
  extra: {
58033
- supabaseJwt: entry.supabaseJwt,
58034
- supabaseRefreshToken: entry.supabaseRefreshToken,
58035
- teamId: entry.teamId
58178
+ principal,
58179
+ ...principal.type === "user" ? {
58180
+ supabaseJwt: principal.supabaseJwt,
58181
+ supabaseRefreshToken: principal.supabaseRefreshToken
58182
+ } : {},
58183
+ teamId: principal.teamId
58036
58184
  }
58037
58185
  };
58038
58186
  }
@@ -58062,6 +58210,29 @@ class KadoaOAuthProvider {
58062
58210
  res.redirect(redirectUrl.toString());
58063
58211
  }
58064
58212
  }
58213
+ async handleServiceAccountConsent(req, res) {
58214
+ const { state } = req.body;
58215
+ if (!state) {
58216
+ res.status(400).send("Missing state parameter");
58217
+ return;
58218
+ }
58219
+ const pending = await this.store.get("pending_auths", state);
58220
+ if (!pending) {
58221
+ res.status(400).send("Unknown or expired state parameter");
58222
+ return;
58223
+ }
58224
+ const config2 = getServiceAccountOAuthConfig();
58225
+ if (!config2 || pending.client.client_id !== config2.clientId) {
58226
+ res.status(403).send("Service-account authorization is not available for this client");
58227
+ return;
58228
+ }
58229
+ await this.store.del("pending_auths", state);
58230
+ await this.completeAuthFlow(pending, res, {
58231
+ type: "serviceAccount",
58232
+ credentialRef: config2.credentialRef,
58233
+ teamId: config2.teamId
58234
+ });
58235
+ }
58065
58236
  async handleTeamSelection(req, res) {
58066
58237
  const { token, teamId } = req.body;
58067
58238
  if (!token || !teamId) {
@@ -58086,8 +58257,9 @@ class KadoaOAuthProvider {
58086
58257
  try {
58087
58258
  const refreshed = await setActiveTeamAndRefresh(entry.supabaseJwt, entry.supabaseRefreshToken, teamId);
58088
58259
  await this.completeAuthFlow(entry.pending, res, {
58089
- jwt: refreshed.jwt,
58090
- refreshToken: refreshed.refreshToken,
58260
+ type: "user",
58261
+ supabaseJwt: refreshed.jwt,
58262
+ supabaseRefreshToken: refreshed.refreshToken,
58091
58263
  teamId
58092
58264
  });
58093
58265
  } catch (error48) {
@@ -58101,12 +58273,10 @@ class KadoaOAuthProvider {
58101
58273
  res.redirect(redirectUrl.toString());
58102
58274
  }
58103
58275
  }
58104
- async completeAuthFlow(pending, res, credentials) {
58276
+ async completeAuthFlow(pending, res, principal) {
58105
58277
  const mcpCode = randomToken();
58106
58278
  await this.store.set("auth_codes", mcpCode, {
58107
- supabaseJwt: credentials.jwt,
58108
- supabaseRefreshToken: credentials.refreshToken,
58109
- teamId: credentials.teamId,
58279
+ ...persistPrincipal(principal),
58110
58280
  codeChallenge: pending.params.codeChallenge,
58111
58281
  clientId: pending.client.client_id,
58112
58282
  redirectUri: pending.params.redirectUri,
@@ -58120,6 +58290,33 @@ class KadoaOAuthProvider {
58120
58290
  res.redirect(redirectUrl.toString());
58121
58291
  }
58122
58292
  }
58293
+ function renderServiceAccountConsentPage(state, label) {
58294
+ return `<!DOCTYPE html>
58295
+ <html lang="en">
58296
+ <head>
58297
+ <meta charset="utf-8" />
58298
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
58299
+ <title>Connect shared Kadoa workspace</title>
58300
+ <style>
58301
+ body { font-family: ui-sans-serif, system-ui, sans-serif; background: #fafafa; color: #18181b; min-height: 100vh; display: grid; place-items: center; margin: 0; }
58302
+ main { width: min(420px, calc(100% - 2rem)); background: white; border: 1px solid #e4e4e7; border-radius: 8px; padding: 2rem; box-sizing: border-box; }
58303
+ h1 { font-size: 1.25rem; margin: 0 0 0.75rem; }
58304
+ p { color: #52525b; line-height: 1.5; }
58305
+ 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; }
58306
+ </style>
58307
+ </head>
58308
+ <body>
58309
+ <main>
58310
+ <h1>Connect ${escapeHtml(label)}</h1>
58311
+ <p>Claude will use the shared Kadoa service account for this organization. Activity will not be attributed to your individual Kadoa identity.</p>
58312
+ <form method="POST" action="/auth/service-account">
58313
+ <input type="hidden" name="state" value="${escapeHtml(state)}" />
58314
+ <button type="submit">Continue with shared workspace</button>
58315
+ </form>
58316
+ </main>
58317
+ </body>
58318
+ </html>`;
58319
+ }
58123
58320
  function renderTeamSelectionPage(teams, selectionToken) {
58124
58321
  const teamButtons = teams.map((t) => `
58125
58322
  <button type="submit" name="teamId" value="${t.id}" class="team-btn">
@@ -58542,6 +58739,7 @@ function escapeHtml(str) {
58542
58739
  var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
58543
58740
  var init_auth2 = __esm(() => {
58544
58741
  init_errors4();
58742
+ init_service_account_config();
58545
58743
  TEAM_SELECTION_TTL = 10 * 60 * 1000;
58546
58744
  ACCESS_TOKEN_TTL = 7 * 24 * 3600;
58547
58745
  });
@@ -58657,14 +58855,21 @@ function jwtClaims2(jwt2) {
58657
58855
  }
58658
58856
  function resolveAuth(req) {
58659
58857
  const extra = req.auth?.extra;
58660
- if (!extra) {
58661
- console.error("[AUTH_RESOLVE] FAIL: req.auth.extra is missing");
58858
+ if (!extra || !extra.principal || typeof extra.principal !== "object") {
58859
+ console.error("[AUTH_RESOLVE] FAIL: req.auth.extra.principal is missing");
58662
58860
  return;
58663
58861
  }
58664
- if (typeof extra.supabaseJwt === "string") {
58665
- const claims = jwtClaims2(extra.supabaseJwt);
58666
- const userId = claims.sub;
58667
- if (!userId) {
58862
+ const principal = extra.principal;
58863
+ if (principal.type === "serviceAccount") {
58864
+ if (!principal.credentialRef || !principal.teamId) {
58865
+ console.error("[AUTH_RESOLVE] FAIL: service-account principal is incomplete");
58866
+ return;
58867
+ }
58868
+ return { principal, mcpToken: req.auth.token };
58869
+ }
58870
+ if (principal.type === "user") {
58871
+ const claims = jwtClaims2(principal.supabaseJwt);
58872
+ if (!claims.sub) {
58668
58873
  console.error(`[AUTH_RESOLVE] FAIL: JWT missing sub claim (email=${claims.email ?? "unknown"})`);
58669
58874
  return;
58670
58875
  }
@@ -58672,20 +58877,14 @@ function resolveAuth(req) {
58672
58877
  if (exp) {
58673
58878
  const remainingSec = exp - Math.floor(Date.now() / 1000);
58674
58879
  if (remainingSec <= 0) {
58675
- console.error(`[AUTH_RESOLVE] WARN: JWT already expired ${-remainingSec}s ago (email=${claims.email}, team=${extra.teamId})`);
58880
+ console.error(`[AUTH_RESOLVE] WARN: JWT already expired ${-remainingSec}s ago (team=${principal.teamId})`);
58676
58881
  } else if (remainingSec < 300) {
58677
- console.error(`[AUTH_RESOLVE] WARN: JWT expires in ${remainingSec}s (email=${claims.email}, team=${extra.teamId})`);
58882
+ console.error(`[AUTH_RESOLVE] WARN: JWT expires in ${remainingSec}s (team=${principal.teamId})`);
58678
58883
  }
58679
58884
  }
58680
- return {
58681
- jwt: extra.supabaseJwt,
58682
- refreshToken: extra.supabaseRefreshToken ?? "",
58683
- teamId: extra.teamId ?? "",
58684
- userId,
58685
- mcpToken: req.auth.token
58686
- };
58885
+ return { principal, mcpToken: req.auth.token };
58687
58886
  }
58688
- console.error(`[AUTH_RESOLVE] FAIL: no supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
58887
+ console.error("[AUTH_RESOLVE] FAIL: unknown principal type");
58689
58888
  return;
58690
58889
  }
58691
58890
  async function startHttpServer(options) {
@@ -58710,6 +58909,9 @@ async function startHttpServer(options) {
58710
58909
  app.post("/auth/login", express8.urlencoded({ extended: false }), (req, res) => {
58711
58910
  provider.handleEmailPasswordLogin(req, res);
58712
58911
  });
58912
+ app.post("/auth/service-account", express8.urlencoded({ extended: false }), (req, res) => {
58913
+ provider.handleServiceAccountConsent(req, res);
58914
+ });
58713
58915
  app.post("/auth/sso", express8.urlencoded({ extended: false }), (req, res) => {
58714
58916
  provider.handleSSOLogin(req, res);
58715
58917
  });
@@ -58735,19 +58937,24 @@ async function startHttpServer(options) {
58735
58937
  });
58736
58938
  return;
58737
58939
  }
58738
- const identity2 = `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...`;
58739
- if (isJwtExpired(auth.jwt)) {
58940
+ let principal = auth.principal;
58941
+ const identity2 = `mode=${principal.type}:team=${principal.teamId.slice(0, 8)}...`;
58942
+ if (principal.type === "user" && isJwtExpired(principal.supabaseJwt)) {
58740
58943
  try {
58741
- const refreshed = await refreshSupabaseJwtRaw(auth.refreshToken);
58944
+ const refreshed = await refreshSupabaseJwtRaw(principal.supabaseRefreshToken);
58742
58945
  if (refreshed) {
58743
- auth.jwt = refreshed.jwt;
58744
- auth.refreshToken = refreshed.refreshToken;
58946
+ principal = {
58947
+ ...principal,
58948
+ supabaseJwt: refreshed.jwt,
58949
+ supabaseRefreshToken: refreshed.refreshToken
58950
+ };
58745
58951
  const entry = await store.get("access_tokens", auth.mcpToken);
58746
58952
  if (entry) {
58747
58953
  const remainingMs = entry.expiresAt - Date.now();
58748
58954
  if (remainingMs > 0) {
58749
58955
  await store.set("access_tokens", auth.mcpToken, {
58750
58956
  ...entry,
58957
+ principal,
58751
58958
  supabaseJwt: refreshed.jwt,
58752
58959
  supabaseRefreshToken: refreshed.refreshToken
58753
58960
  }, Math.ceil(remainingMs / 1000));
@@ -58774,31 +58981,32 @@ async function startHttpServer(options) {
58774
58981
  const transport = new StreamableHTTPServerTransport({
58775
58982
  sessionIdGenerator: undefined
58776
58983
  });
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)`);
58984
+ const persist = principal.type === "user" ? async (state) => {
58985
+ const entry = await store.get("access_tokens", auth.mcpToken);
58986
+ if (!entry) {
58987
+ console.error("[PERSIST] WARN: access token gone from store");
58988
+ return;
58800
58989
  }
58801
- });
58990
+ const remainingMs = entry.expiresAt - Date.now();
58991
+ if (remainingMs <= 0) {
58992
+ console.error("[PERSIST] WARN: access token already expired, skipping persist");
58993
+ return;
58994
+ }
58995
+ const updatedPrincipal = {
58996
+ type: "user",
58997
+ supabaseJwt: state.supabaseJwt ?? principal.supabaseJwt,
58998
+ supabaseRefreshToken: state.supabaseRefreshToken ?? principal.supabaseRefreshToken,
58999
+ teamId: state.teamId ?? principal.teamId
59000
+ };
59001
+ await store.set("access_tokens", auth.mcpToken, {
59002
+ ...entry,
59003
+ principal: updatedPrincipal,
59004
+ supabaseJwt: updatedPrincipal.supabaseJwt,
59005
+ supabaseRefreshToken: updatedPrincipal.supabaseRefreshToken,
59006
+ teamId: updatedPrincipal.teamId
59007
+ }, Math.ceil(remainingMs / 1000));
59008
+ } : undefined;
59009
+ const server = await createServer({ principal, persist });
58802
59010
  await server.connect(transport);
58803
59011
  await transport.handleRequest(req, res, req.body);
58804
59012
  } catch (error48) {
@@ -58852,11 +59060,18 @@ var init_http2 = __esm(async () => {
58852
59060
 
58853
59061
  // src/index.ts
58854
59062
  async function createServer(auth, options) {
58855
- const ctx = {
58856
- client: createKadoaClient({ jwt: auth.jwt, teamId: auth.teamId }),
59063
+ const principal = "principal" in auth ? auth.principal : {
59064
+ type: "user",
58857
59065
  supabaseJwt: auth.jwt,
58858
59066
  supabaseRefreshToken: auth.refreshToken,
58859
- teamId: auth.teamId,
59067
+ teamId: auth.teamId
59068
+ };
59069
+ const ctx = {
59070
+ client: createKadoaClient({ principal }),
59071
+ principal,
59072
+ supabaseJwt: principal.type === "user" ? principal.supabaseJwt : undefined,
59073
+ supabaseRefreshToken: principal.type === "user" ? principal.supabaseRefreshToken : undefined,
59074
+ teamId: principal.teamId,
58860
59075
  persist: auth.persist
58861
59076
  };
58862
59077
  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.11",
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",