Files
skill-seekers-reference/src/skill_seekers/mcp/server_fastmcp.py
yusyus 891ce2dbc6 feat: Complete multi-platform feature parity implementation
This commit implements full feature parity across all platforms (Claude, Gemini, OpenAI, Markdown) and all skill modes (Docs, GitHub, PDF, Unified, Local Repo).

## Core Changes

### Phase 1: MCP Package Tool Multi-Platform Support
- Added `target` parameter to `package_skill_tool()` in packaging_tools.py
- Updated MCP server definition to expose `target` parameter
- Platform-specific packaging: ZIP for Claude/OpenAI/Markdown, tar.gz for Gemini
- Platform-specific output messages and instructions

### Phase 2: MCP Upload Tool Multi-Platform Support
- Added `target` parameter to `upload_skill_tool()` in packaging_tools.py
- Added optional `api_key` parameter for API key override
- Updated MCP server definition with platform selection
- Platform-specific API key validation (ANTHROPIC_API_KEY, GOOGLE_API_KEY, OPENAI_API_KEY)
- Graceful handling of Markdown (upload not supported)

### Phase 3: Standalone MCP Enhancement Tool
- Created new `enhance_skill_tool()` function (140+ lines)
- Supports both 'local' mode (Claude Code Max) and 'api' mode (platform APIs)
- Added MCP server definition for `enhance_skill`
- Works with Claude, Gemini, and OpenAI
- Integrated into MCP tools exports

### Phase 4: Unified Config Splitting Support
- Added `is_unified_config()` method to detect multi-source configs
- Implemented `split_by_source()` method to split by source type (docs, github, pdf)
- Updated auto-detection to recommend 'source' strategy for unified configs
- Added 'source' to valid CLI strategy choices
- Updated MCP tool documentation for unified support

### Phase 5: Comprehensive Feature Matrix Documentation
- Created `docs/FEATURE_MATRIX.md` (~400 lines)
- Complete platform comparison tables
- Skill mode support matrix
- CLI and MCP tool coverage matrices
- Platform-specific notes and FAQs
- Workflow examples for each combination
- Updated README.md with feature matrix section

## Files Modified

**Core Implementation:**
- src/skill_seekers/mcp/tools/packaging_tools.py
- src/skill_seekers/mcp/server_fastmcp.py
- src/skill_seekers/mcp/tools/__init__.py
- src/skill_seekers/cli/split_config.py
- src/skill_seekers/mcp/tools/splitting_tools.py

**Documentation:**
- docs/FEATURE_MATRIX.md (NEW)
- README.md

**Tests:**
- tests/test_install_multiplatform.py (already existed)

## Test Results
-  699 tests passing
-  All multiplatform install tests passing (6/6)
-  No regressions introduced
-  All syntax checks passed
-  Import tests successful

## Breaking Changes
None - all changes are backward compatible with default `target='claude'`

## Migration Guide
Existing MCP calls without `target` parameter will continue to work (defaults to 'claude').

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 21:35:21 +03:00

982 lines
34 KiB
Python

#!/usr/bin/env python3
"""
Skill Seeker MCP Server (FastMCP Implementation)
Modern, decorator-based MCP server using FastMCP for simplified tool registration.
Provides 17 tools for generating Claude AI skills from documentation.
This is a streamlined alternative to server.py (2200 lines → 708 lines, 68% reduction).
All tool implementations are delegated to modular tool files in tools/ directory.
**Architecture:**
- FastMCP server with decorator-based tool registration
- 17 tools organized into 5 categories:
* Config tools (3): generate_config, list_configs, validate_config
* Scraping tools (4): estimate_pages, scrape_docs, scrape_github, scrape_pdf
* Packaging tools (3): package_skill, upload_skill, install_skill
* Splitting tools (2): split_config, generate_router
* Source tools (5): fetch_config, submit_config, add_config_source, list_config_sources, remove_config_source
**Usage:**
# Stdio transport (default, backward compatible)
python -m skill_seekers.mcp.server_fastmcp
# HTTP transport (new)
python -m skill_seekers.mcp.server_fastmcp --http
python -m skill_seekers.mcp.server_fastmcp --http --port 8080
**MCP Integration:**
Stdio (default):
{
"mcpServers": {
"skill-seeker": {
"command": "python",
"args": ["-m", "skill_seekers.mcp.server_fastmcp"]
}
}
}
HTTP (alternative):
{
"mcpServers": {
"skill-seeker": {
"url": "http://localhost:8000/sse"
}
}
}
"""
import sys
import argparse
import logging
from pathlib import Path
from typing import Any
# Import FastMCP
MCP_AVAILABLE = False
FastMCP = None
TextContent = None
try:
from mcp.server import FastMCP
from mcp.types import TextContent
MCP_AVAILABLE = True
except ImportError as e:
# Only exit if running as main module, not when importing for tests
if __name__ == "__main__":
print("❌ Error: mcp package not installed")
print("Install with: pip install mcp")
print(f"Import error: {e}")
sys.exit(1)
# Import all tool implementations
try:
from .tools import (
# Config tools
generate_config_impl,
list_configs_impl,
validate_config_impl,
# Scraping tools
estimate_pages_impl,
scrape_docs_impl,
scrape_github_impl,
scrape_pdf_impl,
# Packaging tools
package_skill_impl,
upload_skill_impl,
enhance_skill_impl,
install_skill_impl,
# Splitting tools
split_config_impl,
generate_router_impl,
# Source tools
fetch_config_impl,
submit_config_impl,
add_config_source_impl,
list_config_sources_impl,
remove_config_source_impl,
)
except ImportError:
# Fallback for direct script execution
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from tools import (
generate_config_impl,
list_configs_impl,
validate_config_impl,
estimate_pages_impl,
scrape_docs_impl,
scrape_github_impl,
scrape_pdf_impl,
package_skill_impl,
upload_skill_impl,
enhance_skill_impl,
install_skill_impl,
split_config_impl,
generate_router_impl,
fetch_config_impl,
submit_config_impl,
add_config_source_impl,
list_config_sources_impl,
remove_config_source_impl,
)
# Initialize FastMCP server
mcp = None
if MCP_AVAILABLE and FastMCP is not None:
mcp = FastMCP(
name="skill-seeker",
instructions="Skill Seeker MCP Server - Generate Claude AI skills from documentation",
)
# Helper decorator for tests (when MCP is not available)
def safe_tool_decorator(*args, **kwargs):
"""Decorator that works when mcp is None (for testing)"""
if mcp is not None:
return mcp.tool(*args, **kwargs)
else:
# Return a pass-through decorator for testing
def wrapper(func):
return func
return wrapper
# ============================================================================
# CONFIG TOOLS (3 tools)
# ============================================================================
@safe_tool_decorator(
description="Generate a config file for documentation scraping. Interactively creates a JSON config for any documentation website."
)
async def generate_config(
name: str,
url: str,
description: str,
max_pages: int = 100,
unlimited: bool = False,
rate_limit: float = 0.5,
) -> str:
"""
Generate a config file for documentation scraping.
Args:
name: Skill name (lowercase, alphanumeric, hyphens, underscores)
url: Base documentation URL (must include http:// or https://)
description: Description of when to use this skill
max_pages: Maximum pages to scrape (default: 100, use -1 for unlimited)
unlimited: Remove all limits - scrape all pages (default: false). Overrides max_pages.
rate_limit: Delay between requests in seconds (default: 0.5)
Returns:
Success message with config path and next steps, or error message.
"""
args = {
"name": name,
"url": url,
"description": description,
"max_pages": max_pages,
"unlimited": unlimited,
"rate_limit": rate_limit,
}
result = await generate_config_impl(args)
# Extract text from TextContent objects
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="List all available preset configurations."
)
async def list_configs() -> str:
"""
List all available preset configurations.
Returns:
List of available configs with categories and descriptions.
"""
result = await list_configs_impl({})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Validate a config file for errors."
)
async def validate_config(config_path: str) -> str:
"""
Validate a config file for errors.
Args:
config_path: Path to config JSON file
Returns:
Validation result with any errors or success message.
"""
result = await validate_config_impl({"config_path": config_path})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
# ============================================================================
# SCRAPING TOOLS (4 tools)
# ============================================================================
@safe_tool_decorator(
description="Estimate how many pages will be scraped from a config. Fast preview without downloading content."
)
async def estimate_pages(
config_path: str,
max_discovery: int = 1000,
unlimited: bool = False,
) -> str:
"""
Estimate how many pages will be scraped from a config.
Args:
config_path: Path to config JSON file (e.g., configs/react.json)
max_discovery: Maximum pages to discover during estimation (default: 1000, use -1 for unlimited)
unlimited: Remove discovery limit - estimate all pages (default: false). Overrides max_discovery.
Returns:
Estimation results with page count and recommendations.
"""
args = {
"config_path": config_path,
"max_discovery": max_discovery,
"unlimited": unlimited,
}
result = await estimate_pages_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Scrape documentation and build Claude skill. Supports both single-source (legacy) and unified multi-source configs. Creates SKILL.md and reference files. Automatically detects llms.txt files for 10x faster processing. Falls back to HTML scraping if not available."
)
async def scrape_docs(
config_path: str,
unlimited: bool = False,
enhance_local: bool = False,
skip_scrape: bool = False,
dry_run: bool = False,
merge_mode: str | None = None,
) -> str:
"""
Scrape documentation and build Claude skill.
Args:
config_path: Path to config JSON file (e.g., configs/react.json or configs/godot_unified.json)
unlimited: Remove page limit - scrape all pages (default: false). Overrides max_pages in config.
enhance_local: Open terminal for local enhancement with Claude Code (default: false)
skip_scrape: Skip scraping, use cached data (default: false)
dry_run: Preview what will be scraped without saving (default: false)
merge_mode: Override merge mode for unified configs: 'rule-based' or 'claude-enhanced' (default: from config)
Returns:
Scraping results with file paths and statistics.
"""
args = {
"config_path": config_path,
"unlimited": unlimited,
"enhance_local": enhance_local,
"skip_scrape": skip_scrape,
"dry_run": dry_run,
}
if merge_mode:
args["merge_mode"] = merge_mode
result = await scrape_docs_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Scrape GitHub repository and build Claude skill. Extracts README, Issues, Changelog, Releases, and code structure."
)
async def scrape_github(
repo: str | None = None,
config_path: str | None = None,
name: str | None = None,
description: str | None = None,
token: str | None = None,
no_issues: bool = False,
no_changelog: bool = False,
no_releases: bool = False,
max_issues: int = 100,
scrape_only: bool = False,
) -> str:
"""
Scrape GitHub repository and build Claude skill.
Args:
repo: GitHub repository (owner/repo, e.g., facebook/react)
config_path: Path to GitHub config JSON file (e.g., configs/react_github.json)
name: Skill name (default: repo name)
description: Skill description
token: GitHub personal access token (or use GITHUB_TOKEN env var)
no_issues: Skip GitHub issues extraction (default: false)
no_changelog: Skip CHANGELOG extraction (default: false)
no_releases: Skip releases extraction (default: false)
max_issues: Maximum issues to fetch (default: 100)
scrape_only: Only scrape, don't build skill (default: false)
Returns:
GitHub scraping results with file paths.
"""
args = {}
if repo:
args["repo"] = repo
if config_path:
args["config_path"] = config_path
if name:
args["name"] = name
if description:
args["description"] = description
if token:
args["token"] = token
args["no_issues"] = no_issues
args["no_changelog"] = no_changelog
args["no_releases"] = no_releases
args["max_issues"] = max_issues
args["scrape_only"] = scrape_only
result = await scrape_github_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Scrape PDF documentation and build Claude skill. Extracts text, code, and images from PDF files."
)
async def scrape_pdf(
config_path: str | None = None,
pdf_path: str | None = None,
name: str | None = None,
description: str | None = None,
from_json: str | None = None,
) -> str:
"""
Scrape PDF documentation and build Claude skill.
Args:
config_path: Path to PDF config JSON file (e.g., configs/manual_pdf.json)
pdf_path: Direct PDF path (alternative to config_path)
name: Skill name (required with pdf_path)
description: Skill description (optional)
from_json: Build from extracted JSON file (e.g., output/manual_extracted.json)
Returns:
PDF scraping results with file paths.
"""
args = {}
if config_path:
args["config_path"] = config_path
if pdf_path:
args["pdf_path"] = pdf_path
if name:
args["name"] = name
if description:
args["description"] = description
if from_json:
args["from_json"] = from_json
result = await scrape_pdf_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
# ============================================================================
# PACKAGING TOOLS (3 tools)
# ============================================================================
@safe_tool_decorator(
description="Package skill directory into platform-specific format (ZIP for Claude/OpenAI/Markdown, tar.gz for Gemini). Supports all platforms: claude, gemini, openai, markdown. Automatically uploads if platform API key is set."
)
async def package_skill(
skill_dir: str,
target: str = "claude",
auto_upload: bool = True,
) -> str:
"""
Package skill directory for target LLM platform.
Args:
skill_dir: Path to skill directory to package (e.g., output/react/)
target: Target platform (default: 'claude'). Options: claude, gemini, openai, markdown
auto_upload: Auto-upload after packaging if API key is available (default: true). Requires platform-specific API key: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY.
Returns:
Packaging results with file path and platform info.
"""
args = {
"skill_dir": skill_dir,
"target": target,
"auto_upload": auto_upload,
}
result = await package_skill_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Upload skill package to target LLM platform API. Requires platform-specific API key. Supports: claude (Anthropic Skills API), gemini (Google Files API), openai (Assistants API). Does NOT support markdown."
)
async def upload_skill(
skill_zip: str,
target: str = "claude",
api_key: str | None = None,
) -> str:
"""
Upload skill package to target platform.
Args:
skill_zip: Path to skill package (.zip or .tar.gz, e.g., output/react.zip)
target: Target platform (default: 'claude'). Options: claude, gemini, openai
api_key: Optional API key (uses env var if not provided: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY)
Returns:
Upload results with skill ID and platform URL.
"""
args = {
"skill_zip": skill_zip,
"target": target,
}
if api_key:
args["api_key"] = api_key
result = await upload_skill_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Enhance SKILL.md with AI using target platform's model. Local mode uses Claude Code Max (no API key). API mode uses platform API (requires key). Transforms basic templates into comprehensive 500+ line guides with examples."
)
async def enhance_skill(
skill_dir: str,
target: str = "claude",
mode: str = "local",
api_key: str | None = None,
) -> str:
"""
Enhance SKILL.md with AI.
Args:
skill_dir: Path to skill directory containing SKILL.md (e.g., output/react/)
target: Target platform (default: 'claude'). Options: claude, gemini, openai
mode: Enhancement mode (default: 'local'). Options: local (Claude Code, no API), api (uses platform API)
api_key: Optional API key for 'api' mode (uses env var if not provided: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY)
Returns:
Enhancement results with backup location.
"""
args = {
"skill_dir": skill_dir,
"target": target,
"mode": mode,
}
if api_key:
args["api_key"] = api_key
result = await enhance_skill_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Complete one-command workflow: fetch config → scrape docs → AI enhance (MANDATORY) → package → upload. Enhancement required for quality (3/10→9/10). Takes 20-45 min depending on config size. Supports multiple LLM platforms: claude (default), gemini, openai, markdown. Auto-uploads if platform API key is set."
)
async def install_skill(
config_name: str | None = None,
config_path: str | None = None,
destination: str = "output",
auto_upload: bool = True,
unlimited: bool = False,
dry_run: bool = False,
target: str = "claude",
) -> str:
"""
Complete one-command workflow to install a skill.
Args:
config_name: Config name from API (e.g., 'react', 'django'). Mutually exclusive with config_path. Tool will fetch this config from the official API before scraping.
config_path: Path to existing config JSON file (e.g., 'configs/custom.json'). Mutually exclusive with config_name. Use this if you already have a config file.
destination: Output directory for skill files (default: 'output')
auto_upload: Auto-upload after packaging (requires platform API key). Default: true. Set to false to skip upload.
unlimited: Remove page limits during scraping (default: false). WARNING: Can take hours for large sites.
dry_run: Preview workflow without executing (default: false). Shows all phases that would run.
target: Target LLM platform (default: 'claude'). Options: claude, gemini, openai, markdown. Requires corresponding API key: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY.
Returns:
Workflow results with all phase statuses.
"""
args = {
"destination": destination,
"auto_upload": auto_upload,
"unlimited": unlimited,
"dry_run": dry_run,
"target": target,
}
if config_name:
args["config_name"] = config_name
if config_path:
args["config_path"] = config_path
result = await install_skill_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
# ============================================================================
# SPLITTING TOOLS (2 tools)
# ============================================================================
@safe_tool_decorator(
description="Split large configs into multiple focused skills. Supports documentation (10K+ pages) and unified multi-source configs. Auto-detects config type and recommends best strategy."
)
async def split_config(
config_path: str,
strategy: str = "auto",
target_pages: int = 5000,
dry_run: bool = False,
) -> str:
"""
Split large configs into multiple skills.
Supports:
- Documentation configs: Split by categories, size, or create router skills
- Unified configs: Split by source type (documentation, github, pdf)
Args:
config_path: Path to config JSON file (e.g., configs/godot.json or configs/react_unified.json)
strategy: Split strategy: auto, none, source, category, router, size (default: auto). 'source' is for unified configs.
target_pages: Target pages per skill for doc configs (default: 5000)
dry_run: Preview without saving files (default: false)
Returns:
Splitting results with generated config paths.
"""
args = {
"config_path": config_path,
"strategy": strategy,
"target_pages": target_pages,
"dry_run": dry_run,
}
result = await split_config_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Generate router/hub skill for split documentation. Creates intelligent routing to sub-skills."
)
async def generate_router(
config_pattern: str,
router_name: str | None = None,
) -> str:
"""
Generate router/hub skill for split documentation.
Args:
config_pattern: Config pattern for sub-skills (e.g., 'configs/godot-*.json')
router_name: Router skill name (optional, inferred from configs)
Returns:
Router generation results with file paths.
"""
args = {"config_pattern": config_pattern}
if router_name:
args["router_name"] = router_name
result = await generate_router_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
# ============================================================================
# SOURCE TOOLS (5 tools)
# ============================================================================
@safe_tool_decorator(
description="Fetch config from API, git URL, or registered source. Supports three modes: (1) Named source from registry, (2) Direct git URL, (3) API (default). List available configs or download a specific one by name."
)
async def fetch_config(
config_name: str | None = None,
destination: str = "configs",
list_available: bool = False,
category: str | None = None,
git_url: str | None = None,
source: str | None = None,
branch: str = "main",
token: str | None = None,
refresh: bool = False,
) -> str:
"""
Fetch config from API, git URL, or registered source.
Args:
config_name: Name of the config to download (e.g., 'react', 'django', 'godot'). Required for git modes. Omit to list all available configs in API mode.
destination: Directory to save the config file (default: 'configs/')
list_available: List all available configs from the API (only works in API mode, default: false)
category: Filter configs by category when listing in API mode (e.g., 'web-frameworks', 'game-engines', 'devops')
git_url: Git repository URL containing configs. If provided, fetches from git instead of API. Supports HTTPS and SSH URLs. Example: 'https://github.com/myorg/configs.git'
source: Named source from registry (highest priority). Use add_config_source to register sources first. Example: 'team', 'company'
branch: Git branch to use (default: 'main'). Only used with git_url or source.
token: Authentication token for private repos (optional). Prefer using environment variables (GITHUB_TOKEN, GITLAB_TOKEN, etc.).
refresh: Force refresh cached git repository (default: false). Deletes cache and re-clones. Only used with git modes.
Returns:
Fetch results with config path or list of available configs.
"""
args = {
"destination": destination,
"list_available": list_available,
"branch": branch,
"refresh": refresh,
}
if config_name:
args["config_name"] = config_name
if category:
args["category"] = category
if git_url:
args["git_url"] = git_url
if source:
args["source"] = source
if token:
args["token"] = token
result = await fetch_config_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Submit a custom config file to the community. Validates config (legacy or unified format) and creates a GitHub issue in skill-seekers-configs repo for review."
)
async def submit_config(
config_path: str | None = None,
config_json: str | None = None,
testing_notes: str | None = None,
github_token: str | None = None,
) -> str:
"""
Submit a custom config file to the community.
Args:
config_path: Path to config JSON file to submit (e.g., 'configs/myframework.json')
config_json: Config JSON as string (alternative to config_path)
testing_notes: Notes about testing (e.g., 'Tested with 20 pages, works well')
github_token: GitHub personal access token (or use GITHUB_TOKEN env var)
Returns:
Submission results with GitHub issue URL.
"""
args = {}
if config_path:
args["config_path"] = config_path
if config_json:
args["config_json"] = config_json
if testing_notes:
args["testing_notes"] = testing_notes
if github_token:
args["github_token"] = github_token
result = await submit_config_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Register a git repository as a config source. Allows fetching configs from private/team repos. Use this to set up named sources that can be referenced by fetch_config. Supports GitHub, GitLab, Gitea, Bitbucket, and custom git servers."
)
async def add_config_source(
name: str,
git_url: str,
source_type: str = "github",
token_env: str | None = None,
branch: str = "main",
priority: int = 100,
enabled: bool = True,
) -> str:
"""
Register a git repository as a config source.
Args:
name: Source identifier (lowercase, alphanumeric, hyphens/underscores allowed). Example: 'team', 'company-internal', 'my_configs'
git_url: Git repository URL (HTTPS or SSH). Example: 'https://github.com/myorg/configs.git' or 'git@github.com:myorg/configs.git'
source_type: Source type (default: 'github'). Options: 'github', 'gitlab', 'gitea', 'bitbucket', 'custom'
token_env: Environment variable name for auth token (optional). Auto-detected if not provided. Example: 'GITHUB_TOKEN', 'GITLAB_TOKEN', 'MY_CUSTOM_TOKEN'
branch: Git branch to use (default: 'main'). Example: 'main', 'master', 'develop'
priority: Source priority (lower = higher priority, default: 100). Used for conflict resolution when same config exists in multiple sources.
enabled: Whether source is enabled (default: true)
Returns:
Registration results with source details.
"""
args = {
"name": name,
"git_url": git_url,
"source_type": source_type,
"branch": branch,
"priority": priority,
"enabled": enabled,
}
if token_env:
args["token_env"] = token_env
result = await add_config_source_impl(args)
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="List all registered config sources. Shows git repositories that have been registered with add_config_source. Use this to see available sources for fetch_config."
)
async def list_config_sources(enabled_only: bool = False) -> str:
"""
List all registered config sources.
Args:
enabled_only: Only show enabled sources (default: false)
Returns:
List of registered sources with details.
"""
result = await list_config_sources_impl({"enabled_only": enabled_only})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Remove a registered config source. Deletes the source from the registry. Does not delete cached git repository data."
)
async def remove_config_source(name: str) -> str:
"""
Remove a registered config source.
Args:
name: Source identifier to remove. Example: 'team', 'company-internal'
Returns:
Removal results with success/error message.
"""
result = await remove_config_source_impl({"name": name})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
# ============================================================================
# MAIN ENTRY POINT
# ============================================================================
def parse_args():
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Skill Seeker MCP Server - Generate Claude AI skills from documentation",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Transport Modes:
stdio (default): Standard input/output communication for Claude Desktop
http: HTTP server with SSE for web-based MCP clients
Examples:
# Stdio transport (default, backward compatible)
python -m skill_seekers.mcp.server_fastmcp
# HTTP transport on default port 8000
python -m skill_seekers.mcp.server_fastmcp --http
# HTTP transport on custom port
python -m skill_seekers.mcp.server_fastmcp --http --port 8080
# Debug logging
python -m skill_seekers.mcp.server_fastmcp --http --log-level DEBUG
""",
)
parser.add_argument(
"--http",
action="store_true",
help="Use HTTP transport instead of stdio (default: stdio)",
)
parser.add_argument(
"--port",
type=int,
default=8000,
help="Port for HTTP server (default: 8000)",
)
parser.add_argument(
"--host",
type=str,
default="127.0.0.1",
help="Host for HTTP server (default: 127.0.0.1)",
)
parser.add_argument(
"--log-level",
type=str,
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Logging level (default: INFO)",
)
return parser.parse_args()
def setup_logging(log_level: str):
"""Configure logging."""
logging.basicConfig(
level=getattr(logging, log_level),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
async def run_http_server(host: str, port: int):
"""Run the MCP server with HTTP transport using uvicorn."""
try:
import uvicorn
except ImportError:
logging.error("❌ Error: uvicorn package not installed")
logging.error("Install with: pip install uvicorn")
sys.exit(1)
try:
# Get the SSE Starlette app from FastMCP
app = mcp.sse_app()
# Add CORS middleware for cross-origin requests
try:
from starlette.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
logging.info("✓ CORS middleware enabled")
except ImportError:
logging.warning("⚠ CORS middleware not available (starlette not installed)")
# Add health check endpoint
from starlette.responses import JSONResponse
from starlette.routing import Route
async def health_check(request):
"""Health check endpoint."""
return JSONResponse(
{
"status": "healthy",
"server": "skill-seeker-mcp",
"version": "2.1.1",
"transport": "http",
"endpoints": {
"health": "/health",
"sse": "/sse",
"messages": "/messages/",
},
}
)
# Add route before the catch-all SSE route
app.routes.insert(0, Route("/health", health_check, methods=["GET"]))
logging.info(f"🚀 Starting Skill Seeker MCP Server (HTTP mode)")
logging.info(f"📡 Server URL: http://{host}:{port}")
logging.info(f"🔗 SSE Endpoint: http://{host}:{port}/sse")
logging.info(f"💚 Health Check: http://{host}:{port}/health")
logging.info(f"📝 Messages: http://{host}:{port}/messages/")
logging.info("")
logging.info("Claude Desktop Configuration (HTTP):")
logging.info('{')
logging.info(' "mcpServers": {')
logging.info(' "skill-seeker": {')
logging.info(f' "url": "http://{host}:{port}/sse"')
logging.info(' }')
logging.info(' }')
logging.info('}')
logging.info("")
logging.info("Press Ctrl+C to stop the server")
# Run the uvicorn server
config = uvicorn.Config(
app=app,
host=host,
port=port,
log_level=logging.getLogger().level,
access_log=True,
)
server = uvicorn.Server(config)
await server.serve()
except Exception as e:
logging.error(f"❌ Failed to start HTTP server: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
def main():
"""Run the MCP server with stdio or HTTP transport."""
import asyncio
# Check if MCP is available
if not MCP_AVAILABLE or mcp is None:
print("❌ Error: mcp package not installed or FastMCP not available")
print("Install with: pip install mcp>=1.25")
sys.exit(1)
# Parse command-line arguments
args = parse_args()
# Setup logging
setup_logging(args.log_level)
if args.http:
# HTTP transport mode
logging.info(f"🌐 Using HTTP transport on {args.host}:{args.port}")
try:
asyncio.run(run_http_server(args.host, args.port))
except KeyboardInterrupt:
logging.info("\n👋 Server stopped by user")
sys.exit(0)
else:
# Stdio transport mode (default, backward compatible)
logging.info("📺 Using stdio transport (default)")
try:
asyncio.run(mcp.run_stdio_async())
except KeyboardInterrupt:
logging.info("\n👋 Server stopped by user")
sys.exit(0)
if __name__ == "__main__":
main()