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:
@@ -143,7 +143,7 @@ function getToolConfigs(): Map<string, ToolConfig> {
|
|||||||
const categoryTools: Record<ToolCategory, string[]> = {
|
const categoryTools: Record<ToolCategory, string[]> = {
|
||||||
global: ['get_current_page', 'navigate_to', 'list_available_tools', 'activate_tool', 'deactivate_tool', 'pin_tool', 'page_refresh'],
|
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'],
|
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'],
|
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'],
|
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'],
|
source: ['get_repo_info', 'list_repo_files', 'read_repo_file', 'search_repo_code'],
|
||||||
|
|||||||
@@ -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[] {
|
export function createComponentHandlers(): ToolConfig[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -152,6 +155,104 @@ export function createComponentHandlers(): ToolConfig[] {
|
|||||||
return `Error: ${e.message}`
|
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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export const ALL_TOOL_METAS: ToolMeta[] = [
|
|||||||
{ name: 'load_vue_component', description: 'Carga y renderiza un componente guardado', category: 'component' },
|
{ name: 'load_vue_component', description: 'Carga y renderiza un componente guardado', category: 'component' },
|
||||||
{ name: 'list_vue_components', description: 'Lista componentes guardados', category: 'component' },
|
{ name: 'list_vue_components', description: 'Lista componentes guardados', category: 'component' },
|
||||||
{ name: 'delete_vue_component', description: 'Elimina un componente', 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
|
// Theme tools
|
||||||
{ name: 'get_design_tokens', description: 'Obtiene los design tokens del tema activo', category: 'theme' },
|
{ name: 'get_design_tokens', description: 'Obtiene los design tokens del tema activo', category: 'theme' },
|
||||||
|
|||||||
Reference in New Issue
Block a user