-
Notifications
You must be signed in to change notification settings - Fork 4
feat(modules-local): add dynamic loading of local modules and update .gitignore #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -110,6 +110,8 @@ import { tarifasToolDefinition, tarifasToolImplementation } from './modules/conf | |
| import { totalModelesToolDefinition, totalModelesToolImplementation } from './modules/system/totalmodeles/index.js'; | ||
| import { variantesToolDefinition, variantesToolImplementation } from './modules/core-business/variantes/index.js'; | ||
| import { workEventesToolDefinition, workEventesToolImplementation } from './modules/system/workeventes/index.js'; | ||
| import { loadLocalModules } from './local-loader.js'; | ||
| import type { LoadedLocalModule } from './types/local-module.js'; | ||
|
|
||
| const server = new Server( | ||
| { | ||
|
|
@@ -203,6 +205,8 @@ const subcuentasResource = new SubcuentasResource(fsClient); | |
| const tarifasResource = new TarifasResource(fsClient); | ||
| const totalModelesResource = new TotalModelesResource(fsClient); | ||
| const workEventesResource = new WorkEventesResource(fsClient); | ||
| // Módulos locales — se cargan en runServer() antes de conectar | ||
| let localModules: LoadedLocalModule[] = []; | ||
|
|
||
| server.setRequestHandler(ListToolsRequestSchema, async () => { | ||
| return { | ||
|
|
@@ -2095,6 +2099,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { | |
| }, | ||
| }, | ||
| }, | ||
| // Módulos locales (cargados dinámicamente desde src/modules-local/) | ||
| ...localModules.map(m => m.toolDefinition), | ||
| ], | ||
| }; | ||
| }); | ||
|
|
@@ -2570,6 +2576,13 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => { | |
| description: 'Lista de eventos y trabajos del sistema para monitoreo y seguimiento de procesos', | ||
| mimeType: 'application/json', | ||
| }, | ||
| // Módulos locales (cargados dinámicamente desde src/modules-local/) | ||
| ...localModules.map(m => ({ | ||
| uri: `facturascripts://${m.resourceName}`, | ||
| name: `FacturaScripts Local — ${m.resourceName}`, | ||
| description: m.resourceDescription, | ||
| mimeType: 'application/json', | ||
| })), | ||
| ], | ||
| }; | ||
| }); | ||
|
|
@@ -2889,6 +2902,13 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { | |
| return await workEventesResource.getResource(uri); | ||
| } | ||
|
|
||
| // Módulos locales | ||
| for (const localModule of localModules) { | ||
| if (localModule.instance.matchesUri(uri)) { | ||
| return await localModule.instance.getResource(uri); | ||
| } | ||
| } | ||
|
|
||
| throw new Error(`Resource not found: ${uri}`); | ||
| }); | ||
|
|
||
|
|
@@ -3836,8 +3856,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { | |
| }; | ||
| } | ||
|
|
||
| default: | ||
|
|
||
| default: { | ||
| // Módulos locales | ||
| const localMod = localModules.find(m => m.toolDefinition.name === name); | ||
| if (localMod) { | ||
| const uri = buildUri(localMod.resourceName); | ||
| const result = await localMod.instance.getResource(uri); | ||
| return { | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: (result as any).contents?.[0]?.text || 'No data', | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
Comment on lines
+3860
to
+3874
|
||
| throw new Error(`Unknown tool: ${name}`); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| return { | ||
|
|
@@ -3853,6 +3889,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { | |
| }); | ||
|
|
||
| async function runServer() { | ||
| localModules = await loadLocalModules(fsClient); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| console.error('MCP FacturaScripts server running on stdio'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,81 @@ | ||||||||||||||||||||||||||||||||||||||
| import { readdirSync, existsSync } from 'fs'; | ||||||||||||||||||||||||||||||||||||||
| import { join, dirname } from 'path'; | ||||||||||||||||||||||||||||||||||||||
| import { fileURLToPath, pathToFileURL } from 'url'; | ||||||||||||||||||||||||||||||||||||||
| import { FacturaScriptsClient } from './fs/client.js'; | ||||||||||||||||||||||||||||||||||||||
| import type { LocalModuleConfig, LoadedLocalModule } from './types/local-module.js'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const __filename = fileURLToPath(import.meta.url); | ||||||||||||||||||||||||||||||||||||||
| const __dirname = dirname(__filename); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Carga dinámicamente todos los módulos locales de la carpeta modules-local/. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * Cada subcarpeta de modules-local/ debe tener un index.js (compilado desde index.ts) | ||||||||||||||||||||||||||||||||||||||
| * que exporte `moduleConfig` siguiendo la interfaz LocalModuleConfig. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * La carpeta src/modules-local/ está en .gitignore, por lo que los módulos | ||||||||||||||||||||||||||||||||||||||
| * locales nunca se subirán al repositorio. | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| export async function loadLocalModules(fsClient: FacturaScriptsClient): Promise<LoadedLocalModule[]> { | ||||||||||||||||||||||||||||||||||||||
| const localModulesDir = join(__dirname, 'modules-local'); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!existsSync(localModulesDir)) { | ||||||||||||||||||||||||||||||||||||||
| return []; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const loaded: LoadedLocalModule[] = []; | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+26
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let entries: import('fs').Dirent<string>[]; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| let entries: import('fs').Dirent<string>[]; | |
| let entries: import('fs').Dirent[]; |
Copilot
AI
Mar 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
El loader solo busca index.js. Con npm run dev:ts (que ejecuta tsx src/index.ts) __dirname apunta a src/ y en módulos locales normalmente habrá index.ts, no index.js, así que en modo dev no se cargarán. Para que funcione en ambos modos, contempla resolver index.ts cuando se ejecute desde TS (o buscar primero index.js y si no existe, probar index.ts).
| const indexPath = join(localModulesDir, dir.name as string, 'index.js'); | |
| if (!existsSync(indexPath)) { | |
| console.error(`[local-loader] Módulo "${dir.name}" no tiene index.js compilado — ejecuta npm run build`); | |
| const jsIndexPath = join(localModulesDir, dir.name as string, 'index.js'); | |
| const tsIndexPath = join(localModulesDir, dir.name as string, 'index.ts'); | |
| let indexPath: string; | |
| if (existsSync(jsIndexPath)) { | |
| indexPath = jsIndexPath; | |
| } else if (existsSync(tsIndexPath)) { | |
| indexPath = tsIndexPath; | |
| } else { | |
| console.error( | |
| `[local-loader] Módulo "${dir.name}" no tiene ni index.js compilado ni index.ts — ` + | |
| 'ejecuta npm run build o crea un index.ts en modules-local/' | |
| ); |
Copilot
AI
Mar 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Antes de instanciar y registrar el módulo, convendría validar que config.Resource realmente expone matchesUri() y getResource() (o que la instancia las tiene como funciones). Ahora mismo, si un módulo local exporta una clase incorrecta, el servidor puede reventar más tarde al atender ReadResource/CallTool. Considera hacer esta validación aquí y saltarte el módulo con un log claro.
| // Validar que el Resource exportado tiene la forma esperada antes de instanciarlo. | |
| const resourceProto = (config.Resource as any)?.prototype; | |
| if ( | |
| !resourceProto || | |
| typeof resourceProto.matchesUri !== 'function' || | |
| typeof resourceProto.getResource !== 'function' | |
| ) { | |
| console.error( | |
| `[local-loader] Módulo "${dir.name}": Resource no implementa matchesUri()/getResource(), módulo ignorado` | |
| ); | |
| continue; | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { FacturaScriptsClient } from '../fs/client.js'; | ||
|
|
||
| /** | ||
| * Interfaz que debe exportar cada módulo local como `moduleConfig`. | ||
| * | ||
| * Ejemplo de uso en src/modules-local/mi-modulo/index.ts: | ||
| * | ||
| * import { MiResource } from './resource.js'; | ||
| * import { toolDefinition } from './tool.js'; | ||
| * import type { LocalModuleConfig } from '../../types/local-module.js'; | ||
| * | ||
| * export const moduleConfig: LocalModuleConfig = { | ||
| * resourceName: 'mi-recurso', | ||
| * resourceDescription: 'Descripción de mi recurso local', | ||
| * Resource: MiResource, | ||
| * toolDefinition, | ||
| * }; | ||
| */ | ||
| export interface LocalModuleConfig { | ||
| /** Nombre del recurso, usado en la URI: facturascripts://{resourceName} */ | ||
| resourceName: string; | ||
| /** Descripción visible en el listado de recursos MCP */ | ||
| resourceDescription: string; | ||
| /** Clase del recurso. Debe implementar getResource(uri) y matchesUri(uri) */ | ||
| Resource: new (client: FacturaScriptsClient) => any; | ||
| /** Definición de la herramienta MCP (name, description, inputSchema) */ | ||
| toolDefinition: { | ||
| name: string; | ||
| description: string; | ||
| inputSchema: object; | ||
| }; | ||
|
Comment on lines
+24
to
+31
|
||
| } | ||
|
|
||
| export interface LoadedLocalModule { | ||
| instance: any; | ||
| toolDefinition: LocalModuleConfig['toolDefinition']; | ||
| resourceName: string; | ||
| resourceDescription: string; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aquí se asume que
localModule.instance.matchesUriexiste y es callable. Si un módulo local está mal implementado, esto puede tirar abajo el handler conTypeError. Para robustez, añade un type-guard (comprobar quematchesUri/getResourceson funciones) o garantiza esa validación enloadLocalModules()antes de añadir el módulo alocalModules.