mirror of
https://github.com/umbra2728/ctfd-mcp.git
synced 2026-02-08 06:18: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
|
## 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.
|
||||||
|
|||||||
@@ -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
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
|
import httpx
|
||||||
|
|
||||||
from config import Config
|
from .config import Config
|
||||||
|
|
||||||
|
|
||||||
class CTFdClientError(Exception):
|
class CTFdClientError(Exception):
|
||||||
@@ -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:
|
||||||
@@ -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,
|
||||||
@@ -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]):
|
||||||
|
|||||||
@@ -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
4
uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user