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,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"]