@foundation0/api 1.1.8 → 1.1.10

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/mcp/server.ts CHANGED
@@ -174,9 +174,6 @@ const isDir = (candidate: string): boolean => {
174
174
  }
175
175
  };
176
176
 
177
- const looksLikeRepoRoot = (candidate: string): boolean =>
178
- isDir(path.join(candidate, "projects")) && isDir(path.join(candidate, "api"));
179
-
180
177
  const fileExists = (candidate: string): boolean => {
181
178
  try {
182
179
  fs.statSync(candidate);
@@ -198,8 +195,8 @@ const findGitRepoRoot = (startDir: string): string | null => {
198
195
  return null;
199
196
  };
200
197
 
201
- const resolveGitDir = (repoRoot: string): string | null => {
202
- const dotGit = path.join(repoRoot, ".git");
198
+ const resolveGitDir = (workspaceRoot: string): string | null => {
199
+ const dotGit = path.join(workspaceRoot, ".git");
203
200
  if (isDir(dotGit)) return dotGit;
204
201
  if (!fileExists(dotGit)) return null;
205
202
 
@@ -209,14 +206,22 @@ const resolveGitDir = (repoRoot: string): string | null => {
209
206
  if (!match) return null;
210
207
  const raw = match[1].trim();
211
208
  if (!raw) return null;
212
- const resolved = path.resolve(repoRoot, raw);
209
+ const resolved = path.resolve(workspaceRoot, raw);
213
210
  return isDir(resolved) ? resolved : null;
214
211
  } catch {
215
212
  return null;
216
213
  }
217
214
  };
218
215
 
219
- const parseRepoNameFromRemoteUrl = (remoteUrl: string): string | null => {
216
+ type GitRemoteIdentity = {
217
+ owner: string | null;
218
+ repo: string;
219
+ fullName: string | null;
220
+ };
221
+
222
+ const parseGitRemoteIdentityFromUrl = (
223
+ remoteUrl: string,
224
+ ): GitRemoteIdentity | null => {
220
225
  const trimmed = remoteUrl.trim();
221
226
  if (!trimmed) return null;
222
227
 
@@ -225,11 +230,34 @@ const parseRepoNameFromRemoteUrl = (remoteUrl: string): string | null => {
225
230
  const withoutGit = withoutQuery.endsWith(".git")
226
231
  ? withoutQuery.slice(0, -4)
227
232
  : withoutQuery;
233
+ const normalized = withoutGit.replace(/\\/g, "/");
234
+ const scpPathMatch = normalized.match(/^[^@]+@[^:]+:(.+)$/);
235
+ let pathPart = scpPathMatch?.[1] ?? normalized;
228
236
 
229
- const lastSep = Math.max(withoutGit.lastIndexOf("/"), withoutGit.lastIndexOf(":"));
230
- const candidate = (lastSep >= 0 ? withoutGit.slice(lastSep + 1) : withoutGit).trim();
231
- if (!candidate) return null;
232
- return candidate;
237
+ if (!scpPathMatch) {
238
+ try {
239
+ const url = new URL(normalized);
240
+ pathPart = url.pathname;
241
+ } catch {
242
+ // Keep normalized value as-is.
243
+ }
244
+ }
245
+
246
+ const cleanPath = pathPart.replace(/^\/+|\/+$/g, "");
247
+ if (!cleanPath) return null;
248
+ const segments = cleanPath
249
+ .split("/")
250
+ .map((segment) => segment.trim())
251
+ .filter((segment) => segment.length > 0);
252
+ if (segments.length === 0) return null;
253
+ const repo = segments[segments.length - 1];
254
+ if (!repo) return null;
255
+ const owner = segments.length > 1 ? segments[segments.length - 2] : null;
256
+ return {
257
+ owner,
258
+ repo,
259
+ fullName: owner ? `${owner}/${repo}` : null,
260
+ };
233
261
  };
234
262
 
235
263
  const readGitRemoteUrl = (gitDir: string): string | null => {
@@ -267,114 +295,55 @@ const readGitRemoteUrl = (gitDir: string): string | null => {
267
295
  }
268
296
  };
269
297
 
270
- const detectRepoNameFromGitConfig = (repoRoot: string): string | null => {
271
- const gitDir = resolveGitDir(repoRoot);
298
+ const detectRepoIdentityFromGitConfig = (
299
+ workspaceRoot: string,
300
+ ): GitRemoteIdentity | null => {
301
+ const gitDir = resolveGitDir(workspaceRoot);
272
302
  if (!gitDir) return null;
273
303
  const remoteUrl = readGitRemoteUrl(gitDir);
274
304
  if (!remoteUrl) return null;
275
- return parseRepoNameFromRemoteUrl(remoteUrl);
305
+ return parseGitRemoteIdentityFromUrl(remoteUrl);
276
306
  };
277
307
 
278
- const normalizeRepoRoot = (raw: string): string => {
279
- const resolved = path.resolve(raw);
280
- if (looksLikeRepoRoot(resolved)) return resolved;
281
-
282
- // Common mistake: passing a project root like ".../projects/adl" as repoRoot.
283
- // Try to find the containing repo root by walking up a few levels.
284
- let current = resolved;
285
- for (let depth = 0; depth < 8; depth += 1) {
286
- const parent = path.dirname(current);
287
- if (parent === current) break;
288
- if (looksLikeRepoRoot(parent)) return parent;
289
- current = parent;
290
- }
291
-
292
- const parts = resolved.split(path.sep).filter((part) => part.length > 0);
293
- const projectsIndex = parts.lastIndexOf("projects");
294
- if (projectsIndex >= 0) {
295
- const candidate = parts.slice(0, projectsIndex).join(path.sep);
296
- if (candidate && looksLikeRepoRoot(candidate)) return candidate;
297
- }
298
-
299
- return resolved;
300
- };
301
-
302
- const normalizeRepoRootOption = (
303
- options: Record<string, unknown>,
304
- ): Record<string, unknown> => {
305
- const rawRepoRoot = typeof options.repoRoot === "string" ? options.repoRoot : null;
306
- const rawProcessRoot =
307
- typeof options.processRoot === "string" ? options.processRoot : null;
308
- const raw = rawRepoRoot ?? rawProcessRoot;
309
-
310
- if (typeof raw !== "string" || raw.trim().length === 0) {
311
- const next = { ...options };
312
- delete next.repoRoot;
313
- delete next.processRoot;
314
- return next;
315
- }
316
-
317
- const trimmed = raw.trim();
318
- const normalized = normalizeRepoRoot(trimmed);
319
-
320
- const next: Record<string, unknown> = { ...options, repoRoot: normalized };
321
- delete next.processRoot;
322
-
323
- const alreadyCanonical =
324
- rawRepoRoot !== null && rawRepoRoot === normalized && !("processRoot" in options);
325
- return alreadyCanonical ? options : next;
326
- };
327
-
328
- const looksLikePathish = (value: string): boolean => {
329
- const trimmed = value.trim();
330
- if (!trimmed) return false;
331
- if (/^[a-zA-Z]:[\\/]/.test(trimmed)) return true;
332
- return trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith(".");
308
+ type RepoResolutionContext = {
309
+ defaultProcessRoot: string;
310
+ defaultRepoName: string;
311
+ defaultRepoFullName: string | null;
312
+ repoNames: string[];
313
+ repoNameByKey: Record<string, string>;
333
314
  };
334
315
 
335
- const parseRepoMap = (raw: string | undefined): Record<string, string> => {
336
- const input = typeof raw === "string" ? raw.trim() : "";
337
- if (!input) return {};
338
-
339
- if (input.startsWith("{")) {
340
- try {
341
- const parsed = JSON.parse(input);
342
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
343
- return {};
344
- }
345
- const out: Record<string, string> = {};
346
- for (const [name, root] of Object.entries(parsed)) {
347
- if (!name || typeof root !== "string" || !root.trim()) continue;
348
- out[name.trim()] = root.trim();
349
- }
350
- return out;
351
- } catch {
352
- return {};
353
- }
354
- }
316
+ const repoNameKey = (value: string): string => value.trim().toLowerCase();
355
317
 
356
- const entries = input
357
- .split(/[;,]/g)
358
- .map((entry) => entry.trim())
359
- .filter((entry) => entry.length > 0);
360
-
361
- const out: Record<string, string> = {};
362
- for (const entry of entries) {
363
- const [name, root] = entry.split("=", 2);
364
- if (!name || !root) continue;
365
- const key = name.trim();
366
- const value = root.trim();
367
- if (!key || !value) continue;
368
- out[key] = value;
318
+ const uniqueStrings = (values: Array<string | null | undefined>): string[] => {
319
+ const seen = new Set<string>();
320
+ const out: string[] = [];
321
+ for (const value of values) {
322
+ if (!value) continue;
323
+ const trimmed = value.trim();
324
+ if (!trimmed) continue;
325
+ const key = repoNameKey(trimmed);
326
+ if (seen.has(key)) continue;
327
+ seen.add(key);
328
+ out.push(trimmed);
369
329
  }
370
330
  return out;
371
331
  };
372
332
 
373
- type RepoResolutionContext = {
374
- defaultRepoRoot: string;
375
- defaultRepoName: string;
376
- repoMapByKey: Record<string, string>;
377
- repoNames: string[];
333
+ const buildRepoNameContext = (
334
+ defaultRepoName: string,
335
+ defaultRepoFullName: string | null,
336
+ ): { repoNames: string[]; repoNameByKey: Record<string, string> } => {
337
+ const repoNames = uniqueStrings([
338
+ defaultRepoName,
339
+ defaultRepoFullName,
340
+ defaultRepoFullName?.split("/").at(-1) ?? null,
341
+ ]);
342
+ const repoNameByKey: Record<string, string> = {};
343
+ for (const name of repoNames) {
344
+ repoNameByKey[repoNameKey(name)] = name;
345
+ }
346
+ return { repoNames, repoNameByKey };
378
347
  };
379
348
 
380
349
  const resolveRepoSelectorOptions = (
@@ -382,68 +351,100 @@ const resolveRepoSelectorOptions = (
382
351
  ctx: RepoResolutionContext,
383
352
  ): Record<string, unknown> => {
384
353
  const next: Record<string, unknown> = { ...options };
385
-
386
- const explicitRoot =
387
- (typeof next.repoRoot === "string" && next.repoRoot.trim().length > 0
388
- ? next.repoRoot.trim()
389
- : null) ??
390
- (typeof next.processRoot === "string" && next.processRoot.trim().length > 0
391
- ? next.processRoot.trim()
392
- : null);
393
-
394
- if (explicitRoot) {
395
- next.repoRoot = normalizeRepoRoot(explicitRoot);
396
- delete next.processRoot;
397
- delete next.repoName;
398
- return next;
399
- }
354
+ delete next.repoRoot;
355
+ delete next.processRoot;
400
356
 
401
357
  const repoName =
402
358
  typeof next.repoName === "string" && next.repoName.trim().length > 0
403
359
  ? next.repoName.trim()
404
360
  : null;
405
361
  if (!repoName) {
406
- delete next.processRoot;
362
+ next.processRoot = ctx.defaultProcessRoot;
407
363
  return next;
408
364
  }
409
365
 
410
- if (looksLikePathish(repoName)) {
411
- next.repoRoot = normalizeRepoRoot(repoName);
412
- delete next.processRoot;
413
- delete next.repoName;
366
+ const needle = repoNameKey(repoName);
367
+ const canonical = ctx.repoNameByKey[needle];
368
+ if (canonical) {
369
+ next.processRoot = ctx.defaultProcessRoot;
370
+ next.repoName = canonical;
414
371
  return next;
415
372
  }
416
373
 
417
- const needle = repoName.toLowerCase();
418
- if (needle === ctx.defaultRepoName.toLowerCase()) {
419
- next.repoRoot = ctx.defaultRepoRoot;
420
- delete next.processRoot;
421
- delete next.repoName;
374
+ if (ctx.defaultRepoFullName && needle === repoNameKey(ctx.defaultRepoFullName)) {
375
+ next.processRoot = ctx.defaultProcessRoot;
376
+ next.repoName = ctx.defaultRepoFullName;
422
377
  return next;
423
378
  }
424
379
 
425
- const mapped = ctx.repoMapByKey[needle];
426
- if (mapped) {
427
- next.repoRoot = normalizeRepoRoot(mapped);
428
- delete next.processRoot;
429
- delete next.repoName;
380
+ if (needle === repoNameKey(ctx.defaultRepoName)) {
381
+ next.processRoot = ctx.defaultProcessRoot;
382
+ next.repoName = ctx.defaultRepoName;
430
383
  return next;
431
384
  }
432
385
 
386
+ const slashParts = repoName.split("/").filter((part) => part.trim().length > 0);
387
+ if (slashParts.length >= 2) {
388
+ const repoSegment = slashParts[slashParts.length - 1]?.trim();
389
+ if (repoSegment) {
390
+ const matched = ctx.repoNameByKey[repoNameKey(repoSegment)];
391
+ if (matched) {
392
+ next.processRoot = ctx.defaultProcessRoot;
393
+ next.repoName = matched;
394
+ return next;
395
+ }
396
+ }
397
+ }
398
+
433
399
  const suggestions = ctx.repoNames
434
- .filter((name) => name.toLowerCase().includes(needle))
400
+ .filter((name) => repoNameKey(name).includes(needle))
435
401
  .slice(0, 8);
436
402
  const hint =
437
403
  suggestions.length > 0
438
404
  ? ` Did you mean: ${suggestions.join(", ")}?`
439
- : ctx.repoNames.length > 0
440
- ? ` Available repos: ${ctx.repoNames.join(", ")}.`
441
- : "";
405
+ : ` Available repoName values: ${ctx.repoNames.join(", ")}.`;
442
406
  throw new Error(
443
- `Unknown repoName: ${repoName}.${hint} Tip: call mcp.workspace to see the server repo context.`,
407
+ `Unknown repoName: ${repoName}.${hint} Tip: use "adl" or "F0/adl" style selectors.`,
444
408
  );
445
409
  };
446
410
 
411
+ const normalizeRepoNameForDisplay = (
412
+ requestedRepoName: string | null,
413
+ ctx: RepoResolutionContext,
414
+ ): string => {
415
+ if (!requestedRepoName) return ctx.defaultRepoName;
416
+ const canonical = ctx.repoNameByKey[repoNameKey(requestedRepoName)];
417
+ return canonical ?? requestedRepoName;
418
+ };
419
+
420
+ const resolveProcessRootFromGit = (fallbackDir: string): string => {
421
+ const cwdRoot = findGitRepoRoot(process.cwd());
422
+ if (cwdRoot) return cwdRoot;
423
+ const fallbackRoot = findGitRepoRoot(fallbackDir);
424
+ if (fallbackRoot) return fallbackRoot;
425
+ return path.resolve(process.cwd());
426
+ };
427
+
428
+ const resolveRepoIdentityFromGit = (
429
+ processRoot: string,
430
+ ): {
431
+ defaultRepoName: string;
432
+ defaultRepoFullName: string | null;
433
+ } => {
434
+ const gitIdentity = detectRepoIdentityFromGitConfig(processRoot);
435
+ if (gitIdentity) {
436
+ return {
437
+ defaultRepoName: gitIdentity.repo,
438
+ defaultRepoFullName: gitIdentity.fullName,
439
+ };
440
+ }
441
+
442
+ return {
443
+ defaultRepoName: path.basename(processRoot),
444
+ defaultRepoFullName: null,
445
+ };
446
+ };
447
+
447
448
  type NormalizedToolPayload = {
448
449
  args: unknown[];
449
450
  options: Record<string, unknown>;
@@ -562,7 +563,7 @@ const normalizePayload = (payload: unknown): NormalizedToolPayload => {
562
563
 
563
564
  return {
564
565
  args,
565
- options: normalizeRepoRootOption(options),
566
+ options,
566
567
  };
567
568
  };
568
569
 
@@ -669,7 +670,7 @@ const coercePayloadForTool = (
669
670
 
670
671
  switch (toolName) {
671
672
  case "projects.listProjects": {
672
- // No positional args. repoRoot is resolved from repoName/repoRoot/processRoot and passed via buildRepoRootOnly.
673
+ // No positional args. processRoot is injected from the selected repoName.
673
674
  break;
674
675
  }
675
676
  case "projects.resolveProjectRoot":
@@ -759,7 +760,7 @@ const coercePayloadForTool = (
759
760
  break;
760
761
  }
761
762
 
762
- return { args, options: normalizeRepoRootOption(options) };
763
+ return { args, options };
763
764
  };
764
765
 
765
766
  const normalizeBatchToolCall = (
@@ -1099,7 +1100,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1099
1100
  repoName: {
1100
1101
  type: "string",
1101
1102
  description:
1102
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1103
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1103
1104
  },
1104
1105
  args: {
1105
1106
  type: "array",
@@ -1125,7 +1126,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1125
1126
  repoName: {
1126
1127
  type: "string",
1127
1128
  description:
1128
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1129
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1129
1130
  },
1130
1131
  args: {
1131
1132
  type: "array",
@@ -1156,7 +1157,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1156
1157
  repoName: {
1157
1158
  type: "string",
1158
1159
  description:
1159
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1160
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1160
1161
  },
1161
1162
  args: {
1162
1163
  type: "array",
@@ -1234,7 +1235,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1234
1235
  repoName: {
1235
1236
  type: "string",
1236
1237
  description:
1237
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1238
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1238
1239
  },
1239
1240
  args: {
1240
1241
  type: "array",
@@ -1312,7 +1313,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1312
1313
  repoName: {
1313
1314
  type: "string",
1314
1315
  description:
1315
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1316
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1316
1317
  },
1317
1318
  args: {
1318
1319
  type: "array",
@@ -1429,7 +1430,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1429
1430
  repoName: {
1430
1431
  type: "string",
1431
1432
  description:
1432
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1433
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1433
1434
  },
1434
1435
  args: {
1435
1436
  type: "array",
@@ -1471,7 +1472,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1471
1472
  repoName: {
1472
1473
  type: "string",
1473
1474
  description:
1474
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1475
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1475
1476
  },
1476
1477
  args: {
1477
1478
  type: "array",
@@ -1529,7 +1530,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1529
1530
  repoName: {
1530
1531
  type: "string",
1531
1532
  description:
1532
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1533
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1533
1534
  },
1534
1535
  args: {
1535
1536
  type: "array",
@@ -1564,7 +1565,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1564
1565
  repoName: {
1565
1566
  type: "string",
1566
1567
  description:
1567
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1568
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1568
1569
  },
1569
1570
  args: {
1570
1571
  type: "array",
@@ -1586,7 +1587,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1586
1587
  repoName: {
1587
1588
  type: "string",
1588
1589
  description:
1589
- "Optional repo selector (LLM-friendly). Omit to use server default. You can also pass a server filesystem path via legacy repoRoot/processRoot.",
1590
+ 'Optional repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1590
1591
  },
1591
1592
  },
1592
1593
  required: [],
@@ -1654,7 +1655,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1654
1655
  repoName: {
1655
1656
  type: "string",
1656
1657
  description:
1657
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1658
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1658
1659
  },
1659
1660
  },
1660
1661
  $comment: safeJsonStringify({
@@ -1801,9 +1802,9 @@ const getInvocationPlanName = (toolName: string): string => {
1801
1802
  const plan = toolInvocationPlans[toolName];
1802
1803
  if (!plan) return "default";
1803
1804
  if (plan === buildOptionsOnly) return "optionsOnly";
1804
- if (plan === buildOptionsThenRepoRoot) return "optionsThenRepoRoot";
1805
- if (plan === buildRepoRootThenOptions) return "repoRootThenOptions";
1806
- if (plan === buildRepoRootOnly) return "repoRootOnly";
1805
+ if (plan === buildOptionsThenProcessRoot) return "optionsThenProcessRoot";
1806
+ if (plan === buildProcessRootThenOptions) return "processRootThenOptions";
1807
+ if (plan === buildProcessRootOnly) return "processRootOnly";
1807
1808
  return "custom";
1808
1809
  };
1809
1810
 
@@ -1815,16 +1816,16 @@ const buildInvocationExample = (toolName: string): Record<string, unknown> => {
1815
1816
  const example: Record<string, unknown> = {};
1816
1817
  if (requiredArgs && requiredArgs.length > 0) {
1817
1818
  example.args = [...requiredArgs];
1818
- } else if (plan !== "repoRootOnly") {
1819
+ } else if (plan !== "processRootOnly") {
1819
1820
  example.args = ["<arg0>"];
1820
1821
  }
1821
1822
 
1822
- if (plan === "repoRootOnly") {
1823
+ if (plan === "processRootOnly") {
1823
1824
  example.options = { repoName: "<repo-name>", ...defaultOptions };
1824
1825
  return example;
1825
1826
  }
1826
1827
 
1827
- if (plan === "optionsThenRepoRoot" || plan === "repoRootThenOptions") {
1828
+ if (plan === "optionsThenProcessRoot" || plan === "processRootThenOptions") {
1828
1829
  example.options = { repoName: "<repo-name>", ...defaultOptions };
1829
1830
  return example;
1830
1831
  }
@@ -1859,7 +1860,7 @@ const defaultToolInputSchema = (toolName: string) => ({
1859
1860
  repoName: {
1860
1861
  type: "string",
1861
1862
  description:
1862
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1863
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1863
1864
  },
1864
1865
  },
1865
1866
  $comment: safeJsonStringify({
@@ -1975,13 +1976,13 @@ const buildToolList = (
1975
1976
  type ToolInvoker = (
1976
1977
  args: unknown[],
1977
1978
  options: Record<string, unknown>,
1978
- defaultRepoRoot: string,
1979
+ defaultProcessRoot: string,
1979
1980
  ) => unknown[];
1980
1981
 
1981
1982
  const buildOptionsOnly = (
1982
1983
  args: unknown[],
1983
1984
  options: Record<string, unknown>,
1984
- _defaultRepoRoot: string,
1985
+ _defaultProcessRoot: string,
1985
1986
  ): unknown[] => {
1986
1987
  const invocationArgs: unknown[] = [...args];
1987
1988
  if (Object.keys(options).length > 0) {
@@ -1990,44 +1991,48 @@ const buildOptionsOnly = (
1990
1991
  return invocationArgs;
1991
1992
  };
1992
1993
 
1993
- const buildOptionsThenRepoRoot = (
1994
+ const buildOptionsThenProcessRoot = (
1994
1995
  args: unknown[],
1995
1996
  options: Record<string, unknown>,
1996
- defaultRepoRoot: string,
1997
+ defaultProcessRoot: string,
1997
1998
  ): unknown[] => {
1998
1999
  const invocationArgs: unknown[] = [...args];
1999
2000
  const remaining = { ...options };
2000
- const repoRoot = remaining.repoRoot;
2001
- if (typeof repoRoot === "string") {
2002
- delete remaining.repoRoot;
2001
+ const processRoot = remaining.processRoot;
2002
+ if (typeof processRoot === "string") {
2003
+ delete remaining.processRoot;
2003
2004
  }
2004
- const resolvedRepoRoot =
2005
- typeof repoRoot === "string" ? repoRoot : defaultRepoRoot;
2005
+ delete remaining.repoRoot;
2006
+ const resolvedProcessRoot =
2007
+ typeof processRoot === "string" ? processRoot : defaultProcessRoot;
2006
2008
 
2007
2009
  if (Object.keys(remaining).length > 0) {
2008
2010
  invocationArgs.push(remaining);
2009
- } else if (resolvedRepoRoot) {
2010
- // Preserve positional slot for signatures like fn(projectName, options?, repoRoot?).
2011
+ } else if (resolvedProcessRoot) {
2012
+ // Preserve positional slot for signatures like fn(projectName, options?, processRoot?).
2011
2013
  invocationArgs.push({});
2012
2014
  }
2013
- invocationArgs.push(resolvedRepoRoot);
2015
+ invocationArgs.push(resolvedProcessRoot);
2014
2016
 
2015
2017
  return invocationArgs;
2016
2018
  };
2017
2019
 
2018
- const buildRepoRootThenOptions = (
2020
+ const buildProcessRootThenOptions = (
2019
2021
  args: unknown[],
2020
2022
  options: Record<string, unknown>,
2021
- defaultRepoRoot: string,
2023
+ defaultProcessRoot: string,
2022
2024
  ): unknown[] => {
2023
2025
  const invocationArgs: unknown[] = [...args];
2024
2026
  const remaining = { ...options };
2025
- const repoRoot = remaining.repoRoot;
2026
- if (typeof repoRoot === "string") {
2027
- delete remaining.repoRoot;
2027
+ const processRoot = remaining.processRoot;
2028
+ if (typeof processRoot === "string") {
2029
+ delete remaining.processRoot;
2028
2030
  }
2031
+ delete remaining.repoRoot;
2029
2032
 
2030
- invocationArgs.push(typeof repoRoot === "string" ? repoRoot : defaultRepoRoot);
2033
+ invocationArgs.push(
2034
+ typeof processRoot === "string" ? processRoot : defaultProcessRoot,
2035
+ );
2031
2036
  if (Object.keys(remaining).length > 0) {
2032
2037
  invocationArgs.push(remaining);
2033
2038
  }
@@ -2035,39 +2040,42 @@ const buildRepoRootThenOptions = (
2035
2040
  return invocationArgs;
2036
2041
  };
2037
2042
 
2038
- const buildRepoRootOnly = (
2043
+ const buildProcessRootOnly = (
2039
2044
  args: unknown[],
2040
2045
  options: Record<string, unknown>,
2041
- defaultRepoRoot: string,
2046
+ defaultProcessRoot: string,
2042
2047
  ): unknown[] => {
2043
2048
  const invocationArgs: unknown[] = [...args];
2044
- const repoRoot = options.repoRoot;
2045
- invocationArgs.push(typeof repoRoot === "string" ? repoRoot : defaultRepoRoot);
2049
+ const processRoot = options.processRoot;
2050
+ invocationArgs.push(
2051
+ typeof processRoot === "string" ? processRoot : defaultProcessRoot,
2052
+ );
2046
2053
  return invocationArgs;
2047
2054
  };
2048
2055
 
2049
2056
  const toolInvocationPlans: Record<string, ToolInvoker> = {
2050
- "agents.setActive": buildRepoRootThenOptions,
2051
- "agents.resolveAgentsRootFrom": buildRepoRootOnly,
2052
- "projects.setActive": buildRepoRootThenOptions,
2053
- "projects.generateSpec": buildOptionsThenRepoRoot,
2054
- "projects.syncTasks": buildOptionsThenRepoRoot,
2055
- "projects.clearIssues": buildOptionsThenRepoRoot,
2056
- "projects.fetchGitTasks": buildOptionsThenRepoRoot,
2057
- "projects.createGitIssue": buildOptionsThenRepoRoot,
2058
- "projects.readGitTask": buildOptionsThenRepoRoot,
2059
- "projects.writeGitTask": buildOptionsThenRepoRoot,
2057
+ "agents.setActive": buildProcessRootThenOptions,
2058
+ "agents.resolveAgentsRootFrom": buildProcessRootOnly,
2059
+ "projects.setActive": buildProcessRootThenOptions,
2060
+ "projects.generateSpec": buildOptionsThenProcessRoot,
2061
+ "projects.syncTasks": buildOptionsThenProcessRoot,
2062
+ "projects.clearIssues": buildOptionsThenProcessRoot,
2063
+ "projects.fetchGitTasks": buildOptionsThenProcessRoot,
2064
+ "projects.createGitIssue": buildOptionsThenProcessRoot,
2065
+ "projects.readGitTask": buildOptionsThenProcessRoot,
2066
+ "projects.writeGitTask": buildOptionsThenProcessRoot,
2060
2067
  "agents.resolveTargetFile": buildOptionsOnly,
2061
2068
  "projects.resolveProjectTargetFile": buildOptionsOnly,
2062
- "agents.loadAgent": buildRepoRootOnly,
2063
- "agents.loadAgentPrompt": buildRepoRootOnly,
2064
- "projects.resolveImplementationPlan": (args, options, _defaultRepoRoot) => {
2069
+ "agents.loadAgent": buildProcessRootOnly,
2070
+ "agents.loadAgentPrompt": buildProcessRootOnly,
2071
+ "projects.resolveImplementationPlan": (args, options, _defaultProcessRoot) => {
2065
2072
  const invocationArgs: unknown[] = [...args];
2066
2073
  const remaining = { ...options };
2067
- const repoRoot = remaining.repoRoot;
2068
- if (typeof repoRoot === "string") {
2069
- delete remaining.repoRoot;
2074
+ const processRoot = remaining.processRoot;
2075
+ if (typeof processRoot === "string") {
2076
+ delete remaining.processRoot;
2070
2077
  }
2078
+ delete remaining.repoRoot;
2071
2079
 
2072
2080
  // This tool is a low-level helper: projects.resolveImplementationPlan(projectRoot, inputFile?, options?)
2073
2081
  // If the caller provides options but no inputFile, preserve the positional slot.
@@ -2078,17 +2086,17 @@ const toolInvocationPlans: Record<string, ToolInvoker> = {
2078
2086
  invocationArgs.push(remaining);
2079
2087
  }
2080
2088
 
2081
- // Intentionally do NOT append repoRoot: projectRoot is the first positional argument.
2089
+ // Intentionally do NOT append processRoot: projectRoot is the first positional argument.
2082
2090
  return invocationArgs;
2083
2091
  },
2084
- "agents.main": buildRepoRootOnly,
2085
- "agents.resolveAgentsRoot": buildRepoRootOnly,
2086
- "agents.listAgents": buildRepoRootOnly,
2087
- "projects.resolveProjectRoot": buildRepoRootOnly,
2088
- "projects.listProjects": buildRepoRootOnly,
2089
- "projects.listProjectDocs": buildRepoRootOnly,
2090
- "projects.readProjectDoc": buildRepoRootOnly,
2091
- "projects.main": buildRepoRootOnly,
2092
+ "agents.main": buildProcessRootOnly,
2093
+ "agents.resolveAgentsRoot": buildProcessRootOnly,
2094
+ "agents.listAgents": buildProcessRootOnly,
2095
+ "projects.resolveProjectRoot": buildProcessRootOnly,
2096
+ "projects.listProjects": buildProcessRootOnly,
2097
+ "projects.listProjectDocs": buildProcessRootOnly,
2098
+ "projects.readProjectDoc": buildProcessRootOnly,
2099
+ "projects.main": buildProcessRootOnly,
2092
2100
  };
2093
2101
 
2094
2102
  const invokeTool = async (
@@ -2104,14 +2112,14 @@ const invokeTool = async (
2104
2112
  };
2105
2113
  const invoke =
2106
2114
  toolInvocationPlans[tool.name] ??
2107
- ((rawArgs, rawOptions, _repoRoot) => {
2115
+ ((rawArgs, rawOptions, _processRoot) => {
2108
2116
  const invocationArgs = [...rawArgs];
2109
2117
  if (Object.keys(rawOptions).length > 0) {
2110
2118
  invocationArgs.push(rawOptions);
2111
2119
  }
2112
2120
  return invocationArgs;
2113
2121
  });
2114
- const invocationArgs = invoke(args, options, repoCtx.defaultRepoRoot);
2122
+ const invocationArgs = invoke(args, options, repoCtx.defaultProcessRoot);
2115
2123
 
2116
2124
  return Promise.resolve(tool.method(...invocationArgs));
2117
2125
  };
@@ -2121,13 +2129,8 @@ export interface ExampleMcpServerOptions {
2121
2129
  serverVersion?: string;
2122
2130
  toolsPrefix?: string;
2123
2131
  /**
2124
- * Optional default repo root on the server filesystem.
2125
- * If omitted, the server attempts to auto-detect by walking up from cwd.
2126
- */
2127
- repoRoot?: string;
2128
- /**
2129
- * Optional default repo name (LLM-friendly identifier).
2130
- * If omitted, defaults to the basename of the resolved repo root.
2132
+ * Optional default repo selector (for MCP repoName matching).
2133
+ * If omitted, auto-detected from git remote (fallback: workspace folder name).
2131
2134
  */
2132
2135
  repoName?: string;
2133
2136
  allowedRootEndpoints?: string[];
@@ -2262,48 +2265,23 @@ export const createExampleMcpServer = (
2262
2265
  ): ExampleMcpServerInstance => {
2263
2266
  let toolCatalog: unknown[] = [];
2264
2267
 
2265
- const configuredRepoRoot =
2266
- options.repoRoot ?? process.env.MCP_REPO_ROOT ?? process.env.F0_REPO_ROOT;
2267
- const cwd = process.cwd();
2268
- const cwdNormalized = normalizeRepoRoot(cwd);
2269
- const cwdLooksLikeRepoRoot = looksLikeRepoRoot(cwdNormalized);
2270
- const cwdGitRoot = findGitRepoRoot(cwd);
2271
2268
  const serverFileDir = path.dirname(fileURLToPath(import.meta.url));
2272
- const serverFileGitRoot = findGitRepoRoot(serverFileDir);
2273
-
2274
- const rawDefaultRepoRoot =
2275
- configuredRepoRoot ??
2276
- (cwdLooksLikeRepoRoot ? cwdNormalized : null) ??
2277
- cwdGitRoot ??
2278
- serverFileGitRoot ??
2279
- cwd ??
2280
- serverFileDir;
2281
- const defaultRepoRoot = normalizeRepoRoot(rawDefaultRepoRoot);
2282
-
2283
- const configuredRepoName =
2284
- (options.repoName ?? process.env.MCP_REPO_NAME ?? process.env.F0_REPO_NAME)?.trim() ||
2285
- null;
2286
- const gitDerivedRepoName =
2287
- detectRepoNameFromGitConfig(defaultRepoRoot) ??
2288
- (cwdGitRoot ? detectRepoNameFromGitConfig(cwdGitRoot) : null) ??
2289
- (serverFileGitRoot ? detectRepoNameFromGitConfig(serverFileGitRoot) : null);
2290
- const defaultRepoName =
2291
- configuredRepoName || gitDerivedRepoName || path.basename(defaultRepoRoot);
2292
- const repoMapRaw = process.env.MCP_REPOS ?? process.env.F0_REPOS;
2293
- const repoMap = {
2294
- ...parseRepoMap(repoMapRaw),
2295
- [defaultRepoName]: defaultRepoRoot,
2296
- };
2297
- const repoNames = Object.keys(repoMap).sort((a, b) => a.localeCompare(b));
2298
- const repoMapByKey: Record<string, string> = {};
2299
- for (const [name, root] of Object.entries(repoMap)) {
2300
- repoMapByKey[name.toLowerCase()] = root;
2301
- }
2269
+ const defaultProcessRoot = resolveProcessRootFromGit(serverFileDir);
2270
+ const configuredRepoName = options.repoName?.trim() || null;
2271
+ const resolvedRepoIdentity = resolveRepoIdentityFromGit(defaultProcessRoot);
2272
+ const defaultRepoName = configuredRepoName || resolvedRepoIdentity.defaultRepoName;
2273
+ const defaultRepoFullName = resolvedRepoIdentity.defaultRepoFullName;
2274
+ const { repoNames, repoNameByKey } = buildRepoNameContext(
2275
+ defaultRepoName,
2276
+ defaultRepoFullName,
2277
+ );
2278
+
2302
2279
  const repoCtx: RepoResolutionContext = {
2303
- defaultRepoRoot,
2280
+ defaultProcessRoot,
2304
2281
  defaultRepoName,
2305
- repoMapByKey,
2282
+ defaultRepoFullName,
2306
2283
  repoNames,
2284
+ repoNameByKey,
2307
2285
  };
2308
2286
 
2309
2287
  const parseString = (value: unknown): string | null => {
@@ -2344,7 +2322,7 @@ export const createExampleMcpServer = (
2344
2322
  "F0 MCP helper tools:",
2345
2323
  "- mcp.listTools: returns tool catalog with access + invocation hints",
2346
2324
  "- mcp.describeTool: describe one tool by name (prefixed or unprefixed)",
2347
- "- mcp.workspace: explain server filesystem context (cwd, repoName/repoRoot, projects)",
2325
+ "- mcp.workspace: explain Git workspace context (cwd, repoName selectors, projects)",
2348
2326
  "- mcp.search: LLM-friendly search over project docs/spec (local-first)",
2349
2327
  "",
2350
2328
  'Tip: Prefer mcp.search for "search spec/docs" requests.',
@@ -2356,35 +2334,35 @@ export const createExampleMcpServer = (
2356
2334
  ? {
2357
2335
  keys: Object.keys(input),
2358
2336
  repoName: (input as any).repoName ?? null,
2359
- repoRoot: (input as any).repoRoot ?? null,
2360
- processRoot: (input as any).processRoot ?? null,
2361
2337
  }
2362
- : { keys: [], repoName: null, repoRoot: null, processRoot: null };
2338
+ : { keys: [], repoName: null };
2363
2339
  const requestedRepoName = parseString(payload.repoName);
2364
2340
  const resolved = resolveRepoSelectorOptions(payload, repoCtx);
2365
- const repoRoot =
2366
- typeof resolved.repoRoot === "string"
2367
- ? resolved.repoRoot
2368
- : repoCtx.defaultRepoRoot;
2369
- const effectiveRepoName =
2370
- requestedRepoName && !looksLikePathish(requestedRepoName)
2371
- ? requestedRepoName
2372
- : repoCtx.defaultRepoName;
2373
-
2374
- const projectsDir = path.join(repoRoot, "projects");
2375
- const apiDir = path.join(repoRoot, "api");
2376
- const agentsDir = path.join(repoRoot, "agents");
2341
+ const processRoot =
2342
+ typeof resolved.processRoot === "string"
2343
+ ? resolved.processRoot
2344
+ : repoCtx.defaultProcessRoot;
2345
+ const effectiveRepoName = normalizeRepoNameForDisplay(
2346
+ requestedRepoName,
2347
+ repoCtx,
2348
+ );
2349
+
2350
+ const projectsDir = path.join(processRoot, "projects");
2351
+ const apiDir = path.join(processRoot, "api");
2352
+ const agentsDir = path.join(processRoot, "agents");
2377
2353
  const hasProjectsDir = isDir(projectsDir);
2378
2354
  const hasApiDir = isDir(apiDir);
2379
2355
  const hasAgentsDir = isDir(agentsDir);
2380
2356
 
2381
- const projects = hasProjectsDir ? projectsApi.listProjects(repoRoot) : [];
2357
+ const projects = hasProjectsDir
2358
+ ? projectsApi.listProjects(processRoot)
2359
+ : [];
2382
2360
  const hint = (() => {
2383
2361
  if (!hasProjectsDir) {
2384
2362
  return [
2385
- "Repo does not contain /projects on the server filesystem.",
2386
- "Start the MCP server from the monorepo root (the folder that contains both /api and /projects), or configure the server with --repo-root / MCP_REPO_ROOT.",
2387
- "Tool callers should usually omit repoName and let the server use its default workspace.",
2363
+ "Git workspace does not contain /projects.",
2364
+ "Pass repoName as \"adl\" or \"F0/adl\".",
2365
+ "For single-repo layouts, use source:\"auto\" or source:\"gitea\" on search tools.",
2388
2366
  ].join(" ");
2389
2367
  }
2390
2368
  if (hasProjectsDir && projects.length === 0) {
@@ -2400,9 +2378,10 @@ export const createExampleMcpServer = (
2400
2378
  received,
2401
2379
  cwd: process.cwd(),
2402
2380
  defaultRepoName: repoCtx.defaultRepoName,
2403
- defaultRepoRoot: repoCtx.defaultRepoRoot,
2381
+ defaultRepoFullName: repoCtx.defaultRepoFullName,
2382
+ workspaceRoot: repoCtx.defaultProcessRoot,
2404
2383
  repoName: effectiveRepoName,
2405
- repoRoot,
2384
+ processRoot,
2406
2385
  availableRepoNames: repoCtx.repoNames,
2407
2386
  hasProjectsDir,
2408
2387
  hasApiDir,
@@ -2452,10 +2431,10 @@ export const createExampleMcpServer = (
2452
2431
  search: async (input: unknown) => {
2453
2432
  const payload = isRecord(input) ? input : {};
2454
2433
  const resolved = resolveRepoSelectorOptions(payload, repoCtx);
2455
- const repoRoot =
2456
- typeof resolved.repoRoot === "string"
2457
- ? resolved.repoRoot
2458
- : repoCtx.defaultRepoRoot;
2434
+ const processRoot =
2435
+ typeof resolved.processRoot === "string"
2436
+ ? resolved.processRoot
2437
+ : repoCtx.defaultProcessRoot;
2459
2438
 
2460
2439
  const sectionRaw = parseString(payload.section)?.toLowerCase();
2461
2440
  const section = sectionRaw === "docs" ? "docs" : "spec";
@@ -2502,11 +2481,11 @@ export const createExampleMcpServer = (
2502
2481
 
2503
2482
  const projectName = ensureProjectName(
2504
2483
  parseString(payload.projectName),
2505
- repoRoot,
2484
+ processRoot,
2506
2485
  );
2507
2486
 
2508
2487
  const searchOptions: Record<string, unknown> = {
2509
- processRoot: repoRoot,
2488
+ processRoot,
2510
2489
  };
2511
2490
 
2512
2491
  const source = parseString(payload.source)?.toLowerCase();
@@ -2829,9 +2808,9 @@ export const createExampleMcpServer = (
2829
2808
  /projects[\\/].+projects[\\/]/i.test(message)
2830
2809
  ) {
2831
2810
  details.hint =
2832
- "You likely passed a project root as repoName (or legacy repoRoot/processRoot). Repo selection should point at the monorepo root that contains /projects.";
2811
+ 'Repo selection is invalid. repoName must be "<repo>" or "<owner>/<repo>", for example "adl" or "F0/adl".';
2833
2812
  details.suggestion =
2834
- "Call mcp.workspace to see the server’s cwd/default repoRoot, then omit repoName or pass the correct repoName.";
2813
+ "Call mcp.workspace to see accepted repoName selectors.";
2835
2814
  }
2836
2815
 
2837
2816
  if (
@@ -2842,7 +2821,7 @@ export const createExampleMcpServer = (
2842
2821
  details.hint =
2843
2822
  "Repo selection might be wrong, or the server filesystem does not contain /projects for this workspace.";
2844
2823
  details.suggestion =
2845
- "Call mcp.workspace to see the server’s cwd/default repoRoot and available repoName values.";
2824
+ "Call mcp.workspace to see workspaceRoot and available repoName values.";
2846
2825
  details.example = {
2847
2826
  tool: prefix ? `${prefix}.mcp.workspace` : "mcp.workspace",
2848
2827
  arguments: {},