From ebfb6e0492d40efc6fd516496c2cb86ddadca0f0 Mon Sep 17 00:00:00 2001 From: umbra2728 Date: Sun, 4 Jan 2026 15:36:02 +0300 Subject: [PATCH] package: move code into src layout and add PyPI publish workflow --- .github/workflows/publish.yml | 62 +++++++++++++++++++ README.md | 40 +++++------- pyproject.toml | 8 +++ src/ctfd_mcp/__init__.py | 12 ++++ src/ctfd_mcp/__main__.py | 4 ++ config.py => src/ctfd_mcp/config.py | 0 ctfd_client.py => src/ctfd_mcp/ctfd_client.py | 2 +- main.py => src/ctfd_mcp/main.py | 2 +- server.py => src/ctfd_mcp/server.py | 4 +- tests/test_config.py | 14 ++++- tests/test_ctfd_client.py | 18 +++--- uv.lock | 4 +- 12 files changed, 133 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 src/ctfd_mcp/__init__.py create mode 100644 src/ctfd_mcp/__main__.py rename config.py => src/ctfd_mcp/config.py (100%) rename ctfd_client.py => src/ctfd_mcp/ctfd_client.py (99%) rename main.py => src/ctfd_mcp/main.py (81%) rename server.py => src/ctfd_mcp/server.py (98%) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..ee2d9e4 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,62 @@ +name: Publish + +on: + workflow_dispatch: + push: + tags: + - "v*" + +permissions: + contents: read + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Set up uv + uses: astral-sh/setup-uv@v3 + + - name: Install project (frozen) + run: uv sync --frozen --no-editable + + - name: Run tests + run: uv run python -m unittest discover -s tests + + - name: Build distributions + run: uv build --no-sources + + - name: Upload dist artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/** + + publish: + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/ctfd-mcp + permissions: + id-token: write + contents: read + steps: + - name: Download dist artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages_dir: dist diff --git a/README.md b/README.md index 1a8f269..a2cfd11 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,16 @@ CTFD_PASSWORD=your_password ## Install -```bash -git clone https://github.com/umbra2728/ctfd-mcp.git -uv sync -``` +- From PyPI (recommended): `uvx ctfd-mcp --help` +- From source checkout (no install): `uvx --from . ctfd-mcp --help` ## Run MCP server (stdio) ```bash -uv run python -m main +# installed from PyPI +uvx ctfd-mcp +# from local checkout +uvx --from . ctfd-mcp ``` ## Cursor and Claude MCP config example @@ -48,15 +49,8 @@ uv run python -m main { "mcpServers": { "ctfd-mcp": { - "command": "uv", - "args": [ - "--directory", - "/absolute/path/to/ctfd-mcp", - "run", - "python", - "-m", - "main" - ], + "command": "uvx", + "args": ["ctfd-mcp"], "env": { "CTFD_URL": "https://ctfd.example.com", "CTFD_TOKEN": "your_user_token" @@ -70,15 +64,8 @@ uv run python -m main ```toml [mcp_servers.ctfd-mcp] -command = "uv" -args = [ - "--directory", - "/absolute/path/to/ctfd-mcp", - "run", - "python", - "-m", - "main" -] +command = "uvx" +args = ["ctfd-mcp"] [mcp_servers.ctfd-mcp.env] CTFD_URL = "https://ctfd.example.com" @@ -116,6 +103,13 @@ Attachments are returned as absolute URLs in `files`; the client/host can fetch - The client now supports logging in with `CTFD_USERNAME` and `CTFD_PASSWORD`; these fields take precedence over stale tokens/sessions. - Auth priority: username/password first, then token, then session cookie. Lower-priority credentials are ignored when a higher-priority option is present. +## Support / feedback + +If something breaks or you have questions, reach out: +- Telegram: @ismailgaleev +- Jabber: ismailgaleev@chat.merlok.ru +- Email: umbra2728@gmail.com + ## Testing - Run `uv run python -m tests.test_ctfd_client` (requires a real `CTFD_URL` plus token or username/password) to exercise challenge fetching/submission flows. diff --git a/pyproject.toml b/pyproject.toml index 2b534cc..44b3fba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] name = "ctfd-mcp" version = "0.1.0" @@ -6,6 +10,7 @@ readme = "README.md" requires-python = ">=3.13" license = { file = "LICENSE" } dependencies = [ + "anyio>=4.4", "httpx>=0.28.1", "mcp>=1.23.3", "python-dotenv>=1.2.1", @@ -16,6 +21,9 @@ Homepage = "https://github.com/umbra2728/ctfd-mcp" Repository = "https://github.com/umbra2728/ctfd-mcp" Issues = "https://github.com/umbra2728/ctfd-mcp/issues" +[project.scripts] +ctfd-mcp = "ctfd_mcp.main:main" + [dependency-groups] dev = [ "pre-commit>=3.6.0", diff --git a/src/ctfd_mcp/__init__.py b/src/ctfd_mcp/__init__.py new file mode 100644 index 0000000..d60b305 --- /dev/null +++ b/src/ctfd_mcp/__init__.py @@ -0,0 +1,12 @@ +""" +ctfd-mcp: MCP server for CTFd user-scope operations. + +Public entrypoints: +- `ctfd_mcp.main.main` function (console script `ctfd-mcp`) +- `ctfd_mcp.server.mcp` FastMCP instance +""" + +from .main import main +from .server import mcp + +__all__ = ["main", "mcp"] diff --git a/src/ctfd_mcp/__main__.py b/src/ctfd_mcp/__main__.py new file mode 100644 index 0000000..40e2b01 --- /dev/null +++ b/src/ctfd_mcp/__main__.py @@ -0,0 +1,4 @@ +from .main import main + +if __name__ == "__main__": + main() diff --git a/config.py b/src/ctfd_mcp/config.py similarity index 100% rename from config.py rename to src/ctfd_mcp/config.py diff --git a/ctfd_client.py b/src/ctfd_mcp/ctfd_client.py similarity index 99% rename from ctfd_client.py rename to src/ctfd_mcp/ctfd_client.py index f63abfe..84a0a4e 100644 --- a/ctfd_client.py +++ b/src/ctfd_mcp/ctfd_client.py @@ -7,7 +7,7 @@ from typing import Any import httpx -from config import Config +from .config import Config class CTFdClientError(Exception): diff --git a/main.py b/src/ctfd_mcp/main.py similarity index 81% rename from main.py rename to src/ctfd_mcp/main.py index 3aa27c3..5ce5c97 100644 --- a/main.py +++ b/src/ctfd_mcp/main.py @@ -1,6 +1,6 @@ from __future__ import annotations -from server import run +from .server import run def main() -> None: diff --git a/server.py b/src/ctfd_mcp/server.py similarity index 98% rename from server.py rename to src/ctfd_mcp/server.py index 32858ce..8950105 100644 --- a/server.py +++ b/src/ctfd_mcp/server.py @@ -6,8 +6,8 @@ from typing import Any import anyio from mcp.server.fastmcp import FastMCP -from config import ConfigError, load_config -from ctfd_client import ( +from .config import ConfigError, load_config +from .ctfd_client import ( AuthError, CTFdClient, CTFdClientError, diff --git a/tests/test_config.py b/tests/test_config.py index 4710e71..a3965d9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,8 +1,20 @@ +"""Config loading tests (ruff: ignore E402 for sys.path adjustment).""" + +# ruff: noqa: E402 + import os +import sys import unittest +from pathlib import Path from unittest.mock import patch -from config import load_config +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" +for path in (SRC, ROOT): + if str(path) not in sys.path: + sys.path.insert(0, str(path)) + +from ctfd_mcp.config import load_config def _load_with_env(env: dict[str, str]): diff --git a/tests/test_ctfd_client.py b/tests/test_ctfd_client.py index 721bbb1..54854f8 100644 --- a/tests/test_ctfd_client.py +++ b/tests/test_ctfd_client.py @@ -1,3 +1,7 @@ +"""CTFd client tests (ruff: ignore E402 for sys.path adjustment).""" + +# ruff: noqa: E402 + import asyncio import json import os @@ -9,19 +13,17 @@ from pathlib import Path import httpx ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) +SRC = ROOT / "src" +for path in (SRC, ROOT): + if str(path) not in sys.path: + sys.path.insert(0, str(path)) # Avoid optional dependency issues in the test runner (python-dotenv). if "dotenv" not in sys.modules: sys.modules["dotenv"] = types.SimpleNamespace(load_dotenv=lambda *_, **__: None) -from config import Config # type: ignore # noqa: E402 # added to path above -from ctfd_client import ( # type: ignore # noqa: E402 - AuthError, - CTFdClient, - CTFdClientError, -) +from ctfd_mcp.config import Config # type: ignore +from ctfd_mcp.ctfd_client import AuthError, CTFdClient, CTFdClientError # type: ignore CTFD_URL = os.getenv("CTFD_URL") CTFD_USERNAME = os.getenv("CTFD_USERNAME") diff --git a/uv.lock b/uv.lock index 7cfebde..7e5e07d 100644 --- a/uv.lock +++ b/uv.lock @@ -175,8 +175,9 @@ wheels = [ [[package]] name = "ctfd-mcp" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ + { name = "anyio" }, { name = "httpx" }, { name = "mcp" }, { name = "python-dotenv" }, @@ -190,6 +191,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "anyio", specifier = ">=4.4" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "mcp", specifier = ">=1.23.3" }, { name = "python-dotenv", specifier = ">=1.2.1" },