Skill: jalanchao/mcp-to-skill (version 1.0.0)

--- name: mcp-to-skill description: | Converts any MCP server into a standalone skill package with zero runtime dependencies (no MCP process required). Trigger when user says: "convert this MCP to a skill", "I don't want to use MCP anymore", "wrap MCP X as a skill", "MCP is too heavy", "turn MCP capabilities into a skill". Does: connects to MCP server to extract tool schemas, analyzes source code to infer equivalent Bash commands, generates a ready-to-use skill package and registers it with the agent, optionally asks user to remove the original MCP. Does NOT: execute MCP tool calls to complete tasks (that's using MCP, not converting it); wrap existing Bash scripts as skills (use skill-creator); execute MCP business logic. Optional dependency: skill-creator (improves generated SKILL.md quality). --- # mcp-to-skill Converts an MCP server into a zero-dependency skill package so AI agents can invoke tools directly via Bash commands, without launching an MCP process or injecting all tool definitions upfront. --- ## Step 1: Get MCP information Determine the input type: **A — User provided a command string / local path / URL:** Confirm the command is available, proceed to Step 2. **B — User pasted tool schema JSON:** Save the JSON to a temp file, **skip Step 2**, go directly to Step 3. Use the Write tool to save the pasted JSON to `/tmp/mcp-schema-input.json`. In Step 3, use `--schema-json /tmp/mcp-schema-input.json`. **C — User hasn't specified, wants to pick from registered MCPs:** List the MCPs currently registered with the agent and let the user choose. In Claude Code: run `claude mcp list` **Language preference:** If the user specifies a language for the generated skill (e.g. "generate in Chinese", "用中文生成"), note it and apply it to all generated files in Step 5. Default is English. --- ## Step 2: Run mcp_inspector.py (only for input type A or C) Locate `mcp_inspector.py`: it is in the same directory as this SKILL.md. Determine the absolute path of that directory from the path information provided by the agent framework when loading this skill, then run: ```bash # Ensure mcp SDK is installed pip show mcp > /dev/null 2>&1 || pip install mcp python /path/to/skill-dir/mcp_inspector.py "<MCP command>" --output /tmp/mcp-inspector-output.json ``` Example output: ``` ✓ Written to /tmp/mcp-inspector-output.json: 12 tools, source: /tmp/mcp-to-skill-cache/server-github ``` Use the Read tool to read `/tmp/mcp-inspector-output.json` and extract: `server_name`, `source_path` (may be null), `tools[]`. --- ## Step 3: AI analysis — infer equivalent commands Read the inspector output (or the schema file from Step 1B). **If source_path is not null:** Use Read / Grep tools to read the source files, locate the implementation for each tool, and extract: - HTTP endpoint (URL, method, headers, body structure) - or CLI invocation pattern **If source_path is null:** Infer reasonable equivalent commands based solely on each tool's `description` and `inputSchema`. Write a command draft for each tool with a confidence marker: - `[VERIFIED]` — confirmed by source code (only when source is available) - `[INFERRED]` — AI-inferred, logically sound but untested (max level when source_path is null) - `[TODO]` — cannot be auto-generated, leave a placeholder with explanation --- ## Step 4: Test read-only commands For each `[INFERRED]` command that is a read-only operation (GET request, query), execute it with the Bash tool: - Pass → upgrade to `[VERIFIED]` - Fail → keep `[INFERRED]`, add a comment above the command noting the failure reason - Write operations (POST/PUT/DELETE, file modifications) — **skip testing**, keep `[INFERRED]` - `[TODO]` items — do not test --- ## Step 5: Generate skill package Create the skill directory in the user's current working directory (or a user-specified path): ``` <mcp-server-name>/ SKILL.md config.json # public config (safe to commit) secrets.json # secrets (gitignored, never commit) secrets.json.example # secrets template (safe to commit) .gitignore helpers/ (create on demand, do not pre-create empty) tools-extended.md (when tool count > 8) <tool>.py / <tool>.sh (when logic cannot fit in a single command) ``` **Progressive disclosure rules:** - tool count ≤ 8: write all tools into the SKILL.md quick-reference section - tool count > 8: SKILL.md lists only the 8 most common tools; the rest go into `helpers/tools-extended.md`; add a note at the bottom of SKILL.md: "More tools: see helpers/tools-extended.md" **SKILL.md frontmatter template:** ```yaml --- name: <server-name> description: | [When to use]: <summarize usage scenarios from tool descriptions> [Does]: <core capabilities> [Does NOT]: <explicitly excluded scenarios> [Requires]: <runtime dependencies; write "no runtime dependencies" if none> --- ``` **Config file separation (secret safety):** `config.json` — public config only, safe to commit: ```json { "endpoint": "<base URL extracted from source, or leave as placeholder>" } ``` `secrets.json` — secrets only, **must be gitignored**: ```json { "auth_token": "<actual token>" } ``` `secrets.json.example` — secrets template, safe to commit, for onboarding: ```json { "auth_token": "your-api-token-here" } ``` `.gitignore` — contents: ``` secrets.json ``` **Read order for scripts:** `secrets.json` → environment variable (e.g. `X_API_TOKEN`). If both are empty, error and prompt user to copy the example file and fill it in. Important: `config.json` and `secrets.json` are read on every tool call (not cached at startup). **Language:** Generate all text content in the language specified in Step 1. Default is English. **If skill-creator is loaded in the agent context:** Pass the analysis results (tool list + inferred commands + confidence markers) to skill-creator to generate SKILL.md. --- ## Step 6: Register skill with the current AI agent Goal: register the generated skill directory so it is immediately available. Probe in order and use the first that works: 1. Check if `npx skills` is available: ```bash which npx && npx skills --version 2>/dev/null ``` If available: `npx skills add <skill-path> -g -y` 2. Check if running in Claude Code: ```bash claude --version 2>/dev/null ``` If available: symlink to `~/.claude/skills/<skill-name>`: ```bash ln -sf <skill-path> ~/.claude/skills/<skill-name> ``` Note: `/add-dir` is an interactive slash command and cannot be called via Bash. 3. If neither applies: output the skill path and tell the user how to register manually: > "Skill generated at `<path>`. Please register it with your AI agent. > Claude Code users: run `/add-dir <path>` > npx skills users: run `npx skills add <path> -g`" --- ## Step 7: Ask about removing the original MCP (optional) Only prompt when Step 1 was type A or C (not pasted schema): > "MCP `<server-name>` has been converted to a skill. Would you like to remove the MCP configuration from your AI agent?" - User confirms → assist with removal (agent decides how) - User declines or no response → skip, skill and MCP can coexist

path: mcp_inspector.py
content: #!/usr/bin/env python3
"""
mcp_inspector.py - MCP server inspector for mcp-to-skill

连接 MCP server，提取 tool schemas，尝试拉取源码。
输出 inspector.json 供 AI 层分析。

用法：
  python mcp_inspector.py "npx -y @modelcontextprotocol/server-github" --output inspector.json
  python mcp_inspector.py --schema-json tools.json --server-name github
"""

import argparse
import asyncio
import json
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Optional

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


def detect_package(command: str) -> Optional[str]:
    """从命令字符串推断 npm 包名。返回包名或 None。"""
    parts = command.split()
    for i, part in enumerate(parts):
        # npx [-y/--yes] <package>
        if part in ('-y', '--yes') and i + 1 < len(parts):
            candidate = parts[i + 1]
            # 排除本地路径（以 . 或 / 开头）
            if not candidate.startswith('.') and not candidate.startswith('/'):
                return candidate
    return None


def fetch_source(package: Optional[str], local_path: Optional[str] = None) -> Optional[str]:
    """
    拉取 MCP server 源码。失败返回 None，不抛出异常。
    优先级：local_path > npm pack > 返回 None
    """
    # 本地路径优先
    if local_path and Path(local_path).exists():
        return local_path

    if not package:
        return None

    # 构建缓存目录
    safe_name = package.replace('/', '-').lstrip('@-')
    cache_dir = Path(tempfile.gettempdir()) / "mcp-to-skill-cache" / safe_name

    # 已缓存则直接返回（排除仅含 .tgz 的失败解压目录）
    if cache_dir.exists() and any(f for f in cache_dir.iterdir() if f.suffix != '.tgz'):
        return str(cache_dir)

    cache_dir.mkdir(parents=True, exist_ok=True)

    try:
        # npm pack 下载 tarball
        result = subprocess.run(
            ['npm', 'pack', package],
            capture_output=True, text=True, cwd=str(cache_dir), timeout=60
        )
        if result.returncode != 0:
            return None

        tarball = next(cache_dir.glob('*.tgz'), None)
        if not tarball:
            return None

        # 解压（--strip-components=1 去掉 package/ 前缀）
        extract_result = subprocess.run(
            ['tar', 'xzf', str(tarball), '--strip-components=1'],
            capture_output=True, cwd=str(cache_dir), timeout=30
        )
        if extract_result.returncode != 0:
            return None

        return str(cache_dir)
    except Exception:
        return None


async def connect_and_list_tools(command: str) -> list[dict]:
    """
    通过 MCP JSON-RPC 协议连接 server，返回 tool 列表。
    command: 完整命令字符串，如 "npx -y @mcp/server-github"
    """
    parts = command.split()
    cmd = parts[0]
    cmd_args = parts[1:]

    params = StdioServerParameters(command=cmd, args=cmd_args)
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.list_tools()
            return [
                {
                    "name": tool.name,
                    "description": tool.description or "",
                    "inputSchema": tool.inputSchema or {}
                }
                for tool in result.tools
            ]


def main():
    parser = argparse.ArgumentParser(
        description="MCP Inspector — 提取 MCP server tool schemas 和源码",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  # 连接已安装的 MCP server
  python mcp_inspector.py "npx -y @modelcontextprotocol/server-github" --output inspector.json

  # 使用已有 schema JSON（跳过 MCP 连接）
  python mcp_inspector.py --schema-json tools.json --server-name github
        """
    )
    parser.add_argument("command", nargs="?", help="MCP server 启动命令")
    parser.add_argument("--schema-json", help="已有 tool schema JSON 文件路径（跳过 MCP 连接）")
    parser.add_argument("--server-name", help="覆盖 server 名称")
    parser.add_argument("--output", default="inspector.json", help="输出文件路径（默认：inspector.json）")
    args = parser.parse_args()

    # 模式 1：直接使用 schema JSON
    if args.schema_json:
        with open(args.schema_json) as f:
            tools = json.load(f)
        result = {
            "server_name": args.server_name or "unknown",
            "package": None,
            "source_path": None,
            "tools": tools
        }
        _write_output(result, args.output)
        return

    if not args.command:
        parser.error("需要提供 'command' 或 '--schema-json'")

    # 模式 2：连接 MCP server
    tools = asyncio.run(connect_and_list_tools(args.command))
    package = detect_package(args.command)
    source_path = fetch_source(package)

    # 推断 server_name：包名末段 或 命令末段
    if args.server_name:
        server_name = args.server_name
    elif package:
        server_name = package.split("/")[-1]
    else:
        server_name = args.command.split()[-1].split("/")[-1]

    result = {
        "server_name": server_name,
        "package": package,
        "source_path": source_path,
        "tools": tools
    }
    _write_output(result, args.output)


def _write_output(result: dict, output_path: str):
    """写入 inspector.json 并打印摘要。"""
    with open(output_path, "w") as f:
        json.dump(result, f, indent=2, ensure_ascii=False)
    tool_count = len(result["tools"])
    src = result["source_path"] or "（无源码）"
    print(f"✓ 写入 {output_path}：{tool_count} 个 tool，源码：{src}")


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)