@kadoa/mcp 0.3.7-rc.2 → 0.3.7-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -6
- package/dist/index.js +306 -29
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
// @bun
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -28800,6 +28800,102 @@ var init_mcp = __esm(() => {
|
|
|
28800
28800
|
};
|
|
28801
28801
|
});
|
|
28802
28802
|
|
|
28803
|
+
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
28804
|
+
class ReadBuffer {
|
|
28805
|
+
append(chunk) {
|
|
28806
|
+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
|
28807
|
+
}
|
|
28808
|
+
readMessage() {
|
|
28809
|
+
if (!this._buffer) {
|
|
28810
|
+
return null;
|
|
28811
|
+
}
|
|
28812
|
+
const index = this._buffer.indexOf(`
|
|
28813
|
+
`);
|
|
28814
|
+
if (index === -1) {
|
|
28815
|
+
return null;
|
|
28816
|
+
}
|
|
28817
|
+
const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
|
|
28818
|
+
this._buffer = this._buffer.subarray(index + 1);
|
|
28819
|
+
return deserializeMessage(line);
|
|
28820
|
+
}
|
|
28821
|
+
clear() {
|
|
28822
|
+
this._buffer = undefined;
|
|
28823
|
+
}
|
|
28824
|
+
}
|
|
28825
|
+
function deserializeMessage(line) {
|
|
28826
|
+
return JSONRPCMessageSchema.parse(JSON.parse(line));
|
|
28827
|
+
}
|
|
28828
|
+
function serializeMessage(message) {
|
|
28829
|
+
return JSON.stringify(message) + `
|
|
28830
|
+
`;
|
|
28831
|
+
}
|
|
28832
|
+
var init_stdio = __esm(() => {
|
|
28833
|
+
init_types2();
|
|
28834
|
+
});
|
|
28835
|
+
|
|
28836
|
+
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
28837
|
+
import process3 from "node:process";
|
|
28838
|
+
|
|
28839
|
+
class StdioServerTransport {
|
|
28840
|
+
constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
|
|
28841
|
+
this._stdin = _stdin;
|
|
28842
|
+
this._stdout = _stdout;
|
|
28843
|
+
this._readBuffer = new ReadBuffer;
|
|
28844
|
+
this._started = false;
|
|
28845
|
+
this._ondata = (chunk) => {
|
|
28846
|
+
this._readBuffer.append(chunk);
|
|
28847
|
+
this.processReadBuffer();
|
|
28848
|
+
};
|
|
28849
|
+
this._onerror = (error48) => {
|
|
28850
|
+
this.onerror?.(error48);
|
|
28851
|
+
};
|
|
28852
|
+
}
|
|
28853
|
+
async start() {
|
|
28854
|
+
if (this._started) {
|
|
28855
|
+
throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");
|
|
28856
|
+
}
|
|
28857
|
+
this._started = true;
|
|
28858
|
+
this._stdin.on("data", this._ondata);
|
|
28859
|
+
this._stdin.on("error", this._onerror);
|
|
28860
|
+
}
|
|
28861
|
+
processReadBuffer() {
|
|
28862
|
+
while (true) {
|
|
28863
|
+
try {
|
|
28864
|
+
const message = this._readBuffer.readMessage();
|
|
28865
|
+
if (message === null) {
|
|
28866
|
+
break;
|
|
28867
|
+
}
|
|
28868
|
+
this.onmessage?.(message);
|
|
28869
|
+
} catch (error48) {
|
|
28870
|
+
this.onerror?.(error48);
|
|
28871
|
+
}
|
|
28872
|
+
}
|
|
28873
|
+
}
|
|
28874
|
+
async close() {
|
|
28875
|
+
this._stdin.off("data", this._ondata);
|
|
28876
|
+
this._stdin.off("error", this._onerror);
|
|
28877
|
+
const remainingDataListeners = this._stdin.listenerCount("data");
|
|
28878
|
+
if (remainingDataListeners === 0) {
|
|
28879
|
+
this._stdin.pause();
|
|
28880
|
+
}
|
|
28881
|
+
this._readBuffer.clear();
|
|
28882
|
+
this.onclose?.();
|
|
28883
|
+
}
|
|
28884
|
+
send(message) {
|
|
28885
|
+
return new Promise((resolve) => {
|
|
28886
|
+
const json2 = serializeMessage(message);
|
|
28887
|
+
if (this._stdout.write(json2)) {
|
|
28888
|
+
resolve();
|
|
28889
|
+
} else {
|
|
28890
|
+
this._stdout.once("drain", resolve);
|
|
28891
|
+
}
|
|
28892
|
+
});
|
|
28893
|
+
}
|
|
28894
|
+
}
|
|
28895
|
+
var init_stdio2 = __esm(() => {
|
|
28896
|
+
init_stdio();
|
|
28897
|
+
});
|
|
28898
|
+
|
|
28803
28899
|
// node_modules/axios/lib/helpers/bind.js
|
|
28804
28900
|
function bind(fn, thisArg) {
|
|
28805
28901
|
return function wrap() {
|
|
@@ -48947,10 +49043,51 @@ var init_dist2 = __esm(() => {
|
|
|
48947
49043
|
});
|
|
48948
49044
|
|
|
48949
49045
|
// src/client.ts
|
|
49046
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
49047
|
+
import { dirname, join } from "node:path";
|
|
49048
|
+
import { homedir } from "node:os";
|
|
49049
|
+
function getConfigPath() {
|
|
49050
|
+
const home = process.env.HOME || homedir();
|
|
49051
|
+
return join(home, ".kadoa", "config.json");
|
|
49052
|
+
}
|
|
49053
|
+
function loadConfig() {
|
|
49054
|
+
const configFile = getConfigPath();
|
|
49055
|
+
if (!existsSync(configFile))
|
|
49056
|
+
return {};
|
|
49057
|
+
try {
|
|
49058
|
+
return JSON.parse(readFileSync(configFile, "utf-8"));
|
|
49059
|
+
} catch {
|
|
49060
|
+
return {};
|
|
49061
|
+
}
|
|
49062
|
+
}
|
|
49063
|
+
function loadApiKeyFromConfig() {
|
|
49064
|
+
return loadConfig().apiKey;
|
|
49065
|
+
}
|
|
49066
|
+
function resolveApiKey(apiKey) {
|
|
49067
|
+
const key = apiKey || process.env.KADOA_API_KEY || loadApiKeyFromConfig();
|
|
49068
|
+
if (!key) {
|
|
49069
|
+
throw new Error("No API key found. Set KADOA_API_KEY env var or run 'kadoa login' (npm i -g @kadoa/cli).");
|
|
49070
|
+
}
|
|
49071
|
+
return key;
|
|
49072
|
+
}
|
|
48950
49073
|
function createKadoaClient(auth) {
|
|
48951
|
-
|
|
49074
|
+
let client;
|
|
49075
|
+
let teamId;
|
|
49076
|
+
if (typeof auth === "object" && auth !== null) {
|
|
49077
|
+
if ("jwt" in auth) {
|
|
49078
|
+
client = new KadoaClient({ bearerToken: auth.jwt });
|
|
49079
|
+
teamId = auth.teamId;
|
|
49080
|
+
} else {
|
|
49081
|
+
client = new KadoaClient({ apiKey: auth.apiKey });
|
|
49082
|
+
}
|
|
49083
|
+
} else {
|
|
49084
|
+
client = new KadoaClient({ apiKey: resolveApiKey(auth) });
|
|
49085
|
+
}
|
|
48952
49086
|
client.axiosInstance.interceptors.request.use((config2) => {
|
|
48953
49087
|
config2.headers["x-kadoa-source"] = "mcp";
|
|
49088
|
+
if (teamId) {
|
|
49089
|
+
config2.headers["x-team-id"] = teamId;
|
|
49090
|
+
}
|
|
48954
49091
|
return config2;
|
|
48955
49092
|
});
|
|
48956
49093
|
return client;
|
|
@@ -48962,6 +49099,41 @@ var init_client = __esm(() => {
|
|
|
48962
49099
|
});
|
|
48963
49100
|
|
|
48964
49101
|
// src/client.ts
|
|
49102
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
49103
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
49104
|
+
import { homedir as homedir2 } from "node:os";
|
|
49105
|
+
function getConfigPath2() {
|
|
49106
|
+
const home = process.env.HOME || homedir2();
|
|
49107
|
+
return join2(home, ".kadoa", "config.json");
|
|
49108
|
+
}
|
|
49109
|
+
function loadConfig2() {
|
|
49110
|
+
const configFile = getConfigPath2();
|
|
49111
|
+
if (!existsSync2(configFile))
|
|
49112
|
+
return {};
|
|
49113
|
+
try {
|
|
49114
|
+
return JSON.parse(readFileSync2(configFile, "utf-8"));
|
|
49115
|
+
} catch {
|
|
49116
|
+
return {};
|
|
49117
|
+
}
|
|
49118
|
+
}
|
|
49119
|
+
function saveConfig(config2) {
|
|
49120
|
+
const configFile = getConfigPath2();
|
|
49121
|
+
mkdirSync2(dirname2(configFile), { recursive: true });
|
|
49122
|
+
writeFileSync2(configFile, JSON.stringify(config2, null, 2), "utf-8");
|
|
49123
|
+
}
|
|
49124
|
+
function decodeJwtClaims(jwt2) {
|
|
49125
|
+
try {
|
|
49126
|
+
const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
|
|
49127
|
+
return {
|
|
49128
|
+
sub: payload.sub,
|
|
49129
|
+
email: payload.email,
|
|
49130
|
+
activeTeamId: payload.active_team_id ?? payload.app_metadata?.active_team_id,
|
|
49131
|
+
exp: payload.exp
|
|
49132
|
+
};
|
|
49133
|
+
} catch {
|
|
49134
|
+
return {};
|
|
49135
|
+
}
|
|
49136
|
+
}
|
|
48965
49137
|
function isJwtExpired(jwt2) {
|
|
48966
49138
|
try {
|
|
48967
49139
|
const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
|
|
@@ -49000,6 +49172,15 @@ async function refreshSupabaseJwt(ctx) {
|
|
|
49000
49172
|
ctx.supabaseJwt = data.access_token;
|
|
49001
49173
|
ctx.supabaseRefreshToken = data.refresh_token;
|
|
49002
49174
|
ctx.client.setBearerToken(data.access_token);
|
|
49175
|
+
try {
|
|
49176
|
+
await ctx.persist?.({
|
|
49177
|
+
supabaseJwt: ctx.supabaseJwt,
|
|
49178
|
+
supabaseRefreshToken: ctx.supabaseRefreshToken,
|
|
49179
|
+
teamId: ctx.teamId
|
|
49180
|
+
});
|
|
49181
|
+
} catch (e) {
|
|
49182
|
+
console.error("[JWT_REFRESH] WARN: persist failed, tokens updated in-memory only:", e);
|
|
49183
|
+
}
|
|
49003
49184
|
console.error(`[JWT_REFRESH] OK: token refreshed (team=${ctx.teamId ?? "unknown"}, newRefreshToken=${data.refresh_token.slice(0, 12)}...)`);
|
|
49004
49185
|
return data.access_token;
|
|
49005
49186
|
} catch (error48) {
|
|
@@ -49084,7 +49265,7 @@ function classifyError(error48) {
|
|
|
49084
49265
|
if (httpError.httpStatus === 403) {
|
|
49085
49266
|
return `Access denied${status}. Your current team role may not have permission for this action. Use the whoami tool to check your role, or contact your team admin to request elevated access.`;
|
|
49086
49267
|
}
|
|
49087
|
-
return `Authentication failed${status}. Please re-authenticate
|
|
49268
|
+
return `Authentication failed${status}. Your Kadoa API key may be invalid or expired. Please check your KADOA_API_KEY or re-authenticate.`;
|
|
49088
49269
|
case "NOT_FOUND":
|
|
49089
49270
|
return `Not found${status}. The workflow may have been deleted or the ID is incorrect.`;
|
|
49090
49271
|
case "RATE_LIMITED":
|
|
@@ -49102,7 +49283,7 @@ function classifyError(error48) {
|
|
|
49102
49283
|
}
|
|
49103
49284
|
switch (code) {
|
|
49104
49285
|
case "AUTH_ERROR":
|
|
49105
|
-
return "Authentication failed. Please re-authenticate
|
|
49286
|
+
return "Authentication failed. Your Kadoa API key may be invalid or expired. Please check your KADOA_API_KEY or re-authenticate.";
|
|
49106
49287
|
case "NOT_FOUND":
|
|
49107
49288
|
return "Not found. The workflow may have been deleted or the ID is incorrect.";
|
|
49108
49289
|
case "RATE_LIMITED":
|
|
@@ -49128,6 +49309,13 @@ function registerTools(server, ctx) {
|
|
|
49128
49309
|
return async (...args) => {
|
|
49129
49310
|
try {
|
|
49130
49311
|
await getValidJwt(ctx);
|
|
49312
|
+
if (ctx.teamId && ctx.supabaseJwt) {
|
|
49313
|
+
const claims = decodeJwtClaims(ctx.supabaseJwt);
|
|
49314
|
+
if (claims.activeTeamId && claims.activeTeamId !== ctx.teamId) {
|
|
49315
|
+
console.error(`[TEAM_SYNC] JWT active_team_id (${claims.activeTeamId}) != ctx.teamId (${ctx.teamId}), calling setActiveTeam`);
|
|
49316
|
+
await ctx.client.setActiveTeam(ctx.teamId);
|
|
49317
|
+
}
|
|
49318
|
+
}
|
|
49131
49319
|
return await handler(...args);
|
|
49132
49320
|
} catch (error48) {
|
|
49133
49321
|
let message = classifyError(error48);
|
|
@@ -49135,7 +49323,8 @@ function registerTools(server, ctx) {
|
|
|
49135
49323
|
try {
|
|
49136
49324
|
const jwt2 = ctx.supabaseJwt;
|
|
49137
49325
|
const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
|
|
49138
|
-
const
|
|
49326
|
+
const config2 = loadConfig2();
|
|
49327
|
+
const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
|
|
49139
49328
|
const activeTeam = teams.find((t) => t.id === activeTeamId);
|
|
49140
49329
|
if (activeTeam?.adminEmail) {
|
|
49141
49330
|
message += ` Your team admin is ${activeTeam.adminEmail}.`;
|
|
@@ -49154,11 +49343,13 @@ function registerTools(server, ctx) {
|
|
|
49154
49343
|
}, withErrorHandling("whoami", async () => {
|
|
49155
49344
|
const jwt2 = await getValidJwt(ctx);
|
|
49156
49345
|
const user = await ctx.client.user.getCurrentUser();
|
|
49346
|
+
const authMethod = jwt2 ? "OAuth (JWT)" : "API Key";
|
|
49157
49347
|
const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
|
|
49158
|
-
const
|
|
49348
|
+
const config2 = loadConfig2();
|
|
49349
|
+
const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
|
|
49159
49350
|
return jsonResult({
|
|
49160
49351
|
email: user.email,
|
|
49161
|
-
authMethod
|
|
49352
|
+
authMethod,
|
|
49162
49353
|
teams: teams.map((t) => ({
|
|
49163
49354
|
name: t.name,
|
|
49164
49355
|
memberRole: t.memberRole,
|
|
@@ -49802,7 +49993,8 @@ function registerTools(server, ctx) {
|
|
|
49802
49993
|
}, withErrorHandling("team_list", async () => {
|
|
49803
49994
|
const jwt2 = await getValidJwt(ctx);
|
|
49804
49995
|
const teams = await ctx.client.listTeams(jwt2 ? { bearerToken: jwt2 } : undefined);
|
|
49805
|
-
const
|
|
49996
|
+
const config2 = loadConfig2();
|
|
49997
|
+
const activeTeamId = ctx.teamId ?? config2.teamId ?? teams[0]?.id;
|
|
49806
49998
|
return jsonResult({
|
|
49807
49999
|
teams: teams.map((t) => ({
|
|
49808
50000
|
id: t.id,
|
|
@@ -49822,6 +50014,9 @@ function registerTools(server, ctx) {
|
|
|
49822
50014
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
49823
50015
|
}, withErrorHandling("team_switch", async (args) => {
|
|
49824
50016
|
const jwt2 = await getValidJwt(ctx);
|
|
50017
|
+
if (!jwt2) {
|
|
50018
|
+
return errorResult("Team switching requires OAuth authentication (HTTP mode). " + "In stdio mode, re-run with a different KADOA_API_KEY or use 'kadoa login' to authenticate via the CLI.");
|
|
50019
|
+
}
|
|
49825
50020
|
const teams = await ctx.client.listTeams({ bearerToken: jwt2 });
|
|
49826
50021
|
const identifier = args.teamIdentifier;
|
|
49827
50022
|
const match = teams.find((t) => t.id === identifier || t.name.toLowerCase() === identifier.toLowerCase());
|
|
@@ -49829,11 +50024,16 @@ function registerTools(server, ctx) {
|
|
|
49829
50024
|
return errorResult(`Team not found: "${identifier}". Available teams: ${teams.map((t) => t.name).join(", ")}`);
|
|
49830
50025
|
}
|
|
49831
50026
|
await ctx.client.setActiveTeam(match.id);
|
|
49832
|
-
const newJwt = await refreshSupabaseJwt(ctx);
|
|
49833
|
-
if (!newJwt) {
|
|
49834
|
-
return errorResult("Failed to refresh session after team switch");
|
|
49835
|
-
}
|
|
49836
50027
|
ctx.teamId = match.id;
|
|
50028
|
+
try {
|
|
50029
|
+
await ctx.persist?.({ teamId: match.id });
|
|
50030
|
+
} catch (e) {
|
|
50031
|
+
console.error("[TEAM_SWITCH] WARN: persist failed:", e);
|
|
50032
|
+
}
|
|
50033
|
+
const config2 = loadConfig2();
|
|
50034
|
+
config2.teamId = match.id;
|
|
50035
|
+
config2.teamName = match.name;
|
|
50036
|
+
saveConfig(config2);
|
|
49837
50037
|
return jsonResult({
|
|
49838
50038
|
success: true,
|
|
49839
50039
|
teamId: match.id,
|
|
@@ -53895,7 +54095,11 @@ async function refreshSupabaseToken(supabaseRefreshToken, context) {
|
|
|
53895
54095
|
function jwtClaims(jwt2) {
|
|
53896
54096
|
try {
|
|
53897
54097
|
const payload = JSON.parse(Buffer.from(jwt2.split(".")[1], "base64url").toString());
|
|
53898
|
-
return {
|
|
54098
|
+
return {
|
|
54099
|
+
email: payload.email,
|
|
54100
|
+
sub: payload.sub,
|
|
54101
|
+
activeTeamId: payload.app_metadata?.active_team_id
|
|
54102
|
+
};
|
|
53899
54103
|
} catch {
|
|
53900
54104
|
return {};
|
|
53901
54105
|
}
|
|
@@ -54202,20 +54406,22 @@ class KadoaOAuthProvider {
|
|
|
54202
54406
|
} else {
|
|
54203
54407
|
console.error(`[AUTH] REFRESH_WARN: using stale Supabase JWT as fallback (${context})`);
|
|
54204
54408
|
}
|
|
54409
|
+
const freshClaims = jwtClaims(supabaseJwt);
|
|
54410
|
+
const teamId = freshClaims.activeTeamId ?? entry.teamId;
|
|
54205
54411
|
const newAccessToken = randomToken();
|
|
54206
54412
|
const newRefreshToken = randomToken();
|
|
54207
54413
|
const expiresAt = Date.now() + ACCESS_TOKEN_TTL * 1000;
|
|
54208
54414
|
await this.store.set("access_tokens", newAccessToken, {
|
|
54209
54415
|
supabaseJwt,
|
|
54210
54416
|
supabaseRefreshToken,
|
|
54211
|
-
teamId
|
|
54417
|
+
teamId,
|
|
54212
54418
|
clientId: entry.clientId,
|
|
54213
54419
|
expiresAt
|
|
54214
54420
|
}, ACCESS_TOKEN_TTL);
|
|
54215
54421
|
await this.store.set("refresh_tokens", newRefreshToken, {
|
|
54216
54422
|
supabaseJwt,
|
|
54217
54423
|
supabaseRefreshToken,
|
|
54218
|
-
teamId
|
|
54424
|
+
teamId,
|
|
54219
54425
|
clientId: entry.clientId
|
|
54220
54426
|
}, 2592000);
|
|
54221
54427
|
return {
|
|
@@ -54226,6 +54432,15 @@ class KadoaOAuthProvider {
|
|
|
54226
54432
|
};
|
|
54227
54433
|
}
|
|
54228
54434
|
async verifyAccessToken(token) {
|
|
54435
|
+
if (token.startsWith("tk-")) {
|
|
54436
|
+
return {
|
|
54437
|
+
token,
|
|
54438
|
+
clientId: "direct-api-key",
|
|
54439
|
+
scopes: [],
|
|
54440
|
+
expiresAt: Math.floor(Date.now() / 1000) + 3600,
|
|
54441
|
+
extra: { apiKey: token }
|
|
54442
|
+
};
|
|
54443
|
+
}
|
|
54229
54444
|
const entry = await this.store.get("access_tokens", token);
|
|
54230
54445
|
if (!entry) {
|
|
54231
54446
|
const sessionCount = await this.store.size("access_tokens");
|
|
@@ -54877,6 +55092,9 @@ function resolveAuth(req) {
|
|
|
54877
55092
|
console.error("[AUTH_RESOLVE] FAIL: req.auth.extra is missing");
|
|
54878
55093
|
return;
|
|
54879
55094
|
}
|
|
55095
|
+
if (typeof extra.apiKey === "string" && extra.apiKey.startsWith("tk-")) {
|
|
55096
|
+
return { kind: "apiKey", apiKey: extra.apiKey };
|
|
55097
|
+
}
|
|
54880
55098
|
if (typeof extra.supabaseJwt === "string") {
|
|
54881
55099
|
const claims = jwtClaims2(extra.supabaseJwt);
|
|
54882
55100
|
const userId = claims.sub;
|
|
@@ -54894,13 +55112,15 @@ function resolveAuth(req) {
|
|
|
54894
55112
|
}
|
|
54895
55113
|
}
|
|
54896
55114
|
return {
|
|
55115
|
+
kind: "jwt",
|
|
54897
55116
|
jwt: extra.supabaseJwt,
|
|
54898
55117
|
refreshToken: extra.supabaseRefreshToken ?? "",
|
|
54899
55118
|
teamId: extra.teamId ?? "",
|
|
54900
|
-
userId
|
|
55119
|
+
userId,
|
|
55120
|
+
mcpToken: req.auth.token
|
|
54901
55121
|
};
|
|
54902
55122
|
}
|
|
54903
|
-
console.error(`[AUTH_RESOLVE] FAIL: no supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
|
|
55123
|
+
console.error(`[AUTH_RESOLVE] FAIL: no apiKey or supabaseJwt in extra (keys: ${Object.keys(extra).join(", ")})`);
|
|
54904
55124
|
return;
|
|
54905
55125
|
}
|
|
54906
55126
|
async function startHttpServer(options) {
|
|
@@ -54950,17 +55170,37 @@ async function startHttpServer(options) {
|
|
|
54950
55170
|
});
|
|
54951
55171
|
return;
|
|
54952
55172
|
}
|
|
54953
|
-
const identity = `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...`;
|
|
55173
|
+
const identity = auth.kind === "jwt" ? `jwt:${auth.userId.slice(0, 8)}...:team=${auth.teamId.slice(0, 8)}...` : `apiKey:${auth.apiKey.slice(0, 12)}...`;
|
|
54954
55174
|
try {
|
|
54955
55175
|
console.error(`[MCP] POST method=${method} auth=${identity}`);
|
|
54956
55176
|
const transport = new StreamableHTTPServerTransport({
|
|
54957
55177
|
sessionIdGenerator: undefined
|
|
54958
55178
|
});
|
|
54959
|
-
const server = createServer({
|
|
55179
|
+
const server = auth.kind === "jwt" ? createServer({
|
|
54960
55180
|
jwt: auth.jwt,
|
|
54961
55181
|
refreshToken: auth.refreshToken,
|
|
54962
|
-
teamId: auth.teamId
|
|
54963
|
-
|
|
55182
|
+
teamId: auth.teamId,
|
|
55183
|
+
persist: async (state) => {
|
|
55184
|
+
const entry = await store.get("access_tokens", auth.mcpToken);
|
|
55185
|
+
if (!entry) {
|
|
55186
|
+
console.error(`[PERSIST] WARN: access token gone from store (token=${auth.mcpToken.slice(0, 12)}...)`);
|
|
55187
|
+
return;
|
|
55188
|
+
}
|
|
55189
|
+
const remainingMs = entry.expiresAt - Date.now();
|
|
55190
|
+
if (remainingMs <= 0) {
|
|
55191
|
+
console.error(`[PERSIST] WARN: access token already expired, skipping persist`);
|
|
55192
|
+
return;
|
|
55193
|
+
}
|
|
55194
|
+
const ttlSeconds = Math.ceil(remainingMs / 1000);
|
|
55195
|
+
await store.set("access_tokens", auth.mcpToken, {
|
|
55196
|
+
...entry,
|
|
55197
|
+
supabaseJwt: state.supabaseJwt ?? entry.supabaseJwt,
|
|
55198
|
+
supabaseRefreshToken: state.supabaseRefreshToken ?? entry.supabaseRefreshToken,
|
|
55199
|
+
teamId: state.teamId ?? entry.teamId
|
|
55200
|
+
}, ttlSeconds);
|
|
55201
|
+
console.error(`[PERSIST] OK: updated access token in store (token=${auth.mcpToken.slice(0, 12)}..., team=${state.teamId ?? "unchanged"}, ttl=${ttlSeconds}s)`);
|
|
55202
|
+
}
|
|
55203
|
+
}) : createServer({ apiKey: auth.apiKey });
|
|
54964
55204
|
await server.connect(transport);
|
|
54965
55205
|
await transport.handleRequest(req, res, req.body);
|
|
54966
55206
|
} catch (error48) {
|
|
@@ -55013,24 +55253,61 @@ var init_http2 = __esm(async () => {
|
|
|
55013
55253
|
|
|
55014
55254
|
// src/index.ts
|
|
55015
55255
|
function createServer(auth) {
|
|
55016
|
-
|
|
55017
|
-
|
|
55018
|
-
|
|
55019
|
-
|
|
55020
|
-
|
|
55021
|
-
|
|
55256
|
+
let ctx;
|
|
55257
|
+
if (typeof auth === "object" && auth !== null && "jwt" in auth) {
|
|
55258
|
+
ctx = {
|
|
55259
|
+
client: createKadoaClient({ jwt: auth.jwt, teamId: auth.teamId }),
|
|
55260
|
+
supabaseJwt: auth.jwt,
|
|
55261
|
+
supabaseRefreshToken: auth.refreshToken,
|
|
55262
|
+
teamId: auth.teamId,
|
|
55263
|
+
persist: auth.persist
|
|
55264
|
+
};
|
|
55265
|
+
} else if (typeof auth === "object" && auth !== null && "apiKey" in auth) {
|
|
55266
|
+
ctx = {
|
|
55267
|
+
client: createKadoaClient({ apiKey: auth.apiKey })
|
|
55268
|
+
};
|
|
55269
|
+
} else {
|
|
55270
|
+
ctx = {
|
|
55271
|
+
client: createKadoaClient(auth)
|
|
55272
|
+
};
|
|
55273
|
+
}
|
|
55022
55274
|
const server = new McpServer({ name: "kadoa", version: "0.3.2" });
|
|
55023
55275
|
registerTools(server, ctx);
|
|
55024
55276
|
server.server.onerror = (error48) => console.error("[MCP Error]", error48);
|
|
55025
55277
|
return server;
|
|
55026
55278
|
}
|
|
55279
|
+
async function validateApiKey() {
|
|
55280
|
+
const client = createKadoaClient();
|
|
55281
|
+
try {
|
|
55282
|
+
await client.workflow.list({ limit: 1 });
|
|
55283
|
+
} catch (error48) {
|
|
55284
|
+
if (KadoaSdkException.isInstance(error48) && error48.code === "AUTH_ERROR") {
|
|
55285
|
+
console.error("Kadoa MCP: Invalid API key. Check KADOA_API_KEY or run 'kadoa login'.");
|
|
55286
|
+
process.exit(1);
|
|
55287
|
+
}
|
|
55288
|
+
}
|
|
55289
|
+
}
|
|
55027
55290
|
var init_src = __esm(async () => {
|
|
55028
55291
|
init_mcp();
|
|
55292
|
+
init_stdio2();
|
|
55029
55293
|
init_client();
|
|
55030
55294
|
init_tools();
|
|
55031
55295
|
if (!process.env.VITEST && !process.env.BUN_TEST) {
|
|
55032
|
-
const
|
|
55033
|
-
|
|
55296
|
+
const httpMode = process.argv.includes("--http") || process.env.MCP_HTTP === "1";
|
|
55297
|
+
if (httpMode) {
|
|
55298
|
+
const { startHttpServer: startHttpServer2 } = await init_http2().then(() => exports_http);
|
|
55299
|
+
await startHttpServer2();
|
|
55300
|
+
} else {
|
|
55301
|
+
await validateApiKey();
|
|
55302
|
+
const server = createServer();
|
|
55303
|
+
const transport = new StdioServerTransport;
|
|
55304
|
+
await server.connect(transport);
|
|
55305
|
+
console.error("Kadoa MCP Server started");
|
|
55306
|
+
process.on("SIGINT", async () => {
|
|
55307
|
+
await server.close();
|
|
55308
|
+
process.exit(0);
|
|
55309
|
+
});
|
|
55310
|
+
}
|
|
55034
55311
|
}
|
|
55035
55312
|
});
|
|
55036
55313
|
await init_src();
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kadoa/mcp",
|
|
3
|
-
"version": "0.3.7-rc.
|
|
3
|
+
"version": "0.3.7-rc.4",
|
|
4
4
|
"description": "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"kadoa-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
7
10
|
"publishConfig": {
|
|
8
11
|
"access": "public"
|
|
9
12
|
},
|
|
@@ -15,7 +18,8 @@
|
|
|
15
18
|
"lint": "bunx biome check",
|
|
16
19
|
"lint:fix": "bunx biome check --write",
|
|
17
20
|
"dev": "bun src/index.ts",
|
|
18
|
-
"
|
|
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",
|