taskover-mcp 1.0.1 → 1.2.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.
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * One-time migration: imports data/tracker.json into data/taskover.db
4
+ * Run: node mcp-server/migrate-json-to-sqlite.js
5
+ */
6
+
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const { initDb, getDb, closeDb, saveToDisk } = require("./db");
10
+
11
+ const JSON_FILE = path.join(__dirname, "..", "data", "tracker.json");
12
+
13
+ async function migrate() {
14
+ if (!fs.existsSync(JSON_FILE)) {
15
+ console.log("No tracker.json found — nothing to migrate.");
16
+ return;
17
+ }
18
+
19
+ const raw = fs.readFileSync(JSON_FILE, "utf-8");
20
+ const data = JSON.parse(raw);
21
+ await initDb();
22
+ const db = getDb();
23
+
24
+ const now = new Date().toISOString();
25
+ const today = now.split("T")[0];
26
+
27
+ let counts = {};
28
+
29
+ // Projects
30
+ for (const p of data.projects || []) {
31
+ const d = { ...p }; delete d.id;
32
+ db.prepare("INSERT OR REPLACE INTO projects (id, data, created_at, last_updated) VALUES (?, ?, ?, ?)")
33
+ .run(p.id, JSON.stringify(d), p.createdAt || now, p.lastUpdated || now);
34
+ }
35
+ counts.projects = (data.projects || []).length;
36
+
37
+ // Tasks
38
+ for (const t of data.tasks || []) {
39
+ const d = { ...t }; delete d.id; delete d.projectId; delete d.status;
40
+ db.prepare("INSERT OR REPLACE INTO tasks (id, project_id, status, data, created_at, last_updated) VALUES (?, ?, ?, ?, ?, ?)")
41
+ .run(t.id, t.projectId, t.status || "todo", JSON.stringify(d), t.createdAt || now, t.lastUpdated || now);
42
+ }
43
+ counts.tasks = (data.tasks || []).length;
44
+
45
+ // Systems
46
+ for (const s of data.systems || []) {
47
+ const d = { ...s }; delete d.id; delete d.projectId;
48
+ db.prepare("INSERT OR REPLACE INTO systems (id, project_id, data, created_at, last_updated) VALUES (?, ?, ?, ?, ?)")
49
+ .run(s.id, s.projectId, JSON.stringify(d), s.createdAt || now, s.lastUpdated || today);
50
+ }
51
+ counts.systems = (data.systems || []).length;
52
+
53
+ // Sessions
54
+ for (const s of data.sessions || []) {
55
+ const d = { ...s }; delete d.id; delete d.projectId; delete d.date;
56
+ db.prepare("INSERT OR REPLACE INTO sessions (id, project_id, date, data, created_at) VALUES (?, ?, ?, ?, ?)")
57
+ .run(s.id, s.projectId, s.date || today, JSON.stringify(d), s.timestamp || now);
58
+ }
59
+ counts.sessions = (data.sessions || []).length;
60
+
61
+ // Changelog
62
+ for (const c of data.changelog || []) {
63
+ const d = { ...c }; delete d.id; delete d.projectId; delete d.date;
64
+ db.prepare("INSERT OR REPLACE INTO changelog (id, project_id, date, data, created_at) VALUES (?, ?, ?, ?, ?)")
65
+ .run(c.id, c.projectId, c.date || today, JSON.stringify(d), c.timestamp || now);
66
+ }
67
+ counts.changelog = (data.changelog || []).length;
68
+
69
+ // Bugs
70
+ for (const b of data.bugs || []) {
71
+ const d = { ...b }; delete d.id; delete d.projectId; delete d.status;
72
+ db.prepare("INSERT OR REPLACE INTO bugs (id, project_id, status, data, created_at) VALUES (?, ?, ?, ?, ?)")
73
+ .run(b.id, b.projectId, b.status || "open", JSON.stringify(d), b.createdAt || now);
74
+ }
75
+ counts.bugs = (data.bugs || []).length;
76
+
77
+ // Decisions
78
+ for (const dd of data.decisions || []) {
79
+ const d = { ...dd }; delete d.id; delete d.projectId; delete d.date;
80
+ db.prepare("INSERT OR REPLACE INTO decisions (id, project_id, date, data, created_at) VALUES (?, ?, ?, ?, ?)")
81
+ .run(dd.id, dd.projectId, dd.date || today, JSON.stringify(d), dd.timestamp || now);
82
+ }
83
+ counts.decisions = (data.decisions || []).length;
84
+
85
+ // Blueprints
86
+ for (const bp of data.blueprints || []) {
87
+ const d = { ...bp }; delete d.id; delete d.projectId;
88
+ db.prepare("INSERT OR REPLACE INTO blueprints (id, project_id, data, last_updated) VALUES (?, ?, ?, ?)")
89
+ .run(bp.id, bp.projectId, JSON.stringify(d), bp.lastUpdated || today);
90
+ }
91
+ counts.blueprints = (data.blueprints || []).length;
92
+
93
+ // Notes
94
+ for (const n of data.notes || []) {
95
+ const d = { ...n }; delete d.id; delete d.projectId; delete d.parentType; delete d.parentId;
96
+ db.prepare("INSERT OR REPLACE INTO notes (id, project_id, parent_type, parent_id, data, created_at) VALUES (?, ?, ?, ?, ?, ?)")
97
+ .run(n.id, n.projectId, n.parentType || "project", n.parentId || n.projectId, JSON.stringify(d), n.createdAt || now);
98
+ }
99
+ counts.notes = (data.notes || []).length;
100
+
101
+ // Backups
102
+ for (const b of data.backups || []) {
103
+ const d = { ...b }; delete d.id; delete d.projectId; delete d.date;
104
+ db.prepare("INSERT OR REPLACE INTO backups_log (id, project_id, date, data, created_at) VALUES (?, ?, ?, ?, ?)")
105
+ .run(b.id, b.projectId, b.date || today, JSON.stringify(d), b.timestamp || now);
106
+ }
107
+ counts.backups = (data.backups || []).length;
108
+
109
+ // Levels
110
+ for (const l of data.levels || []) {
111
+ const d = { ...l }; delete d.id; delete d.projectId;
112
+ db.prepare("INSERT OR REPLACE INTO levels (id, project_id, data, created_at, last_updated) VALUES (?, ?, ?, ?, ?)")
113
+ .run(l.id, l.projectId, JSON.stringify(d), l.createdAt || now, l.lastUpdated || now);
114
+ }
115
+ counts.levels = (data.levels || []).length;
116
+
117
+ // Plugins
118
+ for (const pl of data.plugins || []) {
119
+ const d = { ...pl }; delete d.id; delete d.projectId;
120
+ db.prepare("INSERT OR REPLACE INTO plugins (id, project_id, data, created_at, last_updated) VALUES (?, ?, ?, ?, ?)")
121
+ .run(pl.id, pl.projectId, JSON.stringify(d), pl.createdAt || now, pl.lastUpdated || now);
122
+ }
123
+ counts.plugins = (data.plugins || []).length;
124
+
125
+ // Build Errors
126
+ for (const e of data.buildErrors || []) {
127
+ const d = { ...e }; delete d.id; delete d.projectId; delete d.status;
128
+ db.prepare("INSERT OR REPLACE INTO build_errors (id, project_id, status, data, created_at) VALUES (?, ?, ?, ?, ?)")
129
+ .run(e.id, e.projectId, e.status || "open", JSON.stringify(d), e.createdAt || now);
130
+ }
131
+ counts.buildErrors = (data.buildErrors || []).length;
132
+
133
+ // Optimize Items
134
+ for (const o of data.optimizeItems || []) {
135
+ const d = { ...o }; delete d.id; delete d.projectId; delete d.status;
136
+ db.prepare("INSERT OR REPLACE INTO optimize_items (id, project_id, status, data, created_at) VALUES (?, ?, ?, ?, ?)")
137
+ .run(o.id, o.projectId, o.status || "open", JSON.stringify(d), o.createdAt || now);
138
+ }
139
+ counts.optimizeItems = (data.optimizeItems || []).length;
140
+
141
+ // Perf Budget
142
+ for (const pb of data.perfBudget || []) {
143
+ const d = { ...pb }; delete d.id; delete d.projectId;
144
+ db.prepare("INSERT OR REPLACE INTO perf_budget (id, project_id, data, created_at) VALUES (?, ?, ?, ?)")
145
+ .run(pb.id, pb.projectId, JSON.stringify(d), pb.createdAt || now);
146
+ }
147
+ counts.perfBudget = (data.perfBudget || []).length;
148
+
149
+ // Playtests
150
+ for (const pt of data.playtests || []) {
151
+ const d = { ...pt }; delete d.id; delete d.projectId; delete d.date;
152
+ db.prepare("INSERT OR REPLACE INTO playtests (id, project_id, date, data, created_at) VALUES (?, ?, ?, ?, ?)")
153
+ .run(pt.id, pt.projectId, pt.date || today, JSON.stringify(d), pt.createdAt || now);
154
+ }
155
+ counts.playtests = (data.playtests || []).length;
156
+
157
+ // Milestones
158
+ for (const m of data.milestones || []) {
159
+ const d = { ...m }; delete d.id; delete d.projectId; delete d.status;
160
+ db.prepare("INSERT OR REPLACE INTO milestones (id, project_id, status, data, created_at) VALUES (?, ?, ?, ?, ?)")
161
+ .run(m.id, m.projectId, m.status || "upcoming", JSON.stringify(d), m.createdAt || now);
162
+ }
163
+ counts.milestones = (data.milestones || []).length;
164
+
165
+ // Iterations
166
+ for (const it of data.iterations || []) {
167
+ const d = { ...it }; delete d.id; delete d.projectId;
168
+ db.prepare("INSERT OR REPLACE INTO iterations (id, project_id, data, created_at) VALUES (?, ?, ?, ?)")
169
+ .run(it.id, it.projectId, JSON.stringify(d), it.createdAt || now);
170
+ }
171
+ counts.iterations = (data.iterations || []).length;
172
+
173
+ // Dialogues
174
+ for (const dl of data.dialogues || []) {
175
+ const d = { ...dl }; delete d.id; delete d.projectId;
176
+ db.prepare("INSERT OR REPLACE INTO dialogues (id, project_id, data, created_at) VALUES (?, ?, ?, ?)")
177
+ .run(dl.id, dl.projectId, JSON.stringify(d), dl.createdAt || now);
178
+ }
179
+ counts.dialogues = (data.dialogues || []).length;
180
+
181
+ // Sounds
182
+ for (const s of data.sounds || []) {
183
+ const d = { ...s }; delete d.id; delete d.projectId;
184
+ db.prepare("INSERT OR REPLACE INTO sounds (id, project_id, data, created_at) VALUES (?, ?, ?, ?)")
185
+ .run(s.id, s.projectId, JSON.stringify(d), s.createdAt || now);
186
+ }
187
+ counts.sounds = (data.sounds || []).length;
188
+
189
+ // Controls
190
+ for (const c of data.controls || []) {
191
+ const d = { ...c }; delete d.id; delete d.projectId;
192
+ db.prepare("INSERT OR REPLACE INTO controls (id, project_id, data, created_at) VALUES (?, ?, ?, ?)")
193
+ .run(c.id, c.projectId, JSON.stringify(d), c.createdAt || now);
194
+ }
195
+ counts.controls = (data.controls || []).length;
196
+
197
+ // Assets
198
+ for (const a of data.assets || []) {
199
+ const d = { ...a }; delete d.id; delete d.projectId; delete d.status;
200
+ db.prepare("INSERT OR REPLACE INTO assets (id, project_id, status, data, created_at, last_updated) VALUES (?, ?, ?, ?, ?, ?)")
201
+ .run(a.id, a.projectId, a.status || "concept", JSON.stringify(d), a.createdAt || now, a.lastUpdated || now);
202
+ }
203
+ counts.assets = (data.assets || []).length;
204
+
205
+ // Refs
206
+ for (const r of data.refs || []) {
207
+ const d = { ...r }; delete d.id; delete d.projectId;
208
+ db.prepare("INSERT OR REPLACE INTO refs (id, project_id, data, created_at) VALUES (?, ?, ?, ?)")
209
+ .run(r.id, r.projectId, JSON.stringify(d), r.createdAt || now);
210
+ }
211
+ counts.refs = (data.refs || []).length;
212
+
213
+ // Marketing Items
214
+ for (const m of data.marketingItems || []) {
215
+ const d = { ...m }; delete d.id; delete d.projectId; delete d.status;
216
+ db.prepare("INSERT OR REPLACE INTO marketing_items (id, project_id, status, data, created_at) VALUES (?, ?, ?, ?, ?)")
217
+ .run(m.id, m.projectId, m.status || "todo", JSON.stringify(d), m.createdAt || now);
218
+ }
219
+ counts.marketingItems = (data.marketingItems || []).length;
220
+
221
+ // Wiki Pages
222
+ for (const w of data.wikiPages || []) {
223
+ const d = { ...w }; delete d.id; delete d.projectId;
224
+ db.prepare("INSERT OR REPLACE INTO wiki_pages (id, project_id, data, created_at, last_updated) VALUES (?, ?, ?, ?, ?)")
225
+ .run(w.id, w.projectId, JSON.stringify(d), w.createdAt || now, w.lastUpdated || now);
226
+ }
227
+ counts.wikiPages = (data.wikiPages || []).length;
228
+
229
+ // Ship Checked
230
+ for (const s of data.shipChecked || []) {
231
+ db.prepare("INSERT OR REPLACE INTO ship_checked (project_id, step_id) VALUES (?, ?)")
232
+ .run(s.projectId, s.stepId);
233
+ }
234
+ counts.shipChecked = (data.shipChecked || []).length;
235
+
236
+ // Open Questions
237
+ for (const q of data.openQuestions || []) {
238
+ const d = { ...q }; delete d.id; delete d.projectId; delete d.resolved;
239
+ db.prepare("INSERT OR REPLACE INTO open_questions (id, project_id, resolved, data, created_at) VALUES (?, ?, ?, ?, ?)")
240
+ .run(q.id, q.projectId, q.resolved ? 1 : 0, JSON.stringify(d), q.createdAt || now);
241
+ }
242
+ counts.openQuestions = (data.openQuestions || []).length;
243
+
244
+ saveToDisk();
245
+ closeDb();
246
+
247
+ console.log("\nMigration complete!");
248
+ console.log("Records imported:");
249
+ for (const [k, v] of Object.entries(counts)) {
250
+ if (v > 0) console.log(` ${k}: ${v}`);
251
+ }
252
+ console.log(`\nDatabase: data/taskover.db`);
253
+ console.log(`Original JSON preserved at: data/tracker.json`);
254
+ }
255
+
256
+ migrate().catch(e => { console.error("Migration failed:", e); process.exit(1); });
package/package.json CHANGED
@@ -1,16 +1,29 @@
1
- {
2
- "name": "taskover-mcp",
3
- "version": "1.0.1",
4
- "description": "MCP server for TaskOver.gg - connect your AI assistant to your game dev projects",
5
- "main": "index.js",
6
- "bin": "index.js",
7
- "keywords": ["mcp", "taskover", "gamedev", "project-management", "claude"],
8
- "author": "TaskOver, LLC <hello@taskover.gg>",
9
- "license": "MIT",
10
- "homepage": "https://taskover.gg",
11
- "repository": { "type": "git", "url": "https://github.com/taskover/mcp" },
12
- "engines": { "node": ">=20.0.0" },
13
- "dependencies": {
14
- "@modelcontextprotocol/sdk": "^1.0.0"
15
- }
16
- }
1
+ {
2
+ "name": "taskover-mcp",
3
+ "version": "1.2.0",
4
+ "description": "MCP server for TaskOver game dev project management",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "taskover": "./index.js"
8
+ },
9
+ "dependencies": {
10
+ "@aws-sdk/client-s3": "^3.700.0",
11
+ "@aws-sdk/s3-request-presigner": "^3.700.0",
12
+ "@modelcontextprotocol/sdk": "^1.0.0",
13
+ "@sentry/node": "^8.40.0",
14
+ "file-type": "^16.5.4",
15
+ "keytar": "^7.9.0",
16
+ "open": "^11.0.0",
17
+ "otpauth": "^9.5.0",
18
+ "pdfkit": "^0.15.2",
19
+ "qrcode": "^1.5.4",
20
+ "sharp": "^0.34.5",
21
+ "sql.js": "^1.14.0",
22
+ "stripe": "^17.4.0",
23
+ "ws": "^8.19.0"
24
+ },
25
+ "optionalDependencies": {
26
+ "@node-rs/argon2": "^2.0.2",
27
+ "better-sqlite3": "^11.10.0"
28
+ }
29
+ }
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); }
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 };