aiohttp-tiny-mcp 0.1.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.
Files changed (41) hide show
  1. aiohttp_tiny_mcp/__init__.py +76 -0
  2. aiohttp_tiny_mcp/adapter.py +321 -0
  3. aiohttp_tiny_mcp/auth.py +129 -0
  4. aiohttp_tiny_mcp/client.py +111 -0
  5. aiohttp_tiny_mcp/client_base.py +295 -0
  6. aiohttp_tiny_mcp/console/__init__.py +98 -0
  7. aiohttp_tiny_mcp/console/console.css +525 -0
  8. aiohttp_tiny_mcp/console/console.js +1273 -0
  9. aiohttp_tiny_mcp/console/index.html +100 -0
  10. aiohttp_tiny_mcp/core.py +327 -0
  11. aiohttp_tiny_mcp/dispatcher.py +309 -0
  12. aiohttp_tiny_mcp/endpoint.py +531 -0
  13. aiohttp_tiny_mcp/exchange.py +279 -0
  14. aiohttp_tiny_mcp/http_sse.py +267 -0
  15. aiohttp_tiny_mcp/hub.py +109 -0
  16. aiohttp_tiny_mcp/models.py +346 -0
  17. aiohttp_tiny_mcp/namespaces.py +36 -0
  18. aiohttp_tiny_mcp/postgres.py +454 -0
  19. aiohttp_tiny_mcp/protocol/__init__.py +0 -0
  20. aiohttp_tiny_mcp/protocol/selection.py +92 -0
  21. aiohttp_tiny_mcp/protocol/v2024_11_05.py +30 -0
  22. aiohttp_tiny_mcp/protocol/v2025_03_26.py +165 -0
  23. aiohttp_tiny_mcp/protocol/v2025_06_18.py +11 -0
  24. aiohttp_tiny_mcp/protocol/v2025_11_25.py +164 -0
  25. aiohttp_tiny_mcp/protocol/v2026_07_28.py +363 -0
  26. aiohttp_tiny_mcp/py.typed +0 -0
  27. aiohttp_tiny_mcp/redis.py +195 -0
  28. aiohttp_tiny_mcp/registry.py +162 -0
  29. aiohttp_tiny_mcp/request_state.py +107 -0
  30. aiohttp_tiny_mcp/schema.py +131 -0
  31. aiohttp_tiny_mcp/sessions.py +347 -0
  32. aiohttp_tiny_mcp/specs.py +268 -0
  33. aiohttp_tiny_mcp/sqlite.py +236 -0
  34. aiohttp_tiny_mcp/sse.py +260 -0
  35. aiohttp_tiny_mcp/stdio.py +187 -0
  36. aiohttp_tiny_mcp/stdio_client.py +91 -0
  37. aiohttp_tiny_mcp/subscriptions.py +124 -0
  38. aiohttp_tiny_mcp/testing.py +200 -0
  39. aiohttp_tiny_mcp-0.1.0.dist-info/METADATA +12 -0
  40. aiohttp_tiny_mcp-0.1.0.dist-info/RECORD +41 -0
  41. aiohttp_tiny_mcp-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,91 @@
1
+ """BaseClient transport over asyncio streams."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from collections.abc import AsyncIterator, Callable, Mapping
8
+ from contextlib import asynccontextmanager
9
+ from typing import Any, Protocol, runtime_checkable
10
+
11
+ from .adapter import Adapter
12
+ from .client_base import BaseClient, ClientError, Elicitor
13
+ from .models import Implementation
14
+
15
+
16
+ @runtime_checkable
17
+ class ByteWriter(Protocol):
18
+ """Writer interface required by StdioClient."""
19
+
20
+ def write(self, data: bytes) -> Any: ...
21
+
22
+ async def drain(self) -> Any: ...
23
+
24
+
25
+ class StdioClient(BaseClient):
26
+ def __init__(
27
+ self,
28
+ reader: asyncio.StreamReader,
29
+ writer: ByteWriter,
30
+ adapter: Adapter,
31
+ *,
32
+ client_info: Implementation | None = None,
33
+ on_ask: Elicitor | None = None,
34
+ on_notification: Callable[[Mapping[str, Any]], Any] | None = None,
35
+ log_level: str | None = None,
36
+ ) -> None:
37
+ super().__init__(
38
+ adapter,
39
+ client_info=client_info,
40
+ on_ask=on_ask,
41
+ on_notification=on_notification,
42
+ log_level=log_level,
43
+ )
44
+ self.reader = reader
45
+ self.writer = writer
46
+
47
+ def write_line(self, envelope: dict[str, Any]) -> None:
48
+ self.writer.write((json.dumps(envelope, ensure_ascii=False) + "\n").encode())
49
+
50
+ async def exchange(
51
+ self, envelope: dict[str, Any], *, method: str, name: str | None
52
+ ) -> AsyncIterator[dict[str, Any]]:
53
+ self.write_line(envelope)
54
+ await self.writer.drain()
55
+ while True:
56
+ line = await self.reader.readline()
57
+ if not line:
58
+ raise ClientError(-32000, "stdio server closed the stream before replying")
59
+ yield json.loads(line)
60
+
61
+ async def send_notification(self, envelope: dict[str, Any], *, method: str) -> None:
62
+ self.write_line(envelope)
63
+ await self.writer.drain()
64
+
65
+ async def reply(self, envelope: dict[str, Any]) -> None:
66
+ """Send the answer on the shared JSON-RPC channel."""
67
+ self.write_line(envelope)
68
+ await self.writer.drain()
69
+
70
+ @classmethod
71
+ @asynccontextmanager
72
+ async def spawn(
73
+ cls,
74
+ *cmd: str,
75
+ adapter: Adapter,
76
+ client_info: Implementation | None = None,
77
+ on_ask: Elicitor | None = None,
78
+ ) -> AsyncIterator[StdioClient]:
79
+ """Launch cmd with protocol pipes; inherit stderr so logging cannot corrupt stdout."""
80
+ process = await asyncio.create_subprocess_exec(
81
+ *cmd, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE
82
+ )
83
+ assert process.stdin is not None
84
+ assert process.stdout is not None
85
+ try:
86
+ yield cls(
87
+ process.stdout, process.stdin, adapter, client_info=client_info, on_ask=on_ask
88
+ )
89
+ finally:
90
+ process.terminate()
91
+ await process.wait()
@@ -0,0 +1,124 @@
1
+ """Notification filtering for modern request streams and legacy session subscriptions.
2
+
3
+ Both read the same hub topic; legacy streams reload subscriptions from the shared session.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Mapping
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from . import hub as hubs
12
+ from .models import EmptyResult, ListenParams, Meta
13
+ from .sessions import SUBSCRIPTIONS_KEY, Session
14
+
15
+ if TYPE_CHECKING:
16
+ from .exchange import Exchange
17
+
18
+ SUBSCRIPTION_ID = "io.modelcontextprotocol/subscriptionId"
19
+ LIST_CHANGES = {
20
+ "notifications/tools/list_changed": ("toolsListChanged", "tools"),
21
+ "notifications/prompts/list_changed": ("promptsListChanged", "prompts"),
22
+ "notifications/resources/list_changed": ("resourcesListChanged", "resources"),
23
+ }
24
+
25
+
26
+ def tag(payload: Mapping[str, Any], subscription_id: str | int) -> dict[str, Any]:
27
+ params = dict(payload.get("params") or {})
28
+ params["_meta"] = {**params.get("_meta", {}), SUBSCRIPTION_ID: subscription_id}
29
+ return {**payload, "params": params}
30
+
31
+
32
+ def relays(payload: Mapping[str, Any], accepted: Mapping[str, Any]) -> bool:
33
+ """Whether this event is one the client asked for."""
34
+ method = payload.get("method")
35
+ if "id" in payload or payload.get("jsonrpc") != "2.0":
36
+ return False
37
+ if method == "notifications/resources/updated":
38
+ params = payload.get("params")
39
+ if not isinstance(params, Mapping):
40
+ return False
41
+ return params.get("uri") in accepted.get("resourceSubscriptions", [])
42
+ return method in LIST_CHANGES and bool(accepted.get(LIST_CHANGES[method][0]))
43
+
44
+
45
+ async def listen(ex: Exchange) -> EmptyResult:
46
+ assert isinstance(ex.call.params, ListenParams)
47
+ wanted = ex.call.params.notifications
48
+ caps = ex.adapter.capabilities(ex.registry)
49
+ accepted: dict[str, Any] = {}
50
+ requested = wanted.wire()
51
+ for field, capability in LIST_CHANGES.values():
52
+ if requested.get(field) and caps.get(capability, {}).get("listChanged"):
53
+ accepted[field] = True
54
+ if caps.get("resources", {}).get("subscribe"):
55
+ uris = [uri for uri in wanted.resource_subscriptions if ex.registry.match_resource(uri)]
56
+ if uris:
57
+ accepted["resourceSubscriptions"] = list(dict.fromkeys(uris))
58
+
59
+ ack = tag(
60
+ {
61
+ "jsonrpc": "2.0",
62
+ "method": "notifications/subscriptions/acknowledged",
63
+ "params": {"notifications": accepted},
64
+ },
65
+ ex.id,
66
+ )
67
+ hub = ex.registry.hub
68
+ if not accepted:
69
+ await ex.emit(ack)
70
+ return EmptyResult(meta=Meta.model_validate({SUBSCRIPTION_ID: ex.id}))
71
+
72
+ topic = hubs.topic(hubs.NOTIFICATIONS)
73
+ cursor = await hub.position(topic)
74
+ await ex.emit(ack)
75
+ while not ex.cancelled.is_set():
76
+ messages, cursor = await hub.poll(topic, cursor, timeout=ex.registry.hub_poll_seconds)
77
+ for payload in messages:
78
+ if relays(payload, accepted):
79
+ await ex.emit(tag(payload, ex.id))
80
+ return EmptyResult(meta=Meta.model_validate({SUBSCRIPTION_ID: ex.id}))
81
+
82
+
83
+ URIS = "uris"
84
+
85
+
86
+ def subscribed(session: Session | None) -> list[str]:
87
+ """Resources this client asked to hear about."""
88
+ if session is None:
89
+ return []
90
+ uris = session.slot(SUBSCRIPTIONS_KEY).get(URIS)
91
+ if not isinstance(uris, Mapping):
92
+ return []
93
+ return [uri for uri in uris if isinstance(uri, str)]
94
+
95
+
96
+ async def subscribe(session: Session, uri: str) -> None:
97
+ await session.update_slot(
98
+ SUBSCRIPTIONS_KEY, lambda held: {**held, URIS: {**dict(held.get(URIS) or {}), uri: True}}
99
+ )
100
+
101
+
102
+ async def unsubscribe(session: Session, uri: str) -> None:
103
+ await session.update_slot(
104
+ SUBSCRIPTIONS_KEY,
105
+ lambda held: {
106
+ **held,
107
+ URIS: {key: True for key in dict(held.get(URIS) or {}) if key != uri},
108
+ },
109
+ )
110
+
111
+
112
+ def wanted(capabilities: Mapping[str, Any], session: Session | None) -> dict[str, Any]:
113
+ """Legacy list changes follow advertised capabilities; resource updates follow session
114
+ subscriptions.
115
+ """
116
+ accepted: dict[str, Any] = {}
117
+ for field, capability in LIST_CHANGES.values():
118
+ if capabilities.get(capability, {}).get("listChanged"):
119
+ accepted[field] = True
120
+ if capabilities.get("resources", {}).get("subscribe"):
121
+ uris = subscribed(session)
122
+ if uris:
123
+ accepted["resourceSubscriptions"] = uris
124
+ return accepted
@@ -0,0 +1,200 @@
1
+ """Test helpers for in-memory and HTTP MCP clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
8
+ from contextlib import asynccontextmanager
9
+ from typing import Any
10
+
11
+ from aiohttp import web
12
+
13
+ from .adapter import Adapter
14
+ from .client import Client
15
+ from .client_base import BaseClient, Elicitor
16
+ from .core import elicit_accept, elicit_decline
17
+ from .endpoint import Endpoint
18
+ from .models import Implementation
19
+ from .protocol.selection import AdapterSet
20
+ from .registry import Registry
21
+ from .stdio import serve_stdio
22
+ from .stdio_client import StdioClient
23
+
24
+
25
+ class Pipe:
26
+ """One direction of an in-memory connection."""
27
+
28
+ def __init__(self) -> None:
29
+ self.reader = asyncio.StreamReader()
30
+
31
+ def write(self, data: bytes) -> None:
32
+ self.reader.feed_data(data)
33
+
34
+ async def drain(self) -> None:
35
+ return None
36
+
37
+ def close(self) -> None:
38
+ self.reader.feed_eof()
39
+
40
+
41
+ def answering(answers: Mapping[str, Any] | None) -> Elicitor | None:
42
+ """Build an elicitation callback from a mapping."""
43
+ if answers is None:
44
+ return None
45
+
46
+ async def answer(request: Mapping[str, Any]) -> Mapping[str, Any]:
47
+ key = str((request.get("params") or {}).get("message", ""))
48
+ for wanted, value in answers.items():
49
+ if wanted in key:
50
+ return elicit_decline() if value is None else elicit_accept(value)
51
+ return elicit_decline()
52
+
53
+ return answer
54
+
55
+
56
+ @asynccontextmanager
57
+ async def connect(
58
+ registry: Registry,
59
+ *,
60
+ adapter: Adapter | str | None = None,
61
+ answers: Mapping[str, Any] | None = None,
62
+ on_ask: Elicitor | None = None,
63
+ client_info: Implementation | None = None,
64
+ initialize: bool = True,
65
+ ) -> AsyncIterator[BaseClient]:
66
+ """A client talking to `registry` through memory.
67
+
68
+ No port, no subprocess, and nothing to tear down. The protocol is the
69
+ real one: pick `adapter` to see what a client of that revision sees.
70
+
71
+ Answer a handler's questions with `answers`, keyed by any part of the
72
+ question's text::
73
+
74
+ async with connect(registry, answers={"Deploy": {"ok": True}}) as client:
75
+ result = await client.call_tool("deploy", {"service": "web"})
76
+ """
77
+ chosen = pick(adapter)
78
+ to_server, to_client = Pipe(), Pipe()
79
+
80
+ def send(payload: Mapping[str, Any]) -> None:
81
+ to_client.write((json.dumps(payload, ensure_ascii=False) + "\n").encode())
82
+
83
+ served = asyncio.ensure_future(serve_stdio(registry, to_server.reader, send))
84
+ client = StdioClient(
85
+ to_client.reader,
86
+ to_server,
87
+ chosen,
88
+ client_info=client_info,
89
+ on_ask=on_ask or answering(answers),
90
+ )
91
+ try:
92
+ if initialize:
93
+ await client.initialize()
94
+ yield client
95
+ finally:
96
+ to_server.close()
97
+ served.cancel()
98
+ await asyncio.gather(served, return_exceptions=True)
99
+
100
+
101
+ @asynccontextmanager
102
+ async def serving(
103
+ registry: Registry,
104
+ *,
105
+ path: str = "/mcp",
106
+ adapters: AdapterSet | None = None,
107
+ middlewares: list[Any] | None = None,
108
+ ) -> AsyncIterator[str]:
109
+ """`registry` on a loopback port, for the examples that are about HTTP.
110
+
111
+ Yields the URL. The port is whichever one was free, so nothing collides
112
+ with anything else running.
113
+ """
114
+ app = web.Application(middlewares=middlewares or [])
115
+ Endpoint(registry, adapters=adapters).setup(app, path)
116
+ runner = web.AppRunner(app, access_log=None)
117
+ await runner.setup()
118
+ site = web.TCPSite(runner, "127.0.0.1", 0)
119
+ await site.start()
120
+ host, port = runner.addresses[0]
121
+ try:
122
+ yield f"http://{host}:{port}{path}"
123
+ finally:
124
+ await runner.cleanup()
125
+
126
+
127
+ @asynccontextmanager
128
+ async def over_http(
129
+ registry: Registry,
130
+ *,
131
+ adapter: Adapter | str | None = None,
132
+ answers: Mapping[str, Any] | None = None,
133
+ on_ask: Elicitor | None = None,
134
+ headers: Mapping[str, str] | None = None,
135
+ initialize: bool = True,
136
+ ) -> AsyncIterator[Client]:
137
+ """A real HTTP client against a real server, both started here.
138
+
139
+ Use it where the transport is the subject: a session that has to survive
140
+ a second request, a stream, a header the server reads. Where it is not,
141
+ `connect` says the same thing without a socket.
142
+ """
143
+ import aiohttp
144
+
145
+ async with serving(registry) as url:
146
+ async with aiohttp.ClientSession(headers=dict(headers or {})) as session:
147
+ async with Client(
148
+ url,
149
+ pick(adapter),
150
+ session=session,
151
+ on_ask=on_ask or answering(answers),
152
+ ) as client:
153
+ if initialize:
154
+ await client.initialize()
155
+ yield client
156
+
157
+
158
+ def pick(adapter: Adapter | str | None) -> Adapter:
159
+ """The adapter to speak, named or taken whole.
160
+
161
+ Defaults to the newest revision, which is what a client that knows
162
+ nothing about this server would offer.
163
+ """
164
+ known = AdapterSet.default()
165
+ if adapter is None:
166
+ return known.adapters[0]
167
+ if isinstance(adapter, str):
168
+ return known.by_version[adapter]
169
+ return adapter
170
+
171
+
172
+ async def every_revision(
173
+ registry: Registry,
174
+ call: Callable[[BaseClient], Awaitable[Any]],
175
+ *,
176
+ answers: Mapping[str, Any] | None = None,
177
+ on_ask: Elicitor | None = None,
178
+ ) -> dict[str, Any]:
179
+ """Run `call` once per revision, and report what each one saw.
180
+
181
+ The claim this package makes is that one declaration serves five
182
+ revisions. This is how an application checks that claim about its own
183
+ handlers, in one line instead of a loop with a fixture in it.
184
+ """
185
+ seen: dict[str, Any] = {}
186
+ for adapter in AdapterSet.default().adapters:
187
+ async with connect(registry, adapter=adapter, answers=answers, on_ask=on_ask) as client:
188
+ seen[adapter.version] = await call(client)
189
+ return seen
190
+
191
+
192
+ __all__ = [
193
+ "Pipe",
194
+ "answering",
195
+ "connect",
196
+ "every_revision",
197
+ "over_http",
198
+ "pick",
199
+ "serving",
200
+ ]
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.3
2
+ Name: aiohttp-tiny-mcp
3
+ Version: 0.1.0
4
+ Requires-Dist: aiohttp>=3.14
5
+ Requires-Dist: pydantic>=2.13
6
+ Requires-Dist: psycopg[binary,pool]>=3 ; extra == 'postgres'
7
+ Requires-Dist: redis>=5 ; extra == 'redis'
8
+ Requires-Dist: aiosqlite>=0.20 ; extra == 'sqlite'
9
+ Requires-Python: >=3.10
10
+ Provides-Extra: postgres
11
+ Provides-Extra: redis
12
+ Provides-Extra: sqlite
@@ -0,0 +1,41 @@
1
+ aiohttp_tiny_mcp/__init__.py,sha256=s02rK6AmMEmUfG4M-evjV7UwjTEYBTJLUJfhVMP1djo,1614
2
+ aiohttp_tiny_mcp/adapter.py,sha256=esTqt_Ncq8yRoWF3Yyr5o_rkhO0yfAbLbmdQv1aQuaY,12575
3
+ aiohttp_tiny_mcp/auth.py,sha256=1tIH2GYmy1j1QRM1jNOL9Vavi_4ibonu5enwU-Rc-T4,4420
4
+ aiohttp_tiny_mcp/client.py,sha256=jVD3cKBDziglmhbea32AQEANlpkUiPyVzhgs9aB3CYI,4347
5
+ aiohttp_tiny_mcp/client_base.py,sha256=NwKlOS1Y9JC9xjpR7uXcYlN7JuqcQ9wX1Hj4PeK8d4Y,11793
6
+ aiohttp_tiny_mcp/console/__init__.py,sha256=b0qVFsqneXj9B_-D13t_DYc1lzu6HyC0TM92_0TzJPg,3207
7
+ aiohttp_tiny_mcp/console/console.css,sha256=H8zAjc4UShyJ3-S7i4pNBocSROzmFSyOb9pymABV_Yo,13257
8
+ aiohttp_tiny_mcp/console/console.js,sha256=Z8iFEwzXmwQPUq5pKIoAPLwvvUreo8rhzcQkeAozVfE,41871
9
+ aiohttp_tiny_mcp/console/index.html,sha256=fyj19zbaB9NxJUEIWRwG_n7cDYt-LkutS_yLPHH2o6w,3446
10
+ aiohttp_tiny_mcp/core.py,sha256=r29q1VqiH0hmTP7vKx2a4_i3IXiSNBKUJM1_SzKdFY8,9640
11
+ aiohttp_tiny_mcp/dispatcher.py,sha256=6oZU0y6GEGdyxnr9TFv80AKcNahDWWQjzvPJuVwd5IE,12608
12
+ aiohttp_tiny_mcp/endpoint.py,sha256=bLNgtMzmvCuJGydDceHNRs5lGgeu_qfVT2MhRQk99zI,21223
13
+ aiohttp_tiny_mcp/exchange.py,sha256=VGvzus31WyOjjqE4C8VZZ_hRcC-MFxiZAUnrMvga_II,9901
14
+ aiohttp_tiny_mcp/http_sse.py,sha256=fXdQySm16gAlyBWvundYb8D6j5_5ABwJ4pEPA3hBeok,9427
15
+ aiohttp_tiny_mcp/hub.py,sha256=WqA4lMhsDC2KXjJG6uI9f8t3gh3nHNzDfviNLgAzRTo,3638
16
+ aiohttp_tiny_mcp/models.py,sha256=J4RlhL5OmfTLUImz8yL1MhcODSMLNmkNCmZ_QWvnlaU,9229
17
+ aiohttp_tiny_mcp/namespaces.py,sha256=km-hQfhTNf0cNQARJ6BFcYWjZMOg9dOOS_226Zuxv2s,997
18
+ aiohttp_tiny_mcp/postgres.py,sha256=_w5T_IuRWcRWgpOonOmmgtVLeEDI2umW3QdNajXvfck,15843
19
+ aiohttp_tiny_mcp/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ aiohttp_tiny_mcp/protocol/selection.py,sha256=JfdjGQzG9cNow-415Wu03sza3NAP8cWOhGS7FuSZIpQ,3358
21
+ aiohttp_tiny_mcp/protocol/v2024_11_05.py,sha256=UH1tdkUc1lKCaxnmaFlgJHtGNp_hHFeYG0TnnJZsSrQ,1176
22
+ aiohttp_tiny_mcp/protocol/v2025_03_26.py,sha256=sUYJ6HQAuBz53-m_EtfNUikGU5SO0myjPruXKj9Zpzo,6352
23
+ aiohttp_tiny_mcp/protocol/v2025_06_18.py,sha256=zbq2p4CT5-aD0SqE9LHjoXpTPa2_RRCobt58aIBsyzg,331
24
+ aiohttp_tiny_mcp/protocol/v2025_11_25.py,sha256=TDR_RqBTOb2vLURt87jaGwZMLDh5D6qWYPM7N53ZPIE,6017
25
+ aiohttp_tiny_mcp/protocol/v2026_07_28.py,sha256=TGBUia7Riu-A_Qrhx80Sa0lfvJjLFighX9mlP8ZI8Ro,14181
26
+ aiohttp_tiny_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ aiohttp_tiny_mcp/redis.py,sha256=Jy8T0bimOcGeOrQDRHgVsiGKgPbBh64284DVLhrCAEA,6069
28
+ aiohttp_tiny_mcp/registry.py,sha256=aCTioi4w9IR9huXFQ9htyBz_BUDNWKOSdGkwA8QcojM,7067
29
+ aiohttp_tiny_mcp/request_state.py,sha256=yhW9wyNxVZcFOGjPqC-wtJPmyYOtsJbxTsfQPejlVeQ,3404
30
+ aiohttp_tiny_mcp/schema.py,sha256=Y5W-Xvszdpi9QMdS7KL3Hy4Q9iO5F8C1Zn4_GisK9ss,4940
31
+ aiohttp_tiny_mcp/sessions.py,sha256=knm-3r9qen2rU-M4KXq6wJZNoclU6U2w-IP1BjNSy6U,11762
32
+ aiohttp_tiny_mcp/specs.py,sha256=n_1cyuJxfwZmp0vwj-5xjHcjSoBqdphup0ix54w2DLI,8944
33
+ aiohttp_tiny_mcp/sqlite.py,sha256=_2Tg6gbP3Me8NFkhz9fPXIURKIrr8KMzpQCciy8-Buo,8565
34
+ aiohttp_tiny_mcp/sse.py,sha256=QO7FPPNKRmWK3yCqLhw1lg50yKMG8ukclnfJ-DuU4E0,8843
35
+ aiohttp_tiny_mcp/stdio.py,sha256=0irKliwRWyRj3Y3xC5tpLAX4novmTIj7ZAP8uh1iwfI,6593
36
+ aiohttp_tiny_mcp/stdio_client.py,sha256=rRuLovXCJpEwyaVN_74aRknx_cQmVb_yGmnC1KFEj0c,2947
37
+ aiohttp_tiny_mcp/subscriptions.py,sha256=5YkJU2e3FX0kwmYzkh-wAg-psT0xhzKiA-4g9lK9Ig0,4440
38
+ aiohttp_tiny_mcp/testing.py,sha256=AIL3tdxnKwUi6PG9jbY_Dh4bM17eb-t0QORzQPPXots,6044
39
+ aiohttp_tiny_mcp-0.1.0.dist-info/WHEEL,sha256=oy45bIW3ehK02fnLDUKD-FzeaR3VpJxY5Jdblkktnpw,80
40
+ aiohttp_tiny_mcp-0.1.0.dist-info/METADATA,sha256=xwTlYhy4lTrXDKusVrEys46XnNGf6Z0Yv7un38upVzc,368
41
+ aiohttp_tiny_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any