1414import json
1515import os
1616import subprocess
17+ import sys
18+
19+ import click
1720
1821try :
1922 from fastmcp import FastMCP
2023except ImportError :
2124 FastMCP = None
2225
2326# ---------------------------------------------------------------------------
24- # Lite mode: expose only ~15 core tools when ROAM_MCP_LITE=1
27+ # Lite mode (default): expose only core tools for better agent tool selection.
28+ # Set ROAM_MCP_LITE=0 to expose all tools.
2529# ---------------------------------------------------------------------------
2630
27- _LITE = os .environ .get ("ROAM_MCP_LITE" , "1" ).lower () in ("1 " , "true " , "yes " )
31+ _LITE = os .environ .get ("ROAM_MCP_LITE" , "1" ).lower () not in ("0 " , "false " , "no " )
2832
2933_CORE_TOOLS = {
3034 # comprehension (5)
5559 mcp = None
5660
5761
58- def _tool (name = None ):
59- """MCP tool decorator. In lite mode (ROAM_MCP_LITE=1), only core tools register."""
62+ _REGISTERED_TOOLS : list [str ] = []
63+
64+
65+ def _tool (name : str ):
66+ """Register an MCP tool. In lite mode, only _CORE_TOOLS are registered."""
6067 def decorator (fn ):
6168 if mcp is None :
6269 return fn
63- tool_name = name or fn .__name__
64- if _LITE and tool_name not in _CORE_TOOLS :
70+ if _LITE and name not in _CORE_TOOLS :
6571 return fn
66- if name :
67- return mcp .tool (name = name )(fn )
68- return mcp .tool ()(fn )
72+ _REGISTERED_TOOLS .append (name )
73+ return mcp .tool (name = name )(fn )
6974 return decorator
7075
7176
@@ -74,35 +79,36 @@ def decorator(fn):
7479# ---------------------------------------------------------------------------
7580
7681
82+ _ERROR_PATTERNS : list [tuple [str , str , str ]] = [
83+ # (pattern, error_code, hint) — checked in order, first match wins.
84+ # More specific patterns MUST come before broader ones.
85+ ("no .roam" , "INDEX_NOT_FOUND" , "run `roam init` to create the codebase index." ),
86+ ("not found in index" , "INDEX_NOT_FOUND" , "run `roam init` to create the codebase index." ),
87+ ("index is stale" , "INDEX_STALE" , "run `roam index` to refresh." ),
88+ ("out of date" , "INDEX_STALE" , "run `roam index` to refresh." ),
89+ ("not a git repository" ,"NOT_GIT_REPO" , "some commands require git history. run: git init." ),
90+ ("database is locked" , "DB_LOCKED" , "another roam process is running. wait or delete .roam/index.lock." ),
91+ ("permission denied" , "PERMISSION_DENIED" ,"check file permissions." ),
92+ ("cannot open index" , "INDEX_NOT_FOUND" , "run `roam init` to create the codebase index." ),
93+ ("symbol not found" , "NO_RESULTS" , "try a different search term or check spelling." ),
94+ ("no matches" , "NO_RESULTS" , "try a different search term or check spelling." ),
95+ ("no results" , "NO_RESULTS" , "try a different search term or check spelling." ),
96+ ]
97+
98+
7799def _classify_error (stderr : str , exit_code : int ) -> tuple [str , str ]:
78- """classify error and return (error_code, hint)."""
100+ """Classify error and return (error_code, hint)."""
79101 s = stderr .lower ()
80- if "not found in index" in s or "no .roam" in s or "index.db" in s :
81- return ("INDEX_NOT_FOUND" , "run `roam init` to create the codebase index." )
82- if "stale" in s or "out of date" in s :
83- return ("INDEX_STALE" , "run `roam index` to refresh." )
84- if "not found" in s or "no matches" in s or "no results" in s :
85- return ("NO_RESULTS" , "try a different search term or check spelling." )
86- if "not a git repository" in s :
87- return ("NOT_GIT_REPO" , "some commands require git history. run: git init" )
88- if "permission denied" in s :
89- return ("PERMISSION_DENIED" , "check file permissions." )
90- if "database" in s and "locked" in s :
91- return ("DB_LOCKED" , "another roam process is running. wait or delete .roam/index.lock" )
92- if exit_code == 1 :
102+ for pattern , code , hint in _ERROR_PATTERNS :
103+ if pattern in s :
104+ return (code , hint )
105+ if exit_code != 0 :
93106 return ("COMMAND_FAILED" , "check arguments and try again." )
94107 return ("UNKNOWN" , "check the error message for details." )
95108
96109
97110def _ensure_fresh_index (root : str = "." ) -> dict | None :
98- """check index freshness, rebuild if stale. returns None if ok."""
99- from pathlib import Path
100- index_path = Path (root ).resolve () / ".roam" / "index.db"
101- if not index_path .exists ():
102- result = _run_roam (["index" ], root )
103- if "error" in result :
104- return {"error" : f"failed to create index: { result ['error' ]} " }
105- return None
111+ """Run incremental index to ensure freshness. Returns None on success."""
106112 result = _run_roam (["index" ], root )
107113 if "error" in result :
108114 return {"error" : f"index update failed: { result ['error' ]} " }
@@ -1990,41 +1996,50 @@ def roam_api_drift(model: str = "", confidence: str = "medium",
19901996# CLI command
19911997# ---------------------------------------------------------------------------
19921998
1993- import click
1994-
19951999
19962000@click .command ()
19972001@click .option ('--transport' , type = click .Choice (['stdio' , 'sse' ]), default = 'stdio' ,
19982002 help = 'transport protocol (default: stdio)' )
19992003@click .option ('--host' , default = 'localhost' , help = 'host for SSE mode' )
20002004@click .option ('--port' , type = int , default = 8000 , help = 'port for SSE mode' )
20012005@click .option ('--no-auto-index' , is_flag = True , help = 'skip automatic index freshness check' )
2002- def mcp_cmd (transport , host , port , no_auto_index ):
2006+ @click .option ('--list-tools' , is_flag = True , help = 'list registered tools and exit' )
2007+ def mcp_cmd (transport , host , port , no_auto_index , list_tools ):
20032008 """Start the roam MCP server.
20042009
20052010 \b
20062011 usage:
20072012 roam mcp # stdio (for Claude Code, Cursor, etc.)
20082013 roam mcp --transport sse # SSE on localhost:8000
2014+ roam mcp --list-tools # show registered tools
20092015
20102016 \b
20112017 environment:
2012- ROAM_MCP_LITE=1 # expose only core tools
2018+ ROAM_MCP_LITE=0 # expose all tools (default: lite/ core only)
20132019
20142020 \b
20152021 integration:
20162022 claude mcp add roam-code -- roam mcp
2017- """
2018- import sys
20192023
2024+ \b
2025+ requires:
2026+ pip install roam-code[mcp]
2027+ """
20202028 if mcp is None :
20212029 click .echo (
20222030 "error: fastmcp is required for the MCP server.\n "
2023- "install it with: pip install fastmcp " ,
2031+ "install it with: pip install roam-code[mcp] " ,
20242032 err = True ,
20252033 )
20262034 raise SystemExit (1 )
20272035
2036+ if list_tools :
2037+ mode = "lite" if _LITE else "full"
2038+ click .echo (f"{ len (_REGISTERED_TOOLS )} tools registered ({ mode } mode):\n " )
2039+ for t in sorted (_REGISTERED_TOOLS ):
2040+ click .echo (f" { t } " )
2041+ return
2042+
20282043 if not no_auto_index :
20292044 sys .stderr .write ("checking index freshness...\n " )
20302045 err = _ensure_fresh_index ("." )
@@ -2044,4 +2059,9 @@ def mcp_cmd(transport, host, port, no_auto_index):
20442059# ---------------------------------------------------------------------------
20452060
20462061if __name__ == "__main__" :
2062+ if mcp is None :
2063+ raise SystemExit (
2064+ "fastmcp is required for the MCP server.\n "
2065+ "Install it with: pip install roam-code[mcp]"
2066+ )
20472067 mcp .run ()
0 commit comments