fast-cxt-mcp 1.0.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,235 @@
1
+ /**
2
+ * Windsurf API Key extraction from local installation.
3
+ *
4
+ * Cross-platform: macOS / Windows / Linux.
5
+ * Supports both legacy (plaintext windsurfAuthStatus) and new
6
+ * (Chromium safeStorage encrypted) storage formats.
7
+ */
8
+
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { homedir, platform } from "node:os";
12
+ import { execSync } from "node:child_process";
13
+ import crypto from "node:crypto";
14
+ import Database from "better-sqlite3";
15
+
16
+ const SECRET_KEY = 'secret://{"extensionId":"codeium.windsurf","key":"windsurf_auth.sessions"}';
17
+
18
+ /**
19
+ * Get the platform-specific path to Windsurf's state.vscdb.
20
+ * @returns {string}
21
+ */
22
+ export function getDbPath() {
23
+ const plat = platform();
24
+ const home = homedir();
25
+
26
+ if (plat === "darwin") {
27
+ return join(home, "Library", "Application Support", "Windsurf", "User", "globalStorage", "state.vscdb");
28
+ } else if (plat === "win32") {
29
+ const appdata = process.env.APPDATA || "";
30
+ if (!appdata) throw new Error("Cannot determine APPDATA path");
31
+ return join(appdata, "Windsurf", "User", "globalStorage", "state.vscdb");
32
+ } else {
33
+ // Linux
34
+ const config = process.env.XDG_CONFIG_HOME || join(home, ".config");
35
+ return join(config, "Windsurf", "User", "globalStorage", "state.vscdb");
36
+ }
37
+ }
38
+
39
+ // ─── Legacy extraction (plaintext windsurfAuthStatus) ──────
40
+
41
+ /**
42
+ * Try extracting API key from legacy plaintext storage.
43
+ * @param {Database.Database} db
44
+ * @returns {{ api_key?: string } | null} null if key not found
45
+ */
46
+ function extractFromLegacy(db) {
47
+ const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus'").get();
48
+ if (!row) return null;
49
+
50
+ let data;
51
+ try {
52
+ data = JSON.parse(row.value);
53
+ } catch {
54
+ return null;
55
+ }
56
+
57
+ const apiKey = data.apiKey || "";
58
+ return apiKey ? { api_key: apiKey } : null;
59
+ }
60
+
61
+ // ─── New encrypted extraction (Chromium safeStorage) ───────
62
+
63
+ /**
64
+ * Retrieve the master password from system keychain.
65
+ * @returns {string}
66
+ */
67
+ function getMasterPassword() {
68
+ const plat = platform();
69
+
70
+ if (plat === "darwin") {
71
+ return execSync(
72
+ 'security find-generic-password -s "Windsurf Safe Storage" -a "Windsurf Key" -w',
73
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
74
+ ).trim();
75
+ }
76
+
77
+ if (plat === "linux") {
78
+ // Try libsecret (GNOME Keyring / KDE Wallet)
79
+ try {
80
+ return execSync(
81
+ 'secret-tool lookup application windsurf service "Windsurf Safe Storage" account "Windsurf Key"',
82
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
83
+ ).trim();
84
+ } catch {
85
+ // No keyring available — Chromium falls back to hardcoded password
86
+ return "peanuts";
87
+ }
88
+ }
89
+
90
+ // Windows: not applicable (uses DPAPI directly, no master password)
91
+ return "";
92
+ }
93
+
94
+ /**
95
+ * Decrypt safeStorage data on macOS / Linux.
96
+ * Format: 'v10' (3 bytes) + AES-128-CBC ciphertext (PBKDF2 derived key).
97
+ * @param {Buffer} encrypted
98
+ * @param {string} masterPassword
99
+ * @returns {string}
100
+ */
101
+ function decryptAesCbc(encrypted, masterPassword) {
102
+ const plat = platform();
103
+ const iterations = plat === "darwin" ? 1003 : 1;
104
+ const derivedKey = crypto.pbkdf2Sync(masterPassword, "saltysalt", iterations, 16, "sha1");
105
+
106
+ const ciphertext = encrypted.slice(3); // strip 'v10' prefix
107
+ const iv = Buffer.alloc(16, 0x20);
108
+
109
+ const decipher = crypto.createDecipheriv("aes-128-cbc", derivedKey, iv);
110
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
111
+ }
112
+
113
+ /**
114
+ * Decrypt safeStorage data on Windows via DPAPI (PowerShell).
115
+ * Electron on Windows encrypts with CryptProtectData directly — no 'v10' prefix.
116
+ * Uses -EncodedCommand to avoid cmd.exe quoting/escaping issues.
117
+ * @param {Buffer} encrypted
118
+ * @returns {string}
119
+ */
120
+ function decryptDpapi(encrypted) {
121
+ const b64 = encrypted.toString("base64");
122
+ const psScript =
123
+ `Add-Type -AssemblyName System.Security;` +
124
+ `$bytes = [Convert]::FromBase64String('${b64}');` +
125
+ `$dec = [Security.Cryptography.ProtectedData]::Unprotect($bytes, $null, 'CurrentUser');` +
126
+ `[Text.Encoding]::UTF8.GetString($dec)`;
127
+
128
+ // Encode the script as UTF-16LE base64 for -EncodedCommand
129
+ const encoded = Buffer.from(psScript, "utf16le").toString("base64");
130
+
131
+ return execSync(`powershell -NoProfile -EncodedCommand ${encoded}`, {
132
+ encoding: "utf8",
133
+ stdio: ["pipe", "pipe", "pipe"],
134
+ }).trim();
135
+ }
136
+
137
+ /**
138
+ * Try extracting API key from encrypted secret storage.
139
+ * @param {Database.Database} db
140
+ * @returns {{ api_key?: string, method?: string } | null}
141
+ */
142
+ function extractFromSecret(db) {
143
+ const row = db.prepare("SELECT value FROM ItemTable WHERE key = ?").get(SECRET_KEY);
144
+ if (!row) return null;
145
+
146
+ let encryptedBuf;
147
+ try {
148
+ const parsed = JSON.parse(row.value);
149
+ encryptedBuf = Buffer.from(parsed.data);
150
+ } catch {
151
+ return null;
152
+ }
153
+
154
+ let decrypted;
155
+ const plat = platform();
156
+
157
+ if (plat === "win32") {
158
+ // Windows: raw DPAPI blob (no 'v10' prefix)
159
+ decrypted = decryptDpapi(encryptedBuf);
160
+ } else {
161
+ // macOS / Linux: 'v10' + AES-128-CBC
162
+ const prefix = encryptedBuf.slice(0, 3).toString("utf8");
163
+ if (prefix !== "v10" && prefix !== "v11") return null;
164
+ const masterPassword = getMasterPassword();
165
+ decrypted = decryptAesCbc(encryptedBuf, masterPassword);
166
+ }
167
+
168
+ let sessions;
169
+ try {
170
+ sessions = JSON.parse(decrypted);
171
+ } catch {
172
+ return null;
173
+ }
174
+
175
+ // sessions is an array — take the first entry with an accessToken
176
+ const entry = Array.isArray(sessions)
177
+ ? sessions.find((s) => s.accessToken)
178
+ : sessions;
179
+
180
+ const token = entry?.accessToken || "";
181
+ return token ? { api_key: token, method: "safeStorage" } : null;
182
+ }
183
+
184
+ // ─── Main entry point ──────────────────────────────────────
185
+
186
+ /**
187
+ * Extract API Key from Windsurf state.vscdb.
188
+ * Tries legacy plaintext first, then falls back to encrypted safeStorage.
189
+ * @param {string} [dbPath]
190
+ * @returns {{ api_key?: string, db_path: string, method?: string, error?: string, hint?: string }}
191
+ */
192
+ export function extractKey(dbPath) {
193
+ if (!dbPath) {
194
+ dbPath = getDbPath();
195
+ }
196
+
197
+ if (!existsSync(dbPath)) {
198
+ return {
199
+ error: `Windsurf database not found: ${dbPath}`,
200
+ hint: "Ensure Windsurf is installed and logged in.",
201
+ db_path: dbPath,
202
+ };
203
+ }
204
+
205
+ let db;
206
+ try {
207
+ db = new Database(dbPath, { readonly: true });
208
+ } catch (e) {
209
+ return { error: `Failed to open database: ${e.message}`, db_path: dbPath };
210
+ }
211
+
212
+ try {
213
+ // 1) Legacy plaintext
214
+ const legacy = extractFromLegacy(db);
215
+ if (legacy) {
216
+ return { ...legacy, db_path: dbPath, method: "legacy" };
217
+ }
218
+
219
+ // 2) Encrypted safeStorage
220
+ const secret = extractFromSecret(db);
221
+ if (secret) {
222
+ return { ...secret, db_path: dbPath };
223
+ }
224
+
225
+ return {
226
+ error: "No API key found in database (tried legacy + safeStorage)",
227
+ hint: "Ensure Windsurf is logged in. You can also set WINDSURF_API_KEY manually.",
228
+ db_path: dbPath,
229
+ };
230
+ } catch (e) {
231
+ return { error: `Extraction failed: ${e.message}`, db_path: dbPath };
232
+ } finally {
233
+ db.close();
234
+ }
235
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Hand-written Protobuf encoder/decoder + Connect-RPC frame handling.
3
+ *
4
+ * Matches the Windsurf wire format exactly.
5
+ * Python bytearray → Node.js Buffer
6
+ * struct.pack(">I", len) → buf.writeUInt32BE
7
+ * gzip.compress/decompress → zlib.gzipSync/gunzipSync
8
+ */
9
+
10
+ import { gzipSync, gunzipSync } from "node:zlib";
11
+
12
+ // ─── Protobuf Encoder ──────────────────────────────────────
13
+
14
+ export class ProtobufEncoder {
15
+ constructor() {
16
+ /** @type {Buffer[]} */
17
+ this._chunks = [];
18
+ }
19
+
20
+ /**
21
+ * Encode an unsigned varint into a Buffer.
22
+ * @param {number} value
23
+ * @returns {Buffer}
24
+ */
25
+ _varint(value) {
26
+ const bytes = [];
27
+ while (value > 0x7f) {
28
+ bytes.push((value & 0x7f) | 0x80);
29
+ value >>>= 7;
30
+ }
31
+ bytes.push(value & 0x7f);
32
+ return Buffer.from(bytes);
33
+ }
34
+
35
+ /**
36
+ * Encode a field tag.
37
+ * @param {number} field
38
+ * @param {number} wire
39
+ * @returns {Buffer}
40
+ */
41
+ _tag(field, wire) {
42
+ return this._varint((field << 3) | wire);
43
+ }
44
+
45
+ /**
46
+ * Write a varint field.
47
+ * @param {number} field
48
+ * @param {number} value
49
+ * @returns {ProtobufEncoder}
50
+ */
51
+ writeVarint(field, value) {
52
+ this._chunks.push(this._tag(field, 0), this._varint(value));
53
+ return this;
54
+ }
55
+
56
+ /**
57
+ * Write a length-delimited string field.
58
+ * @param {number} field
59
+ * @param {string} value
60
+ * @returns {ProtobufEncoder}
61
+ */
62
+ writeString(field, value) {
63
+ const data = Buffer.from(value, "utf-8");
64
+ this._chunks.push(this._tag(field, 2), this._varint(data.length), data);
65
+ return this;
66
+ }
67
+
68
+ /**
69
+ * Write a length-delimited bytes field.
70
+ * @param {number} field
71
+ * @param {Buffer|Uint8Array} value
72
+ * @returns {ProtobufEncoder}
73
+ */
74
+ writeBytes(field, value) {
75
+ const buf = Buffer.isBuffer(value) ? value : Buffer.from(value);
76
+ this._chunks.push(this._tag(field, 2), this._varint(buf.length), buf);
77
+ return this;
78
+ }
79
+
80
+ /**
81
+ * Write a nested message field.
82
+ * @param {number} field
83
+ * @param {ProtobufEncoder} sub
84
+ * @returns {ProtobufEncoder}
85
+ */
86
+ writeMessage(field, sub) {
87
+ const data = sub.toBuffer();
88
+ this._chunks.push(this._tag(field, 2), this._varint(data.length), data);
89
+ return this;
90
+ }
91
+
92
+ /**
93
+ * Return the encoded bytes as a Buffer.
94
+ * @returns {Buffer}
95
+ */
96
+ toBuffer() {
97
+ return Buffer.concat(this._chunks);
98
+ }
99
+ }
100
+
101
+ // ─── Varint Decode ─────────────────────────────────────────
102
+
103
+ /**
104
+ * Decode a varint from a buffer at the given offset.
105
+ * @param {Buffer} buf
106
+ * @param {number} offset
107
+ * @returns {[number, number]} [value, newOffset]
108
+ */
109
+ export function decodeVarint(buf, offset) {
110
+ let value = 0;
111
+ let shift = 0;
112
+ while (offset < buf.length) {
113
+ const b = buf[offset++];
114
+ value |= (b & 0x7f) << shift;
115
+ shift += 7;
116
+ if (!(b & 0x80)) break;
117
+ }
118
+ return [value, offset];
119
+ }
120
+
121
+ // ─── Protobuf String Extraction ────────────────────────────
122
+
123
+ /**
124
+ * Extract all UTF-8 strings (length > 5) from raw protobuf data
125
+ * by parsing wire types. Matches Python proto_extract_strings().
126
+ * @param {Buffer} data
127
+ * @returns {string[]}
128
+ */
129
+ export function extractStrings(data) {
130
+ const strings = [];
131
+ let i = 0;
132
+ while (i < data.length) {
133
+ // Read tag varint
134
+ let tag = 0;
135
+ let shift = 0;
136
+ while (i < data.length) {
137
+ const b = data[i++];
138
+ tag |= (b & 0x7f) << shift;
139
+ shift += 7;
140
+ if (!(b & 0x80)) break;
141
+ }
142
+ const wire = tag & 0x7;
143
+ if (wire === 0) {
144
+ // Varint — skip
145
+ while (i < data.length) {
146
+ const b = data[i++];
147
+ if (!(b & 0x80)) break;
148
+ }
149
+ } else if (wire === 1) {
150
+ // 64-bit fixed
151
+ i += 8;
152
+ } else if (wire === 2) {
153
+ // Length-delimited
154
+ let length = 0;
155
+ shift = 0;
156
+ while (i < data.length) {
157
+ const b = data[i++];
158
+ length |= (b & 0x7f) << shift;
159
+ shift += 7;
160
+ if (!(b & 0x80)) break;
161
+ }
162
+ if (i + length <= data.length) {
163
+ const raw = data.subarray(i, i + length);
164
+ try {
165
+ const text = raw.toString("utf-8");
166
+ if (text.length > 5) {
167
+ strings.push(text);
168
+ }
169
+ } catch {
170
+ // Not valid UTF-8, skip
171
+ }
172
+ }
173
+ i += length;
174
+ } else if (wire === 5) {
175
+ // 32-bit fixed
176
+ i += 4;
177
+ } else {
178
+ // Unknown wire type — stop
179
+ break;
180
+ }
181
+ }
182
+ return strings;
183
+ }
184
+
185
+ // ─── Connect-RPC Frame Encode/Decode ───────────────────────
186
+
187
+ /**
188
+ * Encode protobuf bytes into a gzip-compressed Connect-RPC frame.
189
+ * Frame format: 1-byte flags + 4-byte big-endian length + payload
190
+ * @param {Buffer} protoBytes
191
+ * @param {boolean} [compress=true]
192
+ * @returns {Buffer}
193
+ */
194
+ export function connectFrameEncode(protoBytes, compress = true) {
195
+ let payload;
196
+ let flags;
197
+ if (compress) {
198
+ payload = gzipSync(protoBytes);
199
+ flags = 1; // gzip compressed
200
+ } else {
201
+ payload = protoBytes;
202
+ flags = 0;
203
+ }
204
+ const header = Buffer.alloc(5);
205
+ header[0] = flags;
206
+ header.writeUInt32BE(payload.length, 1);
207
+ return Buffer.concat([header, payload]);
208
+ }
209
+
210
+ /**
211
+ * Decode Connect-RPC frames from raw response data.
212
+ * Handles gzip-compressed frames (flags 1 or 3).
213
+ * @param {Buffer} data
214
+ * @returns {Buffer[]}
215
+ */
216
+ export function connectFrameDecode(data) {
217
+ const frames = [];
218
+ let i = 0;
219
+ while (i + 5 <= data.length) {
220
+ const flags = data[i];
221
+ const length = data.readUInt32BE(i + 1);
222
+ i += 5;
223
+ let payload = data.subarray(i, i + length);
224
+ i += length;
225
+ if (flags === 1 || flags === 3) {
226
+ try {
227
+ payload = gunzipSync(payload);
228
+ } catch {
229
+ // Decompression failed — use raw payload
230
+ }
231
+ }
232
+ frames.push(Buffer.from(payload));
233
+ }
234
+ return frames;
235
+ }
package/src/server.mjs ADDED
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Windsurf Fast Context MCP Server (Node.js)
4
+ *
5
+ * AI-driven semantic code search via reverse-engineered Windsurf protocol.
6
+ *
7
+ * Configuration (environment variables):
8
+ * WINDSURF_API_KEY — Windsurf API key (auto-discovered from local install if not set)
9
+ * FC_MAX_TURNS — Search rounds per query (default: 3)
10
+ * FC_MAX_COMMANDS — Max parallel commands per round (default: 8)
11
+ * FC_TIMEOUT_MS — Connect-Timeout-Ms for streaming requests (default: 30000)
12
+ *
13
+ * Start:
14
+ * node src/server.mjs
15
+ */
16
+
17
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+ import { z } from "zod";
20
+
21
+ import { searchWithContent, extractKeyInfo } from "./core.mjs";
22
+
23
+ /**
24
+ * Parse an integer env var with optional clamping.
25
+ * @param {string} name
26
+ * @param {number} defaultValue
27
+ * @param {{ min?: number, max?: number }} [opts]
28
+ * @returns {number}
29
+ */
30
+ function readIntEnv(name, defaultValue, opts = {}) {
31
+ const raw = process.env[name];
32
+ const parsed = Number.parseInt(raw ?? "", 10);
33
+ if (!Number.isFinite(parsed)) return defaultValue;
34
+ const min = typeof opts.min === "number" ? opts.min : null;
35
+ const max = typeof opts.max === "number" ? opts.max : null;
36
+ let value = parsed;
37
+ if (min !== null) value = Math.max(min, value);
38
+ if (max !== null) value = Math.min(max, value);
39
+ return value;
40
+ }
41
+
42
+ // Read config from environment
43
+ const MAX_TURNS = readIntEnv("FC_MAX_TURNS", 3, { min: 1, max: 5 });
44
+ const MAX_COMMANDS = readIntEnv("FC_MAX_COMMANDS", 8, { min: 1, max: 20 });
45
+ const TIMEOUT_MS = readIntEnv("FC_TIMEOUT_MS", 30000, { min: 1000, max: 300000 });
46
+
47
+ const server = new McpServer({
48
+ name: "windsurf-fast-context",
49
+ version: "1.1.0",
50
+ instructions:
51
+ "Windsurf Fast Context — AI-driven semantic code search. " +
52
+ "Returns file paths with line ranges and grep keywords.\n" +
53
+ "Tunable parameters:\n" +
54
+ "- tree_depth (1-6, default 3): How much directory structure the remote AI sees. " +
55
+ "REDUCE if you get payload/size errors. INCREASE for small projects where deeper structure helps.\n" +
56
+ "- max_turns (1-5, default 3): How many search rounds. " +
57
+ "INCREASE if results are incomplete. Use 1 for quick lookups.\n" +
58
+ "- max_results (1-30, default 10): Maximum number of files to return.\n" +
59
+ "- exclude_paths (string array, default []): Directory/file patterns to exclude from tree. " +
60
+ "Use for large repos to reduce payload size (e.g. ['node_modules', 'dist', '.git']).\n" +
61
+ "The response includes [config] and [diagnostic] lines — read them to decide if you should retry with different parameters.",
62
+ });
63
+
64
+ // ─── Tool: fast_context_search ─────────────────────────────
65
+
66
+ server.tool(
67
+ "fast_context_search",
68
+ "AI-driven semantic code search using Windsurf's Devstral model. " +
69
+ "Searches a codebase with natural language and returns relevant file paths with line ranges, " +
70
+ "plus suggested grep keywords for follow-up searches.\n" +
71
+ "Parameter tuning guide:\n" +
72
+ "- tree_depth: Controls how much directory structure the remote AI sees before searching. " +
73
+ "If you get a payload/size error, REDUCE this value. " +
74
+ "If search results are too shallow (missing files in deep subdirectories), INCREASE this value.\n" +
75
+ "- max_turns: Controls how many search-execute-feedback rounds the remote AI gets. " +
76
+ "If results are incomplete or the AI didn't find enough files, INCREASE this value. " +
77
+ "If you want a quick rough answer, use 1.\n" +
78
+ "Response includes a [config] line showing actual parameters used — use this to decide adjustments on retry.",
79
+ {
80
+ query: z.string().describe(
81
+ 'Natural language search query (e.g. "where is auth handled", "database connection pool")'
82
+ ),
83
+ project_path: z
84
+ .string()
85
+ .default("")
86
+ .describe("Absolute path to project root. Empty = current working directory."),
87
+ tree_depth: z
88
+ .number()
89
+ .int()
90
+ .min(1)
91
+ .max(6)
92
+ .default(3)
93
+ .describe(
94
+ "Directory tree depth for the initial repo map sent to the remote AI. " +
95
+ "Default 3. Use 1-2 for huge monorepos (>5000 files) or if you get payload size errors. " +
96
+ "Use 4-6 for small projects (<200 files) where you want the AI to see deeper structure. " +
97
+ "Auto falls back to a lower depth if tree output exceeds 250KB."
98
+ ),
99
+ max_turns: z
100
+ .number()
101
+ .int()
102
+ .min(1)
103
+ .max(5)
104
+ .default(MAX_TURNS)
105
+ .describe(
106
+ "Number of search rounds. Each round: remote AI generates search commands → local execution → results sent back. " +
107
+ "Default 3. Use 1 for quick simple lookups. Use 4-5 for complex queries requiring deep tracing across many files. " +
108
+ "More rounds = better results but slower and uses more API quota."
109
+ ),
110
+ max_results: z
111
+ .number()
112
+ .int()
113
+ .min(1)
114
+ .max(30)
115
+ .default(10)
116
+ .describe(
117
+ "Maximum number of files to return. Default 10. " +
118
+ "Use a smaller value (3-5) for focused queries. " +
119
+ "Use a larger value (15-30) for broad exploration queries."
120
+ ),
121
+ exclude_paths: z
122
+ .array(z.string())
123
+ .default([])
124
+ .describe(
125
+ "Directory/file patterns to exclude from tree and search context. " +
126
+ "Useful for reducing payload size on large repos. " +
127
+ "Examples: ['node_modules', 'dist', '.git', 'build', 'coverage', '*.min.*']"
128
+ ),
129
+ },
130
+ async ({ query, project_path, tree_depth, max_turns, max_results, exclude_paths }) => {
131
+ let projectPath = project_path || process.cwd();
132
+
133
+ try {
134
+ const { statSync } = await import("node:fs");
135
+ if (!statSync(projectPath).isDirectory()) {
136
+ return { content: [{ type: "text", text: `Error: project path does not exist: ${projectPath}` }] };
137
+ }
138
+ } catch {
139
+ return { content: [{ type: "text", text: `Error: project path does not exist: ${projectPath}` }] };
140
+ }
141
+
142
+ try {
143
+ const result = await searchWithContent({
144
+ query,
145
+ projectRoot: projectPath,
146
+ maxTurns: max_turns,
147
+ maxCommands: MAX_COMMANDS,
148
+ maxResults: max_results,
149
+ treeDepth: tree_depth,
150
+ timeoutMs: TIMEOUT_MS,
151
+ excludePaths: exclude_paths,
152
+ });
153
+ return { content: [{ type: "text", text: result }] };
154
+ } catch (e) {
155
+ const code = e.code || "UNKNOWN";
156
+ return { content: [{ type: "text", text:
157
+ `Error [${code}]: ${e.message}\n\n` +
158
+ `[hint] Suggestions based on error type:\n` +
159
+ ` - Reduce tree_depth (current: ${tree_depth})\n` +
160
+ ` - Add exclude_paths to filter large directories (e.g. ['node_modules', 'dist'])\n` +
161
+ ` - Narrow project_path to a subdirectory\n` +
162
+ ` - Reduce max_turns (current: ${max_turns})`
163
+ }] };
164
+ }
165
+ }
166
+ );
167
+
168
+ // ─── Tool: extract_windsurf_key ────────────────────────────
169
+
170
+ server.tool(
171
+ "extract_windsurf_key",
172
+ "Extract Windsurf API Key from local installation. " +
173
+ "Auto-detects OS (macOS/Windows/Linux) and reads the API key from " +
174
+ "Windsurf's local database. Set the result as WINDSURF_API_KEY env var.",
175
+ {},
176
+ async () => {
177
+ const result = extractKeyInfo();
178
+
179
+ if (result.error) {
180
+ const text = `Error: ${result.error}\n${result.hint || ""}\nDB path: ${result.db_path || "N/A"}`;
181
+ return { content: [{ type: "text", text }] };
182
+ }
183
+
184
+ const key = result.api_key;
185
+ const method = result.method === "safeStorage" ? "safeStorage (encrypted)" : "legacy (plaintext)";
186
+ const text =
187
+ `Windsurf API Key extracted successfully\n\n` +
188
+ ` Key: ${key.slice(0, 30)}...${key.slice(-10)}\n` +
189
+ ` Length: ${key.length}\n` +
190
+ ` Method: ${method}\n` +
191
+ ` Source: ${result.db_path}\n\n` +
192
+ `Usage:\n` +
193
+ ` export WINDSURF_API_KEY="${key}"`;
194
+
195
+ return { content: [{ type: "text", text }] };
196
+ }
197
+ );
198
+
199
+ // ─── Start ─────────────────────────────────────────────────
200
+
201
+ async function main() {
202
+ const transport = new StdioServerTransport();
203
+ await server.connect(transport);
204
+ }
205
+
206
+ main().catch((err) => {
207
+ console.error("Fatal error:", err);
208
+ process.exit(1);
209
+ });