taskover-mcp 1.0.0 → 1.1.0
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/auth-flow.js +275 -0
- package/cloud-adapter.js +50 -26
- package/credential-store.js +93 -0
- package/index.js +55 -690
- package/package.json +8 -3
package/auth-flow.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const http = require("http");
|
|
3
|
+
const credentialStore = require("./credential-store.js");
|
|
4
|
+
|
|
5
|
+
const API_BASE = "https://api.taskover.gg";
|
|
6
|
+
const AUTH_PAGE_BASE = "https://taskover.gg";
|
|
7
|
+
|
|
8
|
+
function generatePKCE() {
|
|
9
|
+
const verifier = crypto.randomBytes(32).toString("base64url");
|
|
10
|
+
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
11
|
+
return { verifier, challenge };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function generateState() {
|
|
15
|
+
return crypto.randomBytes(32).toString("hex");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function openBrowser(url) {
|
|
19
|
+
try {
|
|
20
|
+
const { default: open } = await import("open");
|
|
21
|
+
await open(url);
|
|
22
|
+
return true;
|
|
23
|
+
} catch (_) {
|
|
24
|
+
const { exec } = require("child_process");
|
|
25
|
+
const cmd = process.platform === "win32" ? `start "" "${url}"`
|
|
26
|
+
: process.platform === "darwin" ? `open "${url}"`
|
|
27
|
+
: `xdg-open "${url}"`;
|
|
28
|
+
try { exec(cmd); return true; } catch (_e) { return false; }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function startCallbackServer(expectedState) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const server = http.createServer();
|
|
35
|
+
let callbackUsed = false;
|
|
36
|
+
|
|
37
|
+
server.listen(0, "127.0.0.1", () => {
|
|
38
|
+
const port = server.address().port;
|
|
39
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
40
|
+
|
|
41
|
+
const codePromise = new Promise((resolveCode, rejectCode) => {
|
|
42
|
+
server.on("request", (req, res) => {
|
|
43
|
+
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
44
|
+
|
|
45
|
+
if (url.pathname !== "/callback") {
|
|
46
|
+
res.writeHead(404).end();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (callbackUsed) {
|
|
51
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
52
|
+
res.end(htmlPage("Already processed", "This authorization has already been handled.", "#fbbf24"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const code = url.searchParams.get("code");
|
|
57
|
+
const returnedState = url.searchParams.get("state");
|
|
58
|
+
const error = url.searchParams.get("error");
|
|
59
|
+
|
|
60
|
+
if (!returnedState || returnedState !== expectedState) {
|
|
61
|
+
callbackUsed = true;
|
|
62
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
63
|
+
res.end(htmlPage("Security Error", "State parameter mismatch. This may be a CSRF attack. The authorization has been rejected.", "#ef4444"));
|
|
64
|
+
setTimeout(() => server.close(), 500);
|
|
65
|
+
rejectCode(new Error("State mismatch on callback — possible CSRF. Auth rejected."));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
callbackUsed = true;
|
|
70
|
+
|
|
71
|
+
if (error) {
|
|
72
|
+
const msg = error === "user_cancelled" ? "You cancelled the authorization." : `Auth error: ${error}`;
|
|
73
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
74
|
+
res.end(htmlPage("Cancelled", msg, "#fbbf24"));
|
|
75
|
+
setTimeout(() => server.close(), 500);
|
|
76
|
+
rejectCode(new Error(msg));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!code) {
|
|
81
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
82
|
+
res.end(htmlPage("Error", "No authorization code received.", "#ef4444"));
|
|
83
|
+
setTimeout(() => server.close(), 500);
|
|
84
|
+
rejectCode(new Error("No auth code received in callback"));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
89
|
+
res.end(htmlPage("Connected!", "You can close this tab. Your AI assistant is authenticated.", "#4ade80"));
|
|
90
|
+
setTimeout(() => server.close(), 500);
|
|
91
|
+
resolveCode(code);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
if (!callbackUsed) {
|
|
96
|
+
callbackUsed = true;
|
|
97
|
+
server.close();
|
|
98
|
+
rejectCode(new Error("Auth timed out (5 min). Restart MCP server to try again."));
|
|
99
|
+
}
|
|
100
|
+
}, 300000);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
resolve({ port, redirectUri, codePromise, server });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
server.on("error", (err) => {
|
|
107
|
+
if (err.code === "EADDRINUSE" || err.code === "EACCES") {
|
|
108
|
+
reject(new Error(`Cannot bind localhost port for auth callback: ${err.message}. Is another instance running?`));
|
|
109
|
+
} else {
|
|
110
|
+
reject(err);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function escapeHtml(s) { return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
|
117
|
+
|
|
118
|
+
function htmlPage(title, message, color) {
|
|
119
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head>`
|
|
120
|
+
+ `<body style="font-family:system-ui,sans-serif;background:#161920;color:${color};`
|
|
121
|
+
+ `display:flex;align-items:center;justify-content:center;min-height:100vh;text-align:center">`
|
|
122
|
+
+ `<div><h2>${escapeHtml(title)}</h2><p style="color:#afafba">${escapeHtml(message)}</p></div></body></html>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function exchangeCodeForTokens(code, codeVerifier, state, deviceLabel) {
|
|
126
|
+
const res = await fetch(`${API_BASE}/api/mcp/token`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
grant_type: "authorization_code",
|
|
131
|
+
code,
|
|
132
|
+
code_verifier: codeVerifier,
|
|
133
|
+
state,
|
|
134
|
+
device_label: deviceLabel,
|
|
135
|
+
}),
|
|
136
|
+
signal: AbortSignal.timeout(15000),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const data = await res.json().catch(() => ({}));
|
|
141
|
+
throw new Error(data.error || `Token exchange failed (HTTP ${res.status})`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return res.json();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function refreshAccessToken(refreshToken) {
|
|
148
|
+
const res = await fetch(`${API_BASE}/api/mcp/token`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: { "Content-Type": "application/json" },
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
grant_type: "refresh_token",
|
|
153
|
+
refresh_token: refreshToken,
|
|
154
|
+
}),
|
|
155
|
+
signal: AbortSignal.timeout(15000),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
const data = await res.json().catch(() => ({}));
|
|
160
|
+
throw new Error(data.error || `Token refresh failed (HTTP ${res.status})`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return res.json();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function authenticate(hostType) {
|
|
167
|
+
const cached = await credentialStore.read();
|
|
168
|
+
if (cached && cached.access_token && cached.refresh_token) {
|
|
169
|
+
if (cached.expires_at && Date.now() < cached.expires_at - 30000) {
|
|
170
|
+
return { accessToken: cached.access_token, refreshToken: cached.refresh_token };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const res = await fetch(`${API_BASE}/api/auth/validate`, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: { "Authorization": `Bearer ${cached.access_token}` },
|
|
177
|
+
signal: AbortSignal.timeout(5000),
|
|
178
|
+
});
|
|
179
|
+
if (res.ok) {
|
|
180
|
+
const data = await res.json();
|
|
181
|
+
return { accessToken: cached.access_token, refreshToken: cached.refresh_token, displayName: data.displayName || "user" };
|
|
182
|
+
}
|
|
183
|
+
} catch (_) {}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
console.error("[AUTH] Access token expired, refreshing...");
|
|
187
|
+
const tokens = await refreshAccessToken(cached.refresh_token);
|
|
188
|
+
await credentialStore.write({
|
|
189
|
+
access_token: tokens.access_token,
|
|
190
|
+
refresh_token: tokens.refresh_token,
|
|
191
|
+
expires_at: Date.now() + (tokens.expires_in * 1000),
|
|
192
|
+
host_type: hostType,
|
|
193
|
+
});
|
|
194
|
+
return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token };
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error(`[AUTH] Refresh failed: ${err.message}. Starting browser login...`);
|
|
197
|
+
await credentialStore.clear();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.error("[AUTH] Opening browser for login...");
|
|
202
|
+
|
|
203
|
+
const pkce = generatePKCE();
|
|
204
|
+
const state = generateState();
|
|
205
|
+
|
|
206
|
+
let callbackResult;
|
|
207
|
+
try {
|
|
208
|
+
callbackResult = await startCallbackServer(state);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error(`[AUTH] ${err.message}`);
|
|
211
|
+
console.error("[AUTH] Fallback: set TASKOVER_API_KEY environment variable for headless auth.");
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { port, redirectUri, codePromise } = callbackResult;
|
|
216
|
+
|
|
217
|
+
const authUrl = new URL(`${AUTH_PAGE_BASE}/mcp-auth`);
|
|
218
|
+
authUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
219
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
220
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
221
|
+
authUrl.searchParams.set("state", state);
|
|
222
|
+
authUrl.searchParams.set("host", hostType || "MCP");
|
|
223
|
+
|
|
224
|
+
console.error("[AUTH] If your browser doesn't open, visit:");
|
|
225
|
+
console.error(`[AUTH] ${authUrl.toString()}`);
|
|
226
|
+
|
|
227
|
+
const opened = await openBrowser(authUrl.toString());
|
|
228
|
+
if (!opened) {
|
|
229
|
+
console.error("[AUTH] Could not open browser automatically. Please open the URL above.");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.error("[AUTH] Waiting for browser authorization...");
|
|
233
|
+
let code;
|
|
234
|
+
try {
|
|
235
|
+
code = await codePromise;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error(`[AUTH] ${err.message}`);
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.error("[AUTH] Exchanging authorization code...");
|
|
242
|
+
const tokens = await exchangeCodeForTokens(code, pkce.verifier, state, hostType);
|
|
243
|
+
|
|
244
|
+
const storageType = await credentialStore.write({
|
|
245
|
+
access_token: tokens.access_token,
|
|
246
|
+
refresh_token: tokens.refresh_token,
|
|
247
|
+
expires_at: Date.now() + (tokens.expires_in * 1000),
|
|
248
|
+
host_type: hostType,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
console.error(`[AUTH] Authenticated successfully! (credentials stored in ${storageType})`);
|
|
252
|
+
return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function reauthenticate(currentRefreshToken) {
|
|
256
|
+
try {
|
|
257
|
+
const tokens = await refreshAccessToken(currentRefreshToken);
|
|
258
|
+
await credentialStore.write({
|
|
259
|
+
access_token: tokens.access_token,
|
|
260
|
+
refresh_token: tokens.refresh_token,
|
|
261
|
+
expires_at: Date.now() + (tokens.expires_in * 1000),
|
|
262
|
+
});
|
|
263
|
+
return tokens;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
await credentialStore.clear();
|
|
266
|
+
throw new Error("Session expired. Restart the MCP server to re-authenticate via browser.");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function logout() {
|
|
271
|
+
await credentialStore.clear();
|
|
272
|
+
console.error("[AUTH] Logged out. Credentials cleared.");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = { authenticate, reauthenticate, logout };
|
package/cloud-adapter.js
CHANGED
|
@@ -102,10 +102,19 @@ const MCP_ALLOWED_METHODS = new Set([...MCP_ALLOWED_READS, ...MCP_ALLOWED_WRITES
|
|
|
102
102
|
|
|
103
103
|
let _apiKey = null;
|
|
104
104
|
|
|
105
|
+
// Token-based auth state (OAuth flow — set via initWithToken)
|
|
106
|
+
let _accessToken = null;
|
|
107
|
+
let _refreshToken = null;
|
|
108
|
+
|
|
105
109
|
function init(apiKey) {
|
|
106
110
|
_apiKey = apiKey;
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
function initWithToken(accessToken, refreshToken) {
|
|
114
|
+
_accessToken = accessToken;
|
|
115
|
+
_refreshToken = refreshToken;
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
function isAllowed(method) {
|
|
110
119
|
return MCP_ALLOWED_METHODS.has(method);
|
|
111
120
|
}
|
|
@@ -114,8 +123,8 @@ function isWrite(method) {
|
|
|
114
123
|
return MCP_ALLOWED_WRITES.has(method);
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
async function
|
|
118
|
-
if (!
|
|
126
|
+
async function _doRpcCall(method, args, token) {
|
|
127
|
+
if (!token) throw new Error("Cloud adapter not initialized — no token");
|
|
119
128
|
if (!isAllowed(method)) throw new Error(`Method "${method}" is not allowed through MCP`);
|
|
120
129
|
|
|
121
130
|
const body = JSON.stringify({ method, args: args || [] });
|
|
@@ -131,7 +140,7 @@ async function callRpc(method, args) {
|
|
|
131
140
|
method: "POST",
|
|
132
141
|
headers: {
|
|
133
142
|
"Content-Type": "application/json",
|
|
134
|
-
"Authorization": `Bearer ${
|
|
143
|
+
"Authorization": `Bearer ${token}`,
|
|
135
144
|
},
|
|
136
145
|
body,
|
|
137
146
|
signal: controller.signal,
|
|
@@ -139,16 +148,22 @@ async function callRpc(method, args) {
|
|
|
139
148
|
|
|
140
149
|
clearTimeout(timer);
|
|
141
150
|
|
|
142
|
-
if (res.status === 401)
|
|
143
|
-
throw new Error("AUTH_FAILED");
|
|
144
|
-
}
|
|
151
|
+
if (res.status === 401) throw new Error("AUTH_FAILED");
|
|
145
152
|
if (res.status === 429) {
|
|
146
153
|
const retryAfter = res.headers.get("retry-after") || "60";
|
|
147
154
|
throw new Error(`RATE_LIMITED:${retryAfter}`);
|
|
148
155
|
}
|
|
149
156
|
if (!res.ok) {
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
// Privacy: extract machine-readable code only — raw error body may echo customer content.
|
|
158
|
+
let errDetail = "";
|
|
159
|
+
try {
|
|
160
|
+
const errJson = await res.json();
|
|
161
|
+
// Data Control denials have a server-generated message safe to surface (no customer content)
|
|
162
|
+
if (errJson.code && errJson.code.startsWith("DATA_CONTROL_") && errJson.message) throw new Error(errJson.message);
|
|
163
|
+
if (errJson.code) errDetail = ` code=${errJson.code}`;
|
|
164
|
+
else if (typeof errJson.error === "string" && errJson.error.length <= 60) errDetail = ` msg=${errJson.error}`;
|
|
165
|
+
} catch (e) { if (e.message && !e.message.startsWith("Cloud API")) throw e; await res.text().catch(() => ""); /* drain body */ }
|
|
166
|
+
throw new Error(`Cloud API error ${res.status}${errDetail}`);
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
// SECURITY: Enforce response size limit using byte-accurate check.
|
|
@@ -170,12 +185,29 @@ async function callRpc(method, args) {
|
|
|
170
185
|
}
|
|
171
186
|
}
|
|
172
187
|
|
|
188
|
+
async function callRpc(method, args) {
|
|
189
|
+
const token = _accessToken || _apiKey;
|
|
190
|
+
try {
|
|
191
|
+
return await _doRpcCall(method, args, token);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err.message === "AUTH_FAILED" && _accessToken && _refreshToken) {
|
|
194
|
+
try {
|
|
195
|
+
const authFlow = require("./auth-flow.js");
|
|
196
|
+
const tokens = await authFlow.reauthenticate(_refreshToken);
|
|
197
|
+
_accessToken = tokens.access_token;
|
|
198
|
+
_refreshToken = tokens.refresh_token;
|
|
199
|
+
return await _doRpcCall(method, args, _accessToken);
|
|
200
|
+
} catch (refreshErr) {
|
|
201
|
+
throw new Error("Session expired. Restart the MCP server to re-authenticate.");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
173
208
|
async function validateKey() {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Does NOT load business data (unlike listProjects which fetches all projects).
|
|
177
|
-
// /api/health is unauthenticated and must NEVER be used for key validation.
|
|
178
|
-
if (!_apiKey) return { valid: false, error: "No API key configured" };
|
|
209
|
+
const token = _accessToken || _apiKey;
|
|
210
|
+
if (!token) return { valid: false, error: "No token or API key configured" };
|
|
179
211
|
|
|
180
212
|
const controller = new AbortController();
|
|
181
213
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
@@ -183,35 +215,27 @@ async function validateKey() {
|
|
|
183
215
|
try {
|
|
184
216
|
const res = await fetch(`${API_BASE}/api/auth/validate`, {
|
|
185
217
|
method: "POST",
|
|
186
|
-
headers: {
|
|
187
|
-
"Content-Type": "application/json",
|
|
188
|
-
"Authorization": `Bearer ${_apiKey}`,
|
|
189
|
-
},
|
|
218
|
+
headers: { "Authorization": `Bearer ${token}` },
|
|
190
219
|
signal: controller.signal,
|
|
191
220
|
});
|
|
192
221
|
|
|
193
222
|
clearTimeout(timer);
|
|
194
223
|
|
|
195
|
-
if (res.status === 401) {
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
if (!res.ok) {
|
|
199
|
-
return { valid: false, error: `Unexpected response from api.taskover.gg (HTTP ${res.status})` };
|
|
200
|
-
}
|
|
224
|
+
if (res.status === 401) return { valid: false, error: "Token/key invalid or expired." };
|
|
225
|
+
if (!res.ok) return { valid: false, error: `Unexpected response (HTTP ${res.status})` };
|
|
201
226
|
|
|
202
227
|
const data = await res.json();
|
|
203
228
|
return { valid: true, displayName: data.displayName || "user" };
|
|
204
229
|
} catch (err) {
|
|
205
230
|
clearTimeout(timer);
|
|
206
|
-
if (err.name === "AbortError") {
|
|
207
|
-
return { valid: false, error: "Cannot reach api.taskover.gg (timeout). Check your internet connection." };
|
|
208
|
-
}
|
|
231
|
+
if (err.name === "AbortError") return { valid: false, error: "Cannot reach api.taskover.gg (timeout)" };
|
|
209
232
|
return { valid: false, error: `Cannot reach api.taskover.gg: ${err.message}` };
|
|
210
233
|
}
|
|
211
234
|
}
|
|
212
235
|
|
|
213
236
|
module.exports = {
|
|
214
237
|
init,
|
|
238
|
+
initWithToken,
|
|
215
239
|
isAllowed,
|
|
216
240
|
isWrite,
|
|
217
241
|
callRpc,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
|
|
5
|
+
const SERVICE_NAME = "taskover-mcp";
|
|
6
|
+
const ACCOUNT_NAME = "default";
|
|
7
|
+
const AUTH_DIR = path.join(os.homedir(), ".taskover");
|
|
8
|
+
const TOKEN_FILE = path.join(AUTH_DIR, "auth.json");
|
|
9
|
+
|
|
10
|
+
let _keytar = null;
|
|
11
|
+
let _keytarAvailable = null;
|
|
12
|
+
|
|
13
|
+
async function _getKeytar() {
|
|
14
|
+
if (_keytarAvailable === false) return null;
|
|
15
|
+
if (_keytar) return _keytar;
|
|
16
|
+
try {
|
|
17
|
+
_keytar = require("keytar");
|
|
18
|
+
await _keytar.getPassword(SERVICE_NAME, "__probe__");
|
|
19
|
+
_keytarAvailable = true;
|
|
20
|
+
return _keytar;
|
|
21
|
+
} catch (_) {
|
|
22
|
+
_keytarAvailable = false;
|
|
23
|
+
_keytar = null;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function _ensureAuthDir() {
|
|
29
|
+
if (!fs.existsSync(AUTH_DIR)) {
|
|
30
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _readFile() {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(TOKEN_FILE, "utf8"));
|
|
37
|
+
} catch (_) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _writeFile(data) {
|
|
43
|
+
_ensureAuthDir();
|
|
44
|
+
fs.writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _deleteFile() {
|
|
48
|
+
try { fs.unlinkSync(TOKEN_FILE); } catch (_) {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function read() {
|
|
52
|
+
const kt = await _getKeytar();
|
|
53
|
+
if (kt) {
|
|
54
|
+
try {
|
|
55
|
+
const raw = await kt.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
56
|
+
if (raw) return JSON.parse(raw);
|
|
57
|
+
} catch (_) {}
|
|
58
|
+
}
|
|
59
|
+
return _readFile();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function write(tokens) {
|
|
63
|
+
const kt = await _getKeytar();
|
|
64
|
+
if (kt) {
|
|
65
|
+
try {
|
|
66
|
+
await kt.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(tokens));
|
|
67
|
+
// Metadata-only stub — NO tokens or secrets in the file when keychain is available
|
|
68
|
+
_writeFile({ host_type: tokens.host_type, storage: "keychain", expires_at: tokens.expires_at, issued_at: Date.now() });
|
|
69
|
+
return "keychain";
|
|
70
|
+
} catch (_) {}
|
|
71
|
+
}
|
|
72
|
+
_writeFile(tokens);
|
|
73
|
+
if (process.platform === "win32") {
|
|
74
|
+
console.error("[AUTH] Tokens stored in " + TOKEN_FILE);
|
|
75
|
+
console.error("[AUTH] Ensure this directory is not shared or synced to cloud storage.");
|
|
76
|
+
}
|
|
77
|
+
return "file";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function clear() {
|
|
81
|
+
const kt = await _getKeytar();
|
|
82
|
+
if (kt) {
|
|
83
|
+
try { await kt.deletePassword(SERVICE_NAME, ACCOUNT_NAME); } catch (_) {}
|
|
84
|
+
}
|
|
85
|
+
_deleteFile();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function getStorageType() {
|
|
89
|
+
const kt = await _getKeytar();
|
|
90
|
+
return kt ? "keychain" : "file";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { read, write, clear, getStorageType };
|
package/index.js
CHANGED
|
@@ -7,21 +7,9 @@ const {
|
|
|
7
7
|
CallToolRequestSchema,
|
|
8
8
|
} = require("@modelcontextprotocol/sdk/types.js");
|
|
9
9
|
|
|
10
|
-
// SECURITY: Mode selection
|
|
10
|
+
// SECURITY: Mode selection — API key (headless) or browser auth (default).
|
|
11
11
|
const CLOUD_MODE = !!process.env.TASKOVER_API_KEY;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
if (!CLOUD_MODE && !LOCAL_MODE) {
|
|
15
|
-
console.error("[ERROR] No API key found.");
|
|
16
|
-
console.error("Set TASKOVER_API_KEY environment variable.");
|
|
17
|
-
console.error("Get your key at taskover.gg -> Settings -> Security.");
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (LOCAL_MODE) {
|
|
22
|
-
console.error("[DEV MODE] Using local SQLite database. This mode is not for production use.");
|
|
23
|
-
console.error("[DEV MODE] Set TASKOVER_API_KEY for cloud mode.");
|
|
24
|
-
}
|
|
12
|
+
const BROWSER_AUTH_MODE = !CLOUD_MODE;
|
|
25
13
|
|
|
26
14
|
// SECURITY: Refuse API key passed as CLI argument
|
|
27
15
|
if (process.argv.some(arg => arg.startsWith("tok_"))) {
|
|
@@ -30,18 +18,14 @@ if (process.argv.some(arg => arg.startsWith("tok_"))) {
|
|
|
30
18
|
process.exit(1);
|
|
31
19
|
}
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
|
|
21
|
+
const cloudAdapter = require("./cloud-adapter.js");
|
|
22
|
+
const { TOOL_MAP: toolMap, validateSchema } = require("./tool-map.js");
|
|
23
|
+
let authFlow;
|
|
35
24
|
|
|
36
25
|
if (CLOUD_MODE) {
|
|
37
|
-
cloudAdapter = require("./cloud-adapter.js");
|
|
38
|
-
const tm = require("./tool-map.js");
|
|
39
|
-
toolMap = tm.TOOL_MAP;
|
|
40
|
-
validateSchema = tm.validateSchema;
|
|
41
26
|
cloudAdapter.init(process.env.TASKOVER_API_KEY);
|
|
42
27
|
} else {
|
|
43
|
-
|
|
44
|
-
({ initDb, reloadFromDisk } = require("./db"));
|
|
28
|
+
authFlow = require("./auth-flow.js");
|
|
45
29
|
}
|
|
46
30
|
|
|
47
31
|
const server = new Server(
|
|
@@ -94,10 +78,10 @@ const TOOLS = [
|
|
|
94
78
|
},
|
|
95
79
|
{
|
|
96
80
|
name: "taskovergg_add_project",
|
|
97
|
-
description: `DO NOT call this tool directly. Projects must be created by the user
|
|
81
|
+
description: `DO NOT call this tool directly. Projects must be created by the user on the TaskOver website.
|
|
98
82
|
|
|
99
83
|
Instead of calling this tool, instruct the user with these steps:
|
|
100
|
-
1.
|
|
84
|
+
1. Go to taskover.gg and log in
|
|
101
85
|
2. Click the project dropdown in the top-left of the sidebar
|
|
102
86
|
3. Click "+ New Project" at the bottom of the dropdown
|
|
103
87
|
4. Follow the 5-step project creation wizard:
|
|
@@ -1368,701 +1352,82 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1368
1352
|
});
|
|
1369
1353
|
|
|
1370
1354
|
// ===== PROJECT SCOPE GUARD =====
|
|
1371
|
-
const EXEMPT_TOOLS = new Set(["taskovergg_list_projects", "taskovergg_add_project"]);
|
|
1372
|
-
|
|
1373
|
-
function getActiveProject() {
|
|
1374
|
-
return store.getSetting("active_project");
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
1355
|
// ===== HANDLE TOOL CALLS =====
|
|
1378
1356
|
|
|
1379
1357
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1380
1358
|
const { name, arguments: args } = request.params;
|
|
1381
1359
|
|
|
1382
|
-
|
|
1383
|
-
if (
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
}
|
|
1388
|
-
if (!cloudAdapter.isAllowed(mapping.rpc)) {
|
|
1389
|
-
return { content: [{ type: "text", text: `Method "${mapping.rpc}" is not available in cloud mode` }] };
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
// SECURITY: Schema validation -- strips unknown fields, checks required + types
|
|
1393
|
-
const raw = args || {};
|
|
1394
|
-
const { errors, cleaned } = validateSchema(raw, mapping.schema);
|
|
1395
|
-
if (errors.length > 0) {
|
|
1396
|
-
return { content: [{ type: "text", text: `Validation error: ${errors.join("; ")}` }] };
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
try {
|
|
1400
|
-
const rpcArgs = mapping.args(cleaned, raw);
|
|
1401
|
-
const result = await cloudAdapter.callRpc(mapping.rpc, rpcArgs);
|
|
1402
|
-
return { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }] };
|
|
1403
|
-
} catch (err) {
|
|
1404
|
-
if (err.message === "AUTH_FAILED") {
|
|
1405
|
-
console.error("[ERROR] API key rejected. Shutting down.");
|
|
1406
|
-
process.exit(1);
|
|
1407
|
-
}
|
|
1408
|
-
if (err.message.startsWith("RATE_LIMITED:")) {
|
|
1409
|
-
const secs = err.message.split(":")[1];
|
|
1410
|
-
return { content: [{ type: "text", text: `Rate limited. Try again in ${secs} seconds.` }] };
|
|
1411
|
-
}
|
|
1412
|
-
return { content: [{ type: "text", text: `Error: ${err.message}` }] };
|
|
1413
|
-
}
|
|
1360
|
+
const mapping = toolMap[name];
|
|
1361
|
+
if (!mapping) {
|
|
1362
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
1363
|
+
}
|
|
1364
|
+
if (!cloudAdapter.isAllowed(mapping.rpc)) {
|
|
1365
|
+
return { content: [{ type: "text", text: `Method "${mapping.rpc}" is not available` }] };
|
|
1414
1366
|
}
|
|
1415
1367
|
|
|
1416
|
-
//
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
// Auto-resolve project_id: if a tool needs a project_id, use the active project from TaskOver
|
|
1422
|
-
if (!EXEMPT_TOOLS.has(name)) {
|
|
1423
|
-
const activeProject = getActiveProject();
|
|
1424
|
-
if (activeProject) {
|
|
1425
|
-
// Always use the active project — no guessing needed from the AI
|
|
1426
|
-
args.project_id = activeProject;
|
|
1427
|
-
}
|
|
1368
|
+
// SECURITY: Schema validation -- strips unknown fields, checks required + types
|
|
1369
|
+
const raw = args || {};
|
|
1370
|
+
const { errors, cleaned } = validateSchema(raw, mapping.schema);
|
|
1371
|
+
if (errors.length > 0) {
|
|
1372
|
+
return { content: [{ type: "text", text: `Validation error: ${errors.join("; ")}` }] };
|
|
1428
1373
|
}
|
|
1429
1374
|
|
|
1430
1375
|
try {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
// Dashboard & Context
|
|
1435
|
-
case "taskovergg_dashboard":
|
|
1436
|
-
result = store.dashboard(args.project_id);
|
|
1437
|
-
break;
|
|
1438
|
-
case "taskovergg_context_export":
|
|
1439
|
-
result = store.contextExport(args.project_id);
|
|
1440
|
-
break;
|
|
1441
|
-
case "taskovergg_search":
|
|
1442
|
-
result = store.search(args.project_id, args.query);
|
|
1443
|
-
break;
|
|
1444
|
-
|
|
1445
|
-
// Projects
|
|
1446
|
-
case "taskovergg_list_projects":
|
|
1447
|
-
result = store.listProjects();
|
|
1448
|
-
break;
|
|
1449
|
-
case "taskovergg_add_project":
|
|
1450
|
-
result = {
|
|
1451
|
-
error: "Projects must be created in the TaskOver desktop app — not via MCP.",
|
|
1452
|
-
instructions: [
|
|
1453
|
-
"1. Open the TaskOver desktop app",
|
|
1454
|
-
"2. Click the project dropdown in the top-left of the sidebar",
|
|
1455
|
-
"3. Click '+ New Project' at the bottom of the dropdown",
|
|
1456
|
-
"4. Follow the 5-step project creation wizard (name, color/icon, systems, settings, review)",
|
|
1457
|
-
"5. Once the project appears in the list, come back and tell me the project name",
|
|
1458
|
-
"6. I'll then use taskovergg_list_projects to get the project_id and start adding tasks, systems, etc."
|
|
1459
|
-
],
|
|
1460
|
-
hint: "After the user creates the project, use taskovergg_list_projects to find the project_id."
|
|
1461
|
-
};
|
|
1462
|
-
break;
|
|
1463
|
-
|
|
1464
|
-
// Tasks
|
|
1465
|
-
case "taskovergg_get_tasks":
|
|
1466
|
-
result = store.getTasks(args.project_id, args.status, args.phase);
|
|
1467
|
-
break;
|
|
1468
|
-
case "taskovergg_add_task": {
|
|
1469
|
-
const DEFAULT_STATUSES = new Set(["todo", "doing", "done", "scrapped"]);
|
|
1470
|
-
const taskStatus = (args.status || "todo").toLowerCase();
|
|
1471
|
-
|
|
1472
|
-
// If the status matches a custom column, add as a card to that column instead
|
|
1473
|
-
if (!DEFAULT_STATUSES.has(taskStatus) && args.project_id) {
|
|
1474
|
-
const proj = store.getProject(args.project_id);
|
|
1475
|
-
const customCols = proj?.customBoardCols || [];
|
|
1476
|
-
const matchCol = customCols.find(cc =>
|
|
1477
|
-
cc.name.toLowerCase() === taskStatus ||
|
|
1478
|
-
cc.tag === taskStatus ||
|
|
1479
|
-
String(cc.id) === taskStatus
|
|
1480
|
-
);
|
|
1481
|
-
if (matchCol) {
|
|
1482
|
-
const newCard = {
|
|
1483
|
-
id: Date.now(),
|
|
1484
|
-
title: args.title,
|
|
1485
|
-
description: args.description || "",
|
|
1486
|
-
priority: args.priority || "medium",
|
|
1487
|
-
tags: args.tags || [],
|
|
1488
|
-
createdBy: "Assistant",
|
|
1489
|
-
createdAt: new Date().toISOString(),
|
|
1490
|
-
};
|
|
1491
|
-
const updCols = customCols.map(cc =>
|
|
1492
|
-
String(cc.id) === String(matchCol.id) ? { ...cc, cards: [...(cc.cards || []), newCard] } : cc
|
|
1493
|
-
);
|
|
1494
|
-
store.updateProjectField(args.project_id, "customBoardCols", updCols);
|
|
1495
|
-
result = { ...newCard, column: matchCol.name };
|
|
1496
|
-
break;
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
result = store.addTask({
|
|
1501
|
-
projectId: args.project_id, title: args.title, description: args.description,
|
|
1502
|
-
status: args.status, phase: args.phase, priority: args.priority,
|
|
1503
|
-
tags: args.tags, checklist: args.checklist, createdBy: "Assistant",
|
|
1504
|
-
});
|
|
1505
|
-
break;
|
|
1506
|
-
}
|
|
1507
|
-
case "taskovergg_update_task":
|
|
1508
|
-
if (!args.task_id) { result = { error: "task_id is required" }; break; }
|
|
1509
|
-
result = store.updateTask(args.task_id, {
|
|
1510
|
-
title: args.title, description: args.description, status: args.status,
|
|
1511
|
-
phase: args.phase, priority: args.priority, tags: args.tags, checklist: args.checklist,
|
|
1512
|
-
});
|
|
1513
|
-
if (!result) { result = { error: `Task not found: ${args.task_id}` }; }
|
|
1514
|
-
break;
|
|
1515
|
-
case "taskovergg_move_task":
|
|
1516
|
-
result = store.moveTask(args.task_id, args.status);
|
|
1517
|
-
break;
|
|
1518
|
-
case "taskovergg_add_task_comment":
|
|
1519
|
-
result = store.addTaskComment(args.task_id, args.text);
|
|
1520
|
-
break;
|
|
1521
|
-
|
|
1522
|
-
// Systems
|
|
1523
|
-
case "taskovergg_get_systems":
|
|
1524
|
-
result = store.getSystems(args.project_id);
|
|
1525
|
-
break;
|
|
1526
|
-
case "taskovergg_add_system":
|
|
1527
|
-
result = store.addSystem({
|
|
1528
|
-
projectId: args.project_id, name: args.name, description: args.description,
|
|
1529
|
-
status: args.status, keyFiles: args.key_files, notes: args.notes,
|
|
1530
|
-
criticalRules: args.critical_rules,
|
|
1531
|
-
});
|
|
1532
|
-
break;
|
|
1533
|
-
case "taskovergg_update_system":
|
|
1534
|
-
result = store.updateSystem(args.system_id, {
|
|
1535
|
-
name: args.name, description: args.description, status: args.status,
|
|
1536
|
-
keyFiles: args.key_files, notes: args.notes, criticalRules: args.critical_rules,
|
|
1537
|
-
});
|
|
1538
|
-
break;
|
|
1539
|
-
|
|
1540
|
-
// Sessions
|
|
1541
|
-
case "taskovergg_get_sessions":
|
|
1542
|
-
result = store.getSessions(args.project_id, args.limit);
|
|
1543
|
-
break;
|
|
1544
|
-
case "taskovergg_log_session":
|
|
1545
|
-
result = store.logSession({
|
|
1546
|
-
projectId: args.project_id, title: args.title, summary: args.summary,
|
|
1547
|
-
whatWeDid: args.what_we_did, whereWeLeftOff: args.where_we_left_off,
|
|
1548
|
-
nextSteps: args.next_steps, systemsWorkedOn: args.systems_worked_on,
|
|
1549
|
-
});
|
|
1550
|
-
break;
|
|
1551
|
-
|
|
1552
|
-
// Changelog
|
|
1553
|
-
case "taskovergg_get_changelog":
|
|
1554
|
-
result = store.getChangelog(args.project_id, args.limit);
|
|
1555
|
-
break;
|
|
1556
|
-
case "taskovergg_add_changelog":
|
|
1557
|
-
result = store.addChangelog({
|
|
1558
|
-
projectId: args.project_id, title: args.title,
|
|
1559
|
-
changes: args.changes, systemsAffected: args.systems_affected,
|
|
1560
|
-
});
|
|
1561
|
-
break;
|
|
1562
|
-
|
|
1563
|
-
// Bugs
|
|
1564
|
-
case "taskovergg_get_bugs":
|
|
1565
|
-
result = store.getBugs(args.project_id, args.status);
|
|
1566
|
-
break;
|
|
1567
|
-
case "taskovergg_add_bug":
|
|
1568
|
-
result = store.addBug({
|
|
1569
|
-
projectId: args.project_id, systemId: args.system_id, title: args.title,
|
|
1570
|
-
description: args.description, priority: args.priority,
|
|
1571
|
-
stepsToReproduce: args.steps_to_reproduce,
|
|
1572
|
-
});
|
|
1573
|
-
break;
|
|
1574
|
-
case "taskovergg_fix_bug":
|
|
1575
|
-
result = store.fixBug(args.bug_id, args.fix_description);
|
|
1576
|
-
break;
|
|
1577
|
-
|
|
1578
|
-
// Decisions
|
|
1579
|
-
case "taskovergg_get_decisions":
|
|
1580
|
-
result = store.getDecisions(args.project_id);
|
|
1581
|
-
break;
|
|
1582
|
-
case "taskovergg_log_decision":
|
|
1583
|
-
result = store.logDecision({
|
|
1584
|
-
projectId: args.project_id, decision: args.decision, context: args.context,
|
|
1585
|
-
systemId: args.system_id, alternatives: args.alternatives,
|
|
1586
|
-
});
|
|
1587
|
-
break;
|
|
1588
|
-
|
|
1589
|
-
// Blueprints
|
|
1590
|
-
case "taskovergg_get_blueprints":
|
|
1591
|
-
result = store.getBlueprints(args.project_id);
|
|
1592
|
-
break;
|
|
1593
|
-
case "taskovergg_add_blueprint":
|
|
1594
|
-
result = store.addBlueprint({
|
|
1595
|
-
projectId: args.project_id, name: args.name, path: args.path,
|
|
1596
|
-
parentClass: args.parent_class, systemId: args.system_id,
|
|
1597
|
-
description: args.description, variables: args.variables,
|
|
1598
|
-
keyFunctions: args.key_functions,
|
|
1599
|
-
});
|
|
1600
|
-
break;
|
|
1601
|
-
case "taskovergg_update_blueprint":
|
|
1602
|
-
result = store.updateBlueprint(args.blueprint_id, {
|
|
1603
|
-
name: args.name, path: args.path, parentClass: args.parent_class,
|
|
1604
|
-
description: args.description, variables: args.variables,
|
|
1605
|
-
keyFunctions: args.key_functions,
|
|
1606
|
-
});
|
|
1607
|
-
break;
|
|
1608
|
-
|
|
1609
|
-
// Blueprint Graphs
|
|
1610
|
-
case "taskovergg_get_blueprint_graphs":
|
|
1611
|
-
result = store.getBlueprintGraphs(args.blueprint_id);
|
|
1612
|
-
break;
|
|
1613
|
-
case "taskovergg_set_blueprint_graph":
|
|
1614
|
-
result = store.setBlueprintGraph(args.blueprint_id, {
|
|
1615
|
-
name: args.graph_name, type: args.graph_type || "event",
|
|
1616
|
-
nodes: args.nodes, connections: args.connections,
|
|
1617
|
-
});
|
|
1618
|
-
break;
|
|
1619
|
-
case "taskovergg_add_graph_nodes":
|
|
1620
|
-
result = store.addGraphNodes(args.blueprint_id, args.graph_name, {
|
|
1621
|
-
nodes: args.nodes || [], connections: args.connections || [],
|
|
1622
|
-
});
|
|
1623
|
-
break;
|
|
1624
|
-
|
|
1625
|
-
// Notes
|
|
1626
|
-
case "taskovergg_get_notes":
|
|
1627
|
-
result = store.getNotes(args.project_id, args.parent_type, args.parent_id);
|
|
1628
|
-
break;
|
|
1629
|
-
case "taskovergg_add_note":
|
|
1630
|
-
result = store.addNote({
|
|
1631
|
-
projectId: args.project_id, parentType: args.parent_type,
|
|
1632
|
-
parentId: args.parent_id, title: args.title, content: args.content,
|
|
1633
|
-
});
|
|
1634
|
-
break;
|
|
1635
|
-
|
|
1636
|
-
// Backups
|
|
1637
|
-
case "taskovergg_log_backup":
|
|
1638
|
-
result = store.logBackup({
|
|
1639
|
-
projectId: args.project_id, title: args.title, description: args.description,
|
|
1640
|
-
});
|
|
1641
|
-
break;
|
|
1642
|
-
|
|
1643
|
-
// Levels
|
|
1644
|
-
case "taskovergg_get_levels":
|
|
1645
|
-
result = store.getLevels(args.project_id);
|
|
1646
|
-
break;
|
|
1647
|
-
case "taskovergg_add_level":
|
|
1648
|
-
result = store.addLevel({
|
|
1649
|
-
projectId: args.project_id, name: args.name, map: args.map,
|
|
1650
|
-
actorCount: args.actor_count, status: args.status,
|
|
1651
|
-
});
|
|
1652
|
-
break;
|
|
1653
|
-
case "taskovergg_update_level":
|
|
1654
|
-
result = store.updateLevel(args.level_id, {
|
|
1655
|
-
name: args.name, map: args.map, actorCount: args.actor_count, status: args.status,
|
|
1656
|
-
});
|
|
1657
|
-
break;
|
|
1658
|
-
case "taskovergg_add_actor":
|
|
1659
|
-
result = store.addActor(args.level_id, {
|
|
1660
|
-
name: args.name, x: args.x, y: args.y, z: args.z, notes: args.notes,
|
|
1661
|
-
});
|
|
1662
|
-
break;
|
|
1663
|
-
case "taskovergg_remove_actor":
|
|
1664
|
-
result = store.removeActor(args.level_id, args.actor_index);
|
|
1665
|
-
break;
|
|
1666
|
-
|
|
1667
|
-
// Plugins
|
|
1668
|
-
case "taskovergg_get_plugins":
|
|
1669
|
-
result = store.getPlugins(args.project_id);
|
|
1670
|
-
break;
|
|
1671
|
-
case "taskovergg_add_plugin":
|
|
1672
|
-
result = store.addPlugin({
|
|
1673
|
-
projectId: args.project_id, name: args.name, version: args.version,
|
|
1674
|
-
source: args.source, dependents: args.dependents,
|
|
1675
|
-
});
|
|
1676
|
-
break;
|
|
1677
|
-
case "taskovergg_update_plugin":
|
|
1678
|
-
result = store.updatePlugin(args.plugin_id, {
|
|
1679
|
-
name: args.name, version: args.version, source: args.source, dependents: args.dependents,
|
|
1680
|
-
});
|
|
1681
|
-
break;
|
|
1682
|
-
case "taskovergg_delete_plugin":
|
|
1683
|
-
result = store.deletePlugin(args.plugin_id);
|
|
1684
|
-
break;
|
|
1685
|
-
|
|
1686
|
-
// Build Errors
|
|
1687
|
-
case "taskovergg_get_build_errors":
|
|
1688
|
-
result = store.getBuildErrors(args.project_id, args.status);
|
|
1689
|
-
break;
|
|
1690
|
-
case "taskovergg_add_build_error":
|
|
1691
|
-
result = store.addBuildError({
|
|
1692
|
-
projectId: args.project_id, error: args.error, bp: args.bp,
|
|
1693
|
-
});
|
|
1694
|
-
break;
|
|
1695
|
-
case "taskovergg_fix_build_error":
|
|
1696
|
-
result = store.fixBuildError(args.error_id, args.fix);
|
|
1697
|
-
break;
|
|
1698
|
-
|
|
1699
|
-
// Optimize Items
|
|
1700
|
-
case "taskovergg_get_optimize_items":
|
|
1701
|
-
result = store.getOptimizeItems(args.project_id);
|
|
1702
|
-
break;
|
|
1703
|
-
case "taskovergg_add_optimize_item":
|
|
1704
|
-
result = store.addOptimizeItem({
|
|
1705
|
-
projectId: args.project_id, title: args.title, category: args.category,
|
|
1706
|
-
status: args.status, savings: args.savings,
|
|
1707
|
-
});
|
|
1708
|
-
break;
|
|
1709
|
-
case "taskovergg_update_optimize_item":
|
|
1710
|
-
result = store.updateOptimizeItem(args.item_id, {
|
|
1711
|
-
title: args.title, category: args.category, status: args.status, savings: args.savings,
|
|
1712
|
-
});
|
|
1713
|
-
break;
|
|
1714
|
-
|
|
1715
|
-
// Perf Budget
|
|
1716
|
-
case "taskovergg_get_perf_budget":
|
|
1717
|
-
result = store.getPerfBudget(args.project_id);
|
|
1718
|
-
break;
|
|
1719
|
-
case "taskovergg_add_perf_budget":
|
|
1720
|
-
result = store.addPerfBudget({
|
|
1721
|
-
projectId: args.project_id, scene: args.scene, fps: args.fps,
|
|
1722
|
-
gpu: args.gpu, memory: args.memory, notes: args.notes,
|
|
1723
|
-
});
|
|
1724
|
-
break;
|
|
1725
|
-
|
|
1726
|
-
// Playtests
|
|
1727
|
-
case "taskovergg_get_playtests":
|
|
1728
|
-
result = store.getPlaytests(args.project_id);
|
|
1729
|
-
break;
|
|
1730
|
-
case "taskovergg_add_playtest":
|
|
1731
|
-
result = store.addPlaytest({
|
|
1732
|
-
projectId: args.project_id, duration: args.duration, issues: args.issues,
|
|
1733
|
-
rating: args.rating, notes: args.notes, tester: args.tester,
|
|
1734
|
-
buildVersion: args.build_version,
|
|
1735
|
-
});
|
|
1736
|
-
break;
|
|
1737
|
-
|
|
1738
|
-
// Milestones
|
|
1739
|
-
case "taskovergg_get_milestones":
|
|
1740
|
-
result = store.getMilestones(args.project_id);
|
|
1741
|
-
break;
|
|
1742
|
-
case "taskovergg_add_milestone":
|
|
1743
|
-
result = store.addMilestone({
|
|
1744
|
-
projectId: args.project_id, title: args.title, date: args.date,
|
|
1745
|
-
status: args.status, notes: args.notes,
|
|
1746
|
-
});
|
|
1747
|
-
break;
|
|
1748
|
-
case "taskovergg_update_milestone":
|
|
1749
|
-
result = store.updateMilestone(args.milestone_id, {
|
|
1750
|
-
title: args.title, date: args.date, status: args.status, notes: args.notes,
|
|
1751
|
-
});
|
|
1752
|
-
break;
|
|
1753
|
-
|
|
1754
|
-
// Iterations
|
|
1755
|
-
case "taskovergg_get_iterations":
|
|
1756
|
-
result = store.getIterations(args.project_id);
|
|
1757
|
-
break;
|
|
1758
|
-
case "taskovergg_add_iteration":
|
|
1759
|
-
result = store.addIteration({
|
|
1760
|
-
projectId: args.project_id, feature: args.feature, version: args.version,
|
|
1761
|
-
change: args.change, reason: args.reason,
|
|
1762
|
-
});
|
|
1763
|
-
break;
|
|
1764
|
-
|
|
1765
|
-
// Dialogues
|
|
1766
|
-
case "taskovergg_get_dialogues":
|
|
1767
|
-
result = store.getDialogues(args.project_id);
|
|
1768
|
-
break;
|
|
1769
|
-
case "taskovergg_add_dialogue":
|
|
1770
|
-
result = store.addDialogue({
|
|
1771
|
-
projectId: args.project_id, npc: args.npc, line: args.line,
|
|
1772
|
-
choices: args.choices, nextNode: args.next_node, notes: args.notes,
|
|
1773
|
-
});
|
|
1774
|
-
break;
|
|
1775
|
-
case "taskovergg_update_dialogue":
|
|
1776
|
-
result = store.updateDialogue(args.dialogue_id, {
|
|
1777
|
-
npc: args.npc, line: args.line, choices: args.choices,
|
|
1778
|
-
nextNode: args.next_node, notes: args.notes,
|
|
1779
|
-
});
|
|
1780
|
-
break;
|
|
1781
|
-
|
|
1782
|
-
// Sounds
|
|
1783
|
-
case "taskovergg_get_sounds":
|
|
1784
|
-
result = store.getSounds(args.project_id);
|
|
1785
|
-
break;
|
|
1786
|
-
case "taskovergg_add_sound":
|
|
1787
|
-
result = store.addSound({
|
|
1788
|
-
projectId: args.project_id, name: args.name, actor: args.actor,
|
|
1789
|
-
trigger: args.trigger, spatial: args.spatial, attenuation: args.attenuation,
|
|
1790
|
-
notes: args.notes,
|
|
1791
|
-
});
|
|
1792
|
-
break;
|
|
1793
|
-
case "taskovergg_update_sound":
|
|
1794
|
-
result = store.updateSound(args.sound_id, {
|
|
1795
|
-
name: args.name, actor: args.actor, trigger: args.trigger,
|
|
1796
|
-
spatial: args.spatial, attenuation: args.attenuation, notes: args.notes,
|
|
1797
|
-
});
|
|
1798
|
-
break;
|
|
1799
|
-
|
|
1800
|
-
// Controls
|
|
1801
|
-
case "taskovergg_get_controls":
|
|
1802
|
-
result = store.getControls(args.project_id);
|
|
1803
|
-
break;
|
|
1804
|
-
case "taskovergg_add_control":
|
|
1805
|
-
result = store.addControl({
|
|
1806
|
-
projectId: args.project_id, key: args.key, action: args.action,
|
|
1807
|
-
context: args.context, condition: args.condition,
|
|
1808
|
-
});
|
|
1809
|
-
break;
|
|
1810
|
-
case "taskovergg_update_control":
|
|
1811
|
-
result = store.updateControl(args.control_id, {
|
|
1812
|
-
key: args.key, action: args.action, context: args.context, condition: args.condition,
|
|
1813
|
-
});
|
|
1814
|
-
break;
|
|
1815
|
-
|
|
1816
|
-
// Assets
|
|
1817
|
-
case "taskovergg_get_assets":
|
|
1818
|
-
result = store.getAssets(args.project_id);
|
|
1819
|
-
break;
|
|
1820
|
-
case "taskovergg_add_asset":
|
|
1821
|
-
result = store.addAsset({
|
|
1822
|
-
projectId: args.project_id, name: args.name, type: args.type,
|
|
1823
|
-
path: args.path, size: args.size, status: args.status, usedBy: args.used_by,
|
|
1824
|
-
});
|
|
1825
|
-
break;
|
|
1826
|
-
case "taskovergg_update_asset":
|
|
1827
|
-
result = store.updateAsset(args.asset_id, {
|
|
1828
|
-
name: args.name, type: args.type, path: args.path,
|
|
1829
|
-
size: args.size, status: args.status, usedBy: args.used_by,
|
|
1830
|
-
});
|
|
1831
|
-
break;
|
|
1832
|
-
|
|
1833
|
-
// References
|
|
1834
|
-
case "taskovergg_get_refs":
|
|
1835
|
-
result = store.getRefs(args.project_id);
|
|
1836
|
-
break;
|
|
1837
|
-
case "taskovergg_add_ref":
|
|
1838
|
-
result = store.addRef({
|
|
1839
|
-
projectId: args.project_id, title: args.title, type: args.type,
|
|
1840
|
-
url: args.url, notes: args.notes,
|
|
1841
|
-
});
|
|
1842
|
-
break;
|
|
1843
|
-
|
|
1844
|
-
// Marketing
|
|
1845
|
-
case "taskovergg_get_marketing_items":
|
|
1846
|
-
result = store.getMarketingItems(args.project_id);
|
|
1847
|
-
break;
|
|
1848
|
-
case "taskovergg_add_marketing_item":
|
|
1849
|
-
result = store.addMarketingItem({
|
|
1850
|
-
projectId: args.project_id, item: args.item, category: args.category,
|
|
1851
|
-
status: args.status, notes: args.notes,
|
|
1852
|
-
});
|
|
1853
|
-
break;
|
|
1854
|
-
case "taskovergg_update_marketing_item":
|
|
1855
|
-
result = store.updateMarketingItem(args.marketing_id, {
|
|
1856
|
-
item: args.item, category: args.category, status: args.status, notes: args.notes,
|
|
1857
|
-
});
|
|
1858
|
-
break;
|
|
1859
|
-
|
|
1860
|
-
// Wiki
|
|
1861
|
-
case "taskovergg_get_wiki_pages":
|
|
1862
|
-
result = store.getWikiPages(args.project_id);
|
|
1863
|
-
break;
|
|
1864
|
-
case "taskovergg_add_wiki_page":
|
|
1865
|
-
result = store.addWikiPage({
|
|
1866
|
-
projectId: args.project_id, title: args.title, category: args.category,
|
|
1867
|
-
content: args.content,
|
|
1868
|
-
});
|
|
1869
|
-
break;
|
|
1870
|
-
case "taskovergg_update_wiki_page":
|
|
1871
|
-
result = store.updateWikiPage(args.wiki_id, {
|
|
1872
|
-
title: args.title, category: args.category, content: args.content,
|
|
1873
|
-
});
|
|
1874
|
-
break;
|
|
1875
|
-
|
|
1876
|
-
// Ship Readiness
|
|
1877
|
-
case "taskovergg_get_ship_checked":
|
|
1878
|
-
result = store.getShipChecked(args.project_id);
|
|
1879
|
-
break;
|
|
1880
|
-
case "taskovergg_toggle_ship_check":
|
|
1881
|
-
result = store.toggleShipCheck(args.project_id, args.step_id);
|
|
1882
|
-
break;
|
|
1883
|
-
|
|
1884
|
-
// Open Questions
|
|
1885
|
-
case "taskovergg_get_open_questions":
|
|
1886
|
-
result = store.getOpenQuestions(args.project_id);
|
|
1887
|
-
break;
|
|
1888
|
-
case "taskovergg_add_open_question":
|
|
1889
|
-
result = store.addOpenQuestion({
|
|
1890
|
-
projectId: args.project_id, text: args.text,
|
|
1891
|
-
});
|
|
1892
|
-
break;
|
|
1893
|
-
case "taskovergg_toggle_question":
|
|
1894
|
-
result = store.toggleQuestion(args.question_id);
|
|
1895
|
-
break;
|
|
1896
|
-
|
|
1897
|
-
// Board Columns (Blocks)
|
|
1898
|
-
case "taskovergg_add_board_column": {
|
|
1899
|
-
const pid = args.project_id;
|
|
1900
|
-
if (!pid) { result = { error: "No active project" }; break; }
|
|
1901
|
-
const proj = store.getProject(pid);
|
|
1902
|
-
if (!proj) { result = { error: `Project not found: ${pid}` }; break; }
|
|
1903
|
-
const newCol = {
|
|
1904
|
-
id: Date.now(),
|
|
1905
|
-
name: args.name,
|
|
1906
|
-
color: args.color || "#9B95A0",
|
|
1907
|
-
tag: args.name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
|
|
1908
|
-
cards: [],
|
|
1909
|
-
};
|
|
1910
|
-
const existingCols = proj.customBoardCols || [];
|
|
1911
|
-
store.updateProjectField(pid, "customBoardCols", [...existingCols, newCol]);
|
|
1912
|
-
result = newCol;
|
|
1913
|
-
break;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
// Stories (Narrative Bible)
|
|
1917
|
-
case "taskovergg_get_stories":
|
|
1918
|
-
result = store.getStories(args.project_id);
|
|
1919
|
-
break;
|
|
1920
|
-
case "taskovergg_add_story":
|
|
1921
|
-
result = store.addStory({
|
|
1922
|
-
projectId: args.project_id, sectionId: args.section_id,
|
|
1923
|
-
section: args.section, content: args.content,
|
|
1924
|
-
});
|
|
1925
|
-
break;
|
|
1926
|
-
case "taskovergg_update_story":
|
|
1927
|
-
result = store.updateStory(args.story_id, {
|
|
1928
|
-
section: args.section, content: args.content,
|
|
1929
|
-
});
|
|
1930
|
-
break;
|
|
1931
|
-
|
|
1932
|
-
// Scenes
|
|
1933
|
-
case "taskovergg_get_scenes":
|
|
1934
|
-
result = store.getScenes(args.project_id);
|
|
1935
|
-
break;
|
|
1936
|
-
case "taskovergg_add_scene":
|
|
1937
|
-
result = store.addScene({
|
|
1938
|
-
projectId: args.project_id, number: args.number, title: args.title,
|
|
1939
|
-
location: args.location, characters: args.characters,
|
|
1940
|
-
mood: args.mood, round: args.round, content: args.content,
|
|
1941
|
-
});
|
|
1942
|
-
break;
|
|
1943
|
-
case "taskovergg_update_scene":
|
|
1944
|
-
result = store.updateScene(args.scene_id, {
|
|
1945
|
-
number: args.number, title: args.title, location: args.location,
|
|
1946
|
-
characters: args.characters, mood: args.mood, round: args.round, content: args.content,
|
|
1947
|
-
});
|
|
1948
|
-
break;
|
|
1949
|
-
case "taskovergg_delete_scene":
|
|
1950
|
-
result = store.deleteScene(args.scene_id);
|
|
1951
|
-
break;
|
|
1952
|
-
|
|
1953
|
-
// Story Bible (Story NEW tab)
|
|
1954
|
-
case "taskovergg_get_story_bible":
|
|
1955
|
-
result = store.getStoryBible(args.project_id);
|
|
1956
|
-
break;
|
|
1957
|
-
case "taskovergg_update_story_bible":
|
|
1958
|
-
result = store.updateStoryBible(args.project_id, {
|
|
1959
|
-
title: args.title, logline: args.logline, genre: args.genre,
|
|
1960
|
-
tone: args.tone, script: args.script,
|
|
1961
|
-
});
|
|
1962
|
-
break;
|
|
1963
|
-
case "taskovergg_add_story_character":
|
|
1964
|
-
result = store.addStoryCharacter(args.project_id, {
|
|
1965
|
-
name: args.name, role: args.role, description: args.description,
|
|
1966
|
-
});
|
|
1967
|
-
break;
|
|
1968
|
-
case "taskovergg_update_story_character":
|
|
1969
|
-
result = store.updateStoryCharacter(args.project_id, args.character_id, {
|
|
1970
|
-
name: args.name, role: args.role, description: args.description,
|
|
1971
|
-
});
|
|
1972
|
-
break;
|
|
1973
|
-
case "taskovergg_get_scene_content":
|
|
1974
|
-
result = store.getSceneContent(args.scene_id);
|
|
1975
|
-
break;
|
|
1976
|
-
case "taskovergg_update_scene_content":
|
|
1977
|
-
result = store.updateSceneContent(args.project_id, args.scene_id, {
|
|
1978
|
-
title: args.title, location: args.location, mood: args.mood,
|
|
1979
|
-
content: args.content, round: args.round,
|
|
1980
|
-
characters: args.characters, objectives: args.objectives,
|
|
1981
|
-
});
|
|
1982
|
-
break;
|
|
1983
|
-
|
|
1984
|
-
default:
|
|
1985
|
-
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
return {
|
|
1989
|
-
content: [{
|
|
1990
|
-
type: "text",
|
|
1991
|
-
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
1992
|
-
}],
|
|
1993
|
-
};
|
|
1994
|
-
|
|
1376
|
+
const rpcArgs = mapping.args(cleaned, raw);
|
|
1377
|
+
const result = await cloudAdapter.callRpc(mapping.rpc, rpcArgs);
|
|
1378
|
+
return { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }] };
|
|
1995
1379
|
} catch (err) {
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
}
|
|
1380
|
+
if (err.message === "AUTH_FAILED") {
|
|
1381
|
+
console.error("[ERROR] Authentication failed. Shutting down.");
|
|
1382
|
+
process.exit(1);
|
|
1383
|
+
}
|
|
1384
|
+
if (err.message.startsWith("RATE_LIMITED:")) {
|
|
1385
|
+
const secs = err.message.split(":")[1];
|
|
1386
|
+
return { content: [{ type: "text", text: `Rate limited. Try again in ${secs} seconds.` }] };
|
|
1387
|
+
}
|
|
1388
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }] };
|
|
2000
1389
|
}
|
|
2001
1390
|
});
|
|
2002
1391
|
|
|
2003
|
-
// ===== START =====
|
|
2004
|
-
|
|
2005
|
-
const MCP_TRANSPORT = (process.env.MCP_TRANSPORT || "stdio").toLowerCase();
|
|
2006
|
-
const MCP_PORT = parseInt(process.env.MCP_PORT || "8484", 10);
|
|
2007
1392
|
|
|
2008
|
-
|
|
2009
|
-
const transport = new StdioServerTransport();
|
|
2010
|
-
await server.connect(transport);
|
|
2011
|
-
console.error("TaskOver MCP server running (stdio)");
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
async function startHttp() {
|
|
2015
|
-
const http = require("http");
|
|
2016
|
-
const { randomUUID } = require("crypto");
|
|
2017
|
-
const { StreamableHTTPServerTransport } = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
2018
|
-
|
|
2019
|
-
const transport = new StreamableHTTPServerTransport({
|
|
2020
|
-
sessionIdGenerator: () => randomUUID(),
|
|
2021
|
-
});
|
|
2022
|
-
await server.connect(transport);
|
|
2023
|
-
|
|
2024
|
-
const httpServer = http.createServer(async (req, res) => {
|
|
2025
|
-
// Only bind to /mcp endpoint
|
|
2026
|
-
const url = req.url.split("?")[0];
|
|
2027
|
-
if (url === "/mcp") {
|
|
2028
|
-
await transport.handleRequest(req, res);
|
|
2029
|
-
} else if (url === "/health") {
|
|
2030
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2031
|
-
res.end(JSON.stringify({ status: "ok", transport: "http", tools: TOOLS.length }));
|
|
2032
|
-
} else {
|
|
2033
|
-
res.writeHead(404);
|
|
2034
|
-
res.end("Not found");
|
|
2035
|
-
}
|
|
2036
|
-
});
|
|
2037
|
-
|
|
2038
|
-
httpServer.listen(MCP_PORT, "127.0.0.1", () => {
|
|
2039
|
-
console.error(`TaskOver MCP server running (http) on http://127.0.0.1:${MCP_PORT}/mcp`);
|
|
2040
|
-
});
|
|
2041
|
-
}
|
|
1393
|
+
// ===== START =====
|
|
2042
1394
|
|
|
2043
1395
|
async function main() {
|
|
2044
1396
|
if (CLOUD_MODE) {
|
|
2045
|
-
// Validate key before accepting connections
|
|
2046
1397
|
const check = await cloudAdapter.validateKey();
|
|
2047
1398
|
if (!check.valid) {
|
|
2048
1399
|
console.error(`[ERROR] ${check.error}`);
|
|
2049
1400
|
process.exit(1);
|
|
2050
1401
|
}
|
|
2051
|
-
console.error(
|
|
1402
|
+
console.error("[CLOUD] Connected to api.taskover.gg (API key mode)");
|
|
2052
1403
|
} else {
|
|
2053
|
-
|
|
1404
|
+
try {
|
|
1405
|
+
const hostType = process.env.TASKOVER_HOST_TYPE || "MCP";
|
|
1406
|
+
const auth = await authFlow.authenticate(hostType);
|
|
1407
|
+
cloudAdapter.initWithToken(auth.accessToken, auth.refreshToken);
|
|
1408
|
+
if (auth.displayName) {
|
|
1409
|
+
console.error(`[AUTH] Connected as ${auth.displayName}`);
|
|
1410
|
+
} else {
|
|
1411
|
+
console.error("[AUTH] Connected to api.taskover.gg");
|
|
1412
|
+
}
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
console.error(`[ERROR] ${err.message}`);
|
|
1415
|
+
console.error("[HINT] Set TASKOVER_API_KEY for headless/CI environments.");
|
|
1416
|
+
process.exit(1);
|
|
1417
|
+
}
|
|
2054
1418
|
}
|
|
2055
1419
|
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
await startStdio();
|
|
2060
|
-
}
|
|
1420
|
+
const transport = new StdioServerTransport();
|
|
1421
|
+
await server.connect(transport);
|
|
1422
|
+
console.error("TaskOver MCP server running (stdio)");
|
|
2061
1423
|
}
|
|
2062
1424
|
|
|
2063
1425
|
main().catch((err) => {
|
|
2064
|
-
// SECURITY: Never log
|
|
1426
|
+
// SECURITY: Never log secrets in crash output
|
|
2065
1427
|
const msg = err.message || String(err);
|
|
2066
|
-
|
|
1428
|
+
const redacted = msg
|
|
1429
|
+
.replace(/tok_[a-zA-Z0-9]+/g, "tok_[REDACTED]")
|
|
1430
|
+
.replace(/ht_[a-zA-Z0-9]+/g, "ht_[REDACTED]");
|
|
1431
|
+
console.error(`[FATAL] ${redacted}`);
|
|
2067
1432
|
process.exit(1);
|
|
2068
1433
|
});
|
package/package.json
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskover-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for TaskOver.gg - connect your AI assistant to your game dev projects",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": "index.js",
|
|
7
|
-
"
|
|
7
|
+
"files": ["index.js", "cloud-adapter.js", "tool-map.js", "auth-flow.js", "credential-store.js", "README.md"],
|
|
8
|
+
"keywords": ["mcp", "taskover", "gamedev", "project-management", "claude", "cursor", "windsurf"],
|
|
8
9
|
"author": "TaskOver, LLC <hello@taskover.gg>",
|
|
9
10
|
"license": "MIT",
|
|
10
11
|
"homepage": "https://taskover.gg",
|
|
11
12
|
"repository": { "type": "git", "url": "https://github.com/taskover/mcp" },
|
|
12
13
|
"engines": { "node": ">=20.0.0" },
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
16
|
+
"open": "^11.0.0"
|
|
17
|
+
},
|
|
18
|
+
"optionalDependencies": {
|
|
19
|
+
"keytar": "^7.9.0"
|
|
15
20
|
}
|
|
16
21
|
}
|