@kadoa/mcp 0.3.7-rc.2 → 0.3.7-rc.4

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 +114 -6
  2. package/dist/index.js +306 -29
  3. package/package.json +6 -2
package/README.md CHANGED
@@ -4,7 +4,7 @@ Use [Kadoa](https://kadoa.com) from ChatGPT, Claude.ai, Claude Code, Cursor, and
4
4
 
5
5
  ## Remote Server (no install needed)
6
6
 
7
- A hosted MCP server is available at `https://mcp.kadoa.com/mcp`. Connect from any MCP client — no local install needed. You sign in with your Kadoa account via OAuth.
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
8
 
9
9
  ### Claude Code
10
10
 
@@ -24,7 +24,7 @@ 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
- ### Cursor
27
+ ### Cursor (Remote)
28
28
 
29
29
  Add to `.cursor/mcp.json`:
30
30
 
@@ -43,6 +43,101 @@ Add to `.cursor/mcp.json`:
43
43
 
44
44
  Point your client to `https://mcp.kadoa.com/mcp` with OAuth authentication.
45
45
 
46
+ ---
47
+
48
+ ## Local Setup (stdio)
49
+
50
+ If you prefer to run the MCP server locally (e.g., for development or to use your own API key), install via npx:
51
+
52
+ ### Claude Code
53
+
54
+ ```bash
55
+ claude mcp add --transport stdio -e KADOA_API_KEY=tk-your_api_key kadoa -- npx -y @kadoa/mcp
56
+ ```
57
+
58
+ Add `-s user` to enable for all projects. If you have the [Kadoa CLI](https://www.npmjs.com/package/@kadoa/cli) installed, you can skip the `-e` flag — just run `kadoa login` and the MCP will use your saved key automatically.
59
+
60
+ ### Claude Desktop
61
+
62
+ Add to `~/.config/Claude/claude_desktop_config.json`:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "kadoa": {
68
+ "command": "npx",
69
+ "args": ["-y", "@kadoa/mcp"],
70
+ "env": {
71
+ "KADOA_API_KEY": "tk-your_api_key"
72
+ }
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ Restart Claude Desktop.
79
+
80
+ ### Cursor
81
+
82
+ Add to `.cursor/mcp.json`:
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "kadoa": {
88
+ "command": "npx",
89
+ "args": ["-y", "@kadoa/mcp"],
90
+ "env": {
91
+ "KADOA_API_KEY": "tk-your_api_key"
92
+ }
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Codex
99
+
100
+ ```bash
101
+ codex mcp add kadoa -- npx -y @kadoa/mcp
102
+ ```
103
+
104
+ Or add to `~/.codex/config.toml`:
105
+
106
+ ```toml
107
+ [mcp_servers.kadoa]
108
+ command = "npx"
109
+ args = ["-y", "@kadoa/mcp"]
110
+
111
+ [mcp_servers.kadoa.env]
112
+ KADOA_API_KEY = "tk-your_api_key"
113
+ ```
114
+
115
+ ### Gemini CLI
116
+
117
+ ```bash
118
+ gemini mcp add -t stdio kadoa npx -- -y @kadoa/mcp
119
+ ```
120
+
121
+ Or add to `~/.gemini/settings.json`:
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "kadoa": {
127
+ "command": "npx",
128
+ "args": ["-y", "@kadoa/mcp"],
129
+ "env": {
130
+ "KADOA_API_KEY": "tk-your_api_key"
131
+ }
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ ## Get Your API Key
138
+
139
+ Get your API key from [kadoa.com/settings](https://kadoa.com/settings).
140
+
46
141
  ## Tools
47
142
 
48
143
  | Tool | Description |
@@ -119,10 +214,14 @@ delete_workflow for each, confirming before proceeding.
119
214
 
120
215
  ## Troubleshooting
121
216
 
217
+ **"No API key found"**
218
+ - Run `kadoa login` (requires `npm i -g @kadoa/cli`), or
219
+ - Set `KADOA_API_KEY` in your MCP config or environment
220
+ - API keys start with `tk-`
221
+
122
222
  **Claude says "I don't have access to Kadoa"**
123
223
  - Verify the MCP server is configured correctly
124
224
  - Restart your MCP client
125
- - Re-authenticate via OAuth if prompted
126
225
 
127
226
  ## Deploying the Remote Server
128
227
 
@@ -157,15 +256,24 @@ bun run build # Build for distribution
157
256
 
158
257
  To develop and test against a local Kadoa backend (instead of the production API), point the MCP at your local `public-api` service using the `KADOA_PUBLIC_API_URI` environment variable.
159
258
 
160
- **Prerequisites:** the `public-api` service must be running locally (default port `12380`).
259
+ **Prerequisites:** the `public-api` service must be running locally (default port `12380`). You also need a local API key — check your backend seed data or API key table.
161
260
 
162
261
  **Run the MCP server locally:**
163
262
 
164
263
  ```bash
165
- KADOA_PUBLIC_API_URI=http://localhost:12380 bun run dev
264
+ KADOA_PUBLIC_API_URI=http://localhost:12380 KADOA_API_KEY=tk-your_local_api_key bun src/index.ts
265
+ ```
266
+
267
+ **Add as a local MCP in Claude Code** (alongside the remote one):
268
+
269
+ ```bash
270
+ claude mcp add --transport stdio \
271
+ -e KADOA_PUBLIC_API_URI=http://localhost:12380 \
272
+ -e KADOA_API_KEY=tk-your_local_api_key \
273
+ kadoa-local -- bun /path/to/kadoa-mcp/src/index.ts
166
274
  ```
167
275
 
168
- The server starts in HTTP mode. You authenticate via OAuth the same way as with the remote server.
276
+ This registers a `kadoa-local` server that coexists with the production `kadoa` server, so you can use both without conflicts (`mcp__kadoa__*` for prod, `mcp__kadoa-local__*` for local).
169
277
 
170
278
  ## License
171
279
 
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  // @bun
3
3
  import { createRequire } from "node:module";
4
4
  var __create = Object.create;
@@ -28800,6 +28800,102 @@ var init_mcp = __esm(() => {
28800
28800
  };
28801
28801
  });
28802
28802
 
28803
+ // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
28804
+ class ReadBuffer {
28805
+ append(chunk) {
28806
+ this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
28807
+ }
28808
+ readMessage() {
28809
+ if (!this._buffer) {
28810
+ return null;
28811
+ }
28812
+ const index = this._buffer.indexOf(`
28813
+ `);
28814
+ if (index === -1) {
28815
+ return null;
28816
+ }
28817
+ const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
28818
+ this._buffer = this._buffer.subarray(index + 1);
28819
+ return deserializeMessage(line);
28820
+ }
28821
+ clear() {
28822
+ this._buffer = undefined;
28823
+ }
28824
+ }
28825
+ function deserializeMessage(line) {
28826
+ return JSONRPCMessageSchema.parse(JSON.parse(line));
28827
+ }
28828
+ function serializeMessage(message) {
28829
+ return JSON.stringify(message) + `
28830
+ `;
28831
+ }
28832
+ var init_stdio = __esm(() => {
28833
+ init_types2();
28834
+ });
28835
+
28836
+ // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
28837
+ import process3 from "node:process";
28838
+
28839
+ class StdioServerTransport {
28840
+ constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
28841
+ this._stdin = _stdin;
28842
+ this._stdout = _stdout;
28843
+ this._readBuffer = new ReadBuffer;
28844
+ this._started = false;
28845
+ this._ondata = (chunk) => {
28846
+ this._readBuffer.append(chunk);
28847
+ this.processReadBuffer();
28848
+ };
28849
+ this._onerror = (error48) => {
28850
+ this.onerror?.(error48);
28851
+ };
28852
+ }
28853
+ async start() {
28854
+ if (this._started) {
28855
+ throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");
28856
+ }
28857
+ this._started = true;
28858
+ this._stdin.on("data", this._ondata);
28859
+ this._stdin.on("error", this._onerror);
28860
+ }
28861
+ processReadBuffer() {
28862
+ while (true) {
28863
+ try {
28864
+ const message = this._readBuffer.readMessage();
28865
+ if (message === null) {
28866
+ break;
28867
+ }
28868
+ this.onmessage?.(message);
28869
+ } catch (error48) {
28870
+ this.onerror?.(error48);
28871
+ }
28872
+ }
28873
+ }
28874
+ async close() {
28875
+ this._stdin.off("data", this._ondata);
28876
+ this._stdin.off("error", this._onerror);
28877
+ const remainingDataListeners = this._stdin.listenerCount("data");
28878
+ if (remainingDataListeners === 0) {
28879
+ this._stdin.pause();
28880
+ }
28881
+ this._readBuffer.clear();
28882
+ this.onclose?.();
28883
+ }
28884
+ send(message) {
28885
+ return new Promise((resolve) => {
28886
+ const json2 = serializeMessage(message);
28887
+ if (this._stdout.write(json2)) {
28888
+ resolve();
28889
+ } else {
28890
+ this._stdout.once("drain", resolve);
28891
+ }
28892
+ });
28893
+ }
28894
+ }
28895
+ var init_stdio2 = __esm(() => {
28896
+ init_stdio();
28897
+ });
28898
+
28803
28899
  // node_modules/axios/lib/helpers/bind.js
28804
28900
  function bind(fn, thisArg) {
28805
28901
  return function wrap() {
@@ -48947,10 +49043,51 @@ var init_dist2 = __esm(() => {
48947
49043
  });
48948
49044
 
48949
49045
  // src/client.ts
49046
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
49047
+ import { dirname, join } from "node:path";
49048
+ import { homedir } from "node:os";
49049
+ function getConfigPath() {
49050
+ const home = process.env.HOME || homedir();
49051
+ return join(home, ".kadoa", "config.json");
49052
+ }
49053
+ function loadConfig() {
49054
+ const configFile = getConfigPath();
49055
+ if (!existsSync(configFile))
49056
+ return {};
49057
+ try {
49058
+ return JSON.parse(readFileSync(configFile, "utf-8"));
49059
+ } catch {
49060
+ return {};
49061
+ }
49062
+ }
49063
+ function loadApiKeyFromConfig() {
49064
+ return loadConfig().apiKey;
49065
+ }
49066
+ function resolveApiKey(apiKey) {
49067
+ const key = apiKey || process.env.KADOA_API_KEY || loadApiKeyFromConfig();
49068
+ if (!key) {
49069
+ throw new Error("No API key found. Set KADOA_API_KEY env var or run 'kadoa login' (npm i -g @kadoa/cli).");
49070
+ }
49071
+ return key;
49072
+ }
48950
49073
  function createKadoaClient(auth) {
48951
- const client = new KadoaClient({ bearerToken: auth.jwt });
49074
+ let client;
49075
+ let teamId;
49076
+ if (typeof auth === "object" && auth !== null) {
49077
+ if ("jwt" in auth) {
49078
+ client = new KadoaClient({ bearerToken: auth.jwt });
49079
+ teamId = auth.teamId;
49080
+ } else {
49081
+ client = new KadoaClient({ apiKey: auth.apiKey });
49082
+ }
49083
+ } else {
49084
+ client = new KadoaClient({ apiKey: resolveApiKey(auth) });
49085
+ }
48952
49086
  client.axiosInstance.interceptors.request.use((config2) => {
48953
49087
  config2.headers["x-kadoa-source"] = "mcp";
49088
+ if (teamId) {
49089
+ config2.headers["x-team-id"] = teamId;
49090
+ }
48954
49091
  return config2;
48955
49092
  });
48956
49093
  return client;
@@ -48962,6 +49099,41 @@ var init_client = __esm(() => {
48962
49099
  });
48963
49100
 
48964
49101
  // src/client.ts
49102
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
49103
+ import { dirname as dirname2, join as join2 } from "node:path";
49104
+ import { homedir as homedir2 } from "node:os";
49105
+ function getConfigPath2() {
49106
+ const home = process.env.HOME || homedir2();
49107
+ return join2(home, ".kadoa", "config.json");
49108
+ }
49109
+ function loadConfig2() {
49110
+ const configFile = getConfigPath2();
49111
+ if (!existsSync2(configFile))
49112
+ return {};
49113
+ try {
49114
+ return JSON.parse(readFileSync2(configFile, "utf-8"));
49115
+ } catch {
49116
+ return {};
49117
+ }
49118
+ }
49119
+ function saveConfig(config2) {
49120
+ const configFile = getConfigPath2();
49121
+ mkdirSync2(dirname2(configFile), { recursive: true });
49122
+ writeFileSync2(configFile, JSON.stringify(config2, null, 2), "utf-8");
49123
+ }
49124
+ function decodeJwtClaims(jwt2) {
49125
+ try {
49126
+ const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
49127
+ return {
49128
+ sub: payload.sub,
49129
+ email: payload.email,
49130
+ activeTeamId: payload.active_team_id ?? payload.app_metadata?.active_team_id,
49131
+ exp: payload.exp
49132
+ };
49133
+ } catch {
49134
+ return {};
49135
+ }
49136
+ }
48965
49137
  function isJwtExpired(jwt2) {
48966
49138
  try {
48967
49139
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
@@ -49000,6 +49172,15 @@ async function refreshSupabaseJwt(ctx) {
49000
49172
  ctx.supabaseJwt = data.access_token;
49001
49173
  ctx.supabaseRefreshToken = data.refresh_token;
49002
49174
  ctx.client.setBearerToken(data.access_token);
49175
+ try {
49176
+ await ctx.persist?.({
49177
+ supabaseJwt: ctx.supabaseJwt,
49178
+ supabaseRefreshToken: ctx.supabaseRefreshToken,
49179
+ teamId: ctx.teamId
49180
+ });
49181
+ } catch (e) {
49182
+ console.error("[JWT_REFRESH] WARN: persist failed, tokens updated in-memory only:", e);
49183
+ }
49003
49184
  console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${data.refresh_token.slice(0, 12)}...)`);
49004
49185
  return data.access_token;
49005
49186
  } catch (error48) {
@@ -49084,7 +49265,7 @@ function classifyError(error48) {
49084
49265
  if (httpError.httpStatus === 403) {
49085
49266
  return `Access denied${status}. Your current team role may not have permission for this action. Use the whoami tool to check your role, or contact your team admin to request elevated access.`;
49086
49267
  }
49087
- return `Authentication failed${status}. Please re-authenticate via OAuth.`;
49268
+ return `Authentication failed${status}. Your Kadoa API key may be invalid or expired. Please check your KADOA_API_KEY or re-authenticate.`;
49088
49269
  case "NOT_FOUND":
49089
49270
  return `Not found${status}. The workflow may have been deleted or the ID is incorrect.`;
49090
49271
  case "RATE_LIMITED":
@@ -49102,7 +49283,7 @@ function classifyError(error48) {
49102
49283
  }
49103
49284
  switch (code) {
49104
49285
  case "AUTH_ERROR":
49105
- return "Authentication failed. Please re-authenticate via OAuth.";
49286
+ return "Authentication failed. Your Kadoa API key may be invalid or expired. Please check your KADOA_API_KEY or re-authenticate.";
49106
49287
  case "NOT_FOUND":
49107
49288
  return "Not found. The workflow may have been deleted or the ID is incorrect.";
49108
49289
  case "RATE_LIMITED":
@@ -49128,6 +49309,13 @@ function registerTools(server, ctx) {
49128
49309
  return async (...args) => {
49129
49310
  try {
49130
49311
  await getValidJwt(ctx);
49312
+ if (ctx.teamId && ctx.supabaseJwt) {
49313
+ const claims = decodeJwtClaims(ctx.supabaseJwt);
49314
+ if (claims.activeTeamId && claims.activeTeamId !== ctx.teamId) {
49315
+ console.error(`[TEAM_SYNC] JWT active_team_id (${claims.activeTeamId}) != ctx.teamId (${ctx.teamId}), calling setActiveTeam`);
49316
+ await ctx.client.setActiveTeam(ctx.teamId);
49317
+ }
49318
+ }
49131
49319
  return await handler(...args);
49132
49320
  } catch (error48) {
49133
49321
  let message = classifyError(error48);
@@ -49135,7 +49323,8 @@ function registerTools(server, ctx) {
49135
49323
  try {
49136
49324
  const jwt2 = ctx.supabaseJwt;
49137
49325
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
49138
- const activeTeamId = ctx.teamId ?? teams[0]?.id;
49326
+ const config2 = loadConfig2();
49327
+ const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
49139
49328
  const activeTeam = teams.find((t) => t.id === activeTeamId);
49140
49329
  if (activeTeam?.adminEmail) {
49141
49330
  message += ` Your team admin is ${activeTeam.adminEmail}.`;
@@ -49154,11 +49343,13 @@ function registerTools(server, ctx) {
49154
49343
  }, withErrorHandling("whoami", async () => {
49155
49344
  const jwt2 = await getValidJwt(ctx);
49156
49345
  const user = await ctx.client.user.getCurrentUser();
49346
+ const authMethod = jwt2 ? "OAuth (JWT)" : "API Key";
49157
49347
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
49158
- const activeTeamId = ctx.teamId ?? teams[0]?.id;
49348
+ const config2 = loadConfig2();
49349
+ const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
49159
49350
  return jsonResult({
49160
49351
  email: user.email,
49161
- authMethod: "OAuth",
49352
+ authMethod,
49162
49353
  teams: teams.map((t) => ({
49163
49354
  name: t.name,
49164
49355
  memberRole: t.memberRole,
@@ -49802,7 +49993,8 @@ function registerTools(server, ctx) {
49802
49993
  }, withErrorHandling("team_list", async () => {
49803
49994
  const jwt2 = await getValidJwt(ctx);
49804
49995
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
49805
- const activeTeamId = ctx.teamId ?? teams[0]?.id;
49996
+ const config2 = loadConfig2();
49997
+ const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
49806
49998
  return jsonResult({
49807
49999
  teams: teams.map((t) => ({
49808
50000
  id: t.id,
@@ -49822,6 +50014,9 @@ function registerTools(server, ctx) {
49822
50014
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
49823
50015
  }, withErrorHandling("team_switch", async (args) => {
49824
50016
  const jwt2 = await getValidJwt(ctx);
50017
+ if (!jwt2) {
50018
+ return errorResult("Team switching requires OAuth authentication (HTTP mode). " + "In stdio mode, re-run with a different KADOA_API_KEY or use 'kadoa login' to authenticate via the CLI.");
50019
+ }
49825
50020
  const teams = await ctx.client.listTeams({ bearerToken: jwt2 });
49826
50021
  const identifier = args.teamIdentifier;
49827
50022
  const match = teams.find((t) => t.id === identifier || t.name.toLowerCase() === identifier.toLowerCase());
@@ -49829,11 +50024,16 @@ function registerTools(server, ctx) {
49829
50024
  return errorResult(`Team not found: "${identifier}". Available teams: ${teams.map((t) => t.name).join(", ")}`);
49830
50025
  }
49831
50026
  await ctx.client.setActiveTeam(match.id);
49832
- const newJwt = await refreshSupabaseJwt(ctx);
49833
- if (!newJwt) {
49834
- return errorResult("Failed to refresh session after team switch");
49835
- }
49836
50027
  ctx.teamId = match.id;
50028
+ try {
50029
+ await ctx.persist?.({ teamId: match.id });
50030
+ } catch (e) {
50031
+ console.error("[TEAM_SWITCH] WARN: persist failed:", e);
50032
+ }
50033
+ const config2 = loadConfig2();
50034
+ config2.teamId = match.id;
50035
+ config2.teamName = match.name;
50036
+ saveConfig(config2);
49837
50037
  return jsonResult({
49838
50038
  success: true,
49839
50039
  teamId: match.id,
@@ -53895,7 +54095,11 @@ async function refreshSupabaseToken(supabaseRefreshToken, context) {
53895
54095
  function jwtClaims(jwt2) {
53896
54096
  try {
53897
54097
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
53898
- return { email: payload.email, sub: payload.sub };
54098
+ return {
54099
+ email: payload.email,
54100
+ sub: payload.sub,
54101
+ activeTeamId: payload.app_metadata?.active_team_id
54102
+ };
53899
54103
  } catch {
53900
54104
  return {};
53901
54105
  }
@@ -54202,20 +54406,22 @@ class KadoaOAuthProvider {
54202
54406
  } else {
54203
54407
  console.error(`[AUTH] REFRESH_WARN: using stale Supabase JWT as fallback (${context})`);
54204
54408
  }
54409
+ const freshClaims = jwtClaims(supabaseJwt);
54410
+ const teamId = freshClaims.activeTeamId ?? entry.teamId;
54205
54411
  const newAccessToken = randomToken();
54206
54412
  const newRefreshToken = randomToken();
54207
54413
  const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
54208
54414
  await this.store.set("access_tokens", newAccessToken, {
54209
54415
  supabaseJwt,
54210
54416
  supabaseRefreshToken,
54211
- teamId: entry.teamId,
54417
+ teamId,
54212
54418
  clientId: entry.clientId,
54213
54419
  expiresAt
54214
54420
  }, ACCESS_TOKEN_TTL);
54215
54421
  await this.store.set("refresh_tokens", newRefreshToken, {
54216
54422
  supabaseJwt,
54217
54423
  supabaseRefreshToken,
54218
- teamId: entry.teamId,
54424
+ teamId,
54219
54425
  clientId: entry.clientId
54220
54426
  }, 2592000);
54221
54427
  return {
@@ -54226,6 +54432,15 @@ class KadoaOAuthProvider {
54226
54432
  };
54227
54433
  }
54228
54434
  async verifyAccessToken(token) {
54435
+ if (token.startsWith("tk-")) {
54436
+ return {
54437
+ token,
54438
+ clientId: "direct-api-key",
54439
+ scopes: [],
54440
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
54441
+ extra: { apiKey: token }
54442
+ };
54443
+ }
54229
54444
  const entry = await this.store.get("access_tokens", token);
54230
54445
  if (!entry) {
54231
54446
  const sessionCount = await this.store.size("access_tokens");
@@ -54877,6 +55092,9 @@ function resolveAuth(req) {
54877
55092
  console.error("[AUTH_RESOLVE] FAIL: req.auth.extra is missing");
54878
55093
  return;
54879
55094
  }
55095
+ if (typeof extra.apiKey === "string" && extra.apiKey.startsWith("tk-")) {
55096
+ return { kind: "apiKey", apiKey: extra.apiKey };
55097
+ }
54880
55098
  if (typeof extra.supabaseJwt === "string") {
54881
55099
  const claims = jwtClaims2(extra.supabaseJwt);
54882
55100
  const userId = claims.sub;
@@ -54894,13 +55112,15 @@ function resolveAuth(req) {
54894
55112
  }
54895
55113
  }
54896
55114
  return {
55115
+ kind: "jwt",
54897
55116
  jwt: extra.supabaseJwt,
54898
55117
  refreshToken: extra.supabaseRefreshToken ?? "",
54899
55118
  teamId: extra.teamId ?? "",
54900
- userId
55119
+ userId,
55120
+ mcpToken: req.auth.token
54901
55121
  };
54902
55122
  }
54903
- console.error(`[AUTH_RESOLVE] FAIL: no supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
55123
+ console.error(`[AUTH_RESOLVE] FAIL: no apiKey or supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
54904
55124
  return;
54905
55125
  }
54906
55126
  async function startHttpServer(options) {
@@ -54950,17 +55170,37 @@ async function startHttpServer(options) {
54950
55170
  });
54951
55171
  return;
54952
55172
  }
54953
- const identity = `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...`;
55173
+ const identity = auth.kind === "jwt" ? `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...` : `apiKey:${auth.apiKey.slice(0, 12)}...`;
54954
55174
  try {
54955
55175
  console.error(`[MCP] POST method=${method} auth=${identity}`);
54956
55176
  const transport = new StreamableHTTPServerTransport({
54957
55177
  sessionIdGenerator: undefined
54958
55178
  });
54959
- const server = createServer({
55179
+ const server = auth.kind === "jwt" ? createServer({
54960
55180
  jwt: auth.jwt,
54961
55181
  refreshToken: auth.refreshToken,
54962
- teamId: auth.teamId
54963
- });
55182
+ teamId: auth.teamId,
55183
+ persist: async (state) => {
55184
+ const entry = await store.get("access_tokens", auth.mcpToken);
55185
+ if (!entry) {
55186
+ console.error(`[PERSIST] WARN: access token gone from store (token=${auth.mcpToken.slice(0, 12)}...)`);
55187
+ return;
55188
+ }
55189
+ const remainingMs = entry.expiresAt - Date.now();
55190
+ if (remainingMs <= 0) {
55191
+ console.error(`[PERSIST] WARN: access token already expired, skipping persist`);
55192
+ return;
55193
+ }
55194
+ const ttlSeconds = Math.ceil(remainingMs / 1000);
55195
+ await store.set("access_tokens", auth.mcpToken, {
55196
+ ...entry,
55197
+ supabaseJwt: state.supabaseJwt ?? entry.supabaseJwt,
55198
+ supabaseRefreshToken: state.supabaseRefreshToken ?? entry.supabaseRefreshToken,
55199
+ teamId: state.teamId ?? entry.teamId
55200
+ }, ttlSeconds);
55201
+ console.error(`[PERSIST] OK: updated access token in store (token=${auth.mcpToken.slice(0, 12)}..., team=${state.teamId ?? "unchanged"}, ttl=${ttlSeconds}s)`);
55202
+ }
55203
+ }) : createServer({ apiKey: auth.apiKey });
54964
55204
  await server.connect(transport);
54965
55205
  await transport.handleRequest(req, res, req.body);
54966
55206
  } catch (error48) {
@@ -55013,24 +55253,61 @@ var init_http2 = __esm(async () => {
55013
55253
 
55014
55254
  // src/index.ts
55015
55255
  function createServer(auth) {
55016
- const ctx = {
55017
- client: createKadoaClient({ jwt: auth.jwt }),
55018
- supabaseJwt: auth.jwt,
55019
- supabaseRefreshToken: auth.refreshToken,
55020
- teamId: auth.teamId
55021
- };
55256
+ let ctx;
55257
+ if (typeof auth === "object" && auth !== null && "jwt" in auth) {
55258
+ ctx = {
55259
+ client: createKadoaClient({ jwt: auth.jwt, teamId: auth.teamId }),
55260
+ supabaseJwt: auth.jwt,
55261
+ supabaseRefreshToken: auth.refreshToken,
55262
+ teamId: auth.teamId,
55263
+ persist: auth.persist
55264
+ };
55265
+ } else if (typeof auth === "object" && auth !== null && "apiKey" in auth) {
55266
+ ctx = {
55267
+ client: createKadoaClient({ apiKey: auth.apiKey })
55268
+ };
55269
+ } else {
55270
+ ctx = {
55271
+ client: createKadoaClient(auth)
55272
+ };
55273
+ }
55022
55274
  const server = new McpServer({ name: "kadoa", version: "0.3.2" });
55023
55275
  registerTools(server, ctx);
55024
55276
  server.server.onerror = (error48) => console.error("[MCP Error]", error48);
55025
55277
  return server;
55026
55278
  }
55279
+ async function validateApiKey() {
55280
+ const client = createKadoaClient();
55281
+ try {
55282
+ await client.workflow.list({ limit: 1 });
55283
+ } catch (error48) {
55284
+ if (KadoaSdkException.isInstance(error48) && error48.code === "AUTH_ERROR") {
55285
+ console.error("Kadoa MCP: Invalid API key. Check KADOA_API_KEY or run 'kadoa login'.");
55286
+ process.exit(1);
55287
+ }
55288
+ }
55289
+ }
55027
55290
  var init_src = __esm(async () => {
55028
55291
  init_mcp();
55292
+ init_stdio2();
55029
55293
  init_client();
55030
55294
  init_tools();
55031
55295
  if (!process.env.VITEST && !process.env.BUN_TEST) {
55032
- const { startHttpServer: startHttpServer2 } = await init_http2().then(() => exports_http);
55033
- await startHttpServer2();
55296
+ const httpMode = process.argv.includes("--http") || process.env.MCP_HTTP === "1";
55297
+ if (httpMode) {
55298
+ const { startHttpServer: startHttpServer2 } = await init_http2().then(() => exports_http);
55299
+ await startHttpServer2();
55300
+ } else {
55301
+ await validateApiKey();
55302
+ const server = createServer();
55303
+ const transport = new StdioServerTransport;
55304
+ await server.connect(transport);
55305
+ console.error("Kadoa MCP Server started");
55306
+ process.on("SIGINT", async () => {
55307
+ await server.close();
55308
+ process.exit(0);
55309
+ });
55310
+ }
55034
55311
  }
55035
55312
  });
55036
55313
  await init_src();
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.3.7-rc.2",
3
+ "version": "0.3.7-rc.4",
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",
7
+ "bin": {
8
+ "kadoa-mcp": "dist/index.js"
9
+ },
7
10
  "publishConfig": {
8
11
  "access": "public"
9
12
  },
@@ -15,7 +18,8 @@
15
18
  "lint": "bunx biome check",
16
19
  "lint:fix": "bunx biome check --write",
17
20
  "dev": "bun src/index.ts",
18
- "build": "bun build src/index.ts --outdir=dist --target=node --external express --external ioredis",
21
+ "dev:http": "MCP_HTTP=1 bun src/index.ts",
22
+ "build": "bun build src/index.ts --outdir=dist --target=node --external express --external ioredis && node -e \"const f='dist/index.js';require('fs').writeFileSync(f,require('fs').readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\"",
19
23
  "check-types": "tsc --noEmit",
20
24
  "test": "BUN_TEST=1 bun test",
21
25
  "test:unit": "BUN_TEST=1 bun test tests/unit --timeout=120000",