chore(F.15): FastMCP 3.x upgrade + legacy cleanup — v3.5.0
Some checks failed
Release / Test before release (push) Has been cancelled
Release / Publish to PyPI (push) Has been cancelled
Release / Publish to Docker Hub (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled

- Upgrade fastmcp from >=2.14.0,<3.0.0 to >=3.0.0,<4.0.0
- Remove server_multi.py (legacy multi-endpoint server)
- Remove core/project_manager.py (legacy project manager)
- Refactor HealthMonitor to use SiteManager exclusively
- Replace _tool_manager._tools with tracked _tool_counts dict
- Clean core/__init__.py exports
- Update CHANGELOG, CLAUDE.md, tests
- Auto-format fixes (black + ruff)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 03:29:38 +02:00
parent c47ec7fd28
commit 1615daf1a7
16 changed files with 102 additions and 1392 deletions

View File

@@ -5,6 +5,25 @@ All notable changes to MCP Hub will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.5.0] — 2026-04-02
### FastMCP Upgrade & Legacy Cleanup (Track F.15)
Major cleanup release: upgraded to FastMCP 3.x, removed all legacy modules, and simplified the architecture.
#### Changed
- **FastMCP 3.x**: Upgraded from `fastmcp>=2.14.0,<3.0.0` to `fastmcp>=3.0.0,<4.0.0`
- **HealthMonitor refactored**: Removed `ProjectManager` dependency — now uses `SiteManager` exclusively for site discovery and health checks
- **Endpoint introspection**: Replaced internal `_tool_manager._tools` access with tracked `_tool_counts` dict in `MCPEndpointFactory`
#### Removed
- **`server_multi.py`**: Legacy multi-endpoint server entry point (replaced by unified `server.py` since v3.0)
- **`core/project_manager.py`**: Legacy project manager shell — HealthMonitor no longer depends on it
- **Legacy exports**: Removed `ProjectManager` and `get_project_manager` from `core/__init__.py`
- References from `pyproject.toml` (py-modules, ruff per-file-ignores), community build script, and documentation
---
## [3.4.0] — 2026-03-31 ## [3.4.0] — 2026-03-31
### OpenPanel Plugin — Public Release (Track F.10) ### OpenPanel Plugin — Public Release (Track F.10)

View File

@@ -69,7 +69,6 @@ All configured in `pyproject.toml`:
``` ```
├── server.py # Primary entry point ├── server.py # Primary entry point
├── server_multi.py # Alternative multi-endpoint server
├── core/ # Layer 1: Core system modules ├── core/ # Layer 1: Core system modules
├── plugins/ # Layer 2: Plugin system (9 plugins) ├── plugins/ # Layer 2: Plugin system (9 plugins)
├── core/templates/ # Jinja2 templates (dashboard + OAuth) ├── core/templates/ # Jinja2 templates (dashboard + OAuth)
@@ -93,8 +92,6 @@ Layer 3: API & Web UI (server.py + core/dashboard/) — FastMCP server, Starle
### Entry Points ### Entry Points
- **`server.py`** (~3500 lines) — Primary entry point. Handles FastMCP server, Starlette routes, middleware, plugins. - **`server.py`** (~3500 lines) — Primary entry point. Handles FastMCP server, Starlette routes, middleware, plugins.
- **`server_multi.py`** — Alternative multi-endpoint server (legacy, predates unified `server.py` endpoints).
### Multi-Endpoint Architecture ### Multi-Endpoint Architecture
``` ```
@@ -171,10 +168,6 @@ Web UI at the server root, built with Starlette + Jinja2 + HTMX + Tailwind CSS.
**Admin pages**: Login, Home, Projects, API Keys, OAuth Clients, Audit Logs, Health, Settings. **Admin pages**: Login, Home, Projects, API Keys, OAuth Clients, Audit Logs, Health, Settings.
**User pages**: Login (OAuth), Home, My Sites (list/add/edit/test/delete), Connect (API keys + config snippets + OAuth clients), Profile. **User pages**: Login (OAuth), Home, My Sites (list/add/edit/test/delete), Connect (API keys + config snippets + OAuth clients), Profile.
### Legacy Modules (Deprecated)
`core/project_manager.py` (retained for HealthMonitor compatibility), `core/site_registry.py`, `core/unified_tools.py` — kept for backward compatibility. New code should use `SiteManager`, `ToolRegistry`, and `ToolGenerator` instead.
## Commit Style ## Commit Style
``` ```
@@ -197,7 +190,6 @@ v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claud
## Gotchas ## Gotchas
- Test files exist in both `tests/` (proper) and root directory (legacy `test_*.py`). Run `pytest tests/` for organized tests only. - Test files exist in both `tests/` (proper) and root directory (legacy `test_*.py`). Run `pytest tests/` for organized tests only.
- `server_multi.py` is the alternative multi-endpoint entry point; `server.py` is the primary
- `wordpress-plugin/` contains companion WP plugins (openpanel, airano-mcp-seo-bridge) — these are PHP, not Python - `wordpress-plugin/` contains companion WP plugins (openpanel, airano-mcp-seo-bridge) — these are PHP, not Python
- `env.example` has "FUTURE" labels for Supabase/Gitea but both are fully implemented - `env.example` has "FUTURE" labels for Supabase/Gitea but both are fully implemented
- 4 plugins are tested for public use: WordPress, WooCommerce, Supabase, OpenPanel. Others are admin-only or disabled. - 4 plugins are tested for public use: WordPress, WooCommerce, Supabase, OpenPanel. Others are admin-only or disabled.

View File

@@ -37,14 +37,9 @@ from core.health import (
initialize_health_monitor, initialize_health_monitor,
) )
# Project and Site Management
from core.project_manager import ProjectManager, get_project_manager
# Rate Limiting # Rate Limiting
from core.rate_limiter import RateLimitConfig, RateLimiter, get_rate_limiter from core.rate_limiter import RateLimitConfig, RateLimiter, get_rate_limiter
from core.site_manager import SiteConfig, SiteManager, get_site_manager from core.site_manager import SiteConfig, SiteManager, get_site_manager
# Legacy (kept for backward compatibility, will be removed in v2.0)
from core.tool_generator import ToolGenerator from core.tool_generator import ToolGenerator
# Tool Management (Option B architecture) # Tool Management (Option B architecture)
@@ -56,9 +51,7 @@ __all__ = [
"get_auth_manager", "get_auth_manager",
"APIKeyManager", "APIKeyManager",
"get_api_key_manager", "get_api_key_manager",
# Project/Site Management # Site Management
"ProjectManager",
"get_project_manager",
"SiteManager", "SiteManager",
"SiteConfig", "SiteConfig",
"get_site_manager", "get_site_manager",

View File

@@ -10,12 +10,12 @@ import json
import logging import logging
import os import os
import secrets import secrets
import bcrypt
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
import bcrypt
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -850,9 +850,7 @@ async def get_all_projects(
is_admin = False is_admin = False
current_user_id = None current_user_id = None
if user_session: if user_session:
if hasattr(user_session, "user_type") and user_session.user_type == "master": if hasattr(user_session, "user_type") and user_session.user_type == "master" or isinstance(user_session, dict) and user_session.get("role") == "admin":
is_admin = True
elif isinstance(user_session, dict) and user_session.get("role") == "admin":
is_admin = True is_admin = True
elif isinstance(user_session, dict) and "user_id" in user_session: elif isinstance(user_session, dict) and "user_id" in user_session:
current_user_id = user_session["user_id"] current_user_id = user_session["user_id"]

View File

@@ -48,6 +48,7 @@ class MCPEndpointFactory:
self.tool_registry = tool_registry self.tool_registry = tool_registry
self.middleware_classes = middleware_classes or [] self.middleware_classes = middleware_classes or []
self.endpoints: dict[str, FastMCP] = {} self.endpoints: dict[str, FastMCP] = {}
self._tool_counts: dict[str, int] = {}
self._tool_handlers: dict[str, Callable] = {} self._tool_handlers: dict[str, Callable] = {}
def register_tool_handler(self, tool_name: str, handler: Callable): def register_tool_handler(self, tool_name: str, handler: Callable):
@@ -92,8 +93,9 @@ class MCPEndpointFactory:
for middleware in custom_middleware: for middleware in custom_middleware:
mcp.add_middleware(middleware) mcp.add_middleware(middleware)
# Store endpoint # Store endpoint and tool count
self.endpoints[config.path] = mcp self.endpoints[config.path] = mcp
self._tool_counts[config.path] = len(tools)
logger.info(f" - Endpoint {config.path} created successfully") logger.info(f" - Endpoint {config.path} created successfully")
@@ -282,9 +284,7 @@ class MCPEndpointFactory:
""" """
info = [] info = []
for path, mcp in self.endpoints.items(): for path, mcp in self.endpoints.items():
# Get tool count tool_count = self._tool_counts.get(path, 0)
# Note: This requires accessing FastMCP internals
tool_count = len(mcp._tool_manager._tools) if hasattr(mcp, "_tool_manager") else 0
info.append( info.append(
{ {

View File

@@ -149,11 +149,7 @@ class EndpointRegistry:
tool_count = 0 tool_count = 0
if mcp: if mcp:
# Try to get tool count from FastMCP tool_count = self.factory._tool_counts.get(path, 0)
try:
tool_count = len(mcp._tool_manager._tools)
except AttributeError:
pass
endpoints.append( endpoints.append(
EndpointInfo( EndpointInfo(

View File

@@ -23,7 +23,6 @@ from pathlib import Path
from typing import Any from typing import Any
from core.audit_log import AuditLogger from core.audit_log import AuditLogger
from core.project_manager import ProjectManager
from core.site_manager import SiteManager from core.site_manager import SiteManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -130,7 +129,6 @@ class HealthMonitor:
def __init__( def __init__(
self, self,
project_manager: ProjectManager,
audit_logger: AuditLogger | None = None, audit_logger: AuditLogger | None = None,
metrics_retention_hours: int = 24, metrics_retention_hours: int = 24,
max_metrics_per_project: int = 1000, max_metrics_per_project: int = 1000,
@@ -140,13 +138,11 @@ class HealthMonitor:
Initialize health monitor. Initialize health monitor.
Args: Args:
project_manager: Project manager instance
audit_logger: Optional audit logger for logging health events audit_logger: Optional audit logger for logging health events
metrics_retention_hours: Hours to retain historical metrics metrics_retention_hours: Hours to retain historical metrics
max_metrics_per_project: Maximum metrics to store per project max_metrics_per_project: Maximum metrics to store per project
site_manager: Optional SiteManager for comprehensive site discovery site_manager: SiteManager for site discovery
""" """
self.project_manager = project_manager
self.site_manager = site_manager self.site_manager = site_manager
self.audit_logger = audit_logger self.audit_logger = audit_logger
self.metrics_retention_hours = metrics_retention_hours self.metrics_retention_hours = metrics_retention_hours
@@ -410,15 +406,7 @@ class HealthMonitor:
start_time = time.time() start_time = time.time()
try: try:
# Get plugin instance from ProjectManager if self.site_manager:
plugin = self.project_manager.projects.get(project_id)
if plugin:
# Perform health check via plugin instance
health_result = await plugin.health_check()
elif self.site_manager:
# Site exists in SiteManager but not legacy ProjectManager
# Create a temporary plugin instance for a proper health check
health_result = await self._site_manager_health_check(project_id) health_result = await self._site_manager_health_check(project_id)
else: else:
return ProjectHealthStatus( return ProjectHealthStatus(
@@ -524,7 +512,7 @@ class HealthMonitor:
async def _site_manager_health_check(self, project_id: str) -> dict[str, Any]: async def _site_manager_health_check(self, project_id: str) -> dict[str, Any]:
""" """
Health check for sites managed by SiteManager (not in legacy ProjectManager). Health check for a site via SiteManager.
Creates a temporary plugin instance and calls its health_check() method, Creates a temporary plugin instance and calls its health_check() method,
falling back to a basic HTTP check if plugin instantiation fails. falling back to a basic HTTP check if plugin instantiation fails.
@@ -650,8 +638,8 @@ class HealthMonitor:
""" """
health_statuses = {} health_statuses = {}
# Collect all known project/site IDs from both sources # Collect all known site IDs from SiteManager
all_project_ids = set(self.project_manager.projects.keys()) all_project_ids = set()
if self.site_manager: if self.site_manager:
for site_info in self.site_manager.list_all_sites(): for site_info in self.site_manager.list_all_sites():
all_project_ids.add(site_info["full_id"]) all_project_ids.add(site_info["full_id"])
@@ -860,7 +848,6 @@ def get_health_monitor() -> HealthMonitor | None:
def initialize_health_monitor( def initialize_health_monitor(
project_manager: ProjectManager,
audit_logger: AuditLogger | None = None, audit_logger: AuditLogger | None = None,
site_manager: SiteManager | None = None, site_manager: SiteManager | None = None,
**kwargs, **kwargs,
@@ -869,16 +856,13 @@ def initialize_health_monitor(
Initialize the global health monitor. Initialize the global health monitor.
Args: Args:
project_manager: Project manager instance
audit_logger: Optional audit logger audit_logger: Optional audit logger
site_manager: Optional SiteManager for comprehensive site discovery site_manager: SiteManager for site discovery
**kwargs: Additional configuration options **kwargs: Additional configuration options
Returns: Returns:
HealthMonitor instance HealthMonitor instance
""" """
global _health_monitor global _health_monitor
_health_monitor = HealthMonitor( _health_monitor = HealthMonitor(audit_logger=audit_logger, site_manager=site_manager, **kwargs)
project_manager, audit_logger, site_manager=site_manager, **kwargs
)
return _health_monitor return _health_monitor

View File

@@ -1,139 +0,0 @@
"""
Project Manager (Legacy)
Retained for backward compatibility with HealthMonitor.
Sites are now managed via the web dashboard (DB-based).
"""
import logging
from typing import Any
from plugins import BasePlugin
logger = logging.getLogger(__name__)
class ProjectManager:
"""
Legacy project manager — retained for HealthMonitor compatibility.
Sites are now managed via the web dashboard and stored in SQLite.
"""
def __init__(self):
"""Initialize project manager."""
self.projects: dict[str, BasePlugin] = {}
self.logger = logging.getLogger("ProjectManager")
def get_project(self, full_id: str) -> BasePlugin | None:
"""
Get a project plugin instance.
Args:
full_id: Full project identifier (plugin_type_project_id)
Returns:
Plugin instance or None
"""
return self.projects.get(full_id)
def get_all_projects(self) -> dict[str, BasePlugin]:
"""Get all project instances."""
return self.projects.copy()
def get_projects_by_type(self, plugin_type: str) -> dict[str, BasePlugin]:
"""
Get all projects of a specific type.
Args:
plugin_type: Plugin type to filter by
Returns:
Dict of project_id -> plugin
"""
prefix = plugin_type + "_"
return {
full_id: plugin
for full_id, plugin in self.projects.items()
if full_id.startswith(prefix)
}
def get_all_tools(self) -> list[dict[str, Any]]:
"""
Get all MCP tools from all projects.
Returns:
List of tool definitions
"""
all_tools = []
for full_id, plugin in self.projects.items():
try:
tools = plugin.get_tools()
all_tools.extend(tools)
self.logger.debug(f"Loaded {len(tools)} tools from {full_id}")
except Exception as e:
self.logger.error(f"Error loading tools from {full_id}: {e}", exc_info=True)
self.logger.debug(f"Total tools loaded: {len(all_tools)}")
return all_tools
async def check_all_health(self) -> dict[str, dict[str, Any]]:
"""
Check health of all projects.
Returns:
Dict mapping project ID to health status
"""
health_results = {}
for full_id, plugin in self.projects.items():
try:
health = await plugin.health_check()
health_results[full_id] = health
except Exception as e:
health_results[full_id] = {
"healthy": False,
"message": f"Health check failed: {str(e)}",
}
return health_results
def get_project_info(self, full_id: str) -> dict[str, Any] | None:
"""
Get information about a specific project.
Args:
full_id: Full project identifier
Returns:
Project info dict or None
"""
plugin = self.get_project(full_id)
if plugin:
return plugin.get_project_info()
return None
def list_projects(self) -> list[dict[str, Any]]:
"""
List all projects with basic information.
Returns:
List of project info dicts
"""
return [
{"id": full_id, "type": plugin.get_plugin_name(), "project_id": plugin.project_id}
for full_id, plugin in self.projects.items()
]
# Global project manager instance
_project_manager: ProjectManager | None = None
def get_project_manager() -> ProjectManager:
"""Get the global project manager instance."""
global _project_manager
if _project_manager is None:
_project_manager = ProjectManager()
return _project_manager

View File

@@ -483,8 +483,6 @@ async def delete_user_factor(client: SupabaseClient, user_id: str, factor_id: st
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False) return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
async def search_users( async def search_users(
client: SupabaseClient, query: str, page: int = 1, per_page: int = 50 client: SupabaseClient, query: str, page: int = 1, per_page: int = 50
) -> str: ) -> str:

View File

@@ -77,8 +77,12 @@ class WPCLIManager:
try: try:
# First, test if we have Docker socket access # First, test if we have Docker socket access
test_process = await asyncio.create_subprocess_exec( test_process = await asyncio.create_subprocess_exec(
"docker", "version", "--format", "{{.Server.Version}}", "docker",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE "version",
"--format",
"{{.Server.Version}}",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
) )
test_stdout, test_stderr = await asyncio.wait_for( test_stdout, test_stderr = await asyncio.wait_for(
@@ -99,10 +103,15 @@ class WPCLIManager:
# Now check for our specific container using exact name match # Now check for our specific container using exact name match
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
"docker", "ps", "--all", "docker",
"--filter", f"name=^{self.container_name}$", "ps",
"--format", "{{.Names}}|{{.Status}}", "--all",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE "--filter",
f"name=^{self.container_name}$",
"--format",
"{{.Names}}|{{.Status}}",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
) )
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0) stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0)
@@ -117,8 +126,13 @@ class WPCLIManager:
if not output: if not output:
# Container not found - get list of available containers for helpful error # Container not found - get list of available containers for helpful error
list_process = await asyncio.create_subprocess_exec( list_process = await asyncio.create_subprocess_exec(
"docker", "ps", "--all", "--format", "{{.Names}}", "docker",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE "ps",
"--all",
"--format",
"{{.Names}}",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
) )
list_stdout, _ = await list_process.communicate() list_stdout, _ = await list_process.communicate()
available = list_stdout.decode().strip().split("\n") if list_stdout else [] available = list_stdout.decode().strip().split("\n") if list_stdout else []
@@ -170,9 +184,14 @@ class WPCLIManager:
try: try:
# Try to run wp --version # Try to run wp --version
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
"docker", "exec", self.container_name, "docker",
"wp", "--version", "--allow-root", "exec",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE self.container_name,
"wp",
"--version",
"--allow-root",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
) )
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0) stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0)
@@ -291,7 +310,9 @@ class WPCLIManager:
) )
# 4. Build docker exec command (split command into args to prevent shell injection) # 4. Build docker exec command (split command into args to prevent shell injection)
cmd_parts = ["docker", "exec", self.container_name, "wp"] + command.split() + ["--allow-root"] cmd_parts = (
["docker", "exec", self.container_name, "wp"] + command.split() + ["--allow-root"]
)
self.logger.info(f"Executing: {' '.join(cmd_parts)}") self.logger.info(f"Executing: {' '.join(cmd_parts)}")
@@ -582,17 +603,29 @@ class WPCLIManager:
try: try:
# Try GNU stat first, fall back to BSD stat # Try GNU stat first, fall back to BSD stat
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
"docker", "exec", self.container_name, "docker",
"stat", "-c", "%s", export_path, "exec",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE self.container_name,
"stat",
"-c",
"%s",
export_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
) )
stdout, _ = await asyncio.wait_for(process.communicate(), timeout=5.0) stdout, _ = await asyncio.wait_for(process.communicate(), timeout=5.0)
if process.returncode != 0: if process.returncode != 0:
# BSD stat fallback # BSD stat fallback
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
"docker", "exec", self.container_name, "docker",
"stat", "-f", "%z", export_path, "exec",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE self.container_name,
"stat",
"-f",
"%z",
export_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
) )
stdout, _ = await asyncio.wait_for(process.communicate(), timeout=5.0) stdout, _ = await asyncio.wait_for(process.communicate(), timeout=5.0)

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "mcphub-server" name = "mcphub-server"
version = "3.4.0" version = "3.5.0"
description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)" description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)"
authors = [ authors = [
{name = "MCP Hub", email = "contact@mcphub.dev"} {name = "MCP Hub", email = "contact@mcphub.dev"}
@@ -28,7 +28,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"fastmcp>=2.14.0,<3.0.0", "fastmcp>=3.0.0,<4.0.0",
"httpx>=0.25.0", "httpx>=0.25.0",
"aiohttp>=3.9.0", "aiohttp>=3.9.0",
"pydantic>=2.5.0", "pydantic>=2.5.0",
@@ -69,7 +69,7 @@ requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.setuptools] [tool.setuptools]
py-modules = ["server", "server_multi"] py-modules = ["server"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["core*", "plugins*"] include = ["core*", "plugins*"]
@@ -208,7 +208,6 @@ unfixable = []
"tests/*" = ["S101", "B007"] "tests/*" = ["S101", "B007"]
"tests/legacy/*" = ["E402", "B007", "F401"] "tests/legacy/*" = ["E402", "B007", "F401"]
"server.py" = ["E402", "F405", "F403", "UP045", "B023"] # Late imports + star imports + dynamic types "server.py" = ["E402", "F405", "F403", "UP045", "B023"] # Late imports + star imports + dynamic types
"server_multi.py" = ["E402", "F405", "F403", "UP045"]
"core/__init__.py" = ["F401", "F403"] # Re-exports "core/__init__.py" = ["F401", "F403"] # Re-exports
"plugins/__init__.py" = ["F401"] "plugins/__init__.py" = ["F401"]
"plugins/*/__init__.py" = ["F401", "F403", "F405"] "plugins/*/__init__.py" = ["F401", "F403", "F405"]

View File

@@ -51,7 +51,6 @@ from core import (
get_api_key_manager, get_api_key_manager,
get_audit_logger, get_audit_logger,
get_auth_manager, get_auth_manager,
get_project_manager,
get_rate_limiter, get_rate_limiter,
# Core architecture modules # Core architecture modules
get_site_manager, get_site_manager,
@@ -67,6 +66,8 @@ from core.dashboard.routes import (
api_get_config, api_get_config,
api_list_keys, api_list_keys,
api_list_sites, api_list_sites,
# K.5: Settings routes
api_save_setting,
api_test_site, api_test_site,
api_update_site, api_update_site,
# E.2: OAuth Social Login routes # E.2: OAuth Social Login routes
@@ -108,8 +109,6 @@ from core.dashboard.routes import (
# F.3: Service pages # F.3: Service pages
dashboard_service_page, dashboard_service_page,
dashboard_services_list, dashboard_services_list,
# K.5: Settings routes
api_save_setting,
dashboard_settings_page, dashboard_settings_page,
# E.3: Site Management pages # E.3: Site Management pages
dashboard_sites_add, dashboard_sites_add,
@@ -299,7 +298,6 @@ logger.info("Jinja2 template engine initialized")
# Initialize managers # Initialize managers
auth_manager = get_auth_manager() auth_manager = get_auth_manager()
api_key_manager = get_api_key_manager() api_key_manager = get_api_key_manager()
project_manager = get_project_manager()
audit_logger = get_audit_logger() audit_logger = get_audit_logger()
csrf_manager = get_csrf_manager() csrf_manager = get_csrf_manager()
@@ -1106,7 +1104,6 @@ from core import initialize_health_monitor
# Initialize health monitor # Initialize health monitor
health_monitor = initialize_health_monitor( health_monitor = initialize_health_monitor(
project_manager=project_manager,
audit_logger=audit_logger, audit_logger=audit_logger,
site_manager=site_manager, site_manager=site_manager,
metrics_retention_hours=24, metrics_retention_hours=24,
@@ -1385,15 +1382,11 @@ async def get_project_info(project_id: str) -> str:
import json import json
try: try:
# Try legacy ProjectManager first info = None
info = project_manager.get_project_info(project_id) for site_data in site_manager.list_all_sites():
if site_data["full_id"] == project_id:
# Fallback to SiteManager if legacy finds nothing info = site_data
if info is None: break
for site_data in site_manager.list_all_sites():
if site_data["full_id"] == project_id:
info = site_data
break
if info is None: if info is None:
return f"Project '{project_id}' not found. Use list_projects to see available projects." return f"Project '{project_id}' not found. Use list_projects to see available projects."
@@ -3783,12 +3776,11 @@ Use get_endpoints() to see all available MCP endpoints."""
import json import json
try: try:
info = project_manager.get_project_info(project_id) info = None
if info is None: for site_data in site_manager.list_all_sites():
for site_data in site_manager.list_all_sites(): if site_data["full_id"] == project_id:
if site_data["full_id"] == project_id: info = site_data
info = site_data break
break
if info is None: if info is None:
return f"Project '{project_id}' not found. Use list_projects to see available projects." return f"Project '{project_id}' not found. Use list_projects to see available projects."
return json.dumps(info, indent=2) return json.dumps(info, indent=2)

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ if sys.platform == "win32":
# Add project root to path # Add project root to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from core import AuditLogger, ProjectManager, initialize_health_monitor from core import AuditLogger, initialize_health_monitor
async def test_health_monitor(): async def test_health_monitor():
@@ -35,11 +35,9 @@ async def test_health_monitor():
# Initialize components # Initialize components
print("\n1. Initializing components...") print("\n1. Initializing components...")
project_manager = ProjectManager()
audit_logger = AuditLogger() audit_logger = AuditLogger()
health_monitor = initialize_health_monitor( health_monitor = initialize_health_monitor(
project_manager=project_manager,
audit_logger=audit_logger, audit_logger=audit_logger,
metrics_retention_hours=24, metrics_retention_hours=24,
max_metrics_per_project=1000, max_metrics_per_project=1000,

View File

@@ -1,6 +1,5 @@
"""Tests for Admin System Unification (F.4).""" """Tests for Admin System Unification (F.4)."""
from core.dashboard.auth import ( from core.dashboard.auth import (
DashboardSession, DashboardSession,
get_session_display_info, get_session_display_info,