|
| 1 | +"""MCP (Model Context Protocol) server for sit. |
| 2 | +
|
| 3 | +Exposes sit commands as MCP tools so LLM agents can call them natively |
| 4 | +via the MCP protocol. Uses the Python SDK under the hood — no subprocess. |
| 5 | +
|
| 6 | +Usage:: |
| 7 | +
|
| 8 | + # Run as stdio MCP server (default transport): |
| 9 | + python -m sit.mcp_server |
| 10 | +
|
| 11 | + # Or import and customise: |
| 12 | + from sit.mcp_server import create_server |
| 13 | + server = create_server() |
| 14 | + server.run() |
| 15 | +""" |
| 16 | + |
| 17 | +from __future__ import annotations |
| 18 | + |
| 19 | +import json |
| 20 | +from pathlib import Path |
| 21 | +from typing import Any |
| 22 | + |
| 23 | +try: |
| 24 | + from mcp.server import Server |
| 25 | + from mcp.server.stdio import stdio_server |
| 26 | + from mcp.types import TextContent, Tool |
| 27 | +except ImportError: |
| 28 | + raise ImportError( |
| 29 | + "MCP support requires the 'mcp' package. " |
| 30 | + "Install it with: pip install 'sit-toolkit[mcp]' or pip install mcp" |
| 31 | + ) |
| 32 | + |
| 33 | +from .sdk import Sit |
| 34 | + |
| 35 | +# --------------------------------------------------------------------------- |
| 36 | +# Tool definitions |
| 37 | +# --------------------------------------------------------------------------- |
| 38 | + |
| 39 | +TOOLS: list[Tool] = [ |
| 40 | + Tool( |
| 41 | + name="sit_info", |
| 42 | + description=( |
| 43 | + "Get package info for a sit skill package. " |
| 44 | + "Returns the sit.info.v1 contract: name, version, git state, " |
| 45 | + "file listing, validation status, golden test results, reports." |
| 46 | + ), |
| 47 | + inputSchema={ |
| 48 | + "type": "object", |
| 49 | + "properties": { |
| 50 | + "package_path": { |
| 51 | + "type": "string", |
| 52 | + "description": "Path to the skill package directory or skill.yaml file.", |
| 53 | + }, |
| 54 | + }, |
| 55 | + "required": ["package_path"], |
| 56 | + }, |
| 57 | + ), |
| 58 | + Tool( |
| 59 | + name="sit_validate", |
| 60 | + description=( |
| 61 | + "Validate a sit skill package structure. " |
| 62 | + "Checks skill.yaml fields, prompt/schema/test paths, schema validity, " |
| 63 | + "JSONL format. Returns pass/fail with messages." |
| 64 | + ), |
| 65 | + inputSchema={ |
| 66 | + "type": "object", |
| 67 | + "properties": { |
| 68 | + "package_path": { |
| 69 | + "type": "string", |
| 70 | + "description": "Path to the skill package directory or skill.yaml file.", |
| 71 | + }, |
| 72 | + }, |
| 73 | + "required": ["package_path"], |
| 74 | + }, |
| 75 | + ), |
| 76 | + Tool( |
| 77 | + name="sit_test", |
| 78 | + description=( |
| 79 | + "Run validation + golden schema tests on a sit skill package. " |
| 80 | + "Returns the sit.test.v1 contract with validation and test results." |
| 81 | + ), |
| 82 | + inputSchema={ |
| 83 | + "type": "object", |
| 84 | + "properties": { |
| 85 | + "package_path": { |
| 86 | + "type": "string", |
| 87 | + "description": "Path to the skill package directory or skill.yaml file.", |
| 88 | + }, |
| 89 | + "run_actual": { |
| 90 | + "type": "boolean", |
| 91 | + "description": "If true, run the actual test runner (commands.run_case) instead of static validation only.", |
| 92 | + "default": False, |
| 93 | + }, |
| 94 | + "runner": { |
| 95 | + "type": "string", |
| 96 | + "description": "Override the test runner command (default: from skill.yaml commands.run_case).", |
| 97 | + }, |
| 98 | + "timeout": { |
| 99 | + "type": "integer", |
| 100 | + "description": "Runner timeout in seconds (default: 30).", |
| 101 | + "default": 30, |
| 102 | + }, |
| 103 | + }, |
| 104 | + "required": ["package_path"], |
| 105 | + }, |
| 106 | + ), |
| 107 | + Tool( |
| 108 | + name="sit_diff", |
| 109 | + description=( |
| 110 | + "Semantic diff between two sit skill packages. " |
| 111 | + "Returns the sit.diff.v1 contract with change events, risk assessment, " |
| 112 | + "and suggested version bump." |
| 113 | + ), |
| 114 | + inputSchema={ |
| 115 | + "type": "object", |
| 116 | + "properties": { |
| 117 | + "old_path": { |
| 118 | + "type": "string", |
| 119 | + "description": "Path to the baseline/old skill package.", |
| 120 | + }, |
| 121 | + "new_path": { |
| 122 | + "type": "string", |
| 123 | + "description": "Path to the current/new skill package.", |
| 124 | + }, |
| 125 | + "include_text_diffs": { |
| 126 | + "type": "boolean", |
| 127 | + "description": "Include full unified diff lines in text_diffs (default: false).", |
| 128 | + "default": False, |
| 129 | + }, |
| 130 | + }, |
| 131 | + "required": ["old_path", "new_path"], |
| 132 | + }, |
| 133 | + ), |
| 134 | + Tool( |
| 135 | + name="sit_pr_summary", |
| 136 | + description=( |
| 137 | + "Generate a PR summary for a skill package change. " |
| 138 | + "Returns the sit.pr_summary.v1 contract with baseline/current refs, " |
| 139 | + "validation, tests, risk, and semantic diff." |
| 140 | + ), |
| 141 | + inputSchema={ |
| 142 | + "type": "object", |
| 143 | + "properties": { |
| 144 | + "baseline_path": { |
| 145 | + "type": "string", |
| 146 | + "description": "Path to the baseline skill package.", |
| 147 | + }, |
| 148 | + "current_path": { |
| 149 | + "type": "string", |
| 150 | + "description": "Path to the current skill package.", |
| 151 | + }, |
| 152 | + }, |
| 153 | + "required": ["baseline_path", "current_path"], |
| 154 | + }, |
| 155 | + ), |
| 156 | + Tool( |
| 157 | + name="sit_report", |
| 158 | + description=( |
| 159 | + "Build a full sit report for a skill package. " |
| 160 | + "Returns the sit.report.v1 contract with validation, golden tests, " |
| 161 | + "optional diff, and reproducibility commands." |
| 162 | + ), |
| 163 | + inputSchema={ |
| 164 | + "type": "object", |
| 165 | + "properties": { |
| 166 | + "package_path": { |
| 167 | + "type": "string", |
| 168 | + "description": "Path to the skill package directory or skill.yaml file.", |
| 169 | + }, |
| 170 | + "compare_path": { |
| 171 | + "type": "string", |
| 172 | + "description": "Optional path to a baseline package for diff comparison.", |
| 173 | + }, |
| 174 | + }, |
| 175 | + "required": ["package_path"], |
| 176 | + }, |
| 177 | + ), |
| 178 | + Tool( |
| 179 | + name="sit_doctor", |
| 180 | + description=( |
| 181 | + "Run environment diagnostics for a sit skill package. " |
| 182 | + "Checks git, GitHub remote, manifest, validation, and golden tests." |
| 183 | + ), |
| 184 | + inputSchema={ |
| 185 | + "type": "object", |
| 186 | + "properties": { |
| 187 | + "package_path": { |
| 188 | + "type": "string", |
| 189 | + "description": "Path to the skill package directory or skill.yaml file.", |
| 190 | + }, |
| 191 | + }, |
| 192 | + "required": ["package_path"], |
| 193 | + }, |
| 194 | + ), |
| 195 | +] |
| 196 | + |
| 197 | + |
| 198 | +# --------------------------------------------------------------------------- |
| 199 | +# Handler |
| 200 | +# --------------------------------------------------------------------------- |
| 201 | + |
| 202 | +def _handle_tool(name: str, arguments: dict[str, Any]) -> str: |
| 203 | + """Dispatch an MCP tool call to the SDK and return JSON string.""" |
| 204 | + try: |
| 205 | + if name == "sit_info": |
| 206 | + result = Sit(arguments["package_path"]).info() |
| 207 | + elif name == "sit_validate": |
| 208 | + result = Sit(arguments["package_path"]).validate() |
| 209 | + elif name == "sit_test": |
| 210 | + result = Sit(arguments["package_path"]).test( |
| 211 | + run_actual=arguments.get("run_actual", False), |
| 212 | + runner=arguments.get("runner"), |
| 213 | + timeout=arguments.get("timeout", 30), |
| 214 | + ) |
| 215 | + elif name == "sit_diff": |
| 216 | + result = Sit(arguments["new_path"]).diff( |
| 217 | + arguments["old_path"], |
| 218 | + include_text_diffs=arguments.get("include_text_diffs", False), |
| 219 | + ) |
| 220 | + elif name == "sit_pr_summary": |
| 221 | + result = Sit(arguments["current_path"]).pr_summary(arguments["baseline_path"]) |
| 222 | + elif name == "sit_report": |
| 223 | + result = Sit(arguments["package_path"]).report( |
| 224 | + compare=arguments.get("compare_path"), |
| 225 | + ) |
| 226 | + elif name == "sit_doctor": |
| 227 | + result = Sit(arguments["package_path"]).doctor() |
| 228 | + else: |
| 229 | + return json.dumps({"error": f"Unknown tool: {name}"}) |
| 230 | + return json.dumps(result, ensure_ascii=False, indent=2) |
| 231 | + except Exception as exc: |
| 232 | + return json.dumps({"error": f"{type(exc).__name__}: {exc}"}) |
| 233 | + |
| 234 | + |
| 235 | +# --------------------------------------------------------------------------- |
| 236 | +# Server factory |
| 237 | +# --------------------------------------------------------------------------- |
| 238 | + |
| 239 | +def create_server() -> Server: |
| 240 | + """Create and configure an MCP Server instance with sit tools.""" |
| 241 | + server = Server("sit-mcp-server") |
| 242 | + |
| 243 | + @server.list_tools() |
| 244 | + async def list_tools() -> list[Tool]: |
| 245 | + return TOOLS |
| 246 | + |
| 247 | + @server.call_tool() |
| 248 | + async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: |
| 249 | + result = _handle_tool(name, arguments) |
| 250 | + return [TextContent(type="text", text=result)] |
| 251 | + |
| 252 | + return server |
| 253 | + |
| 254 | + |
| 255 | +# --------------------------------------------------------------------------- |
| 256 | +# Entry point |
| 257 | +# --------------------------------------------------------------------------- |
| 258 | + |
| 259 | +async def _run_stdio() -> None: |
| 260 | + server = create_server() |
| 261 | + async with stdio_server() as (read_stream, write_stream): |
| 262 | + await server.run(read_stream, write_stream, server.create_initialization_options()) |
| 263 | + |
| 264 | + |
| 265 | +def main() -> None: |
| 266 | + """Run the sit MCP server over stdio transport.""" |
| 267 | + import asyncio |
| 268 | + |
| 269 | + asyncio.run(_run_stdio()) |
| 270 | + |
| 271 | + |
| 272 | +if __name__ == "__main__": |
| 273 | + main() |
0 commit comments