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

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 +277 -26
  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,8 +49043,44 @@ 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
+ if (typeof auth === "object" && auth !== null) {
49076
+ if ("jwt" in auth) {
49077
+ client = new KadoaClient({ bearerToken: auth.jwt });
49078
+ } else {
49079
+ client = new KadoaClient({ apiKey: auth.apiKey });
49080
+ }
49081
+ } else {
49082
+ client = new KadoaClient({ apiKey: resolveApiKey(auth) });
49083
+ }
48952
49084
  client.axiosInstance.interceptors.request.use((config2) => {
48953
49085
  config2.headers["x-kadoa-source"] = "mcp";
48954
49086
  return config2;
@@ -48962,6 +49094,28 @@ var init_client = __esm(() => {
48962
49094
  });
48963
49095
 
48964
49096
  // src/client.ts
49097
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
49098
+ import { dirname as dirname2, join as join2 } from "node:path";
49099
+ import { homedir as homedir2 } from "node:os";
49100
+ function getConfigPath2() {
49101
+ const home = process.env.HOME || homedir2();
49102
+ return join2(home, ".kadoa", "config.json");
49103
+ }
49104
+ function loadConfig2() {
49105
+ const configFile = getConfigPath2();
49106
+ if (!existsSync2(configFile))
49107
+ return {};
49108
+ try {
49109
+ return JSON.parse(readFileSync2(configFile, "utf-8"));
49110
+ } catch {
49111
+ return {};
49112
+ }
49113
+ }
49114
+ function saveConfig(config2) {
49115
+ const configFile = getConfigPath2();
49116
+ mkdirSync2(dirname2(configFile), { recursive: true });
49117
+ writeFileSync2(configFile, JSON.stringify(config2, null, 2), "utf-8");
49118
+ }
48965
49119
  function isJwtExpired(jwt2) {
48966
49120
  try {
48967
49121
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
@@ -49000,6 +49154,15 @@ async function refreshSupabaseJwt(ctx) {
49000
49154
  ctx.supabaseJwt = data.access_token;
49001
49155
  ctx.supabaseRefreshToken = data.refresh_token;
49002
49156
  ctx.client.setBearerToken(data.access_token);
49157
+ try {
49158
+ await ctx.persist?.({
49159
+ supabaseJwt: ctx.supabaseJwt,
49160
+ supabaseRefreshToken: ctx.supabaseRefreshToken,
49161
+ teamId: ctx.teamId
49162
+ });
49163
+ } catch (e) {
49164
+ console.error("[JWT_REFRESH] WARN: persist failed, tokens updated in-memory only:", e);
49165
+ }
49003
49166
  console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${data.refresh_token.slice(0, 12)}...)`);
49004
49167
  return data.access_token;
49005
49168
  } catch (error48) {
@@ -49084,7 +49247,7 @@ function classifyError(error48) {
49084
49247
  if (httpError.httpStatus === 403) {
49085
49248
  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
49249
  }
49087
- return `Authentication failed${status}. Please re-authenticate via OAuth.`;
49250
+ return `Authentication failed${status}. Your Kadoa API key may be invalid or expired. Please check your KADOA_API_KEY or re-authenticate.`;
49088
49251
  case "NOT_FOUND":
49089
49252
  return `Not found${status}. The workflow may have been deleted or the ID is incorrect.`;
49090
49253
  case "RATE_LIMITED":
@@ -49102,7 +49265,7 @@ function classifyError(error48) {
49102
49265
  }
49103
49266
  switch (code) {
49104
49267
  case "AUTH_ERROR":
49105
- return "Authentication failed. Please re-authenticate via OAuth.";
49268
+ return "Authentication failed. Your Kadoa API key may be invalid or expired. Please check your KADOA_API_KEY or re-authenticate.";
49106
49269
  case "NOT_FOUND":
49107
49270
  return "Not found. The workflow may have been deleted or the ID is incorrect.";
49108
49271
  case "RATE_LIMITED":
@@ -49135,7 +49298,8 @@ function registerTools(server, ctx) {
49135
49298
  try {
49136
49299
  const jwt2 = ctx.supabaseJwt;
49137
49300
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
49138
- const activeTeamId = ctx.teamId ?? teams[0]?.id;
49301
+ const config2 = loadConfig2();
49302
+ const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
49139
49303
  const activeTeam = teams.find((t) => t.id === activeTeamId);
49140
49304
  if (activeTeam?.adminEmail) {
49141
49305
  message += ` Your team admin is ${activeTeam.adminEmail}.`;
@@ -49154,11 +49318,13 @@ function registerTools(server, ctx) {
49154
49318
  }, withErrorHandling("whoami", async () => {
49155
49319
  const jwt2 = await getValidJwt(ctx);
49156
49320
  const user = await ctx.client.user.getCurrentUser();
49321
+ const authMethod = jwt2 ? "OAuth (JWT)" : "API Key";
49157
49322
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
49158
- const activeTeamId = ctx.teamId ?? teams[0]?.id;
49323
+ const config2 = loadConfig2();
49324
+ const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
49159
49325
  return jsonResult({
49160
49326
  email: user.email,
49161
- authMethod: "OAuth",
49327
+ authMethod,
49162
49328
  teams: teams.map((t) => ({
49163
49329
  name: t.name,
49164
49330
  memberRole: t.memberRole,
@@ -49802,7 +49968,8 @@ function registerTools(server, ctx) {
49802
49968
  }, withErrorHandling("team_list", async () => {
49803
49969
  const jwt2 = await getValidJwt(ctx);
49804
49970
  const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
49805
- const activeTeamId = ctx.teamId ?? teams[0]?.id;
49971
+ const config2 = loadConfig2();
49972
+ const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
49806
49973
  return jsonResult({
49807
49974
  teams: teams.map((t) => ({
49808
49975
  id: t.id,
@@ -49822,6 +49989,9 @@ function registerTools(server, ctx) {
49822
49989
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
49823
49990
  }, withErrorHandling("team_switch", async (args) => {
49824
49991
  const jwt2 = await getValidJwt(ctx);
49992
+ if (!jwt2) {
49993
+ 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.");
49994
+ }
49825
49995
  const teams = await ctx.client.listTeams({ bearerToken: jwt2 });
49826
49996
  const identifier = args.teamIdentifier;
49827
49997
  const match = teams.find((t) => t.id === identifier || t.name.toLowerCase() === identifier.toLowerCase());
@@ -49829,11 +49999,15 @@ function registerTools(server, ctx) {
49829
49999
  return errorResult(`Team not found: "${identifier}". Available teams: ${teams.map((t) => t.name).join(", ")}`);
49830
50000
  }
49831
50001
  await ctx.client.setActiveTeam(match.id);
50002
+ ctx.teamId = match.id;
49832
50003
  const newJwt = await refreshSupabaseJwt(ctx);
49833
50004
  if (!newJwt) {
49834
50005
  return errorResult("Failed to refresh session after team switch");
49835
50006
  }
49836
- ctx.teamId = match.id;
50007
+ const config2 = loadConfig2();
50008
+ config2.teamId = match.id;
50009
+ config2.teamName = match.name;
50010
+ saveConfig(config2);
49837
50011
  return jsonResult({
49838
50012
  success: true,
49839
50013
  teamId: match.id,
@@ -53895,7 +54069,11 @@ async function refreshSupabaseToken(supabaseRefreshToken, context) {
53895
54069
  function jwtClaims(jwt2) {
53896
54070
  try {
53897
54071
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
53898
- return { email: payload.email, sub: payload.sub };
54072
+ return {
54073
+ email: payload.email,
54074
+ sub: payload.sub,
54075
+ activeTeamId: payload.app_metadata?.active_team_id
54076
+ };
53899
54077
  } catch {
53900
54078
  return {};
53901
54079
  }
@@ -54202,20 +54380,22 @@ class KadoaOAuthProvider {
54202
54380
  } else {
54203
54381
  console.error(`[AUTH] REFRESH_WARN: using stale Supabase JWT as fallback (${context})`);
54204
54382
  }
54383
+ const freshClaims = jwtClaims(supabaseJwt);
54384
+ const teamId = freshClaims.activeTeamId ?? entry.teamId;
54205
54385
  const newAccessToken = randomToken();
54206
54386
  const newRefreshToken = randomToken();
54207
54387
  const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
54208
54388
  await this.store.set("access_tokens", newAccessToken, {
54209
54389
  supabaseJwt,
54210
54390
  supabaseRefreshToken,
54211
- teamId: entry.teamId,
54391
+ teamId,
54212
54392
  clientId: entry.clientId,
54213
54393
  expiresAt
54214
54394
  }, ACCESS_TOKEN_TTL);
54215
54395
  await this.store.set("refresh_tokens", newRefreshToken, {
54216
54396
  supabaseJwt,
54217
54397
  supabaseRefreshToken,
54218
- teamId: entry.teamId,
54398
+ teamId,
54219
54399
  clientId: entry.clientId
54220
54400
  }, 2592000);
54221
54401
  return {
@@ -54226,6 +54406,15 @@ class KadoaOAuthProvider {
54226
54406
  };
54227
54407
  }
54228
54408
  async verifyAccessToken(token) {
54409
+ if (token.startsWith("tk-")) {
54410
+ return {
54411
+ token,
54412
+ clientId: "direct-api-key",
54413
+ scopes: [],
54414
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
54415
+ extra: { apiKey: token }
54416
+ };
54417
+ }
54229
54418
  const entry = await this.store.get("access_tokens", token);
54230
54419
  if (!entry) {
54231
54420
  const sessionCount = await this.store.size("access_tokens");
@@ -54877,6 +55066,9 @@ function resolveAuth(req) {
54877
55066
  console.error("[AUTH_RESOLVE] FAIL: req.auth.extra is missing");
54878
55067
  return;
54879
55068
  }
55069
+ if (typeof extra.apiKey === "string" && extra.apiKey.startsWith("tk-")) {
55070
+ return { kind: "apiKey", apiKey: extra.apiKey };
55071
+ }
54880
55072
  if (typeof extra.supabaseJwt === "string") {
54881
55073
  const claims = jwtClaims2(extra.supabaseJwt);
54882
55074
  const userId = claims.sub;
@@ -54894,13 +55086,15 @@ function resolveAuth(req) {
54894
55086
  }
54895
55087
  }
54896
55088
  return {
55089
+ kind: "jwt",
54897
55090
  jwt: extra.supabaseJwt,
54898
55091
  refreshToken: extra.supabaseRefreshToken ?? "",
54899
55092
  teamId: extra.teamId ?? "",
54900
- userId
55093
+ userId,
55094
+ mcpToken: req.auth.token
54901
55095
  };
54902
55096
  }
54903
- console.error(`[AUTH_RESOLVE] FAIL: no supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
55097
+ console.error(`[AUTH_RESOLVE] FAIL: no apiKey or supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
54904
55098
  return;
54905
55099
  }
54906
55100
  async function startHttpServer(options) {
@@ -54950,17 +55144,37 @@ async function startHttpServer(options) {
54950
55144
  });
54951
55145
  return;
54952
55146
  }
54953
- const identity = `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...`;
55147
+ const identity = auth.kind === "jwt" ? `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...` : `apiKey:${auth.apiKey.slice(0, 12)}...`;
54954
55148
  try {
54955
55149
  console.error(`[MCP] POST method=${method} auth=${identity}`);
54956
55150
  const transport = new StreamableHTTPServerTransport({
54957
55151
  sessionIdGenerator: undefined
54958
55152
  });
54959
- const server = createServer({
55153
+ const server = auth.kind === "jwt" ? createServer({
54960
55154
  jwt: auth.jwt,
54961
55155
  refreshToken: auth.refreshToken,
54962
- teamId: auth.teamId
54963
- });
55156
+ teamId: auth.teamId,
55157
+ persist: async (state) => {
55158
+ const entry = await store.get("access_tokens", auth.mcpToken);
55159
+ if (!entry) {
55160
+ console.error(`[PERSIST] WARN: access token gone from store (token=${auth.mcpToken.slice(0, 12)}...)`);
55161
+ return;
55162
+ }
55163
+ const remainingMs = entry.expiresAt - Date.now();
55164
+ if (remainingMs <= 0) {
55165
+ console.error(`[PERSIST] WARN: access token already expired, skipping persist`);
55166
+ return;
55167
+ }
55168
+ const ttlSeconds = Math.ceil(remainingMs / 1000);
55169
+ await store.set("access_tokens", auth.mcpToken, {
55170
+ ...entry,
55171
+ supabaseJwt: state.supabaseJwt ?? entry.supabaseJwt,
55172
+ supabaseRefreshToken: state.supabaseRefreshToken ?? entry.supabaseRefreshToken,
55173
+ teamId: state.teamId ?? entry.teamId
55174
+ }, ttlSeconds);
55175
+ console.error(`[PERSIST] OK: updated access token in store (token=${auth.mcpToken.slice(0, 12)}..., team=${state.teamId ?? "unchanged"}, ttl=${ttlSeconds}s)`);
55176
+ }
55177
+ }) : createServer({ apiKey: auth.apiKey });
54964
55178
  await server.connect(transport);
54965
55179
  await transport.handleRequest(req, res, req.body);
54966
55180
  } catch (error48) {
@@ -55013,24 +55227,61 @@ var init_http2 = __esm(async () => {
55013
55227
 
55014
55228
  // src/index.ts
55015
55229
  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
- };
55230
+ let ctx;
55231
+ if (typeof auth === "object" && auth !== null && "jwt" in auth) {
55232
+ ctx = {
55233
+ client: createKadoaClient({ jwt: auth.jwt }),
55234
+ supabaseJwt: auth.jwt,
55235
+ supabaseRefreshToken: auth.refreshToken,
55236
+ teamId: auth.teamId,
55237
+ persist: auth.persist
55238
+ };
55239
+ } else if (typeof auth === "object" && auth !== null && "apiKey" in auth) {
55240
+ ctx = {
55241
+ client: createKadoaClient({ apiKey: auth.apiKey })
55242
+ };
55243
+ } else {
55244
+ ctx = {
55245
+ client: createKadoaClient(auth)
55246
+ };
55247
+ }
55022
55248
  const server = new McpServer({ name: "kadoa", version: "0.3.2" });
55023
55249
  registerTools(server, ctx);
55024
55250
  server.server.onerror = (error48) => console.error("[MCP Error]", error48);
55025
55251
  return server;
55026
55252
  }
55253
+ async function validateApiKey() {
55254
+ const client = createKadoaClient();
55255
+ try {
55256
+ await client.workflow.list({ limit: 1 });
55257
+ } catch (error48) {
55258
+ if (KadoaSdkException.isInstance(error48) && error48.code === "AUTH_ERROR") {
55259
+ console.error("Kadoa MCP: Invalid API key. Check KADOA_API_KEY or run 'kadoa login'.");
55260
+ process.exit(1);
55261
+ }
55262
+ }
55263
+ }
55027
55264
  var init_src = __esm(async () => {
55028
55265
  init_mcp();
55266
+ init_stdio2();
55029
55267
  init_client();
55030
55268
  init_tools();
55031
55269
  if (!process.env.VITEST && !process.env.BUN_TEST) {
55032
- const { startHttpServer: startHttpServer2 } = await init_http2().then(() => exports_http);
55033
- await startHttpServer2();
55270
+ const httpMode = process.argv.includes("--http") || process.env.MCP_HTTP === "1";
55271
+ if (httpMode) {
55272
+ const { startHttpServer: startHttpServer2 } = await init_http2().then(() => exports_http);
55273
+ await startHttpServer2();
55274
+ } else {
55275
+ await validateApiKey();
55276
+ const server = createServer();
55277
+ const transport = new StdioServerTransport;
55278
+ await server.connect(transport);
55279
+ console.error("Kadoa MCP Server started");
55280
+ process.on("SIGINT", async () => {
55281
+ await server.close();
55282
+ process.exit(0);
55283
+ });
55284
+ }
55034
55285
  }
55035
55286
  });
55036
55287
  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.3",
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",