diff --git a/AGENTS.md b/AGENTS.md index 1cf18ef..27d7d1d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ Choose the entry that matches the requested task before installing dependencies | Answer a BlockVectra documentation question | [Documentation skill](skills/blockvectra-docs/SKILL.md): use configured documentation tools or an available HTTP reader. | | Read documentation from Python through MCP | [Python MCP example](examples/mcp_docs.py) and the README section “Read documentation from a Python agent”; `--query` searches before reading in the selected language. | | Read documentation through LangChain tools | [LangChain MCP example](examples/langchain_docs.py) and the README section “Read the same page with LangChain”; Python 3.11+, uv, no model or account key. | +| Read documentation through Google ADK tools | [ADK MCP example](examples/adk_docs.py) and the README section “Read documentation with Google ADK”; Python 3.11+, uv, no model, Cloud project or account key. | | Check a chain's identity with Python | [Standard-library public reader](examples/public_chain.py) and the README section “Python standard-library entry”. | | Check chain identity and read the latest block number | [Public TypeScript reader](src/public-read-cli.ts) and the README section “Start with a keyless read”. | | Inspect a saved BSC receipt and fixed-block logs offline | [Pinned BSC event sample](https://gist.github.com/gjwang/5a8f80f5e441a5701e2e01ae73f187bf/0f680ce849c78375537fce5ed1ce9e447fe2f341) and the README section “Check a saved BSC receipt and its logs”; Node.js 22+, no account or key. | diff --git a/README.md b/README.md index e3754c9..9ad634e 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,20 @@ The script declares `langchain[mcp]==1.4.9`, `fastmcp==4.1.6` and `mcp==2.3.0` i It verifies that only `search_docs` and `read_doc` are exposed, searches for English `quickstart`, then reads that page through LangChain tools. JSON output contains the protocol version, tool names and both tool messages with the document text. No LLM, model API key, BlockVectra key, wallet or chain RPC is needed. The command disables LangSmith tracing. Operation timeouts are 15 seconds, with a 60-second cooperative overall timeout; dependency installation happens before those timeouts. The existing call logic was checked with the pinned versions; this does not verify a model-driven agent workflow. +### Read documentation with Google ADK + +Use Python 3.11+ and [uv](https://docs.astral.sh/uv/) from this checkout: + +```bash +uv run examples/adk_docs.py +``` + +The [example](examples/adk_docs.py) pins `google-adk[mcp]==2.14.0` and `mcp==2.3.0`. It connects `McpToolset` to the remote `/mcp/docs` endpoint, checks for exactly `search_docs` and `read_doc`, searches for `quickstart`, then reads that English page and prints its Markdown in JSON. It needs no BlockVectra account, wallet, API key, Google Cloud project or model API key; the in-memory ADK context runs no model. + +The two-tool scope is enforced by the server. Keep this toolset open while using its tools, then call `await toolset.close()` in `finally`. For a model-backed agent, add this same toolset to your ADK agent's `tools` and configure your model provider separately. See the [official ADK MCP guide](https://adk.dev/tools-custom/mcp-tools/). + +The example sets 15-second HTTP/read timeouts and a 60-second overall timeout, follows no HTTP redirects, and adds no retry loop. ADK retains its own session-recovery behavior. A failed search, tool error, empty document or unexpected tool list stops the example. This reads documentation only; it does not execute the RPC or account steps described in the page. + ### 1. Claude Code Configuration #### Keyless Documentation MCP diff --git a/README.zh.md b/README.zh.md index f364699..37efe15 100644 --- a/README.zh.md +++ b/README.zh.md @@ -409,6 +409,20 @@ LANGSMITH_TRACING=false uv run --script --python 3.11 \ 它先确认仅暴露 `search_docs` 和 `read_doc`,检索英文 `quickstart`,再通过 LangChain 工具读取该页。JSON 输出包含协议版本、工具名和两次工具消息及文档正文。无需 LLM、模型 API key、BlockVectra key 或钱包,不调用链上 RPC;上述命令关闭 LangSmith tracing。操作超时为15秒,协作式总超时为60秒,安装依赖不计入这些时限。既有调用逻辑已用所固定版本核验,不代表模型驱动的完整 Agent 工作流已经验证。 +### 用 Google ADK 读取文档 + +准备 Python 3.11+ 和 [uv](https://docs.astral.sh/uv/),在本仓库目录运行: + +```bash +uv run examples/adk_docs.py +``` + +[示例](examples/adk_docs.py)固定 `google-adk[mcp]==2.14.0` 与 `mcp==2.3.0`,通过 `McpToolset` 连接远程 `/mcp/docs`,核对只有 `search_docs`、`read_doc`,搜索 `quickstart` 后读取英文页面,将 Markdown 放在 JSON 中输出。无需 BlockVectra 账户、钱包、API Key、Google Cloud 项目或模型 API Key;内存中的 ADK 上下文不运行模型。 + +双工具范围由服务端执行。使用工具期间保持 toolset 打开,并在 `finally` 中调用 `await toolset.close()`。需要模型参与时,将同一 toolset 放入 ADK agent 的 `tools`,另外配置模型提供方。参见 [ADK 官方 MCP 指南](https://adk.dev/tools-custom/mcp-tools/)。 + +示例设有 15 秒 HTTP/读取超时和 60 秒整体超时,不跟随 HTTP 重定向、不另加重试循环;ADK 自身的会话恢复行为保留。搜索失败、工具错误、空文档或工具清单不符都会停止。这里只读文档,不会执行页面中介绍的 RPC 或开户步骤。 + ### 1. Claude Code 配置 #### 免 Key 文档服务 diff --git a/examples/adk_docs.py b/examples/adk_docs.py new file mode 100644 index 0000000..6bc1da6 --- /dev/null +++ b/examples/adk_docs.py @@ -0,0 +1,82 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = ["google-adk[mcp]==2.14.0", "mcp==2.3.0"] +# /// +"""Search and read BlockVectra docs through Google ADK, without a model or key.""" + +import asyncio +import json +import os + +import httpx2 +from google.adk.agents import BaseAgent +from google.adk.agents.invocation_context import InvocationContext +from google.adk.sessions import InMemorySessionService, Session +from google.adk.tools import ToolContext +from google.adk.tools.mcp_tool import McpToolset +from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams + +ENDPOINT = "https://docs.blockvectra.com/mcp/docs" +USER_AGENT = "BlockVectraADKDocs/1" +if os.environ.get("BLOCKVECTRA_INTERNAL_QA") == "1": + USER_AGENT += " BlockVectraQA/1" + + +def http_client_factory(**kwargs): + kwargs.update(timeout=httpx2.Timeout(15), follow_redirects=False, trust_env=False) + return httpx2.AsyncClient(**kwargs) + + +def tool_text(result: dict, name: str) -> str: + if not isinstance(result, dict) or result.get("isError") or "error" in result: + raise RuntimeError(f"{name} did not succeed; stopping") + texts = [item["text"] for item in result.get("content", []) if item.get("type") == "text"] + if not texts or not any(text.strip() for text in texts): + raise RuntimeError(f"{name} returned no documentation text; stopping") + return "\n".join(texts) + + +async def read_docs(factory=http_client_factory) -> dict: + toolset = McpToolset( + connection_params=StreamableHTTPConnectionParams( + url=ENDPOINT, + headers={"User-Agent": USER_AGENT}, + timeout=15, + sse_read_timeout=15, + httpx_client_factory=factory, + ), + ) + context = ToolContext( + InvocationContext( + invocation_id="docs-first-read", + agent=BaseAgent(name="docs_reader"), + session=Session(id="docs-first-read", app_name="docs-reader", user_id="local"), + session_service=InMemorySessionService(), + ) + ) + try: + tools = {tool.name: tool for tool in await toolset.get_tools()} + if sorted(tools) != ["read_doc", "search_docs"]: + raise RuntimeError("The endpoint did not expose exactly the two documentation tools") + search = await tools["search_docs"].run_async( + args={"query": "quickstart", "lang": "en", "limit": 1}, tool_context=context, + ) + hits = json.loads(tool_text(search, "search_docs")).get("results", []) + if not any(isinstance(hit, dict) and hit.get("path") == "quickstart" for hit in hits): + raise RuntimeError("Search did not return quickstart; stopping") + document = await tools["read_doc"].run_async( + args={"path": "quickstart", "lang": "en"}, tool_context=context, + ) + return {"tools": sorted(tools), "path": "quickstart", "lang": "en", + "text": tool_text(document, "read_doc")} + finally: + await toolset.close() + + +async def main() -> None: + async with asyncio.timeout(60): + print(json.dumps(await read_docs(), ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + asyncio.run(main())