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.
Files changed (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -680
  3. package/bin/pomera-ai-commander.js +62 -62
  4. package/core/__init__.py +65 -65
  5. package/core/app_context.py +482 -482
  6. package/core/async_text_processor.py +421 -421
  7. package/core/backup_manager.py +655 -655
  8. package/core/backup_recovery_manager.py +1033 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_validator.py +1066 -1066
  12. package/core/database_connection_manager.py +744 -744
  13. package/core/database_curl_settings_manager.py +608 -608
  14. package/core/database_promera_ai_settings_manager.py +446 -446
  15. package/core/database_schema.py +411 -411
  16. package/core/database_schema_manager.py +395 -395
  17. package/core/database_settings_manager.py +1507 -1507
  18. package/core/database_settings_manager_interface.py +456 -456
  19. package/core/dialog_manager.py +734 -734
  20. package/core/efficient_line_numbers.py +510 -510
  21. package/core/error_handler.py +746 -746
  22. package/core/error_service.py +431 -431
  23. package/core/event_consolidator.py +511 -511
  24. package/core/mcp/__init__.py +43 -43
  25. package/core/mcp/protocol.py +288 -288
  26. package/core/mcp/schema.py +251 -251
  27. package/core/mcp/server_stdio.py +299 -299
  28. package/core/mcp/tool_registry.py +2372 -2345
  29. package/core/memory_efficient_text_widget.py +711 -711
  30. package/core/migration_manager.py +914 -914
  31. package/core/migration_test_suite.py +1085 -1085
  32. package/core/migration_validator.py +1143 -1143
  33. package/core/optimized_find_replace.py +714 -714
  34. package/core/optimized_pattern_engine.py +424 -424
  35. package/core/optimized_search_highlighter.py +552 -552
  36. package/core/performance_monitor.py +674 -674
  37. package/core/persistence_manager.py +712 -712
  38. package/core/progressive_stats_calculator.py +632 -632
  39. package/core/regex_pattern_cache.py +529 -529
  40. package/core/regex_pattern_library.py +350 -350
  41. package/core/search_operation_manager.py +434 -434
  42. package/core/settings_defaults_registry.py +1087 -1087
  43. package/core/settings_integrity_validator.py +1111 -1111
  44. package/core/settings_serializer.py +557 -557
  45. package/core/settings_validator.py +1823 -1823
  46. package/core/smart_stats_calculator.py +709 -709
  47. package/core/statistics_update_manager.py +619 -619
  48. package/core/stats_config_manager.py +858 -858
  49. package/core/streaming_text_handler.py +723 -723
  50. package/core/task_scheduler.py +596 -596
  51. package/core/update_pattern_library.py +168 -168
  52. package/core/visibility_monitor.py +596 -596
  53. package/core/widget_cache.py +498 -498
  54. package/mcp.json +51 -61
  55. package/package.json +61 -57
  56. package/pomera.py +7482 -7482
  57. package/pomera_mcp_server.py +183 -144
  58. package/requirements.txt +32 -0
  59. package/tools/__init__.py +4 -4
  60. package/tools/ai_tools.py +2891 -2891
  61. package/tools/ascii_art_generator.py +352 -352
  62. package/tools/base64_tools.py +183 -183
  63. package/tools/base_tool.py +511 -511
  64. package/tools/case_tool.py +308 -308
  65. package/tools/column_tools.py +395 -395
  66. package/tools/cron_tool.py +884 -884
  67. package/tools/curl_history.py +600 -600
  68. package/tools/curl_processor.py +1207 -1207
  69. package/tools/curl_settings.py +502 -502
  70. package/tools/curl_tool.py +5467 -5467
  71. package/tools/diff_viewer.py +1071 -1071
  72. package/tools/email_extraction_tool.py +248 -248
  73. package/tools/email_header_analyzer.py +425 -425
  74. package/tools/extraction_tools.py +250 -250
  75. package/tools/find_replace.py +1750 -1750
  76. package/tools/folder_file_reporter.py +1463 -1463
  77. package/tools/folder_file_reporter_adapter.py +480 -480
  78. package/tools/generator_tools.py +1216 -1216
  79. package/tools/hash_generator.py +255 -255
  80. package/tools/html_tool.py +656 -656
  81. package/tools/jsonxml_tool.py +729 -729
  82. package/tools/line_tools.py +419 -419
  83. package/tools/markdown_tools.py +561 -561
  84. package/tools/mcp_widget.py +1417 -1417
  85. package/tools/notes_widget.py +973 -973
  86. package/tools/number_base_converter.py +372 -372
  87. package/tools/regex_extractor.py +571 -571
  88. package/tools/slug_generator.py +310 -310
  89. package/tools/sorter_tools.py +458 -458
  90. package/tools/string_escape_tool.py +392 -392
  91. package/tools/text_statistics_tool.py +365 -365
  92. package/tools/text_wrapper.py +430 -430
  93. package/tools/timestamp_converter.py +421 -421
  94. package/tools/tool_loader.py +710 -710
  95. package/tools/translator_tools.py +522 -522
  96. package/tools/url_link_extractor.py +261 -261
  97. package/tools/url_parser.py +204 -204
  98. package/tools/whitespace_tools.py +355 -355
  99. package/tools/word_frequency_counter.py +146 -146
  100. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  102. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  103. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  104. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  105. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  106. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  107. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  108. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  109. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  110. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  111. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  112. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  113. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  114. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  115. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  116. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  117. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  118. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  119. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  120. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  121. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  122. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  123. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  124. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  125. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  126. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  127. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  128. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  129. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  131. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  132. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  134. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  135. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  136. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  137. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  138. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  139. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  140. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  141. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  142. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  143. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  144. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  145. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  146. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  147. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  148. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  151. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  152. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  153. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  154. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  155. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  156. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  157. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  158. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  159. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  160. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  161. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  162. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  163. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  164. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  165. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  166. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  167. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  168. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  169. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  170. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  191. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
@@ -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
+