feat: Add database explorer page with SQLite management
- Add /database page with table explorer, query executor and stats - Implement MCP tools: list_tables, get_table_schema, get_table_data, get_database_stats, execute_query - Add database API endpoints with security (SELECT only) - Add database icon to toolbar
This commit is contained in:
@@ -24,8 +24,13 @@ import {
|
||||
unregisterProjectCanvasTools,
|
||||
PROJECT_CANVAS_TOOLS
|
||||
} from './tools/projectCanvasTools'
|
||||
import {
|
||||
registerDatabaseTools,
|
||||
unregisterDatabaseTools,
|
||||
DATABASE_TOOLS
|
||||
} from './tools/databaseTools'
|
||||
|
||||
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas'
|
||||
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database'
|
||||
|
||||
interface PageToolSet {
|
||||
register: () => void
|
||||
@@ -85,6 +90,11 @@ const pageTools: Record<PageName, PageToolSet> = {
|
||||
register: registerThemeTools,
|
||||
unregister: unregisterThemeTools,
|
||||
toolNames: THEME_TOOLS
|
||||
},
|
||||
database: {
|
||||
register: registerDatabaseTools,
|
||||
unregister: unregisterDatabaseTools,
|
||||
toolNames: DATABASE_TOOLS
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
231
frontend/src/services/tools/databaseTools.ts
Normal file
231
frontend/src/services/tools/databaseTools.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { registerTool, unregisterTools } from '../webmcp'
|
||||
|
||||
export const DATABASE_TOOLS = [
|
||||
'list_tables',
|
||||
'get_table_schema',
|
||||
'get_table_data',
|
||||
'get_database_stats',
|
||||
'execute_query'
|
||||
]
|
||||
|
||||
const API_BASE = 'http://localhost:4101'
|
||||
|
||||
export function registerDatabaseTools() {
|
||||
// list_tables
|
||||
registerTool(
|
||||
'list_tables',
|
||||
'Lista todas las tablas de la base de datos SQLite con su conteo de registros',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {}
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/database/tables`)
|
||||
if (!res.ok) throw new Error('Failed to fetch tables')
|
||||
const tables = await res.json()
|
||||
|
||||
if (tables.length === 0) {
|
||||
return 'No hay tablas en la base de datos'
|
||||
}
|
||||
|
||||
const tableList = tables.map((t: any) => ` - ${t.name}: ${t.count} registros`).join('\n')
|
||||
const total = tables.reduce((sum: number, t: any) => sum + t.count, 0)
|
||||
|
||||
return `Tablas en la base de datos (${tables.length}):\n\n${tableList}\n\nTotal de registros: ${total}`
|
||||
} catch (e: any) {
|
||||
return `Error: ${e.message}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// get_table_schema
|
||||
registerTool(
|
||||
'get_table_schema',
|
||||
'Obtiene el esquema (columnas y tipos) de una tabla',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'string',
|
||||
description: 'Nombre de la tabla'
|
||||
}
|
||||
},
|
||||
required: ['table']
|
||||
},
|
||||
async (args: { table: string }) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/database/tables/${args.table}/schema`)
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) return `Tabla "${args.table}" no encontrada`
|
||||
throw new Error('Failed to fetch schema')
|
||||
}
|
||||
const schema = await res.json()
|
||||
|
||||
if (schema.length === 0) {
|
||||
return `La tabla "${args.table}" no tiene columnas definidas`
|
||||
}
|
||||
|
||||
const columns = schema.map((col: any) => {
|
||||
const flags = []
|
||||
if (col.pk) flags.push('PRIMARY KEY')
|
||||
if (col.notnull) flags.push('NOT NULL')
|
||||
const flagStr = flags.length > 0 ? ` (${flags.join(', ')})` : ''
|
||||
return ` - ${col.name}: ${col.type}${flagStr}`
|
||||
}).join('\n')
|
||||
|
||||
return `Esquema de la tabla "${args.table}":\n\n${columns}`
|
||||
} catch (e: any) {
|
||||
return `Error: ${e.message}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// get_table_data
|
||||
registerTool(
|
||||
'get_table_data',
|
||||
'Obtiene los datos de una tabla con paginacion',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'string',
|
||||
description: 'Nombre de la tabla'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Numero de registros a retornar (default: 20, max: 100)'
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'Registros a saltar para paginacion (default: 0)'
|
||||
}
|
||||
},
|
||||
required: ['table']
|
||||
},
|
||||
async (args: { table: string; limit?: number; offset?: number }) => {
|
||||
try {
|
||||
const limit = Math.min(args.limit || 20, 100)
|
||||
const offset = args.offset || 0
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/database/tables/${args.table}/data?limit=${limit}&offset=${offset}`
|
||||
)
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) return `Tabla "${args.table}" no encontrada`
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
const result = await res.json()
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return `La tabla "${args.table}" no tiene registros`
|
||||
}
|
||||
|
||||
// Format as readable table
|
||||
const rows = result.rows.map((row: any, idx: number) => {
|
||||
const entries = Object.entries(row).map(([k, v]) => {
|
||||
let value = v
|
||||
if (typeof v === 'string' && v.length > 50) {
|
||||
value = v.substring(0, 50) + '...'
|
||||
} else if (typeof v === 'object') {
|
||||
value = JSON.stringify(v).substring(0, 50) + '...'
|
||||
}
|
||||
return `${k}: ${value}`
|
||||
}).join(', ')
|
||||
return `[${offset + idx + 1}] ${entries}`
|
||||
}).join('\n')
|
||||
|
||||
return `Datos de "${args.table}" (${offset + 1}-${offset + result.rows.length} de ${result.total}):\n\n${rows}\n\nUsa offset=${offset + limit} para ver mas registros`
|
||||
} catch (e: any) {
|
||||
return `Error: ${e.message}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// get_database_stats
|
||||
registerTool(
|
||||
'get_database_stats',
|
||||
'Obtiene estadisticas generales de la base de datos',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {}
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/database/stats`)
|
||||
if (!res.ok) throw new Error('Failed to fetch stats')
|
||||
const stats = await res.json()
|
||||
|
||||
return `Estadisticas de la base de datos:\n\n` +
|
||||
` Tamano: ${stats.size}\n` +
|
||||
` Tablas: ${stats.tables}\n` +
|
||||
` Registros totales: ${stats.totalRecords}\n\n` +
|
||||
`Desglose por tabla:\n` +
|
||||
stats.breakdown.map((t: any) => ` - ${t.name}: ${t.count} registros`).join('\n')
|
||||
} catch (e: any) {
|
||||
return `Error: ${e.message}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// execute_query
|
||||
registerTool(
|
||||
'execute_query',
|
||||
'Ejecuta una consulta SQL SELECT en la base de datos (solo lectura)',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Consulta SQL (solo SELECT permitido)'
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
},
|
||||
async (args: { query: string }) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/database/query`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: args.query })
|
||||
})
|
||||
|
||||
const result = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
return `Error en la consulta: ${result.error}`
|
||||
}
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return 'La consulta no retorno resultados'
|
||||
}
|
||||
|
||||
// Format results
|
||||
const columns = Object.keys(result.rows[0])
|
||||
const header = columns.join(' | ')
|
||||
const separator = columns.map(() => '---').join(' | ')
|
||||
const rows = result.rows.slice(0, 50).map((row: any) => {
|
||||
return columns.map(col => {
|
||||
let value = row[col]
|
||||
if (value === null) return 'NULL'
|
||||
if (typeof value === 'object') return JSON.stringify(value)
|
||||
if (typeof value === 'string' && value.length > 40) {
|
||||
return value.substring(0, 40) + '...'
|
||||
}
|
||||
return String(value)
|
||||
}).join(' | ')
|
||||
}).join('\n')
|
||||
|
||||
const truncated = result.rows.length > 50 ? `\n\n... y ${result.rows.length - 50} filas mas` : ''
|
||||
|
||||
return `Resultados (${result.rows.length} filas):\n\n${header}\n${separator}\n${rows}${truncated}`
|
||||
} catch (e: any) {
|
||||
return `Error: ${e.message}`
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function unregisterDatabaseTools() {
|
||||
unregisterTools(DATABASE_TOOLS)
|
||||
}
|
||||
Reference in New Issue
Block a user