package: move code into src layout and add PyPI publish workflow

This commit is contained in:
2026-01-04 15:36:02 +03:00
parent f7ceacd07e
commit ebfb6e0492
12 changed files with 133 additions and 37 deletions

62
.github/workflows/publish.yml vendored Normal file
View File

@@ -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

View File

@@ -31,15 +31,16 @@ CTFD_PASSWORD=your_password
## Install ## Install
```bash - From PyPI (recommended): `uvx ctfd-mcp --help`
git clone https://github.com/umbra2728/ctfd-mcp.git - From source checkout (no install): `uvx --from . ctfd-mcp --help`
uv sync
```
## Run MCP server (stdio) ## Run MCP server (stdio)
```bash ```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 ## Cursor and Claude MCP config example
@@ -48,15 +49,8 @@ uv run python -m main
{ {
"mcpServers": { "mcpServers": {
"ctfd-mcp": { "ctfd-mcp": {
"command": "uv", "command": "uvx",
"args": [ "args": ["ctfd-mcp"],
"--directory",
"/absolute/path/to/ctfd-mcp",
"run",
"python",
"-m",
"main"
],
"env": { "env": {
"CTFD_URL": "https://ctfd.example.com", "CTFD_URL": "https://ctfd.example.com",
"CTFD_TOKEN": "your_user_token" "CTFD_TOKEN": "your_user_token"
@@ -70,15 +64,8 @@ uv run python -m main
```toml ```toml
[mcp_servers.ctfd-mcp] [mcp_servers.ctfd-mcp]
command = "uv" command = "uvx"
args = [ args = ["ctfd-mcp"]
"--directory",
"/absolute/path/to/ctfd-mcp",
"run",
"python",
"-m",
"main"
]
[mcp_servers.ctfd-mcp.env] [mcp_servers.ctfd-mcp.env]
CTFD_URL = "https://ctfd.example.com" 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. - 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. - 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 ## 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. - 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.

View File

@@ -1,3 +1,7 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project] [project]
name = "ctfd-mcp" name = "ctfd-mcp"
version = "0.1.0" version = "0.1.0"
@@ -6,6 +10,7 @@ readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
license = { file = "LICENSE" } license = { file = "LICENSE" }
dependencies = [ dependencies = [
"anyio>=4.4",
"httpx>=0.28.1", "httpx>=0.28.1",
"mcp>=1.23.3", "mcp>=1.23.3",
"python-dotenv>=1.2.1", "python-dotenv>=1.2.1",
@@ -16,6 +21,9 @@ Homepage = "https://github.com/umbra2728/ctfd-mcp"
Repository = "https://github.com/umbra2728/ctfd-mcp" Repository = "https://github.com/umbra2728/ctfd-mcp"
Issues = "https://github.com/umbra2728/ctfd-mcp/issues" Issues = "https://github.com/umbra2728/ctfd-mcp/issues"
[project.scripts]
ctfd-mcp = "ctfd_mcp.main:main"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"pre-commit>=3.6.0", "pre-commit>=3.6.0",

12
src/ctfd_mcp/__init__.py Normal file
View File

@@ -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"]

4
src/ctfd_mcp/__main__.py Normal file
View File

@@ -0,0 +1,4 @@
from .main import main
if __name__ == "__main__":
main()

View File

@@ -7,7 +7,7 @@ from typing import Any
import httpx import httpx
from config import Config from .config import Config
class CTFdClientError(Exception): class CTFdClientError(Exception):

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from server import run from .server import run
def main() -> None: def main() -> None:

View File

@@ -6,8 +6,8 @@ from typing import Any
import anyio import anyio
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from config import ConfigError, load_config from .config import ConfigError, load_config
from ctfd_client import ( from .ctfd_client import (
AuthError, AuthError,
CTFdClient, CTFdClient,
CTFdClientError, CTFdClientError,

View File

@@ -1,8 +1,20 @@
"""Config loading tests (ruff: ignore E402 for sys.path adjustment)."""
# ruff: noqa: E402
import os import os
import sys
import unittest import unittest
from pathlib import Path
from unittest.mock import patch 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]): def _load_with_env(env: dict[str, str]):

View File

@@ -1,3 +1,7 @@
"""CTFd client tests (ruff: ignore E402 for sys.path adjustment)."""
# ruff: noqa: E402
import asyncio import asyncio
import json import json
import os import os
@@ -9,19 +13,17 @@ from pathlib import Path
import httpx import httpx
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path: SRC = ROOT / "src"
sys.path.insert(0, str(ROOT)) 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). # Avoid optional dependency issues in the test runner (python-dotenv).
if "dotenv" not in sys.modules: if "dotenv" not in sys.modules:
sys.modules["dotenv"] = types.SimpleNamespace(load_dotenv=lambda *_, **__: None) sys.modules["dotenv"] = types.SimpleNamespace(load_dotenv=lambda *_, **__: None)
from config import Config # type: ignore # noqa: E402 # added to path above from ctfd_mcp.config import Config # type: ignore
from ctfd_client import ( # type: ignore # noqa: E402 from ctfd_mcp.ctfd_client import AuthError, CTFdClient, CTFdClientError # type: ignore
AuthError,
CTFdClient,
CTFdClientError,
)
CTFD_URL = os.getenv("CTFD_URL") CTFD_URL = os.getenv("CTFD_URL")
CTFD_USERNAME = os.getenv("CTFD_USERNAME") CTFD_USERNAME = os.getenv("CTFD_USERNAME")

4
uv.lock generated
View File

@@ -175,8 +175,9 @@ wheels = [
[[package]] [[package]]
name = "ctfd-mcp" name = "ctfd-mcp"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "anyio" },
{ name = "httpx" }, { name = "httpx" },
{ name = "mcp" }, { name = "mcp" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
@@ -190,6 +191,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "anyio", specifier = ">=4.4" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "mcp", specifier = ">=1.23.3" }, { name = "mcp", specifier = ">=1.23.3" },
{ name = "python-dotenv", specifier = ">=1.2.1" }, { name = "python-dotenv", specifier = ">=1.2.1" },