Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
workspace/
config_*.json
workspace_*/
emma_paper/
final_outputs/
*.wav
Expand Down
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,39 @@
<div align="center">
<br>

# Toolomics
<img src="./docs/images/logo_toolomics.png" width="22%" style="border-radius: 8px;" alt="Toolomics Logo">

*A suite of tools from the Holobiomics Lab for Agents, organized as a set of MCP servers.*
</div>

<h1 align="center">Toolomics</h1>

<p align="center">
<em>A suite of MCP-based Tools from the HolobiomicsLab. Used by AI-Agents such as ***Mimosa-AI***</em>
</p>

<p align="center">
<img src="https://img.shields.io/github/license/HolobiomicsLab/Toolomics?style=flat-square&logo=opensourceinitiative&logoColor=white&color=4caf82" alt="license">
<img src="https://img.shields.io/github/last-commit/HolobiomicsLab/Toolomics?style=flat-square&logo=git&logoColor=white&color=4caf82" alt="last-commit">
<img src="https://img.shields.io/github/languages/count/HolobiomicsLab/Toolomics?style=flat-square&color=4caf82" alt="repo-language-count">
</p>

<p align="center">
<a href="https://github.com/HolobiomicsLab/Toolomics/stargazers">
<img src="https://img.shields.io/github/stars/HolobiomicsLab/Toolomics?style=social" alt="GitHub Stars">
</a>
<a href="https://opensource.org/licenses/Apache-2.0">
<img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square" alt="License: Apache 2.0">
</a>
</p>

---

> ***Toolomics*** — deploys containerized tools, manages isolated instances, and enables file sharing across AI agents for bioinformatics, metabolomics, molecular docking, and beyond.

**Use cases:**
- Deploy MCP servers for browser automation, PDF processing, and data extraction
- Run isolated, multi-instance agent workspaces with automatic resource management
- Orchestrate containerized bioinformatics pipelines (XCMS, RStudio, Redis) with zero config

## Install & deploy tools

Expand Down
34 changes: 22 additions & 12 deletions config_86517947.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,73 @@
"enabled": false
},
{
"path": "mcp_host/pdf/server.py",
"path": "mcp_host/memory/server.py",
"port": 5001,
"enabled": false
},
{
"path": "mcp_host/html/server.py",
"path": "mcp_host/pdf/server.py",
"port": 5002,
"enabled": true
},
{
"path": "mcp_host/html/server.py",
"port": 5003,
"enabled": false
},
{
"path": "mcp_host/graph_rag/server.py",
"port": 5003,
"port": 5004,
"enabled": false
},
{
"path": "mcp_host/browser/server.py",
"port": 5004,
"port": 5005,
"enabled": false
},
{
"path": "mcp_host/image_analysis/server.py",
"port": 5013,
"port": 5006,
"enabled": true
},
{
"path": "mcp_host/csv/server.py",
"port": 5006,
"port": 5007,
"enabled": true
},
{
"path": "mcp_host/skills/server.py",
"port": 5008,
"enabled": false
},
{
"path": "mcp_host/txt_editor/server.py",
"port": 5007,
"port": 5009,
"enabled": true
},
{
"path": "mcp_host/python_editor/server.py",
"port": 5008,
"port": 5010,
"enabled": true
},
{
"path": "mcp_host/Rscript/docker-compose.yml",
"port": 5009,
"port": 5011,
"enabled": false
},
{
"path": "mcp_host/shell/docker-compose.yml",
"port": 5010,
"port": 5012,
"enabled": true
},
{
"path": "mcp_host/browser/searxng/docker-compose.yml",
"port": 5011,
"port": 5013,
"enabled": false
},
{
"path": "mcp_host/decimer/docker-compose.yml",
"port": 5012,
"port": 5014,
"enabled": false
}
]
38 changes: 18 additions & 20 deletions deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,22 +480,9 @@ def load_config(self) -> Dict[str, dict]:
if self.config_path.stat().st_size == 0:
logger.warning(f"Config file {self.config_path} is empty.")
return {}

# Check if file is corrupted by reading first character
try:
with open(self.config_path, 'rb') as f:
first_byte = f.read(1)
if first_byte and first_byte != b'[':
logger.error(f"Config file {self.config_path} is corrupted (starts with {first_byte!r} instead of b'[')")
logger.warning(f"Deleting corrupted config file to regenerate fresh")
self.config_path.unlink()
return {}
except Exception as e:
logger.error(f"Error checking config file: {e}")
return {}

try:
with open(self.config_path, 'r', encoding='utf-8') as f:
with open(self.config_path, 'r', encoding='utf-8-sig') as f: # utf-8-sig handles BOM
# Acquire shared lock for reading
try:
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
Expand Down Expand Up @@ -525,7 +512,7 @@ def load_config(self) -> Dict[str, dict]:
else:
raise ValueError("Can't parse config.json file")

logger.debug(f"Successfully loaded {len(config_dict)} items from config")
logger.info(f"Successfully loaded {len(config_dict)} items from config (enabled: {sum(1 for v in config_dict.values() if v.get('enabled'))})")
return config_dict
finally:
# Release lock
Expand All @@ -534,17 +521,28 @@ def load_config(self) -> Dict[str, dict]:
except:
pass

except (json.JSONDecodeError, KeyError, IndexError) as e:
except (json.JSONDecodeError, KeyError, IndexError, ValueError) as e:
logger.error(f"Error loading config from {self.config_path}: {e}")
# Try to read the file again to see what's actually there
try:
file_size = self.config_path.stat().st_size
with open(self.config_path, 'rb') as f:
raw_bytes = f.read(100)
logger.error(f"File size: {file_size}, first 100 bytes: {raw_bytes}")
logger.error(f"File size: {file_size}, first 100 bytes: {raw_bytes!r}")
except Exception as debug_e:
logger.error(f"Could not read file for debugging: {debug_e}")
logger.warning(f"Returning empty config due to parse error. File will be regenerated.")

# Backup the corrupted file instead of silently regenerating
backup_path = self.config_path.with_suffix('.json.backup')
try:
import shutil
shutil.copy2(self.config_path, backup_path)
logger.warning(f"Backed up corrupted config to: {backup_path}")
except Exception as backup_e:
logger.warning(f"Could not backup config: {backup_e}")

logger.warning(f"Config file appears corrupted. Will regenerate fresh config.")
logger.warning(f"If you had enabled services, please re-enable them after checking {backup_path}")
return {}

def save_config(self, config: Dict[str, dict]) -> None:
Expand Down Expand Up @@ -622,7 +620,7 @@ def assign_ports(self, server_files: List[Path], compose_files: List[Path] = Non

config[server_str] = {'port': next_host_port, 'enabled': False}
used_ports.add(next_host_port)
logger.info(f"Assigned host port {next_host_port} to {server_str} (enabled by default)")
logger.info(f"Assigned host port {next_host_port} to {server_str} (disabled - edit config to enable)")
next_host_port += 1

# Assign ports to docker-compose files
Expand All @@ -640,7 +638,7 @@ def assign_ports(self, server_files: List[Path], compose_files: List[Path] = Non

config[compose_str] = {'port': next_host_port, 'enabled': False}
used_ports.add(next_host_port)
logger.info(f"Assigned host port {next_host_port} to {compose_str} (enabled by default)")
logger.info(f"Assigned host port {next_host_port} to {compose_str} (disabled - edit config to enable)")
next_host_port += 1

self.save_config(config)
Expand Down
Loading
Loading