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.
- agentruntime_mcp-0.1.2/PKG-INFO +128 -0
- agentruntime_mcp-0.1.2/README.md +109 -0
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/pyproject.toml +11 -3
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/setup.cfg +4 -4
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime/__init__.py +2 -2
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/__init__.py +21 -0
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/adapter_registry.py +87 -0
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/auth_http.py +43 -0
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/config_schema.py +86 -0
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/context.py +76 -0
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime/mcp/decorators.py +106 -74
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/env_merge.py +85 -0
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/errors.py +32 -0
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/middleware.py +697 -0
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/proxy.py +155 -0
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/runtime.py +176 -0
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime/mcp/schemas.py +170 -170
- agentruntime_mcp-0.1.2/src/agentruntime/mcp/webhook.py +52 -0
- agentruntime_mcp-0.1.2/src/agentruntime_mcp.egg-info/PKG-INFO +128 -0
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime_mcp.egg-info/SOURCES.txt +11 -2
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime_mcp.egg-info/requires.txt +4 -1
- agentruntime_mcp-0.1.2/tests/test_context.py +22 -0
- agentruntime_mcp-0.1.2/tests/test_runtime.py +64 -0
- agentruntime_mcp-0.1.2/tests/test_runtime_router.py +32 -0
- agentruntime_mcp-0.0.1/PKG-INFO +0 -62
- agentruntime_mcp-0.0.1/README.md +0 -45
- agentruntime_mcp-0.0.1/src/agentruntime/mcp/__init__.py +0 -4
- agentruntime_mcp-0.0.1/src/agentruntime/mcp/generators/openapi_to_mcp.py +0 -48
- agentruntime_mcp-0.0.1/src/agentruntime/mcp/middleware.py +0 -207
- agentruntime_mcp-0.0.1/src/agentruntime/mcp/proxy.py +0 -76
- agentruntime_mcp-0.0.1/src/agentruntime/mcp/runtime.py +0 -47
- agentruntime_mcp-0.0.1/src/agentruntime_mcp.egg-info/PKG-INFO +0 -62
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.1.2}/src/agentruntime_mcp.egg-info/dependency_links.txt +0 -0
- {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.
|
|
8
|
-
description = "AgentRuntime MCP SDK (decorators, runtime, middleware, schemas, proxy
|
|
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
|