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.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/auth/__init__.py +16 -6
- mcp_bridge/auth/cli.py +202 -11
- mcp_bridge/auth/oauth.py +1 -2
- mcp_bridge/auth/openai_oauth.py +4 -7
- mcp_bridge/auth/token_store.py +112 -11
- mcp_bridge/cli/__init__.py +1 -1
- mcp_bridge/cli/install_hooks.py +503 -107
- mcp_bridge/cli/session_report.py +0 -3
- mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
- mcp_bridge/config/README.md +276 -0
- mcp_bridge/config/__init__.py +2 -2
- mcp_bridge/config/hook_config.py +247 -0
- mcp_bridge/config/hooks_manifest.json +138 -0
- mcp_bridge/config/rate_limits.py +317 -0
- mcp_bridge/config/skills_manifest.json +128 -0
- mcp_bridge/hooks/HOOKS_SETTINGS.json +17 -4
- mcp_bridge/hooks/__init__.py +19 -4
- mcp_bridge/hooks/agent_reminder.py +4 -4
- mcp_bridge/hooks/auto_slash_command.py +5 -5
- mcp_bridge/hooks/budget_optimizer.py +2 -2
- mcp_bridge/hooks/claude_limits_hook.py +114 -0
- mcp_bridge/hooks/comment_checker.py +3 -4
- mcp_bridge/hooks/compaction.py +2 -2
- mcp_bridge/hooks/context.py +2 -1
- mcp_bridge/hooks/context_monitor.py +2 -2
- mcp_bridge/hooks/delegation_policy.py +85 -0
- mcp_bridge/hooks/directory_context.py +3 -3
- mcp_bridge/hooks/edit_recovery.py +3 -2
- mcp_bridge/hooks/edit_recovery_policy.py +49 -0
- mcp_bridge/hooks/empty_message_sanitizer.py +2 -2
- mcp_bridge/hooks/events.py +160 -0
- mcp_bridge/hooks/git_noninteractive.py +4 -4
- mcp_bridge/hooks/keyword_detector.py +8 -10
- mcp_bridge/hooks/manager.py +43 -22
- mcp_bridge/hooks/notification_hook.py +13 -6
- mcp_bridge/hooks/parallel_enforcement_policy.py +67 -0
- mcp_bridge/hooks/parallel_enforcer.py +5 -5
- mcp_bridge/hooks/parallel_execution.py +22 -10
- mcp_bridge/hooks/post_tool/parallel_validation.py +103 -0
- mcp_bridge/hooks/pre_compact.py +8 -9
- mcp_bridge/hooks/pre_tool/agent_spawn_validator.py +115 -0
- mcp_bridge/hooks/preemptive_compaction.py +2 -3
- mcp_bridge/hooks/routing_notifications.py +80 -0
- mcp_bridge/hooks/rules_injector.py +11 -19
- mcp_bridge/hooks/session_idle.py +4 -4
- mcp_bridge/hooks/session_notifier.py +4 -4
- mcp_bridge/hooks/session_recovery.py +4 -5
- mcp_bridge/hooks/stravinsky_mode.py +1 -1
- mcp_bridge/hooks/subagent_stop.py +1 -3
- mcp_bridge/hooks/task_validator.py +2 -2
- mcp_bridge/hooks/tmux_manager.py +7 -8
- mcp_bridge/hooks/todo_delegation.py +4 -1
- mcp_bridge/hooks/todo_enforcer.py +180 -10
- mcp_bridge/hooks/tool_messaging.py +113 -10
- mcp_bridge/hooks/truncation_policy.py +37 -0
- mcp_bridge/hooks/truncator.py +1 -2
- mcp_bridge/metrics/cost_tracker.py +115 -0
- mcp_bridge/native_search.py +93 -0
- mcp_bridge/native_watcher.py +118 -0
- mcp_bridge/notifications.py +150 -0
- mcp_bridge/orchestrator/enums.py +11 -0
- mcp_bridge/orchestrator/router.py +165 -0
- mcp_bridge/orchestrator/state.py +32 -0
- mcp_bridge/orchestrator/visualization.py +14 -0
- mcp_bridge/orchestrator/wisdom.py +34 -0
- mcp_bridge/prompts/__init__.py +1 -8
- mcp_bridge/prompts/dewey.py +1 -1
- mcp_bridge/prompts/planner.py +2 -4
- mcp_bridge/prompts/stravinsky.py +53 -31
- mcp_bridge/proxy/__init__.py +0 -0
- mcp_bridge/proxy/client.py +70 -0
- mcp_bridge/proxy/model_server.py +157 -0
- mcp_bridge/routing/__init__.py +43 -0
- mcp_bridge/routing/config.py +250 -0
- mcp_bridge/routing/model_tiers.py +135 -0
- mcp_bridge/routing/provider_state.py +261 -0
- mcp_bridge/routing/task_classifier.py +190 -0
- mcp_bridge/server.py +542 -59
- mcp_bridge/server_tools.py +738 -6
- mcp_bridge/tools/__init__.py +40 -25
- mcp_bridge/tools/agent_manager.py +616 -697
- mcp_bridge/tools/background_tasks.py +13 -17
- mcp_bridge/tools/code_search.py +70 -53
- mcp_bridge/tools/continuous_loop.py +0 -1
- mcp_bridge/tools/dashboard.py +19 -0
- mcp_bridge/tools/find_code.py +296 -0
- mcp_bridge/tools/init.py +1 -0
- mcp_bridge/tools/list_directory.py +42 -0
- mcp_bridge/tools/lsp/__init__.py +12 -5
- mcp_bridge/tools/lsp/manager.py +471 -0
- mcp_bridge/tools/lsp/tools.py +723 -207
- mcp_bridge/tools/model_invoke.py +1195 -273
- mcp_bridge/tools/mux_client.py +75 -0
- mcp_bridge/tools/project_context.py +1 -2
- mcp_bridge/tools/query_classifier.py +406 -0
- mcp_bridge/tools/read_file.py +84 -0
- mcp_bridge/tools/replace.py +45 -0
- mcp_bridge/tools/run_shell_command.py +38 -0
- mcp_bridge/tools/search_enhancements.py +347 -0
- mcp_bridge/tools/semantic_search.py +3627 -0
- mcp_bridge/tools/session_manager.py +0 -2
- mcp_bridge/tools/skill_loader.py +0 -1
- mcp_bridge/tools/task_runner.py +5 -7
- mcp_bridge/tools/templates.py +3 -3
- mcp_bridge/tools/tool_search.py +331 -0
- mcp_bridge/tools/write_file.py +29 -0
- mcp_bridge/update_manager.py +585 -0
- mcp_bridge/update_manager_pypi.py +297 -0
- mcp_bridge/utils/cache.py +82 -0
- mcp_bridge/utils/process.py +71 -0
- mcp_bridge/utils/session_state.py +51 -0
- mcp_bridge/utils/truncation.py +76 -0
- stravinsky-0.4.66.dist-info/METADATA +517 -0
- stravinsky-0.4.66.dist-info/RECORD +198 -0
- {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/entry_points.txt +1 -0
- stravinsky_claude_assets/HOOKS_INTEGRATION.md +316 -0
- stravinsky_claude_assets/agents/HOOKS.md +437 -0
- stravinsky_claude_assets/agents/code-reviewer.md +210 -0
- stravinsky_claude_assets/agents/comment_checker.md +580 -0
- stravinsky_claude_assets/agents/debugger.md +254 -0
- stravinsky_claude_assets/agents/delphi.md +495 -0
- stravinsky_claude_assets/agents/dewey.md +248 -0
- stravinsky_claude_assets/agents/explore.md +1198 -0
- stravinsky_claude_assets/agents/frontend.md +472 -0
- stravinsky_claude_assets/agents/implementation-lead.md +164 -0
- stravinsky_claude_assets/agents/momus.md +464 -0
- stravinsky_claude_assets/agents/research-lead.md +141 -0
- stravinsky_claude_assets/agents/stravinsky.md +730 -0
- stravinsky_claude_assets/commands/delphi.md +9 -0
- stravinsky_claude_assets/commands/dewey.md +54 -0
- stravinsky_claude_assets/commands/git-master.md +112 -0
- stravinsky_claude_assets/commands/index.md +49 -0
- stravinsky_claude_assets/commands/publish.md +86 -0
- stravinsky_claude_assets/commands/review.md +73 -0
- stravinsky_claude_assets/commands/str/agent_cancel.md +70 -0
- stravinsky_claude_assets/commands/str/agent_list.md +56 -0
- stravinsky_claude_assets/commands/str/agent_output.md +92 -0
- stravinsky_claude_assets/commands/str/agent_progress.md +74 -0
- stravinsky_claude_assets/commands/str/agent_retry.md +94 -0
- stravinsky_claude_assets/commands/str/cancel.md +51 -0
- stravinsky_claude_assets/commands/str/clean.md +97 -0
- stravinsky_claude_assets/commands/str/continue.md +38 -0
- stravinsky_claude_assets/commands/str/index.md +199 -0
- stravinsky_claude_assets/commands/str/list_watchers.md +96 -0
- stravinsky_claude_assets/commands/str/search.md +205 -0
- stravinsky_claude_assets/commands/str/start_filewatch.md +136 -0
- stravinsky_claude_assets/commands/str/stats.md +71 -0
- stravinsky_claude_assets/commands/str/stop_filewatch.md +89 -0
- stravinsky_claude_assets/commands/str/unwatch.md +42 -0
- stravinsky_claude_assets/commands/str/watch.md +45 -0
- stravinsky_claude_assets/commands/strav.md +53 -0
- stravinsky_claude_assets/commands/stravinsky.md +292 -0
- stravinsky_claude_assets/commands/verify.md +60 -0
- stravinsky_claude_assets/commands/version.md +5 -0
- stravinsky_claude_assets/hooks/README.md +248 -0
- stravinsky_claude_assets/hooks/comment_checker.py +193 -0
- stravinsky_claude_assets/hooks/context.py +38 -0
- stravinsky_claude_assets/hooks/context_monitor.py +153 -0
- stravinsky_claude_assets/hooks/dependency_tracker.py +73 -0
- stravinsky_claude_assets/hooks/edit_recovery.py +46 -0
- stravinsky_claude_assets/hooks/execution_state_tracker.py +68 -0
- stravinsky_claude_assets/hooks/notification_hook.py +103 -0
- stravinsky_claude_assets/hooks/notification_hook_v2.py +96 -0
- stravinsky_claude_assets/hooks/parallel_execution.py +241 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement.py +106 -0
- stravinsky_claude_assets/hooks/parallel_reinforcement_v2.py +112 -0
- stravinsky_claude_assets/hooks/pre_compact.py +123 -0
- stravinsky_claude_assets/hooks/ralph_loop.py +173 -0
- stravinsky_claude_assets/hooks/session_recovery.py +263 -0
- stravinsky_claude_assets/hooks/stop_hook.py +89 -0
- stravinsky_claude_assets/hooks/stravinsky_metrics.py +164 -0
- stravinsky_claude_assets/hooks/stravinsky_mode.py +146 -0
- stravinsky_claude_assets/hooks/subagent_stop.py +98 -0
- stravinsky_claude_assets/hooks/todo_continuation.py +111 -0
- stravinsky_claude_assets/hooks/todo_delegation.py +96 -0
- stravinsky_claude_assets/hooks/tool_messaging.py +281 -0
- stravinsky_claude_assets/hooks/truncator.py +23 -0
- stravinsky_claude_assets/rules/deployment_safety.md +51 -0
- stravinsky_claude_assets/rules/integration_wiring.md +89 -0
- stravinsky_claude_assets/rules/pypi_deployment.md +220 -0
- stravinsky_claude_assets/rules/stravinsky_orchestrator.md +32 -0
- stravinsky_claude_assets/settings.json +152 -0
- stravinsky_claude_assets/skills/chrome-devtools/SKILL.md +81 -0
- stravinsky_claude_assets/skills/sqlite/SKILL.md +77 -0
- stravinsky_claude_assets/skills/supabase/SKILL.md +74 -0
- stravinsky_claude_assets/task_dependencies.json +34 -0
- stravinsky-0.2.67.dist-info/METADATA +0 -284
- stravinsky-0.2.67.dist-info/RECORD +0 -76
- {stravinsky-0.2.67.dist-info → stravinsky-0.4.66.dist-info}/WHEEL +0 -0
mcp_bridge/tools/skill_loader.py
CHANGED
mcp_bridge/tools/task_runner.py
CHANGED
|
@@ -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
|
|
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-
|
|
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
|
|
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
|
|
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(
|
mcp_bridge/tools/templates.py
CHANGED
|
@@ -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
|
-
- **
|
|
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
|
-
- `
|
|
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
|
|
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)}"
|