Skip to content

Commit d789a72

Browse files
feat: add dedicated gateway plugin creation APIs (#197)
2 parents 7bfaa4d + a99953d commit d789a72

6 files changed

Lines changed: 383 additions & 0 deletions

File tree

‎agentkit/sdk/gateway/__init__.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from .client import AgentkitGatewayClient
16+
from .types import (
17+
CreateGatewayPluginBindingRequest,
18+
CreateGatewayPluginBindingResponse,
19+
CreateGatewayPluginRequest,
20+
CreateGatewayPluginResponse,
21+
)
22+
23+
__all__ = [
24+
"AgentkitGatewayClient",
25+
"CreateGatewayPluginBindingRequest",
26+
"CreateGatewayPluginBindingResponse",
27+
"CreateGatewayPluginRequest",
28+
"CreateGatewayPluginResponse",
29+
]

‎agentkit/sdk/gateway/client.py‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import ClassVar
18+
19+
from agentkit.client import BaseAgentkitClient
20+
21+
from .types import (
22+
CreateGatewayPluginBindingRequest,
23+
CreateGatewayPluginBindingResponse,
24+
CreateGatewayPluginRequest,
25+
CreateGatewayPluginResponse,
26+
)
27+
28+
29+
class AgentkitGatewayClient(BaseAgentkitClient):
30+
"""Manage plugins on dedicated AgentKit gateways and bind them to MCP services."""
31+
32+
API_ACTIONS: ClassVar[dict[str, str]] = {
33+
"CreateGatewayPlugin": "CreateGatewayPlugin",
34+
"CreateGatewayPluginBinding": "CreateGatewayPluginBinding",
35+
}
36+
37+
def __init__(
38+
self,
39+
access_key: str = "",
40+
secret_key: str = "",
41+
region: str = "",
42+
session_token: str = "",
43+
) -> None:
44+
super().__init__(
45+
access_key=access_key,
46+
secret_key=secret_key,
47+
region=region,
48+
session_token=session_token,
49+
service_name="gateway",
50+
)
51+
52+
def create_gateway_plugin(
53+
self, request: CreateGatewayPluginRequest
54+
) -> CreateGatewayPluginResponse:
55+
"""Install a plugin; create a binding separately to apply it to requests."""
56+
return self._invoke_api(
57+
api_action="CreateGatewayPlugin",
58+
request=request,
59+
response_type=CreateGatewayPluginResponse,
60+
)
61+
62+
def create_gateway_plugin_binding(
63+
self, request: CreateGatewayPluginBindingRequest
64+
) -> CreateGatewayPluginBindingResponse:
65+
"""Bind an installed gateway plugin to a target MCP service."""
66+
return self._invoke_api(
67+
api_action="CreateGatewayPluginBinding",
68+
request=request,
69+
response_type=CreateGatewayPluginBindingResponse,
70+
)

‎agentkit/sdk/gateway/types.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from pydantic import BaseModel, Field
18+
19+
20+
class GatewayBaseModel(BaseModel):
21+
model_config = {"populate_by_name": True}
22+
23+
24+
class CreateGatewayPluginRequest(GatewayBaseModel):
25+
gateway_instance_id: str = Field(..., alias="GatewayInstanceId")
26+
gateway_plugin_name: str = Field(..., alias="GatewayPluginName")
27+
gateway_plugin_config: str = Field(..., alias="GatewayPluginConfig")
28+
29+
30+
class CreateGatewayPluginResponse(GatewayBaseModel):
31+
gateway_plugin_id: str | None = Field(default=None, alias="GatewayPluginId")
32+
33+
34+
class CreateGatewayPluginBindingRequest(GatewayBaseModel):
35+
gateway_plugin_id: str = Field(..., alias="GatewayPluginId")
36+
target_id: str = Field(..., alias="TargetId")
37+
gateway_plugin_binding_config: str = Field(..., alias="GatewayPluginBindingConfig")
38+
enable: bool | None = Field(default=None, alias="Enable")
39+
comments: str | None = Field(default=None, alias="Comments")
40+
41+
42+
class CreateGatewayPluginBindingResponse(GatewayBaseModel):
43+
gateway_plugin_binding_id: str | None = Field(
44+
default=None, alias="GatewayPluginBindingId"
45+
)

‎docs/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
本目录包含 AgentKit SDK 和 CLI 的完整文档。
44

5+
专属网关插件的 SDK 调用示例见[专属网关插件](./gateway_plugins.md)。
6+
57
## 本地开发
68

79
### 安装依赖

‎docs/gateway_plugins.md‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# 专属网关插件
2+
3+
`AgentkitGatewayClient` 支持 AgentKit 的 `CreateGatewayPlugin` 和
4+
`CreateGatewayPluginBinding` OpenAPI,版本为 `2025-10-30`。
5+
客户端复用 SDK 现有的凭据、区域、服务地址和请求签名配置。
6+
7+
## 安装 Identity 鉴权插件并绑定 MCP 服务
8+
9+
调用前,需要准备专属 MCP 网关及该网关下处于运行中的 MCP 服务。
10+
还需在 Identity 中创建权限空间,并为调用方身份配置访问规则。
11+
12+
```python
13+
import json
14+
15+
from agentkit.sdk.gateway import (
16+
AgentkitGatewayClient,
17+
CreateGatewayPluginBindingRequest,
18+
CreateGatewayPluginRequest,
19+
)
20+
21+
# Credentials are resolved through the SDK's existing credential chain.
22+
client = AgentkitGatewayClient(region="cn-beijing")
23+
24+
plugin = client.create_gateway_plugin(
25+
CreateGatewayPluginRequest(
26+
gateway_instance_id="<gateway-instance-id>",
27+
gateway_plugin_name="IdentityAuthorization",
28+
gateway_plugin_config="{}",
29+
)
30+
)
31+
if not plugin.gateway_plugin_id:
32+
raise RuntimeError("CreateGatewayPlugin returned no GatewayPluginId")
33+
34+
binding = client.create_gateway_plugin_binding(
35+
CreateGatewayPluginBindingRequest(
36+
gateway_plugin_id=plugin.gateway_plugin_id,
37+
target_id="<mcp-service-id>",
38+
gateway_plugin_binding_config=json.dumps(
39+
{
40+
"Namespace": "default",
41+
"TokenPosition": {
42+
"Header": "x-ve-tip-token",
43+
"Prefix": "",
44+
},
45+
}
46+
),
47+
enable=True,
48+
comments="Identity authorization for this MCP service",
49+
)
50+
)
51+
print(binding.gateway_plugin_binding_id)
52+
```
53+
54+
安装插件后,需要启用绑定规则,鉴权才会对目标 MCP 服务生效。
55+
`Namespace` 填写 Identity 权限空间名称,与 OAuth 用户池 ID 无关。
56+
示例从 `X-Ve-TIP-Token` 请求头读取 TIP Token,鉴权失败时拒绝请求。
57+
MCP 服务原有的入站认证仍然生效。
58+
59+
## 请求字段
60+
61+
两个配置字段都必填,类型为**字符串**。配置对象需要先用 `json.dumps` 序列化,
62+
再传给 SDK。客户端原样发送配置字符串,具体配置内容由对应插件处理。
63+
64+
| 请求模型 | 必填字段 | 可选字段 | 响应字段 |
65+
| --- | --- | --- | --- |
66+
| `CreateGatewayPluginRequest` | `gateway_instance_id`、`gateway_plugin_name`、`gateway_plugin_config` | 无 | `gateway_plugin_id` |
67+
| `CreateGatewayPluginBindingRequest` | `gateway_plugin_id`、`target_id`、`gateway_plugin_binding_config` | `enable`、`comments` | `gateway_plugin_binding_id` |
68+
69+
模型也接受 `GatewayInstanceId`、`TargetId` 等 OpenAPI 字段名。
70+
未设置的可选字段不会发送;显式设置的 `enable=False` 会保留。
71+
72+
这两个接口执行创建操作。插件或绑定已存在时,服务可能返回 `DuplicatedResource`,
73+
SDK 会抛出包含该错误码的 `ApiError`。为同一插件添加其他绑定时,复用已有插件 ID。
74+
客户端不会自动查询、更新或删除资源。
75+
76+
控制台配置方式和权限策略前提见[专属网关插件指南](https://docs.volcengine.com/docs/agentkit/dedicated-gateway-plugin-configuration-description)。
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
17+
import pytest
18+
from pydantic import ValidationError
19+
20+
from agentkit.platform.context import default_cloud_provider
21+
from agentkit.sdk.gateway import (
22+
AgentkitGatewayClient,
23+
CreateGatewayPluginBindingRequest,
24+
CreateGatewayPluginBindingResponse,
25+
CreateGatewayPluginRequest,
26+
CreateGatewayPluginResponse,
27+
)
28+
from agentkit.toolkit.errors import ApiError
29+
30+
31+
@pytest.fixture
32+
def client():
33+
with default_cloud_provider("volcengine"):
34+
return AgentkitGatewayClient(
35+
access_key="AK_LOCAL_TEST_ONLY",
36+
secret_key="SK_LOCAL_TEST_ONLY",
37+
region="cn-beijing",
38+
session_token="TOKEN_LOCAL_TEST_ONLY",
39+
)
40+
41+
42+
def test_create_plugin_sends_agentkit_request_and_parses_id(client, monkeypatch):
43+
def fake_json(api, params, body):
44+
assert api == "CreateGatewayPlugin"
45+
assert params == {}
46+
assert json.loads(body) == {
47+
"GatewayInstanceId": "g-test",
48+
"GatewayPluginName": "IdentityAuthorization",
49+
"GatewayPluginConfig": "{}",
50+
}
51+
return json.dumps({"Result": {"GatewayPluginId": "gpl-test"}})
52+
53+
monkeypatch.setattr(client, "json", fake_json)
54+
assert client.service == "agentkit"
55+
assert client.session_token == "TOKEN_LOCAL_TEST_ONLY"
56+
for action, info in client.api_info.items():
57+
assert info.method == "POST"
58+
assert info.path == "/"
59+
assert info.query == {"Action": action, "Version": "2025-10-30"}
60+
61+
response = client.create_gateway_plugin(
62+
CreateGatewayPluginRequest(
63+
gateway_instance_id="g-test",
64+
gateway_plugin_name="IdentityAuthorization",
65+
gateway_plugin_config="{}",
66+
)
67+
)
68+
assert isinstance(response, CreateGatewayPluginResponse)
69+
assert response.gateway_plugin_id == "gpl-test"
70+
71+
72+
@pytest.mark.parametrize("enable", [None, False, True])
73+
def test_create_binding_preserves_config_and_optional_values(
74+
client, monkeypatch, enable
75+
):
76+
config = json.dumps(
77+
{
78+
"Namespace": "default",
79+
"TokenPosition": {"Header": "x-ve-tip-token", "Prefix": ""},
80+
}
81+
)
82+
expected = {
83+
"GatewayPluginId": "gpl-test",
84+
"TargetId": "m-test",
85+
"GatewayPluginBindingConfig": config,
86+
}
87+
if enable is not None:
88+
expected.update(Enable=enable, Comments="")
89+
90+
def fake_json(api, params, body):
91+
assert api == "CreateGatewayPluginBinding"
92+
assert params == {}
93+
assert json.loads(body) == expected
94+
return json.dumps({"Result": {"GatewayPluginBindingId": "gpb-test"}})
95+
96+
monkeypatch.setattr(client, "json", fake_json)
97+
response = client.create_gateway_plugin_binding(
98+
CreateGatewayPluginBindingRequest(
99+
GatewayPluginId="gpl-test",
100+
TargetId="m-test",
101+
GatewayPluginBindingConfig=config,
102+
Enable=enable,
103+
Comments="" if enable is not None else None,
104+
)
105+
)
106+
assert isinstance(response, CreateGatewayPluginBindingResponse)
107+
assert response.gateway_plugin_binding_id == "gpb-test"
108+
109+
110+
@pytest.mark.parametrize(
111+
"model,values,config_field",
112+
[
113+
(
114+
CreateGatewayPluginRequest,
115+
{
116+
"gateway_instance_id": "g-test",
117+
"gateway_plugin_name": "IdentityAuthorization",
118+
},
119+
"gateway_plugin_config",
120+
),
121+
(
122+
CreateGatewayPluginBindingRequest,
123+
{"gateway_plugin_id": "gpl-test", "target_id": "m-test"},
124+
"gateway_plugin_binding_config",
125+
),
126+
],
127+
)
128+
def test_config_is_required_and_must_be_a_string(model, values, config_field):
129+
with pytest.raises(ValidationError):
130+
model(**values)
131+
with pytest.raises(ValidationError):
132+
model(**values, **{config_field: {"Namespace": "default"}})
133+
134+
135+
def test_duplicate_binding_error_is_preserved(client, monkeypatch):
136+
calls = []
137+
138+
def fake_json(api, params, body):
139+
calls.append(api)
140+
return json.dumps(
141+
{
142+
"ResponseMetadata": {
143+
"Error": {
144+
"Code": "DuplicatedResource",
145+
"Message": "The specified resource already exists.",
146+
}
147+
}
148+
}
149+
)
150+
151+
monkeypatch.setattr(client, "json", fake_json)
152+
with pytest.raises(ApiError) as error:
153+
client.create_gateway_plugin_binding(
154+
CreateGatewayPluginBindingRequest(
155+
gateway_plugin_id="gpl-test",
156+
target_id="m-test",
157+
gateway_plugin_binding_config="{}",
158+
)
159+
)
160+
assert error.value.error_code == "DuplicatedResource"
161+
assert calls == ["CreateGatewayPluginBinding"]

0 commit comments

Comments
 (0)