token-goat 2.6.30 → 2.6.31

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.
Files changed (33) hide show
  1. package/README.md +54 -15
  2. package/SECURITY.md +72 -0
  3. package/dist/token-goat-chunk-4WGVGN6W.mjs +54 -0
  4. package/dist/token-goat-chunk-56C3XUQY.mjs +156 -0
  5. package/dist/token-goat-chunk-DCGBS2YZ.mjs +112 -0
  6. package/dist/token-goat-chunk-HPY7XE77.mjs +18311 -0
  7. package/dist/token-goat-chunk-J6JTYE2B.mjs +24 -0
  8. package/dist/token-goat-chunk-N2S7UAVU.mjs +13674 -0
  9. package/dist/token-goat-chunk-OIBTYBVR.mjs +901 -0
  10. package/dist/token-goat-chunk-P7I6LS5H.mjs +9643 -0
  11. package/dist/token-goat-chunk-PWD5XZ6C.mjs +9441 -0
  12. package/dist/token-goat-chunk-Q4N6SQWR.mjs +14860 -0
  13. package/dist/token-goat-chunk-QL2743EW.mjs +707 -0
  14. package/dist/token-goat-chunk-VYC34TBW.mjs +33 -0
  15. package/dist/token-goat-chunk-X7PIWZMB.mjs +5337 -0
  16. package/dist/token-goat-chunk-XEJBPPD7.mjs +15545 -0
  17. package/dist/token-goat-hook-chunk-2K7FV2ME.mjs +9438 -0
  18. package/dist/token-goat-hook-chunk-3OJELLK5.mjs +156 -0
  19. package/dist/{token-goat-hook-chunk-ZRJUZZQE.mjs → token-goat-hook-chunk-5AQYD2SZ.mjs} +1 -2
  20. package/dist/token-goat-hook-chunk-66W63A4Y.mjs +9643 -0
  21. package/dist/token-goat-hook-chunk-JT5RMRIY.mjs +901 -0
  22. package/dist/token-goat-hook-chunk-JVJUBWKT.mjs +13674 -0
  23. package/dist/{token-goat-hook-chunk-FMQ37OQD.mjs → token-goat-hook-chunk-K6HLYRRI.mjs} +25 -23
  24. package/dist/token-goat-hook-chunk-NIVRFAJ4.mjs +5323 -0
  25. package/dist/token-goat-hook-chunk-SJM4BUZQ.mjs +18309 -0
  26. package/dist/token-goat-hook-chunk-ULGSXGZK.mjs +23 -0
  27. package/dist/{token-goat-hook-chunk-XNZLMFQ2.mjs → token-goat-hook-chunk-YHRJMMEF.mjs} +2629 -801
  28. package/dist/token-goat-hook.mjs +5 -2
  29. package/dist/token-goat.core.mjs +14 -89574
  30. package/package.json +5 -4
  31. package/dist/token-goat-hook-chunk-4SM2PAR6.mjs +0 -33590
  32. package/dist/token-goat-hook-chunk-5B3OWRNC.mjs +0 -145
  33. package/dist/token-goat-hook-chunk-VKAQNN2R.mjs +0 -24416
@@ -0,0 +1,707 @@
1
+ import { createRequire as __cjsRequire } from 'node:module';
2
+ const require = __cjsRequire(import.meta.url);
3
+ import {
4
+ ABSENT_PIN,
5
+ CONTENT_MAX_INPUT_CHARS,
6
+ ConfinementIdentityError,
7
+ compressText,
8
+ createHandoff,
9
+ fileIdentity,
10
+ findSpecSeparator,
11
+ parseLineRange,
12
+ pinKey,
13
+ resolveHandoff,
14
+ retrieveText,
15
+ runBrief,
16
+ runChanged,
17
+ runExports,
18
+ runGrep,
19
+ runImports,
20
+ runOutline,
21
+ runRead,
22
+ runRefs,
23
+ runSection,
24
+ runSemantic,
25
+ runSkeleton,
26
+ runSymbol,
27
+ withPinnedReads
28
+ } from "./token-goat-chunk-P7I6LS5H.mjs";
29
+ import {
30
+ buildProjectMap,
31
+ embeddingsDepsAvailable,
32
+ formatProjectMap,
33
+ getDirtyPathsFor,
34
+ getProjectIndexCounts,
35
+ isWorkerRunning,
36
+ mapLookupBytesSaved
37
+ } from "./token-goat-chunk-HPY7XE77.mjs";
38
+ import {
39
+ VERSION,
40
+ dataDir,
41
+ extractErrorMessage,
42
+ getDb,
43
+ globalDbPath,
44
+ loadConfig,
45
+ normalizePath,
46
+ recordStat,
47
+ resolveProjectRoot
48
+ } from "./token-goat-chunk-X7PIWZMB.mjs";
49
+ import "./token-goat-chunk-VYC34TBW.mjs";
50
+ import {
51
+ init_define_import_meta_env
52
+ } from "./token-goat-chunk-4WGVGN6W.mjs";
53
+
54
+ // src/mcp_server.ts
55
+ init_define_import_meta_env();
56
+ import * as fs from "fs";
57
+ import * as path from "path";
58
+ var TOKEN_GOAT_RETRY_RE = /token-goat (\w[\w-]*) "([^"]+)"/g;
59
+ var MCP_MAX_LIMIT = 1e3;
60
+ var MCP_MAX_CONTEXT_LINES = 50;
61
+ var MCP_MAX_OUTPUT_LINES = 1e4;
62
+ var RETRY_PARAM_BY_COMMAND = {
63
+ read: "spec",
64
+ section: "spec",
65
+ symbol: "name",
66
+ skeleton: "file",
67
+ outline: "file"
68
+ };
69
+ function mcpFriendlyText(text) {
70
+ let out = text.replace(TOKEN_GOAT_RETRY_RE, (_match, cmd, arg) => {
71
+ const param = RETRY_PARAM_BY_COMMAND[cmd] ?? "parameter";
72
+ return `the "${cmd}" tool again with a more specific ${param} (e.g. "${arg}")`;
73
+ });
74
+ out = out.replace(/--json\b/g, "the json parameter");
75
+ out = out.replace(/--limit\b/g, "the limit parameter");
76
+ out = out.replace(/--top\b/g, "the top parameter");
77
+ out = out.replace(/--grep PATTERN, --section HEADING, or --tail N/g, "a narrower query");
78
+ return out;
79
+ }
80
+ function toCallToolResult(result) {
81
+ return {
82
+ content: [{ type: "text", text: mcpFriendlyText(result.text) }],
83
+ isError: result.code !== 0
84
+ };
85
+ }
86
+ function compressionPayload(result) {
87
+ if (result.inlineWins) return { ...result };
88
+ const { compact: _compact, ...rest } = result;
89
+ return { ...rest, payloadWithheld: "inlining the compact payload would cost more tokens than the original text; use the recovery command to retrieve it" };
90
+ }
91
+ function toRawCallToolResult(result) {
92
+ return {
93
+ content: [{ type: "text", text: result.text }],
94
+ isError: result.code !== 0
95
+ };
96
+ }
97
+ function captureOutput(fn) {
98
+ const chunks = [];
99
+ const record = (chunk, encodingOrCb, maybeCb) => {
100
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8"));
101
+ const callback = typeof encodingOrCb === "function" ? encodingOrCb : typeof maybeCb === "function" ? maybeCb : void 0;
102
+ if (typeof callback === "function") callback();
103
+ return true;
104
+ };
105
+ const origStdoutWrite = process.stdout.write.bind(process.stdout);
106
+ const origStderrWrite = process.stderr.write.bind(process.stderr);
107
+ process.stdout.write = record;
108
+ process.stderr.write = record;
109
+ try {
110
+ const code = fn();
111
+ return { code, text: chunks.join("") };
112
+ } finally {
113
+ process.stdout.write = origStdoutWrite;
114
+ process.stderr.write = origStderrWrite;
115
+ }
116
+ }
117
+ function toCallToolResultFromExitCode(fn) {
118
+ const { code, text } = captureOutput(fn);
119
+ return toCallToolResult({ text, code });
120
+ }
121
+ function realPathOrSelf(p) {
122
+ try {
123
+ return fs.realpathSync.native(p);
124
+ } catch {
125
+ return p;
126
+ }
127
+ }
128
+ function forCompare(p) {
129
+ return process.platform === "win32" ? p.toLowerCase() : p;
130
+ }
131
+ function resolveToolRoot(projectRoot) {
132
+ return resolveProjectRoot(projectRoot !== void 0 ? { project: projectRoot } : {});
133
+ }
134
+ function checkWithinProjectRoot(target, resolvedRoot) {
135
+ const root = forCompare(normalizePath(realPathOrSelf(resolvedRoot)));
136
+ const abs = path.resolve(resolvedRoot, normalizePath(target));
137
+ let identity;
138
+ try {
139
+ identity = fileIdentity(fs.statSync(abs, { bigint: true }));
140
+ } catch {
141
+ identity = null;
142
+ }
143
+ const realNative = realPathOrSelf(abs);
144
+ const real = forCompare(normalizePath(realNative));
145
+ return { inside: real === root || real.startsWith(root.endsWith("/") ? root : root + "/"), abs, real: realNative, identity };
146
+ }
147
+ function specFilePart(spec) {
148
+ const range = parseLineRange(spec);
149
+ if (range !== null) return range.file;
150
+ const colonIdx = findSpecSeparator(spec);
151
+ return colonIdx === -1 ? spec : spec.slice(0, colonIdx);
152
+ }
153
+ var NO_PINS = /* @__PURE__ */ new Map();
154
+ function confineTargets(targets, resolvedRoot, splitCommas = true) {
155
+ if (!loadConfig(resolvedRoot).mcp.confine_reads_to_project_root) return { ok: true, targets, pins: NO_PINS };
156
+ const allowedRoots = loadConfig().mcp.allowed_roots;
157
+ if (allowedRoots.length > 0 && !allowedRoots.some((allowed) => checkWithinProjectRoot(resolvedRoot, allowed).inside)) {
158
+ return {
159
+ ok: false,
160
+ refusal: toCallToolResult({
161
+ text: `refused: "${resolvedRoot}" is not inside any root listed in mcp.allowed_roots. A caller-supplied projectRoot is untrusted input, so this deployment pins which roots may be named; add the root to mcp.allowed_roots (or TOKEN_GOAT_MCP_ALLOWED_ROOTS) to permit it.`,
162
+ code: 1
163
+ })
164
+ };
165
+ }
166
+ const checked = [];
167
+ const pins = /* @__PURE__ */ new Map();
168
+ for (const raw of targets) {
169
+ const parts = splitCommas ? raw.split(",") : [raw];
170
+ for (const part of parts) {
171
+ const file = specFilePart(part);
172
+ if (file === "") continue;
173
+ const check = checkWithinProjectRoot(file, resolvedRoot);
174
+ if (!check.inside) {
175
+ return {
176
+ ok: false,
177
+ refusal: toCallToolResult({
178
+ text: `refused: "${file}" is outside the project root. The MCP tools are confined to the workspace. Set mcp.confine_reads_to_project_root = false (or TOKEN_GOAT_MCP_CONFINE_READS=0) to allow cross-root reads.`,
179
+ code: 1
180
+ })
181
+ };
182
+ }
183
+ const pinIdentity = check.identity ?? ABSENT_PIN;
184
+ pins.set(pinKey(check.abs), pinIdentity);
185
+ pins.set(pinKey(check.real), pinIdentity);
186
+ }
187
+ checked.push(splitCommas ? parts.join(",") : parts[0]);
188
+ }
189
+ return { ok: true, targets: checked, pins };
190
+ }
191
+ function withConfinedRead(pins, fn) {
192
+ try {
193
+ return withPinnedReads(pins, fn);
194
+ } catch (err) {
195
+ if (err instanceof ConfinementIdentityError) {
196
+ return toCallToolResult({
197
+ text: `${err.message} The MCP tools are confined to the workspace. Set mcp.confine_reads_to_project_root = false (or TOKEN_GOAT_MCP_CONFINE_READS=0) to allow cross-root reads.`,
198
+ code: 1
199
+ });
200
+ }
201
+ throw err;
202
+ }
203
+ }
204
+ async function createMcpServer() {
205
+ const [{ McpServer }, { z }] = await Promise.all([import("@modelcontextprotocol/sdk/server/mcp.js"), import("./token-goat-chunk-Q4N6SQWR.mjs")]);
206
+ const server = new McpServer({ name: "token-goat", version: VERSION });
207
+ const makeProjectRootField = (verb) => z.string().optional().describe(
208
+ `absolute path to the workspace root to scope this ${verb} to; defaults to the MCP server process's cwd, which is not always the actual workspace root for MCP clients -- pass this explicitly when it might differ`
209
+ );
210
+ const projectRootField = makeProjectRootField("lookup");
211
+ server.registerTool(
212
+ "symbol",
213
+ {
214
+ description: "Search for a symbol by name across the indexed project.",
215
+ inputSchema: {
216
+ name: z.string().describe("symbol name to search for"),
217
+ limit: z.number().int().positive().max(MCP_MAX_LIMIT).optional().describe("max results (default: 20)"),
218
+ file: z.string().optional().describe("restrict to one file"),
219
+ kind: z.string().optional().describe("restrict to one kind (function, class, ...)"),
220
+ json: z.boolean().optional().describe("output as JSON"),
221
+ projectRoot: projectRootField
222
+ }
223
+ },
224
+ (args) => {
225
+ const { name, limit, file, kind, json, projectRoot } = args;
226
+ const root = resolveToolRoot(projectRoot);
227
+ let confinedFile = file;
228
+ let pins = NO_PINS;
229
+ if (file !== void 0) {
230
+ const gate = confineTargets([file], root);
231
+ if (!gate.ok) return gate.refusal;
232
+ confinedFile = gate.targets[0];
233
+ pins = gate.pins;
234
+ }
235
+ return withConfinedRead(
236
+ pins,
237
+ () => toCallToolResult(
238
+ runSymbol({
239
+ name,
240
+ limit: limit ?? 20,
241
+ ...confinedFile !== void 0 ? { file: confinedFile } : {},
242
+ ...kind !== void 0 ? { kind } : {},
243
+ ...json === true ? { json: true } : {},
244
+ projectRoot: root
245
+ })
246
+ )
247
+ );
248
+ }
249
+ );
250
+ server.registerTool(
251
+ "read",
252
+ {
253
+ description: "Read one symbol's full body, given a spec of the form file::symbol, or a line range file@N-M / file@N, or a bare file path. Pass a comma-separated spec (file::a,b) to fetch several symbols' bodies from one file in a single call.",
254
+ inputSchema: {
255
+ spec: z.string().describe("file::symbol, file@N-M, file@N, a bare file path, or comma-separated file::a,b for a merged multi-symbol view"),
256
+ json: z.boolean().optional().describe("output as JSON"),
257
+ forceRefresh: z.boolean().optional().describe("reparse file from disk before querying (ignore stale index)"),
258
+ stats: z.boolean().optional().describe("add per-symbol reference count and doc-coverage flag"),
259
+ projectRoot: projectRootField
260
+ }
261
+ },
262
+ (args) => {
263
+ const { spec, json, forceRefresh, stats, projectRoot } = args;
264
+ const root = resolveToolRoot(projectRoot);
265
+ const gate = confineTargets([spec], root);
266
+ if (!gate.ok) return gate.refusal;
267
+ return withConfinedRead(
268
+ gate.pins,
269
+ () => toCallToolResult(
270
+ runRead({
271
+ spec: gate.targets[0],
272
+ ...json === true ? { json: true } : {},
273
+ ...forceRefresh === true ? { forceRefresh: true } : {},
274
+ ...stats === true ? { stats: true } : {},
275
+ projectRoot: root
276
+ })
277
+ )
278
+ );
279
+ }
280
+ );
281
+ server.registerTool(
282
+ "section",
283
+ {
284
+ description: "Read one section from a file, given a spec of the form file::Heading.",
285
+ inputSchema: {
286
+ spec: z.string().describe("file::Heading"),
287
+ json: z.boolean().optional().describe("output as JSON"),
288
+ projectRoot: projectRootField
289
+ }
290
+ },
291
+ (args) => {
292
+ const { spec, json, projectRoot } = args;
293
+ const root = resolveToolRoot(projectRoot);
294
+ const gate = confineTargets([spec], root);
295
+ if (!gate.ok) return gate.refusal;
296
+ return withConfinedRead(
297
+ gate.pins,
298
+ () => toCallToolResult(
299
+ runSection({
300
+ spec: gate.targets[0],
301
+ ...json === true ? { json: true } : {},
302
+ projectRoot: root
303
+ })
304
+ )
305
+ );
306
+ }
307
+ );
308
+ server.registerTool(
309
+ "skeleton",
310
+ {
311
+ description: "List all symbols in a file without bodies (name, kind, line range).",
312
+ inputSchema: {
313
+ file: z.string().describe("file path"),
314
+ json: z.boolean().optional().describe("output as JSON"),
315
+ minLines: z.number().int().optional().describe("only show symbols at least N lines long"),
316
+ forceRefresh: z.boolean().optional().describe("reparse file from disk before querying (ignore stale index)"),
317
+ stats: z.boolean().optional().describe("add per-symbol reference count and doc-coverage flag"),
318
+ projectRoot: projectRootField
319
+ }
320
+ },
321
+ (args) => {
322
+ const { file, json, minLines, forceRefresh, stats, projectRoot } = args;
323
+ const root = resolveToolRoot(projectRoot);
324
+ const gate = confineTargets([file], root);
325
+ if (!gate.ok) return gate.refusal;
326
+ return withConfinedRead(
327
+ gate.pins,
328
+ () => toCallToolResult(
329
+ runSkeleton({
330
+ file: gate.targets[0],
331
+ ...json === true ? { json: true } : {},
332
+ ...minLines !== void 0 ? { minLines } : {},
333
+ ...forceRefresh === true ? { forceRefresh: true } : {},
334
+ ...stats === true ? { stats: true } : {},
335
+ projectRoot: root
336
+ })
337
+ )
338
+ );
339
+ }
340
+ );
341
+ server.registerTool(
342
+ "outline",
343
+ {
344
+ description: "List symbols in a file with line ranges and docstrings.",
345
+ inputSchema: {
346
+ file: z.string().describe("file path"),
347
+ json: z.boolean().optional().describe("output as JSON"),
348
+ minLines: z.number().int().optional().describe("only show symbols at least N lines long"),
349
+ forceRefresh: z.boolean().optional().describe("reparse file from disk before querying (ignore stale index)"),
350
+ stats: z.boolean().optional().describe("add per-symbol reference count and doc-coverage flag"),
351
+ projectRoot: projectRootField
352
+ }
353
+ },
354
+ (args) => {
355
+ const { file, json, minLines, forceRefresh, stats, projectRoot } = args;
356
+ const root = resolveToolRoot(projectRoot);
357
+ const gate = confineTargets([file], root);
358
+ if (!gate.ok) return gate.refusal;
359
+ return withConfinedRead(
360
+ gate.pins,
361
+ () => toCallToolResult(
362
+ runOutline({
363
+ file: gate.targets[0],
364
+ ...json === true ? { json: true } : {},
365
+ ...minLines !== void 0 ? { minLines } : {},
366
+ ...forceRefresh === true ? { forceRefresh: true } : {},
367
+ ...stats === true ? { stats: true } : {},
368
+ projectRoot: root
369
+ })
370
+ )
371
+ );
372
+ }
373
+ );
374
+ server.registerTool(
375
+ "semantic",
376
+ {
377
+ description: "Semantic search over the indexed project (falls back to full-text search when no embedding index is available). Scoped to projectRoot if given, else the MCP server process's own cwd -- which may not be the actual workspace root for a client that launched the server from elsewhere, so pass projectRoot explicitly when in doubt.",
378
+ inputSchema: {
379
+ query: z.string().describe("natural-language search query"),
380
+ limit: z.number().int().positive().max(MCP_MAX_LIMIT).optional().describe("max results (default: 20)"),
381
+ grep: z.string().optional().describe("filter to hits whose file path matches this regex (literal substring if it does not compile as regex); matched against the path as rendered, same convention as refs --grep"),
382
+ excludeTests: z.boolean().optional().describe("hide hits whose file is a test file (opt-in; default output is unchanged)"),
383
+ json: z.boolean().optional().describe("output as JSON"),
384
+ projectRoot: makeProjectRootField("search")
385
+ }
386
+ },
387
+ async (args) => {
388
+ const { query, limit, grep, excludeTests, json, projectRoot } = args;
389
+ const root = resolveToolRoot(projectRoot);
390
+ return toCallToolResult(
391
+ await runSemantic(query, {
392
+ ...limit !== void 0 ? { limit } : {},
393
+ ...grep !== void 0 ? { grep } : {},
394
+ ...excludeTests === true ? { excludeTests: true } : {},
395
+ ...json === true ? { json: true } : {},
396
+ projectRoot: root
397
+ })
398
+ );
399
+ }
400
+ );
401
+ server.registerTool(
402
+ "index_status",
403
+ {
404
+ description: 'Report whether the index for a project can be trusted right now: whether it has ever been indexed at all, current file/symbol counts, dirty-reindex-queue depth, whether the background worker is alive, and whether embeddings are available (semantic silently degrades to full-text search without them). Call this after an unexpectedly empty result from another token-goat tool (symbol/read/semantic/refs/brief/...) to tell apart "no match" from "the index is not ready yet" -- an MCP-only client has no hook layer to warn about this on its own, so an empty tool result and a stale/unindexed project look identical without this check.',
405
+ inputSchema: {
406
+ projectRoot: makeProjectRootField("check")
407
+ }
408
+ },
409
+ (args) => {
410
+ const { projectRoot } = args;
411
+ const rootDir = resolveToolRoot(projectRoot);
412
+ const dbPath = globalDbPath();
413
+ const databaseExists = fs.existsSync(dbPath);
414
+ let fileCount = 0;
415
+ let symbolCount = 0;
416
+ let queryError;
417
+ if (databaseExists) {
418
+ try {
419
+ const counts = getProjectIndexCounts(dbPath, rootDir);
420
+ fileCount = counts.fileCount;
421
+ symbolCount = counts.symbolCount;
422
+ } catch (err) {
423
+ queryError = extractErrorMessage(err);
424
+ }
425
+ }
426
+ const resolvedDataDir = dataDir();
427
+ const dirtyQueueDepth = getDirtyPathsFor(resolvedDataDir).length;
428
+ const workerAlive = isWorkerRunning(resolvedDataDir);
429
+ let embeddingsAvailable = false;
430
+ if (databaseExists && queryError === void 0) {
431
+ try {
432
+ embeddingsAvailable = embeddingsDepsAvailable(getDb(dbPath));
433
+ } catch {
434
+ embeddingsAvailable = false;
435
+ }
436
+ }
437
+ const embeddingsEnabled = loadConfig(rootDir).indexing?.embeddings_enabled ?? true;
438
+ const status = {
439
+ projectRoot: rootDir,
440
+ databaseExists,
441
+ indexedForProject: fileCount > 0,
442
+ fileCount,
443
+ symbolCount,
444
+ ...queryError !== void 0 ? { queryError } : {},
445
+ dirtyQueueDepth,
446
+ workerAlive,
447
+ embeddingsEnabled,
448
+ embeddingsAvailable
449
+ };
450
+ return toCallToolResult({ text: JSON.stringify(status, null, 2), code: 0 });
451
+ }
452
+ );
453
+ server.registerTool(
454
+ "refs",
455
+ {
456
+ description: "Find references to one or more symbols (spec: file::symbol, symbol, or comma-separated a,b,c / file::a,b for a merged multi-symbol view). For an unambiguous TypeScript symbol, automatically type-resolves candidates via the TypeScript compiler API to drop same-named-different-symbol false positives; falls back to name-based matching when that is not possible.",
457
+ inputSchema: {
458
+ spec: z.string().describe("file::symbol, symbol, or comma-separated a,b,c / file::a,b for a merged multi-symbol view"),
459
+ callers: z.boolean().optional().describe("group references by their enclosing caller symbol"),
460
+ limit: z.number().int().positive().max(MCP_MAX_LIMIT).optional().describe("max results"),
461
+ top: z.number().int().positive().max(MCP_MAX_LIMIT).optional().describe(
462
+ "for a high-fanout symbol, group references by file (count only) and show only the top N files by reference count instead of a per-line dump"
463
+ ),
464
+ json: z.boolean().optional().describe("output as JSON"),
465
+ projectRoot: projectRootField
466
+ }
467
+ },
468
+ (args) => {
469
+ const { spec, callers, limit, top, json, projectRoot } = args;
470
+ const root = resolveToolRoot(projectRoot);
471
+ const gate = confineTargets([spec], root);
472
+ if (!gate.ok) return gate.refusal;
473
+ return withConfinedRead(
474
+ gate.pins,
475
+ () => toCallToolResultFromExitCode(
476
+ () => runRefs({
477
+ spec: gate.targets[0],
478
+ ...callers === true ? { callers: true } : {},
479
+ ...json === true ? { json: true } : {},
480
+ ...limit !== void 0 ? { limit } : {},
481
+ ...top !== void 0 ? { top } : {},
482
+ projectRoot: root
483
+ })
484
+ )
485
+ );
486
+ }
487
+ );
488
+ server.registerTool(
489
+ "brief",
490
+ {
491
+ description: "One-shot symbol orientation: signature, location, token count, body, callers, and containing doc section, in a single call (spec: file::symbol; comma-separated file::a,b for a merged multi-symbol view; cross-file a.ts::x,b.ts::y is also supported -- unlike refs, a bare symbol name with no file is not accepted). Prefer this over separate read + refs calls when the goal is to understand a symbol, not just fetch its source: it folds the work of read (body) and refs --callers (call sites) into one result, at a fraction of the combined round-trip cost.",
492
+ inputSchema: {
493
+ spec: z.string().describe("file::symbol; comma-separated file::a,b for a merged multi-symbol view; cross-file a.ts::x,b.ts::y is also supported"),
494
+ limit: z.number().int().positive().max(MCP_MAX_LIMIT).optional().describe("max callers to show (default: 20)"),
495
+ json: z.boolean().optional().describe("output as JSON"),
496
+ context: z.number().int().nonnegative().max(MCP_MAX_CONTEXT_LINES).optional().describe("lines of call-site source to show before and after each caller (default 0)"),
497
+ excludeTests: z.boolean().optional().describe("hide callers whose call site lives in a test file (opt-in; default output is unchanged)"),
498
+ grep: z.string().optional().describe("only show callers whose enclosing symbol name matches this regex (literal substring if it is not valid regex)"),
499
+ projectRoot: makeProjectRootField("orient")
500
+ }
501
+ },
502
+ (args) => {
503
+ const { spec, limit, json, context, excludeTests, grep, projectRoot } = args;
504
+ const root = resolveToolRoot(projectRoot);
505
+ const gate = confineTargets([spec], root);
506
+ if (!gate.ok) return gate.refusal;
507
+ return withConfinedRead(
508
+ gate.pins,
509
+ () => toCallToolResultFromExitCode(
510
+ () => runBrief({
511
+ spec: gate.targets[0],
512
+ ...json === true ? { json: true } : {},
513
+ ...limit !== void 0 ? { limit } : {},
514
+ ...context !== void 0 ? { context } : {},
515
+ ...excludeTests === true ? { excludeTests: true } : {},
516
+ ...grep !== void 0 ? { grep } : {},
517
+ projectRoot: root
518
+ })
519
+ )
520
+ );
521
+ }
522
+ );
523
+ server.registerTool(
524
+ "map",
525
+ {
526
+ description: "Project overview: file count, languages, headline symbols, and recently modified files.",
527
+ inputSchema: {
528
+ compact: z.boolean().optional().describe("compact, low-token summary"),
529
+ projectRoot: makeProjectRootField("overview")
530
+ }
531
+ },
532
+ (args) => {
533
+ const { compact, projectRoot } = args;
534
+ const map = buildProjectMap(resolveToolRoot(projectRoot), { compact: compact === true });
535
+ const text = formatProjectMap(map, map.compact);
536
+ const bytesSaved = mapLookupBytesSaved(map, text);
537
+ recordStat("map_lookup", bytesSaved, Math.round(bytesSaved / 4));
538
+ return toCallToolResult({ text, code: 0 });
539
+ }
540
+ );
541
+ server.registerTool(
542
+ "changed",
543
+ {
544
+ description: "List files or symbols changed since a git ref.",
545
+ inputSchema: {
546
+ ref: z.string().optional().describe("git ref to compare against (default: HEAD~5)"),
547
+ symbolMode: z.boolean().optional().describe("list symbols instead of files"),
548
+ json: z.boolean().optional().describe("output as JSON"),
549
+ projectRoot: projectRootField
550
+ }
551
+ },
552
+ (args) => {
553
+ const { ref, symbolMode, json, projectRoot } = args;
554
+ return toCallToolResultFromExitCode(
555
+ () => runChanged({
556
+ ...ref !== void 0 ? { ref } : {},
557
+ ...symbolMode === true ? { symbolMode: true } : {},
558
+ ...json === true ? { json: true } : {},
559
+ projectRoot: resolveToolRoot(projectRoot)
560
+ })
561
+ );
562
+ }
563
+ );
564
+ server.registerTool(
565
+ "grep",
566
+ {
567
+ description: "Regex search over files, caching nothing (session-aware grep).",
568
+ inputSchema: {
569
+ pattern: z.string().describe("regex pattern to search for"),
570
+ path: z.array(z.string()).optional().describe("files or directories to search; defaults to this server process's cwd"),
571
+ maxLines: z.number().int().positive().max(MCP_MAX_OUTPUT_LINES).optional().describe("max matching lines to print"),
572
+ json: z.boolean().optional().describe("output as JSON"),
573
+ recursive: z.boolean().optional().describe("descend into subdirectories (default: true)"),
574
+ context: z.number().int().nonnegative().max(MCP_MAX_CONTEXT_LINES).optional().describe("lines of context to show before and after each match"),
575
+ // runGrep takes no projectRoot of its own (its `path` array is its scope), so this field only names the root the confinement check is made against -- without it, a search rooted anywhere but the server process's cwd is refused.
576
+ projectRoot: makeProjectRootField("search")
577
+ }
578
+ },
579
+ (args) => {
580
+ const { pattern, path: searchPath, maxLines, json, recursive, context, projectRoot } = args;
581
+ const root = resolveToolRoot(projectRoot);
582
+ const gate = confineTargets(searchPath === void 0 ? [root] : searchPath, root, false);
583
+ if (!gate.ok) return gate.refusal;
584
+ const confinedPath = searchPath === void 0 ? void 0 : [...gate.targets];
585
+ return withConfinedRead(
586
+ gate.pins,
587
+ () => toCallToolResultFromExitCode(
588
+ () => runGrep({
589
+ pattern,
590
+ ...confinedPath !== void 0 && confinedPath.length > 0 ? { path: confinedPath } : {},
591
+ ...json === true ? { json: true } : {},
592
+ ...maxLines !== void 0 ? { maxLines } : {},
593
+ ...recursive === false ? { recursive: false } : {},
594
+ ...context !== void 0 ? { context } : {},
595
+ projectRoot: root
596
+ })
597
+ )
598
+ );
599
+ }
600
+ );
601
+ server.registerTool(
602
+ "imports",
603
+ {
604
+ description: "List the modules a file imports.",
605
+ inputSchema: {
606
+ file: z.string().describe("file path"),
607
+ json: z.boolean().optional().describe("output as JSON"),
608
+ projectRoot: projectRootField
609
+ }
610
+ },
611
+ (args) => {
612
+ const { file, json, projectRoot } = args;
613
+ const root = resolveToolRoot(projectRoot);
614
+ const gate = confineTargets([file], root);
615
+ if (!gate.ok) return gate.refusal;
616
+ return withConfinedRead(
617
+ gate.pins,
618
+ () => toCallToolResultFromExitCode(
619
+ () => runImports({ file: gate.targets[0], ...json === true ? { json: true } : {}, projectRoot: root })
620
+ )
621
+ );
622
+ }
623
+ );
624
+ server.registerTool(
625
+ "exports",
626
+ {
627
+ description: "List exported (public) symbols in a file.",
628
+ inputSchema: {
629
+ file: z.string().describe("file path"),
630
+ json: z.boolean().optional().describe("output as JSON"),
631
+ projectRoot: projectRootField
632
+ }
633
+ },
634
+ (args) => {
635
+ const { file, json, projectRoot } = args;
636
+ const root = resolveToolRoot(projectRoot);
637
+ const gate = confineTargets([file], root);
638
+ if (!gate.ok) return gate.refusal;
639
+ return withConfinedRead(
640
+ gate.pins,
641
+ () => toCallToolResultFromExitCode(
642
+ () => runExports({ file: gate.targets[0], ...json === true ? { json: true } : {}, projectRoot: root })
643
+ )
644
+ );
645
+ }
646
+ );
647
+ server.registerTool(
648
+ "compress_text",
649
+ {
650
+ description: "Compress arbitrary local text, persist it in the bounded local cache, and return an opaque recovery ID plus metadata.",
651
+ inputSchema: {
652
+ text: z.string().max(CONTENT_MAX_INPUT_CHARS).describe("text to compress")
653
+ }
654
+ },
655
+ (args) => toCallToolResult({ text: JSON.stringify(compressionPayload(compressText(args.text)), null, 2), code: 0 })
656
+ );
657
+ server.registerTool(
658
+ "retrieve_text",
659
+ {
660
+ description: "Retrieve original text from a token-goat compression ID.",
661
+ inputSchema: {
662
+ id: z.string().regex(/^tg_[0-9a-f]{16}$/).describe("opaque token-goat content ID")
663
+ }
664
+ },
665
+ (args) => {
666
+ const text = retrieveText(args.id);
667
+ return text === null ? toCallToolResult({ text: `no token-goat content for id: ${args.id}`, code: 1 }) : toRawCallToolResult({ text, code: 0 });
668
+ }
669
+ );
670
+ server.registerTool(
671
+ "handoff_create",
672
+ {
673
+ description: "Create a bounded, project-local named compressed handoff for another agent.",
674
+ inputSchema: {
675
+ name: z.string().regex(/^[A-Za-z0-9._-]{1,128}$/).describe("handoff name"),
676
+ text: z.string().max(CONTENT_MAX_INPUT_CHARS).describe("handoff text"),
677
+ projectRoot: makeProjectRootField("scope")
678
+ }
679
+ },
680
+ (args) => toCallToolResult({
681
+ text: JSON.stringify(createHandoff(args.name, args.text, resolveToolRoot(args.projectRoot)), null, 2),
682
+ code: 0
683
+ })
684
+ );
685
+ server.registerTool(
686
+ "handoff_resolve",
687
+ {
688
+ description: "Resolve a project-local handoff compactly or in full. MCP does not intercept built-in file reads.",
689
+ inputSchema: {
690
+ name: z.string().regex(/^[A-Za-z0-9._-]{1,128}$/).describe("handoff name"),
691
+ full: z.boolean().optional().describe("return full text instead of a compact payload"),
692
+ projectRoot: makeProjectRootField("scope")
693
+ }
694
+ },
695
+ (args) => {
696
+ const result = resolveHandoff(args.name, {
697
+ projectRoot: resolveToolRoot(args.projectRoot),
698
+ ...args.full === true ? { full: true } : {}
699
+ });
700
+ return result === null ? toCallToolResult({ text: `no local handoff named "${args.name}" in this project`, code: 1 }) : typeof result === "string" ? toRawCallToolResult({ text: result, code: 0 }) : toCallToolResult({ text: JSON.stringify(compressionPayload(result), null, 2), code: 0 });
701
+ }
702
+ );
703
+ return server;
704
+ }
705
+ export {
706
+ createMcpServer
707
+ };