@cubis/foundry 0.3.40 → 0.3.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +67 -3
  2. package/bin/cubis.js +360 -26
  3. package/mcp/README.md +72 -8
  4. package/mcp/config.json +3 -0
  5. package/mcp/dist/index.js +315 -68
  6. package/mcp/src/config/index.test.ts +1 -0
  7. package/mcp/src/config/schema.ts +5 -0
  8. package/mcp/src/index.ts +40 -9
  9. package/mcp/src/server.ts +66 -10
  10. package/mcp/src/telemetry/tokenBudget.ts +114 -0
  11. package/mcp/src/tools/index.ts +7 -0
  12. package/mcp/src/tools/skillBrowseCategory.ts +22 -5
  13. package/mcp/src/tools/skillBudgetReport.ts +128 -0
  14. package/mcp/src/tools/skillGet.ts +18 -0
  15. package/mcp/src/tools/skillListCategories.ts +19 -6
  16. package/mcp/src/tools/skillSearch.ts +22 -5
  17. package/mcp/src/tools/skillTools.test.ts +61 -9
  18. package/mcp/src/vault/manifest.test.ts +19 -1
  19. package/mcp/src/vault/manifest.ts +12 -1
  20. package/mcp/src/vault/scanner.test.ts +1 -0
  21. package/mcp/src/vault/scanner.ts +1 -0
  22. package/mcp/src/vault/types.ts +6 -0
  23. package/package.json +1 -1
  24. package/workflows/workflows/agent-environment-setup/platforms/antigravity/rules/GEMINI.md +28 -0
  25. package/workflows/workflows/agent-environment-setup/platforms/codex/rules/AGENTS.md +31 -2
  26. package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/AGENTS.md +28 -0
  27. package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/copilot-instructions.md +28 -0
  28. package/workflows/workflows/agent-environment-setup/platforms/cursor/rules/.cursorrules +28 -0
  29. package/workflows/workflows/agent-environment-setup/platforms/windsurf/rules/.windsurfrules +28 -0
package/mcp/dist/index.js CHANGED
@@ -17,6 +17,9 @@ var ServerConfigSchema = z.object({
17
17
  roots: z.array(z.string()).min(1),
18
18
  summaryMaxLength: z.number().int().positive().default(200)
19
19
  }),
20
+ telemetry: z.object({
21
+ charsPerToken: z.number().positive().default(4)
22
+ }).default({ charsPerToken: 4 }),
20
23
  transport: z.object({
21
24
  default: z.enum(["stdio", "streamable-http"]).default("stdio"),
22
25
  http: z.object({
@@ -148,7 +151,8 @@ async function scanVaultRoots(roots, basePath) {
148
151
  skills.push({
149
152
  id: entry,
150
153
  category: deriveCategory(entry),
151
- path: skillFile
154
+ path: skillFile,
155
+ fileBytes: skillStat.size
152
156
  });
153
157
  }
154
158
  }
@@ -241,14 +245,80 @@ function deriveCategory(skillId) {
241
245
 
242
246
  // src/vault/manifest.ts
243
247
  import { readFile } from "fs/promises";
244
- function buildManifest(skills) {
248
+
249
+ // src/telemetry/tokenBudget.ts
250
+ var TOKEN_ESTIMATOR_VERSION = "char-estimator-v1";
251
+ function normalizeCharsPerToken(value) {
252
+ if (!Number.isFinite(value) || value <= 0) return 4;
253
+ return value;
254
+ }
255
+ function estimateTokensFromCharCount(charCount, charsPerToken) {
256
+ const safeChars = Math.max(0, Math.ceil(charCount));
257
+ const ratio = normalizeCharsPerToken(charsPerToken);
258
+ return Math.ceil(safeChars / ratio);
259
+ }
260
+ function estimateTokensFromText(text, charsPerToken) {
261
+ return estimateTokensFromCharCount(text.length, charsPerToken);
262
+ }
263
+ function estimateTokensFromBytes(byteCount, charsPerToken) {
264
+ return estimateTokensFromCharCount(byteCount, charsPerToken);
265
+ }
266
+ function estimateSavings(fullCatalogEstimatedTokens, usedEstimatedTokens) {
267
+ const full = Math.max(0, Math.ceil(fullCatalogEstimatedTokens));
268
+ const used = Math.max(0, Math.ceil(usedEstimatedTokens));
269
+ if (full <= 0) {
270
+ return {
271
+ estimatedSavingsTokens: 0,
272
+ estimatedSavingsPercent: 0
273
+ };
274
+ }
275
+ const estimatedSavingsTokens = Math.max(0, full - used);
276
+ const estimatedSavingsPercent = Number(
277
+ (estimatedSavingsTokens / full * 100).toFixed(2)
278
+ );
279
+ return {
280
+ estimatedSavingsTokens,
281
+ estimatedSavingsPercent
282
+ };
283
+ }
284
+ function buildSkillToolMetrics({
285
+ charsPerToken,
286
+ fullCatalogEstimatedTokens,
287
+ responseEstimatedTokens,
288
+ selectedSkillsEstimatedTokens = null,
289
+ loadedSkillEstimatedTokens = null
290
+ }) {
291
+ const usedEstimatedTokens = loadedSkillEstimatedTokens ?? selectedSkillsEstimatedTokens ?? responseEstimatedTokens;
292
+ const savings = estimateSavings(fullCatalogEstimatedTokens, usedEstimatedTokens);
293
+ return {
294
+ estimatorVersion: TOKEN_ESTIMATOR_VERSION,
295
+ charsPerToken: normalizeCharsPerToken(charsPerToken),
296
+ fullCatalogEstimatedTokens: Math.max(0, fullCatalogEstimatedTokens),
297
+ responseEstimatedTokens: Math.max(0, responseEstimatedTokens),
298
+ selectedSkillsEstimatedTokens: selectedSkillsEstimatedTokens === null ? null : Math.max(0, selectedSkillsEstimatedTokens),
299
+ loadedSkillEstimatedTokens: loadedSkillEstimatedTokens === null ? null : Math.max(0, loadedSkillEstimatedTokens),
300
+ estimatedSavingsVsFullCatalog: savings.estimatedSavingsTokens,
301
+ estimatedSavingsVsFullCatalogPercent: savings.estimatedSavingsPercent,
302
+ estimated: true
303
+ };
304
+ }
305
+
306
+ // src/vault/manifest.ts
307
+ function buildManifest(skills, charsPerToken) {
245
308
  const categorySet = /* @__PURE__ */ new Set();
309
+ let fullCatalogBytes = 0;
246
310
  for (const skill of skills) {
247
311
  categorySet.add(skill.category);
312
+ fullCatalogBytes += skill.fileBytes;
248
313
  }
249
314
  return {
250
315
  categories: [...categorySet].sort(),
251
- skills
316
+ skills,
317
+ fullCatalogBytes,
318
+ fullCatalogEstimatedTokens: estimateTokensFromBytes(
319
+ fullCatalogBytes,
320
+ charsPerToken
321
+ )
252
322
  };
253
323
  }
254
324
  async function extractDescription(skillPath, maxLength) {
@@ -290,14 +360,14 @@ async function enrichWithDescriptions(skills, maxLength) {
290
360
 
291
361
  // src/server.ts
292
362
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
293
- import { z as z12 } from "zod";
363
+ import { z as z13 } from "zod";
294
364
 
295
365
  // src/tools/skillListCategories.ts
296
366
  import { z as z2 } from "zod";
297
367
  var skillListCategoriesName = "skill_list_categories";
298
368
  var skillListCategoriesDescription = "List all skill categories available in the vault. Returns category names and skill counts.";
299
369
  var skillListCategoriesSchema = z2.object({});
300
- function handleSkillListCategories(manifest) {
370
+ function handleSkillListCategories(manifest, charsPerToken) {
301
371
  const categoryCounts = {};
302
372
  for (const skill of manifest.skills) {
303
373
  categoryCounts[skill.category] = (categoryCounts[skill.category] ?? 0) + 1;
@@ -306,17 +376,23 @@ function handleSkillListCategories(manifest) {
306
376
  category: cat,
307
377
  skillCount: categoryCounts[cat] ?? 0
308
378
  }));
379
+ const payload = { categories, totalSkills: manifest.skills.length };
380
+ const text = JSON.stringify(payload, null, 2);
381
+ const metrics = buildSkillToolMetrics({
382
+ charsPerToken,
383
+ fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
384
+ responseEstimatedTokens: estimateTokensFromText(text, charsPerToken)
385
+ });
309
386
  return {
310
387
  content: [
311
388
  {
312
389
  type: "text",
313
- text: JSON.stringify(
314
- { categories, totalSkills: manifest.skills.length },
315
- null,
316
- 2
317
- )
390
+ text
318
391
  }
319
- ]
392
+ ],
393
+ structuredContent: {
394
+ metrics
395
+ }
320
396
  };
321
397
  }
322
398
 
@@ -350,7 +426,7 @@ var skillBrowseCategoryDescription = "Browse skills within a specific category.
350
426
  var skillBrowseCategorySchema = z3.object({
351
427
  category: z3.string().describe("The category name to browse (from skill_list_categories)")
352
428
  });
353
- async function handleSkillBrowseCategory(args, manifest, summaryMaxLength) {
429
+ async function handleSkillBrowseCategory(args, manifest, summaryMaxLength, charsPerToken) {
354
430
  const { category } = args;
355
431
  if (!manifest.categories.includes(category)) {
356
432
  notFound("Category", category);
@@ -361,17 +437,28 @@ async function handleSkillBrowseCategory(args, manifest, summaryMaxLength) {
361
437
  id: s.id,
362
438
  description: s.description ?? "(no description)"
363
439
  }));
440
+ const payload = { category, skills, count: skills.length };
441
+ const text = JSON.stringify(payload, null, 2);
442
+ const selectedSkillsEstimatedTokens = matching.reduce(
443
+ (sum, skill) => sum + estimateTokensFromBytes(skill.fileBytes, charsPerToken),
444
+ 0
445
+ );
446
+ const metrics = buildSkillToolMetrics({
447
+ charsPerToken,
448
+ fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
449
+ responseEstimatedTokens: estimateTokensFromText(text, charsPerToken),
450
+ selectedSkillsEstimatedTokens
451
+ });
364
452
  return {
365
453
  content: [
366
454
  {
367
455
  type: "text",
368
- text: JSON.stringify(
369
- { category, skills, count: skills.length },
370
- null,
371
- 2
372
- )
456
+ text
373
457
  }
374
- ]
458
+ ],
459
+ structuredContent: {
460
+ metrics
461
+ }
375
462
  };
376
463
  }
377
464
 
@@ -384,7 +471,7 @@ var skillSearchSchema = z4.object({
384
471
  "Search keyword or phrase to match against skill IDs and descriptions"
385
472
  )
386
473
  });
387
- async function handleSkillSearch(args, manifest, summaryMaxLength) {
474
+ async function handleSkillSearch(args, manifest, summaryMaxLength, charsPerToken) {
388
475
  const { query } = args;
389
476
  const lower = query.toLowerCase();
390
477
  let matches = manifest.skills.filter(
@@ -406,17 +493,28 @@ async function handleSkillSearch(args, manifest, summaryMaxLength) {
406
493
  category: s.category,
407
494
  description: s.description ?? "(no description)"
408
495
  }));
496
+ const payload = { query, results, count: results.length };
497
+ const text = JSON.stringify(payload, null, 2);
498
+ const selectedSkillsEstimatedTokens = matches.reduce(
499
+ (sum, skill) => sum + estimateTokensFromBytes(skill.fileBytes, charsPerToken),
500
+ 0
501
+ );
502
+ const metrics = buildSkillToolMetrics({
503
+ charsPerToken,
504
+ fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
505
+ responseEstimatedTokens: estimateTokensFromText(text, charsPerToken),
506
+ selectedSkillsEstimatedTokens
507
+ });
409
508
  return {
410
509
  content: [
411
510
  {
412
511
  type: "text",
413
- text: JSON.stringify(
414
- { query, results, count: results.length },
415
- null,
416
- 2
417
- )
512
+ text
418
513
  }
419
- ]
514
+ ],
515
+ structuredContent: {
516
+ metrics
517
+ }
420
518
  };
421
519
  }
422
520
 
@@ -427,25 +525,121 @@ var skillGetDescription = "Get the full content of a specific skill by ID. Retur
427
525
  var skillGetSchema = z5.object({
428
526
  id: z5.string().describe("The skill ID (directory name) to retrieve")
429
527
  });
430
- async function handleSkillGet(args, manifest) {
528
+ async function handleSkillGet(args, manifest, charsPerToken) {
431
529
  const { id } = args;
432
530
  const skill = manifest.skills.find((s) => s.id === id);
433
531
  if (!skill) {
434
532
  notFound("Skill", id);
435
533
  }
436
534
  const content = await readFullSkillContent(skill.path);
535
+ const loadedSkillEstimatedTokens = estimateTokensFromText(
536
+ content,
537
+ charsPerToken
538
+ );
539
+ const metrics = buildSkillToolMetrics({
540
+ charsPerToken,
541
+ fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
542
+ responseEstimatedTokens: loadedSkillEstimatedTokens,
543
+ loadedSkillEstimatedTokens
544
+ });
437
545
  return {
438
546
  content: [
439
547
  {
440
548
  type: "text",
441
549
  text: content
442
550
  }
443
- ]
551
+ ],
552
+ structuredContent: {
553
+ metrics
554
+ }
444
555
  };
445
556
  }
446
557
 
447
- // src/tools/postmanGetMode.ts
558
+ // src/tools/skillBudgetReport.ts
448
559
  import { z as z6 } from "zod";
560
+ var skillBudgetReportName = "skill_budget_report";
561
+ var skillBudgetReportDescription = "Report estimated context/token budget for selected and loaded skills compared to the full skill catalog.";
562
+ var skillBudgetReportSchema = z6.object({
563
+ selectedSkillIds: z6.array(z6.string()).default([]).describe("Skill IDs selected after search/browse."),
564
+ loadedSkillIds: z6.array(z6.string()).default([]).describe("Skill IDs loaded via skill_get.")
565
+ });
566
+ function uniqueStrings(values) {
567
+ return [...new Set(values.map((value) => String(value)))];
568
+ }
569
+ function handleSkillBudgetReport(args, manifest, charsPerToken) {
570
+ const selectedSkillIds = uniqueStrings(args.selectedSkillIds ?? []);
571
+ const loadedSkillIds = uniqueStrings(args.loadedSkillIds ?? []);
572
+ const skillById = new Map(manifest.skills.map((skill) => [skill.id, skill]));
573
+ const selectedSkills = selectedSkillIds.map((id) => {
574
+ const skill = skillById.get(id);
575
+ if (!skill) return null;
576
+ return {
577
+ id: skill.id,
578
+ category: skill.category,
579
+ estimatedTokens: estimateTokensFromBytes(skill.fileBytes, charsPerToken)
580
+ };
581
+ }).filter((item) => Boolean(item));
582
+ const loadedSkills = loadedSkillIds.map((id) => {
583
+ const skill = skillById.get(id);
584
+ if (!skill) return null;
585
+ return {
586
+ id: skill.id,
587
+ category: skill.category,
588
+ estimatedTokens: estimateTokensFromBytes(skill.fileBytes, charsPerToken)
589
+ };
590
+ }).filter((item) => Boolean(item));
591
+ const unknownSelectedSkillIds = selectedSkillIds.filter(
592
+ (id) => !skillById.has(id)
593
+ );
594
+ const unknownLoadedSkillIds = loadedSkillIds.filter((id) => !skillById.has(id));
595
+ const selectedSkillsEstimatedTokens = selectedSkills.reduce(
596
+ (sum, skill) => sum + skill.estimatedTokens,
597
+ 0
598
+ );
599
+ const loadedSkillsEstimatedTokens = loadedSkills.reduce(
600
+ (sum, skill) => sum + skill.estimatedTokens,
601
+ 0
602
+ );
603
+ const usedEstimatedTokens = loadedSkills.length > 0 ? loadedSkillsEstimatedTokens : selectedSkillsEstimatedTokens;
604
+ const savings = estimateSavings(
605
+ manifest.fullCatalogEstimatedTokens,
606
+ usedEstimatedTokens
607
+ );
608
+ const selectedIdSet = new Set(selectedSkills.map((skill) => skill.id));
609
+ const loadedIdSet = new Set(loadedSkills.map((skill) => skill.id));
610
+ const skippedSkills = manifest.skills.filter((skill) => !selectedIdSet.has(skill.id) && !loadedIdSet.has(skill.id)).map((skill) => skill.id).sort((a, b) => a.localeCompare(b));
611
+ const payload = {
612
+ skillLog: {
613
+ selectedSkills,
614
+ loadedSkills,
615
+ skippedSkills,
616
+ unknownSelectedSkillIds,
617
+ unknownLoadedSkillIds
618
+ },
619
+ contextBudget: {
620
+ estimatorVersion: TOKEN_ESTIMATOR_VERSION,
621
+ charsPerToken,
622
+ fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
623
+ selectedSkillsEstimatedTokens,
624
+ loadedSkillsEstimatedTokens,
625
+ estimatedSavingsTokens: savings.estimatedSavingsTokens,
626
+ estimatedSavingsPercent: savings.estimatedSavingsPercent,
627
+ estimated: true
628
+ }
629
+ };
630
+ return {
631
+ content: [
632
+ {
633
+ type: "text",
634
+ text: JSON.stringify(payload, null, 2)
635
+ }
636
+ ],
637
+ structuredContent: payload
638
+ };
639
+ }
640
+
641
+ // src/tools/postmanGetMode.ts
642
+ import { z as z7 } from "zod";
449
643
 
450
644
  // src/cbxConfig/paths.ts
451
645
  import path3 from "path";
@@ -705,8 +899,8 @@ function isValidMode(mode) {
705
899
  // src/tools/postmanGetMode.ts
706
900
  var postmanGetModeName = "postman_get_mode";
707
901
  var postmanGetModeDescription = "Get the current Postman MCP mode from cbx_config.json. Returns the friendly mode name and URL.";
708
- var postmanGetModeSchema = z6.object({
709
- scope: z6.enum(["global", "project", "auto"]).optional().describe(
902
+ var postmanGetModeSchema = z7.object({
903
+ scope: z7.enum(["global", "project", "auto"]).optional().describe(
710
904
  "Config scope to read. Default: auto (project if exists, else global)"
711
905
  )
712
906
  });
@@ -761,12 +955,12 @@ function handlePostmanGetMode(args) {
761
955
  }
762
956
 
763
957
  // src/tools/postmanSetMode.ts
764
- import { z as z7 } from "zod";
958
+ import { z as z8 } from "zod";
765
959
  var postmanSetModeName = "postman_set_mode";
766
960
  var postmanSetModeDescription = "Set the Postman MCP mode in cbx_config.json. Modes: minimal, code, full.";
767
- var postmanSetModeSchema = z7.object({
768
- mode: z7.enum(["minimal", "code", "full"]).describe("Postman MCP mode to set: minimal, code, or full"),
769
- scope: z7.enum(["global", "project", "auto"]).optional().describe(
961
+ var postmanSetModeSchema = z8.object({
962
+ mode: z8.enum(["minimal", "code", "full"]).describe("Postman MCP mode to set: minimal, code, or full"),
963
+ scope: z8.enum(["global", "project", "auto"]).optional().describe(
770
964
  "Config scope to write. Default: auto (project if exists, else global)"
771
965
  )
772
966
  });
@@ -805,11 +999,11 @@ function handlePostmanSetMode(args) {
805
999
  }
806
1000
 
807
1001
  // src/tools/postmanGetStatus.ts
808
- import { z as z8 } from "zod";
1002
+ import { z as z9 } from "zod";
809
1003
  var postmanGetStatusName = "postman_get_status";
810
1004
  var postmanGetStatusDescription = "Get full Postman configuration status including mode, URL, and workspace ID.";
811
- var postmanGetStatusSchema = z8.object({
812
- scope: z8.enum(["global", "project", "auto"]).optional().describe(
1005
+ var postmanGetStatusSchema = z9.object({
1006
+ scope: z9.enum(["global", "project", "auto"]).optional().describe(
813
1007
  "Config scope to read. Default: auto (project if exists, else global)"
814
1008
  )
815
1009
  });
@@ -849,11 +1043,11 @@ function handlePostmanGetStatus(args) {
849
1043
  }
850
1044
 
851
1045
  // src/tools/stitchGetMode.ts
852
- import { z as z9 } from "zod";
1046
+ import { z as z10 } from "zod";
853
1047
  var stitchGetModeName = "stitch_get_mode";
854
1048
  var stitchGetModeDescription = "Get the active Stitch profile name and URL from cbx_config.json. Never exposes API keys.";
855
- var stitchGetModeSchema = z9.object({
856
- scope: z9.enum(["global", "project", "auto"]).optional().describe(
1049
+ var stitchGetModeSchema = z10.object({
1050
+ scope: z10.enum(["global", "project", "auto"]).optional().describe(
857
1051
  "Config scope to read. Default: auto (project if exists, else global)"
858
1052
  )
859
1053
  });
@@ -888,12 +1082,12 @@ function handleStitchGetMode(args) {
888
1082
  }
889
1083
 
890
1084
  // src/tools/stitchSetProfile.ts
891
- import { z as z10 } from "zod";
1085
+ import { z as z11 } from "zod";
892
1086
  var stitchSetProfileName = "stitch_set_profile";
893
1087
  var stitchSetProfileDescription = "Set the active Stitch profile in cbx_config.json. The profile must already exist in the config.";
894
- var stitchSetProfileSchema = z10.object({
895
- profileName: z10.string().min(1).describe("Name of the Stitch profile to activate"),
896
- scope: z10.enum(["global", "project", "auto"]).optional().describe(
1088
+ var stitchSetProfileSchema = z11.object({
1089
+ profileName: z11.string().min(1).describe("Name of the Stitch profile to activate"),
1090
+ scope: z11.enum(["global", "project", "auto"]).optional().describe(
897
1091
  "Config scope to write. Default: auto (project if exists, else global)"
898
1092
  )
899
1093
  });
@@ -938,11 +1132,11 @@ function handleStitchSetProfile(args) {
938
1132
  }
939
1133
 
940
1134
  // src/tools/stitchGetStatus.ts
941
- import { z as z11 } from "zod";
1135
+ import { z as z12 } from "zod";
942
1136
  var stitchGetStatusName = "stitch_get_status";
943
1137
  var stitchGetStatusDescription = "Get full Stitch configuration status including active profile, all profile names, and URLs. Never exposes API keys.";
944
- var stitchGetStatusSchema = z11.object({
945
- scope: z11.enum(["global", "project", "auto"]).optional().describe(
1138
+ var stitchGetStatusSchema = z12.object({
1139
+ scope: z12.enum(["global", "project", "auto"]).optional().describe(
946
1140
  "Config scope to read. Default: auto (project if exists, else global)"
947
1141
  )
948
1142
  });
@@ -1219,75 +1413,102 @@ function toolCallErrorResult({
1219
1413
  }
1220
1414
  async function createServer({
1221
1415
  config,
1222
- manifest
1416
+ manifest,
1417
+ defaultConfigScope = "auto"
1223
1418
  }) {
1224
1419
  const server = new McpServer({
1225
1420
  name: config.server.name,
1226
1421
  version: config.server.version
1227
1422
  });
1228
1423
  const maxLen = config.vault.summaryMaxLength;
1424
+ const charsPerToken = config.telemetry?.charsPerToken ?? 4;
1425
+ const withDefaultScope = (args) => {
1426
+ const safeArgs = args ?? {};
1427
+ return {
1428
+ ...safeArgs,
1429
+ scope: typeof safeArgs.scope === "string" ? safeArgs.scope : defaultConfigScope
1430
+ };
1431
+ };
1229
1432
  server.tool(
1230
1433
  skillListCategoriesName,
1231
1434
  skillListCategoriesDescription,
1232
1435
  skillListCategoriesSchema.shape,
1233
- async () => handleSkillListCategories(manifest)
1436
+ async () => handleSkillListCategories(manifest, charsPerToken)
1234
1437
  );
1235
1438
  server.tool(
1236
1439
  skillBrowseCategoryName,
1237
1440
  skillBrowseCategoryDescription,
1238
1441
  skillBrowseCategorySchema.shape,
1239
- async (args) => handleSkillBrowseCategory(args, manifest, maxLen)
1442
+ async (args) => handleSkillBrowseCategory(args, manifest, maxLen, charsPerToken)
1240
1443
  );
1241
1444
  server.tool(
1242
1445
  skillSearchName,
1243
1446
  skillSearchDescription,
1244
1447
  skillSearchSchema.shape,
1245
- async (args) => handleSkillSearch(args, manifest, maxLen)
1448
+ async (args) => handleSkillSearch(args, manifest, maxLen, charsPerToken)
1246
1449
  );
1247
1450
  server.tool(
1248
1451
  skillGetName,
1249
1452
  skillGetDescription,
1250
1453
  skillGetSchema.shape,
1251
- async (args) => handleSkillGet(args, manifest)
1454
+ async (args) => handleSkillGet(args, manifest, charsPerToken)
1455
+ );
1456
+ server.tool(
1457
+ skillBudgetReportName,
1458
+ skillBudgetReportDescription,
1459
+ skillBudgetReportSchema.shape,
1460
+ async (args) => handleSkillBudgetReport(args, manifest, charsPerToken)
1252
1461
  );
1253
1462
  server.tool(
1254
1463
  postmanGetModeName,
1255
1464
  postmanGetModeDescription,
1256
1465
  postmanGetModeSchema.shape,
1257
- async (args) => handlePostmanGetMode(args)
1466
+ async (args) => handlePostmanGetMode(
1467
+ withDefaultScope(args)
1468
+ )
1258
1469
  );
1259
1470
  server.tool(
1260
1471
  postmanSetModeName,
1261
1472
  postmanSetModeDescription,
1262
1473
  postmanSetModeSchema.shape,
1263
- async (args) => handlePostmanSetMode(args)
1474
+ async (args) => handlePostmanSetMode(
1475
+ withDefaultScope(args)
1476
+ )
1264
1477
  );
1265
1478
  server.tool(
1266
1479
  postmanGetStatusName,
1267
1480
  postmanGetStatusDescription,
1268
1481
  postmanGetStatusSchema.shape,
1269
- async (args) => handlePostmanGetStatus(args)
1482
+ async (args) => handlePostmanGetStatus(
1483
+ withDefaultScope(args)
1484
+ )
1270
1485
  );
1271
1486
  server.tool(
1272
1487
  stitchGetModeName,
1273
1488
  stitchGetModeDescription,
1274
1489
  stitchGetModeSchema.shape,
1275
- async (args) => handleStitchGetMode(args)
1490
+ async (args) => handleStitchGetMode(
1491
+ withDefaultScope(args)
1492
+ )
1276
1493
  );
1277
1494
  server.tool(
1278
1495
  stitchSetProfileName,
1279
1496
  stitchSetProfileDescription,
1280
1497
  stitchSetProfileSchema.shape,
1281
- async (args) => handleStitchSetProfile(args)
1498
+ async (args) => handleStitchSetProfile(
1499
+ withDefaultScope(args)
1500
+ )
1282
1501
  );
1283
1502
  server.tool(
1284
1503
  stitchGetStatusName,
1285
1504
  stitchGetStatusDescription,
1286
1505
  stitchGetStatusSchema.shape,
1287
- async (args) => handleStitchGetStatus(args)
1506
+ async (args) => handleStitchGetStatus(
1507
+ withDefaultScope(args)
1508
+ )
1288
1509
  );
1289
1510
  const upstreamCatalogs = await discoverUpstreamCatalogs();
1290
- const dynamicArgsShape = z12.object({}).passthrough().shape;
1511
+ const dynamicArgsShape = z13.object({}).passthrough().shape;
1291
1512
  for (const catalog of [upstreamCatalogs.postman, upstreamCatalogs.stitch]) {
1292
1513
  for (const tool of catalog.tools) {
1293
1514
  const namespaced = tool.namespacedName;
@@ -1357,8 +1578,11 @@ import { fileURLToPath as fileURLToPath2 } from "url";
1357
1578
  var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
1358
1579
  function parseArgs(argv) {
1359
1580
  let transport = "stdio";
1581
+ let scope = "auto";
1360
1582
  let scanOnly = false;
1361
1583
  let debug = false;
1584
+ let port;
1585
+ let host;
1362
1586
  let configPath;
1363
1587
  for (let i = 2; i < argv.length; i++) {
1364
1588
  const arg = argv[i];
@@ -1372,6 +1596,23 @@ function parseArgs(argv) {
1372
1596
  logger.error(`Unknown transport: ${val}. Use "stdio" or "http".`);
1373
1597
  process.exit(1);
1374
1598
  }
1599
+ } else if (arg === "--scope" && argv[i + 1]) {
1600
+ const val = argv[++i];
1601
+ if (val === "auto" || val === "global" || val === "project") {
1602
+ scope = val;
1603
+ } else {
1604
+ logger.error(`Unknown scope: ${val}. Use "auto", "global", or "project".`);
1605
+ process.exit(1);
1606
+ }
1607
+ } else if (arg === "--port" && argv[i + 1]) {
1608
+ const val = Number.parseInt(argv[++i], 10);
1609
+ if (!Number.isInteger(val) || val <= 0 || val > 65535) {
1610
+ logger.error(`Invalid port: ${argv[i]}. Use an integer from 1 to 65535.`);
1611
+ process.exit(1);
1612
+ }
1613
+ port = val;
1614
+ } else if (arg === "--host" && argv[i + 1]) {
1615
+ host = argv[++i];
1375
1616
  } else if (arg === "--scan-only") {
1376
1617
  scanOnly = true;
1377
1618
  } else if (arg === "--debug") {
@@ -1380,7 +1621,7 @@ function parseArgs(argv) {
1380
1621
  configPath = argv[++i];
1381
1622
  }
1382
1623
  }
1383
- return { transport, scanOnly, debug, configPath };
1624
+ return { transport, scope, scanOnly, debug, port, host, configPath };
1384
1625
  }
1385
1626
  function printStartupBanner(skillCount, categoryCount, transportName) {
1386
1627
  logger.raw("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
@@ -1392,9 +1633,9 @@ function printStartupBanner(skillCount, categoryCount, transportName) {
1392
1633
  logger.raw(`\u2502 Transport: ${transportName.padEnd(33)}\u2502`);
1393
1634
  logger.raw("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
1394
1635
  }
1395
- function printConfigStatus() {
1636
+ function printConfigStatus(scope) {
1396
1637
  try {
1397
- const effective = readEffectiveConfig("auto");
1638
+ const effective = readEffectiveConfig(scope);
1398
1639
  if (!effective) {
1399
1640
  logger.warn(
1400
1641
  "cbx_config.json not found. Postman/Stitch tools will return config-not-found errors."
@@ -1431,7 +1672,8 @@ async function main() {
1431
1672
  const serverConfig = loadServerConfig(args.configPath);
1432
1673
  const basePath = path6.resolve(__dirname2, "..");
1433
1674
  const skills = await scanVaultRoots(serverConfig.vault.roots, basePath);
1434
- const manifest = buildManifest(skills);
1675
+ const charsPerToken = serverConfig.telemetry.charsPerToken;
1676
+ const manifest = buildManifest(skills, charsPerToken);
1435
1677
  await enrichWithDescriptions(
1436
1678
  manifest.skills,
1437
1679
  serverConfig.vault.summaryMaxLength
@@ -1446,18 +1688,23 @@ async function main() {
1446
1688
  }
1447
1689
  process.exit(0);
1448
1690
  }
1449
- const transportName = args.transport === "http" ? `Streamable HTTP :${serverConfig.transport.http?.port ?? 3100}` : "stdio";
1691
+ const resolvedHttpPort = args.port ?? serverConfig.transport.http?.port ?? 3100;
1692
+ const transportName = args.transport === "http" ? `Streamable HTTP :${resolvedHttpPort}` : "stdio";
1450
1693
  printStartupBanner(
1451
1694
  manifest.skills.length,
1452
1695
  manifest.categories.length,
1453
1696
  transportName
1454
1697
  );
1455
- printConfigStatus();
1456
- const mcpServer = await createServer({ config: serverConfig, manifest });
1698
+ printConfigStatus(args.scope);
1699
+ const mcpServer = await createServer({
1700
+ config: serverConfig,
1701
+ manifest,
1702
+ defaultConfigScope: args.scope
1703
+ });
1457
1704
  if (args.transport === "http") {
1458
1705
  const httpOpts = {
1459
- port: serverConfig.transport.http?.port ?? 3100,
1460
- host: serverConfig.transport.http?.host ?? "127.0.0.1"
1706
+ port: resolvedHttpPort,
1707
+ host: args.host ?? serverConfig.transport.http?.host ?? "127.0.0.1"
1461
1708
  };
1462
1709
  const { transport, httpServer } = createStreamableHttpTransport(httpOpts);
1463
1710
  await mcpServer.connect(transport);
@@ -40,6 +40,7 @@ describe("config loading", () => {
40
40
  const config = loadServerConfig(configPath);
41
41
  expect(config.server.name).toBe("cubis-foundry-mcp");
42
42
  expect(config.vault.summaryMaxLength).toBe(200);
43
+ expect(config.telemetry.charsPerToken).toBe(4);
43
44
  });
44
45
 
45
46
  it("throws for a missing config file", () => {
@@ -16,6 +16,11 @@ export const ServerConfigSchema = z.object({
16
16
  roots: z.array(z.string()).min(1),
17
17
  summaryMaxLength: z.number().int().positive().default(200),
18
18
  }),
19
+ telemetry: z
20
+ .object({
21
+ charsPerToken: z.number().positive().default(4),
22
+ })
23
+ .default({ charsPerToken: 4 }),
19
24
  transport: z.object({
20
25
  default: z.enum(["stdio", "streamable-http"]).default("stdio"),
21
26
  http: z