Skip to content

Commit ab4ab26

Browse files
committed
cocalc-api/mcp: improve project.get (limit, filtering)
1 parent a8c4ee2 commit ab4ab26

File tree

2 files changed

+106
-7
lines changed

2 files changed

+106
-7
lines changed

src/python/cocalc-api/src/cocalc_api/hub.py

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,35 +155,112 @@ class Projects:
155155
def __init__(self, parent: "Hub"):
156156
self._parent = parent
157157

158-
def get(self, fields: Optional[list[str]] = None, all: Optional[bool] = False, project_id: Optional[str] = None) -> list[dict[str, Any]]:
158+
def get(
159+
self,
160+
fields: Optional[list[str]] = None,
161+
all: Optional[bool] = False,
162+
project_id: Optional[str] = None,
163+
limit: Optional[int] = None,
164+
deleted: Optional[bool] = None,
165+
hidden: Optional[bool] = None,
166+
state: Optional[str] = None,
167+
account_id_for_hidden: Optional[str] = None,
168+
) -> list[dict[str, Any]]:
159169
"""
160170
Get data about projects that you are a collaborator on. Only gets
161171
recent projects by default; set all=True to get all projects.
162172
163173
Args:
164174
fields (Optional[list[str]]): The fields about the project to get.
165-
Default: ['project_id', 'title', 'last_edited', 'state'], but see
175+
Default: ['project_id', 'title', 'last_edited', 'created', 'state', 'deleted', 'users'], but see
166176
https://github.com/sagemathinc/cocalc/blob/master/src/packages/util/db-schema/projects.ts
167177
all (Optional[bool]): If True, return ALL your projects,
168178
not just the recent ones. False by default.
169179
project_id (Optional[str]): If given, gets just this
170180
one project (as a list of length 1).
181+
limit (Optional[int]): Maximum number of projects to return after filtering. None means no limit.
182+
deleted (Optional[bool]): If set, filter deleted status (True -> only deleted, False -> only not deleted).
183+
hidden (Optional[bool]): If set, filter by collaborator-specific hidden flag. Default None (no filter).
184+
state (Optional[str]): If set, only return projects whose state matches (e.g., 'opened', 'running').
185+
account_id_for_hidden (Optional[str]): Account ID used to evaluate the hidden flag in the users map.
171186
172187
Returns:
173188
list[dict[str, Any]]: List of projects.
174189
"""
190+
from datetime import datetime
191+
192+
def _parse_ts(value: Any) -> float:
193+
if value is None:
194+
return 0.0
195+
if isinstance(value, (int, float)):
196+
return float(value)
197+
if isinstance(value, str):
198+
try:
199+
return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
200+
except ValueError:
201+
try:
202+
return float(value)
203+
except Exception:
204+
return 0.0
205+
return 0.0
206+
207+
def _state_str(val: Any) -> str:
208+
if isinstance(val, dict):
209+
return str(val.get("state") or val.get("status") or "")
210+
if val is None:
211+
return ""
212+
return str(val)
213+
175214
if fields is None:
176-
fields = ['project_id', 'title', 'last_edited', 'state']
215+
fields = ['project_id', 'title', 'last_edited', 'created', 'state', 'deleted', 'users']
177216
v: list[dict[str, Any]] = [{}]
178217
for field in fields:
179218
v[0][field] = None
180219
if project_id:
181220
v[0]['project_id'] = project_id
182-
query: dict[str, list[dict[str, None]]] = {}
221+
query: dict[str, list[dict[str, Any]]] = {}
183222
table = 'projects_all' if all else 'projects'
184223
query[table] = v
185224
result = self._parent.db.query(query)
186-
return result[table]
225+
projects: list[dict[str, Any]] = result[table]
226+
227+
filtered: list[dict[str, Any]] = []
228+
for project in projects:
229+
if deleted is not None:
230+
if bool(project.get("deleted")) != deleted:
231+
continue
232+
233+
if state:
234+
project_state = _state_str(project.get("state")).lower()
235+
if project_state != state.lower():
236+
continue
237+
238+
if hidden is not None and account_id_for_hidden:
239+
users = project.get("users") or {}
240+
if isinstance(users, dict):
241+
user_info = users.get(account_id_for_hidden, {})
242+
is_hidden = False
243+
if isinstance(user_info, dict):
244+
is_hidden = bool(user_info.get("hide"))
245+
if is_hidden != hidden:
246+
continue
247+
248+
filtered.append(project)
249+
250+
filtered.sort(
251+
key=lambda p: (
252+
_parse_ts(p.get("last_edited")),
253+
_parse_ts(p.get("created")),
254+
(p.get("title") or "").lower(),
255+
p.get("project_id") or "",
256+
),
257+
reverse=True,
258+
)
259+
260+
if limit is not None and limit >= 0:
261+
filtered = filtered[:limit]
262+
263+
return filtered
187264

188265
@api_method("projects.copyPathBetweenProjects")
189266
def copy_path_between_projects(

src/python/cocalc-api/src/cocalc_api/mcp/tools/projects_search.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ def register_projects_search_tool(mcp) -> None:
1515
"""Register the projects search tool with the given FastMCP instance."""
1616

1717
@mcp.tool()
18-
def projects_search(query: str = "") -> str:
18+
def projects_search(
19+
query: str = "",
20+
limit: int = 100,
21+
deleted: bool = False,
22+
hidden: bool = False,
23+
state: str | None = None,
24+
) -> str:
1925
"""
2026
Search for and list projects you have access to.
2127
@@ -28,6 +34,10 @@ def projects_search(query: str = "") -> str:
2834
Args:
2935
query (str): Search string to filter projects by title.
3036
Default "" lists all projects.
37+
limit (int): Maximum number of projects to return. Default 100.
38+
deleted (bool): If True, only show deleted projects. Default False.
39+
hidden (bool): If True, only show hidden projects; if False, exclude them. Default False.
40+
state (Optional[str]): Filter by state (e.g., "opened" or "running"). Default None (all states).
3141
3242
Returns:
3343
Formatted list of projects with:
@@ -51,6 +61,11 @@ def projects_search(query: str = "") -> str:
5161

5262
hub = Hub(api_key=_api_key, host=_host)
5363

64+
# Normalize limit
65+
limit = max(0, limit if limit is not None else 100)
66+
67+
account_id = _api_key_scope.get("account_id")
68+
5469
# Get all projects with full details
5570
projects = hub.projects.get(
5671
all=True,
@@ -59,10 +74,17 @@ def projects_search(query: str = "") -> str:
5974
"title",
6075
"description",
6176
"last_edited",
77+
"created",
6278
"state",
6379
"deleted",
6480
"users", # collaborators
65-
])
81+
],
82+
limit=limit,
83+
deleted=deleted,
84+
hidden=hidden,
85+
state=state,
86+
account_id_for_hidden=account_id,
87+
)
6688

6789
if not projects:
6890
return "No projects found"

0 commit comments

Comments
 (0)