Metadata-Version: 2.5
Name: yjgr-agent-dns
Version: 1.1.0
Summary: YJGR Agent DNS MCP server: DNS-based agent discovery with working Bearer / OAuth2 / MCP-spec OAuth authentication. Fork of dns-aid-core.
Project-URL: Homepage, https://github.com/yjgr/agent-dns-mcp
Project-URL: Repository, https://github.com/yjgr/agent-dns-mcp
Project-URL: Upstream, https://github.com/dns-aid/dns-aid-core
Author: YJGR, DNS-AID Contributors
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: a2a,agent,ai,discovery,dns,dnssec,mcp,svcb
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: Name Service (DNS)
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.11
Requires-Dist: cryptography>=50.0.0
Requires-Dist: dnspython>=2.6.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: mcp<2.0.0,>=1.28.1
Requires-Dist: pydantic>=2.5.0
Requires-Dist: pygments>=2.20.0
Requires-Dist: pyjwt>=2.13.0
Requires-Dist: python-dotenv>=1.2.2
Requires-Dist: python-multipart>=0.0.27
Requires-Dist: rich>=13.0.0
Requires-Dist: structlog>=24.0.0
Requires-Dist: typer>=0.12.0
Requires-Dist: uvicorn>=0.30.0
Provides-Extra: akamai-edgedns
Requires-Dist: edgegrid-python>=2.0.7; extra == 'akamai-edgedns'
Requires-Dist: requests>=2.33.0; extra == 'akamai-edgedns'
Requires-Dist: urllib3>=2.7.0; extra == 'akamai-edgedns'
Provides-Extra: all
Requires-Dist: boto3>=1.34.0; extra == 'all'
Requires-Dist: cel-python>=0.5.0; extra == 'all'
Requires-Dist: common-expression-language>=0.5.0; extra == 'all'
Requires-Dist: edgegrid-python>=2.0.7; extra == 'all'
Requires-Dist: google-auth>=2.30.0; extra == 'all'
Requires-Dist: opentelemetry-api<2.0.0,>=1.20.0; extra == 'all'
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc<2.0.0,>=1.20.0; extra == 'all'
Requires-Dist: opentelemetry-sdk<2.0.0,>=1.20.0; extra == 'all'
Requires-Dist: pqcrypto>=0.4.0; extra == 'all'
Requires-Dist: pygments>=2.20.0; extra == 'all'
Requires-Dist: requests>=2.33.0; extra == 'all'
Requires-Dist: urllib3>=2.7.0; extra == 'all'
Provides-Extra: cel
Requires-Dist: cel-python>=0.5.0; extra == 'cel'
Requires-Dist: common-expression-language>=0.5.0; extra == 'cel'
Requires-Dist: pygments>=2.20.0; extra == 'cel'
Provides-Extra: cli
Provides-Extra: cloud-dns
Requires-Dist: google-auth>=2.30.0; extra == 'cloud-dns'
Requires-Dist: requests>=2.33.0; extra == 'cloud-dns'
Requires-Dist: urllib3>=2.7.0; extra == 'cloud-dns'
Provides-Extra: cloudflare
Provides-Extra: ddns
Provides-Extra: dev
Requires-Dist: boto3-stubs[route53]>=1.34.0; extra == 'dev'
Requires-Dist: cel-python>=0.5.0; extra == 'dev'
Requires-Dist: common-expression-language>=0.5.0; extra == 'dev'
Requires-Dist: cryptography>=50.0.0; extra == 'dev'
Requires-Dist: cyclonedx-bom>=4.0.0; extra == 'dev'
Requires-Dist: httpx>=0.27.0; extra == 'dev'
Requires-Dist: lxml>=6.1.0; extra == 'dev'
Requires-Dist: mypy>=1.8.0; extra == 'dev'
Requires-Dist: pqcrypto>=0.4.0; extra == 'dev'
Requires-Dist: pygments>=2.20.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest>=9.0.3; extra == 'dev'
Requires-Dist: ruff>=0.3.0; extra == 'dev'
Requires-Dist: urllib3>=2.7.0; extra == 'dev'
Provides-Extra: infoblox
Provides-Extra: jws
Provides-Extra: mcp
Provides-Extra: nios
Provides-Extra: ns1
Provides-Extra: otel
Requires-Dist: opentelemetry-api<2.0.0,>=1.20.0; extra == 'otel'
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc<2.0.0,>=1.20.0; extra == 'otel'
Requires-Dist: opentelemetry-sdk<2.0.0,>=1.20.0; extra == 'otel'
Provides-Extra: pqc
Requires-Dist: pqcrypto>=0.4.0; extra == 'pqc'
Provides-Extra: route53
Requires-Dist: boto3>=1.34.0; extra == 'route53'
Requires-Dist: urllib3>=2.7.0; extra == 'route53'
Provides-Extra: sdk
Description-Content-Type: text/markdown

# yjgr-agent-dns

基于 DNS 的 AI Agent 发现与调用 MCP 服务器（DNS-AID 协议），**修好了远程 MCP 的鉴权**。

这是 [dns-aid/dns-aid-core](https://github.com/dns-aid/dns-aid-core)（Apache-2.0）的二次开发版本。
上游的 MCP 插件在调用需要鉴权的远程 Agent 时，Bearer 只有一种精确写法能通，
OAuth 对 MCP 场景基本不可用，而且所有 401 都被吞成一句 `Invocation failed (status: error)`。
我们向上游反馈过，没有回应，所以自己修、自己发。

## 在 Claude Code / Codex 里使用

发布到 PyPI 后，两边都只需要一段配置，不用预装任何东西（依赖 [uv](https://docs.astral.sh/uv/)）。

**Claude Code**（写入项目根目录 `.mcp.json`，或 `~/.claude.json` 的 `mcpServers`）：

```json
{
  "mcpServers": {
    "yjgr-agent-dns": {
      "command": "uvx",
      "args": ["--from", "yjgr-agent-dns", "yjgr-agent-dns-mcp"]
    }
  }
}
```

或者一条命令：

```bash
claude mcp add yjgr-agent-dns -- uvx --from yjgr-agent-dns yjgr-agent-dns-mcp
```

**Codex**（`~/.codex/config.toml`）：

```toml
[mcp_servers.yjgr-agent-dns]
command = "uvx"
args = ["--from", "yjgr-agent-dns", "yjgr-agent-dns-mcp"]
```

只做发现和调用时不需要任何环境变量。要发布自己的 Agent 到 DNS 时，
按上游文档配置 `DNS_AID_BACKEND` 等后端凭据（见 `docs/UPSTREAM-README.md`）。

## 鉴权怎么用

远程 Agent 有三种常见鉴权方式，现在三种都能用：

| 远程服务要求 | 怎么调 |
|---|---|
| Bearer token / API key | `call_agent_tool(..., credentials={"token": "..."})`。`auth_type` 可省略，会按凭据的键名推断；`access_token`、`bearer_token`、`api_key` 都认。 |
| OAuth2 客户端凭据（机器对机器） | `credentials={"client_id": "...", "client_secret": "..."}`，token 地址来自发现元数据或 `auth_config.token_url`。 |
| **MCP 规范 OAuth（浏览器登录）** | 先调一次 `agent_login(endpoint)`，浏览器里完成授权；之后对该服务的所有调用自动带 token、自动刷新，不用再传任何凭据。 |
| 还要一个平台/租户标识 | 用 `headers={"X-Platform": "..."}`。有些服务在检查 token 之前先看这个头，不带就直接 403。鉴权头始终盖过这里设的同名头。 |

判断远程服务用哪种：直接调一次，401 的报错里会带服务端的 `WWW-Authenticate` 挑战和下一步提示，例如：

```
HTTP 401 Unauthorized from https://agent.example.com/mcp;
WWW-Authenticate: Bearer resource_metadata="https://agent.example.com/.well-known/oauth-protected-resource";
The server uses MCP OAuth: run the agent_login tool for this endpoint (browser login), then retry.
```

MCP 规范 OAuth 就是 Claude Code / Codex 连远程 MCP 用的那套：
RFC 9728 资源元数据 → RFC 8414 授权服务器元数据 → RFC 7591 动态注册 → 授权码 + PKCE → 回调到本机 → 换 token。
实现直接用官方 `mcp` SDK 的 `OAuthClientProvider`，本项目只补了 token 落盘、本机回调服务和浏览器拉起。

新增的 MCP 工具：

- `agent_login(endpoint, scopes?, client_id?, client_secret?, timeout_seconds?)` 浏览器登录
- `agent_logout(endpoint)` 忘掉登录
- `agent_auth_status(endpoint)` 查看登录状态（不返回 token 值）

`list_agent_tools` 也补上了 `auth_type / auth_config / credentials` 参数，上游版本连这个入口都没有。

### A2A 多轮对话

`message/send` 返回的是一个 Task，不只是一段文字。智能体可能先回 `state="input-required"`
要你确认，这时返回里会带 `task_id`、`context_id`、`state` 和 `data_parts`。
续话要把 `task_id` 和 `context_id` 原样传回去，否则每次都是新任务，确认流程永远走不完：

```python
r1 = send_a2a_message(endpoint=EP, message="给我推荐一些商品", credentials={"token": TOKEN})
# r1: state="input-required", data_parts=[{"action_type":"confirmation_required", ...}]
r2 = send_a2a_message(endpoint=EP, message="", credentials={"token": TOKEN},
                      task_id=r1["task_id"], context_id=r1["context_id"],
                      data={"action_type": "confirmed"})
```

确认的 `data` 具体是什么形状由对方智能体定义，看它 `data_parts` 里的 `task_intent`。

命令行同样可用：

```bash
yjgr-agent-dns agent login  https://agent.example.com/mcp
yjgr-agent-dns agent status https://agent.example.com/mcp
yjgr-agent-dns list-tools   https://agent.example.com/mcp            # 自动用已登录的 token
yjgr-agent-dns call         https://agent.example.com/mcp echo -a '{"text":"hi"}' --token XXX
```

token 存在 `~/.yjgr-agent-dns/oauth/<origin>.json`，权限 0600，按服务的 origin 隔离。

### 相关环境变量

| 变量 | 作用 |
|---|---|
| `YJGR_AGENT_DNS_HOME` | token 存放根目录（默认 `~/.yjgr-agent-dns`） |
| `YJGR_AGENT_DNS_OAUTH_LOGIN_TIMEOUT` | 等浏览器登录的秒数（默认 180） |
| `YJGR_AGENT_DNS_OAUTH_CALLBACK_PORT` | 固定回调端口（默认随机；授权服务器要求固定回调地址时用） |
| `YJGR_AGENT_DNS_OAUTH_NO_BROWSER` | 设为 1 则不自动拉浏览器，只在 stderr 打印授权 URL |
| `DNS_AID_FETCH_ALLOWLIST` | 逗号分隔的主机白名单，白名单内的主机不再受 HTTPS-only 和公网 IP 限制 |
| `DNS_AID_ALLOW_PRIVATE_NETWORKS` | 设为 1 允许 http:// 和内网 / 回环地址（内网部署、内网 IdP 必开） |

### 内网部署注意

上游的 SSRF 防护把所有 http:// 和私网地址一律拒绝，连白名单都绕不过，
所以 OAuth2 客户端凭据模式的 token 地址不能指向内网 IdP。
现在：白名单的主机不限 scheme；或者整体开 `DNS_AID_ALLOW_PRIVATE_NETWORKS=1`。
`file://` 和 URL 内嵌账号密码依旧拒绝。

## 相对上游改了什么

全部改动都在 `git log` 里逐条可查，这里列结论：

1. **凭据被静默丢弃**：`auth_type` 为空时上游直接不鉴权。现在按凭据推断类型，或自动用已存的登录。
2. **`list_agent_tools` 没有鉴权参数**：补上。
3. **401 / 403 被吞**：从 anyio 的 ExceptionGroup 里解出 HTTP 状态和 `WWW-Authenticate`，报错说明是没带凭据还是凭据被拒。
4. **没有 MCP 规范 OAuth**：上游只有 client_credentials。新增 `sdk/auth/mcp_oauth.py` 与三个工具。
5. **内网 IdP 被 SSRF 防护拦死**：白名单优先于 scheme 检查，新增 `DNS_AID_ALLOW_PRIVATE_NETWORKS`。
6. **A2A 的 401/403 也被吞**：只报 `HTTP 401: {body}`，没有 `WWW-Authenticate`、不说该做什么。三个协议处理器现在共用一个报错构造器，会区分「没带凭据」「带了被拒」「缺身份头」。
7. **发不了自定义请求头**：请求头写死，需要 `X-Platform` 这类标识的服务根本调不了。
8. **A2A 只能单轮**：每次调用都新建 task，`data` part 被丢弃，确认类流程无法完成。
9. **发现阶段不报鉴权要求**：agent card 里明明声明了 bearer，`auth_type` 却一直是 null；而且 card 里的 `http` / `httpAuth` 不是处理器名字，直接用会报 Unknown auth_type。
10. **依赖声明打架**：上游 main 允许 `mcp<3`，装上 mcp 2.x 后客户端直接报 transport unavailable。现钉死 `<2.0.0`，且 `mcp`、`typer`、`rich` 进核心依赖，`uvx --from yjgr-agent-dns yjgr-agent-dns-mcp` 不需要任何 extra（`--from` 是 uvx 的规则：命令名和包名不同就要写）。

包名、模块名、命令名全部改为 `yjgr-agent-dns` / `yjgr_agent_dns`。
线上协议标识（`X-DNS-AID-*` 头、`dns-aid-jwks.json`、`DNS_AID_*` 环境变量）**刻意没改**，
保证和上游工具发布的 DNS 记录互通。

## 开发

```bash
uv venv --python 3.12 && uv pip install -e '.[all,dev]'
scripts/test_per_file.sh                 # 单元测试（每个文件独立进程，上游测试之间有状态泄漏）
.venv/bin/python -m pytest tests/integration/test_mcp_oauth_e2e.py   # OAuth 全流程端到端
.venv/bin/ruff check src/ && .venv/bin/mypy src/yjgr_agent_dns
uv build                                 # dist/ 下出 wheel
```

`tests/testbed/oauth_mcp_target.py` 是一个符合 MCP 规范的本地授权服务器 + 受保护 MCP 服务，
可以单独起来手工验证：`python tests/testbed/oauth_mcp_target.py 8766`，然后
`yjgr-agent-dns agent login http://127.0.0.1:8766/mcp`。

同步上游：`git fetch upstream && git merge upstream/main`（remote 名为 `upstream`）。

## 许可

Apache-2.0，与上游一致。保留上游 `LICENSE` 与 `NOTICE`，修改部分版权归 YJGR。
