@kadoa/mcp 0.3.6-rc.4 → 0.3.6-rc.5

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 (2) hide show
  1. package/dist/index.js +166 -62
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -54028,8 +54028,27 @@ async function setActiveTeamAndRefresh(jwt2, refreshToken, teamId) {
54028
54028
  }
54029
54029
 
54030
54030
  class KadoaOAuthProvider {
54031
+ store;
54032
+ constructor(store) {
54033
+ this.store = store;
54034
+ }
54031
54035
  get clientsStore() {
54032
- return kadoaClientsStore;
54036
+ const store = this.store;
54037
+ return {
54038
+ async getClient(clientId) {
54039
+ return store.get("clients", clientId);
54040
+ },
54041
+ async registerClient(client) {
54042
+ const clientId = randomToken(16);
54043
+ const full = {
54044
+ ...client,
54045
+ client_id: clientId,
54046
+ client_id_issued_at: Math.floor(Date.now() / 1000)
54047
+ };
54048
+ await store.set("clients", clientId, full, 2592000);
54049
+ return full;
54050
+ }
54051
+ };
54033
54052
  }
54034
54053
  async authorize(client, params, res) {
54035
54054
  const supabaseUrl = process.env.SUPABASE_URL;
@@ -54039,16 +54058,16 @@ class KadoaOAuthProvider {
54039
54058
  }
54040
54059
  const state = randomToken();
54041
54060
  const { verifier, challenge } = generatePKCE();
54042
- pendingAuths.set(state, {
54061
+ await this.store.set("pending_auths", state, {
54043
54062
  client,
54044
54063
  params,
54045
54064
  supabaseCodeVerifier: verifier
54046
- });
54065
+ }, 600);
54047
54066
  res.type("html").send(renderLoginPage(state));
54048
54067
  }
54049
54068
  async handleGoogleLogin(req, res) {
54050
54069
  const { state } = req.body;
54051
- const pending = pendingAuths.get(state);
54070
+ const pending = await this.store.get("pending_auths", state);
54052
54071
  if (!pending) {
54053
54072
  res.status(400).send("Unknown or expired state parameter");
54054
54073
  return;
@@ -54073,7 +54092,7 @@ class KadoaOAuthProvider {
54073
54092
  res.status(400).send("Missing required fields");
54074
54093
  return;
54075
54094
  }
54076
- const pending = pendingAuths.get(state);
54095
+ const pending = await this.store.get("pending_auths", state);
54077
54096
  if (!pending) {
54078
54097
  res.status(400).type("html").send(renderLoginPage(state, "Session expired — please try again"));
54079
54098
  return;
@@ -54099,7 +54118,7 @@ class KadoaOAuthProvider {
54099
54118
  return;
54100
54119
  }
54101
54120
  const data = await tokenRes.json();
54102
- pendingAuths.delete(state);
54121
+ await this.store.del("pending_auths", state);
54103
54122
  await this.completeAuthWithTokens(pending, res, data.access_token, data.refresh_token);
54104
54123
  } catch (error48) {
54105
54124
  console.error("Email/password login error:", error48);
@@ -54112,7 +54131,7 @@ class KadoaOAuthProvider {
54112
54131
  res.status(400).send("Missing required fields");
54113
54132
  return;
54114
54133
  }
54115
- const pending = pendingAuths.get(state);
54134
+ const pending = await this.store.get("pending_auths", state);
54116
54135
  if (!pending) {
54117
54136
  res.status(400).type("html").send(renderLoginPage(state, "Session expired — please try again"));
54118
54137
  return;
@@ -54160,7 +54179,7 @@ class KadoaOAuthProvider {
54160
54179
  const teams = await fetchUserTeams(supabaseJwt);
54161
54180
  if (teams.length === 1) {
54162
54181
  const refreshed = await setActiveTeamAndRefresh(supabaseJwt, supabaseRefreshToken, teams[0].id);
54163
- this.completeAuthFlow(pending, res, {
54182
+ await this.completeAuthFlow(pending, res, {
54164
54183
  jwt: refreshed.jwt,
54165
54184
  refreshToken: refreshed.refreshToken,
54166
54185
  teamId: teams[0].id
@@ -54168,27 +54187,27 @@ class KadoaOAuthProvider {
54168
54187
  return;
54169
54188
  }
54170
54189
  const selectionToken = randomToken();
54171
- pendingTeamSelections.set(selectionToken, {
54190
+ await this.store.set("pending_team_selections", selectionToken, {
54172
54191
  supabaseJwt,
54173
54192
  supabaseRefreshToken,
54174
54193
  teams,
54175
54194
  pending,
54176
54195
  expiresAt: Date.now() + TEAM_SELECTION_TTL
54177
- });
54196
+ }, 600);
54178
54197
  res.type("html").send(renderTeamSelectionPage(teams, selectionToken));
54179
54198
  }
54180
54199
  async challengeForAuthorizationCode(_client, authorizationCode) {
54181
- const entry = authCodes.get(authorizationCode);
54200
+ const entry = await this.store.get("auth_codes", authorizationCode);
54182
54201
  if (!entry)
54183
54202
  throw new Error("Unknown authorization code");
54184
54203
  return entry.codeChallenge;
54185
54204
  }
54186
54205
  async exchangeAuthorizationCode(_client, authorizationCode, _codeVerifier, redirectUri) {
54187
- const entry = authCodes.get(authorizationCode);
54206
+ const entry = await this.store.get("auth_codes", authorizationCode);
54188
54207
  if (!entry)
54189
54208
  throw new Error("Unknown authorization code");
54190
54209
  if (entry.expiresAt < Date.now()) {
54191
- authCodes.delete(authorizationCode);
54210
+ await this.store.del("auth_codes", authorizationCode);
54192
54211
  throw new Error("Authorization code expired");
54193
54212
  }
54194
54213
  if (redirectUri && redirectUri !== entry.redirectUri) {
@@ -54197,22 +54216,23 @@ class KadoaOAuthProvider {
54197
54216
  const accessToken = randomToken();
54198
54217
  const refreshToken = randomToken();
54199
54218
  const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
54200
- accessTokens.set(accessToken, {
54219
+ await this.store.set("access_tokens", accessToken, {
54201
54220
  supabaseJwt: entry.supabaseJwt,
54202
54221
  supabaseRefreshToken: entry.supabaseRefreshToken,
54203
54222
  teamId: entry.teamId,
54204
54223
  clientId: entry.clientId,
54205
54224
  expiresAt
54206
- });
54207
- refreshTokens.set(refreshToken, {
54225
+ }, ACCESS_TOKEN_TTL);
54226
+ await this.store.set("refresh_tokens", refreshToken, {
54208
54227
  supabaseJwt: entry.supabaseJwt,
54209
54228
  supabaseRefreshToken: entry.supabaseRefreshToken,
54210
54229
  teamId: entry.teamId,
54211
54230
  clientId: entry.clientId
54212
- });
54213
- authCodes.delete(authorizationCode);
54231
+ }, 2592000);
54232
+ await this.store.del("auth_codes", authorizationCode);
54214
54233
  const claims = jwtClaims(entry.supabaseJwt);
54215
- console.log(`[AUTH] LOGIN: tokens issued (email=${claims.email}, team=${entry.teamId}, token=${accessToken.slice(0, 12)}..., ttl=${ACCESS_TOKEN_TTL}s, active_sessions=${accessTokens.size})`);
54234
+ const sessionCount = await this.store.size("access_tokens");
54235
+ console.log(`[AUTH] LOGIN: tokens issued (email=${claims.email}, team=${entry.teamId}, token=${accessToken.slice(0, 12)}..., ttl=${ACCESS_TOKEN_TTL}s, active_sessions=${sessionCount})`);
54216
54236
  return {
54217
54237
  access_token: accessToken,
54218
54238
  token_type: "bearer",
@@ -54221,12 +54241,13 @@ class KadoaOAuthProvider {
54221
54241
  };
54222
54242
  }
54223
54243
  async exchangeRefreshToken(_client, refreshToken) {
54224
- const entry = refreshTokens.get(refreshToken);
54244
+ const entry = await this.store.get("refresh_tokens", refreshToken);
54225
54245
  if (!entry) {
54226
- console.error(`[AUTH] REFRESH_FAIL: unknown refresh token (token=${refreshToken.slice(0, 12)}..., active_sessions=${refreshTokens.size})`);
54246
+ const sessionCount = await this.store.size("refresh_tokens");
54247
+ console.error(`[AUTH] REFRESH_FAIL: unknown refresh token (token=${refreshToken.slice(0, 12)}..., active_sessions=${sessionCount})`);
54227
54248
  throw new InvalidTokenError("Unknown or expired refresh token");
54228
54249
  }
54229
- refreshTokens.delete(refreshToken);
54250
+ await this.store.del("refresh_tokens", refreshToken);
54230
54251
  let { supabaseJwt, supabaseRefreshToken } = entry;
54231
54252
  try {
54232
54253
  const supabaseUrl = process.env.SUPABASE_URL;
@@ -54258,19 +54279,19 @@ class KadoaOAuthProvider {
54258
54279
  const newAccessToken = randomToken();
54259
54280
  const newRefreshToken = randomToken();
54260
54281
  const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
54261
- accessTokens.set(newAccessToken, {
54282
+ await this.store.set("access_tokens", newAccessToken, {
54262
54283
  supabaseJwt,
54263
54284
  supabaseRefreshToken,
54264
54285
  teamId: entry.teamId,
54265
54286
  clientId: entry.clientId,
54266
54287
  expiresAt
54267
- });
54268
- refreshTokens.set(newRefreshToken, {
54288
+ }, ACCESS_TOKEN_TTL);
54289
+ await this.store.set("refresh_tokens", newRefreshToken, {
54269
54290
  supabaseJwt,
54270
54291
  supabaseRefreshToken,
54271
54292
  teamId: entry.teamId,
54272
54293
  clientId: entry.clientId
54273
- });
54294
+ }, 2592000);
54274
54295
  return {
54275
54296
  access_token: newAccessToken,
54276
54297
  token_type: "bearer",
@@ -54288,16 +54309,17 @@ class KadoaOAuthProvider {
54288
54309
  extra: { apiKey: token }
54289
54310
  };
54290
54311
  }
54291
- const entry = accessTokens.get(token);
54312
+ const entry = await this.store.get("access_tokens", token);
54292
54313
  if (!entry) {
54293
- console.error(`[AUTH] VERIFY_FAIL: unknown token (token=${token.slice(0, 12)}..., active_sessions=${accessTokens.size})`);
54314
+ const sessionCount = await this.store.size("access_tokens");
54315
+ console.error(`[AUTH] VERIFY_FAIL: unknown token (token=${token.slice(0, 12)}..., active_sessions=${sessionCount})`);
54294
54316
  throw new InvalidTokenError("Unknown or expired access token");
54295
54317
  }
54296
54318
  if (entry.expiresAt < Date.now()) {
54297
54319
  const expiredAgo = Math.round((Date.now() - entry.expiresAt) / 1000);
54298
54320
  const claims = jwtClaims(entry.supabaseJwt);
54299
54321
  console.error(`[AUTH] VERIFY_FAIL: token expired ${expiredAgo}s ago (email=${claims.email}, team=${entry.teamId}, token=${token.slice(0, 12)}...)`);
54300
- accessTokens.delete(token);
54322
+ await this.store.del("access_tokens", token);
54301
54323
  throw new InvalidTokenError("Access token expired");
54302
54324
  }
54303
54325
  return {
@@ -54318,12 +54340,12 @@ class KadoaOAuthProvider {
54318
54340
  res.status(400).send("Missing code or mcp_state parameter");
54319
54341
  return;
54320
54342
  }
54321
- const pending = pendingAuths.get(state);
54343
+ const pending = await this.store.get("pending_auths", state);
54322
54344
  if (!pending) {
54323
54345
  res.status(400).send("Unknown or expired state parameter");
54324
54346
  return;
54325
54347
  }
54326
- pendingAuths.delete(state);
54348
+ await this.store.del("pending_auths", state);
54327
54349
  try {
54328
54350
  const supabaseTokens = await exchangeSupabaseCode(code, pending.supabaseCodeVerifier);
54329
54351
  await this.completeAuthWithTokens(pending, res, supabaseTokens.accessToken, supabaseTokens.refreshToken);
@@ -54344,13 +54366,13 @@ class KadoaOAuthProvider {
54344
54366
  res.status(400).send("Missing token or teamId");
54345
54367
  return;
54346
54368
  }
54347
- const entry = pendingTeamSelections.get(token);
54369
+ const entry = await this.store.get("pending_team_selections", token);
54348
54370
  if (!entry) {
54349
54371
  res.status(400).send("Unknown or expired team selection token");
54350
54372
  return;
54351
54373
  }
54352
54374
  if (entry.expiresAt < Date.now()) {
54353
- pendingTeamSelections.delete(token);
54375
+ await this.store.del("pending_team_selections", token);
54354
54376
  res.status(400).send("Team selection expired — please log in again");
54355
54377
  return;
54356
54378
  }
@@ -54358,10 +54380,10 @@ class KadoaOAuthProvider {
54358
54380
  res.status(403).send("Invalid team selection");
54359
54381
  return;
54360
54382
  }
54361
- pendingTeamSelections.delete(token);
54383
+ await this.store.del("pending_team_selections", token);
54362
54384
  try {
54363
54385
  const refreshed = await setActiveTeamAndRefresh(entry.supabaseJwt, entry.supabaseRefreshToken, teamId);
54364
- this.completeAuthFlow(entry.pending, res, {
54386
+ await this.completeAuthFlow(entry.pending, res, {
54365
54387
  jwt: refreshed.jwt,
54366
54388
  refreshToken: refreshed.refreshToken,
54367
54389
  teamId
@@ -54377,9 +54399,9 @@ class KadoaOAuthProvider {
54377
54399
  res.redirect(redirectUrl.toString());
54378
54400
  }
54379
54401
  }
54380
- completeAuthFlow(pending, res, credentials) {
54402
+ async completeAuthFlow(pending, res, credentials) {
54381
54403
  const mcpCode = randomToken();
54382
- authCodes.set(mcpCode, {
54404
+ await this.store.set("auth_codes", mcpCode, {
54383
54405
  supabaseJwt: credentials.jwt,
54384
54406
  supabaseRefreshToken: credentials.refreshToken,
54385
54407
  teamId: credentials.teamId,
@@ -54387,7 +54409,7 @@ class KadoaOAuthProvider {
54387
54409
  clientId: pending.client.client_id,
54388
54410
  redirectUri: pending.params.redirectUri,
54389
54411
  expiresAt: Date.now() + 10 * 60 * 1000
54390
- });
54412
+ }, 600);
54391
54413
  const redirectUrl = new URL(pending.params.redirectUri);
54392
54414
  redirectUrl.searchParams.set("code", mcpCode);
54393
54415
  if (pending.params.state) {
@@ -54815,34 +54837,109 @@ function renderLoginPage(state, error48) {
54815
54837
  function escapeHtml(str) {
54816
54838
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
54817
54839
  }
54818
- var clients, pendingAuths, pendingTeamSelections, authCodes, accessTokens, refreshTokens, TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL, kadoaClientsStore;
54840
+ var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
54819
54841
  var init_auth2 = __esm(() => {
54820
54842
  init_errors4();
54821
- clients = new Map;
54822
- pendingAuths = new Map;
54823
- pendingTeamSelections = new Map;
54824
- authCodes = new Map;
54825
- accessTokens = new Map;
54826
- refreshTokens = new Map;
54827
54843
  TEAM_SELECTION_TTL = 10 * 60 * 1000;
54828
54844
  ACCESS_TOKEN_TTL = 7 * 24 * 3600;
54829
- kadoaClientsStore = {
54830
- getClient(clientId) {
54831
- return clients.get(clientId);
54832
- },
54833
- registerClient(client) {
54834
- const clientId = randomToken(16);
54835
- const full = {
54836
- ...client,
54837
- client_id: clientId,
54838
- client_id_issued_at: Math.floor(Date.now() / 1000)
54839
- };
54840
- clients.set(clientId, full);
54841
- return full;
54842
- }
54843
- };
54844
54845
  });
54845
54846
 
54847
+ // src/redis-store.ts
54848
+ import Redis from "ioredis";
54849
+
54850
+ class RedisTokenStore {
54851
+ redis = null;
54852
+ fallback = new Map;
54853
+ constructor(redisHost) {
54854
+ const host = redisHost ?? process.env.REDIS_HOST;
54855
+ if (host) {
54856
+ const port = Number(process.env.REDIS_PORT) || 6379;
54857
+ console.error(`[Redis] Connecting to ${host}:${port}...`);
54858
+ this.redis = new Redis({
54859
+ host,
54860
+ port,
54861
+ maxRetriesPerRequest: 3,
54862
+ lazyConnect: true
54863
+ });
54864
+ this.redis.on("ready", () => {
54865
+ console.error(`[Redis] Connected to ${host}:${port}`);
54866
+ });
54867
+ this.redis.on("error", (err) => {
54868
+ console.error("[Redis] Error:", err.message);
54869
+ });
54870
+ this.redis.connect().catch((err) => {
54871
+ console.error("[Redis] Connection failed, falling back to in-memory:", err.message);
54872
+ this.redis?.disconnect();
54873
+ this.redis = null;
54874
+ });
54875
+ } else {
54876
+ console.error("[Redis] No REDIS_HOST set, using in-memory token store");
54877
+ }
54878
+ }
54879
+ get useRedis() {
54880
+ return this.redis?.status === "ready";
54881
+ }
54882
+ isConnected() {
54883
+ return this.useRedis;
54884
+ }
54885
+ async get(namespace, key) {
54886
+ const fullKey = `${KEY_PREFIX}:${namespace}:${key}`;
54887
+ const raw = this.useRedis ? await this.redis.get(fullKey) : this.fallback.get(fullKey);
54888
+ if (!raw)
54889
+ return;
54890
+ return JSON.parse(raw);
54891
+ }
54892
+ async set(namespace, key, value, ttlSeconds) {
54893
+ const fullKey = `${KEY_PREFIX}:${namespace}:${key}`;
54894
+ const json2 = JSON.stringify(value);
54895
+ if (this.useRedis) {
54896
+ if (ttlSeconds) {
54897
+ await this.redis.set(fullKey, json2, "EX", ttlSeconds);
54898
+ } else {
54899
+ await this.redis.set(fullKey, json2);
54900
+ }
54901
+ } else {
54902
+ this.fallback.set(fullKey, json2);
54903
+ }
54904
+ }
54905
+ async size(namespace) {
54906
+ const pattern = `${KEY_PREFIX}:${namespace}:*`;
54907
+ if (this.useRedis) {
54908
+ let count2 = 0;
54909
+ let cursor = "0";
54910
+ do {
54911
+ const [next, keys] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
54912
+ cursor = next;
54913
+ count2 += keys.length;
54914
+ } while (cursor !== "0");
54915
+ return count2;
54916
+ }
54917
+ let count = 0;
54918
+ for (const key of this.fallback.keys()) {
54919
+ if (key.startsWith(`${KEY_PREFIX}:${namespace}:`))
54920
+ count++;
54921
+ }
54922
+ return count;
54923
+ }
54924
+ async del(namespace, key) {
54925
+ const fullKey = `${KEY_PREFIX}:${namespace}:${key}`;
54926
+ if (this.useRedis) {
54927
+ await this.redis.del(fullKey);
54928
+ } else {
54929
+ this.fallback.delete(fullKey);
54930
+ }
54931
+ }
54932
+ async disconnect() {
54933
+ if (this.redis) {
54934
+ console.error("[Redis] Disconnecting...");
54935
+ await this.redis.quit();
54936
+ this.redis = null;
54937
+ }
54938
+ }
54939
+ }
54940
+ var KEY_PREFIX = "kadoa-mcp";
54941
+ var init_redis_store = () => {};
54942
+
54846
54943
  // src/http.ts
54847
54944
  var exports_http = {};
54848
54945
  __export(exports_http, {
@@ -54883,7 +54980,8 @@ async function startHttpServer() {
54883
54980
  const app = createMcpExpressApp({ host: "0.0.0.0" });
54884
54981
  app.set("trust proxy", 1);
54885
54982
  const sessions = {};
54886
- const provider = new KadoaOAuthProvider;
54983
+ const store = new RedisTokenStore;
54984
+ const provider = new KadoaOAuthProvider(store);
54887
54985
  const serverUrl = process.env.MCP_SERVER_URL || `http://localhost:${port}`;
54888
54986
  app.use(mcpAuthRouter({
54889
54987
  provider,
@@ -54907,7 +55005,11 @@ async function startHttpServer() {
54907
55005
  provider.handleTeamSelection(req, res);
54908
55006
  });
54909
55007
  app.get("/health", (_req, res) => {
54910
- res.json({ status: "ok", sessions: Object.keys(sessions).length });
55008
+ res.json({
55009
+ status: "ok",
55010
+ sessions: Object.keys(sessions).length,
55011
+ redis: store.isConnected() ? "connected" : "fallback"
55012
+ });
54911
55013
  });
54912
55014
  const bearerAuth = requireBearerAuth({ verifier: provider });
54913
55015
  app.post("/mcp", bearerAuth, async (req, res) => {
@@ -55015,6 +55117,7 @@ async function startHttpServer() {
55015
55117
  await sessions[sid].transport.close();
55016
55118
  delete sessions[sid];
55017
55119
  }
55120
+ await store.disconnect();
55018
55121
  httpServer.close();
55019
55122
  process.exit(0);
55020
55123
  };
@@ -55029,6 +55132,7 @@ var init_http2 = __esm(async () => {
55029
55132
  init_bearerAuth();
55030
55133
  init_types2();
55031
55134
  init_auth2();
55135
+ init_redis_store();
55032
55136
  await init_src();
55033
55137
  });
55034
55138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.3.6-rc.4",
3
+ "version": "0.3.6-rc.5",
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",
@@ -19,7 +19,7 @@
19
19
  "lint:fix": "bunx biome check --write",
20
20
  "dev": "bun src/index.ts",
21
21
  "dev:http": "MCP_HTTP=1 bun src/index.ts",
22
- "build": "bun build src/index.ts --outdir=dist --target=node --external express && 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'))\"",
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'))\"",
23
23
  "check-types": "tsc --noEmit",
24
24
  "test": "BUN_TEST=1 bun test",
25
25
  "test:unit": "BUN_TEST=1 bun test tests/unit --timeout=120000",
@@ -31,6 +31,7 @@
31
31
  "@kadoa/node-sdk": "^0.23.0",
32
32
  "@modelcontextprotocol/sdk": "^1.26.0",
33
33
  "express": "^5.2.1",
34
+ "ioredis": "^5.6.1",
34
35
  "zod": "^4.3.6"
35
36
  },
36
37
  "devDependencies": {