@kadoa/mcp 0.5.12 → 0.5.14
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.
- package/README.md +14 -0
- package/dist/index.js +338 -146
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -24,6 +24,20 @@ 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 service connector
|
|
28
|
+
|
|
29
|
+
A Kadoa workspace admin can generate service connector credentials under **Workspace settings → MCP 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://service.mcp.kadoa.com/mcp` and paste the generated OAuth Client ID/Secret under Advanced settings.
|
|
34
|
+
4. In the connector's tool settings, allow only the tools that organization members should use.
|
|
35
|
+
5. Organization members find the custom connector under **Settings → Connectors** and select **Connect**.
|
|
36
|
+
|
|
37
|
+
The service connector uses a separate hostname from the personal connector at `https://mcp.kadoa.com/mcp`, so Claude users can configure both. 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.
|
|
38
|
+
|
|
39
|
+
Do not paste a Kadoa workspace API key into Claude.
|
|
40
|
+
|
|
27
41
|
### Cursor
|
|
28
42
|
|
|
29
43
|
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 =
|
|
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 =
|
|
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.
|
|
53665
|
+
version: "0.5.14",
|
|
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,78 @@ var init_bearerAuth = __esm(() => {
|
|
|
57683
57695
|
init_errors4();
|
|
57684
57696
|
});
|
|
57685
57697
|
|
|
57686
|
-
// src/
|
|
57687
|
-
function
|
|
57688
|
-
|
|
57689
|
-
|
|
57690
|
-
|
|
57691
|
-
|
|
57692
|
-
|
|
57693
|
-
|
|
57698
|
+
// src/managed-connector-client.ts
|
|
57699
|
+
function isManagedConnectorClientId(clientId) {
|
|
57700
|
+
return MANAGED_CONNECTOR_CLIENT_ID_PREFIXES.some((prefix) => clientId.startsWith(prefix));
|
|
57701
|
+
}
|
|
57702
|
+
var MANAGED_CONNECTOR_CLIENT_ID_PREFIXES;
|
|
57703
|
+
var init_managed_connector_client = __esm(() => {
|
|
57704
|
+
MANAGED_CONNECTOR_CLIENT_ID_PREFIXES = ["kadoa_mcp_", "kadoa_claude_"];
|
|
57705
|
+
});
|
|
57706
|
+
|
|
57707
|
+
// src/managed-connector-registry.ts
|
|
57708
|
+
class ManagedConnectorRegistry {
|
|
57709
|
+
baseUrl;
|
|
57710
|
+
serviceToken;
|
|
57711
|
+
timeoutMs;
|
|
57712
|
+
constructor(options) {
|
|
57713
|
+
this.baseUrl = (options?.baseUrl ?? process.env.KADOA_PUBLIC_API_URI ?? "https://api.kadoa.com").replace(/\/$/, "");
|
|
57714
|
+
this.serviceToken = options?.serviceToken ?? process.env.MCP_REGISTRY_SERVICE_TOKEN ?? "";
|
|
57715
|
+
this.timeoutMs = options?.timeoutMs ?? Number.parseInt(process.env.MCP_REGISTRY_TIMEOUT_MS ?? "5000", 10);
|
|
57716
|
+
if (this.serviceToken.length < 32)
|
|
57717
|
+
throw new Error("MCP_REGISTRY_SERVICE_TOKEN must be at least 32 characters");
|
|
57694
57718
|
}
|
|
57695
|
-
|
|
57696
|
-
|
|
57697
|
-
|
|
57719
|
+
async get(clientId) {
|
|
57720
|
+
let response;
|
|
57721
|
+
try {
|
|
57722
|
+
response = await fetch(`${this.baseUrl}/v4/internal/mcp/connectors/${encodeURIComponent(clientId)}`, {
|
|
57723
|
+
headers: { "x-mcp-registry-token": this.serviceToken },
|
|
57724
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
57725
|
+
});
|
|
57726
|
+
} catch {
|
|
57727
|
+
throw new ManagedConnectorRegistryUnavailableError;
|
|
57728
|
+
}
|
|
57729
|
+
if (response.status === 404)
|
|
57730
|
+
return null;
|
|
57731
|
+
if (!response.ok)
|
|
57732
|
+
throw new ManagedConnectorRegistryUnavailableError;
|
|
57733
|
+
return response.json();
|
|
57734
|
+
}
|
|
57735
|
+
async authenticate(clientId, clientSecret) {
|
|
57736
|
+
let response;
|
|
57737
|
+
try {
|
|
57738
|
+
response = await fetch(`${this.baseUrl}/v4/internal/mcp/connectors/${encodeURIComponent(clientId)}/authenticate`, {
|
|
57739
|
+
method: "POST",
|
|
57740
|
+
headers: {
|
|
57741
|
+
"content-type": "application/json",
|
|
57742
|
+
"x-mcp-registry-token": this.serviceToken
|
|
57743
|
+
},
|
|
57744
|
+
body: JSON.stringify({ clientSecret }),
|
|
57745
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
57746
|
+
});
|
|
57747
|
+
} catch {
|
|
57748
|
+
throw new ManagedConnectorRegistryUnavailableError;
|
|
57749
|
+
}
|
|
57750
|
+
if (response.status === 401 || response.status === 404)
|
|
57751
|
+
throw new InvalidManagedConnectorClientError;
|
|
57752
|
+
if (!response.ok)
|
|
57753
|
+
throw new ManagedConnectorRegistryUnavailableError;
|
|
57754
|
+
return response.json();
|
|
57698
57755
|
}
|
|
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
57756
|
}
|
|
57708
|
-
|
|
57709
|
-
|
|
57710
|
-
|
|
57711
|
-
|
|
57712
|
-
|
|
57713
|
-
|
|
57714
|
-
|
|
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"]
|
|
57757
|
+
var ManagedConnectorRegistryUnavailableError, InvalidManagedConnectorClientError;
|
|
57758
|
+
var init_managed_connector_registry = __esm(() => {
|
|
57759
|
+
ManagedConnectorRegistryUnavailableError = class ManagedConnectorRegistryUnavailableError extends Error {
|
|
57760
|
+
constructor() {
|
|
57761
|
+
super("Managed connector registry is unavailable");
|
|
57762
|
+
this.name = "ManagedConnectorRegistryUnavailableError";
|
|
57763
|
+
}
|
|
57724
57764
|
};
|
|
57725
|
-
|
|
57726
|
-
|
|
57727
|
-
|
|
57728
|
-
|
|
57729
|
-
|
|
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"
|
|
57765
|
+
InvalidManagedConnectorClientError = class InvalidManagedConnectorClientError extends Error {
|
|
57766
|
+
constructor() {
|
|
57767
|
+
super("Managed connector client authentication failed");
|
|
57768
|
+
this.name = "InvalidManagedConnectorClientError";
|
|
57769
|
+
}
|
|
57735
57770
|
};
|
|
57736
57771
|
});
|
|
57737
57772
|
|
|
@@ -57854,14 +57889,36 @@ async function setActiveTeamAndRefresh(jwt2, refreshToken, teamId) {
|
|
|
57854
57889
|
|
|
57855
57890
|
class KadoaOAuthProvider {
|
|
57856
57891
|
store;
|
|
57857
|
-
|
|
57892
|
+
managedRegistry;
|
|
57893
|
+
constructor(store, managedRegistry) {
|
|
57858
57894
|
this.store = store;
|
|
57895
|
+
this.managedRegistry = managedRegistry;
|
|
57896
|
+
}
|
|
57897
|
+
getManagedRegistry() {
|
|
57898
|
+
this.managedRegistry ??= new ManagedConnectorRegistry;
|
|
57899
|
+
return this.managedRegistry;
|
|
57859
57900
|
}
|
|
57860
57901
|
get clientsStore() {
|
|
57861
57902
|
const store = this.store;
|
|
57903
|
+
const thisProvider = this;
|
|
57862
57904
|
return {
|
|
57863
57905
|
async getClient(clientId) {
|
|
57864
|
-
|
|
57906
|
+
if (isManagedConnectorClientId(clientId)) {
|
|
57907
|
+
const connector = await thisProvider.getManagedRegistry().get(clientId);
|
|
57908
|
+
if (!connector)
|
|
57909
|
+
return;
|
|
57910
|
+
return {
|
|
57911
|
+
client_id: connector.clientId,
|
|
57912
|
+
client_id_issued_at: 0,
|
|
57913
|
+
client_name: "Kadoa shared workspace",
|
|
57914
|
+
redirect_uris: connector.redirectUris,
|
|
57915
|
+
token_endpoint_auth_method: connector.tokenEndpointAuthMethod,
|
|
57916
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
57917
|
+
response_types: ["code"],
|
|
57918
|
+
managedConnector: connector
|
|
57919
|
+
};
|
|
57920
|
+
}
|
|
57921
|
+
return store.get("clients", clientId);
|
|
57865
57922
|
},
|
|
57866
57923
|
async registerClient(client) {
|
|
57867
57924
|
const clientId = randomToken(16);
|
|
@@ -57880,21 +57937,18 @@ class KadoaOAuthProvider {
|
|
|
57880
57937
|
if (!serverUrl) {
|
|
57881
57938
|
throw new Error("MCP_SERVER_URL must be configured");
|
|
57882
57939
|
}
|
|
57940
|
+
const managedConnector = client.managedConnector;
|
|
57941
|
+
if (managedConnector) {
|
|
57942
|
+
await this.completeManagedAuthFlow({ clientId: client.client_id, params, supabaseCodeVerifier: "" }, res, managedConnector);
|
|
57943
|
+
return;
|
|
57944
|
+
}
|
|
57883
57945
|
const state = randomToken();
|
|
57884
|
-
const { verifier
|
|
57946
|
+
const { verifier } = generatePKCE();
|
|
57885
57947
|
await this.store.set("pending_auths", state, {
|
|
57886
|
-
client,
|
|
57948
|
+
clientId: client.client_id,
|
|
57887
57949
|
params,
|
|
57888
57950
|
supabaseCodeVerifier: verifier
|
|
57889
57951
|
}, 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
57952
|
res.type("html").send(renderLoginPage(state));
|
|
57899
57953
|
}
|
|
57900
57954
|
async handleGoogleLogin(req, res) {
|
|
@@ -58049,6 +58103,9 @@ class KadoaOAuthProvider {
|
|
|
58049
58103
|
if (redirectUri && redirectUri !== entry.redirectUri) {
|
|
58050
58104
|
throw new Error("redirect_uri mismatch");
|
|
58051
58105
|
}
|
|
58106
|
+
if (entry.managedConnector) {
|
|
58107
|
+
throw new InvalidGrantError("Managed authorization code requires confidential-client exchange");
|
|
58108
|
+
}
|
|
58052
58109
|
const accessToken = randomToken();
|
|
58053
58110
|
const refreshToken = randomToken();
|
|
58054
58111
|
const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
|
|
@@ -58072,6 +58129,75 @@ class KadoaOAuthProvider {
|
|
|
58072
58129
|
refresh_token: refreshToken
|
|
58073
58130
|
};
|
|
58074
58131
|
}
|
|
58132
|
+
async exchangeManagedAuthorizationCode(connector, authorizationCode, redirectUri) {
|
|
58133
|
+
const entry = await this.store.get("auth_codes", authorizationCode);
|
|
58134
|
+
if (!entry || !entry.managedConnector)
|
|
58135
|
+
throw new InvalidGrantError("Unknown authorization code");
|
|
58136
|
+
if (entry.expiresAt < Date.now()) {
|
|
58137
|
+
await this.store.del("auth_codes", authorizationCode);
|
|
58138
|
+
throw new InvalidGrantError("Authorization code expired");
|
|
58139
|
+
}
|
|
58140
|
+
if (entry.clientId !== connector.clientId)
|
|
58141
|
+
throw new InvalidGrantError("OAuth client mismatch");
|
|
58142
|
+
if (redirectUri && redirectUri !== entry.redirectUri)
|
|
58143
|
+
throw new InvalidGrantError("redirect_uri mismatch");
|
|
58144
|
+
const binding = entry.managedConnector;
|
|
58145
|
+
if (binding.connectorId !== connector.connectorId || binding.teamId !== connector.teamId || binding.serviceUserId !== connector.serviceUserId || binding.secretVersion !== connector.secretVersion) {
|
|
58146
|
+
throw new InvalidGrantError("Managed connector binding changed");
|
|
58147
|
+
}
|
|
58148
|
+
const tokens = await this.issueManagedTokens(connector);
|
|
58149
|
+
await this.store.del("auth_codes", authorizationCode);
|
|
58150
|
+
return tokens;
|
|
58151
|
+
}
|
|
58152
|
+
async exchangeManagedRefreshToken(connector, refreshToken) {
|
|
58153
|
+
const entry = await this.store.get("refresh_tokens", refreshToken);
|
|
58154
|
+
if (!entry || !entry.managedConnector)
|
|
58155
|
+
throw new InvalidGrantError("Unknown or expired refresh token");
|
|
58156
|
+
if (entry.clientId !== connector.clientId)
|
|
58157
|
+
throw new InvalidGrantError("OAuth client mismatch");
|
|
58158
|
+
const binding = entry.managedConnector;
|
|
58159
|
+
if (binding.connectorId !== connector.connectorId || binding.teamId !== connector.teamId || binding.serviceUserId !== connector.serviceUserId) {
|
|
58160
|
+
throw new InvalidGrantError("Managed connector binding changed");
|
|
58161
|
+
}
|
|
58162
|
+
await this.store.del("refresh_tokens", refreshToken);
|
|
58163
|
+
return this.issueManagedTokens(connector);
|
|
58164
|
+
}
|
|
58165
|
+
async issueManagedTokens(connector) {
|
|
58166
|
+
const accessToken = randomToken();
|
|
58167
|
+
const refreshToken = randomToken();
|
|
58168
|
+
const accessTokenTtl = Math.min(ACCESS_TOKEN_TTL, connector.expiresIn);
|
|
58169
|
+
const expiresAt = Date.now() + accessTokenTtl * 1000;
|
|
58170
|
+
const principal = {
|
|
58171
|
+
type: "serviceAccount",
|
|
58172
|
+
connectorId: connector.connectorId,
|
|
58173
|
+
teamId: connector.teamId,
|
|
58174
|
+
serviceUserId: connector.serviceUserId,
|
|
58175
|
+
secretVersion: connector.secretVersion,
|
|
58176
|
+
kadoaAccessToken: connector.kadoaAccessToken,
|
|
58177
|
+
kadoaAccessTokenExpiresAt: Date.now() + connector.expiresIn * 1000
|
|
58178
|
+
};
|
|
58179
|
+
await this.store.set("access_tokens", accessToken, {
|
|
58180
|
+
...persistPrincipal(principal),
|
|
58181
|
+
clientId: connector.clientId,
|
|
58182
|
+
expiresAt
|
|
58183
|
+
}, accessTokenTtl);
|
|
58184
|
+
await this.store.set("refresh_tokens", refreshToken, {
|
|
58185
|
+
clientId: connector.clientId,
|
|
58186
|
+
managedConnector: {
|
|
58187
|
+
connectorId: connector.connectorId,
|
|
58188
|
+
clientId: connector.clientId,
|
|
58189
|
+
teamId: connector.teamId,
|
|
58190
|
+
serviceUserId: connector.serviceUserId,
|
|
58191
|
+
secretVersion: connector.secretVersion
|
|
58192
|
+
}
|
|
58193
|
+
}, 2592000);
|
|
58194
|
+
return {
|
|
58195
|
+
access_token: accessToken,
|
|
58196
|
+
token_type: "bearer",
|
|
58197
|
+
expires_in: accessTokenTtl,
|
|
58198
|
+
refresh_token: refreshToken
|
|
58199
|
+
};
|
|
58200
|
+
}
|
|
58075
58201
|
async exchangeRefreshToken(_client, refreshToken) {
|
|
58076
58202
|
const entry = await this.store.get("refresh_tokens", refreshToken);
|
|
58077
58203
|
if (!entry) {
|
|
@@ -58082,8 +58208,12 @@ class KadoaOAuthProvider {
|
|
|
58082
58208
|
if (entry.clientId !== _client.client_id) {
|
|
58083
58209
|
throw new Error("OAuth client mismatch for refresh token");
|
|
58084
58210
|
}
|
|
58211
|
+
if (entry.managedConnector) {
|
|
58212
|
+
throw new InvalidGrantError("Managed refresh token requires confidential-client exchange");
|
|
58213
|
+
}
|
|
58214
|
+
const persistedEntry = entry;
|
|
58085
58215
|
await this.store.del("refresh_tokens", refreshToken);
|
|
58086
|
-
const storedPrincipal = readPrincipal(
|
|
58216
|
+
const storedPrincipal = readPrincipal(persistedEntry);
|
|
58087
58217
|
if (storedPrincipal.type === "serviceAccount") {
|
|
58088
58218
|
const newAccessToken2 = randomToken();
|
|
58089
58219
|
const newRefreshToken2 = randomToken();
|
|
@@ -58106,7 +58236,7 @@ class KadoaOAuthProvider {
|
|
|
58106
58236
|
}
|
|
58107
58237
|
let { supabaseJwt, supabaseRefreshToken } = storedPrincipal;
|
|
58108
58238
|
const claims = jwtClaims(storedPrincipal.supabaseJwt);
|
|
58109
|
-
const context = `email=${claims.email}, team=${
|
|
58239
|
+
const context = `email=${claims.email}, team=${persistedEntry.teamId}`;
|
|
58110
58240
|
try {
|
|
58111
58241
|
const { refreshSupabaseJwtRaw: refreshSupabaseJwtRaw2, SessionExpiredError: SessionExpiredError2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
|
|
58112
58242
|
const refreshed = await refreshSupabaseJwtRaw2(supabaseRefreshToken);
|
|
@@ -58125,7 +58255,7 @@ class KadoaOAuthProvider {
|
|
|
58125
58255
|
console.error(`[AUTH] REFRESH_WARN: unexpected error, using stale JWT (${context}):`, error48);
|
|
58126
58256
|
}
|
|
58127
58257
|
const freshClaims = jwtClaims(supabaseJwt);
|
|
58128
|
-
const teamId = freshClaims.activeTeamId ??
|
|
58258
|
+
const teamId = freshClaims.activeTeamId ?? persistedEntry.teamId;
|
|
58129
58259
|
const newAccessToken = randomToken();
|
|
58130
58260
|
const newRefreshToken = randomToken();
|
|
58131
58261
|
const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
|
|
@@ -58165,6 +58295,13 @@ class KadoaOAuthProvider {
|
|
|
58165
58295
|
throw new InvalidTokenError("Access token expired");
|
|
58166
58296
|
}
|
|
58167
58297
|
const principal = readPrincipal(entry);
|
|
58298
|
+
if (principal.type === "serviceAccount") {
|
|
58299
|
+
const connector = await this.getManagedRegistry().get(entry.clientId);
|
|
58300
|
+
if (!connector || connector.connectorId !== principal.connectorId || connector.teamId !== principal.teamId || connector.serviceUserId !== principal.serviceUserId || connector.secretVersion !== principal.secretVersion) {
|
|
58301
|
+
await this.store.del("access_tokens", token);
|
|
58302
|
+
throw new InvalidTokenError("Managed connector is inactive or changed");
|
|
58303
|
+
}
|
|
58304
|
+
}
|
|
58168
58305
|
return {
|
|
58169
58306
|
token,
|
|
58170
58307
|
clientId: entry.clientId,
|
|
@@ -58206,29 +58343,6 @@ class KadoaOAuthProvider {
|
|
|
58206
58343
|
res.redirect(redirectUrl.toString());
|
|
58207
58344
|
}
|
|
58208
58345
|
}
|
|
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
58346
|
async handleTeamSelection(req, res) {
|
|
58233
58347
|
const { token, teamId } = req.body;
|
|
58234
58348
|
if (!token || !teamId) {
|
|
@@ -58269,12 +58383,33 @@ class KadoaOAuthProvider {
|
|
|
58269
58383
|
res.redirect(redirectUrl.toString());
|
|
58270
58384
|
}
|
|
58271
58385
|
}
|
|
58386
|
+
async completeManagedAuthFlow(pending, res, connector) {
|
|
58387
|
+
const mcpCode = randomToken();
|
|
58388
|
+
await this.store.set("auth_codes", mcpCode, {
|
|
58389
|
+
managedConnector: {
|
|
58390
|
+
connectorId: connector.connectorId,
|
|
58391
|
+
clientId: connector.clientId,
|
|
58392
|
+
teamId: connector.teamId,
|
|
58393
|
+
serviceUserId: connector.serviceUserId,
|
|
58394
|
+
secretVersion: connector.secretVersion
|
|
58395
|
+
},
|
|
58396
|
+
codeChallenge: pending.params.codeChallenge,
|
|
58397
|
+
clientId: pending.clientId,
|
|
58398
|
+
redirectUri: pending.params.redirectUri,
|
|
58399
|
+
expiresAt: Date.now() + 600000
|
|
58400
|
+
}, 600);
|
|
58401
|
+
const redirectUrl = new URL(pending.params.redirectUri);
|
|
58402
|
+
redirectUrl.searchParams.set("code", mcpCode);
|
|
58403
|
+
if (pending.params.state)
|
|
58404
|
+
redirectUrl.searchParams.set("state", pending.params.state);
|
|
58405
|
+
res.redirect(redirectUrl.toString());
|
|
58406
|
+
}
|
|
58272
58407
|
async completeAuthFlow(pending, res, principal) {
|
|
58273
58408
|
const mcpCode = randomToken();
|
|
58274
58409
|
await this.store.set("auth_codes", mcpCode, {
|
|
58275
58410
|
...persistPrincipal(principal),
|
|
58276
58411
|
codeChallenge: pending.params.codeChallenge,
|
|
58277
|
-
clientId: pending.
|
|
58412
|
+
clientId: pending.clientId,
|
|
58278
58413
|
redirectUri: pending.params.redirectUri,
|
|
58279
58414
|
expiresAt: Date.now() + 600000
|
|
58280
58415
|
}, 600);
|
|
@@ -58286,33 +58421,6 @@ class KadoaOAuthProvider {
|
|
|
58286
58421
|
res.redirect(redirectUrl.toString());
|
|
58287
58422
|
}
|
|
58288
58423
|
}
|
|
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
58424
|
function renderTeamSelectionPage(teams, selectionToken) {
|
|
58317
58425
|
const teamButtons = teams.map((t) => `
|
|
58318
58426
|
<button type="submit" name="teamId" value="${t.id}" class="team-btn">
|
|
@@ -58735,7 +58843,8 @@ function escapeHtml(str) {
|
|
|
58735
58843
|
var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
|
|
58736
58844
|
var init_auth2 = __esm(() => {
|
|
58737
58845
|
init_errors4();
|
|
58738
|
-
|
|
58846
|
+
init_managed_connector_client();
|
|
58847
|
+
init_managed_connector_registry();
|
|
58739
58848
|
TEAM_SELECTION_TTL = 10 * 60 * 1000;
|
|
58740
58849
|
ACCESS_TOKEN_TTL = 7 * 24 * 3600;
|
|
58741
58850
|
});
|
|
@@ -58836,6 +58945,76 @@ class RedisTokenStore {
|
|
|
58836
58945
|
var KEY_PREFIX = "kadoa-mcp";
|
|
58837
58946
|
var init_redis_store = () => {};
|
|
58838
58947
|
|
|
58948
|
+
// src/managed-token-handler.ts
|
|
58949
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
58950
|
+
function requireString(body, key, maxLength) {
|
|
58951
|
+
const value = body[key];
|
|
58952
|
+
if (typeof value !== "string" || !value)
|
|
58953
|
+
throw new InvalidRequestError(`${key} is required`);
|
|
58954
|
+
if (value.length > maxLength)
|
|
58955
|
+
throw new InvalidRequestError(`${key} is invalid`);
|
|
58956
|
+
return value;
|
|
58957
|
+
}
|
|
58958
|
+
function verifyPkce(verifier, challenge) {
|
|
58959
|
+
return createHash3("sha256").update(verifier).digest("base64url") === challenge;
|
|
58960
|
+
}
|
|
58961
|
+
function managedTokenHandler(options) {
|
|
58962
|
+
const { provider } = options;
|
|
58963
|
+
let registry2 = options.registry;
|
|
58964
|
+
return async (req, res, next) => {
|
|
58965
|
+
const body = req.body ?? {};
|
|
58966
|
+
const clientId = typeof body.client_id === "string" ? body.client_id : "";
|
|
58967
|
+
if (!isManagedConnectorClientId(clientId)) {
|
|
58968
|
+
next();
|
|
58969
|
+
return;
|
|
58970
|
+
}
|
|
58971
|
+
res.setHeader("Cache-Control", "no-store");
|
|
58972
|
+
try {
|
|
58973
|
+
if (clientId.length > 128)
|
|
58974
|
+
throw new InvalidClientError("Invalid client credentials");
|
|
58975
|
+
const clientSecret = requireString(body, "client_secret", 256);
|
|
58976
|
+
registry2 ??= new ManagedConnectorRegistry;
|
|
58977
|
+
const connector = await registry2.authenticate(clientId, clientSecret);
|
|
58978
|
+
const grantType = requireString(body, "grant_type", 64);
|
|
58979
|
+
let tokens;
|
|
58980
|
+
if (grantType === "authorization_code") {
|
|
58981
|
+
const code = requireString(body, "code", 512);
|
|
58982
|
+
const verifier = requireString(body, "code_verifier", 128);
|
|
58983
|
+
const redirectUri = typeof body.redirect_uri === "string" ? body.redirect_uri : undefined;
|
|
58984
|
+
const challenge = await provider.challengeForAuthorizationCode({ client_id: clientId }, code);
|
|
58985
|
+
if (!verifyPkce(verifier, challenge))
|
|
58986
|
+
throw new InvalidGrantError("code_verifier does not match the challenge");
|
|
58987
|
+
tokens = await provider.exchangeManagedAuthorizationCode(connector, code, redirectUri);
|
|
58988
|
+
} else if (grantType === "refresh_token") {
|
|
58989
|
+
const refreshToken = requireString(body, "refresh_token", 512);
|
|
58990
|
+
tokens = await provider.exchangeManagedRefreshToken(connector, refreshToken);
|
|
58991
|
+
} else {
|
|
58992
|
+
throw new UnsupportedGrantTypeError("The grant type is not supported by this authorization server.");
|
|
58993
|
+
}
|
|
58994
|
+
res.status(200).json(tokens);
|
|
58995
|
+
} catch (error48) {
|
|
58996
|
+
let oauthError;
|
|
58997
|
+
let status = 400;
|
|
58998
|
+
if (error48 instanceof InvalidManagedConnectorClientError) {
|
|
58999
|
+
oauthError = new InvalidClientError("Invalid client credentials");
|
|
59000
|
+
} else if (error48 instanceof ManagedConnectorRegistryUnavailableError) {
|
|
59001
|
+
oauthError = new TemporarilyUnavailableError("Managed connector authentication is temporarily unavailable");
|
|
59002
|
+
status = 503;
|
|
59003
|
+
} else if (error48 instanceof OAuthError) {
|
|
59004
|
+
oauthError = error48;
|
|
59005
|
+
} else {
|
|
59006
|
+
oauthError = new InvalidClientError("Invalid client credentials");
|
|
59007
|
+
}
|
|
59008
|
+
res.status(status).json(oauthError.toResponseObject());
|
|
59009
|
+
}
|
|
59010
|
+
};
|
|
59011
|
+
}
|
|
59012
|
+
var init_managed_token_handler = __esm(() => {
|
|
59013
|
+
init_errors4();
|
|
59014
|
+
init_managed_connector_client();
|
|
59015
|
+
init_managed_connector_registry();
|
|
59016
|
+
});
|
|
59017
|
+
|
|
58839
59018
|
// src/http.ts
|
|
58840
59019
|
var exports_http = {};
|
|
58841
59020
|
__export(exports_http, {
|
|
@@ -58857,7 +59036,7 @@ function resolveAuth(req) {
|
|
|
58857
59036
|
}
|
|
58858
59037
|
const principal = extra.principal;
|
|
58859
59038
|
if (principal.type === "serviceAccount") {
|
|
58860
|
-
if (!principal.
|
|
59039
|
+
if (!principal.connectorId || !principal.teamId || !principal.serviceUserId || !principal.kadoaAccessToken) {
|
|
58861
59040
|
console.error("[AUTH_RESOLVE] FAIL: service-account principal is incomplete");
|
|
58862
59041
|
return;
|
|
58863
59042
|
}
|
|
@@ -58888,8 +59067,20 @@ async function startHttpServer(options) {
|
|
|
58888
59067
|
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
|
58889
59068
|
app.set("trust proxy", 1);
|
|
58890
59069
|
const store = options?.store ?? new RedisTokenStore;
|
|
58891
|
-
const
|
|
59070
|
+
const managedRegistry = options?.managedRegistry ?? (process.env.MCP_REGISTRY_SERVICE_TOKEN ? new ManagedConnectorRegistry : undefined);
|
|
59071
|
+
const provider = new KadoaOAuthProvider(store, managedRegistry);
|
|
58892
59072
|
const serverUrl = process.env.MCP_SERVER_URL || `http://localhost:${port}`;
|
|
59073
|
+
app.post("/token", express8.urlencoded({ extended: false }), rate_limit_default({
|
|
59074
|
+
windowMs: 15 * 60 * 1000,
|
|
59075
|
+
limit: 50,
|
|
59076
|
+
standardHeaders: true,
|
|
59077
|
+
legacyHeaders: false,
|
|
59078
|
+
skip: (req) => !isManagedConnectorClientId(String(req.body?.client_id ?? "")),
|
|
59079
|
+
message: {
|
|
59080
|
+
error: "too_many_requests",
|
|
59081
|
+
error_description: "Too many managed connector token requests"
|
|
59082
|
+
}
|
|
59083
|
+
}), managedTokenHandler({ provider, registry: managedRegistry }));
|
|
58893
59084
|
app.use(mcpAuthRouter({
|
|
58894
59085
|
provider,
|
|
58895
59086
|
issuerUrl: new URL(serverUrl),
|
|
@@ -58905,9 +59096,6 @@ async function startHttpServer(options) {
|
|
|
58905
59096
|
app.post("/auth/login", express8.urlencoded({ extended: false }), (req, res) => {
|
|
58906
59097
|
provider.handleEmailPasswordLogin(req, res);
|
|
58907
59098
|
});
|
|
58908
|
-
app.post("/auth/service-account", express8.urlencoded({ extended: false }), (req, res) => {
|
|
58909
|
-
provider.handleServiceAccountConsent(req, res);
|
|
58910
|
-
});
|
|
58911
59099
|
app.post("/auth/sso", express8.urlencoded({ extended: false }), (req, res) => {
|
|
58912
59100
|
provider.handleSSOLogin(req, res);
|
|
58913
59101
|
});
|
|
@@ -59006,7 +59194,7 @@ async function startHttpServer(options) {
|
|
|
59006
59194
|
await server.connect(transport);
|
|
59007
59195
|
await transport.handleRequest(req, res, req.body);
|
|
59008
59196
|
} catch (error48) {
|
|
59009
|
-
console.error(`[MCP] 500 method=${method} auth=${identity2} error=`, error48);
|
|
59197
|
+
console.error(`[MCP] 500 method=${method} auth=${identity2} error=`, safeErrorSummary2(error48));
|
|
59010
59198
|
if (!res.headersSent) {
|
|
59011
59199
|
res.status(500).json({
|
|
59012
59200
|
jsonrpc: "2.0",
|
|
@@ -59048,9 +59236,13 @@ var init_http2 = __esm(async () => {
|
|
|
59048
59236
|
init_streamableHttp();
|
|
59049
59237
|
init_router();
|
|
59050
59238
|
init_bearerAuth();
|
|
59239
|
+
init_dist4();
|
|
59051
59240
|
init_auth2();
|
|
59052
59241
|
init_redis_store();
|
|
59053
59242
|
init_client2();
|
|
59243
|
+
init_managed_connector_client();
|
|
59244
|
+
init_managed_connector_registry();
|
|
59245
|
+
init_managed_token_handler();
|
|
59054
59246
|
await init_src();
|
|
59055
59247
|
});
|
|
59056
59248
|
|
|
@@ -59103,7 +59295,7 @@ async function createServer(auth, options) {
|
|
|
59103
59295
|
}
|
|
59104
59296
|
}
|
|
59105
59297
|
registerTools(server, ctx, capabilities);
|
|
59106
|
-
server.server.onerror = (error48) => console.error("[MCP Error]", error48);
|
|
59298
|
+
server.server.onerror = (error48) => console.error("[MCP Error]", safeErrorSummary(error48));
|
|
59107
59299
|
return server;
|
|
59108
59300
|
}
|
|
59109
59301
|
var init_src = __esm(async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kadoa/mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
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
|
},
|