nexo-brain 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,136 +0,0 @@
1
- """Dynamic plugin loader for NEXO MCP server."""
2
-
3
- import importlib
4
- import os
5
- import signal
6
- import sys
7
- import time
8
-
9
- from db import get_db
10
- from fastmcp.tools import Tool
11
-
12
- PLUGINS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "plugins")
13
-
14
- PLUGIN_LOAD_TIMEOUT = 10 # seconds per plugin
15
-
16
-
17
- class _PluginTimeout(Exception):
18
- pass
19
-
20
-
21
- def _timeout_handler(signum, frame):
22
- raise _PluginTimeout("Plugin loading timed out")
23
-
24
-
25
- def load_all_plugins(mcp) -> int:
26
- """Load all plugins from plugins/ directory at startup. Returns total tools loaded."""
27
- if not os.path.isdir(PLUGINS_DIR):
28
- return 0
29
- total = 0
30
- for f in sorted(os.listdir(PLUGINS_DIR)):
31
- if f.endswith(".py") and f != "__init__.py":
32
- try:
33
- old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
34
- signal.alarm(PLUGIN_LOAD_TIMEOUT)
35
- try:
36
- n = load_plugin(mcp, f)
37
- total += n
38
- finally:
39
- signal.alarm(0)
40
- signal.signal(signal.SIGALRM, old_handler)
41
- except _PluginTimeout:
42
- print(f"[PLUGIN TIMEOUT] {f}: skipped after {PLUGIN_LOAD_TIMEOUT}s", file=sys.stderr)
43
- except Exception as e:
44
- print(f"[PLUGIN ERROR] {f}: {e}", file=sys.stderr)
45
- return total
46
-
47
-
48
- def load_plugin(mcp, filename: str) -> int:
49
- """Load or reload a single plugin. Returns number of tools registered."""
50
- if not filename.endswith(".py"):
51
- filename += ".py"
52
-
53
- filepath = os.path.join(PLUGINS_DIR, filename)
54
- if not os.path.isfile(filepath):
55
- raise FileNotFoundError(f"Plugin not found: {filepath}")
56
-
57
- module_name = f"plugins.{filename[:-3]}"
58
-
59
- if module_name in sys.modules:
60
- mod = importlib.reload(sys.modules[module_name])
61
- else:
62
- mod = importlib.import_module(module_name)
63
-
64
- tools_list = getattr(mod, "TOOLS", [])
65
- tool_names = []
66
-
67
- for func, name, description in tools_list:
68
- try:
69
- mcp.local_provider.remove_tool(name)
70
- except Exception:
71
- pass
72
- t = Tool.from_function(func, name=name, description=description)
73
- mcp.add_tool(t)
74
- tool_names.append(name)
75
-
76
- _update_registry(filename, len(tool_names), ",".join(tool_names), "manual")
77
-
78
- return len(tool_names)
79
-
80
-
81
- def remove_plugin(mcp, filename: str) -> list[str]:
82
- """Remove a plugin: unregister its tools, delete file, clean registry."""
83
- if not filename.endswith(".py"):
84
- filename += ".py"
85
-
86
- conn = get_db()
87
- row = conn.execute("SELECT tool_names FROM plugins WHERE filename = ?", (filename,)).fetchone()
88
-
89
- removed = []
90
- if row and row["tool_names"]:
91
- for name in row["tool_names"].split(","):
92
- name = name.strip()
93
- if name:
94
- try:
95
- mcp.local_provider.remove_tool(name)
96
- removed.append(name)
97
- except Exception:
98
- pass
99
-
100
- module_name = f"plugins.{filename[:-3]}"
101
- sys.modules.pop(module_name, None)
102
-
103
- filepath = os.path.join(PLUGINS_DIR, filename)
104
- if os.path.isfile(filepath):
105
- os.remove(filepath)
106
-
107
- conn = get_db()
108
- conn.execute("DELETE FROM plugins WHERE filename = ?", (filename,))
109
- conn.commit()
110
-
111
- return removed
112
-
113
-
114
- def list_plugins() -> list[dict]:
115
- """List all registered plugins."""
116
- conn = get_db()
117
- rows = conn.execute(
118
- "SELECT filename, tools_count, tool_names, loaded_at, created_by FROM plugins ORDER BY filename"
119
- ).fetchall()
120
- return [dict(r) for r in rows]
121
-
122
-
123
- def _update_registry(filename: str, tools_count: int, tool_names: str, created_by: str):
124
- """Insert or update plugin registry entry. Non-fatal on lock — tools still work."""
125
- now = time.time()
126
- try:
127
- conn = get_db()
128
- conn.execute(
129
- "INSERT INTO plugins (filename, tools_count, tool_names, loaded_at, created_by) "
130
- "VALUES (?, ?, ?, ?, ?) "
131
- "ON CONFLICT(filename) DO UPDATE SET tools_count=?, tool_names=?, loaded_at=?",
132
- (filename, tools_count, tool_names, now, created_by, tools_count, tool_names, now),
133
- )
134
- conn.commit()
135
- except Exception as e:
136
- print(f"[PLUGIN REGISTRY] Skipped update for {filename}: {e}")
package/src/server 2.py DELETED
@@ -1,560 +0,0 @@
1
- """NEXO MCP Server — Phase 4: Hot-Reload Plugin System."""
2
-
3
- import os
4
- import signal
5
- import sys
6
-
7
- from fastmcp import FastMCP
8
- from db import init_db, rebuild_fts_index, get_db, close_db, fts_add_dir, fts_remove_dir, fts_list_dirs
9
- from tools_sessions import handle_startup, handle_heartbeat, handle_status
10
- from tools_coordination import (
11
- handle_track, handle_untrack, handle_files,
12
- handle_send, handle_ask, handle_answer, handle_check_answer,
13
- )
14
- from tools_reminders import handle_reminders
15
- from tools_menu import handle_menu
16
- from tools_reminders_crud import (
17
- handle_reminder_create, handle_reminder_update,
18
- handle_reminder_complete, handle_reminder_delete,
19
- handle_followup_create, handle_followup_update,
20
- handle_followup_complete, handle_followup_delete,
21
- )
22
- from tools_learnings import (
23
- handle_learning_add, handle_learning_search,
24
- handle_learning_update, handle_learning_delete, handle_learning_list,
25
- )
26
- from tools_credentials import (
27
- handle_credential_get, handle_credential_create,
28
- handle_credential_update, handle_credential_delete, handle_credential_list,
29
- )
30
- from tools_task_history import (
31
- handle_task_log, handle_task_list, handle_task_frequency,
32
- )
33
- from plugin_loader import load_all_plugins, load_plugin, remove_plugin, list_plugins
34
-
35
-
36
- # ── Graceful shutdown: close DB on any termination signal ──────────
37
- def _shutdown_handler(signum, frame):
38
- close_db()
39
- sys.exit(0)
40
-
41
- signal.signal(signal.SIGTERM, _shutdown_handler)
42
- signal.signal(signal.SIGINT, _shutdown_handler)
43
-
44
- # ── Write PID file for stale process detection ─────────────────────
45
- _pid_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "nexo.pid")
46
- with open(_pid_file, "w") as f:
47
- f.write(str(os.getpid()))
48
-
49
- init_db()
50
-
51
- mcp = FastMCP(
52
- name="nexo",
53
- instructions=(
54
- "NEXO operational server. Provides session coordination, "
55
- "reminders, followups, and menu for user operations.\n\n"
56
- "When working with tool results, write down any important information "
57
- "you might need later in your response, as the original tool result "
58
- "may be cleared later."
59
- ),
60
- )
61
-
62
- _plugins_loaded = load_all_plugins(mcp)
63
-
64
-
65
- # ── Session management (3 tools) ──────────────────────────────────
66
-
67
- @mcp.tool
68
- def nexo_startup(task: str = "Startup") -> str:
69
- """Register new session, clean stale ones, return active sessions + alerts.
70
-
71
- Call this ONCE at the start of every conversation.
72
- Returns the session ID (SID) — store it for use in all other nexo_ tools.
73
- """
74
- return handle_startup(task)
75
-
76
-
77
- @mcp.tool
78
- def nexo_heartbeat(sid: str, task: str) -> str:
79
- """Update session task, check inbox and pending questions.
80
-
81
- Call this at the START of every user interaction (before doing work).
82
- Args:
83
- sid: Your session ID from nexo_startup.
84
- task: Brief description of current work (5-10 words).
85
- """
86
- return handle_heartbeat(sid, task)
87
-
88
-
89
- @mcp.tool
90
- def nexo_status(keyword: str = "") -> str:
91
- """List active sessions. Filter by keyword if provided."""
92
- return handle_status(keyword if keyword else None)
93
-
94
-
95
- # ── File coordination (3 tools) ───────────────────────────────────
96
-
97
- @mcp.tool
98
- def nexo_track(sid: str, paths: list[str]) -> str:
99
- """Track files being edited. Detects conflicts with other sessions.
100
-
101
- MUST call before editing any file outside ~/claude/.
102
- Args:
103
- sid: Your session ID.
104
- paths: List of absolute file paths to track.
105
- """
106
- return handle_track(sid, paths)
107
-
108
-
109
- @mcp.tool
110
- def nexo_untrack(sid: str, paths: list[str] | None = None) -> str:
111
- """Stop tracking files. If no paths given, releases all.
112
-
113
- Args:
114
- sid: Your session ID.
115
- paths: File paths to release. Omit to release all.
116
- """
117
- return handle_untrack(sid, paths)
118
-
119
-
120
- @mcp.tool
121
- def nexo_files() -> str:
122
- """Show all tracked files across all active sessions with conflict detection."""
123
- return handle_files()
124
-
125
-
126
- # ── Messaging (4 tools) ───────────────────────────────────────────
127
-
128
- @mcp.tool
129
- def nexo_send(from_sid: str, to_sid: str, text: str) -> str:
130
- """Send a fire-and-forget message to another session or broadcast.
131
-
132
- Args:
133
- from_sid: Your session ID.
134
- to_sid: Target session ID, or 'all' for broadcast.
135
- text: Message content.
136
- """
137
- return handle_send(from_sid, to_sid, text)
138
-
139
-
140
- @mcp.tool
141
- def nexo_ask(from_sid: str, to_sid: str, question: str) -> str:
142
- """Ask a question to another session (they see it on next heartbeat).
143
-
144
- Args:
145
- from_sid: Your session ID.
146
- to_sid: Target session ID.
147
- question: The question text.
148
- Returns: Question ID (qid) for checking the answer later.
149
- """
150
- return handle_ask(from_sid, to_sid, question)
151
-
152
-
153
- @mcp.tool
154
- def nexo_answer(qid: str, answer: str) -> str:
155
- """Answer a pending question from another session.
156
-
157
- Args:
158
- qid: The question ID shown in heartbeat output.
159
- answer: Your response.
160
- """
161
- return handle_answer(qid, answer)
162
-
163
-
164
- @mcp.tool
165
- def nexo_check_answer(qid: str) -> str:
166
- """Check if a question has been answered.
167
-
168
- Args:
169
- qid: The question ID from nexo_ask.
170
- """
171
- return handle_check_answer(qid)
172
-
173
-
174
- # ── Operations: Reminders + Menu (2 tools, read-only) ─────────────
175
-
176
- @mcp.tool
177
- def nexo_reminders(filter: str = "due") -> str:
178
- """Check reminders and followups.
179
-
180
- Args:
181
- filter: 'due' (vencidos/hoy), 'all' (todos activos), 'followups' (solo NEXO followups)
182
- """
183
- return handle_reminders(filter)
184
-
185
-
186
- @mcp.tool
187
- def nexo_menu() -> str:
188
- """Generate the NEXO operations center menu with alerts and active sessions.
189
-
190
- Shows: date, due alerts, all menu items by category, active sessions.
191
- Uses box-drawing characters for formatting.
192
- """
193
- return handle_menu()
194
-
195
-
196
- # ── Reminders CRUD (4 tools) ──────────────────────────────────────
197
-
198
- @mcp.tool
199
- def nexo_reminder_create(id: str, description: str, date: str = "", category: str = "general") -> str:
200
- """Create a new reminder for the user.
201
-
202
- Args:
203
- id: Unique ID starting with 'R' (e.g., R90).
204
- description: What needs to be done.
205
- date: Target date YYYY-MM-DD (optional).
206
- category: One of: decisiones, tareas, esperando, ideas, general.
207
- """
208
- return handle_reminder_create(id, description, date, category)
209
-
210
-
211
- @mcp.tool
212
- def nexo_reminder_update(id: str, description: str = "", date: str = "", status: str = "", category: str = "") -> str:
213
- """Update fields of an existing reminder. Only non-empty fields are changed.
214
-
215
- Args:
216
- id: Reminder ID (e.g., R87).
217
- description: New description (optional).
218
- date: New date YYYY-MM-DD (optional).
219
- status: New status (optional).
220
- category: New category (optional).
221
- """
222
- return handle_reminder_update(id, description, date, status, category)
223
-
224
-
225
- @mcp.tool
226
- def nexo_reminder_complete(id: str) -> str:
227
- """Mark a reminder as COMPLETADO with today's date.
228
-
229
- Args:
230
- id: Reminder ID (e.g., R87).
231
- """
232
- return handle_reminder_complete(id)
233
-
234
-
235
- @mcp.tool
236
- def nexo_reminder_delete(id: str) -> str:
237
- """Delete a reminder permanently.
238
-
239
- Args:
240
- id: Reminder ID (e.g., R87).
241
- """
242
- return handle_reminder_delete(id)
243
-
244
-
245
- # ── Followups CRUD (4 tools) ──────────────────────────────────────
246
-
247
- @mcp.tool
248
- def nexo_followup_create(id: str, description: str, date: str = "", verification: str = "", reasoning: str = "", recurrence: str = "") -> str:
249
- """Create a new NEXO followup (autonomous task).
250
-
251
- Args:
252
- id: Unique ID starting with 'NF' (e.g., NF-MCP2).
253
- description: What to verify/do.
254
- date: Target date YYYY-MM-DD (optional).
255
- verification: How to verify completion (optional).
256
- reasoning: WHY this followup exists — what decision/context led to it (optional).
257
- recurrence: Auto-regenerate pattern (optional). Formats: 'weekly:monday', 'monthly:1', 'monthly:15', 'quarterly'.
258
- When completed, a new followup is auto-created with the next date. The completed one is archived with date suffix.
259
- """
260
- return handle_followup_create(id, description, date, verification, reasoning, recurrence)
261
-
262
-
263
- @mcp.tool
264
- def nexo_followup_update(id: str, description: str = "", date: str = "", verification: str = "", status: str = "") -> str:
265
- """Update fields of an existing followup. Only non-empty fields are changed.
266
-
267
- Args:
268
- id: Followup ID (e.g., NF45).
269
- description: New description (optional).
270
- date: New date YYYY-MM-DD (optional).
271
- verification: New verification text (optional).
272
- status: New status (optional).
273
- """
274
- return handle_followup_update(id, description, date, verification, status)
275
-
276
-
277
- @mcp.tool
278
- def nexo_followup_complete(id: str, result: str = "") -> str:
279
- """Mark a followup as COMPLETADO. Appends result to verification field.
280
-
281
- Args:
282
- id: Followup ID (e.g., NF45).
283
- result: What was found/done (optional).
284
- """
285
- return handle_followup_complete(id, result)
286
-
287
-
288
- @mcp.tool
289
- def nexo_followup_delete(id: str) -> str:
290
- """Delete a followup permanently.
291
-
292
- Args:
293
- id: Followup ID (e.g., NF45).
294
- """
295
- return handle_followup_delete(id)
296
-
297
-
298
- # ── Learnings CRUD (5 tools) ──────────────────────────────────────
299
-
300
- @mcp.tool
301
- def nexo_learning_add(category: str, title: str, content: str, reasoning: str = "") -> str:
302
- """Add a new learning (resolved error, pattern, gotcha).
303
-
304
- Args:
305
- category: One of: general, code, infrastructure, api, database, security, deployment, testing, performance, ux.
306
- title: Short title for the learning.
307
- content: Full description with context and solution.
308
- reasoning: WHY this matters — what led to discovering this (optional).
309
- """
310
- return handle_learning_add(category, title, content, reasoning)
311
-
312
-
313
- @mcp.tool
314
- def nexo_learning_search(query: str, category: str = "") -> str:
315
- """Search learnings by keyword. Searches title and content.
316
-
317
- Args:
318
- query: Search term.
319
- category: Filter by category (optional).
320
- """
321
- return handle_learning_search(query, category)
322
-
323
-
324
- @mcp.tool
325
- def nexo_learning_update(id: int, title: str = "", content: str = "", category: str = "") -> str:
326
- """Update a learning entry. Only non-empty fields are changed.
327
-
328
- Args:
329
- id: Learning ID number.
330
- title: New title (optional).
331
- content: New content (optional).
332
- category: New category (optional).
333
- """
334
- return handle_learning_update(id, title, content, category)
335
-
336
-
337
- @mcp.tool
338
- def nexo_learning_delete(id: int) -> str:
339
- """Delete a learning entry.
340
-
341
- Args:
342
- id: Learning ID number.
343
- """
344
- return handle_learning_delete(id)
345
-
346
-
347
- @mcp.tool
348
- def nexo_learning_list(category: str = "") -> str:
349
- """List all learnings, grouped by category.
350
-
351
- Args:
352
- category: Filter by category (optional). If empty, shows all grouped.
353
- """
354
- return handle_learning_list(category)
355
-
356
-
357
- # ── Search index ──────────────────────────────────────────────────
358
-
359
- @mcp.tool
360
- def nexo_reindex() -> str:
361
- """Force full rebuild of the FTS5 search index. Use after bulk changes or if search seems stale."""
362
- conn = get_db()
363
- rebuild_fts_index(conn)
364
- count = conn.execute("SELECT COUNT(*) FROM unified_search").fetchone()[0]
365
- sources = conn.execute("SELECT source, COUNT(*) as cnt FROM unified_search GROUP BY source ORDER BY cnt DESC").fetchall()
366
- lines = [f"Index rebuilt: {count} documentos"]
367
- for s in sources:
368
- lines.append(f" {s[0]:12s} → {s[1]}")
369
- return "\n".join(lines)
370
-
371
-
372
- @mcp.tool
373
- def nexo_index_add_dir(path: str, dir_type: str = "code",
374
- patterns: str = "*.php,*.js,*.json,*.py,*.ts,*.tsx",
375
- notes: str = "") -> str:
376
- """Register a new directory for FTS5 search indexing. Survives restarts.
377
-
378
- Args:
379
- path: Absolute path to directory (supports ~).
380
- dir_type: 'code' for source files, 'md' for markdown docs.
381
- patterns: Comma-separated glob patterns (only for code type).
382
- notes: Description of what this directory contains.
383
- """
384
- result = fts_add_dir(path, dir_type, patterns, notes)
385
- if "error" in result:
386
- return f"ERROR: {result['error']}"
387
- return f"Directorio registrado: {result['path']} ({result['dir_type']}, patterns: {result['patterns']})\nUsa nexo_reindex para indexar ahora."
388
-
389
-
390
- @mcp.tool
391
- def nexo_index_remove_dir(path: str) -> str:
392
- """Remove a directory from FTS5 indexing and clean up its entries.
393
-
394
- Args:
395
- path: Path to directory to remove.
396
- """
397
- result = fts_remove_dir(path)
398
- if "error" in result:
399
- return f"ERROR: {result['error']}"
400
- return f"Directorio eliminado del índice: {result['removed']}"
401
-
402
-
403
- @mcp.tool
404
- def nexo_index_dirs() -> str:
405
- """List all directories being indexed by FTS5 (builtin + dynamic)."""
406
- dirs = fts_list_dirs()
407
- if not dirs:
408
- return "Sin directorios configurados."
409
- lines = ["DIRECTORIOS INDEXADOS:"]
410
- for d in dirs:
411
- source_tag = "⚙️" if d["source"] == "builtin" else "➕"
412
- notes = f" — {d['notes']}" if d.get("notes") else ""
413
- lines.append(f" {source_tag} [{d['type']}] {d['path']}")
414
- lines.append(f" patterns: {d['patterns']}{notes}")
415
- return "\n".join(lines)
416
-
417
-
418
- # ── Credentials CRUD (5 tools) ────────────────────────────────────
419
-
420
- @mcp.tool
421
- def nexo_credential_get(service: str, key: str = "") -> str:
422
- """Get credential value(s) for a service.
423
-
424
- Args:
425
- service: Service name (e.g., google-ads, meta-ads, shopify).
426
- key: Specific key (optional). If empty, returns all for the service.
427
- """
428
- return handle_credential_get(service, key)
429
-
430
-
431
- @mcp.tool
432
- def nexo_credential_create(service: str, key: str, value: str, notes: str = "") -> str:
433
- """Store a new credential.
434
-
435
- Args:
436
- service: Service name (e.g., google-ads, cloudflare).
437
- key: Key name (e.g., api_key, token, ssh).
438
- value: The secret value.
439
- notes: Description or context (optional).
440
- """
441
- return handle_credential_create(service, key, value, notes)
442
-
443
-
444
- @mcp.tool
445
- def nexo_credential_update(service: str, key: str, value: str = "", notes: str = "") -> str:
446
- """Update a credential's value and/or notes.
447
-
448
- Args:
449
- service: Service name.
450
- key: Key name.
451
- value: New value (optional).
452
- notes: New notes (optional).
453
- """
454
- return handle_credential_update(service, key, value, notes)
455
-
456
-
457
- @mcp.tool
458
- def nexo_credential_delete(service: str, key: str = "") -> str:
459
- """Delete credential(s). If no key, deletes all for the service.
460
-
461
- Args:
462
- service: Service name.
463
- key: Specific key (optional). If empty, deletes ALL for service.
464
- """
465
- return handle_credential_delete(service, key)
466
-
467
-
468
- @mcp.tool
469
- def nexo_credential_list(service: str = "") -> str:
470
- """List credentials (names and notes only, no values).
471
-
472
- Args:
473
- service: Filter by service (optional). If empty, shows all.
474
- """
475
- return handle_credential_list(service)
476
-
477
-
478
- # ── Task History (3 tools) ────────────────────────────────────────
479
-
480
- @mcp.tool
481
- def nexo_task_log(task_num: str, task_name: str, notes: str = "", reasoning: str = "") -> str:
482
- """Record that an operational task was executed.
483
-
484
- Args:
485
- task_num: Task number from the checklist (e.g., '7', '7b').
486
- task_name: Task name (e.g., 'Google Ads').
487
- notes: Execution summary (optional).
488
- reasoning: WHY this task was executed now — what triggered it (optional).
489
- """
490
- return handle_task_log(task_num, task_name, notes, reasoning)
491
-
492
-
493
- @mcp.tool
494
- def nexo_task_list(task_num: str = "", days: int = 30) -> str:
495
- """Show execution history for operational tasks.
496
-
497
- Args:
498
- task_num: Filter by task number (optional).
499
- days: How many days back to show (default 30).
500
- """
501
- return handle_task_list(task_num, days)
502
-
503
-
504
- @mcp.tool
505
- def nexo_task_frequency() -> str:
506
- """Check which operational tasks are overdue based on their frequency.
507
-
508
- Compares last execution date vs configured frequency.
509
- Returns overdue tasks or 'all tasks up to date'.
510
- """
511
- return handle_task_frequency()
512
-
513
-
514
- # ── Plugin Management (3 tools) ─────────────────────────────────
515
-
516
- @mcp.tool
517
- def nexo_plugin_load(filename: str) -> str:
518
- """Load or reload a plugin from the plugins/ directory.
519
-
520
- Args:
521
- filename: Plugin filename (e.g., 'entities.py').
522
- """
523
- try:
524
- n = load_plugin(mcp, filename)
525
- return f"Plugin {filename}: {n} tools registrados."
526
- except Exception as e:
527
- return f"Error cargando plugin {filename}: {e}"
528
-
529
-
530
- @mcp.tool
531
- def nexo_plugin_list() -> str:
532
- """List all loaded plugins and their tools."""
533
- plugins = list_plugins()
534
- if not plugins:
535
- return "Sin plugins cargados."
536
- lines = ["PLUGINS CARGADOS:"]
537
- for p in plugins:
538
- names = p["tool_names"] or "(sin tools)"
539
- lines.append(f" {p['filename']} — {p['tools_count']} tools: {names}")
540
- return "\n".join(lines)
541
-
542
-
543
- @mcp.tool
544
- def nexo_plugin_remove(filename: str) -> str:
545
- """Remove a plugin: unregister its tools and delete the file.
546
-
547
- Args:
548
- filename: Plugin filename (e.g., 'entities.py').
549
- """
550
- try:
551
- removed = remove_plugin(mcp, filename)
552
- if removed:
553
- return f"Plugin {filename} eliminado. Tools quitados: {', '.join(removed)}"
554
- return f"Plugin {filename} eliminado (no tenía tools registrados)."
555
- except Exception as e:
556
- return f"Error eliminando plugin {filename}: {e}"
557
-
558
-
559
- if __name__ == "__main__":
560
- mcp.run(transport="stdio")