Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ to their retained original without needing Jira. Native MCP tools expose the
same canonical services, including `task_list_projects`, `task_get_project`,
`task_get_collection`, `task_get`, `task_search` and `task_get_source_archive`.
`task_get` accepts legacy Jira keys as well as Von task concept IDs.
Project listings return navigation metadata and descriptions; retrieve source
archives, retained-document references and their histories with `task_get_project`.

A local stdio connection can bind the trusted operator once in its server
environment:
Expand Down
28 changes: 27 additions & 1 deletion src/backend/services/task_project_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@

PROJECT_RECORD = "task_project"
COLLECTION_RECORD = "task_collection"
_PROJECT_LIST_FIELDS = (
"name",
"key",
"description",
"source_system",
"source_site",
"source_id",
"source_url",
"organisation_concept_id",
"domain_project_concept_id",
"collection_concept_ids",
"writer",
"cutover_state",
)


def _find_concept(concept_id):
Expand Down Expand Up @@ -60,10 +74,22 @@ def list_project_collections(project):


def list_task_projects(*, limit=100, offset=0):
"""List navigation metadata; archive details remain in get_task_project.

Retained source histories can grow independently of project navigation.
Do not load those histories and document manifests into every catalogue
response, where they can exceed the MCP transport's response size limit.
"""
query = {f"attributes.{PROJECT_RECORD}": {"$exists": True}}
docs = ConceptsRepository.find(
query,
projection={"concept_id": 1, "attributes.task_project": 1},
projection={
"concept_id": 1,
**{
f"attributes.{PROJECT_RECORD}.{field}": 1
for field in _PROJECT_LIST_FIELDS
},
},
limit=max(1, min(int(limit), 200)),
skip=max(0, int(offset)),
)
Expand Down
56 changes: 56 additions & 0 deletions tests/backend/test_task_project_service.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,66 @@
import copy
import json

import pytest

from src.backend.services import task_project_service as projects


def test_project_catalogue_does_not_grow_with_retained_source_history(monkeypatch):
record = {
"name": "Research project",
"key": "RESEARCH",
"description": "The complete project description",
"writer": "jira",
"source_id": "123",
"collection_concept_ids": ["#V#research_tasks"],
"source_archive_history": [{"manifest": "x" * 100_000}],
"external_document_archives": [{"manifest": "y" * 100_000}],
}
query_seen = {}

def find(query, *, projection, limit, skip):
query_seen.update(query=query, limit=limit, skip=skip)
selected = {
key: value
for key, value in record.items()
if projection.get("attributes.task_project")
or projection.get(f"attributes.task_project.{key}")
}
return [
{
"concept_id": f"#V#project_{index}",
"attributes": {"task_project": selected},
}
for index in range(skip, min(skip + limit, 43))
]

monkeypatch.setattr(projects.ConceptsRepository, "find", find)
result = projects.list_task_projects()
assert len(result) == 43
assert len(json.dumps({"success": True, "projects": result})) < 100_000
assert result[0]["description"] == record["description"]
assert result[0]["collection_concept_ids"] == record["collection_concept_ids"]
assert result[0]["writer"] == "jira"
assert result[0]["source_id"] == "123"
assert projects.list_task_projects(limit=2, offset=41) == result[41:]
assert query_seen == {
"query": {"attributes.task_project": {"$exists": True}},
"limit": 2,
"skip": 41,
}

monkeypatch.setattr(projects, "can_access_concept", lambda key: True)
monkeypatch.setattr(
projects,
"get_concept_by_concept_id_exact",
lambda key: {"attributes": {"task_project": copy.deepcopy(record)}},
)
detail = projects.get_task_project("#V#project_0")
assert detail["source_archive_history"] == record["source_archive_history"]
assert detail["external_document_archives"] == record["external_document_archives"]


def test_project_collection_rerun_preserves_identity_and_writer(monkeypatch):
docs = {}
publish_during_refresh = False
Expand Down
Loading