Skip to content
Open
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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"workbench.colorTheme": "Cursor Dark Midnight"
}
44 changes: 23 additions & 21 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Omnispindle MCP Todo Server Dockerfile
# Multi-stage build for better efficiency
# Multi-stage build with UV package manager

# Build stage for development dependencies
FROM python:3.11-slim as builder
# Build stage
FROM python:3.11-slim AS builder

WORKDIR /app

Expand All @@ -12,32 +12,36 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*

# Copy requirements files
COPY requirements.txt requirements-dev.txt ./
# Install uv via pip
RUN pip install uv

# Install dependencies into a virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy project configuration files
COPY pyproject.toml uv.lock ./

RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir -r requirements-dev.txt
# Install dependencies using uv
RUN uv sync --frozen --no-dev

# Runtime stage
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Copy virtual environment from builder stage
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Install runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
mosquitto-clients \
&& rm -rf /var/lib/apt/lists/*

# Install uv via pip
RUN pip install uv

# Copy virtual environment from builder stage
COPY --from=builder /app/.venv /app/.venv

# Copy project files
COPY pyproject.toml uv.lock ./
COPY Omnispindle/ ./Omnispindle/

# Set environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
Expand All @@ -50,14 +54,12 @@ ENV PYTHONUNBUFFERED=1 \
MQTT_PORT=1883 \
DeNa=omnispindle \
HOST=0.0.0.0 \
PORT=8000
PORT=8000 \
PATH="/app/.venv/bin:$PATH"

# Create non-root user
RUN useradd -m -s /bin/bash appuser

# Copy application code
COPY --chown=appuser:appuser . .

# Create configuration directory if it doesn't exist
RUN mkdir -p /app/config && chown -R appuser:appuser /app

Expand All @@ -71,8 +73,8 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
# Expose the needed ports
EXPOSE 8080 8000 1883

# Set the entrypoint
CMD ["python", "-m", "src.Omnispindle"]
# Set the entrypoint using uv and new package structure
CMD ["uv", "run", "-m", "Omnispindle"]

# Add metadata
LABEL maintainer="Danedens31@gmail.com"
Expand Down
41 changes: 0 additions & 41 deletions IMPLEMENTATION_PLAN.md

This file was deleted.

34 changes: 21 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
# Makefile for FastMCP Todo Server

.PHONY: install run test coverage clean status
.PHONY: install run run-dev test coverage clean status

# Install dependencies
# Install dependencies using uv
install:
uv pip install -r requirements.txt
uv pip install -r requirements-dev.txt
uv
uv sync

# Run the FastMCP server
# Install development dependencies
install-dev:
uv sync --dev

# Run the FastMCP server using uv
run:
python3.11 -m src.Omnispindle
COMMIT_HASH=$(git rev-parse --short HEAD)
mosquitto_pub -h localhost -p 4140 -t "status/$(DeName)/commit" -m "{\"commit_hash\": \"$(COMMIT_HASH)\"}"
uv run -m Omnispindle

# Alternative run command using the console script
run-script:
uv run omnispindle

# Run in development mode with reload
run-dev:
uv run -m Omnispindle

# deploy
deploy:
pm2 deploy ecosystem.config.js production

# Run tests
# Run tests using uv
test:
python3.11 -m pytest tests/
uv run pytest tests/

# Run tests with coverage
# Run tests with coverage using uv
coverage:
python3.11 -m pytest --cov=src tests/
uv run pytest --cov=src tests/

# Clean up __pycache__ directories
clean:
Expand Down
4 changes: 4 additions & 0 deletions src/Omnispindle/__init__.py → Omnispindle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
from .server import Omnispindle, server as existing_server
logger.debug("Server module imported successfully")

# Get transport mode from environment
MCP_TRANSPORT_MODE = os.getenv("MCP_TRANSPORT_MODE", "http")
logger.info(f"Using MCP transport mode: {MCP_TRANSPORT_MODE}")

# Ensure we only have one instance
logger.debug(f"Existing server: {existing_server}, thread: {threading.current_thread().name}")
server = existing_server
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 5 additions & 3 deletions src/Omnispindle/server.py → Omnispindle/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def publish_mqtt_status(topic, message, retain=False):


class Omnispindle(FastMCP):
def __init__(self, name: str = "todo-server", server_type: str = "sse"):
def __init__(self, name: str = "todo-server", server_type: str = "http"):
global _init_counter, _init_stack_traces
_init_counter += 1
current_thread = threading.current_thread().name
Expand Down Expand Up @@ -370,7 +370,7 @@ def register_tools(self, tools_list):
_instance = None
_instance_lock = threading.Lock()

def get_server_instance(name: str = "todo-server", server_type: str = "sse") -> Omnispindle:
def get_server_instance(name: str = "todo-server", server_type: str = "http") -> Omnispindle:
"""
Get the singleton instance of the Omnispindle server.
This ensures we only ever have one server instance.
Expand All @@ -395,5 +395,7 @@ def get_server_instance(name: str = "todo-server", server_type: str = "sse") ->

# Export the server instance
logger.warning("🚀 About to create the server instance in module initialization")
server = get_server_instance()
transport_mode = os.getenv("MCP_TRANSPORT_MODE", "http")
logger.info(f"Using MCP transport mode from environment: {transport_mode}")
server = get_server_instance(server_type=transport_mode)
logger.warning(f"🚀 Server instance created, init count = {_init_counter}")
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions src/Omnispindle/tools.py → Omnispindle/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
MONGODB_DB = os.getenv("MONGODB_DB", "swarmonomicon")
MONGODB_COLLECTION = os.getenv("MONGODB_COLLECTION", "todos")

# MCP Configuration
MCP_TOOL_TIMEOUT = int(os.getenv("MCP_TOOL_TIMEOUT", "30")) # Default 30 seconds, configurable

# Create MongoDB connection at module level
mongo_client = MongoClient(MONGODB_URI)
db = mongo_client[MONGODB_DB]
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion Todomill_projectorium
24 changes: 24 additions & 0 deletions config.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# MCP Server Configuration Example
# Copy this file to .env and modify as needed

# MCP Transport Configuration
MCP_TRANSPORT_MODE=http # Options: "http", "sse", "stdio"
MCP_TOOL_TIMEOUT=60 # Timeout in seconds for tool operations (default: 30)
MCP_ENABLE_SSE=false # Enable SSE for real-time features (optional)

# MongoDB Configuration
MONGODB_URI=mongodb://localhost:27017
MONGODB_DB=swarmonomicon
MONGODB_COLLECTION=todos

# MQTT Configuration
MQTT_HOST=localhost
MQTT_PORT=1883

# Server Configuration
HOST=0.0.0.0
PORT=8000
LOG_LEVEL=INFO

# Deployment Configuration
DeNa=omnispindle
36 changes: 31 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,41 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "Omnispindle"
name = "omnispindle"
version = "0.1.0"
description = "A FastMCP-based todo list server"
requires-python = ">=3.11"
dependencies = [
"fastmcp",
"pymongo",
"python-dotenv",
"fastmcp>=0.1.0",
"lmstudio",
"pymongo>=4.0.0",
"paho-mqtt>=2.0.0",
"python-dotenv>=0.19.0",
"uvicorn>=0.17.0",
"starlette>=0.17.1",
"scikit-learn>=0.0",
"numpy>=1.20.0",
"python-dateutil>=2.8.2",
]

[project.scripts]
omnispindle = "Omnispindle.__main__:main"

[tool.hatch.build.targets.wheel]
packages = ["src/Omnispindle"]
packages = ["Omnispindle"]

[tool.hatch.build.targets.sdist]
include = [
"/Omnispindle",
"/README.md",
]

[tool.uv]
dev-dependencies = [
"pytest==8.0.0",
"pytest-asyncio==0.23.5",
"black==24.1.1",
"flake8==7.0.0",
"mypy==1.8.0",
"pytest-cov",
]
File renamed without changes.
File renamed without changes.
79 changes: 79 additions & 0 deletions test_transport_modes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""
Test script to compare HTTP vs SSE transport modes for MCP timeout issues.
Run this to test which transport works better for your use case.
"""

import os
import asyncio
import time
from src.Omnispindle.tools import add_todo_tool

async def test_transport_mode(mode: str, timeout: int = 60):
"""Test a specific transport mode with given timeout"""
print(f"\n=== Testing {mode.upper()} Transport Mode ===")

# Set environment variables
os.environ["MCP_TRANSPORT_MODE"] = mode
os.environ["MCP_TOOL_TIMEOUT"] = str(timeout)

try:
from src.Omnispindle import run_server

print(f"✓ Successfully imported server with {mode} transport")
print(f"✓ Timeout set to {timeout} seconds")

# Test a simple operation
start_time = time.time()

# Simulate a tool call that might take some time
result = await add_todo_tool(
description=f"Test todo for {mode} transport - {int(start_time)}",
project="omnispindle",
priority="medium"
)

end_time = time.time()
elapsed = end_time - start_time

print(f"✓ Tool call completed in {elapsed:.2f} seconds")
print(f"✓ Result: {result}")

return True

except Exception as e:
print(f"✗ Error testing {mode} transport: {str(e)}")
return False

async def main():
"""Main test function"""
print("🔧 MCP Transport Mode Timeout Test")
print("=" * 50)

# Test HTTP mode (recommended)
http_success = await test_transport_mode("http", 60)

# Test SSE mode (your current setup)
sse_success = await test_transport_mode("sse", 60)

print("\n" + "=" * 50)
print("📊 RESULTS SUMMARY")
print("=" * 50)
print(f"HTTP Transport: {'✓ SUCCESS' if http_success else '✗ FAILED'}")
print(f"SSE Transport: {'✓ SUCCESS' if sse_success else '✗ FAILED'}")

if http_success and not sse_success:
print("\n💡 RECOMMENDATION: Switch to HTTP transport mode")
print(" Add this to your .env file: MCP_TRANSPORT_MODE=http")
elif not http_success and sse_success:
print("\n💡 RECOMMENDATION: Keep SSE transport mode")
print(" Your SSE setup is working correctly")
elif http_success and sse_success:
print("\n💡 RECOMMENDATION: Both modes work")
print(" Consider HTTP for better scalability")
else:
print("\n⚠️ ISSUE: Both transports failed")
print(" Check your server configuration and dependencies")

if __name__ == "__main__":
asyncio.run(main())
Loading