Files
FabledScribe/src/scribe/mcp/tools/systems.py
T
bvandeusen 85e0501705
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 50s
CI & Build / Build & push image (push) Successful in 1m3s
feat(issues): S2 MCP tools — system CRUD + issue/system wiring
Second slice of Issues + Systems (spec #825).

New mcp/tools/systems.py: create_system, list_systems, get_system (records
split into issues/tasks/notes), update_system (incl. archive via status),
list_system_records (kind/open_only filters), delete_system. Registered in
register_all; read tools (get_system, list_systems, list_system_records) added
to the read-only-key allowlist (write tools default-deny).

create_task/update_task: kind now accepts 'issue'; new system_ids (set-semantics
associations) and arose_from_id (provenance, 0=unchanged/-1=clear) args.
create_note/update_note: new system_ids arg (notes associate with systems too).
services/notes.create_note: arose_from_id passthrough (update_note already
handles it via setattr).

Tests: MCP system tools + create_task issue-wiring (kind/provenance/systems),
service layer mocked.

Refs plan 825 (S2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:07:37 -04:00

151 lines
4.9 KiB
Python

"""System CRUD + record-association MCP tools — wrappers over services/systems.py.
A System is a per-project, reusable, self-describing subsystem/area that any
record (note, task, or issue) can be associated with — so research, build-work,
and corrective work line up under the same area and recurring problem-spots are
visible. The service enforces the multi-user ACL (project permission); these
tools are thin wrappers.
Sentinels (match the milestone/task tool conventions):
- name="" / description="" / color="" / status="" → "leave unchanged" on update
- order_index=-1 → "leave unchanged" on update (0 is a valid order_index)
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import systems as systems_svc
async def create_system(
project_id: int,
name: str,
description: str = "",
color: str = "",
) -> dict:
"""Create a System (a reusable, self-describing subsystem/area) in a project.
Associate records with it via the `system_ids` arg on create/update_task and
create/update_note.
Args:
project_id: The project this system belongs to (required).
name: Short label (required).
description: What the system is and how it's used — a name is rarely enough.
color: Optional UI accent (hex), or empty.
"""
uid = current_user_id()
system = await systems_svc.create_system(
uid, project_id=project_id, name=name,
description=description or None, color=color or None,
)
if system is None:
raise ValueError(f"cannot create system in project {project_id} (no write access)")
return system.to_dict()
async def list_systems(project_id: int, include_archived: bool = False) -> dict:
"""List a project's systems (active by default), ordered by order_index.
Pass include_archived=True to include archived systems.
"""
uid = current_user_id()
rows = await systems_svc.list_systems(uid, project_id, include_archived=include_archived)
return {"systems": [s.to_dict() for s in rows]}
async def get_system(system_id: int) -> dict:
"""Fetch a System plus the records associated with it.
Returns the system, plus its associated records split into `issues`,
`tasks` (work/plan), and `notes`.
"""
uid = current_user_id()
system = await systems_svc.get_system(uid, system_id)
if system is None:
raise ValueError(f"system {system_id} not found")
records = await systems_svc.list_records_for_system(uid, system_id)
issues, tasks, notes = [], [], []
for r in records:
d = r.to_dict()
if r.status is None:
notes.append(d)
elif r.task_kind == "issue":
issues.append(d)
else:
tasks.append(d)
data = system.to_dict()
data["issues"] = issues
data["tasks"] = tasks
data["notes"] = notes
return data
async def update_system(
system_id: int,
name: str = "",
description: str = "",
color: str = "",
status: str = "",
order_index: int = -1,
) -> dict:
"""Update a System. Only explicitly provided fields change.
Args:
status: 'active' or 'archived'. Archive a system to retire it without
losing history; archived systems hide from default lists.
order_index: display position (0-based); -1 = leave unchanged.
"""
uid = current_user_id()
fields: dict = {}
if name:
fields["name"] = name
if description:
fields["description"] = description
if color:
fields["color"] = color
if status:
fields["status"] = status
if order_index >= 0:
fields["order_index"] = order_index
system = await systems_svc.update_system(uid, system_id, **fields)
if system is None:
raise ValueError(f"system {system_id} not found or no write access")
return system.to_dict()
async def list_system_records(
system_id: int, kind: str = "", open_only: bool = False
) -> dict:
"""List records associated with a System.
Args:
kind: filter by task_kind — 'issue', 'work', or 'plan'. Omit for all.
open_only: limit to tasks not done/cancelled (e.g. open issues only).
"""
uid = current_user_id()
rows = await systems_svc.list_records_for_system(
uid, system_id, kind=kind or None, open_only=open_only,
)
return {"records": [r.to_dict() for r in rows]}
async def delete_system(system_id: int) -> dict:
"""Soft-delete a System (recoverable). Its record associations are removed."""
uid = current_user_id()
ok = await systems_svc.delete_system(uid, system_id)
if not ok:
raise ValueError(f"system {system_id} not found or no write access")
return {"message": f"System {system_id} deleted."}
def register(mcp) -> None:
for fn in (
create_system,
list_systems,
get_system,
update_system,
list_system_records,
delete_system,
):
mcp.tool(name=fn.__name__)(fn)