agentkit-client 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgentKit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentkit-client
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency reference client SDK for the AgentKit gateway (discover / quote / call / MCP tools.list)
5
+ Author: AgentKit
6
+ License-Expression: MIT
7
+ Keywords: agentkit,mcp,ai-agent,sdk,reference-client
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # agentkit-client — AgentKit Python Reference SDK
14
+
15
+ 零第三方依赖(仅使用标准库 `urllib` / `json` / `os`)的 AgentKit 网关参考客户端,
16
+ 封装 **discover → quote → call** 流程,并提供只读能力索引与 MCP `tools/list` 入口。
17
+
18
+ - 线上网关:<https://agentkit.ltd>
19
+ - 数据采集时间:2026-10-17(端点事实来自公网实测)
20
+ - 许可证:MIT
21
+ - 要求:Python 3.9+
22
+
23
+ > Open-core 边界:本 SDK 只封装公开的集成协议(检索、报价、调用、MCP 清单),
24
+ > 不包含、也不暴露任何网关内部实现。
25
+
26
+ ## 安装
27
+
28
+ 无第三方依赖,无需安装即可从源码目录直接运行;也可将 `agentkit/` 包复制进项目。
29
+
30
+ ```bash
31
+ # 从源码目录使用(pip 不需要安装任何依赖)
32
+ python3 examples/quote.py
33
+ ```
34
+
35
+ ## 快速开始
36
+
37
+ ```python
38
+ import os
39
+ from agentkit import AgentKit
40
+
41
+ client = AgentKit(
42
+ base_url="https://agentkit.ltd",
43
+ api_key=os.environ.get("AGENTKIT_API_KEY"), # 仅 call() 需要
44
+ )
45
+
46
+ # 只读:能力索引
47
+ idx = client.list_skills()
48
+
49
+ # 只读:自然语言检索(当前线上可能返回空结果,SDK 优雅降级)
50
+ found = client.discover("json")
51
+
52
+ # 只读:报价 + 输入 schema
53
+ q = client.quote("jsonfmt")
54
+
55
+ # 执行(需要 API key,会发起真实计费调用)
56
+ res = client.call("jsonfmt", {"text": '{"a": 1}'})
57
+ ```
58
+
59
+ API key 未在构造函数传入时,客户端自动读取环境变量 `AGENTKIT_API_KEY`。
60
+
61
+ ## API
62
+
63
+ | 方法 | 端点 | 鉴权 | 说明 |
64
+ |---|---|---|---|
65
+ | `list_skills()` | `GET /api/skills.json` | 无 | 能力索引,返回 `{ meta, hostedCount, catalogCount, libraryCount, count, skills[] }` |
66
+ | `discover(text=None, json=None)` | `GET /api/discover?text=...` | 无 | 自然语言检索;返回规范化的 `{ query, count, results[] }`,空结果不报错 |
67
+ | `quote(tool)` | `GET /api/quote?tool=<id>` | 无 | 报价与输入 JSON Schema,返回 `{ tool, name, priceUsd, currency, schema, errorContract, freeTierPerIp }` |
68
+ | `call(tool, arguments=None)` | `POST /api/call` | 需要 key | 执行工具;请求体严格为 `{ "tool": id, "arguments": {...} }` |
69
+ | `mcp_list_tools()` | `POST /mcp`(initialize → tools/list) | 无 | MCP Streamable HTTP 握手后返回 tools 数组 |
70
+
71
+ ### 构造参数
72
+
73
+ ```python
74
+ client = AgentKit(
75
+ base_url="https://agentkit.ltd", # 默认值,可覆盖
76
+ api_key=os.environ.get("AGENTKIT_API_KEY"), # 默认读环境变量
77
+ auth_header=lambda key: f"Bearer {key}", # 可选,覆盖鉴权头值
78
+ mcp_client_info={"name": "my-agent", "version": "1.0.0"}, # 可选
79
+ timeout=30.0,
80
+ )
81
+ ```
82
+
83
+ **鉴权头约定(待确认)**:`call()` 默认按 `Authorization: Bearer <key>` 发送,
84
+ 这是 2026-10-17 时的默认实现,待与线上网关最终确认;如需改用其它头格式,
85
+ 通过构造参数 `auth_header`(字符串或 `api_key -> str` 可调用对象)覆盖。
86
+
87
+ 调用前若没有任何 key,客户端在本地直接抛出 `MissingApiKeyError`,**不会发出请求**。
88
+
89
+ ## 错误处理
90
+
91
+ 所有非 2xx 响应被规范化为 `AgentKitError`,属性:
92
+
93
+ | 属性 | 类型 | 说明 |
94
+ |---|---|---|
95
+ | `http_status` | int | HTTP 状态码;本地错误为 `0` |
96
+ | `error_code` | str | 响应体 `error` 字段(如 `INVALID_KEY`) |
97
+ | `retryable` | bool | 响应体 `retryable` 字段,指示是否可重试 |
98
+ | `detail` | str | 响应体 `detail` 字段 |
99
+
100
+ 公开错误码:`INVALID_KEY`、`INVALID_QUERY`、`INTERNAL_ERROR`、`UNKNOWN_TOOL`、
101
+ `ANONYMOUS_QUOTA_EXCEEDED`、`INSUFFICIENT_BALANCE`、`GUARDRAIL_BLOCKED`。
102
+
103
+ ```python
104
+ from agentkit import AgentKit, AgentKitError
105
+
106
+ try:
107
+ client.call("jsonfmt", {"text": "{}"})
108
+ except AgentKitError as err:
109
+ print(err.error_code, err.http_status, err.retryable, err.detail)
110
+ ```
111
+
112
+ `MissingApiKeyError` 是 `AgentKitError` 的子类(`error_code: 'MISSING_API_KEY'`,本地抛出)。
113
+
114
+ ## MCP 说明
115
+
116
+ `mcp_list_tools()` 对 `POST /mcp` 依次发送:
117
+
118
+ 1. JSON-RPC `initialize`(`id=1`,`protocolVersion: "2025-03-26"`,`clientInfo` 可配置);
119
+ 2. JSON-RPC `tools/list`(`id=2`)。
120
+
121
+ 请求头固定为 `Content-Type: application/json`、`Accept: application/json, text/event-stream`;
122
+ 响应同时兼容 JSON 与 SSE(`data:` 行)两种格式。当前网关只暴露 tools 能力,
123
+ 返回 13 个 hosted 工具,每个含 `name`、`description`、`inputSchema`(与 `quote` 的 `schema` 一致)。
124
+
125
+ ## 示例
126
+
127
+ | 文件 | 用途 | 需要 key |
128
+ |---|---|---|
129
+ | `examples/discover.py` | 列出索引 + discover 检索 | 否 |
130
+ | `examples/quote.py` | `quote('jsonfmt')` 报价演示 | 否 |
131
+ | `examples/mcp.py` | initialize + tools/list 演示 | 否 |
132
+ | `examples/call.py` | call 执行演示 | **是,会产生真实计费,无 key 请勿运行** |
133
+
134
+ 只读冒烟(无 key 可跑):
135
+
136
+ ```bash
137
+ python3 scripts/smoke.py
138
+ ```
139
+
140
+ 离线单元测试(不访问网络):
141
+
142
+ ```bash
143
+ python3 -m unittest discover -s tests -v
144
+ ```
145
+
146
+ ## Open-core 边界声明
147
+
148
+ 本仓库只公开协议契约与参考客户端;网关的执行、配额与密钥管理等内部实现
149
+ 不在开源范围内。SDK 不内置任何真实密钥,示例一律从 `os.environ` 读取或使用
150
+ 占位符 `YOUR_API_KEY`。
151
+
152
+ ## 许可证
153
+
154
+ MIT, Copyright (c) 2026 AgentKit
@@ -0,0 +1,142 @@
1
+ # agentkit-client — AgentKit Python Reference SDK
2
+
3
+ 零第三方依赖(仅使用标准库 `urllib` / `json` / `os`)的 AgentKit 网关参考客户端,
4
+ 封装 **discover → quote → call** 流程,并提供只读能力索引与 MCP `tools/list` 入口。
5
+
6
+ - 线上网关:<https://agentkit.ltd>
7
+ - 数据采集时间:2026-10-17(端点事实来自公网实测)
8
+ - 许可证:MIT
9
+ - 要求:Python 3.9+
10
+
11
+ > Open-core 边界:本 SDK 只封装公开的集成协议(检索、报价、调用、MCP 清单),
12
+ > 不包含、也不暴露任何网关内部实现。
13
+
14
+ ## 安装
15
+
16
+ 无第三方依赖,无需安装即可从源码目录直接运行;也可将 `agentkit/` 包复制进项目。
17
+
18
+ ```bash
19
+ # 从源码目录使用(pip 不需要安装任何依赖)
20
+ python3 examples/quote.py
21
+ ```
22
+
23
+ ## 快速开始
24
+
25
+ ```python
26
+ import os
27
+ from agentkit import AgentKit
28
+
29
+ client = AgentKit(
30
+ base_url="https://agentkit.ltd",
31
+ api_key=os.environ.get("AGENTKIT_API_KEY"), # 仅 call() 需要
32
+ )
33
+
34
+ # 只读:能力索引
35
+ idx = client.list_skills()
36
+
37
+ # 只读:自然语言检索(当前线上可能返回空结果,SDK 优雅降级)
38
+ found = client.discover("json")
39
+
40
+ # 只读:报价 + 输入 schema
41
+ q = client.quote("jsonfmt")
42
+
43
+ # 执行(需要 API key,会发起真实计费调用)
44
+ res = client.call("jsonfmt", {"text": '{"a": 1}'})
45
+ ```
46
+
47
+ API key 未在构造函数传入时,客户端自动读取环境变量 `AGENTKIT_API_KEY`。
48
+
49
+ ## API
50
+
51
+ | 方法 | 端点 | 鉴权 | 说明 |
52
+ |---|---|---|---|
53
+ | `list_skills()` | `GET /api/skills.json` | 无 | 能力索引,返回 `{ meta, hostedCount, catalogCount, libraryCount, count, skills[] }` |
54
+ | `discover(text=None, json=None)` | `GET /api/discover?text=...` | 无 | 自然语言检索;返回规范化的 `{ query, count, results[] }`,空结果不报错 |
55
+ | `quote(tool)` | `GET /api/quote?tool=<id>` | 无 | 报价与输入 JSON Schema,返回 `{ tool, name, priceUsd, currency, schema, errorContract, freeTierPerIp }` |
56
+ | `call(tool, arguments=None)` | `POST /api/call` | 需要 key | 执行工具;请求体严格为 `{ "tool": id, "arguments": {...} }` |
57
+ | `mcp_list_tools()` | `POST /mcp`(initialize → tools/list) | 无 | MCP Streamable HTTP 握手后返回 tools 数组 |
58
+
59
+ ### 构造参数
60
+
61
+ ```python
62
+ client = AgentKit(
63
+ base_url="https://agentkit.ltd", # 默认值,可覆盖
64
+ api_key=os.environ.get("AGENTKIT_API_KEY"), # 默认读环境变量
65
+ auth_header=lambda key: f"Bearer {key}", # 可选,覆盖鉴权头值
66
+ mcp_client_info={"name": "my-agent", "version": "1.0.0"}, # 可选
67
+ timeout=30.0,
68
+ )
69
+ ```
70
+
71
+ **鉴权头约定(待确认)**:`call()` 默认按 `Authorization: Bearer <key>` 发送,
72
+ 这是 2026-10-17 时的默认实现,待与线上网关最终确认;如需改用其它头格式,
73
+ 通过构造参数 `auth_header`(字符串或 `api_key -> str` 可调用对象)覆盖。
74
+
75
+ 调用前若没有任何 key,客户端在本地直接抛出 `MissingApiKeyError`,**不会发出请求**。
76
+
77
+ ## 错误处理
78
+
79
+ 所有非 2xx 响应被规范化为 `AgentKitError`,属性:
80
+
81
+ | 属性 | 类型 | 说明 |
82
+ |---|---|---|
83
+ | `http_status` | int | HTTP 状态码;本地错误为 `0` |
84
+ | `error_code` | str | 响应体 `error` 字段(如 `INVALID_KEY`) |
85
+ | `retryable` | bool | 响应体 `retryable` 字段,指示是否可重试 |
86
+ | `detail` | str | 响应体 `detail` 字段 |
87
+
88
+ 公开错误码:`INVALID_KEY`、`INVALID_QUERY`、`INTERNAL_ERROR`、`UNKNOWN_TOOL`、
89
+ `ANONYMOUS_QUOTA_EXCEEDED`、`INSUFFICIENT_BALANCE`、`GUARDRAIL_BLOCKED`。
90
+
91
+ ```python
92
+ from agentkit import AgentKit, AgentKitError
93
+
94
+ try:
95
+ client.call("jsonfmt", {"text": "{}"})
96
+ except AgentKitError as err:
97
+ print(err.error_code, err.http_status, err.retryable, err.detail)
98
+ ```
99
+
100
+ `MissingApiKeyError` 是 `AgentKitError` 的子类(`error_code: 'MISSING_API_KEY'`,本地抛出)。
101
+
102
+ ## MCP 说明
103
+
104
+ `mcp_list_tools()` 对 `POST /mcp` 依次发送:
105
+
106
+ 1. JSON-RPC `initialize`(`id=1`,`protocolVersion: "2025-03-26"`,`clientInfo` 可配置);
107
+ 2. JSON-RPC `tools/list`(`id=2`)。
108
+
109
+ 请求头固定为 `Content-Type: application/json`、`Accept: application/json, text/event-stream`;
110
+ 响应同时兼容 JSON 与 SSE(`data:` 行)两种格式。当前网关只暴露 tools 能力,
111
+ 返回 13 个 hosted 工具,每个含 `name`、`description`、`inputSchema`(与 `quote` 的 `schema` 一致)。
112
+
113
+ ## 示例
114
+
115
+ | 文件 | 用途 | 需要 key |
116
+ |---|---|---|
117
+ | `examples/discover.py` | 列出索引 + discover 检索 | 否 |
118
+ | `examples/quote.py` | `quote('jsonfmt')` 报价演示 | 否 |
119
+ | `examples/mcp.py` | initialize + tools/list 演示 | 否 |
120
+ | `examples/call.py` | call 执行演示 | **是,会产生真实计费,无 key 请勿运行** |
121
+
122
+ 只读冒烟(无 key 可跑):
123
+
124
+ ```bash
125
+ python3 scripts/smoke.py
126
+ ```
127
+
128
+ 离线单元测试(不访问网络):
129
+
130
+ ```bash
131
+ python3 -m unittest discover -s tests -v
132
+ ```
133
+
134
+ ## Open-core 边界声明
135
+
136
+ 本仓库只公开协议契约与参考客户端;网关的执行、配额与密钥管理等内部实现
137
+ 不在开源范围内。SDK 不内置任何真实密钥,示例一律从 `os.environ` 读取或使用
138
+ 占位符 `YOUR_API_KEY`。
139
+
140
+ ## 许可证
141
+
142
+ MIT, Copyright (c) 2026 AgentKit
@@ -0,0 +1,7 @@
1
+ """AgentKit reference client SDK (zero third-party dependencies, stdlib only)."""
2
+
3
+ from .client import AgentKit
4
+ from .errors import AgentKitError, MissingApiKeyError
5
+
6
+ __all__ = ["AgentKit", "AgentKitError", "MissingApiKeyError"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,263 @@
1
+ """AgentKit reference client (standard library only: urllib / json / os).
2
+
3
+ Flow: discover (search the index) -> quote (read price + input schema) -> call (execute).
4
+ Requires Python 3.9+.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from typing import Any, Dict, Optional
10
+ from urllib import request as _urlrequest
11
+ from urllib.error import HTTPError, URLError
12
+ from urllib.parse import quote, urlencode
13
+
14
+ from .errors import AgentKitError, MissingApiKeyError
15
+
16
+ DEFAULT_BASE_URL = "https://agentkit.ltd"
17
+ DEFAULT_MCP_CLIENT_INFO = {"name": "agentkit-python-client", "version": "0.1.0"}
18
+
19
+
20
+ def _parse_body(raw: str, content_type: str) -> Dict[str, Any]:
21
+ """Parse a JSON body or an MCP SSE stream (``data: {...}`` lines)."""
22
+ if not raw:
23
+ return {}
24
+ if "text/event-stream" in content_type.lower():
25
+ last = None
26
+ for line in raw.splitlines():
27
+ line = line.strip()
28
+ if line.startswith("data:"):
29
+ payload = line[5:].strip()
30
+ if payload:
31
+ last = payload
32
+ if last is None:
33
+ return {}
34
+ try:
35
+ parsed = json.loads(last)
36
+ except (ValueError, TypeError):
37
+ return {}
38
+ return parsed if isinstance(parsed, dict) else {}
39
+ try:
40
+ parsed = json.loads(raw)
41
+ except (ValueError, TypeError):
42
+ return {"raw": raw}
43
+ return parsed if isinstance(parsed, dict) else {}
44
+
45
+
46
+ class AgentKit:
47
+ """Reference client for the AgentKit gateway (https://agentkit.ltd).
48
+
49
+ :param base_url: Gateway base URL, defaults to ``https://agentkit.ltd``.
50
+ :param api_key: API key; defaults to the ``AGENTKIT_API_KEY`` environment
51
+ variable. Only :meth:`call` requires it.
52
+ :param auth_header: Override the Authorization header value sent by
53
+ :meth:`call`. May be a static string or a callable ``(api_key) -> str``.
54
+ NOTE: the header convention used here (``Authorization: Bearer <key>``)
55
+ is the default implementation as of 2026-10-17 and remains to be
56
+ finalized with the live gateway; override via this parameter if needed.
57
+ :param mcp_client_info: ``clientInfo`` reported during the MCP initialize
58
+ handshake.
59
+ :param timeout: Per-request timeout in seconds.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ base_url: str = DEFAULT_BASE_URL,
65
+ api_key: Optional[str] = None,
66
+ auth_header: Any = None,
67
+ mcp_client_info: Optional[Dict[str, str]] = None,
68
+ timeout: float = 30.0,
69
+ ) -> None:
70
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
71
+ self.api_key = api_key if api_key is not None else os.environ.get("AGENTKIT_API_KEY", "")
72
+ self.auth_header = auth_header
73
+ self.mcp_client_info = {**DEFAULT_MCP_CLIENT_INFO, **(mcp_client_info or {})}
74
+ self.timeout = timeout
75
+
76
+ def _auth_headers(self) -> Dict[str, str]:
77
+ if not self.api_key:
78
+ raise MissingApiKeyError()
79
+ if callable(self.auth_header):
80
+ value = self.auth_header(self.api_key)
81
+ elif isinstance(self.auth_header, str) and self.auth_header:
82
+ value = self.auth_header
83
+ else:
84
+ # Default convention (2026-10-17, pending final confirmation with the gateway).
85
+ value = f"Bearer {self.api_key}"
86
+ return {"Authorization": value}
87
+
88
+ def _request(
89
+ self,
90
+ method: str,
91
+ path: str,
92
+ payload: Optional[Dict[str, Any]] = None,
93
+ extra_headers: Optional[Dict[str, str]] = None,
94
+ ) -> Dict[str, Any]:
95
+ url = f"{self.base_url}{path}"
96
+ data = None
97
+ headers = {"Accept": "application/json"}
98
+ if payload is not None:
99
+ data = json.dumps(payload).encode("utf-8")
100
+ headers["Content-Type"] = "application/json"
101
+ if extra_headers:
102
+ headers.update(extra_headers)
103
+
104
+ req = _urlrequest.Request(url, data=data, headers=headers, method=method)
105
+ try:
106
+ with _urlrequest.urlopen(req, timeout=self.timeout) as resp:
107
+ raw = resp.read().decode("utf-8", errors="replace")
108
+ content_type = resp.headers.get("Content-Type", "")
109
+ return _parse_body(raw, content_type)
110
+ except HTTPError as exc:
111
+ try:
112
+ raw = exc.read().decode("utf-8", errors="replace")
113
+ body = _parse_body(raw, exc.headers.get("Content-Type", "") if exc.headers else "")
114
+ finally:
115
+ exc.close()
116
+ code = body.get("error") or f"HTTP_{exc.code}"
117
+ retryable = bool(body.get("retryable", False))
118
+ detail = body.get("detail") or body.get("message") or ""
119
+ raise AgentKitError(
120
+ http_status=exc.code,
121
+ error_code=code,
122
+ retryable=retryable,
123
+ detail=detail,
124
+ ) from exc
125
+ except URLError as exc:
126
+ raise AgentKitError(
127
+ http_status=0,
128
+ error_code="NETWORK_ERROR",
129
+ retryable=True,
130
+ detail=str(getattr(exc, "reason", exc)),
131
+ ) from exc
132
+
133
+ def list_skills(self) -> Dict[str, Any]:
134
+ """GET /api/skills.json — read-only capability index.
135
+
136
+ :return: Full index: ``{ meta, hostedCount, catalogCount, libraryCount,
137
+ count, skills[] }``.
138
+ """
139
+ return self._request("GET", "/api/skills.json")
140
+
141
+ def discover(
142
+ self,
143
+ text: Optional[str] = None,
144
+ json: Any = None,
145
+ ) -> Dict[str, Any]:
146
+ """GET /api/discover?text=<query> — natural-language search over the index.
147
+
148
+ NOTE: as of 2026-10-17 the live endpoint intermittently returns an empty
149
+ result set; the SDK always resolves with a normalized
150
+ ``{ query, count, results: [] }`` shape instead of raising.
151
+
152
+ :param text: Natural-language task text.
153
+ :param json: Structured request sent as the ``json`` query parameter
154
+ (dicts are serialized to JSON).
155
+ :return: ``{ query, count, results }``.
156
+ """
157
+ params = {}
158
+ if text:
159
+ params["text"] = text
160
+ if json is not None:
161
+ params["json"] = json if isinstance(json, str) else json.dumps(json, ensure_ascii=False)
162
+ qs = urlencode(params)
163
+ path = f"/api/discover?{qs}" if qs else "/api/discover"
164
+ body = self._request("GET", path)
165
+ results = body.get("results")
166
+ return {
167
+ "query": body.get("query") if body.get("query") is not None else text,
168
+ "count": body.get("count") if isinstance(body.get("count"), int) else 0,
169
+ "results": results if isinstance(results, list) else [],
170
+ }
171
+
172
+ def quote(self, tool: str) -> Dict[str, Any]:
173
+ """GET /api/quote?tool=<id> — price, input schema and error contract.
174
+
175
+ :param tool: Tool id (e.g. ``"jsonfmt"``).
176
+ :return: Quote object: ``{ tool, name, priceUsd, currency, schema,
177
+ errorContract, freeTierPerIp }``.
178
+ """
179
+ if not tool:
180
+ raise AgentKitError(
181
+ http_status=0,
182
+ error_code="INVALID_QUERY",
183
+ retryable=False,
184
+ detail="tool id is required",
185
+ )
186
+ return self._request("GET", f"/api/quote?tool={quote(tool, safe='')}")
187
+
188
+ def call(self, tool: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
189
+ """POST /api/call — execute a hosted tool. Requires an API key.
190
+
191
+ This performs a real billed call on the gateway; do not run it without
192
+ reading the quote first and confirming the key.
193
+
194
+ :param tool: Tool id (e.g. ``"jsonfmt"``).
195
+ :param arguments: Arguments matching the tool's quote schema.
196
+ :return: Gateway response body.
197
+ """
198
+ if not tool:
199
+ raise AgentKitError(
200
+ http_status=0,
201
+ error_code="INVALID_QUERY",
202
+ retryable=False,
203
+ detail="tool id is required",
204
+ )
205
+ headers = self._auth_headers()
206
+ return self._request(
207
+ "POST",
208
+ "/api/call",
209
+ payload={"tool": tool, "arguments": arguments or {}},
210
+ extra_headers=headers,
211
+ )
212
+
213
+ def mcp_list_tools(self) -> list:
214
+ """MCP Streamable HTTP handshake over POST /mcp.
215
+
216
+ Sends ``initialize`` (id=1) then ``tools/list`` (id=2) and returns the
217
+ tools array.
218
+ """
219
+ headers = {
220
+ "Content-Type": "application/json",
221
+ "Accept": "application/json, text/event-stream",
222
+ }
223
+ init = self._request(
224
+ "POST",
225
+ "/mcp",
226
+ payload={
227
+ "jsonrpc": "2.0",
228
+ "id": 1,
229
+ "method": "initialize",
230
+ "params": {
231
+ "protocolVersion": "2025-03-26",
232
+ "capabilities": {},
233
+ "clientInfo": self.mcp_client_info,
234
+ },
235
+ },
236
+ extra_headers=headers,
237
+ )
238
+ if init.get("error"):
239
+ err = init["error"]
240
+ raise AgentKitError(
241
+ http_status=0,
242
+ error_code=err.get("code") if isinstance(err, dict) else "MCP_ERROR",
243
+ retryable=False,
244
+ detail=err.get("message", "MCP initialize failed") if isinstance(err, dict) else "MCP initialize failed",
245
+ )
246
+
247
+ listed = self._request(
248
+ "POST",
249
+ "/mcp",
250
+ payload={"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
251
+ extra_headers=headers,
252
+ )
253
+ if listed.get("error"):
254
+ err = listed["error"]
255
+ raise AgentKitError(
256
+ http_status=0,
257
+ error_code=err.get("code") if isinstance(err, dict) else "MCP_ERROR",
258
+ retryable=False,
259
+ detail=err.get("message", "MCP tools/list failed") if isinstance(err, dict) else "MCP tools/list failed",
260
+ )
261
+ result = listed.get("result") or {}
262
+ tools = result.get("tools")
263
+ return tools if isinstance(tools, list) else []
@@ -0,0 +1,50 @@
1
+ """Structured errors raised by the AgentKit Python client.
2
+
3
+ Gateway failure body shape:
4
+ {"error": "INVALID_KEY", "retryable": false, "detail": "..."}
5
+
6
+ Public error codes (see README.md):
7
+ INVALID_KEY, INVALID_QUERY, INTERNAL_ERROR, UNKNOWN_TOOL,
8
+ ANONYMOUS_QUOTA_EXCEEDED, INSUFFICIENT_BALANCE, GUARDRAIL_BLOCKED
9
+ """
10
+
11
+
12
+ class AgentKitError(Exception):
13
+ """Normalized error carrying HTTP status, machine-readable code and retryability."""
14
+
15
+ def __init__(
16
+ self,
17
+ message: str = "",
18
+ http_status: int = 0,
19
+ error_code: str = "UNKNOWN_ERROR",
20
+ retryable: bool = False,
21
+ detail: str = "",
22
+ ) -> None:
23
+ if not message:
24
+ message = f"{error_code} (HTTP {http_status}): {detail}".strip()
25
+ super().__init__(message)
26
+ self.http_status = http_status
27
+ self.error_code = error_code
28
+ self.retryable = bool(retryable)
29
+ self.detail = detail
30
+
31
+
32
+ class MissingApiKeyError(AgentKitError):
33
+ """Raised locally by ``call()`` before any request when no API key is configured.
34
+
35
+ No network request is made in this case.
36
+ """
37
+
38
+ def __init__(self, message: str = "") -> None:
39
+ if not message:
40
+ message = (
41
+ "Missing API key: pass api_key to the AgentKit constructor or set "
42
+ "the AGENTKIT_API_KEY environment variable before calling call()."
43
+ )
44
+ super().__init__(
45
+ message=message,
46
+ http_status=0,
47
+ error_code="MISSING_API_KEY",
48
+ retryable=False,
49
+ detail=message,
50
+ )
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentkit-client
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency reference client SDK for the AgentKit gateway (discover / quote / call / MCP tools.list)
5
+ Author: AgentKit
6
+ License-Expression: MIT
7
+ Keywords: agentkit,mcp,ai-agent,sdk,reference-client
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # agentkit-client — AgentKit Python Reference SDK
14
+
15
+ 零第三方依赖(仅使用标准库 `urllib` / `json` / `os`)的 AgentKit 网关参考客户端,
16
+ 封装 **discover → quote → call** 流程,并提供只读能力索引与 MCP `tools/list` 入口。
17
+
18
+ - 线上网关:<https://agentkit.ltd>
19
+ - 数据采集时间:2026-10-17(端点事实来自公网实测)
20
+ - 许可证:MIT
21
+ - 要求:Python 3.9+
22
+
23
+ > Open-core 边界:本 SDK 只封装公开的集成协议(检索、报价、调用、MCP 清单),
24
+ > 不包含、也不暴露任何网关内部实现。
25
+
26
+ ## 安装
27
+
28
+ 无第三方依赖,无需安装即可从源码目录直接运行;也可将 `agentkit/` 包复制进项目。
29
+
30
+ ```bash
31
+ # 从源码目录使用(pip 不需要安装任何依赖)
32
+ python3 examples/quote.py
33
+ ```
34
+
35
+ ## 快速开始
36
+
37
+ ```python
38
+ import os
39
+ from agentkit import AgentKit
40
+
41
+ client = AgentKit(
42
+ base_url="https://agentkit.ltd",
43
+ api_key=os.environ.get("AGENTKIT_API_KEY"), # 仅 call() 需要
44
+ )
45
+
46
+ # 只读:能力索引
47
+ idx = client.list_skills()
48
+
49
+ # 只读:自然语言检索(当前线上可能返回空结果,SDK 优雅降级)
50
+ found = client.discover("json")
51
+
52
+ # 只读:报价 + 输入 schema
53
+ q = client.quote("jsonfmt")
54
+
55
+ # 执行(需要 API key,会发起真实计费调用)
56
+ res = client.call("jsonfmt", {"text": '{"a": 1}'})
57
+ ```
58
+
59
+ API key 未在构造函数传入时,客户端自动读取环境变量 `AGENTKIT_API_KEY`。
60
+
61
+ ## API
62
+
63
+ | 方法 | 端点 | 鉴权 | 说明 |
64
+ |---|---|---|---|
65
+ | `list_skills()` | `GET /api/skills.json` | 无 | 能力索引,返回 `{ meta, hostedCount, catalogCount, libraryCount, count, skills[] }` |
66
+ | `discover(text=None, json=None)` | `GET /api/discover?text=...` | 无 | 自然语言检索;返回规范化的 `{ query, count, results[] }`,空结果不报错 |
67
+ | `quote(tool)` | `GET /api/quote?tool=<id>` | 无 | 报价与输入 JSON Schema,返回 `{ tool, name, priceUsd, currency, schema, errorContract, freeTierPerIp }` |
68
+ | `call(tool, arguments=None)` | `POST /api/call` | 需要 key | 执行工具;请求体严格为 `{ "tool": id, "arguments": {...} }` |
69
+ | `mcp_list_tools()` | `POST /mcp`(initialize → tools/list) | 无 | MCP Streamable HTTP 握手后返回 tools 数组 |
70
+
71
+ ### 构造参数
72
+
73
+ ```python
74
+ client = AgentKit(
75
+ base_url="https://agentkit.ltd", # 默认值,可覆盖
76
+ api_key=os.environ.get("AGENTKIT_API_KEY"), # 默认读环境变量
77
+ auth_header=lambda key: f"Bearer {key}", # 可选,覆盖鉴权头值
78
+ mcp_client_info={"name": "my-agent", "version": "1.0.0"}, # 可选
79
+ timeout=30.0,
80
+ )
81
+ ```
82
+
83
+ **鉴权头约定(待确认)**:`call()` 默认按 `Authorization: Bearer <key>` 发送,
84
+ 这是 2026-10-17 时的默认实现,待与线上网关最终确认;如需改用其它头格式,
85
+ 通过构造参数 `auth_header`(字符串或 `api_key -> str` 可调用对象)覆盖。
86
+
87
+ 调用前若没有任何 key,客户端在本地直接抛出 `MissingApiKeyError`,**不会发出请求**。
88
+
89
+ ## 错误处理
90
+
91
+ 所有非 2xx 响应被规范化为 `AgentKitError`,属性:
92
+
93
+ | 属性 | 类型 | 说明 |
94
+ |---|---|---|
95
+ | `http_status` | int | HTTP 状态码;本地错误为 `0` |
96
+ | `error_code` | str | 响应体 `error` 字段(如 `INVALID_KEY`) |
97
+ | `retryable` | bool | 响应体 `retryable` 字段,指示是否可重试 |
98
+ | `detail` | str | 响应体 `detail` 字段 |
99
+
100
+ 公开错误码:`INVALID_KEY`、`INVALID_QUERY`、`INTERNAL_ERROR`、`UNKNOWN_TOOL`、
101
+ `ANONYMOUS_QUOTA_EXCEEDED`、`INSUFFICIENT_BALANCE`、`GUARDRAIL_BLOCKED`。
102
+
103
+ ```python
104
+ from agentkit import AgentKit, AgentKitError
105
+
106
+ try:
107
+ client.call("jsonfmt", {"text": "{}"})
108
+ except AgentKitError as err:
109
+ print(err.error_code, err.http_status, err.retryable, err.detail)
110
+ ```
111
+
112
+ `MissingApiKeyError` 是 `AgentKitError` 的子类(`error_code: 'MISSING_API_KEY'`,本地抛出)。
113
+
114
+ ## MCP 说明
115
+
116
+ `mcp_list_tools()` 对 `POST /mcp` 依次发送:
117
+
118
+ 1. JSON-RPC `initialize`(`id=1`,`protocolVersion: "2025-03-26"`,`clientInfo` 可配置);
119
+ 2. JSON-RPC `tools/list`(`id=2`)。
120
+
121
+ 请求头固定为 `Content-Type: application/json`、`Accept: application/json, text/event-stream`;
122
+ 响应同时兼容 JSON 与 SSE(`data:` 行)两种格式。当前网关只暴露 tools 能力,
123
+ 返回 13 个 hosted 工具,每个含 `name`、`description`、`inputSchema`(与 `quote` 的 `schema` 一致)。
124
+
125
+ ## 示例
126
+
127
+ | 文件 | 用途 | 需要 key |
128
+ |---|---|---|
129
+ | `examples/discover.py` | 列出索引 + discover 检索 | 否 |
130
+ | `examples/quote.py` | `quote('jsonfmt')` 报价演示 | 否 |
131
+ | `examples/mcp.py` | initialize + tools/list 演示 | 否 |
132
+ | `examples/call.py` | call 执行演示 | **是,会产生真实计费,无 key 请勿运行** |
133
+
134
+ 只读冒烟(无 key 可跑):
135
+
136
+ ```bash
137
+ python3 scripts/smoke.py
138
+ ```
139
+
140
+ 离线单元测试(不访问网络):
141
+
142
+ ```bash
143
+ python3 -m unittest discover -s tests -v
144
+ ```
145
+
146
+ ## Open-core 边界声明
147
+
148
+ 本仓库只公开协议契约与参考客户端;网关的执行、配额与密钥管理等内部实现
149
+ 不在开源范围内。SDK 不内置任何真实密钥,示例一律从 `os.environ` 读取或使用
150
+ 占位符 `YOUR_API_KEY`。
151
+
152
+ ## 许可证
153
+
154
+ MIT, Copyright (c) 2026 AgentKit
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ agentkit/__init__.py
5
+ agentkit/client.py
6
+ agentkit/errors.py
7
+ agentkit_client.egg-info/PKG-INFO
8
+ agentkit_client.egg-info/SOURCES.txt
9
+ agentkit_client.egg-info/dependency_links.txt
10
+ agentkit_client.egg-info/top_level.txt
11
+ tests/test_client.py
@@ -0,0 +1,18 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agentkit-client"
7
+ version = "0.1.0"
8
+ description = "Zero-dependency reference client SDK for the AgentKit gateway (discover / quote / call / MCP tools.list)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "AgentKit" }]
14
+ keywords = ["agentkit", "mcp", "ai-agent", "sdk", "reference-client"]
15
+ dependencies = []
16
+
17
+ [tool.setuptools.packages.find]
18
+ include = ["agentkit*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,165 @@
1
+ """Offline unit tests for the AgentKit client (unittest + mock, no network).
2
+
3
+ Run from sdk/python/: python3 -m unittest discover -s tests -v
4
+ """
5
+ import io
6
+ import json
7
+ import os
8
+ import sys
9
+ import unittest
10
+ from urllib.error import HTTPError
11
+ from unittest import mock
12
+
13
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
14
+
15
+ from agentkit import AgentKit, AgentKitError, MissingApiKeyError # noqa: E402
16
+
17
+
18
+ class FakeResp:
19
+ def __init__(self, body, content_type="application/json"):
20
+ self._body = body if isinstance(body, str) else json.dumps(body)
21
+ self.headers = {"Content-Type": content_type}
22
+
23
+ def read(self):
24
+ return self._body.encode("utf-8")
25
+
26
+ def __enter__(self):
27
+ return self
28
+
29
+ def __exit__(self, *args):
30
+ return False
31
+
32
+
33
+ def make_http_error(status, body):
34
+ raw = io.BytesIO(json.dumps(body).encode("utf-8"))
35
+ return HTTPError("https://agentkit.ltd/api/call", status, "error", {}, raw)
36
+
37
+
38
+ class ClientTests(unittest.TestCase):
39
+ def make_client(self, **kwargs):
40
+ kwargs.setdefault("base_url", "https://agentkit.ltd")
41
+ return AgentKit(**kwargs)
42
+
43
+ def test_missing_api_key_raises_before_request(self):
44
+ client = self.make_client(api_key="")
45
+ with mock.patch("agentkit.client._urlrequest.urlopen") as urlopen:
46
+ with self.assertRaises(MissingApiKeyError):
47
+ client.call("jsonfmt", {"text": "{}"})
48
+ urlopen.assert_not_called()
49
+
50
+ def test_call_sends_bearer_header_and_body(self):
51
+ client = self.make_client(api_key="test-key")
52
+ captured = {}
53
+
54
+ def fake_urlopen(req, timeout=None):
55
+ captured["req"] = req
56
+ return FakeResp({"ok": True, "tool": "jsonfmt"})
57
+
58
+ with mock.patch("agentkit.client._urlrequest.urlopen", side_effect=fake_urlopen):
59
+ res = client.call("jsonfmt", {"text": '{"a":1}'})
60
+
61
+ req = captured["req"]
62
+ self.assertEqual(req.get_method(), "POST")
63
+ self.assertEqual(req.get_header("Authorization"), "Bearer test-key")
64
+ sent = json.loads(req.data.decode("utf-8"))
65
+ self.assertEqual(sent, {"tool": "jsonfmt", "arguments": {"text": '{"a":1}'}})
66
+ self.assertTrue(res["ok"])
67
+
68
+ def test_auth_header_override(self):
69
+ client = self.make_client(api_key="test-key", auth_header="Token test-key")
70
+ captured = {}
71
+
72
+ def fake_urlopen(req, timeout=None):
73
+ captured["req"] = req
74
+ return FakeResp({"ok": True})
75
+
76
+ with mock.patch("agentkit.client._urlrequest.urlopen", side_effect=fake_urlopen):
77
+ client.call("jsonfmt", {})
78
+
79
+ req = captured["req"]
80
+ self.assertEqual(req.get_header("Authorization"), "Token test-key")
81
+
82
+ def test_auth_header_callable_override(self):
83
+ client = self.make_client(api_key="test-key", auth_header=lambda k: f"Api-Key {k}")
84
+ captured = {}
85
+
86
+ def fake_urlopen(req, timeout=None):
87
+ captured["req"] = req
88
+ return FakeResp({"ok": True})
89
+
90
+ with mock.patch("agentkit.client._urlrequest.urlopen", side_effect=fake_urlopen):
91
+ client.call("jsonfmt", {})
92
+
93
+ self.assertEqual(captured["req"].get_header("Authorization"), "Api-Key test-key")
94
+
95
+ def test_quote_parses_price(self):
96
+ body = {
97
+ "tool": "jsonfmt",
98
+ "priceUsd": 0.1,
99
+ "currency": "USD",
100
+ "schema": {"type": "object", "required": ["text"], "properties": {}},
101
+ }
102
+ with mock.patch("agentkit.client._urlrequest.urlopen", return_value=FakeResp(body)):
103
+ q = self.make_client().quote("jsonfmt")
104
+ self.assertEqual(q["priceUsd"], 0.1)
105
+ self.assertIn("text", q["schema"]["required"])
106
+
107
+ def test_discover_normalizes_empty_result(self):
108
+ body = {"query": None, "count": 0, "results": []}
109
+ with mock.patch("agentkit.client._urlrequest.urlopen", return_value=FakeResp(body)):
110
+ found = self.make_client().discover("json")
111
+ self.assertEqual(found["count"], 0)
112
+ self.assertEqual(found["results"], [])
113
+
114
+ def test_http_error_maps_to_agentkit_error(self):
115
+ err_body = {"error": "INVALID_KEY", "retryable": False, "detail": "API Key 无效"}
116
+ client = self.make_client(api_key="bad")
117
+ with mock.patch(
118
+ "agentkit.client._urlrequest.urlopen",
119
+ side_effect=make_http_error(401, err_body),
120
+ ):
121
+ with self.assertRaises(AgentKitError) as ctx:
122
+ client.call("jsonfmt", {})
123
+ self.assertEqual(ctx.exception.http_status, 401)
124
+ self.assertEqual(ctx.exception.error_code, "INVALID_KEY")
125
+ self.assertFalse(ctx.exception.retryable)
126
+ self.assertIn("API Key", ctx.exception.detail)
127
+
128
+ def test_mcp_list_tools_initializes_then_lists(self):
129
+ init_resp = {
130
+ "jsonrpc": "2.0",
131
+ "id": 1,
132
+ "result": {
133
+ "protocolVersion": "2025-03-26",
134
+ "capabilities": {"tools": {"listChanged": False}},
135
+ "serverInfo": {"name": "agentkit", "version": "3.0.0"},
136
+ },
137
+ }
138
+ tools = [{"name": f"tool-{i}", "description": "", "inputSchema": {}} for i in range(13)]
139
+ list_resp = {"jsonrpc": "2.0", "id": 2, "result": {"tools": tools}}
140
+ calls = []
141
+
142
+ def fake_urlopen(req, timeout=None):
143
+ calls.append(json.loads(req.data.decode("utf-8")))
144
+ return FakeResp(init_resp if len(calls) == 1 else list_resp)
145
+
146
+ with mock.patch("agentkit.client._urlrequest.urlopen", side_effect=fake_urlopen):
147
+ result = self.make_client().mcp_list_tools()
148
+
149
+ self.assertEqual(len(result), 13)
150
+ self.assertEqual(calls[0]["method"], "initialize")
151
+ self.assertEqual(calls[0]["params"]["protocolVersion"], "2025-03-26")
152
+ self.assertEqual(calls[1]["method"], "tools/list")
153
+
154
+ def test_sse_body_is_parsed(self):
155
+ sse = 'event: message\ndata: {"jsonrpc":"2.0","id":2,"result":{"tools":[]}}\n\n'
156
+ with mock.patch(
157
+ "agentkit.client._urlrequest.urlopen",
158
+ return_value=FakeResp(sse, content_type="text/event-stream"),
159
+ ):
160
+ result = self.make_client().mcp_list_tools()
161
+ self.assertEqual(result, [])
162
+
163
+
164
+ if __name__ == "__main__":
165
+ unittest.main()