agentruntime-mcp 0.2.0__tar.gz → 0.3.1__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 (34) hide show
  1. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/PKG-INFO +14 -1
  2. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/README.md +13 -0
  3. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/pyproject.toml +1 -1
  4. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/__init__.py +14 -1
  5. agentruntime_mcp-0.3.1/src/agentruntime/mcp/request_bearer.py +12 -0
  6. agentruntime_mcp-0.3.1/src/agentruntime/mcp/toolorg/__init__.py +29 -0
  7. agentruntime_mcp-0.3.1/src/agentruntime/mcp/toolorg/toolorg.py +225 -0
  8. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime_mcp.egg-info/PKG-INFO +14 -1
  9. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime_mcp.egg-info/SOURCES.txt +5 -1
  10. agentruntime_mcp-0.3.1/tests/test_toolorg.py +54 -0
  11. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/setup.cfg +0 -0
  12. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/__init__.py +0 -0
  13. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/adapter_registry.py +0 -0
  14. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/auth_http.py +0 -0
  15. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/bridge.py +0 -0
  16. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/bridge_auth.py +0 -0
  17. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/config_schema.py +0 -0
  18. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/context.py +0 -0
  19. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/control.py +0 -0
  20. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/decorators.py +0 -0
  21. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/env_merge.py +0 -0
  22. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/errors.py +0 -0
  23. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/middleware.py +0 -0
  24. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/proxy.py +0 -0
  25. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/runtime.py +0 -0
  26. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/schemas.py +0 -0
  27. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime/mcp/webhook.py +0 -0
  28. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime_mcp.egg-info/dependency_links.txt +0 -0
  29. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime_mcp.egg-info/requires.txt +0 -0
  30. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/src/agentruntime_mcp.egg-info/top_level.txt +0 -0
  31. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/tests/test_bridge_auth.py +0 -0
  32. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/tests/test_context.py +0 -0
  33. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/tests/test_runtime.py +0 -0
  34. {agentruntime_mcp-0.2.0 → agentruntime_mcp-0.3.1}/tests/test_runtime_router.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentruntime-mcp
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: AgentRuntime MCP SDK (decorators, runtime, middleware, schemas, proxy)
5
5
  Author: AgentRuntime
6
6
  Requires-Python: >=3.10
@@ -115,6 +115,19 @@ run_proxy(
115
115
  )
116
116
  ```
117
117
 
118
+ ## Tool organization (`toolorg`)
119
+
120
+ Large connectors can build publish metadata for Control catalog grouping. Parity with Go `toolorg` — see [MCP_SDK_PARITY.md](../../docs/mcp/MCP_SDK_PARITY.md).
121
+
122
+ ```python
123
+ from agentruntime.mcp.toolorg import publisher_metadata, Metadata
124
+
125
+ meta = publisher_metadata("clickup_get_task", Metadata())
126
+ # → display_name, suggested_group, suggested_tags for mcp_tools.metadata
127
+ ```
128
+
129
+ Per-request caller bearer (for downstream HTTP): `request_bearer_from_context()`.
130
+
118
131
  ## TypeScript sibling SDK
119
132
 
120
133
  For Node deployments, use the monorepo package [`packages/agentruntime-mcp-ts`](../agentruntime-mcp-ts/README.md) (`@agentruntime-labs/agentruntime-mcp`): official **`@modelcontextprotocol/server`**, Zod 4, **`runWithRouter`**.
@@ -96,6 +96,19 @@ run_proxy(
96
96
  )
97
97
  ```
98
98
 
99
+ ## Tool organization (`toolorg`)
100
+
101
+ Large connectors can build publish metadata for Control catalog grouping. Parity with Go `toolorg` — see [MCP_SDK_PARITY.md](../../docs/mcp/MCP_SDK_PARITY.md).
102
+
103
+ ```python
104
+ from agentruntime.mcp.toolorg import publisher_metadata, Metadata
105
+
106
+ meta = publisher_metadata("clickup_get_task", Metadata())
107
+ # → display_name, suggested_group, suggested_tags for mcp_tools.metadata
108
+ ```
109
+
110
+ Per-request caller bearer (for downstream HTTP): `request_bearer_from_context()`.
111
+
99
112
  ## TypeScript sibling SDK
100
113
 
101
114
  For Node deployments, use the monorepo package [`packages/agentruntime-mcp-ts`](../agentruntime-mcp-ts/README.md) (`@agentruntime-labs/agentruntime-mcp`): official **`@modelcontextprotocol/server`**, Zod 4, **`runWithRouter`**.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentruntime-mcp"
7
- version = "0.2.0"
7
+ version = "0.3.1"
8
8
  description = "AgentRuntime MCP SDK (decorators, runtime, middleware, schemas, proxy)"
9
9
  requires-python = ">=3.10"
10
10
  readme = "README.md"
@@ -27,4 +27,17 @@ from .control import ( # noqa: F401
27
27
  build_runtime_context_from_request,
28
28
  )
29
29
  from .webhook import sign_mode_b, deliver_mode_b, ModeBRequest # noqa: F401
30
-
30
+ from .request_bearer import request_bearer_from_context # noqa: F401
31
+ from .toolorg import ( # noqa: F401
32
+ Metadata,
33
+ EffectiveOrganization,
34
+ ToolGroup,
35
+ suggest_from_wire_name,
36
+ format_display_name,
37
+ publisher_metadata,
38
+ default_publisher_metadata,
39
+ group_label,
40
+ parse_metadata,
41
+ metadata_is_empty,
42
+ merge_effective,
43
+ )
@@ -0,0 +1,12 @@
1
+ """Per-request bearer token context (parity with agentruntime-mcp-go request_bearer)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .context import get_auth_token
6
+
7
+ __all__ = ["request_bearer_from_context"]
8
+
9
+
10
+ def request_bearer_from_context() -> str | None:
11
+ """Return the incoming MCP caller bearer token for the active request, if set."""
12
+ return get_auth_token()
@@ -0,0 +1,29 @@
1
+ """Tool organization helpers for MCP publish metadata (parity with agentruntime-mcp-go/toolorg)."""
2
+
3
+ from .toolorg import (
4
+ Metadata,
5
+ EffectiveOrganization,
6
+ ToolGroup,
7
+ suggest_from_wire_name,
8
+ format_display_name,
9
+ publisher_metadata,
10
+ default_publisher_metadata,
11
+ group_label,
12
+ parse_metadata,
13
+ metadata_is_empty,
14
+ merge_effective,
15
+ )
16
+
17
+ __all__ = [
18
+ "Metadata",
19
+ "EffectiveOrganization",
20
+ "ToolGroup",
21
+ "suggest_from_wire_name",
22
+ "format_display_name",
23
+ "publisher_metadata",
24
+ "default_publisher_metadata",
25
+ "group_label",
26
+ "parse_metadata",
27
+ "metadata_is_empty",
28
+ "merge_effective",
29
+ ]
@@ -0,0 +1,225 @@
1
+ """Publish-time tool organization — mirror agentruntime-mcp-go/toolorg."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Mapping, Optional, Sequence
8
+
9
+ _DEFAULT_GROUP_LABELS: dict[str, str] = {
10
+ "hierarchy": "Hierarchy",
11
+ "tasks": "Tasks",
12
+ "search_members": "Search & members",
13
+ "comments": "Comments",
14
+ "time": "Time tracking",
15
+ "docs": "Docs",
16
+ "chat": "Chat",
17
+ "uncategorized": "Other",
18
+ }
19
+
20
+
21
+ @dataclass
22
+ class Metadata:
23
+ display_name: str = ""
24
+ suggested_group: str = ""
25
+ suggested_tags: list[str] = field(default_factory=list)
26
+ suggested_rank_key: str = ""
27
+
28
+
29
+ @dataclass
30
+ class EffectiveOrganization:
31
+ group_id: str = ""
32
+ group_label: str = ""
33
+ rank_key: str = ""
34
+ tags: list[str] = field(default_factory=list)
35
+
36
+
37
+ @dataclass
38
+ class ToolGroup:
39
+ id: str = ""
40
+ label: str = ""
41
+ rank_key: str = ""
42
+ source: str = ""
43
+
44
+
45
+ def _normalize_tags(tags: Sequence[str]) -> list[str]:
46
+ seen: set[str] = set()
47
+ out: list[str] = []
48
+ for t in tags:
49
+ t = str(t).strip().lower()
50
+ if not t or t in seen:
51
+ continue
52
+ seen.add(t)
53
+ out.append(t)
54
+ out.sort()
55
+ return out
56
+
57
+
58
+ def _infer_group_id(body: str) -> str:
59
+ if "chat" in body:
60
+ return "chat"
61
+ if "doc" in body:
62
+ return "docs"
63
+ if "comment" in body:
64
+ return "comments"
65
+ if "time" in body:
66
+ return "time"
67
+ if body == "search" or "member" in body or "assignee" in body or "filtered_team" in body:
68
+ return "search_members"
69
+ if (
70
+ "workspace" in body
71
+ or "space" in body
72
+ or "folder" in body
73
+ or "hierarchy" in body
74
+ or body == "get_list"
75
+ or body.startswith("get_lists")
76
+ or body.startswith("create_list")
77
+ or body.startswith("update_list")
78
+ or body.startswith("create_folder")
79
+ or body.startswith("update_folder")
80
+ ):
81
+ return "hierarchy"
82
+ return "tasks"
83
+
84
+
85
+ def _is_read_only_verb(body: str) -> bool:
86
+ for prefix in ("get_", "list_", "find_", "resolve_", "search"):
87
+ if body.startswith(prefix) or body == prefix.rstrip("_"):
88
+ return True
89
+ return False
90
+
91
+
92
+ def _infer_tags(body: str, group_id: str) -> list[str]:
93
+ tags = [group_id]
94
+ tags.append("read_only" if _is_read_only_verb(body) else "mutating")
95
+ tags.append("v3" if ("chat" in body or "doc" in body) else "v2")
96
+ return _normalize_tags(tags)
97
+
98
+
99
+ def suggest_from_wire_name(tool_name: str) -> tuple[str, list[str]]:
100
+ name = tool_name.strip().lower()
101
+ parts = name.split("_")
102
+ if len(parts) < 2:
103
+ return "uncategorized", ["mutating"]
104
+ body = "_".join(parts[1:])
105
+ group_id = _infer_group_id(body)
106
+ return group_id, _infer_tags(body, group_id)
107
+
108
+
109
+ def format_display_name(tool_name: str) -> str:
110
+ parts = tool_name.strip().split("_")
111
+ if len(parts) <= 1:
112
+ return tool_name
113
+ body = " ".join(parts[1:])
114
+ if not body:
115
+ return tool_name
116
+ return body[0].upper() + body[1:]
117
+
118
+
119
+ def publisher_metadata(tool_name: str, overrides: Optional[Metadata] = None) -> dict[str, Any]:
120
+ ov = overrides or Metadata()
121
+ out: dict[str, Any] = {}
122
+ display = ov.display_name.strip()
123
+ if not display:
124
+ display = format_display_name(tool_name)
125
+ if display:
126
+ out["display_name"] = display
127
+ group = ov.suggested_group.strip().lower()
128
+ if not group:
129
+ group, _ = suggest_from_wire_name(tool_name)
130
+ if group:
131
+ out["suggested_group"] = group
132
+ tags = list(ov.suggested_tags)
133
+ if not tags:
134
+ _, tags = suggest_from_wire_name(tool_name)
135
+ if tags:
136
+ out["suggested_tags"] = tags
137
+ rk = ov.suggested_rank_key.strip()
138
+ if rk:
139
+ out["suggested_rank_key"] = rk
140
+ return out
141
+
142
+
143
+ def default_publisher_metadata(tool_name: str) -> dict[str, Any]:
144
+ return publisher_metadata(tool_name, Metadata())
145
+
146
+
147
+ def group_label(group_id: str) -> str:
148
+ gid = group_id.strip().lower()
149
+ if gid in _DEFAULT_GROUP_LABELS:
150
+ return _DEFAULT_GROUP_LABELS[gid]
151
+ parts = gid.split("_")
152
+ titled = []
153
+ for p in parts:
154
+ if not p:
155
+ continue
156
+ titled.append(p[0].upper() + p[1:])
157
+ return " ".join(titled)
158
+
159
+
160
+ def _normalize_group_id(group_id: str) -> str:
161
+ return group_id.strip().lower().replace(" ", "_")
162
+
163
+
164
+ def parse_metadata(raw: bytes | str) -> Metadata:
165
+ if not raw:
166
+ return Metadata()
167
+ if isinstance(raw, bytes):
168
+ text = raw.decode("utf-8", errors="replace")
169
+ else:
170
+ text = raw
171
+ try:
172
+ parsed = json.loads(text)
173
+ except json.JSONDecodeError:
174
+ return Metadata()
175
+ if not isinstance(parsed, dict):
176
+ return Metadata()
177
+ tags_raw = parsed.get("suggested_tags") or []
178
+ tags: list[str] = []
179
+ if isinstance(tags_raw, list):
180
+ tags = [str(t) for t in tags_raw]
181
+ return Metadata(
182
+ display_name=str(parsed.get("display_name") or ""),
183
+ suggested_group=_normalize_group_id(str(parsed.get("suggested_group") or "")),
184
+ suggested_tags=_normalize_tags(tags),
185
+ suggested_rank_key=str(parsed.get("suggested_rank_key") or ""),
186
+ )
187
+
188
+
189
+ def metadata_is_empty(meta: Mapping[str, Any]) -> bool:
190
+ if not meta:
191
+ return True
192
+ for k, v in meta.items():
193
+ if v is None:
194
+ continue
195
+ if k == "suggested_tags":
196
+ if isinstance(v, list) and len(v) > 0:
197
+ return False
198
+ continue
199
+ if isinstance(v, str) and v.strip():
200
+ return False
201
+ return True
202
+
203
+
204
+ def merge_effective(
205
+ published: Metadata,
206
+ overlay_group_id: Optional[str] = None,
207
+ overlay_rank_key: Optional[str] = None,
208
+ ) -> EffectiveOrganization:
209
+ group_id = published.suggested_group.strip()
210
+ tags = list(published.suggested_tags)
211
+ if overlay_group_id is not None:
212
+ g = _normalize_group_id(overlay_group_id)
213
+ if g:
214
+ group_id = g
215
+ rank_key = published.suggested_rank_key.strip()
216
+ if overlay_rank_key is not None:
217
+ rk = overlay_rank_key.strip()
218
+ if rk:
219
+ rank_key = rk
220
+ return EffectiveOrganization(
221
+ group_id=group_id,
222
+ group_label=group_label(group_id),
223
+ rank_key=rank_key,
224
+ tags=tags,
225
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentruntime-mcp
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: AgentRuntime MCP SDK (decorators, runtime, middleware, schemas, proxy)
5
5
  Author: AgentRuntime
6
6
  Requires-Python: >=3.10
@@ -115,6 +115,19 @@ run_proxy(
115
115
  )
116
116
  ```
117
117
 
118
+ ## Tool organization (`toolorg`)
119
+
120
+ Large connectors can build publish metadata for Control catalog grouping. Parity with Go `toolorg` — see [MCP_SDK_PARITY.md](../../docs/mcp/MCP_SDK_PARITY.md).
121
+
122
+ ```python
123
+ from agentruntime.mcp.toolorg import publisher_metadata, Metadata
124
+
125
+ meta = publisher_metadata("clickup_get_task", Metadata())
126
+ # → display_name, suggested_group, suggested_tags for mcp_tools.metadata
127
+ ```
128
+
129
+ Per-request caller bearer (for downstream HTTP): `request_bearer_from_context()`.
130
+
118
131
  ## TypeScript sibling SDK
119
132
 
120
133
  For Node deployments, use the monorepo package [`packages/agentruntime-mcp-ts`](../agentruntime-mcp-ts/README.md) (`@agentruntime-labs/agentruntime-mcp`): official **`@modelcontextprotocol/server`**, Zod 4, **`runWithRouter`**.
@@ -14,9 +14,12 @@ src/agentruntime/mcp/env_merge.py
14
14
  src/agentruntime/mcp/errors.py
15
15
  src/agentruntime/mcp/middleware.py
16
16
  src/agentruntime/mcp/proxy.py
17
+ src/agentruntime/mcp/request_bearer.py
17
18
  src/agentruntime/mcp/runtime.py
18
19
  src/agentruntime/mcp/schemas.py
19
20
  src/agentruntime/mcp/webhook.py
21
+ src/agentruntime/mcp/toolorg/__init__.py
22
+ src/agentruntime/mcp/toolorg/toolorg.py
20
23
  src/agentruntime_mcp.egg-info/PKG-INFO
21
24
  src/agentruntime_mcp.egg-info/SOURCES.txt
22
25
  src/agentruntime_mcp.egg-info/dependency_links.txt
@@ -25,4 +28,5 @@ src/agentruntime_mcp.egg-info/top_level.txt
25
28
  tests/test_bridge_auth.py
26
29
  tests/test_context.py
27
30
  tests/test_runtime.py
28
- tests/test_runtime_router.py
31
+ tests/test_runtime_router.py
32
+ tests/test_toolorg.py
@@ -0,0 +1,54 @@
1
+ """Tests for toolorg parity with agentruntime-mcp-go/toolorg."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from agentruntime.mcp.toolorg import (
11
+ Metadata,
12
+ default_publisher_metadata,
13
+ merge_effective,
14
+ suggest_from_wire_name,
15
+ )
16
+
17
+
18
+ def _load_cases() -> list[dict]:
19
+ path = (
20
+ Path(__file__).resolve().parent.parent
21
+ / "src"
22
+ / "agentruntime"
23
+ / "mcp"
24
+ / "toolorg"
25
+ / "fixtures"
26
+ / "suggest_cases.json"
27
+ )
28
+ return json.loads(path.read_text(encoding="utf-8"))
29
+
30
+
31
+ @pytest.mark.parametrize("case", _load_cases(), ids=lambda c: c["tool_name"])
32
+ def test_suggest_from_wire_name_fixtures(case: dict) -> None:
33
+ group, tags = suggest_from_wire_name(case["tool_name"])
34
+ assert group == case["group"]
35
+ assert tags == case["tags"]
36
+
37
+
38
+ @pytest.mark.parametrize("case", _load_cases(), ids=lambda c: c["tool_name"])
39
+ def test_default_publisher_metadata_fixtures(case: dict) -> None:
40
+ meta = default_publisher_metadata(case["tool_name"])
41
+ assert meta["suggested_group"] == case["group"]
42
+
43
+
44
+ def test_merge_effective_explicit_only() -> None:
45
+ eff = merge_effective(Metadata(), overlay_group_id="docs", overlay_rank_key="n")
46
+ assert eff.group_id == "docs"
47
+ assert eff.rank_key == "n"
48
+
49
+ eff = merge_effective(Metadata(suggested_group="tasks"))
50
+ assert eff.group_id == "tasks"
51
+
52
+ eff = merge_effective(Metadata())
53
+ assert eff.group_id == ""
54
+ assert eff.tags == []