Guides

Tools and connectors

Give your agent new abilities with a Python tool, a remote MCP server added as a connector, MCP servers in the Hub folder or the shared browser.

Every Hub starts with the built-in tools for files, knowledge, skills, search and the web. This guide adds your own. By the end, my-hub will have a Python tool that works out how long ago an order was delivered, an eval that proves the agent uses it, and you will know how to add a remote MCP server as a connector.

Video lesson 3 · 3:23Tools, connectors and MCP: how AI agents take actionDescribe a tool with clear inputs, then see how an agent calls it.

What you need

  • The my-hub starter from the quickstart, with its model working.
  • A text editor. The tool is a few lines of Python.

Choose the right mechanism

You needUseWhere it lives
Your own logic, or a call to an internal API with one service credentialA Python tooltools_local/*.py
A remote MCP server, used with each person's own account, one company key or no sign-inA connectorconnectors/<id>.yaml, or Settings when browser editing is on
A local MCP server started as a command, or a remote one that everyone reaches with the same credentialAn MCP server in the Hub folderconnectors/.mcp.json
Pages that need a real browserThe shared browserHUBZOID_BROWSER=true
A tool only some people or workflows may useA restricted toolrestricted/<permission>.py
Where each mechanism works, and who can make the agent call it

A Python tool works on every surface of the Hub, and an MCP server from connectors/.mcp.json on every surface except the Hub's own MCP server. Neither checks who is asking, so everyone who can use the Hub can make the agent call them. A connector reaches a person only when the agent offers it and they hold its capability. When a Python tool must not reach everyone, use a restricted tool.

Add a Python tool

The starter's house rules give a 30-day refund window. Counting days is arithmetic, so give it to a tool rather than the model.

Create the tool file

Any *.py file in tools_local/ is imported when the Hub starts.

my-hub/tools_local/delivery.py
"""Date arithmetic for the Kestrel & Oak support team."""
from __future__ import annotations

import json
from datetime import date

from agents import function_tool


@function_tool
def days_since_delivery(delivered_on: str) -> str:
    """How many days ago an order was delivered. Use it before applying the refund rule.

    Args:
        delivered_on: The delivery date as YYYY-MM-DD, for example "2026-09-14".

    Returns:
        JSON with the delivery date, today's date and the days between them.
    """
    try:
        delivered = date.fromisoformat(delivered_on)
    except ValueError:
        return f"{delivered_on!r} is not a date. Use YYYY-MM-DD."
    today = date.today()
    return json.dumps({
        "delivered_on": delivered.isoformat(),
        "today": today.isoformat(),
        "days": (today - delivered).days,
    })
How the decorator, docstring and type hints define the tool

@function_tool comes from the agents package (the OpenAI Agents SDK, installed with Hubzoid). The function name becomes the tool name, the docstring becomes the description the model reads, and the type hints and Args: section define the arguments. The model decides when to call a tool from these alone, so say what it returns and when to use it. Return a short explanation for an expected problem, such as a badly written date, rather than raising.

Restart and ask

Tools load when the Hub starts. Stop hubzoid run with Ctrl+C, start it again, and ask:

Message
An order was delivered on 2026-09-14. Can the customer still get a refund?

The answer shows a tool step for days_since_delivery, then applies the 30-day rule from knowledge/house-rules.md to the number of days the tool returned. To try it without the web app, use hubzoid test my-hub --prompt "...".

Keep it working

Add an eval that expects the tool, so a later change to AGENTS.md cannot quietly stop the agent from using it:

my-hub/evals/refund-by-date.md
---
expect_tools: [days_since_delivery]
---
## Prompt
An order was delivered on 2026-09-14. Can the customer still get a refund?

## Criteria
Uses the number of days the tool returned and applies the 30-day refund
rule from house-rules.md to it.
Terminal
hubzoid eval run my-hub --case refund-by-date

If the agent never calls the tool, the case fails with never called: days_since_delivery. See evals.

Rules for tools_local/

Each file is loaded on its own, and a broken file loses only its own tools. Keep secrets in the Hub's .env, never in tool arguments.

All rules for tools_local/
  • Every module-level object created with @function_tool becomes a tool. @function_tool(name_override="...") gives it a different name.
  • Files whose names start with _ are skipped, so you can keep helpers and unfinished tools beside the others.
  • A file that fails to import, for example because a package is missing, loses only its own tools. The rest of the Hub starts, and hubzoid doctor names the file and the error.
  • Two tools with the same name: the first file in alphabetical order wins and a warning is logged.
  • A tool with the same name as a built-in replaces the built-in, for example your own web_search.
  • Tool files load one by one, not as a package, so a tool cannot import a sibling file from tools_local/. Keep each file self-contained, or install shared code as a package.
  • Tools run inside the bridge process with the Hub's environment, so read secrets with os.environ inside the function and keep them in the Hub's .env. Never make a secret a tool argument, because the model would have to supply it.
  • The same tool works on all three runtimes. On claude-local the model sees it as mcp__hubzoid__days_since_delivery. On the OpenAI Agents and Codex runtimes it keeps its own name.
  • Tools are also served to MCP clients when the Hub serves MCP. See connect an assistant.
  • A sub-agent that runs on its own model (a delegate) can be limited with tools: [days_since_delivery, read_knowledge] in its frontmatter. See Hub files.

Add a connector

A connector is a remote MCP server the agent reaches as the person it is working for, or with one company key. Each connector is one file, connectors/<id>.yaml.

Where connector files live

In a Hub run on its own, such as my-hub, that is the Hub's own connectors/ folder, beside .mcp.json. In a deployment with a hubzoid.yaml, it is the connectors/ folder beside hubzoid.yaml. Settings reads and writes the same files.

Each connector signs in one of three ways: each person signs in (oauth, the default), one shared key (shared) or no sign-in (none).

How each sign-in kind appears in Settings and who connects
sign_inIn SettingsWho connectsGrant shown as
oauth (the default)Each person signs inEvery person, with their own account, through the provider's sign-inConnect and the name, for example Connect Gmail
sharedShared keyNobody. One company key is sent as a header for everyone granted the capability.Use and the name
noneNo sign-inEvery person switches it on once. The server sees no account.Connect and the name

Write the connector file

This example adds an order lookup server with one company key. Replace the address with your server's MCP endpoint.

my-hub/connectors/orders.yaml
version: 1
name: Order lookup
url: https://mcp.example.com/mcp
sign_in: shared
shared_key: env:ORDERS_MCP_KEY
tools: [get_order]
hubs: [my-hub]
enabled: true
Connector ID rules, the capability name, hubs and tools

The file name is the connector's ID: 2 to 40 lowercase letters, digits or _, starting with a letter. It is also the capability connector_<id>, here connector_orders. hubs lists the agents (Hub folder names) that offer it. tools limits which of the server's tools reach agents. Leave it out to allow every tool.

Add the key

A connector file never holds a secret. It names an environment variable with env:NAME. Put the value in the Hub's .env:

my-hub/.env
ORDERS_MCP_KEY=your-key

The key is sent as Authorization: Bearer <key>.

Another header, or a connector each person signs in to

Set shared_header: X-API-Key in the file to send it in another header. For a connector each person signs in to, there is no key: use sign_in: oauth, and add client_id, client_secret: env:NAME and scopes only when the provider needs a client registered in advance.

Grant it

Hubzoid rereads connector files when they change. Open Settings → Agents → My Hub → Access, edit your own row and turn on Use Order lookup in the Connectors group. From a terminal, the same grant is:

Terminal
hubzoid grant admin@localhost connector_orders my-hub

admin@localhost is the local owner when sign-in is off. On a shared Hub, grant each person who should use it.

Use it

Ask the agent something the server answers, such as "Look up order 1042". The tool step shows the connector's tool, named mcp__my_orders__get_order.

When each person signs in to the connector

For a connector each person signs in to, each person connects once first, in Settings → Connected accounts or from a link the agent sends. See per-user MCP servers.

Connector file keys

A connector file accepts only the keys below.

All connector file keys, and what an unknown key does

Any other key makes the file invalid, and Hubzoid logs the problem and leaves that connector out.

KeyValue
version1
nameThe name people see. Defaults to the ID.
urlThe server's MCP endpoint. HTTPS is required. Plain HTTP works only on this computer (localhost, 127.0.0.1, ::1), for development.
sign_inoauth, shared or none. Default oauth.
client_id, client_secretoauth only. A client registered with the provider in advance. The secret is a reference such as env:NAME, never the value.
scopesoauth only. A list, or text separated by spaces.
shared_key, shared_headershared only. The key as env:NAME, and the header it goes in (default Authorization). Headers that would change the request, such as Host or Cookie, are refused.
toolsThe allowed tools. Leave it out to allow all.
hubsThe agents that offer the connector.
enabledtrue or false.

The log line names the file, for example connectors: orders.yaml: unknown key 'tool'.

Add a connector in Settings

Adding and changing connectors in Settings needs browser editing, which is Beta and off by default. Turn it on with in_app_editing: true in the deployment's hubzoid.yaml, or with HUBZOID_IN_APP_EDITING=true in the .env of a Hub run on its own. It stays off when the Hub's files come from an image.

With browser editing off, the agent's Connectors tab is read-only and says where to change connectors instead: the files above.

Settings, the My Hub agent page on its Connectors tab with no connectors. A line says changing this agent's files from the browser is off: edit the agent's files on this computer, or ask Claude Code or Codex.Settings, the My Hub agent page on its Connectors tab with no connectors. A line says changing this agent's files from the browser is off: edit the agent's files on this computer, or ask Claude Code or Codex.
With browser editing off, connectors are read-only in Settings and live in connectors/.

With browser editing on, organization administrators manage connectors here, and Settings writes the same connectors/<id>.yaml files.

Add the connector

Open Settings → Agents, choose the agent, then Connectors and Add connector.

The list of popular servers, the three parts of the form, and where secrets are saved

It opens on a searchable list of popular remote MCP servers, such as Linear, Notion, Atlassian, Sentry, Stripe, GitHub and the Google Workspace servers, each labelled by how people sign in: Each person signs in, Needs an OAuth app or No sign-in. Choosing one fills in its name, ID, address and sign-in. Custom server starts empty.

The form has three parts:

  • Server. A Name, an ID such as gmail (it cannot be changed later) and the Server URL.
  • Sign-in. One of the three kinds. A Shared key takes the key and, optionally, its header. For a server marked Needs an OAuth app, the provider does not let Hubzoid register itself: create an OAuth app there with the redirect URI Settings shows, then enter its client ID and client secret. Scopes are optional.
  • Tools. The allowed tools. Leave the list empty to allow every tool the server offers.

Secrets entered in the form are saved in the deployment's .env and referenced from the file, never shown again.

Test it

Test reads the server's metadata. It registers nothing and changes no settings. It answers Test passed or Test failed.

What Test shows, and loading a server's tools

The answer shows the authorization server, whether Hubzoid can register itself, and the redirect URI to register with a provider that needs a client created in advance: <public origin>/oauth/connectors/<id>/callback.

It also lists the server's tools and marks which reach agents. For Each person signs in it lists them through your own connection, so connect your account first. When you edit a saved connector, Load from server lists its tools to pick from. The list of popular servers holds addresses only, so run Test before people use one.

Grant it and connect

Grant Connect or Use and the connector's name on the agent's Access tab, in the Connectors group. See Restrict a tool for the steps. For Each person signs in and No sign-in, each person then connects once.

Addresses Hubzoid builds redirect URIs from

Set HUBZOID_PUBLIC_URL whenever people reach Hubzoid at an address other than this computer, and list any other addresses in HUBZOID_ALLOWED_ORIGINS. Redirect URIs are built only from a configured address or from localhost, never from the host a request happens to carry.

Google Workspace connectors

Gmail, Google Calendar, Drive, Docs and Sheets are in the list. Google's Workspace MCP servers are a Google Developer Preview.

Google Cloud setup, scopes and renewal

Each needs its API enabled in Google Cloud and a Web application OAuth client with the redirect URI Settings shows.

Their entries fill in read-only scopes and, as allowed tools, the server's read-only tools. Without scopes Hubzoid asks for every scope a server lists, which for Google includes full access. Hubzoid asks Google for offline access, so a connection renews itself instead of ending after an hour.

Change or remove a connector

Only organization administrators change connectors in Settings.

What other managers see, and how to offer, stop offering, delete and edit

Other agent managers see the Hub folder's servers and are told that administrators add connectors.

  • Offer an existing connector offers a connector that another agent already has.
  • Stop offering takes it out of one agent and removes its grants there. People's connections and other agents stay.
  • Delete connector removes it from every agent. Connected people lose their connections, which are revoked at the provider where it supports revocation.
  • Changing a connector's URL or sign-in removes everyone's connection to it, and a Shared key must be entered again, so a credential is never sent to a server other than the one it was meant for.

Tool names

On every runtime a connector's tools are named mcp__my_<id>__<tool>.

How names change on OpenAI runtimes

OpenAI accepts only letters, digits, _ and - in a tool name, up to 64 characters, so on the OpenAI Agents and Codex runtimes other characters become _ and the name ends in a short hash. A name that is too long is cut and also ends in a hash. Two tools never share a name. Claude keeps names as the server spells them.

MCP servers in the Hub folder

Servers in connectors/.mcp.json are part of the Hub's files. Use them for a local server started as a command, or for a remote server that everyone reaches with one credential. The format is the familiar mcpServers map, and the starter's file is empty.

Example .mcp.json with a local and a remote server
my-hub/connectors/.mcp.json
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GH_TOKEN}" }
    },
    "tickets": {
      "transport": "streamable-http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer ${TICKETS_TOKEN}" },
      "client_session_timeout_seconds": 60
    }
  }
}

Every person who uses the Hub reaches these servers with the same credential. Give that credential the narrowest scope the task needs, for example read-only. Changes take effect when the Hub restarts.

Transports, secrets, timeouts, names and reach
  • Local servers use command, args and env, and run as child processes. Pass secrets in env, because a secret in args is visible in the process list.
  • Remote servers use url and headers. Set "transport": "streamable-http" (or "http") for a Streamable HTTP server. A server with a url and no transport is treated as SSE.
  • Secrets. ${NAME} in any string is replaced with that variable after the Hub's .env is loaded. Keep tokens in .env, never in the JSON file.
  • Timeouts. One tool call may take 30 seconds on the OpenAI Agents and Codex runtimes. HUBZOID_MCP_TIMEOUT changes that for the Hub, and client_session_timeout_seconds in a server's entry wins for that server. On claude-local the claude CLI times calls with its own default, and an explicit HUBZOID_MCP_TIMEOUT is passed on to it.
  • Names. On claude-local the model sees mcp__<server>__<tool>, for example mcp__github__create_issue. On the other runtimes each tool keeps its own name.
  • Reach. These servers serve chat, markdown tasks, code workflow agent calls and evals. The Hub's own MCP server does not relay them to MCP clients.
  • In Settings. The agent's Connectors tab lists them read-only under From the hub folder: the name, whether it is a local command or a URL, and the environment variable names they use with whether each is set. Values, headers, arguments and URLs are never shown. Local command servers stay in the file, because adding a command from a browser would let any administrator run it on the server.

Per-user MCP servers

For services where each person must act as themselves, such as their own mailbox or ticket tracker, add a connector with sign_in: oauth (Each person signs in). Each person connects their own account once through Hubzoid's OAuth sign-in. From then on the agent reaches the server with that person's token on their turns, on all three runtimes, and so does a workflow run that acts as them.

Connect an account

People connect from either place:

  • Settings → Connected accounts, which lists each app with its status and the agents that use it.
  • A link from the agent. See connect from chat below.
Settings, Connected accounts: outside accounts you signed in to for your agents, only you can use them. Nothing to connect yet, ask your administrator for an app to connect.Settings, Connected accounts: outside accounts you signed in to for your agents, only you can use them. Nothing to connect yet, ask your administrator for an app to connect.
Each person connects their own accounts here once they are granted a connector.
What the link page shows and how tokens are kept

Before anything happens, the link page shows the account, the agent, the server's host and any scopes the connector requests. Disconnect asks first, and revokes the token at the provider when the provider supports it.

Tokens are encrypted with the deployment key and refreshed before they expire, one refresh at a time across the deployment. A refresh the provider refuses marks the connection as expired, and the person connects again. A provider that cannot be reached marks it as an error, and the next turn tries again.

Rules on every runtime

A connector is used in a turn only when the agent offers it and the person holds its capability there. The token never enters the prompt, a log line or a tool result.

Grant, tools, shadowing, surfaces, scope and credentials
  • Grant. A connector is used in a turn only when the agent offers it and the person holds its capability there. If the access store cannot be read, no connector is used that turn.
  • Allowed tools. The connector's allowed tools apply.
  • No shadowing. A connector never replaces a Hub tool. When names clash, that server is left out for the turn and a warning is logged. A server that cannot be reached is also left out, and the turn goes on.
  • Surfaces. A connection is used only on surfaces allowed to reach restricted tools (HUBZOID_RESTRICTED_SURFACES). A shared Slack channel never carries the token of the person who mentioned the agent.
  • Scope. Only the main agent gets connectors. Delegates do not. People who have not connected get nothing extra.
  • Credentials. The token travels only in the MCP client's request header. It never enters the prompt, a log line or a tool result.

Connect from chat

A person can ask the agent to connect an app, for example "connect my Gmail", in the web app or WhatsApp. The agent has a connect_account tool on every runtime.

Listing apps, turning it off, how the connect link works and what it needs

Called without an app, it lists what the person can connect in this agent, with each one's status.

It is on by default. HUBZOID_CONNECT_JOURNEY=false in the Hub's .env removes the tool.

  • The tool checks the person's capability for the connector, then answers that the app is already connected or sends a personal link to /portal/connect/<id>, never a provider URL.
  • The link works only for the person who asked, signed in, and expires after HUBZOID_CONNECT_TTL seconds (600 by default, from 60 to 3600). A signed-out person signs in and comes back to the link. Anyone else gets "This link is for another account".
  • After the provider's consent screen, a Hubzoid page confirms the result from Hubzoid's own records, never from the return address. On WhatsApp the person also gets a confirmation message and can continue the request that was waiting.
  • It needs the surface in HUBZOID_RESTRICTED_SURFACES (add whatsapp for WhatsApp) and HUBZOID_PUBLIC_URL set to the address people open.
  • A Shared key connector has nothing to connect, and the agent says so.

Limits

Not every OAuth provider can be connected with Each person signs in.

Provider requirements and token limits
  • A provider needs dynamic client registration, or a client registered in advance that authenticates with a client secret or with PKCE and no secret. private_key_jwt and provider-specific authorization parameters are not supported.
  • A server that publishes no OAuth authorization server metadata cannot be connected with Each person signs in.
  • An access token without an expiry is not checked between turns. If the provider revokes it, the person connects again.

The shared browser

HUBZOID_BROWSER=true gives the Hub one shared, resource-limited browser, exposed as the Playwright MCP tools (browser_navigate, browser_click, browser_snapshot and the rest).

Reach, direct and pooled modes, setup and memory

One browser serves every agent in the Hub, so several agents do not mean several browsers. Nothing else needs configuring: Hubzoid adds a playwright entry to the Hub's MCP servers, and an entry of your own with that name replaces it.

The browser tools serve chat, tasks and workflows, and are not relayed to MCP clients. Every browser variable is listed in configuration.

It runs in one of two modes:

ModeWhenWhat you get
DirectOnly HUBZOID_BROWSER=true is setHubzoid starts the Playwright MCP sidecar with npx on port 8931 and stops it with the Hub. One headless browser, with a separate in-memory context per session. Optional memory watchdog with HUBZOID_BROWSER_MAX_RSS_MB.
PooledHUBZOID_BROWSER_CDP_URL points at a browserless poolThe sidecar attaches to the pool, which enforces hard limits: a fixed number of parallel sessions, a queue for the rest, a timeout for stuck sessions and a memory ceiling. Recommended for production.

Direct mode needs Node on the host and the Playwright Chromium build, installed once:

Terminal
npx -p @playwright/mcp@latest playwright install chromium

Without npx, the Hub starts without browser tools and logs a warning. For production, the shipped compose file runs the pool and the sidecar and points the Hub at them:

Terminal
docker compose -f docker/docker-compose.yml -f docker/browser-compose.yml up

Memory grows with the number of browser sessions active at the same time, not with the number of agents, so HUBZOID_BROWSER_CONCURRENT is the setting that bounds it and HUBZOID_BROWSER_MEMORY is the hard ceiling.

When to restrict a tool instead

Put a tool in restricted/<permission>.py instead of tools_local/ when it reads sensitive data or takes an action that only some people, or some workflows, should trigger. The full walkthrough is in restrict tools.

How the code and permission name work, and what a restricted tool adds

The code is the same @function_tool. The file name is the permission: every tool in restricted/finance.py needs finance.

  • Every call is checked against the caller's permission before the tool runs, and recorded in the audit log. On every runtime the agent does not even see the tool unless the caller holds the permission.
  • Surfaces without a verified personal login, such as Slack channels, never reach it.
  • Secrets go in restricted/.env, which the model cannot read through the file tools.
  • A workflow reaches it only when the account it runs as holds the permission. See workflows and schedules.

If something goes wrong

What you seeWhat to do
The agent does not call your new toolRestart hubzoid run, since tools load at start. Then make the docstring say when to use it, or name the tool in AGENTS.md.
hubzoid doctor names a file in tools_local/ with an errorThat file failed to import, often a missing package. Install it in the Hub's environment or fix the error, then restart.
never called: days_since_delivery in an evalThe agent answered without the tool. Make the instruction to use it clearer in the docstring or in AGENTS.md.
A Hub folder server is never calledCheck its transport. A server with a url and no transport is treated as SSE. Restart after changing .mcp.json.
More problems and fixes
What you seeWhat to do
Add connector is missing in Settings, and the tab says changing files from the browser is offBrowser editing is off. Add the connector as a file in connectors/, or turn on browser editing.
A log line such as connectors: orders.yaml: unknown key 'tool'.Fix the file. Hubzoid leaves an invalid connector out.
A log line saying the shared key of a connector is not setAdd the variable its shared_key: env:NAME names to the .env.
Connected accounts says there is nothing to connectNobody has granted you a connector in this agent yet. An administrator grants it on the agent's Access tab.

Next steps

Choose which cookies Hubzoid can use. You can change this at any time from Cookie settings in the footer. Read the Cookie Policy for details.