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
3 changes: 3 additions & 0 deletions app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ class Settings(BaseSettings):
# Scoped Permissions Configuration
FORGETFUL_SCOPES: str = "*" # Comma-separated scopes (e.g. "read", "write:memories", "read,write:entities")

# MCP descriptor presentation
MCP_DESCRIPTOR_MODE: str = "verbose" # "verbose" (default) or "compact" meta-tool docstrings

# Memory Configuration
MEMORY_TITLE_MAX_LENGTH: int = 200 # Must be "easily titled" - scannable
MEMORY_CONTENT_MAX_LENGTH: int = 2000 # ~300-400 words - single concept
Expand Down
51 changes: 51 additions & 0 deletions app/routes/mcp/meta_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,58 @@ def _build_tool_categories_line() -> str:
return " | ".join(categories)


def _get_mcp_descriptor_mode() -> str:
"""Return the normalized descriptor mode for meta-tool docstrings."""
mode = str(getattr(settings, "MCP_DESCRIPTOR_MODE", "verbose")).strip().lower()
if mode == "compact":
return "compact"
return "verbose"


def _build_compact_discover_docstring(categories: str) -> str:
"""Build a compact discover_forgetful_tools docstring."""
return f"""\
Discover available Forgetful tools, optionally filtered by category.

Use this tool to list the available inner tools. Call how_to_use_forgetful_tool(tool_name)
for full parameter documentation and examples before uncommon or write operations.

Args:
category: Optional category filter ({categories})
ctx: FastMCP Context (automatically injected)

Returns:
Dictionary with tools_by_category, total_count, categories_available, and filtered_by.
"""


def _build_compact_execute_docstring(tool_categories: str) -> str:
"""Build a compact execute_forgetful_tool docstring."""
return f"""\
Execute any registered Forgetful inner tool by name.

Typical flow:
1. Call discover_forgetful_tools(category?) to find tool names.
2. Call how_to_use_forgetful_tool(tool_name) for full schemas and examples.
3. Call execute_forgetful_tool(tool_name, arguments) to run the selected tool.

Tool categories: {tool_categories}

Args:
tool_name: Name of the inner tool to execute
arguments: Dictionary of arguments for the selected tool
ctx: FastMCP Context (automatically injected)

Returns:
Tool execution result (format depends on the selected tool)
"""


def _build_discover_docstring() -> str:
"""Build discover_forgetful_tools docstring based on feature flags."""
categories = _build_category_list()
if _get_mcp_descriptor_mode() == "compact":
return _build_compact_discover_docstring(categories)

parts = [
f"""\
Expand Down Expand Up @@ -289,6 +338,8 @@ def _build_discover_docstring() -> str:
def _build_execute_docstring() -> str:
"""Build execute_forgetful_tool docstring based on feature flags."""
tool_categories = _build_tool_categories_line()
if _get_mcp_descriptor_mode() == "compact":
return _build_compact_execute_docstring(tool_categories)

parts = [
f"""\
Expand Down
9 changes: 9 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ Forgetful provides three Docker Compose configurations:
- `json` - Structured JSON format (recommended for production)
- **Example**: `LOG_FORMAT=json`

### `MCP_DESCRIPTOR_MODE`

- **Default**: `verbose`
- **Description**: Controls how much guidance is embedded in the MCP meta-tool descriptions exposed to clients
- **Values**:
- `verbose` - Include detailed inline guidance in `discover_forgetful_tools` and `execute_forgetful_tool`
- `compact` - Keep meta-tool descriptions short and use `how_to_use_forgetful_tool` for detailed schemas and examples
- **Example**: `MCP_DESCRIPTOR_MODE=compact`

---

## Docker Configuration
Expand Down
67 changes: 67 additions & 0 deletions tests/integration/test_meta_tools_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
_build_discover_docstring,
_build_execute_docstring,
_build_tool_categories_line,
_get_mcp_descriptor_mode,
)


Expand Down Expand Up @@ -134,6 +135,37 @@ def test_all_categories_when_all_enabled(self):
assert "task" in result


class TestMcpDescriptorMode:
"""Verify descriptor mode normalization."""

def test_default_descriptor_mode_is_verbose(self):
"""Missing descriptor mode falls back to verbose."""
with patch("app.routes.mcp.meta_tools.settings") as mock_settings:
mock_settings.MCP_DESCRIPTOR_MODE = ""

result = _get_mcp_descriptor_mode()

assert result == "verbose"

def test_compact_descriptor_mode_is_normalized(self):
"""Compact mode accepts case and whitespace differences."""
with patch("app.routes.mcp.meta_tools.settings") as mock_settings:
mock_settings.MCP_DESCRIPTOR_MODE = " Compact "

result = _get_mcp_descriptor_mode()

assert result == "compact"

def test_invalid_descriptor_mode_falls_back_to_verbose(self):
"""Unknown descriptor mode values are deterministic and safe."""
with patch("app.routes.mcp.meta_tools.settings") as mock_settings:
mock_settings.MCP_DESCRIPTOR_MODE = "minimal"

result = _get_mcp_descriptor_mode()

assert result == "verbose"


class TestDiscoverDocstring:
"""Verify discover_forgetful_tools docstring includes/excludes sections by flag."""

Expand Down Expand Up @@ -228,6 +260,23 @@ def test_workflow_section_always_present(self):

assert "## Workflow" in doc

def test_compact_mode_omits_inline_tool_catalog(self):
"""Compact mode keeps discovery guidance short."""
with patch("app.routes.mcp.meta_tools.settings") as mock_settings:
mock_settings.MCP_DESCRIPTOR_MODE = "compact"
mock_settings.SKILLS_ENABLED = True
mock_settings.FILES_ENABLED = True
mock_settings.PLANNING_ENABLED = True

doc = _build_discover_docstring()

assert "Discover available Forgetful tools" in doc
assert "how_to_use_forgetful_tool(tool_name)" in doc
assert "## All Available Tools" not in doc
assert "**Memory Tools**" not in doc
assert "**Task Tools**" not in doc
assert len(doc) < 900


class TestExecuteDocstring:
"""Verify execute_forgetful_tool docstring includes/excludes sections by flag."""
Expand Down Expand Up @@ -304,3 +353,21 @@ def test_linking_section_always_present(self):
doc = _build_execute_docstring()

assert "## Linking Best Practices" in doc

def test_compact_mode_omits_inline_examples(self):
"""Compact mode points to how_to_use instead of embedding long examples."""
with patch("app.routes.mcp.meta_tools.settings") as mock_settings:
mock_settings.MCP_DESCRIPTOR_MODE = "compact"
mock_settings.SKILLS_ENABLED = True
mock_settings.FILES_ENABLED = True
mock_settings.PLANNING_ENABLED = True

doc = _build_execute_docstring()

assert "Execute any registered Forgetful inner tool by name" in doc
assert "how_to_use_forgetful_tool(tool_name)" in doc
assert "Tool categories:" in doc
assert "## Quick Start" not in doc
assert "**Memory Operations:**" not in doc
assert "## Linking Best Practices" not in doc
assert len(doc) < 1000
Loading