agentruntime-mcp 0.0.1__tar.gz → 0.1.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 (34) hide show
  1. agentruntime_mcp-0.1.2/PKG-INFO +128 -0
  2. agentruntime_mcp-0.1.2/README.md +109 -0
  3. {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/pyproject.toml +11 -3
  4. {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/setup.cfg +4 -4
  5. {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime/__init__.py +2 -2
  6. agentruntime_mcp-0.1.2/src/agentruntime/mcp/__init__.py +21 -0
  7. agentruntime_mcp-0.1.2/src/agentruntime/mcp/adapter_registry.py +87 -0
  8. agentruntime_mcp-0.1.2/src/agentruntime/mcp/auth_http.py +43 -0
  9. agentruntime_mcp-0.1.2/src/agentruntime/mcp/config_schema.py +86 -0
  10. agentruntime_mcp-0.1.2/src/agentruntime/mcp/context.py +76 -0
  11. {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime/mcp/decorators.py +106 -74
  12. agentruntime_mcp-0.1.2/src/agentruntime/mcp/env_merge.py +85 -0
  13. agentruntime_mcp-0.1.2/src/agentruntime/mcp/errors.py +32 -0
  14. agentruntime_mcp-0.1.2/src/agentruntime/mcp/middleware.py +697 -0
  15. agentruntime_mcp-0.1.2/src/agentruntime/mcp/proxy.py +155 -0
  16. agentruntime_mcp-0.1.2/src/agentruntime/mcp/runtime.py +176 -0
  17. {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime/mcp/schemas.py +170 -170
  18. agentruntime_mcp-0.1.2/src/agentruntime/mcp/webhook.py +52 -0
  19. agentruntime_mcp-0.1.2/src/agentruntime_mcp.egg-info/PKG-INFO +128 -0
  20. {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime_mcp.egg-info/SOURCES.txt +11 -2
  21. {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime_mcp.egg-info/requires.txt +4 -1
  22. agentruntime_mcp-0.1.2/tests/test_context.py +22 -0
  23. agentruntime_mcp-0.1.2/tests/test_runtime.py +64 -0
  24. agentruntime_mcp-0.1.2/tests/test_runtime_router.py +32 -0
  25. agentruntime_mcp-0.0.1/PKG-INFO +0 -62
  26. agentruntime_mcp-0.0.1/README.md +0 -45
  27. agentruntime_mcp-0.0.1/src/agentruntime/mcp/__init__.py +0 -4
  28. agentruntime_mcp-0.0.1/src/agentruntime/mcp/generators/openapi_to_mcp.py +0 -48
  29. agentruntime_mcp-0.0.1/src/agentruntime/mcp/middleware.py +0 -207
  30. agentruntime_mcp-0.0.1/src/agentruntime/mcp/proxy.py +0 -76
  31. agentruntime_mcp-0.0.1/src/agentruntime/mcp/runtime.py +0 -47
  32. agentruntime_mcp-0.0.1/src/agentruntime_mcp.egg-info/PKG-INFO +0 -62
  33. {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime_mcp.egg-info/dependency_links.txt +0 -0
  34. {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime_mcp.egg-info/top_level.txt +0 -0
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentruntime-mcp
3
+ Version: 0.1.2
4
+ Summary: AgentRuntime MCP SDK (decorators, runtime, middleware, schemas, proxy)
5
+ Author: AgentRuntime
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: fastmcp>=2.10.0
9
+ Requires-Dist: pydantic>=2
10
+ Requires-Dist: starlette>=0.37
11
+ Requires-Dist: uvicorn[standard]>=0.30
12
+ Requires-Dist: opentelemetry-api>=1.25
13
+ Requires-Dist: opentelemetry-sdk>=1.25
14
+ Requires-Dist: opentelemetry-exporter-otlp>=1.25
15
+ Requires-Dist: PyYAML>=6
16
+ Requires-Dist: httpx>=0.25
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest>=7.0; extra == "test"
19
+
20
+ # AgentRuntime MCP SDK (Python)
21
+
22
+ Opinionated SDK for MCP connectors using **FastMCP** with the same **Control** integration as [`agentruntime-mcp-go`](https://github.com/agentruntime-io/agentruntime-mcp-go): optional `POST /mcp/config`, env merge, and request metadata (run token, instance id).
23
+
24
+ There is **no mandatory legacy standalone token/HMAC middleware chain** in front of every request. Probe-style auth is the **Bearer run token** (or `X-MCP-Token`) and headers AgentRuntime sends; optional `auth.mode` in `config.yaml` is reserved for future templates and does not switch Control gating off.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install agentruntime-mcp
30
+ ```
31
+
32
+ ## Runtimes
33
+
34
+ | Entry | When to use | HTTP |
35
+ |--------|-------------|------|
36
+ | **`run(config_path)`** | Decorator-only `tool()` modules; tools registered from `config.yaml` / `build_schemas` | `/mcp` |
37
+ | **`run_with_registry(config_path, *names)`** | Plugins registered with **`register_adapter`**; one process, one MCP server | `/mcp` |
38
+ | **`run_with_router(config_path)`** | Same registry, **monolith routing** (Go **`RunWithRouter`** / TS **`runWithRouter`** parity) | `/{adapter}/mcp` plus optional **webhook** routes on the **same** ASGI app |
39
+
40
+ Use **`register_adapter`** for anything that should share a process with other adapters. Keep plain **`run()`** for single-connector demos that only use decorators and YAML tools.
41
+
42
+ **Webhooks:** Implement **`register_webhook(self, mux: ServeMux)`** on the adapter. Paths are registered on the **root** Starlette app **before** adapter mounts (same dispatch order as Go/TS). Operators expose vendor callbacks as `https://<host>:<port><path>` (same host/port as MCP).
43
+
44
+ ## Minimal example
45
+
46
+ ```python
47
+ from pydantic import BaseModel, Field
48
+ from agentruntime.mcp.decorators import tool
49
+ from agentruntime.mcp.runtime import run
50
+ from fastmcp import Context
51
+
52
+ class In(BaseModel):
53
+ a: float = Field(...)
54
+ b: float = Field(...)
55
+
56
+ class Out(BaseModel):
57
+ result: float
58
+ expression: str
59
+
60
+ @tool(name="add", input_model=In, output_model=Out)
61
+ def add(a: float, b: float, ctx: Context) -> Out:
62
+ region = ctx.config.region # resolved config when schema + Control are used
63
+ return Out(result=a + b, expression=f"{a} + {b}")
64
+
65
+ if __name__ == "__main__":
66
+ run("config.yaml")
67
+ ```
68
+
69
+ ## Config (`config.yaml`)
70
+
71
+ - **`server`:** `name`, `host`, `port`, `stateless_http`
72
+ - **`tracing`:** enable OpenTelemetry when dependencies are present
73
+ - **`config`:** declarative schema for Control / `GET /mcp/config/schema`
74
+
75
+ Env overrides: **`HOST`**, **`PORT`** (see [`docs/mcp/mcp_env.md`](../../docs/mcp/mcp_env.md)).
76
+
77
+ Example:
78
+
79
+ ```yaml
80
+ server:
81
+ name: "MyConnector"
82
+ host: "127.0.0.1"
83
+ port: 8012
84
+ stateless_http: true
85
+
86
+ tracing:
87
+ enabled: false
88
+
89
+ config:
90
+ apiKey:
91
+ type: string
92
+ displayName: API Key
93
+ required: true
94
+ ```
95
+
96
+ ## Control config resolution
97
+
98
+ When the adapter registers at least one config key:
99
+
100
+ - Set **`MCP_CONTROL_SERVER_URL`** to your control-service base URL.
101
+ - The SDK may call **`POST /mcp/config`** with the run token from the request and your schema.
102
+ - Set **`MCP_CONFIG_FETCH_REQUIRED=false`** for local work without Control (when you still declare schema keys).
103
+
104
+ See [`docs/mcp/mcp_env.md`](../../docs/mcp/mcp_env.md) for the full variable list.
105
+
106
+ ## Proxy (library)
107
+
108
+ ```python
109
+ from agentruntime.mcp.proxy import run_proxy
110
+ run_proxy(
111
+ target_url="http://127.0.0.1:8000/mcp",
112
+ overlay_file="tools.yaml",
113
+ host="127.0.0.1",
114
+ port=8010,
115
+ )
116
+ ```
117
+
118
+ ## TypeScript sibling SDK
119
+
120
+ 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`**.
121
+
122
+ ## Examples in this repo
123
+
124
+ - [`connectors/py-connectors/resend-connector`](../../connectors/py-connectors/resend-connector/) — Python Resend connector (illustrative layout)
125
+
126
+ ## Releasing
127
+
128
+ See [`../RELEASE.md`](../RELEASE.md) (Python section): lightweight Git tags (`v0.1.0`, `v0.1.1`), bump `version` in `pyproject.toml`, do not commit `__pycache__` / `*.egg-info/`.
@@ -0,0 +1,109 @@
1
+ # AgentRuntime MCP SDK (Python)
2
+
3
+ Opinionated SDK for MCP connectors using **FastMCP** with the same **Control** integration as [`agentruntime-mcp-go`](https://github.com/agentruntime-io/agentruntime-mcp-go): optional `POST /mcp/config`, env merge, and request metadata (run token, instance id).
4
+
5
+ There is **no mandatory legacy standalone token/HMAC middleware chain** in front of every request. Probe-style auth is the **Bearer run token** (or `X-MCP-Token`) and headers AgentRuntime sends; optional `auth.mode` in `config.yaml` is reserved for future templates and does not switch Control gating off.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install agentruntime-mcp
11
+ ```
12
+
13
+ ## Runtimes
14
+
15
+ | Entry | When to use | HTTP |
16
+ |--------|-------------|------|
17
+ | **`run(config_path)`** | Decorator-only `tool()` modules; tools registered from `config.yaml` / `build_schemas` | `/mcp` |
18
+ | **`run_with_registry(config_path, *names)`** | Plugins registered with **`register_adapter`**; one process, one MCP server | `/mcp` |
19
+ | **`run_with_router(config_path)`** | Same registry, **monolith routing** (Go **`RunWithRouter`** / TS **`runWithRouter`** parity) | `/{adapter}/mcp` plus optional **webhook** routes on the **same** ASGI app |
20
+
21
+ Use **`register_adapter`** for anything that should share a process with other adapters. Keep plain **`run()`** for single-connector demos that only use decorators and YAML tools.
22
+
23
+ **Webhooks:** Implement **`register_webhook(self, mux: ServeMux)`** on the adapter. Paths are registered on the **root** Starlette app **before** adapter mounts (same dispatch order as Go/TS). Operators expose vendor callbacks as `https://<host>:<port><path>` (same host/port as MCP).
24
+
25
+ ## Minimal example
26
+
27
+ ```python
28
+ from pydantic import BaseModel, Field
29
+ from agentruntime.mcp.decorators import tool
30
+ from agentruntime.mcp.runtime import run
31
+ from fastmcp import Context
32
+
33
+ class In(BaseModel):
34
+ a: float = Field(...)
35
+ b: float = Field(...)
36
+
37
+ class Out(BaseModel):
38
+ result: float
39
+ expression: str
40
+
41
+ @tool(name="add", input_model=In, output_model=Out)
42
+ def add(a: float, b: float, ctx: Context) -> Out:
43
+ region = ctx.config.region # resolved config when schema + Control are used
44
+ return Out(result=a + b, expression=f"{a} + {b}")
45
+
46
+ if __name__ == "__main__":
47
+ run("config.yaml")
48
+ ```
49
+
50
+ ## Config (`config.yaml`)
51
+
52
+ - **`server`:** `name`, `host`, `port`, `stateless_http`
53
+ - **`tracing`:** enable OpenTelemetry when dependencies are present
54
+ - **`config`:** declarative schema for Control / `GET /mcp/config/schema`
55
+
56
+ Env overrides: **`HOST`**, **`PORT`** (see [`docs/mcp/mcp_env.md`](../../docs/mcp/mcp_env.md)).
57
+
58
+ Example:
59
+
60
+ ```yaml
61
+ server:
62
+ name: "MyConnector"
63
+ host: "127.0.0.1"
64
+ port: 8012
65
+ stateless_http: true
66
+
67
+ tracing:
68
+ enabled: false
69
+
70
+ config:
71
+ apiKey:
72
+ type: string
73
+ displayName: API Key
74
+ required: true
75
+ ```
76
+
77
+ ## Control config resolution
78
+
79
+ When the adapter registers at least one config key:
80
+
81
+ - Set **`MCP_CONTROL_SERVER_URL`** to your control-service base URL.
82
+ - The SDK may call **`POST /mcp/config`** with the run token from the request and your schema.
83
+ - Set **`MCP_CONFIG_FETCH_REQUIRED=false`** for local work without Control (when you still declare schema keys).
84
+
85
+ See [`docs/mcp/mcp_env.md`](../../docs/mcp/mcp_env.md) for the full variable list.
86
+
87
+ ## Proxy (library)
88
+
89
+ ```python
90
+ from agentruntime.mcp.proxy import run_proxy
91
+ run_proxy(
92
+ target_url="http://127.0.0.1:8000/mcp",
93
+ overlay_file="tools.yaml",
94
+ host="127.0.0.1",
95
+ port=8010,
96
+ )
97
+ ```
98
+
99
+ ## TypeScript sibling SDK
100
+
101
+ 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`**.
102
+
103
+ ## Examples in this repo
104
+
105
+ - [`connectors/py-connectors/resend-connector`](../../connectors/py-connectors/resend-connector/) — Python Resend connector (illustrative layout)
106
+
107
+ ## Releasing
108
+
109
+ See [`../RELEASE.md`](../RELEASE.md) (Python section): lightweight Git tags (`v0.1.0`, `v0.1.1`), bump `version` in `pyproject.toml`, do not commit `__pycache__` / `*.egg-info/`.
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentruntime-mcp"
7
- version = "0.0.1"
8
- description = "AgentRuntime MCP SDK (decorators, runtime, middleware, schemas, proxy, generators)"
7
+ version = "0.1.2"
8
+ description = "AgentRuntime MCP SDK (decorators, runtime, middleware, schemas, proxy)"
9
9
  requires-python = ">=3.10"
10
10
  readme = "README.md"
11
11
  authors = [{ name = "AgentRuntime" }]
12
12
  dependencies = [
13
- "fastmcp",
13
+ "fastmcp>=2.10.0",
14
14
  "pydantic>=2",
15
15
  "starlette>=0.37",
16
16
  "uvicorn[standard]>=0.30",
@@ -21,7 +21,15 @@ dependencies = [
21
21
  "httpx>=0.25",
22
22
  ]
23
23
 
24
+ [project.optional-dependencies]
25
+ test = ["pytest>=7.0"]
26
+
24
27
  [tool.setuptools]
25
28
  package-dir = {"" = "src"}
26
29
 
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
27
35
 
@@ -1,4 +1,4 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -1,2 +1,2 @@
1
- # Namespace package for AgentRuntime
2
-
1
+ # Namespace package for AgentRuntime
2
+
@@ -0,0 +1,21 @@
1
+ from .decorators import tool, mount_tools # noqa: F401
2
+ from .runtime import run, make_server, load_config, run_with_registry, run_with_router # noqa: F401
3
+ from .schemas import emit_json_shape, emit_flat_shape, build_schemas # noqa: F401
4
+ from .context import get_config, ConfigView # noqa: F401
5
+ from .adapter_registry import register_adapter, list_adapter_names, instantiate_adapters # noqa: F401
6
+ from .adapter_registry import Adapter, WebhookAdapter, ServeMux # noqa: F401
7
+ from .config_schema import ( # noqa: F401
8
+ CONFIG_TYPE_STRING,
9
+ CONFIG_TYPE_NUMBER,
10
+ CONFIG_TYPE_BOOL,
11
+ field,
12
+ string_field,
13
+ opt_required,
14
+ opt_default,
15
+ new_schema_writer,
16
+ write_schema,
17
+ config_schema_has_keys,
18
+ )
19
+ from .errors import ErrAdapterNotFound, ControlError, human_message_from_control_api_body # noqa: F401
20
+ from .webhook import sign_mode_b, deliver_mode_b, ModeBRequest # noqa: F401
21
+
@@ -0,0 +1,87 @@
1
+ """Adapter registry mirroring agentruntime-mcp-go adapter.go."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Any,
9
+ Callable,
10
+ Dict,
11
+ Iterable,
12
+ List,
13
+ Protocol,
14
+ Tuple,
15
+ runtime_checkable,
16
+ )
17
+
18
+ from .errors import ErrAdapterNotFound
19
+
20
+ if TYPE_CHECKING:
21
+ from fastmcp import FastMCP
22
+
23
+ from .config_schema import SchemaWriter
24
+
25
+
26
+ @runtime_checkable
27
+ class Adapter(Protocol):
28
+ def register(self, mcp: "FastMCP", schema: "SchemaWriter") -> None: ...
29
+
30
+
31
+ @runtime_checkable
32
+ class WebhookAdapter(Protocol):
33
+ def register_webhook(self, mux: "ServeMux") -> None: ...
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class AdapterConstructorInput:
38
+ """Reserved for future use (parity with Go AdapterConstructorInput)."""
39
+
40
+
41
+ AdapterConstructor = Callable[[AdapterConstructorInput], Adapter]
42
+
43
+
44
+ class ServeMux:
45
+ """HTTP route table (exact path → handler). Order preserved (first match wins when iterating).
46
+
47
+ Handlers should be Starlette-style ``async def(request: Request) -> Response``.
48
+ """
49
+
50
+ def __init__(self) -> None:
51
+ self._routes: List[Tuple[str, Any]] = []
52
+
53
+ def handle(self, path: str, handler: Any) -> None:
54
+ self._routes.append((normalize_path(path), handler))
55
+
56
+ def __iter__(self) -> Iterable[Tuple[str, Any]]:
57
+ return iter(self._routes)
58
+
59
+
60
+ def normalize_path(path: str) -> str:
61
+ if not path or path == "/":
62
+ return "/"
63
+ return path[:-1] if path.endswith("/") else path
64
+
65
+
66
+ _registry: Dict[str, AdapterConstructor] = {}
67
+
68
+
69
+ def register_adapter(name: str, ctor: AdapterConstructor) -> None:
70
+ _registry[name] = ctor
71
+
72
+
73
+ def list_adapter_names() -> List[str]:
74
+ return sorted(_registry.keys())
75
+
76
+
77
+ def instantiate_adapters(names: List[str]) -> List[Adapter]:
78
+ inp = AdapterConstructorInput()
79
+ if not names:
80
+ return [ctor(inp) for ctor in _registry.values()]
81
+ out: List[Adapter] = []
82
+ for n in names:
83
+ ctor = _registry.get(n)
84
+ if ctor is None:
85
+ raise ErrAdapterNotFound(n)
86
+ out.append(ctor(inp))
87
+ return out
@@ -0,0 +1,43 @@
1
+ """HTTP helpers mirroring agentruntime-mcp-go auth.go."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, Optional
6
+ import urllib.parse
7
+
8
+ NO_CONFIG_METHODS = frozenset({"tools/list", "initialize", "notifications/initialized"})
9
+
10
+
11
+ def extract_token_from_headers(headers: Mapping[str, Any]) -> str:
12
+ auth = headers.get("authorization") or headers.get("Authorization")
13
+ if isinstance(auth, str) and auth.lower().startswith("bearer "):
14
+ return auth[7:].strip()
15
+ xt = headers.get("x-mcp-token") or headers.get("X-MCP-Token")
16
+ if isinstance(xt, str) and xt.strip():
17
+ return xt.strip()
18
+ return ""
19
+
20
+
21
+ def normalize_url_path(url_path: str) -> str:
22
+ p = urllib.parse.urlsplit(url_path).path or ""
23
+ if len(p) > 1 and p.endswith("/"):
24
+ p = p[:-1]
25
+ return p or "/"
26
+
27
+
28
+ def is_schema_endpoint_for_mount(method: str, url_path: str, mount_path: str) -> bool:
29
+ if method.upper() != "GET":
30
+ return False
31
+ path = normalize_url_path(url_path)
32
+ mp = mount_path.rstrip("/") or "/"
33
+ schema_path = normalize_url_path(mp + "/config/schema")
34
+ return path == schema_path
35
+
36
+
37
+ def needs_resolved_config_from_body(data: Optional[Mapping[str, Any]]) -> bool:
38
+ if not data or not isinstance(data, Mapping):
39
+ return True
40
+ m = data.get("method")
41
+ if not isinstance(m, str):
42
+ return True
43
+ return m not in NO_CONFIG_METHODS
@@ -0,0 +1,86 @@
1
+ """Config schema helpers mirroring agentruntime-mcp-go config_schema.go."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable, Dict, List, Mapping, Optional
7
+
8
+ CONFIG_TYPE_STRING = "string"
9
+ CONFIG_TYPE_NUMBER = "number"
10
+ CONFIG_TYPE_BOOL = "boolean"
11
+
12
+
13
+ @dataclass
14
+ class ConfigField:
15
+ type: str
16
+ display_name: str
17
+ required: bool = False
18
+ default: Any = None
19
+
20
+
21
+ @dataclass
22
+ class ConfigFieldDef:
23
+ key: str
24
+ field: ConfigField
25
+
26
+
27
+ ConfigFieldOpt = Callable[[ConfigField], None]
28
+
29
+
30
+ def opt_required() -> ConfigFieldOpt:
31
+ def _apply(f: ConfigField) -> None:
32
+ f.required = True
33
+
34
+ return _apply
35
+
36
+
37
+ def opt_default(v: Any) -> ConfigFieldOpt:
38
+ def _apply(f: ConfigField) -> None:
39
+ f.default = v
40
+
41
+ return _apply
42
+
43
+
44
+ def field(key: str, typ: str, display_name: str, *opts: ConfigFieldOpt) -> ConfigFieldDef:
45
+ cf = ConfigField(type=typ, display_name=display_name)
46
+ for o in opts:
47
+ o(cf)
48
+ return ConfigFieldDef(key=key, field=cf)
49
+
50
+
51
+ def string_field(key: str, display_name: str, *opts: ConfigFieldOpt) -> ConfigFieldDef:
52
+ return field(key, CONFIG_TYPE_STRING, display_name, *opts)
53
+
54
+
55
+ class SchemaWriter:
56
+ """Writes merged Control config_schema entries into a dict."""
57
+
58
+ def __init__(self, into: Dict[str, Any]) -> None:
59
+ self._into = into
60
+
61
+ def add(self, key: str, f: ConfigField) -> None:
62
+ m: Dict[str, Any] = {
63
+ "type": f.type,
64
+ "displayName": f.display_name,
65
+ "required": f.required,
66
+ }
67
+ if f.default is not None:
68
+ m["default"] = f.default
69
+ self._into[key] = m
70
+
71
+
72
+ def new_schema_writer(into: Optional[Dict[str, Any]] = None) -> SchemaWriter:
73
+ target = into if into is not None else {}
74
+ return SchemaWriter(target)
75
+
76
+
77
+ def write_schema(sw: Optional[SchemaWriter], prefix: str, defs: List[ConfigFieldDef]) -> None:
78
+ if sw is None:
79
+ return
80
+ for d in defs:
81
+ sw.add(prefix + d.key, d.field)
82
+
83
+
84
+ def config_schema_has_keys(schema: Mapping[str, Any]) -> bool:
85
+ return len(schema) > 0
86
+
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from contextvars import ContextVar, Token
4
+ from typing import Any, Dict, Iterable, Mapping, Optional
5
+
6
+
7
+ _current_token: ContextVar[Optional[str]] = ContextVar("agentruntime_mcp_auth_token", default=None)
8
+ _current_config: ContextVar["ConfigView"] = ContextVar("agentruntime_mcp_resolved_config", default=None) # type: ignore[assignment]
9
+
10
+
11
+ class ConfigView(dict):
12
+ """Dictionary with attribute access for config values."""
13
+
14
+ def __init__(self, data: Optional[Mapping[str, Any]] = None) -> None:
15
+ super().__init__()
16
+ if data:
17
+ for k, v in data.items():
18
+ self[k] = _to_config_value(v)
19
+
20
+ def __getattr__(self, item: str) -> Any:
21
+ try:
22
+ return self[item]
23
+ except KeyError as exc:
24
+ raise AttributeError(item) from exc
25
+
26
+ def __setattr__(self, key: str, value: Any) -> None:
27
+ self[key] = _to_config_value(value)
28
+
29
+ def copy(self) -> "ConfigView":
30
+ return ConfigView(self)
31
+
32
+
33
+ def _to_config_value(value: Any) -> Any:
34
+ if isinstance(value, ConfigView):
35
+ return value
36
+ if isinstance(value, Mapping):
37
+ return ConfigView(value)
38
+ if isinstance(value, list):
39
+ return [_to_config_value(v) for v in value]
40
+ return value
41
+
42
+
43
+ def set_request_context(token: Optional[str], config: Optional[Mapping[str, Any]]) -> tuple[Token, Token]:
44
+ cfg = ConfigView(config or {})
45
+ t1 = _current_token.set(token)
46
+ t2 = _current_config.set(cfg)
47
+ return t1, t2
48
+
49
+
50
+ def reset_request_context(tokens: Iterable[Token]) -> None:
51
+ t = list(tokens)
52
+ if len(t) >= 1:
53
+ _current_token.reset(t[0])
54
+ if len(t) >= 2:
55
+ _current_config.reset(t[1])
56
+
57
+
58
+ def get_auth_token() -> Optional[str]:
59
+ return _current_token.get()
60
+
61
+
62
+ def get_config() -> ConfigView:
63
+ cfg = _current_config.get()
64
+ if cfg is None:
65
+ return ConfigView({})
66
+ return cfg
67
+
68
+
69
+ def attach_config_to_ctx(ctx: Any, config: Optional[Mapping[str, Any]] = None) -> None:
70
+ if ctx is None:
71
+ return
72
+ cfg = ConfigView(config or get_config())
73
+ try:
74
+ setattr(ctx, "config", cfg)
75
+ except Exception:
76
+ pass