CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

service-factory/gds-assessment-plugin

Granular 4-phase, 4-role GDS Service Standard assurance skills, panel orchestrator, and synthesis engine.

Quality

87%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

reports_mcp_server.pyskills/gds-assessment-how-to/

"""MCP server for a project's GOV.UK service assessment library.

Stdio transport. The corpus is markdown the project downloaded with
scrape_gds_reports.py, usually docs/past-assessments. This plugin checkout
can also read context/. GOV.UK is not contacted at assessment time.
"""

import json
import sys
from pathlib import Path

from library_paths import find_existing

REPORTS, CATALOGUE = find_existing(Path(__file__))
CURATED = {
    "discovery-lead-stop.md",
    "alpha-tech-spikes.md",
    "beta-ur-fail-assistive-tech.md",
    "beta-tech-pass.md",
}
PHASES = ("discovery", "alpha", "beta", "live")
PASS_RATINGS = {"green", "met", "met-with-advice"}
WEAK_RATINGS = {"amber", "red", "not-met"}
MAX_LIMIT = 8
FALLBACK_PROTOCOL = "2024-11-05"

TOOLS = [
    {
        "name": "search_past_assessments",
        "description": (
            "Shortlist published UK government service assessment reports for one "
            "phase. Returns catalogue rows only, not the report body. Prefer "
            "standard 14. At most 8 rows."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "phase": {
                    "type": "string",
                    "enum": list(PHASES),
                    "description": "Discovery, alpha, beta, or live.",
                },
                "point": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 14,
                    "description": "Current Service Standard point to match. Omit to list recent reports for the phase.",
                },
                "rating": {
                    "type": "string",
                    "enum": sorted(PASS_RATINGS | WEAK_RATINGS),
                    "description": "Point rating to require. Omit to mix reports that failed and passed the point.",
                },
                "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": MAX_LIMIT,
                    "description": "Maximum rows. Defaults to 8.",
                },
            },
            "required": ["phase"],
            "additionalProperties": False,
        },
    },
    {
        "name": "get_past_assessment",
        "description": (
            "Return the full markdown of one ingested report. Pass the file name "
            "from search_past_assessments, such as digital-waste-tracking.md."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "file": {
                    "type": "string",
                    "description": "Report file name, not a path.",
                }
            },
            "required": ["file"],
            "additionalProperties": False,
        },
    },
]


def normalise_rating(value):
    if value is None:
        return None
    key = str(value).strip().lower().replace("_", "-")
    aliases = {
        "not met": "not-met",
        "met with advice": "met-with-advice",
    }
    key = aliases.get(key, key)
    allowed = PASS_RATINGS | WEAK_RATINGS
    if key not in allowed:
        raise ValueError(
            f"Unknown rating {value!r}. Use one of: {', '.join(sorted(allowed))}."
        )
    return key


def load_phase(phase):
    phase = phase.strip().lower()
    if phase not in PHASES:
        raise ValueError(f"Unknown phase {phase!r}. Use one of: {', '.join(PHASES)}.")
    path = CATALOGUE / f"{phase}.json"
    if not path.is_file():
        raise FileNotFoundError(
            "No past-assessment catalogue. From the project root, run "
            "scrape_gds_reports.py in the gds-assessment-how-to skill. "
            "It writes docs/past-assessments. Then restart the gds-assessment-reports server."
        )
    payload = json.loads(path.read_text(encoding="utf-8"))
    return [
        report
        for report in payload.get("reports", [])
        if report.get("file") not in CURATED and report.get("standard") == "14"
    ]


def row(report, point):
    item = {
        "file": report.get("file"),
        "title": report.get("title"),
        "phase": report.get("phase"),
        "stage": report.get("stage"),
        "result": report.get("result"),
        "assessed": report.get("assessed"),
        "source": report.get("source"),
        "standard": report.get("standard"),
        "weak_points": report.get("weak_points") or [],
    }
    if point is not None:
        item["point"] = point
        item["point_rating"] = (report.get("points") or {}).get(str(point))
    return item


def search(phase, point=None, rating=None, limit=MAX_LIMIT):
    limit = MAX_LIMIT if limit is None else min(max(int(limit), 1), MAX_LIMIT)
    rating = normalise_rating(rating)
    if point is not None:
        point = int(point)
        if not 1 <= point <= 14:
            raise ValueError("point must be from 1 to 14.")
    reports = load_phase(phase)

    def point_rating(report):
        return (report.get("points") or {}).get(str(point))

    if point is None:
        chosen = reports[:limit]
    elif rating is not None:
        chosen = [report for report in reports if point_rating(report) == rating][:limit]
    else:
        weak = [report for report in reports if point_rating(report) in WEAK_RATINGS]
        passed = [report for report in reports if point_rating(report) in PASS_RATINGS]
        weak_take = min(len(weak), max(1, limit // 2)) if weak and passed else min(len(weak), limit)
        pass_take = min(len(passed), limit - weak_take)
        chosen = weak[:weak_take] + passed[:pass_take]

    return {
        "phase": phase,
        "point": point,
        "rating": rating,
        "count": len(chosen),
        "reports": [row(report, point) for report in chosen],
    }


def get_report(file_name):
    name = Path(str(file_name)).name
    if name != str(file_name).strip() or not name.endswith(".md"):
        raise ValueError("Pass a markdown file name, not a path.")
    path = REPORTS / name
    if not path.is_file():
        raise FileNotFoundError(f"No ingested report named {name}.")
    return path.read_text(encoding="utf-8")


def call_tool(name, arguments):
    arguments = arguments or {}
    if name == "search_past_assessments":
        payload = search(
            arguments["phase"],
            arguments.get("point"),
            arguments.get("rating"),
            arguments.get("limit", MAX_LIMIT),
        )
        return json.dumps(payload, indent=2, ensure_ascii=False)
    if name == "get_past_assessment":
        return get_report(arguments["file"])
    raise ValueError(f"Unknown tool {name}.")


# Cursor's client uses Content-Length framing. A line of raw JSON is accepted
# too, and the reply uses whichever form the client just used.
FRAMING = "headers"


def send(message):
    body = json.dumps(message, separators=(",", ":")).encode("utf-8")
    if FRAMING == "line":
        sys.stdout.buffer.write(body + b"\n")
    else:
        header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
        sys.stdout.buffer.write(header)
        sys.stdout.buffer.write(body)
    sys.stdout.buffer.flush()


def read_message():
    global FRAMING
    first = sys.stdin.buffer.read(1)
    if first == b"":
        return None
    if first == b"{":
        FRAMING = "line"
        rest = sys.stdin.buffer.readline()
        if rest == b"":
            return None
        return json.loads((first + rest).decode("utf-8"))

    FRAMING = "headers"
    chunk = first
    while b"\r\n\r\n" not in chunk and b"\n\n" not in chunk:
        nxt = sys.stdin.buffer.read(1)
        if nxt == b"":
            return None
        chunk += nxt
        if len(chunk) > 8192:
            return None
    separator = b"\r\n\r\n" if b"\r\n\r\n" in chunk else b"\n\n"
    header_blob, leftover = chunk.split(separator, 1)
    headers = {}
    for line in header_blob.decode("ascii", errors="replace").splitlines():
        if ":" in line:
            key, value = line.split(":", 1)
            headers[key.strip().lower()] = value.strip()
    length = int(headers.get("content-length", "0"))
    if length <= 0:
        return None
    while len(leftover) < length:
        nxt = sys.stdin.buffer.read(length - len(leftover))
        if nxt == b"":
            return None
        leftover += nxt
    return json.loads(leftover[:length].decode("utf-8"))


def result(request_id, value):
    send({"jsonrpc": "2.0", "id": request_id, "result": value})


def error(request_id, code, message):
    send(
        {
            "jsonrpc": "2.0",
            "id": request_id,
            "error": {"code": code, "message": message},
        }
    )


def handle(message):
    if message is None or "id" not in message:
        return
    request_id = message.get("id")
    method = message.get("method")
    params = message.get("params") or {}
    if method == "initialize":
        requested = params.get("protocolVersion") or FALLBACK_PROTOCOL
        result(
            request_id,
            {
                "protocolVersion": requested,
                "capabilities": {"tools": {"listChanged": False}},
                "serverInfo": {"name": "gds-assessment-reports", "version": "1.0.0"},
            },
        )
        return
    if method == "ping":
        result(request_id, {})
        return
    if method == "tools/list":
        result(request_id, {"tools": TOOLS})
        return
    if method == "tools/call":
        try:
            text = call_tool(params.get("name"), params.get("arguments"))
        except (KeyError, TypeError, ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
            result(
                request_id,
                {
                    "content": [{"type": "text", "text": str(exc)}],
                    "isError": True,
                },
            )
            return
        result(request_id, {"content": [{"type": "text", "text": text}]})
        return
    error(request_id, -32601, f"Method not found: {method}")


def main():
    print("gds-assessment-reports: waiting for MCP client", file=sys.stderr, flush=True)
    while True:
        message = read_message()
        if message is None:
            break
        try:
            handle(message)
        except Exception as exc:
            request_id = message.get("id")
            if request_id is not None:
                error(request_id, -32603, str(exc))


if __name__ == "__main__":
    main()

skills

gds-assessment-how-to

build_past_assessment_catalogue.py

create-evidence-kit.py

create-example.py

library_paths.py

reports_mcp_server.py

scrape_gds_reports.py

SKILL.md

.mcp.json

README.md

tile.json