feat(mcp): make Scribe reflexively recall + scope reads to the active project #61
@@ -39,6 +39,22 @@ Mechanics:
|
|||||||
"not set". On update_task, -1 clears an existing FK (e.g. milestone_id=-1
|
"not set". On update_task, -1 clears an existing FK (e.g. milestone_id=-1
|
||||||
removes the task from its milestone); 0 leaves it unchanged.
|
removes the task from its milestone); 0 leaves it unchanged.
|
||||||
|
|
||||||
|
Reach for Scribe to RECALL, not just to record. Scribe is a second brain —
|
||||||
|
its value is mostly in what it already holds, so make searching it a reflex,
|
||||||
|
not something you wait to be asked for:
|
||||||
|
- Before you answer a question about the user's work, or start a task, search
|
||||||
|
Scribe first (search / list_tasks / list_notes). Assume relevant prior art
|
||||||
|
already exists — a related ticket, an earlier decision, a dev-log — and look
|
||||||
|
before you re-derive it or open a duplicate.
|
||||||
|
- Before creating a task, search for an existing one (search content_type=
|
||||||
|
'task') — don't open a second ticket for work already tracked.
|
||||||
|
- Scope reads to the project in scope. When a project is active (you called
|
||||||
|
enter_project), pass its project_id to search / list_tasks / list_notes so
|
||||||
|
results stay inside that project. Querying with no project_id pulls in every
|
||||||
|
project and bleeds unrelated work into the session — only do it for a
|
||||||
|
deliberate cross-project sweep. The active project does not stick on the
|
||||||
|
server (each call is self-contained); carrying its id forward is on you.
|
||||||
|
|
||||||
Keep task state honest — this is what makes the project a trustworthy record:
|
Keep task state honest — this is what makes the project a trustworthy record:
|
||||||
- When you begin working a task, set it to in_progress (update_task
|
- When you begin working a task, set it to in_progress (update_task
|
||||||
status=in_progress).
|
status=in_progress).
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ async def list_notes(
|
|||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
tag: str = "",
|
tag: str = "",
|
||||||
search_text: str = "",
|
search_text: str = "",
|
||||||
|
project_id: int = 0,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""List notes (non-task documents) stored in Scribe.
|
"""List notes (non-task documents) stored in Scribe.
|
||||||
|
|
||||||
@@ -30,6 +31,12 @@ async def list_notes(
|
|||||||
search against title and body. Results are ordered by last-updated descending.
|
search against title and body. Results are ordered by last-updated descending.
|
||||||
|
|
||||||
Use search for semantic/meaning-based lookup instead of exact keyword search.
|
Use search for semantic/meaning-based lookup instead of exact keyword search.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_id: Scope to one project. PASS THE ACTIVE PROJECT'S ID whenever a
|
||||||
|
project is in scope so you list that project's notes, not every
|
||||||
|
project's. 0 = no filter (all projects — use only for a deliberate
|
||||||
|
cross-project view).
|
||||||
"""
|
"""
|
||||||
uid = current_user_id()
|
uid = current_user_id()
|
||||||
rows, total = await notes_svc.list_notes(
|
rows, total = await notes_svc.list_notes(
|
||||||
@@ -37,6 +44,7 @@ async def list_notes(
|
|||||||
q=search_text or None,
|
q=search_text or None,
|
||||||
tags=[tag] if tag else None,
|
tags=[tag] if tag else None,
|
||||||
is_task=False,
|
is_task=False,
|
||||||
|
project_id=project_id or None,
|
||||||
limit=max(1, min(limit, 100)),
|
limit=max(1, min(limit, 100)),
|
||||||
offset=max(0, offset),
|
offset=max(0, offset),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,13 +11,30 @@ from scribe.mcp._context import current_user_id
|
|||||||
from scribe.services.embeddings import semantic_search_notes
|
from scribe.services.embeddings import semantic_search_notes
|
||||||
|
|
||||||
|
|
||||||
async def search(q: str, content_type: str = "all", limit: int = 10) -> dict:
|
async def search(
|
||||||
"""Semantic search over the user's notes and tasks.
|
q: str,
|
||||||
|
content_type: str = "all",
|
||||||
|
limit: int = 10,
|
||||||
|
project_id: int = 0,
|
||||||
|
) -> dict:
|
||||||
|
"""Semantic search over the user's existing notes and tasks — Scribe's recall.
|
||||||
|
|
||||||
|
Reach for this BEFORE answering a question about the user's work or starting
|
||||||
|
a task: the user's second-brain almost always already holds related prior
|
||||||
|
art. Check for an existing ticket before opening a new one (search with
|
||||||
|
content_type='task'), and for prior notes/decisions before re-deriving them.
|
||||||
|
Treating Scribe as the first place to look — not a place to only write — is
|
||||||
|
the difference between it being a trustworthy record and a write-only log.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
q: search query string.
|
q: search query string.
|
||||||
content_type: 'all' (default), 'note' (notes only), or 'task' (tasks only).
|
content_type: 'all' (default), 'note' (notes only), or 'task' (tasks only).
|
||||||
limit: maximum number of results (1-50).
|
limit: maximum number of results (1-50).
|
||||||
|
project_id: Scope results to one project. PASS THE ACTIVE PROJECT'S ID
|
||||||
|
whenever a project is in scope (the one you entered with
|
||||||
|
enter_project) — otherwise this searches across ALL projects and
|
||||||
|
bleeds unrelated work into the result set. 0 = search everything
|
||||||
|
(use only when you genuinely want a cross-project sweep).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
{"results": [{"id", "title", "body", "is_task", "tags", "similarity"}],
|
{"results": [{"id", "title", "body", "is_task", "tags", "similarity"}],
|
||||||
@@ -28,6 +45,7 @@ async def search(q: str, content_type: str = "all", limit: int = 10) -> dict:
|
|||||||
is_task = {"note": False, "task": True}.get(content_type) # None => any
|
is_task = {"note": False, "task": True}.get(content_type) # None => any
|
||||||
raw = await semantic_search_notes(
|
raw = await semantic_search_notes(
|
||||||
uid, q, limit=limit, is_task=is_task,
|
uid, q, limit=limit, is_task=is_task,
|
||||||
|
project_id=project_id or None,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"results": [
|
"results": [
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ async def list_tasks(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
status: Filter by status — one of: todo, in_progress, done, cancelled. Omit for all.
|
status: Filter by status — one of: todo, in_progress, done, cancelled. Omit for all.
|
||||||
project_id: Filter to a specific project. Use 0 for no filter.
|
project_id: Filter to a specific project. PASS THE ACTIVE PROJECT'S ID
|
||||||
|
whenever a project is in scope so you list that project's tasks, not
|
||||||
|
every project's. 0 = no filter (all projects — use only for a
|
||||||
|
deliberate cross-project view).
|
||||||
kind: Filter by task kind — 'work' or 'plan'. Omit (empty) for all kinds.
|
kind: Filter by task kind — 'work' or 'plan'. Omit (empty) for all kinds.
|
||||||
|
|
||||||
Results are ordered by last-updated descending.
|
Results are ordered by last-updated descending.
|
||||||
|
|||||||
Reference in New Issue
Block a user