From 35499da9227a44b758b723751aad4ff115dc16fb Mon Sep 17 00:00:00 2001 From: yusyus Date: Sun, 19 Oct 2025 19:43:56 +0300 Subject: [PATCH] Add MCP configuration and setup scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete setup infrastructure for MCP integration: - example-mcp-config.json: Template Claude Code MCP configuration - setup_mcp.sh: Automated one-command setup script - test_mcp_server.py: Comprehensive test suite (25 tests, 100% pass) The setup script automates: - Dependency installation - Configuration file generation with absolute paths - Claude Code config directory creation - Validation and verification Tests cover: - All 6 MCP tool functions - Error handling and edge cases - Config validation - Page estimation - Skill packaging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- example-mcp-config.json | 11 + setup_mcp.sh | 222 ++++++++++++++ tests/test_mcp_server.py | 621 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 854 insertions(+) create mode 100644 example-mcp-config.json create mode 100755 setup_mcp.sh create mode 100644 tests/test_mcp_server.py diff --git a/example-mcp-config.json b/example-mcp-config.json new file mode 100644 index 0000000..80d946c --- /dev/null +++ b/example-mcp-config.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "skill-seeker": { + "command": "python3", + "args": [ + "/mnt/1ece809a-2821-4f10-aecb-fcdf34760c0b/Git/Skill_Seekers/mcp/server.py" + ], + "cwd": "/mnt/1ece809a-2821-4f10-aecb-fcdf34760c0b/Git/Skill_Seekers" + } + } +} diff --git a/setup_mcp.sh b/setup_mcp.sh new file mode 100755 index 0000000..294c034 --- /dev/null +++ b/setup_mcp.sh @@ -0,0 +1,222 @@ +#!/bin/bash +# Skill Seeker MCP Server - Quick Setup Script +# This script automates the MCP server setup for Claude Code + +set -e # Exit on error + +echo "==================================================" +echo "Skill Seeker MCP Server - Quick Setup" +echo "==================================================" +echo "" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Step 1: Check Python version +echo "Step 1: Checking Python version..." +if ! command -v python3 &> /dev/null; then + echo -e "${RED}❌ Error: python3 not found${NC}" + echo "Please install Python 3.7 or higher" + exit 1 +fi + +PYTHON_VERSION=$(python3 --version | cut -d' ' -f2) +echo -e "${GREEN}✓${NC} Python $PYTHON_VERSION found" +echo "" + +# Step 2: Get repository path +REPO_PATH=$(pwd) +echo "Step 2: Repository location" +echo "Path: $REPO_PATH" +echo "" + +# Step 3: Install dependencies +echo "Step 3: Installing Python dependencies..." +echo "This will install: mcp, requests, beautifulsoup4" +read -p "Continue? (y/n) " -n 1 -r +echo "" + +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Installing MCP server dependencies..." + pip3 install -r mcp/requirements.txt || { + echo -e "${RED}❌ Failed to install MCP dependencies${NC}" + exit 1 + } + + echo "Installing CLI tool dependencies..." + pip3 install requests beautifulsoup4 || { + echo -e "${RED}❌ Failed to install CLI dependencies${NC}" + exit 1 + } + + echo -e "${GREEN}✓${NC} Dependencies installed successfully" +else + echo "Skipping dependency installation" +fi +echo "" + +# Step 4: Test MCP server +echo "Step 4: Testing MCP server..." +timeout 3 python3 mcp/server.py 2>/dev/null || { + if [ $? -eq 124 ]; then + echo -e "${GREEN}✓${NC} MCP server starts correctly (timeout expected)" + else + echo -e "${YELLOW}⚠${NC} MCP server test inconclusive, but may still work" + fi +} +echo "" + +# Step 5: Optional - Run tests +echo "Step 5: Run test suite? (optional)" +read -p "Run MCP tests to verify everything works? (y/n) " -n 1 -r +echo "" + +if [[ $REPLY =~ ^[Yy]$ ]]; then + # Check if pytest is installed + if ! command -v pytest &> /dev/null; then + echo "Installing pytest..." + pip3 install pytest || { + echo -e "${YELLOW}⚠${NC} Could not install pytest, skipping tests" + } + fi + + if command -v pytest &> /dev/null; then + echo "Running MCP server tests..." + python3 -m pytest tests/test_mcp_server.py -v --tb=short || { + echo -e "${RED}❌ Some tests failed${NC}" + echo "The server may still work, but please check the errors above" + } + fi +else + echo "Skipping tests" +fi +echo "" + +# Step 6: Configure Claude Code +echo "Step 6: Configure Claude Code" +echo "==================================================" +echo "" +echo "You need to add this configuration to Claude Code:" +echo "" +echo -e "${YELLOW}Configuration file:${NC} ~/.config/claude-code/mcp.json" +echo "" +echo "Add this JSON configuration:" +echo "" +echo -e "${GREEN}{" +echo " \"mcpServers\": {" +echo " \"skill-seeker\": {" +echo " \"command\": \"python3\"," +echo " \"args\": [" +echo " \"$REPO_PATH/mcp/server.py\"" +echo " ]," +echo " \"cwd\": \"$REPO_PATH\"" +echo " }" +echo " }" +echo -e "}${NC}" +echo "" +echo "To configure automatically, run:" +echo "" +echo -e "${YELLOW} mkdir -p ~/.config/claude-code${NC}" +echo "" +echo "Then edit ~/.config/claude-code/mcp.json and add the configuration above" +echo "" +echo "Or use this one-liner (BE CAREFUL - this may overwrite existing config):" +echo "" +echo -e "${RED}cat > ~/.config/claude-code/mcp.json << 'EOF' +{ + \"mcpServers\": { + \"skill-seeker\": { + \"command\": \"python3\", + \"args\": [ + \"$REPO_PATH/mcp/server.py\" + ], + \"cwd\": \"$REPO_PATH\" + } + } +} +EOF${NC}" +echo "" + +# Ask if user wants auto-configure +echo "" +read -p "Auto-configure Claude Code now? (y/n) " -n 1 -r +echo "" + +if [[ $REPLY =~ ^[Yy]$ ]]; then + # Check if config already exists + if [ -f ~/.config/claude-code/mcp.json ]; then + echo -e "${YELLOW}⚠ Warning: ~/.config/claude-code/mcp.json already exists${NC}" + echo "Current contents:" + cat ~/.config/claude-code/mcp.json + echo "" + read -p "Overwrite? (y/n) " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Skipping auto-configuration" + echo "Please manually add the skill-seeker server to your config" + exit 0 + fi + fi + + # Create config directory + mkdir -p ~/.config/claude-code + + # Write configuration + cat > ~/.config/claude-code/mcp.json << EOF +{ + "mcpServers": { + "skill-seeker": { + "command": "python3", + "args": [ + "$REPO_PATH/mcp/server.py" + ], + "cwd": "$REPO_PATH" + } + } +} +EOF + + echo -e "${GREEN}✓${NC} Configuration written to ~/.config/claude-code/mcp.json" +else + echo "Skipping auto-configuration" + echo "Please manually configure Claude Code using the JSON above" +fi +echo "" + +# Step 7: Final instructions +echo "==================================================" +echo "Setup Complete!" +echo "==================================================" +echo "" +echo "Next steps:" +echo "" +echo " 1. ${YELLOW}Restart Claude Code${NC} (quit and reopen, don't just close window)" +echo " 2. In Claude Code, test with: ${GREEN}\"List all available configs\"${NC}" +echo " 3. You should see 6 Skill Seeker tools available" +echo "" +echo "Available MCP Tools:" +echo " • generate_config - Create new config files" +echo " • estimate_pages - Estimate scraping time" +echo " • scrape_docs - Scrape documentation" +echo " • package_skill - Create .zip files" +echo " • list_configs - Show available configs" +echo " • validate_config - Validate config files" +echo "" +echo "Example commands to try in Claude Code:" +echo " • ${GREEN}List all available configs${NC}" +echo " • ${GREEN}Validate configs/react.json${NC}" +echo " • ${GREEN}Generate config for Tailwind at https://tailwindcss.com/docs${NC}" +echo "" +echo "Documentation:" +echo " • MCP Setup Guide: ${YELLOW}docs/MCP_SETUP.md${NC}" +echo " • Full docs: ${YELLOW}README.md${NC}" +echo "" +echo "Troubleshooting:" +echo " • Check logs: ~/Library/Logs/Claude Code/ (macOS)" +echo " • Test server: python3 mcp/server.py" +echo " • Run tests: python3 -m pytest tests/test_mcp_server.py -v" +echo "" +echo "Happy skill creating! 🚀" diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py new file mode 100644 index 0000000..cb9695b --- /dev/null +++ b/tests/test_mcp_server.py @@ -0,0 +1,621 @@ +#!/usr/bin/env python3 +""" +Comprehensive test suite for Skill Seeker MCP Server +Tests all MCP tools and server functionality +""" + +import sys +import os +import unittest +import json +import tempfile +import shutil +import asyncio +from pathlib import Path +from unittest.mock import Mock, patch, AsyncMock, MagicMock + +# Add parent directory to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Import MCP package components (from installed package) +try: + from mcp.server import Server + from mcp.types import Tool, TextContent + MCP_AVAILABLE = True +except ImportError: + MCP_AVAILABLE = False + print("Warning: MCP package not available, skipping MCP tests") + +# Import our local MCP server module +if MCP_AVAILABLE: + # Add mcp directory to path to import our server module + mcp_dir = Path(__file__).parent.parent / "mcp" + sys.path.insert(0, str(mcp_dir)) + try: + import server as skill_seeker_server + except ImportError as e: + print(f"Warning: Could not import skill_seeker server: {e}") + skill_seeker_server = None + + +@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed") +class TestMCPServerInitialization(unittest.TestCase): + """Test MCP server initialization""" + + def test_server_import(self): + """Test that server module can be imported""" + from mcp import server as mcp_server_module + self.assertIsNotNone(mcp_server_module) + + def test_server_initialization(self): + """Test server initializes correctly""" + import mcp.server + app = mcp.server.Server("test-skill-seeker") + self.assertEqual(app.name, "test-skill-seeker") + + +@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed") +class TestListTools(unittest.IsolatedAsyncioTestCase): + """Test list_tools functionality""" + + async def test_list_tools_returns_tools(self): + """Test that list_tools returns all expected tools""" + tools = await skill_seeker_server.list_tools() + + self.assertIsInstance(tools, list) + self.assertGreater(len(tools), 0) + + # Check all expected tools are present + tool_names = [tool.name for tool in tools] + expected_tools = [ + "generate_config", + "estimate_pages", + "scrape_docs", + "package_skill", + "list_configs", + "validate_config" + ] + + for expected in expected_tools: + self.assertIn(expected, tool_names, f"Missing tool: {expected}") + + async def test_tool_schemas(self): + """Test that all tools have valid schemas""" + tools = await skill_seeker_server.list_tools() + + for tool in tools: + self.assertIsInstance(tool.name, str) + self.assertIsInstance(tool.description, str) + self.assertIn("inputSchema", tool.__dict__) + + # Verify schema has required structure + schema = tool.inputSchema + self.assertEqual(schema["type"], "object") + self.assertIn("properties", schema) + + +@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed") +class TestGenerateConfigTool(unittest.IsolatedAsyncioTestCase): + """Test generate_config tool""" + + async def asyncSetUp(self): + """Set up test environment""" + self.temp_dir = tempfile.mkdtemp() + self.original_cwd = os.getcwd() + os.chdir(self.temp_dir) + + async def asyncTearDown(self): + """Clean up test environment""" + os.chdir(self.original_cwd) + shutil.rmtree(self.temp_dir, ignore_errors=True) + + async def test_generate_config_basic(self): + """Test basic config generation""" + args = { + "name": "test-framework", + "url": "https://test-framework.dev/", + "description": "Test framework skill" + } + + result = await skill_seeker_server.generate_config_tool(args) + + self.assertIsInstance(result, list) + self.assertGreater(len(result), 0) + self.assertIsInstance(result[0], TextContent) + self.assertIn("✅", result[0].text) + + # Verify config file was created + config_path = Path("configs/test-framework.json") + self.assertTrue(config_path.exists()) + + # Verify config content + with open(config_path) as f: + config = json.load(f) + self.assertEqual(config["name"], "test-framework") + self.assertEqual(config["base_url"], "https://test-framework.dev/") + self.assertEqual(config["description"], "Test framework skill") + + async def test_generate_config_with_options(self): + """Test config generation with custom options""" + args = { + "name": "custom-framework", + "url": "https://custom.dev/", + "description": "Custom skill", + "max_pages": 200, + "rate_limit": 1.0 + } + + result = await skill_seeker_server.generate_config_tool(args) + + # Verify config has custom options + config_path = Path("configs/custom-framework.json") + with open(config_path) as f: + config = json.load(f) + self.assertEqual(config["max_pages"], 200) + self.assertEqual(config["rate_limit"], 1.0) + + async def test_generate_config_defaults(self): + """Test that default values are applied correctly""" + args = { + "name": "default-test", + "url": "https://test.dev/", + "description": "Test defaults" + } + + result = await skill_seeker_server.generate_config_tool(args) + + config_path = Path("configs/default-test.json") + with open(config_path) as f: + config = json.load(f) + self.assertEqual(config["max_pages"], 100) # Default + self.assertEqual(config["rate_limit"], 0.5) # Default + + +@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed") +class TestEstimatePagesTool(unittest.IsolatedAsyncioTestCase): + """Test estimate_pages tool""" + + async def asyncSetUp(self): + """Set up test environment""" + self.temp_dir = tempfile.mkdtemp() + self.original_cwd = os.getcwd() + os.chdir(self.temp_dir) + + # Create a test config + os.makedirs("configs", exist_ok=True) + self.config_path = Path("configs/test.json") + config_data = { + "name": "test", + "base_url": "https://example.com/", + "selectors": { + "main_content": "article", + "title": "h1", + "code_blocks": "pre" + }, + "rate_limit": 0.5, + "max_pages": 50 + } + with open(self.config_path, 'w') as f: + json.dump(config_data, f) + + async def asyncTearDown(self): + """Clean up test environment""" + os.chdir(self.original_cwd) + shutil.rmtree(self.temp_dir, ignore_errors=True) + + @patch('subprocess.run') + async def test_estimate_pages_success(self, mock_run): + """Test successful page estimation""" + # Mock successful subprocess run + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "Estimated 50 pages" + mock_run.return_value = mock_result + + args = { + "config_path": str(self.config_path) + } + + result = await skill_seeker_server.estimate_pages_tool(args) + + self.assertIsInstance(result, list) + self.assertIsInstance(result[0], TextContent) + self.assertIn("50 pages", result[0].text) + + @patch('subprocess.run') + async def test_estimate_pages_with_max_discovery(self, mock_run): + """Test page estimation with custom max_discovery""" + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "Estimated 100 pages" + mock_run.return_value = mock_result + + args = { + "config_path": str(self.config_path), + "max_discovery": 500 + } + + result = await skill_seeker_server.estimate_pages_tool(args) + + # Verify subprocess was called with correct args + mock_run.assert_called_once() + call_args = mock_run.call_args[0][0] + self.assertIn("--max-discovery", call_args) + self.assertIn("500", call_args) + + @patch('subprocess.run') + async def test_estimate_pages_error(self, mock_run): + """Test error handling in page estimation""" + mock_result = MagicMock() + mock_result.returncode = 1 + mock_result.stderr = "Config file not found" + mock_run.return_value = mock_result + + args = { + "config_path": "nonexistent.json" + } + + result = await skill_seeker_server.estimate_pages_tool(args) + + self.assertIn("Error", result[0].text) + + +@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed") +class TestScrapeDocsTool(unittest.IsolatedAsyncioTestCase): + """Test scrape_docs tool""" + + async def asyncSetUp(self): + """Set up test environment""" + self.temp_dir = tempfile.mkdtemp() + self.original_cwd = os.getcwd() + os.chdir(self.temp_dir) + + # Create test config + os.makedirs("configs", exist_ok=True) + self.config_path = Path("configs/test.json") + config_data = { + "name": "test", + "base_url": "https://example.com/", + "selectors": { + "main_content": "article", + "title": "h1", + "code_blocks": "pre" + } + } + with open(self.config_path, 'w') as f: + json.dump(config_data, f) + + async def asyncTearDown(self): + """Clean up test environment""" + os.chdir(self.original_cwd) + shutil.rmtree(self.temp_dir, ignore_errors=True) + + @patch('subprocess.run') + async def test_scrape_docs_basic(self, mock_run): + """Test basic documentation scraping""" + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "Scraping completed successfully" + mock_run.return_value = mock_result + + args = { + "config_path": str(self.config_path) + } + + result = await skill_seeker_server.scrape_docs_tool(args) + + self.assertIsInstance(result, list) + self.assertIn("success", result[0].text.lower()) + + @patch('subprocess.run') + async def test_scrape_docs_with_skip_scrape(self, mock_run): + """Test scraping with skip_scrape flag""" + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "Using cached data" + mock_run.return_value = mock_result + + args = { + "config_path": str(self.config_path), + "skip_scrape": True + } + + result = await skill_seeker_server.scrape_docs_tool(args) + + # Verify --skip-scrape was passed + call_args = mock_run.call_args[0][0] + self.assertIn("--skip-scrape", call_args) + + @patch('subprocess.run') + async def test_scrape_docs_with_dry_run(self, mock_run): + """Test scraping with dry_run flag""" + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "Dry run completed" + mock_run.return_value = mock_result + + args = { + "config_path": str(self.config_path), + "dry_run": True + } + + result = await skill_seeker_server.scrape_docs_tool(args) + + call_args = mock_run.call_args[0][0] + self.assertIn("--dry-run", call_args) + + @patch('subprocess.run') + async def test_scrape_docs_with_enhance_local(self, mock_run): + """Test scraping with local enhancement""" + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "Scraping with enhancement" + mock_run.return_value = mock_result + + args = { + "config_path": str(self.config_path), + "enhance_local": True + } + + result = await skill_seeker_server.scrape_docs_tool(args) + + call_args = mock_run.call_args[0][0] + self.assertIn("--enhance-local", call_args) + + +@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed") +class TestPackageSkillTool(unittest.IsolatedAsyncioTestCase): + """Test package_skill tool""" + + async def asyncSetUp(self): + """Set up test environment""" + self.temp_dir = tempfile.mkdtemp() + self.original_cwd = os.getcwd() + os.chdir(self.temp_dir) + + # Create a mock skill directory + self.skill_dir = Path("output/test-skill") + self.skill_dir.mkdir(parents=True) + (self.skill_dir / "SKILL.md").write_text("# Test Skill") + (self.skill_dir / "references").mkdir() + (self.skill_dir / "references/index.md").write_text("# Index") + + async def asyncTearDown(self): + """Clean up test environment""" + os.chdir(self.original_cwd) + shutil.rmtree(self.temp_dir, ignore_errors=True) + + @patch('subprocess.run') + async def test_package_skill_success(self, mock_run): + """Test successful skill packaging""" + mock_result = MagicMock() + mock_result.returncode = 0 + mock_result.stdout = "Package created: test-skill.zip" + mock_run.return_value = mock_result + + args = { + "skill_dir": str(self.skill_dir) + } + + result = await skill_seeker_server.package_skill_tool(args) + + self.assertIsInstance(result, list) + self.assertIn("test-skill", result[0].text) + + @patch('subprocess.run') + async def test_package_skill_error(self, mock_run): + """Test error handling in skill packaging""" + mock_result = MagicMock() + mock_result.returncode = 1 + mock_result.stderr = "Directory not found" + mock_run.return_value = mock_result + + args = { + "skill_dir": "nonexistent-dir" + } + + result = await skill_seeker_server.package_skill_tool(args) + + self.assertIn("Error", result[0].text) + + +@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed") +class TestListConfigsTool(unittest.IsolatedAsyncioTestCase): + """Test list_configs tool""" + + async def asyncSetUp(self): + """Set up test environment""" + self.temp_dir = tempfile.mkdtemp() + self.original_cwd = os.getcwd() + os.chdir(self.temp_dir) + + # Create test configs + os.makedirs("configs", exist_ok=True) + + configs = [ + { + "name": "test1", + "description": "Test 1 skill", + "base_url": "https://test1.dev/" + }, + { + "name": "test2", + "description": "Test 2 skill", + "base_url": "https://test2.dev/" + } + ] + + for config in configs: + path = Path(f"configs/{config['name']}.json") + with open(path, 'w') as f: + json.dump(config, f) + + async def asyncTearDown(self): + """Clean up test environment""" + os.chdir(self.original_cwd) + shutil.rmtree(self.temp_dir, ignore_errors=True) + + async def test_list_configs_success(self): + """Test listing all configs""" + result = await skill_seeker_server.list_configs_tool({}) + + self.assertIsInstance(result, list) + self.assertIsInstance(result[0], TextContent) + self.assertIn("test1", result[0].text) + self.assertIn("test2", result[0].text) + self.assertIn("https://test1.dev/", result[0].text) + self.assertIn("https://test2.dev/", result[0].text) + + async def test_list_configs_empty(self): + """Test listing configs when directory is empty""" + # Remove all configs + for config_file in Path("configs").glob("*.json"): + config_file.unlink() + + result = await skill_seeker_server.list_configs_tool({}) + + self.assertIn("No config files found", result[0].text) + + async def test_list_configs_no_directory(self): + """Test listing configs when directory doesn't exist""" + # Remove configs directory + shutil.rmtree("configs") + + result = await skill_seeker_server.list_configs_tool({}) + + self.assertIn("No configs directory", result[0].text) + + +@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed") +class TestValidateConfigTool(unittest.IsolatedAsyncioTestCase): + """Test validate_config tool""" + + async def asyncSetUp(self): + """Set up test environment""" + self.temp_dir = tempfile.mkdtemp() + self.original_cwd = os.getcwd() + os.chdir(self.temp_dir) + + os.makedirs("configs", exist_ok=True) + + async def asyncTearDown(self): + """Clean up test environment""" + os.chdir(self.original_cwd) + shutil.rmtree(self.temp_dir, ignore_errors=True) + + async def test_validate_valid_config(self): + """Test validating a valid config""" + # Create valid config + config_path = Path("configs/valid.json") + valid_config = { + "name": "valid-test", + "base_url": "https://example.com/", + "selectors": { + "main_content": "article", + "title": "h1", + "code_blocks": "pre" + }, + "rate_limit": 0.5, + "max_pages": 100 + } + with open(config_path, 'w') as f: + json.dump(valid_config, f) + + args = { + "config_path": str(config_path) + } + + result = await skill_seeker_server.validate_config_tool(args) + + self.assertIsInstance(result, list) + self.assertIn("✅", result[0].text) + self.assertIn("valid", result[0].text.lower()) + + async def test_validate_invalid_config(self): + """Test validating an invalid config""" + # Create invalid config + config_path = Path("configs/invalid.json") + invalid_config = { + "name": "invalid@name", # Invalid characters + "base_url": "example.com" # Missing protocol + } + with open(config_path, 'w') as f: + json.dump(invalid_config, f) + + args = { + "config_path": str(config_path) + } + + result = await skill_seeker_server.validate_config_tool(args) + + self.assertIn("❌", result[0].text) + + async def test_validate_nonexistent_config(self): + """Test validating a nonexistent config""" + args = { + "config_path": "configs/nonexistent.json" + } + + result = await skill_seeker_server.validate_config_tool(args) + + self.assertIn("Error", result[0].text) + + +@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed") +class TestCallToolRouter(unittest.IsolatedAsyncioTestCase): + """Test call_tool routing""" + + async def test_call_tool_unknown(self): + """Test calling an unknown tool""" + result = await skill_seeker_server.call_tool("unknown_tool", {}) + + self.assertIsInstance(result, list) + self.assertIn("Unknown tool", result[0].text) + + async def test_call_tool_exception_handling(self): + """Test that exceptions are caught and returned as errors""" + # Call with invalid arguments that should cause an exception + result = await skill_seeker_server.call_tool("generate_config", {}) + + self.assertIsInstance(result, list) + self.assertIn("Error", result[0].text) + + +@unittest.skipUnless(MCP_AVAILABLE, "MCP package not installed") +class TestMCPServerIntegration(unittest.IsolatedAsyncioTestCase): + """Integration tests for MCP server""" + + async def test_full_workflow_simulation(self): + """Test complete workflow: generate config -> validate -> estimate""" + temp_dir = tempfile.mkdtemp() + original_cwd = os.getcwd() + os.chdir(temp_dir) + + try: + # Step 1: Generate config using skill_seeker_server + generate_args = { + "name": "workflow-test", + "url": "https://workflow-test.dev/", + "description": "Workflow test skill" + } + result1 = await skill_seeker_server.generate_config_tool(generate_args) + self.assertIn("✅", result1[0].text) + + # Step 2: Validate config + validate_args = { + "config_path": "configs/workflow-test.json" + } + result2 = await skill_seeker_server.validate_config_tool(validate_args) + self.assertIn("✅", result2[0].text) + + # Step 3: List configs + result3 = await skill_seeker_server.list_configs_tool({}) + self.assertIn("workflow-test", result3[0].text) + + finally: + os.chdir(original_cwd) + shutil.rmtree(temp_dir, ignore_errors=True) + + +if __name__ == '__main__': + unittest.main()