feat: Add source code viewer with Gitea integration
- Add /source page with file explorer and code viewer - Connect to Gitea API for repository browsing - Implement MCP tools: get_repo_info, list_repo_files, read_repo_file, search_repo_code - Default to nucleo000/agent-ui repository - Support branch switching and file search
This commit is contained in:
@@ -29,8 +29,13 @@ import {
|
||||
unregisterDatabaseTools,
|
||||
DATABASE_TOOLS
|
||||
} from './tools/databaseTools'
|
||||
import {
|
||||
registerSourceCodeTools,
|
||||
unregisterSourceCodeTools,
|
||||
SOURCE_CODE_TOOLS
|
||||
} from './tools/sourceCodeTools'
|
||||
|
||||
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database'
|
||||
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source'
|
||||
|
||||
interface PageToolSet {
|
||||
register: () => void
|
||||
@@ -95,6 +100,11 @@ const pageTools: Record<PageName, PageToolSet> = {
|
||||
register: registerDatabaseTools,
|
||||
unregister: unregisterDatabaseTools,
|
||||
toolNames: DATABASE_TOOLS
|
||||
},
|
||||
source: {
|
||||
register: registerSourceCodeTools,
|
||||
unregister: unregisterSourceCodeTools,
|
||||
toolNames: SOURCE_CODE_TOOLS
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
290
frontend/src/services/tools/sourceCodeTools.ts
Normal file
290
frontend/src/services/tools/sourceCodeTools.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { registerTool, unregisterTools } from '../webmcp'
|
||||
|
||||
export const SOURCE_CODE_TOOLS = [
|
||||
'get_repo_info',
|
||||
'list_repo_files',
|
||||
'read_repo_file',
|
||||
'search_repo_code'
|
||||
]
|
||||
|
||||
const API_BASE = 'http://localhost:4101'
|
||||
|
||||
// Store credentials in memory (not persisted)
|
||||
let giteaCredentials: {
|
||||
giteaUrl: string
|
||||
username: string
|
||||
password: string
|
||||
owner: string
|
||||
repo: string
|
||||
branch: string
|
||||
} | null = null
|
||||
|
||||
export function setGiteaCredentials(creds: typeof giteaCredentials) {
|
||||
giteaCredentials = creds
|
||||
}
|
||||
|
||||
export function registerSourceCodeTools() {
|
||||
// get_repo_info
|
||||
registerTool(
|
||||
'get_repo_info',
|
||||
'Obtiene informacion del repositorio conectado en Gitea',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {}
|
||||
},
|
||||
async () => {
|
||||
if (!giteaCredentials) {
|
||||
return 'No hay conexion a Gitea. Conectate primero en la pagina de Source Code.'
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/gitea/repo`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(giteaCredentials)
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
return `Error: ${err.error}`
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
const repo = data.repo
|
||||
|
||||
return `Repositorio: ${repo.owner.login}/${repo.name}\n` +
|
||||
`Descripcion: ${repo.description || 'Sin descripcion'}\n` +
|
||||
`Rama default: ${repo.default_branch}\n` +
|
||||
`Stars: ${repo.stars_count}\n` +
|
||||
`Forks: ${repo.forks_count}\n` +
|
||||
`Ramas disponibles: ${data.branches.join(', ')}`
|
||||
} catch (e: any) {
|
||||
return `Error: ${e.message}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// list_repo_files
|
||||
registerTool(
|
||||
'list_repo_files',
|
||||
'Lista archivos y carpetas en una ruta del repositorio',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Ruta dentro del repositorio (vacio para raiz)'
|
||||
}
|
||||
}
|
||||
},
|
||||
async (args: { path?: string }) => {
|
||||
if (!giteaCredentials) {
|
||||
return 'No hay conexion a Gitea. Conectate primero en la pagina de Source Code.'
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/gitea/tree`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...giteaCredentials,
|
||||
path: args.path || ''
|
||||
})
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
return `Error: ${err.error}`
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
const path = args.path || '/'
|
||||
|
||||
if (data.tree.length === 0) {
|
||||
return `No hay archivos en ${path}`
|
||||
}
|
||||
|
||||
const folders = data.tree.filter((f: any) => f.type === 'dir')
|
||||
const files = data.tree.filter((f: any) => f.type === 'file')
|
||||
|
||||
let result = `Contenido de ${path}:\n\n`
|
||||
|
||||
if (folders.length > 0) {
|
||||
result += `Carpetas (${folders.length}):\n`
|
||||
result += folders.map((f: any) => ` [DIR] ${f.name}/`).join('\n')
|
||||
result += '\n\n'
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
result += `Archivos (${files.length}):\n`
|
||||
result += files.map((f: any) => ` ${f.name}`).join('\n')
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (e: any) {
|
||||
return `Error: ${e.message}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// read_repo_file
|
||||
registerTool(
|
||||
'read_repo_file',
|
||||
'Lee el contenido de un archivo del repositorio',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Ruta del archivo dentro del repositorio'
|
||||
},
|
||||
lines: {
|
||||
type: 'number',
|
||||
description: 'Numero maximo de lineas a retornar (default: 100)'
|
||||
}
|
||||
},
|
||||
required: ['path']
|
||||
},
|
||||
async (args: { path: string; lines?: number }) => {
|
||||
if (!giteaCredentials) {
|
||||
return 'No hay conexion a Gitea. Conectate primero en la pagina de Source Code.'
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/gitea/file`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...giteaCredentials,
|
||||
path: args.path
|
||||
})
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
return `Error: ${err.error}`
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
let content = data.content
|
||||
|
||||
// Limit lines if specified
|
||||
const maxLines = args.lines || 100
|
||||
const lines = content.split('\n')
|
||||
if (lines.length > maxLines) {
|
||||
content = lines.slice(0, maxLines).join('\n')
|
||||
content += `\n\n... (${lines.length - maxLines} lineas mas)`
|
||||
}
|
||||
|
||||
return `Archivo: ${args.path}\nTamano: ${data.size} bytes\n\n${content}`
|
||||
} catch (e: any) {
|
||||
return `Error: ${e.message}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// search_repo_code
|
||||
registerTool(
|
||||
'search_repo_code',
|
||||
'Busca codigo en el repositorio (busqueda simple en archivos)',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Texto a buscar en los archivos'
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Ruta donde buscar (default: raiz)'
|
||||
},
|
||||
extension: {
|
||||
type: 'string',
|
||||
description: 'Extension de archivos a buscar (ej: ts, vue, js)'
|
||||
}
|
||||
},
|
||||
required: ['query']
|
||||
},
|
||||
async (args: { query: string; path?: string; extension?: string }) => {
|
||||
if (!giteaCredentials) {
|
||||
return 'No hay conexion a Gitea. Conectate primero en la pagina de Source Code.'
|
||||
}
|
||||
|
||||
try {
|
||||
// First get the file tree
|
||||
const treeRes = await fetch(`${API_BASE}/api/gitea/tree`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...giteaCredentials,
|
||||
path: args.path || ''
|
||||
})
|
||||
})
|
||||
|
||||
if (!treeRes.ok) {
|
||||
return 'Error al obtener lista de archivos'
|
||||
}
|
||||
|
||||
const treeData = await treeRes.json()
|
||||
const files = treeData.tree.filter((f: any) => {
|
||||
if (f.type !== 'file') return false
|
||||
if (args.extension) {
|
||||
return f.name.endsWith(`.${args.extension}`)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const results: string[] = []
|
||||
const maxFiles = 10
|
||||
let filesSearched = 0
|
||||
|
||||
for (const file of files.slice(0, maxFiles)) {
|
||||
try {
|
||||
const fileRes = await fetch(`${API_BASE}/api/gitea/file`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...giteaCredentials,
|
||||
path: file.path
|
||||
})
|
||||
})
|
||||
|
||||
if (fileRes.ok) {
|
||||
const fileData = await fileRes.json()
|
||||
const lines = fileData.content.split('\n')
|
||||
const matches: string[] = []
|
||||
|
||||
lines.forEach((line: string, idx: number) => {
|
||||
if (line.toLowerCase().includes(args.query.toLowerCase())) {
|
||||
matches.push(` L${idx + 1}: ${line.trim().substring(0, 80)}`)
|
||||
}
|
||||
})
|
||||
|
||||
if (matches.length > 0) {
|
||||
results.push(`${file.path}:\n${matches.slice(0, 5).join('\n')}`)
|
||||
}
|
||||
}
|
||||
filesSearched++
|
||||
} catch (e) {
|
||||
// Skip file errors
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return `No se encontro "${args.query}" en los primeros ${filesSearched} archivos`
|
||||
}
|
||||
|
||||
return `Busqueda: "${args.query}"\n` +
|
||||
`Archivos buscados: ${filesSearched}\n` +
|
||||
`Coincidencias:\n\n${results.join('\n\n')}`
|
||||
} catch (e: any) {
|
||||
return `Error: ${e.message}`
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function unregisterSourceCodeTools() {
|
||||
unregisterTools(SOURCE_CODE_TOOLS)
|
||||
giteaCredentials = null
|
||||
}
|
||||
Reference in New Issue
Block a user