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,346 @@
|
|
|
1
|
+
"""Pydantic wire models for content, results, JSON-RPC envelopes, and request params."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Annotated, Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr
|
|
12
|
+
from pydantic.alias_generators import to_camel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Model(BaseModel):
|
|
16
|
+
"""camelCase on the wire; unknown fields allowed for forward compatibility."""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, extra="allow")
|
|
19
|
+
|
|
20
|
+
def wire(self) -> dict:
|
|
21
|
+
return self.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Implementation(Model):
|
|
25
|
+
name: str
|
|
26
|
+
version: str
|
|
27
|
+
title: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TextContent(Model):
|
|
31
|
+
type: Literal["text"] = "text"
|
|
32
|
+
text: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ImageContent(Model):
|
|
36
|
+
type: Literal["image"] = "image"
|
|
37
|
+
data: str
|
|
38
|
+
mime_type: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AudioContent(Model):
|
|
42
|
+
type: Literal["audio"] = "audio"
|
|
43
|
+
data: str
|
|
44
|
+
mime_type: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ResourceLink(Model):
|
|
48
|
+
type: Literal["resource_link"] = "resource_link"
|
|
49
|
+
uri: str
|
|
50
|
+
name: str
|
|
51
|
+
description: str | None = None
|
|
52
|
+
mime_type: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
ContentBlock = Annotated[
|
|
56
|
+
TextContent | ImageContent | AudioContent | ResourceLink,
|
|
57
|
+
Field(discriminator="type"),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ResultModel(Model):
|
|
62
|
+
"""resultType is required on 2026-07-28; legacy adapters leave it unset."""
|
|
63
|
+
|
|
64
|
+
result_type: Literal["complete"] | None = Field(None, alias="resultType")
|
|
65
|
+
meta: Meta | None = Field(None, alias="_meta")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CacheableResult(ResultModel):
|
|
69
|
+
"""2026-07-28 freshness hints (SEP-2549), omitted on older revisions."""
|
|
70
|
+
|
|
71
|
+
ttl_ms: int | None = None
|
|
72
|
+
cache_scope: Literal["private", "public"] | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Hint(Enum):
|
|
76
|
+
"""Tool annotations, combined with | and negated with ~.
|
|
77
|
+
|
|
78
|
+
annotations=Hint.READ_ONLY | Hint.IDEMPOTENT | ~Hint.OPEN_WORLD
|
|
79
|
+
|
|
80
|
+
DESTRUCTIVE and OPEN_WORLD default to true; READ_ONLY and IDEMPOTENT default to false. Hints
|
|
81
|
+
inform hosts but are not enforced. Set the display title through registry.tool(title=...).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
READ_ONLY = MappingProxyType({"readOnlyHint": True})
|
|
85
|
+
DESTRUCTIVE = MappingProxyType({"destructiveHint": True})
|
|
86
|
+
IDEMPOTENT = MappingProxyType({"idempotentHint": True})
|
|
87
|
+
OPEN_WORLD = MappingProxyType({"openWorldHint": True})
|
|
88
|
+
|
|
89
|
+
def keys(self):
|
|
90
|
+
return self.value.keys()
|
|
91
|
+
|
|
92
|
+
def __getitem__(self, key: str) -> bool:
|
|
93
|
+
return self.value[key]
|
|
94
|
+
|
|
95
|
+
def __or__(self, other: Any) -> Mapping[str, Any]:
|
|
96
|
+
"""Combine annotations; the right side wins on conflicts."""
|
|
97
|
+
return MappingProxyType({**self, **other})
|
|
98
|
+
|
|
99
|
+
def __ror__(self, other: Any) -> Mapping[str, Any]:
|
|
100
|
+
return MappingProxyType({**other, **self})
|
|
101
|
+
|
|
102
|
+
def __invert__(self) -> Mapping[str, Any]:
|
|
103
|
+
"""Negate this hint, including the true defaults DESTRUCTIVE and OPEN_WORLD."""
|
|
104
|
+
return MappingProxyType({key: not value for key, value in self.value.items()})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ToolDef(Model):
|
|
108
|
+
name: str
|
|
109
|
+
title: str | None = None
|
|
110
|
+
description: str
|
|
111
|
+
input_schema: dict[str, Any]
|
|
112
|
+
output_schema: dict[str, Any] | None = None
|
|
113
|
+
annotations: dict[str, Any] | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ListToolsResult(CacheableResult):
|
|
117
|
+
tools: list[ToolDef]
|
|
118
|
+
next_cursor: str | None = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class CallToolResult(ResultModel):
|
|
122
|
+
content: list[ContentBlock]
|
|
123
|
+
structured_content: Any = None
|
|
124
|
+
is_error: bool = False
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def failure(cls, text: str) -> CallToolResult:
|
|
128
|
+
return cls(content=[TextContent(text=text)], is_error=True)
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def of(cls, value: Any) -> CallToolResult:
|
|
132
|
+
"""Coerce whatever a tool returned into a result."""
|
|
133
|
+
match value:
|
|
134
|
+
case cls():
|
|
135
|
+
return value
|
|
136
|
+
case str():
|
|
137
|
+
return cls(content=[TextContent(text=value)])
|
|
138
|
+
case BaseModel():
|
|
139
|
+
data = value.model_dump(mode="json", by_alias=True)
|
|
140
|
+
return cls(
|
|
141
|
+
content=[TextContent(text=json.dumps(data, ensure_ascii=False))],
|
|
142
|
+
structured_content=data,
|
|
143
|
+
)
|
|
144
|
+
case _:
|
|
145
|
+
text = json.dumps(value, ensure_ascii=False, default=str)
|
|
146
|
+
return cls(content=[TextContent(text=text)], structured_content=json.loads(text))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ResourceDef(Model):
|
|
150
|
+
uri: str
|
|
151
|
+
name: str
|
|
152
|
+
title: str | None = None
|
|
153
|
+
description: str | None = None
|
|
154
|
+
mime_type: str | None = None
|
|
155
|
+
size: int | None = None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ResourceTemplateDef(Model):
|
|
159
|
+
uri_template: str
|
|
160
|
+
name: str
|
|
161
|
+
title: str | None = None
|
|
162
|
+
description: str | None = None
|
|
163
|
+
mime_type: str | None = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TextResourceContents(Model):
|
|
167
|
+
uri: str
|
|
168
|
+
text: str
|
|
169
|
+
mime_type: str | None = None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class BlobResourceContents(Model):
|
|
173
|
+
uri: str
|
|
174
|
+
blob: str
|
|
175
|
+
mime_type: str | None = None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ListResourcesResult(CacheableResult):
|
|
179
|
+
resources: list[ResourceDef]
|
|
180
|
+
next_cursor: str | None = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ListResourceTemplatesResult(CacheableResult):
|
|
184
|
+
resource_templates: list[ResourceTemplateDef]
|
|
185
|
+
next_cursor: str | None = None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ReadResourceResult(CacheableResult):
|
|
189
|
+
contents: list[TextResourceContents | BlobResourceContents]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class PromptArgumentDef(Model):
|
|
193
|
+
name: str
|
|
194
|
+
description: str | None = None
|
|
195
|
+
required: bool = False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class PromptDef(Model):
|
|
199
|
+
name: str
|
|
200
|
+
title: str | None = None
|
|
201
|
+
description: str | None = None
|
|
202
|
+
arguments: list[PromptArgumentDef] = Field(default_factory=list)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class ListPromptsResult(CacheableResult):
|
|
206
|
+
prompts: list[PromptDef]
|
|
207
|
+
next_cursor: str | None = None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class PromptMessage(Model):
|
|
211
|
+
role: Literal["user", "assistant"]
|
|
212
|
+
content: ContentBlock
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class GetPromptResult(ResultModel):
|
|
216
|
+
messages: list[PromptMessage]
|
|
217
|
+
description: str | None = None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class Completion(Model):
|
|
221
|
+
values: list[str]
|
|
222
|
+
total: int | None = None
|
|
223
|
+
has_more: bool = False
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class CompleteResult(ResultModel):
|
|
227
|
+
completion: Completion
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class EmptyResult(ResultModel):
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class Incoming(Model):
|
|
235
|
+
jsonrpc: Literal["2.0"]
|
|
236
|
+
method: str
|
|
237
|
+
id: StrictStr | StrictInt | None = None
|
|
238
|
+
params: dict[str, Any] = Field(default_factory=dict)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def is_notification(self) -> bool:
|
|
242
|
+
return "id" not in self.model_fields_set
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class ErrorBody(Model):
|
|
246
|
+
code: int
|
|
247
|
+
message: str
|
|
248
|
+
data: Any | None = None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ErrorResponse(Model):
|
|
252
|
+
jsonrpc: Literal["2.0"] = "2.0"
|
|
253
|
+
id: str | int | None = None
|
|
254
|
+
error: ErrorBody
|
|
255
|
+
|
|
256
|
+
def wire(self) -> dict:
|
|
257
|
+
payload = super().wire()
|
|
258
|
+
# Parse/Invalid Request failures have an unknown request id. JSON-RPC
|
|
259
|
+
# requires an explicit null here; omitting the member is not valid.
|
|
260
|
+
payload["id"] = self.id
|
|
261
|
+
return payload
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class Meta(Model):
|
|
265
|
+
"""Wire _meta, aliased because Pydantic treats leading underscores as private."""
|
|
266
|
+
|
|
267
|
+
protocol_version: str | None = Field(None, alias="io.modelcontextprotocol/protocolVersion")
|
|
268
|
+
client_info: Implementation | None = Field(None, alias="io.modelcontextprotocol/clientInfo")
|
|
269
|
+
client_capabilities: dict[str, Any] | None = Field(
|
|
270
|
+
None, alias="io.modelcontextprotocol/clientCapabilities"
|
|
271
|
+
)
|
|
272
|
+
#: 2026-07-28 server identity, included in every result.
|
|
273
|
+
server_info: Implementation | None = Field(None, alias="io.modelcontextprotocol/serverInfo")
|
|
274
|
+
progress_token: str | int | None = Field(None, alias="progressToken")
|
|
275
|
+
log_level: str | None = Field(None, alias="io.modelcontextprotocol/logLevel") # draft, SEP-2575
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class Params(Model):
|
|
279
|
+
meta: Meta = Field(default_factory=Meta, alias="_meta")
|
|
280
|
+
# MRTR (SEP-2322).
|
|
281
|
+
input_responses: dict[str, Any] | None = None
|
|
282
|
+
request_state: str | None = None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class InitializeParams(Params):
|
|
286
|
+
protocol_version: str
|
|
287
|
+
capabilities: dict[str, Any] = Field(default_factory=dict)
|
|
288
|
+
client_info: Implementation | None = None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class ListParams(Params):
|
|
292
|
+
cursor: str | None = None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class ListenNotifications(Model):
|
|
296
|
+
tools_list_changed: StrictBool = False
|
|
297
|
+
prompts_list_changed: StrictBool = False
|
|
298
|
+
resources_list_changed: StrictBool = False
|
|
299
|
+
resource_subscriptions: list[StrictStr] = Field(default_factory=list)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ListenParams(Params):
|
|
303
|
+
notifications: ListenNotifications = Field(default_factory=ListenNotifications)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class CallToolParams(Params):
|
|
307
|
+
name: str
|
|
308
|
+
arguments: dict[str, Any] = Field(default_factory=dict)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class SetLevelParams(Params):
|
|
312
|
+
"""Session-level logging/setLevel; 2026-07-28 uses per-request metadata."""
|
|
313
|
+
|
|
314
|
+
level: str
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class SubscribeParams(Params):
|
|
318
|
+
"""Legacy resource subscription changes, accumulated in the session."""
|
|
319
|
+
|
|
320
|
+
uri: str
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class ReadResourceParams(Params):
|
|
324
|
+
uri: str
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class GetPromptParams(Params):
|
|
328
|
+
name: str
|
|
329
|
+
arguments: dict[str, Any] = Field(default_factory=dict)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class CompleteRef(Model):
|
|
333
|
+
type: Literal["ref/prompt", "ref/resource"]
|
|
334
|
+
name: str | None = None
|
|
335
|
+
uri: str | None = None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class CompleteArgument(Model):
|
|
339
|
+
name: str
|
|
340
|
+
value: str
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class CompleteParams(Params):
|
|
344
|
+
ref: CompleteRef
|
|
345
|
+
argument: CompleteArgument
|
|
346
|
+
context: dict[str, Any] | None = None
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Request namespaces isolate sessions, subscriptions, and pending questions.
|
|
2
|
+
|
|
3
|
+
Set namespace in authenticated middleware; spawned tasks inherit it:
|
|
4
|
+
|
|
5
|
+
@web.middleware
|
|
6
|
+
async def tenant(request, handler):
|
|
7
|
+
namespace.set(request["user"].org_id)
|
|
8
|
+
return await handler(request)
|
|
9
|
+
|
|
10
|
+
An unset namespace leaves keys bare for single-tenant deployments.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from contextvars import ContextVar
|
|
16
|
+
from urllib.parse import quote
|
|
17
|
+
|
|
18
|
+
namespace: ContextVar[str | None] = ContextVar("aiohttp_tiny_mcp_namespace", default=None)
|
|
19
|
+
|
|
20
|
+
SEPARATOR = ":"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def current() -> str | None:
|
|
24
|
+
"""The namespace in force, or `None` where none was set."""
|
|
25
|
+
return namespace.get()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def scoped(key: str) -> str:
|
|
29
|
+
"""Prefix a key with the percent-encoded namespace to prevent separator collisions."""
|
|
30
|
+
name = namespace.get()
|
|
31
|
+
if not name:
|
|
32
|
+
return key
|
|
33
|
+
return f"{quote(name, safe='')}{SEPARATOR}{key}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["SEPARATOR", "current", "namespace", "scoped"]
|