Skip to content

API reference

The existing server exposes two tools. Their names and response shapes are part of the utility client's contract.

Generate a TOTP code

generate_totp(secret, digits=6, period=30, algorithm="sha1")

Argument Accepted values
secret Base32 secret; spaces, dashes and mixed case are normalized
digits 6, 7 or 8
period Positive integer seconds
algorithm sha1, sha256 or sha512

A successful response contains a string code and the seconds left in the period:

{"status": "ok", "data": {"code": "123456", "expires_in": 18}}

Invalid inputs return an INVALID: error string. The example code above is illustrative; the actual code depends on the secret and current time.

Decode a QR code

decode_qr(image_path) reads a local image accessible to the server process. Pillow opens the image and zxing-cpp decodes exactly one QR code. The tool accepts a file path; it has no base64 argument.

{"status": "ok", "data": {"payload": "https://example.com/login"}}

Empty or missing paths return INVALID:. Invalid images, images without a QR, and images containing multiple QRs return DECODE_FAILED:. Pillow and zxing-cpp are required runtime dependencies and must be installed before starting the server.

Response helpers

Success uses status: "ok"; failure uses status: "error" and one error string:

{"status": "error", "error": "INVALID: image_path is required"}

Response builders for utility MCP tools.

error_response(error)

Build an error response containing the supplied message.

Source code in src/justpen_utility_mcp/responses.py
14
15
16
def error_response(error: str) -> dict[str, Any]:
    """Build an error response containing the supplied message."""
    return {"status": "error", "error": error}

ok_response(data=None)

Build an ok response, including data only when provided.

Source code in src/justpen_utility_mcp/responses.py
 6
 7
 8
 9
10
11
def ok_response(data: dict[str, Any] | list[Any] | None = None) -> dict[str, Any]:
    """Build an ok response, including data only when provided."""
    r: dict[str, Any] = {"status": "ok"}
    if data is not None:
        r["data"] = data
    return r

Tool modules

The registration functions below attach the documented tools to FastMCP. The Python source is shown without changing the existing runtime implementation.

QR code decoding tool.

register(mcp)

Register the local QR decoding tool on the MCP server.

Source code in src/justpen_utility_mcp/tools/qr.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def register(mcp: FastMCP) -> None:
    """Register the local QR decoding tool on the MCP server."""

    @mcp.tool
    async def decode_qr(image_path: str) -> dict[str, Any]:
        """Decode a single QR code from a local image file.

        Returns on success:
            data: {payload: str} — the raw payload string

        Errors:
            INVALID — empty path or file not found
            DECODE_FAILED — corrupt image, no QR found, or multiple QRs
            DEPENDENCY_MISSING — zxingcpp or Pillow not installed
        """
        if not image_path or not image_path.strip():
            return error_response("INVALID: image_path is required")

        try:
            payload = _decode_qr_from_image(Path(image_path))
        except ImportError as e:
            install_name = {"zxingcpp": "zxing-cpp", "PIL": "Pillow"}.get(e.name or "", e.name or "required dependency")
            return error_response(f"DEPENDENCY_MISSING: {install_name} not installed")
        except FileNotFoundError:
            return error_response(f"INVALID: image not found: {image_path}")
        except ValueError as e:
            kind = str(e)
            messages = {
                "invalid_image": "failed to read image: not a valid image file",
                "no_qr_found": "no QR code found in image",
                "multiple_qrs_found": "multiple QR codes found (expected 1)",
            }
            msg = messages.get(kind, f"unexpected decode error: {kind}")
            return error_response(f"DECODE_FAILED: {msg}")

        return ok_response(data={"payload": payload})

TOTP code generation tool.

register(mcp)

Register the Base32 TOTP generation tool on the MCP server.

Source code in src/justpen_utility_mcp/tools/totp.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def register(mcp: FastMCP) -> None:
    """Register the Base32 TOTP generation tool on the MCP server."""

    @mcp.tool
    async def generate_totp(
        secret: str,
        digits: int = 6,
        period: int = 30,
        algorithm: str = "sha1",
    ) -> dict[str, Any]:
        """Generate a TOTP code from a Base32 secret.

        Args:
            secret: Base32 encoded TOTP secret (spaces/dashes/mixed case OK)
            digits: Code length — 6, 7, or 8 (default: 6)
            period: Time period in seconds (default: 30)
            algorithm: Hash algorithm — sha1, sha256, sha512 (default: sha1)

        Returns on success:
            data: {code: str, expires_in: int}

        Errors:
            INVALID — bad secret, unsupported digits/period/algorithm
        """
        if not secret or not secret.strip():
            return error_response("INVALID: secret is required")

        if digits not in _VALID_DIGITS:
            return error_response(f"INVALID: digits must be 6, 7, or 8 (got {digits})")

        if period <= 0:
            return error_response(f"INVALID: period must be positive (got {period})")

        if algorithm not in _HASH_FUNCS:
            return error_response(f"INVALID: algorithm must be sha1, sha256, or sha512 (got {algorithm})")

        try:
            secret_bytes = _normalize_secret(secret)
        except ValueError:
            return error_response("INVALID: secret is not valid Base32")

        code, remaining = _generate_code(secret_bytes, digits, period, algorithm)
        return ok_response(data={"code": code, "expires_in": remaining})