feat: Add read_component and edit_component MCP tools

Surgical read/edit tools for saved Vue components, avoiding full
rewrites via save_vue_component. edit_component supports replace_all
for non-unique strings. Token-optimized schemas and responses.
This commit is contained in:
2026-02-17 02:33:35 -06:00
parent 0a9fcc467f
commit c0e616212d
3 changed files with 104 additions and 1 deletions

View File

@@ -143,7 +143,7 @@ function getToolConfigs(): Map<string, ToolConfig> {
const categoryTools: Record<ToolCategory, string[]> = {
global: ['get_current_page', 'navigate_to', 'list_available_tools', 'activate_tool', 'deactivate_tool', 'pin_tool', 'page_refresh'],
canvas: ['render_html', 'render_vue_component', 'move_window', 'resize_window', 'close_window', 'list_windows', 'inspect_window', 'get_canvas', 'edit_canvas', 'canvas_css', 'canvas_js', 'get_canvas_css', 'save_canvas_snapshot', 'load_canvas_snapshot', 'list_canvas_snapshots', 'delete_canvas_snapshot'],
component: ['save_vue_component', 'load_vue_component', 'list_vue_components', 'delete_vue_component'],
component: ['save_vue_component', 'load_vue_component', 'list_vue_components', 'delete_vue_component', 'read_component', 'edit_component'],
theme: ['get_design_tokens', 'get_active_theme', 'set_theme_variable', 'save_theme', 'list_themes', 'switch_theme', 'reset_theme'],
database: ['list_tables', 'get_table_schema', 'get_table_data', 'get_database_stats', 'execute_query'],
source: ['get_repo_info', 'list_repo_files', 'read_repo_file', 'search_repo_code'],

View File

@@ -19,6 +19,9 @@ function removePlaceholder(container: HTMLElement) {
}
}
// Track which component fields have been read (for edit validation)
const readComponents = new Set<string>() // format: "component_id:field"
export function createComponentHandlers(): ToolConfig[] {
return [
{
@@ -152,6 +155,104 @@ export function createComponentHandlers(): ToolConfig[] {
return `Error: ${e.message}`
}
}
},
{
name: 'read_component',
description: 'Lee campos especificos de un componente guardado',
category: 'component',
schema: {
type: 'object',
properties: {
id: { type: 'string', description: 'ID del componente' },
fields: {
type: 'array',
items: { type: 'string', enum: ['template', 'setup', 'style', 'props', 'imports'] },
description: 'Campos a leer (default: template, setup, style)'
}
},
required: ['id']
},
handler: async (args: { id: string; fields?: string[] }) => {
try {
const definition = await componentsApi.getById(args.id)
if (!definition) return `Error: "${args.id}" not found`
const fields = args.fields || ['template', 'setup', 'style']
const validFields = ['template', 'setup', 'style', 'props', 'imports'] as const
const output: string[] = []
for (const field of fields) {
if (!validFields.includes(field as any)) continue
const value = definition[field as keyof typeof definition]
readComponents.add(`${args.id}:${field}`)
if (field === 'props' || field === 'imports') {
const arr = value as string[] | undefined
output.push(`--- ${field} ---\n${arr?.length ? JSON.stringify(arr) : '(empty)'}`)
} else {
const str = (value as string) || ''
output.push(`--- ${field} (${str.length}) ---\n${str || '(empty)'}`)
}
}
return output.join('\n\n')
} catch (e: any) {
return `Error: ${e.message}`
}
}
},
{
name: 'edit_component',
description: 'Edita un campo de componente con reemplazo de strings (requiere read_component previo)',
category: 'component',
schema: {
type: 'object',
properties: {
id: { type: 'string', description: 'ID del componente' },
field: { type: 'string', enum: ['template', 'setup', 'style'], description: 'Campo a editar' },
old_string: { type: 'string', description: 'Texto a reemplazar (debe ser unico en el campo)' },
new_string: { type: 'string', description: 'Texto de reemplazo' },
replace_all: { type: 'boolean', description: 'Reemplazar todas las ocurrencias (default: false)' }
},
required: ['id', 'field', 'old_string', 'new_string']
},
handler: async (args: { id: string; field: string; old_string: string; new_string: string; replace_all?: boolean }) => {
try {
if (!readComponents.has(`${args.id}:${args.field}`)) {
return `Error: read_component "${args.id}" "${args.field}" first`
}
const definition = await componentsApi.getById(args.id)
if (!definition) return `Error: "${args.id}" not found`
const currentValue = (definition[args.field as keyof typeof definition] as string) || ''
let count = 0
let pos = 0
while ((pos = currentValue.indexOf(args.old_string, pos)) !== -1) {
count++
pos += args.old_string.length
}
if (count === 0) return `Error: old_string not found in ${args.field}`
if (count > 1 && !args.replace_all) {
return `Error: ${count} matches found. Use replace_all:true or add context.`
}
const newValue = args.replace_all
? currentValue.replaceAll(args.old_string, args.new_string)
: currentValue.replace(args.old_string, args.new_string)
await componentsApi.update(args.id, { [args.field]: newValue })
readComponents.add(`${args.id}:${args.field}`)
return count > 1
? `OK ${args.field} ${currentValue.length}${newValue.length} (${count} replaced)`
: `OK ${args.field} ${currentValue.length}${newValue.length}`
} catch (e: any) {
return `Error: ${e.message}`
}
}
}
]
}

View File

@@ -40,6 +40,8 @@ export const ALL_TOOL_METAS: ToolMeta[] = [
{ name: 'load_vue_component', description: 'Carga y renderiza un componente guardado', category: 'component' },
{ name: 'list_vue_components', description: 'Lista componentes guardados', category: 'component' },
{ name: 'delete_vue_component', description: 'Elimina un componente', category: 'component' },
{ name: 'read_component', description: 'Lee campos especificos de un componente guardado', category: 'component' },
{ name: 'edit_component', description: 'Edita un campo de componente con reemplazo de strings (requiere lectura previa)', category: 'component' },
// Theme tools
{ name: 'get_design_tokens', description: 'Obtiene los design tokens del tema activo', category: 'theme' },