The signature contract test pins exact tool parameters; update it for
the new workspace parameter.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
write_note now accepts workspace= alongside project= and project_id=,
matching the parameter surface and docstring guidance already present in
edit_note. The _compose_workspace_project_route helper is added locally
(mirroring the edit_note pattern) so agents can route writes to same-named
projects across workspaces using workspace/project qualified syntax.
Closes#882
Other write-path tools that share the same gap (move_note, delete_note)
are noted here but left for a separate PR to keep this change focused.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
Every basic-memory CLI invocation paid roughly 2 seconds of module-import
cost before any work started, which blew the Claude Code plugin's
SessionStart hook budget on cold machines (#886). The cost came from
module-level imports that pulled the entire server stack into CLI startup:
- mcp/async_client.py imported FastAPI at module level, so every consumer
of get_client() loaded FastAPI even for cloud-routed or help-only paths.
- mcp/clients/*.py imported call_* helpers from basic_memory.mcp.tools.utils,
which executes the whole tools package __init__ — every MCP tool module
plus fastmcp and the mcp SDK.
- mcp/project_context.py imported fastmcp.Context and ToolError eagerly.
- CLI command modules (tool, ci, schema) imported MCP tool functions at
module level; db and the import_* commands pulled SQLAlchemy/Alembic and
the markdown/file-service stack; status/doctor/orphans/command_utils
imported ToolError (the mcp SDK) and basic_memory.db.
- schemas/base.py imported dateparser (~0.13s) for one helper function.
The fix only defers imports to the point of use (no behavior changes):
FastAPI now loads inside _resolve_local_asgi_database alongside the
existing lazy api.app import, so it is only paid when a request actually
routes through the in-process ASGI transport; the typed clients import
call_* per method; project_context uses PEP 563 annotations with Context
under TYPE_CHECKING; the CLI command modules import their heavy
dependencies inside the command bodies. Tests that patched the old
module-level aliases now patch the source modules instead.
Measured on a warm cache (python -X importtime / wall time):
- import basic_memory.cli.main: 1.92s -> 0.45s
- bm --help: 2.40s -> 0.52s
- bm tool search-notes --help: 2.40s -> 0.86s
A regression test asserts that importing the CLI entry module with full
command registration leaves fastapi, sqlalchemy, alembic, fastmcp, mcp,
basic_memory.api.app, basic_memory.db, basic_memory.markdown,
basic_memory.mcp.tools, and basic_memory.services out of sys.modules.
Fixes#886
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
The edit_note docstring (and write_note in PR #964) advertise that the
workspace segment of a "workspace/project" route may be a slug, name, or
tenant_id, but resolve_workspace_project_identifier() only matched against
WorkspaceInfo.slug. Cloud routes using a display name or tenant UUID failed
with "Workspace ... was not found" despite the documented contract.
Extend the first-segment matching (only the matching logic; the overall
resolution flow is unchanged) to honor, in priority order:
1. slug (casefold) — unchanged, checked first so working routes keep meaning
2. tenant_id — exact match on the opaque id
3. display name (casefold) — fails fast on collisions, listing candidate slugs
A name that collides with another workspace's slug resolves to the slug owner.
Unknown identifiers raise a not-found error naming the forms that were tried.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
Adds human-readable `title` and categorization `tags` to all ~24 @mcp.tool
decorators across the MCP tools package. FastMCP 3.3.1 (pinned in pyproject.toml)
supports both fields natively. Tags used: notes, search, projects, cloud, schema,
navigation, canvas, ui.
Extends test_tool_contracts.py with an async test that asserts every registered
tool has a non-empty title and at least one tag to prevent future regressions.
output_schema is explicitly deferred as a follow-up (phase 2): it requires
per-tool design decisions about which tools reliably return structured JSON and
how to handle tools that return str|dict depending on output_format.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
Codex review of PR #962 identified two real issues:
1. CLI bypass: the BeforeValidator(parse_str_list) on note_types, entity_types, and
categories only fires through MCP/Pydantic validation. The CLI path in
cli/commands/tool.py calls search_notes() directly, so `bm tool search-notes
--type note,task` arrived as note_types=["note,task"] and matched nothing.
Fix: add in-body parse_str_list() normalization for all three params (mirroring
the existing parse_tags() call for tags on the same code path).
2. Silent stringify: parse_str_list used str(raw) in the list branch, so [42] became
["42"] before Pydantic saw it, accepting invalid input as a no-result search
instead of rejecting it. Fix: guard against non-string list elements and return
the original value unchanged so Pydantic rejects it with a clear error.
Tests added: annotation-level split tests for note_types/entity_types/categories,
non-string-element rejection tests, async direct-call regression for note_types,
and unit-level parse_str_list non-string list tests in test_coerce.py.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
has_frontmatter(), parse_frontmatter(), and remove_frontmatter() detected
frontmatter by substring/split (`content.startswith("---")` plus
`content.split("---", 2)`) rather than by line-anchored fences. A single-line
string that merely starts with `---` — e.g. `---\nstatus: active\n---\nBody`
where `\n` are literal backslash-n characters, a common CLI/agent input shape —
was misread as frontmatter: yaml parsed `\nstatus` as a key that got merged into
the note's YAML on disk, and the body was silently transformed.
Introduce a shared `_split_frontmatter()` helper that anchors both fences to
their own lines (`^---[ \t]*$`), tolerating leading blank lines so dedented
heredoc-style content still parses. All three public helpers now delegate to it,
preserving existing behavior for valid frontmatter (BOM stripping, empty -> {},
non-dict -> ParseError, and the existing ParseError messages callers assert on).
Closes#972
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>