Skip to content

resource_tools

Python · package toolnexus · SPEC §2 · python/src/toolnexus/mcp_source.py

McpSource.resource_tools(self) -> list[Tool] # [list_mcp_resources, read_mcp_resource]
# list_mcp_resources {server?} one line per resource: "<server> <uri> — <name> (<mime>): <description>"
# read_mcp_resource {server, uri} the resource's text contents

MCP servers can expose resources (files, records, fixed documents) besides tools. A plain load_mcp exposes only tools; resource_tools() returns two extra tools over the source’s connected servers that let the model list and read them. Both carry ToolHints(read_only=True). They are opt-in — register them yourself — and stop working when the source is closed.

  • A server publishes its knowledge as resources (docs, schemas, config) and you want the model to browse them without writing a custom tool per server.
  • A coding or support agent that should read MCP-provided context but never call a mutating tool.

1. The smallest useful call — list what the servers offer

Section titled “1. The smallest useful call — list what the servers offer”
import asyncio
import shutil
from toolnexus import load_mcp
async def main():
src = await load_mcp({"mcp": {"progress": {
"command": [shutil.which("node"), "examples/mcp-progress-server.mjs"], "timeout": 5000}}})
try:
list_tool, _ = src.resource_tools()
r = await list_tool.execute({})
assert r.output == "progress fixture://hello — hello (text/plain): A fixed text resource.", r.output
print("ok:", r.output)
finally:
try:
await src.close()
except Exception: # noqa: BLE001
pass
asyncio.run(main())

2. The realistic case — give the model the tools, read-only

Section titled “2. The realistic case — give the model the tools, read-only”
import asyncio
import shutil
from toolnexus import create_toolkit, load_mcp
async def main():
src = await load_mcp({"mcp": {"progress": {
"command": [shutil.which("node"), "examples/mcp-progress-server.mjs"], "timeout": 5000}}})
try:
tk = await create_toolkit(builtins=False, extra_tools=src.resource_tools())
names = sorted(t.name for t in tk.tools())
assert names == ["list_mcp_resources", "read_mcp_resource"], names
assert all(t.hints.read_only for t in tk.tools())
r = await tk.execute("read_mcp_resource", {"server": "progress", "uri": "fixture://hello"})
assert not r.is_error and r.output == "hello resource"
print("ok:", r.output)
finally:
try:
await src.close()
except Exception: # noqa: BLE001
pass
asyncio.run(main())

3. The full surface — errors are tool results, not exceptions

Section titled “3. The full surface — errors are tool results, not exceptions”
import asyncio
import shutil
from toolnexus import load_mcp
async def main():
src = await load_mcp({"mcp": {"progress": {
"command": [shutil.which("node"), "examples/mcp-progress-server.mjs"], "timeout": 5000}}})
try:
_, read = src.resource_tools()
bad_uri = await read.execute({"server": "progress", "uri": "fixture://nope"})
bad_server = await read.execute({"server": "nope", "uri": "x"})
assert bad_uri.is_error and bad_server.is_error
assert "connected: progress" in bad_server.output, bad_server.output
print("ok: both failures came back as error results")
finally:
try:
await src.close()
except Exception: # noqa: BLE001
pass
asyncio.run(main())
  • load_mcp — Read an mcp.json, connect every local stdio and remote streamable-HTTP server, expose each server tool as a Tool.
  • load_mcp_with_context — The ctx-aware load: bound connection time and cancel a slow or hung server without leaking a child process.
  • list_mcp_tools — List what each configured server would expose, plus per-server status, without wiring it into a toolkit.
  • parse_mcp_config — Parse and validate config without connecting — the fast fail for a malformed or misspelled server block.