@foundation0/api 1.1.7 → 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.
@@ -204,7 +204,8 @@ describe("createExampleMcpServer request handling", () => {
204
204
  }
205
205
  });
206
206
 
207
- it("accepts repoName for projects tools (preferred over legacy repoRoot/processRoot)", async () => {
207
+ it("accepts repoName selectors in short and owner/repo format", async () => {
208
+ const originalCwd = process.cwd();
208
209
  const tempDir = await fs.mkdtemp(
209
210
  path.join(os.tmpdir(), "f0-mcp-server-reponame-"),
210
211
  );
@@ -216,36 +217,64 @@ describe("createExampleMcpServer request handling", () => {
216
217
  await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
217
218
  recursive: true,
218
219
  });
220
+ await fs.mkdir(path.join(tempDir, ".git"), { recursive: true });
221
+ await fs.writeFile(
222
+ path.join(tempDir, ".git", "config"),
223
+ [
224
+ '[remote "origin"]',
225
+ "\turl = https://example.com/F0/adl.git",
226
+ "",
227
+ ].join("\n"),
228
+ );
219
229
 
220
- const instance = createExampleMcpServer({ repoRoot: tempDir, repoName: "test" });
230
+ process.chdir(tempDir);
231
+ const instance = createExampleMcpServer();
221
232
  const handler = getToolHandler(instance);
222
233
 
223
- const result = await handler(
234
+ const shortSelector = await handler(
224
235
  {
225
236
  method: "tools/call",
226
237
  params: {
227
238
  name: "projects.listProjects",
228
239
  arguments: {
229
- repoName: "test",
240
+ repoName: "adl",
230
241
  },
231
242
  },
232
243
  },
233
244
  {},
234
245
  );
246
+ expect(shortSelector.isError).toBe(false);
247
+ const shortPayload = JSON.parse(shortSelector.content?.[0]?.text ?? "{}");
248
+ expect(shortPayload.ok).toBe(true);
249
+ expect(shortPayload.result).toContain("adl");
250
+ expect(shortPayload.result).toContain("beta");
235
251
 
236
- expect(result.isError).toBe(false);
237
- const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
238
- expect(payload.ok).toBe(true);
239
- expect(payload.result).toContain("adl");
240
- expect(payload.result).toContain("beta");
252
+ const fullSelector = await handler(
253
+ {
254
+ method: "tools/call",
255
+ params: {
256
+ name: "projects.listProjects",
257
+ arguments: {
258
+ repoName: "F0/adl",
259
+ },
260
+ },
261
+ },
262
+ {},
263
+ );
264
+ expect(fullSelector.isError).toBe(false);
265
+ const fullPayload = JSON.parse(fullSelector.content?.[0]?.text ?? "{}");
266
+ expect(fullPayload.ok).toBe(true);
267
+ expect(fullPayload.result).toContain("adl");
268
+ expect(fullPayload.result).toContain("beta");
241
269
  } finally {
270
+ process.chdir(originalCwd);
242
271
  await fs.rm(tempDir, { recursive: true, force: true });
243
272
  }
244
273
  });
245
274
 
246
- it("auto-detects repoRoot from process.cwd() when repoName is omitted", async () => {
275
+ it("auto-detects Git workspace from process.cwd() when repoName is omitted", async () => {
247
276
  const tempDir = await fs.mkdtemp(
248
- path.join(os.tmpdir(), "f0-mcp-server-autoreporoot-"),
277
+ path.join(os.tmpdir(), "f0-mcp-server-autoworkspace-"),
249
278
  );
250
279
  const originalCwd = process.cwd();
251
280
 
@@ -257,6 +286,15 @@ describe("createExampleMcpServer request handling", () => {
257
286
  await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
258
287
  recursive: true,
259
288
  });
289
+ await fs.mkdir(path.join(tempDir, ".git"), { recursive: true });
290
+ await fs.writeFile(
291
+ path.join(tempDir, ".git", "config"),
292
+ [
293
+ '[remote "origin"]',
294
+ "\turl = https://example.com/F0/adl.git",
295
+ "",
296
+ ].join("\n"),
297
+ );
260
298
 
261
299
  process.chdir(tempDir);
262
300
 
@@ -285,16 +323,35 @@ describe("createExampleMcpServer request handling", () => {
285
323
  }
286
324
  });
287
325
 
288
- it("still accepts legacy processRoot for backwards compatibility", async () => {
289
- const tempDir = await fs.mkdtemp(
290
- path.join(os.tmpdir(), "f0-mcp-server-processroot-"),
326
+ it("ignores legacy processRoot/repoRoot payload fields and uses Git workspace", async () => {
327
+ const originalCwd = process.cwd();
328
+ const workspaceA = await fs.mkdtemp(
329
+ path.join(os.tmpdir(), "f0-mcp-server-legacy-fields-a-"),
330
+ );
331
+ const workspaceB = await fs.mkdtemp(
332
+ path.join(os.tmpdir(), "f0-mcp-server-legacy-fields-b-"),
291
333
  );
292
334
  try {
293
- await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
294
- await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
335
+ await fs.mkdir(path.join(workspaceA, "api"), { recursive: true });
336
+ await fs.mkdir(path.join(workspaceA, "projects", "adl", "docs"), {
337
+ recursive: true,
338
+ });
339
+ await fs.mkdir(path.join(workspaceA, ".git"), { recursive: true });
340
+ await fs.writeFile(
341
+ path.join(workspaceA, ".git", "config"),
342
+ [
343
+ '[remote "origin"]',
344
+ "\turl = https://example.com/F0/adl.git",
345
+ "",
346
+ ].join("\n"),
347
+ );
348
+
349
+ await fs.mkdir(path.join(workspaceB, "api"), { recursive: true });
350
+ await fs.mkdir(path.join(workspaceB, "projects", "beta", "docs"), {
295
351
  recursive: true,
296
352
  });
297
353
 
354
+ process.chdir(workspaceA);
298
355
  const instance = createExampleMcpServer();
299
356
  const handler = getToolHandler(instance);
300
357
 
@@ -304,7 +361,8 @@ describe("createExampleMcpServer request handling", () => {
304
361
  params: {
305
362
  name: "projects.listProjects",
306
363
  arguments: {
307
- processRoot: tempDir,
364
+ processRoot: workspaceB,
365
+ repoRoot: workspaceB,
308
366
  },
309
367
  },
310
368
  },
@@ -315,24 +373,35 @@ describe("createExampleMcpServer request handling", () => {
315
373
  const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
316
374
  expect(payload.ok).toBe(true);
317
375
  expect(payload.result).toContain("adl");
376
+ expect(payload.result).not.toContain("beta");
318
377
  } finally {
319
- await fs.rm(tempDir, { recursive: true, force: true });
378
+ process.chdir(originalCwd);
379
+ await fs.rm(workspaceA, { recursive: true, force: true });
380
+ await fs.rm(workspaceB, { recursive: true, force: true });
320
381
  }
321
382
  });
322
383
 
323
- it("normalizes legacy repoRoot when caller accidentally passes a project root", async () => {
384
+ it("exposes mcp.workspace with Git workspace context", async () => {
385
+ const originalCwd = process.cwd();
324
386
  const tempDir = await fs.mkdtemp(
325
- path.join(os.tmpdir(), "f0-mcp-server-reporoot-normalize-"),
387
+ path.join(os.tmpdir(), "f0-mcp-server-workspace-"),
326
388
  );
327
389
  try {
328
390
  await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
329
391
  await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
330
392
  recursive: true,
331
393
  });
332
- await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
333
- recursive: true,
334
- });
394
+ await fs.mkdir(path.join(tempDir, ".git"), { recursive: true });
395
+ await fs.writeFile(
396
+ path.join(tempDir, ".git", "config"),
397
+ [
398
+ '[remote "origin"]',
399
+ "\turl = https://example.com/F0/adl.git",
400
+ "",
401
+ ].join("\n"),
402
+ );
335
403
 
404
+ process.chdir(tempDir);
336
405
  const instance = createExampleMcpServer();
337
406
  const handler = getToolHandler(instance);
338
407
 
@@ -340,10 +409,8 @@ describe("createExampleMcpServer request handling", () => {
340
409
  {
341
410
  method: "tools/call",
342
411
  params: {
343
- name: "projects.listProjects",
344
- arguments: {
345
- repoRoot: path.join(tempDir, "projects", "adl"),
346
- },
412
+ name: "mcp.workspace",
413
+ arguments: {},
347
414
  },
348
415
  },
349
416
  {},
@@ -352,64 +419,114 @@ describe("createExampleMcpServer request handling", () => {
352
419
  expect(result.isError).toBe(false);
353
420
  const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
354
421
  expect(payload.ok).toBe(true);
355
- expect(payload.result).toContain("adl");
356
- expect(payload.result).toContain("beta");
422
+ expect(payload.result.workspaceRoot).toBe(path.resolve(tempDir));
423
+ expect(payload.result.processRoot).toBe(path.resolve(tempDir));
424
+ expect(payload.result.defaultRepoName).toBe("adl");
425
+ expect(payload.result.defaultRepoFullName).toBe("F0/adl");
426
+ expect(payload.result.availableRepoNames).toContain("adl");
427
+ expect(payload.result.availableRepoNames).toContain("F0/adl");
428
+ expect(payload.result.hasProjectsDir).toBe(true);
429
+ expect(Array.isArray(payload.result.projects)).toBe(true);
357
430
  } finally {
431
+ process.chdir(originalCwd);
358
432
  await fs.rm(tempDir, { recursive: true, force: true });
359
433
  }
360
434
  });
361
435
 
362
- it("uses server default repoRoot when repoName is omitted", async () => {
436
+ it("auto-detects repo identity from .git/config when no repoName is provided", async () => {
437
+ const originalCwd = process.cwd();
363
438
  const tempDir = await fs.mkdtemp(
364
- path.join(os.tmpdir(), "f0-mcp-server-default-reporoot-"),
439
+ path.join(os.tmpdir(), "f0-mcp-server-git-detect-"),
365
440
  );
366
441
  try {
367
- await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
442
+ await fs.mkdir(path.join(tempDir, "api", "mcp"), { recursive: true });
368
443
  await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
369
444
  recursive: true,
370
445
  });
371
- await fs.mkdir(path.join(tempDir, "projects", "beta", "docs"), {
372
- recursive: true,
373
- });
446
+ await fs.mkdir(path.join(tempDir, ".git"), { recursive: true });
447
+ await fs.writeFile(
448
+ path.join(tempDir, ".git", "config"),
449
+ [
450
+ '[remote "origin"]',
451
+ "\turl = https://example.com/F0/adl.git",
452
+ "",
453
+ ].join("\n"),
454
+ );
374
455
 
375
- const instance = createExampleMcpServer({ repoRoot: tempDir });
456
+ process.chdir(path.join(tempDir, "api", "mcp"));
457
+ const instance = createExampleMcpServer();
376
458
  const handler = getToolHandler(instance);
377
459
 
378
- const result = await handler(
460
+ const workspace = await handler(
379
461
  {
380
462
  method: "tools/call",
381
463
  params: {
382
- name: "projects.listProjects",
464
+ name: "mcp.workspace",
383
465
  arguments: {},
384
466
  },
385
467
  },
386
468
  {},
387
469
  );
388
470
 
389
- expect(result.isError).toBe(false);
390
- const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
471
+ expect(workspace.isError).toBe(false);
472
+ const payload = JSON.parse(workspace.content?.[0]?.text ?? "{}");
391
473
  expect(payload.ok).toBe(true);
392
- expect(payload.result).toContain("adl");
393
- expect(payload.result).toContain("beta");
474
+ expect(payload.result.workspaceRoot).toBe(path.resolve(tempDir));
475
+ expect(payload.result.processRoot).toBe(path.resolve(tempDir));
476
+ expect(payload.result.defaultRepoName).toBe("adl");
477
+ expect(payload.result.defaultRepoFullName).toBe("F0/adl");
478
+ expect(payload.result.repoName).toBe("adl");
479
+ expect(payload.result.availableRepoNames).toContain("adl");
480
+ expect(payload.result.availableRepoNames).toContain("F0/adl");
481
+
482
+ const list = await handler(
483
+ {
484
+ method: "tools/call",
485
+ params: {
486
+ name: "projects.listProjects",
487
+ arguments: {},
488
+ },
489
+ },
490
+ {},
491
+ );
492
+ expect(list.isError).toBe(false);
493
+ const listPayload = JSON.parse(list.content?.[0]?.text ?? "{}");
494
+ expect(listPayload.ok).toBe(true);
495
+ expect(listPayload.result).toContain("adl");
394
496
  } finally {
497
+ process.chdir(originalCwd);
395
498
  await fs.rm(tempDir, { recursive: true, force: true });
396
499
  }
397
500
  });
398
501
 
399
- it("exposes mcp.workspace to explain server filesystem context", async () => {
502
+ it("supports worktree-style .git file (gitdir: ...) when deriving repoName from config", async () => {
503
+ const originalCwd = process.cwd();
400
504
  const tempDir = await fs.mkdtemp(
401
- path.join(os.tmpdir(), "f0-mcp-server-workspace-"),
505
+ path.join(os.tmpdir(), "f0-mcp-server-gitfile-detect-"),
402
506
  );
403
507
  try {
404
- await fs.mkdir(path.join(tempDir, "api"), { recursive: true });
508
+ await fs.mkdir(path.join(tempDir, "api", "mcp"), { recursive: true });
405
509
  await fs.mkdir(path.join(tempDir, "projects", "adl", "docs"), {
406
510
  recursive: true,
407
511
  });
408
512
 
409
- const instance = createExampleMcpServer({ repoRoot: tempDir });
513
+ const gitDir = path.join(tempDir, ".gitdir");
514
+ await fs.mkdir(gitDir, { recursive: true });
515
+ await fs.writeFile(
516
+ path.join(gitDir, "config"),
517
+ [
518
+ '[remote "origin"]',
519
+ "\turl = git@github.com:F0/adl.git",
520
+ "",
521
+ ].join("\n"),
522
+ );
523
+ await fs.writeFile(path.join(tempDir, ".git"), "gitdir: .gitdir\n");
524
+
525
+ process.chdir(path.join(tempDir, "api", "mcp"));
526
+ const instance = createExampleMcpServer();
410
527
  const handler = getToolHandler(instance);
411
528
 
412
- const result = await handler(
529
+ const workspace = await handler(
413
530
  {
414
531
  method: "tools/call",
415
532
  params: {
@@ -420,14 +537,14 @@ describe("createExampleMcpServer request handling", () => {
420
537
  {},
421
538
  );
422
539
 
423
- expect(result.isError).toBe(false);
424
- const payload = JSON.parse(result.content?.[0]?.text ?? "{}");
540
+ expect(workspace.isError).toBe(false);
541
+ const payload = JSON.parse(workspace.content?.[0]?.text ?? "{}");
425
542
  expect(payload.ok).toBe(true);
426
- expect(payload.result.repoRoot).toBe(path.resolve(tempDir));
427
- expect(payload.result.defaultRepoRoot).toBe(path.resolve(tempDir));
428
- expect(payload.result.hasProjectsDir).toBe(true);
429
- expect(Array.isArray(payload.result.projects)).toBe(true);
543
+ expect(payload.result.defaultRepoName).toBe("adl");
544
+ expect(payload.result.defaultRepoFullName).toBe("F0/adl");
545
+ expect(payload.result.availableRepoNames).toContain("F0/adl");
430
546
  } finally {
547
+ process.chdir(originalCwd);
431
548
  await fs.rm(tempDir, { recursive: true, force: true });
432
549
  }
433
550
  });