@sammysnake/fast-context-mcp 1.3.0-beta.1
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/LICENSE +21 -0
- package/README.md +274 -0
- package/package.json +47 -0
- package/src/core.mjs +1906 -0
- package/src/directory-scorer.mjs +1059 -0
- package/src/executor.mjs +597 -0
- package/src/extract-key.mjs +93 -0
- package/src/protobuf.mjs +235 -0
- package/src/server.mjs +320 -0
package/src/protobuf.mjs
ADDED
|
@@ -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,320 @@
|
|
|
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
|
+
/**
|
|
43
|
+
* Parse a boolean env var.
|
|
44
|
+
* @param {string} name
|
|
45
|
+
* @param {boolean} defaultValue
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
function readBoolEnv(name, defaultValue) {
|
|
49
|
+
const raw = process.env[name];
|
|
50
|
+
if (raw == null) return defaultValue;
|
|
51
|
+
const v = String(raw).trim().toLowerCase();
|
|
52
|
+
if (["1", "true", "yes", "on"].includes(v)) return true;
|
|
53
|
+
if (["0", "false", "no", "off"].includes(v)) return false;
|
|
54
|
+
return defaultValue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Read config from environment
|
|
58
|
+
const MAX_TURNS = readIntEnv("FC_MAX_TURNS", 3, { min: 1, max: 5 });
|
|
59
|
+
const MAX_COMMANDS = readIntEnv("FC_MAX_COMMANDS", 8, { min: 1, max: 20 });
|
|
60
|
+
const TIMEOUT_MS = readIntEnv("FC_TIMEOUT_MS", 30000, { min: 1000, max: 300000 });
|
|
61
|
+
|
|
62
|
+
// Repo-map optimizer defaults
|
|
63
|
+
const DEFAULT_REPO_MAP_MODE = process.env.FC_REPO_MAP_MODE === "classic" ? "classic" : "bootstrap_hotspot";
|
|
64
|
+
const DEFAULT_BOOTSTRAP_TREE_DEPTH = readIntEnv("FC_BOOTSTRAP_TREE_DEPTH", 1, { min: 1, max: 3 });
|
|
65
|
+
const DEFAULT_HOTSPOT_TOP_K = readIntEnv("FC_HOTSPOT_TOP_K", 4, { min: 0, max: 8 });
|
|
66
|
+
const DEFAULT_HOTSPOT_TREE_DEPTH = readIntEnv("FC_HOTSPOT_TREE_DEPTH", 2, { min: 1, max: 4 });
|
|
67
|
+
const DEFAULT_HOTSPOT_MAX_BYTES = readIntEnv("FC_HOTSPOT_MAX_BYTES", 122880, { min: 16384, max: 262144 });
|
|
68
|
+
const DEFAULT_BOOTSTRAP_ENABLED = readBoolEnv("FC_BOOTSTRAP_ENABLED", true);
|
|
69
|
+
const DEFAULT_BOOTSTRAP_MAX_TURNS = readIntEnv("FC_BOOTSTRAP_MAX_TURNS", 2, { min: 1, max: 3 });
|
|
70
|
+
const DEFAULT_BOOTSTRAP_MAX_COMMANDS = readIntEnv("FC_BOOTSTRAP_MAX_COMMANDS", 6, { min: 1, max: 8 });
|
|
71
|
+
|
|
72
|
+
const server = new McpServer({
|
|
73
|
+
name: "windsurf-fast-context",
|
|
74
|
+
version: "1.2.0",
|
|
75
|
+
instructions:
|
|
76
|
+
"Windsurf Fast Context — AI-driven semantic code search. " +
|
|
77
|
+
"Returns file paths with line ranges and grep keywords.\n" +
|
|
78
|
+
"Tunable parameters:\n" +
|
|
79
|
+
"- tree_depth (0-6, default 3; 0=auto): How much directory structure the remote AI sees. " +
|
|
80
|
+
"REDUCE if you get payload/size errors. INCREASE for small projects where deeper structure helps.\n" +
|
|
81
|
+
"- max_turns (1-5, default 3): How many search rounds. " +
|
|
82
|
+
"INCREASE if results are incomplete. Use 1 for quick lookups.\n" +
|
|
83
|
+
"- max_results (1-30, default 10): Maximum number of files to return.\n" +
|
|
84
|
+
"- exclude_paths (string array, default []): Directory/file patterns to exclude from tree. " +
|
|
85
|
+
"Use for large repos to reduce payload size (e.g. ['node_modules', 'dist', '.git']).\n" +
|
|
86
|
+
"- repo_map_mode (classic | bootstrap_hotspot, default bootstrap_hotspot): Repo-map build strategy.\n" +
|
|
87
|
+
"- bootstrap_tree_depth (1-3, default 1): Bootstrap tree depth used by bootstrap_hotspot mode.\n" +
|
|
88
|
+
"- hotspot_top_k (0-8, default 4): Number of hotspot top-level directories to include.\n" +
|
|
89
|
+
"- hotspot_tree_depth (1-4, default 2): Tree depth for each hotspot subtree.\n" +
|
|
90
|
+
"- hotspot_max_bytes (16384-262144, default 122880): Repo-map byte budget in bootstrap_hotspot mode.\n" +
|
|
91
|
+
"- bootstrap_enabled (default true): Enable standalone bootstrap phase for hotspot hint collection.\n" +
|
|
92
|
+
"- bootstrap_max_turns (1-3, default 2): Bootstrap phase turns.\n" +
|
|
93
|
+
"- bootstrap_max_commands (1-8, default 6): Bootstrap commands per turn.\n" +
|
|
94
|
+
"The response includes [config] and [diagnostic] lines — read them to decide if you should retry with different parameters.",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ─── Tool: fast_context_search ─────────────────────────────
|
|
98
|
+
|
|
99
|
+
server.tool(
|
|
100
|
+
"fast_context_search",
|
|
101
|
+
"AI-driven semantic code search using Windsurf's Devstral model. " +
|
|
102
|
+
"Searches a codebase with natural language and returns relevant file paths with line ranges, " +
|
|
103
|
+
"plus suggested grep keywords for follow-up searches.\n" +
|
|
104
|
+
"Parameter tuning guide:\n" +
|
|
105
|
+
"- tree_depth: Controls how much directory structure the remote AI sees before searching. " +
|
|
106
|
+
"If you get a payload/size error, REDUCE this value. " +
|
|
107
|
+
"If search results are too shallow (missing files in deep subdirectories), INCREASE this value. " +
|
|
108
|
+
"Use 0 for auto depth based on project size.\n" +
|
|
109
|
+
"- max_turns: Controls how many search-execute-feedback rounds the remote AI gets. " +
|
|
110
|
+
"If results are incomplete or the AI didn't find enough files, INCREASE this value. " +
|
|
111
|
+
"If you want a quick rough answer, use 1.\n" +
|
|
112
|
+
"Response includes a [config] line showing actual parameters used — use this to decide adjustments on retry.",
|
|
113
|
+
{
|
|
114
|
+
query: z.string().describe(
|
|
115
|
+
'Natural language search query (e.g. "where is auth handled", "database connection pool")'
|
|
116
|
+
),
|
|
117
|
+
project_path: z
|
|
118
|
+
.string()
|
|
119
|
+
.default("")
|
|
120
|
+
.describe("Absolute path to project root. Empty = current working directory."),
|
|
121
|
+
tree_depth: z
|
|
122
|
+
.number()
|
|
123
|
+
.int()
|
|
124
|
+
.min(0)
|
|
125
|
+
.max(6)
|
|
126
|
+
.default(3)
|
|
127
|
+
.describe(
|
|
128
|
+
"Directory tree depth for the initial repo map sent to the remote AI. " +
|
|
129
|
+
"Use 0 for auto depth based on project size. " +
|
|
130
|
+
"Default 3. Use 1-2 for huge monorepos (>5000 files) or if you get payload size errors. " +
|
|
131
|
+
"Use 4-6 for small projects (<200 files) where you want the AI to see deeper structure. " +
|
|
132
|
+
"Auto falls back to a lower depth if tree output exceeds 250KB."
|
|
133
|
+
),
|
|
134
|
+
max_turns: z
|
|
135
|
+
.number()
|
|
136
|
+
.int()
|
|
137
|
+
.min(1)
|
|
138
|
+
.max(5)
|
|
139
|
+
.default(MAX_TURNS)
|
|
140
|
+
.describe(
|
|
141
|
+
"Number of search rounds. Each round: remote AI generates search commands → local execution → results sent back. " +
|
|
142
|
+
"Default 3. Use 1 for quick simple lookups. Use 4-5 for complex queries requiring deep tracing across many files. " +
|
|
143
|
+
"More rounds = better results but slower and uses more API quota."
|
|
144
|
+
),
|
|
145
|
+
max_results: z
|
|
146
|
+
.number()
|
|
147
|
+
.int()
|
|
148
|
+
.min(1)
|
|
149
|
+
.max(30)
|
|
150
|
+
.default(10)
|
|
151
|
+
.describe(
|
|
152
|
+
"Maximum number of files to return. Default 10. " +
|
|
153
|
+
"Use a smaller value (3-5) for focused queries. " +
|
|
154
|
+
"Use a larger value (15-30) for broad exploration queries."
|
|
155
|
+
),
|
|
156
|
+
exclude_paths: z
|
|
157
|
+
.array(z.string())
|
|
158
|
+
.default([])
|
|
159
|
+
.describe(
|
|
160
|
+
"Directory/file patterns to exclude from tree and search context. " +
|
|
161
|
+
"Useful for reducing payload size on large repos. " +
|
|
162
|
+
"Examples: ['node_modules', 'dist', '.git', 'build', 'coverage', '*.min.*']"
|
|
163
|
+
),
|
|
164
|
+
repo_map_mode: z
|
|
165
|
+
.enum(["classic", "bootstrap_hotspot"])
|
|
166
|
+
.default(DEFAULT_REPO_MAP_MODE)
|
|
167
|
+
.describe(
|
|
168
|
+
"Repo map strategy. classic = single tree map. bootstrap_hotspot = bootstrap mini-tree + query-scored hotspot subtrees."
|
|
169
|
+
),
|
|
170
|
+
bootstrap_tree_depth: z
|
|
171
|
+
.number()
|
|
172
|
+
.int()
|
|
173
|
+
.min(1)
|
|
174
|
+
.max(3)
|
|
175
|
+
.default(DEFAULT_BOOTSTRAP_TREE_DEPTH)
|
|
176
|
+
.describe("Bootstrap tree depth used when repo_map_mode=bootstrap_hotspot."),
|
|
177
|
+
hotspot_top_k: z
|
|
178
|
+
.number()
|
|
179
|
+
.int()
|
|
180
|
+
.min(0)
|
|
181
|
+
.max(8)
|
|
182
|
+
.default(DEFAULT_HOTSPOT_TOP_K)
|
|
183
|
+
.describe("Maximum number of hotspot top-level directories to append in repo map."),
|
|
184
|
+
hotspot_tree_depth: z
|
|
185
|
+
.number()
|
|
186
|
+
.int()
|
|
187
|
+
.min(1)
|
|
188
|
+
.max(4)
|
|
189
|
+
.default(DEFAULT_HOTSPOT_TREE_DEPTH)
|
|
190
|
+
.describe("Tree depth for each hotspot subtree in repo map."),
|
|
191
|
+
hotspot_max_bytes: z
|
|
192
|
+
.number()
|
|
193
|
+
.int()
|
|
194
|
+
.min(16384)
|
|
195
|
+
.max(262144)
|
|
196
|
+
.default(DEFAULT_HOTSPOT_MAX_BYTES)
|
|
197
|
+
.describe("Maximum bytes budget for optimized repo map output."),
|
|
198
|
+
bootstrap_enabled: z
|
|
199
|
+
.boolean()
|
|
200
|
+
.default(DEFAULT_BOOTSTRAP_ENABLED)
|
|
201
|
+
.describe("Enable standalone bootstrap phase before main search phase."),
|
|
202
|
+
bootstrap_max_turns: z
|
|
203
|
+
.number()
|
|
204
|
+
.int()
|
|
205
|
+
.min(1)
|
|
206
|
+
.max(3)
|
|
207
|
+
.default(DEFAULT_BOOTSTRAP_MAX_TURNS)
|
|
208
|
+
.describe("Max turns for bootstrap phase (independent from main max_turns)."),
|
|
209
|
+
bootstrap_max_commands: z
|
|
210
|
+
.number()
|
|
211
|
+
.int()
|
|
212
|
+
.min(1)
|
|
213
|
+
.max(8)
|
|
214
|
+
.default(DEFAULT_BOOTSTRAP_MAX_COMMANDS)
|
|
215
|
+
.describe("Max commands per turn for bootstrap phase."),
|
|
216
|
+
},
|
|
217
|
+
async ({
|
|
218
|
+
query,
|
|
219
|
+
project_path,
|
|
220
|
+
tree_depth,
|
|
221
|
+
max_turns,
|
|
222
|
+
max_results,
|
|
223
|
+
exclude_paths,
|
|
224
|
+
repo_map_mode,
|
|
225
|
+
bootstrap_tree_depth,
|
|
226
|
+
hotspot_top_k,
|
|
227
|
+
hotspot_tree_depth,
|
|
228
|
+
hotspot_max_bytes,
|
|
229
|
+
bootstrap_enabled,
|
|
230
|
+
bootstrap_max_turns,
|
|
231
|
+
bootstrap_max_commands,
|
|
232
|
+
}) => {
|
|
233
|
+
let projectPath = project_path || process.cwd();
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const { statSync } = await import("node:fs");
|
|
237
|
+
if (!statSync(projectPath).isDirectory()) {
|
|
238
|
+
return { content: [{ type: "text", text: `Error: project path does not exist: ${projectPath}` }] };
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
return { content: [{ type: "text", text: `Error: project path does not exist: ${projectPath}` }] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const result = await searchWithContent({
|
|
246
|
+
query,
|
|
247
|
+
projectRoot: projectPath,
|
|
248
|
+
maxTurns: max_turns,
|
|
249
|
+
maxCommands: MAX_COMMANDS,
|
|
250
|
+
maxResults: max_results,
|
|
251
|
+
treeDepth: tree_depth,
|
|
252
|
+
timeoutMs: TIMEOUT_MS,
|
|
253
|
+
excludePaths: exclude_paths,
|
|
254
|
+
repoMapMode: repo_map_mode,
|
|
255
|
+
bootstrapTreeDepth: bootstrap_tree_depth,
|
|
256
|
+
hotspotTopK: hotspot_top_k,
|
|
257
|
+
hotspotTreeDepth: hotspot_tree_depth,
|
|
258
|
+
hotspotMaxBytes: hotspot_max_bytes,
|
|
259
|
+
bootstrapEnabled: bootstrap_enabled,
|
|
260
|
+
bootstrapMaxTurns: bootstrap_max_turns,
|
|
261
|
+
bootstrapMaxCommands: bootstrap_max_commands,
|
|
262
|
+
});
|
|
263
|
+
return { content: [{ type: "text", text: result }] };
|
|
264
|
+
} catch (e) {
|
|
265
|
+
const code = e.code || "UNKNOWN";
|
|
266
|
+
return {
|
|
267
|
+
content: [{
|
|
268
|
+
type: "text", text:
|
|
269
|
+
`Error [${code}]: ${e.message}\n\n` +
|
|
270
|
+
`[hint] Suggestions based on error type:\n` +
|
|
271
|
+
` - Reduce tree_depth (current: ${tree_depth})\n` +
|
|
272
|
+
` - Add exclude_paths to filter large directories (e.g. ['node_modules', 'dist'])\n` +
|
|
273
|
+
` - Narrow project_path to a subdirectory\n` +
|
|
274
|
+
` - Reduce max_turns (current: ${max_turns})`
|
|
275
|
+
}]
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// ─── Tool: extract_windsurf_key ────────────────────────────
|
|
282
|
+
|
|
283
|
+
server.tool(
|
|
284
|
+
"extract_windsurf_key",
|
|
285
|
+
"Extract Windsurf API Key from local installation. " +
|
|
286
|
+
"Auto-detects OS (macOS/Windows/Linux) and reads the API key from " +
|
|
287
|
+
"Windsurf's local database. Set the result as WINDSURF_API_KEY env var.",
|
|
288
|
+
{},
|
|
289
|
+
async () => {
|
|
290
|
+
const result = await extractKeyInfo();
|
|
291
|
+
|
|
292
|
+
if (result.error) {
|
|
293
|
+
const text = `Error: ${result.error}\n${result.hint || ""}\nDB path: ${result.db_path || "N/A"}`;
|
|
294
|
+
return { content: [{ type: "text", text }] };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const key = result.api_key;
|
|
298
|
+
const text =
|
|
299
|
+
`Windsurf API Key extracted successfully\n\n` +
|
|
300
|
+
` Key: ${key.slice(0, 30)}...${key.slice(-10)}\n` +
|
|
301
|
+
` Length: ${key.length}\n` +
|
|
302
|
+
` Source: ${result.db_path}\n\n` +
|
|
303
|
+
`Usage:\n` +
|
|
304
|
+
` export WINDSURF_API_KEY="${key}"`;
|
|
305
|
+
|
|
306
|
+
return { content: [{ type: "text", text }] };
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// ─── Start ─────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
async function main() {
|
|
313
|
+
const transport = new StdioServerTransport();
|
|
314
|
+
await server.connect(transport);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
main().catch((err) => {
|
|
318
|
+
console.error("Fatal error:", err);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
});
|