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/core/mcp/server_stdio.py
CHANGED
|
@@ -1,299 +1,299 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MCP Server stdio Transport - Standard I/O transport for MCP server
|
|
3
|
-
|
|
4
|
-
This module implements the stdio transport for the MCP server,
|
|
5
|
-
allowing communication with MCP clients (like Claude Desktop, Cursor)
|
|
6
|
-
via standard input/output streams.
|
|
7
|
-
|
|
8
|
-
The server reads JSON-RPC messages from stdin and writes responses to stdout.
|
|
9
|
-
Each message is a single line of JSON.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import sys
|
|
13
|
-
import json
|
|
14
|
-
import logging
|
|
15
|
-
import asyncio
|
|
16
|
-
from typing import Optional, Callable, Dict, Any
|
|
17
|
-
|
|
18
|
-
from .schema import (
|
|
19
|
-
MCPMessage,
|
|
20
|
-
MCPServerCapabilities,
|
|
21
|
-
MCPServerInfo,
|
|
22
|
-
MCPToolResult,
|
|
23
|
-
MCPResource,
|
|
24
|
-
)
|
|
25
|
-
from .protocol import MCPProtocol, MCPProtocolError
|
|
26
|
-
from .tool_registry import ToolRegistry, get_registry
|
|
27
|
-
|
|
28
|
-
logger = logging.getLogger(__name__)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class StdioMCPServer:
|
|
32
|
-
"""
|
|
33
|
-
MCP Server using stdio transport.
|
|
34
|
-
|
|
35
|
-
Reads JSON-RPC messages from stdin, processes them, and writes
|
|
36
|
-
responses to stdout. This is the primary transport for integration
|
|
37
|
-
with Claude Desktop and Cursor.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
def __init__(
|
|
41
|
-
self,
|
|
42
|
-
tool_registry: Optional[ToolRegistry] = None,
|
|
43
|
-
server_name: str = "pomera-mcp-server",
|
|
44
|
-
server_version: str = "0.1.0",
|
|
45
|
-
resource_provider: Optional[Callable[[str], str]] = None
|
|
46
|
-
):
|
|
47
|
-
"""
|
|
48
|
-
Initialize the stdio MCP server.
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
tool_registry: Registry of available tools (uses default if None)
|
|
52
|
-
server_name: Name to advertise in server info
|
|
53
|
-
server_version: Version to advertise in server info
|
|
54
|
-
resource_provider: Optional callback to read resources by URI
|
|
55
|
-
"""
|
|
56
|
-
self.registry = tool_registry or get_registry()
|
|
57
|
-
self.server_info = MCPServerInfo(name=server_name, version=server_version)
|
|
58
|
-
self.capabilities = MCPServerCapabilities(
|
|
59
|
-
tools=True,
|
|
60
|
-
resources=resource_provider is not None,
|
|
61
|
-
prompts=False,
|
|
62
|
-
logging=False
|
|
63
|
-
)
|
|
64
|
-
self.resource_provider = resource_provider
|
|
65
|
-
self.running = False
|
|
66
|
-
self._initialized = False
|
|
67
|
-
|
|
68
|
-
# Resources list (can be populated externally)
|
|
69
|
-
self._resources: list[MCPResource] = []
|
|
70
|
-
|
|
71
|
-
def add_resource(self, resource: MCPResource) -> None:
|
|
72
|
-
"""Add a resource to the server's resource list."""
|
|
73
|
-
self._resources.append(resource)
|
|
74
|
-
self.capabilities.resources = True
|
|
75
|
-
|
|
76
|
-
def set_resources(self, resources: list[MCPResource]) -> None:
|
|
77
|
-
"""Set the server's resource list."""
|
|
78
|
-
self._resources = resources
|
|
79
|
-
self.capabilities.resources = len(resources) > 0
|
|
80
|
-
|
|
81
|
-
async def run(self) -> None:
|
|
82
|
-
"""
|
|
83
|
-
Run the server, reading from stdin and writing to stdout.
|
|
84
|
-
|
|
85
|
-
This method runs indefinitely until stdin is closed or
|
|
86
|
-
stop() is called.
|
|
87
|
-
"""
|
|
88
|
-
self.running = True
|
|
89
|
-
logger.info("MCP stdio server starting...")
|
|
90
|
-
|
|
91
|
-
# Use asyncio for non-blocking stdin reading
|
|
92
|
-
loop = asyncio.get_event_loop()
|
|
93
|
-
reader = asyncio.StreamReader()
|
|
94
|
-
protocol = asyncio.StreamReaderProtocol(reader)
|
|
95
|
-
|
|
96
|
-
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
97
|
-
|
|
98
|
-
while self.running:
|
|
99
|
-
try:
|
|
100
|
-
# Read a line from stdin
|
|
101
|
-
line = await reader.readline()
|
|
102
|
-
if not line:
|
|
103
|
-
logger.info("stdin closed, shutting down")
|
|
104
|
-
break
|
|
105
|
-
|
|
106
|
-
line_str = line.decode('utf-8').strip()
|
|
107
|
-
if not line_str:
|
|
108
|
-
continue
|
|
109
|
-
|
|
110
|
-
logger.debug(f"Received: {line_str[:100]}...")
|
|
111
|
-
|
|
112
|
-
# Process the message
|
|
113
|
-
response = self._handle_message(line_str)
|
|
114
|
-
|
|
115
|
-
if response:
|
|
116
|
-
self._send_response(response)
|
|
117
|
-
|
|
118
|
-
except asyncio.CancelledError:
|
|
119
|
-
logger.info("Server cancelled")
|
|
120
|
-
break
|
|
121
|
-
except Exception as e:
|
|
122
|
-
logger.exception(f"Error processing message: {e}")
|
|
123
|
-
# Send error response
|
|
124
|
-
error_response = MCPProtocol.internal_error(None, str(e))
|
|
125
|
-
self._send_response(error_response)
|
|
126
|
-
|
|
127
|
-
self.running = False
|
|
128
|
-
logger.info("MCP stdio server stopped")
|
|
129
|
-
|
|
130
|
-
def run_sync(self) -> None:
|
|
131
|
-
"""
|
|
132
|
-
Run the server synchronously (blocking).
|
|
133
|
-
|
|
134
|
-
Simpler alternative to async run() for single-threaded use.
|
|
135
|
-
"""
|
|
136
|
-
self.running = True
|
|
137
|
-
logger.info("MCP stdio server starting (sync mode)...")
|
|
138
|
-
|
|
139
|
-
while self.running:
|
|
140
|
-
try:
|
|
141
|
-
line = sys.stdin.readline()
|
|
142
|
-
if not line:
|
|
143
|
-
logger.info("stdin closed, shutting down")
|
|
144
|
-
break
|
|
145
|
-
|
|
146
|
-
line = line.strip()
|
|
147
|
-
if not line:
|
|
148
|
-
continue
|
|
149
|
-
|
|
150
|
-
logger.debug(f"Received: {line[:100]}...")
|
|
151
|
-
|
|
152
|
-
response = self._handle_message(line)
|
|
153
|
-
|
|
154
|
-
if response:
|
|
155
|
-
self._send_response(response)
|
|
156
|
-
|
|
157
|
-
except KeyboardInterrupt:
|
|
158
|
-
logger.info("Keyboard interrupt, shutting down")
|
|
159
|
-
break
|
|
160
|
-
except Exception as e:
|
|
161
|
-
logger.exception(f"Error processing message: {e}")
|
|
162
|
-
error_response = MCPProtocol.internal_error(None, str(e))
|
|
163
|
-
self._send_response(error_response)
|
|
164
|
-
|
|
165
|
-
self.running = False
|
|
166
|
-
logger.info("MCP stdio server stopped")
|
|
167
|
-
|
|
168
|
-
def stop(self) -> None:
|
|
169
|
-
"""Signal the server to stop."""
|
|
170
|
-
self.running = False
|
|
171
|
-
|
|
172
|
-
def _send_response(self, msg: MCPMessage) -> None:
|
|
173
|
-
"""Send a response message to stdout."""
|
|
174
|
-
json_str = MCPProtocol.serialize(msg)
|
|
175
|
-
logger.debug(f"Sending: {json_str[:100]}...")
|
|
176
|
-
print(json_str, flush=True)
|
|
177
|
-
|
|
178
|
-
def _handle_message(self, data: str) -> Optional[MCPMessage]:
|
|
179
|
-
"""
|
|
180
|
-
Handle an incoming message and return response.
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
data: JSON string of incoming message
|
|
184
|
-
|
|
185
|
-
Returns:
|
|
186
|
-
MCPMessage response or None for notifications
|
|
187
|
-
"""
|
|
188
|
-
try:
|
|
189
|
-
msg = MCPProtocol.parse(data)
|
|
190
|
-
except MCPProtocolError as e:
|
|
191
|
-
return MCPProtocol.create_error(None, e.code, e.message)
|
|
192
|
-
|
|
193
|
-
# Notifications don't get responses
|
|
194
|
-
if msg.is_notification():
|
|
195
|
-
self._handle_notification(msg)
|
|
196
|
-
return None
|
|
197
|
-
|
|
198
|
-
# Route to appropriate handler
|
|
199
|
-
method = msg.method
|
|
200
|
-
params = msg.params or {}
|
|
201
|
-
|
|
202
|
-
if method == "initialize":
|
|
203
|
-
return self._handle_initialize(msg.id, params)
|
|
204
|
-
elif method == "initialized":
|
|
205
|
-
# This is a notification, but some clients send it as request
|
|
206
|
-
self._initialized = True
|
|
207
|
-
return MCPProtocol.create_response(msg.id, {})
|
|
208
|
-
elif method == "ping":
|
|
209
|
-
return MCPProtocol.create_response(msg.id, {})
|
|
210
|
-
elif method == "tools/list":
|
|
211
|
-
return self._handle_tools_list(msg.id)
|
|
212
|
-
elif method == "tools/call":
|
|
213
|
-
return self._handle_tools_call(msg.id, params)
|
|
214
|
-
elif method == "resources/list":
|
|
215
|
-
return self._handle_resources_list(msg.id)
|
|
216
|
-
elif method == "resources/read":
|
|
217
|
-
return self._handle_resources_read(msg.id, params)
|
|
218
|
-
else:
|
|
219
|
-
return MCPProtocol.method_not_found(msg.id, method)
|
|
220
|
-
|
|
221
|
-
def _handle_notification(self, msg: MCPMessage) -> None:
|
|
222
|
-
"""Handle notification messages (no response)."""
|
|
223
|
-
if msg.method == "notifications/initialized":
|
|
224
|
-
self._initialized = True
|
|
225
|
-
logger.info("Client initialized")
|
|
226
|
-
elif msg.method == "notifications/cancelled":
|
|
227
|
-
logger.info(f"Request cancelled: {msg.params}")
|
|
228
|
-
|
|
229
|
-
def _handle_initialize(self, id: int, params: Dict[str, Any]) -> MCPMessage:
|
|
230
|
-
"""Handle 'initialize' request."""
|
|
231
|
-
client_info = params.get("clientInfo", {})
|
|
232
|
-
logger.info(f"Client initializing: {client_info.get('name', 'unknown')} "
|
|
233
|
-
f"v{client_info.get('version', 'unknown')}")
|
|
234
|
-
|
|
235
|
-
return MCPProtocol.create_initialize_response(
|
|
236
|
-
id,
|
|
237
|
-
self.server_info,
|
|
238
|
-
self.capabilities
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
def _handle_tools_list(self, id: int) -> MCPMessage:
|
|
242
|
-
"""Handle 'tools/list' request."""
|
|
243
|
-
tools = self.registry.list_tools()
|
|
244
|
-
logger.debug(f"Listing {len(tools)} tools")
|
|
245
|
-
return MCPProtocol.create_tools_list_response(id, tools)
|
|
246
|
-
|
|
247
|
-
def _handle_tools_call(self, id: int, params: Dict[str, Any]) -> MCPMessage:
|
|
248
|
-
"""Handle 'tools/call' request."""
|
|
249
|
-
tool_name = params.get("name")
|
|
250
|
-
arguments = params.get("arguments", {})
|
|
251
|
-
|
|
252
|
-
if not tool_name:
|
|
253
|
-
return MCPProtocol.invalid_params(id, "Missing 'name' parameter")
|
|
254
|
-
|
|
255
|
-
if tool_name not in self.registry:
|
|
256
|
-
return MCPProtocol.tool_not_found(id, tool_name)
|
|
257
|
-
|
|
258
|
-
logger.info(f"Executing tool: {tool_name}")
|
|
259
|
-
result = self.registry.execute(tool_name, arguments)
|
|
260
|
-
|
|
261
|
-
return MCPProtocol.create_tools_call_response(id, result)
|
|
262
|
-
|
|
263
|
-
def _handle_resources_list(self, id: int) -> MCPMessage:
|
|
264
|
-
"""Handle 'resources/list' request."""
|
|
265
|
-
logger.debug(f"Listing {len(self._resources)} resources")
|
|
266
|
-
return MCPProtocol.create_resources_list_response(id, self._resources)
|
|
267
|
-
|
|
268
|
-
def _handle_resources_read(self, id: int, params: Dict[str, Any]) -> MCPMessage:
|
|
269
|
-
"""Handle 'resources/read' request."""
|
|
270
|
-
uri = params.get("uri")
|
|
271
|
-
|
|
272
|
-
if not uri:
|
|
273
|
-
return MCPProtocol.invalid_params(id, "Missing 'uri' parameter")
|
|
274
|
-
|
|
275
|
-
# Find the resource
|
|
276
|
-
resource = None
|
|
277
|
-
for r in self._resources:
|
|
278
|
-
if r.uri == uri:
|
|
279
|
-
resource = r
|
|
280
|
-
break
|
|
281
|
-
|
|
282
|
-
if resource is None:
|
|
283
|
-
return MCPProtocol.resource_not_found(id, uri)
|
|
284
|
-
|
|
285
|
-
# Read content via provider
|
|
286
|
-
if self.resource_provider:
|
|
287
|
-
try:
|
|
288
|
-
content = self.resource_provider(uri)
|
|
289
|
-
return MCPProtocol.create_resources_read_response(id, [{
|
|
290
|
-
"uri": uri,
|
|
291
|
-
"mimeType": resource.mimeType,
|
|
292
|
-
"text": content
|
|
293
|
-
}])
|
|
294
|
-
except Exception as e:
|
|
295
|
-
logger.exception(f"Error reading resource: {uri}")
|
|
296
|
-
return MCPProtocol.internal_error(id, f"Error reading resource: {str(e)}")
|
|
297
|
-
else:
|
|
298
|
-
return MCPProtocol.internal_error(id, "No resource provider configured")
|
|
299
|
-
|
|
1
|
+
"""
|
|
2
|
+
MCP Server stdio Transport - Standard I/O transport for MCP server
|
|
3
|
+
|
|
4
|
+
This module implements the stdio transport for the MCP server,
|
|
5
|
+
allowing communication with MCP clients (like Claude Desktop, Cursor)
|
|
6
|
+
via standard input/output streams.
|
|
7
|
+
|
|
8
|
+
The server reads JSON-RPC messages from stdin and writes responses to stdout.
|
|
9
|
+
Each message is a single line of JSON.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import asyncio
|
|
16
|
+
from typing import Optional, Callable, Dict, Any
|
|
17
|
+
|
|
18
|
+
from .schema import (
|
|
19
|
+
MCPMessage,
|
|
20
|
+
MCPServerCapabilities,
|
|
21
|
+
MCPServerInfo,
|
|
22
|
+
MCPToolResult,
|
|
23
|
+
MCPResource,
|
|
24
|
+
)
|
|
25
|
+
from .protocol import MCPProtocol, MCPProtocolError
|
|
26
|
+
from .tool_registry import ToolRegistry, get_registry
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StdioMCPServer:
|
|
32
|
+
"""
|
|
33
|
+
MCP Server using stdio transport.
|
|
34
|
+
|
|
35
|
+
Reads JSON-RPC messages from stdin, processes them, and writes
|
|
36
|
+
responses to stdout. This is the primary transport for integration
|
|
37
|
+
with Claude Desktop and Cursor.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
tool_registry: Optional[ToolRegistry] = None,
|
|
43
|
+
server_name: str = "pomera-mcp-server",
|
|
44
|
+
server_version: str = "0.1.0",
|
|
45
|
+
resource_provider: Optional[Callable[[str], str]] = None
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Initialize the stdio MCP server.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
tool_registry: Registry of available tools (uses default if None)
|
|
52
|
+
server_name: Name to advertise in server info
|
|
53
|
+
server_version: Version to advertise in server info
|
|
54
|
+
resource_provider: Optional callback to read resources by URI
|
|
55
|
+
"""
|
|
56
|
+
self.registry = tool_registry or get_registry()
|
|
57
|
+
self.server_info = MCPServerInfo(name=server_name, version=server_version)
|
|
58
|
+
self.capabilities = MCPServerCapabilities(
|
|
59
|
+
tools=True,
|
|
60
|
+
resources=resource_provider is not None,
|
|
61
|
+
prompts=False,
|
|
62
|
+
logging=False
|
|
63
|
+
)
|
|
64
|
+
self.resource_provider = resource_provider
|
|
65
|
+
self.running = False
|
|
66
|
+
self._initialized = False
|
|
67
|
+
|
|
68
|
+
# Resources list (can be populated externally)
|
|
69
|
+
self._resources: list[MCPResource] = []
|
|
70
|
+
|
|
71
|
+
def add_resource(self, resource: MCPResource) -> None:
|
|
72
|
+
"""Add a resource to the server's resource list."""
|
|
73
|
+
self._resources.append(resource)
|
|
74
|
+
self.capabilities.resources = True
|
|
75
|
+
|
|
76
|
+
def set_resources(self, resources: list[MCPResource]) -> None:
|
|
77
|
+
"""Set the server's resource list."""
|
|
78
|
+
self._resources = resources
|
|
79
|
+
self.capabilities.resources = len(resources) > 0
|
|
80
|
+
|
|
81
|
+
async def run(self) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Run the server, reading from stdin and writing to stdout.
|
|
84
|
+
|
|
85
|
+
This method runs indefinitely until stdin is closed or
|
|
86
|
+
stop() is called.
|
|
87
|
+
"""
|
|
88
|
+
self.running = True
|
|
89
|
+
logger.info("MCP stdio server starting...")
|
|
90
|
+
|
|
91
|
+
# Use asyncio for non-blocking stdin reading
|
|
92
|
+
loop = asyncio.get_event_loop()
|
|
93
|
+
reader = asyncio.StreamReader()
|
|
94
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
95
|
+
|
|
96
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
97
|
+
|
|
98
|
+
while self.running:
|
|
99
|
+
try:
|
|
100
|
+
# Read a line from stdin
|
|
101
|
+
line = await reader.readline()
|
|
102
|
+
if not line:
|
|
103
|
+
logger.info("stdin closed, shutting down")
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
line_str = line.decode('utf-8').strip()
|
|
107
|
+
if not line_str:
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
logger.debug(f"Received: {line_str[:100]}...")
|
|
111
|
+
|
|
112
|
+
# Process the message
|
|
113
|
+
response = self._handle_message(line_str)
|
|
114
|
+
|
|
115
|
+
if response:
|
|
116
|
+
self._send_response(response)
|
|
117
|
+
|
|
118
|
+
except asyncio.CancelledError:
|
|
119
|
+
logger.info("Server cancelled")
|
|
120
|
+
break
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.exception(f"Error processing message: {e}")
|
|
123
|
+
# Send error response
|
|
124
|
+
error_response = MCPProtocol.internal_error(None, str(e))
|
|
125
|
+
self._send_response(error_response)
|
|
126
|
+
|
|
127
|
+
self.running = False
|
|
128
|
+
logger.info("MCP stdio server stopped")
|
|
129
|
+
|
|
130
|
+
def run_sync(self) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Run the server synchronously (blocking).
|
|
133
|
+
|
|
134
|
+
Simpler alternative to async run() for single-threaded use.
|
|
135
|
+
"""
|
|
136
|
+
self.running = True
|
|
137
|
+
logger.info("MCP stdio server starting (sync mode)...")
|
|
138
|
+
|
|
139
|
+
while self.running:
|
|
140
|
+
try:
|
|
141
|
+
line = sys.stdin.readline()
|
|
142
|
+
if not line:
|
|
143
|
+
logger.info("stdin closed, shutting down")
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
line = line.strip()
|
|
147
|
+
if not line:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
logger.debug(f"Received: {line[:100]}...")
|
|
151
|
+
|
|
152
|
+
response = self._handle_message(line)
|
|
153
|
+
|
|
154
|
+
if response:
|
|
155
|
+
self._send_response(response)
|
|
156
|
+
|
|
157
|
+
except KeyboardInterrupt:
|
|
158
|
+
logger.info("Keyboard interrupt, shutting down")
|
|
159
|
+
break
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.exception(f"Error processing message: {e}")
|
|
162
|
+
error_response = MCPProtocol.internal_error(None, str(e))
|
|
163
|
+
self._send_response(error_response)
|
|
164
|
+
|
|
165
|
+
self.running = False
|
|
166
|
+
logger.info("MCP stdio server stopped")
|
|
167
|
+
|
|
168
|
+
def stop(self) -> None:
|
|
169
|
+
"""Signal the server to stop."""
|
|
170
|
+
self.running = False
|
|
171
|
+
|
|
172
|
+
def _send_response(self, msg: MCPMessage) -> None:
|
|
173
|
+
"""Send a response message to stdout."""
|
|
174
|
+
json_str = MCPProtocol.serialize(msg)
|
|
175
|
+
logger.debug(f"Sending: {json_str[:100]}...")
|
|
176
|
+
print(json_str, flush=True)
|
|
177
|
+
|
|
178
|
+
def _handle_message(self, data: str) -> Optional[MCPMessage]:
|
|
179
|
+
"""
|
|
180
|
+
Handle an incoming message and return response.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
data: JSON string of incoming message
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
MCPMessage response or None for notifications
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
msg = MCPProtocol.parse(data)
|
|
190
|
+
except MCPProtocolError as e:
|
|
191
|
+
return MCPProtocol.create_error(None, e.code, e.message)
|
|
192
|
+
|
|
193
|
+
# Notifications don't get responses
|
|
194
|
+
if msg.is_notification():
|
|
195
|
+
self._handle_notification(msg)
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
# Route to appropriate handler
|
|
199
|
+
method = msg.method
|
|
200
|
+
params = msg.params or {}
|
|
201
|
+
|
|
202
|
+
if method == "initialize":
|
|
203
|
+
return self._handle_initialize(msg.id, params)
|
|
204
|
+
elif method == "initialized":
|
|
205
|
+
# This is a notification, but some clients send it as request
|
|
206
|
+
self._initialized = True
|
|
207
|
+
return MCPProtocol.create_response(msg.id, {})
|
|
208
|
+
elif method == "ping":
|
|
209
|
+
return MCPProtocol.create_response(msg.id, {})
|
|
210
|
+
elif method == "tools/list":
|
|
211
|
+
return self._handle_tools_list(msg.id)
|
|
212
|
+
elif method == "tools/call":
|
|
213
|
+
return self._handle_tools_call(msg.id, params)
|
|
214
|
+
elif method == "resources/list":
|
|
215
|
+
return self._handle_resources_list(msg.id)
|
|
216
|
+
elif method == "resources/read":
|
|
217
|
+
return self._handle_resources_read(msg.id, params)
|
|
218
|
+
else:
|
|
219
|
+
return MCPProtocol.method_not_found(msg.id, method)
|
|
220
|
+
|
|
221
|
+
def _handle_notification(self, msg: MCPMessage) -> None:
|
|
222
|
+
"""Handle notification messages (no response)."""
|
|
223
|
+
if msg.method == "notifications/initialized":
|
|
224
|
+
self._initialized = True
|
|
225
|
+
logger.info("Client initialized")
|
|
226
|
+
elif msg.method == "notifications/cancelled":
|
|
227
|
+
logger.info(f"Request cancelled: {msg.params}")
|
|
228
|
+
|
|
229
|
+
def _handle_initialize(self, id: int, params: Dict[str, Any]) -> MCPMessage:
|
|
230
|
+
"""Handle 'initialize' request."""
|
|
231
|
+
client_info = params.get("clientInfo", {})
|
|
232
|
+
logger.info(f"Client initializing: {client_info.get('name', 'unknown')} "
|
|
233
|
+
f"v{client_info.get('version', 'unknown')}")
|
|
234
|
+
|
|
235
|
+
return MCPProtocol.create_initialize_response(
|
|
236
|
+
id,
|
|
237
|
+
self.server_info,
|
|
238
|
+
self.capabilities
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _handle_tools_list(self, id: int) -> MCPMessage:
|
|
242
|
+
"""Handle 'tools/list' request."""
|
|
243
|
+
tools = self.registry.list_tools()
|
|
244
|
+
logger.debug(f"Listing {len(tools)} tools")
|
|
245
|
+
return MCPProtocol.create_tools_list_response(id, tools)
|
|
246
|
+
|
|
247
|
+
def _handle_tools_call(self, id: int, params: Dict[str, Any]) -> MCPMessage:
|
|
248
|
+
"""Handle 'tools/call' request."""
|
|
249
|
+
tool_name = params.get("name")
|
|
250
|
+
arguments = params.get("arguments", {})
|
|
251
|
+
|
|
252
|
+
if not tool_name:
|
|
253
|
+
return MCPProtocol.invalid_params(id, "Missing 'name' parameter")
|
|
254
|
+
|
|
255
|
+
if tool_name not in self.registry:
|
|
256
|
+
return MCPProtocol.tool_not_found(id, tool_name)
|
|
257
|
+
|
|
258
|
+
logger.info(f"Executing tool: {tool_name}")
|
|
259
|
+
result = self.registry.execute(tool_name, arguments)
|
|
260
|
+
|
|
261
|
+
return MCPProtocol.create_tools_call_response(id, result)
|
|
262
|
+
|
|
263
|
+
def _handle_resources_list(self, id: int) -> MCPMessage:
|
|
264
|
+
"""Handle 'resources/list' request."""
|
|
265
|
+
logger.debug(f"Listing {len(self._resources)} resources")
|
|
266
|
+
return MCPProtocol.create_resources_list_response(id, self._resources)
|
|
267
|
+
|
|
268
|
+
def _handle_resources_read(self, id: int, params: Dict[str, Any]) -> MCPMessage:
|
|
269
|
+
"""Handle 'resources/read' request."""
|
|
270
|
+
uri = params.get("uri")
|
|
271
|
+
|
|
272
|
+
if not uri:
|
|
273
|
+
return MCPProtocol.invalid_params(id, "Missing 'uri' parameter")
|
|
274
|
+
|
|
275
|
+
# Find the resource
|
|
276
|
+
resource = None
|
|
277
|
+
for r in self._resources:
|
|
278
|
+
if r.uri == uri:
|
|
279
|
+
resource = r
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
if resource is None:
|
|
283
|
+
return MCPProtocol.resource_not_found(id, uri)
|
|
284
|
+
|
|
285
|
+
# Read content via provider
|
|
286
|
+
if self.resource_provider:
|
|
287
|
+
try:
|
|
288
|
+
content = self.resource_provider(uri)
|
|
289
|
+
return MCPProtocol.create_resources_read_response(id, [{
|
|
290
|
+
"uri": uri,
|
|
291
|
+
"mimeType": resource.mimeType,
|
|
292
|
+
"text": content
|
|
293
|
+
}])
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.exception(f"Error reading resource: {uri}")
|
|
296
|
+
return MCPProtocol.internal_error(id, f"Error reading resource: {str(e)}")
|
|
297
|
+
else:
|
|
298
|
+
return MCPProtocol.internal_error(id, "No resource provider configured")
|
|
299
|
+
|