aiohttp-tiny-mcp 0.3.3__tar.gz → 0.3.4__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.3 → aiohttp_tiny_mcp-0.3.4}/PKG-INFO +6 -2
  2. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/README.md +5 -1
  3. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/pyproject.toml +1 -1
  4. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/pyproject.toml.orig +1 -1
  5. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/console/console.css +2 -0
  6. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/console/console.js +41 -3
  7. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/console/index.html +3 -3
  8. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/endpoint.py +25 -2
  9. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/LICENSE +0 -0
  10. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/__init__.py +0 -0
  11. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/adapter.py +0 -0
  12. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/auth.py +0 -0
  13. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/client.py +0 -0
  14. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/client_base.py +0 -0
  15. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/console/__init__.py +0 -0
  16. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/core.py +0 -0
  17. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/dispatcher.py +0 -0
  18. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/exchange.py +0 -0
  19. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/extensions.py +0 -0
  20. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/http_sse.py +0 -0
  21. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/hub.py +0 -0
  22. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/models.py +0 -0
  23. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/namespaces.py +0 -0
  24. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/postgres.py +0 -0
  25. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/protocol/__init__.py +0 -0
  26. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/protocol/selection.py +0 -0
  27. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/protocol/v2024_11_05.py +0 -0
  28. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/protocol/v2025_03_26.py +0 -0
  29. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/protocol/v2025_06_18.py +0 -0
  30. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/protocol/v2025_11_25.py +0 -0
  31. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/protocol/v2026_07_28.py +0 -0
  32. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/py.typed +0 -0
  33. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/redis.py +0 -0
  34. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/registry.py +0 -0
  35. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/request_state.py +0 -0
  36. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/schema.py +0 -0
  37. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/sessions.py +0 -0
  38. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/skills.py +0 -0
  39. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/specs.py +0 -0
  40. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/sqlite.py +0 -0
  41. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/sse.py +0 -0
  42. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/stdio.py +0 -0
  43. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/stdio_client.py +0 -0
  44. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/subscriptions.py +0 -0
  45. {aiohttp_tiny_mcp-0.3.3 → aiohttp_tiny_mcp-0.3.4}/src/aiohttp_tiny_mcp/testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohttp-tiny-mcp
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: Remote MCP server and client library for aiohttp
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -146,7 +146,11 @@ async def deploy(args: Deploy, ex: Exchange) -> str:
146
146
  return f"deployed {args.service}"
147
147
 
148
148
 
149
- app = Endpoint(registry).app("/mcp")
149
+ app = Endpoint(
150
+ registry,
151
+ # Behind a reverse proxy, uncomment this and let the proxy check Origin.
152
+ # trust_proxy_origin_validation=True,
153
+ ).app("/mcp")
150
154
 
151
155
  if __name__ == "__main__":
152
156
  web.run_app(app, host="127.0.0.1", port=8080)
@@ -104,7 +104,11 @@ async def deploy(args: Deploy, ex: Exchange) -> str:
104
104
  return f"deployed {args.service}"
105
105
 
106
106
 
107
- app = Endpoint(registry).app("/mcp")
107
+ app = Endpoint(
108
+ registry,
109
+ # Behind a reverse proxy, uncomment this and let the proxy check Origin.
110
+ # trust_proxy_origin_validation=True,
111
+ ).app("/mcp")
108
112
 
109
113
  if __name__ == "__main__":
110
114
  web.run_app(app, host="127.0.0.1", port=8080)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiohttp-tiny-mcp"
3
- version = "0.3.3"
3
+ version = "0.3.4"
4
4
  description = "Remote MCP server and client library for aiohttp"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiohttp-tiny-mcp"
3
- version = "0.3.3"
3
+ version = "0.3.4"
4
4
  description = "Remote MCP server and client library for aiohttp"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -71,6 +71,8 @@ body {
71
71
  }
72
72
 
73
73
  .field { display: flex; align-items: center; gap: 6px; }
74
+ .field.grow { flex: 1 1 12em; min-width: 0; }
75
+ .field.grow > input { flex: 1 1 auto; min-width: 8em; }
74
76
  .field > span { color: var(--ink-soft); font-size: 12px; }
75
77
 
76
78
  input[type="text"], input[type="number"], select, textarea {
@@ -384,6 +384,34 @@ const page = {
384
384
  let client = null;
385
385
  let chosen = null;
386
386
  let endpointUrl = null;
387
+ //: Where the page was told the endpoint is. The field starts from it and may add a query.
388
+ let configuredEndpoint = null;
389
+
390
+ // The endpoint field: the path, plus any query string a person adds, such as
391
+ // `?mcp=2025-06-18` to pin a revision or whatever the server reads from the URL.
392
+ // The value is kept for the next visit while its path is still the configured one,
393
+ // so a server moved elsewhere is not chased at a stale address.
394
+ function endpointKey() {
395
+ return `mcp-console-endpoint:${location.pathname}`;
396
+ }
397
+
398
+ function rememberedEndpoint() {
399
+ const kept = localStorage.getItem(endpointKey());
400
+ if (!kept) return null;
401
+ const url = new URL(kept, location.origin);
402
+ return url.pathname === configuredEndpoint.pathname ? kept : null;
403
+ }
404
+
405
+ function chosenEndpoint() {
406
+ const typed = page.endpoint.value.trim() || configuredEndpoint.pathname;
407
+ const url = new URL(typed, location.origin);
408
+ if (url.origin !== location.origin) {
409
+ throw new Error(`The endpoint must be on this server, not ${url.origin}.`);
410
+ }
411
+ page.endpoint.value = url.pathname + url.search;
412
+ localStorage.setItem(endpointKey(), page.endpoint.value);
413
+ return url;
414
+ }
387
415
 
388
416
  function element(tag, className, text) {
389
417
  const made = document.createElement(tag);
@@ -1297,6 +1325,7 @@ function disconnect() {
1297
1325
  page.refresh.disabled = true;
1298
1326
  page.revision.disabled = false;
1299
1327
  page.answerable.disabled = false;
1328
+ page.endpoint.disabled = false;
1300
1329
  page.connect.textContent = "Connect";
1301
1330
  say("not connected", "");
1302
1331
  }
@@ -1305,9 +1334,11 @@ async function connect() {
1305
1334
  page.connect.disabled = true;
1306
1335
  page.revision.disabled = true;
1307
1336
  page.answerable.disabled = true;
1337
+ page.endpoint.disabled = true;
1308
1338
  say("connecting", "");
1309
1339
  page.outcome.textContent = "";
1310
1340
  try {
1341
+ endpointUrl = chosenEndpoint();
1311
1342
  client = new Client(endpointUrl.href, page.revision.value, {
1312
1343
  answerable: page.answerable.checked,
1313
1344
  onFrame: record,
@@ -1330,6 +1361,7 @@ async function connect() {
1330
1361
  page.refresh.disabled = true;
1331
1362
  page.revision.disabled = false;
1332
1363
  page.answerable.disabled = false;
1364
+ page.endpoint.disabled = false;
1333
1365
  page.connect.textContent = "Connect";
1334
1366
  say(error.message, "off");
1335
1367
  report("Could not connect", connectionHelp(error), true);
@@ -1340,7 +1372,7 @@ async function connect() {
1340
1372
 
1341
1373
  function connectionHelp(error) {
1342
1374
  const lines = [error.message];
1343
- if (error instanceof TypeError || /fetch/i.test(error.message)) {
1375
+ if (endpointUrl && (error instanceof TypeError || /fetch/i.test(error.message))) {
1344
1376
  lines.push("", `The request to ${endpointUrl.href} did not reach a server.`);
1345
1377
  }
1346
1378
  return lines.join("\n");
@@ -1409,8 +1441,14 @@ function start() {
1409
1441
  const remembered = localStorage.getItem("mcp-console-revision");
1410
1442
  page.revision.value = REVISIONS[remembered] ? remembered : Object.keys(REVISIONS)[0];
1411
1443
 
1412
- endpointUrl = new URL(document.documentElement.dataset.endpoint || "/mcp", location.origin);
1413
- page.endpoint.textContent = endpointUrl.pathname;
1444
+ configuredEndpoint = new URL(document.documentElement.dataset.endpoint || "/mcp", location.origin);
1445
+ page.endpoint.value = rememberedEndpoint() || configuredEndpoint.pathname + configuredEndpoint.search;
1446
+ page.endpoint.onkeydown = (event) => {
1447
+ if (event.key === "Enter" && !client) {
1448
+ event.preventDefault();
1449
+ connect();
1450
+ }
1451
+ };
1414
1452
 
1415
1453
  page.connect.onclick = () => (client ? disconnect() : connect());
1416
1454
  page.refresh.onclick = async () => {
@@ -11,10 +11,10 @@
11
11
 
12
12
  <header class="bar">
13
13
  <h1>{{TITLE}}</h1>
14
- <span class="field">
14
+ <label class="field grow" title="The path the server serves MCP on. A query string is sent as typed">
15
15
  <span>Endpoint</span>
16
- <code id="endpoint" class="endpoint"></code>
17
- </span>
16
+ <input id="endpoint" class="endpoint" type="text" spellcheck="false" autocomplete="off">
17
+ </label>
18
18
  <label class="field">
19
19
  <span>Revision</span>
20
20
  <select id="revision" class="revision"></select>
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import logging
8
+ import re
8
9
  from collections.abc import Mapping
9
10
  from contextlib import suppress
10
11
  from dataclasses import replace
@@ -51,6 +52,24 @@ log = logging.getLogger("aiohttp_tiny_mcp")
51
52
  MAY_ASK = frozenset({Operation.CALL_TOOL, Operation.GET_PROMPT, Operation.READ_RESOURCE})
52
53
 
53
54
 
55
+ def origin_pattern(spec: str) -> re.Pattern[str]:
56
+ """Compile an allowed origin with wildcards.
57
+
58
+ `*` matches one host label, `**` matches one or more labels, so
59
+ `https://*.example.com` admits `https://app.example.com` and `https://**.example.com`
60
+ admits `https://a.b.example.com` as well. Everything else matches literally.
61
+ """
62
+ parts = []
63
+ for piece in re.split(r"(\*\*|\*)", spec):
64
+ if piece == "**":
65
+ parts.append(r"[^./]+(?:\.[^./]+)*")
66
+ elif piece == "*":
67
+ parts.append(r"[^./]+")
68
+ else:
69
+ parts.append(re.escape(piece))
70
+ return re.compile("".join(parts))
71
+
72
+
54
73
  class Endpoint:
55
74
  """The MCP endpoint, in the spec's sense: one path that accepts POST.
56
75
 
@@ -81,6 +100,7 @@ class Endpoint:
81
100
  self.adapters = adapters or AdapterSet.default()
82
101
  self.dispatcher = Dispatcher(registry)
83
102
  self.allowed_origins = allowed_origins
103
+ self.origin_patterns = [origin_pattern(o) for o in allowed_origins or () if "*" in o]
84
104
  self.trust_proxy_origin_validation = trust_proxy_origin_validation
85
105
  self.compress = compress
86
106
 
@@ -187,8 +207,11 @@ class Endpoint:
187
207
  return
188
208
  if origin == self.own_origin(request):
189
209
  return
190
- if self.allowed_origins is None or origin not in self.allowed_origins:
191
- raise Rejected(Failure(FailureKind.ORIGIN_REJECTED, "origin not allowed"))
210
+ if self.allowed_origins is not None and origin in self.allowed_origins:
211
+ return
212
+ if any(pattern.fullmatch(origin) for pattern in self.origin_patterns):
213
+ return
214
+ raise Rejected(Failure(FailureKind.ORIGIN_REJECTED, "origin not allowed"))
192
215
 
193
216
  def own_origin(self, request: web.Request) -> str:
194
217
  """Origin of pages served by this endpoint. A rebound page retains the attacker's origin