@kadoa/mcp 0.2.4 → 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 +22 -0
- package/dist/index.js +197 -21
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -144,6 +144,8 @@ Get your API key from [kadoa.com/settings](https://kadoa.com/settings).
|
|
|
144
144
|
| `delete_workflow` | Delete a workflow |
|
|
145
145
|
| `approve_workflow` | Approve and activate a workflow |
|
|
146
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 |
|
|
147
149
|
|
|
148
150
|
## Usage Examples
|
|
149
151
|
|
|
@@ -214,6 +216,26 @@ delete_workflow for each, confirming before proceeding.
|
|
|
214
216
|
- Verify the MCP server is configured correctly
|
|
215
217
|
- Restart your MCP client
|
|
216
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
|
+
|
|
217
239
|
## Development
|
|
218
240
|
|
|
219
241
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -53301,7 +53301,7 @@ async function exchangeSupabaseCode(code, codeVerifier) {
|
|
|
53301
53301
|
const data = await res.json();
|
|
53302
53302
|
return data.access_token;
|
|
53303
53303
|
}
|
|
53304
|
-
async function
|
|
53304
|
+
async function fetchUserTeams(supabaseJwt) {
|
|
53305
53305
|
const kadoaApiUrl = process.env.KADOA_PUBLIC_API_URI || "https://api.kadoa.com";
|
|
53306
53306
|
const userRes = await fetch(`${kadoaApiUrl}/v4/user`, {
|
|
53307
53307
|
headers: { Authorization: `Bearer ${supabaseJwt}` }
|
|
@@ -53314,7 +53314,14 @@ async function resolveApiKey2(supabaseJwt) {
|
|
|
53314
53314
|
if (!userData.teams?.length) {
|
|
53315
53315
|
throw new Error("User has no teams");
|
|
53316
53316
|
}
|
|
53317
|
-
|
|
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";
|
|
53318
53325
|
const keyRes = await fetch(`${kadoaApiUrl}/v4/team/${teamId}/api-key`, {
|
|
53319
53326
|
headers: { Authorization: `Bearer ${supabaseJwt}` }
|
|
53320
53327
|
});
|
|
@@ -53454,21 +53461,20 @@ class KadoaOAuthProvider {
|
|
|
53454
53461
|
pendingAuths.delete(state);
|
|
53455
53462
|
try {
|
|
53456
53463
|
const supabaseJwt = await exchangeSupabaseCode(code, pending.supabaseCodeVerifier);
|
|
53457
|
-
const
|
|
53458
|
-
|
|
53459
|
-
|
|
53460
|
-
apiKey,
|
|
53461
|
-
|
|
53462
|
-
clientId: pending.client.client_id,
|
|
53463
|
-
redirectUri: pending.params.redirectUri,
|
|
53464
|
-
expiresAt: Date.now() + 10 * 60 * 1000
|
|
53465
|
-
});
|
|
53466
|
-
const redirectUrl = new URL(pending.params.redirectUri);
|
|
53467
|
-
redirectUrl.searchParams.set("code", mcpCode);
|
|
53468
|
-
if (pending.params.state) {
|
|
53469
|
-
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;
|
|
53470
53469
|
}
|
|
53471
|
-
|
|
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));
|
|
53472
53478
|
} catch (error48) {
|
|
53473
53479
|
console.error("Auth callback error:", error48);
|
|
53474
53480
|
const redirectUrl = new URL(pending.params.redirectUri);
|
|
@@ -53480,14 +53486,180 @@ class KadoaOAuthProvider {
|
|
|
53480
53486
|
res.redirect(redirectUrl.toString());
|
|
53481
53487
|
}
|
|
53482
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
|
+
}
|
|
53483
53540
|
}
|
|
53484
|
-
|
|
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;
|
|
53485
53655
|
var init_auth2 = __esm(() => {
|
|
53486
53656
|
clients = new Map;
|
|
53487
53657
|
pendingAuths = new Map;
|
|
53658
|
+
pendingTeamSelections = new Map;
|
|
53488
53659
|
authCodes = new Map;
|
|
53489
53660
|
accessTokens = new Map;
|
|
53490
53661
|
refreshTokens = new Map;
|
|
53662
|
+
TEAM_SELECTION_TTL = 10 * 60 * 1000;
|
|
53491
53663
|
kadoaClientsStore = {
|
|
53492
53664
|
getClient(clientId) {
|
|
53493
53665
|
return clients.get(clientId);
|
|
@@ -53511,7 +53683,8 @@ __export(exports_http, {
|
|
|
53511
53683
|
startHttpServer: () => startHttpServer
|
|
53512
53684
|
});
|
|
53513
53685
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
53514
|
-
|
|
53686
|
+
import express8 from "express";
|
|
53687
|
+
function resolveApiKey2(req) {
|
|
53515
53688
|
return req.auth?.extra?.apiKey;
|
|
53516
53689
|
}
|
|
53517
53690
|
async function startHttpServer() {
|
|
@@ -53530,13 +53703,16 @@ async function startHttpServer() {
|
|
|
53530
53703
|
app.get("/auth/callback", (req, res) => {
|
|
53531
53704
|
provider.handleAuthCallback(req, res);
|
|
53532
53705
|
});
|
|
53706
|
+
app.post("/team-select", express8.urlencoded({ extended: false }), (req, res) => {
|
|
53707
|
+
provider.handleTeamSelection(req, res);
|
|
53708
|
+
});
|
|
53533
53709
|
app.get("/health", (_req, res) => {
|
|
53534
53710
|
res.json({ status: "ok", sessions: Object.keys(sessions).length });
|
|
53535
53711
|
});
|
|
53536
53712
|
const bearerAuth = requireBearerAuth({ verifier: provider });
|
|
53537
53713
|
app.post("/mcp", bearerAuth, async (req, res) => {
|
|
53538
53714
|
const sessionId = req.headers["mcp-session-id"];
|
|
53539
|
-
const apiKey =
|
|
53715
|
+
const apiKey = resolveApiKey2(req);
|
|
53540
53716
|
if (!apiKey) {
|
|
53541
53717
|
res.status(401).json({
|
|
53542
53718
|
jsonrpc: "2.0",
|
|
@@ -53593,7 +53769,7 @@ async function startHttpServer() {
|
|
|
53593
53769
|
});
|
|
53594
53770
|
app.get("/mcp", bearerAuth, async (req, res) => {
|
|
53595
53771
|
const sessionId = req.headers["mcp-session-id"];
|
|
53596
|
-
const apiKey =
|
|
53772
|
+
const apiKey = resolveApiKey2(req);
|
|
53597
53773
|
if (!apiKey) {
|
|
53598
53774
|
res.status(401).send("Unauthorized: Bearer token required");
|
|
53599
53775
|
return;
|
|
@@ -53610,7 +53786,7 @@ async function startHttpServer() {
|
|
|
53610
53786
|
});
|
|
53611
53787
|
app.delete("/mcp", bearerAuth, async (req, res) => {
|
|
53612
53788
|
const sessionId = req.headers["mcp-session-id"];
|
|
53613
|
-
const apiKey =
|
|
53789
|
+
const apiKey = resolveApiKey2(req);
|
|
53614
53790
|
if (!apiKey) {
|
|
53615
53791
|
res.status(401).send("Unauthorized: Bearer token required");
|
|
53616
53792
|
return;
|
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
|
}
|