Run mattermost-search MCP over shared HTTP and fix FTS hangs.
Avoid per-agent stdio contention on the sqlite index, sort FTS by rank with created tie-break, and add mm_search_tag for dated #plan/#report substring search. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+103
-12
@@ -1,13 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MCP server (stdio) exposing mm_search.py search capabilities.
|
||||
"""MCP server exposing mm_search.py search capabilities.
|
||||
|
||||
Cursor / Claude Desktop config example (~/.cursor/mcp.json):
|
||||
Prefer a single shared HTTP instance when multiple Cursor agents use this MCP
|
||||
(stdio spawns one process per agent and they contend on the sqlite index).
|
||||
|
||||
HTTP (recommended for multi-agent):
|
||||
|
||||
MM_MCP_TRANSPORT=http MM_MCP_HOST=127.0.0.1 MM_MCP_PORT=8766 \\
|
||||
/path/to/.venv/bin/python /path/to/mm_search_mcp.py
|
||||
|
||||
Cursor ~/.cursor/mcp.json:
|
||||
|
||||
{
|
||||
"mcpServers": {
|
||||
"mattermost-search": {
|
||||
"command": "/path/to/mattermost-dl/.venv/bin/python",
|
||||
"args": ["/path/to/mattermost-dl/mm_search_mcp.py"],
|
||||
"url": "http://127.0.0.1:8766/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Stdio (default, one process per client):
|
||||
|
||||
{
|
||||
"mcpServers": {
|
||||
"mattermost-search": {
|
||||
"command": "/path/to/.venv/bin/python",
|
||||
"args": ["/path/to/mm_search_mcp.py"],
|
||||
"cwd": "/path/to/mattermost-dl",
|
||||
"env": {
|
||||
"MM_SEARCH_ROOT": "results",
|
||||
@@ -298,6 +316,66 @@ def mm_user_directory_refresh(
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def mm_search_tag(
|
||||
keyword: str,
|
||||
after: str,
|
||||
before: str,
|
||||
user: str = None,
|
||||
channel: str = None,
|
||||
team: str = None,
|
||||
limit: int = 50,
|
||||
db: str = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Find posts containing #keyword in a required date range (newest first).
|
||||
|
||||
Plain SQL substring match — no FTS/rank. Use for daily tags like plan/report
|
||||
when you have after/before (e.g. one day or one week).
|
||||
|
||||
Args:
|
||||
keyword: Tag without or with leading # (e.g. "plan", "report", "#plan").
|
||||
after: Inclusive start date (YYYY-MM-DD or ISO) — required.
|
||||
before: Inclusive end date (YYYY-MM-DD or ISO) — required.
|
||||
user: Filter by username (exact match).
|
||||
channel: Filter by channel display_name or name (exact match).
|
||||
team: Filter by team name (exact match).
|
||||
limit: Max matches (default: 50).
|
||||
db: SQLite index path (default: MM_SEARCH_DB).
|
||||
"""
|
||||
resolved_db = db or _env_db()
|
||||
parsed_after = _parse_date(after)
|
||||
parsed_before = _parse_date(before)
|
||||
# Date-only before → end of that UTC day so a single day is inclusive.
|
||||
if "T" not in before.strip():
|
||||
parsed_before = parsed_before + dt.timedelta(days=1) - dt.timedelta(microseconds=1)
|
||||
|
||||
matches = list(
|
||||
mm_search_lib.search_tag(
|
||||
resolved_db,
|
||||
keyword,
|
||||
parsed_after,
|
||||
parsed_before,
|
||||
user=user,
|
||||
channel=channel,
|
||||
team=team,
|
||||
limit=limit,
|
||||
)
|
||||
)
|
||||
tag = keyword.strip()
|
||||
if not tag.startswith("#"):
|
||||
tag = f"#{tag}"
|
||||
return {
|
||||
"count": len(matches),
|
||||
"used_index": True,
|
||||
"search_mode": "tag",
|
||||
"tag": tag,
|
||||
"after": after,
|
||||
"before": before,
|
||||
"db": resolved_db,
|
||||
"matches": matches,
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def mm_search_index_status(
|
||||
db: Optional[str] = None,
|
||||
@@ -316,13 +394,12 @@ def mm_search_index_status(
|
||||
user_count = 0
|
||||
if db_exists:
|
||||
try:
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect(resolved_db)
|
||||
mm_users.ensure_users_schema(conn)
|
||||
user_count = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
||||
conn.close()
|
||||
except sqlite3.Error:
|
||||
conn = mm_search_lib.open_db(resolved_db, readonly=True)
|
||||
try:
|
||||
user_count = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
user_count = None
|
||||
|
||||
return {
|
||||
@@ -337,4 +414,18 @@ def mm_search_index_status(
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="stdio")
|
||||
transport = os.environ.get("MM_MCP_TRANSPORT", "stdio").strip().lower()
|
||||
if transport in ("http", "streamable-http", "sse"):
|
||||
host = os.environ.get("MM_MCP_HOST", "127.0.0.1")
|
||||
port = int(os.environ.get("MM_MCP_PORT", "8766"))
|
||||
path = os.environ.get("MM_MCP_PATH", "/mcp")
|
||||
# Stateless HTTP: safe for multiple Cursor agents against one process.
|
||||
mcp.run(
|
||||
transport="http" if transport == "streamable-http" else transport,
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
stateless_http=True,
|
||||
)
|
||||
else:
|
||||
mcp.run(transport="stdio")
|
||||
|
||||
Reference in New Issue
Block a user