stravinsky 0.2.67__py3-none-any.whl → 0.4.66__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of stravinsky might be problematic. Click here for more details.

Files changed (190) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/__init__.py +16 -6
  3. mcp_bridge/auth/cli.py +202 -11
  4. mcp_bridge/auth/oauth.py +1 -2
  5. mcp_bridge/auth/openai_oauth.py +4 -7
  6. mcp_bridge/auth/token_store.py +112 -11
  7. mcp_bridge/cli/__init__.py +1 -1
  8. mcp_bridge/cli/install_hooks.py +503 -107
  9. mcp_bridge/cli/session_report.py +0 -3
  10. mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
  11. mcp_bridge/config/README.md +276 -0
  12. mcp_bridge/config/__init__.py +2 -2
  13. mcp_bridge/config/hook_config.py +247 -0
  14. mcp_bridge/config/hooks_manifest.json +138 -0
  15. mcp_bridge/config/rate_limits.py +317 -0
  16. mcp_bridge/config/skills_manifest.json +128 -0
  17. mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
  18. mcp_bridge/hooks/__init__.py +19 -4
  19. mcp_bridge/hooks/agent_reminder.py +4 -4
  20. mcp_bridge/hooks/auto_slash_command.py +5 -5
  21. mcp_bridge/hooks/budget_optimizer.py +2 -2
  22. mcp_bridge/hooks/claude_limits_hook.py +114 -0
  23. mcp_bridge/hooks/comment_checker.py +3 -4
  24. mcp_bridge/hooks/compaction.py +2 -2
  25. mcp_bridge/hooks/context.py +2 -1
  26. mcp_bridge/hooks/context_monitor.py +2 -2
  27. mcp_bridge/hooks/delegation_policy.py +85 -0
  28. mcp_bridge/hooks/directory_context.py +3 -3
  29. mcp_bridge/hooks/edit_recovery.py +3 -2
  30. mcp_bridge/hooks/edit_recovery_policy.py +49 -0
  31. mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
  32. mcp_bridge/hooks/events.py +160 -0
  33. mcp_bridge/hooks/git_noninteractive.py +4 -4
  34. mcp_bridge/hooks/keyword_detector.py +8 -10
  35. mcp_bridge/hooks/manager.py +43 -22
  36. mcp_bridge/hooks/notification_hook.py +13 -6
  37. mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
  38. mcp_bridge/hooks/parallel_enforcer.py +5 -5
  39. mcp_bridge/hooks/parallel_execution.py +22 -10
  40. mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
  41. mcp_bridge/hooks/pre_compact.py +8 -9
  42. mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
  43. mcp_bridge/hooks/preemptive_compaction.py +2 -3
  44. mcp_bridge/hooks/routing_notifications.py +80 -0
  45. mcp_bridge/hooks/rules_injector.py +11 -19
  46. mcp_bridge/hooks/session_idle.py +4 -4
  47. mcp_bridge/hooks/session_notifier.py +4 -4
  48. mcp_bridge/hooks/session_recovery.py +4 -5
  49. mcp_bridge/hooks/stravinsky_mode.py +1 -1
  50. mcp_bridge/hooks/subagent_stop.py +1 -3
  51. mcp_bridge/hooks/task_validator.py +2 -2
  52. mcp_bridge/hooks/tmux_manager.py +7 -8
  53. mcp_bridge/hooks/todo_delegation.py +4 -1
  54. mcp_bridge/hooks/todo_enforcer.py +180 -10
  55. mcp_bridge/hooks/tool_messaging.py +113 -10
  56. mcp_bridge/hooks/truncation_policy.py +37 -0
  57. mcp_bridge/hooks/truncator.py +1 -2
  58. mcp_bridge/metrics/cost_tracker.py +115 -0
  59. mcp_bridge/native_search.py +93 -0
  60. mcp_bridge/native_watcher.py +118 -0
  61. mcp_bridge/notifications.py +150 -0
  62. mcp_bridge/orchestrator/enums.py +11 -0
  63. mcp_bridge/orchestrator/router.py +165 -0
  64. mcp_bridge/orchestrator/state.py +32 -0
  65. mcp_bridge/orchestrator/visualization.py +14 -0
  66. mcp_bridge/orchestrator/wisdom.py +34 -0
  67. mcp_bridge/prompts/__init__.py +1 -8
  68. mcp_bridge/prompts/dewey.py +1 -1
  69. mcp_bridge/prompts/planner.py +2 -4
  70. mcp_bridge/prompts/stravinsky.py +53 -31
  71. mcp_bridge/proxy/__init__.py +0 -0
  72. mcp_bridge/proxy/client.py +70 -0
  73. mcp_bridge/proxy/model_server.py +157 -0
  74. mcp_bridge/routing/__init__.py +43 -0
  75. mcp_bridge/routing/config.py +250 -0
  76. mcp_bridge/routing/model_tiers.py +135 -0
  77. mcp_bridge/routing/provider_state.py +261 -0
  78. mcp_bridge/routing/task_classifier.py +190 -0
  79. mcp_bridge/server.py +542 -59
  80. mcp_bridge/server_tools.py +738 -6
  81. mcp_bridge/tools/__init__.py +40 -25
  82. mcp_bridge/tools/agent_manager.py +616 -697
  83. mcp_bridge/tools/background_tasks.py +13 -17
  84. mcp_bridge/tools/code_search.py +70 -53
  85. mcp_bridge/tools/continuous_loop.py +0 -1
  86. mcp_bridge/tools/dashboard.py +19 -0
  87. mcp_bridge/tools/find_code.py +296 -0
  88. mcp_bridge/tools/init.py +1 -0
  89. mcp_bridge/tools/list_directory.py +42 -0
  90. mcp_bridge/tools/lsp/__init__.py +12 -5
  91. mcp_bridge/tools/lsp/manager.py +471 -0
  92. mcp_bridge/tools/lsp/tools.py +723 -207
  93. mcp_bridge/tools/model_invoke.py +1195 -273
  94. mcp_bridge/tools/mux_client.py +75 -0
  95. mcp_bridge/tools/project_context.py +1 -2
  96. mcp_bridge/tools/query_classifier.py +406 -0
  97. mcp_bridge/tools/read_file.py +84 -0
  98. mcp_bridge/tools/replace.py +45 -0
  99. mcp_bridge/tools/run_shell_command.py +38 -0
  100. mcp_bridge/tools/search_enhancements.py +347 -0
  101. mcp_bridge/tools/semantic_search.py +3627 -0
  102. mcp_bridge/tools/session_manager.py +0 -2
  103. mcp_bridge/tools/skill_loader.py +0 -1
  104. mcp_bridge/tools/task_runner.py +5 -7
  105. mcp_bridge/tools/templates.py +3 -3
  106. mcp_bridge/tools/tool_search.py +331 -0
  107. mcp_bridge/tools/write_file.py +29 -0
  108. mcp_bridge/update_manager.py +585 -0
  109. mcp_bridge/update_manager_pypi.py +297 -0
  110. mcp_bridge/utils/cache.py +82 -0
  111. mcp_bridge/utils/process.py +71 -0
  112. mcp_bridge/utils/session_state.py +51 -0
  113. mcp_bridge/utils/truncation.py +76 -0
  114. stravinsky-0.4.66.dist-info/METADATA +517 -0
  115. stravinsky-0.4.66.dist-info/RECORD +198 -0
  116. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
  117. stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
  118. stravinsky_claude_assets/agents/HOOKS.md +437 -0
  119. stravinsky_claude_assets/agents/code-reviewer.md +210 -0
  120. stravinsky_claude_assets/agents/comment_checker.md +580 -0
  121. stravinsky_claude_assets/agents/debugger.md +254 -0
  122. stravinsky_claude_assets/agents/delphi.md +495 -0
  123. stravinsky_claude_assets/agents/dewey.md +248 -0
  124. stravinsky_claude_assets/agents/explore.md +1198 -0
  125. stravinsky_claude_assets/agents/frontend.md +472 -0
  126. stravinsky_claude_assets/agents/implementation-lead.md +164 -0
  127. stravinsky_claude_assets/agents/momus.md +464 -0
  128. stravinsky_claude_assets/agents/research-lead.md +141 -0
  129. stravinsky_claude_assets/agents/stravinsky.md +730 -0
  130. stravinsky_claude_assets/commands/delphi.md +9 -0
  131. stravinsky_claude_assets/commands/dewey.md +54 -0
  132. stravinsky_claude_assets/commands/git-master.md +112 -0
  133. stravinsky_claude_assets/commands/index.md +49 -0
  134. stravinsky_claude_assets/commands/publish.md +86 -0
  135. stravinsky_claude_assets/commands/review.md +73 -0
  136. stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
  137. stravinsky_claude_assets/commands/str/agent_list.md +56 -0
  138. stravinsky_claude_assets/commands/str/agent_output.md +92 -0
  139. stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
  140. stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
  141. stravinsky_claude_assets/commands/str/cancel.md +51 -0
  142. stravinsky_claude_assets/commands/str/clean.md +97 -0
  143. stravinsky_claude_assets/commands/str/continue.md +38 -0
  144. stravinsky_claude_assets/commands/str/index.md +199 -0
  145. stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
  146. stravinsky_claude_assets/commands/str/search.md +205 -0
  147. stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
  148. stravinsky_claude_assets/commands/str/stats.md +71 -0
  149. stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
  150. stravinsky_claude_assets/commands/str/unwatch.md +42 -0
  151. stravinsky_claude_assets/commands/str/watch.md +45 -0
  152. stravinsky_claude_assets/commands/strav.md +53 -0
  153. stravinsky_claude_assets/commands/stravinsky.md +292 -0
  154. stravinsky_claude_assets/commands/verify.md +60 -0
  155. stravinsky_claude_assets/commands/version.md +5 -0
  156. stravinsky_claude_assets/hooks/README.md +248 -0
  157. stravinsky_claude_assets/hooks/comment_checker.py +193 -0
  158. stravinsky_claude_assets/hooks/context.py +38 -0
  159. stravinsky_claude_assets/hooks/context_monitor.py +153 -0
  160. stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
  161. stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
  162. stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
  163. stravinsky_claude_assets/hooks/notification_hook.py +103 -0
  164. stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
  165. stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
  166. stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
  167. stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
  168. stravinsky_claude_assets/hooks/pre_compact.py +123 -0
  169. stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
  170. stravinsky_claude_assets/hooks/session_recovery.py +263 -0
  171. stravinsky_claude_assets/hooks/stop_hook.py +89 -0
  172. stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
  173. stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
  174. stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
  175. stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
  176. stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
  177. stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
  178. stravinsky_claude_assets/hooks/truncator.py +23 -0
  179. stravinsky_claude_assets/rules/deployment_safety.md +51 -0
  180. stravinsky_claude_assets/rules/integration_wiring.md +89 -0
  181. stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
  182. stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
  183. stravinsky_claude_assets/settings.json +152 -0
  184. stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
  185. stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
  186. stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
  187. stravinsky_claude_assets/task_dependencies.json +34 -0
  188. stravinsky-0.2.67.dist-info/METADATA +0 -284
  189. stravinsky-0.2.67.dist-info/RECORD +0 -76
  190. {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
@@ -6,10 +6,8 @@ Sessions are stored in ~/.claude/projects/ as JSONL files.
6
6
  """
7
7
 
8
8
  import json
9
- import os
10
9
  from datetime import datetime
11
10
  from pathlib import Path
12
- from typing import Any
13
11
 
14
12
 
15
13
  def get_sessions_directory() -> Path:
@@ -8,7 +8,6 @@ Discovers and lists available skills (slash commands) from:
8
8
  Skills are markdown files with frontmatter defining the command behavior.
9
9
  """
10
10
 
11
- import os
12
11
  import re
13
12
  from pathlib import Path
14
13
  from typing import Any
@@ -6,12 +6,10 @@ capture output, and update status in tasks.json.
6
6
  """
7
7
 
8
8
  import argparse
9
+ import asyncio
9
10
  import json
10
11
  import logging
11
12
  import os
12
- import sys
13
- import asyncio
14
- import subprocess
15
13
  from datetime import datetime
16
14
  from pathlib import Path
17
15
 
@@ -27,7 +25,7 @@ async def run_task(task_id: str, base_dir: str):
27
25
 
28
26
  # Load task details
29
27
  try:
30
- with open(tasks_file, "r") as f:
28
+ with open(tasks_file) as f:
31
29
  tasks = json.load(f)
32
30
  task = tasks.get(task_id)
33
31
  except Exception as e:
@@ -39,7 +37,7 @@ async def run_task(task_id: str, base_dir: str):
39
37
  return
40
38
 
41
39
  prompt = task.get("prompt")
42
- model = task.get("model", "gemini-2.0-flash")
40
+ model = task.get("model", "gemini-3-flash")
43
41
 
44
42
  output_file = agents_dir / f"{task_id}.out"
45
43
  agents_dir.mkdir(parents=True, exist_ok=True)
@@ -83,7 +81,7 @@ async def run_task(task_id: str, base_dir: str):
83
81
  f.write(result)
84
82
 
85
83
  # Update status
86
- with open(tasks_file, "r") as f:
84
+ with open(tasks_file) as f:
87
85
  tasks = json.load(f)
88
86
 
89
87
  if task_id in tasks:
@@ -104,7 +102,7 @@ async def run_task(task_id: str, base_dir: str):
104
102
 
105
103
  # Update status with error
106
104
  try:
107
- with open(tasks_file, "r") as f:
105
+ with open(tasks_file) as f:
108
106
  tasks = json.load(f)
109
107
  if task_id in tasks:
110
108
  tasks[task_id].update(
@@ -33,7 +33,7 @@ For ANY task with 2+ independent steps:
33
33
  3. Monitor with agent_progress, collect with agent_output
34
34
 
35
35
  ### Trigger Commands
36
- - **IRONSTAR** / **IRS**: Maximum parallel execution - spawn agents aggressively for every subtask
36
+ - **ULTRAWORK** / **UW**: Maximum parallel execution - spawn agents aggressively for every subtask
37
37
  - **ULTRATHINK**: Engage exhaustive deep reasoning, multi-dimensional analysis
38
38
  - **SEARCH**: Maximize search effort across codebase and external resources
39
39
  - **ANALYZE**: Deep analysis mode with delphi consultation for complex issues
@@ -99,11 +99,11 @@ stravinsky:agent_output(task_id="[id]", block=true)
99
99
  - This enables auto-delegation without manual /stravinsky invocation
100
100
 
101
101
  ### Execution Modes:
102
- - `ironstar` / `irs` / `ultrawork` - Maximum parallel execution (10+ agents)
102
+ - `ultrawork` / `irs` - Maximum parallel execution (10+ agents)
103
103
  - `ultrathink` - Deep reasoning with delphi consultation
104
104
  - `search` - Exhaustive multi-agent search
105
105
 
106
- **Your FIRST action must be spawning agents, not using Read/Search tools.**
106
+ **Your FUWT action must be spawning agents, not using Read/Search tools.**
107
107
  """
108
108
 
109
109
  COMMAND_PARALLEL = """---
@@ -0,0 +1,331 @@
1
+ """
2
+ Tool Search - BM25-based relevance search across MCP tools
3
+
4
+ Provides intelligent tool discovery using BM25 (Okapi) ranking algorithm.
5
+ Enables queries like "find github tools", "search semantic code", etc.
6
+
7
+ Features:
8
+ - BM25Okapi relevance scoring across tool names, descriptions, parameters
9
+ - Tag filtering (e.g., "github", "lsp", "semantic")
10
+ - Category filtering (e.g., "search", "code", "git")
11
+ - Top-K result ranking with scores
12
+ - Comprehensive error handling with timeouts
13
+ - Query logging for debugging
14
+
15
+ Architecture:
16
+ - Uses rank-bm25 for fast text-based relevance ranking
17
+ - Searches across: tool name, description, parameter names, parameter descriptions, tags
18
+ - Returns ranked list of tool names with relevance scores
19
+ - Supports both broad queries ("find code") and specific queries ("github file search")
20
+ """
21
+
22
+ import logging
23
+ import signal
24
+ from contextlib import contextmanager
25
+ from typing import Any
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Lazy import to avoid startup cost
30
+ _bm25 = None
31
+ _import_lock = None
32
+
33
+
34
+ def get_bm25():
35
+ """Lazy import of rank_bm25."""
36
+ global _bm25, _import_lock
37
+ if _bm25 is None:
38
+ if _import_lock is None:
39
+ import threading
40
+
41
+ _import_lock = threading.Lock()
42
+
43
+ with _import_lock:
44
+ if _bm25 is None:
45
+ try:
46
+ from rank_bm25 import BM25Okapi
47
+
48
+ _bm25 = BM25Okapi
49
+ except ImportError as e:
50
+ raise ImportError(
51
+ "rank-bm25 is required for tool search. "
52
+ "Install with: uv add rank-bm25"
53
+ ) from e
54
+ return _bm25
55
+
56
+
57
+ class TimeoutError(Exception):
58
+ """Raised when tool search times out."""
59
+
60
+ pass
61
+
62
+
63
+ @contextmanager
64
+ def timeout(seconds: int):
65
+ """Context manager for operation timeout."""
66
+
67
+ def timeout_handler(signum, frame):
68
+ raise TimeoutError(f"Tool search timed out after {seconds} seconds")
69
+
70
+ # Set the signal handler
71
+ original_handler = signal.signal(signal.SIGALRM, timeout_handler)
72
+ signal.alarm(seconds)
73
+
74
+ try:
75
+ yield
76
+ finally:
77
+ # Restore original handler and cancel alarm
78
+ signal.alarm(0)
79
+ signal.signal(signal.SIGALRM, original_handler)
80
+
81
+
82
+ def extract_tool_text(tool: Any) -> str:
83
+ """
84
+ Extract searchable text from a tool definition.
85
+
86
+ Args:
87
+ tool: Tool object (Pydantic model from mcp.types) with name, description, inputSchema, etc.
88
+
89
+ Returns:
90
+ Combined text for BM25 indexing (lowercased, space-separated).
91
+ """
92
+ parts = []
93
+
94
+ # Tool name (most important - add multiple times for weight boost)
95
+ name = getattr(tool, "name", "")
96
+ if name:
97
+ parts.extend([name, name, name]) # Triple weight for name
98
+
99
+ # Description (second most important - add twice)
100
+ description = getattr(tool, "description", "")
101
+ if description:
102
+ parts.extend([description, description])
103
+
104
+ # Parameter names and descriptions
105
+ input_schema = getattr(tool, "inputSchema", {})
106
+ properties = input_schema.get("properties", {}) if isinstance(input_schema, dict) else {}
107
+ for param_name, param_info in properties.items():
108
+ parts.append(param_name)
109
+ param_desc = param_info.get("description", "") if isinstance(param_info, dict) else ""
110
+ if param_desc:
111
+ parts.append(param_desc)
112
+
113
+ # Tags (if present - add twice for importance)
114
+ # Note: tags may be in tool.meta if present
115
+ meta = getattr(tool, "meta", None)
116
+ tags = meta.get("tags", []) if meta and isinstance(meta, dict) else []
117
+ if tags:
118
+ parts.extend(tags)
119
+ parts.extend(tags) # Double weight for tags
120
+
121
+ # Category (if present - add twice)
122
+ category = meta.get("category", "") if meta and isinstance(meta, dict) else ""
123
+ if category:
124
+ parts.extend([category, category])
125
+
126
+ # Join and tokenize
127
+ text = " ".join(str(p) for p in parts if p).lower()
128
+ return text
129
+
130
+
131
+ def tokenize(text: str) -> list[str]:
132
+ """
133
+ Simple tokenization for BM25.
134
+
135
+ Args:
136
+ text: Input text to tokenize.
137
+
138
+ Returns:
139
+ List of lowercase tokens (split on whitespace and punctuation).
140
+ """
141
+ # Replace common punctuation with spaces
142
+ for char in ".,;:!?()[]{}\"'`-_/\\|@#$%^&*+=<>":
143
+ text = text.replace(char, " ")
144
+
145
+ # Split on whitespace and filter empty strings
146
+ tokens = [t.lower() for t in text.split() if t.strip()]
147
+ return tokens
148
+
149
+
150
+ def search_tools(
151
+ query: str,
152
+ tools: list[Any],
153
+ top_k: int = 10,
154
+ tag_filter: str | None = None,
155
+ category_filter: str | None = None,
156
+ timeout_seconds: int = 5,
157
+ ) -> list[dict[str, Any]]:
158
+ """
159
+ Search tools using BM25 relevance ranking.
160
+
161
+ Args:
162
+ query: Natural language search query (e.g., "find github tools")
163
+ tools: List of Tool objects (Pydantic models from mcp.types)
164
+ top_k: Maximum number of results to return (default: 10)
165
+ tag_filter: Optional tag to filter by (e.g., "github", "lsp")
166
+ category_filter: Optional category to filter by (e.g., "search", "code")
167
+ timeout_seconds: Maximum execution time (default: 5 seconds)
168
+
169
+ Returns:
170
+ List of dicts with keys: name, score, tool (original tool object)
171
+ Sorted by relevance score (highest first).
172
+
173
+ Raises:
174
+ TimeoutError: If search exceeds timeout_seconds
175
+ ValueError: If query is empty or tools list is invalid
176
+ ImportError: If rank-bm25 is not installed
177
+
178
+ Example:
179
+ >>> tools = [...] # List of Tool objects from MCP
180
+ >>> results = search_tools("find github tools", tools, top_k=5)
181
+ >>> print(results[0]["name"]) # "github_search"
182
+ """
183
+ # Input validation
184
+ if not query or not query.strip():
185
+ raise ValueError("Query cannot be empty")
186
+
187
+ if not tools or not isinstance(tools, list):
188
+ raise ValueError("Tools must be a non-empty list")
189
+
190
+ if top_k <= 0:
191
+ raise ValueError("top_k must be positive")
192
+
193
+ logger.info(
194
+ f"Tool search: query='{query}', tools={len(tools)}, "
195
+ f"tag_filter={tag_filter}, category_filter={category_filter}, top_k={top_k}"
196
+ )
197
+
198
+ try:
199
+ with timeout(timeout_seconds):
200
+ # Apply filters first (reduce search space)
201
+ filtered_tools = tools
202
+ if tag_filter:
203
+ tag_lower = tag_filter.lower()
204
+ filtered_tools = [
205
+ t
206
+ for t in filtered_tools
207
+ if tag_lower in [
208
+ tag.lower()
209
+ for tag in (getattr(t, "meta", {}).get("tags", []) if isinstance(getattr(t, "meta", None), dict) else [])
210
+ ]
211
+ ]
212
+ logger.debug(f"Tag filter '{tag_filter}' reduced to {len(filtered_tools)} tools")
213
+
214
+ if category_filter:
215
+ cat_lower = category_filter.lower()
216
+ filtered_tools = [
217
+ t for t in filtered_tools
218
+ if (getattr(t, "meta", {}).get("category", "") if isinstance(getattr(t, "meta", None), dict) else "").lower() == cat_lower
219
+ ]
220
+ logger.debug(
221
+ f"Category filter '{category_filter}' reduced to {len(filtered_tools)} tools"
222
+ )
223
+
224
+ if not filtered_tools:
225
+ logger.warning("No tools remaining after filtering")
226
+ return []
227
+
228
+ # Extract and tokenize tool text
229
+ tool_texts = [extract_tool_text(t) for t in filtered_tools]
230
+ tokenized_corpus = [tokenize(text) for text in tool_texts]
231
+
232
+ # Initialize BM25
233
+ BM25Okapi = get_bm25()
234
+ bm25 = BM25Okapi(tokenized_corpus)
235
+
236
+ # Tokenize query
237
+ query_tokens = tokenize(query)
238
+ logger.debug(f"Query tokens: {query_tokens}")
239
+
240
+ # Score all documents
241
+ scores = bm25.get_scores(query_tokens)
242
+
243
+ # Create results with scores
244
+ results = [
245
+ {"name": getattr(tool, "name", ""), "score": float(score), "tool": tool}
246
+ for tool, score in zip(filtered_tools, scores)
247
+ ]
248
+
249
+ # Sort by score (descending) and take top K
250
+ results.sort(key=lambda x: x["score"], reverse=True)
251
+ top_results = results[:top_k]
252
+
253
+ # Log results
254
+ logger.info(
255
+ f"Tool search results: {len(top_results)} tools found "
256
+ f"(top score: {top_results[0]['score']:.2f} if top_results else 0)"
257
+ )
258
+ for i, result in enumerate(top_results[:5], 1): # Log top 5
259
+ logger.debug(f" {i}. {result['name']} (score: {result['score']:.2f})")
260
+
261
+ return top_results
262
+
263
+ except TimeoutError:
264
+ logger.error(f"Tool search timed out after {timeout_seconds}s: query='{query}'")
265
+ raise
266
+ except Exception as e:
267
+ logger.error(f"Tool search failed: query='{query}', error={e}", exc_info=True)
268
+ raise
269
+
270
+
271
+ def search_tool_names(
272
+ query: str,
273
+ tools: list[Any],
274
+ top_k: int = 10,
275
+ tag_filter: str | None = None,
276
+ category_filter: str | None = None,
277
+ ) -> list[str]:
278
+ """
279
+ Convenience function that returns just tool names (no scores).
280
+
281
+ Args:
282
+ query: Search query
283
+ tools: List of Tool objects (Pydantic models from mcp.types)
284
+ top_k: Maximum results
285
+ tag_filter: Optional tag filter
286
+ category_filter: Optional tag filter
287
+
288
+ Returns:
289
+ List of tool names ordered by relevance.
290
+
291
+ Example:
292
+ >>> tools = [...] # List of Tool objects
293
+ >>> names = search_tool_names("github search", tools, top_k=5)
294
+ >>> print(names) # ["github_search", "search_code", ...]
295
+ """
296
+ results = search_tools(query, tools, top_k, tag_filter, category_filter)
297
+ return [r["name"] for r in results]
298
+
299
+
300
+ def format_search_results(results: list[dict[str, Any]], include_scores: bool = True) -> str:
301
+ """
302
+ Format search results for display.
303
+
304
+ Args:
305
+ results: List of result dicts from search_tools()
306
+ include_scores: Whether to include relevance scores
307
+
308
+ Returns:
309
+ Formatted string for display.
310
+
311
+ Example:
312
+ >>> results = search_tools("github", tools)
313
+ >>> print(format_search_results(results))
314
+ Found 3 tools:
315
+ 1. github_search (score: 8.52)
316
+ 2. github_create_issue (score: 6.31)
317
+ 3. search_code (score: 2.14)
318
+ """
319
+ if not results:
320
+ return "No tools found"
321
+
322
+ lines = [f"Found {len(results)} tool(s):"]
323
+ for i, result in enumerate(results, 1):
324
+ name = result["name"]
325
+ if include_scores:
326
+ score = result["score"]
327
+ lines.append(f"{i}. {name} (score: {score:.2f})")
328
+ else:
329
+ lines.append(f"{i}. {name}")
330
+
331
+ return "\n".join(lines)
@@ -0,0 +1,29 @@
1
+ import os
2
+ from pathlib import Path
3
+ from mcp_bridge.utils.cache import IOCache
4
+
5
+ async def write_file(path: str, content: str) -> str:
6
+ """
7
+ Write content to a file and invalidate cache.
8
+ """
9
+ # USER-VISIBLE NOTIFICATION
10
+ import sys
11
+ print(f"📝 WRITE: {path} ({len(content)} bytes)", file=sys.stderr)
12
+
13
+ file_path = Path(path)
14
+ try:
15
+ # Ensure parent directories exist
16
+ file_path.parent.mkdir(parents=True, exist_ok=True)
17
+
18
+ # Write file
19
+ file_path.write_text(content, encoding="utf-8")
20
+
21
+ # Invalidate cache for this path and its parent (directory listing)
22
+ cache = IOCache.get_instance()
23
+ cache.invalidate_path(str(file_path))
24
+ cache.invalidate_path(str(file_path.parent))
25
+
26
+ return f"Successfully wrote {len(content)} bytes to {path}"
27
+
28
+ except Exception as e:
29
+ return f"Error writing file {path}: {str(e)}"