Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions tools/mcp-server/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,15 @@ server.tool('loop_estimate_cost', 'Estimate daily token cost for a pattern at a
try {
result = estimateCost({ pattern, level, cadence });
}
catch {
return { content: [{ type: 'text', text: `Invalid cadence: ${cadence ?? pattern.cadence}` }] };
catch (err) {
// estimateCost() can fail for reasons that have nothing to do with
// cadence -- e.g. a registry.yaml entry with a missing/malformed
// `cost` block throws a TypeError reading its fields. Hardcoding
// "Invalid cadence" here regardless of the actual failure misled
// whoever was debugging a broken registry entry. Surface the real
// message instead.
const message = err instanceof Error ? err.message : String(err);
return { content: [{ type: 'text', text: `Cost estimate failed: ${message}` }] };
}
const fmt = (n) => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1_000 ? `${Math.round(n / 1_000)}k` : String(n);
const { scenarios, suggestedDailyCap } = result;
Expand Down
11 changes: 9 additions & 2 deletions tools/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,15 @@ server.tool(
let result;
try {
result = estimateCost({ pattern, level, cadence });
} catch {
return { content: [{ type: 'text' as const, text: `Invalid cadence: ${cadence ?? pattern.cadence}` }] };
} catch (err) {
// estimateCost() can fail for reasons that have nothing to do with
// cadence -- e.g. a registry.yaml entry with a missing/malformed
// `cost` block throws a TypeError reading its fields. Hardcoding
// "Invalid cadence" here regardless of the actual failure misled
// whoever was debugging a broken registry entry. Surface the real
// message instead.
const message = err instanceof Error ? err.message : String(err);
return { content: [{ type: 'text' as const, text: `Cost estimate failed: ${message}` }] };
}

const fmt = (n: number) => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1_000 ? `${Math.round(n / 1_000)}k` : String(n);
Expand Down
53 changes: 53 additions & 0 deletions tools/mcp-server/test/server.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,59 @@ test('loop_estimate_cost accounts for early_exit_required instead of a flat mix'
}
});

test('loop_estimate_cost surfaces the real error for a bad cadence override', async () => {
const root = await setup();
try {
const res = await callServer(root, [{
id: 1, method: 'tools/call',
params: { name: 'loop_estimate_cost', arguments: { patternId: 'daily-triage', level: 'L2', cadence: 'not-a-cadence' } },
}]);
const text = res.get(1).result.content[0].text;
// Propagates loop-cost's own message (e.g. "Invalid cadence interval:
// not") instead of a hardcoded guess.
assert.match(text, /Invalid cadence/i);
} finally {
await cleanup();
}
});

test('loop_estimate_cost does not mislabel a broken registry entry as an invalid cadence', async () => {
// A pattern missing its `cost` block throws a TypeError inside
// estimateCost() that has nothing to do with cadence -- the catch-all
// used to always report "Invalid cadence" regardless of the real cause.
const brokenRoot = await mkdtemp(path.join(tmpdir(), 'mcp-test-broken-registry-'));
try {
await mkdir(path.join(brokenRoot, 'patterns'), { recursive: true });
await writeFile(
path.join(brokenRoot, 'patterns', 'registry.yaml'),
`patterns:
- id: no-cost-block
name: No Cost Block
file: no-cost-block.md
goal: Missing its cost block
cadence: 1d
risk: low
tools: [grok]
skills: []
state: STATE.md
phases: [report]
human_gates: []
starter: starters/minimal-loop
week_one_mode: L1
token_cost: low
`,
);
const res = await callServer(brokenRoot, [{
id: 1, method: 'tools/call',
params: { name: 'loop_estimate_cost', arguments: { patternId: 'no-cost-block', level: 'L2' } },
}]);
const text = res.get(1).result.content[0].text;
assert.ok(!text.includes('Invalid cadence'), `should not blame cadence for a missing cost block: ${text}`);
} finally {
await rm(brokenRoot, { recursive: true, force: true });
}
});

test('pattern resource is readable over stdio', async () => {
const root = await setup();
try {
Expand Down