Tooling (ToolNode)¶
What it is / when to use it¶
ToolNode is PenguiFlow’s integration surface for exposing external tools to ReactPlanner with minimal wrapper code.
Use ToolNode when you want to:
- connect to MCP servers (via FastMCP),
- connect to HTTP APIs (via UTCP/OpenAPI discovery),
- expose tools with consistent schemas, retries, timeouts, concurrency, and artifact handling.
Non-goals / boundaries¶
- ToolNode does not replace your service’s auth/policy layer; it helps you connect to tools safely.
- Presets are convenience configs for development; production should treat tool servers as owned services.
- ToolNode is not a “global singleton” by default; you are responsible for lifecycle (connect, reuse, close where relevant).
Contract surface¶
Core types¶
The relevant surfaces live in:
penguiflow.tools.node.ToolNodepenguiflow.tools.config.ExternalToolConfigpenguiflow.tools.presets.get_preset
At a high level:
ToolNode.connect()performs discovery (and may perform auth handshakes depending on configuration).ToolNode.get_tools()returns specs to include in the planner catalog.- Tool names are namespaced:
{toolnode_name}.{tool_name}(example:github.create_issue).
Discovery + catalog build¶
Common patterns:
- a shared
ModelRegistryfor typed args/out models, - one or more ToolNodes that perform discovery,
- a single combined catalog passed to
ReactPlanner.
For planner-level filtering and tool discovery (tool_search, tool_get, deferred activation), see:
Operational defaults¶
- Call
await tool_node.connect()at service startup (or warm it on first request) and reuse the same ToolNode across sessions. - Use
tool_filter(or equivalent) to expose only a safe subset of tools to the LLM. - Set
ExternalToolConfig.max_concurrencyconservatively for external APIs (3–5 is a typical starting point). - If you expose MCP Apps, keep the
ToolNodereachable after the initial tool call. The HTML artifact is only the view; the host still needs a live backend route fortools/call,tools/list,resources/list, andresources/read. - For web/playground hosts, the recommended lookup key is
session_id + namespace. If you emit an app artifact and then drop theToolNode/MCP client handle, the app will load but fail on its first follow-up action. - Prefer bounded outputs:
- small tool outputs are returned inline,
- large/binary content should become artifacts (see Tools docs).
Failure modes & recovery¶
Tool discovery fails¶
Likely causes
- missing local dependencies (Node.js for
npx-based presets) - missing env vars (config uses
${VAR}substitution and can be fail-fast) - network/auth failures to the tool server
Fix
- validate config with the same environment your worker uses
- use explicit timeouts/retries (and log failures) so “connect” doesn’t hang silently
- run MCP servers as services in production and connect via URL instead of spawning via
npx
Planner sees tools you didn’t intend¶
Fix
- apply ToolNode-level filtering and planner-level tool visibility/policy (belt + suspenders)
- avoid mixing tenant-specific tools into a shared global ToolNode without visibility controls
Observability¶
Recommended:
- emit a config snapshot at startup (max concurrency, retries, timeouts, tool filter)
- record connect/discovery duration and tool counts (per ToolNode)
- record tool call latency/error rates via planner
event_callback
See Planner observability and Tools configuration.
Security / multi-tenancy notes¶
- Don’t leak auth tokens into
llm_context. - Assume an LLM can try to call any visible tool: enforce allowlists and require HITL for write/external side effects.
- Keep per-tenant tool visibility separate from shared process state (use
ToolVisibilityPolicyon planner calls when needed).
Runnable example: connect an MCP preset¶
This example prints discovered tool names.
from penguiflow import ModelRegistry
from penguiflow.tools.node import ToolNode
from penguiflow.tools.presets import get_preset
registry = ModelRegistry()
github = ToolNode(config=get_preset("github"), registry=registry)
await github.connect()
for spec in github.get_tools():
print(spec.name)
Note
Presets are for local development and often use npx -y ... (Node.js required).
For production, prefer running MCP servers as separate services and connecting via URL.
Configuration highlights (what matters operationally)¶
ExternalToolConfig supports:
- env var substitution via
${VAR}in config fields, - tool filtering (
tool_filter) to expose a safe subset, - per-tool source concurrency (
max_concurrency) to protect rate-limited backends, - retries and timeouts (
retry_policy,timeout_s), - artifact extraction for large/binary content.
See the curated runbooks:
Troubleshooting checklist¶
connect()hangs: set explicit timeouts; verify network/auth; ensure spawned dependencies exist (Node.js for presets).- Tools missing: confirm discovery succeeded and
tool_filterisn’t excluding them. - Too many requests / rate-limits: lower
max_concurrency, add retries/backoff, and reduce planner parallelism hints.