da2-mcp-socket 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mcp_socket/server.py ADDED
@@ -0,0 +1,559 @@
1
+ """
2
+ Copyright (c) 2026 Danny, DA2 Studio (https://da2.35g.tw)
3
+ Socket MCP Server - Main server implementation.
4
+
5
+ Provides tools for AI assistants to communicate via TCP/IP sockets
6
+ including connection management, data transmission, and status monitoring.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ import os
12
+ import sys
13
+ from collections.abc import AsyncIterator
14
+ from contextlib import asynccontextmanager
15
+ from dataclasses import dataclass
16
+ from typing import Any, Optional
17
+
18
+ from dotenv import load_dotenv
19
+ from mcp.server.fastmcp import FastMCP
20
+
21
+ # Support both direct execution and module import
22
+ try:
23
+ from .socket_manager import SocketManager
24
+ except ImportError:
25
+ from pathlib import Path
26
+ sys.path.insert(0, str(Path(__file__).parent))
27
+ from socket_manager import SocketManager
28
+
29
+
30
+ # Load environment variables from .env file
31
+ env_path = os.path.join(os.getcwd(), '.env')
32
+ if os.path.exists(env_path):
33
+ load_dotenv(env_path)
34
+ else:
35
+ load_dotenv()
36
+
37
+ # Configure logging
38
+ LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
39
+ logging.basicConfig(
40
+ level=getattr(logging, LOG_LEVEL, logging.INFO),
41
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
42
+ datefmt="%Y-%m-%d %H:%M:%S",
43
+ stream=sys.stderr
44
+ )
45
+ logger = logging.getLogger("mcp-socket")
46
+
47
+ # Default settings from environment
48
+ DEFAULT_TIMEOUT = float(os.getenv("DEFAULT_TIMEOUT", "10.0"))
49
+ DEFAULT_READ_TIMEOUT = float(os.getenv("DEFAULT_READ_TIMEOUT", "5.0"))
50
+ DEFAULT_BUFFER_SIZE = int(os.getenv("DEFAULT_BUFFER_SIZE", "4096"))
51
+
52
+
53
+ # Global socket manager instance
54
+ _socket_manager: Optional[SocketManager] = None
55
+
56
+
57
+ def get_socket_manager() -> SocketManager:
58
+ """Get or create the global socket manager instance."""
59
+ global _socket_manager
60
+ if _socket_manager is None:
61
+ _socket_manager = SocketManager()
62
+ return _socket_manager
63
+
64
+
65
+ @dataclass
66
+ class AppContext:
67
+ """Application context with socket manager."""
68
+ manager: SocketManager
69
+
70
+
71
+ @asynccontextmanager
72
+ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
73
+ """Manage application lifecycle with socket manager."""
74
+ logger.info("Starting Socket MCP Server...")
75
+ manager = get_socket_manager()
76
+ logger.info(f"Default settings: timeout={DEFAULT_TIMEOUT}s, read_timeout={DEFAULT_READ_TIMEOUT}s, buffer_size={DEFAULT_BUFFER_SIZE}")
77
+ try:
78
+ yield AppContext(manager=manager)
79
+ finally:
80
+ logger.info("Shutting down Socket MCP Server...")
81
+ result = manager.disconnect_all()
82
+ if result.get("count", 0) > 0:
83
+ logger.info(f"Closed {result['count']} connections: {result['closed_connections']}")
84
+
85
+
86
+ # Create MCP Server
87
+ mcp = FastMCP(
88
+ "Socket MCP Server",
89
+ lifespan=app_lifespan,
90
+ debug=True
91
+ )
92
+
93
+
94
+ def _format_result(data: Any) -> str:
95
+ """Format data as JSON string for MCP response."""
96
+ return json.dumps(data, ensure_ascii=False, indent=2, default=str)
97
+
98
+
99
+ # ==================== Connection Management ====================
100
+
101
+ @mcp.tool()
102
+ def connect(
103
+ host: str,
104
+ port: int,
105
+ timeout: Optional[float] = None,
106
+ read_timeout: Optional[float] = None,
107
+ buffer_size: Optional[int] = None
108
+ ) -> str:
109
+ """建立 TCP 連線到指定的主機和埠號。
110
+
111
+ 連線成功後會返回 connection_id,用於後續操作。一個 MCP Server 可以同時維護多個連線。
112
+
113
+ Args:
114
+ host: 目標主機(IP 位址或主機名稱)
115
+ port: 目標埠號(1-65535)
116
+ timeout: 連線超時秒數(預設 10.0)
117
+ read_timeout: 讀取超時秒數(預設 5.0)
118
+ buffer_size: 接收緩衝區大小(預設 4096)
119
+
120
+ Returns:
121
+ JSON 格式的執行結果,包含 connection_id
122
+
123
+ Examples:
124
+ - connect("192.168.1.100", 8080)
125
+ - connect("localhost", 80)
126
+ - connect("example.com", 443, timeout=5.0)
127
+ """
128
+ logger.info(f"Connecting to {host}:{port}")
129
+ manager = get_socket_manager()
130
+ result = manager.connect(
131
+ host=host,
132
+ port=port,
133
+ timeout=timeout or DEFAULT_TIMEOUT,
134
+ read_timeout=read_timeout or DEFAULT_READ_TIMEOUT,
135
+ buffer_size=buffer_size or DEFAULT_BUFFER_SIZE
136
+ )
137
+ if result.get("success"):
138
+ logger.info(f"Connected: {result.get('connection_id')}")
139
+ else:
140
+ logger.warning(f"Connection failed: {result.get('error')}")
141
+ return _format_result(result)
142
+
143
+
144
+ @mcp.tool()
145
+ def disconnect(connection_id: str) -> str:
146
+ """關閉指定的 TCP 連線。
147
+
148
+ 關閉連線後會釋放資源,並顯示傳輸統計資訊。
149
+
150
+ Args:
151
+ connection_id: 要關閉的連線 ID(由 connect 返回)
152
+
153
+ Returns:
154
+ JSON 格式的執行結果,包含傳輸統計
155
+
156
+ Examples:
157
+ - disconnect("192.168.1.100:8080_abc12345")
158
+ """
159
+ logger.info(f"Disconnecting: {connection_id}")
160
+ manager = get_socket_manager()
161
+ result = manager.disconnect(connection_id)
162
+ return _format_result(result)
163
+
164
+
165
+ @mcp.tool()
166
+ def get_connection_status(connection_id: Optional[str] = None) -> str:
167
+ """查詢連線狀態。
168
+
169
+ 可以查詢特定連線或所有已建立連線的狀態。
170
+
171
+ Args:
172
+ connection_id: 連線 ID(選填,不指定則列出所有連線)
173
+
174
+ Returns:
175
+ JSON 格式的連線狀態資訊
176
+
177
+ Examples:
178
+ - get_connection_status() # 列出所有連線
179
+ - get_connection_status("192.168.1.100:8080_abc12345") # 查詢特定連線
180
+ """
181
+ manager = get_socket_manager()
182
+ result = manager.get_connection_status(connection_id)
183
+ return _format_result(result)
184
+
185
+
186
+ # ==================== Data Transmission ====================
187
+
188
+ @mcp.tool()
189
+ def send_data(
190
+ connection_id: str,
191
+ data: str,
192
+ encoding: str = "utf-8",
193
+ as_hex: bool = False
194
+ ) -> str:
195
+ """通過 Socket 連線發送資料。
196
+
197
+ 可以發送文字或十六進位資料。
198
+
199
+ Args:
200
+ connection_id: 連線 ID
201
+ data: 要發送的資料(文字或 Hex 字串)
202
+ encoding: 文字編碼(預設 utf-8)
203
+ as_hex: 是否將 data 解析為 Hex 字串(如 \"48454C4C4F\" 或 \"48 45 4C 4C 4F\")
204
+
205
+ Returns:
206
+ JSON 格式的執行結果,包含發送的位元組數
207
+
208
+ Examples:
209
+ - send_data("conn_id", "Hello\\r\\n")
210
+ - send_data("conn_id", "GET / HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n")
211
+ - send_data("conn_id", "48454C4C4F", as_hex=True)
212
+ """
213
+ logger.debug(f"Sending to {connection_id}: {data[:50]}{'...' if len(data) > 50 else ''}")
214
+ manager = get_socket_manager()
215
+ result = manager.send_data(connection_id=connection_id, data=data, encoding=encoding, as_hex=as_hex)
216
+ if result.get("success"):
217
+ logger.debug(f"Sent {result.get('bytes_sent')} bytes")
218
+ return _format_result(result)
219
+
220
+
221
+ @mcp.tool()
222
+ def receive_data(
223
+ connection_id: str,
224
+ size: Optional[int] = None,
225
+ timeout: Optional[float] = None,
226
+ as_hex: bool = False
227
+ ) -> str:
228
+ """從 Socket 連線接收資料。
229
+
230
+ 接收指定數量的位元組,或直到超時。
231
+
232
+ Args:
233
+ connection_id: 連線 ID
234
+ size: 最大接收位元組數(預設使用 buffer_size)
235
+ timeout: 接收超時秒數(選填,預設使用連線的設定)
236
+ as_hex: 只返回 Hex 格式(預設同時返回文字和 Hex)
237
+
238
+ Returns:
239
+ JSON 格式的接收結果,包含資料內容
240
+
241
+ Examples:
242
+ - receive_data("conn_id")
243
+ - receive_data("conn_id", size=1024, timeout=10.0)
244
+ """
245
+ manager = get_socket_manager()
246
+ result = manager.receive_data(connection_id=connection_id, size=size, timeout=timeout, as_hex=as_hex)
247
+ if result.get("success"):
248
+ logger.debug(f"Received {result.get('bytes_received')} bytes")
249
+ return _format_result(result)
250
+
251
+
252
+ @mcp.tool()
253
+ def receive_line(
254
+ connection_id: str,
255
+ timeout: Optional[float] = None,
256
+ encoding: str = "utf-8"
257
+ ) -> str:
258
+ """從 Socket 連線接收一行(直到換行符號)。
259
+
260
+ 適合接收以換行結尾的文字訊息。
261
+
262
+ Args:
263
+ connection_id: 連線 ID
264
+ timeout: 接收超時秒數(選填)
265
+ encoding: 文字編碼(預設 utf-8)
266
+
267
+ Returns:
268
+ JSON 格式的接收結果,包含接收的行
269
+
270
+ Examples:
271
+ - receive_line("conn_id")
272
+ - receive_line("conn_id", timeout=5.0)
273
+ """
274
+ manager = get_socket_manager()
275
+ result = manager.receive_line(connection_id=connection_id, timeout=timeout, encoding=encoding)
276
+ return _format_result(result)
277
+
278
+
279
+ @mcp.tool()
280
+ def receive_until(
281
+ connection_id: str,
282
+ terminator: str = "\n",
283
+ size: int = 4096,
284
+ timeout: Optional[float] = None
285
+ ) -> str:
286
+ """接收資料直到遇到指定的結束符號。
287
+
288
+ Args:
289
+ connection_id: 連線 ID
290
+ terminator: 結束符號(預設換行 \\n)
291
+ size: 最大接收位元組數(預設 4096)
292
+ timeout: 接收超時秒數(選填)
293
+
294
+ Returns:
295
+ JSON 格式的接收結果
296
+
297
+ Examples:
298
+ - receive_until("conn_id", terminator="\\r\\n")
299
+ - receive_until("conn_id", terminator=">", timeout=10.0)
300
+ """
301
+ manager = get_socket_manager()
302
+ result = manager.receive_until(connection_id=connection_id, terminator=terminator, size=size, timeout=timeout)
303
+ return _format_result(result)
304
+
305
+
306
+ # ==================== Protocol Management ====================
307
+
308
+ @mcp.tool()
309
+ def set_protocol(
310
+ connection_id: str,
311
+ mode: str = "raw",
312
+ start_delimiter: Optional[str] = None,
313
+ end_delimiter: Optional[str] = None,
314
+ start_delimiter_hex: Optional[str] = None,
315
+ end_delimiter_hex: Optional[str] = None,
316
+ include_delimiters: bool = False
317
+ ) -> str:
318
+ """設定連線的接收協議(封包格式)。
319
+
320
+ 配置資料應該如何被接收和解析。
321
+
322
+ Args:
323
+ connection_id: 連線 ID
324
+ mode: 接收模式
325
+ - raw: 原始模式,直接接收資料
326
+ - line: 行模式,以換行符號 (\\n) 為結尾
327
+ - packet: 封包模式,使用自訂的起始/結束符號解析資料流
328
+ start_delimiter: 封包起始符號(文字格式)
329
+ end_delimiter: 封包結束符號(文字格式)
330
+ start_delimiter_hex: 起始符號(Hex 格式,如 "0A")
331
+ end_delimiter_hex: 結束符號(Hex 格式,如 "0D0A")
332
+ include_delimiters: 是否在接收的資料中包含分隔符號
333
+
334
+ Returns:
335
+ JSON 格式的協議設定結果
336
+
337
+ Examples:
338
+ - set_protocol("conn_id", mode="line")
339
+ - set_protocol("conn_id", mode="packet", end_delimiter_hex="0D0A")
340
+ - set_protocol("conn_id", mode="packet", start_delimiter_hex="0A", end_delimiter_hex="0D0A")
341
+ """
342
+ manager = get_socket_manager()
343
+ result = manager.set_protocol(
344
+ connection_id=connection_id,
345
+ mode=mode,
346
+ start_delimiter=start_delimiter,
347
+ end_delimiter=end_delimiter,
348
+ start_delimiter_hex=start_delimiter_hex,
349
+ end_delimiter_hex=end_delimiter_hex,
350
+ include_delimiters=include_delimiters
351
+ )
352
+ if result.get("success"):
353
+ logger.info(f"Set protocol for {connection_id}: mode={mode}")
354
+ return _format_result(result)
355
+
356
+
357
+ @mcp.tool()
358
+ def receive_packet(
359
+ connection_id: str,
360
+ timeout: Optional[float] = None,
361
+ max_size: int = 4096
362
+ ) -> str:
363
+ """依照設定的協議接收完整封包。
364
+
365
+ 使用 set_protocol 設定的起始/結束符號解析資料流。
366
+ - packet 模式:等待 start_delimiter 和 end_delimiter 之間的完整資料
367
+ - line 模式:等同於 receive_line
368
+ - raw 模式:等同於 receive_data
369
+
370
+ Args:
371
+ connection_id: 連線 ID
372
+ timeout: 接收超時秒數(選填)
373
+ max_size: 最大封包大小(預設 4096)
374
+
375
+ Returns:
376
+ JSON 格式的封包資料,包含 payload_hex 和 payload_text
377
+
378
+ Examples:
379
+ - receive_packet("conn_id")
380
+ - receive_packet("conn_id", timeout=10.0)
381
+ """
382
+ manager = get_socket_manager()
383
+ result = manager.receive_packet(
384
+ connection_id=connection_id,
385
+ timeout=timeout,
386
+ max_size=max_size
387
+ )
388
+ return _format_result(result)
389
+
390
+
391
+ @mcp.tool()
392
+ def get_protocol(connection_id: str) -> str:
393
+ """查詢連線的當前接收協議設定。
394
+
395
+ Args:
396
+ connection_id: 連線 ID
397
+
398
+ Returns:
399
+ JSON 格式的協議設定資訊
400
+
401
+ Examples:
402
+ - get_protocol("conn_id")
403
+ """
404
+ manager = get_socket_manager()
405
+ result = manager.get_protocol(connection_id)
406
+ return _format_result(result)
407
+
408
+
409
+ # ==================== Background Listener ====================
410
+
411
+ @mcp.tool()
412
+ def start_listening(connection_id: str) -> str:
413
+ """啟動背景封包監聽器。
414
+
415
+ 開始在背景執行緒中持續接收資料並解析封包,解析後的封包會存入 buffer。
416
+ 使用 get_packets 取得累積的封包。
417
+
418
+ Args:
419
+ connection_id: 連線 ID
420
+
421
+ Returns:
422
+ JSON 格式的執行結果
423
+
424
+ Examples:
425
+ - start_listening("conn_id")
426
+ """
427
+ logger.info(f"Starting listener for {connection_id}")
428
+ manager = get_socket_manager()
429
+ result = manager.start_listening(connection_id)
430
+ return _format_result(result)
431
+
432
+
433
+ @mcp.tool()
434
+ def stop_listening(connection_id: str) -> str:
435
+ """停止背景封包監聽器。
436
+
437
+ Args:
438
+ connection_id: 連線 ID
439
+
440
+ Returns:
441
+ JSON 格式的執行結果
442
+
443
+ Examples:
444
+ - stop_listening("conn_id")
445
+ """
446
+ logger.info(f"Stopping listener for {connection_id}")
447
+ manager = get_socket_manager()
448
+ result = manager.stop_listening(connection_id)
449
+ return _format_result(result)
450
+
451
+
452
+ @mcp.tool()
453
+ def get_packets(connection_id: str, clear: bool = True) -> str:
454
+ """取得所有累積的封包。
455
+
456
+ 返回背景監聽器收到的所有封包陣列,預設會清空 buffer。
457
+
458
+ Args:
459
+ connection_id: 連線 ID
460
+ clear: 取得後是否清空 buffer(預設 True)
461
+
462
+ Returns:
463
+ JSON 格式的封包陣列
464
+
465
+ Examples:
466
+ - get_packets("conn_id")
467
+ - get_packets("conn_id", clear=False)
468
+ """
469
+ manager = get_socket_manager()
470
+ result = manager.get_packets(connection_id, clear=clear)
471
+ if result.get("success"):
472
+ logger.info(f"Got {result.get('count', 0)} packets from {connection_id}")
473
+ return _format_result(result)
474
+
475
+
476
+ @mcp.tool()
477
+ def get_buffer_status(connection_id: str) -> str:
478
+ """查詢 buffer 狀態。
479
+
480
+ 返回目前 buffer 中的封包數量、最大容量、溢出次數等資訊。
481
+
482
+ Args:
483
+ connection_id: 連線 ID
484
+
485
+ Returns:
486
+ JSON 格式的 buffer 狀態
487
+
488
+ Examples:
489
+ - get_buffer_status("conn_id")
490
+ """
491
+ manager = get_socket_manager()
492
+ result = manager.get_buffer_status(connection_id)
493
+ return _format_result(result)
494
+
495
+
496
+ # ==================== Server Info ====================
497
+
498
+ @mcp.tool()
499
+ def get_server_info() -> str:
500
+ """取得 MCP Server 的資訊。
501
+
502
+ 返回伺服器版本、支援的功能和預設設定。
503
+
504
+ Returns:
505
+ JSON 格式的伺服器資訊
506
+ """
507
+ manager = get_socket_manager()
508
+ status = manager.get_connection_status()
509
+
510
+ return _format_result({
511
+ "server_name": "Socket MCP Server",
512
+ "version": "0.2.0",
513
+ "features": {
514
+ "connection_management": ["connect", "disconnect", "status"],
515
+ "data_transmission": ["send_data", "receive_data", "receive_line", "receive_until"],
516
+ "protocol_management": ["set_protocol", "receive_packet", "get_protocol"],
517
+ "background_listener": ["start_listening", "stop_listening", "get_packets", "get_buffer_status"],
518
+ },
519
+ "default_settings": {
520
+ "timeout": DEFAULT_TIMEOUT,
521
+ "read_timeout": DEFAULT_READ_TIMEOUT,
522
+ "buffer_size": DEFAULT_BUFFER_SIZE
523
+ },
524
+ "protocol_modes": ["raw", "line", "packet"],
525
+ "active_connections": status.get("count", 0)
526
+ })
527
+
528
+
529
+ # ==================== Entry Point ====================
530
+
531
+ def main():
532
+ """Entry point for the MCP server."""
533
+ import argparse
534
+
535
+ parser = argparse.ArgumentParser(description="Socket MCP Server")
536
+ parser.add_argument("--http", action="store_true", help="Run in HTTP mode (default: stdio mode)")
537
+ parser.add_argument("--port", type=int, default=8000, help="Port for HTTP mode (default: 8000)")
538
+ parser.add_argument("--host", type=str, default="0.0.0.0", help="Host for HTTP mode (default: 0.0.0.0)")
539
+ args = parser.parse_args()
540
+
541
+ logger.info("=" * 50)
542
+ logger.info("Socket MCP Server v0.1.0")
543
+ logger.info("=" * 50)
544
+ logger.info(f"Log level: {LOG_LEVEL}")
545
+
546
+ if args.http:
547
+ mcp.settings.host = args.host
548
+ mcp.settings.port = args.port
549
+ logger.info(f"Starting HTTP mode on http://localhost:{args.port}/mcp")
550
+ logger.info("Use MCP Inspector to connect: npx @modelcontextprotocol/inspector")
551
+ mcp.run(transport="streamable-http")
552
+ else:
553
+ logger.info("Starting stdio mode (for Claude Desktop, Cursor, etc.)")
554
+ logger.info("Waiting for MCP client connection...")
555
+ mcp.run()
556
+
557
+
558
+ if __name__ == "__main__":
559
+ main()