diff --git a/CHANGELOG.md b/CHANGELOG.md index 995e0d5..68cb7a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ All notable changes to the `agent-root` CLI are documented in this file. Format (keyword-only). One request per failed search; `--all` unaffected. `--json` gains a top-level `approximate` boolean. +### Fixed + +- `resolve` and `install` now render MCP record tool names when a manifest + publishes `tools` as a string array (e.g. `["search", "get_info"]`) — previously + they showed an empty `tools: , , ,` line. A shared `normalizeTools` helper + tolerates both the spec's `[{name, description}]` shape and plain strings. + ## [0.3.0] 2026-05-20 First standalone release. Cut from the independent diff --git a/src/commands/install.ts b/src/commands/install.ts index 45aaecc..5b36fca 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -9,6 +9,7 @@ import { note } from '../cli/streams'; import { RECORD_TYPES } from '../constants/record-types'; import { searchWithFallback, selectResult, promptSearch, type SearchResult } from './search'; import { installSkill } from '../services/install/install-skill'; +import { normalizeTools } from '../utils/normalize-tools'; import type { JsonOut } from '../types/install'; // Re-exported for callers that still import these from this module. @@ -44,9 +45,10 @@ export function installMcp( console.log(`${colors.green('found')} MCP server: ${displayName}\n`); if (record['description']) console.log(` ${record['description'] as string}\n`); - if (Array.isArray(record['tools']) && record['tools'].length > 0) { + const tools = normalizeTools(record['tools']); + if (tools.length > 0) { console.log(` ${colors.dim('Tools:')}`); - for (const t of record['tools'] as Array<{ name: string; description?: string }>) { + for (const t of tools) { console.log(` ${colors.cyan(t.name)}${t.description ? `: ${colors.dim(t.description)}` : ''}`); } console.log(); diff --git a/src/utils/format-record.ts b/src/utils/format-record.ts index d55b274..1cad81c 100644 --- a/src/utils/format-record.ts +++ b/src/utils/format-record.ts @@ -1,5 +1,6 @@ import { colors } from '../cli/colors'; import { RECORD_TYPES } from '../constants/record-types'; +import { normalizeTools } from './normalize-tools'; export function formatRecord(r: Record, indent?: string): string { const ind = indent || ' '; @@ -18,8 +19,9 @@ export function formatRecord(r: Record, indent?: string): strin if (Array.isArray(r.capabilities) && r.capabilities.length > 0) { out += `${ind}${colors.dim('caps:')} ${(r.capabilities as string[]).join(', ')}\n`; } - if (Array.isArray(r.tools) && r.tools.length > 0) { - out += `${ind}${colors.dim('tools:')} ${(r.tools as Array<{ name: string }>).map(t => t.name).join(', ')}\n`; + const tools = normalizeTools(r.tools); + if (tools.length > 0) { + out += `${ind}${colors.dim('tools:')} ${tools.map(t => t.name).join(', ')}\n`; } return out; } diff --git a/src/utils/normalize-tools.ts b/src/utils/normalize-tools.ts new file mode 100644 index 0000000..11208bc --- /dev/null +++ b/src/utils/normalize-tools.ts @@ -0,0 +1,28 @@ +export interface NormalizedTool { + name: string; + description?: string; +} + +/** + * Normalize an mcp record's `tools` into a uniform list of named tools. + * Tolerates the spec shape (`[{ name, description }]`) and the real-world string + * shape (`["search", …]`), plus mixed/malformed arrays. Entries without a usable + * name are dropped; non-array input returns []. + */ +export function normalizeTools(tools: unknown): NormalizedTool[] { + if (!Array.isArray(tools)) return []; + const out: NormalizedTool[] = []; + for (const t of tools) { + if (typeof t === 'string') { + const name = t.trim(); + if (name) out.push({ name }); + } else if (t && typeof t === 'object' && 'name' in t) { + const name = String((t as { name: unknown }).name ?? '').trim(); + if (!name) continue; + const desc = (t as { description?: unknown }).description; + out.push(typeof desc === 'string' && desc ? { name, description: desc } : { name }); + } + // else: skip malformed entries (numbers, null, name-less objects) + } + return out; +} diff --git a/tests/utils/format-record.test.ts b/tests/utils/format-record.test.ts index 1040c01..2b63a0a 100644 --- a/tests/utils/format-record.test.ts +++ b/tests/utils/format-record.test.ts @@ -82,6 +82,17 @@ describe('formatRecord', () => { expect(out).toContain('tool-a, tool-b'); }); + it('renders a string tool array as comma-joined names (regression: tools were blank)', () => { + const out = formatRecord({ + id: 'r', + type: 'mcp', + _domain: 'czarcash.com', + tools: ['search', 'get_info', 'list_pages'], + }); + expect(out).toContain('tools:'); + expect(out).toContain('search, get_info, list_pages'); + }); + it('omits caps line when capabilities array is empty', () => { const out = formatRecord({ id: 'r', type: 'agent', _domain: 'x.com', capabilities: [] }); expect(out).not.toContain('caps:'); diff --git a/tests/utils/normalize-tools.test.ts b/tests/utils/normalize-tools.test.ts new file mode 100644 index 0000000..c8781bc --- /dev/null +++ b/tests/utils/normalize-tools.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest'; +import { normalizeTools } from '../../src/utils/normalize-tools'; + +describe('normalizeTools', () => { + it('maps a string array to named tools', () => { + expect(normalizeTools(['search', 'get_info'])).toEqual([{ name: 'search' }, { name: 'get_info' }]); + }); + + it('passes through {name, description} objects', () => { + expect(normalizeTools([{ name: 'query', description: 'Run SQL' }])).toEqual([{ name: 'query', description: 'Run SQL' }]); + }); + + it('drops description when absent or non-string', () => { + expect(normalizeTools([{ name: 'a' }, { name: 'b', description: 42 }])).toEqual([{ name: 'a' }, { name: 'b' }]); + }); + + it('handles a mixed array (strings + objects)', () => { + expect(normalizeTools(['search', { name: 'query', description: 'q' }])).toEqual([{ name: 'search' }, { name: 'query', description: 'q' }]); + }); + + it('skips name-less objects, numbers, null, and empty/whitespace strings', () => { + expect(normalizeTools([{ description: 'no name' }, 7, null, '', ' ', { name: '' }])).toEqual([]); + }); + + it('trims tool names', () => { + expect(normalizeTools([' search '])).toEqual([{ name: 'search' }]); + }); + + it('returns [] for non-array input', () => { + expect(normalizeTools(undefined)).toEqual([]); + expect(normalizeTools(null)).toEqual([]); + expect(normalizeTools('search')).toEqual([]); + expect(normalizeTools({ name: 'x' })).toEqual([]); + }); +});