kollabor 0.4.9__py3-none-any.whl → 0.4.15__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.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
core/llm/mcp_integration.py
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Provides integration with MCP servers for tool discovery and execution,
|
|
4
4
|
enabling the LLM to interact with external tools and services.
|
|
5
|
+
|
|
6
|
+
Implements MCP JSON-RPC 2.0 protocol over stdio for:
|
|
7
|
+
- Server initialization handshake
|
|
8
|
+
- tools/list for tool discovery
|
|
9
|
+
- tools/call for tool execution
|
|
5
10
|
"""
|
|
6
11
|
|
|
7
12
|
import asyncio
|
|
@@ -10,32 +15,208 @@ import logging
|
|
|
10
15
|
import re
|
|
11
16
|
import shlex
|
|
12
17
|
import subprocess
|
|
18
|
+
import uuid
|
|
13
19
|
from pathlib import Path
|
|
14
|
-
from typing import Any, Dict, List, Optional
|
|
20
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
21
|
|
|
16
22
|
logger = logging.getLogger(__name__)
|
|
17
23
|
|
|
18
24
|
|
|
25
|
+
class MCPServerConnection:
|
|
26
|
+
"""Manages a connection to an MCP server via stdio."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, server_name: str, command: str):
|
|
29
|
+
self.server_name = server_name
|
|
30
|
+
self.command = command
|
|
31
|
+
self.process: Optional[asyncio.subprocess.Process] = None
|
|
32
|
+
self.initialized = False
|
|
33
|
+
self._read_buffer = ""
|
|
34
|
+
|
|
35
|
+
async def connect(self) -> bool:
|
|
36
|
+
"""Start the MCP server process."""
|
|
37
|
+
try:
|
|
38
|
+
command_parts = shlex.split(self.command)
|
|
39
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
40
|
+
*command_parts,
|
|
41
|
+
stdin=asyncio.subprocess.PIPE,
|
|
42
|
+
stdout=asyncio.subprocess.PIPE,
|
|
43
|
+
stderr=asyncio.subprocess.PIPE
|
|
44
|
+
)
|
|
45
|
+
logger.info(f"Started MCP server process: {self.server_name}")
|
|
46
|
+
return True
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Failed to start MCP server {self.server_name}: {e}")
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
async def initialize(self) -> bool:
|
|
52
|
+
"""Send MCP initialize request and wait for response."""
|
|
53
|
+
if not self.process:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
request = {
|
|
57
|
+
"jsonrpc": "2.0",
|
|
58
|
+
"id": str(uuid.uuid4()),
|
|
59
|
+
"method": "initialize",
|
|
60
|
+
"params": {
|
|
61
|
+
"protocolVersion": "2024-11-05",
|
|
62
|
+
"capabilities": {},
|
|
63
|
+
"clientInfo": {
|
|
64
|
+
"name": "kollabor-cli",
|
|
65
|
+
"version": "1.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
response = await self._send_request(request)
|
|
71
|
+
if response and "result" in response:
|
|
72
|
+
self.initialized = True
|
|
73
|
+
logger.info(f"MCP server {self.server_name} initialized")
|
|
74
|
+
|
|
75
|
+
# Send initialized notification
|
|
76
|
+
notification = {
|
|
77
|
+
"jsonrpc": "2.0",
|
|
78
|
+
"method": "notifications/initialized"
|
|
79
|
+
}
|
|
80
|
+
await self._send_notification(notification)
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
logger.warning(f"MCP server {self.server_name} initialization failed")
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
async def list_tools(self) -> List[Dict[str, Any]]:
|
|
87
|
+
"""Request tools list from server."""
|
|
88
|
+
if not self.initialized:
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
request = {
|
|
92
|
+
"jsonrpc": "2.0",
|
|
93
|
+
"id": str(uuid.uuid4()),
|
|
94
|
+
"method": "tools/list",
|
|
95
|
+
"params": {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
response = await self._send_request(request)
|
|
99
|
+
if response and "result" in response:
|
|
100
|
+
tools = response["result"].get("tools", [])
|
|
101
|
+
logger.info(f"Got {len(tools)} tools from {self.server_name}")
|
|
102
|
+
return tools
|
|
103
|
+
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
107
|
+
"""Call a tool on the server."""
|
|
108
|
+
if not self.initialized:
|
|
109
|
+
return {"error": "Server not initialized"}
|
|
110
|
+
|
|
111
|
+
request = {
|
|
112
|
+
"jsonrpc": "2.0",
|
|
113
|
+
"id": str(uuid.uuid4()),
|
|
114
|
+
"method": "tools/call",
|
|
115
|
+
"params": {
|
|
116
|
+
"name": tool_name,
|
|
117
|
+
"arguments": arguments
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
response = await self._send_request(request)
|
|
122
|
+
if response:
|
|
123
|
+
if "result" in response:
|
|
124
|
+
return response["result"]
|
|
125
|
+
elif "error" in response:
|
|
126
|
+
return {"error": response["error"].get("message", "Unknown error")}
|
|
127
|
+
|
|
128
|
+
return {"error": "No response from server"}
|
|
129
|
+
|
|
130
|
+
async def _send_request(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
131
|
+
"""Send JSON-RPC request and wait for response."""
|
|
132
|
+
if not self.process or not self.process.stdin:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
message = json.dumps(request) + "\n"
|
|
137
|
+
self.process.stdin.write(message.encode())
|
|
138
|
+
await self.process.stdin.drain()
|
|
139
|
+
|
|
140
|
+
# Read response with timeout
|
|
141
|
+
response = await asyncio.wait_for(self._read_response(), timeout=30)
|
|
142
|
+
return response
|
|
143
|
+
except asyncio.TimeoutError:
|
|
144
|
+
logger.warning(f"Timeout waiting for response from {self.server_name}")
|
|
145
|
+
return None
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Error sending request to {self.server_name}: {e}")
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
async def _send_notification(self, notification: Dict[str, Any]) -> None:
|
|
151
|
+
"""Send JSON-RPC notification (no response expected)."""
|
|
152
|
+
if not self.process or not self.process.stdin:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
message = json.dumps(notification) + "\n"
|
|
157
|
+
self.process.stdin.write(message.encode())
|
|
158
|
+
await self.process.stdin.drain()
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Error sending notification to {self.server_name}: {e}")
|
|
161
|
+
|
|
162
|
+
async def _read_response(self) -> Optional[Dict[str, Any]]:
|
|
163
|
+
"""Read a JSON-RPC response from stdout."""
|
|
164
|
+
if not self.process or not self.process.stdout:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
while True:
|
|
168
|
+
# Check if we have a complete message in buffer
|
|
169
|
+
if "\n" in self._read_buffer:
|
|
170
|
+
line, self._read_buffer = self._read_buffer.split("\n", 1)
|
|
171
|
+
if line.strip():
|
|
172
|
+
try:
|
|
173
|
+
return json.loads(line)
|
|
174
|
+
except json.JSONDecodeError:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
# Read more data
|
|
178
|
+
chunk = await self.process.stdout.read(4096)
|
|
179
|
+
if not chunk:
|
|
180
|
+
return None
|
|
181
|
+
self._read_buffer += chunk.decode()
|
|
182
|
+
|
|
183
|
+
async def close(self) -> None:
|
|
184
|
+
"""Close the server connection."""
|
|
185
|
+
if self.process:
|
|
186
|
+
try:
|
|
187
|
+
self.process.terminate()
|
|
188
|
+
await asyncio.wait_for(self.process.wait(), timeout=5)
|
|
189
|
+
except asyncio.TimeoutError:
|
|
190
|
+
self.process.kill()
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.warning(f"Error closing MCP server {self.server_name}: {e}")
|
|
193
|
+
self.process = None
|
|
194
|
+
self.initialized = False
|
|
195
|
+
logger.info(f"Closed MCP server connection: {self.server_name}")
|
|
196
|
+
|
|
197
|
+
|
|
19
198
|
class MCPIntegration:
|
|
20
199
|
"""MCP server and tool integration.
|
|
21
|
-
|
|
200
|
+
|
|
22
201
|
Manages discovery, registration, and execution of MCP tools,
|
|
23
202
|
bridging external services with the LLM core service.
|
|
203
|
+
|
|
204
|
+
Uses proper MCP JSON-RPC protocol for server communication.
|
|
24
205
|
"""
|
|
25
|
-
|
|
206
|
+
|
|
26
207
|
def __init__(self):
|
|
27
208
|
"""Initialize MCP integration."""
|
|
28
|
-
self.mcp_servers = {}
|
|
29
|
-
self.tool_registry = {}
|
|
30
|
-
self.
|
|
31
|
-
|
|
209
|
+
self.mcp_servers: Dict[str, Dict[str, Any]] = {}
|
|
210
|
+
self.tool_registry: Dict[str, Dict[str, Any]] = {}
|
|
211
|
+
self.server_connections: Dict[str, MCPServerConnection] = {}
|
|
212
|
+
|
|
32
213
|
# MCP configuration directories (local project first, then global)
|
|
33
214
|
self.local_mcp_dir = Path.cwd() / ".kollabor-cli" / "mcp"
|
|
34
215
|
self.global_mcp_dir = Path.home() / ".kollabor-cli" / "mcp"
|
|
35
|
-
|
|
216
|
+
|
|
36
217
|
# Load from both local and global configs
|
|
37
218
|
self._load_mcp_config()
|
|
38
|
-
|
|
219
|
+
|
|
39
220
|
logger.info("MCP Integration initialized")
|
|
40
221
|
|
|
41
222
|
def _load_mcp_config(self):
|
|
@@ -50,7 +231,7 @@ class MCPIntegration:
|
|
|
50
231
|
|
|
51
232
|
def _load_config_from_dir(self, config_dir: Path, config_type: str):
|
|
52
233
|
"""Load MCP config from a specific directory.
|
|
53
|
-
|
|
234
|
+
|
|
54
235
|
Args:
|
|
55
236
|
config_dir: Directory to load config from
|
|
56
237
|
config_type: Type of config (local/global) for logging
|
|
@@ -67,28 +248,97 @@ class MCPIntegration:
|
|
|
67
248
|
logger.warning(f"Failed to load {config_type} MCP config: {e}")
|
|
68
249
|
|
|
69
250
|
async def discover_mcp_servers(self) -> Dict[str, Any]:
|
|
70
|
-
"""Auto-discover available MCP servers.
|
|
71
|
-
|
|
251
|
+
"""Auto-discover available MCP servers and their tools.
|
|
252
|
+
|
|
253
|
+
Connects to each configured stdio server using MCP protocol,
|
|
254
|
+
initializes it, and queries available tools.
|
|
255
|
+
|
|
72
256
|
Returns:
|
|
73
257
|
Dictionary of discovered MCP servers and their capabilities
|
|
74
258
|
"""
|
|
75
259
|
discovered = {}
|
|
76
|
-
|
|
77
|
-
# Check for local MCP servers
|
|
260
|
+
|
|
261
|
+
# Check for local MCP servers (manifest-based)
|
|
78
262
|
await self._discover_local_servers(discovered)
|
|
79
|
-
|
|
80
|
-
#
|
|
263
|
+
|
|
264
|
+
# Connect to configured stdio servers using MCP protocol
|
|
81
265
|
for server_name, server_config in self.mcp_servers.items():
|
|
82
|
-
if
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
266
|
+
if not server_config.get("enabled", True):
|
|
267
|
+
logger.debug(f"Skipping disabled MCP server: {server_name}")
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
if server_config.get("type") == "stdio":
|
|
271
|
+
command = server_config.get("command")
|
|
272
|
+
if command:
|
|
273
|
+
tools = await self._connect_and_list_tools(server_name, command)
|
|
274
|
+
discovered[server_name] = {
|
|
275
|
+
"name": server_name,
|
|
276
|
+
"type": "stdio",
|
|
277
|
+
"tools": [t.get("name") for t in tools],
|
|
278
|
+
"tool_count": len(tools),
|
|
279
|
+
"status": "connected" if tools else "no_tools"
|
|
280
|
+
}
|
|
281
|
+
|
|
91
282
|
return discovered
|
|
283
|
+
|
|
284
|
+
async def _connect_and_list_tools(self, server_name: str, command: str) -> List[Dict[str, Any]]:
|
|
285
|
+
"""Connect to an MCP server and list its tools.
|
|
286
|
+
|
|
287
|
+
Uses proper MCP JSON-RPC protocol:
|
|
288
|
+
1. Start server process
|
|
289
|
+
2. Send initialize request
|
|
290
|
+
3. Send tools/list request
|
|
291
|
+
4. Register discovered tools
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
server_name: Name of the server
|
|
295
|
+
command: Command to start the server
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
List of tool definitions from the server
|
|
299
|
+
"""
|
|
300
|
+
# Close existing connection if any
|
|
301
|
+
if server_name in self.server_connections:
|
|
302
|
+
await self.server_connections[server_name].close()
|
|
303
|
+
|
|
304
|
+
# Create new connection
|
|
305
|
+
connection = MCPServerConnection(server_name, command)
|
|
306
|
+
|
|
307
|
+
if not await connection.connect():
|
|
308
|
+
logger.warning(f"Failed to connect to MCP server: {server_name}")
|
|
309
|
+
return []
|
|
310
|
+
|
|
311
|
+
if not await connection.initialize():
|
|
312
|
+
logger.warning(f"Failed to initialize MCP server: {server_name}")
|
|
313
|
+
await connection.close()
|
|
314
|
+
return []
|
|
315
|
+
|
|
316
|
+
# List tools
|
|
317
|
+
tools = await connection.list_tools()
|
|
318
|
+
|
|
319
|
+
# Register tools
|
|
320
|
+
for tool in tools:
|
|
321
|
+
tool_name = tool.get("name")
|
|
322
|
+
if tool_name:
|
|
323
|
+
self.tool_registry[tool_name] = {
|
|
324
|
+
"server": server_name,
|
|
325
|
+
"definition": {
|
|
326
|
+
"name": tool_name,
|
|
327
|
+
"description": tool.get("description", ""),
|
|
328
|
+
"parameters": tool.get("inputSchema", {
|
|
329
|
+
"type": "object",
|
|
330
|
+
"properties": {},
|
|
331
|
+
"required": []
|
|
332
|
+
})
|
|
333
|
+
},
|
|
334
|
+
"enabled": True
|
|
335
|
+
}
|
|
336
|
+
logger.info(f"Registered MCP tool: {tool_name} from {server_name}")
|
|
337
|
+
|
|
338
|
+
# Keep connection open for tool calls
|
|
339
|
+
self.server_connections[server_name] = connection
|
|
340
|
+
|
|
341
|
+
return tools
|
|
92
342
|
|
|
93
343
|
async def _discover_local_servers(self, discovered: Dict):
|
|
94
344
|
"""Discover locally running MCP servers."""
|
|
@@ -184,12 +434,12 @@ class MCPIntegration:
|
|
|
184
434
|
return False
|
|
185
435
|
|
|
186
436
|
async def call_mcp_tool(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
187
|
-
"""Execute an MCP tool call.
|
|
188
|
-
|
|
437
|
+
"""Execute an MCP tool call using proper MCP protocol.
|
|
438
|
+
|
|
189
439
|
Args:
|
|
190
440
|
tool_name: Name of the tool to execute
|
|
191
441
|
params: Parameters for the tool
|
|
192
|
-
|
|
442
|
+
|
|
193
443
|
Returns:
|
|
194
444
|
Tool execution result
|
|
195
445
|
"""
|
|
@@ -198,28 +448,30 @@ class MCPIntegration:
|
|
|
198
448
|
"error": f"Tool '{tool_name}' not found",
|
|
199
449
|
"available_tools": list(self.tool_registry.keys())
|
|
200
450
|
}
|
|
201
|
-
|
|
451
|
+
|
|
202
452
|
tool_info = self.tool_registry[tool_name]
|
|
203
453
|
server_name = tool_info["server"]
|
|
204
|
-
|
|
454
|
+
|
|
205
455
|
if not tool_info["enabled"]:
|
|
206
456
|
return {"error": f"Tool '{tool_name}' is disabled"}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
457
|
+
|
|
458
|
+
# Get active connection
|
|
459
|
+
connection = self.server_connections.get(server_name)
|
|
460
|
+
if not connection or not connection.initialized:
|
|
461
|
+
# Try to reconnect
|
|
210
462
|
server_config = self.mcp_servers.get(server_name, {})
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
463
|
+
command = server_config.get("command")
|
|
464
|
+
if command:
|
|
465
|
+
await self._connect_and_list_tools(server_name, command)
|
|
466
|
+
connection = self.server_connections.get(server_name)
|
|
467
|
+
|
|
468
|
+
if not connection or not connection.initialized:
|
|
469
|
+
return {"error": f"No active connection to server '{server_name}'"}
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
result = await connection.call_tool(tool_name, params)
|
|
220
473
|
logger.info(f"Executed MCP tool: {tool_name}")
|
|
221
474
|
return result
|
|
222
|
-
|
|
223
475
|
except Exception as e:
|
|
224
476
|
logger.error(f"Failed to execute MCP tool {tool_name}: {e}")
|
|
225
477
|
return {"error": str(e)}
|
|
@@ -257,8 +509,9 @@ class MCPIntegration:
|
|
|
257
509
|
# Add parameters as JSON input
|
|
258
510
|
input_json = json.dumps(params)
|
|
259
511
|
|
|
260
|
-
|
|
261
|
-
|
|
512
|
+
def run_subprocess():
|
|
513
|
+
"""Run subprocess in thread to avoid blocking event loop."""
|
|
514
|
+
return subprocess.run(
|
|
262
515
|
command_parts,
|
|
263
516
|
shell=False,
|
|
264
517
|
input=input_json,
|
|
@@ -266,7 +519,12 @@ class MCPIntegration:
|
|
|
266
519
|
text=True,
|
|
267
520
|
timeout=30
|
|
268
521
|
)
|
|
269
|
-
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
# Run blocking subprocess in executor to not freeze event loop
|
|
525
|
+
loop = asyncio.get_event_loop()
|
|
526
|
+
result = await loop.run_in_executor(None, run_subprocess)
|
|
527
|
+
|
|
270
528
|
if result.returncode == 0:
|
|
271
529
|
# Try to parse JSON output
|
|
272
530
|
try:
|
|
@@ -275,7 +533,7 @@ class MCPIntegration:
|
|
|
275
533
|
return {"output": result.stdout}
|
|
276
534
|
else:
|
|
277
535
|
return {"error": result.stderr or f"Tool exited with code {result.returncode}"}
|
|
278
|
-
|
|
536
|
+
|
|
279
537
|
except subprocess.TimeoutExpired:
|
|
280
538
|
return {"error": "Tool execution timed out"}
|
|
281
539
|
except Exception as e:
|
|
@@ -327,7 +585,7 @@ class MCPIntegration:
|
|
|
327
585
|
|
|
328
586
|
def list_available_tools(self) -> List[Dict[str, Any]]:
|
|
329
587
|
"""List all available MCP tools.
|
|
330
|
-
|
|
588
|
+
|
|
331
589
|
Returns:
|
|
332
590
|
List of available tools with their information
|
|
333
591
|
"""
|
|
@@ -340,6 +598,233 @@ class MCPIntegration:
|
|
|
340
598
|
"definition": tool_info.get("definition", {})
|
|
341
599
|
})
|
|
342
600
|
return tools
|
|
601
|
+
|
|
602
|
+
def get_tool_definitions_for_api(self) -> List[Dict[str, Any]]:
|
|
603
|
+
"""Convert registered MCP tools to API tool schema format.
|
|
604
|
+
|
|
605
|
+
Returns generic format that adapters (OpenAI/Anthropic) auto-convert:
|
|
606
|
+
- OpenAI wraps in: {type: "function", function: {...}}
|
|
607
|
+
- Anthropic uses: {name, description, input_schema}
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
List of tool definitions in generic API format
|
|
611
|
+
"""
|
|
612
|
+
tools = []
|
|
613
|
+
|
|
614
|
+
# Add MCP tools from registry
|
|
615
|
+
for tool_name, tool_info in self.tool_registry.items():
|
|
616
|
+
if not tool_info.get("enabled", True):
|
|
617
|
+
continue
|
|
618
|
+
|
|
619
|
+
definition = tool_info.get("definition", {})
|
|
620
|
+
tools.append({
|
|
621
|
+
"name": tool_name,
|
|
622
|
+
"description": definition.get("description", f"MCP tool: {tool_name}"),
|
|
623
|
+
"parameters": definition.get("parameters", definition.get("inputSchema", {
|
|
624
|
+
"type": "object",
|
|
625
|
+
"properties": {},
|
|
626
|
+
"required": []
|
|
627
|
+
}))
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
# Add built-in file operation tools
|
|
631
|
+
tools.extend(self._get_file_operation_tools())
|
|
632
|
+
|
|
633
|
+
logger.debug(f"Prepared {len(tools)} tools for API ({len(self.tool_registry)} MCP + file ops)")
|
|
634
|
+
return tools
|
|
635
|
+
|
|
636
|
+
def _get_file_operation_tools(self) -> List[Dict[str, Any]]:
|
|
637
|
+
"""Get built-in file operation tool definitions.
|
|
638
|
+
|
|
639
|
+
These tools provide file manipulation capabilities that work alongside
|
|
640
|
+
MCP tools. They match the XML-based file operations but as native functions.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
List of file operation tool definitions
|
|
644
|
+
"""
|
|
645
|
+
return [
|
|
646
|
+
{
|
|
647
|
+
"name": "file_read",
|
|
648
|
+
"description": "Read content from a file. Use this to examine existing files before editing.",
|
|
649
|
+
"parameters": {
|
|
650
|
+
"type": "object",
|
|
651
|
+
"properties": {
|
|
652
|
+
"file": {"type": "string", "description": "Relative file path to read"},
|
|
653
|
+
"offset": {"type": "integer", "description": "Line offset to start reading from (0-indexed, optional)"},
|
|
654
|
+
"limit": {"type": "integer", "description": "Number of lines to read (optional)"}
|
|
655
|
+
},
|
|
656
|
+
"required": ["file"]
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
"name": "file_create",
|
|
661
|
+
"description": "Create a new file with content. Fails if file already exists.",
|
|
662
|
+
"parameters": {
|
|
663
|
+
"type": "object",
|
|
664
|
+
"properties": {
|
|
665
|
+
"file": {"type": "string", "description": "Relative file path to create"},
|
|
666
|
+
"content": {"type": "string", "description": "Content to write to the file"}
|
|
667
|
+
},
|
|
668
|
+
"required": ["file", "content"]
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
"name": "file_create_overwrite",
|
|
673
|
+
"description": "Create or overwrite a file with content. Creates backup if file exists.",
|
|
674
|
+
"parameters": {
|
|
675
|
+
"type": "object",
|
|
676
|
+
"properties": {
|
|
677
|
+
"file": {"type": "string", "description": "Relative file path to create/overwrite"},
|
|
678
|
+
"content": {"type": "string", "description": "Content to write to the file"}
|
|
679
|
+
},
|
|
680
|
+
"required": ["file", "content"]
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
"name": "file_edit",
|
|
685
|
+
"description": "Find and replace text in a file. Replaces ALL occurrences of the pattern.",
|
|
686
|
+
"parameters": {
|
|
687
|
+
"type": "object",
|
|
688
|
+
"properties": {
|
|
689
|
+
"file": {"type": "string", "description": "Relative file path to edit"},
|
|
690
|
+
"find": {"type": "string", "description": "Text pattern to find (exact match)"},
|
|
691
|
+
"replace": {"type": "string", "description": "Text to replace with"}
|
|
692
|
+
},
|
|
693
|
+
"required": ["file", "find", "replace"]
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
"name": "file_append",
|
|
698
|
+
"description": "Append content to the end of a file.",
|
|
699
|
+
"parameters": {
|
|
700
|
+
"type": "object",
|
|
701
|
+
"properties": {
|
|
702
|
+
"file": {"type": "string", "description": "Relative file path to append to"},
|
|
703
|
+
"content": {"type": "string", "description": "Content to append"}
|
|
704
|
+
},
|
|
705
|
+
"required": ["file", "content"]
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
"name": "file_insert_after",
|
|
710
|
+
"description": "Insert content after a pattern in a file. Pattern must match exactly.",
|
|
711
|
+
"parameters": {
|
|
712
|
+
"type": "object",
|
|
713
|
+
"properties": {
|
|
714
|
+
"file": {"type": "string", "description": "Relative file path"},
|
|
715
|
+
"pattern": {"type": "string", "description": "Pattern to find (exact match)"},
|
|
716
|
+
"content": {"type": "string", "description": "Content to insert after pattern"}
|
|
717
|
+
},
|
|
718
|
+
"required": ["file", "pattern", "content"]
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
"name": "file_insert_before",
|
|
723
|
+
"description": "Insert content before a pattern in a file. Pattern must match exactly.",
|
|
724
|
+
"parameters": {
|
|
725
|
+
"type": "object",
|
|
726
|
+
"properties": {
|
|
727
|
+
"file": {"type": "string", "description": "Relative file path"},
|
|
728
|
+
"pattern": {"type": "string", "description": "Pattern to find (exact match)"},
|
|
729
|
+
"content": {"type": "string", "description": "Content to insert before pattern"}
|
|
730
|
+
},
|
|
731
|
+
"required": ["file", "pattern", "content"]
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
"name": "file_delete",
|
|
736
|
+
"description": "Delete a file. Creates backup before deletion.",
|
|
737
|
+
"parameters": {
|
|
738
|
+
"type": "object",
|
|
739
|
+
"properties": {
|
|
740
|
+
"file": {"type": "string", "description": "Relative file path to delete"}
|
|
741
|
+
},
|
|
742
|
+
"required": ["file"]
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
"name": "file_move",
|
|
747
|
+
"description": "Move or rename a file.",
|
|
748
|
+
"parameters": {
|
|
749
|
+
"type": "object",
|
|
750
|
+
"properties": {
|
|
751
|
+
"from": {"type": "string", "description": "Source file path"},
|
|
752
|
+
"to": {"type": "string", "description": "Destination file path"}
|
|
753
|
+
},
|
|
754
|
+
"required": ["from", "to"]
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
"name": "file_copy",
|
|
759
|
+
"description": "Copy a file. Fails if destination exists.",
|
|
760
|
+
"parameters": {
|
|
761
|
+
"type": "object",
|
|
762
|
+
"properties": {
|
|
763
|
+
"from": {"type": "string", "description": "Source file path"},
|
|
764
|
+
"to": {"type": "string", "description": "Destination file path"}
|
|
765
|
+
},
|
|
766
|
+
"required": ["from", "to"]
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
"name": "file_copy_overwrite",
|
|
771
|
+
"description": "Copy a file, overwriting destination if it exists.",
|
|
772
|
+
"parameters": {
|
|
773
|
+
"type": "object",
|
|
774
|
+
"properties": {
|
|
775
|
+
"from": {"type": "string", "description": "Source file path"},
|
|
776
|
+
"to": {"type": "string", "description": "Destination file path"}
|
|
777
|
+
},
|
|
778
|
+
"required": ["from", "to"]
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
"name": "file_mkdir",
|
|
783
|
+
"description": "Create a directory (including parent directories).",
|
|
784
|
+
"parameters": {
|
|
785
|
+
"type": "object",
|
|
786
|
+
"properties": {
|
|
787
|
+
"path": {"type": "string", "description": "Directory path to create"}
|
|
788
|
+
},
|
|
789
|
+
"required": ["path"]
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
"name": "file_rmdir",
|
|
794
|
+
"description": "Remove an empty directory.",
|
|
795
|
+
"parameters": {
|
|
796
|
+
"type": "object",
|
|
797
|
+
"properties": {
|
|
798
|
+
"path": {"type": "string", "description": "Directory path to remove"}
|
|
799
|
+
},
|
|
800
|
+
"required": ["path"]
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
"name": "file_grep",
|
|
805
|
+
"description": "Search for a pattern in a file and return matching lines.",
|
|
806
|
+
"parameters": {
|
|
807
|
+
"type": "object",
|
|
808
|
+
"properties": {
|
|
809
|
+
"file": {"type": "string", "description": "Relative file path to search"},
|
|
810
|
+
"pattern": {"type": "string", "description": "Text pattern to search for"},
|
|
811
|
+
"case_insensitive": {"type": "boolean", "description": "Whether to ignore case (default: false)"}
|
|
812
|
+
},
|
|
813
|
+
"required": ["file", "pattern"]
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
"name": "terminal",
|
|
818
|
+
"description": "Execute a terminal/shell command and return the output.",
|
|
819
|
+
"parameters": {
|
|
820
|
+
"type": "object",
|
|
821
|
+
"properties": {
|
|
822
|
+
"command": {"type": "string", "description": "Shell command to execute"}
|
|
823
|
+
},
|
|
824
|
+
"required": ["command"]
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
]
|
|
343
828
|
|
|
344
829
|
def enable_tool(self, tool_name: str) -> bool:
|
|
345
830
|
"""Enable an MCP tool.
|
|
@@ -372,15 +857,14 @@ class MCPIntegration:
|
|
|
372
857
|
return False
|
|
373
858
|
|
|
374
859
|
async def shutdown(self):
|
|
375
|
-
"""Shutdown MCP integration and close connections."""
|
|
376
|
-
|
|
377
|
-
for connection_id, connection in self.active_connections.items():
|
|
860
|
+
"""Shutdown MCP integration and close all server connections."""
|
|
861
|
+
for server_name, connection in self.server_connections.items():
|
|
378
862
|
try:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
logger.debug(f"Closed MCP connection: {connection_id}")
|
|
863
|
+
await connection.close()
|
|
864
|
+
logger.debug(f"Closed MCP server connection: {server_name}")
|
|
382
865
|
except Exception as e:
|
|
383
|
-
logger.warning(f"Error closing MCP connection {
|
|
384
|
-
|
|
385
|
-
self.
|
|
866
|
+
logger.warning(f"Error closing MCP connection {server_name}: {e}")
|
|
867
|
+
|
|
868
|
+
self.server_connections.clear()
|
|
869
|
+
self.tool_registry.clear()
|
|
386
870
|
logger.info("MCP Integration shutdown complete")
|