Platform tools
Inside an agent skill, ctx.tools gives you governed access to platform services — every call is authenticated with the current run's token, scoped to the current tenant, and routed through the MCP Gateway. You never construct or manage these bindings yourself; AgentExecutionContext builds ctx.tools for you from the inbound request.
What's available
ToolBindings exposes one attribute per tool category:
| Binding | Purpose |
|---|---|
ctx.tools.knowledge |
RAG search, document ingestion, GraphRAG queries |
ctx.tools.agents |
Call another agent, or discover agents by capability |
ctx.tools.hitl |
Request human approval |
ctx.tools.files |
Scoped file upload/download/list |
ctx.tools.memory |
Governed memory write / query (see Agent memory) |
ctx.tools.notify |
Send a notification (email, Slack, ...) |
ctx.tools.http |
Governed outbound HTTP (domain-allowlisted) |
ctx.tools.browser |
Browser automation (navigate, screenshot, extract) |
ctx.tools.code |
Sandboxed code execution |
ctx.tools.connectors |
Generic third-party connector invocation (ClickUp, Slack, ...) |
For memory, ctx.tools.memory covers only governed write and query; the full memory surface (forget, hint, assemble_context, typed results, retries) is on ctx.memory.client(), which talks to Memory Service directly rather than through the Gateway. Agent memory compares the two.
Every call returns a ToolResponse — success: bool, data: Any, error: str | None — rather than raising for a failed tool call, so a skill can inspect and handle a failure inline:
result = await ctx.tools.knowledge.search("compliance regulations")
if result.success:
print(result.data)
else:
ctx.logger.error("Knowledge search failed", extra={"error": result.error})
Timeouts
Every tool call accepts a timeout_s override — for example ctx.tools.files.upload(filename, content, *, timeout_s=None), the same pattern on every method across every binding. Left unset, it falls back to a default of 128 seconds, deliberately set above the MCP Gateway's own upstream ceilings (up to 120s for built-in tools, 30s by default for manifest-registered ones) so a slow call surfaces the Gateway's own, more informative timeout error instead of a generic client-side one:
Configure the instance-wide default with the NOVAOS_TOOL_TIMEOUT_S environment variable, or per-binding via ToolBindings(default_timeout_s=...). There is no single Gateway-wide timeout to validate against — it varies by dispatch path — so an opt-in floor check (gateway_timeout_s) is available if you know the ceiling for the tools you're calling, but it's off by default; a shorter effective timeout than the floor raises ValueError before any network call is made.
Knowledge search and ingestion
ctx.tools.knowledge retries transient failures (HTTP 429/502/503/504, network timeouts) automatically with exponential backoff and a shared circuit breaker:
# Search
result = await ctx.tools.knowledge.search("compliance regulations", limit=5)
# Ingest and wait for completion
ingest_result = await ctx.tools.knowledge.ingest(document_text)
final = await ctx.tools.knowledge.poll_until_complete(ingest_result.data["job_id"])
# Batch ingest with bounded concurrency
from novaos_client_sdk import IngestItem
batch = await ctx.tools.knowledge.ingest_batch([
IngestItem(content="doc one"),
IngestItem(content="doc two"),
])
final_batch = await ctx.tools.knowledge.poll_batch_until_complete(batch)
GraphRAG (entity-graph) search follows the same build-then-query shape: build_graph(collection_id) kicks off indexing, get_graph_status / poll_graph_build_until_complete observe it, and query_graph_local / query_graph_global answer entity-centric vs. corpus-wide questions once the graph is ready.
Calling connectors
ctx.tools.connectors.call(connector_id, operation, params) invokes a third-party integration (ClickUp, Slack, Gmail, ...) that doesn't have a bespoke binding of its own:
result = await ctx.tools.connectors.call(
"clickup",
"create_task",
params={"list_id": "123", "name": "Follow up"},
)
params is forwarded verbatim as the connector's request body, capped at 256 KiB client-side — check the specific connector's documentation for its expected shape. Pass params_schema (a JSON Schema, requires the schema extra) for client-side validation before the call goes out. Unlike knowledge, connector calls are not retried automatically — many connector operations (create a task, send a message) aren't safe to retry blindly.
Most connectors are tier="standard" (the default). A licensed or third-party ("ext") connector needs its tier passed explicitly:
result = await ctx.tools.connectors.call(
"some-crm",
"get-deal",
params={"deal_id": "123"},
tier="licensed", # or "ext"
)
The SDK has no catalog access to know a connector's tier on its own, so calling a licensed/ext connector without tier= fails tool-not-found — that error's message is augmented with a hint to retry with the right tier. The MCP Gateway enforces whether your token actually has the scope for that tier; this argument only picks the tool name.
Human-in-the-loop approval
approval = await ctx.tools.hitl.request_approval(
"Deploy to production?",
options=["approve", "reject"],
)
ctx.tools.hitl.request_input (free-form user input outside the approval flow) is not backed by a platform service and fails closed — it returns success=False without making a network call, rather than silently hanging or approving. Use the HITL pause pattern for input requests instead.
Calling other agents
result = await ctx.tools.agents.call("other-agent-id", "Summarize this thread")
discovered = await ctx.tools.agents.discover(query="summarization")
This is a governed, token-chained call — distinct from using Client directly, which is for a standalone process rather than a running agent's own skill.
Escape hatch: call_ungoverned_tool
If you need to reach an MCP tool that has no first-class binding above, ctx.tools.<any binding>.call_ungoverned_tool(tool_name, arguments) is available on every binding class. It bypasses the SDK's typed tool surface — the platform's own policy may still deny the call — and is logged as a warning each time it's used. Treat repeated use of this for the same tool as a signal that the SDK should grow a proper binding for it, not as a long-term pattern.