aiohttp-tiny-mcp 0.3.0__tar.gz → 0.3.2__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 (45) hide show
  1. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/LICENSE +47 -17
  2. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/PKG-INFO +7 -2
  3. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/README.md +4 -1
  4. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/pyproject.toml +13 -2
  5. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/pyproject.toml.orig +13 -2
  6. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/__init__.py +2 -0
  7. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/adapter.py +40 -9
  8. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/client_base.py +16 -0
  9. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/console/console.css +12 -5
  10. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/console/console.js +172 -25
  11. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/core.py +1 -0
  12. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/dispatcher.py +40 -5
  13. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/endpoint.py +1 -1
  14. aiohttp_tiny_mcp-0.3.2/src/aiohttp_tiny_mcp/extensions.py +143 -0
  15. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/models.py +5 -0
  16. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/protocol/v2026_07_28.py +12 -0
  17. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/registry.py +53 -3
  18. aiohttp_tiny_mcp-0.3.2/src/aiohttp_tiny_mcp/skills.py +223 -0
  19. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/specs.py +4 -0
  20. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/stdio.py +1 -1
  21. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/auth.py +0 -0
  22. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/client.py +0 -0
  23. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/console/__init__.py +0 -0
  24. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/console/index.html +0 -0
  25. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/exchange.py +0 -0
  26. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/http_sse.py +0 -0
  27. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/hub.py +0 -0
  28. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/namespaces.py +0 -0
  29. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/postgres.py +0 -0
  30. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/protocol/__init__.py +0 -0
  31. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/protocol/selection.py +0 -0
  32. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/protocol/v2024_11_05.py +0 -0
  33. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/protocol/v2025_03_26.py +0 -0
  34. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/protocol/v2025_06_18.py +0 -0
  35. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/protocol/v2025_11_25.py +0 -0
  36. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/py.typed +0 -0
  37. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/redis.py +0 -0
  38. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/request_state.py +0 -0
  39. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/schema.py +0 -0
  40. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/sessions.py +0 -0
  41. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/sqlite.py +0 -0
  42. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/sse.py +0 -0
  43. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/stdio_client.py +0 -0
  44. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/subscriptions.py +0 -0
  45. {aiohttp_tiny_mcp-0.3.0 → aiohttp_tiny_mcp-0.3.2}/src/aiohttp_tiny_mcp/testing.py +0 -0
@@ -1,3 +1,4 @@
1
+
1
2
  Apache License
2
3
  Version 2.0, January 2004
3
4
  http://www.apache.org/licenses/
@@ -86,21 +87,22 @@
86
87
  granted to You under this License for that Work shall terminate
87
88
  as of the date such litigation is filed.
88
89
 
89
- 4. Redistribution. You may reproduce and distribute copies of the Work
90
- or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You meet
92
- the following conditions:
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
93
94
 
94
- (a) You must give any other recipients of the Work or Derivative Works
95
- a copy of this License; and
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
96
97
 
97
98
  (b) You must cause any modified files to carry prominent notices
98
99
  stating that You changed the files; and
99
100
 
100
- (c) You must retain, in the Source form of any Derivative Works that
101
- You distribute, all copyright, patent, trademark, and attribution
102
- notices from the Source form of the Work, excluding those notices
103
- that do not pertain to any part of the Derivative Works; and
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
104
106
 
105
107
  (d) If the Work includes a "NOTICE" text file as part of its
106
108
  distribution, then any Derivative Works that You distribute must
@@ -130,11 +132,14 @@
130
132
  any Contribution intentionally submitted for inclusion in the Work
131
133
  by You to the Licensor shall be under the terms and conditions of
132
134
  this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
133
138
 
134
139
  6. Trademarks. This License does not grant permission to use the trade
135
140
  names, trademarks, service marks, or product names of the Licensor,
136
- except as required for reasonable and customary use in describing
137
- the origin of the Work and reproducing the content of the NOTICE file.
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
138
143
 
139
144
  7. Disclaimer of Warranty. Unless required by applicable law or
140
145
  agreed to in writing, Licensor provides the Work (and each
@@ -152,11 +157,11 @@
152
157
  negligent acts) or agreed to in writing, shall any Contributor be
153
158
  liable to You for damages, including any direct, indirect, special,
154
159
  incidental, or consequential damages of any character arising as a
155
- result of this License or out of the use or inability to use the Work
156
- (including but not limited to damages for loss of goodwill, work
157
- stoppage, computer failure or malfunction, or any and all other
158
- commercial damages or losses), even if such Contributor has been
159
- advised of the possibility of such damages.
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
160
165
 
161
166
  9. Accepting Warranty or Additional Liability. While redistributing
162
167
  the Work or Derivative Works thereof, You may choose to offer,
@@ -170,3 +175,28 @@
170
175
  of your accepting any such warranty or additional liability.
171
176
 
172
177
  END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohttp-tiny-mcp
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Remote MCP server and client library for aiohttp
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -28,6 +28,7 @@ Requires-Dist: aiohttp>=3.14
28
28
  Requires-Dist: pydantic>=2.13
29
29
  Requires-Dist: psycopg[binary,pool]>=3 ; extra == 'postgres'
30
30
  Requires-Dist: redis>=5 ; extra == 'redis'
31
+ Requires-Dist: pyyaml>=6 ; extra == 'skills'
31
32
  Requires-Dist: aiosqlite>=0.20 ; extra == 'sqlite'
32
33
  Requires-Python: >=3.10
33
34
  Project-URL: Documentation, https://mosquito.github.io/aiohttp-tiny-mcp/
@@ -35,12 +36,14 @@ Project-URL: Repository, https://github.com/mosquito/aiohttp-tiny-mcp
35
36
  Project-URL: Issues, https://github.com/mosquito/aiohttp-tiny-mcp/issues
36
37
  Provides-Extra: postgres
37
38
  Provides-Extra: redis
39
+ Provides-Extra: skills
38
40
  Provides-Extra: sqlite
39
41
  Description-Content-Type: text/markdown
40
42
 
41
43
  # aiohttp-tiny-mcp
42
44
 
43
45
  [![Tests](https://github.com/mosquito/aiohttp-tiny-mcp/actions/workflows/tests.yml/badge.svg)](https://github.com/mosquito/aiohttp-tiny-mcp/actions/workflows/tests.yml)
46
+ [![Coverage](https://img.shields.io/coveralls/github/mosquito/aiohttp-tiny-mcp/master)](https://coveralls.io/github/mosquito/aiohttp-tiny-mcp?branch=master)
44
47
  [![Latest release](https://img.shields.io/github/v/release/mosquito/aiohttp-tiny-mcp)](https://github.com/mosquito/aiohttp-tiny-mcp/releases)
45
48
  [![License](https://img.shields.io/github/license/mosquito/aiohttp-tiny-mcp)](https://github.com/mosquito/aiohttp-tiny-mcp/blob/master/LICENSE)
46
49
  [![Documentation](https://img.shields.io/badge/docs-online-blue.svg)](https://mosquito.github.io/aiohttp-tiny-mcp/)
@@ -230,6 +233,7 @@ Start with [the documentation overview](https://mosquito.github.io/aiohttp-tiny-
230
233
  4. [Using Exchange](https://mosquito.github.io/aiohttp-tiny-mcp/guide/exchange.html): request context, progress, questions, and state.
231
234
  5. [Authentication](https://mosquito.github.io/aiohttp-tiny-mcp/guide/auth.html): bearer-token verification and OAuth resource metadata.
232
235
  6. [Stores and hubs](https://mosquito.github.io/aiohttp-tiny-mcp/deployment/stores.html): shared backend contracts and deployment requirements.
236
+ 7. [Extensions and skills](https://mosquito.github.io/aiohttp-tiny-mcp/guide/extensions.html): custom methods, skill directories, and resources for older clients.
233
237
 
234
238
  The server supports `2026-07-28`, `2025-11-25`, `2025-06-18`, `2025-03-26`
235
239
  and `2024-11-05`.
@@ -240,6 +244,7 @@ Delivery mechanisms and client support differ; see the
240
244
  Documentation examples are checked by the test suite. From a source checkout:
241
245
 
242
246
  ```bash
247
+ uv sync --all-groups --all-extras
243
248
  uv run pytest docs README.md
244
- uv run --group docs sphinx-build -W -b html docs docs/_build
249
+ uv run sphinx-build -W -b html docs docs/_build
245
250
  ```
@@ -1,6 +1,7 @@
1
1
  # aiohttp-tiny-mcp
2
2
 
3
3
  [![Tests](https://github.com/mosquito/aiohttp-tiny-mcp/actions/workflows/tests.yml/badge.svg)](https://github.com/mosquito/aiohttp-tiny-mcp/actions/workflows/tests.yml)
4
+ [![Coverage](https://img.shields.io/coveralls/github/mosquito/aiohttp-tiny-mcp/master)](https://coveralls.io/github/mosquito/aiohttp-tiny-mcp?branch=master)
4
5
  [![Latest release](https://img.shields.io/github/v/release/mosquito/aiohttp-tiny-mcp)](https://github.com/mosquito/aiohttp-tiny-mcp/releases)
5
6
  [![License](https://img.shields.io/github/license/mosquito/aiohttp-tiny-mcp)](https://github.com/mosquito/aiohttp-tiny-mcp/blob/master/LICENSE)
6
7
  [![Documentation](https://img.shields.io/badge/docs-online-blue.svg)](https://mosquito.github.io/aiohttp-tiny-mcp/)
@@ -190,6 +191,7 @@ Start with [the documentation overview](https://mosquito.github.io/aiohttp-tiny-
190
191
  4. [Using Exchange](https://mosquito.github.io/aiohttp-tiny-mcp/guide/exchange.html): request context, progress, questions, and state.
191
192
  5. [Authentication](https://mosquito.github.io/aiohttp-tiny-mcp/guide/auth.html): bearer-token verification and OAuth resource metadata.
192
193
  6. [Stores and hubs](https://mosquito.github.io/aiohttp-tiny-mcp/deployment/stores.html): shared backend contracts and deployment requirements.
194
+ 7. [Extensions and skills](https://mosquito.github.io/aiohttp-tiny-mcp/guide/extensions.html): custom methods, skill directories, and resources for older clients.
193
195
 
194
196
  The server supports `2026-07-28`, `2025-11-25`, `2025-06-18`, `2025-03-26`
195
197
  and `2024-11-05`.
@@ -200,6 +202,7 @@ Delivery mechanisms and client support differ; see the
200
202
  Documentation examples are checked by the test suite. From a source checkout:
201
203
 
202
204
  ```bash
205
+ uv sync --all-groups --all-extras
203
206
  uv run pytest docs README.md
204
- uv run --group docs sphinx-build -W -b html docs docs/_build
207
+ uv run sphinx-build -W -b html docs docs/_build
205
208
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiohttp-tiny-mcp"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Remote MCP server and client library for aiohttp"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -39,12 +39,14 @@ Repository = "https://github.com/mosquito/aiohttp-tiny-mcp"
39
39
  Issues = "https://github.com/mosquito/aiohttp-tiny-mcp/issues"
40
40
 
41
41
  [project.optional-dependencies]
42
+ skills = ["PyYAML>=6"]
42
43
  sqlite = ["aiosqlite>=0.20"]
43
44
  redis = ["redis>=5"]
44
45
  postgres = ["psycopg[binary,pool]>=3"]
45
46
 
46
47
  [dependency-groups]
47
48
  dev = [
49
+ "PyYAML>=6",
48
50
  "PyJWT>=2.13",
49
51
  "aiosqlite>=0.20",
50
52
  "furo>=2025.12.19",
@@ -54,6 +56,7 @@ dev = [
54
56
  "psycopg[binary,pool]>=3",
55
57
  "pytest",
56
58
  "pytest-asyncio",
59
+ "pytest-cov>=7.1.0",
57
60
  "pytest-timeout",
58
61
  "redis>=5",
59
62
  "ruff",
@@ -69,6 +72,14 @@ source-include = [
69
72
  "src/aiohttp_tiny_mcp/console/*.js",
70
73
  ]
71
74
 
75
+ [tool.coverage.run]
76
+ source = ["aiohttp_tiny_mcp"]
77
+ branch = true
78
+
79
+ [tool.coverage.report]
80
+ skip_covered = true
81
+ show_missing = true
82
+
72
83
  [tool.pytest.ini_options]
73
84
  asyncio_mode = "auto"
74
85
  timeout = 30
@@ -102,5 +113,5 @@ root = [
102
113
  exclude = ["examples/docker-mcp"]
103
114
 
104
115
  [build-system]
105
- requires = ["uv_build>=0.11.32,<0.12"]
116
+ requires = ["uv_build>=0.12,<0.13"]
106
117
  build-backend = "uv_build"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiohttp-tiny-mcp"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Remote MCP server and client library for aiohttp"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -39,6 +39,7 @@ Repository = "https://github.com/mosquito/aiohttp-tiny-mcp"
39
39
  Issues = "https://github.com/mosquito/aiohttp-tiny-mcp/issues"
40
40
 
41
41
  [project.optional-dependencies]
42
+ skills = ["PyYAML>=6"]
42
43
  # A hub and a session store on one SQLite file: aiohttp_tiny_mcp.sqlite.
43
44
  sqlite = ["aiosqlite>=0.20"]
44
45
  redis = [
@@ -50,6 +51,7 @@ postgres = [
50
51
 
51
52
  [dependency-groups]
52
53
  dev = [
54
+ "PyYAML>=6",
53
55
  "PyJWT>=2.13",
54
56
  "aiosqlite>=0.20",
55
57
  "furo>=2025.12.19",
@@ -59,6 +61,7 @@ dev = [
59
61
  "psycopg[binary,pool]>=3",
60
62
  "pytest",
61
63
  "pytest-asyncio",
64
+ "pytest-cov>=7.1.0",
62
65
  "pytest-timeout",
63
66
  "redis>=5",
64
67
  "ruff",
@@ -72,9 +75,17 @@ dev = [
72
75
  source-include = ["src/aiohttp_tiny_mcp/console/*.html", "src/aiohttp_tiny_mcp/console/*.css", "src/aiohttp_tiny_mcp/console/*.js"]
73
76
 
74
77
  [build-system]
75
- requires = ["uv_build>=0.11.32,<0.12"]
78
+ requires = ["uv_build>=0.12,<0.13"]
76
79
  build-backend = "uv_build"
77
80
 
81
+ [tool.coverage.run]
82
+ source = ["aiohttp_tiny_mcp"]
83
+ branch = true
84
+
85
+ [tool.coverage.report]
86
+ skip_covered = true
87
+ show_missing = true
88
+
78
89
  [tool.pytest.ini_options]
79
90
  asyncio_mode = "auto"
80
91
  # The guard against a hung test, not a measure of how fast the machine is. A
@@ -15,6 +15,7 @@ from .core import (
15
15
  )
16
16
  from .endpoint import Endpoint
17
17
  from .exchange import Exchange
18
+ from .extensions import Extension
18
19
  from .http_sse import SseEndpoint
19
20
  from .hub import Hub, MemoryHub
20
21
  from .models import (
@@ -55,6 +56,7 @@ __all__ = [
55
56
  "Endpoint",
56
57
  "SSEResponse",
57
58
  "Exchange",
59
+ "Extension",
58
60
  "GetPromptResult",
59
61
  "Hint",
60
62
  "Hub",
@@ -26,6 +26,7 @@ from .core import (
26
26
  answer_actions,
27
27
  decode_failure_target,
28
28
  )
29
+ from .extensions import ExtensionSpec
29
30
  from .hub import Hub
30
31
  from .models import (
31
32
  CallToolParams,
@@ -72,6 +73,8 @@ class RegistryProtocol(Protocol):
72
73
  def prompts(self) -> Mapping[str, PromptSpec]: ...
73
74
  @property
74
75
  def completer(self) -> Bound | None: ...
76
+ @property
77
+ def extensions(self) -> Mapping[str, ExtensionSpec]: ...
75
78
  def match_resource(self, uri: str) -> tuple[ResourceSpec, dict[str, str]] | None: ...
76
79
 
77
80
 
@@ -113,7 +116,9 @@ class Adapter(ABC):
113
116
  """Raise `Rejected` on a revision-specific HTTP binding violation."""
114
117
  return None # noqa: B027 -- concrete default, most revisions check nothing
115
118
 
116
- def decode(self, pre: Preamble) -> Sequence[Call | DecodeFailure]:
119
+ def decode(
120
+ self, pre: Preamble, registry: RegistryProtocol | None = None
121
+ ) -> Sequence[Call | DecodeFailure]:
117
122
  """Decode messages independently; one invalid item does not abort the batch."""
118
123
  if pre.parse_error:
119
124
  raise Rejected(Failure(FailureKind.PARSE, "invalid JSON body"))
@@ -122,10 +127,12 @@ class Adapter(ABC):
122
127
  raise Rejected(Failure(FailureKind.MALFORMED, "batching not supported"))
123
128
  if not pre.body: # `is_batch` already means the body is a list
124
129
  raise Rejected(Failure(FailureKind.MALFORMED, "empty batch"))
125
- return [self.decode_item(item) for item in pre.body]
126
- return [self.decode_item(pre.body)]
130
+ return [self.decode_item(item, registry) for item in pre.body]
131
+ return [self.decode_item(pre.body, registry)]
127
132
 
128
- def decode_item(self, item: object) -> Call | DecodeFailure:
133
+ def decode_item(
134
+ self, item: object, registry: RegistryProtocol | None = None
135
+ ) -> Call | DecodeFailure:
129
136
  """Preserve the request id on failure without aborting sibling batch items."""
130
137
  if not isinstance(item, dict):
131
138
  return DecodeFailure(
@@ -135,12 +142,12 @@ class Adapter(ABC):
135
142
  )
136
143
  body = cast(dict[str, Any], item)
137
144
  try:
138
- return self.decode_one(body)
145
+ return self.decode_one(body, registry)
139
146
  except Rejected as e:
140
147
  call_id, must_respond = decode_failure_target(body)
141
148
  return DecodeFailure(id=call_id, failure=e.failure, must_respond=must_respond)
142
149
 
143
- def decode_one(self, body: dict[str, Any]) -> Call:
150
+ def decode_one(self, body: dict[str, Any], registry: RegistryProtocol | None = None) -> Call:
144
151
  if "id" in body and body["id"] is None:
145
152
  raise Rejected(Failure(FailureKind.MALFORMED, "id must not be null"))
146
153
  try:
@@ -148,6 +155,12 @@ class Adapter(ABC):
148
155
  except ValidationError as e:
149
156
  raise Rejected(Failure(FailureKind.MALFORMED, str(e))) from None
150
157
  operation = self.operation_for(msg.method)
158
+ if operation is None and registry is not None and self.supports_extensions:
159
+ if any(
160
+ msg.method in spec.methods and spec.min_revision <= self.version
161
+ for spec in registry.extensions.values()
162
+ ):
163
+ operation = Operation.EXTENSION
151
164
  if operation is None:
152
165
  raise Rejected(Failure(FailureKind.UNKNOWN_METHOD, f"unknown method: {msg.method}"))
153
166
  self.check_message(operation, msg)
@@ -156,7 +169,17 @@ class Adapter(ABC):
156
169
  except ValidationError as e:
157
170
  raise Rejected(Failure(FailureKind.INVALID_PARAMS, str(e))) from None
158
171
  self.check_params(params)
159
- return self.build_call(operation, msg, params)
172
+ call = self.build_call(operation, msg, params)
173
+ if operation is Operation.EXTENSION:
174
+ if msg.is_notification:
175
+ raise Rejected(Failure(FailureKind.MALFORMED, "extension requests require an id"))
176
+ call.target = msg.method
177
+ call.arguments = {
178
+ key: value
179
+ for key, value in msg.params.items()
180
+ if key not in {"_meta", "inputResponses", "requestState"}
181
+ }
182
+ return call
160
183
 
161
184
  def build_call(self, operation: Operation, msg: Incoming, params: Params) -> Call:
162
185
  target: str | None = None
@@ -283,7 +306,10 @@ class Adapter(ABC):
283
306
  caps: dict[str, Any] = {}
284
307
  if registry.tools:
285
308
  caps["tools"] = {"listChanged": True}
286
- if registry.resources_fixed or registry.resources_templated:
309
+ if any(
310
+ self.describe_resource(spec) is not None
311
+ for spec in (*registry.resources_fixed.values(), *registry.resources_templated)
312
+ ):
287
313
  caps["resources"] = {"listChanged": True, "subscribe": True}
288
314
  if registry.prompts:
289
315
  caps["prompts"] = {"listChanged": True}
@@ -300,7 +326,11 @@ class Adapter(ABC):
300
326
  def describe_tool(self, spec: ToolSpec) -> ToolDef | None: ...
301
327
 
302
328
  def describe_resource(self, spec: ResourceSpec) -> ResourceDef | ResourceTemplateDef | None:
303
- """Resource definitions are shared across revisions."""
329
+ """Project extension resources onto the URI understood by this revision."""
330
+ if spec.legacy_only and self.supports_extensions:
331
+ return None
332
+ if spec.definition is not None and spec.legacy_uri and not self.supports_extensions:
333
+ return spec.definition.model_copy(update={"uri": spec.legacy_uri})
304
334
  return spec.definition if spec.definition is not None else spec.template
305
335
 
306
336
  def describe_prompt(self, spec: PromptSpec) -> PromptDef | None:
@@ -319,3 +349,4 @@ class Adapter(ABC):
319
349
  allows_batch: ClassVar[bool] = False
320
350
  #: Negotiate once via initialize; retain the revision and capabilities in a session.
321
351
  has_handshake: ClassVar[bool] = False
352
+ supports_extensions: ClassVar[bool] = False
@@ -144,6 +144,22 @@ class BaseClient(ABC):
144
144
  method = self.adapter.method_for(operation)
145
145
  if method is None:
146
146
  raise ValueError(f"{self.adapter.version} does not expose {operation}")
147
+ return await self.request_method(method, params, name=name)
148
+
149
+ async def request_method(
150
+ self,
151
+ method: str,
152
+ params: Params | Mapping[str, Any] | None = None,
153
+ *,
154
+ name: str | None = None,
155
+ ) -> dict[str, Any]:
156
+ """Call a named method, including extensions, with protocol metadata and headers.
157
+
158
+ Check server discovery for the extension before using its methods.
159
+ Results are returned as wire dictionaries; RPC errors raise ClientError.
160
+ """
161
+ if not isinstance(params, Params):
162
+ params = Params.model_validate(dict(params or {}))
147
163
  params = self.adapter.client_decorate_params(params, self.profile)
148
164
  envelope = {
149
165
  "jsonrpc": "2.0",
@@ -151,7 +151,7 @@ button.quiet { color: var(--ink-soft); }
151
151
  @media (max-width: 900px) {
152
152
  /* Avoid auto rows distributing spare height to collapsed panels. */
153
153
  .layout {
154
- grid-template-columns: 1fr;
154
+ grid-template-columns: minmax(0, 1fr);
155
155
  grid-template-rows: auto minmax(0, 1fr) auto;
156
156
  }
157
157
  .layout > .panel { min-height: 0; }
@@ -164,6 +164,8 @@ button.quiet { color: var(--ink-soft); }
164
164
  display: flex;
165
165
  flex-direction: column;
166
166
  min-height: 0;
167
+ min-width: 0;
168
+ overflow-wrap: anywhere;
167
169
  }
168
170
 
169
171
  .panel-head {
@@ -183,12 +185,14 @@ button.quiet { color: var(--ink-soft); }
183
185
  color: var(--ink-soft);
184
186
  margin: 0;
185
187
  flex: 1;
188
+ min-width: 0;
186
189
  overflow: hidden;
187
190
  text-overflow: ellipsis;
188
191
  white-space: nowrap;
189
192
  }
190
193
 
191
194
  .work .panel-head h2 { text-transform: none; letter-spacing: 0; font-size: 14px; color: var(--ink); font-family: var(--mono); }
195
+ .panel-head > button { flex-shrink: 0; }
192
196
 
193
197
  .scroll { flex: 1; overflow: auto; padding: 12px 14px; min-height: 0; }
194
198
 
@@ -246,6 +250,8 @@ button.quiet { color: var(--ink-soft); }
246
250
  .item {
247
251
  display: block;
248
252
  width: 100%;
253
+ min-width: 0;
254
+ overflow-wrap: anywhere;
249
255
  text-align: left;
250
256
  border: 1px solid transparent;
251
257
  background: transparent;
@@ -301,7 +307,7 @@ button.quiet { color: var(--ink-soft); }
301
307
  border: 1px solid var(--line);
302
308
  border-radius: 4px;
303
309
  padding: 1px 4px;
304
- white-space: nowrap;
310
+ white-space: normal;
305
311
  }
306
312
 
307
313
  .about pre {
@@ -371,10 +377,10 @@ button.quiet { color: var(--ink-soft); }
371
377
  button.tiny { font-size: 11px; padding: 1px 7px; }
372
378
 
373
379
 
374
- /* minmax(0, ...) prevents long keys from consuming the value column. */
380
+ /* Cap long keys so values and copy controls keep usable space. */
375
381
  .fields {
376
382
  display: grid;
377
- grid-template-columns: minmax(0, auto) minmax(0, 1fr);
383
+ grid-template-columns: fit-content(35%) minmax(0, 1fr);
378
384
  gap: 3px 14px;
379
385
  align-items: baseline;
380
386
  }
@@ -385,7 +391,8 @@ button.tiny { font-size: 11px; padding: 1px 7px; }
385
391
  .field-name {
386
392
  color: var(--ink-soft);
387
393
  font-size: 12px;
388
- white-space: nowrap;
394
+ min-width: 0;
395
+ white-space: normal;
389
396
  overflow-wrap: anywhere;
390
397
  }
391
398