agentruntime-mcp 0.0.1__tar.gz → 0.2.0__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.2.0/PKG-INFO +128 -0
- agentruntime_mcp-0.2.0/README.md +109 -0
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.2.0}/pyproject.toml +11 -3
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.2.0}/setup.cfg +4 -4
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.2.0}/src/agentruntime/__init__.py +2 -2
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/__init__.py +30 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/adapter_registry.py +87 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/auth_http.py +43 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/bridge.py +164 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/bridge_auth.py +109 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/config_schema.py +86 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/context.py +76 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/control.py +124 -0
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.2.0}/src/agentruntime/mcp/decorators.py +106 -74
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/env_merge.py +85 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/errors.py +32 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/middleware.py +697 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/proxy.py +155 -0
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/runtime.py +178 -0
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.2.0}/src/agentruntime/mcp/schemas.py +170 -170
- agentruntime_mcp-0.2.0/src/agentruntime/mcp/webhook.py +52 -0
- agentruntime_mcp-0.2.0/src/agentruntime_mcp.egg-info/PKG-INFO +128 -0
- agentruntime_mcp-0.2.0/src/agentruntime_mcp.egg-info/SOURCES.txt +28 -0
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.2.0}/src/agentruntime_mcp.egg-info/requires.txt +4 -1
- agentruntime_mcp-0.2.0/tests/test_bridge_auth.py +63 -0
- agentruntime_mcp-0.2.0/tests/test_context.py +22 -0
- agentruntime_mcp-0.2.0/tests/test_runtime.py +64 -0
- agentruntime_mcp-0.2.0/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/src/agentruntime_mcp.egg-info/SOURCES.txt +0 -15
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.2.0}/src/agentruntime_mcp.egg-info/dependency_links.txt +0 -0
- {agentruntime_mcp-0.0.1 → agentruntime_mcp-0.2.0}/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.2.0
|
|
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
|
|
8
|
-
description = "AgentRuntime MCP SDK (decorators, runtime, middleware, schemas, proxy
|
|
7
|
+
version = "0.2.0"
|
|
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,30 @@
|
|
|
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 .bridge import BRIDGE_MOUNT_PATH # noqa: F401
|
|
21
|
+
from .bridge_auth import apply_auth_mapping, apply_header_mappings, apply_bridge_headers # noqa: F401
|
|
22
|
+
from .control import ( # noqa: F401
|
|
23
|
+
HEADER_MCP_INSTANCE_ID,
|
|
24
|
+
HEADER_MCP_SERVER_ID,
|
|
25
|
+
ControlPayload,
|
|
26
|
+
fetch_control_payload,
|
|
27
|
+
build_runtime_context_from_request,
|
|
28
|
+
)
|
|
29
|
+
from .webhook import sign_mode_b, deliver_mode_b, ModeBRequest # noqa: F401
|
|
30
|
+
|
|
@@ -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,164 @@
|
|
|
1
|
+
"""Generic MCP bridge route — mirror agentruntime-mcp-go bridge.go."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Dict, Mapping, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from starlette.requests import Request
|
|
11
|
+
from starlette.responses import JSONResponse, PlainTextResponse, Response
|
|
12
|
+
|
|
13
|
+
from .auth_http import extract_token_from_headers, is_schema_endpoint_for_mount
|
|
14
|
+
from .bridge_auth import apply_bridge_headers
|
|
15
|
+
from .control import build_runtime_context_from_request, fetch_control_payload
|
|
16
|
+
from .errors import ControlError, human_message_from_control_api_body
|
|
17
|
+
|
|
18
|
+
BRIDGE_MOUNT_PATH = "/bridge/mcp"
|
|
19
|
+
_LOG = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _bridge_control_err(err: BaseException) -> str:
|
|
23
|
+
if isinstance(err, ControlError):
|
|
24
|
+
hm = human_message_from_control_api_body(err.body)
|
|
25
|
+
if hm:
|
|
26
|
+
return hm
|
|
27
|
+
return str(err)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _bridge_upstream_from_payload(bridge: Optional[Mapping[str, Any]]) -> str:
|
|
31
|
+
if not bridge:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"server is not configured for bridge mode (missing metadata.bridge upstream_url)"
|
|
34
|
+
)
|
|
35
|
+
upstream = str(bridge.get("upstream_url", "")).strip()
|
|
36
|
+
if not upstream:
|
|
37
|
+
raise ValueError("bridge upstream_url is not set on mcp_servers.metadata")
|
|
38
|
+
return upstream
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _tool_name_from_params(data: Mapping[str, Any]) -> Optional[str]:
|
|
42
|
+
params = data.get("params")
|
|
43
|
+
if not isinstance(params, dict):
|
|
44
|
+
return None
|
|
45
|
+
name = str(params.get("name", "")).strip()
|
|
46
|
+
return name or None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _json_rpc_response(
|
|
50
|
+
req_id: Any,
|
|
51
|
+
result: Optional[Dict[str, Any]] = None,
|
|
52
|
+
rpc_err: Optional[Dict[str, Any]] = None,
|
|
53
|
+
) -> JSONResponse:
|
|
54
|
+
body: Dict[str, Any] = {"jsonrpc": "2.0", "id": req_id}
|
|
55
|
+
if rpc_err is not None:
|
|
56
|
+
body["error"] = rpc_err
|
|
57
|
+
else:
|
|
58
|
+
body["result"] = result or {}
|
|
59
|
+
return JSONResponse(body)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _json_rpc_err(code: int, message: str) -> Dict[str, Any]:
|
|
63
|
+
return {"code": code, "message": message}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def _bridge_post(
|
|
67
|
+
target_url: str,
|
|
68
|
+
payload: Mapping[str, Any],
|
|
69
|
+
extra_headers: Mapping[str, str],
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
if not target_url.strip():
|
|
72
|
+
raise ValueError("proxy target not configured")
|
|
73
|
+
headers = {
|
|
74
|
+
"Accept": "application/json, text/event-stream",
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
**dict(extra_headers),
|
|
77
|
+
}
|
|
78
|
+
async with httpx.AsyncClient() as client:
|
|
79
|
+
resp = await client.post(target_url, json=dict(payload), headers=headers)
|
|
80
|
+
if resp.status_code != 200:
|
|
81
|
+
raise ValueError(f"proxy target not configured: target returned {resp.status_code}: {resp.text}")
|
|
82
|
+
try:
|
|
83
|
+
parsed = resp.json()
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
raise ValueError(f"proxy target not configured: {exc}") from exc
|
|
86
|
+
if not isinstance(parsed, dict):
|
|
87
|
+
raise ValueError("proxy target not configured: invalid JSON object")
|
|
88
|
+
return parsed
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def bridge_http_endpoint(request: Request) -> Response:
|
|
92
|
+
mount = BRIDGE_MOUNT_PATH
|
|
93
|
+
path = request.url.path
|
|
94
|
+
|
|
95
|
+
if is_schema_endpoint_for_mount(request.method, path, mount):
|
|
96
|
+
token = extract_token_from_headers(request.headers)
|
|
97
|
+
ctx = build_runtime_context_from_request(request)
|
|
98
|
+
try:
|
|
99
|
+
payload = fetch_control_payload(token, {}, ctx)
|
|
100
|
+
return JSONResponse(payload.config_schema or {})
|
|
101
|
+
except Exception as err:
|
|
102
|
+
return PlainTextResponse(_bridge_control_err(err), status_code=502)
|
|
103
|
+
|
|
104
|
+
if request.method.upper() != "POST":
|
|
105
|
+
return PlainTextResponse("Method Not Allowed", status_code=405)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
data = await request.json()
|
|
109
|
+
except Exception:
|
|
110
|
+
return _json_rpc_response(None, rpc_err=_json_rpc_err(-32700, "Parse error"))
|
|
111
|
+
if not isinstance(data, dict):
|
|
112
|
+
return _json_rpc_response(None, rpc_err=_json_rpc_err(-32700, "Parse error"))
|
|
113
|
+
|
|
114
|
+
req_id = data.get("id")
|
|
115
|
+
method = data.get("method") if isinstance(data.get("method"), str) else ""
|
|
116
|
+
|
|
117
|
+
ctx = build_runtime_context_from_request(request)
|
|
118
|
+
if method == "tools/call":
|
|
119
|
+
tn = _tool_name_from_params(data)
|
|
120
|
+
if tn:
|
|
121
|
+
ctx["tool_name"] = tn
|
|
122
|
+
|
|
123
|
+
token = extract_token_from_headers(request.headers)
|
|
124
|
+
try:
|
|
125
|
+
control_payload = fetch_control_payload(token, {}, ctx)
|
|
126
|
+
except Exception as err:
|
|
127
|
+
return _json_rpc_response(req_id, rpc_err=_json_rpc_err(-32603, _bridge_control_err(err)))
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
upstream = _bridge_upstream_from_payload(control_payload.bridge)
|
|
131
|
+
except Exception as err:
|
|
132
|
+
return _json_rpc_response(req_id, rpc_err=_json_rpc_err(-32603, str(err)))
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
auth_hdr = apply_bridge_headers(control_payload.config, control_payload.bridge)
|
|
136
|
+
except Exception as err:
|
|
137
|
+
return _json_rpc_response(req_id, rpc_err=_json_rpc_err(-32603, f"upstream auth: {err}"))
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
proxied = await _bridge_post(upstream, data, auth_hdr)
|
|
141
|
+
except Exception as err:
|
|
142
|
+
return _json_rpc_response(req_id, rpc_err=_json_rpc_err(-32603, str(err)))
|
|
143
|
+
|
|
144
|
+
if method in ("tools/list", "tools/call"):
|
|
145
|
+
result = proxied.get("result")
|
|
146
|
+
if not isinstance(result, dict):
|
|
147
|
+
result = {}
|
|
148
|
+
return _json_rpc_response(req_id, result=result)
|
|
149
|
+
return JSONResponse(proxied)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def bridge_routes() -> list[Any]:
|
|
153
|
+
"""Starlette routes for the generic bridge mount."""
|
|
154
|
+
from starlette.routing import Route
|
|
155
|
+
|
|
156
|
+
return [
|
|
157
|
+
Route(BRIDGE_MOUNT_PATH, endpoint=bridge_http_endpoint, methods=["GET", "POST"]),
|
|
158
|
+
Route(f"{BRIDGE_MOUNT_PATH}/", endpoint=bridge_http_endpoint, methods=["GET", "POST"]),
|
|
159
|
+
Route(
|
|
160
|
+
f"{BRIDGE_MOUNT_PATH}/config/schema",
|
|
161
|
+
endpoint=bridge_http_endpoint,
|
|
162
|
+
methods=["GET"],
|
|
163
|
+
),
|
|
164
|
+
]
|