Toolkit
Toolkits: your connections' operations, the operations that need no account, and capabilities as LLM tools.
gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True)
toolkit = noclick.Toolkit(["search", gmail])
response = anthropic.messages.create(..., tools=toolkit.definitions())
messages.append({"role": "user", "content": toolkit.handle(response)})A call never raises for something the model can act on: a missing connection,
a policy refusal or a hold for approval comes back as an is_error result.
A hold's approval_required (approval_id, request_id, approve_url,
operation, approver) is never shown to the model; pass on_approval=
to the handle helpers to receive it, and on_connect= to receive each
result that needs a connection. request_id (the request a person answers),
approve_url and operation (the approval's operation object) are None
unless the request is addressed to you; the approver is emailed either way.
Once the person approves, the identical call goes through.
noclick.Toolkit(tools, *, user=..., client=None, simulated=False, scenario=None)A set of tools for your LLM. Entries are connections (a Connection
or its name: every operation, as {connection}__{operation} tools),
single operations ("gmail.send_email_message"), capabilities
("search", "fetch", "code") or a Database ({database}__query,
__execute and __schema; invite-only beta), a Bucket and
operations with arguments bound (gmail.only("send_email_message").bind(to=...):
the bound parameters leave the tool's schema and are filled on every call, so a
toolkit sends its entries with each call). A name resolves in the
user the calls act for (user= if given, else the user current at each call,
as_user): a connection there, else an integration's operations that need no account
(on NoClick's key), so "exa.search" needs no connection. A Connection object from
another scope, another user's or your account's, stays that connection (invite-only beta).
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)FalseNoneBuild one toolkit at module level and use it for every user: "gmail" is
each user's own connection named gmail, and "exa.search" needs none:
toolkit = noclick.Toolkit(["gmail", "slack.send_message_to_channel", "exa.search"])
print(toolkit.tools)Or pin it to one user, with only some of a connection's operations:
gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True, user="user_42")
toolkit = noclick.Toolkit([gmail.only("send_email_message"), "fetch"], user="user_42")To test your loop without touching anyone's accounts, let a simulated world answer every call. The definitions are the real ones; the toolkit is one world, so what one call creates the next one finds, for 30 minutes after its last call, and each answer is billed as AI at NoClick's model price, as a test run's world is:
toolkit = noclick.Toolkit(["gmail", "stripe"], simulated=True, scenario="Ana has two unpaid invoices")
result = toolkit.call("stripe__list_invoices", {"status": "open"}, user="user_42")
print(result.text) # made up, consistent with the scenario; nothing was read or sent| refs | list[Any] | |
| simulated | bool | |
| scenario | str | None | |
| tools | list[str] | The refs as names (a Connection known only by id shows its id until used). |
mcp_server
toolkit.mcp_server(name) -> McpServerThese tools as a hosted MCP server called name, in the toolkit's
user (McpServer.from_name with create_if_missing=True). Sends nothing yet.
The server, created on first use (or brought in step with this toolkit's tools); its url is
what you give an MCP client.
toolkit = noclick.Toolkit(["hubspot", "search"], user="acme")
server = toolkit.mcp_server("crm-tools")
print(server.url)definitionsasync
toolkit.definitions(format='anthropic', *, user=...) -> list[dict[str, Any]]Describe the toolkit's tools for your LLM API
(format = "anthropic", "openai" or "mcp"). Cached per format.
'anthropic'Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The tool definitions in that API's shape, ready to pass as tools. The first call for a
format fetches them; later calls reuse them.
For Claude, the default format:
toolkit = noclick.Toolkit(["gmail", "search"])
with noclick.as_user("user_42"):
tools = toolkit.definitions()
print([tool["name"] for tool in tools]) # ['gmail__send_email_message', ..., 'search']For OpenAI, or for an MCP server of your own:
toolkit = noclick.Toolkit(["gmail", "search"])
openai_tools = toolkit.definitions("openai", user="user_42")
mcp_tools = toolkit.definitions("mcp", user="user_42")callasync
toolkit.call(name, arguments=None, *, approval_id=None, user=...) -> ToolResultRun one tool by its wire name (gmail__send_email_message for the
connection named gmail, search).
A policy refusal or a hold is an is_error result, never an
exception; a held call goes through once approved when it is repeated
with the same arguments (approval_id= names the approval explicitly).
NoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The tool's content (text as one string) and whether it is_error. A call that couldn't
run says why in content, with needs_connection or approval_required set when that's the reason.
Run a tool by the name the model uses:
toolkit = noclick.Toolkit(["gmail", "search"])
result = toolkit.call("gmail__fetch_emails_from_inbox", {"max_results": 5}, user="user_42")
print(result.is_error, result.text)When the user hasn't connected the account yet, the result carries the link to send them:
toolkit = noclick.Toolkit(["gmail"])
result = toolkit.call("gmail__fetch_emails_from_inbox", {"max_results": 5}, user="user_42")
if result.needs_connection:
print(result.connect_url)handleasync
toolkit.handle(
content, *, user=..., on_approval=None, on_connect=None
) -> list[dict[str, Any]]Run every Anthropic tool_use block (from a Message, its
content list, or dicts) concurrently. A tool your policy
denies or holds for approval comes back as an error result;
on_approval(result) (sync or async) receives each held call's
ToolResult, in call order, and on_connect(result) each one whose
needs_connection is set, so your app can show the user the connect link
instead of relying on the model to pass it on. Each result carries the
model's call_id and the arguments.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
NoneNoneOne tool_result block per tool_use, in order: the content of the next user turn.
Run the model loop with Claude:
import anthropic
claude = anthropic.Anthropic()
toolkit = noclick.Toolkit(["gmail", "search"])
messages = [{"role": "user", "content": "Email Ana the notes from today's standup"}]
with noclick.as_user("user_42"):
while True:
response = claude.messages.create(
model="claude-sonnet-5", max_tokens=1024, tools=toolkit.definitions(), messages=messages,
)
if response.stop_reason != "tool_use":
break
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": toolkit.handle(response)})The model never sees who approves a held call or the approval link. Pass on_approval to get them:
toolkit = noclick.Toolkit(["gmail"])
blocks = toolkit.handle(response, on_approval=lambda result: notify_approver(result.approval_required))Show the user a Connect button when a tool needs their account:
toolkit = noclick.Toolkit(["gmail", "google_calendar"])
blocks = toolkit.handle(
response, user="user_42",
on_connect=lambda result: show_connect_button(result.needs_connection["integration"], result.connect_url),
)handle_openaiasync
toolkit.handle_openai(
tool_calls, *, user=..., on_approval=None, on_connect=None
) -> list[dict[str, Any]]Run OpenAI tool calls (a ChatCompletion, its message, a
tool_calls list, or Responses function_call items).
on_approval and on_connect work as in handle.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
NoneNoneOne {"role": "tool"} message per call, in order, or a function_call_output item for each
Responses call. A call whose arguments aren't valid JSON isn't run; its message tells the model so.
from openai import OpenAI
gpt = OpenAI()
toolkit = noclick.Toolkit(["gmail", "search"])
messages = [{"role": "user", "content": "Summarize the emails Ana sent me this week"}]
with noclick.as_user("user_42"):
while True:
completion = gpt.chat.completions.create(
model="gpt-5.1", tools=toolkit.definitions("openai"), messages=messages,
)
message = completion.choices[0].message
if not message.tool_calls:
break
messages.append(message)
messages += toolkit.handle_openai(completion)ToolResult
class ToolResultThe outcome of one tool call. An unconnected account is a normal result
whose needs_connection names the connection, its integration
and the connect_url to send the user;
a call held for approval carries approval_required: approval_id,
request_id, approve_url, operation and approver ("user"
or "owner"). request_id (the request a person answers),
approve_url and operation (the approval's operation object) are
None unless the request is addressed to you; the approver is emailed
either way.
| name | str | |
| content | Any | |
| is_error | bool | |
| needs_connection | dict[str, Any] | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
| approval_required | dict[str, Any] | None | |
| arguments | dict[str, Any] | None | The arguments the call was made with, as the model gave them. |
| call_id | str | None | The model's id for the call (its |
| connect_url | str | None | |
| approval | HumanRequest | None | The approval a held call waits on, as a request: |
| approve_url | str | None | The approval link, when the call waits for your approval; once approved, repeat the identical call. |
| text | str | The content as one string (what an OpenAI tool message carries). |