@kadoa/mcp 0.3.7 → 0.3.9-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.
Files changed (3) hide show
  1. package/README.md +6 -114
  2. package/dist/index.js +156 -310
  3. package/package.json +2 -6
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, no API key config. 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 needed. 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 (Remote)
27
+ ### Cursor
28
28
 
29
29
  Add to `.cursor/mcp.json`:
30
30
 
@@ -43,101 +43,6 @@ 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
-
141
46
  ## Tools
142
47
 
143
48
  | Tool | Description |
@@ -214,14 +119,10 @@ delete_workflow for each, confirming before proceeding.
214
119
 
215
120
  ## Troubleshooting
216
121
 
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
-
222
122
  **Claude says "I don't have access to Kadoa"**
223
123
  - Verify the MCP server is configured correctly
224
124
  - Restart your MCP client
125
+ - Re-authenticate via OAuth if prompted
225
126
 
226
127
  ## Deploying the Remote Server
227
128
 
@@ -256,24 +157,15 @@ bun run build # Build for distribution
256
157
 
257
158
  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.
258
159
 
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.
160
+ **Prerequisites:** the `public-api` service must be running locally (default port `12380`).
260
161
 
261
162
  **Run the MCP server locally:**
262
163
 
263
164
  ```bash
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
165
+ KADOA_PUBLIC_API_URI=http://localhost:12380 bun run dev
274
166
  ```
275
167
 
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).
168
+ The server starts in HTTP mode. You authenticate via OAuth the same way as with the remote server.
277
169
 
278
170
  ## License
279
171
 
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  // @bun
3
3
  import { createRequire } from "node:module";
4
4
  var __create = Object.create;
@@ -28800,102 +28800,6 @@ 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
-
28899
28803
  // node_modules/axios/lib/helpers/bind.js
28900
28804
  function bind(fn, thisArg) {
28901
28805
  return function wrap() {
@@ -49043,83 +48947,47 @@ var init_dist2 = __esm(() => {
49043
48947
  });
49044
48948
 
49045
48949
  // 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
- }
49073
48950
  function createKadoaClient(auth) {
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
- }
48951
+ const client = new KadoaClient({ bearerToken: auth.jwt });
49086
48952
  client.axiosInstance.interceptors.request.use((config2) => {
49087
48953
  config2.headers["x-kadoa-source"] = "mcp";
49088
- if (teamId) {
49089
- config2.headers["x-team-id"] = teamId;
48954
+ if (auth.teamId) {
48955
+ config2.headers["x-team-id"] = auth.teamId;
49090
48956
  }
49091
48957
  return config2;
49092
48958
  });
49093
48959
  return client;
49094
48960
  }
49095
- var ctxRefreshMutex;
48961
+ var refreshRawMutex, ctxRefreshMutex;
49096
48962
  var init_client = __esm(() => {
49097
48963
  init_dist2();
48964
+ refreshRawMutex = new Map;
49098
48965
  ctxRefreshMutex = new WeakMap;
49099
48966
  });
49100
48967
 
49101
48968
  // 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");
48969
+ var exports_client = {};
48970
+ __export(exports_client, {
48971
+ refreshSupabaseJwtRaw: () => refreshSupabaseJwtRaw,
48972
+ refreshSupabaseJwt: () => refreshSupabaseJwt,
48973
+ isJwtExpired: () => isJwtExpired,
48974
+ getValidJwt: () => getValidJwt,
48975
+ decodeJwtClaims: () => decodeJwtClaims,
48976
+ createKadoaClient: () => createKadoaClient2,
48977
+ SessionExpiredError: () => SessionExpiredError,
48978
+ KadoaSdkException: () => KadoaSdkException,
48979
+ KadoaClient: () => KadoaClient
48980
+ });
48981
+ function createKadoaClient2(auth) {
48982
+ const client = new KadoaClient({ bearerToken: auth.jwt });
48983
+ client.axiosInstance.interceptors.request.use((config2) => {
48984
+ config2.headers["x-kadoa-source"] = "mcp";
48985
+ if (auth.teamId) {
48986
+ config2.headers["x-team-id"] = auth.teamId;
48987
+ }
48988
+ return config2;
48989
+ });
48990
+ return client;
49123
48991
  }
49124
48992
  function decodeJwtClaims(jwt2) {
49125
48993
  try {
@@ -49142,36 +49010,57 @@ function isJwtExpired(jwt2) {
49142
49010
  return true;
49143
49011
  }
49144
49012
  }
49145
- async function refreshSupabaseJwt(ctx) {
49146
- if (!ctx.supabaseRefreshToken) {
49147
- console.error("[JWT_REFRESH] No refresh token available, cannot refresh");
49148
- return;
49013
+ async function refreshSupabaseJwtRaw(supabaseRefreshToken) {
49014
+ const inflight = refreshRawMutex2.get(supabaseRefreshToken);
49015
+ if (inflight) {
49016
+ console.error(`[JWT_REFRESH] DEDUP: reusing in-flight raw refresh`);
49017
+ return inflight;
49149
49018
  }
49019
+ const promise3 = _doRefreshRaw(supabaseRefreshToken).finally(() => {
49020
+ refreshRawMutex2.delete(supabaseRefreshToken);
49021
+ });
49022
+ refreshRawMutex2.set(supabaseRefreshToken, promise3);
49023
+ return promise3;
49024
+ }
49025
+ async function _doRefreshRaw(supabaseRefreshToken) {
49150
49026
  const supabaseUrl = process.env.SUPABASE_URL;
49151
49027
  if (!supabaseUrl) {
49152
49028
  console.error("[JWT_REFRESH] SUPABASE_URL not set, cannot refresh");
49029
+ return null;
49030
+ }
49031
+ const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
49032
+ method: "POST",
49033
+ headers: {
49034
+ "Content-Type": "application/json",
49035
+ apikey: process.env.SUPABASE_ANON_KEY
49036
+ },
49037
+ body: JSON.stringify({ refresh_token: supabaseRefreshToken })
49038
+ });
49039
+ if (res.ok) {
49040
+ const data = await res.json();
49041
+ return { jwt: data.access_token, refreshToken: data.refresh_token };
49042
+ }
49043
+ const body = await res.text().catch(() => "");
49044
+ console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status} (refreshToken=${supabaseRefreshToken.slice(0, 12)}...): ${body}`);
49045
+ if (body.includes("session_expired") || body.includes("refresh_token_not_found") || body.includes("refresh_token_already_used")) {
49046
+ throw new SessionExpiredError("Your Kadoa session has expired due to inactivity. Please reconnect to re-authenticate.");
49047
+ }
49048
+ return null;
49049
+ }
49050
+ async function refreshSupabaseJwt(ctx) {
49051
+ if (!ctx.supabaseRefreshToken) {
49052
+ console.error("[JWT_REFRESH] No refresh token available, cannot refresh");
49153
49053
  return;
49154
49054
  }
49155
49055
  try {
49156
49056
  const refreshToken = ctx.supabaseRefreshToken;
49157
49057
  console.error(`[JWT_REFRESH] Refreshing Supabase JWT (refreshToken=${refreshToken.slice(0, 12)}..., team=${ctx.teamId ?? "unknown"})`);
49158
- const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
49159
- method: "POST",
49160
- headers: {
49161
- "Content-Type": "application/json",
49162
- apikey: process.env.SUPABASE_ANON_KEY
49163
- },
49164
- body: JSON.stringify({ refresh_token: refreshToken })
49165
- });
49166
- if (!res.ok) {
49167
- const body = await res.text().catch(() => "");
49168
- console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status} (refreshToken=${refreshToken.slice(0, 12)}...): ${body}`);
49058
+ const result = await refreshSupabaseJwtRaw(refreshToken);
49059
+ if (!result)
49169
49060
  return;
49170
- }
49171
- const data = await res.json();
49172
- ctx.supabaseJwt = data.access_token;
49173
- ctx.supabaseRefreshToken = data.refresh_token;
49174
- ctx.client.setBearerToken(data.access_token);
49061
+ ctx.supabaseJwt = result.jwt;
49062
+ ctx.supabaseRefreshToken = result.refreshToken;
49063
+ ctx.client.setBearerToken(result.jwt);
49175
49064
  try {
49176
49065
  await ctx.persist?.({
49177
49066
  supabaseJwt: ctx.supabaseJwt,
@@ -49181,9 +49070,11 @@ async function refreshSupabaseJwt(ctx) {
49181
49070
  } catch (e) {
49182
49071
  console.error("[JWT_REFRESH] WARN: persist failed, tokens updated in-memory only:", e);
49183
49072
  }
49184
- console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${data.refresh_token.slice(0, 12)}...)`);
49185
- return data.access_token;
49073
+ console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${result.refreshToken.slice(0, 12)}...)`);
49074
+ return result.jwt;
49186
49075
  } catch (error48) {
49076
+ if (error48 instanceof SessionExpiredError)
49077
+ throw error48;
49187
49078
  console.error("[JWT_REFRESH] FAIL: threw", error48);
49188
49079
  return;
49189
49080
  }
@@ -49204,9 +49095,16 @@ async function getValidJwt(ctx) {
49204
49095
  ctxRefreshMutex2.set(ctx, promise3);
49205
49096
  return promise3;
49206
49097
  }
49207
- var ctxRefreshMutex2;
49098
+ var SessionExpiredError, refreshRawMutex2, ctxRefreshMutex2;
49208
49099
  var init_client2 = __esm(() => {
49209
49100
  init_dist2();
49101
+ SessionExpiredError = class SessionExpiredError extends Error {
49102
+ constructor(message) {
49103
+ super(message ?? "Supabase session expired. Please re-authenticate.");
49104
+ this.name = "SessionExpiredError";
49105
+ }
49106
+ };
49107
+ refreshRawMutex2 = new Map;
49210
49108
  ctxRefreshMutex2 = new WeakMap;
49211
49109
  });
49212
49110
 
@@ -49265,7 +49163,7 @@ function classifyError(error48) {
49265
49163
  if (httpError.httpStatus === 403) {
49266
49164
  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.`;
49267
49165
  }
49268
- return `Authentication failed${status}. Your Kadoa API key may be invalid or expired. Please check your KADOA_API_KEY or re-authenticate.`;
49166
+ return `Authentication failed${status}. Please re-authenticate via OAuth.`;
49269
49167
  case "NOT_FOUND":
49270
49168
  return `Not found${status}. The workflow may have been deleted or the ID is incorrect.`;
49271
49169
  case "RATE_LIMITED":
@@ -49283,7 +49181,7 @@ function classifyError(error48) {
49283
49181
  }
49284
49182
  switch (code) {
49285
49183
  case "AUTH_ERROR":
49286
- return "Authentication failed. Your Kadoa API key may be invalid or expired. Please check your KADOA_API_KEY or re-authenticate.";
49184
+ return "Authentication failed. Please re-authenticate via OAuth.";
49287
49185
  case "NOT_FOUND":
49288
49186
  return "Not found. The workflow may have been deleted or the ID is incorrect.";
49289
49187
  case "RATE_LIMITED":
@@ -49324,13 +49222,16 @@ function registerTools(server, ctx) {
49324
49222
  }
49325
49223
  return await handler(...args);
49326
49224
  } catch (error48) {
49225
+ if (error48 instanceof SessionExpiredError) {
49226
+ console.error(`[Tool Error] ${name}: session expired, user must re-authenticate`);
49227
+ return errorResult("Your session has expired. Please reconnect the MCP server to re-authenticate.");
49228
+ }
49327
49229
  let message = classifyError(error48);
49328
49230
  if (KadoaHttpException.isInstance(error48) && error48.httpStatus === 403) {
49329
49231
  try {
49330
49232
  const jwt2 = ctx.supabaseJwt;
49331
49233
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
49332
- const config2 = loadConfig2();
49333
- const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
49234
+ const activeTeamId = ctx.teamId ?? teams[0]?.id;
49334
49235
  const activeTeam = teams.find((t) => t.id === activeTeamId);
49335
49236
  if (activeTeam?.adminEmail) {
49336
49237
  message += ` Your team admin is ${activeTeam.adminEmail}.`;
@@ -49349,13 +49250,11 @@ function registerTools(server, ctx) {
49349
49250
  }, withErrorHandling("whoami", async () => {
49350
49251
  const jwt2 = await getValidJwt(ctx);
49351
49252
  const user = await ctx.client.user.getCurrentUser();
49352
- const authMethod = jwt2 ? "OAuth (JWT)" : "API Key";
49353
49253
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
49354
- const config2 = loadConfig2();
49355
- const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
49254
+ const activeTeamId = ctx.teamId ?? teams[0]?.id;
49356
49255
  return jsonResult({
49357
49256
  email: user.email,
49358
- authMethod,
49257
+ authMethod: "OAuth",
49359
49258
  teams: teams.map((t) => ({
49360
49259
  name: t.name,
49361
49260
  memberRole: t.memberRole,
@@ -49999,8 +49898,7 @@ function registerTools(server, ctx) {
49999
49898
  }, withErrorHandling("team_list", async () => {
50000
49899
  const jwt2 = await getValidJwt(ctx);
50001
49900
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
50002
- const config2 = loadConfig2();
50003
- const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
49901
+ const activeTeamId = ctx.teamId ?? teams[0]?.id;
50004
49902
  return jsonResult({
50005
49903
  teams: teams.map((t) => ({
50006
49904
  id: t.id,
@@ -50020,9 +49918,6 @@ function registerTools(server, ctx) {
50020
49918
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
50021
49919
  }, withErrorHandling("team_switch", async (args) => {
50022
49920
  const jwt2 = await getValidJwt(ctx);
50023
- if (!jwt2) {
50024
- 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.");
50025
- }
50026
49921
  const teams = await ctx.client.listTeams({ bearerToken: jwt2 });
50027
49922
  const identifier = args.teamIdentifier;
50028
49923
  const match = teams.find((t) => t.id === identifier || t.name.toLowerCase() === identifier.toLowerCase());
@@ -50036,10 +49931,6 @@ function registerTools(server, ctx) {
50036
49931
  } catch (e) {
50037
49932
  console.error("[TEAM_SWITCH] WARN: persist failed:", e);
50038
49933
  }
50039
- const config2 = loadConfig2();
50040
- config2.teamId = match.id;
50041
- config2.teamName = match.name;
50042
- saveConfig(config2);
50043
49934
  return jsonResult({
50044
49935
  success: true,
50045
49936
  teamId: match.id,
@@ -54059,45 +53950,6 @@ function generatePKCE() {
54059
53950
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
54060
53951
  return { verifier, challenge };
54061
53952
  }
54062
- async function refreshSupabaseToken(supabaseRefreshToken, context) {
54063
- const inflight = supabaseRefreshMutex.get(supabaseRefreshToken);
54064
- if (inflight) {
54065
- console.error(`[AUTH] REFRESH_DEDUP: reusing in-flight refresh (${context})`);
54066
- return inflight;
54067
- }
54068
- const promise3 = (async () => {
54069
- const supabaseUrl = process.env.SUPABASE_URL;
54070
- if (!supabaseUrl || !supabaseRefreshToken)
54071
- return null;
54072
- try {
54073
- const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
54074
- method: "POST",
54075
- headers: {
54076
- "Content-Type": "application/json",
54077
- apikey: process.env.SUPABASE_ANON_KEY
54078
- },
54079
- body: JSON.stringify({ refresh_token: supabaseRefreshToken })
54080
- });
54081
- if (res.ok) {
54082
- const data = await res.json();
54083
- const newClaims = jwtClaims(data.access_token);
54084
- console.log(`[AUTH] REFRESH_OK: Supabase JWT refreshed (${context}, newEmail=${newClaims.email})`);
54085
- return { jwt: data.access_token, refreshToken: data.refresh_token };
54086
- }
54087
- const body = await res.text().catch(() => "");
54088
- console.error(`[AUTH] REFRESH_FAIL: Supabase returned ${res.status} (${context}): ${body.slice(0, 200)}`);
54089
- return null;
54090
- } catch (err) {
54091
- console.error(`[AUTH] REFRESH_FAIL: Supabase refresh threw (${context}):`, err);
54092
- return null;
54093
- }
54094
- })();
54095
- supabaseRefreshMutex.set(supabaseRefreshToken, promise3);
54096
- promise3.finally(() => {
54097
- supabaseRefreshMutex.delete(supabaseRefreshToken);
54098
- });
54099
- return promise3;
54100
- }
54101
53953
  function jwtClaims(jwt2) {
54102
53954
  try {
54103
53955
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
@@ -54405,12 +54257,22 @@ class KadoaOAuthProvider {
54405
54257
  let { supabaseJwt, supabaseRefreshToken } = entry;
54406
54258
  const claims = jwtClaims(entry.supabaseJwt);
54407
54259
  const context = `email=${claims.email}, team=${entry.teamId}`;
54408
- const refreshed = await refreshSupabaseToken(supabaseRefreshToken, context);
54409
- if (refreshed) {
54410
- supabaseJwt = refreshed.jwt;
54411
- supabaseRefreshToken = refreshed.refreshToken;
54412
- } else {
54413
- console.error(`[AUTH] REFRESH_WARN: using stale Supabase JWT as fallback (${context})`);
54260
+ try {
54261
+ const { refreshSupabaseJwtRaw: refreshSupabaseJwtRaw2, SessionExpiredError: SessionExpiredError2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
54262
+ const refreshed = await refreshSupabaseJwtRaw2(supabaseRefreshToken);
54263
+ if (refreshed) {
54264
+ supabaseJwt = refreshed.jwt;
54265
+ supabaseRefreshToken = refreshed.refreshToken;
54266
+ console.error(`[AUTH] REFRESH_OK: Supabase JWT refreshed (${context})`);
54267
+ } else {
54268
+ console.error(`[AUTH] REFRESH_WARN: using stale Supabase JWT as fallback (${context})`);
54269
+ }
54270
+ } catch (error48) {
54271
+ if (error48 instanceof Error && error48.name === "SessionExpiredError") {
54272
+ console.error(`[AUTH] REFRESH_DEAD: session permanently expired (${context}): ${error48.message}`);
54273
+ throw new InvalidTokenError("Supabase session expired. Please re-authenticate.");
54274
+ }
54275
+ console.error(`[AUTH] REFRESH_WARN: unexpected error, using stale JWT (${context}):`, error48);
54414
54276
  }
54415
54277
  const freshClaims = jwtClaims(supabaseJwt);
54416
54278
  const teamId = freshClaims.activeTeamId ?? entry.teamId;
@@ -54438,15 +54300,6 @@ class KadoaOAuthProvider {
54438
54300
  };
54439
54301
  }
54440
54302
  async verifyAccessToken(token) {
54441
- if (token.startsWith("tk-")) {
54442
- return {
54443
- token,
54444
- clientId: "direct-api-key",
54445
- scopes: [],
54446
- expiresAt: Math.floor(Date.now() / 1000) + 3600,
54447
- extra: { apiKey: token }
54448
- };
54449
- }
54450
54303
  const entry = await this.store.get("access_tokens", token);
54451
54304
  if (!entry) {
54452
54305
  const sessionCount = await this.store.size("access_tokens");
@@ -54546,7 +54399,7 @@ class KadoaOAuthProvider {
54546
54399
  codeChallenge: pending.params.codeChallenge,
54547
54400
  clientId: pending.client.client_id,
54548
54401
  redirectUri: pending.params.redirectUri,
54549
- expiresAt: Date.now() + 10 * 60 * 1000
54402
+ expiresAt: Date.now() + 600000
54550
54403
  }, 600);
54551
54404
  const redirectUrl = new URL(pending.params.redirectUri);
54552
54405
  redirectUrl.searchParams.set("code", mcpCode);
@@ -54975,12 +54828,11 @@ function renderLoginPage(state, error48) {
54975
54828
  function escapeHtml(str) {
54976
54829
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
54977
54830
  }
54978
- var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL, supabaseRefreshMutex;
54831
+ var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
54979
54832
  var init_auth2 = __esm(() => {
54980
54833
  init_errors4();
54981
54834
  TEAM_SELECTION_TTL = 10 * 60 * 1000;
54982
54835
  ACCESS_TOKEN_TTL = 7 * 24 * 3600;
54983
- supabaseRefreshMutex = new Map;
54984
54836
  });
54985
54837
 
54986
54838
  // src/redis-store.ts
@@ -55098,9 +54950,6 @@ function resolveAuth(req) {
55098
54950
  console.error("[AUTH_RESOLVE] FAIL: req.auth.extra is missing");
55099
54951
  return;
55100
54952
  }
55101
- if (typeof extra.apiKey === "string" && extra.apiKey.startsWith("tk-")) {
55102
- return { kind: "apiKey", apiKey: extra.apiKey };
55103
- }
55104
54953
  if (typeof extra.supabaseJwt === "string") {
55105
54954
  const claims = jwtClaims2(extra.supabaseJwt);
55106
54955
  const userId = claims.sub;
@@ -55118,7 +54967,6 @@ function resolveAuth(req) {
55118
54967
  }
55119
54968
  }
55120
54969
  return {
55121
- kind: "jwt",
55122
54970
  jwt: extra.supabaseJwt,
55123
54971
  refreshToken: extra.supabaseRefreshToken ?? "",
55124
54972
  teamId: extra.teamId ?? "",
@@ -55126,7 +54974,7 @@ function resolveAuth(req) {
55126
54974
  mcpToken: req.auth.token
55127
54975
  };
55128
54976
  }
55129
- console.error(`[AUTH_RESOLVE] FAIL: no apiKey or supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
54977
+ console.error(`[AUTH_RESOLVE] FAIL: no supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
55130
54978
  return;
55131
54979
  }
55132
54980
  async function startHttpServer(options) {
@@ -55176,13 +55024,46 @@ async function startHttpServer(options) {
55176
55024
  });
55177
55025
  return;
55178
55026
  }
55179
- const identity = auth.kind === "jwt" ? `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...` : `apiKey:${auth.apiKey.slice(0, 12)}...`;
55027
+ const identity = `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...`;
55028
+ if (isJwtExpired(auth.jwt)) {
55029
+ try {
55030
+ const refreshed = await refreshSupabaseJwtRaw(auth.refreshToken);
55031
+ if (refreshed) {
55032
+ auth.jwt = refreshed.jwt;
55033
+ auth.refreshToken = refreshed.refreshToken;
55034
+ const entry = await store.get("access_tokens", auth.mcpToken);
55035
+ if (entry) {
55036
+ const remainingMs = entry.expiresAt - Date.now();
55037
+ if (remainingMs > 0) {
55038
+ await store.set("access_tokens", auth.mcpToken, {
55039
+ ...entry,
55040
+ supabaseJwt: refreshed.jwt,
55041
+ supabaseRefreshToken: refreshed.refreshToken
55042
+ }, Math.ceil(remainingMs / 1000));
55043
+ console.error(`[PROACTIVE_REFRESH] OK: JWT refreshed on ${method} (${identity})`);
55044
+ }
55045
+ }
55046
+ }
55047
+ } catch (error48) {
55048
+ if (error48 instanceof SessionExpiredError) {
55049
+ console.error(`[PROACTIVE_REFRESH] Session dead on ${method} (${identity}): ${error48.message}`);
55050
+ await store.del("access_tokens", auth.mcpToken);
55051
+ res.status(401).json({
55052
+ jsonrpc: "2.0",
55053
+ error: { code: -32001, message: error48.message },
55054
+ id: req.body?.id ?? null
55055
+ });
55056
+ return;
55057
+ }
55058
+ console.error(`[PROACTIVE_REFRESH] WARN: refresh failed on ${method}, continuing with stale JWT`, error48);
55059
+ }
55060
+ }
55180
55061
  try {
55181
55062
  console.error(`[MCP] POST method=${method} auth=${identity}`);
55182
55063
  const transport = new StreamableHTTPServerTransport({
55183
55064
  sessionIdGenerator: undefined
55184
55065
  });
55185
- const server = auth.kind === "jwt" ? createServer({
55066
+ const server = createServer({
55186
55067
  jwt: auth.jwt,
55187
55068
  refreshToken: auth.refreshToken,
55188
55069
  teamId: auth.teamId,
@@ -55206,7 +55087,7 @@ async function startHttpServer(options) {
55206
55087
  }, ttlSeconds);
55207
55088
  console.error(`[PERSIST] OK: updated access token in store (token=${auth.mcpToken.slice(0, 12)}..., team=${state.teamId ?? "unchanged"}, ttl=${ttlSeconds}s)`);
55208
55089
  }
55209
- }) : createServer({ apiKey: auth.apiKey });
55090
+ });
55210
55091
  await server.connect(transport);
55211
55092
  await transport.handleRequest(req, res, req.body);
55212
55093
  } catch (error48) {
@@ -55254,66 +55135,31 @@ var init_http2 = __esm(async () => {
55254
55135
  init_bearerAuth();
55255
55136
  init_auth2();
55256
55137
  init_redis_store();
55138
+ init_client2();
55257
55139
  await init_src();
55258
55140
  });
55259
55141
 
55260
55142
  // src/index.ts
55261
55143
  function createServer(auth) {
55262
- let ctx;
55263
- if (typeof auth === "object" && auth !== null && "jwt" in auth) {
55264
- ctx = {
55265
- client: createKadoaClient({ jwt: auth.jwt, teamId: auth.teamId }),
55266
- supabaseJwt: auth.jwt,
55267
- supabaseRefreshToken: auth.refreshToken,
55268
- teamId: auth.teamId,
55269
- persist: auth.persist
55270
- };
55271
- } else if (typeof auth === "object" && auth !== null && "apiKey" in auth) {
55272
- ctx = {
55273
- client: createKadoaClient({ apiKey: auth.apiKey })
55274
- };
55275
- } else {
55276
- ctx = {
55277
- client: createKadoaClient(auth)
55278
- };
55279
- }
55144
+ const ctx = {
55145
+ client: createKadoaClient({ jwt: auth.jwt, teamId: auth.teamId }),
55146
+ supabaseJwt: auth.jwt,
55147
+ supabaseRefreshToken: auth.refreshToken,
55148
+ teamId: auth.teamId,
55149
+ persist: auth.persist
55150
+ };
55280
55151
  const server = new McpServer({ name: "kadoa", version: "0.3.2" });
55281
55152
  registerTools(server, ctx);
55282
55153
  server.server.onerror = (error48) => console.error("[MCP Error]", error48);
55283
55154
  return server;
55284
55155
  }
55285
- async function validateApiKey() {
55286
- const client = createKadoaClient();
55287
- try {
55288
- await client.workflow.list({ limit: 1 });
55289
- } catch (error48) {
55290
- if (KadoaSdkException.isInstance(error48) && error48.code === "AUTH_ERROR") {
55291
- console.error("Kadoa MCP: Invalid API key. Check KADOA_API_KEY or run 'kadoa login'.");
55292
- process.exit(1);
55293
- }
55294
- }
55295
- }
55296
55156
  var init_src = __esm(async () => {
55297
55157
  init_mcp();
55298
- init_stdio2();
55299
55158
  init_client();
55300
55159
  init_tools();
55301
55160
  if (!process.env.VITEST && !process.env.BUN_TEST) {
55302
- const httpMode = process.argv.includes("--http") || process.env.MCP_HTTP === "1";
55303
- if (httpMode) {
55304
- const { startHttpServer: startHttpServer2 } = await init_http2().then(() => exports_http);
55305
- await startHttpServer2();
55306
- } else {
55307
- await validateApiKey();
55308
- const server = createServer();
55309
- const transport = new StdioServerTransport;
55310
- await server.connect(transport);
55311
- console.error("Kadoa MCP Server started");
55312
- process.on("SIGINT", async () => {
55313
- await server.close();
55314
- process.exit(0);
55315
- });
55316
- }
55161
+ const { startHttpServer: startHttpServer2 } = await init_http2().then(() => exports_http);
55162
+ await startHttpServer2();
55317
55163
  }
55318
55164
  });
55319
55165
  await init_src();
package/package.json CHANGED
@@ -1,12 +1,9 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.3.7",
3
+ "version": "0.3.9-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",
7
- "bin": {
8
- "kadoa-mcp": "dist/index.js"
9
- },
10
7
  "publishConfig": {
11
8
  "access": "public"
12
9
  },
@@ -18,8 +15,7 @@
18
15
  "lint": "bunx biome check",
19
16
  "lint:fix": "bunx biome check --write",
20
17
  "dev": "bun src/index.ts",
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'))\"",
18
+ "build": "bun build src/index.ts --outdir=dist --target=node --external express --external ioredis",
23
19
  "check-types": "tsc --noEmit",
24
20
  "test": "BUN_TEST=1 bun test",
25
21
  "test:unit": "BUN_TEST=1 bun test tests/unit --timeout=120000",