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.
- aiohttp_tiny_mcp/__init__.py +76 -0
- aiohttp_tiny_mcp/adapter.py +321 -0
- aiohttp_tiny_mcp/auth.py +129 -0
- aiohttp_tiny_mcp/client.py +111 -0
- aiohttp_tiny_mcp/client_base.py +295 -0
- aiohttp_tiny_mcp/console/__init__.py +98 -0
- aiohttp_tiny_mcp/console/console.css +525 -0
- aiohttp_tiny_mcp/console/console.js +1273 -0
- aiohttp_tiny_mcp/console/index.html +100 -0
- aiohttp_tiny_mcp/core.py +327 -0
- aiohttp_tiny_mcp/dispatcher.py +309 -0
- aiohttp_tiny_mcp/endpoint.py +531 -0
- aiohttp_tiny_mcp/exchange.py +279 -0
- aiohttp_tiny_mcp/http_sse.py +267 -0
- aiohttp_tiny_mcp/hub.py +109 -0
- aiohttp_tiny_mcp/models.py +346 -0
- aiohttp_tiny_mcp/namespaces.py +36 -0
- aiohttp_tiny_mcp/postgres.py +454 -0
- aiohttp_tiny_mcp/protocol/__init__.py +0 -0
- aiohttp_tiny_mcp/protocol/selection.py +92 -0
- aiohttp_tiny_mcp/protocol/v2024_11_05.py +30 -0
- aiohttp_tiny_mcp/protocol/v2025_03_26.py +165 -0
- aiohttp_tiny_mcp/protocol/v2025_06_18.py +11 -0
- aiohttp_tiny_mcp/protocol/v2025_11_25.py +164 -0
- aiohttp_tiny_mcp/protocol/v2026_07_28.py +363 -0
- aiohttp_tiny_mcp/py.typed +0 -0
- aiohttp_tiny_mcp/redis.py +195 -0
- aiohttp_tiny_mcp/registry.py +162 -0
- aiohttp_tiny_mcp/request_state.py +107 -0
- aiohttp_tiny_mcp/schema.py +131 -0
- aiohttp_tiny_mcp/sessions.py +347 -0
- aiohttp_tiny_mcp/specs.py +268 -0
- aiohttp_tiny_mcp/sqlite.py +236 -0
- aiohttp_tiny_mcp/sse.py +260 -0
- aiohttp_tiny_mcp/stdio.py +187 -0
- aiohttp_tiny_mcp/stdio_client.py +91 -0
- aiohttp_tiny_mcp/subscriptions.py +124 -0
- aiohttp_tiny_mcp/testing.py +200 -0
- aiohttp_tiny_mcp-0.1.0.dist-info/METADATA +12 -0
- aiohttp_tiny_mcp-0.1.0.dist-info/RECORD +41 -0
- 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,,
|