diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 210190e..ac061d2 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -10,7 +10,7 @@ import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './servi
const route = useRoute()
const router = useRouter()
-type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database'
+type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source'
onMounted(async () => {
// Initialize WebMCP connection
diff --git a/frontend/src/components/Toolbar.vue b/frontend/src/components/Toolbar.vue
index 7f214ef..5000ae9 100644
--- a/frontend/src/components/Toolbar.vue
+++ b/frontend/src/components/Toolbar.vue
@@ -103,6 +103,13 @@ onMounted(() => {
+
+
+
+
diff --git a/frontend/src/pages/SourceCodePage.vue b/frontend/src/pages/SourceCodePage.vue
new file mode 100644
index 0000000..eb1e689
--- /dev/null
+++ b/frontend/src/pages/SourceCodePage.vue
@@ -0,0 +1,951 @@
+
+
+
+
+
+
+
+
+
+
+
+
Source Code Viewer
+
Connect to Gitea to browse your repository
+
+
+
+
+
Select a file to view its contents
+
+
+
+
+
+
+
Loading file...
+
+
+ {{ num }}
+
+
{{ fileContent }}
+
+
+
+
+
+
+
+
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index aa0b08c..b29c0a3 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -38,6 +38,11 @@ const router = createRouter({
path: '/database',
name: 'database',
component: () => import('../pages/DatabasePage.vue')
+ },
+ {
+ path: '/source',
+ name: 'source',
+ component: () => import('../pages/SourceCodePage.vue')
}
]
})
diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts
index 592309f..40089f0 100644
--- a/frontend/src/services/toolRegistry.ts
+++ b/frontend/src/services/toolRegistry.ts
@@ -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 = {
register: registerDatabaseTools,
unregister: unregisterDatabaseTools,
toolNames: DATABASE_TOOLS
+ },
+ source: {
+ register: registerSourceCodeTools,
+ unregister: unregisterSourceCodeTools,
+ toolNames: SOURCE_CODE_TOOLS
}
}
diff --git a/frontend/src/services/tools/sourceCodeTools.ts b/frontend/src/services/tools/sourceCodeTools.ts
new file mode 100644
index 0000000..a8813d2
--- /dev/null
+++ b/frontend/src/services/tools/sourceCodeTools.ts
@@ -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
+}
diff --git a/server/index.ts b/server/index.ts
index b6e1a21..b2198ce 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -861,6 +861,142 @@ Bun.serve({
}, { headers: corsHeaders })
}
+ // =====================
+ // API de Gitea (Source Code Viewer)
+ // =====================
+
+ // POST /api/gitea/repo - Connect and get repo info
+ if (url.pathname === '/api/gitea/repo' && req.method === 'POST') {
+ const body = await req.json()
+ const { giteaUrl, username, password, owner, repo } = body
+
+ if (!giteaUrl || !username || !password || !owner || !repo) {
+ return Response.json({ error: 'Missing required fields' }, { status: 400, headers: corsHeaders })
+ }
+
+ try {
+ const auth = Buffer.from(`${username}:${password}`).toString('base64')
+
+ // Get repo info
+ const repoRes = await fetch(`${giteaUrl}/api/v1/repos/${owner}/${repo}`, {
+ headers: { 'Authorization': `Basic ${auth}` }
+ })
+
+ if (!repoRes.ok) {
+ if (repoRes.status === 401) {
+ return Response.json({ error: 'Invalid credentials' }, { status: 401, headers: corsHeaders })
+ }
+ if (repoRes.status === 404) {
+ return Response.json({ error: 'Repository not found' }, { status: 404, headers: corsHeaders })
+ }
+ throw new Error('Failed to connect to Gitea')
+ }
+
+ const repoData = await repoRes.json()
+
+ // Get branches
+ const branchesRes = await fetch(`${giteaUrl}/api/v1/repos/${owner}/${repo}/branches`, {
+ headers: { 'Authorization': `Basic ${auth}` }
+ })
+
+ let branches = ['main']
+ if (branchesRes.ok) {
+ const branchesData = await branchesRes.json()
+ branches = branchesData.map((b: any) => b.name)
+ }
+
+ return Response.json({
+ repo: {
+ name: repoData.name,
+ description: repoData.description,
+ default_branch: repoData.default_branch,
+ stars_count: repoData.stars_count,
+ forks_count: repoData.forks_count,
+ owner: { login: repoData.owner?.login || owner }
+ },
+ branches
+ }, { headers: corsHeaders })
+ } catch (e: any) {
+ return Response.json({ error: e.message }, { status: 500, headers: corsHeaders })
+ }
+ }
+
+ // POST /api/gitea/tree - Get file tree
+ if (url.pathname === '/api/gitea/tree' && req.method === 'POST') {
+ const body = await req.json()
+ const { giteaUrl, username, password, owner, repo, branch, path } = body
+
+ try {
+ const auth = Buffer.from(`${username}:${password}`).toString('base64')
+ const apiPath = path ? `${giteaUrl}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${branch}`
+ : `${giteaUrl}/api/v1/repos/${owner}/${repo}/contents?ref=${branch}`
+
+ const res = await fetch(apiPath, {
+ headers: { 'Authorization': `Basic ${auth}` }
+ })
+
+ if (!res.ok) {
+ throw new Error('Failed to load tree')
+ }
+
+ const data = await res.json()
+ const items = Array.isArray(data) ? data : [data]
+
+ const tree = items
+ .map((item: any) => ({
+ name: item.name,
+ path: item.path,
+ type: item.type === 'dir' ? 'dir' : 'file',
+ children: item.type === 'dir' ? [] : undefined
+ }))
+ .sort((a: any, b: any) => {
+ // Folders first, then files
+ if (a.type !== b.type) return a.type === 'dir' ? -1 : 1
+ return a.name.localeCompare(b.name)
+ })
+
+ return Response.json({ tree }, { headers: corsHeaders })
+ } catch (e: any) {
+ return Response.json({ error: e.message }, { status: 500, headers: corsHeaders })
+ }
+ }
+
+ // POST /api/gitea/file - Get file content
+ if (url.pathname === '/api/gitea/file' && req.method === 'POST') {
+ const body = await req.json()
+ const { giteaUrl, username, password, owner, repo, branch, path } = body
+
+ try {
+ const auth = Buffer.from(`${username}:${password}`).toString('base64')
+
+ const res = await fetch(
+ `${giteaUrl}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
+ { headers: { 'Authorization': `Basic ${auth}` } }
+ )
+
+ if (!res.ok) {
+ throw new Error('Failed to load file')
+ }
+
+ const data = await res.json()
+
+ // Decode base64 content
+ let content = ''
+ if (data.content) {
+ content = Buffer.from(data.content, 'base64').toString('utf-8')
+ }
+
+ return Response.json({
+ content,
+ encoding: data.encoding,
+ size: data.size,
+ sha: data.sha
+ }, { headers: corsHeaders })
+ } catch (e: any) {
+ return Response.json({ error: e.message }, { status: 500, headers: corsHeaders })
+ }
+ }
+
// =====================
// API de Database Explorer
// =====================