diff --git a/AGENTS.md b/AGENTS.md index 502ecae..1cf18ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,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. | | 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 a8bbe1d..63b79d3 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,18 @@ The example pins `mcp==2.3.0` in its script metadata, lists the two documentatio The separate standard-library RPC example above still needs no third-party Python packages. +### Read the same page with LangChain + +For LangChain tools, use [examples/langchain_docs.py](examples/langchain_docs.py). With Python 3.11+ and uv, run from the cloned repository: + +```bash +LANGSMITH_TRACING=false uv run examples/langchain_docs.py +``` + +The script declares `langchain[mcp]==1.4.9`, `fastmcp==4.1.6` and `mcp==2.3.0` in inline metadata; uv manages its environment separately from project dependencies. It uses the current `langchain.mcp.MCPAdapter` API, which is **beta**, rather than the deprecated adapters package. See the [LangChain migration guide](https://docs.langchain.com/oss/python/migrate/langchain-mcp-adapters). + +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. + ### 1. Claude Code Configuration #### Keyless Documentation MCP diff --git a/README.zh.md b/README.zh.md index 4daa3d2..713914f 100644 --- a/README.zh.md +++ b/README.zh.md @@ -380,6 +380,18 @@ uv run examples/mcp_docs.py --query "Robinhood 主网 RPC" --lang zh 前面的独立标准库 RPC 示例仍无需第三方 Python 包。 +### 通过 LangChain 读取同一页面 + +需要 LangChain 工具时,使用 [examples/langchain_docs.py](examples/langchain_docs.py)。准备 Python 3.11+ 和 uv,在克隆的仓库中执行: + +```bash +LANGSMITH_TRACING=false uv run examples/langchain_docs.py +``` + +脚本在内联元数据中声明 `langchain[mcp]==1.4.9`、`fastmcp==4.1.6`、`mcp==2.3.0`,uv 会管理独立于项目依赖的环境。示例采用当前仍为 **beta** 的 `langchain.mcp.MCPAdapter`,不安装已弃用的 adapters 包。迁移说明见 [LangChain 官方指南](https://docs.langchain.com/oss/python/migrate/langchain-mcp-adapters)。 + +它先确认仅暴露 `search_docs` 和 `read_doc`,检索英文 `quickstart`,再通过 LangChain 工具读取该页。JSON 输出包含协议版本、工具名和两次工具消息及文档正文。无需 LLM、模型 API key、BlockVectra key 或钱包,不调用链上 RPC;上述命令关闭 LangSmith tracing。操作超时为15秒,协作式总超时为60秒,安装依赖不计入这些时限。既有调用逻辑已用所固定版本核验,不代表模型驱动的完整 Agent 工作流已经验证。 + ### 1. Claude Code 配置 #### 免 Key 文档服务 diff --git a/examples/langchain_docs.py b/examples/langchain_docs.py new file mode 100644 index 0000000..5077ef2 --- /dev/null +++ b/examples/langchain_docs.py @@ -0,0 +1,79 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "langchain[mcp]==1.4.9", +# "fastmcp==4.1.6", +# "mcp==2.3.0", +# ] +# /// +"""Read remote docs through langchain.mcp, without an LLM or API key. +Python 3.11+; pip install 'langchain[mcp]==1.4.9' 'fastmcp==4.1.6' 'mcp==2.3.0' +langchain.mcp is currently beta; keep that warning visible. +""" +import asyncio +import json +import os + +import httpx2 +from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport +from langchain.mcp import MCPAdapter +from langchain_core.messages import ToolMessage + +ENDPOINT = "https://docs.blockvectra.com/mcp/docs" +USER_AGENT = "langchain-docs-example/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) + + +async def invoke(tool, arguments, call_id): + message = await tool.ainvoke({ + "type": "tool_call", "name": tool.name, "args": arguments, "id": call_id, + }) + if not isinstance(message, ToolMessage) or message.status != "success": + raise RuntimeError(f"{tool.name} did not succeed; stopping") + return message + + +async def read_docs(factory=http_client_factory): + transport = StreamableHttpTransport( + ENDPOINT, auth=None, headers={"User-Agent": USER_AGENT}, + httpx_client_factory=factory, + ) + client = Client(transport, timeout=15, init_timeout=15) + async with MCPAdapter(client) as adapter: + tools = {tool.name: tool for tool in await adapter.list_tools()} + if sorted(tools) != ["read_doc", "search_docs"]: + raise RuntimeError(f"Unexpected tool scope: {sorted(tools)}") + search = await invoke( + tools["search_docs"], {"query": "quickstart", "lang": "en", "limit": 1}, + "docs-search", + ) + results = (search.artifact or {}).get("structured_content", {}).get("results", []) + if not any(hit.get("path") == "quickstart" for hit in results): + raise RuntimeError("Search did not return quickstart; stopping") + document = await invoke( + tools["read_doc"], {"path": "quickstart", "lang": "en"}, "docs-read", + ) + if not document.content: + raise RuntimeError("Empty document; stopping") + return { + "protocol_version": adapter.client.protocol_version, + "tools": sorted(tools), + "search": search.model_dump(mode="json"), + "document": document.model_dump(mode="json"), + } + + +async def main(): + async with asyncio.timeout(60): + print(json.dumps(await read_docs(), ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + asyncio.run(main())