@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.
- package/README.md +67 -3
- package/bin/cubis.js +360 -26
- package/mcp/README.md +72 -8
- package/mcp/config.json +3 -0
- package/mcp/dist/index.js +315 -68
- package/mcp/src/config/index.test.ts +1 -0
- package/mcp/src/config/schema.ts +5 -0
- package/mcp/src/index.ts +40 -9
- package/mcp/src/server.ts +66 -10
- package/mcp/src/telemetry/tokenBudget.ts +114 -0
- package/mcp/src/tools/index.ts +7 -0
- package/mcp/src/tools/skillBrowseCategory.ts +22 -5
- package/mcp/src/tools/skillBudgetReport.ts +128 -0
- package/mcp/src/tools/skillGet.ts +18 -0
- package/mcp/src/tools/skillListCategories.ts +19 -6
- package/mcp/src/tools/skillSearch.ts +22 -5
- package/mcp/src/tools/skillTools.test.ts +61 -9
- package/mcp/src/vault/manifest.test.ts +19 -1
- package/mcp/src/vault/manifest.ts +12 -1
- package/mcp/src/vault/scanner.test.ts +1 -0
- package/mcp/src/vault/scanner.ts +1 -0
- package/mcp/src/vault/types.ts +6 -0
- package/package.json +1 -1
- package/workflows/workflows/agent-environment-setup/platforms/antigravity/rules/GEMINI.md +28 -0
- package/workflows/workflows/agent-environment-setup/platforms/codex/rules/AGENTS.md +31 -2
- package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/AGENTS.md +28 -0
- package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/copilot-instructions.md +28 -0
- package/workflows/workflows/agent-environment-setup/platforms/cursor/rules/.cursorrules +28 -0
- package/workflows/workflows/agent-environment-setup/platforms/windsurf/rules/.windsurfrules +28 -0
package/mcp/src/index.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* cubis-mcp # stdio (default)
|
|
7
7
|
* cubis-mcp --transport stdio # explicit stdio
|
|
8
8
|
* cubis-mcp --transport http # Streamable HTTP
|
|
9
|
+
* cubis-mcp --scope global # default config scope for built-in config tools
|
|
10
|
+
* cubis-mcp --host 0.0.0.0 --port 3100
|
|
9
11
|
* cubis-mcp --scan-only # scan vault and exit
|
|
10
12
|
*/
|
|
11
13
|
|
|
@@ -31,13 +33,19 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
31
33
|
|
|
32
34
|
function parseArgs(argv: string[]): {
|
|
33
35
|
transport: "stdio" | "http";
|
|
36
|
+
scope: "auto" | "global" | "project";
|
|
34
37
|
scanOnly: boolean;
|
|
35
38
|
debug: boolean;
|
|
39
|
+
port?: number;
|
|
40
|
+
host?: string;
|
|
36
41
|
configPath?: string;
|
|
37
42
|
} {
|
|
38
43
|
let transport: "stdio" | "http" = "stdio";
|
|
44
|
+
let scope: "auto" | "global" | "project" = "auto";
|
|
39
45
|
let scanOnly = false;
|
|
40
46
|
let debug = false;
|
|
47
|
+
let port: number | undefined;
|
|
48
|
+
let host: string | undefined;
|
|
41
49
|
let configPath: string | undefined;
|
|
42
50
|
|
|
43
51
|
for (let i = 2; i < argv.length; i++) {
|
|
@@ -52,6 +60,23 @@ function parseArgs(argv: string[]): {
|
|
|
52
60
|
logger.error(`Unknown transport: ${val}. Use "stdio" or "http".`);
|
|
53
61
|
process.exit(1);
|
|
54
62
|
}
|
|
63
|
+
} else if (arg === "--scope" && argv[i + 1]) {
|
|
64
|
+
const val = argv[++i];
|
|
65
|
+
if (val === "auto" || val === "global" || val === "project") {
|
|
66
|
+
scope = val;
|
|
67
|
+
} else {
|
|
68
|
+
logger.error(`Unknown scope: ${val}. Use "auto", "global", or "project".`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
} else if (arg === "--port" && argv[i + 1]) {
|
|
72
|
+
const val = Number.parseInt(argv[++i], 10);
|
|
73
|
+
if (!Number.isInteger(val) || val <= 0 || val > 65535) {
|
|
74
|
+
logger.error(`Invalid port: ${argv[i]}. Use an integer from 1 to 65535.`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
port = val;
|
|
78
|
+
} else if (arg === "--host" && argv[i + 1]) {
|
|
79
|
+
host = argv[++i];
|
|
55
80
|
} else if (arg === "--scan-only") {
|
|
56
81
|
scanOnly = true;
|
|
57
82
|
} else if (arg === "--debug") {
|
|
@@ -61,7 +86,7 @@ function parseArgs(argv: string[]): {
|
|
|
61
86
|
}
|
|
62
87
|
}
|
|
63
88
|
|
|
64
|
-
return { transport, scanOnly, debug, configPath };
|
|
89
|
+
return { transport, scope, scanOnly, debug, port, host, configPath };
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
// ─── Startup banner ──────────────────────────────────────────
|
|
@@ -81,9 +106,9 @@ function printStartupBanner(
|
|
|
81
106
|
logger.raw("└──────────────────────────────────────────────┘");
|
|
82
107
|
}
|
|
83
108
|
|
|
84
|
-
function printConfigStatus(): void {
|
|
109
|
+
function printConfigStatus(scope: "auto" | "global" | "project"): void {
|
|
85
110
|
try {
|
|
86
|
-
const effective = readEffectiveConfig(
|
|
111
|
+
const effective = readEffectiveConfig(scope);
|
|
87
112
|
if (!effective) {
|
|
88
113
|
logger.warn(
|
|
89
114
|
"cbx_config.json not found. Postman/Stitch tools will return config-not-found errors.",
|
|
@@ -135,7 +160,8 @@ async function main(): Promise<void> {
|
|
|
135
160
|
// `index.ts` is in `<pkg>/src` during dev and `<pkg>/dist` after build.
|
|
136
161
|
const basePath = path.resolve(__dirname, "..");
|
|
137
162
|
const skills = await scanVaultRoots(serverConfig.vault.roots, basePath);
|
|
138
|
-
const
|
|
163
|
+
const charsPerToken = serverConfig.telemetry.charsPerToken;
|
|
164
|
+
const manifest = buildManifest(skills, charsPerToken);
|
|
139
165
|
|
|
140
166
|
// Enrich with descriptions for faster browse/search at runtime
|
|
141
167
|
await enrichWithDescriptions(
|
|
@@ -156,25 +182,30 @@ async function main(): Promise<void> {
|
|
|
156
182
|
}
|
|
157
183
|
|
|
158
184
|
// Print startup banner
|
|
185
|
+
const resolvedHttpPort = args.port ?? serverConfig.transport.http?.port ?? 3100;
|
|
159
186
|
const transportName =
|
|
160
187
|
args.transport === "http"
|
|
161
|
-
? `Streamable HTTP :${
|
|
188
|
+
? `Streamable HTTP :${resolvedHttpPort}`
|
|
162
189
|
: "stdio";
|
|
163
190
|
printStartupBanner(
|
|
164
191
|
manifest.skills.length,
|
|
165
192
|
manifest.categories.length,
|
|
166
193
|
transportName,
|
|
167
194
|
);
|
|
168
|
-
printConfigStatus();
|
|
195
|
+
printConfigStatus(args.scope);
|
|
169
196
|
|
|
170
197
|
// Create MCP server
|
|
171
|
-
const mcpServer = await createServer({
|
|
198
|
+
const mcpServer = await createServer({
|
|
199
|
+
config: serverConfig,
|
|
200
|
+
manifest,
|
|
201
|
+
defaultConfigScope: args.scope,
|
|
202
|
+
});
|
|
172
203
|
|
|
173
204
|
// Connect transport
|
|
174
205
|
if (args.transport === "http") {
|
|
175
206
|
const httpOpts = {
|
|
176
|
-
port:
|
|
177
|
-
host: serverConfig.transport.http?.host ?? "127.0.0.1",
|
|
207
|
+
port: resolvedHttpPort,
|
|
208
|
+
host: args.host ?? serverConfig.transport.http?.host ?? "127.0.0.1",
|
|
178
209
|
};
|
|
179
210
|
const { transport, httpServer } = createStreamableHttpTransport(httpOpts);
|
|
180
211
|
await mcpServer.connect(transport);
|
package/mcp/src/server.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import type { ServerConfig } from "./config/schema.js";
|
|
11
|
+
import type { ConfigScope } from "./cbxConfig/types.js";
|
|
11
12
|
import type { VaultManifest } from "./vault/types.js";
|
|
12
13
|
import {
|
|
13
14
|
// Skill tools
|
|
@@ -27,6 +28,10 @@ import {
|
|
|
27
28
|
skillGetDescription,
|
|
28
29
|
skillGetSchema,
|
|
29
30
|
handleSkillGet,
|
|
31
|
+
skillBudgetReportName,
|
|
32
|
+
skillBudgetReportDescription,
|
|
33
|
+
skillBudgetReportSchema,
|
|
34
|
+
handleSkillBudgetReport,
|
|
30
35
|
// Postman tools
|
|
31
36
|
postmanGetModeName,
|
|
32
37
|
postmanGetModeDescription,
|
|
@@ -62,6 +67,7 @@ import {
|
|
|
62
67
|
export interface CreateServerOptions {
|
|
63
68
|
config: ServerConfig;
|
|
64
69
|
manifest: VaultManifest;
|
|
70
|
+
defaultConfigScope?: ConfigScope | "auto";
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
function toolCallErrorResult({
|
|
@@ -95,6 +101,7 @@ function toolCallErrorResult({
|
|
|
95
101
|
export async function createServer({
|
|
96
102
|
config,
|
|
97
103
|
manifest,
|
|
104
|
+
defaultConfigScope = "auto",
|
|
98
105
|
}: CreateServerOptions): McpServer {
|
|
99
106
|
const server = new McpServer({
|
|
100
107
|
name: config.server.name,
|
|
@@ -102,6 +109,17 @@ export async function createServer({
|
|
|
102
109
|
});
|
|
103
110
|
|
|
104
111
|
const maxLen = config.vault.summaryMaxLength;
|
|
112
|
+
const charsPerToken = config.telemetry?.charsPerToken ?? 4;
|
|
113
|
+
const withDefaultScope = (
|
|
114
|
+
args: Record<string, unknown> | undefined,
|
|
115
|
+
): Record<string, unknown> => {
|
|
116
|
+
const safeArgs = args ?? {};
|
|
117
|
+
return {
|
|
118
|
+
...safeArgs,
|
|
119
|
+
scope:
|
|
120
|
+
typeof safeArgs.scope === "string" ? safeArgs.scope : defaultConfigScope,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
105
123
|
|
|
106
124
|
// ─── Skill vault tools ───────────────────────────────────────
|
|
107
125
|
|
|
@@ -109,28 +127,36 @@ export async function createServer({
|
|
|
109
127
|
skillListCategoriesName,
|
|
110
128
|
skillListCategoriesDescription,
|
|
111
129
|
skillListCategoriesSchema.shape,
|
|
112
|
-
async () => handleSkillListCategories(manifest),
|
|
130
|
+
async () => handleSkillListCategories(manifest, charsPerToken),
|
|
113
131
|
);
|
|
114
132
|
|
|
115
133
|
server.tool(
|
|
116
134
|
skillBrowseCategoryName,
|
|
117
135
|
skillBrowseCategoryDescription,
|
|
118
136
|
skillBrowseCategorySchema.shape,
|
|
119
|
-
async (args) =>
|
|
137
|
+
async (args) =>
|
|
138
|
+
handleSkillBrowseCategory(args, manifest, maxLen, charsPerToken),
|
|
120
139
|
);
|
|
121
140
|
|
|
122
141
|
server.tool(
|
|
123
142
|
skillSearchName,
|
|
124
143
|
skillSearchDescription,
|
|
125
144
|
skillSearchSchema.shape,
|
|
126
|
-
async (args) => handleSkillSearch(args, manifest, maxLen),
|
|
145
|
+
async (args) => handleSkillSearch(args, manifest, maxLen, charsPerToken),
|
|
127
146
|
);
|
|
128
147
|
|
|
129
148
|
server.tool(
|
|
130
149
|
skillGetName,
|
|
131
150
|
skillGetDescription,
|
|
132
151
|
skillGetSchema.shape,
|
|
133
|
-
async (args) => handleSkillGet(args, manifest),
|
|
152
|
+
async (args) => handleSkillGet(args, manifest, charsPerToken),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
server.tool(
|
|
156
|
+
skillBudgetReportName,
|
|
157
|
+
skillBudgetReportDescription,
|
|
158
|
+
skillBudgetReportSchema.shape,
|
|
159
|
+
async (args) => handleSkillBudgetReport(args, manifest, charsPerToken),
|
|
134
160
|
);
|
|
135
161
|
|
|
136
162
|
// ─── Postman tools ──────────────────────────────────────────
|
|
@@ -139,21 +165,36 @@ export async function createServer({
|
|
|
139
165
|
postmanGetModeName,
|
|
140
166
|
postmanGetModeDescription,
|
|
141
167
|
postmanGetModeSchema.shape,
|
|
142
|
-
async (args) =>
|
|
168
|
+
async (args) =>
|
|
169
|
+
handlePostmanGetMode(
|
|
170
|
+
withDefaultScope(args as Record<string, unknown>) as z.infer<
|
|
171
|
+
typeof postmanGetModeSchema
|
|
172
|
+
>,
|
|
173
|
+
),
|
|
143
174
|
);
|
|
144
175
|
|
|
145
176
|
server.tool(
|
|
146
177
|
postmanSetModeName,
|
|
147
178
|
postmanSetModeDescription,
|
|
148
179
|
postmanSetModeSchema.shape,
|
|
149
|
-
async (args) =>
|
|
180
|
+
async (args) =>
|
|
181
|
+
handlePostmanSetMode(
|
|
182
|
+
withDefaultScope(args as Record<string, unknown>) as z.infer<
|
|
183
|
+
typeof postmanSetModeSchema
|
|
184
|
+
>,
|
|
185
|
+
),
|
|
150
186
|
);
|
|
151
187
|
|
|
152
188
|
server.tool(
|
|
153
189
|
postmanGetStatusName,
|
|
154
190
|
postmanGetStatusDescription,
|
|
155
191
|
postmanGetStatusSchema.shape,
|
|
156
|
-
async (args) =>
|
|
192
|
+
async (args) =>
|
|
193
|
+
handlePostmanGetStatus(
|
|
194
|
+
withDefaultScope(args as Record<string, unknown>) as z.infer<
|
|
195
|
+
typeof postmanGetStatusSchema
|
|
196
|
+
>,
|
|
197
|
+
),
|
|
157
198
|
);
|
|
158
199
|
|
|
159
200
|
// ─── Stitch tools ──────────────────────────────────────────
|
|
@@ -162,21 +203,36 @@ export async function createServer({
|
|
|
162
203
|
stitchGetModeName,
|
|
163
204
|
stitchGetModeDescription,
|
|
164
205
|
stitchGetModeSchema.shape,
|
|
165
|
-
async (args) =>
|
|
206
|
+
async (args) =>
|
|
207
|
+
handleStitchGetMode(
|
|
208
|
+
withDefaultScope(args as Record<string, unknown>) as z.infer<
|
|
209
|
+
typeof stitchGetModeSchema
|
|
210
|
+
>,
|
|
211
|
+
),
|
|
166
212
|
);
|
|
167
213
|
|
|
168
214
|
server.tool(
|
|
169
215
|
stitchSetProfileName,
|
|
170
216
|
stitchSetProfileDescription,
|
|
171
217
|
stitchSetProfileSchema.shape,
|
|
172
|
-
async (args) =>
|
|
218
|
+
async (args) =>
|
|
219
|
+
handleStitchSetProfile(
|
|
220
|
+
withDefaultScope(args as Record<string, unknown>) as z.infer<
|
|
221
|
+
typeof stitchSetProfileSchema
|
|
222
|
+
>,
|
|
223
|
+
),
|
|
173
224
|
);
|
|
174
225
|
|
|
175
226
|
server.tool(
|
|
176
227
|
stitchGetStatusName,
|
|
177
228
|
stitchGetStatusDescription,
|
|
178
229
|
stitchGetStatusSchema.shape,
|
|
179
|
-
async (args) =>
|
|
230
|
+
async (args) =>
|
|
231
|
+
handleStitchGetStatus(
|
|
232
|
+
withDefaultScope(args as Record<string, unknown>) as z.infer<
|
|
233
|
+
typeof stitchGetStatusSchema
|
|
234
|
+
>,
|
|
235
|
+
),
|
|
180
236
|
);
|
|
181
237
|
|
|
182
238
|
// ─── Dynamic upstream passthrough tools ────────────────────
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cubis Foundry MCP Server – deterministic token budget estimator.
|
|
3
|
+
*
|
|
4
|
+
* Estimates tokens by character count using a configurable chars/token ratio.
|
|
5
|
+
* This is intentionally model-agnostic and marked as estimated.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const TOKEN_ESTIMATOR_VERSION = "char-estimator-v1";
|
|
9
|
+
|
|
10
|
+
export function normalizeCharsPerToken(value: number): number {
|
|
11
|
+
if (!Number.isFinite(value) || value <= 0) return 4;
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function estimateTokensFromCharCount(
|
|
16
|
+
charCount: number,
|
|
17
|
+
charsPerToken: number,
|
|
18
|
+
): number {
|
|
19
|
+
const safeChars = Math.max(0, Math.ceil(charCount));
|
|
20
|
+
const ratio = normalizeCharsPerToken(charsPerToken);
|
|
21
|
+
return Math.ceil(safeChars / ratio);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function estimateTokensFromText(
|
|
25
|
+
text: string,
|
|
26
|
+
charsPerToken: number,
|
|
27
|
+
): number {
|
|
28
|
+
return estimateTokensFromCharCount(text.length, charsPerToken);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Byte-size estimation is used for pointer-only operations where file content
|
|
33
|
+
* is intentionally not loaded.
|
|
34
|
+
*/
|
|
35
|
+
export function estimateTokensFromBytes(
|
|
36
|
+
byteCount: number,
|
|
37
|
+
charsPerToken: number,
|
|
38
|
+
): number {
|
|
39
|
+
return estimateTokensFromCharCount(byteCount, charsPerToken);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function estimateSavings(
|
|
43
|
+
fullCatalogEstimatedTokens: number,
|
|
44
|
+
usedEstimatedTokens: number,
|
|
45
|
+
): { estimatedSavingsTokens: number; estimatedSavingsPercent: number } {
|
|
46
|
+
const full = Math.max(0, Math.ceil(fullCatalogEstimatedTokens));
|
|
47
|
+
const used = Math.max(0, Math.ceil(usedEstimatedTokens));
|
|
48
|
+
if (full <= 0) {
|
|
49
|
+
return {
|
|
50
|
+
estimatedSavingsTokens: 0,
|
|
51
|
+
estimatedSavingsPercent: 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const estimatedSavingsTokens = Math.max(0, full - used);
|
|
56
|
+
const estimatedSavingsPercent = Number(
|
|
57
|
+
((estimatedSavingsTokens / full) * 100).toFixed(2),
|
|
58
|
+
);
|
|
59
|
+
return {
|
|
60
|
+
estimatedSavingsTokens,
|
|
61
|
+
estimatedSavingsPercent,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SkillToolMetrics {
|
|
66
|
+
estimatorVersion: string;
|
|
67
|
+
charsPerToken: number;
|
|
68
|
+
fullCatalogEstimatedTokens: number;
|
|
69
|
+
responseEstimatedTokens: number;
|
|
70
|
+
selectedSkillsEstimatedTokens: number | null;
|
|
71
|
+
loadedSkillEstimatedTokens: number | null;
|
|
72
|
+
estimatedSavingsVsFullCatalog: number;
|
|
73
|
+
estimatedSavingsVsFullCatalogPercent: number;
|
|
74
|
+
estimated: true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildSkillToolMetrics({
|
|
78
|
+
charsPerToken,
|
|
79
|
+
fullCatalogEstimatedTokens,
|
|
80
|
+
responseEstimatedTokens,
|
|
81
|
+
selectedSkillsEstimatedTokens = null,
|
|
82
|
+
loadedSkillEstimatedTokens = null,
|
|
83
|
+
}: {
|
|
84
|
+
charsPerToken: number;
|
|
85
|
+
fullCatalogEstimatedTokens: number;
|
|
86
|
+
responseEstimatedTokens: number;
|
|
87
|
+
selectedSkillsEstimatedTokens?: number | null;
|
|
88
|
+
loadedSkillEstimatedTokens?: number | null;
|
|
89
|
+
}): SkillToolMetrics {
|
|
90
|
+
const usedEstimatedTokens =
|
|
91
|
+
loadedSkillEstimatedTokens ??
|
|
92
|
+
selectedSkillsEstimatedTokens ??
|
|
93
|
+
responseEstimatedTokens;
|
|
94
|
+
const savings = estimateSavings(fullCatalogEstimatedTokens, usedEstimatedTokens);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
estimatorVersion: TOKEN_ESTIMATOR_VERSION,
|
|
98
|
+
charsPerToken: normalizeCharsPerToken(charsPerToken),
|
|
99
|
+
fullCatalogEstimatedTokens: Math.max(0, fullCatalogEstimatedTokens),
|
|
100
|
+
responseEstimatedTokens: Math.max(0, responseEstimatedTokens),
|
|
101
|
+
selectedSkillsEstimatedTokens:
|
|
102
|
+
selectedSkillsEstimatedTokens === null
|
|
103
|
+
? null
|
|
104
|
+
: Math.max(0, selectedSkillsEstimatedTokens),
|
|
105
|
+
loadedSkillEstimatedTokens:
|
|
106
|
+
loadedSkillEstimatedTokens === null
|
|
107
|
+
? null
|
|
108
|
+
: Math.max(0, loadedSkillEstimatedTokens),
|
|
109
|
+
estimatedSavingsVsFullCatalog: savings.estimatedSavingsTokens,
|
|
110
|
+
estimatedSavingsVsFullCatalogPercent: savings.estimatedSavingsPercent,
|
|
111
|
+
estimated: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
package/mcp/src/tools/index.ts
CHANGED
|
@@ -33,6 +33,13 @@ export {
|
|
|
33
33
|
handleSkillGet,
|
|
34
34
|
} from "./skillGet.js";
|
|
35
35
|
|
|
36
|
+
export {
|
|
37
|
+
skillBudgetReportName,
|
|
38
|
+
skillBudgetReportDescription,
|
|
39
|
+
skillBudgetReportSchema,
|
|
40
|
+
handleSkillBudgetReport,
|
|
41
|
+
} from "./skillBudgetReport.js";
|
|
42
|
+
|
|
36
43
|
export {
|
|
37
44
|
postmanGetModeName,
|
|
38
45
|
postmanGetModeDescription,
|
|
@@ -8,6 +8,11 @@ import { z } from "zod";
|
|
|
8
8
|
import type { VaultManifest } from "../vault/types.js";
|
|
9
9
|
import { enrichWithDescriptions } from "../vault/manifest.js";
|
|
10
10
|
import { notFound } from "../utils/errors.js";
|
|
11
|
+
import {
|
|
12
|
+
buildSkillToolMetrics,
|
|
13
|
+
estimateTokensFromBytes,
|
|
14
|
+
estimateTokensFromText,
|
|
15
|
+
} from "../telemetry/tokenBudget.js";
|
|
11
16
|
|
|
12
17
|
export const skillBrowseCategoryName = "skill_browse_category";
|
|
13
18
|
|
|
@@ -24,6 +29,7 @@ export async function handleSkillBrowseCategory(
|
|
|
24
29
|
args: z.infer<typeof skillBrowseCategorySchema>,
|
|
25
30
|
manifest: VaultManifest,
|
|
26
31
|
summaryMaxLength: number,
|
|
32
|
+
charsPerToken: number,
|
|
27
33
|
) {
|
|
28
34
|
const { category } = args;
|
|
29
35
|
|
|
@@ -38,17 +44,28 @@ export async function handleSkillBrowseCategory(
|
|
|
38
44
|
id: s.id,
|
|
39
45
|
description: s.description ?? "(no description)",
|
|
40
46
|
}));
|
|
47
|
+
const payload = { category, skills, count: skills.length };
|
|
48
|
+
const text = JSON.stringify(payload, null, 2);
|
|
49
|
+
const selectedSkillsEstimatedTokens = matching.reduce(
|
|
50
|
+
(sum, skill) => sum + estimateTokensFromBytes(skill.fileBytes, charsPerToken),
|
|
51
|
+
0,
|
|
52
|
+
);
|
|
53
|
+
const metrics = buildSkillToolMetrics({
|
|
54
|
+
charsPerToken,
|
|
55
|
+
fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
|
|
56
|
+
responseEstimatedTokens: estimateTokensFromText(text, charsPerToken),
|
|
57
|
+
selectedSkillsEstimatedTokens,
|
|
58
|
+
});
|
|
41
59
|
|
|
42
60
|
return {
|
|
43
61
|
content: [
|
|
44
62
|
{
|
|
45
63
|
type: "text" as const,
|
|
46
|
-
text
|
|
47
|
-
{ category, skills, count: skills.length },
|
|
48
|
-
null,
|
|
49
|
-
2,
|
|
50
|
-
),
|
|
64
|
+
text,
|
|
51
65
|
},
|
|
52
66
|
],
|
|
67
|
+
structuredContent: {
|
|
68
|
+
metrics,
|
|
69
|
+
},
|
|
53
70
|
};
|
|
54
71
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cubis Foundry MCP Server – skill_budget_report tool.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates selected/loaded skill IDs into a deterministic context budget
|
|
5
|
+
* summary using estimated token counts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import type { VaultManifest } from "../vault/types.js";
|
|
10
|
+
import {
|
|
11
|
+
TOKEN_ESTIMATOR_VERSION,
|
|
12
|
+
estimateSavings,
|
|
13
|
+
estimateTokensFromBytes,
|
|
14
|
+
} from "../telemetry/tokenBudget.js";
|
|
15
|
+
|
|
16
|
+
export const skillBudgetReportName = "skill_budget_report";
|
|
17
|
+
|
|
18
|
+
export const skillBudgetReportDescription =
|
|
19
|
+
"Report estimated context/token budget for selected and loaded skills compared to the full skill catalog.";
|
|
20
|
+
|
|
21
|
+
export const skillBudgetReportSchema = z.object({
|
|
22
|
+
selectedSkillIds: z
|
|
23
|
+
.array(z.string())
|
|
24
|
+
.default([])
|
|
25
|
+
.describe("Skill IDs selected after search/browse."),
|
|
26
|
+
loadedSkillIds: z
|
|
27
|
+
.array(z.string())
|
|
28
|
+
.default([])
|
|
29
|
+
.describe("Skill IDs loaded via skill_get."),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function uniqueStrings(values: string[]): string[] {
|
|
33
|
+
return [...new Set(values.map((value) => String(value)))];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function handleSkillBudgetReport(
|
|
37
|
+
args: z.infer<typeof skillBudgetReportSchema>,
|
|
38
|
+
manifest: VaultManifest,
|
|
39
|
+
charsPerToken: number,
|
|
40
|
+
) {
|
|
41
|
+
const selectedSkillIds = uniqueStrings(args.selectedSkillIds ?? []);
|
|
42
|
+
const loadedSkillIds = uniqueStrings(args.loadedSkillIds ?? []);
|
|
43
|
+
const skillById = new Map(manifest.skills.map((skill) => [skill.id, skill]));
|
|
44
|
+
|
|
45
|
+
const selectedSkills = selectedSkillIds
|
|
46
|
+
.map((id) => {
|
|
47
|
+
const skill = skillById.get(id);
|
|
48
|
+
if (!skill) return null;
|
|
49
|
+
return {
|
|
50
|
+
id: skill.id,
|
|
51
|
+
category: skill.category,
|
|
52
|
+
estimatedTokens: estimateTokensFromBytes(skill.fileBytes, charsPerToken),
|
|
53
|
+
};
|
|
54
|
+
})
|
|
55
|
+
.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
|
56
|
+
|
|
57
|
+
const loadedSkills = loadedSkillIds
|
|
58
|
+
.map((id) => {
|
|
59
|
+
const skill = skillById.get(id);
|
|
60
|
+
if (!skill) return null;
|
|
61
|
+
return {
|
|
62
|
+
id: skill.id,
|
|
63
|
+
category: skill.category,
|
|
64
|
+
estimatedTokens: estimateTokensFromBytes(skill.fileBytes, charsPerToken),
|
|
65
|
+
};
|
|
66
|
+
})
|
|
67
|
+
.filter((item): item is NonNullable<typeof item> => Boolean(item));
|
|
68
|
+
|
|
69
|
+
const unknownSelectedSkillIds = selectedSkillIds.filter(
|
|
70
|
+
(id) => !skillById.has(id),
|
|
71
|
+
);
|
|
72
|
+
const unknownLoadedSkillIds = loadedSkillIds.filter((id) => !skillById.has(id));
|
|
73
|
+
|
|
74
|
+
const selectedSkillsEstimatedTokens = selectedSkills.reduce(
|
|
75
|
+
(sum, skill) => sum + skill.estimatedTokens,
|
|
76
|
+
0,
|
|
77
|
+
);
|
|
78
|
+
const loadedSkillsEstimatedTokens = loadedSkills.reduce(
|
|
79
|
+
(sum, skill) => sum + skill.estimatedTokens,
|
|
80
|
+
0,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const usedEstimatedTokens =
|
|
84
|
+
loadedSkills.length > 0
|
|
85
|
+
? loadedSkillsEstimatedTokens
|
|
86
|
+
: selectedSkillsEstimatedTokens;
|
|
87
|
+
const savings = estimateSavings(
|
|
88
|
+
manifest.fullCatalogEstimatedTokens,
|
|
89
|
+
usedEstimatedTokens,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const selectedIdSet = new Set(selectedSkills.map((skill) => skill.id));
|
|
93
|
+
const loadedIdSet = new Set(loadedSkills.map((skill) => skill.id));
|
|
94
|
+
const skippedSkills = manifest.skills
|
|
95
|
+
.filter((skill) => !selectedIdSet.has(skill.id) && !loadedIdSet.has(skill.id))
|
|
96
|
+
.map((skill) => skill.id)
|
|
97
|
+
.sort((a, b) => a.localeCompare(b));
|
|
98
|
+
|
|
99
|
+
const payload = {
|
|
100
|
+
skillLog: {
|
|
101
|
+
selectedSkills,
|
|
102
|
+
loadedSkills,
|
|
103
|
+
skippedSkills,
|
|
104
|
+
unknownSelectedSkillIds,
|
|
105
|
+
unknownLoadedSkillIds,
|
|
106
|
+
},
|
|
107
|
+
contextBudget: {
|
|
108
|
+
estimatorVersion: TOKEN_ESTIMATOR_VERSION,
|
|
109
|
+
charsPerToken,
|
|
110
|
+
fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
|
|
111
|
+
selectedSkillsEstimatedTokens,
|
|
112
|
+
loadedSkillsEstimatedTokens,
|
|
113
|
+
estimatedSavingsTokens: savings.estimatedSavingsTokens,
|
|
114
|
+
estimatedSavingsPercent: savings.estimatedSavingsPercent,
|
|
115
|
+
estimated: true,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text" as const,
|
|
123
|
+
text: JSON.stringify(payload, null, 2),
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
structuredContent: payload,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -9,6 +9,10 @@ import { z } from "zod";
|
|
|
9
9
|
import type { VaultManifest } from "../vault/types.js";
|
|
10
10
|
import { readFullSkillContent } from "../vault/manifest.js";
|
|
11
11
|
import { notFound } from "../utils/errors.js";
|
|
12
|
+
import {
|
|
13
|
+
buildSkillToolMetrics,
|
|
14
|
+
estimateTokensFromText,
|
|
15
|
+
} from "../telemetry/tokenBudget.js";
|
|
12
16
|
|
|
13
17
|
export const skillGetName = "skill_get";
|
|
14
18
|
|
|
@@ -22,6 +26,7 @@ export const skillGetSchema = z.object({
|
|
|
22
26
|
export async function handleSkillGet(
|
|
23
27
|
args: z.infer<typeof skillGetSchema>,
|
|
24
28
|
manifest: VaultManifest,
|
|
29
|
+
charsPerToken: number,
|
|
25
30
|
) {
|
|
26
31
|
const { id } = args;
|
|
27
32
|
|
|
@@ -31,6 +36,16 @@ export async function handleSkillGet(
|
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
const content = await readFullSkillContent(skill.path);
|
|
39
|
+
const loadedSkillEstimatedTokens = estimateTokensFromText(
|
|
40
|
+
content,
|
|
41
|
+
charsPerToken,
|
|
42
|
+
);
|
|
43
|
+
const metrics = buildSkillToolMetrics({
|
|
44
|
+
charsPerToken,
|
|
45
|
+
fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
|
|
46
|
+
responseEstimatedTokens: loadedSkillEstimatedTokens,
|
|
47
|
+
loadedSkillEstimatedTokens,
|
|
48
|
+
});
|
|
34
49
|
|
|
35
50
|
return {
|
|
36
51
|
content: [
|
|
@@ -39,5 +54,8 @@ export async function handleSkillGet(
|
|
|
39
54
|
text: content,
|
|
40
55
|
},
|
|
41
56
|
],
|
|
57
|
+
structuredContent: {
|
|
58
|
+
metrics,
|
|
59
|
+
},
|
|
42
60
|
};
|
|
43
61
|
}
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import type { VaultManifest } from "../vault/types.js";
|
|
9
|
+
import {
|
|
10
|
+
buildSkillToolMetrics,
|
|
11
|
+
estimateTokensFromText,
|
|
12
|
+
} from "../telemetry/tokenBudget.js";
|
|
9
13
|
|
|
10
14
|
export const skillListCategoriesName = "skill_list_categories";
|
|
11
15
|
|
|
@@ -14,7 +18,10 @@ export const skillListCategoriesDescription =
|
|
|
14
18
|
|
|
15
19
|
export const skillListCategoriesSchema = z.object({});
|
|
16
20
|
|
|
17
|
-
export function handleSkillListCategories(
|
|
21
|
+
export function handleSkillListCategories(
|
|
22
|
+
manifest: VaultManifest,
|
|
23
|
+
charsPerToken: number,
|
|
24
|
+
) {
|
|
18
25
|
const categoryCounts: Record<string, number> = {};
|
|
19
26
|
for (const skill of manifest.skills) {
|
|
20
27
|
categoryCounts[skill.category] = (categoryCounts[skill.category] ?? 0) + 1;
|
|
@@ -24,17 +31,23 @@ export function handleSkillListCategories(manifest: VaultManifest) {
|
|
|
24
31
|
category: cat,
|
|
25
32
|
skillCount: categoryCounts[cat] ?? 0,
|
|
26
33
|
}));
|
|
34
|
+
const payload = { categories, totalSkills: manifest.skills.length };
|
|
35
|
+
const text = JSON.stringify(payload, null, 2);
|
|
36
|
+
const metrics = buildSkillToolMetrics({
|
|
37
|
+
charsPerToken,
|
|
38
|
+
fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
|
|
39
|
+
responseEstimatedTokens: estimateTokensFromText(text, charsPerToken),
|
|
40
|
+
});
|
|
27
41
|
|
|
28
42
|
return {
|
|
29
43
|
content: [
|
|
30
44
|
{
|
|
31
45
|
type: "text" as const,
|
|
32
|
-
text
|
|
33
|
-
{ categories, totalSkills: manifest.skills.length },
|
|
34
|
-
null,
|
|
35
|
-
2,
|
|
36
|
-
),
|
|
46
|
+
text,
|
|
37
47
|
},
|
|
38
48
|
],
|
|
49
|
+
structuredContent: {
|
|
50
|
+
metrics,
|
|
51
|
+
},
|
|
39
52
|
};
|
|
40
53
|
}
|