pomera-ai-commander 0.1.0 → 1.2.1
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.
- package/LICENSE +21 -21
- package/README.md +105 -680
- package/bin/pomera-ai-commander.js +62 -62
- package/core/__init__.py +65 -65
- package/core/app_context.py +482 -482
- package/core/async_text_processor.py +421 -421
- package/core/backup_manager.py +655 -655
- package/core/backup_recovery_manager.py +1033 -1033
- package/core/content_hash_cache.py +508 -508
- package/core/context_menu.py +313 -313
- package/core/data_validator.py +1066 -1066
- package/core/database_connection_manager.py +744 -744
- package/core/database_curl_settings_manager.py +608 -608
- package/core/database_promera_ai_settings_manager.py +446 -446
- package/core/database_schema.py +411 -411
- package/core/database_schema_manager.py +395 -395
- package/core/database_settings_manager.py +1507 -1507
- package/core/database_settings_manager_interface.py +456 -456
- package/core/dialog_manager.py +734 -734
- package/core/efficient_line_numbers.py +510 -510
- package/core/error_handler.py +746 -746
- package/core/error_service.py +431 -431
- package/core/event_consolidator.py +511 -511
- package/core/mcp/__init__.py +43 -43
- package/core/mcp/protocol.py +288 -288
- package/core/mcp/schema.py +251 -251
- package/core/mcp/server_stdio.py +299 -299
- package/core/mcp/tool_registry.py +2372 -2345
- package/core/memory_efficient_text_widget.py +711 -711
- package/core/migration_manager.py +914 -914
- package/core/migration_test_suite.py +1085 -1085
- package/core/migration_validator.py +1143 -1143
- package/core/optimized_find_replace.py +714 -714
- package/core/optimized_pattern_engine.py +424 -424
- package/core/optimized_search_highlighter.py +552 -552
- package/core/performance_monitor.py +674 -674
- package/core/persistence_manager.py +712 -712
- package/core/progressive_stats_calculator.py +632 -632
- package/core/regex_pattern_cache.py +529 -529
- package/core/regex_pattern_library.py +350 -350
- package/core/search_operation_manager.py +434 -434
- package/core/settings_defaults_registry.py +1087 -1087
- package/core/settings_integrity_validator.py +1111 -1111
- package/core/settings_serializer.py +557 -557
- package/core/settings_validator.py +1823 -1823
- package/core/smart_stats_calculator.py +709 -709
- package/core/statistics_update_manager.py +619 -619
- package/core/stats_config_manager.py +858 -858
- package/core/streaming_text_handler.py +723 -723
- package/core/task_scheduler.py +596 -596
- package/core/update_pattern_library.py +168 -168
- package/core/visibility_monitor.py +596 -596
- package/core/widget_cache.py +498 -498
- package/mcp.json +51 -61
- package/package.json +61 -57
- package/pomera.py +7482 -7482
- package/pomera_mcp_server.py +183 -144
- package/requirements.txt +32 -0
- package/tools/__init__.py +4 -4
- package/tools/ai_tools.py +2891 -2891
- package/tools/ascii_art_generator.py +352 -352
- package/tools/base64_tools.py +183 -183
- package/tools/base_tool.py +511 -511
- package/tools/case_tool.py +308 -308
- package/tools/column_tools.py +395 -395
- package/tools/cron_tool.py +884 -884
- package/tools/curl_history.py +600 -600
- package/tools/curl_processor.py +1207 -1207
- package/tools/curl_settings.py +502 -502
- package/tools/curl_tool.py +5467 -5467
- package/tools/diff_viewer.py +1071 -1071
- package/tools/email_extraction_tool.py +248 -248
- package/tools/email_header_analyzer.py +425 -425
- package/tools/extraction_tools.py +250 -250
- package/tools/find_replace.py +1750 -1750
- package/tools/folder_file_reporter.py +1463 -1463
- package/tools/folder_file_reporter_adapter.py +480 -480
- package/tools/generator_tools.py +1216 -1216
- package/tools/hash_generator.py +255 -255
- package/tools/html_tool.py +656 -656
- package/tools/jsonxml_tool.py +729 -729
- package/tools/line_tools.py +419 -419
- package/tools/markdown_tools.py +561 -561
- package/tools/mcp_widget.py +1417 -1417
- package/tools/notes_widget.py +973 -973
- package/tools/number_base_converter.py +372 -372
- package/tools/regex_extractor.py +571 -571
- package/tools/slug_generator.py +310 -310
- package/tools/sorter_tools.py +458 -458
- package/tools/string_escape_tool.py +392 -392
- package/tools/text_statistics_tool.py +365 -365
- package/tools/text_wrapper.py +430 -430
- package/tools/timestamp_converter.py +421 -421
- package/tools/tool_loader.py +710 -710
- package/tools/translator_tools.py +522 -522
- package/tools/url_link_extractor.py +261 -261
- package/tools/url_parser.py +204 -204
- package/tools/whitespace_tools.py +355 -355
- package/tools/word_frequency_counter.py +146 -146
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/app_context.cpython-313.pyc +0 -0
- package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
- package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
- package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
- package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/error_service.cpython-313.pyc +0 -0
- package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
- package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
- package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
- package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
- package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
- package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
- package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
- package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
- package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
- package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
- package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
- package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
package/tools/mcp_widget.py
CHANGED
|
@@ -1,1417 +1,1417 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MCP Manager Widget - UI for MCP Server Control
|
|
3
|
-
|
|
4
|
-
This module provides a graphical interface for managing the MCP server,
|
|
5
|
-
allowing users to start/stop the server and monitor its status.
|
|
6
|
-
|
|
7
|
-
Features:
|
|
8
|
-
- Start/Stop MCP server (as detached background process)
|
|
9
|
-
- Detect running server on startup
|
|
10
|
-
- View server status and connection info
|
|
11
|
-
- View available tools
|
|
12
|
-
- Server log display
|
|
13
|
-
- Dynamic path configuration for Claude Desktop/Cursor
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import tkinter as tk
|
|
17
|
-
from tkinter import ttk, scrolledtext, messagebox
|
|
18
|
-
import threading
|
|
19
|
-
import queue
|
|
20
|
-
import sys
|
|
21
|
-
import os
|
|
22
|
-
import json
|
|
23
|
-
import logging
|
|
24
|
-
import subprocess
|
|
25
|
-
import signal
|
|
26
|
-
import atexit
|
|
27
|
-
from datetime import datetime
|
|
28
|
-
from typing import Optional, Callable
|
|
29
|
-
|
|
30
|
-
# Add project root to path
|
|
31
|
-
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
32
|
-
if PROJECT_ROOT not in sys.path:
|
|
33
|
-
sys.path.insert(0, PROJECT_ROOT)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# PID file for tracking server process
|
|
37
|
-
PID_FILE = os.path.join(PROJECT_ROOT, ".mcp_server.pid")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def get_pomera_executable_path() -> tuple:
|
|
41
|
-
"""
|
|
42
|
-
Get the path to the Pomera executable/script for MCP server mode.
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
Tuple of (command, args) for running the MCP server.
|
|
46
|
-
For frozen exe: ("pomera.exe", ["--mcp-server"])
|
|
47
|
-
For Python: ("python", ["pomera.py", "--mcp-server"])
|
|
48
|
-
"""
|
|
49
|
-
if getattr(sys, 'frozen', False):
|
|
50
|
-
# Running as compiled executable
|
|
51
|
-
exe_path = sys.executable
|
|
52
|
-
return (exe_path, ["--mcp-server"])
|
|
53
|
-
else:
|
|
54
|
-
# Running as Python script
|
|
55
|
-
pomera_script = os.path.join(PROJECT_ROOT, "pomera.py")
|
|
56
|
-
return (sys.executable, [pomera_script, "--mcp-server"])
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def get_mcp_config_json() -> str:
|
|
60
|
-
"""
|
|
61
|
-
Generate MCP configuration JSON with dynamic path.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
JSON string for claude_desktop_config.json or .cursor/mcp.json
|
|
65
|
-
"""
|
|
66
|
-
command, args = get_pomera_executable_path()
|
|
67
|
-
|
|
68
|
-
# Format path for JSON (use forward slashes)
|
|
69
|
-
command = command.replace("\\", "/")
|
|
70
|
-
args = [arg.replace("\\", "/") for arg in args]
|
|
71
|
-
|
|
72
|
-
config = {
|
|
73
|
-
"mcpServers": {
|
|
74
|
-
"pomera": {
|
|
75
|
-
"command": command,
|
|
76
|
-
"args": args
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return json.dumps(config, indent=2)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def read_pid_file() -> Optional[int]:
|
|
84
|
-
"""Read the PID from the PID file if it exists."""
|
|
85
|
-
try:
|
|
86
|
-
if os.path.exists(PID_FILE):
|
|
87
|
-
with open(PID_FILE, 'r') as f:
|
|
88
|
-
return int(f.read().strip())
|
|
89
|
-
except (ValueError, IOError):
|
|
90
|
-
pass
|
|
91
|
-
return None
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def write_pid_file(pid: int):
|
|
95
|
-
"""Write the PID to the PID file."""
|
|
96
|
-
try:
|
|
97
|
-
with open(PID_FILE, 'w') as f:
|
|
98
|
-
f.write(str(pid))
|
|
99
|
-
except IOError as e:
|
|
100
|
-
logging.getLogger(__name__).error(f"Failed to write PID file: {e}")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def remove_pid_file():
|
|
104
|
-
"""Remove the PID file."""
|
|
105
|
-
try:
|
|
106
|
-
if os.path.exists(PID_FILE):
|
|
107
|
-
os.remove(PID_FILE)
|
|
108
|
-
except IOError:
|
|
109
|
-
pass
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def is_process_running(pid: int) -> bool:
|
|
113
|
-
"""Check if a process with the given PID is running."""
|
|
114
|
-
if pid is None:
|
|
115
|
-
return False
|
|
116
|
-
try:
|
|
117
|
-
if sys.platform == "win32":
|
|
118
|
-
# Windows: use tasklist
|
|
119
|
-
result = subprocess.run(
|
|
120
|
-
["tasklist", "/FI", f"PID eq {pid}"],
|
|
121
|
-
capture_output=True,
|
|
122
|
-
text=True
|
|
123
|
-
)
|
|
124
|
-
return str(pid) in result.stdout
|
|
125
|
-
else:
|
|
126
|
-
# Unix: send signal 0 to check if process exists
|
|
127
|
-
os.kill(pid, 0)
|
|
128
|
-
return True
|
|
129
|
-
except (OSError, subprocess.SubprocessError):
|
|
130
|
-
return False
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def find_running_mcp_server() -> Optional[int]:
|
|
134
|
-
"""
|
|
135
|
-
Find a running MCP server process.
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
PID of running server, or None if not found
|
|
139
|
-
"""
|
|
140
|
-
# First check PID file
|
|
141
|
-
pid = read_pid_file()
|
|
142
|
-
if pid and is_process_running(pid):
|
|
143
|
-
return pid
|
|
144
|
-
|
|
145
|
-
# Clean up stale PID file
|
|
146
|
-
remove_pid_file()
|
|
147
|
-
return None
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
class MCPServerProcess:
|
|
151
|
-
"""Manages the MCP server as a detached subprocess that persists after app close."""
|
|
152
|
-
|
|
153
|
-
def __init__(self, log_queue: queue.Queue, on_started: Callable, on_stopped: Callable):
|
|
154
|
-
self.log_queue = log_queue
|
|
155
|
-
self.on_started = on_started
|
|
156
|
-
self.on_stopped = on_stopped
|
|
157
|
-
self.process: Optional[subprocess.Popen] = None
|
|
158
|
-
self.monitor_thread: Optional[threading.Thread] = None
|
|
159
|
-
self._stopping = False
|
|
160
|
-
self._external_pid: Optional[int] = None # PID of externally started server
|
|
161
|
-
|
|
162
|
-
def check_existing_server(self) -> bool:
|
|
163
|
-
"""
|
|
164
|
-
Check if a server is already running.
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
True if server is running, False otherwise
|
|
168
|
-
"""
|
|
169
|
-
pid = find_running_mcp_server()
|
|
170
|
-
if pid:
|
|
171
|
-
self._external_pid = pid
|
|
172
|
-
self.log_queue.put(("INFO", f"Found existing MCP server (PID: {pid})"))
|
|
173
|
-
return True
|
|
174
|
-
return False
|
|
175
|
-
|
|
176
|
-
def start(self, detached: bool = False) -> bool:
|
|
177
|
-
"""
|
|
178
|
-
Start the MCP server as a subprocess for testing.
|
|
179
|
-
|
|
180
|
-
Args:
|
|
181
|
-
detached: If True, server runs with minimal parent connection.
|
|
182
|
-
Note: stdio MCP servers are designed to be started by MCP clients
|
|
183
|
-
(Claude Desktop, Cursor). This test mode captures logs for debugging.
|
|
184
|
-
"""
|
|
185
|
-
# Check if already running
|
|
186
|
-
if self.is_running():
|
|
187
|
-
self.log_queue.put(("WARNING", "Server is already running"))
|
|
188
|
-
return False
|
|
189
|
-
|
|
190
|
-
try:
|
|
191
|
-
command, args = get_pomera_executable_path()
|
|
192
|
-
full_args = [command] + args + ["--debug"]
|
|
193
|
-
|
|
194
|
-
self.log_queue.put(("INFO", f"Starting MCP server: {' '.join(full_args)}"))
|
|
195
|
-
|
|
196
|
-
# Platform-specific process creation
|
|
197
|
-
# Note: We always capture stderr for logging, but stdin/stdout are for MCP protocol
|
|
198
|
-
if sys.platform == "win32":
|
|
199
|
-
creation_flags = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
200
|
-
if detached:
|
|
201
|
-
creation_flags |= subprocess.CREATE_NO_WINDOW
|
|
202
|
-
|
|
203
|
-
self.process = subprocess.Popen(
|
|
204
|
-
full_args,
|
|
205
|
-
stdout=subprocess.PIPE, # Capture MCP responses for logging
|
|
206
|
-
stderr=subprocess.PIPE, # Capture debug logs
|
|
207
|
-
stdin=subprocess.PIPE, # For sending MCP requests (testing)
|
|
208
|
-
creationflags=creation_flags,
|
|
209
|
-
text=True,
|
|
210
|
-
bufsize=1
|
|
211
|
-
)
|
|
212
|
-
else:
|
|
213
|
-
# Unix
|
|
214
|
-
self.process = subprocess.Popen(
|
|
215
|
-
full_args,
|
|
216
|
-
stdout=subprocess.PIPE,
|
|
217
|
-
stderr=subprocess.PIPE,
|
|
218
|
-
stdin=subprocess.PIPE,
|
|
219
|
-
text=True,
|
|
220
|
-
bufsize=1,
|
|
221
|
-
start_new_session=detached
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
self._stopping = False
|
|
225
|
-
self._external_pid = None
|
|
226
|
-
|
|
227
|
-
# Write PID file for tracking
|
|
228
|
-
write_pid_file(self.process.pid)
|
|
229
|
-
|
|
230
|
-
# Start monitoring thread to capture stderr logs
|
|
231
|
-
if self.process.stderr:
|
|
232
|
-
self.monitor_thread = threading.Thread(target=self._monitor_process, daemon=True)
|
|
233
|
-
self.monitor_thread.start()
|
|
234
|
-
|
|
235
|
-
self.log_queue.put(("INFO", f"Server started (PID: {self.process.pid})"))
|
|
236
|
-
self.log_queue.put(("INFO", "Server is running in test mode - logs will be captured"))
|
|
237
|
-
self.log_queue.put(("INFO", "Note: For production use, configure your MCP client to start the server"))
|
|
238
|
-
|
|
239
|
-
self.on_started()
|
|
240
|
-
return True
|
|
241
|
-
|
|
242
|
-
except Exception as e:
|
|
243
|
-
self.log_queue.put(("ERROR", f"Failed to start server: {str(e)}"))
|
|
244
|
-
return False
|
|
245
|
-
|
|
246
|
-
def stop(self):
|
|
247
|
-
"""Stop the MCP server subprocess."""
|
|
248
|
-
pid_to_stop = None
|
|
249
|
-
|
|
250
|
-
if self.process and self.process.poll() is None:
|
|
251
|
-
pid_to_stop = self.process.pid
|
|
252
|
-
elif self._external_pid and is_process_running(self._external_pid):
|
|
253
|
-
pid_to_stop = self._external_pid
|
|
254
|
-
|
|
255
|
-
if not pid_to_stop:
|
|
256
|
-
self.log_queue.put(("WARNING", "No server process to stop"))
|
|
257
|
-
return
|
|
258
|
-
|
|
259
|
-
self._stopping = True
|
|
260
|
-
self.log_queue.put(("INFO", f"Stopping MCP server (PID: {pid_to_stop})..."))
|
|
261
|
-
|
|
262
|
-
try:
|
|
263
|
-
if sys.platform == "win32":
|
|
264
|
-
# Windows: use taskkill
|
|
265
|
-
subprocess.run(["taskkill", "/F", "/PID", str(pid_to_stop)],
|
|
266
|
-
capture_output=True)
|
|
267
|
-
else:
|
|
268
|
-
# Unix: send SIGTERM then SIGKILL
|
|
269
|
-
os.kill(pid_to_stop, signal.SIGTERM)
|
|
270
|
-
# Give it a moment to terminate
|
|
271
|
-
import time
|
|
272
|
-
time.sleep(1)
|
|
273
|
-
if is_process_running(pid_to_stop):
|
|
274
|
-
os.kill(pid_to_stop, signal.SIGKILL)
|
|
275
|
-
|
|
276
|
-
self.log_queue.put(("INFO", "Server stopped"))
|
|
277
|
-
except Exception as e:
|
|
278
|
-
self.log_queue.put(("ERROR", f"Error stopping server: {str(e)}"))
|
|
279
|
-
finally:
|
|
280
|
-
self.process = None
|
|
281
|
-
self._external_pid = None
|
|
282
|
-
remove_pid_file()
|
|
283
|
-
self.on_stopped()
|
|
284
|
-
|
|
285
|
-
def _monitor_process(self):
|
|
286
|
-
"""Monitor the server process and capture output."""
|
|
287
|
-
if not self.process or not self.process.stderr:
|
|
288
|
-
return
|
|
289
|
-
|
|
290
|
-
# Read stderr for log messages
|
|
291
|
-
try:
|
|
292
|
-
for line in self.process.stderr:
|
|
293
|
-
if self._stopping:
|
|
294
|
-
break
|
|
295
|
-
line = line.strip()
|
|
296
|
-
if line:
|
|
297
|
-
# Parse log level from line if present
|
|
298
|
-
# Format: "2024-12-06 15:33:17,123 - module - LEVEL - message"
|
|
299
|
-
if " - CRITICAL - " in line:
|
|
300
|
-
self.log_queue.put(("CRITICAL", line))
|
|
301
|
-
elif " - ERROR - " in line:
|
|
302
|
-
self.log_queue.put(("ERROR", line))
|
|
303
|
-
elif " - WARNING - " in line:
|
|
304
|
-
self.log_queue.put(("WARNING", line))
|
|
305
|
-
elif " - DEBUG - " in line:
|
|
306
|
-
self.log_queue.put(("DEBUG", line))
|
|
307
|
-
elif " - INFO - " in line:
|
|
308
|
-
self.log_queue.put(("INFO", line))
|
|
309
|
-
else:
|
|
310
|
-
# Default to INFO for unrecognized format
|
|
311
|
-
self.log_queue.put(("INFO", line))
|
|
312
|
-
except Exception as e:
|
|
313
|
-
self.log_queue.put(("ERROR", f"Error reading server output: {e}"))
|
|
314
|
-
|
|
315
|
-
# Check if process ended unexpectedly
|
|
316
|
-
if self.process and not self._stopping:
|
|
317
|
-
return_code = self.process.poll()
|
|
318
|
-
if return_code is not None:
|
|
319
|
-
self.log_queue.put(("WARNING", f"Server process ended (exit code: {return_code})"))
|
|
320
|
-
remove_pid_file()
|
|
321
|
-
self.on_stopped()
|
|
322
|
-
|
|
323
|
-
def is_running(self) -> bool:
|
|
324
|
-
"""Check if the server is running (either our process or external)."""
|
|
325
|
-
if self.process is not None and self.process.poll() is None:
|
|
326
|
-
return True
|
|
327
|
-
if self._external_pid and is_process_running(self._external_pid):
|
|
328
|
-
return True
|
|
329
|
-
# Also check PID file
|
|
330
|
-
pid = find_running_mcp_server()
|
|
331
|
-
if pid:
|
|
332
|
-
self._external_pid = pid
|
|
333
|
-
return True
|
|
334
|
-
return False
|
|
335
|
-
|
|
336
|
-
def get_pid(self) -> Optional[int]:
|
|
337
|
-
"""Get the PID of the running server."""
|
|
338
|
-
if self.process and self.process.poll() is None:
|
|
339
|
-
return self.process.pid
|
|
340
|
-
if self._external_pid and is_process_running(self._external_pid):
|
|
341
|
-
return self._external_pid
|
|
342
|
-
return find_running_mcp_server()
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
class MCPManagerWidget(ttk.Frame):
|
|
346
|
-
"""
|
|
347
|
-
MCP Manager Widget for controlling the MCP server.
|
|
348
|
-
|
|
349
|
-
Provides UI for:
|
|
350
|
-
- Starting/stopping the server
|
|
351
|
-
- Viewing server status
|
|
352
|
-
- Viewing available tools
|
|
353
|
-
- Displaying server logs
|
|
354
|
-
- Configuration help
|
|
355
|
-
"""
|
|
356
|
-
|
|
357
|
-
def __init__(self, parent, app):
|
|
358
|
-
super().__init__(parent)
|
|
359
|
-
self.app = app
|
|
360
|
-
self.logger = app.logger if hasattr(app, 'logger') else logging.getLogger(__name__)
|
|
361
|
-
|
|
362
|
-
# Server state
|
|
363
|
-
self.server_process: Optional[MCPServerProcess] = None
|
|
364
|
-
self.server_running = False
|
|
365
|
-
self.log_queue = queue.Queue()
|
|
366
|
-
|
|
367
|
-
# Tool registry for display
|
|
368
|
-
self.registry = None
|
|
369
|
-
self._load_registry()
|
|
370
|
-
|
|
371
|
-
self.create_widgets()
|
|
372
|
-
self.start_log_polling()
|
|
373
|
-
|
|
374
|
-
# Check for existing server after UI is ready
|
|
375
|
-
self.after(500, self.check_existing_server)
|
|
376
|
-
|
|
377
|
-
def _load_registry(self):
|
|
378
|
-
"""Load tool registry for display purposes."""
|
|
379
|
-
try:
|
|
380
|
-
from core.mcp.tool_registry import ToolRegistry
|
|
381
|
-
self.registry = ToolRegistry()
|
|
382
|
-
except Exception as e:
|
|
383
|
-
self.logger.error(f"Failed to load tool registry: {e}")
|
|
384
|
-
self.registry = None
|
|
385
|
-
|
|
386
|
-
def create_widgets(self):
|
|
387
|
-
"""Create the widget interface."""
|
|
388
|
-
# Main container with notebook
|
|
389
|
-
self.notebook = ttk.Notebook(self)
|
|
390
|
-
self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
391
|
-
|
|
392
|
-
# Server Control Tab
|
|
393
|
-
self.control_frame = ttk.Frame(self.notebook)
|
|
394
|
-
self.notebook.add(self.control_frame, text="Server Control")
|
|
395
|
-
self.create_control_tab()
|
|
396
|
-
|
|
397
|
-
# Tools Tab
|
|
398
|
-
self.tools_frame = ttk.Frame(self.notebook)
|
|
399
|
-
self.notebook.add(self.tools_frame, text="Available Tools")
|
|
400
|
-
self.create_tools_tab()
|
|
401
|
-
|
|
402
|
-
# Configuration Tab
|
|
403
|
-
self.config_frame = ttk.Frame(self.notebook)
|
|
404
|
-
self.notebook.add(self.config_frame, text="Configuration")
|
|
405
|
-
self.create_config_tab()
|
|
406
|
-
|
|
407
|
-
# Test Tool Tab
|
|
408
|
-
self.test_frame = ttk.Frame(self.notebook)
|
|
409
|
-
self.notebook.add(self.test_frame, text="Test Tool")
|
|
410
|
-
self.create_test_tab()
|
|
411
|
-
|
|
412
|
-
# Log Tab
|
|
413
|
-
self.log_frame = ttk.Frame(self.notebook)
|
|
414
|
-
self.notebook.add(self.log_frame, text="Server Log")
|
|
415
|
-
self.create_log_tab()
|
|
416
|
-
|
|
417
|
-
def create_control_tab(self):
|
|
418
|
-
"""Create the server control tab."""
|
|
419
|
-
# Status frame
|
|
420
|
-
status_frame = ttk.LabelFrame(self.control_frame, text="Server Status", padding=10)
|
|
421
|
-
status_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
422
|
-
|
|
423
|
-
# Status indicator
|
|
424
|
-
status_row = ttk.Frame(status_frame)
|
|
425
|
-
status_row.pack(fill=tk.X, pady=5)
|
|
426
|
-
|
|
427
|
-
ttk.Label(status_row, text="Status:").pack(side=tk.LEFT, padx=(0, 10))
|
|
428
|
-
|
|
429
|
-
self.status_indicator = tk.Canvas(status_row, width=16, height=16, highlightthickness=0)
|
|
430
|
-
self.status_indicator.pack(side=tk.LEFT, padx=(0, 5))
|
|
431
|
-
self._draw_status_indicator(False)
|
|
432
|
-
|
|
433
|
-
self.status_label = ttk.Label(status_row, text="Stopped", font=("", 10, "bold"))
|
|
434
|
-
self.status_label.pack(side=tk.LEFT)
|
|
435
|
-
|
|
436
|
-
# Server info
|
|
437
|
-
info_frame = ttk.Frame(status_frame)
|
|
438
|
-
info_frame.pack(fill=tk.X, pady=5)
|
|
439
|
-
|
|
440
|
-
ttk.Label(info_frame, text="Server:").pack(side=tk.LEFT, padx=(0, 10))
|
|
441
|
-
ttk.Label(info_frame, text="pomera-mcp-server v0.1.0").pack(side=tk.LEFT)
|
|
442
|
-
|
|
443
|
-
tools_frame = ttk.Frame(status_frame)
|
|
444
|
-
tools_frame.pack(fill=tk.X, pady=5)
|
|
445
|
-
|
|
446
|
-
ttk.Label(tools_frame, text="Tools:").pack(side=tk.LEFT, padx=(0, 10))
|
|
447
|
-
tool_count = len(self.registry) if self.registry else 0
|
|
448
|
-
self.tools_count_label = ttk.Label(tools_frame, text=f"{tool_count} available")
|
|
449
|
-
self.tools_count_label.pack(side=tk.LEFT)
|
|
450
|
-
|
|
451
|
-
# PID display
|
|
452
|
-
pid_frame = ttk.Frame(status_frame)
|
|
453
|
-
pid_frame.pack(fill=tk.X, pady=5)
|
|
454
|
-
|
|
455
|
-
ttk.Label(pid_frame, text="Process ID:").pack(side=tk.LEFT, padx=(0, 10))
|
|
456
|
-
self.pid_label = ttk.Label(pid_frame, text="Not running")
|
|
457
|
-
self.pid_label.pack(side=tk.LEFT)
|
|
458
|
-
|
|
459
|
-
# Control buttons
|
|
460
|
-
button_frame = ttk.LabelFrame(self.control_frame, text="Controls", padding=10)
|
|
461
|
-
button_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
462
|
-
|
|
463
|
-
btn_row = ttk.Frame(button_frame)
|
|
464
|
-
btn_row.pack(fill=tk.X)
|
|
465
|
-
|
|
466
|
-
self.start_btn = ttk.Button(btn_row, text="▶ Start Server", command=self.start_server, width=15)
|
|
467
|
-
self.start_btn.pack(side=tk.LEFT, padx=5)
|
|
468
|
-
|
|
469
|
-
self.stop_btn = ttk.Button(btn_row, text="■ Stop Server", command=self.stop_server, width=15, state=tk.DISABLED)
|
|
470
|
-
self.stop_btn.pack(side=tk.LEFT, padx=5)
|
|
471
|
-
|
|
472
|
-
ttk.Button(btn_row, text="🔄 Refresh Tools", command=self.refresh_tools, width=15).pack(side=tk.LEFT, padx=5)
|
|
473
|
-
|
|
474
|
-
# Info note
|
|
475
|
-
note_frame = ttk.LabelFrame(self.control_frame, text="How to Use", padding=10)
|
|
476
|
-
note_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
477
|
-
|
|
478
|
-
note_text = (
|
|
479
|
-
"The MCP server exposes Pomera's text tools via the Model Context Protocol.\n\n"
|
|
480
|
-
"Option 1: Start from here (for testing/debugging)\n"
|
|
481
|
-
" • Click 'Start Server' to test the MCP server\n"
|
|
482
|
-
" • Server logs will be captured in the 'Server Log' tab\n"
|
|
483
|
-
" • Server will stop when Pomera closes (stdio transport limitation)\n\n"
|
|
484
|
-
"Option 2: Configure your AI client (RECOMMENDED for production)\n"
|
|
485
|
-
" • Copy the configuration from the 'Configuration' tab\n"
|
|
486
|
-
" • Add it to your Claude Desktop or Cursor config\n"
|
|
487
|
-
" • The client will start/stop the server automatically as needed\n"
|
|
488
|
-
" • This is the standard way to use MCP stdio servers"
|
|
489
|
-
)
|
|
490
|
-
ttk.Label(note_frame, text=note_text, wraplength=500, justify=tk.LEFT).pack(anchor=tk.W)
|
|
491
|
-
|
|
492
|
-
def create_tools_tab(self):
|
|
493
|
-
"""Create the available tools tab."""
|
|
494
|
-
# Use PanedWindow for resizable split between list and details
|
|
495
|
-
paned = ttk.PanedWindow(self.tools_frame, orient=tk.VERTICAL)
|
|
496
|
-
paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
497
|
-
|
|
498
|
-
# Top pane: Tools list with scrollbar
|
|
499
|
-
list_frame = ttk.Frame(paned)
|
|
500
|
-
|
|
501
|
-
# Treeview for tools
|
|
502
|
-
columns = ("name", "description")
|
|
503
|
-
self.tools_tree = ttk.Treeview(list_frame, columns=columns, show="headings")
|
|
504
|
-
|
|
505
|
-
self.tools_tree.heading("name", text="Tool Name")
|
|
506
|
-
self.tools_tree.heading("description", text="Description")
|
|
507
|
-
|
|
508
|
-
self.tools_tree.column("name", width=200, minwidth=150)
|
|
509
|
-
self.tools_tree.column("description", width=400, minwidth=200)
|
|
510
|
-
|
|
511
|
-
# Scrollbar for treeview
|
|
512
|
-
tree_scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tools_tree.yview)
|
|
513
|
-
self.tools_tree.configure(yscrollcommand=tree_scrollbar.set)
|
|
514
|
-
|
|
515
|
-
self.tools_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
516
|
-
tree_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
517
|
-
|
|
518
|
-
paned.add(list_frame, weight=1)
|
|
519
|
-
|
|
520
|
-
# Populate tools
|
|
521
|
-
self._populate_tools_list()
|
|
522
|
-
|
|
523
|
-
# Bottom pane: Tool details
|
|
524
|
-
details_frame = ttk.LabelFrame(paned, text="Tool Details", padding=10)
|
|
525
|
-
|
|
526
|
-
self.tool_details = scrolledtext.ScrolledText(details_frame, wrap=tk.WORD, state=tk.DISABLED)
|
|
527
|
-
self.tool_details.pack(fill=tk.BOTH, expand=True)
|
|
528
|
-
|
|
529
|
-
paned.add(details_frame, weight=1)
|
|
530
|
-
|
|
531
|
-
# Bind selection
|
|
532
|
-
self.tools_tree.bind("<<TreeviewSelect>>", self._on_tool_select)
|
|
533
|
-
|
|
534
|
-
def create_config_tab(self):
|
|
535
|
-
"""Create the configuration tab."""
|
|
536
|
-
# Get dynamic configuration
|
|
537
|
-
command, args = get_pomera_executable_path()
|
|
538
|
-
|
|
539
|
-
# Path selection frame
|
|
540
|
-
path_frame = ttk.LabelFrame(self.config_frame, text="Pomera Path Configuration", padding=10)
|
|
541
|
-
path_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
542
|
-
|
|
543
|
-
# Execution mode label
|
|
544
|
-
if getattr(sys, 'frozen', False):
|
|
545
|
-
mode_text = "Mode: Compiled executable"
|
|
546
|
-
default_path = command
|
|
547
|
-
else:
|
|
548
|
-
mode_text = "Mode: Python script"
|
|
549
|
-
default_path = args[0] if args else os.path.join(PROJECT_ROOT, "pomera.py")
|
|
550
|
-
|
|
551
|
-
ttk.Label(path_frame, text=mode_text, font=("", 9, "bold")).pack(anchor=tk.W)
|
|
552
|
-
|
|
553
|
-
# Path entry row
|
|
554
|
-
path_row = ttk.Frame(path_frame)
|
|
555
|
-
path_row.pack(fill=tk.X, pady=(5, 0))
|
|
556
|
-
|
|
557
|
-
ttk.Label(path_row, text="Path:").pack(side=tk.LEFT, padx=(0, 5))
|
|
558
|
-
|
|
559
|
-
self.path_var = tk.StringVar(value=default_path)
|
|
560
|
-
self.path_entry = ttk.Entry(path_row, textvariable=self.path_var, width=60)
|
|
561
|
-
self.path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
|
|
562
|
-
|
|
563
|
-
ttk.Button(path_row, text="Select...", command=self._select_pomera_path).pack(side=tk.LEFT, padx=(0, 5))
|
|
564
|
-
ttk.Button(path_row, text="Apply", command=self._apply_path_config).pack(side=tk.LEFT)
|
|
565
|
-
|
|
566
|
-
# Python executable row (only for script mode)
|
|
567
|
-
if not getattr(sys, 'frozen', False):
|
|
568
|
-
python_row = ttk.Frame(path_frame)
|
|
569
|
-
python_row.pack(fill=tk.X, pady=(5, 0))
|
|
570
|
-
|
|
571
|
-
ttk.Label(python_row, text="Python:").pack(side=tk.LEFT, padx=(0, 5))
|
|
572
|
-
|
|
573
|
-
self.python_var = tk.StringVar(value=sys.executable)
|
|
574
|
-
self.python_entry = ttk.Entry(python_row, textvariable=self.python_var, width=60)
|
|
575
|
-
self.python_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
|
|
576
|
-
|
|
577
|
-
ttk.Button(python_row, text="Select...", command=self._select_python_path).pack(side=tk.LEFT)
|
|
578
|
-
|
|
579
|
-
# Claude Desktop config
|
|
580
|
-
claude_frame = ttk.LabelFrame(self.config_frame, text="Claude Desktop Configuration", padding=10)
|
|
581
|
-
claude_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
582
|
-
|
|
583
|
-
ttk.Label(claude_frame, text="Add to claude_desktop_config.json:").pack(anchor=tk.W)
|
|
584
|
-
|
|
585
|
-
self.claude_config = scrolledtext.ScrolledText(claude_frame, height=8, wrap=tk.WORD)
|
|
586
|
-
self.claude_config.pack(fill=tk.X, pady=5)
|
|
587
|
-
|
|
588
|
-
claude_btn_row = ttk.Frame(claude_frame)
|
|
589
|
-
claude_btn_row.pack(fill=tk.X)
|
|
590
|
-
ttk.Button(claude_btn_row, text="Copy to Clipboard",
|
|
591
|
-
command=self._copy_claude_config).pack(side=tk.LEFT)
|
|
592
|
-
|
|
593
|
-
# Cursor config
|
|
594
|
-
cursor_frame = ttk.LabelFrame(self.config_frame, text="Cursor Configuration", padding=10)
|
|
595
|
-
cursor_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
596
|
-
|
|
597
|
-
ttk.Label(cursor_frame, text="Add to .cursor/mcp.json in your project:").pack(anchor=tk.W)
|
|
598
|
-
|
|
599
|
-
self.cursor_config = scrolledtext.ScrolledText(cursor_frame, height=8, wrap=tk.WORD)
|
|
600
|
-
self.cursor_config.pack(fill=tk.X, pady=5)
|
|
601
|
-
|
|
602
|
-
cursor_btn_row = ttk.Frame(cursor_frame)
|
|
603
|
-
cursor_btn_row.pack(fill=tk.X)
|
|
604
|
-
ttk.Button(cursor_btn_row, text="Copy to Clipboard",
|
|
605
|
-
command=self._copy_cursor_config).pack(side=tk.LEFT)
|
|
606
|
-
|
|
607
|
-
# Command line usage
|
|
608
|
-
cli_frame = ttk.LabelFrame(self.config_frame, text="Command Line Usage", padding=10)
|
|
609
|
-
cli_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
610
|
-
|
|
611
|
-
self.cli_config = scrolledtext.ScrolledText(cli_frame, height=6, wrap=tk.WORD)
|
|
612
|
-
self.cli_config.pack(fill=tk.X, pady=5)
|
|
613
|
-
|
|
614
|
-
# Initial population of config text areas
|
|
615
|
-
self._update_config_displays()
|
|
616
|
-
|
|
617
|
-
def _select_pomera_path(self):
|
|
618
|
-
"""Open file dialog to select Pomera executable or script."""
|
|
619
|
-
from tkinter import filedialog
|
|
620
|
-
|
|
621
|
-
if getattr(sys, 'frozen', False):
|
|
622
|
-
filetypes = [("Executable", "*.exe"), ("All files", "*.*")]
|
|
623
|
-
title = "Select pomera.exe"
|
|
624
|
-
else:
|
|
625
|
-
filetypes = [("Python script", "*.py"), ("All files", "*.*")]
|
|
626
|
-
title = "Select pomera.py"
|
|
627
|
-
|
|
628
|
-
path = filedialog.askopenfilename(
|
|
629
|
-
title=title,
|
|
630
|
-
filetypes=filetypes,
|
|
631
|
-
initialdir=os.path.dirname(self.path_var.get()) if self.path_var.get() else PROJECT_ROOT
|
|
632
|
-
)
|
|
633
|
-
|
|
634
|
-
if path:
|
|
635
|
-
self.path_var.set(path)
|
|
636
|
-
self._update_config_displays()
|
|
637
|
-
|
|
638
|
-
def _select_python_path(self):
|
|
639
|
-
"""Open file dialog to select Python executable."""
|
|
640
|
-
from tkinter import filedialog
|
|
641
|
-
|
|
642
|
-
if sys.platform == "win32":
|
|
643
|
-
filetypes = [("Executable", "*.exe"), ("All files", "*.*")]
|
|
644
|
-
else:
|
|
645
|
-
filetypes = [("All files", "*.*")]
|
|
646
|
-
|
|
647
|
-
path = filedialog.askopenfilename(
|
|
648
|
-
title="Select Python executable",
|
|
649
|
-
filetypes=filetypes,
|
|
650
|
-
initialdir=os.path.dirname(self.python_var.get()) if hasattr(self, 'python_var') and self.python_var.get() else None
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
if path:
|
|
654
|
-
self.python_var.set(path)
|
|
655
|
-
self._update_config_displays()
|
|
656
|
-
|
|
657
|
-
def _apply_path_config(self):
|
|
658
|
-
"""Apply the current path configuration and update displays."""
|
|
659
|
-
self._update_config_displays()
|
|
660
|
-
self._add_log("INFO", "Configuration updated with new path")
|
|
661
|
-
|
|
662
|
-
def _generate_config_json(self) -> str:
|
|
663
|
-
"""Generate MCP configuration JSON based on current path settings."""
|
|
664
|
-
pomera_path = self.path_var.get().replace("\\", "/")
|
|
665
|
-
|
|
666
|
-
if getattr(sys, 'frozen', False):
|
|
667
|
-
# Compiled executable mode
|
|
668
|
-
config = {
|
|
669
|
-
"mcpServers": {
|
|
670
|
-
"pomera": {
|
|
671
|
-
"command": pomera_path,
|
|
672
|
-
"args": ["--mcp-server"]
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
else:
|
|
677
|
-
# Python script mode
|
|
678
|
-
python_path = self.python_var.get().replace("\\", "/") if hasattr(self, 'python_var') else sys.executable.replace("\\", "/")
|
|
679
|
-
config = {
|
|
680
|
-
"mcpServers": {
|
|
681
|
-
"pomera": {
|
|
682
|
-
"command": python_path,
|
|
683
|
-
"args": [pomera_path, "--mcp-server"]
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
return json.dumps(config, indent=2)
|
|
689
|
-
|
|
690
|
-
def _generate_cli_text(self) -> str:
|
|
691
|
-
"""Generate CLI usage text based on current path settings."""
|
|
692
|
-
pomera_path = self.path_var.get()
|
|
693
|
-
|
|
694
|
-
if getattr(sys, 'frozen', False):
|
|
695
|
-
return f"""# List available tools:
|
|
696
|
-
"{pomera_path}" --mcp-server --list-tools
|
|
697
|
-
|
|
698
|
-
# Run server (for manual testing):
|
|
699
|
-
"{pomera_path}" --mcp-server
|
|
700
|
-
|
|
701
|
-
# Run with debug logging:
|
|
702
|
-
"{pomera_path}" --mcp-server --debug"""
|
|
703
|
-
else:
|
|
704
|
-
python_path = self.python_var.get() if hasattr(self, 'python_var') else sys.executable
|
|
705
|
-
return f"""# List available tools:
|
|
706
|
-
"{python_path}" "{pomera_path}" --mcp-server --list-tools
|
|
707
|
-
|
|
708
|
-
# Run server (for manual testing):
|
|
709
|
-
"{python_path}" "{pomera_path}" --mcp-server
|
|
710
|
-
|
|
711
|
-
# Run with debug logging:
|
|
712
|
-
"{python_path}" "{pomera_path}" --mcp-server --debug"""
|
|
713
|
-
|
|
714
|
-
def _update_config_displays(self):
|
|
715
|
-
"""Update all configuration text displays with current path settings."""
|
|
716
|
-
config_json = self._generate_config_json()
|
|
717
|
-
cli_text = self._generate_cli_text()
|
|
718
|
-
|
|
719
|
-
# Update Claude config
|
|
720
|
-
self.claude_config.config(state=tk.NORMAL)
|
|
721
|
-
self.claude_config.delete("1.0", tk.END)
|
|
722
|
-
self.claude_config.insert(tk.END, config_json)
|
|
723
|
-
self.claude_config.config(state=tk.DISABLED)
|
|
724
|
-
|
|
725
|
-
# Update Cursor config
|
|
726
|
-
self.cursor_config.config(state=tk.NORMAL)
|
|
727
|
-
self.cursor_config.delete("1.0", tk.END)
|
|
728
|
-
self.cursor_config.insert(tk.END, config_json)
|
|
729
|
-
self.cursor_config.config(state=tk.DISABLED)
|
|
730
|
-
|
|
731
|
-
# Update CLI text
|
|
732
|
-
self.cli_config.config(state=tk.NORMAL)
|
|
733
|
-
self.cli_config.delete("1.0", tk.END)
|
|
734
|
-
self.cli_config.insert(tk.END, cli_text)
|
|
735
|
-
self.cli_config.config(state=tk.DISABLED)
|
|
736
|
-
|
|
737
|
-
def _copy_claude_config(self):
|
|
738
|
-
"""Copy Claude Desktop config to clipboard."""
|
|
739
|
-
config = self._generate_config_json()
|
|
740
|
-
self._copy_to_clipboard(config)
|
|
741
|
-
|
|
742
|
-
def _copy_cursor_config(self):
|
|
743
|
-
"""Copy Cursor config to clipboard."""
|
|
744
|
-
config = self._generate_config_json()
|
|
745
|
-
self._copy_to_clipboard(config)
|
|
746
|
-
|
|
747
|
-
def create_test_tab(self):
|
|
748
|
-
"""Create the test tool tab for sending MCP requests."""
|
|
749
|
-
# Tool selection frame
|
|
750
|
-
select_frame = ttk.LabelFrame(self.test_frame, text="Select Tool", padding=10)
|
|
751
|
-
select_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
752
|
-
|
|
753
|
-
# Tool dropdown
|
|
754
|
-
tool_row = ttk.Frame(select_frame)
|
|
755
|
-
tool_row.pack(fill=tk.X)
|
|
756
|
-
|
|
757
|
-
ttk.Label(tool_row, text="Tool:").pack(side=tk.LEFT, padx=(0, 5))
|
|
758
|
-
|
|
759
|
-
self.test_tool_var = tk.StringVar()
|
|
760
|
-
self.test_tool_combo = ttk.Combobox(
|
|
761
|
-
tool_row,
|
|
762
|
-
textvariable=self.test_tool_var,
|
|
763
|
-
state="readonly",
|
|
764
|
-
width=40
|
|
765
|
-
)
|
|
766
|
-
self.test_tool_combo.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
|
|
767
|
-
self.test_tool_combo.bind("<<ComboboxSelected>>", self._on_test_tool_select)
|
|
768
|
-
|
|
769
|
-
# Parameters frame
|
|
770
|
-
params_frame = ttk.LabelFrame(self.test_frame, text="Parameters (JSON)", padding=10)
|
|
771
|
-
params_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
772
|
-
|
|
773
|
-
# Parameter hints label
|
|
774
|
-
self.param_hints_label = ttk.Label(params_frame, text="Select a tool to see required parameters", foreground="gray")
|
|
775
|
-
self.param_hints_label.pack(anchor=tk.W, pady=(0, 5))
|
|
776
|
-
|
|
777
|
-
# Parameters text area
|
|
778
|
-
self.test_params = scrolledtext.ScrolledText(params_frame, height=8, wrap=tk.WORD)
|
|
779
|
-
self.test_params.pack(fill=tk.BOTH, expand=True)
|
|
780
|
-
self.test_params.insert(tk.END, "{}")
|
|
781
|
-
|
|
782
|
-
# Execute button frame
|
|
783
|
-
exec_frame = ttk.Frame(self.test_frame)
|
|
784
|
-
exec_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
785
|
-
|
|
786
|
-
self.test_execute_btn = ttk.Button(
|
|
787
|
-
exec_frame,
|
|
788
|
-
text="▶ Execute Tool",
|
|
789
|
-
command=self._execute_test_tool,
|
|
790
|
-
width=20
|
|
791
|
-
)
|
|
792
|
-
self.test_execute_btn.pack(side=tk.LEFT)
|
|
793
|
-
|
|
794
|
-
ttk.Button(
|
|
795
|
-
exec_frame,
|
|
796
|
-
text="Clear Result",
|
|
797
|
-
command=self._clear_test_result,
|
|
798
|
-
width=15
|
|
799
|
-
).pack(side=tk.LEFT, padx=(10, 0))
|
|
800
|
-
|
|
801
|
-
# Server-only checkbox
|
|
802
|
-
self.test_server_only_var = tk.BooleanVar(value=False)
|
|
803
|
-
self.test_server_only_cb = ttk.Checkbutton(
|
|
804
|
-
exec_frame,
|
|
805
|
-
text="Test via server only",
|
|
806
|
-
variable=self.test_server_only_var
|
|
807
|
-
)
|
|
808
|
-
self.test_server_only_cb.pack(side=tk.LEFT, padx=(20, 0))
|
|
809
|
-
|
|
810
|
-
# Status label
|
|
811
|
-
self.test_status_label = ttk.Label(exec_frame, text="")
|
|
812
|
-
self.test_status_label.pack(side=tk.RIGHT)
|
|
813
|
-
|
|
814
|
-
# Result frame
|
|
815
|
-
result_frame = ttk.LabelFrame(self.test_frame, text="Result", padding=10)
|
|
816
|
-
result_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
817
|
-
|
|
818
|
-
self.test_result = scrolledtext.ScrolledText(result_frame, height=10, wrap=tk.WORD, state=tk.DISABLED)
|
|
819
|
-
self.test_result.pack(fill=tk.BOTH, expand=True)
|
|
820
|
-
|
|
821
|
-
# Configure tags for result display
|
|
822
|
-
self.test_result.tag_configure("success", foreground="green")
|
|
823
|
-
self.test_result.tag_configure("error", foreground="red")
|
|
824
|
-
self.test_result.tag_configure("info", foreground="blue")
|
|
825
|
-
|
|
826
|
-
# Populate tool dropdown (after all widgets are created)
|
|
827
|
-
self._populate_test_tool_combo()
|
|
828
|
-
|
|
829
|
-
def _populate_test_tool_combo(self):
|
|
830
|
-
"""Populate the test tool dropdown with available tools."""
|
|
831
|
-
if self.registry:
|
|
832
|
-
tool_names = [tool.name for tool in self.registry.list_tools()]
|
|
833
|
-
self.test_tool_combo['values'] = sorted(tool_names)
|
|
834
|
-
if tool_names:
|
|
835
|
-
self.test_tool_combo.set(sorted(tool_names)[0])
|
|
836
|
-
self._on_test_tool_select(None)
|
|
837
|
-
|
|
838
|
-
def _on_test_tool_select(self, event):
|
|
839
|
-
"""Handle tool selection in test tab - show parameter hints."""
|
|
840
|
-
tool_name = self.test_tool_var.get()
|
|
841
|
-
if not tool_name or not self.registry:
|
|
842
|
-
return
|
|
843
|
-
|
|
844
|
-
# Check if UI elements exist yet
|
|
845
|
-
if not hasattr(self, 'param_hints_label') or not hasattr(self, 'test_params'):
|
|
846
|
-
return
|
|
847
|
-
|
|
848
|
-
tool = self.registry.get_tool(tool_name)
|
|
849
|
-
if not tool:
|
|
850
|
-
return
|
|
851
|
-
|
|
852
|
-
mcp_tool = tool.to_mcp_tool()
|
|
853
|
-
schema = mcp_tool.inputSchema
|
|
854
|
-
|
|
855
|
-
# Build parameter hints
|
|
856
|
-
properties = schema.get("properties", {})
|
|
857
|
-
required = schema.get("required", [])
|
|
858
|
-
|
|
859
|
-
hints = []
|
|
860
|
-
for param_name, param_info in properties.items():
|
|
861
|
-
param_type = param_info.get("type", "any")
|
|
862
|
-
param_desc = param_info.get("description", "")
|
|
863
|
-
is_required = param_name in required
|
|
864
|
-
req_marker = "*" if is_required else ""
|
|
865
|
-
hints.append(f" • {param_name}{req_marker} ({param_type}): {param_desc[:50]}...")
|
|
866
|
-
|
|
867
|
-
if hints:
|
|
868
|
-
hint_text = "Parameters (* = required):\n" + "\n".join(hints[:5])
|
|
869
|
-
if len(hints) > 5:
|
|
870
|
-
hint_text += f"\n ... and {len(hints) - 5} more"
|
|
871
|
-
else:
|
|
872
|
-
hint_text = "No parameters required"
|
|
873
|
-
|
|
874
|
-
self.param_hints_label.config(text=hint_text)
|
|
875
|
-
|
|
876
|
-
# Generate example JSON with ALL parameters (required + optional with defaults)
|
|
877
|
-
example = {}
|
|
878
|
-
for param_name, param_info in properties.items():
|
|
879
|
-
param_type = param_info.get("type", "string")
|
|
880
|
-
is_required = param_name in required
|
|
881
|
-
|
|
882
|
-
# Use default value if available, otherwise generate placeholder
|
|
883
|
-
if "default" in param_info:
|
|
884
|
-
example[param_name] = param_info["default"]
|
|
885
|
-
elif "enum" in param_info:
|
|
886
|
-
example[param_name] = param_info["enum"][0]
|
|
887
|
-
elif param_type == "string":
|
|
888
|
-
example[param_name] = f"<{param_name}>" if is_required else ""
|
|
889
|
-
elif param_type == "integer":
|
|
890
|
-
example[param_name] = 0
|
|
891
|
-
elif param_type == "number":
|
|
892
|
-
example[param_name] = 0
|
|
893
|
-
elif param_type == "boolean":
|
|
894
|
-
example[param_name] = False
|
|
895
|
-
else:
|
|
896
|
-
example[param_name] = f"<{param_name}>" if is_required else None
|
|
897
|
-
|
|
898
|
-
self.test_params.delete("1.0", tk.END)
|
|
899
|
-
self.test_params.insert(tk.END, json.dumps(example, indent=2))
|
|
900
|
-
|
|
901
|
-
def _execute_test_tool(self):
|
|
902
|
-
"""Execute the selected tool with the provided parameters."""
|
|
903
|
-
tool_name = self.test_tool_var.get()
|
|
904
|
-
if not tool_name:
|
|
905
|
-
self._show_test_error("Please select a tool")
|
|
906
|
-
return
|
|
907
|
-
|
|
908
|
-
# Parse parameters
|
|
909
|
-
try:
|
|
910
|
-
params_text = self.test_params.get("1.0", tk.END).strip()
|
|
911
|
-
params = json.loads(params_text) if params_text else {}
|
|
912
|
-
except json.JSONDecodeError as e:
|
|
913
|
-
self._show_test_error(f"Invalid JSON parameters: {e}")
|
|
914
|
-
return
|
|
915
|
-
|
|
916
|
-
# Check if "server only" mode is enabled
|
|
917
|
-
server_only = self.test_server_only_var.get()
|
|
918
|
-
server_running = self.server_process and self.server_process.is_running() and self.server_process.process
|
|
919
|
-
|
|
920
|
-
if server_only and not server_running:
|
|
921
|
-
self._show_test_error("Server is not running. Start the server first or uncheck 'Test via server only'.")
|
|
922
|
-
return
|
|
923
|
-
|
|
924
|
-
# Check if server is running
|
|
925
|
-
if server_running:
|
|
926
|
-
# Send request to running server via stdin
|
|
927
|
-
self._execute_via_server(tool_name, params)
|
|
928
|
-
else:
|
|
929
|
-
# Execute directly via registry
|
|
930
|
-
self._execute_directly(tool_name, params)
|
|
931
|
-
|
|
932
|
-
def _execute_via_server(self, tool_name: str, params: dict):
|
|
933
|
-
"""Execute tool by sending request to the running server's stdin."""
|
|
934
|
-
self.test_status_label.config(text="Sending request...", foreground="blue")
|
|
935
|
-
self.update_idletasks()
|
|
936
|
-
|
|
937
|
-
try:
|
|
938
|
-
# Build MCP request
|
|
939
|
-
request = {
|
|
940
|
-
"jsonrpc": "2.0",
|
|
941
|
-
"id": 1,
|
|
942
|
-
"method": "tools/call",
|
|
943
|
-
"params": {
|
|
944
|
-
"name": tool_name,
|
|
945
|
-
"arguments": params
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
# Need to send initialize first if not already done
|
|
950
|
-
init_request = {
|
|
951
|
-
"jsonrpc": "2.0",
|
|
952
|
-
"id": 0,
|
|
953
|
-
"method": "initialize",
|
|
954
|
-
"params": {
|
|
955
|
-
"protocolVersion": "2024-11-05",
|
|
956
|
-
"capabilities": {},
|
|
957
|
-
"clientInfo": {"name": "pomera-test", "version": "1.0"}
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
# Send requests
|
|
962
|
-
process = self.server_process.process
|
|
963
|
-
if process and process.stdin and process.stdout:
|
|
964
|
-
# Send initialize
|
|
965
|
-
process.stdin.write(json.dumps(init_request) + "\n")
|
|
966
|
-
process.stdin.flush()
|
|
967
|
-
|
|
968
|
-
# Read init response
|
|
969
|
-
init_response = process.stdout.readline()
|
|
970
|
-
|
|
971
|
-
# Send tool call
|
|
972
|
-
process.stdin.write(json.dumps(request) + "\n")
|
|
973
|
-
process.stdin.flush()
|
|
974
|
-
|
|
975
|
-
# Read response with timeout
|
|
976
|
-
response_line = process.stdout.readline()
|
|
977
|
-
if response_line:
|
|
978
|
-
response = json.loads(response_line)
|
|
979
|
-
self._show_test_result(response)
|
|
980
|
-
else:
|
|
981
|
-
self._show_test_error("No response from server")
|
|
982
|
-
else:
|
|
983
|
-
self._show_test_error("Server stdin/stdout not available")
|
|
984
|
-
|
|
985
|
-
except Exception as e:
|
|
986
|
-
self._show_test_error(f"Error communicating with server: {e}")
|
|
987
|
-
|
|
988
|
-
def _execute_directly(self, tool_name: str, params: dict):
|
|
989
|
-
"""Execute tool directly via the registry (no server needed)."""
|
|
990
|
-
self.test_status_label.config(text="Executing directly...", foreground="blue")
|
|
991
|
-
self.update_idletasks()
|
|
992
|
-
|
|
993
|
-
if not self.registry:
|
|
994
|
-
self._show_test_error("Tool registry not available")
|
|
995
|
-
return
|
|
996
|
-
|
|
997
|
-
try:
|
|
998
|
-
result = self.registry.execute(tool_name, params)
|
|
999
|
-
|
|
1000
|
-
# Format result
|
|
1001
|
-
response = {
|
|
1002
|
-
"success": not result.isError,
|
|
1003
|
-
"content": result.content
|
|
1004
|
-
}
|
|
1005
|
-
self._show_test_result(response)
|
|
1006
|
-
|
|
1007
|
-
except Exception as e:
|
|
1008
|
-
self._show_test_error(f"Execution error: {e}")
|
|
1009
|
-
|
|
1010
|
-
def _show_test_result(self, response: dict):
|
|
1011
|
-
"""Display test result in the result area."""
|
|
1012
|
-
self.test_result.config(state=tk.NORMAL)
|
|
1013
|
-
self.test_result.delete("1.0", tk.END)
|
|
1014
|
-
|
|
1015
|
-
# Check if it's an error response
|
|
1016
|
-
if "error" in response:
|
|
1017
|
-
self.test_status_label.config(text="Error", foreground="red")
|
|
1018
|
-
self.test_result.insert(tk.END, "ERROR:\n", "error")
|
|
1019
|
-
self.test_result.insert(tk.END, json.dumps(response["error"], indent=2))
|
|
1020
|
-
elif response.get("success") == False or response.get("isError"):
|
|
1021
|
-
self.test_status_label.config(text="Tool returned error", foreground="orange")
|
|
1022
|
-
self.test_result.insert(tk.END, "Tool Error:\n", "error")
|
|
1023
|
-
content = response.get("content", response.get("result", {}).get("content", []))
|
|
1024
|
-
if isinstance(content, list):
|
|
1025
|
-
for item in content:
|
|
1026
|
-
if item.get("type") == "text":
|
|
1027
|
-
self.test_result.insert(tk.END, item.get("text", ""))
|
|
1028
|
-
else:
|
|
1029
|
-
self.test_result.insert(tk.END, json.dumps(content, indent=2))
|
|
1030
|
-
else:
|
|
1031
|
-
self.test_status_label.config(text="Success", foreground="green")
|
|
1032
|
-
self.test_result.insert(tk.END, "Result:\n", "success")
|
|
1033
|
-
|
|
1034
|
-
# Extract content from MCP response
|
|
1035
|
-
content = response.get("content", response.get("result", {}).get("content", []))
|
|
1036
|
-
if isinstance(content, list):
|
|
1037
|
-
for item in content:
|
|
1038
|
-
if item.get("type") == "text":
|
|
1039
|
-
self.test_result.insert(tk.END, item.get("text", ""))
|
|
1040
|
-
self.test_result.insert(tk.END, "\n")
|
|
1041
|
-
else:
|
|
1042
|
-
self.test_result.insert(tk.END, json.dumps(content, indent=2))
|
|
1043
|
-
|
|
1044
|
-
self.test_result.config(state=tk.DISABLED)
|
|
1045
|
-
self._add_log("INFO", f"Test executed: {self.test_tool_var.get()}")
|
|
1046
|
-
|
|
1047
|
-
def _show_test_error(self, message: str):
|
|
1048
|
-
"""Display an error message in the test result area."""
|
|
1049
|
-
self.test_status_label.config(text="Error", foreground="red")
|
|
1050
|
-
self.test_result.config(state=tk.NORMAL)
|
|
1051
|
-
self.test_result.delete("1.0", tk.END)
|
|
1052
|
-
self.test_result.insert(tk.END, f"Error: {message}", "error")
|
|
1053
|
-
self.test_result.config(state=tk.DISABLED)
|
|
1054
|
-
self._add_log("ERROR", f"Test error: {message}")
|
|
1055
|
-
|
|
1056
|
-
def _clear_test_result(self):
|
|
1057
|
-
"""Clear the test result area."""
|
|
1058
|
-
self.test_result.config(state=tk.NORMAL)
|
|
1059
|
-
self.test_result.delete("1.0", tk.END)
|
|
1060
|
-
self.test_result.config(state=tk.DISABLED)
|
|
1061
|
-
self.test_status_label.config(text="")
|
|
1062
|
-
|
|
1063
|
-
def create_log_tab(self):
|
|
1064
|
-
"""Create the server log tab."""
|
|
1065
|
-
# Control frame at top
|
|
1066
|
-
control_frame = ttk.Frame(self.log_frame)
|
|
1067
|
-
control_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
1068
|
-
|
|
1069
|
-
# Log level filter
|
|
1070
|
-
ttk.Label(control_frame, text="Log Level:").pack(side=tk.LEFT, padx=(0, 5))
|
|
1071
|
-
|
|
1072
|
-
self.log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
1073
|
-
self.log_level_var = tk.StringVar(value="DEBUG") # Show all by default
|
|
1074
|
-
self.log_level_combo = ttk.Combobox(
|
|
1075
|
-
control_frame,
|
|
1076
|
-
textvariable=self.log_level_var,
|
|
1077
|
-
values=self.log_levels,
|
|
1078
|
-
state="readonly",
|
|
1079
|
-
width=12
|
|
1080
|
-
)
|
|
1081
|
-
self.log_level_combo.pack(side=tk.LEFT, padx=(0, 10))
|
|
1082
|
-
self.log_level_combo.bind("<<ComboboxSelected>>", self._on_log_level_change)
|
|
1083
|
-
|
|
1084
|
-
# Auto-scroll checkbox
|
|
1085
|
-
self.auto_scroll_var = tk.BooleanVar(value=True)
|
|
1086
|
-
ttk.Checkbutton(
|
|
1087
|
-
control_frame,
|
|
1088
|
-
text="Auto-scroll",
|
|
1089
|
-
variable=self.auto_scroll_var
|
|
1090
|
-
).pack(side=tk.LEFT, padx=(0, 10))
|
|
1091
|
-
|
|
1092
|
-
# Clear button
|
|
1093
|
-
ttk.Button(control_frame, text="Clear Log", command=self._clear_log).pack(side=tk.LEFT)
|
|
1094
|
-
|
|
1095
|
-
# Log entry count label
|
|
1096
|
-
self.log_count_label = ttk.Label(control_frame, text="Entries: 0")
|
|
1097
|
-
self.log_count_label.pack(side=tk.RIGHT)
|
|
1098
|
-
|
|
1099
|
-
# Log display
|
|
1100
|
-
self.log_text = scrolledtext.ScrolledText(self.log_frame, height=20, wrap=tk.WORD, state=tk.DISABLED)
|
|
1101
|
-
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
1102
|
-
|
|
1103
|
-
# Configure tags for log levels with colors
|
|
1104
|
-
self.log_text.tag_configure("DEBUG", foreground="gray")
|
|
1105
|
-
self.log_text.tag_configure("INFO", foreground="green")
|
|
1106
|
-
self.log_text.tag_configure("WARNING", foreground="orange")
|
|
1107
|
-
self.log_text.tag_configure("ERROR", foreground="red")
|
|
1108
|
-
self.log_text.tag_configure("CRITICAL", foreground="red", font=("", 10, "bold"))
|
|
1109
|
-
self.log_text.tag_configure("TIMESTAMP", foreground="blue")
|
|
1110
|
-
|
|
1111
|
-
# Store all log entries for filtering
|
|
1112
|
-
self.all_log_entries = []
|
|
1113
|
-
|
|
1114
|
-
# Initial log message
|
|
1115
|
-
self._add_log("INFO", "MCP Manager initialized. Ready to start server.")
|
|
1116
|
-
|
|
1117
|
-
def _on_log_level_change(self, event=None):
|
|
1118
|
-
"""Handle log level filter change - refresh display with filtered entries."""
|
|
1119
|
-
self._refresh_log_display()
|
|
1120
|
-
|
|
1121
|
-
def _get_log_level_priority(self, level: str) -> int:
|
|
1122
|
-
"""Get numeric priority for a log level (higher = more severe)."""
|
|
1123
|
-
priorities = {
|
|
1124
|
-
"DEBUG": 0,
|
|
1125
|
-
"INFO": 1,
|
|
1126
|
-
"WARNING": 2,
|
|
1127
|
-
"ERROR": 3,
|
|
1128
|
-
"CRITICAL": 4
|
|
1129
|
-
}
|
|
1130
|
-
return priorities.get(level.upper(), 0)
|
|
1131
|
-
|
|
1132
|
-
def _refresh_log_display(self):
|
|
1133
|
-
"""Refresh the log display based on current filter level."""
|
|
1134
|
-
min_level = self._get_log_level_priority(self.log_level_var.get())
|
|
1135
|
-
|
|
1136
|
-
self.log_text.config(state=tk.NORMAL)
|
|
1137
|
-
self.log_text.delete("1.0", tk.END)
|
|
1138
|
-
|
|
1139
|
-
visible_count = 0
|
|
1140
|
-
for timestamp, level, message in self.all_log_entries:
|
|
1141
|
-
if self._get_log_level_priority(level) >= min_level:
|
|
1142
|
-
self.log_text.insert(tk.END, f"[{timestamp}] ", "TIMESTAMP")
|
|
1143
|
-
self.log_text.insert(tk.END, f"[{level}] ", level)
|
|
1144
|
-
self.log_text.insert(tk.END, f"{message}\n")
|
|
1145
|
-
visible_count += 1
|
|
1146
|
-
|
|
1147
|
-
self.log_text.config(state=tk.DISABLED)
|
|
1148
|
-
|
|
1149
|
-
if self.auto_scroll_var.get():
|
|
1150
|
-
self.log_text.see(tk.END)
|
|
1151
|
-
|
|
1152
|
-
# Update count label
|
|
1153
|
-
total = len(self.all_log_entries)
|
|
1154
|
-
if visible_count == total:
|
|
1155
|
-
self.log_count_label.config(text=f"Entries: {total}")
|
|
1156
|
-
else:
|
|
1157
|
-
self.log_count_label.config(text=f"Entries: {visible_count}/{total}")
|
|
1158
|
-
|
|
1159
|
-
def _draw_status_indicator(self, running: bool):
|
|
1160
|
-
"""Draw the status indicator circle."""
|
|
1161
|
-
self.status_indicator.delete("all")
|
|
1162
|
-
color = "#00CC00" if running else "#CC0000"
|
|
1163
|
-
self.status_indicator.create_oval(2, 2, 14, 14, fill=color, outline=color)
|
|
1164
|
-
|
|
1165
|
-
def _populate_tools_list(self):
|
|
1166
|
-
"""Populate the tools treeview."""
|
|
1167
|
-
# Clear existing
|
|
1168
|
-
for item in self.tools_tree.get_children():
|
|
1169
|
-
self.tools_tree.delete(item)
|
|
1170
|
-
|
|
1171
|
-
if not self.registry:
|
|
1172
|
-
return
|
|
1173
|
-
|
|
1174
|
-
for tool in self.registry.list_tools():
|
|
1175
|
-
# Truncate description for display
|
|
1176
|
-
desc = tool.description[:80] + "..." if len(tool.description) > 80 else tool.description
|
|
1177
|
-
self.tools_tree.insert("", tk.END, values=(tool.name, desc))
|
|
1178
|
-
|
|
1179
|
-
def _on_tool_select(self, event):
|
|
1180
|
-
"""Handle tool selection in treeview."""
|
|
1181
|
-
selection = self.tools_tree.selection()
|
|
1182
|
-
if not selection:
|
|
1183
|
-
return
|
|
1184
|
-
|
|
1185
|
-
item = self.tools_tree.item(selection[0])
|
|
1186
|
-
tool_name = item["values"][0]
|
|
1187
|
-
|
|
1188
|
-
if not self.registry:
|
|
1189
|
-
return
|
|
1190
|
-
|
|
1191
|
-
tool = self.registry.get_tool(tool_name)
|
|
1192
|
-
if not tool:
|
|
1193
|
-
return
|
|
1194
|
-
|
|
1195
|
-
# Display tool details in a readable format
|
|
1196
|
-
mcp_tool = tool.to_mcp_tool()
|
|
1197
|
-
schema = mcp_tool.inputSchema
|
|
1198
|
-
properties = schema.get("properties", {})
|
|
1199
|
-
required = schema.get("required", [])
|
|
1200
|
-
|
|
1201
|
-
details = f"Tool: {mcp_tool.name}\n"
|
|
1202
|
-
details += "=" * 60 + "\n\n"
|
|
1203
|
-
details += f"Description:\n{mcp_tool.description}\n\n"
|
|
1204
|
-
|
|
1205
|
-
# Parameters section
|
|
1206
|
-
details += "Parameters:\n"
|
|
1207
|
-
details += "-" * 40 + "\n"
|
|
1208
|
-
|
|
1209
|
-
if properties:
|
|
1210
|
-
for param_name, param_info in properties.items():
|
|
1211
|
-
is_required = param_name in required
|
|
1212
|
-
param_type = param_info.get("type", "any")
|
|
1213
|
-
param_desc = param_info.get("description", "No description")
|
|
1214
|
-
|
|
1215
|
-
# Format parameter header
|
|
1216
|
-
req_marker = " [REQUIRED]" if is_required else " [optional]"
|
|
1217
|
-
details += f"\n• {param_name}{req_marker}\n"
|
|
1218
|
-
details += f" Type: {param_type}\n"
|
|
1219
|
-
|
|
1220
|
-
# Show enum values if present
|
|
1221
|
-
if "enum" in param_info:
|
|
1222
|
-
enum_values = ", ".join(str(v) for v in param_info["enum"])
|
|
1223
|
-
details += f" Options: {enum_values}\n"
|
|
1224
|
-
|
|
1225
|
-
# Show default value if present
|
|
1226
|
-
if "default" in param_info:
|
|
1227
|
-
default_val = param_info["default"]
|
|
1228
|
-
if isinstance(default_val, str) and len(default_val) > 50:
|
|
1229
|
-
default_val = default_val[:50] + "..."
|
|
1230
|
-
details += f" Default: {default_val}\n"
|
|
1231
|
-
|
|
1232
|
-
details += f" Description: {param_desc}\n"
|
|
1233
|
-
else:
|
|
1234
|
-
details += "\n(No parameters)\n"
|
|
1235
|
-
|
|
1236
|
-
self.tool_details.config(state=tk.NORMAL)
|
|
1237
|
-
self.tool_details.delete("1.0", tk.END)
|
|
1238
|
-
self.tool_details.insert(tk.END, details)
|
|
1239
|
-
self.tool_details.config(state=tk.DISABLED)
|
|
1240
|
-
|
|
1241
|
-
def _copy_to_clipboard(self, text: str):
|
|
1242
|
-
"""Copy text to clipboard."""
|
|
1243
|
-
self.clipboard_clear()
|
|
1244
|
-
self.clipboard_append(text)
|
|
1245
|
-
self._add_log("INFO", "Configuration copied to clipboard")
|
|
1246
|
-
|
|
1247
|
-
def _add_log(self, level: str, message: str):
|
|
1248
|
-
"""Add a log message to the log display."""
|
|
1249
|
-
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
1250
|
-
|
|
1251
|
-
# Store in all entries for filtering
|
|
1252
|
-
if hasattr(self, 'all_log_entries'):
|
|
1253
|
-
self.all_log_entries.append((timestamp, level.upper(), message))
|
|
1254
|
-
|
|
1255
|
-
# Check if this message should be displayed based on current filter
|
|
1256
|
-
min_level = self._get_log_level_priority(self.log_level_var.get()) if hasattr(self, 'log_level_var') else 0
|
|
1257
|
-
if self._get_log_level_priority(level) >= min_level:
|
|
1258
|
-
self.log_text.config(state=tk.NORMAL)
|
|
1259
|
-
self.log_text.insert(tk.END, f"[{timestamp}] ", "TIMESTAMP")
|
|
1260
|
-
self.log_text.insert(tk.END, f"[{level.upper()}] ", level.upper())
|
|
1261
|
-
self.log_text.insert(tk.END, f"{message}\n")
|
|
1262
|
-
|
|
1263
|
-
if hasattr(self, 'auto_scroll_var') and self.auto_scroll_var.get():
|
|
1264
|
-
self.log_text.see(tk.END)
|
|
1265
|
-
|
|
1266
|
-
self.log_text.config(state=tk.DISABLED)
|
|
1267
|
-
|
|
1268
|
-
# Update count label
|
|
1269
|
-
if hasattr(self, 'log_count_label') and hasattr(self, 'all_log_entries'):
|
|
1270
|
-
total = len(self.all_log_entries)
|
|
1271
|
-
visible = sum(1 for _, lvl, _ in self.all_log_entries
|
|
1272
|
-
if self._get_log_level_priority(lvl) >= min_level)
|
|
1273
|
-
if visible == total:
|
|
1274
|
-
self.log_count_label.config(text=f"Entries: {total}")
|
|
1275
|
-
else:
|
|
1276
|
-
self.log_count_label.config(text=f"Entries: {visible}/{total}")
|
|
1277
|
-
|
|
1278
|
-
def _clear_log(self):
|
|
1279
|
-
"""Clear the log display."""
|
|
1280
|
-
if hasattr(self, 'all_log_entries'):
|
|
1281
|
-
self.all_log_entries.clear()
|
|
1282
|
-
|
|
1283
|
-
self.log_text.config(state=tk.NORMAL)
|
|
1284
|
-
self.log_text.delete("1.0", tk.END)
|
|
1285
|
-
self.log_text.config(state=tk.DISABLED)
|
|
1286
|
-
|
|
1287
|
-
# Update count label
|
|
1288
|
-
if hasattr(self, 'log_count_label'):
|
|
1289
|
-
self.log_count_label.config(text="Entries: 0")
|
|
1290
|
-
|
|
1291
|
-
self._add_log("INFO", "Log cleared")
|
|
1292
|
-
|
|
1293
|
-
def start_log_polling(self):
|
|
1294
|
-
"""Start polling the log queue for messages."""
|
|
1295
|
-
self._poll_log_queue()
|
|
1296
|
-
|
|
1297
|
-
def _poll_log_queue(self):
|
|
1298
|
-
"""Poll the log queue and display messages."""
|
|
1299
|
-
try:
|
|
1300
|
-
while True:
|
|
1301
|
-
level, message = self.log_queue.get_nowait()
|
|
1302
|
-
self._add_log(level, message)
|
|
1303
|
-
except queue.Empty:
|
|
1304
|
-
pass
|
|
1305
|
-
|
|
1306
|
-
# Schedule next poll
|
|
1307
|
-
self.after(100, self._poll_log_queue)
|
|
1308
|
-
|
|
1309
|
-
def start_server(self):
|
|
1310
|
-
"""Start the MCP server as a detached subprocess."""
|
|
1311
|
-
if self.server_running:
|
|
1312
|
-
self._add_log("WARNING", "Server is already running")
|
|
1313
|
-
return
|
|
1314
|
-
|
|
1315
|
-
# Create server process manager
|
|
1316
|
-
self.server_process = MCPServerProcess(
|
|
1317
|
-
self.log_queue,
|
|
1318
|
-
self._on_server_started,
|
|
1319
|
-
self._on_server_stopped
|
|
1320
|
-
)
|
|
1321
|
-
|
|
1322
|
-
# Start in detached mode so it persists after Pomera closes
|
|
1323
|
-
if self.server_process.start(detached=True):
|
|
1324
|
-
self._add_log("INFO", "MCP server started in detached mode")
|
|
1325
|
-
self._add_log("INFO", "Server will continue running after Pomera closes")
|
|
1326
|
-
self._add_log("INFO", "The server is now ready to accept connections from Claude Desktop or Cursor")
|
|
1327
|
-
else:
|
|
1328
|
-
self._add_log("ERROR", "Failed to start MCP server")
|
|
1329
|
-
|
|
1330
|
-
def check_existing_server(self):
|
|
1331
|
-
"""Check if an MCP server is already running and update UI accordingly."""
|
|
1332
|
-
pid = find_running_mcp_server()
|
|
1333
|
-
if pid:
|
|
1334
|
-
self._add_log("INFO", f"Detected existing MCP server (PID: {pid})")
|
|
1335
|
-
# Create process manager to track it
|
|
1336
|
-
self.server_process = MCPServerProcess(
|
|
1337
|
-
self.log_queue,
|
|
1338
|
-
self._on_server_started,
|
|
1339
|
-
self._on_server_stopped
|
|
1340
|
-
)
|
|
1341
|
-
self.server_process._external_pid = pid
|
|
1342
|
-
self._update_status(True)
|
|
1343
|
-
return True
|
|
1344
|
-
return False
|
|
1345
|
-
|
|
1346
|
-
def stop_server(self):
|
|
1347
|
-
"""Stop the MCP server subprocess."""
|
|
1348
|
-
if not self.server_running and (not self.server_process or not self.server_process.is_running()):
|
|
1349
|
-
self._add_log("WARNING", "Server is not running")
|
|
1350
|
-
return
|
|
1351
|
-
|
|
1352
|
-
if self.server_process:
|
|
1353
|
-
self.server_process.stop()
|
|
1354
|
-
self.server_process = None
|
|
1355
|
-
|
|
1356
|
-
def _update_status(self, running: bool):
|
|
1357
|
-
"""Update the server status display."""
|
|
1358
|
-
self.server_running = running
|
|
1359
|
-
self._draw_status_indicator(running)
|
|
1360
|
-
|
|
1361
|
-
if running:
|
|
1362
|
-
self.status_label.config(text="Running")
|
|
1363
|
-
self.start_btn.config(state=tk.DISABLED)
|
|
1364
|
-
self.stop_btn.config(state=tk.NORMAL)
|
|
1365
|
-
# Update PID from process manager
|
|
1366
|
-
if self.server_process:
|
|
1367
|
-
pid = self.server_process.get_pid()
|
|
1368
|
-
if pid:
|
|
1369
|
-
self.pid_label.config(text=str(pid))
|
|
1370
|
-
else:
|
|
1371
|
-
self.pid_label.config(text="Running (PID unknown)")
|
|
1372
|
-
else:
|
|
1373
|
-
self.pid_label.config(text="Running")
|
|
1374
|
-
else:
|
|
1375
|
-
self.status_label.config(text="Stopped")
|
|
1376
|
-
self.start_btn.config(state=tk.NORMAL)
|
|
1377
|
-
self.stop_btn.config(state=tk.DISABLED)
|
|
1378
|
-
self.pid_label.config(text="Not running")
|
|
1379
|
-
|
|
1380
|
-
def _on_server_started(self):
|
|
1381
|
-
"""Callback when server starts."""
|
|
1382
|
-
self.after(0, lambda: self._update_status(True))
|
|
1383
|
-
|
|
1384
|
-
def _on_server_stopped(self):
|
|
1385
|
-
"""Callback when server stops."""
|
|
1386
|
-
self.after(0, lambda: self._update_status(False))
|
|
1387
|
-
|
|
1388
|
-
def refresh_tools(self):
|
|
1389
|
-
"""Refresh the tools list."""
|
|
1390
|
-
self._load_registry()
|
|
1391
|
-
self._populate_tools_list()
|
|
1392
|
-
self._populate_test_tool_combo()
|
|
1393
|
-
|
|
1394
|
-
tool_count = len(self.registry) if self.registry else 0
|
|
1395
|
-
self.tools_count_label.config(text=f"{tool_count} available")
|
|
1396
|
-
|
|
1397
|
-
self._add_log("INFO", f"Tools refreshed: {tool_count} tools available")
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
class MCPManager:
|
|
1401
|
-
"""Main class for MCP Manager integration with Pomera."""
|
|
1402
|
-
|
|
1403
|
-
def __init__(self):
|
|
1404
|
-
self.widget = None
|
|
1405
|
-
|
|
1406
|
-
def create_widget(self, parent, app):
|
|
1407
|
-
"""Create and return the MCP Manager widget."""
|
|
1408
|
-
self.widget = MCPManagerWidget(parent, app)
|
|
1409
|
-
return self.widget
|
|
1410
|
-
|
|
1411
|
-
def get_default_settings(self):
|
|
1412
|
-
"""Return default settings for MCP Manager."""
|
|
1413
|
-
return {
|
|
1414
|
-
"auto_start": False,
|
|
1415
|
-
"log_level": "INFO"
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1
|
+
"""
|
|
2
|
+
MCP Manager Widget - UI for MCP Server Control
|
|
3
|
+
|
|
4
|
+
This module provides a graphical interface for managing the MCP server,
|
|
5
|
+
allowing users to start/stop the server and monitor its status.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Start/Stop MCP server (as detached background process)
|
|
9
|
+
- Detect running server on startup
|
|
10
|
+
- View server status and connection info
|
|
11
|
+
- View available tools
|
|
12
|
+
- Server log display
|
|
13
|
+
- Dynamic path configuration for Claude Desktop/Cursor
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import tkinter as tk
|
|
17
|
+
from tkinter import ttk, scrolledtext, messagebox
|
|
18
|
+
import threading
|
|
19
|
+
import queue
|
|
20
|
+
import sys
|
|
21
|
+
import os
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import subprocess
|
|
25
|
+
import signal
|
|
26
|
+
import atexit
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from typing import Optional, Callable
|
|
29
|
+
|
|
30
|
+
# Add project root to path
|
|
31
|
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
32
|
+
if PROJECT_ROOT not in sys.path:
|
|
33
|
+
sys.path.insert(0, PROJECT_ROOT)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# PID file for tracking server process
|
|
37
|
+
PID_FILE = os.path.join(PROJECT_ROOT, ".mcp_server.pid")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_pomera_executable_path() -> tuple:
|
|
41
|
+
"""
|
|
42
|
+
Get the path to the Pomera executable/script for MCP server mode.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Tuple of (command, args) for running the MCP server.
|
|
46
|
+
For frozen exe: ("pomera.exe", ["--mcp-server"])
|
|
47
|
+
For Python: ("python", ["pomera.py", "--mcp-server"])
|
|
48
|
+
"""
|
|
49
|
+
if getattr(sys, 'frozen', False):
|
|
50
|
+
# Running as compiled executable
|
|
51
|
+
exe_path = sys.executable
|
|
52
|
+
return (exe_path, ["--mcp-server"])
|
|
53
|
+
else:
|
|
54
|
+
# Running as Python script
|
|
55
|
+
pomera_script = os.path.join(PROJECT_ROOT, "pomera.py")
|
|
56
|
+
return (sys.executable, [pomera_script, "--mcp-server"])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_mcp_config_json() -> str:
|
|
60
|
+
"""
|
|
61
|
+
Generate MCP configuration JSON with dynamic path.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
JSON string for claude_desktop_config.json or .cursor/mcp.json
|
|
65
|
+
"""
|
|
66
|
+
command, args = get_pomera_executable_path()
|
|
67
|
+
|
|
68
|
+
# Format path for JSON (use forward slashes)
|
|
69
|
+
command = command.replace("\\", "/")
|
|
70
|
+
args = [arg.replace("\\", "/") for arg in args]
|
|
71
|
+
|
|
72
|
+
config = {
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"pomera": {
|
|
75
|
+
"command": command,
|
|
76
|
+
"args": args
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return json.dumps(config, indent=2)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def read_pid_file() -> Optional[int]:
|
|
84
|
+
"""Read the PID from the PID file if it exists."""
|
|
85
|
+
try:
|
|
86
|
+
if os.path.exists(PID_FILE):
|
|
87
|
+
with open(PID_FILE, 'r') as f:
|
|
88
|
+
return int(f.read().strip())
|
|
89
|
+
except (ValueError, IOError):
|
|
90
|
+
pass
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def write_pid_file(pid: int):
|
|
95
|
+
"""Write the PID to the PID file."""
|
|
96
|
+
try:
|
|
97
|
+
with open(PID_FILE, 'w') as f:
|
|
98
|
+
f.write(str(pid))
|
|
99
|
+
except IOError as e:
|
|
100
|
+
logging.getLogger(__name__).error(f"Failed to write PID file: {e}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def remove_pid_file():
|
|
104
|
+
"""Remove the PID file."""
|
|
105
|
+
try:
|
|
106
|
+
if os.path.exists(PID_FILE):
|
|
107
|
+
os.remove(PID_FILE)
|
|
108
|
+
except IOError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_process_running(pid: int) -> bool:
|
|
113
|
+
"""Check if a process with the given PID is running."""
|
|
114
|
+
if pid is None:
|
|
115
|
+
return False
|
|
116
|
+
try:
|
|
117
|
+
if sys.platform == "win32":
|
|
118
|
+
# Windows: use tasklist
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
["tasklist", "/FI", f"PID eq {pid}"],
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True
|
|
123
|
+
)
|
|
124
|
+
return str(pid) in result.stdout
|
|
125
|
+
else:
|
|
126
|
+
# Unix: send signal 0 to check if process exists
|
|
127
|
+
os.kill(pid, 0)
|
|
128
|
+
return True
|
|
129
|
+
except (OSError, subprocess.SubprocessError):
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def find_running_mcp_server() -> Optional[int]:
|
|
134
|
+
"""
|
|
135
|
+
Find a running MCP server process.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
PID of running server, or None if not found
|
|
139
|
+
"""
|
|
140
|
+
# First check PID file
|
|
141
|
+
pid = read_pid_file()
|
|
142
|
+
if pid and is_process_running(pid):
|
|
143
|
+
return pid
|
|
144
|
+
|
|
145
|
+
# Clean up stale PID file
|
|
146
|
+
remove_pid_file()
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class MCPServerProcess:
|
|
151
|
+
"""Manages the MCP server as a detached subprocess that persists after app close."""
|
|
152
|
+
|
|
153
|
+
def __init__(self, log_queue: queue.Queue, on_started: Callable, on_stopped: Callable):
|
|
154
|
+
self.log_queue = log_queue
|
|
155
|
+
self.on_started = on_started
|
|
156
|
+
self.on_stopped = on_stopped
|
|
157
|
+
self.process: Optional[subprocess.Popen] = None
|
|
158
|
+
self.monitor_thread: Optional[threading.Thread] = None
|
|
159
|
+
self._stopping = False
|
|
160
|
+
self._external_pid: Optional[int] = None # PID of externally started server
|
|
161
|
+
|
|
162
|
+
def check_existing_server(self) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Check if a server is already running.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
True if server is running, False otherwise
|
|
168
|
+
"""
|
|
169
|
+
pid = find_running_mcp_server()
|
|
170
|
+
if pid:
|
|
171
|
+
self._external_pid = pid
|
|
172
|
+
self.log_queue.put(("INFO", f"Found existing MCP server (PID: {pid})"))
|
|
173
|
+
return True
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def start(self, detached: bool = False) -> bool:
|
|
177
|
+
"""
|
|
178
|
+
Start the MCP server as a subprocess for testing.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
detached: If True, server runs with minimal parent connection.
|
|
182
|
+
Note: stdio MCP servers are designed to be started by MCP clients
|
|
183
|
+
(Claude Desktop, Cursor). This test mode captures logs for debugging.
|
|
184
|
+
"""
|
|
185
|
+
# Check if already running
|
|
186
|
+
if self.is_running():
|
|
187
|
+
self.log_queue.put(("WARNING", "Server is already running"))
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
command, args = get_pomera_executable_path()
|
|
192
|
+
full_args = [command] + args + ["--debug"]
|
|
193
|
+
|
|
194
|
+
self.log_queue.put(("INFO", f"Starting MCP server: {' '.join(full_args)}"))
|
|
195
|
+
|
|
196
|
+
# Platform-specific process creation
|
|
197
|
+
# Note: We always capture stderr for logging, but stdin/stdout are for MCP protocol
|
|
198
|
+
if sys.platform == "win32":
|
|
199
|
+
creation_flags = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
200
|
+
if detached:
|
|
201
|
+
creation_flags |= subprocess.CREATE_NO_WINDOW
|
|
202
|
+
|
|
203
|
+
self.process = subprocess.Popen(
|
|
204
|
+
full_args,
|
|
205
|
+
stdout=subprocess.PIPE, # Capture MCP responses for logging
|
|
206
|
+
stderr=subprocess.PIPE, # Capture debug logs
|
|
207
|
+
stdin=subprocess.PIPE, # For sending MCP requests (testing)
|
|
208
|
+
creationflags=creation_flags,
|
|
209
|
+
text=True,
|
|
210
|
+
bufsize=1
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
# Unix
|
|
214
|
+
self.process = subprocess.Popen(
|
|
215
|
+
full_args,
|
|
216
|
+
stdout=subprocess.PIPE,
|
|
217
|
+
stderr=subprocess.PIPE,
|
|
218
|
+
stdin=subprocess.PIPE,
|
|
219
|
+
text=True,
|
|
220
|
+
bufsize=1,
|
|
221
|
+
start_new_session=detached
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self._stopping = False
|
|
225
|
+
self._external_pid = None
|
|
226
|
+
|
|
227
|
+
# Write PID file for tracking
|
|
228
|
+
write_pid_file(self.process.pid)
|
|
229
|
+
|
|
230
|
+
# Start monitoring thread to capture stderr logs
|
|
231
|
+
if self.process.stderr:
|
|
232
|
+
self.monitor_thread = threading.Thread(target=self._monitor_process, daemon=True)
|
|
233
|
+
self.monitor_thread.start()
|
|
234
|
+
|
|
235
|
+
self.log_queue.put(("INFO", f"Server started (PID: {self.process.pid})"))
|
|
236
|
+
self.log_queue.put(("INFO", "Server is running in test mode - logs will be captured"))
|
|
237
|
+
self.log_queue.put(("INFO", "Note: For production use, configure your MCP client to start the server"))
|
|
238
|
+
|
|
239
|
+
self.on_started()
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
except Exception as e:
|
|
243
|
+
self.log_queue.put(("ERROR", f"Failed to start server: {str(e)}"))
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
def stop(self):
|
|
247
|
+
"""Stop the MCP server subprocess."""
|
|
248
|
+
pid_to_stop = None
|
|
249
|
+
|
|
250
|
+
if self.process and self.process.poll() is None:
|
|
251
|
+
pid_to_stop = self.process.pid
|
|
252
|
+
elif self._external_pid and is_process_running(self._external_pid):
|
|
253
|
+
pid_to_stop = self._external_pid
|
|
254
|
+
|
|
255
|
+
if not pid_to_stop:
|
|
256
|
+
self.log_queue.put(("WARNING", "No server process to stop"))
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
self._stopping = True
|
|
260
|
+
self.log_queue.put(("INFO", f"Stopping MCP server (PID: {pid_to_stop})..."))
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
if sys.platform == "win32":
|
|
264
|
+
# Windows: use taskkill
|
|
265
|
+
subprocess.run(["taskkill", "/F", "/PID", str(pid_to_stop)],
|
|
266
|
+
capture_output=True)
|
|
267
|
+
else:
|
|
268
|
+
# Unix: send SIGTERM then SIGKILL
|
|
269
|
+
os.kill(pid_to_stop, signal.SIGTERM)
|
|
270
|
+
# Give it a moment to terminate
|
|
271
|
+
import time
|
|
272
|
+
time.sleep(1)
|
|
273
|
+
if is_process_running(pid_to_stop):
|
|
274
|
+
os.kill(pid_to_stop, signal.SIGKILL)
|
|
275
|
+
|
|
276
|
+
self.log_queue.put(("INFO", "Server stopped"))
|
|
277
|
+
except Exception as e:
|
|
278
|
+
self.log_queue.put(("ERROR", f"Error stopping server: {str(e)}"))
|
|
279
|
+
finally:
|
|
280
|
+
self.process = None
|
|
281
|
+
self._external_pid = None
|
|
282
|
+
remove_pid_file()
|
|
283
|
+
self.on_stopped()
|
|
284
|
+
|
|
285
|
+
def _monitor_process(self):
|
|
286
|
+
"""Monitor the server process and capture output."""
|
|
287
|
+
if not self.process or not self.process.stderr:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Read stderr for log messages
|
|
291
|
+
try:
|
|
292
|
+
for line in self.process.stderr:
|
|
293
|
+
if self._stopping:
|
|
294
|
+
break
|
|
295
|
+
line = line.strip()
|
|
296
|
+
if line:
|
|
297
|
+
# Parse log level from line if present
|
|
298
|
+
# Format: "2024-12-06 15:33:17,123 - module - LEVEL - message"
|
|
299
|
+
if " - CRITICAL - " in line:
|
|
300
|
+
self.log_queue.put(("CRITICAL", line))
|
|
301
|
+
elif " - ERROR - " in line:
|
|
302
|
+
self.log_queue.put(("ERROR", line))
|
|
303
|
+
elif " - WARNING - " in line:
|
|
304
|
+
self.log_queue.put(("WARNING", line))
|
|
305
|
+
elif " - DEBUG - " in line:
|
|
306
|
+
self.log_queue.put(("DEBUG", line))
|
|
307
|
+
elif " - INFO - " in line:
|
|
308
|
+
self.log_queue.put(("INFO", line))
|
|
309
|
+
else:
|
|
310
|
+
# Default to INFO for unrecognized format
|
|
311
|
+
self.log_queue.put(("INFO", line))
|
|
312
|
+
except Exception as e:
|
|
313
|
+
self.log_queue.put(("ERROR", f"Error reading server output: {e}"))
|
|
314
|
+
|
|
315
|
+
# Check if process ended unexpectedly
|
|
316
|
+
if self.process and not self._stopping:
|
|
317
|
+
return_code = self.process.poll()
|
|
318
|
+
if return_code is not None:
|
|
319
|
+
self.log_queue.put(("WARNING", f"Server process ended (exit code: {return_code})"))
|
|
320
|
+
remove_pid_file()
|
|
321
|
+
self.on_stopped()
|
|
322
|
+
|
|
323
|
+
def is_running(self) -> bool:
|
|
324
|
+
"""Check if the server is running (either our process or external)."""
|
|
325
|
+
if self.process is not None and self.process.poll() is None:
|
|
326
|
+
return True
|
|
327
|
+
if self._external_pid and is_process_running(self._external_pid):
|
|
328
|
+
return True
|
|
329
|
+
# Also check PID file
|
|
330
|
+
pid = find_running_mcp_server()
|
|
331
|
+
if pid:
|
|
332
|
+
self._external_pid = pid
|
|
333
|
+
return True
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
def get_pid(self) -> Optional[int]:
|
|
337
|
+
"""Get the PID of the running server."""
|
|
338
|
+
if self.process and self.process.poll() is None:
|
|
339
|
+
return self.process.pid
|
|
340
|
+
if self._external_pid and is_process_running(self._external_pid):
|
|
341
|
+
return self._external_pid
|
|
342
|
+
return find_running_mcp_server()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class MCPManagerWidget(ttk.Frame):
|
|
346
|
+
"""
|
|
347
|
+
MCP Manager Widget for controlling the MCP server.
|
|
348
|
+
|
|
349
|
+
Provides UI for:
|
|
350
|
+
- Starting/stopping the server
|
|
351
|
+
- Viewing server status
|
|
352
|
+
- Viewing available tools
|
|
353
|
+
- Displaying server logs
|
|
354
|
+
- Configuration help
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
def __init__(self, parent, app):
|
|
358
|
+
super().__init__(parent)
|
|
359
|
+
self.app = app
|
|
360
|
+
self.logger = app.logger if hasattr(app, 'logger') else logging.getLogger(__name__)
|
|
361
|
+
|
|
362
|
+
# Server state
|
|
363
|
+
self.server_process: Optional[MCPServerProcess] = None
|
|
364
|
+
self.server_running = False
|
|
365
|
+
self.log_queue = queue.Queue()
|
|
366
|
+
|
|
367
|
+
# Tool registry for display
|
|
368
|
+
self.registry = None
|
|
369
|
+
self._load_registry()
|
|
370
|
+
|
|
371
|
+
self.create_widgets()
|
|
372
|
+
self.start_log_polling()
|
|
373
|
+
|
|
374
|
+
# Check for existing server after UI is ready
|
|
375
|
+
self.after(500, self.check_existing_server)
|
|
376
|
+
|
|
377
|
+
def _load_registry(self):
|
|
378
|
+
"""Load tool registry for display purposes."""
|
|
379
|
+
try:
|
|
380
|
+
from core.mcp.tool_registry import ToolRegistry
|
|
381
|
+
self.registry = ToolRegistry()
|
|
382
|
+
except Exception as e:
|
|
383
|
+
self.logger.error(f"Failed to load tool registry: {e}")
|
|
384
|
+
self.registry = None
|
|
385
|
+
|
|
386
|
+
def create_widgets(self):
|
|
387
|
+
"""Create the widget interface."""
|
|
388
|
+
# Main container with notebook
|
|
389
|
+
self.notebook = ttk.Notebook(self)
|
|
390
|
+
self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
391
|
+
|
|
392
|
+
# Server Control Tab
|
|
393
|
+
self.control_frame = ttk.Frame(self.notebook)
|
|
394
|
+
self.notebook.add(self.control_frame, text="Server Control")
|
|
395
|
+
self.create_control_tab()
|
|
396
|
+
|
|
397
|
+
# Tools Tab
|
|
398
|
+
self.tools_frame = ttk.Frame(self.notebook)
|
|
399
|
+
self.notebook.add(self.tools_frame, text="Available Tools")
|
|
400
|
+
self.create_tools_tab()
|
|
401
|
+
|
|
402
|
+
# Configuration Tab
|
|
403
|
+
self.config_frame = ttk.Frame(self.notebook)
|
|
404
|
+
self.notebook.add(self.config_frame, text="Configuration")
|
|
405
|
+
self.create_config_tab()
|
|
406
|
+
|
|
407
|
+
# Test Tool Tab
|
|
408
|
+
self.test_frame = ttk.Frame(self.notebook)
|
|
409
|
+
self.notebook.add(self.test_frame, text="Test Tool")
|
|
410
|
+
self.create_test_tab()
|
|
411
|
+
|
|
412
|
+
# Log Tab
|
|
413
|
+
self.log_frame = ttk.Frame(self.notebook)
|
|
414
|
+
self.notebook.add(self.log_frame, text="Server Log")
|
|
415
|
+
self.create_log_tab()
|
|
416
|
+
|
|
417
|
+
def create_control_tab(self):
|
|
418
|
+
"""Create the server control tab."""
|
|
419
|
+
# Status frame
|
|
420
|
+
status_frame = ttk.LabelFrame(self.control_frame, text="Server Status", padding=10)
|
|
421
|
+
status_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
422
|
+
|
|
423
|
+
# Status indicator
|
|
424
|
+
status_row = ttk.Frame(status_frame)
|
|
425
|
+
status_row.pack(fill=tk.X, pady=5)
|
|
426
|
+
|
|
427
|
+
ttk.Label(status_row, text="Status:").pack(side=tk.LEFT, padx=(0, 10))
|
|
428
|
+
|
|
429
|
+
self.status_indicator = tk.Canvas(status_row, width=16, height=16, highlightthickness=0)
|
|
430
|
+
self.status_indicator.pack(side=tk.LEFT, padx=(0, 5))
|
|
431
|
+
self._draw_status_indicator(False)
|
|
432
|
+
|
|
433
|
+
self.status_label = ttk.Label(status_row, text="Stopped", font=("", 10, "bold"))
|
|
434
|
+
self.status_label.pack(side=tk.LEFT)
|
|
435
|
+
|
|
436
|
+
# Server info
|
|
437
|
+
info_frame = ttk.Frame(status_frame)
|
|
438
|
+
info_frame.pack(fill=tk.X, pady=5)
|
|
439
|
+
|
|
440
|
+
ttk.Label(info_frame, text="Server:").pack(side=tk.LEFT, padx=(0, 10))
|
|
441
|
+
ttk.Label(info_frame, text="pomera-mcp-server v0.1.0").pack(side=tk.LEFT)
|
|
442
|
+
|
|
443
|
+
tools_frame = ttk.Frame(status_frame)
|
|
444
|
+
tools_frame.pack(fill=tk.X, pady=5)
|
|
445
|
+
|
|
446
|
+
ttk.Label(tools_frame, text="Tools:").pack(side=tk.LEFT, padx=(0, 10))
|
|
447
|
+
tool_count = len(self.registry) if self.registry else 0
|
|
448
|
+
self.tools_count_label = ttk.Label(tools_frame, text=f"{tool_count} available")
|
|
449
|
+
self.tools_count_label.pack(side=tk.LEFT)
|
|
450
|
+
|
|
451
|
+
# PID display
|
|
452
|
+
pid_frame = ttk.Frame(status_frame)
|
|
453
|
+
pid_frame.pack(fill=tk.X, pady=5)
|
|
454
|
+
|
|
455
|
+
ttk.Label(pid_frame, text="Process ID:").pack(side=tk.LEFT, padx=(0, 10))
|
|
456
|
+
self.pid_label = ttk.Label(pid_frame, text="Not running")
|
|
457
|
+
self.pid_label.pack(side=tk.LEFT)
|
|
458
|
+
|
|
459
|
+
# Control buttons
|
|
460
|
+
button_frame = ttk.LabelFrame(self.control_frame, text="Controls", padding=10)
|
|
461
|
+
button_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
462
|
+
|
|
463
|
+
btn_row = ttk.Frame(button_frame)
|
|
464
|
+
btn_row.pack(fill=tk.X)
|
|
465
|
+
|
|
466
|
+
self.start_btn = ttk.Button(btn_row, text="▶ Start Server", command=self.start_server, width=15)
|
|
467
|
+
self.start_btn.pack(side=tk.LEFT, padx=5)
|
|
468
|
+
|
|
469
|
+
self.stop_btn = ttk.Button(btn_row, text="■ Stop Server", command=self.stop_server, width=15, state=tk.DISABLED)
|
|
470
|
+
self.stop_btn.pack(side=tk.LEFT, padx=5)
|
|
471
|
+
|
|
472
|
+
ttk.Button(btn_row, text="🔄 Refresh Tools", command=self.refresh_tools, width=15).pack(side=tk.LEFT, padx=5)
|
|
473
|
+
|
|
474
|
+
# Info note
|
|
475
|
+
note_frame = ttk.LabelFrame(self.control_frame, text="How to Use", padding=10)
|
|
476
|
+
note_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
477
|
+
|
|
478
|
+
note_text = (
|
|
479
|
+
"The MCP server exposes Pomera's text tools via the Model Context Protocol.\n\n"
|
|
480
|
+
"Option 1: Start from here (for testing/debugging)\n"
|
|
481
|
+
" • Click 'Start Server' to test the MCP server\n"
|
|
482
|
+
" • Server logs will be captured in the 'Server Log' tab\n"
|
|
483
|
+
" • Server will stop when Pomera closes (stdio transport limitation)\n\n"
|
|
484
|
+
"Option 2: Configure your AI client (RECOMMENDED for production)\n"
|
|
485
|
+
" • Copy the configuration from the 'Configuration' tab\n"
|
|
486
|
+
" • Add it to your Claude Desktop or Cursor config\n"
|
|
487
|
+
" • The client will start/stop the server automatically as needed\n"
|
|
488
|
+
" • This is the standard way to use MCP stdio servers"
|
|
489
|
+
)
|
|
490
|
+
ttk.Label(note_frame, text=note_text, wraplength=500, justify=tk.LEFT).pack(anchor=tk.W)
|
|
491
|
+
|
|
492
|
+
def create_tools_tab(self):
|
|
493
|
+
"""Create the available tools tab."""
|
|
494
|
+
# Use PanedWindow for resizable split between list and details
|
|
495
|
+
paned = ttk.PanedWindow(self.tools_frame, orient=tk.VERTICAL)
|
|
496
|
+
paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
497
|
+
|
|
498
|
+
# Top pane: Tools list with scrollbar
|
|
499
|
+
list_frame = ttk.Frame(paned)
|
|
500
|
+
|
|
501
|
+
# Treeview for tools
|
|
502
|
+
columns = ("name", "description")
|
|
503
|
+
self.tools_tree = ttk.Treeview(list_frame, columns=columns, show="headings")
|
|
504
|
+
|
|
505
|
+
self.tools_tree.heading("name", text="Tool Name")
|
|
506
|
+
self.tools_tree.heading("description", text="Description")
|
|
507
|
+
|
|
508
|
+
self.tools_tree.column("name", width=200, minwidth=150)
|
|
509
|
+
self.tools_tree.column("description", width=400, minwidth=200)
|
|
510
|
+
|
|
511
|
+
# Scrollbar for treeview
|
|
512
|
+
tree_scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tools_tree.yview)
|
|
513
|
+
self.tools_tree.configure(yscrollcommand=tree_scrollbar.set)
|
|
514
|
+
|
|
515
|
+
self.tools_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
516
|
+
tree_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
517
|
+
|
|
518
|
+
paned.add(list_frame, weight=1)
|
|
519
|
+
|
|
520
|
+
# Populate tools
|
|
521
|
+
self._populate_tools_list()
|
|
522
|
+
|
|
523
|
+
# Bottom pane: Tool details
|
|
524
|
+
details_frame = ttk.LabelFrame(paned, text="Tool Details", padding=10)
|
|
525
|
+
|
|
526
|
+
self.tool_details = scrolledtext.ScrolledText(details_frame, wrap=tk.WORD, state=tk.DISABLED)
|
|
527
|
+
self.tool_details.pack(fill=tk.BOTH, expand=True)
|
|
528
|
+
|
|
529
|
+
paned.add(details_frame, weight=1)
|
|
530
|
+
|
|
531
|
+
# Bind selection
|
|
532
|
+
self.tools_tree.bind("<<TreeviewSelect>>", self._on_tool_select)
|
|
533
|
+
|
|
534
|
+
def create_config_tab(self):
|
|
535
|
+
"""Create the configuration tab."""
|
|
536
|
+
# Get dynamic configuration
|
|
537
|
+
command, args = get_pomera_executable_path()
|
|
538
|
+
|
|
539
|
+
# Path selection frame
|
|
540
|
+
path_frame = ttk.LabelFrame(self.config_frame, text="Pomera Path Configuration", padding=10)
|
|
541
|
+
path_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
542
|
+
|
|
543
|
+
# Execution mode label
|
|
544
|
+
if getattr(sys, 'frozen', False):
|
|
545
|
+
mode_text = "Mode: Compiled executable"
|
|
546
|
+
default_path = command
|
|
547
|
+
else:
|
|
548
|
+
mode_text = "Mode: Python script"
|
|
549
|
+
default_path = args[0] if args else os.path.join(PROJECT_ROOT, "pomera.py")
|
|
550
|
+
|
|
551
|
+
ttk.Label(path_frame, text=mode_text, font=("", 9, "bold")).pack(anchor=tk.W)
|
|
552
|
+
|
|
553
|
+
# Path entry row
|
|
554
|
+
path_row = ttk.Frame(path_frame)
|
|
555
|
+
path_row.pack(fill=tk.X, pady=(5, 0))
|
|
556
|
+
|
|
557
|
+
ttk.Label(path_row, text="Path:").pack(side=tk.LEFT, padx=(0, 5))
|
|
558
|
+
|
|
559
|
+
self.path_var = tk.StringVar(value=default_path)
|
|
560
|
+
self.path_entry = ttk.Entry(path_row, textvariable=self.path_var, width=60)
|
|
561
|
+
self.path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
|
|
562
|
+
|
|
563
|
+
ttk.Button(path_row, text="Select...", command=self._select_pomera_path).pack(side=tk.LEFT, padx=(0, 5))
|
|
564
|
+
ttk.Button(path_row, text="Apply", command=self._apply_path_config).pack(side=tk.LEFT)
|
|
565
|
+
|
|
566
|
+
# Python executable row (only for script mode)
|
|
567
|
+
if not getattr(sys, 'frozen', False):
|
|
568
|
+
python_row = ttk.Frame(path_frame)
|
|
569
|
+
python_row.pack(fill=tk.X, pady=(5, 0))
|
|
570
|
+
|
|
571
|
+
ttk.Label(python_row, text="Python:").pack(side=tk.LEFT, padx=(0, 5))
|
|
572
|
+
|
|
573
|
+
self.python_var = tk.StringVar(value=sys.executable)
|
|
574
|
+
self.python_entry = ttk.Entry(python_row, textvariable=self.python_var, width=60)
|
|
575
|
+
self.python_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
|
|
576
|
+
|
|
577
|
+
ttk.Button(python_row, text="Select...", command=self._select_python_path).pack(side=tk.LEFT)
|
|
578
|
+
|
|
579
|
+
# Claude Desktop config
|
|
580
|
+
claude_frame = ttk.LabelFrame(self.config_frame, text="Claude Desktop Configuration", padding=10)
|
|
581
|
+
claude_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
582
|
+
|
|
583
|
+
ttk.Label(claude_frame, text="Add to claude_desktop_config.json:").pack(anchor=tk.W)
|
|
584
|
+
|
|
585
|
+
self.claude_config = scrolledtext.ScrolledText(claude_frame, height=8, wrap=tk.WORD)
|
|
586
|
+
self.claude_config.pack(fill=tk.X, pady=5)
|
|
587
|
+
|
|
588
|
+
claude_btn_row = ttk.Frame(claude_frame)
|
|
589
|
+
claude_btn_row.pack(fill=tk.X)
|
|
590
|
+
ttk.Button(claude_btn_row, text="Copy to Clipboard",
|
|
591
|
+
command=self._copy_claude_config).pack(side=tk.LEFT)
|
|
592
|
+
|
|
593
|
+
# Cursor config
|
|
594
|
+
cursor_frame = ttk.LabelFrame(self.config_frame, text="Cursor Configuration", padding=10)
|
|
595
|
+
cursor_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
596
|
+
|
|
597
|
+
ttk.Label(cursor_frame, text="Add to .cursor/mcp.json in your project:").pack(anchor=tk.W)
|
|
598
|
+
|
|
599
|
+
self.cursor_config = scrolledtext.ScrolledText(cursor_frame, height=8, wrap=tk.WORD)
|
|
600
|
+
self.cursor_config.pack(fill=tk.X, pady=5)
|
|
601
|
+
|
|
602
|
+
cursor_btn_row = ttk.Frame(cursor_frame)
|
|
603
|
+
cursor_btn_row.pack(fill=tk.X)
|
|
604
|
+
ttk.Button(cursor_btn_row, text="Copy to Clipboard",
|
|
605
|
+
command=self._copy_cursor_config).pack(side=tk.LEFT)
|
|
606
|
+
|
|
607
|
+
# Command line usage
|
|
608
|
+
cli_frame = ttk.LabelFrame(self.config_frame, text="Command Line Usage", padding=10)
|
|
609
|
+
cli_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
610
|
+
|
|
611
|
+
self.cli_config = scrolledtext.ScrolledText(cli_frame, height=6, wrap=tk.WORD)
|
|
612
|
+
self.cli_config.pack(fill=tk.X, pady=5)
|
|
613
|
+
|
|
614
|
+
# Initial population of config text areas
|
|
615
|
+
self._update_config_displays()
|
|
616
|
+
|
|
617
|
+
def _select_pomera_path(self):
|
|
618
|
+
"""Open file dialog to select Pomera executable or script."""
|
|
619
|
+
from tkinter import filedialog
|
|
620
|
+
|
|
621
|
+
if getattr(sys, 'frozen', False):
|
|
622
|
+
filetypes = [("Executable", "*.exe"), ("All files", "*.*")]
|
|
623
|
+
title = "Select pomera.exe"
|
|
624
|
+
else:
|
|
625
|
+
filetypes = [("Python script", "*.py"), ("All files", "*.*")]
|
|
626
|
+
title = "Select pomera.py"
|
|
627
|
+
|
|
628
|
+
path = filedialog.askopenfilename(
|
|
629
|
+
title=title,
|
|
630
|
+
filetypes=filetypes,
|
|
631
|
+
initialdir=os.path.dirname(self.path_var.get()) if self.path_var.get() else PROJECT_ROOT
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
if path:
|
|
635
|
+
self.path_var.set(path)
|
|
636
|
+
self._update_config_displays()
|
|
637
|
+
|
|
638
|
+
def _select_python_path(self):
|
|
639
|
+
"""Open file dialog to select Python executable."""
|
|
640
|
+
from tkinter import filedialog
|
|
641
|
+
|
|
642
|
+
if sys.platform == "win32":
|
|
643
|
+
filetypes = [("Executable", "*.exe"), ("All files", "*.*")]
|
|
644
|
+
else:
|
|
645
|
+
filetypes = [("All files", "*.*")]
|
|
646
|
+
|
|
647
|
+
path = filedialog.askopenfilename(
|
|
648
|
+
title="Select Python executable",
|
|
649
|
+
filetypes=filetypes,
|
|
650
|
+
initialdir=os.path.dirname(self.python_var.get()) if hasattr(self, 'python_var') and self.python_var.get() else None
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
if path:
|
|
654
|
+
self.python_var.set(path)
|
|
655
|
+
self._update_config_displays()
|
|
656
|
+
|
|
657
|
+
def _apply_path_config(self):
|
|
658
|
+
"""Apply the current path configuration and update displays."""
|
|
659
|
+
self._update_config_displays()
|
|
660
|
+
self._add_log("INFO", "Configuration updated with new path")
|
|
661
|
+
|
|
662
|
+
def _generate_config_json(self) -> str:
|
|
663
|
+
"""Generate MCP configuration JSON based on current path settings."""
|
|
664
|
+
pomera_path = self.path_var.get().replace("\\", "/")
|
|
665
|
+
|
|
666
|
+
if getattr(sys, 'frozen', False):
|
|
667
|
+
# Compiled executable mode
|
|
668
|
+
config = {
|
|
669
|
+
"mcpServers": {
|
|
670
|
+
"pomera": {
|
|
671
|
+
"command": pomera_path,
|
|
672
|
+
"args": ["--mcp-server"]
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
else:
|
|
677
|
+
# Python script mode
|
|
678
|
+
python_path = self.python_var.get().replace("\\", "/") if hasattr(self, 'python_var') else sys.executable.replace("\\", "/")
|
|
679
|
+
config = {
|
|
680
|
+
"mcpServers": {
|
|
681
|
+
"pomera": {
|
|
682
|
+
"command": python_path,
|
|
683
|
+
"args": [pomera_path, "--mcp-server"]
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return json.dumps(config, indent=2)
|
|
689
|
+
|
|
690
|
+
def _generate_cli_text(self) -> str:
|
|
691
|
+
"""Generate CLI usage text based on current path settings."""
|
|
692
|
+
pomera_path = self.path_var.get()
|
|
693
|
+
|
|
694
|
+
if getattr(sys, 'frozen', False):
|
|
695
|
+
return f"""# List available tools:
|
|
696
|
+
"{pomera_path}" --mcp-server --list-tools
|
|
697
|
+
|
|
698
|
+
# Run server (for manual testing):
|
|
699
|
+
"{pomera_path}" --mcp-server
|
|
700
|
+
|
|
701
|
+
# Run with debug logging:
|
|
702
|
+
"{pomera_path}" --mcp-server --debug"""
|
|
703
|
+
else:
|
|
704
|
+
python_path = self.python_var.get() if hasattr(self, 'python_var') else sys.executable
|
|
705
|
+
return f"""# List available tools:
|
|
706
|
+
"{python_path}" "{pomera_path}" --mcp-server --list-tools
|
|
707
|
+
|
|
708
|
+
# Run server (for manual testing):
|
|
709
|
+
"{python_path}" "{pomera_path}" --mcp-server
|
|
710
|
+
|
|
711
|
+
# Run with debug logging:
|
|
712
|
+
"{python_path}" "{pomera_path}" --mcp-server --debug"""
|
|
713
|
+
|
|
714
|
+
def _update_config_displays(self):
|
|
715
|
+
"""Update all configuration text displays with current path settings."""
|
|
716
|
+
config_json = self._generate_config_json()
|
|
717
|
+
cli_text = self._generate_cli_text()
|
|
718
|
+
|
|
719
|
+
# Update Claude config
|
|
720
|
+
self.claude_config.config(state=tk.NORMAL)
|
|
721
|
+
self.claude_config.delete("1.0", tk.END)
|
|
722
|
+
self.claude_config.insert(tk.END, config_json)
|
|
723
|
+
self.claude_config.config(state=tk.DISABLED)
|
|
724
|
+
|
|
725
|
+
# Update Cursor config
|
|
726
|
+
self.cursor_config.config(state=tk.NORMAL)
|
|
727
|
+
self.cursor_config.delete("1.0", tk.END)
|
|
728
|
+
self.cursor_config.insert(tk.END, config_json)
|
|
729
|
+
self.cursor_config.config(state=tk.DISABLED)
|
|
730
|
+
|
|
731
|
+
# Update CLI text
|
|
732
|
+
self.cli_config.config(state=tk.NORMAL)
|
|
733
|
+
self.cli_config.delete("1.0", tk.END)
|
|
734
|
+
self.cli_config.insert(tk.END, cli_text)
|
|
735
|
+
self.cli_config.config(state=tk.DISABLED)
|
|
736
|
+
|
|
737
|
+
def _copy_claude_config(self):
|
|
738
|
+
"""Copy Claude Desktop config to clipboard."""
|
|
739
|
+
config = self._generate_config_json()
|
|
740
|
+
self._copy_to_clipboard(config)
|
|
741
|
+
|
|
742
|
+
def _copy_cursor_config(self):
|
|
743
|
+
"""Copy Cursor config to clipboard."""
|
|
744
|
+
config = self._generate_config_json()
|
|
745
|
+
self._copy_to_clipboard(config)
|
|
746
|
+
|
|
747
|
+
def create_test_tab(self):
|
|
748
|
+
"""Create the test tool tab for sending MCP requests."""
|
|
749
|
+
# Tool selection frame
|
|
750
|
+
select_frame = ttk.LabelFrame(self.test_frame, text="Select Tool", padding=10)
|
|
751
|
+
select_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
752
|
+
|
|
753
|
+
# Tool dropdown
|
|
754
|
+
tool_row = ttk.Frame(select_frame)
|
|
755
|
+
tool_row.pack(fill=tk.X)
|
|
756
|
+
|
|
757
|
+
ttk.Label(tool_row, text="Tool:").pack(side=tk.LEFT, padx=(0, 5))
|
|
758
|
+
|
|
759
|
+
self.test_tool_var = tk.StringVar()
|
|
760
|
+
self.test_tool_combo = ttk.Combobox(
|
|
761
|
+
tool_row,
|
|
762
|
+
textvariable=self.test_tool_var,
|
|
763
|
+
state="readonly",
|
|
764
|
+
width=40
|
|
765
|
+
)
|
|
766
|
+
self.test_tool_combo.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
|
|
767
|
+
self.test_tool_combo.bind("<<ComboboxSelected>>", self._on_test_tool_select)
|
|
768
|
+
|
|
769
|
+
# Parameters frame
|
|
770
|
+
params_frame = ttk.LabelFrame(self.test_frame, text="Parameters (JSON)", padding=10)
|
|
771
|
+
params_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
772
|
+
|
|
773
|
+
# Parameter hints label
|
|
774
|
+
self.param_hints_label = ttk.Label(params_frame, text="Select a tool to see required parameters", foreground="gray")
|
|
775
|
+
self.param_hints_label.pack(anchor=tk.W, pady=(0, 5))
|
|
776
|
+
|
|
777
|
+
# Parameters text area
|
|
778
|
+
self.test_params = scrolledtext.ScrolledText(params_frame, height=8, wrap=tk.WORD)
|
|
779
|
+
self.test_params.pack(fill=tk.BOTH, expand=True)
|
|
780
|
+
self.test_params.insert(tk.END, "{}")
|
|
781
|
+
|
|
782
|
+
# Execute button frame
|
|
783
|
+
exec_frame = ttk.Frame(self.test_frame)
|
|
784
|
+
exec_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
785
|
+
|
|
786
|
+
self.test_execute_btn = ttk.Button(
|
|
787
|
+
exec_frame,
|
|
788
|
+
text="▶ Execute Tool",
|
|
789
|
+
command=self._execute_test_tool,
|
|
790
|
+
width=20
|
|
791
|
+
)
|
|
792
|
+
self.test_execute_btn.pack(side=tk.LEFT)
|
|
793
|
+
|
|
794
|
+
ttk.Button(
|
|
795
|
+
exec_frame,
|
|
796
|
+
text="Clear Result",
|
|
797
|
+
command=self._clear_test_result,
|
|
798
|
+
width=15
|
|
799
|
+
).pack(side=tk.LEFT, padx=(10, 0))
|
|
800
|
+
|
|
801
|
+
# Server-only checkbox
|
|
802
|
+
self.test_server_only_var = tk.BooleanVar(value=False)
|
|
803
|
+
self.test_server_only_cb = ttk.Checkbutton(
|
|
804
|
+
exec_frame,
|
|
805
|
+
text="Test via server only",
|
|
806
|
+
variable=self.test_server_only_var
|
|
807
|
+
)
|
|
808
|
+
self.test_server_only_cb.pack(side=tk.LEFT, padx=(20, 0))
|
|
809
|
+
|
|
810
|
+
# Status label
|
|
811
|
+
self.test_status_label = ttk.Label(exec_frame, text="")
|
|
812
|
+
self.test_status_label.pack(side=tk.RIGHT)
|
|
813
|
+
|
|
814
|
+
# Result frame
|
|
815
|
+
result_frame = ttk.LabelFrame(self.test_frame, text="Result", padding=10)
|
|
816
|
+
result_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
817
|
+
|
|
818
|
+
self.test_result = scrolledtext.ScrolledText(result_frame, height=10, wrap=tk.WORD, state=tk.DISABLED)
|
|
819
|
+
self.test_result.pack(fill=tk.BOTH, expand=True)
|
|
820
|
+
|
|
821
|
+
# Configure tags for result display
|
|
822
|
+
self.test_result.tag_configure("success", foreground="green")
|
|
823
|
+
self.test_result.tag_configure("error", foreground="red")
|
|
824
|
+
self.test_result.tag_configure("info", foreground="blue")
|
|
825
|
+
|
|
826
|
+
# Populate tool dropdown (after all widgets are created)
|
|
827
|
+
self._populate_test_tool_combo()
|
|
828
|
+
|
|
829
|
+
def _populate_test_tool_combo(self):
|
|
830
|
+
"""Populate the test tool dropdown with available tools."""
|
|
831
|
+
if self.registry:
|
|
832
|
+
tool_names = [tool.name for tool in self.registry.list_tools()]
|
|
833
|
+
self.test_tool_combo['values'] = sorted(tool_names)
|
|
834
|
+
if tool_names:
|
|
835
|
+
self.test_tool_combo.set(sorted(tool_names)[0])
|
|
836
|
+
self._on_test_tool_select(None)
|
|
837
|
+
|
|
838
|
+
def _on_test_tool_select(self, event):
|
|
839
|
+
"""Handle tool selection in test tab - show parameter hints."""
|
|
840
|
+
tool_name = self.test_tool_var.get()
|
|
841
|
+
if not tool_name or not self.registry:
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
# Check if UI elements exist yet
|
|
845
|
+
if not hasattr(self, 'param_hints_label') or not hasattr(self, 'test_params'):
|
|
846
|
+
return
|
|
847
|
+
|
|
848
|
+
tool = self.registry.get_tool(tool_name)
|
|
849
|
+
if not tool:
|
|
850
|
+
return
|
|
851
|
+
|
|
852
|
+
mcp_tool = tool.to_mcp_tool()
|
|
853
|
+
schema = mcp_tool.inputSchema
|
|
854
|
+
|
|
855
|
+
# Build parameter hints
|
|
856
|
+
properties = schema.get("properties", {})
|
|
857
|
+
required = schema.get("required", [])
|
|
858
|
+
|
|
859
|
+
hints = []
|
|
860
|
+
for param_name, param_info in properties.items():
|
|
861
|
+
param_type = param_info.get("type", "any")
|
|
862
|
+
param_desc = param_info.get("description", "")
|
|
863
|
+
is_required = param_name in required
|
|
864
|
+
req_marker = "*" if is_required else ""
|
|
865
|
+
hints.append(f" • {param_name}{req_marker} ({param_type}): {param_desc[:50]}...")
|
|
866
|
+
|
|
867
|
+
if hints:
|
|
868
|
+
hint_text = "Parameters (* = required):\n" + "\n".join(hints[:5])
|
|
869
|
+
if len(hints) > 5:
|
|
870
|
+
hint_text += f"\n ... and {len(hints) - 5} more"
|
|
871
|
+
else:
|
|
872
|
+
hint_text = "No parameters required"
|
|
873
|
+
|
|
874
|
+
self.param_hints_label.config(text=hint_text)
|
|
875
|
+
|
|
876
|
+
# Generate example JSON with ALL parameters (required + optional with defaults)
|
|
877
|
+
example = {}
|
|
878
|
+
for param_name, param_info in properties.items():
|
|
879
|
+
param_type = param_info.get("type", "string")
|
|
880
|
+
is_required = param_name in required
|
|
881
|
+
|
|
882
|
+
# Use default value if available, otherwise generate placeholder
|
|
883
|
+
if "default" in param_info:
|
|
884
|
+
example[param_name] = param_info["default"]
|
|
885
|
+
elif "enum" in param_info:
|
|
886
|
+
example[param_name] = param_info["enum"][0]
|
|
887
|
+
elif param_type == "string":
|
|
888
|
+
example[param_name] = f"<{param_name}>" if is_required else ""
|
|
889
|
+
elif param_type == "integer":
|
|
890
|
+
example[param_name] = 0
|
|
891
|
+
elif param_type == "number":
|
|
892
|
+
example[param_name] = 0
|
|
893
|
+
elif param_type == "boolean":
|
|
894
|
+
example[param_name] = False
|
|
895
|
+
else:
|
|
896
|
+
example[param_name] = f"<{param_name}>" if is_required else None
|
|
897
|
+
|
|
898
|
+
self.test_params.delete("1.0", tk.END)
|
|
899
|
+
self.test_params.insert(tk.END, json.dumps(example, indent=2))
|
|
900
|
+
|
|
901
|
+
def _execute_test_tool(self):
|
|
902
|
+
"""Execute the selected tool with the provided parameters."""
|
|
903
|
+
tool_name = self.test_tool_var.get()
|
|
904
|
+
if not tool_name:
|
|
905
|
+
self._show_test_error("Please select a tool")
|
|
906
|
+
return
|
|
907
|
+
|
|
908
|
+
# Parse parameters
|
|
909
|
+
try:
|
|
910
|
+
params_text = self.test_params.get("1.0", tk.END).strip()
|
|
911
|
+
params = json.loads(params_text) if params_text else {}
|
|
912
|
+
except json.JSONDecodeError as e:
|
|
913
|
+
self._show_test_error(f"Invalid JSON parameters: {e}")
|
|
914
|
+
return
|
|
915
|
+
|
|
916
|
+
# Check if "server only" mode is enabled
|
|
917
|
+
server_only = self.test_server_only_var.get()
|
|
918
|
+
server_running = self.server_process and self.server_process.is_running() and self.server_process.process
|
|
919
|
+
|
|
920
|
+
if server_only and not server_running:
|
|
921
|
+
self._show_test_error("Server is not running. Start the server first or uncheck 'Test via server only'.")
|
|
922
|
+
return
|
|
923
|
+
|
|
924
|
+
# Check if server is running
|
|
925
|
+
if server_running:
|
|
926
|
+
# Send request to running server via stdin
|
|
927
|
+
self._execute_via_server(tool_name, params)
|
|
928
|
+
else:
|
|
929
|
+
# Execute directly via registry
|
|
930
|
+
self._execute_directly(tool_name, params)
|
|
931
|
+
|
|
932
|
+
def _execute_via_server(self, tool_name: str, params: dict):
|
|
933
|
+
"""Execute tool by sending request to the running server's stdin."""
|
|
934
|
+
self.test_status_label.config(text="Sending request...", foreground="blue")
|
|
935
|
+
self.update_idletasks()
|
|
936
|
+
|
|
937
|
+
try:
|
|
938
|
+
# Build MCP request
|
|
939
|
+
request = {
|
|
940
|
+
"jsonrpc": "2.0",
|
|
941
|
+
"id": 1,
|
|
942
|
+
"method": "tools/call",
|
|
943
|
+
"params": {
|
|
944
|
+
"name": tool_name,
|
|
945
|
+
"arguments": params
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
# Need to send initialize first if not already done
|
|
950
|
+
init_request = {
|
|
951
|
+
"jsonrpc": "2.0",
|
|
952
|
+
"id": 0,
|
|
953
|
+
"method": "initialize",
|
|
954
|
+
"params": {
|
|
955
|
+
"protocolVersion": "2024-11-05",
|
|
956
|
+
"capabilities": {},
|
|
957
|
+
"clientInfo": {"name": "pomera-test", "version": "1.0"}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
# Send requests
|
|
962
|
+
process = self.server_process.process
|
|
963
|
+
if process and process.stdin and process.stdout:
|
|
964
|
+
# Send initialize
|
|
965
|
+
process.stdin.write(json.dumps(init_request) + "\n")
|
|
966
|
+
process.stdin.flush()
|
|
967
|
+
|
|
968
|
+
# Read init response
|
|
969
|
+
init_response = process.stdout.readline()
|
|
970
|
+
|
|
971
|
+
# Send tool call
|
|
972
|
+
process.stdin.write(json.dumps(request) + "\n")
|
|
973
|
+
process.stdin.flush()
|
|
974
|
+
|
|
975
|
+
# Read response with timeout
|
|
976
|
+
response_line = process.stdout.readline()
|
|
977
|
+
if response_line:
|
|
978
|
+
response = json.loads(response_line)
|
|
979
|
+
self._show_test_result(response)
|
|
980
|
+
else:
|
|
981
|
+
self._show_test_error("No response from server")
|
|
982
|
+
else:
|
|
983
|
+
self._show_test_error("Server stdin/stdout not available")
|
|
984
|
+
|
|
985
|
+
except Exception as e:
|
|
986
|
+
self._show_test_error(f"Error communicating with server: {e}")
|
|
987
|
+
|
|
988
|
+
def _execute_directly(self, tool_name: str, params: dict):
|
|
989
|
+
"""Execute tool directly via the registry (no server needed)."""
|
|
990
|
+
self.test_status_label.config(text="Executing directly...", foreground="blue")
|
|
991
|
+
self.update_idletasks()
|
|
992
|
+
|
|
993
|
+
if not self.registry:
|
|
994
|
+
self._show_test_error("Tool registry not available")
|
|
995
|
+
return
|
|
996
|
+
|
|
997
|
+
try:
|
|
998
|
+
result = self.registry.execute(tool_name, params)
|
|
999
|
+
|
|
1000
|
+
# Format result
|
|
1001
|
+
response = {
|
|
1002
|
+
"success": not result.isError,
|
|
1003
|
+
"content": result.content
|
|
1004
|
+
}
|
|
1005
|
+
self._show_test_result(response)
|
|
1006
|
+
|
|
1007
|
+
except Exception as e:
|
|
1008
|
+
self._show_test_error(f"Execution error: {e}")
|
|
1009
|
+
|
|
1010
|
+
def _show_test_result(self, response: dict):
|
|
1011
|
+
"""Display test result in the result area."""
|
|
1012
|
+
self.test_result.config(state=tk.NORMAL)
|
|
1013
|
+
self.test_result.delete("1.0", tk.END)
|
|
1014
|
+
|
|
1015
|
+
# Check if it's an error response
|
|
1016
|
+
if "error" in response:
|
|
1017
|
+
self.test_status_label.config(text="Error", foreground="red")
|
|
1018
|
+
self.test_result.insert(tk.END, "ERROR:\n", "error")
|
|
1019
|
+
self.test_result.insert(tk.END, json.dumps(response["error"], indent=2))
|
|
1020
|
+
elif response.get("success") == False or response.get("isError"):
|
|
1021
|
+
self.test_status_label.config(text="Tool returned error", foreground="orange")
|
|
1022
|
+
self.test_result.insert(tk.END, "Tool Error:\n", "error")
|
|
1023
|
+
content = response.get("content", response.get("result", {}).get("content", []))
|
|
1024
|
+
if isinstance(content, list):
|
|
1025
|
+
for item in content:
|
|
1026
|
+
if item.get("type") == "text":
|
|
1027
|
+
self.test_result.insert(tk.END, item.get("text", ""))
|
|
1028
|
+
else:
|
|
1029
|
+
self.test_result.insert(tk.END, json.dumps(content, indent=2))
|
|
1030
|
+
else:
|
|
1031
|
+
self.test_status_label.config(text="Success", foreground="green")
|
|
1032
|
+
self.test_result.insert(tk.END, "Result:\n", "success")
|
|
1033
|
+
|
|
1034
|
+
# Extract content from MCP response
|
|
1035
|
+
content = response.get("content", response.get("result", {}).get("content", []))
|
|
1036
|
+
if isinstance(content, list):
|
|
1037
|
+
for item in content:
|
|
1038
|
+
if item.get("type") == "text":
|
|
1039
|
+
self.test_result.insert(tk.END, item.get("text", ""))
|
|
1040
|
+
self.test_result.insert(tk.END, "\n")
|
|
1041
|
+
else:
|
|
1042
|
+
self.test_result.insert(tk.END, json.dumps(content, indent=2))
|
|
1043
|
+
|
|
1044
|
+
self.test_result.config(state=tk.DISABLED)
|
|
1045
|
+
self._add_log("INFO", f"Test executed: {self.test_tool_var.get()}")
|
|
1046
|
+
|
|
1047
|
+
def _show_test_error(self, message: str):
|
|
1048
|
+
"""Display an error message in the test result area."""
|
|
1049
|
+
self.test_status_label.config(text="Error", foreground="red")
|
|
1050
|
+
self.test_result.config(state=tk.NORMAL)
|
|
1051
|
+
self.test_result.delete("1.0", tk.END)
|
|
1052
|
+
self.test_result.insert(tk.END, f"Error: {message}", "error")
|
|
1053
|
+
self.test_result.config(state=tk.DISABLED)
|
|
1054
|
+
self._add_log("ERROR", f"Test error: {message}")
|
|
1055
|
+
|
|
1056
|
+
def _clear_test_result(self):
|
|
1057
|
+
"""Clear the test result area."""
|
|
1058
|
+
self.test_result.config(state=tk.NORMAL)
|
|
1059
|
+
self.test_result.delete("1.0", tk.END)
|
|
1060
|
+
self.test_result.config(state=tk.DISABLED)
|
|
1061
|
+
self.test_status_label.config(text="")
|
|
1062
|
+
|
|
1063
|
+
def create_log_tab(self):
|
|
1064
|
+
"""Create the server log tab."""
|
|
1065
|
+
# Control frame at top
|
|
1066
|
+
control_frame = ttk.Frame(self.log_frame)
|
|
1067
|
+
control_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
1068
|
+
|
|
1069
|
+
# Log level filter
|
|
1070
|
+
ttk.Label(control_frame, text="Log Level:").pack(side=tk.LEFT, padx=(0, 5))
|
|
1071
|
+
|
|
1072
|
+
self.log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
1073
|
+
self.log_level_var = tk.StringVar(value="DEBUG") # Show all by default
|
|
1074
|
+
self.log_level_combo = ttk.Combobox(
|
|
1075
|
+
control_frame,
|
|
1076
|
+
textvariable=self.log_level_var,
|
|
1077
|
+
values=self.log_levels,
|
|
1078
|
+
state="readonly",
|
|
1079
|
+
width=12
|
|
1080
|
+
)
|
|
1081
|
+
self.log_level_combo.pack(side=tk.LEFT, padx=(0, 10))
|
|
1082
|
+
self.log_level_combo.bind("<<ComboboxSelected>>", self._on_log_level_change)
|
|
1083
|
+
|
|
1084
|
+
# Auto-scroll checkbox
|
|
1085
|
+
self.auto_scroll_var = tk.BooleanVar(value=True)
|
|
1086
|
+
ttk.Checkbutton(
|
|
1087
|
+
control_frame,
|
|
1088
|
+
text="Auto-scroll",
|
|
1089
|
+
variable=self.auto_scroll_var
|
|
1090
|
+
).pack(side=tk.LEFT, padx=(0, 10))
|
|
1091
|
+
|
|
1092
|
+
# Clear button
|
|
1093
|
+
ttk.Button(control_frame, text="Clear Log", command=self._clear_log).pack(side=tk.LEFT)
|
|
1094
|
+
|
|
1095
|
+
# Log entry count label
|
|
1096
|
+
self.log_count_label = ttk.Label(control_frame, text="Entries: 0")
|
|
1097
|
+
self.log_count_label.pack(side=tk.RIGHT)
|
|
1098
|
+
|
|
1099
|
+
# Log display
|
|
1100
|
+
self.log_text = scrolledtext.ScrolledText(self.log_frame, height=20, wrap=tk.WORD, state=tk.DISABLED)
|
|
1101
|
+
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
1102
|
+
|
|
1103
|
+
# Configure tags for log levels with colors
|
|
1104
|
+
self.log_text.tag_configure("DEBUG", foreground="gray")
|
|
1105
|
+
self.log_text.tag_configure("INFO", foreground="green")
|
|
1106
|
+
self.log_text.tag_configure("WARNING", foreground="orange")
|
|
1107
|
+
self.log_text.tag_configure("ERROR", foreground="red")
|
|
1108
|
+
self.log_text.tag_configure("CRITICAL", foreground="red", font=("", 10, "bold"))
|
|
1109
|
+
self.log_text.tag_configure("TIMESTAMP", foreground="blue")
|
|
1110
|
+
|
|
1111
|
+
# Store all log entries for filtering
|
|
1112
|
+
self.all_log_entries = []
|
|
1113
|
+
|
|
1114
|
+
# Initial log message
|
|
1115
|
+
self._add_log("INFO", "MCP Manager initialized. Ready to start server.")
|
|
1116
|
+
|
|
1117
|
+
def _on_log_level_change(self, event=None):
|
|
1118
|
+
"""Handle log level filter change - refresh display with filtered entries."""
|
|
1119
|
+
self._refresh_log_display()
|
|
1120
|
+
|
|
1121
|
+
def _get_log_level_priority(self, level: str) -> int:
|
|
1122
|
+
"""Get numeric priority for a log level (higher = more severe)."""
|
|
1123
|
+
priorities = {
|
|
1124
|
+
"DEBUG": 0,
|
|
1125
|
+
"INFO": 1,
|
|
1126
|
+
"WARNING": 2,
|
|
1127
|
+
"ERROR": 3,
|
|
1128
|
+
"CRITICAL": 4
|
|
1129
|
+
}
|
|
1130
|
+
return priorities.get(level.upper(), 0)
|
|
1131
|
+
|
|
1132
|
+
def _refresh_log_display(self):
|
|
1133
|
+
"""Refresh the log display based on current filter level."""
|
|
1134
|
+
min_level = self._get_log_level_priority(self.log_level_var.get())
|
|
1135
|
+
|
|
1136
|
+
self.log_text.config(state=tk.NORMAL)
|
|
1137
|
+
self.log_text.delete("1.0", tk.END)
|
|
1138
|
+
|
|
1139
|
+
visible_count = 0
|
|
1140
|
+
for timestamp, level, message in self.all_log_entries:
|
|
1141
|
+
if self._get_log_level_priority(level) >= min_level:
|
|
1142
|
+
self.log_text.insert(tk.END, f"[{timestamp}] ", "TIMESTAMP")
|
|
1143
|
+
self.log_text.insert(tk.END, f"[{level}] ", level)
|
|
1144
|
+
self.log_text.insert(tk.END, f"{message}\n")
|
|
1145
|
+
visible_count += 1
|
|
1146
|
+
|
|
1147
|
+
self.log_text.config(state=tk.DISABLED)
|
|
1148
|
+
|
|
1149
|
+
if self.auto_scroll_var.get():
|
|
1150
|
+
self.log_text.see(tk.END)
|
|
1151
|
+
|
|
1152
|
+
# Update count label
|
|
1153
|
+
total = len(self.all_log_entries)
|
|
1154
|
+
if visible_count == total:
|
|
1155
|
+
self.log_count_label.config(text=f"Entries: {total}")
|
|
1156
|
+
else:
|
|
1157
|
+
self.log_count_label.config(text=f"Entries: {visible_count}/{total}")
|
|
1158
|
+
|
|
1159
|
+
def _draw_status_indicator(self, running: bool):
|
|
1160
|
+
"""Draw the status indicator circle."""
|
|
1161
|
+
self.status_indicator.delete("all")
|
|
1162
|
+
color = "#00CC00" if running else "#CC0000"
|
|
1163
|
+
self.status_indicator.create_oval(2, 2, 14, 14, fill=color, outline=color)
|
|
1164
|
+
|
|
1165
|
+
def _populate_tools_list(self):
|
|
1166
|
+
"""Populate the tools treeview."""
|
|
1167
|
+
# Clear existing
|
|
1168
|
+
for item in self.tools_tree.get_children():
|
|
1169
|
+
self.tools_tree.delete(item)
|
|
1170
|
+
|
|
1171
|
+
if not self.registry:
|
|
1172
|
+
return
|
|
1173
|
+
|
|
1174
|
+
for tool in self.registry.list_tools():
|
|
1175
|
+
# Truncate description for display
|
|
1176
|
+
desc = tool.description[:80] + "..." if len(tool.description) > 80 else tool.description
|
|
1177
|
+
self.tools_tree.insert("", tk.END, values=(tool.name, desc))
|
|
1178
|
+
|
|
1179
|
+
def _on_tool_select(self, event):
|
|
1180
|
+
"""Handle tool selection in treeview."""
|
|
1181
|
+
selection = self.tools_tree.selection()
|
|
1182
|
+
if not selection:
|
|
1183
|
+
return
|
|
1184
|
+
|
|
1185
|
+
item = self.tools_tree.item(selection[0])
|
|
1186
|
+
tool_name = item["values"][0]
|
|
1187
|
+
|
|
1188
|
+
if not self.registry:
|
|
1189
|
+
return
|
|
1190
|
+
|
|
1191
|
+
tool = self.registry.get_tool(tool_name)
|
|
1192
|
+
if not tool:
|
|
1193
|
+
return
|
|
1194
|
+
|
|
1195
|
+
# Display tool details in a readable format
|
|
1196
|
+
mcp_tool = tool.to_mcp_tool()
|
|
1197
|
+
schema = mcp_tool.inputSchema
|
|
1198
|
+
properties = schema.get("properties", {})
|
|
1199
|
+
required = schema.get("required", [])
|
|
1200
|
+
|
|
1201
|
+
details = f"Tool: {mcp_tool.name}\n"
|
|
1202
|
+
details += "=" * 60 + "\n\n"
|
|
1203
|
+
details += f"Description:\n{mcp_tool.description}\n\n"
|
|
1204
|
+
|
|
1205
|
+
# Parameters section
|
|
1206
|
+
details += "Parameters:\n"
|
|
1207
|
+
details += "-" * 40 + "\n"
|
|
1208
|
+
|
|
1209
|
+
if properties:
|
|
1210
|
+
for param_name, param_info in properties.items():
|
|
1211
|
+
is_required = param_name in required
|
|
1212
|
+
param_type = param_info.get("type", "any")
|
|
1213
|
+
param_desc = param_info.get("description", "No description")
|
|
1214
|
+
|
|
1215
|
+
# Format parameter header
|
|
1216
|
+
req_marker = " [REQUIRED]" if is_required else " [optional]"
|
|
1217
|
+
details += f"\n• {param_name}{req_marker}\n"
|
|
1218
|
+
details += f" Type: {param_type}\n"
|
|
1219
|
+
|
|
1220
|
+
# Show enum values if present
|
|
1221
|
+
if "enum" in param_info:
|
|
1222
|
+
enum_values = ", ".join(str(v) for v in param_info["enum"])
|
|
1223
|
+
details += f" Options: {enum_values}\n"
|
|
1224
|
+
|
|
1225
|
+
# Show default value if present
|
|
1226
|
+
if "default" in param_info:
|
|
1227
|
+
default_val = param_info["default"]
|
|
1228
|
+
if isinstance(default_val, str) and len(default_val) > 50:
|
|
1229
|
+
default_val = default_val[:50] + "..."
|
|
1230
|
+
details += f" Default: {default_val}\n"
|
|
1231
|
+
|
|
1232
|
+
details += f" Description: {param_desc}\n"
|
|
1233
|
+
else:
|
|
1234
|
+
details += "\n(No parameters)\n"
|
|
1235
|
+
|
|
1236
|
+
self.tool_details.config(state=tk.NORMAL)
|
|
1237
|
+
self.tool_details.delete("1.0", tk.END)
|
|
1238
|
+
self.tool_details.insert(tk.END, details)
|
|
1239
|
+
self.tool_details.config(state=tk.DISABLED)
|
|
1240
|
+
|
|
1241
|
+
def _copy_to_clipboard(self, text: str):
|
|
1242
|
+
"""Copy text to clipboard."""
|
|
1243
|
+
self.clipboard_clear()
|
|
1244
|
+
self.clipboard_append(text)
|
|
1245
|
+
self._add_log("INFO", "Configuration copied to clipboard")
|
|
1246
|
+
|
|
1247
|
+
def _add_log(self, level: str, message: str):
|
|
1248
|
+
"""Add a log message to the log display."""
|
|
1249
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
1250
|
+
|
|
1251
|
+
# Store in all entries for filtering
|
|
1252
|
+
if hasattr(self, 'all_log_entries'):
|
|
1253
|
+
self.all_log_entries.append((timestamp, level.upper(), message))
|
|
1254
|
+
|
|
1255
|
+
# Check if this message should be displayed based on current filter
|
|
1256
|
+
min_level = self._get_log_level_priority(self.log_level_var.get()) if hasattr(self, 'log_level_var') else 0
|
|
1257
|
+
if self._get_log_level_priority(level) >= min_level:
|
|
1258
|
+
self.log_text.config(state=tk.NORMAL)
|
|
1259
|
+
self.log_text.insert(tk.END, f"[{timestamp}] ", "TIMESTAMP")
|
|
1260
|
+
self.log_text.insert(tk.END, f"[{level.upper()}] ", level.upper())
|
|
1261
|
+
self.log_text.insert(tk.END, f"{message}\n")
|
|
1262
|
+
|
|
1263
|
+
if hasattr(self, 'auto_scroll_var') and self.auto_scroll_var.get():
|
|
1264
|
+
self.log_text.see(tk.END)
|
|
1265
|
+
|
|
1266
|
+
self.log_text.config(state=tk.DISABLED)
|
|
1267
|
+
|
|
1268
|
+
# Update count label
|
|
1269
|
+
if hasattr(self, 'log_count_label') and hasattr(self, 'all_log_entries'):
|
|
1270
|
+
total = len(self.all_log_entries)
|
|
1271
|
+
visible = sum(1 for _, lvl, _ in self.all_log_entries
|
|
1272
|
+
if self._get_log_level_priority(lvl) >= min_level)
|
|
1273
|
+
if visible == total:
|
|
1274
|
+
self.log_count_label.config(text=f"Entries: {total}")
|
|
1275
|
+
else:
|
|
1276
|
+
self.log_count_label.config(text=f"Entries: {visible}/{total}")
|
|
1277
|
+
|
|
1278
|
+
def _clear_log(self):
|
|
1279
|
+
"""Clear the log display."""
|
|
1280
|
+
if hasattr(self, 'all_log_entries'):
|
|
1281
|
+
self.all_log_entries.clear()
|
|
1282
|
+
|
|
1283
|
+
self.log_text.config(state=tk.NORMAL)
|
|
1284
|
+
self.log_text.delete("1.0", tk.END)
|
|
1285
|
+
self.log_text.config(state=tk.DISABLED)
|
|
1286
|
+
|
|
1287
|
+
# Update count label
|
|
1288
|
+
if hasattr(self, 'log_count_label'):
|
|
1289
|
+
self.log_count_label.config(text="Entries: 0")
|
|
1290
|
+
|
|
1291
|
+
self._add_log("INFO", "Log cleared")
|
|
1292
|
+
|
|
1293
|
+
def start_log_polling(self):
|
|
1294
|
+
"""Start polling the log queue for messages."""
|
|
1295
|
+
self._poll_log_queue()
|
|
1296
|
+
|
|
1297
|
+
def _poll_log_queue(self):
|
|
1298
|
+
"""Poll the log queue and display messages."""
|
|
1299
|
+
try:
|
|
1300
|
+
while True:
|
|
1301
|
+
level, message = self.log_queue.get_nowait()
|
|
1302
|
+
self._add_log(level, message)
|
|
1303
|
+
except queue.Empty:
|
|
1304
|
+
pass
|
|
1305
|
+
|
|
1306
|
+
# Schedule next poll
|
|
1307
|
+
self.after(100, self._poll_log_queue)
|
|
1308
|
+
|
|
1309
|
+
def start_server(self):
|
|
1310
|
+
"""Start the MCP server as a detached subprocess."""
|
|
1311
|
+
if self.server_running:
|
|
1312
|
+
self._add_log("WARNING", "Server is already running")
|
|
1313
|
+
return
|
|
1314
|
+
|
|
1315
|
+
# Create server process manager
|
|
1316
|
+
self.server_process = MCPServerProcess(
|
|
1317
|
+
self.log_queue,
|
|
1318
|
+
self._on_server_started,
|
|
1319
|
+
self._on_server_stopped
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
# Start in detached mode so it persists after Pomera closes
|
|
1323
|
+
if self.server_process.start(detached=True):
|
|
1324
|
+
self._add_log("INFO", "MCP server started in detached mode")
|
|
1325
|
+
self._add_log("INFO", "Server will continue running after Pomera closes")
|
|
1326
|
+
self._add_log("INFO", "The server is now ready to accept connections from Claude Desktop or Cursor")
|
|
1327
|
+
else:
|
|
1328
|
+
self._add_log("ERROR", "Failed to start MCP server")
|
|
1329
|
+
|
|
1330
|
+
def check_existing_server(self):
|
|
1331
|
+
"""Check if an MCP server is already running and update UI accordingly."""
|
|
1332
|
+
pid = find_running_mcp_server()
|
|
1333
|
+
if pid:
|
|
1334
|
+
self._add_log("INFO", f"Detected existing MCP server (PID: {pid})")
|
|
1335
|
+
# Create process manager to track it
|
|
1336
|
+
self.server_process = MCPServerProcess(
|
|
1337
|
+
self.log_queue,
|
|
1338
|
+
self._on_server_started,
|
|
1339
|
+
self._on_server_stopped
|
|
1340
|
+
)
|
|
1341
|
+
self.server_process._external_pid = pid
|
|
1342
|
+
self._update_status(True)
|
|
1343
|
+
return True
|
|
1344
|
+
return False
|
|
1345
|
+
|
|
1346
|
+
def stop_server(self):
|
|
1347
|
+
"""Stop the MCP server subprocess."""
|
|
1348
|
+
if not self.server_running and (not self.server_process or not self.server_process.is_running()):
|
|
1349
|
+
self._add_log("WARNING", "Server is not running")
|
|
1350
|
+
return
|
|
1351
|
+
|
|
1352
|
+
if self.server_process:
|
|
1353
|
+
self.server_process.stop()
|
|
1354
|
+
self.server_process = None
|
|
1355
|
+
|
|
1356
|
+
def _update_status(self, running: bool):
|
|
1357
|
+
"""Update the server status display."""
|
|
1358
|
+
self.server_running = running
|
|
1359
|
+
self._draw_status_indicator(running)
|
|
1360
|
+
|
|
1361
|
+
if running:
|
|
1362
|
+
self.status_label.config(text="Running")
|
|
1363
|
+
self.start_btn.config(state=tk.DISABLED)
|
|
1364
|
+
self.stop_btn.config(state=tk.NORMAL)
|
|
1365
|
+
# Update PID from process manager
|
|
1366
|
+
if self.server_process:
|
|
1367
|
+
pid = self.server_process.get_pid()
|
|
1368
|
+
if pid:
|
|
1369
|
+
self.pid_label.config(text=str(pid))
|
|
1370
|
+
else:
|
|
1371
|
+
self.pid_label.config(text="Running (PID unknown)")
|
|
1372
|
+
else:
|
|
1373
|
+
self.pid_label.config(text="Running")
|
|
1374
|
+
else:
|
|
1375
|
+
self.status_label.config(text="Stopped")
|
|
1376
|
+
self.start_btn.config(state=tk.NORMAL)
|
|
1377
|
+
self.stop_btn.config(state=tk.DISABLED)
|
|
1378
|
+
self.pid_label.config(text="Not running")
|
|
1379
|
+
|
|
1380
|
+
def _on_server_started(self):
|
|
1381
|
+
"""Callback when server starts."""
|
|
1382
|
+
self.after(0, lambda: self._update_status(True))
|
|
1383
|
+
|
|
1384
|
+
def _on_server_stopped(self):
|
|
1385
|
+
"""Callback when server stops."""
|
|
1386
|
+
self.after(0, lambda: self._update_status(False))
|
|
1387
|
+
|
|
1388
|
+
def refresh_tools(self):
|
|
1389
|
+
"""Refresh the tools list."""
|
|
1390
|
+
self._load_registry()
|
|
1391
|
+
self._populate_tools_list()
|
|
1392
|
+
self._populate_test_tool_combo()
|
|
1393
|
+
|
|
1394
|
+
tool_count = len(self.registry) if self.registry else 0
|
|
1395
|
+
self.tools_count_label.config(text=f"{tool_count} available")
|
|
1396
|
+
|
|
1397
|
+
self._add_log("INFO", f"Tools refreshed: {tool_count} tools available")
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
class MCPManager:
|
|
1401
|
+
"""Main class for MCP Manager integration with Pomera."""
|
|
1402
|
+
|
|
1403
|
+
def __init__(self):
|
|
1404
|
+
self.widget = None
|
|
1405
|
+
|
|
1406
|
+
def create_widget(self, parent, app):
|
|
1407
|
+
"""Create and return the MCP Manager widget."""
|
|
1408
|
+
self.widget = MCPManagerWidget(parent, app)
|
|
1409
|
+
return self.widget
|
|
1410
|
+
|
|
1411
|
+
def get_default_settings(self):
|
|
1412
|
+
"""Return default settings for MCP Manager."""
|
|
1413
|
+
return {
|
|
1414
|
+
"auto_start": False,
|
|
1415
|
+
"log_level": "INFO"
|
|
1416
|
+
}
|
|
1417
|
+
|