@foundation0/api 1.1.8 → 1.1.11

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);
276
- };
277
-
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;
305
+ return parseGitRemoteIdentityFromUrl(remoteUrl);
326
306
  };
327
307
 
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,105 @@ 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
+ if (repoSegment !== "." && repoSegment !== "..") {
397
+ next.processRoot = ctx.defaultProcessRoot;
398
+ next.repoName = repoSegment;
399
+ return next;
400
+ }
401
+ }
402
+ }
403
+
433
404
  const suggestions = ctx.repoNames
434
- .filter((name) => name.toLowerCase().includes(needle))
405
+ .filter((name) => repoNameKey(name).includes(needle))
435
406
  .slice(0, 8);
436
407
  const hint =
437
408
  suggestions.length > 0
438
409
  ? ` Did you mean: ${suggestions.join(", ")}?`
439
- : ctx.repoNames.length > 0
440
- ? ` Available repos: ${ctx.repoNames.join(", ")}.`
441
- : "";
410
+ : ` Available repoName values: ${ctx.repoNames.join(", ")}.`;
442
411
  throw new Error(
443
- `Unknown repoName: ${repoName}.${hint} Tip: call mcp.workspace to see the server repo context.`,
412
+ `Unknown repoName: ${repoName}.${hint} Tip: use "adl" or "F0/adl" style selectors.`,
444
413
  );
445
414
  };
446
415
 
416
+ const normalizeRepoNameForDisplay = (
417
+ requestedRepoName: string | null,
418
+ ctx: RepoResolutionContext,
419
+ ): string => {
420
+ if (!requestedRepoName) return ctx.defaultRepoName;
421
+ const canonical = ctx.repoNameByKey[repoNameKey(requestedRepoName)];
422
+ return canonical ?? requestedRepoName;
423
+ };
424
+
425
+ const resolveProcessRootFromGit = (fallbackDir: string): string => {
426
+ const cwdRoot = findGitRepoRoot(process.cwd());
427
+ if (cwdRoot) return cwdRoot;
428
+ const fallbackRoot = findGitRepoRoot(fallbackDir);
429
+ if (fallbackRoot) return fallbackRoot;
430
+ return path.resolve(process.cwd());
431
+ };
432
+
433
+ const resolveRepoIdentityFromGit = (
434
+ processRoot: string,
435
+ ): {
436
+ defaultRepoName: string;
437
+ defaultRepoFullName: string | null;
438
+ } => {
439
+ const gitIdentity = detectRepoIdentityFromGitConfig(processRoot);
440
+ if (gitIdentity) {
441
+ return {
442
+ defaultRepoName: gitIdentity.repo,
443
+ defaultRepoFullName: gitIdentity.fullName,
444
+ };
445
+ }
446
+
447
+ return {
448
+ defaultRepoName: path.basename(processRoot),
449
+ defaultRepoFullName: null,
450
+ };
451
+ };
452
+
447
453
  type NormalizedToolPayload = {
448
454
  args: unknown[];
449
455
  options: Record<string, unknown>;
@@ -562,7 +568,7 @@ const normalizePayload = (payload: unknown): NormalizedToolPayload => {
562
568
 
563
569
  return {
564
570
  args,
565
- options: normalizeRepoRootOption(options),
571
+ options,
566
572
  };
567
573
  };
568
574
 
@@ -669,7 +675,7 @@ const coercePayloadForTool = (
669
675
 
670
676
  switch (toolName) {
671
677
  case "projects.listProjects": {
672
- // No positional args. repoRoot is resolved from repoName/repoRoot/processRoot and passed via buildRepoRootOnly.
678
+ // No positional args. processRoot is injected from the selected repoName.
673
679
  break;
674
680
  }
675
681
  case "projects.resolveProjectRoot":
@@ -759,7 +765,7 @@ const coercePayloadForTool = (
759
765
  break;
760
766
  }
761
767
 
762
- return { args, options: normalizeRepoRootOption(options) };
768
+ return { args, options };
763
769
  };
764
770
 
765
771
  const normalizeBatchToolCall = (
@@ -1099,7 +1105,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1099
1105
  repoName: {
1100
1106
  type: "string",
1101
1107
  description:
1102
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1108
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1103
1109
  },
1104
1110
  args: {
1105
1111
  type: "array",
@@ -1125,7 +1131,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1125
1131
  repoName: {
1126
1132
  type: "string",
1127
1133
  description:
1128
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1134
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1129
1135
  },
1130
1136
  args: {
1131
1137
  type: "array",
@@ -1156,7 +1162,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1156
1162
  repoName: {
1157
1163
  type: "string",
1158
1164
  description:
1159
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1165
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1160
1166
  },
1161
1167
  args: {
1162
1168
  type: "array",
@@ -1234,7 +1240,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1234
1240
  repoName: {
1235
1241
  type: "string",
1236
1242
  description:
1237
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1243
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1238
1244
  },
1239
1245
  args: {
1240
1246
  type: "array",
@@ -1312,7 +1318,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1312
1318
  repoName: {
1313
1319
  type: "string",
1314
1320
  description:
1315
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1321
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1316
1322
  },
1317
1323
  args: {
1318
1324
  type: "array",
@@ -1429,7 +1435,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1429
1435
  repoName: {
1430
1436
  type: "string",
1431
1437
  description:
1432
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1438
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1433
1439
  },
1434
1440
  args: {
1435
1441
  type: "array",
@@ -1471,7 +1477,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1471
1477
  repoName: {
1472
1478
  type: "string",
1473
1479
  description:
1474
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1480
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1475
1481
  },
1476
1482
  args: {
1477
1483
  type: "array",
@@ -1529,7 +1535,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1529
1535
  repoName: {
1530
1536
  type: "string",
1531
1537
  description:
1532
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1538
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1533
1539
  },
1534
1540
  args: {
1535
1541
  type: "array",
@@ -1564,7 +1570,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1564
1570
  repoName: {
1565
1571
  type: "string",
1566
1572
  description:
1567
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1573
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1568
1574
  },
1569
1575
  args: {
1570
1576
  type: "array",
@@ -1586,7 +1592,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1586
1592
  repoName: {
1587
1593
  type: "string",
1588
1594
  description:
1589
- "Optional repo selector (LLM-friendly). Omit to use server default. You can also pass a server filesystem path via legacy repoRoot/processRoot.",
1595
+ 'Optional repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1590
1596
  },
1591
1597
  },
1592
1598
  required: [],
@@ -1654,7 +1660,7 @@ const TOOL_INPUT_SCHEMA_OVERRIDES: Record<string, unknown> = {
1654
1660
  repoName: {
1655
1661
  type: "string",
1656
1662
  description:
1657
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1663
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1658
1664
  },
1659
1665
  },
1660
1666
  $comment: safeJsonStringify({
@@ -1801,9 +1807,9 @@ const getInvocationPlanName = (toolName: string): string => {
1801
1807
  const plan = toolInvocationPlans[toolName];
1802
1808
  if (!plan) return "default";
1803
1809
  if (plan === buildOptionsOnly) return "optionsOnly";
1804
- if (plan === buildOptionsThenRepoRoot) return "optionsThenRepoRoot";
1805
- if (plan === buildRepoRootThenOptions) return "repoRootThenOptions";
1806
- if (plan === buildRepoRootOnly) return "repoRootOnly";
1810
+ if (plan === buildOptionsThenProcessRoot) return "optionsThenProcessRoot";
1811
+ if (plan === buildProcessRootThenOptions) return "processRootThenOptions";
1812
+ if (plan === buildProcessRootOnly) return "processRootOnly";
1807
1813
  return "custom";
1808
1814
  };
1809
1815
 
@@ -1815,16 +1821,16 @@ const buildInvocationExample = (toolName: string): Record<string, unknown> => {
1815
1821
  const example: Record<string, unknown> = {};
1816
1822
  if (requiredArgs && requiredArgs.length > 0) {
1817
1823
  example.args = [...requiredArgs];
1818
- } else if (plan !== "repoRootOnly") {
1824
+ } else if (plan !== "processRootOnly") {
1819
1825
  example.args = ["<arg0>"];
1820
1826
  }
1821
1827
 
1822
- if (plan === "repoRootOnly") {
1828
+ if (plan === "processRootOnly") {
1823
1829
  example.options = { repoName: "<repo-name>", ...defaultOptions };
1824
1830
  return example;
1825
1831
  }
1826
1832
 
1827
- if (plan === "optionsThenRepoRoot" || plan === "repoRootThenOptions") {
1833
+ if (plan === "optionsThenProcessRoot" || plan === "processRootThenOptions") {
1828
1834
  example.options = { repoName: "<repo-name>", ...defaultOptions };
1829
1835
  return example;
1830
1836
  }
@@ -1859,7 +1865,7 @@ const defaultToolInputSchema = (toolName: string) => ({
1859
1865
  repoName: {
1860
1866
  type: "string",
1861
1867
  description:
1862
- "Repo selector (LLM-friendly). Omit to use server default. Legacy path aliases: repoRoot/processRoot.",
1868
+ 'Repo selector. Accepts "<repo>" or "<owner>/<repo>". Omit to use the current Git workspace.',
1863
1869
  },
1864
1870
  },
1865
1871
  $comment: safeJsonStringify({
@@ -1975,13 +1981,13 @@ const buildToolList = (
1975
1981
  type ToolInvoker = (
1976
1982
  args: unknown[],
1977
1983
  options: Record<string, unknown>,
1978
- defaultRepoRoot: string,
1984
+ defaultProcessRoot: string,
1979
1985
  ) => unknown[];
1980
1986
 
1981
1987
  const buildOptionsOnly = (
1982
1988
  args: unknown[],
1983
1989
  options: Record<string, unknown>,
1984
- _defaultRepoRoot: string,
1990
+ _defaultProcessRoot: string,
1985
1991
  ): unknown[] => {
1986
1992
  const invocationArgs: unknown[] = [...args];
1987
1993
  if (Object.keys(options).length > 0) {
@@ -1990,44 +1996,48 @@ const buildOptionsOnly = (
1990
1996
  return invocationArgs;
1991
1997
  };
1992
1998
 
1993
- const buildOptionsThenRepoRoot = (
1999
+ const buildOptionsThenProcessRoot = (
1994
2000
  args: unknown[],
1995
2001
  options: Record<string, unknown>,
1996
- defaultRepoRoot: string,
2002
+ defaultProcessRoot: string,
1997
2003
  ): unknown[] => {
1998
2004
  const invocationArgs: unknown[] = [...args];
1999
2005
  const remaining = { ...options };
2000
- const repoRoot = remaining.repoRoot;
2001
- if (typeof repoRoot === "string") {
2002
- delete remaining.repoRoot;
2006
+ const processRoot = remaining.processRoot;
2007
+ if (typeof processRoot === "string") {
2008
+ delete remaining.processRoot;
2003
2009
  }
2004
- const resolvedRepoRoot =
2005
- typeof repoRoot === "string" ? repoRoot : defaultRepoRoot;
2010
+ delete remaining.repoRoot;
2011
+ const resolvedProcessRoot =
2012
+ typeof processRoot === "string" ? processRoot : defaultProcessRoot;
2006
2013
 
2007
2014
  if (Object.keys(remaining).length > 0) {
2008
2015
  invocationArgs.push(remaining);
2009
- } else if (resolvedRepoRoot) {
2010
- // Preserve positional slot for signatures like fn(projectName, options?, repoRoot?).
2016
+ } else if (resolvedProcessRoot) {
2017
+ // Preserve positional slot for signatures like fn(projectName, options?, processRoot?).
2011
2018
  invocationArgs.push({});
2012
2019
  }
2013
- invocationArgs.push(resolvedRepoRoot);
2020
+ invocationArgs.push(resolvedProcessRoot);
2014
2021
 
2015
2022
  return invocationArgs;
2016
2023
  };
2017
2024
 
2018
- const buildRepoRootThenOptions = (
2025
+ const buildProcessRootThenOptions = (
2019
2026
  args: unknown[],
2020
2027
  options: Record<string, unknown>,
2021
- defaultRepoRoot: string,
2028
+ defaultProcessRoot: string,
2022
2029
  ): unknown[] => {
2023
2030
  const invocationArgs: unknown[] = [...args];
2024
2031
  const remaining = { ...options };
2025
- const repoRoot = remaining.repoRoot;
2026
- if (typeof repoRoot === "string") {
2027
- delete remaining.repoRoot;
2032
+ const processRoot = remaining.processRoot;
2033
+ if (typeof processRoot === "string") {
2034
+ delete remaining.processRoot;
2028
2035
  }
2036
+ delete remaining.repoRoot;
2029
2037
 
2030
- invocationArgs.push(typeof repoRoot === "string" ? repoRoot : defaultRepoRoot);
2038
+ invocationArgs.push(
2039
+ typeof processRoot === "string" ? processRoot : defaultProcessRoot,
2040
+ );
2031
2041
  if (Object.keys(remaining).length > 0) {
2032
2042
  invocationArgs.push(remaining);
2033
2043
  }
@@ -2035,39 +2045,42 @@ const buildRepoRootThenOptions = (
2035
2045
  return invocationArgs;
2036
2046
  };
2037
2047
 
2038
- const buildRepoRootOnly = (
2048
+ const buildProcessRootOnly = (
2039
2049
  args: unknown[],
2040
2050
  options: Record<string, unknown>,
2041
- defaultRepoRoot: string,
2051
+ defaultProcessRoot: string,
2042
2052
  ): unknown[] => {
2043
2053
  const invocationArgs: unknown[] = [...args];
2044
- const repoRoot = options.repoRoot;
2045
- invocationArgs.push(typeof repoRoot === "string" ? repoRoot : defaultRepoRoot);
2054
+ const processRoot = options.processRoot;
2055
+ invocationArgs.push(
2056
+ typeof processRoot === "string" ? processRoot : defaultProcessRoot,
2057
+ );
2046
2058
  return invocationArgs;
2047
2059
  };
2048
2060
 
2049
2061
  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,
2062
+ "agents.setActive": buildProcessRootThenOptions,
2063
+ "agents.resolveAgentsRootFrom": buildProcessRootOnly,
2064
+ "projects.setActive": buildProcessRootThenOptions,
2065
+ "projects.generateSpec": buildOptionsThenProcessRoot,
2066
+ "projects.syncTasks": buildOptionsThenProcessRoot,
2067
+ "projects.clearIssues": buildOptionsThenProcessRoot,
2068
+ "projects.fetchGitTasks": buildOptionsThenProcessRoot,
2069
+ "projects.createGitIssue": buildOptionsThenProcessRoot,
2070
+ "projects.readGitTask": buildOptionsThenProcessRoot,
2071
+ "projects.writeGitTask": buildOptionsThenProcessRoot,
2060
2072
  "agents.resolveTargetFile": buildOptionsOnly,
2061
2073
  "projects.resolveProjectTargetFile": buildOptionsOnly,
2062
- "agents.loadAgent": buildRepoRootOnly,
2063
- "agents.loadAgentPrompt": buildRepoRootOnly,
2064
- "projects.resolveImplementationPlan": (args, options, _defaultRepoRoot) => {
2074
+ "agents.loadAgent": buildProcessRootOnly,
2075
+ "agents.loadAgentPrompt": buildProcessRootOnly,
2076
+ "projects.resolveImplementationPlan": (args, options, _defaultProcessRoot) => {
2065
2077
  const invocationArgs: unknown[] = [...args];
2066
2078
  const remaining = { ...options };
2067
- const repoRoot = remaining.repoRoot;
2068
- if (typeof repoRoot === "string") {
2069
- delete remaining.repoRoot;
2079
+ const processRoot = remaining.processRoot;
2080
+ if (typeof processRoot === "string") {
2081
+ delete remaining.processRoot;
2070
2082
  }
2083
+ delete remaining.repoRoot;
2071
2084
 
2072
2085
  // This tool is a low-level helper: projects.resolveImplementationPlan(projectRoot, inputFile?, options?)
2073
2086
  // If the caller provides options but no inputFile, preserve the positional slot.
@@ -2078,17 +2091,17 @@ const toolInvocationPlans: Record<string, ToolInvoker> = {
2078
2091
  invocationArgs.push(remaining);
2079
2092
  }
2080
2093
 
2081
- // Intentionally do NOT append repoRoot: projectRoot is the first positional argument.
2094
+ // Intentionally do NOT append processRoot: projectRoot is the first positional argument.
2082
2095
  return invocationArgs;
2083
2096
  },
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,
2097
+ "agents.main": buildProcessRootOnly,
2098
+ "agents.resolveAgentsRoot": buildProcessRootOnly,
2099
+ "agents.listAgents": buildProcessRootOnly,
2100
+ "projects.resolveProjectRoot": buildProcessRootOnly,
2101
+ "projects.listProjects": buildProcessRootOnly,
2102
+ "projects.listProjectDocs": buildProcessRootOnly,
2103
+ "projects.readProjectDoc": buildProcessRootOnly,
2104
+ "projects.main": buildProcessRootOnly,
2092
2105
  };
2093
2106
 
2094
2107
  const invokeTool = async (
@@ -2104,14 +2117,14 @@ const invokeTool = async (
2104
2117
  };
2105
2118
  const invoke =
2106
2119
  toolInvocationPlans[tool.name] ??
2107
- ((rawArgs, rawOptions, _repoRoot) => {
2120
+ ((rawArgs, rawOptions, _processRoot) => {
2108
2121
  const invocationArgs = [...rawArgs];
2109
2122
  if (Object.keys(rawOptions).length > 0) {
2110
2123
  invocationArgs.push(rawOptions);
2111
2124
  }
2112
2125
  return invocationArgs;
2113
2126
  });
2114
- const invocationArgs = invoke(args, options, repoCtx.defaultRepoRoot);
2127
+ const invocationArgs = invoke(args, options, repoCtx.defaultProcessRoot);
2115
2128
 
2116
2129
  return Promise.resolve(tool.method(...invocationArgs));
2117
2130
  };
@@ -2121,13 +2134,8 @@ export interface ExampleMcpServerOptions {
2121
2134
  serverVersion?: string;
2122
2135
  toolsPrefix?: string;
2123
2136
  /**
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.
2137
+ * Optional default repo selector (for MCP repoName matching).
2138
+ * If omitted, auto-detected from git remote (fallback: workspace folder name).
2131
2139
  */
2132
2140
  repoName?: string;
2133
2141
  allowedRootEndpoints?: string[];
@@ -2262,48 +2270,23 @@ export const createExampleMcpServer = (
2262
2270
  ): ExampleMcpServerInstance => {
2263
2271
  let toolCatalog: unknown[] = [];
2264
2272
 
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
2273
  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
- }
2274
+ const defaultProcessRoot = resolveProcessRootFromGit(serverFileDir);
2275
+ const configuredRepoName = options.repoName?.trim() || null;
2276
+ const resolvedRepoIdentity = resolveRepoIdentityFromGit(defaultProcessRoot);
2277
+ const defaultRepoName = configuredRepoName || resolvedRepoIdentity.defaultRepoName;
2278
+ const defaultRepoFullName = resolvedRepoIdentity.defaultRepoFullName;
2279
+ const { repoNames, repoNameByKey } = buildRepoNameContext(
2280
+ defaultRepoName,
2281
+ defaultRepoFullName,
2282
+ );
2283
+
2302
2284
  const repoCtx: RepoResolutionContext = {
2303
- defaultRepoRoot,
2285
+ defaultProcessRoot,
2304
2286
  defaultRepoName,
2305
- repoMapByKey,
2287
+ defaultRepoFullName,
2306
2288
  repoNames,
2289
+ repoNameByKey,
2307
2290
  };
2308
2291
 
2309
2292
  const parseString = (value: unknown): string | null => {
@@ -2344,7 +2327,7 @@ export const createExampleMcpServer = (
2344
2327
  "F0 MCP helper tools:",
2345
2328
  "- mcp.listTools: returns tool catalog with access + invocation hints",
2346
2329
  "- mcp.describeTool: describe one tool by name (prefixed or unprefixed)",
2347
- "- mcp.workspace: explain server filesystem context (cwd, repoName/repoRoot, projects)",
2330
+ "- mcp.workspace: explain Git workspace context (cwd, repoName selectors, projects)",
2348
2331
  "- mcp.search: LLM-friendly search over project docs/spec (local-first)",
2349
2332
  "",
2350
2333
  'Tip: Prefer mcp.search for "search spec/docs" requests.',
@@ -2356,35 +2339,35 @@ export const createExampleMcpServer = (
2356
2339
  ? {
2357
2340
  keys: Object.keys(input),
2358
2341
  repoName: (input as any).repoName ?? null,
2359
- repoRoot: (input as any).repoRoot ?? null,
2360
- processRoot: (input as any).processRoot ?? null,
2361
2342
  }
2362
- : { keys: [], repoName: null, repoRoot: null, processRoot: null };
2343
+ : { keys: [], repoName: null };
2363
2344
  const requestedRepoName = parseString(payload.repoName);
2364
2345
  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");
2346
+ const processRoot =
2347
+ typeof resolved.processRoot === "string"
2348
+ ? resolved.processRoot
2349
+ : repoCtx.defaultProcessRoot;
2350
+ const effectiveRepoName = normalizeRepoNameForDisplay(
2351
+ requestedRepoName,
2352
+ repoCtx,
2353
+ );
2354
+
2355
+ const projectsDir = path.join(processRoot, "projects");
2356
+ const apiDir = path.join(processRoot, "api");
2357
+ const agentsDir = path.join(processRoot, "agents");
2377
2358
  const hasProjectsDir = isDir(projectsDir);
2378
2359
  const hasApiDir = isDir(apiDir);
2379
2360
  const hasAgentsDir = isDir(agentsDir);
2380
2361
 
2381
- const projects = hasProjectsDir ? projectsApi.listProjects(repoRoot) : [];
2362
+ const projects = hasProjectsDir
2363
+ ? projectsApi.listProjects(processRoot)
2364
+ : [];
2382
2365
  const hint = (() => {
2383
2366
  if (!hasProjectsDir) {
2384
2367
  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.",
2368
+ "Git workspace does not contain /projects.",
2369
+ "Pass repoName as \"adl\" or \"F0/adl\".",
2370
+ "For single-repo layouts, use source:\"auto\" or source:\"gitea\" on search tools.",
2388
2371
  ].join(" ");
2389
2372
  }
2390
2373
  if (hasProjectsDir && projects.length === 0) {
@@ -2400,9 +2383,10 @@ export const createExampleMcpServer = (
2400
2383
  received,
2401
2384
  cwd: process.cwd(),
2402
2385
  defaultRepoName: repoCtx.defaultRepoName,
2403
- defaultRepoRoot: repoCtx.defaultRepoRoot,
2386
+ defaultRepoFullName: repoCtx.defaultRepoFullName,
2387
+ workspaceRoot: repoCtx.defaultProcessRoot,
2404
2388
  repoName: effectiveRepoName,
2405
- repoRoot,
2389
+ processRoot,
2406
2390
  availableRepoNames: repoCtx.repoNames,
2407
2391
  hasProjectsDir,
2408
2392
  hasApiDir,
@@ -2452,10 +2436,10 @@ export const createExampleMcpServer = (
2452
2436
  search: async (input: unknown) => {
2453
2437
  const payload = isRecord(input) ? input : {};
2454
2438
  const resolved = resolveRepoSelectorOptions(payload, repoCtx);
2455
- const repoRoot =
2456
- typeof resolved.repoRoot === "string"
2457
- ? resolved.repoRoot
2458
- : repoCtx.defaultRepoRoot;
2439
+ const processRoot =
2440
+ typeof resolved.processRoot === "string"
2441
+ ? resolved.processRoot
2442
+ : repoCtx.defaultProcessRoot;
2459
2443
 
2460
2444
  const sectionRaw = parseString(payload.section)?.toLowerCase();
2461
2445
  const section = sectionRaw === "docs" ? "docs" : "spec";
@@ -2502,11 +2486,11 @@ export const createExampleMcpServer = (
2502
2486
 
2503
2487
  const projectName = ensureProjectName(
2504
2488
  parseString(payload.projectName),
2505
- repoRoot,
2489
+ processRoot,
2506
2490
  );
2507
2491
 
2508
2492
  const searchOptions: Record<string, unknown> = {
2509
- processRoot: repoRoot,
2493
+ processRoot,
2510
2494
  };
2511
2495
 
2512
2496
  const source = parseString(payload.source)?.toLowerCase();
@@ -2829,9 +2813,9 @@ export const createExampleMcpServer = (
2829
2813
  /projects[\\/].+projects[\\/]/i.test(message)
2830
2814
  ) {
2831
2815
  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.";
2816
+ 'Repo selection is invalid. repoName must be "<repo>" or "<owner>/<repo>", for example "adl" or "F0/adl".';
2833
2817
  details.suggestion =
2834
- "Call mcp.workspace to see the server’s cwd/default repoRoot, then omit repoName or pass the correct repoName.";
2818
+ "Call mcp.workspace to see accepted repoName selectors.";
2835
2819
  }
2836
2820
 
2837
2821
  if (
@@ -2842,7 +2826,7 @@ export const createExampleMcpServer = (
2842
2826
  details.hint =
2843
2827
  "Repo selection might be wrong, or the server filesystem does not contain /projects for this workspace.";
2844
2828
  details.suggestion =
2845
- "Call mcp.workspace to see the server’s cwd/default repoRoot and available repoName values.";
2829
+ "Call mcp.workspace to see workspaceRoot and available repoName values.";
2846
2830
  details.example = {
2847
2831
  tool: prefix ? `${prefix}.mcp.workspace` : "mcp.workspace",
2848
2832
  arguments: {},