search_events
Search through logged events using keywords to retrieve matching summaries with type and timestamp. A token-efficient alternative to generating full summaries.
Instructions
Plain-text search across all logged events.
Token-efficient alternative to get_summary when you only need events
matching a keyword. Returns matching event summaries with type and
timestamp.Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | ||
| limit | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- src/projectmem/mcp_server.py:263-285 (handler)MCP tool handler for 'search_events'. Registered via @mcp.tool() decorator. Reads all events, performs case-insensitive substring match on summary/notes, returns last N matching events formatted as text.
@mcp.tool() @safe_tool def search_events(query: str, limit: int = 10) -> str: """Plain-text search across all logged events. Token-efficient alternative to get_summary when you only need events matching a keyword. Returns matching event summaries with type and timestamp.""" events = read_events() q = query.lower() matches = [] for e in events: if q in e.summary.lower() or (e.notes and q in e.notes.lower()): matches.append(e) matches = matches[-limit:] if not matches: return f"No events match '{query}'." lines = [f"Found {len(matches)} match(es) for '{query}':"] for e in matches: outcome = f" ({e.outcome})" if e.outcome else "" loc = f" @ {e.location}" if e.location else "" lines.append(f" [{e.type}{outcome}] {e.summary}{loc}") return "\n".join(lines) - src/projectmem/models.py:68-123 (schema)Event dataclass used by search_events — defines the structure of events being searched (type, summary, notes, files, location, etc.)
@dataclass class Event: type: str summary: str id: str = field(default_factory=lambda: f"evt_{uuid4().hex[:20]}") timestamp: str = field(default_factory=utc_now_iso) issue_id: str | None = None outcome: str | None = None files: list[str] = field(default_factory=list) command: str | None = None notes: str | None = None git_commit: str | None = None location: str | None = None # Auto-capture fields (P0) auto_captured: bool = False capture_source: str | None = None capture_confidence: str | None = None git_message: str | None = None def __post_init__(self) -> None: if self.type not in VALID_EVENT_TYPES: raise ValueError(f"Unsupported event type: {self.type}") if self.outcome is not None and self.outcome not in VALID_OUTCOMES: raise ValueError(f"Unsupported outcome: {self.outcome}") if self.capture_source is not None and self.capture_source not in VALID_CAPTURE_SOURCES: raise ValueError(f"Unsupported capture source: {self.capture_source}") if self.capture_confidence is not None and self.capture_confidence not in VALID_CONFIDENCE_LEVELS: raise ValueError(f"Unsupported confidence level: {self.capture_confidence}") self.summary = self.summary.strip() if not self.summary: raise ValueError("Event summary cannot be empty") def to_dict(self) -> dict[str, Any]: data = asdict(self) return {key: value for key, value in data.items() if value not in (None, [], False)} @classmethod def from_dict(cls, data: dict[str, Any]) -> "Event": return cls( id=data.get("id") or f"evt_{uuid4().hex[:20]}", timestamp=normalize_timestamp(data.get("timestamp")) if data.get("timestamp") else utc_now_iso(), type=data["type"], issue_id=data.get("issue_id"), summary=data["summary"], outcome=data.get("outcome"), files=list(data.get("files") or []), command=data.get("command"), notes=data.get("notes"), git_commit=data.get("git_commit"), location=data.get("location"), auto_captured=bool(data.get("auto_captured", False)), capture_source=data.get("capture_source"), capture_confidence=data.get("capture_confidence"), git_message=data.get("git_message"), ) - src/projectmem/mcp_server.py:263-265 (registration)Tool registered as MCP tool via @mcp.tool() decorator on the search_events function in mcp_server.py
@mcp.tool() @safe_tool def search_events(query: str, limit: int = 10) -> str: - src/projectmem/search.py:9-39 (helper)Core search logic: case-insensitive substring or regex matching across event summaries, notes, and files. Used by the CLI command and could be imported elsewhere.
def search_events(query: str, regex: bool = False) -> list[Event]: """Search the event log. Default mode is case-insensitive substring match against the summary, notes, and `files` array. With ``regex=True`` the query is treated as a Python regex (case-insensitive) — useful for OR-patterns like ``"carousel|favicon"`` that previously returned `No matches.` because the literal pipe character isn't in any event (L-027c). """ events = read_events() if regex: try: pattern = re.compile(query, re.IGNORECASE) except re.error: # Bad regex → fall back to literal substring rather than crash. return search_events(query, regex=False) return [ event for event in events if pattern.search(event.summary) or (event.notes and pattern.search(event.notes)) or any(pattern.search(f) for f in event.files) ] needle = query.casefold() return [ event for event in events if needle in event.summary.casefold() or (event.notes and needle in event.notes.casefold()) or any(needle in file_path.casefold() for file_path in event.files) ] - src/projectmem/storage.py:379-389 (helper)Helper function that reads events from events.jsonl file, used by the MCP tool handler to load events before searching.
def read_events(root: Path | None = None) -> list[Event]: path = events_path(root) events: list[Event] = [] for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): if not line.strip(): continue try: events.append(Event.from_dict(json.loads(line))) except (json.JSONDecodeError, KeyError, ValueError) as exc: raise ProjectMemError(f"Invalid event at {path}:{line_number}: {exc}") from exc return events