Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Python CI

on:
push:
branches: [ master, release ]
pull_request:
branches: [ master, release ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install poetry
poetry install --all-extras

- name: Run tests
run: |
poetry run pytest tests/ -v

publish:
needs: test
runs-on: ubuntu-latest
# Only run on release branch when push (merge)
if: github.event_name == 'push' && github.ref == 'refs/heads/release'

steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install poetry
python -m pip install build twine

- name: Build and publish
env:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
# Build the package
python -m build

# Publish to PyPI
python -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/* --username __token__ --password $PYPI_API_TOKEN
64 changes: 64 additions & 0 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Version Bump

on:
push:
branches: [ release ]

jobs:
bump-version:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install tomlkit

- name: Configure Git
run: |
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"

- name: Bump version
run: |
# Python script to bump version
python - << 'EOF'
import tomlkit
import re

# Read current version from pyproject.toml
with open('pyproject.toml', 'r') as f:
pyproject = tomlkit.parse(f.read())

current_version = pyproject['project']['version']
print(f"Current version: {current_version}")

# Parse version components
match = re.match(r"(\d+)\.(\d+)\.(\d+)(.*)$", current_version)
major, minor, patch, suffix = match.groups()

# Bump patch version
new_version = f"{major}.{minor}.{int(patch) + 1}{suffix}"
print(f"New version: {new_version}")

# Update pyproject.toml
pyproject['project']['version'] = new_version
with open('pyproject.toml', 'w') as f:
f.write(tomlkit.dumps(pyproject))
EOF

# Commit and push changes
git add pyproject.toml
git commit -m "Bump version for new release"
git push
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,34 @@ MCPHub simplifies the integration of Model Context Protocol (MCP) servers into A

This architecture provides a seamless way to integrate MCP capabilities into any AI application while maintaining clean separation of concerns and framework flexibility.

## Development

### Testing

Run the unit tests with pytest:

```bash
pytest tests/ -v
```

### CI/CD Pipeline

This project uses GitHub Actions for continuous integration and deployment:

1. **Automated Testing**: Tests are run on Python 3.10, 3.11, and 3.12 for every push to main and release branches and for pull requests.

2. **Automatic Version Bumping**: When code is pushed to the `release` branch, the patch version is automatically incremented.

3. **PyPI Publishing**: When code is merged to the `release` branch and tests pass, the package is automatically built and published to PyPI.

#### Setting Up PyPI Deployment

To enable automatic PyPI deployment, you need to add a PyPI API token as a GitHub Secret:

1. Generate a PyPI API token at https://pypi.org/manage/account/token/
2. Go to your GitHub repository settings → Secrets and variables → Actions
3. Add a new repository secret named `PYPI_API_TOKEN` with the token value from PyPI

## Contributing

We welcome contributions! Please check out our [Contributing Guide](CONTRIBUTING.md) for guidelines on how to proceed.
Expand Down
20 changes: 19 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,24 @@ dependencies = [
"python-dotenv>=1.0.0,<2.0.0",
"build (>=1.2.2.post1,<2.0.0)",
"twine (>=6.1.0,<7.0.0)",
]
requires-python = "<4.0,>=3.10"

[project.optional-dependencies]
openai = [
"openai-agents (>=0.0.9,<0.0.10)",
]
langchain = [
"langchain-mcp-adapters (>=0.0.7,<0.0.8)",
]
autogen = [
"autogen-ext[mcp] (>=0.5.1,<0.6.0)",
]
all = [
"openai-agents (>=0.0.9,<0.0.10)",
"langchain-mcp-adapters (>=0.0.7,<0.0.8)",
"autogen-ext[mcp] (>=0.5.1,<0.6.0)",
]
requires-python = "<4.0,>=3.10"

[project.urls]
Documentation = "https://raw.githubusercontent.com/Cognitive-Stack/mcphub/refs/heads/master/README.md"
Expand All @@ -38,3 +51,8 @@ Source = "https://github.com/Cognitive-Stack/mcphub"

[project.scripts]
mcphub = "mcphub.cli.commands:main"

[tool.poetry.group.dev.dependencies]
pytest = "^8.3.5"
pytest-asyncio = "^0.26.0"

120 changes: 120 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import json
import os
import pytest
from pathlib import Path
from unittest import mock


@pytest.fixture
def temp_config_file(tmp_path):
"""Create a temporary .mcphub.json file for testing."""
config_content = {
"mcpServers": {
"test-server": {
"package_name": "test-mcp-server",
"command": "python",
"args": ["-m", "test_server"],
"env": {"TEST_ENV": "test_value"},
"description": "Test MCP Server",
"tags": ["test", "demo"]
}
}
}

config_file = tmp_path / ".mcphub.json"
with open(config_file, "w") as f:
json.dump(config_content, f)

return config_file


@pytest.fixture
def mock_mcp_preconfigured_servers(tmp_path):
"""Mock the mcphub_preconfigured_servers.json file."""
content = {
"predefined-server": {
"command": "python",
"args": ["-m", "predefined_server"],
"description": "Predefined MCP Server",
"tags": ["predefined", "demo"]
}
}

preconfigured_path = tmp_path / "mcphub_preconfigured_servers.json"
with open(preconfigured_path, "w") as f:
json.dump(content, f)

return preconfigured_path


@pytest.fixture
def mock_cache_dir(tmp_path):
"""Create and return a mock cache directory."""
cache_dir = tmp_path / ".mcphub_cache"
cache_dir.mkdir(exist_ok=True)
return cache_dir


@pytest.fixture
def mock_path_exists():
"""Mock Path.exists to always return True for .mcphub.json files."""
original_exists = Path.exists

def patched_exists(self):
if self.name == ".mcphub.json":
return True
return original_exists(self)

with mock.patch("pathlib.Path.exists", patched_exists):
yield


@pytest.fixture
def mock_current_dir(monkeypatch, tmp_path, temp_config_file, mock_cache_dir):
"""Mock the current directory to use the temporary config file."""
# Create a function that returns the parent directory of temp_config_file
def mock_cwd():
return Path(temp_config_file).parent

# Patch Path.cwd() to return our mock directory
monkeypatch.setattr(Path, "cwd", mock_cwd)

# Return the mock current directory
return Path(temp_config_file).parent


@pytest.fixture
def mock_find_config_path(monkeypatch, temp_config_file):
"""Mock the _find_config_path method to return our test config file."""
def mock_find_config(self): # Add self parameter here to fix TypeError
return str(temp_config_file)

# Apply the monkeypatch
monkeypatch.setattr("mcphub.mcphub.MCPHub._find_config_path", mock_find_config)

return str(temp_config_file)


@pytest.fixture
def mock_mcphub_init(monkeypatch, temp_config_file):
"""Mock MCPHub initialization to avoid filesystem operations."""
# Create patch for _find_config_path
def mock_find_config(self):
return str(temp_config_file)

# Create patch for _get_cache_dir in MCPServers
def mock_get_cache_dir(self):
cache_dir = Path(temp_config_file).parent / ".mcphub_cache"
cache_dir.mkdir(exist_ok=True)
return cache_dir

# Create patch for _setup_all_servers in MCPServers
def mock_setup_all_servers(self):
pass # No-op to avoid setup operations

# Apply the monkeypatches
monkeypatch.setattr("mcphub.mcphub.MCPHub._find_config_path", mock_find_config)
monkeypatch.setattr("mcphub.mcp_servers.servers.MCPServers._get_cache_dir", mock_get_cache_dir)
monkeypatch.setattr("mcphub.mcp_servers.servers.MCPServers._setup_all_servers", mock_setup_all_servers)

return str(temp_config_file)
Loading
Loading