@kadoa/mcp 0.2.3 → 0.2.5-rc.1
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 +63 -2
- package/dist/index.js +348 -31
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
1
|
# Kadoa MCP Server
|
|
2
2
|
|
|
3
|
-
Use [Kadoa](https://kadoa.com) from Claude
|
|
3
|
+
Use [Kadoa](https://kadoa.com) from ChatGPT, Claude.ai, Claude Code, Cursor, and other MCP clients.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Remote Server (no install needed)
|
|
6
|
+
|
|
7
|
+
A hosted MCP server is available at `https://mcp.kadoa.com/mcp`. Connect from any MCP client — no local install, no API key config. You sign in with your Kadoa account via OAuth.
|
|
8
|
+
|
|
9
|
+
### ChatGPT
|
|
10
|
+
|
|
11
|
+
1. Go to **Settings → Connectors → Add MCP server**
|
|
12
|
+
2. Enter the URL: `https://mcp.kadoa.com/mcp`
|
|
13
|
+
3. Choose OAuth authentication and sign in with your Kadoa account (Google)
|
|
14
|
+
|
|
15
|
+
### Claude.ai
|
|
16
|
+
|
|
17
|
+
1. Go to **Settings → Connectors → Add custom MCP**
|
|
18
|
+
2. Enter the URL: `https://mcp.kadoa.com/mcp`
|
|
19
|
+
3. Sign in with your Kadoa account via OAuth
|
|
20
|
+
|
|
21
|
+
### Cursor (Remote)
|
|
22
|
+
|
|
23
|
+
Add to `.cursor/mcp.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"kadoa": {
|
|
29
|
+
"type": "http",
|
|
30
|
+
"url": "https://mcp.kadoa.com/mcp"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Any MCP Client
|
|
37
|
+
|
|
38
|
+
Point your client to `https://mcp.kadoa.com/mcp` with OAuth authentication.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Local Setup (stdio)
|
|
43
|
+
|
|
44
|
+
If you prefer to run the MCP server locally (e.g., for development or to use your own API key), install via npx:
|
|
6
45
|
|
|
7
46
|
### Claude Code
|
|
8
47
|
|
|
@@ -105,6 +144,8 @@ Get your API key from [kadoa.com/settings](https://kadoa.com/settings).
|
|
|
105
144
|
| `delete_workflow` | Delete a workflow |
|
|
106
145
|
| `approve_workflow` | Approve and activate a workflow |
|
|
107
146
|
| `update_workflow` | Update workflow configuration and schema |
|
|
147
|
+
| `team_list` | List all teams you belong to and see which is active |
|
|
148
|
+
| `team_switch` | Switch the active team by name or ID |
|
|
108
149
|
|
|
109
150
|
## Usage Examples
|
|
110
151
|
|
|
@@ -175,6 +216,26 @@ delete_workflow for each, confirming before proceeding.
|
|
|
175
216
|
- Verify the MCP server is configured correctly
|
|
176
217
|
- Restart your MCP client
|
|
177
218
|
|
|
219
|
+
## Deploying the Remote Server
|
|
220
|
+
|
|
221
|
+
The remote MCP server at `mcp.kadoa.com` runs as a Docker container on GKE, deployed from the `kadoa-backend` monorepo.
|
|
222
|
+
|
|
223
|
+
**To deploy a new version:**
|
|
224
|
+
|
|
225
|
+
1. Publish a new `@kadoa/mcp` version to npm (`npm publish`)
|
|
226
|
+
2. In `kadoa-backend`, update `infra/docker/mcp/package.json` to the new version and run `bun install` to regenerate the lockfile
|
|
227
|
+
3. Merge to `main` — the CI pipeline (`main-build-deploy.yml`) builds and pushes the Docker image automatically
|
|
228
|
+
4. Trigger the **Deploy to Production** workflow (`deploy-prod.yml`) with:
|
|
229
|
+
- **Target cluster:** `gcp`
|
|
230
|
+
- **Deployment scope:** `mcp`
|
|
231
|
+
- **Image tag:** the tag from step 3 (shown in the build summary)
|
|
232
|
+
- **Method:** `kubectl`
|
|
233
|
+
|
|
234
|
+
Infrastructure files in `kadoa-backend`:
|
|
235
|
+
- `infra/docker/mcp/Dockerfile.mcp-server` — Docker image definition
|
|
236
|
+
- `infra/docker/mcp/package.json` — pinned `@kadoa/mcp` version
|
|
237
|
+
- `infra/cdk8s/mcp/` — Kubernetes manifests (cdk8s)
|
|
238
|
+
|
|
178
239
|
## Development
|
|
179
240
|
|
|
180
241
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -48922,21 +48922,26 @@ var init_dist2 = __esm(() => {
|
|
|
48922
48922
|
});
|
|
48923
48923
|
|
|
48924
48924
|
// src/client.ts
|
|
48925
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
48926
|
-
import { join } from "node:path";
|
|
48925
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
48926
|
+
import { dirname, join } from "node:path";
|
|
48927
48927
|
import { homedir } from "node:os";
|
|
48928
|
-
function
|
|
48928
|
+
function getConfigPath() {
|
|
48929
48929
|
const home = process.env.HOME || homedir();
|
|
48930
|
-
|
|
48930
|
+
return join(home, ".kadoa", "config.json");
|
|
48931
|
+
}
|
|
48932
|
+
function loadConfig() {
|
|
48933
|
+
const configFile = getConfigPath();
|
|
48931
48934
|
if (!existsSync(configFile))
|
|
48932
|
-
return;
|
|
48935
|
+
return {};
|
|
48933
48936
|
try {
|
|
48934
|
-
|
|
48935
|
-
return config2.apiKey;
|
|
48937
|
+
return JSON.parse(readFileSync(configFile, "utf-8"));
|
|
48936
48938
|
} catch {
|
|
48937
|
-
return;
|
|
48939
|
+
return {};
|
|
48938
48940
|
}
|
|
48939
48941
|
}
|
|
48942
|
+
function loadApiKeyFromConfig() {
|
|
48943
|
+
return loadConfig().apiKey;
|
|
48944
|
+
}
|
|
48940
48945
|
function resolveApiKey(apiKey) {
|
|
48941
48946
|
const key = apiKey || process.env.KADOA_API_KEY || loadApiKeyFromConfig();
|
|
48942
48947
|
if (!key) {
|
|
@@ -48951,6 +48956,33 @@ var init_client = __esm(() => {
|
|
|
48951
48956
|
init_dist2();
|
|
48952
48957
|
});
|
|
48953
48958
|
|
|
48959
|
+
// src/client.ts
|
|
48960
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
48961
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
48962
|
+
import { homedir as homedir2 } from "node:os";
|
|
48963
|
+
function getConfigPath2() {
|
|
48964
|
+
const home = process.env.HOME || homedir2();
|
|
48965
|
+
return join2(home, ".kadoa", "config.json");
|
|
48966
|
+
}
|
|
48967
|
+
function loadConfig2() {
|
|
48968
|
+
const configFile = getConfigPath2();
|
|
48969
|
+
if (!existsSync2(configFile))
|
|
48970
|
+
return {};
|
|
48971
|
+
try {
|
|
48972
|
+
return JSON.parse(readFileSync2(configFile, "utf-8"));
|
|
48973
|
+
} catch {
|
|
48974
|
+
return {};
|
|
48975
|
+
}
|
|
48976
|
+
}
|
|
48977
|
+
function saveConfig(config2) {
|
|
48978
|
+
const configFile = getConfigPath2();
|
|
48979
|
+
mkdirSync2(dirname2(configFile), { recursive: true });
|
|
48980
|
+
writeFileSync2(configFile, JSON.stringify(config2, null, 2), "utf-8");
|
|
48981
|
+
}
|
|
48982
|
+
var init_client2 = __esm(() => {
|
|
48983
|
+
init_dist2();
|
|
48984
|
+
});
|
|
48985
|
+
|
|
48954
48986
|
// src/tools.ts
|
|
48955
48987
|
function jsonResult(data) {
|
|
48956
48988
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
@@ -48961,13 +48993,59 @@ function errorResult(message) {
|
|
|
48961
48993
|
isError: true
|
|
48962
48994
|
};
|
|
48963
48995
|
}
|
|
48996
|
+
function classifyError(error48) {
|
|
48997
|
+
if (KadoaSdkException.isInstance(error48)) {
|
|
48998
|
+
const code = error48.code;
|
|
48999
|
+
if (KadoaHttpException.isInstance(error48)) {
|
|
49000
|
+
const status = error48.httpStatus ? ` (HTTP ${error48.httpStatus})` : "";
|
|
49001
|
+
switch (code) {
|
|
49002
|
+
case "AUTH_ERROR":
|
|
49003
|
+
return `Authentication failed${status}. Your Kadoa API key may be invalid or expired. Please check your KADOA_API_KEY or re-authenticate.`;
|
|
49004
|
+
case "NOT_FOUND":
|
|
49005
|
+
return `Not found${status}. The workflow may have been deleted or the ID is incorrect.`;
|
|
49006
|
+
case "RATE_LIMITED":
|
|
49007
|
+
return `Rate limit exceeded${status}. Please wait a moment and try again.`;
|
|
49008
|
+
case "NETWORK_ERROR":
|
|
49009
|
+
return `Unable to reach the Kadoa API${status}. Check your network connection.`;
|
|
49010
|
+
case "TIMEOUT":
|
|
49011
|
+
return `Request timed out${status}. Please try again.`;
|
|
49012
|
+
case "VALIDATION_ERROR":
|
|
49013
|
+
case "BAD_REQUEST":
|
|
49014
|
+
return error48.message;
|
|
49015
|
+
default:
|
|
49016
|
+
return `Kadoa API error${status}. Please try again later.`;
|
|
49017
|
+
}
|
|
49018
|
+
}
|
|
49019
|
+
switch (code) {
|
|
49020
|
+
case "AUTH_ERROR":
|
|
49021
|
+
return "Authentication failed. Your Kadoa API key may be invalid or expired. Please check your KADOA_API_KEY or re-authenticate.";
|
|
49022
|
+
case "NOT_FOUND":
|
|
49023
|
+
return "Not found. The workflow may have been deleted or the ID is incorrect.";
|
|
49024
|
+
case "RATE_LIMITED":
|
|
49025
|
+
return "Rate limit exceeded. Please wait a moment and try again.";
|
|
49026
|
+
case "NETWORK_ERROR":
|
|
49027
|
+
return "Unable to reach the Kadoa API. Check your network connection.";
|
|
49028
|
+
case "TIMEOUT":
|
|
49029
|
+
return "Request timed out. Please try again.";
|
|
49030
|
+
case "VALIDATION_ERROR":
|
|
49031
|
+
case "BAD_REQUEST":
|
|
49032
|
+
return error48.message;
|
|
49033
|
+
default:
|
|
49034
|
+
return error48.message;
|
|
49035
|
+
}
|
|
49036
|
+
}
|
|
49037
|
+
if (error48 instanceof Error) {
|
|
49038
|
+
return error48.message;
|
|
49039
|
+
}
|
|
49040
|
+
return "Unknown error";
|
|
49041
|
+
}
|
|
48964
49042
|
function withErrorHandling(name, handler) {
|
|
48965
49043
|
return async (...args) => {
|
|
48966
49044
|
try {
|
|
48967
49045
|
return await handler(...args);
|
|
48968
49046
|
} catch (error48) {
|
|
48969
|
-
const message = error48
|
|
48970
|
-
console.error(`[Tool Error] ${name}:`,
|
|
49047
|
+
const message = classifyError(error48);
|
|
49048
|
+
console.error(`[Tool Error] ${name}:`, error48);
|
|
48971
49049
|
return errorResult(message);
|
|
48972
49050
|
}
|
|
48973
49051
|
};
|
|
@@ -49143,10 +49221,61 @@ function registerTools(server, client) {
|
|
|
49143
49221
|
workflow: result
|
|
49144
49222
|
});
|
|
49145
49223
|
}));
|
|
49224
|
+
server.registerTool("team_list", {
|
|
49225
|
+
description: "List all teams the current user belongs to, showing which team is currently active",
|
|
49226
|
+
inputSchema: {},
|
|
49227
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
|
|
49228
|
+
}, withErrorHandling("team_list", async () => {
|
|
49229
|
+
const response = await client.axiosInstance.get("/v5/user", {
|
|
49230
|
+
baseURL: client.baseUrl,
|
|
49231
|
+
headers: { "x-api-key": client.apiKey }
|
|
49232
|
+
});
|
|
49233
|
+
const teams = response.data?.teams ?? [];
|
|
49234
|
+
const config2 = loadConfig2();
|
|
49235
|
+
const activeTeamId = config2.teamId ?? teams[0]?.id;
|
|
49236
|
+
return jsonResult({
|
|
49237
|
+
teams: teams.map((t) => ({
|
|
49238
|
+
id: t.id,
|
|
49239
|
+
name: t.name,
|
|
49240
|
+
role: t.role,
|
|
49241
|
+
active: t.id === activeTeamId
|
|
49242
|
+
})),
|
|
49243
|
+
activeTeamId
|
|
49244
|
+
});
|
|
49245
|
+
}));
|
|
49246
|
+
server.registerTool("team_switch", {
|
|
49247
|
+
description: "Switch the active team by name or ID. After switching, all subsequent API calls will use the new team context.",
|
|
49248
|
+
inputSchema: {
|
|
49249
|
+
teamIdentifier: exports_external.string().describe("Team name or team ID to switch to")
|
|
49250
|
+
},
|
|
49251
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
49252
|
+
}, withErrorHandling("team_switch", async (args) => {
|
|
49253
|
+
const response = await client.axiosInstance.get("/v5/user", {
|
|
49254
|
+
baseURL: client.baseUrl,
|
|
49255
|
+
headers: { "x-api-key": client.apiKey }
|
|
49256
|
+
});
|
|
49257
|
+
const teams = response.data?.teams ?? [];
|
|
49258
|
+
const identifier = args.teamIdentifier;
|
|
49259
|
+
const match = teams.find((t) => t.id === identifier || t.name.toLowerCase() === identifier.toLowerCase());
|
|
49260
|
+
if (!match) {
|
|
49261
|
+
return errorResult(`Team not found: "${identifier}". Available teams: ${teams.map((t) => t.name).join(", ")}`);
|
|
49262
|
+
}
|
|
49263
|
+
const config2 = loadConfig2();
|
|
49264
|
+
config2.teamId = match.id;
|
|
49265
|
+
config2.teamName = match.name;
|
|
49266
|
+
saveConfig(config2);
|
|
49267
|
+
return jsonResult({
|
|
49268
|
+
success: true,
|
|
49269
|
+
teamId: match.id,
|
|
49270
|
+
teamName: match.name
|
|
49271
|
+
});
|
|
49272
|
+
}));
|
|
49146
49273
|
}
|
|
49147
49274
|
var SchemaFieldShape;
|
|
49148
49275
|
var init_tools = __esm(() => {
|
|
49149
49276
|
init_zod();
|
|
49277
|
+
init_dist2();
|
|
49278
|
+
init_client2();
|
|
49150
49279
|
SchemaFieldShape = {
|
|
49151
49280
|
name: exports_external.string().describe("Field name"),
|
|
49152
49281
|
description: exports_external.string().optional().describe("What this field contains"),
|
|
@@ -53172,7 +53301,7 @@ async function exchangeSupabaseCode(code, codeVerifier) {
|
|
|
53172
53301
|
const data = await res.json();
|
|
53173
53302
|
return data.access_token;
|
|
53174
53303
|
}
|
|
53175
|
-
async function
|
|
53304
|
+
async function fetchUserTeams(supabaseJwt) {
|
|
53176
53305
|
const kadoaApiUrl = process.env.KADOA_PUBLIC_API_URI || "https://api.kadoa.com";
|
|
53177
53306
|
const userRes = await fetch(`${kadoaApiUrl}/v4/user`, {
|
|
53178
53307
|
headers: { Authorization: `Bearer ${supabaseJwt}` }
|
|
@@ -53185,7 +53314,14 @@ async function resolveApiKey2(supabaseJwt) {
|
|
|
53185
53314
|
if (!userData.teams?.length) {
|
|
53186
53315
|
throw new Error("User has no teams");
|
|
53187
53316
|
}
|
|
53188
|
-
|
|
53317
|
+
return userData.teams.map((t) => ({
|
|
53318
|
+
id: t.id,
|
|
53319
|
+
name: t.name,
|
|
53320
|
+
memberRole: t.memberRole
|
|
53321
|
+
}));
|
|
53322
|
+
}
|
|
53323
|
+
async function resolveTeamApiKey(supabaseJwt, teamId) {
|
|
53324
|
+
const kadoaApiUrl = process.env.KADOA_PUBLIC_API_URI || "https://api.kadoa.com";
|
|
53189
53325
|
const keyRes = await fetch(`${kadoaApiUrl}/v4/team/${teamId}/api-key`, {
|
|
53190
53326
|
headers: { Authorization: `Bearer ${supabaseJwt}` }
|
|
53191
53327
|
});
|
|
@@ -53325,21 +53461,20 @@ class KadoaOAuthProvider {
|
|
|
53325
53461
|
pendingAuths.delete(state);
|
|
53326
53462
|
try {
|
|
53327
53463
|
const supabaseJwt = await exchangeSupabaseCode(code, pending.supabaseCodeVerifier);
|
|
53328
|
-
const
|
|
53329
|
-
|
|
53330
|
-
|
|
53331
|
-
apiKey,
|
|
53332
|
-
|
|
53333
|
-
clientId: pending.client.client_id,
|
|
53334
|
-
redirectUri: pending.params.redirectUri,
|
|
53335
|
-
expiresAt: Date.now() + 10 * 60 * 1000
|
|
53336
|
-
});
|
|
53337
|
-
const redirectUrl = new URL(pending.params.redirectUri);
|
|
53338
|
-
redirectUrl.searchParams.set("code", mcpCode);
|
|
53339
|
-
if (pending.params.state) {
|
|
53340
|
-
redirectUrl.searchParams.set("state", pending.params.state);
|
|
53464
|
+
const teams = await fetchUserTeams(supabaseJwt);
|
|
53465
|
+
if (teams.length === 1) {
|
|
53466
|
+
const apiKey = await resolveTeamApiKey(supabaseJwt, teams[0].id);
|
|
53467
|
+
this.completeAuthFlow(pending, apiKey, res);
|
|
53468
|
+
return;
|
|
53341
53469
|
}
|
|
53342
|
-
|
|
53470
|
+
const selectionToken = randomToken();
|
|
53471
|
+
pendingTeamSelections.set(selectionToken, {
|
|
53472
|
+
supabaseJwt,
|
|
53473
|
+
teams,
|
|
53474
|
+
pending,
|
|
53475
|
+
expiresAt: Date.now() + TEAM_SELECTION_TTL
|
|
53476
|
+
});
|
|
53477
|
+
res.type("html").send(renderTeamSelectionPage(teams, selectionToken));
|
|
53343
53478
|
} catch (error48) {
|
|
53344
53479
|
console.error("Auth callback error:", error48);
|
|
53345
53480
|
const redirectUrl = new URL(pending.params.redirectUri);
|
|
@@ -53351,14 +53486,180 @@ class KadoaOAuthProvider {
|
|
|
53351
53486
|
res.redirect(redirectUrl.toString());
|
|
53352
53487
|
}
|
|
53353
53488
|
}
|
|
53489
|
+
async handleTeamSelection(req, res) {
|
|
53490
|
+
const { token, teamId } = req.body;
|
|
53491
|
+
if (!token || !teamId) {
|
|
53492
|
+
res.status(400).send("Missing token or teamId");
|
|
53493
|
+
return;
|
|
53494
|
+
}
|
|
53495
|
+
const entry = pendingTeamSelections.get(token);
|
|
53496
|
+
if (!entry) {
|
|
53497
|
+
res.status(400).send("Unknown or expired team selection token");
|
|
53498
|
+
return;
|
|
53499
|
+
}
|
|
53500
|
+
if (entry.expiresAt < Date.now()) {
|
|
53501
|
+
pendingTeamSelections.delete(token);
|
|
53502
|
+
res.status(400).send("Team selection expired — please log in again");
|
|
53503
|
+
return;
|
|
53504
|
+
}
|
|
53505
|
+
if (!entry.teams.some((t) => t.id === teamId)) {
|
|
53506
|
+
res.status(403).send("Invalid team selection");
|
|
53507
|
+
return;
|
|
53508
|
+
}
|
|
53509
|
+
pendingTeamSelections.delete(token);
|
|
53510
|
+
try {
|
|
53511
|
+
const apiKey = await resolveTeamApiKey(entry.supabaseJwt, teamId);
|
|
53512
|
+
this.completeAuthFlow(entry.pending, apiKey, res);
|
|
53513
|
+
} catch (error48) {
|
|
53514
|
+
console.error("Team selection error:", error48);
|
|
53515
|
+
const redirectUrl = new URL(entry.pending.params.redirectUri);
|
|
53516
|
+
redirectUrl.searchParams.set("error", "server_error");
|
|
53517
|
+
redirectUrl.searchParams.set("error_description", error48 instanceof Error ? error48.message : "Failed to resolve team API key");
|
|
53518
|
+
if (entry.pending.params.state) {
|
|
53519
|
+
redirectUrl.searchParams.set("state", entry.pending.params.state);
|
|
53520
|
+
}
|
|
53521
|
+
res.redirect(redirectUrl.toString());
|
|
53522
|
+
}
|
|
53523
|
+
}
|
|
53524
|
+
completeAuthFlow(pending, apiKey, res) {
|
|
53525
|
+
const mcpCode = randomToken();
|
|
53526
|
+
authCodes.set(mcpCode, {
|
|
53527
|
+
apiKey,
|
|
53528
|
+
codeChallenge: pending.params.codeChallenge,
|
|
53529
|
+
clientId: pending.client.client_id,
|
|
53530
|
+
redirectUri: pending.params.redirectUri,
|
|
53531
|
+
expiresAt: Date.now() + 10 * 60 * 1000
|
|
53532
|
+
});
|
|
53533
|
+
const redirectUrl = new URL(pending.params.redirectUri);
|
|
53534
|
+
redirectUrl.searchParams.set("code", mcpCode);
|
|
53535
|
+
if (pending.params.state) {
|
|
53536
|
+
redirectUrl.searchParams.set("state", pending.params.state);
|
|
53537
|
+
}
|
|
53538
|
+
res.redirect(redirectUrl.toString());
|
|
53539
|
+
}
|
|
53354
53540
|
}
|
|
53355
|
-
|
|
53541
|
+
function renderTeamSelectionPage(teams, selectionToken) {
|
|
53542
|
+
const teamButtons = teams.map((t) => `
|
|
53543
|
+
<button type="submit" name="teamId" value="${t.id}" class="team-btn">
|
|
53544
|
+
<span class="team-name">${escapeHtml(t.name)}</span>
|
|
53545
|
+
${t.memberRole ? `<span class="team-role">${escapeHtml(t.memberRole.toLowerCase())}</span>` : ""}
|
|
53546
|
+
</button>`).join(`
|
|
53547
|
+
`);
|
|
53548
|
+
return `<!DOCTYPE html>
|
|
53549
|
+
<html lang="en">
|
|
53550
|
+
<head>
|
|
53551
|
+
<meta charset="utf-8" />
|
|
53552
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
53553
|
+
<title>Select Team - Kadoa</title>
|
|
53554
|
+
<style>
|
|
53555
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
53556
|
+
|
|
53557
|
+
body {
|
|
53558
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
53559
|
+
background: #0a0a0f;
|
|
53560
|
+
color: #e4e4e7;
|
|
53561
|
+
min-height: 100vh;
|
|
53562
|
+
display: flex;
|
|
53563
|
+
align-items: center;
|
|
53564
|
+
justify-content: center;
|
|
53565
|
+
}
|
|
53566
|
+
|
|
53567
|
+
.container {
|
|
53568
|
+
width: 100%;
|
|
53569
|
+
max-width: 420px;
|
|
53570
|
+
padding: 2rem;
|
|
53571
|
+
}
|
|
53572
|
+
|
|
53573
|
+
.logo {
|
|
53574
|
+
text-align: center;
|
|
53575
|
+
margin-bottom: 2rem;
|
|
53576
|
+
}
|
|
53577
|
+
|
|
53578
|
+
.logo svg {
|
|
53579
|
+
width: 40px;
|
|
53580
|
+
height: 40px;
|
|
53581
|
+
}
|
|
53582
|
+
|
|
53583
|
+
h1 {
|
|
53584
|
+
font-size: 1.25rem;
|
|
53585
|
+
font-weight: 600;
|
|
53586
|
+
text-align: center;
|
|
53587
|
+
margin-bottom: 0.5rem;
|
|
53588
|
+
color: #fafafa;
|
|
53589
|
+
}
|
|
53590
|
+
|
|
53591
|
+
.subtitle {
|
|
53592
|
+
text-align: center;
|
|
53593
|
+
color: #71717a;
|
|
53594
|
+
font-size: 0.875rem;
|
|
53595
|
+
margin-bottom: 1.5rem;
|
|
53596
|
+
}
|
|
53597
|
+
|
|
53598
|
+
.team-btn {
|
|
53599
|
+
width: 100%;
|
|
53600
|
+
display: flex;
|
|
53601
|
+
align-items: center;
|
|
53602
|
+
justify-content: space-between;
|
|
53603
|
+
padding: 0.875rem 1rem;
|
|
53604
|
+
margin-bottom: 0.5rem;
|
|
53605
|
+
background: #18181b;
|
|
53606
|
+
border: 1px solid #27272a;
|
|
53607
|
+
border-radius: 8px;
|
|
53608
|
+
color: #fafafa;
|
|
53609
|
+
font-size: 0.9375rem;
|
|
53610
|
+
cursor: pointer;
|
|
53611
|
+
transition: background 0.15s, border-color 0.15s;
|
|
53612
|
+
}
|
|
53613
|
+
|
|
53614
|
+
.team-btn:hover {
|
|
53615
|
+
background: #1f1f23;
|
|
53616
|
+
border-color: #3f3f46;
|
|
53617
|
+
}
|
|
53618
|
+
|
|
53619
|
+
.team-btn:active {
|
|
53620
|
+
background: #27272a;
|
|
53621
|
+
}
|
|
53622
|
+
|
|
53623
|
+
.team-name { font-weight: 500; }
|
|
53624
|
+
|
|
53625
|
+
.team-role {
|
|
53626
|
+
font-size: 0.75rem;
|
|
53627
|
+
color: #71717a;
|
|
53628
|
+
text-transform: capitalize;
|
|
53629
|
+
}
|
|
53630
|
+
</style>
|
|
53631
|
+
</head>
|
|
53632
|
+
<body>
|
|
53633
|
+
<div class="container">
|
|
53634
|
+
<div class="logo">
|
|
53635
|
+
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
53636
|
+
<rect width="40" height="40" rx="8" fill="#6d28d9"/>
|
|
53637
|
+
<text x="50%" y="54%" dominant-baseline="middle" text-anchor="middle"
|
|
53638
|
+
fill="white" font-family="sans-serif" font-size="20" font-weight="700">K</text>
|
|
53639
|
+
</svg>
|
|
53640
|
+
</div>
|
|
53641
|
+
<h1>Select a team</h1>
|
|
53642
|
+
<p class="subtitle">Choose which team to connect with this MCP session</p>
|
|
53643
|
+
<form method="POST" action="/team-select">
|
|
53644
|
+
<input type="hidden" name="token" value="${selectionToken}" />
|
|
53645
|
+
${teamButtons}
|
|
53646
|
+
</form>
|
|
53647
|
+
</div>
|
|
53648
|
+
</body>
|
|
53649
|
+
</html>`;
|
|
53650
|
+
}
|
|
53651
|
+
function escapeHtml(str) {
|
|
53652
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
53653
|
+
}
|
|
53654
|
+
var clients, pendingAuths, pendingTeamSelections, authCodes, accessTokens, refreshTokens, TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL = 3600, kadoaClientsStore;
|
|
53356
53655
|
var init_auth2 = __esm(() => {
|
|
53357
53656
|
clients = new Map;
|
|
53358
53657
|
pendingAuths = new Map;
|
|
53658
|
+
pendingTeamSelections = new Map;
|
|
53359
53659
|
authCodes = new Map;
|
|
53360
53660
|
accessTokens = new Map;
|
|
53361
53661
|
refreshTokens = new Map;
|
|
53662
|
+
TEAM_SELECTION_TTL = 10 * 60 * 1000;
|
|
53362
53663
|
kadoaClientsStore = {
|
|
53363
53664
|
getClient(clientId) {
|
|
53364
53665
|
return clients.get(clientId);
|
|
@@ -53382,7 +53683,8 @@ __export(exports_http, {
|
|
|
53382
53683
|
startHttpServer: () => startHttpServer
|
|
53383
53684
|
});
|
|
53384
53685
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
53385
|
-
|
|
53686
|
+
import express8 from "express";
|
|
53687
|
+
function resolveApiKey2(req) {
|
|
53386
53688
|
return req.auth?.extra?.apiKey;
|
|
53387
53689
|
}
|
|
53388
53690
|
async function startHttpServer() {
|
|
@@ -53401,13 +53703,16 @@ async function startHttpServer() {
|
|
|
53401
53703
|
app.get("/auth/callback", (req, res) => {
|
|
53402
53704
|
provider.handleAuthCallback(req, res);
|
|
53403
53705
|
});
|
|
53706
|
+
app.post("/team-select", express8.urlencoded({ extended: false }), (req, res) => {
|
|
53707
|
+
provider.handleTeamSelection(req, res);
|
|
53708
|
+
});
|
|
53404
53709
|
app.get("/health", (_req, res) => {
|
|
53405
53710
|
res.json({ status: "ok", sessions: Object.keys(sessions).length });
|
|
53406
53711
|
});
|
|
53407
53712
|
const bearerAuth = requireBearerAuth({ verifier: provider });
|
|
53408
53713
|
app.post("/mcp", bearerAuth, async (req, res) => {
|
|
53409
53714
|
const sessionId = req.headers["mcp-session-id"];
|
|
53410
|
-
const apiKey =
|
|
53715
|
+
const apiKey = resolveApiKey2(req);
|
|
53411
53716
|
if (!apiKey) {
|
|
53412
53717
|
res.status(401).json({
|
|
53413
53718
|
jsonrpc: "2.0",
|
|
@@ -53464,7 +53769,7 @@ async function startHttpServer() {
|
|
|
53464
53769
|
});
|
|
53465
53770
|
app.get("/mcp", bearerAuth, async (req, res) => {
|
|
53466
53771
|
const sessionId = req.headers["mcp-session-id"];
|
|
53467
|
-
const apiKey =
|
|
53772
|
+
const apiKey = resolveApiKey2(req);
|
|
53468
53773
|
if (!apiKey) {
|
|
53469
53774
|
res.status(401).send("Unauthorized: Bearer token required");
|
|
53470
53775
|
return;
|
|
@@ -53481,7 +53786,7 @@ async function startHttpServer() {
|
|
|
53481
53786
|
});
|
|
53482
53787
|
app.delete("/mcp", bearerAuth, async (req, res) => {
|
|
53483
53788
|
const sessionId = req.headers["mcp-session-id"];
|
|
53484
|
-
const apiKey =
|
|
53789
|
+
const apiKey = resolveApiKey2(req);
|
|
53485
53790
|
if (!apiKey) {
|
|
53486
53791
|
res.status(401).send("Unauthorized: Bearer token required");
|
|
53487
53792
|
return;
|
|
@@ -53530,6 +53835,17 @@ function createServer(apiKey) {
|
|
|
53530
53835
|
server.server.onerror = (error48) => console.error("[MCP Error]", error48);
|
|
53531
53836
|
return server;
|
|
53532
53837
|
}
|
|
53838
|
+
async function validateApiKey() {
|
|
53839
|
+
const client = createKadoaClient();
|
|
53840
|
+
try {
|
|
53841
|
+
await client.workflow.list({ limit: 1 });
|
|
53842
|
+
} catch (error48) {
|
|
53843
|
+
if (KadoaSdkException.isInstance(error48) && error48.code === "AUTH_ERROR") {
|
|
53844
|
+
console.error("Kadoa MCP: Invalid API key. Check KADOA_API_KEY or run 'kadoa login'.");
|
|
53845
|
+
process.exit(1);
|
|
53846
|
+
}
|
|
53847
|
+
}
|
|
53848
|
+
}
|
|
53533
53849
|
var init_src = __esm(async () => {
|
|
53534
53850
|
init_mcp();
|
|
53535
53851
|
init_stdio2();
|
|
@@ -53541,6 +53857,7 @@ var init_src = __esm(async () => {
|
|
|
53541
53857
|
const { startHttpServer: startHttpServer2 } = await init_http2().then(() => exports_http);
|
|
53542
53858
|
await startHttpServer2();
|
|
53543
53859
|
} else {
|
|
53860
|
+
await validateApiKey();
|
|
53544
53861
|
const server = createServer();
|
|
53545
53862
|
const transport = new StdioServerTransport;
|
|
53546
53863
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kadoa/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5-rc.1",
|
|
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",
|
|
@@ -39,6 +39,12 @@
|
|
|
39
39
|
"bun-types": "^1.3.3",
|
|
40
40
|
"typescript": "^5.9.3"
|
|
41
41
|
},
|
|
42
|
-
"keywords": [
|
|
42
|
+
"keywords": [
|
|
43
|
+
"kadoa",
|
|
44
|
+
"mcp",
|
|
45
|
+
"model-context-protocol",
|
|
46
|
+
"web-scraping",
|
|
47
|
+
"data-extraction"
|
|
48
|
+
],
|
|
43
49
|
"license": "MIT"
|
|
44
50
|
}
|