@kadoa/mcp 0.5.12 → 0.5.13

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 +13 -0
  2. package/dist/index.js +326 -146
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -24,6 +24,19 @@ claude mcp add kadoa --transport http https://mcp.kadoa.com/mcp
24
24
  2. Enter the URL: `https://mcp.kadoa.com/mcp`
25
25
  3. Sign in with your Kadoa account via OAuth
26
26
 
27
+ ### Claude organization shared workspace
28
+
29
+ A Kadoa workspace admin can generate organization connector credentials under **Workspace settings → Claude connector**. The generated OAuth client is fixed to that workspace's service account.
30
+
31
+ 1. In Kadoa, select **Generate credentials** and copy the MCP URL, OAuth Client ID, and OAuth Client Secret. The secret is shown once.
32
+ 2. In Claude organization **Admin settings → Connectors**, select **Add custom connector**.
33
+ 3. Enter `https://mcp.kadoa.com/mcp` and paste the generated OAuth Client ID/Secret under Advanced settings.
34
+ 4. Organization members find the custom connector under **Settings → Connectors** and select **Connect**.
35
+
36
+ Members do not need Kadoa accounts. All actions are attributed to the shared Kadoa service account, and team switching is unavailable. To rotate access, rotate the secret in Kadoa, replace it in Claude, and reconnect. Revoking the connector stops access immediately.
37
+
38
+ Do not paste a Kadoa workspace API key into Claude.
39
+
27
40
  ### Cursor
28
41
 
29
42
  Add to `.cursor/mcp.json`:
package/dist/index.js CHANGED
@@ -51861,20 +51861,11 @@ 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
- }
51875
51864
  function createKadoaClient(auth) {
51876
51865
  const { principal } = auth;
51877
- const client = principal.type === "user" ? new KadoaClient({ bearerToken: principal.supabaseJwt }) : new KadoaClient({ apiKey: auth.apiKey ?? resolveServiceAccountApiKey(principal.credentialRef) });
51866
+ const client = new KadoaClient({
51867
+ bearerToken: principal.type === "user" ? principal.supabaseJwt : principal.kadoaAccessToken
51868
+ });
51878
51869
  client.axiosInstance.interceptors.request.use((config2) => {
51879
51870
  config2.headers["x-kadoa-source"] = "mcp";
51880
51871
  config2.headers["x-team-id"] = principal.teamId;
@@ -51889,10 +51880,24 @@ var init_client = __esm(() => {
51889
51880
  ctxRefreshMutex = new WeakMap;
51890
51881
  });
51891
51882
 
51883
+ // src/error-summary.ts
51884
+ function redactCredentialText(value) {
51885
+ return value.replace(/Bearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\b(?:kcs_|tk-)[A-Za-z0-9._~-]+\b/g, "[redacted]").replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted]");
51886
+ }
51887
+ function safeErrorSummary(error48) {
51888
+ const record3 = error48 && typeof error48 === "object" ? error48 : {};
51889
+ const response = record3.response && typeof record3.response === "object" ? record3.response : {};
51890
+ return {
51891
+ name: error48 instanceof Error ? error48.name : "Error",
51892
+ message: redactCredentialText(error48 instanceof Error ? error48.message : String(error48)),
51893
+ ...typeof record3.code === "string" ? { code: record3.code } : {},
51894
+ ...typeof response.status === "number" ? { status: response.status } : {}
51895
+ };
51896
+ }
51897
+
51892
51898
  // src/client.ts
51893
51899
  var exports_client = {};
51894
51900
  __export(exports_client, {
51895
- resolveServiceAccountApiKey: () => resolveServiceAccountApiKey2,
51896
51901
  refreshSupabaseJwtRaw: () => refreshSupabaseJwtRaw,
51897
51902
  refreshSupabaseJwt: () => refreshSupabaseJwt,
51898
51903
  isJwtExpired: () => isJwtExpired,
@@ -51903,20 +51908,11 @@ __export(exports_client, {
51903
51908
  KadoaSdkException: () => KadoaSdkException,
51904
51909
  KadoaClient: () => KadoaClient
51905
51910
  });
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
- }
51917
51911
  function createKadoaClient2(auth) {
51918
51912
  const { principal } = auth;
51919
- const client = principal.type === "user" ? new KadoaClient({ bearerToken: principal.supabaseJwt }) : new KadoaClient({ apiKey: auth.apiKey ?? resolveServiceAccountApiKey2(principal.credentialRef) });
51913
+ const client = new KadoaClient({
51914
+ bearerToken: principal.type === "user" ? principal.supabaseJwt : principal.kadoaAccessToken
51915
+ });
51920
51916
  client.axiosInstance.interceptors.request.use((config2) => {
51921
51917
  config2.headers["x-kadoa-source"] = "mcp";
51922
51918
  config2.headers["x-team-id"] = principal.teamId;
@@ -52043,6 +52039,21 @@ var init_client2 = __esm(() => {
52043
52039
  ctxRefreshMutex2 = new WeakMap;
52044
52040
  });
52045
52041
 
52042
+ // src/error-summary.ts
52043
+ function redactCredentialText2(value) {
52044
+ return value.replace(/Bearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\b(?:kcs_|tk-)[A-Za-z0-9._~-]+\b/g, "[redacted]").replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted]");
52045
+ }
52046
+ function safeErrorSummary2(error48) {
52047
+ const record3 = error48 && typeof error48 === "object" ? error48 : {};
52048
+ const response = record3.response && typeof record3.response === "object" ? record3.response : {};
52049
+ return {
52050
+ name: error48 instanceof Error ? error48.name : "Error",
52051
+ message: redactCredentialText2(error48 instanceof Error ? error48.message : String(error48)),
52052
+ ...typeof record3.code === "string" ? { code: record3.code } : {},
52053
+ ...typeof response.status === "number" ? { status: response.status } : {}
52054
+ };
52055
+ }
52056
+
52046
52057
  // src/tools.ts
52047
52058
  function strictSchema(shape) {
52048
52059
  return exports_external.object(shape).strict();
@@ -52290,7 +52301,7 @@ function registerTools(server, ctx, capabilities) {
52290
52301
  }
52291
52302
  } catch {}
52292
52303
  }
52293
- console.error(`[Tool Error] ${name}:`, error48);
52304
+ console.error(`[Tool Error] ${name}:`, safeErrorSummary2(error48));
52294
52305
  return errorResult(message);
52295
52306
  }
52296
52307
  };
@@ -53651,7 +53662,7 @@ var package_default;
53651
53662
  var init_package = __esm(() => {
53652
53663
  package_default = {
53653
53664
  name: "@kadoa/mcp",
53654
- version: "0.5.12",
53665
+ version: "0.5.13",
53655
53666
  description: "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
53656
53667
  type: "module",
53657
53668
  main: "dist/index.js",
@@ -53678,6 +53689,7 @@ var init_package = __esm(() => {
53678
53689
  "@kadoa/node-sdk": "^0.35.0",
53679
53690
  "@modelcontextprotocol/sdk": "^1.26.0",
53680
53691
  express: "^5.2.1",
53692
+ "express-rate-limit": "^8.2.1",
53681
53693
  ioredis: "^5.6.1",
53682
53694
  zod: "^4.3.6"
53683
53695
  },
@@ -57683,55 +57695,69 @@ var init_bearerAuth = __esm(() => {
57683
57695
  init_errors4();
57684
57696
  });
57685
57697
 
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(", ")}`);
57698
+ // src/managed-connector-registry.ts
57699
+ class ManagedConnectorRegistry {
57700
+ baseUrl;
57701
+ serviceToken;
57702
+ timeoutMs;
57703
+ constructor(options) {
57704
+ this.baseUrl = (options?.baseUrl ?? process.env.KADOA_PUBLIC_API_URI ?? "https://api.kadoa.com").replace(/\/$/, "");
57705
+ this.serviceToken = options?.serviceToken ?? process.env.MCP_REGISTRY_SERVICE_TOKEN ?? "";
57706
+ this.timeoutMs = options?.timeoutMs ?? Number.parseInt(process.env.MCP_REGISTRY_TIMEOUT_MS ?? "5000", 10);
57707
+ if (this.serviceToken.length < 32)
57708
+ throw new Error("MCP_REGISTRY_SERVICE_TOKEN must be at least 32 characters");
57694
57709
  }
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");
57710
+ async get(clientId) {
57711
+ let response;
57712
+ try {
57713
+ response = await fetch(`${this.baseUrl}/v4/internal/mcp/connectors/${encodeURIComponent(clientId)}`, {
57714
+ headers: { "x-mcp-registry-token": this.serviceToken },
57715
+ signal: AbortSignal.timeout(this.timeoutMs)
57716
+ });
57717
+ } catch {
57718
+ throw new ManagedConnectorRegistryUnavailableError;
57719
+ }
57720
+ if (response.status === 404)
57721
+ return null;
57722
+ if (!response.ok)
57723
+ throw new ManagedConnectorRegistryUnavailableError;
57724
+ return response.json();
57725
+ }
57726
+ async authenticate(clientId, clientSecret) {
57727
+ let response;
57728
+ try {
57729
+ response = await fetch(`${this.baseUrl}/v4/internal/mcp/connectors/${encodeURIComponent(clientId)}/authenticate`, {
57730
+ method: "POST",
57731
+ headers: {
57732
+ "content-type": "application/json",
57733
+ "x-mcp-registry-token": this.serviceToken
57734
+ },
57735
+ body: JSON.stringify({ clientSecret }),
57736
+ signal: AbortSignal.timeout(this.timeoutMs)
57737
+ });
57738
+ } catch {
57739
+ throw new ManagedConnectorRegistryUnavailableError;
57740
+ }
57741
+ if (response.status === 401 || response.status === 404)
57742
+ throw new InvalidManagedConnectorClientError;
57743
+ if (!response.ok)
57744
+ throw new ManagedConnectorRegistryUnavailableError;
57745
+ return response.json();
57698
57746
  }
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
57747
  }
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"]
57748
+ var ManagedConnectorRegistryUnavailableError, InvalidManagedConnectorClientError;
57749
+ var init_managed_connector_registry = __esm(() => {
57750
+ ManagedConnectorRegistryUnavailableError = class ManagedConnectorRegistryUnavailableError extends Error {
57751
+ constructor() {
57752
+ super("Managed connector registry is unavailable");
57753
+ this.name = "ManagedConnectorRegistryUnavailableError";
57754
+ }
57724
57755
  };
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"
57756
+ InvalidManagedConnectorClientError = class InvalidManagedConnectorClientError extends Error {
57757
+ constructor() {
57758
+ super("Managed connector client authentication failed");
57759
+ this.name = "InvalidManagedConnectorClientError";
57760
+ }
57735
57761
  };
57736
57762
  });
57737
57763
 
@@ -57854,14 +57880,36 @@ async function setActiveTeamAndRefresh(jwt2, refreshToken, teamId) {
57854
57880
 
57855
57881
  class KadoaOAuthProvider {
57856
57882
  store;
57857
- constructor(store) {
57883
+ managedRegistry;
57884
+ constructor(store, managedRegistry) {
57858
57885
  this.store = store;
57886
+ this.managedRegistry = managedRegistry;
57887
+ }
57888
+ getManagedRegistry() {
57889
+ this.managedRegistry ??= new ManagedConnectorRegistry;
57890
+ return this.managedRegistry;
57859
57891
  }
57860
57892
  get clientsStore() {
57861
57893
  const store = this.store;
57894
+ const thisProvider = this;
57862
57895
  return {
57863
57896
  async getClient(clientId) {
57864
- return getConfiguredServiceAccountClient(clientId) ?? store.get("clients", clientId);
57897
+ if (clientId.startsWith("kadoa_claude_")) {
57898
+ const connector = await thisProvider.getManagedRegistry().get(clientId);
57899
+ if (!connector)
57900
+ return;
57901
+ return {
57902
+ client_id: connector.clientId,
57903
+ client_id_issued_at: 0,
57904
+ client_name: "Kadoa shared workspace",
57905
+ redirect_uris: connector.redirectUris,
57906
+ token_endpoint_auth_method: connector.tokenEndpointAuthMethod,
57907
+ grant_types: ["authorization_code", "refresh_token"],
57908
+ response_types: ["code"],
57909
+ managedConnector: connector
57910
+ };
57911
+ }
57912
+ return store.get("clients", clientId);
57865
57913
  },
57866
57914
  async registerClient(client) {
57867
57915
  const clientId = randomToken(16);
@@ -57880,21 +57928,18 @@ class KadoaOAuthProvider {
57880
57928
  if (!serverUrl) {
57881
57929
  throw new Error("MCP_SERVER_URL must be configured");
57882
57930
  }
57931
+ const managedConnector = client.managedConnector;
57932
+ if (managedConnector) {
57933
+ await this.completeManagedAuthFlow({ clientId: client.client_id, params, supabaseCodeVerifier: "" }, res, managedConnector);
57934
+ return;
57935
+ }
57883
57936
  const state = randomToken();
57884
- const { verifier, challenge } = generatePKCE();
57937
+ const { verifier } = generatePKCE();
57885
57938
  await this.store.set("pending_auths", state, {
57886
- client,
57939
+ clientId: client.client_id,
57887
57940
  params,
57888
57941
  supabaseCodeVerifier: verifier
57889
57942
  }, 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
- }
57898
57943
  res.type("html").send(renderLoginPage(state));
57899
57944
  }
57900
57945
  async handleGoogleLogin(req, res) {
@@ -58049,6 +58094,9 @@ class KadoaOAuthProvider {
58049
58094
  if (redirectUri && redirectUri !== entry.redirectUri) {
58050
58095
  throw new Error("redirect_uri mismatch");
58051
58096
  }
58097
+ if (entry.managedConnector) {
58098
+ throw new InvalidGrantError("Managed authorization code requires confidential-client exchange");
58099
+ }
58052
58100
  const accessToken = randomToken();
58053
58101
  const refreshToken = randomToken();
58054
58102
  const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
@@ -58072,6 +58120,75 @@ class KadoaOAuthProvider {
58072
58120
  refresh_token: refreshToken
58073
58121
  };
58074
58122
  }
58123
+ async exchangeManagedAuthorizationCode(connector, authorizationCode, redirectUri) {
58124
+ const entry = await this.store.get("auth_codes", authorizationCode);
58125
+ if (!entry || !entry.managedConnector)
58126
+ throw new InvalidGrantError("Unknown authorization code");
58127
+ if (entry.expiresAt < Date.now()) {
58128
+ await this.store.del("auth_codes", authorizationCode);
58129
+ throw new InvalidGrantError("Authorization code expired");
58130
+ }
58131
+ if (entry.clientId !== connector.clientId)
58132
+ throw new InvalidGrantError("OAuth client mismatch");
58133
+ if (redirectUri && redirectUri !== entry.redirectUri)
58134
+ throw new InvalidGrantError("redirect_uri mismatch");
58135
+ const binding = entry.managedConnector;
58136
+ if (binding.connectorId !== connector.connectorId || binding.teamId !== connector.teamId || binding.serviceUserId !== connector.serviceUserId || binding.secretVersion !== connector.secretVersion) {
58137
+ throw new InvalidGrantError("Managed connector binding changed");
58138
+ }
58139
+ const tokens = await this.issueManagedTokens(connector);
58140
+ await this.store.del("auth_codes", authorizationCode);
58141
+ return tokens;
58142
+ }
58143
+ async exchangeManagedRefreshToken(connector, refreshToken) {
58144
+ const entry = await this.store.get("refresh_tokens", refreshToken);
58145
+ if (!entry || !entry.managedConnector)
58146
+ throw new InvalidGrantError("Unknown or expired refresh token");
58147
+ if (entry.clientId !== connector.clientId)
58148
+ throw new InvalidGrantError("OAuth client mismatch");
58149
+ const binding = entry.managedConnector;
58150
+ if (binding.connectorId !== connector.connectorId || binding.teamId !== connector.teamId || binding.serviceUserId !== connector.serviceUserId) {
58151
+ throw new InvalidGrantError("Managed connector binding changed");
58152
+ }
58153
+ await this.store.del("refresh_tokens", refreshToken);
58154
+ return this.issueManagedTokens(connector);
58155
+ }
58156
+ async issueManagedTokens(connector) {
58157
+ const accessToken = randomToken();
58158
+ const refreshToken = randomToken();
58159
+ const accessTokenTtl = Math.min(ACCESS_TOKEN_TTL, connector.expiresIn);
58160
+ const expiresAt = Date.now() + accessTokenTtl * 1000;
58161
+ const principal = {
58162
+ type: "serviceAccount",
58163
+ connectorId: connector.connectorId,
58164
+ teamId: connector.teamId,
58165
+ serviceUserId: connector.serviceUserId,
58166
+ secretVersion: connector.secretVersion,
58167
+ kadoaAccessToken: connector.kadoaAccessToken,
58168
+ kadoaAccessTokenExpiresAt: Date.now() + connector.expiresIn * 1000
58169
+ };
58170
+ await this.store.set("access_tokens", accessToken, {
58171
+ ...persistPrincipal(principal),
58172
+ clientId: connector.clientId,
58173
+ expiresAt
58174
+ }, accessTokenTtl);
58175
+ await this.store.set("refresh_tokens", refreshToken, {
58176
+ clientId: connector.clientId,
58177
+ managedConnector: {
58178
+ connectorId: connector.connectorId,
58179
+ clientId: connector.clientId,
58180
+ teamId: connector.teamId,
58181
+ serviceUserId: connector.serviceUserId,
58182
+ secretVersion: connector.secretVersion
58183
+ }
58184
+ }, 2592000);
58185
+ return {
58186
+ access_token: accessToken,
58187
+ token_type: "bearer",
58188
+ expires_in: accessTokenTtl,
58189
+ refresh_token: refreshToken
58190
+ };
58191
+ }
58075
58192
  async exchangeRefreshToken(_client, refreshToken) {
58076
58193
  const entry = await this.store.get("refresh_tokens", refreshToken);
58077
58194
  if (!entry) {
@@ -58082,8 +58199,12 @@ class KadoaOAuthProvider {
58082
58199
  if (entry.clientId !== _client.client_id) {
58083
58200
  throw new Error("OAuth client mismatch for refresh token");
58084
58201
  }
58202
+ if (entry.managedConnector) {
58203
+ throw new InvalidGrantError("Managed refresh token requires confidential-client exchange");
58204
+ }
58205
+ const persistedEntry = entry;
58085
58206
  await this.store.del("refresh_tokens", refreshToken);
58086
- const storedPrincipal = readPrincipal(entry);
58207
+ const storedPrincipal = readPrincipal(persistedEntry);
58087
58208
  if (storedPrincipal.type === "serviceAccount") {
58088
58209
  const newAccessToken2 = randomToken();
58089
58210
  const newRefreshToken2 = randomToken();
@@ -58106,7 +58227,7 @@ class KadoaOAuthProvider {
58106
58227
  }
58107
58228
  let { supabaseJwt, supabaseRefreshToken } = storedPrincipal;
58108
58229
  const claims = jwtClaims(storedPrincipal.supabaseJwt);
58109
- const context = `email=${claims.email}, team=${entry.teamId}`;
58230
+ const context = `email=${claims.email}, team=${persistedEntry.teamId}`;
58110
58231
  try {
58111
58232
  const { refreshSupabaseJwtRaw: refreshSupabaseJwtRaw2, SessionExpiredError: SessionExpiredError2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
58112
58233
  const refreshed = await refreshSupabaseJwtRaw2(supabaseRefreshToken);
@@ -58125,7 +58246,7 @@ class KadoaOAuthProvider {
58125
58246
  console.error(`[AUTH] REFRESH_WARN: unexpected error, using stale JWT (${context}):`, error48);
58126
58247
  }
58127
58248
  const freshClaims = jwtClaims(supabaseJwt);
58128
- const teamId = freshClaims.activeTeamId ?? entry.teamId;
58249
+ const teamId = freshClaims.activeTeamId ?? persistedEntry.teamId;
58129
58250
  const newAccessToken = randomToken();
58130
58251
  const newRefreshToken = randomToken();
58131
58252
  const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
@@ -58165,6 +58286,13 @@ class KadoaOAuthProvider {
58165
58286
  throw new InvalidTokenError("Access token expired");
58166
58287
  }
58167
58288
  const principal = readPrincipal(entry);
58289
+ if (principal.type === "serviceAccount") {
58290
+ const connector = await this.getManagedRegistry().get(entry.clientId);
58291
+ if (!connector || connector.connectorId !== principal.connectorId || connector.teamId !== principal.teamId || connector.serviceUserId !== principal.serviceUserId || connector.secretVersion !== principal.secretVersion) {
58292
+ await this.store.del("access_tokens", token);
58293
+ throw new InvalidTokenError("Managed connector is inactive or changed");
58294
+ }
58295
+ }
58168
58296
  return {
58169
58297
  token,
58170
58298
  clientId: entry.clientId,
@@ -58206,29 +58334,6 @@ class KadoaOAuthProvider {
58206
58334
  res.redirect(redirectUrl.toString());
58207
58335
  }
58208
58336
  }
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
- }
58232
58337
  async handleTeamSelection(req, res) {
58233
58338
  const { token, teamId } = req.body;
58234
58339
  if (!token || !teamId) {
@@ -58269,12 +58374,33 @@ class KadoaOAuthProvider {
58269
58374
  res.redirect(redirectUrl.toString());
58270
58375
  }
58271
58376
  }
58377
+ async completeManagedAuthFlow(pending, res, connector) {
58378
+ const mcpCode = randomToken();
58379
+ await this.store.set("auth_codes", mcpCode, {
58380
+ managedConnector: {
58381
+ connectorId: connector.connectorId,
58382
+ clientId: connector.clientId,
58383
+ teamId: connector.teamId,
58384
+ serviceUserId: connector.serviceUserId,
58385
+ secretVersion: connector.secretVersion
58386
+ },
58387
+ codeChallenge: pending.params.codeChallenge,
58388
+ clientId: pending.clientId,
58389
+ redirectUri: pending.params.redirectUri,
58390
+ expiresAt: Date.now() + 600000
58391
+ }, 600);
58392
+ const redirectUrl = new URL(pending.params.redirectUri);
58393
+ redirectUrl.searchParams.set("code", mcpCode);
58394
+ if (pending.params.state)
58395
+ redirectUrl.searchParams.set("state", pending.params.state);
58396
+ res.redirect(redirectUrl.toString());
58397
+ }
58272
58398
  async completeAuthFlow(pending, res, principal) {
58273
58399
  const mcpCode = randomToken();
58274
58400
  await this.store.set("auth_codes", mcpCode, {
58275
58401
  ...persistPrincipal(principal),
58276
58402
  codeChallenge: pending.params.codeChallenge,
58277
- clientId: pending.client.client_id,
58403
+ clientId: pending.clientId,
58278
58404
  redirectUri: pending.params.redirectUri,
58279
58405
  expiresAt: Date.now() + 600000
58280
58406
  }, 600);
@@ -58286,33 +58412,6 @@ class KadoaOAuthProvider {
58286
58412
  res.redirect(redirectUrl.toString());
58287
58413
  }
58288
58414
  }
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
- }
58316
58415
  function renderTeamSelectionPage(teams, selectionToken) {
58317
58416
  const teamButtons = teams.map((t) => `
58318
58417
  <button type="submit" name="teamId" value="${t.id}" class="team-btn">
@@ -58735,7 +58834,7 @@ function escapeHtml(str) {
58735
58834
  var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
58736
58835
  var init_auth2 = __esm(() => {
58737
58836
  init_errors4();
58738
- init_service_account_config();
58837
+ init_managed_connector_registry();
58739
58838
  TEAM_SELECTION_TTL = 10 * 60 * 1000;
58740
58839
  ACCESS_TOKEN_TTL = 7 * 24 * 3600;
58741
58840
  });
@@ -58836,6 +58935,75 @@ class RedisTokenStore {
58836
58935
  var KEY_PREFIX = "kadoa-mcp";
58837
58936
  var init_redis_store = () => {};
58838
58937
 
58938
+ // src/managed-token-handler.ts
58939
+ import { createHash as createHash3 } from "node:crypto";
58940
+ function requireString(body, key, maxLength) {
58941
+ const value = body[key];
58942
+ if (typeof value !== "string" || !value)
58943
+ throw new InvalidRequestError(`${key} is required`);
58944
+ if (value.length > maxLength)
58945
+ throw new InvalidRequestError(`${key} is invalid`);
58946
+ return value;
58947
+ }
58948
+ function verifyPkce(verifier, challenge) {
58949
+ return createHash3("sha256").update(verifier).digest("base64url") === challenge;
58950
+ }
58951
+ function managedTokenHandler(options) {
58952
+ const { provider } = options;
58953
+ let registry2 = options.registry;
58954
+ return async (req, res, next) => {
58955
+ const body = req.body ?? {};
58956
+ const clientId = typeof body.client_id === "string" ? body.client_id : "";
58957
+ if (!clientId.startsWith("kadoa_claude_")) {
58958
+ next();
58959
+ return;
58960
+ }
58961
+ res.setHeader("Cache-Control", "no-store");
58962
+ try {
58963
+ if (clientId.length > 128)
58964
+ throw new InvalidClientError("Invalid client credentials");
58965
+ const clientSecret = requireString(body, "client_secret", 256);
58966
+ registry2 ??= new ManagedConnectorRegistry;
58967
+ const connector = await registry2.authenticate(clientId, clientSecret);
58968
+ const grantType = requireString(body, "grant_type", 64);
58969
+ let tokens;
58970
+ if (grantType === "authorization_code") {
58971
+ const code = requireString(body, "code", 512);
58972
+ const verifier = requireString(body, "code_verifier", 128);
58973
+ const redirectUri = typeof body.redirect_uri === "string" ? body.redirect_uri : undefined;
58974
+ const challenge = await provider.challengeForAuthorizationCode({ client_id: clientId }, code);
58975
+ if (!verifyPkce(verifier, challenge))
58976
+ throw new InvalidGrantError("code_verifier does not match the challenge");
58977
+ tokens = await provider.exchangeManagedAuthorizationCode(connector, code, redirectUri);
58978
+ } else if (grantType === "refresh_token") {
58979
+ const refreshToken = requireString(body, "refresh_token", 512);
58980
+ tokens = await provider.exchangeManagedRefreshToken(connector, refreshToken);
58981
+ } else {
58982
+ throw new UnsupportedGrantTypeError("The grant type is not supported by this authorization server.");
58983
+ }
58984
+ res.status(200).json(tokens);
58985
+ } catch (error48) {
58986
+ let oauthError;
58987
+ let status = 400;
58988
+ if (error48 instanceof InvalidManagedConnectorClientError) {
58989
+ oauthError = new InvalidClientError("Invalid client credentials");
58990
+ } else if (error48 instanceof ManagedConnectorRegistryUnavailableError) {
58991
+ oauthError = new TemporarilyUnavailableError("Managed connector authentication is temporarily unavailable");
58992
+ status = 503;
58993
+ } else if (error48 instanceof OAuthError) {
58994
+ oauthError = error48;
58995
+ } else {
58996
+ oauthError = new InvalidClientError("Invalid client credentials");
58997
+ }
58998
+ res.status(status).json(oauthError.toResponseObject());
58999
+ }
59000
+ };
59001
+ }
59002
+ var init_managed_token_handler = __esm(() => {
59003
+ init_errors4();
59004
+ init_managed_connector_registry();
59005
+ });
59006
+
58839
59007
  // src/http.ts
58840
59008
  var exports_http = {};
58841
59009
  __export(exports_http, {
@@ -58857,7 +59025,7 @@ function resolveAuth(req) {
58857
59025
  }
58858
59026
  const principal = extra.principal;
58859
59027
  if (principal.type === "serviceAccount") {
58860
- if (!principal.credentialRef || !principal.teamId) {
59028
+ if (!principal.connectorId || !principal.teamId || !principal.serviceUserId || !principal.kadoaAccessToken) {
58861
59029
  console.error("[AUTH_RESOLVE] FAIL: service-account principal is incomplete");
58862
59030
  return;
58863
59031
  }
@@ -58888,8 +59056,20 @@ async function startHttpServer(options) {
58888
59056
  const app = createMcpExpressApp({ host: "0.0.0.0" });
58889
59057
  app.set("trust proxy", 1);
58890
59058
  const store = options?.store ?? new RedisTokenStore;
58891
- const provider = new KadoaOAuthProvider(store);
59059
+ const managedRegistry = options?.managedRegistry ?? (process.env.MCP_REGISTRY_SERVICE_TOKEN ? new ManagedConnectorRegistry : undefined);
59060
+ const provider = new KadoaOAuthProvider(store, managedRegistry);
58892
59061
  const serverUrl = process.env.MCP_SERVER_URL || `http://localhost:${port}`;
59062
+ app.post("/token", express8.urlencoded({ extended: false }), rate_limit_default({
59063
+ windowMs: 15 * 60 * 1000,
59064
+ limit: 50,
59065
+ standardHeaders: true,
59066
+ legacyHeaders: false,
59067
+ skip: (req) => !String(req.body?.client_id ?? "").startsWith("kadoa_claude_"),
59068
+ message: {
59069
+ error: "too_many_requests",
59070
+ error_description: "Too many managed connector token requests"
59071
+ }
59072
+ }), managedTokenHandler({ provider, registry: managedRegistry }));
58893
59073
  app.use(mcpAuthRouter({
58894
59074
  provider,
58895
59075
  issuerUrl: new URL(serverUrl),
@@ -58905,9 +59085,6 @@ async function startHttpServer(options) {
58905
59085
  app.post("/auth/login", express8.urlencoded({ extended: false }), (req, res) => {
58906
59086
  provider.handleEmailPasswordLogin(req, res);
58907
59087
  });
58908
- app.post("/auth/service-account", express8.urlencoded({ extended: false }), (req, res) => {
58909
- provider.handleServiceAccountConsent(req, res);
58910
- });
58911
59088
  app.post("/auth/sso", express8.urlencoded({ extended: false }), (req, res) => {
58912
59089
  provider.handleSSOLogin(req, res);
58913
59090
  });
@@ -59006,7 +59183,7 @@ async function startHttpServer(options) {
59006
59183
  await server.connect(transport);
59007
59184
  await transport.handleRequest(req, res, req.body);
59008
59185
  } catch (error48) {
59009
- console.error(`[MCP] 500 method=${method} auth=${identity2} error=`, error48);
59186
+ console.error(`[MCP] 500 method=${method} auth=${identity2} error=`, safeErrorSummary2(error48));
59010
59187
  if (!res.headersSent) {
59011
59188
  res.status(500).json({
59012
59189
  jsonrpc: "2.0",
@@ -59048,9 +59225,12 @@ var init_http2 = __esm(async () => {
59048
59225
  init_streamableHttp();
59049
59226
  init_router();
59050
59227
  init_bearerAuth();
59228
+ init_dist4();
59051
59229
  init_auth2();
59052
59230
  init_redis_store();
59053
59231
  init_client2();
59232
+ init_managed_connector_registry();
59233
+ init_managed_token_handler();
59054
59234
  await init_src();
59055
59235
  });
59056
59236
 
@@ -59103,7 +59283,7 @@ async function createServer(auth, options) {
59103
59283
  }
59104
59284
  }
59105
59285
  registerTools(server, ctx, capabilities);
59106
- server.server.onerror = (error48) => console.error("[MCP Error]", error48);
59286
+ server.server.onerror = (error48) => console.error("[MCP Error]", safeErrorSummary(error48));
59107
59287
  return server;
59108
59288
  }
59109
59289
  var init_src = __esm(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
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",
@@ -27,6 +27,7 @@
27
27
  "@kadoa/node-sdk": "^0.35.0",
28
28
  "@modelcontextprotocol/sdk": "^1.26.0",
29
29
  "express": "^5.2.1",
30
+ "express-rate-limit": "^8.2.1",
30
31
  "ioredis": "^5.6.1",
31
32
  "zod": "^4.3.6"
32
33
  },