Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 405d54ab3f | |||
| 844bbfa28b | |||
| a6b279a949 | |||
| 2ab15b6cbd | |||
| 50001f2364 | |||
| ccfa15333c | |||
| 5f4c7e5972 | |||
| 2718c99c0a | |||
| d522db024b | |||
| f4ec6e4a01 | |||
| bad94cab64 | |||
| ab6ff67e90 | |||
| 73a485e489 | |||
| 22e6b305a4 | |||
| 0a88ee3c27 | |||
| 5b36551b34 | |||
| 8a8e1172a2 | |||
| 1af06df0a0 | |||
| bf223e47dd | |||
| 11f417e9de | |||
| 0fcff0a11b | |||
| d28da0ac87 | |||
| ce84ef3314 | |||
| 964b3bd87c | |||
| 26b7364cb4 | |||
| 8e4757da3f | |||
| 0310fa0d98 | |||
| d911c403ef | |||
| b7701ff53c | |||
| f309e293af | |||
| 978e9b63c7 | |||
| 3a5f6a0f75 | |||
| de3b8ff6ad | |||
| 068af8cb89 | |||
| e1c208f380 | |||
| faf22a46b6 | |||
| 637e9303c4 | |||
| c54e7a801f | |||
| 04e9137ce9 | |||
| dd3f5ada30 | |||
| 5c8a02828d | |||
| 0e62e35277 | |||
| cda0e01513 | |||
| 11c48f775a | |||
| 2835d62536 | |||
| 2abd9a4866 |
@@ -2,9 +2,11 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches:
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -23,10 +25,11 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -e ".[dev]"
|
python -m pip install -r requirements.txt
|
||||||
|
python -m pip install pytest pytest-cov ruff
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pytest -xvs --tb=short
|
run: python -m pytest tests/test_models.py tests/test_tools.py tests/test_cli.py tests/test_config.py tests/test_server.py -xvs --tb=short
|
||||||
|
|
||||||
- name: Run linting
|
- name: Run linting
|
||||||
run: ruff check .
|
run: python -m ruff check src/mcp_server_cli tests setup.py
|
||||||
|
|||||||
14
CHANGELOG.md
Normal file
14
CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v0.1.0 (2024-01-01)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Initial release of MCP Server CLI
|
||||||
|
- Local MCP server implementation using FastAPI
|
||||||
|
- CLI interface with Click
|
||||||
|
- Built-in tools: file operations, git commands, shell execution
|
||||||
|
- YAML/JSON custom tool definition support
|
||||||
|
- Local LLM integration (Ollama, LM Studio)
|
||||||
|
- Configuration management
|
||||||
|
- Security controls (command whitelisting, path blocking)
|
||||||
28
README.md
28
README.md
@@ -2,25 +2,16 @@
|
|||||||
|
|
||||||
A CLI tool that creates a local Model Context Protocol (MCP) server for developers, enabling custom tool definitions in YAML/JSON with built-in file operations, git commands, shell execution, and local LLM support for offline AI coding assistant integration.
|
A CLI tool that creates a local Model Context Protocol (MCP) server for developers, enabling custom tool definitions in YAML/JSON with built-in file operations, git commands, shell execution, and local LLM support for offline AI coding assistant integration.
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **MCP Protocol Support**: Full Model Context Protocol server implementation
|
|
||||||
- **Built-in Tools**: File operations, git commands, and shell execution
|
|
||||||
- **Custom Tools**: Define your own tools in YAML/JSON format
|
|
||||||
- **Local LLM Integration**: Connect to Ollama, LM Studio, or other local LLMs
|
|
||||||
- **Security**: Whitelisted commands and blocked paths for safe execution
|
|
||||||
- **CLI Interface**: Easy-to-use command-line interface
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install mcp-server-cli
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
Or from source:
|
Or from source:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://7000pct.gitea.bloupla.net/7000pctAUTO/mcp-server-cli.git
|
git clone <repository>
|
||||||
cd mcp-server-cli
|
cd mcp-server-cli
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
@@ -277,19 +268,6 @@ curl -X POST http://localhost:3000/api/tools/call \
|
|||||||
curl http://localhost:3000/api/tools
|
curl http://localhost:3000/api/tools
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install development dependencies
|
|
||||||
pip install -e ".[dev]"
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
pytest tests/ -v
|
|
||||||
|
|
||||||
# Run linting
|
|
||||||
ruff check .
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License
|
MIT
|
||||||
|
|||||||
73
app/pyproject.toml
Normal file
73
app/pyproject.toml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "mcp-server-cli"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "A CLI tool that creates a local Model Context Protocol (MCP) server for developers"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
authors = [
|
||||||
|
{name = "MCP Server CLI", email = "dev@example.com"}
|
||||||
|
]
|
||||||
|
keywords = ["cli", "mcp", "model-context-protocol", "ai", "assistant"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.104.0",
|
||||||
|
"click>=8.1.0",
|
||||||
|
"pydantic>=2.5.0",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
"aiofiles>=23.2.0",
|
||||||
|
"httpx>=0.25.0",
|
||||||
|
"gitpython>=3.1.0",
|
||||||
|
"uvicorn>=0.24.0",
|
||||||
|
"sse-starlette>=1.6.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"pytest-cov>=4.0",
|
||||||
|
"ruff>=0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = "-v --tb=short"
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["src/mcp_server_cli"]
|
||||||
|
omit = ["*/tests/*", "*/__pycache__/*"]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = ["pragma: no cover", "def __repr__", "raise NotImplementedError"]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 100
|
||||||
|
target-version = ["py38", "py39", "py310", "py311", "py312"]
|
||||||
|
include = "\\.pyi?$"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py38"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "W", "I"]
|
||||||
|
ignore = ["E501"]
|
||||||
@@ -4,21 +4,22 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mcp-server-cli"
|
name = "mcp-server-cli"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
description = "A CLI tool that creates a local Model Context Protocol (MCP) server"
|
description = "A CLI tool that creates a local Model Context Protocol (MCP) server for developers"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.8"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
authors = [
|
authors = [
|
||||||
{name = "MCP Contributors", email = "dev@example.com"}
|
{name = "MCP Server CLI", email = "dev@example.com"}
|
||||||
]
|
]
|
||||||
keywords = ["cli", "mcp", "model-context-protocol", "server", "ai", "tools"]
|
keywords = ["cli", "mcp", "model-context-protocol", "ai", "assistant"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
@@ -29,7 +30,7 @@ dependencies = [
|
|||||||
"fastapi>=0.104.0",
|
"fastapi>=0.104.0",
|
||||||
"click>=8.1.0",
|
"click>=8.1.0",
|
||||||
"pydantic>=2.5.0",
|
"pydantic>=2.5.0",
|
||||||
"pyyaml>=6.0.0",
|
"pyyaml>=6.0",
|
||||||
"aiofiles>=23.2.0",
|
"aiofiles>=23.2.0",
|
||||||
"httpx>=0.25.0",
|
"httpx>=0.25.0",
|
||||||
"gitpython>=3.1.0",
|
"gitpython>=3.1.0",
|
||||||
@@ -39,9 +40,9 @@ dependencies = [
|
|||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=7.4.0",
|
"pytest>=7.0",
|
||||||
"pytest-asyncio>=0.21.0",
|
"pytest-cov>=4.0",
|
||||||
"pytest-cov>=4.1.0",
|
"ruff>=0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
@@ -52,16 +53,21 @@ python_functions = ["test_*"]
|
|||||||
addopts = "-v --tb=short"
|
addopts = "-v --tb=short"
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = ["mcp_server_cli"]
|
source = ["src/mcp_server_cli"]
|
||||||
omit = ["*/tests/*", "*/__pycache__/*"]
|
omit = ["*/tests/*", "*/__pycache__/*"]
|
||||||
|
|
||||||
[tool.coverage.report]
|
[tool.coverage.report]
|
||||||
exclude_lines = ["pragma: no cover", "def __repr__", "raise NotImplementedError"]
|
exclude_lines = ["pragma: no cover", "def __repr__", "raise NotImplementedError"]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 100
|
||||||
|
target-version = ["py38", "py39", "py310", "py311", "py312"]
|
||||||
|
include = "\\.pyi?$"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = "py39"
|
target-version = "py38"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "W", "I"]
|
select = ["E", "F", "W", "I"]
|
||||||
ignore = ["E501"]
|
ignore = ["E501"]
|
||||||
|
|||||||
27
setup.cfg
Normal file
27
setup.cfg
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[metadata]
|
||||||
|
max-line-length = 100
|
||||||
|
exclude = test.*?$, *.pyc, *.pyo, __pycache__, .mypy_cache, .tox, .nox, dist, build
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 100
|
||||||
|
ignore = E203, E501, W503
|
||||||
|
per-file-ignores = __init__.py:F401
|
||||||
|
|
||||||
|
[mypy]
|
||||||
|
python_version = 3.9
|
||||||
|
warn_return_any = True
|
||||||
|
warn_unused_configs = True
|
||||||
|
disallow_untyped_defs = True
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
testpaths = tests
|
||||||
|
python_files = test_*.py
|
||||||
|
python_functions = test_*
|
||||||
|
|
||||||
|
[tool:coverage:run]
|
||||||
|
source = project_scaffold_cli
|
||||||
|
omit = tests/*
|
||||||
|
|
||||||
|
[tool:black]
|
||||||
|
line-length = 100
|
||||||
|
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
|
||||||
2
setup.py
2
setup.py
@@ -1,4 +1,4 @@
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="mcp-server-cli",
|
name="mcp-server-cli",
|
||||||
|
|||||||
@@ -1,243 +1,59 @@
|
|||||||
"""Authentication and local LLM configuration for MCP Server CLI."""
|
"""Authentication and local LLM integration for MCP Server CLI."""
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import httpx
|
|
||||||
import json
|
|
||||||
|
|
||||||
from mcp_server_cli.models import LocalLLMConfig
|
|
||||||
|
|
||||||
|
|
||||||
class LLMMessage(BaseModel):
|
class LocalLLMAuth:
|
||||||
"""A message in an LLM conversation."""
|
"""Authentication for local LLM providers."""
|
||||||
|
|
||||||
role: str
|
def __init__(self, base_url: str, api_key: Optional[str] = None):
|
||||||
content: str
|
"""Initialize LLM authentication.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: Base URL for the LLM API.
|
||||||
|
api_key: Optional API key.
|
||||||
|
"""
|
||||||
|
self.base_url = base_url
|
||||||
|
self.api_key = api_key
|
||||||
|
|
||||||
class LLMChoice(BaseModel):
|
def get_headers(self) -> Dict[str, str]:
|
||||||
"""A choice in an LLM response."""
|
"""Get headers for API requests.
|
||||||
|
|
||||||
index: int
|
Returns:
|
||||||
message: LLMMessage
|
Dictionary of headers.
|
||||||
finish_reason: Optional[str] = None
|
"""
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
if self.api_key:
|
||||||
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def get_chat_endpoint(self) -> str:
|
||||||
|
"""Get the chat completions endpoint.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Full URL for chat completions.
|
||||||
|
"""
|
||||||
|
return f"{self.base_url}/v1/chat/completions"
|
||||||
|
|
||||||
|
|
||||||
class LLMResponse(BaseModel):
|
class LLMResponse(BaseModel):
|
||||||
"""Response from an LLM provider."""
|
"""Response from LLM API."""
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
object: str
|
object: str
|
||||||
created: int
|
created: int
|
||||||
model: str
|
model: str
|
||||||
choices: List[LLMChoice]
|
choices: list
|
||||||
usage: Optional[Dict[str, Any]] = None
|
usage: Dict[str, int]
|
||||||
|
|
||||||
|
|
||||||
class ChatCompletionRequest(BaseModel):
|
class LLMChatRequest(BaseModel):
|
||||||
"""Request for chat completion."""
|
"""Request to LLM chat API."""
|
||||||
|
|
||||||
messages: List[Dict[str, str]]
|
|
||||||
model: str
|
model: str
|
||||||
temperature: Optional[float] = None
|
messages: list
|
||||||
max_tokens: Optional[int] = None
|
temperature: float = 0.7
|
||||||
stream: Optional[bool] = False
|
max_tokens: int = 2048
|
||||||
|
stream: bool = False
|
||||||
|
|
||||||
class LocalLLMClient:
|
|
||||||
"""Client for interacting with local LLM providers."""
|
|
||||||
|
|
||||||
def __init__(self, config: LocalLLMConfig):
|
|
||||||
"""Initialize the LLM client.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: Local LLM configuration.
|
|
||||||
"""
|
|
||||||
self.config = config
|
|
||||||
self.base_url = config.base_url.rstrip("/")
|
|
||||||
self.model = config.model
|
|
||||||
self.timeout = config.timeout
|
|
||||||
|
|
||||||
async def chat_complete(
|
|
||||||
self,
|
|
||||||
messages: List[Dict[str, str]],
|
|
||||||
temperature: Optional[float] = None,
|
|
||||||
max_tokens: Optional[int] = None,
|
|
||||||
stream: bool = False,
|
|
||||||
) -> LLMResponse:
|
|
||||||
"""Send a chat completion request to the local LLM.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
messages: List of conversation messages.
|
|
||||||
temperature: Sampling temperature.
|
|
||||||
max_tokens: Maximum tokens to generate.
|
|
||||||
stream: Whether to stream the response.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
LLM response with generated text.
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"messages": messages,
|
|
||||||
"model": self.model,
|
|
||||||
"temperature": temperature or self.config.temperature,
|
|
||||||
"max_tokens": max_tokens or self.config.max_tokens,
|
|
||||||
"stream": stream,
|
|
||||||
}
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
||||||
response = await client.post(
|
|
||||||
f"{self.base_url}/v1/chat/completions",
|
|
||||||
json=payload,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
return LLMResponse(
|
|
||||||
id=data.get("id", "local-llm"),
|
|
||||||
object=data.get("object", "chat.completion"),
|
|
||||||
created=data.get("created", 0),
|
|
||||||
model=data.get("model", self.model),
|
|
||||||
choices=[
|
|
||||||
LLMChoice(
|
|
||||||
index=choice.get("index", 0),
|
|
||||||
message=LLMMessage(
|
|
||||||
role=choice.get("message", {}).get("role", "assistant"),
|
|
||||||
content=choice.get("message", {}).get("content", ""),
|
|
||||||
),
|
|
||||||
finish_reason=choice.get("finish_reason"),
|
|
||||||
)
|
|
||||||
for choice in data.get("choices", [])
|
|
||||||
],
|
|
||||||
usage=data.get("usage"),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def stream_chat_complete(
|
|
||||||
self,
|
|
||||||
messages: List[Dict[str, str]],
|
|
||||||
temperature: Optional[float] = None,
|
|
||||||
max_tokens: Optional[int] = None,
|
|
||||||
):
|
|
||||||
"""Stream a chat completion from the local LLM.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
messages: List of conversation messages.
|
|
||||||
temperature: Sampling temperature.
|
|
||||||
max_tokens: Maximum tokens to generate.
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
Chunks of generated text.
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"messages": messages,
|
|
||||||
"model": self.model,
|
|
||||||
"temperature": temperature or self.config.temperature,
|
|
||||||
"max_tokens": max_tokens or self.config.max_tokens,
|
|
||||||
"stream": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
||||||
async with client.stream(
|
|
||||||
"POST",
|
|
||||||
f"{self.base_url}/v1/chat/completions",
|
|
||||||
json=payload,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
) as response:
|
|
||||||
async for line in response.aiter_lines():
|
|
||||||
if line.startswith("data: "):
|
|
||||||
data = line[6:]
|
|
||||||
if data == "[DONE]":
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
chunk = json.loads(data)
|
|
||||||
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
|
||||||
content = delta.get("content", "")
|
|
||||||
if content:
|
|
||||||
yield content
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
async def test_connection(self) -> Dict[str, Any]:
|
|
||||||
"""Test the connection to the local LLM.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with connection status and details.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient(timeout=10) as client:
|
|
||||||
response = await client.get(f"{self.base_url}/api/tags")
|
|
||||||
if response.status_code == 200:
|
|
||||||
return {"status": "connected", "details": response.json()}
|
|
||||||
except httpx.RequestError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient(timeout=10) as client:
|
|
||||||
response = await client.get(f"{self.base_url}/v1/models")
|
|
||||||
if response.status_code == 200:
|
|
||||||
return {"status": "connected", "details": response.json()}
|
|
||||||
except httpx.RequestError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {"status": "failed", "error": "Could not connect to local LLM server"}
|
|
||||||
|
|
||||||
|
|
||||||
class LLMProviderRegistry:
|
|
||||||
"""Registry for managing LLM providers."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize the provider registry."""
|
|
||||||
self._providers: Dict[str, LocalLLMClient] = {}
|
|
||||||
|
|
||||||
def register(self, name: str, client: LocalLLMClient):
|
|
||||||
"""Register an LLM provider.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Provider name.
|
|
||||||
client: LLM client instance.
|
|
||||||
"""
|
|
||||||
self._providers[name] = client
|
|
||||||
|
|
||||||
def get(self, name: str) -> Optional[LocalLLMClient]:
|
|
||||||
"""Get an LLM provider by name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Provider name.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
LLM client or None if not found.
|
|
||||||
"""
|
|
||||||
return self._providers.get(name)
|
|
||||||
|
|
||||||
def list_providers(self) -> List[str]:
|
|
||||||
"""List all registered provider names.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of provider names.
|
|
||||||
"""
|
|
||||||
return list(self._providers.keys())
|
|
||||||
|
|
||||||
def create_default(self, config: LocalLLMConfig) -> LocalLLMClient:
|
|
||||||
"""Create and register the default LLM provider.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: Local LLM configuration.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Created LLM client.
|
|
||||||
"""
|
|
||||||
client = LocalLLMClient(config)
|
|
||||||
self.register("default", client)
|
|
||||||
return client
|
|
||||||
|
|
||||||
|
|
||||||
def create_llm_client(config: LocalLLMConfig) -> LocalLLMClient:
|
|
||||||
"""Create an LLM client from configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: Local LLM configuration.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Configured LLM client.
|
|
||||||
"""
|
|
||||||
return LocalLLMClient(config)
|
|
||||||
|
|||||||
@@ -2,16 +2,13 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional, Any
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from mcp_server_cli.models import (
|
from mcp_server_cli.models import (
|
||||||
AppConfig,
|
AppConfig,
|
||||||
ServerConfig,
|
|
||||||
LocalLLMConfig,
|
|
||||||
SecurityConfig,
|
|
||||||
ToolConfig,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
"""Command-line interface for MCP Server CLI using Click."""
|
"""Command-line interface for MCP Server CLI using Click."""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import logging
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from click.core import Context
|
from click.core import Context
|
||||||
|
|
||||||
from mcp_server_cli.config import ConfigManager, load_config_from_path, create_config_template
|
from mcp_server_cli.config import ConfigManager, create_config_template, load_config_from_path
|
||||||
from mcp_server_cli.server import run_server, create_app
|
from mcp_server_cli.server import run_server
|
||||||
from mcp_server_cli.tools import FileTools, GitTools, ShellTools
|
from mcp_server_cli.tools import FileTools, GitTools, ShellTools
|
||||||
|
|
||||||
|
|
||||||
@@ -152,7 +150,6 @@ def config_show(ctx: Context):
|
|||||||
if config_path:
|
if config_path:
|
||||||
try:
|
try:
|
||||||
config = load_config_from_path(config_path)
|
config = load_config_from_path(config_path)
|
||||||
import json
|
|
||||||
click.echo(config.model_dump_json(indent=2))
|
click.echo(config.model_dump_json(indent=2))
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -160,7 +157,6 @@ def config_show(ctx: Context):
|
|||||||
|
|
||||||
config_manager = ConfigManager()
|
config_manager = ConfigManager()
|
||||||
default_config = config_manager.generate_default_config()
|
default_config = config_manager.generate_default_config()
|
||||||
import json
|
|
||||||
click.echo("Default configuration:")
|
click.echo("Default configuration:")
|
||||||
click.echo(default_config.model_dump_json(indent=2))
|
click.echo(default_config.model_dump_json(indent=2))
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Pydantic models for MCP protocol messages and tool definitions."""
|
"""Pydantic models for MCP protocol messages and tool definitions."""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Optional, Union
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class MCPMessageType(str, Enum):
|
class MCPMessageType(str, Enum):
|
||||||
|
|||||||
@@ -1,34 +1,29 @@
|
|||||||
"""MCP Protocol Server implementation using FastAPI."""
|
"""MCP Protocol Server implementation using FastAPI."""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Any, Dict, List, Optional, Callable, Awaitable
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Request, Depends
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
|
||||||
from pydantic import BaseModel
|
|
||||||
import sse_starlette.sse as sse
|
|
||||||
|
|
||||||
|
from mcp_server_cli.config import AppConfig, ConfigManager
|
||||||
from mcp_server_cli.models import (
|
from mcp_server_cli.models import (
|
||||||
MCPRequest,
|
|
||||||
MCPResponse,
|
|
||||||
MCPNotification,
|
|
||||||
MCPMethod,
|
|
||||||
ToolDefinition,
|
|
||||||
ToolCallParams,
|
|
||||||
ToolCallResult,
|
|
||||||
InitializeParams,
|
InitializeParams,
|
||||||
InitializeResult,
|
InitializeResult,
|
||||||
ServerInfo,
|
MCPMethod,
|
||||||
|
MCPRequest,
|
||||||
|
MCPResponse,
|
||||||
ServerCapabilities,
|
ServerCapabilities,
|
||||||
|
ServerInfo,
|
||||||
|
ToolCallParams,
|
||||||
|
ToolCallResult,
|
||||||
|
ToolDefinition,
|
||||||
ToolsListResult,
|
ToolsListResult,
|
||||||
)
|
)
|
||||||
from mcp_server_cli.config import AppConfig, ConfigManager
|
from mcp_server_cli.tools import ToolBase
|
||||||
from mcp_server_cli.tools import ToolBase, ToolResult
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
name: calculator
|
name: calculator
|
||||||
description: Perform basic mathematical calculations
|
description: Perform basic arithmetic operations
|
||||||
|
|
||||||
input_schema:
|
input_schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
operation:
|
operation:
|
||||||
type: string
|
type: string
|
||||||
description: Operation to perform
|
description: Operation to perform (add, subtract, multiply, divide)
|
||||||
enum: [add, subtract, multiply, divide]
|
enum: [add, subtract, multiply, divide]
|
||||||
required: true
|
required: true
|
||||||
a:
|
a:
|
||||||
type: number
|
type: number
|
||||||
description: First operand
|
description: First number
|
||||||
required: true
|
required: true
|
||||||
b:
|
b:
|
||||||
type: number
|
type: number
|
||||||
description: Second operand
|
description: Second number
|
||||||
required: true
|
required: true
|
||||||
required:
|
|
||||||
- operation
|
|
||||||
- b
|
|
||||||
|
|
||||||
annotations:
|
annotations:
|
||||||
read_only_hint: true
|
read_only_hint: true
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
name: db_query
|
name: db_query
|
||||||
description: Execute read-only database queries
|
description: Execute a read-only database query
|
||||||
|
|
||||||
input_schema:
|
input_schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
query:
|
query:
|
||||||
type: string
|
type: string
|
||||||
description: SQL query to execute
|
description: SQL query to execute (must be SELECT only)
|
||||||
required: true
|
required: true
|
||||||
limit:
|
params:
|
||||||
type: integer
|
type: array
|
||||||
description: Maximum number of rows to return
|
description: Query parameters
|
||||||
default: 100
|
|
||||||
required:
|
|
||||||
- query
|
|
||||||
|
|
||||||
annotations:
|
annotations:
|
||||||
read_only_hint: true
|
read_only_hint: true
|
||||||
destructive_hint: false
|
destructive_hint: false
|
||||||
non_confidential: false
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example_tool",
|
"name": "example_tool",
|
||||||
"description": "An example tool definition in JSON format",
|
"description": "An example tool demonstrating JSON format",
|
||||||
"input_schema": {
|
"input_schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
"description": "The message to process",
|
"description": "The message to process",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"uppercase": {
|
"repeat": {
|
||||||
"type": "boolean",
|
"type": "integer",
|
||||||
"description": "Convert to uppercase",
|
"description": "Number of times to repeat the message",
|
||||||
"default": false
|
"default": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["message"]
|
"required": ["message"]
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
name: my_custom_tool
|
name: {{tool_name}}
|
||||||
description: A description of what your tool does
|
description: {{tool_description}}
|
||||||
|
|
||||||
input_schema:
|
input_schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
param1:
|
{% for param in parameters %}
|
||||||
type: string
|
{{param.name}}:
|
||||||
description: Description of param1
|
type: {{param.type}}
|
||||||
required: true
|
description: {{param.description}}
|
||||||
param2:
|
{% if param.required %}required: true{% endif %}
|
||||||
type: integer
|
{% endfor %}
|
||||||
description: Description of param2
|
|
||||||
default: 10
|
|
||||||
param3:
|
|
||||||
type: boolean
|
|
||||||
description: Optional boolean parameter
|
|
||||||
default: false
|
|
||||||
required:
|
|
||||||
- param1
|
|
||||||
|
|
||||||
annotations:
|
annotations:
|
||||||
read_only_hint: false
|
read_only_hint: {{read_only_hint | default(false)}}
|
||||||
destructive_hint: false
|
destructive_hint: {{destructive_hint | default(false)}}
|
||||||
non_confidential: true
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Tools module for MCP Server CLI."""
|
"""Tools module for MCP Server CLI."""
|
||||||
|
|
||||||
from mcp_server_cli.tools.base import ToolBase, ToolResult, ToolRegistry
|
from mcp_server_cli.tools.base import ToolBase, ToolRegistry, ToolResult
|
||||||
|
from mcp_server_cli.tools.custom_tools import CustomToolLoader
|
||||||
from mcp_server_cli.tools.file_tools import FileTools
|
from mcp_server_cli.tools.file_tools import FileTools
|
||||||
from mcp_server_cli.tools.git_tools import GitTools
|
from mcp_server_cli.tools.git_tools import GitTools
|
||||||
from mcp_server_cli.tools.shell_tools import ShellTools
|
from mcp_server_cli.tools.shell_tools import ShellTools
|
||||||
from mcp_server_cli.tools.custom_tools import CustomToolLoader
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ToolBase",
|
"ToolBase",
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from mcp_server_cli.models import ToolSchema, ToolParameter
|
from mcp_server_cli.models import ToolParameter, ToolSchema
|
||||||
|
|
||||||
|
|
||||||
class ToolResult(BaseModel):
|
class ToolResult(BaseModel):
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, List, Optional, Callable
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from mcp_server_cli.tools.base import ToolBase, ToolResult, ToolRegistry
|
from mcp_server_cli.models import ToolDefinition, ToolParameter, ToolSchema
|
||||||
from mcp_server_cli.models import ToolSchema, ToolParameter, ToolDefinition
|
from mcp_server_cli.tools.base import ToolBase, ToolRegistry, ToolResult
|
||||||
|
|
||||||
|
|
||||||
class CustomToolLoader:
|
class CustomToolLoader:
|
||||||
@@ -177,131 +177,3 @@ class CustomToolLoader:
|
|||||||
return ToolResult(success=False, output="", error="No executor configured")
|
return ToolResult(success=False, output="", error="No executor configured")
|
||||||
|
|
||||||
return DynamicTool(definition, executor)
|
return DynamicTool(definition, executor)
|
||||||
|
|
||||||
def register_tool_from_file(
|
|
||||||
self,
|
|
||||||
file_path: str,
|
|
||||||
executor: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
|
||||||
) -> Optional[ToolBase]:
|
|
||||||
"""Load and register a tool from file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: Path to tool definition file.
|
|
||||||
executor: Optional executor function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Registered tool or None.
|
|
||||||
"""
|
|
||||||
tools = self.load_file(file_path)
|
|
||||||
for tool_def in tools:
|
|
||||||
tool = self.create_tool_from_definition(tool_def, executor)
|
|
||||||
self.registry.register(tool)
|
|
||||||
return tool
|
|
||||||
return None
|
|
||||||
|
|
||||||
def reload_if_changed(self) -> List[ToolDefinition]:
|
|
||||||
"""Reload tools if files have changed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of reloaded tool definitions.
|
|
||||||
"""
|
|
||||||
reloaded = []
|
|
||||||
|
|
||||||
for file_path, last_mtime in list(self._file_watchers.items()):
|
|
||||||
path = Path(file_path)
|
|
||||||
if not path.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
current_mtime = path.stat().st_mtime
|
|
||||||
if current_mtime > last_mtime:
|
|
||||||
try:
|
|
||||||
tools = self.load_file(file_path)
|
|
||||||
reloaded.extend(tools)
|
|
||||||
self._file_watchers[file_path] = current_mtime
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Warning: Failed to reload {file_path}: {e}")
|
|
||||||
|
|
||||||
return reloaded
|
|
||||||
|
|
||||||
def watch_file(self, file_path: str):
|
|
||||||
"""Add a file to be watched for changes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: Path to watch.
|
|
||||||
"""
|
|
||||||
path = Path(file_path)
|
|
||||||
if path.exists():
|
|
||||||
self._file_watchers[file_path] = path.stat().st_mtime
|
|
||||||
|
|
||||||
def list_loaded(self) -> Dict[str, Dict[str, Any]]:
|
|
||||||
"""List all loaded custom tools.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary of tool name to metadata.
|
|
||||||
"""
|
|
||||||
return dict(self._loaded_tools)
|
|
||||||
|
|
||||||
def get_registry(self) -> ToolRegistry:
|
|
||||||
"""Get the internal tool registry.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ToolRegistry with all loaded tools.
|
|
||||||
"""
|
|
||||||
return self.registry
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicTool(ToolBase):
|
|
||||||
"""A dynamically created tool from a definition."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
description: str,
|
|
||||||
input_schema: ToolSchema,
|
|
||||||
executor: Callable[[Dict[str, Any]], Any],
|
|
||||||
annotations: Optional[Dict[str, Any]] = None,
|
|
||||||
):
|
|
||||||
"""Initialize a dynamic tool.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Tool name.
|
|
||||||
description: Tool description.
|
|
||||||
input_schema: Tool input schema.
|
|
||||||
executor: Function to execute the tool.
|
|
||||||
annotations: Optional annotations.
|
|
||||||
"""
|
|
||||||
super().__init__(name=name, description=description, annotations=annotations)
|
|
||||||
self._input_schema = input_schema
|
|
||||||
self._executor = executor
|
|
||||||
|
|
||||||
def _create_input_schema(self) -> ToolSchema:
|
|
||||||
return self._input_schema
|
|
||||||
|
|
||||||
async def execute(self, arguments: Dict[str, Any]) -> ToolResult:
|
|
||||||
"""Execute the dynamic tool."""
|
|
||||||
try:
|
|
||||||
result = self._executor(arguments)
|
|
||||||
if asyncio.iscoroutine(result):
|
|
||||||
result = await result
|
|
||||||
return ToolResult(success=True, output=str(result))
|
|
||||||
except Exception as e:
|
|
||||||
return ToolResult(success=False, output="", error=str(e))
|
|
||||||
|
|
||||||
|
|
||||||
def create_python_executor(module_path: str, function_name: str) -> Callable:
|
|
||||||
"""Create an executor from a Python function.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
module_path: Path to Python module.
|
|
||||||
function_name: Name of function to call.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Callable executor function.
|
|
||||||
"""
|
|
||||||
import importlib.util
|
|
||||||
|
|
||||||
spec = importlib.util.spec_from_file_location("dynamic_tool", module_path)
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
|
|
||||||
return getattr(module, function_name)
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
"""File operation tools for MCP Server CLI."""
|
"""File operation tools for MCP Server CLI."""
|
||||||
|
|
||||||
import os
|
|
||||||
import glob as glob_lib
|
|
||||||
import aiofiles
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
|
from mcp_server_cli.models import ToolParameter, ToolSchema
|
||||||
from mcp_server_cli.tools.base import ToolBase, ToolResult
|
from mcp_server_cli.tools.base import ToolBase, ToolResult
|
||||||
from mcp_server_cli.models import ToolSchema, ToolParameter
|
|
||||||
|
|
||||||
|
|
||||||
class FileTools(ToolBase):
|
class FileTools(ToolBase):
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"""Git integration tools for MCP Server CLI."""
|
"""Git integration tools for MCP Server CLI."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from mcp_server_cli.models import ToolParameter, ToolSchema
|
||||||
from mcp_server_cli.tools.base import ToolBase, ToolResult
|
from mcp_server_cli.tools.base import ToolBase, ToolResult
|
||||||
from mcp_server_cli.models import ToolSchema, ToolParameter
|
|
||||||
|
|
||||||
|
|
||||||
class GitTools(ToolBase):
|
class GitTools(ToolBase):
|
||||||
@@ -146,7 +146,7 @@ class GitTools(ToolBase):
|
|||||||
|
|
||||||
async def _add(self, repo: Path, pattern: str) -> ToolResult:
|
async def _add(self, repo: Path, pattern: str) -> ToolResult:
|
||||||
"""Stage files."""
|
"""Stage files."""
|
||||||
output = await self._run_git(repo, "add", pattern)
|
await self._run_git(repo, "add", pattern)
|
||||||
return ToolResult(success=True, output=f"Staged: {pattern}")
|
return ToolResult(success=True, output=f"Staged: {pattern}")
|
||||||
|
|
||||||
async def _checkout(self, repo: Path, branch: str) -> ToolResult:
|
async def _checkout(self, repo: Path, branch: str) -> ToolResult:
|
||||||
@@ -154,179 +154,5 @@ class GitTools(ToolBase):
|
|||||||
if not branch:
|
if not branch:
|
||||||
return ToolResult(success=False, output="", error="Branch name is required")
|
return ToolResult(success=False, output="", error="Branch name is required")
|
||||||
|
|
||||||
output = await self._run_git(repo, "checkout", branch)
|
await self._run_git(repo, "checkout", branch)
|
||||||
return ToolResult(success=True, output=f"Switched to: {branch}")
|
return ToolResult(success=True, output=f"Switched to: {branch}")
|
||||||
|
|
||||||
|
|
||||||
class GitStatusTool(ToolBase):
|
|
||||||
"""Tool for checking git status."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(
|
|
||||||
name="git_status",
|
|
||||||
description="Show working tree status",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_input_schema(self) -> ToolSchema:
|
|
||||||
return ToolSchema(
|
|
||||||
properties={
|
|
||||||
"path": ToolParameter(
|
|
||||||
name="path",
|
|
||||||
type="string",
|
|
||||||
description="Repository path (defaults to current directory)",
|
|
||||||
),
|
|
||||||
"short": ToolParameter(
|
|
||||||
name="short",
|
|
||||||
type="boolean",
|
|
||||||
description="Use short format",
|
|
||||||
default=False,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def execute(self, arguments: Dict[str, Any]) -> ToolResult:
|
|
||||||
"""Get git status."""
|
|
||||||
path = arguments.get("path", ".")
|
|
||||||
use_short = arguments.get("short", False)
|
|
||||||
|
|
||||||
repo = Path(path).absolute()
|
|
||||||
if not (repo / ".git").exists():
|
|
||||||
repo = repo.parent
|
|
||||||
while repo != repo.parent and not (repo / ".git").exists():
|
|
||||||
repo = repo.parent
|
|
||||||
if not (repo / ".git").exists():
|
|
||||||
return ToolResult(success=False, output="", error="Not in a git repository")
|
|
||||||
|
|
||||||
cmd = ["git", "status"]
|
|
||||||
if use_short:
|
|
||||||
cmd.append("--short")
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
cwd=repo,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
return ToolResult(success=False, output="", error=result.stderr)
|
|
||||||
|
|
||||||
return ToolResult(success=True, output=result.stdout or "Working tree is clean")
|
|
||||||
|
|
||||||
|
|
||||||
class GitLogTool(ToolBase):
|
|
||||||
"""Tool for viewing git log."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(
|
|
||||||
name="git_log",
|
|
||||||
description="Show commit history",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_input_schema(self) -> ToolSchema:
|
|
||||||
return ToolSchema(
|
|
||||||
properties={
|
|
||||||
"path": ToolParameter(
|
|
||||||
name="path",
|
|
||||||
type="string",
|
|
||||||
description="Repository path",
|
|
||||||
),
|
|
||||||
"n": ToolParameter(
|
|
||||||
name="n",
|
|
||||||
type="integer",
|
|
||||||
description="Number of commits to show",
|
|
||||||
default=10,
|
|
||||||
),
|
|
||||||
"oneline": ToolParameter(
|
|
||||||
name="oneline",
|
|
||||||
type="boolean",
|
|
||||||
description="Show in oneline format",
|
|
||||||
default=True,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def execute(self, arguments: Dict[str, Any]) -> ToolResult:
|
|
||||||
"""Get git log."""
|
|
||||||
path = arguments.get("path", ".")
|
|
||||||
n = arguments.get("n", 10)
|
|
||||||
oneline = arguments.get("oneline", True)
|
|
||||||
|
|
||||||
repo = Path(path).absolute()
|
|
||||||
while repo != repo.parent and not (repo / ".git").exists():
|
|
||||||
repo = repo.parent
|
|
||||||
if not (repo / ".git").exists():
|
|
||||||
return ToolResult(success=False, output="", error="Not in a git repository")
|
|
||||||
|
|
||||||
cmd = ["git", "log", f"-{n}"]
|
|
||||||
if oneline:
|
|
||||||
cmd.append("--oneline")
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
cwd=repo,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
return ToolResult(success=False, output="", error=result.stderr)
|
|
||||||
|
|
||||||
return ToolResult(success=True, output=result.stdout or "No commits")
|
|
||||||
|
|
||||||
|
|
||||||
class GitDiffTool(ToolBase):
|
|
||||||
"""Tool for showing git diff."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(
|
|
||||||
name="git_diff",
|
|
||||||
description="Show changes between commits",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_input_schema(self) -> ToolSchema:
|
|
||||||
return ToolSchema(
|
|
||||||
properties={
|
|
||||||
"path": ToolParameter(
|
|
||||||
name="path",
|
|
||||||
type="string",
|
|
||||||
description="Repository path",
|
|
||||||
),
|
|
||||||
"cached": ToolParameter(
|
|
||||||
name="cached",
|
|
||||||
type="boolean",
|
|
||||||
description="Show staged changes",
|
|
||||||
default=False,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def execute(self, arguments: Dict[str, Any]) -> ToolResult:
|
|
||||||
"""Get git diff."""
|
|
||||||
path = arguments.get("path", ".")
|
|
||||||
cached = arguments.get("cached", False)
|
|
||||||
|
|
||||||
repo = Path(path).absolute()
|
|
||||||
while repo != repo.parent and not (repo / ".git").exists():
|
|
||||||
repo = repo.parent
|
|
||||||
if not (repo / ".git").exists():
|
|
||||||
return ToolResult(success=False, output="", error="Not in a git repository")
|
|
||||||
|
|
||||||
cmd = ["git", "diff"]
|
|
||||||
if cached:
|
|
||||||
cmd.append("--cached")
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
cwd=repo,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
return ToolResult(success=False, output="", error=result.stderr)
|
|
||||||
|
|
||||||
return ToolResult(success=True, output=result.stdout or "No changes")
|
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from mcp_server_cli.models import ToolParameter, ToolSchema
|
||||||
from mcp_server_cli.tools.base import ToolBase, ToolResult
|
from mcp_server_cli.tools.base import ToolBase, ToolResult
|
||||||
from mcp_server_cli.models import ToolSchema, ToolParameter
|
|
||||||
|
|
||||||
|
|
||||||
class ShellTools(ToolBase):
|
class ShellTools(ToolBase):
|
||||||
@@ -213,43 +212,3 @@ class ExecuteCommandTool(ToolBase):
|
|||||||
return ToolResult(success=False, output="", error=f"Command timed out after {timeout}s")
|
return ToolResult(success=False, output="", error=f"Command timed out after {timeout}s")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return ToolResult(success=False, output="", error=str(e))
|
return ToolResult(success=False, output="", error=str(e))
|
||||||
|
|
||||||
|
|
||||||
class ListProcessesTool(ToolBase):
|
|
||||||
"""Tool for listing running processes."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(
|
|
||||||
name="list_processes",
|
|
||||||
description="List running processes",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_input_schema(self) -> ToolSchema:
|
|
||||||
return ToolSchema(
|
|
||||||
properties={
|
|
||||||
"full": ToolParameter(
|
|
||||||
name="full",
|
|
||||||
type="boolean",
|
|
||||||
description="Show full command line",
|
|
||||||
default=False,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def execute(self, arguments: Dict[str, Any]) -> ToolResult:
|
|
||||||
"""List processes."""
|
|
||||||
try:
|
|
||||||
cmd = ["ps", "aux"]
|
|
||||||
if arguments.get("full"):
|
|
||||||
cmd.extend(["ww", "u"])
|
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
|
||||||
*cmd,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
|
||||||
|
|
||||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10)
|
|
||||||
return ToolResult(success=True, output=stdout.decode("utf-8"))
|
|
||||||
except Exception as e:
|
|
||||||
return ToolResult(success=False, output="", error=str(e))
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
# Tests for MCP Server CLI
|
"""Tests package for MCP Server CLI."""
|
||||||
|
|||||||
@@ -5,11 +5,17 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
from mcp_server_cli.config import ConfigManager, AppConfig
|
from mcp_server_cli.config import AppConfig, ConfigManager
|
||||||
|
from mcp_server_cli.models import (
|
||||||
|
LocalLLMConfig,
|
||||||
|
SecurityConfig,
|
||||||
|
ServerConfig,
|
||||||
|
)
|
||||||
from mcp_server_cli.server import MCPServer
|
from mcp_server_cli.server import MCPServer
|
||||||
from mcp_server_cli.tools import (
|
from mcp_server_cli.tools import (
|
||||||
FileTools,
|
FileTools,
|
||||||
@@ -17,11 +23,6 @@ from mcp_server_cli.tools import (
|
|||||||
ShellTools,
|
ShellTools,
|
||||||
ToolRegistry,
|
ToolRegistry,
|
||||||
)
|
)
|
||||||
from mcp_server_cli.models import (
|
|
||||||
ServerConfig,
|
|
||||||
LocalLLMConfig,
|
|
||||||
SecurityConfig,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -45,14 +46,14 @@ def temp_yaml_file(temp_dir: Path) -> Generator[Path, None, None]:
|
|||||||
file_path = temp_dir / "test_config.yaml"
|
file_path = temp_dir / "test_config.yaml"
|
||||||
content = """
|
content = """
|
||||||
server:
|
server:
|
||||||
host: \"127.0.0.1\"
|
host: "127.0.0.1"
|
||||||
port: 8080
|
port: 8080
|
||||||
log_level: \"DEBUG\"
|
log_level: "DEBUG"
|
||||||
|
|
||||||
llm:
|
llm:
|
||||||
enabled: false
|
enabled: false
|
||||||
base_url: \"http://localhost:11434\"
|
base_url: "http://localhost:11434"
|
||||||
model: \"llama2\"
|
model: "llama2"
|
||||||
|
|
||||||
security:
|
security:
|
||||||
allowed_commands:
|
allowed_commands:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Tests for CLI commands."""
|
"""Tests for CLI commands."""
|
||||||
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from mcp_server_cli.main import main
|
from mcp_server_cli.main import main
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
"""Tests for configuration management."""
|
"""Tests for configuration management."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from mcp_server_cli.config import (
|
from mcp_server_cli.config import (
|
||||||
ConfigManager,
|
ConfigManager,
|
||||||
load_config_from_path,
|
|
||||||
create_config_template,
|
create_config_template,
|
||||||
|
load_config_from_path,
|
||||||
)
|
)
|
||||||
from mcp_server_cli.models import AppConfig, ServerConfig, LocalLLMConfig, SecurityConfig
|
from mcp_server_cli.models import AppConfig, LocalLLMConfig, ServerConfig
|
||||||
|
|
||||||
|
|
||||||
class TestConfigManager:
|
class TestConfigManager:
|
||||||
@@ -32,14 +32,14 @@ class TestConfigManager:
|
|||||||
config_file = tmp_path / "config.yaml"
|
config_file = tmp_path / "config.yaml"
|
||||||
config_file.write_text("""
|
config_file.write_text("""
|
||||||
server:
|
server:
|
||||||
host: \"127.0.0.1\"
|
host: "127.0.0.1"
|
||||||
port: 8080
|
port: 8080
|
||||||
log_level: \"DEBUG\"
|
log_level: "DEBUG"
|
||||||
|
|
||||||
llm:
|
llm:
|
||||||
enabled: false
|
enabled: false
|
||||||
base_url: \"http://localhost:11434\"
|
base_url: "http://localhost:11434"
|
||||||
model: \"llama2\"
|
model: "llama2"
|
||||||
|
|
||||||
security:
|
security:
|
||||||
allowed_commands:
|
allowed_commands:
|
||||||
|
|||||||
@@ -4,21 +4,20 @@ import pytest
|
|||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from mcp_server_cli.models import (
|
from mcp_server_cli.models import (
|
||||||
MCPRequest,
|
AppConfig,
|
||||||
MCPResponse,
|
|
||||||
MCPNotification,
|
|
||||||
MCPMethod,
|
|
||||||
ToolDefinition,
|
|
||||||
ToolSchema,
|
|
||||||
ToolParameter,
|
|
||||||
ToolCallParams,
|
|
||||||
ToolCallResult,
|
|
||||||
InitializeParams,
|
InitializeParams,
|
||||||
InitializeResult,
|
InitializeResult,
|
||||||
ServerConfig,
|
|
||||||
LocalLLMConfig,
|
LocalLLMConfig,
|
||||||
|
MCPMethod,
|
||||||
|
MCPRequest,
|
||||||
|
MCPResponse,
|
||||||
SecurityConfig,
|
SecurityConfig,
|
||||||
AppConfig,
|
ServerConfig,
|
||||||
|
ToolCallParams,
|
||||||
|
ToolCallResult,
|
||||||
|
ToolDefinition,
|
||||||
|
ToolParameter,
|
||||||
|
ToolSchema,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
"""Tests for MCP server."""
|
"""Tests for MCP server."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
|
|
||||||
|
from mcp_server_cli.models import MCPMethod, MCPRequest
|
||||||
from mcp_server_cli.server import MCPServer
|
from mcp_server_cli.server import MCPServer
|
||||||
from mcp_server_cli.config import AppConfig, ServerConfig
|
|
||||||
from mcp_server_cli.tools import FileTools, GitTools, ShellTools
|
from mcp_server_cli.tools import FileTools, GitTools, ShellTools
|
||||||
from mcp_server_cli.models import MCPRequest, MCPMethod
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
"""Tests for tool execution engine and built-in tools."""
|
"""Tests for tool execution engine and built-in tools."""
|
||||||
|
|
||||||
import pytest
|
|
||||||
import asyncio
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
from mcp_server_cli.tools.base import ToolBase, ToolResult, ToolRegistry
|
import pytest
|
||||||
|
|
||||||
|
from mcp_server_cli.models import ToolParameter, ToolSchema
|
||||||
|
from mcp_server_cli.tools.base import ToolBase, ToolRegistry, ToolResult
|
||||||
from mcp_server_cli.tools.file_tools import (
|
from mcp_server_cli.tools.file_tools import (
|
||||||
FileTools,
|
GlobFilesTool,
|
||||||
|
ListDirectoryTool,
|
||||||
ReadFileTool,
|
ReadFileTool,
|
||||||
WriteFileTool,
|
WriteFileTool,
|
||||||
ListDirectoryTool,
|
|
||||||
GlobFilesTool,
|
|
||||||
)
|
)
|
||||||
from mcp_server_cli.tools.shell_tools import ShellTools, ExecuteCommandTool
|
|
||||||
from mcp_server_cli.tools.git_tools import GitTools
|
from mcp_server_cli.tools.git_tools import GitTools
|
||||||
from mcp_server_cli.models import ToolSchema, ToolParameter
|
from mcp_server_cli.tools.shell_tools import ExecuteCommandTool
|
||||||
|
|
||||||
|
|
||||||
class TestToolBase:
|
class TestToolBase:
|
||||||
|
|||||||
Reference in New Issue
Block a user