@kadoa/mcp 0.3.6-rc.7 → 0.3.6-rc.8

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 +121 -145
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -49079,8 +49079,10 @@ function createKadoaClient(auth) {
49079
49079
  }
49080
49080
  return new KadoaClient({ apiKey: resolveApiKey(auth) });
49081
49081
  }
49082
+ var ctxRefreshMutex;
49082
49083
  var init_client = __esm(() => {
49083
49084
  init_dist2();
49085
+ ctxRefreshMutex = new WeakMap;
49084
49086
  });
49085
49087
 
49086
49088
  // src/client.ts
@@ -49116,36 +49118,38 @@ function isJwtExpired(jwt2) {
49116
49118
  }
49117
49119
  async function refreshSupabaseJwt(ctx) {
49118
49120
  if (!ctx.supabaseRefreshToken) {
49119
- console.error("[JWT Refresh] No refresh token available, cannot refresh");
49121
+ console.error("[JWT_REFRESH] No refresh token available, cannot refresh");
49120
49122
  return;
49121
49123
  }
49122
49124
  const supabaseUrl = process.env.SUPABASE_URL;
49123
49125
  if (!supabaseUrl) {
49124
- console.error("[JWT Refresh] SUPABASE_URL not set, cannot refresh");
49126
+ console.error("[JWT_REFRESH] SUPABASE_URL not set, cannot refresh");
49125
49127
  return;
49126
49128
  }
49127
49129
  try {
49130
+ const refreshToken = ctx.supabaseRefreshToken;
49131
+ console.error(`[JWT_REFRESH] Refreshing Supabase JWT (refreshToken=${refreshToken.slice(0, 12)}..., team=${ctx.teamId ?? "unknown"})`);
49128
49132
  const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
49129
49133
  method: "POST",
49130
49134
  headers: {
49131
49135
  "Content-Type": "application/json",
49132
49136
  apikey: process.env.SUPABASE_ANON_KEY
49133
49137
  },
49134
- body: JSON.stringify({ refresh_token: ctx.supabaseRefreshToken })
49138
+ body: JSON.stringify({ refresh_token: refreshToken })
49135
49139
  });
49136
49140
  if (!res.ok) {
49137
49141
  const body = await res.text().catch(() => "");
49138
- console.error(`[JWT Refresh] Supabase returned ${res.status}: ${body}`);
49142
+ console.error(`[JWT_REFRESH] FAIL: Supabase returned ${res.status} (refreshToken=${refreshToken.slice(0, 12)}...): ${body}`);
49139
49143
  return;
49140
49144
  }
49141
49145
  const data = await res.json();
49142
49146
  ctx.supabaseJwt = data.access_token;
49143
49147
  ctx.supabaseRefreshToken = data.refresh_token;
49144
49148
  ctx.client.setBearerToken(data.access_token);
49145
- console.error("[JWT Refresh] Token refreshed successfully");
49149
+ console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${data.refresh_token.slice(0, 12)}...)`);
49146
49150
  return data.access_token;
49147
49151
  } catch (error48) {
49148
- console.error("[JWT Refresh] Failed:", error48);
49152
+ console.error("[JWT_REFRESH] FAIL: threw", error48);
49149
49153
  return;
49150
49154
  }
49151
49155
  }
@@ -49154,16 +49158,21 @@ async function getValidJwt(ctx) {
49154
49158
  return;
49155
49159
  if (!isJwtExpired(ctx.supabaseJwt))
49156
49160
  return ctx.supabaseJwt;
49157
- if (refreshPromise)
49158
- return refreshPromise;
49159
- refreshPromise = refreshSupabaseJwt(ctx).finally(() => {
49160
- refreshPromise = null;
49161
+ const inflight = ctxRefreshMutex2.get(ctx);
49162
+ if (inflight) {
49163
+ console.error("[JWT_REFRESH] DEDUP: reusing in-flight refresh");
49164
+ return inflight;
49165
+ }
49166
+ const promise3 = refreshSupabaseJwt(ctx).finally(() => {
49167
+ ctxRefreshMutex2.delete(ctx);
49161
49168
  });
49162
- return refreshPromise;
49169
+ ctxRefreshMutex2.set(ctx, promise3);
49170
+ return promise3;
49163
49171
  }
49164
- var refreshPromise = null;
49172
+ var ctxRefreshMutex2;
49165
49173
  var init_client2 = __esm(() => {
49166
49174
  init_dist2();
49175
+ ctxRefreshMutex2 = new WeakMap;
49167
49176
  });
49168
49177
 
49169
49178
  // src/tools.ts
@@ -54001,6 +54010,45 @@ function generatePKCE() {
54001
54010
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
54002
54011
  return { verifier, challenge };
54003
54012
  }
54013
+ async function refreshSupabaseToken(supabaseRefreshToken, context) {
54014
+ const inflight = supabaseRefreshMutex.get(supabaseRefreshToken);
54015
+ if (inflight) {
54016
+ console.error(`[AUTH] REFRESH_DEDUP: reusing in-flight refresh (${context})`);
54017
+ return inflight;
54018
+ }
54019
+ const promise3 = (async () => {
54020
+ const supabaseUrl = process.env.SUPABASE_URL;
54021
+ if (!supabaseUrl || !supabaseRefreshToken)
54022
+ return null;
54023
+ try {
54024
+ const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
54025
+ method: "POST",
54026
+ headers: {
54027
+ "Content-Type": "application/json",
54028
+ apikey: process.env.SUPABASE_ANON_KEY
54029
+ },
54030
+ body: JSON.stringify({ refresh_token: supabaseRefreshToken })
54031
+ });
54032
+ if (res.ok) {
54033
+ const data = await res.json();
54034
+ const newClaims = jwtClaims(data.access_token);
54035
+ console.log(`[AUTH] REFRESH_OK: Supabase JWT refreshed (${context}, newEmail=${newClaims.email})`);
54036
+ return { jwt: data.access_token, refreshToken: data.refresh_token };
54037
+ }
54038
+ const body = await res.text().catch(() => "");
54039
+ console.error(`[AUTH] REFRESH_FAIL: Supabase returned ${res.status} (${context}): ${body.slice(0, 200)}`);
54040
+ return null;
54041
+ } catch (err) {
54042
+ console.error(`[AUTH] REFRESH_FAIL: Supabase refresh threw (${context}):`, err);
54043
+ return null;
54044
+ }
54045
+ })();
54046
+ supabaseRefreshMutex.set(supabaseRefreshToken, promise3);
54047
+ promise3.finally(() => {
54048
+ supabaseRefreshMutex.delete(supabaseRefreshToken);
54049
+ });
54050
+ return promise3;
54051
+ }
54004
54052
  function jwtClaims(jwt2) {
54005
54053
  try {
54006
54054
  const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
@@ -54302,32 +54350,14 @@ class KadoaOAuthProvider {
54302
54350
  }
54303
54351
  await this.store.del("refresh_tokens", refreshToken);
54304
54352
  let { supabaseJwt, supabaseRefreshToken } = entry;
54305
- try {
54306
- const supabaseUrl = process.env.SUPABASE_URL;
54307
- if (supabaseUrl && supabaseRefreshToken) {
54308
- const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
54309
- method: "POST",
54310
- headers: {
54311
- "Content-Type": "application/json",
54312
- apikey: process.env.SUPABASE_ANON_KEY
54313
- },
54314
- body: JSON.stringify({ refresh_token: supabaseRefreshToken })
54315
- });
54316
- if (res.ok) {
54317
- const data = await res.json();
54318
- supabaseJwt = data.access_token;
54319
- supabaseRefreshToken = data.refresh_token;
54320
- const newClaims = jwtClaims(supabaseJwt);
54321
- console.log(`[AUTH] REFRESH_OK: Supabase JWT refreshed (email=${newClaims.email}, team=${entry.teamId})`);
54322
- } else {
54323
- const body = await res.text().catch(() => "");
54324
- const claims = jwtClaims(entry.supabaseJwt);
54325
- console.error(`[AUTH] REFRESH_WARN: Supabase refresh failed HTTP ${res.status} (email=${claims.email}, team=${entry.teamId}): ${body.slice(0, 200)}`);
54326
- }
54327
- }
54328
- } catch (err) {
54329
- const claims = jwtClaims(entry.supabaseJwt);
54330
- console.error(`[AUTH] REFRESH_WARN: Supabase refresh threw (email=${claims.email}, team=${entry.teamId}):`, err);
54353
+ const claims = jwtClaims(entry.supabaseJwt);
54354
+ const context = `email=${claims.email}, team=${entry.teamId}`;
54355
+ const refreshed = await refreshSupabaseToken(supabaseRefreshToken, context);
54356
+ if (refreshed) {
54357
+ supabaseJwt = refreshed.jwt;
54358
+ supabaseRefreshToken = refreshed.refreshToken;
54359
+ } else {
54360
+ console.error(`[AUTH] REFRESH_WARN: using stale Supabase JWT as fallback (${context})`);
54331
54361
  }
54332
54362
  const newAccessToken = randomToken();
54333
54363
  const newRefreshToken = randomToken();
@@ -54890,11 +54920,12 @@ function renderLoginPage(state, error48) {
54890
54920
  function escapeHtml(str) {
54891
54921
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
54892
54922
  }
54893
- var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL;
54923
+ var TEAM_SELECTION_TTL, ACCESS_TOKEN_TTL, supabaseRefreshMutex;
54894
54924
  var init_auth2 = __esm(() => {
54895
54925
  init_errors4();
54896
54926
  TEAM_SELECTION_TTL = 10 * 60 * 1000;
54897
54927
  ACCESS_TOKEN_TTL = 7 * 24 * 3600;
54928
+ supabaseRefreshMutex = new Map;
54898
54929
  });
54899
54930
 
54900
54931
  // src/redis-store.ts
@@ -54998,23 +55029,39 @@ var exports_http = {};
54998
55029
  __export(exports_http, {
54999
55030
  startHttpServer: () => startHttpServer
55000
55031
  });
55001
- import { randomUUID as randomUUID2 } from "node:crypto";
55002
55032
  import express8 from "express";
55033
+ function jwtClaims2(jwt2) {
55034
+ try {
55035
+ return JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
55036
+ } catch {
55037
+ return {};
55038
+ }
55039
+ }
55003
55040
  function resolveAuth(req) {
55004
55041
  const extra = req.auth?.extra;
55005
- if (!extra)
55042
+ if (!extra) {
55043
+ console.error("[AUTH_RESOLVE] FAIL: req.auth.extra is missing");
55006
55044
  return;
55045
+ }
55007
55046
  if (typeof extra.apiKey === "string" && extra.apiKey.startsWith("tk-")) {
55008
55047
  return { kind: "apiKey", apiKey: extra.apiKey };
55009
55048
  }
55010
55049
  if (typeof extra.supabaseJwt === "string") {
55011
- let userId;
55012
- try {
55013
- const payload = JSON.parse(Buffer.from(extra.supabaseJwt.split(".")[1], "base64url").toString());
55014
- userId = payload.sub;
55015
- } catch {
55050
+ const claims = jwtClaims2(extra.supabaseJwt);
55051
+ const userId = claims.sub;
55052
+ if (!userId) {
55053
+ console.error(`[AUTH_RESOLVE] FAIL: JWT missing sub claim (email=${claims.email ?? "unknown"})`);
55016
55054
  return;
55017
55055
  }
55056
+ const exp = claims.exp;
55057
+ if (exp) {
55058
+ const remainingSec = exp - Math.floor(Date.now() / 1000);
55059
+ if (remainingSec <= 0) {
55060
+ console.error(`[AUTH_RESOLVE] WARN: JWT already expired ${-remainingSec}s ago (email=${claims.email}, team=${extra.teamId})`);
55061
+ } else if (remainingSec < 300) {
55062
+ console.error(`[AUTH_RESOLVE] WARN: JWT expires in ${remainingSec}s (email=${claims.email}, team=${extra.teamId})`);
55063
+ }
55064
+ }
55018
55065
  return {
55019
55066
  kind: "jwt",
55020
55067
  jwt: extra.supabaseJwt,
@@ -55023,16 +55070,13 @@ function resolveAuth(req) {
55023
55070
  userId
55024
55071
  };
55025
55072
  }
55073
+ console.error(`[AUTH_RESOLVE] FAIL: no apiKey or supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
55026
55074
  return;
55027
55075
  }
55028
- function getSessionIdentity(auth) {
55029
- return auth.kind === "jwt" ? auth.userId : auth.apiKey;
55030
- }
55031
55076
  async function startHttpServer() {
55032
55077
  const port = parseInt(process.env.PORT || "3000", 10);
55033
55078
  const app = createMcpExpressApp({ host: "0.0.0.0" });
55034
55079
  app.set("trust proxy", 1);
55035
- const sessions = {};
55036
55080
  const store = new RedisTokenStore;
55037
55081
  const provider = new KadoaOAuthProvider(store);
55038
55082
  const serverUrl = process.env.MCP_SERVER_URL || `http://localhost:${port}`;
@@ -55060,15 +55104,15 @@ async function startHttpServer() {
55060
55104
  app.get("/health", (_req, res) => {
55061
55105
  res.json({
55062
55106
  status: "ok",
55063
- sessions: Object.keys(sessions).length,
55064
55107
  redis: store.isConnected() ? "connected" : "fallback"
55065
55108
  });
55066
55109
  });
55067
55110
  const bearerAuth = requireBearerAuth({ verifier: provider });
55068
55111
  app.post("/mcp", bearerAuth, async (req, res) => {
55069
- const sessionId = req.headers["mcp-session-id"];
55070
55112
  const auth = resolveAuth(req);
55113
+ const method = req.body?.method ?? "unknown";
55071
55114
  if (!auth) {
55115
+ console.error(`[MCP] 401 method=${method} reason=resolve_auth_failed`);
55072
55116
  res.status(401).json({
55073
55117
  jsonrpc: "2.0",
55074
55118
  error: { code: -32001, message: "Unauthorized: unable to resolve credentials" },
@@ -55076,56 +55120,21 @@ async function startHttpServer() {
55076
55120
  });
55077
55121
  return;
55078
55122
  }
55079
- const identity = getSessionIdentity(auth);
55123
+ const identity = auth.kind === "jwt" ? `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...` : `apiKey:${auth.apiKey.slice(0, 12)}...`;
55080
55124
  try {
55081
- if (sessionId && sessions[sessionId]) {
55082
- if (identity !== sessions[sessionId].identity) {
55083
- res.status(403).json({
55084
- jsonrpc: "2.0",
55085
- error: { code: -32002, message: "Forbidden: identity mismatch" },
55086
- id: null
55087
- });
55088
- return;
55089
- }
55090
- await sessions[sessionId].transport.handleRequest(req, res, req.body);
55091
- return;
55092
- }
55093
- if (sessionId && !sessions[sessionId]) {
55094
- res.status(404).json({
55095
- jsonrpc: "2.0",
55096
- error: { code: -32000, message: "Session not found" },
55097
- id: null
55098
- });
55099
- return;
55100
- }
55101
- if (!sessionId && isInitializeRequest(req.body)) {
55102
- const transport = new StreamableHTTPServerTransport({
55103
- sessionIdGenerator: () => randomUUID2(),
55104
- onsessioninitialized: (id) => {
55105
- sessions[id] = { transport, identity };
55106
- }
55107
- });
55108
- transport.onclose = () => {
55109
- const sid = transport.sessionId;
55110
- if (sid)
55111
- delete sessions[sid];
55112
- };
55113
- const server = auth.kind === "jwt" ? createServer({
55114
- jwt: auth.jwt,
55115
- refreshToken: auth.refreshToken,
55116
- teamId: auth.teamId
55117
- }) : createServer({ apiKey: auth.apiKey });
55118
- await server.connect(transport);
55119
- await transport.handleRequest(req, res, req.body);
55120
- return;
55121
- }
55122
- res.status(400).json({
55123
- jsonrpc: "2.0",
55124
- error: { code: -32000, message: "Bad Request: missing session ID or not an initialize request" },
55125
- id: null
55126
- });
55125
+ console.error(`[MCP] POST method=${method} auth=${identity}`);
55126
+ const transport = new StreamableHTTPServerTransport({
55127
+ sessionIdGenerator: undefined
55128
+ });
55129
+ const server = auth.kind === "jwt" ? createServer({
55130
+ jwt: auth.jwt,
55131
+ refreshToken: auth.refreshToken,
55132
+ teamId: auth.teamId
55133
+ }) : createServer({ apiKey: auth.apiKey });
55134
+ await server.connect(transport);
55135
+ await transport.handleRequest(req, res, req.body);
55127
55136
  } catch (error48) {
55128
- console.error("Error handling MCP request:", error48);
55137
+ console.error(`[MCP] 500 method=${method} auth=${identity} error=`, error48);
55129
55138
  if (!res.headersSent) {
55130
55139
  res.status(500).json({
55131
55140
  jsonrpc: "2.0",
@@ -55135,57 +55144,25 @@ async function startHttpServer() {
55135
55144
  }
55136
55145
  }
55137
55146
  });
55138
- app.get("/mcp", bearerAuth, async (req, res) => {
55139
- const sessionId = req.headers["mcp-session-id"];
55140
- const auth = resolveAuth(req);
55141
- if (!auth) {
55142
- res.status(401).send("Unauthorized: Bearer token required");
55143
- return;
55144
- }
55145
- if (!sessionId) {
55146
- res.status(400).send("Missing session ID");
55147
- return;
55148
- }
55149
- if (!sessions[sessionId]) {
55150
- res.status(404).send("Session not found");
55151
- return;
55152
- }
55153
- if (getSessionIdentity(auth) !== sessions[sessionId].identity) {
55154
- res.status(403).send("Forbidden: identity mismatch");
55155
- return;
55156
- }
55157
- await sessions[sessionId].transport.handleRequest(req, res);
55147
+ app.get("/mcp", bearerAuth, (_req, res) => {
55148
+ res.status(405).json({
55149
+ jsonrpc: "2.0",
55150
+ error: { code: -32000, message: "SSE streaming not supported (stateless server)" },
55151
+ id: null
55152
+ });
55158
55153
  });
55159
- app.delete("/mcp", bearerAuth, async (req, res) => {
55160
- const sessionId = req.headers["mcp-session-id"];
55161
- const auth = resolveAuth(req);
55162
- if (!auth) {
55163
- res.status(401).send("Unauthorized: Bearer token required");
55164
- return;
55165
- }
55166
- if (!sessionId) {
55167
- res.status(400).send("Missing session ID");
55168
- return;
55169
- }
55170
- if (!sessions[sessionId]) {
55171
- res.status(404).send("Session not found");
55172
- return;
55173
- }
55174
- if (getSessionIdentity(auth) !== sessions[sessionId].identity) {
55175
- res.status(403).send("Forbidden: identity mismatch");
55176
- return;
55177
- }
55178
- await sessions[sessionId].transport.handleRequest(req, res);
55154
+ app.delete("/mcp", bearerAuth, (_req, res) => {
55155
+ res.status(405).json({
55156
+ jsonrpc: "2.0",
55157
+ error: { code: -32000, message: "Session termination not supported (stateless server)" },
55158
+ id: null
55159
+ });
55179
55160
  });
55180
55161
  const httpServer = app.listen(port, () => {
55181
55162
  console.error(`Kadoa MCP HTTP Server listening on port ${port}`);
55182
55163
  });
55183
55164
  const shutdown = async () => {
55184
55165
  console.error("Shutting down HTTP server...");
55185
- for (const sid of Object.keys(sessions)) {
55186
- await sessions[sid].transport.close();
55187
- delete sessions[sid];
55188
- }
55189
55166
  await store.disconnect();
55190
55167
  httpServer.close();
55191
55168
  process.exit(0);
@@ -55199,7 +55176,6 @@ var init_http2 = __esm(async () => {
55199
55176
  init_streamableHttp();
55200
55177
  init_router();
55201
55178
  init_bearerAuth();
55202
- init_types2();
55203
55179
  init_auth2();
55204
55180
  init_redis_store();
55205
55181
  await init_src();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.3.6-rc.7",
3
+ "version": "0.3.6-rc.8",
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",