libentry 1.23.2__py3-none-any.whl → 1.23.4__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.
libentry/mcp/api.py CHANGED
@@ -19,6 +19,12 @@ from pydantic import BaseModel, ConfigDict
19
19
 
20
20
  API_INFO_FIELD = "__api_info__"
21
21
 
22
+ TAG_SUBROUTINE = "subroutine"
23
+ TAG_JSONRPC = "jsonrpc"
24
+ TAG_ENDPOINT = "endpoint"
25
+ TAG_TOOL = "tool"
26
+ TAG_RESOURCE = "resource"
27
+
22
28
 
23
29
  class APIInfo(BaseModel):
24
30
  path: str
@@ -69,7 +75,7 @@ def tool(path=None, name=None, description=None, **kwargs):
69
75
  methods=["POST"],
70
76
  name=name,
71
77
  description=description,
72
- tag="tool",
78
+ tag=TAG_TOOL,
73
79
  **kwargs
74
80
  )
75
81
 
@@ -80,7 +86,7 @@ def resource(uri, name=None, description=None, mime_type=None, size=None, **kwar
80
86
  methods=["POST"],
81
87
  name=name,
82
88
  description=description,
83
- tag="resource",
89
+ tag=TAG_RESOURCE,
84
90
  mimeType=mime_type,
85
91
  size=size,
86
92
  uri=uri,
libentry/mcp/client.py CHANGED
@@ -244,6 +244,9 @@ class MCPMixIn(JSONRPCMixIn, abc.ABC):
244
244
 
245
245
  return InitializeResult.model_validate(result)
246
246
 
247
+ def ping(self):
248
+ return self.call("ping")
249
+
247
250
  def list_tools(self) -> ListToolsResult:
248
251
  result = self.call("tools/list")
249
252
  return ListToolsResult.model_validate(result)
@@ -530,51 +533,83 @@ class APIClient(SubroutineMixIn, MCPMixIn):
530
533
 
531
534
  class SSESession(MCPMixIn):
532
535
 
533
- def __init__(self, client: APIClient, sse_endpoint: str):
536
+ def __init__(self, client: APIClient, sse_endpoint: str, sse_timeout: int = 6):
534
537
  self.client = client
535
538
  self.sse_endpoint = sse_endpoint
536
-
537
- self.sse_thread = Thread(target=self._sse_loop, daemon=True)
538
- self.sse_thread.start()
539
+ self.sse_timeout = sse_timeout
539
540
 
540
541
  self.lock = Semaphore(0)
541
542
  self.endpoint = None
542
543
  self.pendings = {}
544
+ self.closed = False
545
+
546
+ self.ping_thread = Thread(target=self._ping_loop, daemon=False)
547
+ self.ping_thread.start()
548
+
549
+ self.sse_thread = Thread(target=self._sse_loop, daemon=False)
550
+ self.sse_thread.start()
551
+
552
+ def __enter__(self):
553
+ return self
554
+
555
+ def __exit__(self, exc_type, exc_val, exc_tb):
556
+ self.close()
557
+
558
+ def close(self):
559
+ with self.lock:
560
+ self.closed = True
561
+
562
+ def _ping_loop(self):
563
+ interval = max(self.sse_timeout / 2, 0.5)
564
+ while True:
565
+ with self.lock:
566
+ if self.closed:
567
+ break
568
+ sleep(interval)
569
+ self.ping()
543
570
 
544
571
  def _sse_loop(self):
545
572
  request = HTTPRequest(
546
573
  path=self.sse_endpoint,
547
574
  options=HTTPOptions(
548
575
  method="GET",
549
- timeout=60
576
+ timeout=self.sse_timeout
550
577
  )
551
578
  )
552
579
  response = self.client.http_request(request)
553
580
  assert response.stream
554
581
  type_adapter = TypeAdapter(Union[JSONRPCRequest, JSONRPCResponse, JSONRPCNotification])
555
- for sse in response.content:
556
- assert isinstance(sse, SSE)
557
- if sse.event == "endpoint":
558
- self.endpoint = sse.data
559
- self.lock.release()
560
- elif sse.event == "message":
561
- json_obj = json.loads(sse.data)
562
- obj = type_adapter.validate_python(json_obj)
563
- if isinstance(obj, JSONRPCRequest):
564
- self._on_request(obj)
565
- elif isinstance(obj, JSONRPCNotification):
566
- self._on_notification(obj)
567
- elif isinstance(obj, JSONRPCResponse):
568
- self._on_response(obj)
582
+ try:
583
+ for sse in response.content:
584
+ assert isinstance(sse, SSE)
585
+ if sse.event == "endpoint":
586
+ self.endpoint = sse.data
587
+ self.lock.release()
588
+ elif sse.event == "message":
589
+ json_obj = json.loads(sse.data)
590
+ obj = type_adapter.validate_python(json_obj)
591
+ if isinstance(obj, JSONRPCRequest):
592
+ self._on_request(obj)
593
+ elif isinstance(obj, JSONRPCNotification):
594
+ self._on_notification(obj)
595
+ elif isinstance(obj, JSONRPCResponse):
596
+ self._on_response(obj)
597
+ else:
598
+ pass
569
599
  else:
570
- pass
571
- else:
572
- raise RuntimeError(f"Unknown event {sse.event}.")
600
+ raise RuntimeError(f"Unknown event {sse.event}.")
601
+ with self.lock:
602
+ if self.closed:
603
+ break
604
+ except httpx.Timeout:
605
+ pass
573
606
 
574
607
  def _on_request(self, request: JSONRPCRequest):
608
+ print(request)
575
609
  pass
576
610
 
577
611
  def _on_notification(self, notification: JSONRPCNotification):
612
+ print(notification)
578
613
  pass
579
614
 
580
615
  def _on_response(self, response: JSONRPCResponse):
libentry/mcp/service.py CHANGED
@@ -233,18 +233,18 @@ class FlaskHandler:
233
233
 
234
234
  self.subroutine_adapter = SubroutineAdapter(fn, self.api_signature)
235
235
  self.jsonrpc_adapter = JSONRPCAdapter(fn, self.api_signature)
236
- self.default_adapter = self.subroutine_adapter
237
236
 
237
+ adapter_mapping = {
238
+ api.TAG_ENDPOINT: self.fn,
239
+ "free": self.fn,
240
+ "schema_free": self.fn,
241
+ "schema-free": self.fn,
242
+ api.TAG_JSONRPC: self.jsonrpc_adapter,
243
+ "rpc": self.jsonrpc_adapter,
244
+ "mcp": self.jsonrpc_adapter,
245
+ }
238
246
  tag = self.api_info.tag if self.api_info else None
239
- if tag is not None:
240
- tag = tag.lower()
241
- if tag in {"free", "schema_free", "schema-free"}:
242
- self.default_adapter = self.fn
243
- elif tag in {"jsonrpc", "rpc"}:
244
- self.default_adapter = self.jsonrpc_adapter
245
-
246
- # todo: debug info
247
- print(f"{api_info.path}: {type(self.default_adapter)}\t{self.api_signature.input_model}")
247
+ self.default_adapter = adapter_mapping.get(tag, self.subroutine_adapter)
248
248
 
249
249
  def __call__(self):
250
250
  args = flask_request.args
@@ -369,7 +369,7 @@ class SSEService:
369
369
  self.sse_dict = {}
370
370
 
371
371
  # noinspection PyUnusedLocal
372
- @api.get("/sse", tag="schema_free")
372
+ @api.get("/sse", tag=api.TAG_ENDPOINT)
373
373
  def sse(self, raw_request: Dict[str, Any]) -> Iterable[SSE]:
374
374
  session_id = str(uuid.uuid4())
375
375
  queue = Queue(8)
@@ -395,7 +395,7 @@ class SSEService:
395
395
 
396
396
  return _stream()
397
397
 
398
- @api.route("/sse/message", tag="schema_free")
398
+ @api.route("/sse/message", tag=api.TAG_ENDPOINT)
399
399
  def sse_message(self, raw_request: Dict[str, Any]) -> None:
400
400
  ################################################################################
401
401
  # session validation
@@ -471,7 +471,7 @@ class JSONRPCService:
471
471
 
472
472
  self.type_adapter = TypeAdapter(Union[JSONRPCRequest, JSONRPCNotification])
473
473
 
474
- @api.route(tag="schema_free")
474
+ @api.route(tag=api.TAG_ENDPOINT)
475
475
  def message(self, raw_request: Dict[str, Any]) -> Union[JSONRPCResponse, Iterable[JSONRPCResponse], None]:
476
476
  request = self.type_adapter.validate_python(raw_request)
477
477
  path = f"/{request.method}"
@@ -521,7 +521,7 @@ class ToolsService:
521
521
  self._tool_routes = {}
522
522
  for route in self.service_routes.values():
523
523
  api_info = route.api_info
524
- if api_info.tag != "tool":
524
+ if api_info.tag != api.TAG_TOOL:
525
525
  continue
526
526
  self._tool_routes[api_info.name] = route
527
527
  return self._tool_routes
@@ -636,7 +636,7 @@ class ResourcesService:
636
636
  self._resource_routes = {}
637
637
  for route in self.service_routes.values():
638
638
  api_info = route.api_info
639
- if api_info.tag != "resource":
639
+ if api_info.tag != api.TAG_RESOURCE:
640
640
  continue
641
641
  uri = api_info.model_extra.get("uri", api_info.path)
642
642
  self._resource_routes[uri] = route
@@ -771,10 +771,15 @@ class FlaskServer(Flask):
771
771
  logger.error(f"Async function \"{fn.__name__}\" is not supported.")
772
772
  continue
773
773
 
774
- logger.info(f"Serving {path} as {', '.join(methods)}.")
775
-
776
774
  handler = FlaskHandler(fn, api_info, self)
777
775
  routes[path] = Route(api_info=api_info, fn=fn, handler=handler)
776
+
777
+ mode = api.TAG_ENDPOINT
778
+ if isinstance(handler.default_adapter, SubroutineAdapter):
779
+ mode = api.TAG_SUBROUTINE
780
+ elif isinstance(handler.default_adapter, JSONRPCAdapter):
781
+ mode = api.TAG_JSONRPC
782
+ logger.info(f"{mode.capitalize()}:\tmethod={'|'.join(methods)}\tpath={path}")
778
783
  return routes
779
784
 
780
785
  def ok(self, body: Union[str, Iterable[str], None], mimetype: str):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: libentry
3
- Version: 1.23.2
3
+ Version: 1.23.4
4
4
  Summary: Entries for experimental utilities.
5
5
  Home-page: https://github.com/XoriieInpottn/libentry
6
6
  Author: xi
@@ -10,9 +10,9 @@ libentry/schema.py,sha256=40SOhCF_eytWOF47MWKCRHKHl_lCaQVetx1Af62PkiI,10439
10
10
  libentry/test_api.py,sha256=Xw7B7sH6g1iCTV5sFzyBF3JAJzeOr9xg0AyezTNsnIk,4452
11
11
  libentry/utils.py,sha256=O7P6GadtUIjq0N2IZH7PhHZDUM3NebzcqyDqytet7CM,683
12
12
  libentry/mcp/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
13
- libentry/mcp/api.py,sha256=uoGBYCesMj6umlJpRulKZNS3trm9oG3LUSg1otPDS_8,2362
14
- libentry/mcp/client.py,sha256=lM_bTF40pbdYdBrMmoOqUDRzlNgjqEKh5d4IVkpI6D8,21512
15
- libentry/mcp/service.py,sha256=KDpEUhHuyVXjc_J5Z9_aciJbTcEy9dYA44rpdgAAwGE,32322
13
+ libentry/mcp/api.py,sha256=KSvz6_TNGKQQoFJPNH1XIc8CMji0I2_CqC9VCSOPRfc,2491
14
+ libentry/mcp/client.py,sha256=tEJbMpXiMKQ0qDgLGyavE2A4jpoXliqbrVj_TMfOGXI,22488
15
+ libentry/mcp/service.py,sha256=xInlf_u8AVlGtnWH6gky_l3IolmYahm-FYMqsbm6t3w,32563
16
16
  libentry/mcp/types.py,sha256=xTQCnKAgeJNss4klJ33MrWHGCzG_LeR3urizO_Z9q9U,12239
17
17
  libentry/service/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
18
18
  libentry/service/common.py,sha256=OVaW2afgKA6YqstJmtnprBCqQEUZEWotZ6tHavmJJeU,42
@@ -21,10 +21,10 @@ libentry/service/list.py,sha256=ElHWhTgShGOhaxMUEwVbMXos0NQKjHsODboiQ-3AMwE,1397
21
21
  libentry/service/running.py,sha256=FrPJoJX6wYxcHIysoatAxhW3LajCCm0Gx6l7__6sULQ,5105
22
22
  libentry/service/start.py,sha256=mZT7b9rVULvzy9GTZwxWnciCHgv9dbGN2JbxM60OMn4,1270
23
23
  libentry/service/stop.py,sha256=wOpwZgrEJ7QirntfvibGq-XsTC6b3ELhzRW2zezh-0s,1187
24
- libentry-1.23.2.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
25
- libentry-1.23.2.dist-info/METADATA,sha256=8Gc8UbjbMnPJn_EUsJoYzJRHOCOxVWcLVPGvi_wwKSs,1135
26
- libentry-1.23.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
27
- libentry-1.23.2.dist-info/entry_points.txt,sha256=1v_nLVDsjvVJp9SWhl4ef2zZrsLTBtFWgrYFgqvQBgc,61
28
- libentry-1.23.2.dist-info/top_level.txt,sha256=u2uF6-X5fn2Erf9PYXOg_6tntPqTpyT-yzUZrltEd6I,9
29
- libentry-1.23.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
30
- libentry-1.23.2.dist-info/RECORD,,
24
+ libentry-1.23.4.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
25
+ libentry-1.23.4.dist-info/METADATA,sha256=-gzj-ialfjOVVPyc_tv-Jj1qH7LlrauZnossj2nOFX4,1135
26
+ libentry-1.23.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
27
+ libentry-1.23.4.dist-info/entry_points.txt,sha256=1v_nLVDsjvVJp9SWhl4ef2zZrsLTBtFWgrYFgqvQBgc,61
28
+ libentry-1.23.4.dist-info/top_level.txt,sha256=u2uF6-X5fn2Erf9PYXOg_6tntPqTpyT-yzUZrltEd6I,9
29
+ libentry-1.23.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
30
+ libentry-1.23.4.dist-info/RECORD,,