mirror of
https://github.com/umbra2728/ctfd-mcp.git
synced 2026-02-07 22:08:12 +03:00
package: move code into src layout and add PyPI publish workflow
This commit is contained in:
62
.github/workflows/publish.yml
vendored
Normal file
62
.github/workflows/publish.yml
vendored
Normal 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
|
||||
40
README.md
40
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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
12
src/ctfd_mcp/__init__.py
Normal file
12
src/ctfd_mcp/__init__.py
Normal 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
4
src/ctfd_mcp/__main__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -7,7 +7,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from config import Config
|
||||
from .config import Config
|
||||
|
||||
|
||||
class CTFdClientError(Exception):
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from server import run
|
||||
from .server import run
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -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,
|
||||
@@ -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]):
|
||||
|
||||
@@ -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")
|
||||
|
||||
4
uv.lock
generated
4
uv.lock
generated
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user