aiohttp-tiny-mcp 0.5.6__tar.gz → 0.5.7__tar.gz

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 (58) hide show
  1. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/PKG-INFO +1 -1
  2. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/pyproject.toml +1 -1
  3. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/pyproject.toml.orig +1 -1
  4. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/adapter.py +16 -1
  5. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/models.py +11 -0
  6. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/v2024_11_05.py +2 -2
  7. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/v2025_03_26.py +2 -2
  8. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/v2025_11_25.py +1 -1
  9. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/v2026_07_28.py +1 -1
  10. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/exchange.py +2 -2
  11. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/http.py +12 -9
  12. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/specs.py +11 -1
  13. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/sse.py +4 -2
  14. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/stdio.py +7 -5
  15. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/transport/sse.py +3 -2
  16. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/LICENSE +0 -0
  17. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/README.md +0 -0
  18. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/__init__.py +0 -0
  19. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/auth/__init__.py +0 -0
  20. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/auth/jwt.py +0 -0
  21. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/client/__init__.py +0 -0
  22. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/client/base.py +0 -0
  23. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/client/http.py +0 -0
  24. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/client/stdio.py +0 -0
  25. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/console/__init__.py +0 -0
  26. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/console/console.css +0 -0
  27. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/console/console.js +0 -0
  28. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/console/index.html +0 -0
  29. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/console/oauth-callback.html +0 -0
  30. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/extensions/__init__.py +0 -0
  31. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/extensions/skills.py +0 -0
  32. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/oauth/__init__.py +0 -0
  33. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/oauth/github.py +0 -0
  34. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/oauth/provider.py +0 -0
  35. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/oauth/server.py +0 -0
  36. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/oauth/tokens.py +0 -0
  37. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/__init__.py +0 -0
  38. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/core.py +0 -0
  39. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/schema.py +0 -0
  40. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/selection.py +0 -0
  41. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/protocol/v2025_06_18.py +0 -0
  42. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/py.typed +0 -0
  43. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/__init__.py +0 -0
  44. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/dispatcher.py +0 -0
  45. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/registry.py +0 -0
  46. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/request_state.py +0 -0
  47. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/subscriptions.py +0 -0
  48. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/server/tasks.py +0 -0
  49. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/storage/__init__.py +0 -0
  50. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/storage/hub.py +0 -0
  51. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/storage/namespaces.py +0 -0
  52. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/storage/postgres.py +0 -0
  53. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/storage/redis.py +0 -0
  54. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/storage/sessions.py +0 -0
  55. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/storage/sqlite.py +0 -0
  56. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/testing.py +0 -0
  57. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/transport/__init__.py +0 -0
  58. {aiohttp_tiny_mcp-0.5.6 → aiohttp_tiny_mcp-0.5.7}/src/aiohttp_tiny_mcp/transport/metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohttp-tiny-mcp
3
- Version: 0.5.6
3
+ Version: 0.5.7
4
4
  Summary: Remote MCP server and client library for aiohttp
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiohttp-tiny-mcp"
3
- version = "0.5.6"
3
+ version = "0.5.7"
4
4
  description = "Remote MCP server and client library for aiohttp"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiohttp-tiny-mcp"
3
- version = "0.5.6"
3
+ version = "0.5.7"
4
4
  description = "Remote MCP server and client library for aiohttp"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -320,8 +320,23 @@ class Adapter(ABC):
320
320
  def describe_server(self, registry: RegistryProtocol, call: Call) -> BaseModel:
321
321
  """Negotiate the requested legacy revision from `call.params.protocol_version`."""
322
322
 
323
+ def describe_tool(self, spec: ToolSpec) -> ToolDef | None:
324
+ """Return how this revision shows `spec`, or None where it hides it.
325
+
326
+ The projection is cached on the spec. A tool does not change after
327
+ registration, so each revision simplifies its schemas once instead of
328
+ once per `tools/list` and once per `tools/call`.
329
+ """
330
+ cache = spec.described
331
+ try:
332
+ return cache[self.version]
333
+ except KeyError:
334
+ definition = cache[self.version] = self.build_tool(spec)
335
+ return definition
336
+
323
337
  @abstractmethod
324
- def describe_tool(self, spec: ToolSpec) -> ToolDef | None: ...
338
+ def build_tool(self, spec: ToolSpec) -> ToolDef | None:
339
+ """Project `spec` onto this revision. `describe_tool` caches the result."""
325
340
 
326
341
  def describe_resource(self, spec: ResourceSpec) -> ResourceDef | ResourceTemplateDef | None:
327
342
  """Project extension resources onto the URI understood by this revision."""
@@ -10,6 +10,17 @@ from typing import Annotated, Any, Literal
10
10
 
11
11
  from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr
12
12
  from pydantic.alias_generators import to_camel
13
+ from pydantic_core import to_json
14
+
15
+
16
+ def serialized(payload: Any) -> bytes:
17
+ """Encode one wire payload as UTF-8 JSON.
18
+
19
+ `to_json` is the serializer pydantic already carries. It is about three
20
+ times faster than `json.dumps` and writes the same bytes for the JSON-mode
21
+ values `Model.wire` returns. Non-ASCII characters stay as they are.
22
+ """
23
+ return to_json(payload)
13
24
 
14
25
 
15
26
  class Model(BaseModel):
@@ -16,8 +16,8 @@ class Adapter2024_11_05(Adapter2025_03_26): # noqa: N801 -- revision date, grep
16
16
  allows_batch: ClassVar[bool] = False
17
17
  progress_message: ClassVar[bool] = False
18
18
 
19
- def describe_tool(self, spec: ToolSpec) -> ToolDef | None:
20
- definition = super().describe_tool(spec)
19
+ def build_tool(self, spec: ToolSpec) -> ToolDef | None:
20
+ definition = super().build_tool(spec)
21
21
  if definition is None:
22
22
  return None
23
23
  return definition.model_copy(update={"annotations": None})
@@ -86,8 +86,8 @@ class Adapter2025_03_26(Adapter2025_06_18): # noqa: N801 -- revision date, grep
86
86
  """Only handlers receiving Exchange need the extra answer/state arguments."""
87
87
  return any(kind.__name__ == "Exchange" for _, kind in spec.bound.plan)
88
88
 
89
- def describe_tool(self, spec: ToolSpec) -> ToolDef | None:
90
- definition = super().describe_tool(spec)
89
+ def build_tool(self, spec: ToolSpec) -> ToolDef | None:
90
+ definition = super().build_tool(spec)
91
91
  if definition is None:
92
92
  return None
93
93
  definition = definition.model_copy(update={"output_schema": None, "title": None})
@@ -135,7 +135,7 @@ class Adapter2025_11_25(Adapter): # noqa: N801 -- revision date, greppable agai
135
135
  instructions=registry.instructions,
136
136
  )
137
137
 
138
- def describe_tool(self, spec: ToolSpec) -> ToolDef | None:
138
+ def build_tool(self, spec: ToolSpec) -> ToolDef | None:
139
139
  if spec.min_revision is not None and spec.min_revision > self.version:
140
140
  return None
141
141
  schema = simplify_legacy_schema(spec.input_schema)
@@ -341,7 +341,7 @@ class Adapter2026_07_28(Adapter): # noqa: N801 -- revision date, greppable agai
341
341
  caps["extensions"] = extensions
342
342
  return caps
343
343
 
344
- def describe_tool(self, spec: ToolSpec) -> ToolDef | None:
344
+ def build_tool(self, spec: ToolSpec) -> ToolDef | None:
345
345
  if spec.min_revision is not None and spec.min_revision > self.version:
346
346
  return None
347
347
  return spec.definition(spec.input_schema, spec.output_schema)
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import inspect
7
- import json
8
7
  import logging
9
8
  from collections.abc import Awaitable, Callable, Mapping
10
9
  from contextlib import AsyncExitStack, asynccontextmanager
@@ -25,6 +24,7 @@ from aiohttp_tiny_mcp.protocol.core import (
25
24
  answer_of,
26
25
  logs_at,
27
26
  )
27
+ from aiohttp_tiny_mcp.protocol.models import serialized
28
28
  from aiohttp_tiny_mcp.storage.hub import ASK, Hub, topic
29
29
  from aiohttp_tiny_mcp.storage.sessions import Session, SessionAccess
30
30
  from aiohttp_tiny_mcp.storage.sessions import new_session_id as new_id
@@ -232,7 +232,7 @@ class Exchange:
232
232
  await self.send(payload)
233
233
  return
234
234
  assert self.sse is not None
235
- text = json.dumps(payload, ensure_ascii=False)
235
+ text = serialized(payload).decode()
236
236
  log.debug("-> [%s] %s", self.adapter.version, text)
237
237
  await self.sse.send(text)
238
238
 
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import json
7
6
  import logging
8
7
  import re
9
8
  from collections.abc import Mapping
@@ -26,6 +25,7 @@ from aiohttp_tiny_mcp.protocol.core import (
26
25
  Rejected,
27
26
  Value,
28
27
  )
28
+ from aiohttp_tiny_mcp.protocol.models import serialized
29
29
  from aiohttp_tiny_mcp.protocol.selection import AdapterSet
30
30
  from aiohttp_tiny_mcp.storage.hub import NOTIFICATIONS, Subscription, topic
31
31
  from aiohttp_tiny_mcp.storage.namespaces import current, namespace, scoped
@@ -325,7 +325,8 @@ class Endpoint:
325
325
  return self.render_failure(self.adapters.fallback(), e.failure)
326
326
  held = self.open_values(request, session)
327
327
 
328
- log.debug("<- [%s] %s", adapter.version, raw.decode("utf-8", "replace"))
328
+ if log.isEnabledFor(logging.DEBUG):
329
+ log.debug("<- [%s] %s", adapter.version, raw.decode("utf-8", "replace"))
329
330
 
330
331
  try:
331
332
  items = adapter.decode(pre, self.registry)
@@ -390,13 +391,13 @@ class Endpoint:
390
391
  return web.Response(
391
392
  status=200,
392
393
  content_type="application/json",
393
- text=json.dumps([p for _, p in replies], ensure_ascii=False),
394
+ body=serialized([p for _, p in replies]),
394
395
  )
395
396
  status, payload = replies[0]
396
397
  response = web.Response(
397
398
  status=status,
398
399
  content_type="application/json",
399
- text=json.dumps(payload, ensure_ascii=False),
400
+ body=serialized(payload),
400
401
  )
401
402
  if minted is not None:
402
403
  response.headers[SESSION_HEADER] = minted
@@ -466,7 +467,7 @@ class Endpoint:
466
467
  )
467
468
  for event in found:
468
469
  if relays(event.message, accepted):
469
- text = json.dumps(event.message, ensure_ascii=False)
470
+ text = serialized(event.message).decode()
470
471
  log.debug("-> [%s] %s %s", adapter.version, event.id, text)
471
472
  await response.send(text, id=event.id)
472
473
 
@@ -561,15 +562,17 @@ class Endpoint:
561
562
  ) -> tuple[int, Mapping[str, Any]]:
562
563
  status = adapter.http_status(outcome) if isinstance(outcome, Failure) else 200
563
564
  payload = adapter.encode(call, self.registry, outcome)
564
- log.debug("-> [%s] %s %s", adapter.version, status, json.dumps(payload, ensure_ascii=False))
565
+ if log.isEnabledFor(logging.DEBUG):
566
+ log.debug("-> [%s] %s %s", adapter.version, status, serialized(payload).decode())
565
567
  return status, payload
566
568
 
567
569
  def render_failure(self, adapter: Adapter, failure: Failure) -> web.Response:
568
570
  payload = adapter.encode_failure(None, failure)
569
- text = json.dumps(payload, ensure_ascii=False)
570
- log.debug("-> [%s] %s %s", adapter.version, adapter.http_status(failure), text)
571
+ body = serialized(payload)
572
+ if log.isEnabledFor(logging.DEBUG):
573
+ log.debug("-> [%s] %s %s", adapter.version, adapter.http_status(failure), body.decode())
571
574
  return web.Response(
572
- status=adapter.http_status(failure), content_type="application/json", text=text
575
+ status=adapter.http_status(failure), content_type="application/json", body=body
573
576
  )
574
577
 
575
578
  async def stream(self, request: web.Request, ex: Exchange) -> web.StreamResponse:
@@ -10,7 +10,7 @@ from collections.abc import Awaitable, Callable, Iterable, Mapping
10
10
  from contextlib import AbstractAsyncContextManager
11
11
  from typing import Any, ClassVar, Literal, Protocol, get_type_hints
12
12
 
13
- from pydantic import BaseModel, ConfigDict, Field
13
+ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
14
14
 
15
15
  from aiohttp_tiny_mcp.protocol.models import (
16
16
  BlobResourceContents,
@@ -101,6 +101,16 @@ class ToolSpec(Model):
101
101
  min_revision: str | None = None
102
102
  streaming: bool = False
103
103
 
104
+ #: One projection of this tool per revision, keyed by adapter version.
105
+ #: A spec never changes after registration, so each revision simplifies
106
+ #: the schema once instead of once per request. `None` means the revision
107
+ #: hides the tool. `Adapter.describe_tool` owns this cache.
108
+ _described: dict[str, ToolDef | None] = PrivateAttr(default_factory=dict)
109
+
110
+ @property
111
+ def described(self) -> dict[str, ToolDef | None]:
112
+ return self._described
113
+
104
114
  @classmethod
105
115
  def build(
106
116
  cls,
@@ -23,6 +23,7 @@ from aiohttp_tiny_mcp.protocol.core import (
23
23
  Rejected,
24
24
  Value,
25
25
  )
26
+ from aiohttp_tiny_mcp.protocol.models import serialized
26
27
  from aiohttp_tiny_mcp.protocol.selection import AdapterSet
27
28
  from aiohttp_tiny_mcp.storage.hub import NOTIFICATIONS, Subscription, topic
28
29
  from aiohttp_tiny_mcp.storage.namespaces import scoped
@@ -184,7 +185,7 @@ class SseEndpoint:
184
185
  )
185
186
  for event in found:
186
187
  if relays(event.message, accepted):
187
- text = json.dumps(event.message, ensure_ascii=False)
188
+ text = serialized(event.message).decode()
188
189
  await self.write(response, "message", text)
189
190
 
190
191
  async def write(self, response: SSEResponse, event: str, data: str) -> None:
@@ -211,7 +212,8 @@ class SseEndpoint:
211
212
  )
212
213
 
213
214
  raw = await request.read()
214
- log.debug("<- [%s] %s", VERSION, raw.decode("utf-8", "replace"))
215
+ if log.isEnabledFor(logging.DEBUG):
216
+ log.debug("<- [%s] %s", VERSION, raw.decode("utf-8", "replace"))
215
217
  where = topic(STREAM, session_id)
216
218
 
217
219
  pre = Preamble.of(raw, request.headers)
@@ -7,7 +7,6 @@ In-flight tasks support subscriptions and cancellation. HTTP header checks belon
7
7
  from __future__ import annotations
8
8
 
9
9
  import asyncio
10
- import json
11
10
  import logging
12
11
  import sys
13
12
  from collections.abc import Callable, Mapping
@@ -23,6 +22,7 @@ from aiohttp_tiny_mcp.protocol.core import (
23
22
  Preamble,
24
23
  Rejected,
25
24
  )
25
+ from aiohttp_tiny_mcp.protocol.models import serialized
26
26
  from aiohttp_tiny_mcp.protocol.selection import AdapterSet
27
27
 
28
28
  from .dispatcher import Dispatcher
@@ -60,7 +60,8 @@ async def serve_stdio(
60
60
  level: list[str | None] = [None]
61
61
 
62
62
  def send(version: str, payload: Mapping[str, Any]) -> None:
63
- log.debug("-> [%s] %s", version, json.dumps(payload, ensure_ascii=False))
63
+ if log.isEnabledFor(logging.DEBUG):
64
+ log.debug("-> [%s] %s", version, serialized(payload).decode())
64
65
  write(payload)
65
66
 
66
67
  def keep_log_level(value: str) -> None:
@@ -98,7 +99,8 @@ async def serve_stdio(
98
99
  if not raw:
99
100
  continue
100
101
 
101
- log.debug("<- %s", raw.decode("utf-8", "replace"))
102
+ if log.isEnabledFor(logging.DEBUG):
103
+ log.debug("<- %s", raw.decode("utf-8", "replace"))
102
104
  pre = Preamble.of(raw, {})
103
105
  if is_reply(pre.body):
104
106
  await relay_reply(registry.hub, pre.body)
@@ -172,8 +174,8 @@ async def stdin_reader() -> asyncio.StreamReader:
172
174
 
173
175
 
174
176
  def write_stdout(payload: Mapping[str, Any]) -> None:
175
- sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
176
- sys.stdout.flush()
177
+ sys.stdout.buffer.write(serialized(payload) + b"\n")
178
+ sys.stdout.buffer.flush()
177
179
 
178
180
 
179
181
  async def run_stdio(
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import json
7
6
  from collections.abc import AsyncIterator, Mapping
8
7
  from dataclasses import dataclass, replace
9
8
  from typing import Any, ClassVar
@@ -14,6 +13,8 @@ from aiohttp import web
14
13
  from aiohttp.compression_utils import ZLibCompressor
15
14
  from multidict import CIMultiDict, CIMultiDictProxy
16
15
 
16
+ from aiohttp_tiny_mcp.protocol.models import serialized
17
+
17
18
  BOM = b"\xef\xbb\xbf"
18
19
 
19
20
 
@@ -188,7 +189,7 @@ class SSEResponse(web.StreamResponse):
188
189
  async def send_json(
189
190
  self, value: Any, *, event: str | None = None, id: str | None = None
190
191
  ) -> None:
191
- await self.send(json.dumps(value, ensure_ascii=False), event=event, id=id)
192
+ await self.send(serialized(value).decode(), event=event, id=id)
192
193
 
193
194
  async def comment(self, text: str = "") -> None:
194
195
  await self.push(SSEEvent(comment=text))