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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
MCP_SERVER_URL=http://localhost:8081
MCP_SERVER_URL=http://localhost:8080

GOOGLE_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-your-client-secret
125 changes: 80 additions & 45 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,109 @@ name: CI

on:
push:
branches:
- "**"
paths:
- src/**
- tests/**
- tests_integration/**
- pyproject.toml
- .github/workflows/ci.yaml
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test (Python ${{ matrix.python-version }})
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]

steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/checkout@v4

- name: Set up uv
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
version: "latest"

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install dependencies
run: uv sync
run: uv sync --dev

- name: Lint
run: uv run ruff check .
- name: Run ruff check
run: uv run ruff check src/ tests/

- name: Run tests
run: uv run pytest tests/ -v --tb=short
- name: Run ruff format check
run: uv run ruff format --check src/ tests/

integration-test:
name: Integration Tests
type-check:
name: Type Check
runs-on: ubuntu-latest
timeout-minutes: 15
if: github.ref == 'refs/heads/main'

steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/checkout@v4

- name: Set up uv
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
version: "latest"

- name: Set up Python
run: uv python install 3.13
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install dependencies
run: uv sync
run: uv sync --dev

- name: Run integration tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: uv run pytest tests_integration/ -v -s --tb=short
- name: Run mypy
run: uv run mypy src/

test:
name: Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.13"]
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --dev

- name: Run tests with coverage
run: |
uv run pytest --cov=src --cov-report=xml --cov-report=term-missing

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
if: matrix.python-version == '3.13'
with:
files: ./coverage.xml
fail_ci_if_error: false
continue-on-error: true

all-checks:
name: All Checks Passed
needs: [lint, type-check, test]
runs-on: ubuntu-latest
if: always()
steps:
- name: Check all jobs passed
run: |
if [[ "${{ needs.lint.result }}" != "success" ]] || \
[[ "${{ needs.type-check.result }}" != "success" ]] || \
[[ "${{ needs.test.result }}" != "success" ]]; then
echo "One or more jobs failed"
exit 1
fi
echo "All checks passed!"
23 changes: 23 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
default_language_version:
python: python3.13

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
args: [--unsafe]
- id: check-added-large-files
args: [--maxkb=1000]
- id: check-json
- id: check-toml
- id: check-merge-conflict
- id: detect-private-key
- id: check-case-conflict
- id: check-symlinks
- id: mixed-line-ending
args: [--fix=lf]
- id: no-commit-to-branch
args: [--branch, main]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
Expand All @@ -22,10 +33,12 @@ repos:
rev: v1.13.0
hooks:
- id: mypy
files: ^src/
additional_dependencies: [
"fastapi>=0.104.0",
"pydantic>=2.0.0",
"pandas>=2.3.3",
"types-requests",
]
args: [
--ignore-missing-imports,
Expand All @@ -37,3 +50,13 @@ repos:
--disable-error-code=valid-type,
--disable-error-code=return-value,
]

- repo: local
hooks:
- id: pytest-fast
name: pytest (fast)
entry: uv run pytest -x -q --tb=short
language: system
pass_filenames: false
always_run: true
stages: [pre-push]
24 changes: 23 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,37 @@ dependencies = [
dev = [
"pytest>=9.0.1",
"pytest-asyncio>=0.24.0",
"pytest-cov>=6.0.0",
"ruff>=0.8.0",
"pre-commit>=3.4.0",
"grafi>=0.0.33",
"grafi>=0.0.34",
"mypy>=1.13.0",
]

[tool.pytest.ini_options]
pythonpath = ["."]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = "-v --tb=short"
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::RuntimeWarning:pydub",
]

[tool.coverage.run]
source = ["src"]
branch = true
omit = ["src/tools/google/*"]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
show_missing = true

[tool.ruff]
line-length = 88
Expand Down
39 changes: 39 additions & 0 deletions src/humcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""HuMCP - Human-friendly MCP server with FastAPI adapter."""

from src.humcp.registry import TOOL_REGISTRY, ToolRegistration
from src.humcp.schemas import (
CategorySummary,
GetCategoryResponse,
GetToolResponse,
InputSchema,
ListToolsResponse,
SkillFull,
SkillMetadata,
ToolSummary,
)
from src.humcp.skills import (
Skill,
discover_skills,
get_skill_content,
get_skills_by_category,
)

__all__ = [
# Registry
"TOOL_REGISTRY",
"ToolRegistration",
# Skills
"Skill",
"discover_skills",
"get_skill_content",
"get_skills_by_category",
# Schemas
"CategorySummary",
"GetCategoryResponse",
"GetToolResponse",
"InputSchema",
"ListToolsResponse",
"SkillFull",
"SkillMetadata",
"ToolSummary",
]
23 changes: 20 additions & 3 deletions src/humcp/registry.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
"""Tool registry - stores all registered tools."""
"""Tool registry - stores all registered tools.

This module provides the global registry for MCP tools. Tools are registered
at module import time using the @tool decorator from the decorator module.

Thread Safety:
TOOL_REGISTRY and _TOOL_NAMES are module-level globals that are populated
at import time. They are safe for concurrent reads after server startup.
Dynamic tool registration during runtime is not thread-safe and should
be avoided in production.
"""

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

__all__ = ["ToolRegistration", "TOOL_REGISTRY"]
__all__ = ["ToolRegistration", "TOOL_REGISTRY", "_TOOL_NAMES"]


@dataclass(frozen=True)
class ToolRegistration:
"""A registered tool."""
"""A registered tool.

Attributes:
name: Unique identifier for the tool.
category: Category grouping (e.g., 'google', 'local', 'data').
func: The async function that implements the tool.
"""

name: str
category: str
func: Callable[..., Any]


# Global registry populated at import time by @tool decorators
TOOL_REGISTRY: list[ToolRegistration] = []
_TOOL_NAMES: set[str] = set()
Loading