Typed tools for agents. Governed by default.
Curated connectors for agents built on Algen Agent Runtime. Each connector is a set of narrow, typed actions with a declared side effect, permission and approval rule, registered as a Runtime plugin so Studio can list it before anything runs.
Where Tools fits#
Runtime runs agents; Tools gives them safe ways to touch the outside world. A connector's tools reach Runtime through a declared plugin, Studio lists them from algen-agent-runtime describe, and the same agent.yaml deploys anywhere.
| Layer | Package | Contains |
|---|---|---|
| Core | algen-agent-runtime | Contracts, @tool, presets, Toolkit, plugins, describe, the MCP and HTTP adapters. |
| Connectors | algen-agent-tools | Generic toolkits for databases, email, chat, documents, webhooks and MCP servers, one optional extra each. |
| Domain packs | Their own packages | Product-specific tools such as hiring, HR or text-to-SQL. |
Why a separate package#
Narrow, typed actions
No free-form SQL, URLs, shell commands or vendor request bodies. The model fills typed fields; the connector does the rest.
Visible before it runs
Every tool's side effect, permission, timeout and environment variables appear in describe and Studio without running project code.
MCP, reviewed
A governance overlay turns a remote MCP server into declared, schema-pinned tools; anything undeclared is never exposed.
Curated and owned
Each connector has an owner, a security review and its own release cadence, so the Runtime core stays small.
Pre-release. 0.2.0a10 is an alpha. A tool's name, input schema, side effect and permission are public contract; any change ships with a CHANGELOG entry and an upgrade note.
Install#
Install the extra for each connector you use. The package pulls in a supported algen-agent-runtime.
$ python -m pip install --pre 'algen-agent-tools[postgres]'
$ python -m pip install --pre 'algen-agent-tools[all]'
$ git clone https://github.com/AlgenAI/algen-agent-tools.git $ cd algen-agent-tools $ python3.12 -m venv .venv && source .venv/bin/activate $ python -m pip install -e '.[dev,all]'
Extras#
| Extra | Connector | Installs |
|---|---|---|
postgres | PostgreSQL | asyncpg |
mysql | MySQL / MariaDB | aiomysql[rsa] (with cryptography, for MySQL 8's default authentication) |
sqlite | SQLite | nothing extra (standard library) |
smtp | SMTP | nothing extra (standard library) |
gmail | Gmail | nothing extra (uses the Runtime's HTTP client) |
outlook | Outlook (Microsoft 365) | nothing extra (uses the Runtime's HTTP client) |
imap | IMAP | nothing extra (standard library) |
telegram | Telegram | nothing extra (uses the Runtime's HTTP client) |
discord | Discord | nothing extra (uses the Runtime's HTTP client) |
github | GitHub (MCP overlay) | the Runtime's mcp extra |
documents | Documents | PyMuPDF 1.x, for PDFs (AGPL-3.0); every other format needs nothing |
msg | Documents (Outlook .msg) | olefile (BSD), for Outlook .msg files; Tools parses the message itself |
ocr | Documents (OCR) | pytesseract and Pillow, for TesseractRecognizer; the host installs the Tesseract program and its languages |
webhook | Webhook | nothing extra (uses the Runtime's HTTP client) |
mcp | MCP overlays | the Runtime's mcp extra |
all | everything above | includes PyMuPDF (AGPL-3.0) through documents; install only the extras you use if you cannot ship AGPL code |
Each connector imports its dependency only when its client first opens. If the extra is missing, the error names it, for example pip install "algen-agent-tools[postgres]".
Quickstart: add a connector to an agent#
No code is needed for the built-in connectors. Name the plugin, trust the package, enable the tools and grant their permissions.
- Declare the plugin in
agent.yamlagent.yamlagents: - name: assistant version: 1.0.0 description: Checks that tools are wired up system_instructions: Use sample.echo when asked to echo text. default_model: {name: m, provider: demo} domain_plugins: [algen_agent_tools.sample] # the connector's plugin enabled_tools: [sample.echo] # the tools this agent may call tool_permissions: [sample.read] # and their permissions security: trusted_plugin_prefixes: [algen_agent_tools] - Validate it the way Studio will
zsh
$ algen-agent-runtime check agent.yaml $ algen-agent-runtime describe --config agent.yaml --json
The first command fails on an unknown tool name and suggests the closest match.
describelists each tool with its side effect, permissions, timeout and the environment variables it needs. - Set the connector's environment variables
Each connector reads its settings from
env://references when its client first opens, never at import. See Secrets and environment.
In Studio, use Add connector instead of writing this by hand. The YAML above is what it writes.
In Studio: add a connector#
When your Studio offers the connector catalog, open the project's Connections tab, go to Tools & knowledge → Connectors, and choose Add on a connector. You can also ask azAI ("let the assistant read my Gmail"); it adds the connector the same way and asks first.
- Choose the agents and tools. The connector's default tools are selected. The preview shows what changes: the plugin, the tools and only the permissions they need, the trusted
algen_agent_toolsprefix, and the sites the connector calls (added to the allowed HTTP hosts, where you can see them). - Fill in its settings. Secrets are saved encrypted and never shown again. A database connector's server must also be declared under Organization → Database hosts before hosted runs reach it.
- Build, if Studio says so. Until a build describes the connector, its tools show as Pending build. Where Studio runs catalogued connectors on its own image, no build is needed.
Each connector card shows its maturity (alpha connectors carry a badge), its tools with their side effects, and options only code can set. SMTP cannot run on hosted Studio yet; run it from a local Studio or a deployment. When Studio moves to a newer Tools version, the Connectors section offers Review upgrade, which lists removed tools, new permissions and new settings before anything changes.
Studio's Connectors list comes from the connector catalog of the Tools version Studio ships with, which can be older than the one this page documents. A connector added in a later release appears there once Studio moves to that release.
How it works#
- A connector is a Runtime
Toolkit. Its tools are named<toolkit>.<action>and their permissions sit under the toolkit's prefix (postgres.read,smtp.write). - Governance comes from a Runtime preset.
read,write,external,destructiveorlong_runningsets the side effect, idempotency, retries, timeout and approval default. Connectors never hand-set them. - A plugin registers it. Each connector ships a plugin in the
algen_agent_runtime.pluginsentry-point group under the trusted prefixalgen_agent_tools. Registering opens nothing and resolves no secret. - The client opens late. The toolkit's client opens at container start or on first use, resolves its
env://secrets then, and is closed with the container.
What's new#
0.2.0a10 · 5 November 2026#
- Outlook
.msgfiles. With the newmsgextra (olefile),documents.extract_textreads.msgfiles like.eml, with the same limits; a message whose only body is RTF reportsdocument.body_unread. See Email files. - Quoted history.
include="latest"onget_messageandget_thread(Gmail, Outlook and IMAP) returns the sender's own text without the quoted history or signature, and setsquoted_text_removed. Gmail and IMAP bodies join every inline text part (at most five). See Quoted history. - Content over names. A file whose content names another type is read as what it is: a JPEG named
.pdf, a.docxrenamed.doc. Text, CSV, TSV, Markdown and HTML fall back from UTF-8 to Windows-1252, and read UTF-16 with a byte-order mark. - OpenDocument and RTF. ODT, ODS and ODP, and RTF, with the Office rules for hidden content. An embedded OLE object in RTF is the new security finding
document.embedded_object. See OpenDocument and RTF. - Changed:
get_threaddefaults toinclude="latest". Each message's body is the sender's own text; passinclude="full"for whole bodies, as 0.2.0a9 returned. - Changed: long PDFs are read, not refused. A PDF over 50 pages is read up to page 50 and reports
document.pages_skipped; the result ispartialandtruncated. - Requires
algen-agent-runtime>=0.1.0a23,<0.2(tested on 0.1.0a23).
0.2.0a9 · 5 November 2026#
- Whole threads.
get_threadon Gmail, Outlook and IMAP returns a thread's messages oldest first, at most 50, keeping the newest.get_messageresults gainmessage_id_header,in_reply_toandreferences. - An attachment's text in one call. Built with
documents=DocumentReader(), the Gmail, Outlook and IMAP toolkits offerread_attachment: it downloads the attachment, checks it and returns its inspected text, so the file never passes through the model. See Reading attachments. - Email files.
documents.extract_textreads.emlmessages with their attachments; results gainattachments, and a finding in an attachment names it inpart. - IMAP search and folders.
imap.list_messagestakestext_containsand, on Gmail,gmail_query; the folders setting accepts special-use names such as\Sentand\All;imap.list_folderslists the readable folders. On a plain IMAP serverthread_idis no longer empty. - Requires
algen-agent-runtime>=0.1.0a23,<0.2(tested on 0.1.0a23).
0.2.0a8 · 5 November 2026#
- Unread is not risky. A scanned PDF, an image-only DOCX, a cover note over a scan or an XLSX past the row limit is now
passwithcoveragenoneorpartial, where it wasreview. Code that treatedreviewas "not fully read" checkscoverage != "full"(orneeds_ocr) instead; see Unread content. - OCR, when you want it. Pass a
TextRecognizertodocuments_toolkit(ocr=...)orextractor_forto read images and scanned PDF pages. The newocrextra shipsTesseractRecognizer; OCR is never on by default. See OCR. - Known limitations. Images inside DOCX and PPTX are not recognized, and a recognizer still running when the 120 s timeout fires finishes in its worker thread.
- Requires
algen-agent-runtime>=0.1.0a23,<0.2(tested on 0.1.0a23). Runtime 0.1.0a22 and 0.1.0a23 fix two ways an approval-required workflow tool step could run without its approval; no tool changes are needed.
0.2.0a7 · 31 October 2026#
- Refusals are not "may have happened". A refusal before anything is sent (a token that could not be fetched, an unconfigured host, an attachment refused or over the limit, invalid MCP arguments, a too-large webhook payload) and a provider's definite rejection (a 4xx other than 408 from Graph, Gmail, Telegram, Discord or a webhook receiver; an SMTP connection, login, sender, recipient or data refusal) are recorded as not attempted, so a retry runs instead of manual reconciliation. Timeouts, dropped connections and 5xx stay indeterminate. Errors keep their type and message.
- Reply to what was read. Gmail, Outlook and IMAP results carry
sender_address,sender_name,recipient_addressesandcc_addresses, declaredformat: email, so the defaultpiiguardrail keeps them; see Email and guardrails. - Document inspection lives here. The extractors moved from the Runtime to
algen_agent_tools.documents(extractor_for,ExtractedDocument, …); thedocumentsextra installs PyMuPDF directly. DOCX and PPTX with text and images now returnreview.runtime_attachment_loaderreads the run's own attachments; see Documents. - Fixed. An MCP overlay session closed from another task no longer fails;
OAuthRefreshuses a rotated refresh token, andon_refresh_token=on the Gmail and Outlook clients stores it. - Requires
algen-agent-runtime>=0.1.0a20,<0.2(tested on 0.1.0a20 and 0.1.0a21). Runtime 0.1.0a19 fixed thesession-scoped tool cache.
0.2.0a6 · 30 October 2026#
- The caller's rows.
context_paramsonquery()for PostgreSQL, MySQL and SQLite binds the call'stenant_id,user_id,run_idorstep_id, which the model cannot choose. The tool declares them as itscache_context, so the Runtime's tool-result cache never serves one caller's rows to another. See The caller's rows. - More document formats.
documents.extract_textreads Excel (XLSX), PowerPoint (PPTX), HTML, CSV and TSV as well as PDF, DOCX, text and Markdown, with hidden sheets, slides, shapes and HTML elements reported rather than returned.truncatedis also true when the inspection cut the text. - Requires
algen-agent-runtime>=0.1.0a18,<0.2. Don't scope the tool-result cache tosession: on Runtime 0.1.0a18 it fails every cacheable tool call.
0.2.0a5 · 28 October 2026#
- IMAP: read one mailbox with an app password, read-only (nothing is marked as seen), with typed filters instead of raw IMAP search.
sender_auth(pass,failorunknown) on inbound mail: the receiving server's SPF, DKIM and DMARC verdict, filled by Gmail and IMAP.
0.2.0a4 · 27 October 2026#
- MySQL / MariaDB and SQLite on a shared SQL core, with the same typed queries as PostgreSQL.
- GitHub: the first shipped MCP overlay, 37 reviewed tools on GitHub's hosted MCP server.
- Webhook: typed, signed events to URLs the deployment fixes.
0.2.0a3 · 27 October 2026#
- Telegram and Discord bots on one shared messaging toolkit, limited to an allowlist of conversations.
python -m algen_agent_tools.mcp check <overlay>reports drift between an overlay and its live server.
0.2.0a2 · 27 October 2026#
- Outlook (Microsoft 365) through Microsoft Graph, delegated or application access.
gmail.get_message, andget_attachmentfor Gmail and Outlook when the toolkit has an artifact sink.- Documents:
documents.extract_textwith the Runtime's document inspection.
0.2.0a1 · 27 October 2026#
- The connector catalog,
connectors.json, shipped in the wheel: every connector's settings, egress hosts and tool contracts, which Studio reads to list and enable connectors without running their code. - Shared attachment handling (
AttachmentPolicy, artifact loaders and sinks), andALGEN_TOOLS_SMTP_ALLOWED_DOMAINS/ALGEN_TOOLS_GMAIL_ALLOWED_DOMAINSto bound recipients without code.
0.1.0a2 · 27 October 2026#
- Email under the default guardrails. Address fields in
smtp.send_emailandgmail.send_emaildeclareformat: email, so the Runtime'spiiguardrail leaves recipients alone and still screens the rest of the message. Agents no longer need atool_policiessetting to send; see Email and guardrails. - Requires
algen-agent-runtime>=0.1.0a12,<0.2.
0.1.0a1 · 27 October 2026#
- First release, supporting
algen-agent-runtime>=0.1.0a11,<0.2. - Connectors: PostgreSQL (typed queries and catalog reads), SMTP and Gmail (governed sending, inbox listing), and the dependency-free sample toolkit.
- MCP governance overlays with schema pinning, deny-by-default and a
snapshotcommand. - Contract harness:
assert_toolkit_contract,describe_plugin, and a fake for every connector. - Replaces the Runtime's deprecated
communicationsemail tools anddatabase_tool; see Migrating from Runtime.
PostgreSQL#
Connect an agent to one PostgreSQL (Postgres) database through typed actions. The model never writes SQL: each tool is a named, parameterised query a developer declares in code, and the model fills in its typed parameters.
| Plugin | algen_agent_tools.postgres |
| Extra | postgres (asyncpg) |
| Environment | ALGEN_TOOLS_POSTGRES_DSN |
Zero-configuration tools#
| Tool | Side effect | Permission | Returns |
|---|---|---|---|
postgres.list_tables | read | postgres.read | Tables and views in the public schema with approximate row counts. |
postgres.describe_table | read | postgres.read | Columns: type, nullability, default, primary key. |
Declaring typed queries#
Most agents need the database's own actions. Declare them in your project's plugin:
from pydantic import BaseModel, Field
from algen_agent_runtime.tools.authoring import write
from algen_agent_tools.postgres import PostgresToolkit
class CustomerInvoices(BaseModel):
customer_id: str = Field(min_length=1)
due_before: date
billing = PostgresToolkit("billing", dsn="env://BILLING_DATABASE_URL")
billing.query(
"list_open_invoices",
"SELECT id, amount, due FROM invoices "
"WHERE customer_id = $1 AND status = 'open' AND due < $2 ORDER BY due",
description="List one customer's open invoices that fall due before a date.",
params=CustomerInvoices,
row=Invoice,
)
billing.query(
"add_note",
"INSERT INTO invoice_notes (invoice_id, text, request_key) VALUES ($1, $2, $3) "
"ON CONFLICT (request_key) DO NOTHING",
description="Add a note to an invoice.",
params=NewNote,
preset=write(),
idempotency_key=True, # the call's key is bound as the last parameter
)paramsfields fill$1,$2, … in field order; a query whose placeholders do not match is refused when it is declared. Unknown input fields are rejected.- A read returns
list[row]. A write returnslist[row]withRETURNING, otherwise{"rows_affected": n}. presetis any Runtime preset:read()(default),write(approval=...),destructive()(approval on by default),long_running(...).postgres_toolkit(name, dsn=..., schemas=(...))builds a toolkit that already has the two catalog reads, limited to the schemas you list.
The caller's rows#
In a shared, multi-tenant database, bind the caller's identity with context_params instead of asking the model for it:
billing.query(
"my_invoices",
"SELECT id, amount, due FROM invoices "
"WHERE due < $1 AND tenant_id = $2 AND owner_id = $3 ORDER BY due",
description="List the signed-in user's invoices that fall due before a date.",
params=DueBefore, # $1, from the model
row=Invoice,
context_params=("tenant_id", "user_id"), # $2, $3, from the call
)- The values are
ToolContext.tenant_id,user_id,run_idandstep_id, which the Runtime sets from the run's authenticated request. Nothing else can be bound. - They are not in the tool's input schema, and a call that sends one is rejected. A
paramsfield whose name or alias is a context value is refused when the query is declared. - Placeholders fill in this order:
paramsfields, thencontext_paramsin the order given, then the idempotency key. - Caching: the tool declares the bound values as its
cache_context, so the Runtime's tool-result cache keys each result on them and never serves one caller's rows to another, at any cache scope. Don't use thesessionscope for tool results: on Runtime 0.1.0a18 it fails every cacheable call. - They narrow rows; they don't replace privileges. Keep a least-privilege role, and use row-level security where the database has it.
The same argument works for MySQL (%s) and SQLite (?) toolkits: all three share algen_agent_tools.sql, and each database adds only a dialect for its placeholders, read-only reads, transactions and timeouts.
Guarantees#
- No SQL from the model. Inputs are typed fields; values are always bound parameters.
- Reads cannot write. A read runs in a
READ ONLYtransaction, so PostgreSQL itself refuses anUPDATEhidden in one. - Bounded. Every statement runs under
SET LOCAL statement_timeoutequal to the tool's timeout. Reads stream through a cursor and fail withTooManyRows, rather than silently truncate, past the row limit (500 by default). - Writes are keyed and tried once. With
idempotency_key=Truethe SQL can be exactly-once. - The caller's identity comes from the call.
context_paramsvalues are never tool input, and the result cache keys on them. - Catalog reads are limited to the schemas you list and to tables the role can
SELECT.
Connect with a database role that has only the privileges the declared queries need.
MySQL / MariaDB#
Read a MySQL or MariaDB database through typed tools. Your own queries are declared in code exactly as for PostgreSQL, with %s placeholders (a literal % is %%).
| Plugin | algen_agent_tools.mysql |
| Extra | mysql (aiomysql[rsa]) |
| Environment | ALGEN_TOOLS_MYSQL_DSN (secret): mysql://user:password@host:port/database, with ?ssl=verify for verified TLS |
| Tool | Side effect | Permission | Timeout | Attempts | Approval |
|---|---|---|---|---|---|
mysql.list_tables | read | mysql.read | 30 s | Runtime default | agent policy |
mysql.describe_table | read | mysql.read | 30 s | Runtime default | agent policy |
- Reads run in
START TRANSACTION READ ONLY, so a declared read that writes fails. - Timeouts are enforced three ways: on the server (
max_execution_timeon MySQL,max_statement_timeon MariaDB, detected at connect), on lock waits (innodb_lock_wait_timeout), and by a client-side deadline that drops a stuck connection. - Writes run in their own transaction. With
idempotency_key=True,INSERT IGNOREorON DUPLICATE KEY UPDATEon a unique key makes a write exactly-once. - Errors carry only the MySQL error number, never the server's message, which can quote data.
SQLite#
Read a SQLite database file through typed tools, with your own queries declared in code using plain ? placeholders. Numbered and named placeholders are refused, so the order can't differ from the model's fields.
| Plugin | algen_agent_tools.sqlite |
| Extra | sqlite (no extra packages) |
| Environment | ALGEN_TOOLS_SQLITE_PATH |
| Tool | Side effect | Permission | Timeout | Attempts | Approval |
|---|---|---|---|---|---|
sqlite.list_tables | read | sqlite.read | 30 s | Runtime default | agent policy |
sqlite.describe_table | read | sqlite.read | 30 s | Runtime default | agent policy |
- Read-only by default. The file opens with
mode=ro, and declaring a write fails unless the toolkit is built withread_only=False. Reads run withPRAGMA query_only. - Timeouts: SQLite has no statement timeout, so a progress handler interrupts a statement that runs past the tool's timeout.
- The file must already exist; the connector never creates one. SQLite suits small, local, read-mostly data. For concurrent writers, use PostgreSQL.
SMTP#
Send email over SMTP from the deployment's own address.
| Plugin | algen_agent_tools.smtp |
| Extra | smtp (no extra packages) |
| Environment | ALGEN_TOOLS_SMTP_HOST, ALGEN_TOOLS_SMTP_SENDER (required); ALGEN_TOOLS_SMTP_PORT (587), ALGEN_TOOLS_SMTP_SECURITY (starttls), ALGEN_TOOLS_SMTP_USERNAME, ALGEN_TOOLS_SMTP_PASSWORD |
| Tool | Side effect | Permission | Timeout | Attempts | Approval |
|---|---|---|---|---|---|
smtp.send_email | external | smtp.write | 60 s | 1 | agent policy (asks by default) |
smtp.send_email takes to (1 to 50), cc, bcc, reply_to, subject, and text_body and/or html_body. It returns the delivery receipt: provider, Message-ID, accepted and rejected recipients.
Sending guarantees (SMTP and Gmail)#
- The sender is the deployment's. The model cannot choose the From address or add headers.
- Header-safe. Line breaks in addresses, names or the subject are refused; recipients must be unique, at most 100 in all.
- One message per idempotency key. The key becomes a deterministic Message-ID and the tool is tried once, so a replay does not send twice.
- Bounded audience. With
allowed_domains, every recipient and the Reply-To address must be in those domains or their subdomains. - TLS by default.
securityisstarttlsortls;plainis allowed only to loopback. - Content is not captured in audit metadata, and errors name only the failure type.
Attachments are offered only when you configure an artifact loader; see Configure in code.
Gmail#
Read and send mail for one Gmail or Google Workspace mailbox, through the Gmail API with an OAuth refresh token.
| Plugin | algen_agent_tools.gmail |
| Extra | gmail (no extra packages) |
| Environment | ALGEN_TOOLS_GMAIL_CLIENT_ID, ALGEN_TOOLS_GMAIL_CLIENT_SECRET, ALGEN_TOOLS_GMAIL_REFRESH_TOKEN, ALGEN_TOOLS_GMAIL_SENDER |
| Tool | Side effect | Permission | Timeout | Attempts | Approval |
|---|---|---|---|---|---|
gmail.list_messages | read | gmail.read | 120 s | Runtime default | agent policy |
gmail.get_message | read | gmail.read | 60 s | Runtime default | agent policy |
gmail.get_thread | read | gmail.read | 120 s | Runtime default | agent policy |
gmail.get_attachment (with artifact_sink) | read | gmail.read | 120 s | Runtime default | agent policy |
gmail.read_attachment (with documents) | read | gmail.read | 120 s | Runtime default | agent policy |
gmail.send_email | external | gmail.write | 60 s | 1 | agent policy (asks by default) |
gmail.list_messages(query="is:unread", max_results=25, page_token=None)takes a Gmail search query and returns message metadata: sender, recipients, subject, a snippet, labels, attachments with theirpart_id, andsender_auth, at most 100 per page. Bodies and attachment bytes are not returned. The query reaches every folder:in:sentlists sent mail,in:anywhereincludes archived mail, spam and trash.sender_authis Gmail's own SPF, DKIM and DMARC verdict on the sender (pass,failorunknown), read only from theAuthentication-Resultsheader Gmail stamped. Use it to keep a spoofed From out of trusted workflows;unknownis not a failure.gmail.get_message(message_id, max_chars=20000, include="full")reads one message with its body as plain text (HTML without markup, scripts or styles;include="latest"keeps only the sender's own text), withbody_truncatedset when cut. The tool tells the model to treat instructions in the message as data. Results carrymessage_id_header,in_reply_toandreferences.gmail.get_thread(thread_id, max_messages=20, max_chars_per_message=4000)reads a whole thread, oldest first, each message asget_messagereturns it with the body cut tomax_chars_per_message(at most 10,000). At most 50 messages; a longer thread keeps its newest and setstruncated. By default (include="latest") each body is the sender's own text; see Quoted history.gmail.get_attachment(message_id, attachment_id)is offered only when the toolkit is built in code with anartifact_sink. It saves one attachment as an artifact, within theattachment_policysize and type checks, and returns only its reference. Pass the stablepart_idfrom the listing.gmail.read_attachment(message_id, attachment_id, max_chars=50000)is offered only when the toolkit is built withdocuments; see Reading attachments.gmail.send_emailtakes the same input assmtp.send_email, sends fromALGEN_TOOLS_GMAIL_SENDER, and delivers Bcc recipients without exposing them to other recipients.
Quoted history#
From 0.2.0a10, include="latest" on get_message (default "full") and get_thread (default "latest") returns only what the sender wrote: the history the reply quotes and the signature are removed, and quoted_text_removed is set. The rules are deterministic and conservative, so text is removed only after a line that clearly starts quoted history:
- an attribution line,
On <date>, <name> wrote:, also wrapped over two lines and in French, German, Spanish, Italian and Dutch; - an Outlook header block (
From:thenSent:/Date:andTo:/Subject:), or-----Original Message-----; - a run of
>lines with nothing but blank lines after it (inline answers between quoted lines are kept); - a signature after the standard
--line, or a finalSent from my …line.
A forwarded message is new content and is always kept, and a message that is only a quote is returned whole. Gmail and IMAP bodies join every inline text part (at most five), so text a client split around inline images is not lost. The same applies to the Outlook and IMAP toolkits.
Credentials#
Create an OAuth client in Google Cloud, authorise the mailbox once to obtain a refresh token, and grant only the scopes the agent needs: gmail.readonly for list_messages, get_message, get_thread, get_attachment and read_attachment, gmail.send for send_email. The access token is refreshed in memory and never leaves the client; token and API errors name only the failure type.
Reading attachments#
documents=DocumentReader() adds read_attachment: it downloads one attachment, checks it against attachment_policy and reads it with the Documents inspection, so the model gets the attachment's text in one call instead of get_attachment then documents.extract_text. The result is the same as documents.extract_text (text, disposition, findings, coverage, attachments for an attached email) plus artifact; a quarantined attachment's text is withheld. With an artifact_sink as well, the file is also saved and its reference returned, for example so a person can review a quarantined file. DocumentReader(max_bytes=..., quarantine=..., ocr=...) takes the documents toolkit's options. The same option exists on the Outlook and IMAP toolkits.
from algen_agent_tools.documents import DocumentReader
from algen_agent_tools.gmail import gmail_toolkit
gmail = gmail_toolkit(documents=DocumentReader()) # adds gmail.read_attachment; PDFs need the documents extraOutlook (Microsoft 365)#
Read and send mail for one Outlook mailbox through Microsoft Graph, with an Entra app registration.
| Plugin | algen_agent_tools.outlook |
| Extra | outlook (no extra packages) |
| Environment | ALGEN_TOOLS_OUTLOOK_TENANT_ID, ALGEN_TOOLS_OUTLOOK_CLIENT_ID, ALGEN_TOOLS_OUTLOOK_CLIENT_SECRET, ALGEN_TOOLS_OUTLOOK_MAILBOX; optional ALGEN_TOOLS_OUTLOOK_REFRESH_TOKEN, ALGEN_TOOLS_OUTLOOK_ALLOWED_DOMAINS |
| Tool | Side effect | Permission | Timeout | Attempts | Approval |
|---|---|---|---|---|---|
outlook.list_messages | read | outlook.read | 120 s | Runtime default | agent policy |
outlook.get_message | read | outlook.read | 60 s | Runtime default | agent policy |
outlook.get_thread | read | outlook.read | 120 s | Runtime default | agent policy |
outlook.get_attachment (with artifact_sink) | read | outlook.read | 120 s | Runtime default | agent policy |
outlook.read_attachment (with documents) | read | outlook.read | 120 s | Runtime default | agent policy |
outlook.send_email | external | outlook.write | 60 s | 1 | agent policy (asks by default) |
outlook.list_messages(folder="inbox", search=None, unread_only=False, max_results=25, page_token=None)lists one folder (inbox,sentitems,drafts,archive,junkemail), newest first, at most 50 per page. The page token is opaque and never a URL; the connector rebuilds each request for its own mailbox.get_messageandget_attachmentare the same shared tools as Gmail's, includinginclude="full"|"latest"onget_message. Only file attachments can be saved.outlook.get_thread(thread_id, max_messages=20, max_chars_per_message=4000)reads a conversation (conversationId) across the mailbox's folders, as for Gmail, with each body the sender's own text unlessinclude="full"(see Quoted history). Graph returns at most 50 of its messages in one call;truncatedsays when there were more. In-Reply-To and References come from the message's internet headers, which Graph keeps only for received mail.outlook.read_attachment(message_id, attachment_id, max_chars=50000), withdocuments=DocumentReader(), is the shared tool: an attachment's inspected text in one call; see Reading attachments.outlook.send_emailtakes the same input assmtp.send_emailand saves a copy in Sent Items. Attachments travel inline, at most 3 MB in all.- Delegated access (a refresh token is set) acts as the signed-in user. Application access uses the app's own credentials on
ALGEN_TOOLS_OUTLOOK_MAILBOX.
Application access: limit the app to the one mailbox with an Exchange Online application access policy (or RBAC for Applications). Otherwise the app's Mail.Read grant reaches every mailbox in the tenant.
IMAP#
Read one mailbox over IMAP with an app password: Gmail and Google Workspace without an OAuth app, Zoho, Fastmail or any IMAPS server. The connector is read-only: nothing is moved, flagged, deleted or marked as seen. Send replies with smtp.send_email.
| Plugin | algen_agent_tools.imap |
| Extra | imap (no extra packages) |
| Environment | ALGEN_TOOLS_IMAP_HOST, ALGEN_TOOLS_IMAP_USERNAME, ALGEN_TOOLS_IMAP_PASSWORD; optional ALGEN_TOOLS_IMAP_PORT (993), ALGEN_TOOLS_IMAP_FOLDERS (INBOX), ALGEN_TOOLS_IMAP_AUTHSERV_ID |
| Tool | Side effect | Permission | Timeout | Attempts | Approval |
|---|---|---|---|---|---|
imap.list_messages | read | imap.read | 120 s | Runtime default | agent policy |
imap.list_folders | read | imap.read | 60 s | Runtime default | agent policy |
imap.get_message | read | imap.read | 60 s | Runtime default | agent policy |
imap.get_thread | read | imap.read | 120 s | Runtime default | agent policy |
imap.get_attachment (with artifact_sink) | read | imap.read | 120 s | Runtime default | agent policy |
imap.read_attachment (with documents) | read | imap.read | 120 s | Runtime default | agent policy |
imap.list_messagestakes typed filters, not raw IMAP search:from_address,subject,since,before,unread_onlyandtext_contains(text anywhere in the headers or body, IMAPTEXT), at most 100 per page, newest first. Search text is printable ASCII; anything else is refused rather than sent.gmail_queryonimap.list_messagesis a Gmail search (X-GM-RAW, for examplehas:attachment newer_than:7d) and is refused unless the server is Gmail.- Only the folders in
ALGEN_TOOLS_IMAP_FOLDERScan be read. Folders open withEXAMINEand bodies are fetched withBODY.PEEK, by part and within byte limits. - Special-use folders. An entry in
ALGEN_TOOLS_IMAP_FOLDERSmay be\Sent,\All,\Archive,\Drafts,\Junk,\Trashor\Flagged, resolved withLISTwhen the connection opens, soINBOX,\Sent,\Allreads the inbox, sent mail and, on Gmail, All Mail, whatever the server calls them. A name the server does not mark is left out. On Gmail,\Allalso letsget_messagefind a message by its id after it has left the inbox. imap.list_folders()lists the folders the tools may read, each with its special use when the server marks one, never a folder outsideALGEN_TOOLS_IMAP_FOLDERS.- On Gmail, message and thread ids are Gmail's own, the same ones the Gmail API returns. Elsewhere an id carries the folder's UIDVALIDITY, so a stale id is an error rather than another message, and
thread_id(empty before 0.2.0a9) isr.<base64url>of the thread's first Message-ID. imap.get_thread(thread_id, max_messages=20, max_chars_per_message=4000)reads a thread across the configured folders: byX-GM-THRIDon Gmail, by the References chain elsewhere. Add\Sentto the folders to see the mailbox's own replies.imap.get_messageandimap.get_threadtakeincludeas on Gmail:"full"by default forget_message,"latest"forget_thread. Bodies join every inline text part (at most five); see Quoted history.imap.read_attachment(message_id, attachment_id, max_chars=50000), withdocuments=DocumentReader(), returns an attachment's inspected text in one call; see Reading attachments.sender_authtrusts the topmostAuthentication-Resultsheader only frommx.google.comon Gmail, or from the server named inALGEN_TOOLS_IMAP_AUTHSERV_ID; otherwise it isunknown.- TLS on port 993 is always on, with the certificate and host name verified. The password is sent only in
LOGIN.
Telegram#
Read the messages a Telegram bot receives, save their files as artifacts, and post plain text as the bot, through the Bot API.
| Plugin | algen_agent_tools.telegram |
| Extra | telegram (no extra packages) |
| Environment | ALGEN_TOOLS_TELEGRAM_BOT_TOKEN; optional ALGEN_TOOLS_TELEGRAM_ALLOWED_CHATS |
| Tool | Side effect | Permission | Timeout | Attempts | Approval |
|---|---|---|---|---|---|
telegram.list_messages | read | telegram.read | 60 s | Runtime default | agent policy |
telegram.get_attachment (with artifact_sink) | read | telegram.read | 120 s | Runtime default | agent policy |
telegram.send_message | external | telegram.write | 60 s | 1 | agent policy (asks by default) |
telegram.list_messagesreturns the bot's pending updates, newest first, at most 100, without confirming them, so reading changes nothing. Telegram keeps an update for 24 hours.telegram.send_messageposts up to 4,096 characters of plain text with link previews off, so a link can't make Telegram fetch a URL.- Chat ids are numbers;
@usernameis not accepted. The allowlist limits every tool to those chats. - The bot token is part of every Bot API URL; it never appears in an error and is redacted from request logs.
Telegram and Discord share algen_agent_tools.messaging: the same models (InboundMessage, Conversation, DeliveryReceipt), tool names, inputs and conversation allowlist. A platform declares its capabilities, and messaging_toolkit adds only the tools it supports.
Reading uses getUpdates, which fails with error 409 while the bot has a webhook or another program polls it. Use a bot for this connector alone.
Discord#
List one Discord server's text channels, read their messages, save attachments as artifacts, and post plain text as a bot, through the REST API.
| Plugin | algen_agent_tools.discord |
| Extra | discord (no extra packages) |
| Environment | ALGEN_TOOLS_DISCORD_BOT_TOKEN, ALGEN_TOOLS_DISCORD_GUILD_ID; optional ALGEN_TOOLS_DISCORD_ALLOWED_CHANNELS |
| Tool | Side effect | Permission | Timeout | Attempts | Approval |
|---|---|---|---|---|---|
discord.list_conversations | read | discord.read | 60 s | Runtime default | agent policy |
discord.list_messages | read | discord.read | 60 s | Runtime default | agent policy |
discord.get_attachment (with artifact_sink) | read | discord.read | 120 s | Runtime default | agent policy |
discord.send_message | external | discord.write | 60 s | 1 | agent policy (asks by default) |
- One server. Every channel is checked to be a text channel, announcement channel or thread of
ALGEN_TOOLS_DISCORD_GUILD_ID; direct messages, voice channels and other servers are refused. discord.send_messageposts up to 2,000 characters with no pings (@everyone, roles and users don't notify anyone), no link embeds, and an enforced nonce from the idempotency key, so Discord drops a repeated send.- The token travels only in the
Authorizationheader todiscord.com, never to the CDN. - To read message text, turn on the bot's Message Content privileged intent.
Telegram and Discord share algen_agent_tools.messaging: the same models (InboundMessage, Conversation, DeliveryReceipt), tool names, inputs and conversation allowlist. A platform declares its capabilities, and messaging_toolkit adds only the tools it supports.
GitHub#
Read repositories, commits, issues and pull requests, and work on issues and pull requests, through GitHub's hosted MCP server, governed by a reviewed MCP overlay that ships with the package.
| Plugin | algen_agent_tools.github |
| Extra | github (the Runtime's MCP client) |
| Environment | ALGEN_TOOLS_GITHUB_TOKEN |
| Server | https://api.githubcopilot.com/mcp/, the only host the connector reaches |
It declares 37 of the server's 46 tools as github.<tool>, with github.read for reads and github.write for everything else. Each input schema is pinned to what the server advertised when the overlay was reviewed; a changed tool is refused until it is reviewed again.
| Kind | Tools | Approval |
|---|---|---|
| Read (22) | get_me, get_file_contents, get_commit, list_commits, list_branches, list_tags, get_tag, list_releases, get_latest_release, get_release_by_tag, issue_read, list_issues, search_issues, list_issue_types, list_issue_fields, get_label, pull_request_read, list_pull_requests, search_pull_requests, search_code, search_repositories, search_commits | agent policy |
| Write (10) | add_issue_comment, update_issue_comment, issue_write, sub_issue_write, create_branch, create_pull_request, update_pull_request, update_pull_request_branch, add_reply_to_pull_request_comment, add_comment_to_pending_review | agent policy |
| Write, approved every call (3) | pull_request_review_write (it can approve a pull request), push_files and create_or_update_file (they commit to a branch) | always |
| Destructive (2) | merge_pull_request, delete_file | by default |
Not exposed: repository creation and forks, run_secret_scanning (it uploads the content it is given), the MCP-Apps UI tool, Copilot review requests, and people-directory lookups.
Use a fine-grained personal access token (or a GitHub App user token) limited to the repositories the agent works on, with only the permissions the enabled tools need. The overlay limits which tools an agent can call; the token limits what they can reach. Issue and pull request text comes from outside contributors, so keep write tools behind approval when an agent reads and acts in one run.
Documents#
Read the text of a document an agent received, for example an email attachment saved with gmail.get_attachment. Parsing and inspection are the untrusted-document checks in algen_agent_tools.documents (moved from the Runtime in 0.2.0a7); the connector reads the artifact for the calling tenant, bounds the call and governs it as a tool.
| Module | algen_agent_tools.documents (built in code; it needs an artifact loader, the deployment's or runtime_attachment_loader for the run's attachments, so there is no plugin yet) |
| Extra | documents for PDFs (PyMuPDF, AGPL-3.0); every other format needs nothing. Optional ocr for Tesseract OCR and msg for Outlook .msg files (olefile, BSD) |
| Environment | none |
| Tool | Side effect | Permission | Timeout | Attempts | Approval |
|---|---|---|---|---|---|
documents.extract_text | read | documents.read | 120 s | Runtime default | agent policy |
- Formats come from the file name:
.pdf,.docx,.xlsx,.pptx,.odt,.ods,.odp,.rtf,.html,.htm,.csv,.tsv,.txt,.md,.emland.msg, and images with a recognizer (see OCR). Any other suffix, or content that doesn't match its name (a renamed executable), is refused. - Content wins over the name. From 0.2.0a10, when the content's own signature names another supported type, it is read as that type: a
scan.pdfthat is really a JPEG as an image (with OCR), a.docxrenamed.docas Word, a PDF named.txtas a PDF. Text formats have no signature and go by their name; text, CSV, TSV, Markdown and HTML are read as UTF-8, as UTF-16 when they start with its byte-order mark, and otherwise as Windows-1252, the encoding Excel uses for CSV exports. - Long PDFs are read up to page 50 and report
document.pages_skipped, so the result ispartialandtruncated; before 0.2.0a10 they were refused. - Only visible text is returned, cut to
max_chars(at most 100,000). Hidden PDF text, hidden DOCX runs, hidden XLSX sheets, rows and columns, hidden PPTX slides and shapes, and HTML hidden by attribute or inline style are reported as findings instead. - A disposition (
pass,revieworquarantine) and findings cover hidden text, instructions aimed at the model and risky packaging (macros, remote templates and objects, and an embedded OLE object in RTF,document.embedded_object). A quarantined document's text is withheld unless the toolkit is built withquarantine="return". truncatedis true when the text was cut, bymax_charsor by the inspection (an XLSX sheet past 5,000 rows).- Images are not read without OCR. A PDF, DOCX or PPTX with text and embedded images reports
document.images_excludedas a coverage finding. Pass a recognizer (OCR), or attach the file to the run when a vision model should see it. - Inspection without the tool.
from algen_agent_tools.documents import extractor_forgives the extractors directly. They replacealgen_agent_runtime.security.documents, deprecated in Runtime 0.1.0a19.
from algen_agent_tools.documents import documents_toolkit
documents = documents_toolkit(artifact_loader=load_artifact) # checks the artifact's tenant
# or, for the files attached to the run (Runtime 0.1.0a19+): refuses any other artifact
from algen_agent_tools.attachments import runtime_attachment_loader
documents = documents_toolkit(artifact_loader=runtime_attachment_loader)Unread content#
From 0.2.0a8, content the inspection did not read is reported apart from security findings, so a caller can tell "dangerous" from "not read":
coverageisfull(everything was read),partial(some content was not) ornone(content was found but no text could be read).needs_ocris true when images or scanned pages hold text only OCR can read.- Each finding has a
category,securityorcoverage.document.ocr_required,document.images_excluded,document.ocr_no_text,document.truncated,document.attachment_skipped,document.attachment_unread,document.body_unread(an Outlook message whose only body is RTF) anddocument.pages_skipped(PDF pages past page 50) are coverage findings; only security findings set the disposition. - A scanned ID card without OCR is
passwithcoverage="none"andneeds_ocr=true; a cover note over a scan ispasswithcoverage="partial". Hidden text or an instruction aimed at the model still makes itrevieworquarantine.
pass with coverage="none" means nothing dangerous was found because nothing was read. Before 0.2.0a8 such documents were review; code that used review to mean "not fully read" checks coverage != "full" instead.
OCR#
OCR is off unless the host passes a recognizer, a TextRecognizer: any object with recognize(image, *, media_type) returning RecognizedText. Pass it as documents_toolkit(ocr=...) or extractor_for(filename, ocr=..., ocr_max_pages=3). The ocr extra ships TesseractRecognizer, backed by the Tesseract program, which the host installs with its languages.
# pip install "algen-agent-tools[documents,ocr]"
from algen_agent_tools.documents import TesseractRecognizer, documents_toolkit
documents = documents_toolkit(
artifact_loader=load_artifact, ocr=TesseractRecognizer(languages=("eng", "deu"), timeout=30)
)- Images (
.png,.jpg,.jpeg,.webp,.gif,.bmp,.tif,.tiff) are accepted only with a recognizer, within 40 megapixels and 64 frames. - Scanned PDF pages (images and almost no text) are rendered and recognized, up to
ocr_max_pagesper PDF (default 3); pages past the limit reportdocument.ocr_required. - Recognized text is checked like any other. It passes the same prompt-injection checks, so an instruction in a scan quarantines the document. An image or page with no recognized text reports
document.ocr_no_text. TesseractRecognizerfails when it is built, naming the fix, if the extra, the program or a language is missing. It is never the default.- The recognizer runs inside the tool's 120 s timeout. Images inside DOCX and PPTX are not recognized yet.
OpenDocument#
From 0.2.0a10, ODT, ODS and ODP files (LibreOffice, Google Docs exports) are read with the standard library, with the rules of their Office counterparts.
- Hidden content is left out and reported: text a style or
text:display="none"hides, hidden text fields and paragraphs, text under 2 pt, hidden sheets, rows and columns, and hidden slides. - Spreadsheets come out sheet by sheet under
--- Sheet: <name> ---, cells joined by|, cut at 5,000 rows per sheet. Comments, tracked changes and speaker notes are not extracted. - A Basic macro project, or an image or object loaded from a remote address, is a finding; encrypted files are refused.
RTF#
From 0.2.0a10, RTF is read with a bounded tokenizer from the standard library: paragraphs, tabs and table cells (joined with | ), with escaped and Unicode characters decoded. Font tables, styles, document information, headers, footers and footnotes are not text.
- Hidden text (
\v) and text under 2 pt are left out and reported. - An embedded OLE object, a common exploit carrier, is the security finding
document.embedded_object; a field that loads a remote file (INCLUDEPICTURE,INCLUDETEXT) isdocument.external_reference. Pictures are coverage findings. - A Word file saved as RTF but named
.docis recognised by its content.
Email files#
From 0.2.0a9 an .eml file is read as its headers (From, To, Cc, Date, Subject) and body, then each attachment's text under an [Attachment: <name>] line, read by the extractor for its type. Attached emails are read at most two levels deep, and at most 20 attachments in the whole message.
attachmentslists the attachments that were read (file name, media type, disposition, coverage).- An attachment's findings are the message's. Hidden text or an instruction in an attachment quarantines the message, and the finding names the attachment in
part. - Skipped is not refused. An attachment past a limit, of an unsupported type or unreadable is a coverage finding (
document.attachment_skippedordocument.attachment_unread), so the message ispasswithcoverage="partial". - Outlook
.msgfiles (from 0.2.0a10, with themsgextra,pip install "algen-agent-tools[msg]") are read the same way: Tools reads the sender, recipients, date, subject, body and attachments from the file's OLE streams with olefile, rebuilds the equivalent email and inspects that, with the same limits and findings. Attached Outlook messages are read as attached emails. A message whose only body is RTF reportsdocument.body_unread(coverage), since RTF bodies are not read.
Webhook#
Send typed events to systems you own: chat incoming-webhooks, automation platforms, your own services. The deployment fixes where each event goes and what shape it has; the model only fills in the fields.
| Module | algen_agent_tools.webhook (built in code; there is no plugin) |
| Extra | webhook (no extra packages) |
| Environment | one env:// URL per target, and an optional signing secret per target |
from algen_agent_tools.webhook import WebhookTarget, webhook_toolkit
alerts = webhook_toolkit([
WebhookTarget(
action="notify_ops", # tool webhook.notify_ops
url="env://ALGEN_TOOLS_WEBHOOK_OPS_URL",
signing_secret="env://ALGEN_TOOLS_WEBHOOK_OPS_SECRET",
payload=OpsAlert, # a Pydantic model: the tool's input
description="Post an alert to the operations channel.",
),
])- Each target is one external tool,
webhook.<action>, with permissionwebhook.write, a 30 s timeout and one attempt;approval=Trueasks before every call. - The URL must be
https(orallow_http=Truefor a private network), redirects are not followed, and the URL is treated as a secret: it never appears in an error or a log line. - The receiver gets a JSON
POSTwith anIdempotency-Keyheader and, given a signing secret,X-Algen-Signature: t=<ts>,v1=<HMAC-SHA256>. Check it withverify(secret, header, raw_body). - The result is only
{"action", "status_code"}; the receiver's body is never returned to the model.
Sample toolkit#
A dependency-free toolkit that calls no external service. Use it to check that an install, the plugin entry point and describe work end to end, and as the template for a new connector.
| Tool | Side effect | Permission | Timeout | Input |
|---|---|---|---|---|
sample.echo | read | sample.read | 5 s | text, 1 to 1,000 characters; returns {"text", "length"} |
MCP governance overlays#
When a well-maintained MCP server already does what an agent needs, use a governance overlay instead of a native connector. An overlay is a reviewed file, kept in the project, that declares each remote tool the agent may call: its side effect, whether a person must approve every call, and the input schema it was reviewed against. MCP's own annotations (readOnlyHint, destructiveHint) are hints a server sets about itself; the overlay is the deployment's decision.
What an overlay guarantees#
- Visible before connecting. Each declared tool is an ordinary Runtime tool, so
describeand Studio list it with its side effect, permission and approval rule. - Deny by default. A remote tool the overlay does not declare is never exposed.
- Schema pinning. A declared tool that disappeared, or whose input schema changed on the server, is refused until the overlay is reviewed again (
on_drift: allowrelaxes this). - Validated arguments. Arguments are checked against the reviewed schema, with every object closed to unknown fields, before anything is sent.
- Bounded results. Each tool has a result size limit (256 KB by default) and a timeout.
- Secrets by reference, hosts pinned. Tokens, headers and a stdio server's environment are
env://references; an HTTP transport may reach only the server's own host unlessallowed_hostssays otherwise.
Draft, review, use#
$ python -m pip install --pre 'algen-agent-tools[mcp]' $ python -m algen_agent_tools.mcp snapshot server.yaml --name github --output github.overlay.yaml
The draft pins every advertised tool's schema and leaves each effect empty, with the server's annotation as a comment. It does not load until someone sets every effect. Delete each tool the agent must not call.
name: github # tools are github.<tool>; permissions github.read / github.write
server:
name: github
transport: streamable_http
url: https://api.githubcopilot.com/mcp/
auth_token_ref: env://GITHUB_MCP_TOKEN
tools:
get_issue:
effect: read
input_schema: {...} # pinned by the snapshot
create_issue:
effect: write
approval: true # ask a person before every call
timeout_seconds: 30
input_schema: {...}| Rule field | Default | Meaning |
|---|---|---|
effect | required | read, write, external or destructive |
approval | true for destructive, otherwise the agent's approval policy | ask before every call |
timeout_seconds | 30 | per call |
permissions | <name>.read or <name>.write | explicit permission list |
max_result_bytes | 262144 | result size limit |
description | the server's | what the model reads |
input_schema | required | the reviewed schema |
A server can change a tool at any time. python -m algen_agent_tools.mcp check github.overlay.yaml connects to the server and reports declared tools that are gone or whose schema changed, and tools the server offers that the overlay doesn't declare. It exits 1 on drift (with --strict, also on new tools) and prints JSON with --json, so you can run it on a schedule.
Load it from your project's plugin with overlay_plugin(Path(...).with_name("github.overlay.yaml"), name="acme.github", version="1.0.0", publisher="acme"), then name that plugin in domain_plugins. Do not also list the server under the Runtime's mcp_servers; the overlay owns the connection.
No idempotency key reaches an MCP server. Write tools are single-attempt and the Runtime's ledger stops a replay from calling twice, but a timeout leaves the outcome unknown. If an action must be exactly-once, that is the case for a native connector.
What every tool guarantees#
Every toolkit in the package passes the same contract harness, algen_agent_tools.testing.assert_toolkit_contract, on top of the Runtime's own tool contract:
- Narrow and typed. No field, at any depth, takes free-form SQL, URLs, shell commands or vendor request bodies.
- Honest about what was sent. A call refused before anything was sent, or definitely rejected by the provider, raises an error that is also a
ToolNotAttemptedError, so the Runtime records itfailedand it can be retried. A timeout or 5xx after a write was sent staysindeterminatefor reconciliation. - Named and scoped. Every tool is
<toolkit>.<action>, and every permission sits under the toolkit's prefix. - Governed by a preset. Reads are
read(); anything that writes, sends or deletes iswrite,externalordestructive, keyed and single-attempt. Destructive actions require approval by default. - Bounded. Timeouts, row counts, page sizes and result bytes all have limits.
- Content-minimal errors. Errors name the failure type and the field, never the payload or a secret.
- Outbound hosts are configuration. No tool argument chooses where a request goes.
Email and guardrails#
Email tools run under the Runtime's default guardrails; no guardrail setting is needed to send:
agents:
- name: notifier
domain_plugins: [algen_agent_tools.smtp]
enabled_tools: [smtp.send_email]
tool_permissions: [smtp.write]- Recipients pass. Every address field (
to,cc,bcc,reply_to) declaresformat: email, and thepiiguardrail leaves a single address in such a field alone. - Everything else is screened.
piistill checks display names, the subject and the bodies. If it redacts anything, the Runtime refuses the call withPayload transformed by policy. - Read addresses pass too. From 0.2.0a7 (Runtime 0.1.0a19 or later), Gmail, Outlook and IMAP results carry
sender_address,recipient_addressesandcc_addresses, declaredformat: email, so the agent can reply to the sender it read.piistill redacts addresses in the display strings and bodies of results.
An agent that must quote addresses or phone numbers in a message leaves pii out of its tool boundaries. It still screens messages to and from the model:
guardrail_policy:
tool_policies: [secrets, prompt_injection, output_validation]Keep the agent's approval policy on: by default it asks a person before every external call. Use allowed_domains to bound who the agent can write to.
Secrets and environment#
Every setting is an env://NAME reference, resolved by the Runtime's secret resolver when the client first opens. Nothing is read at import or while a plugin registers, and no secret is logged, returned or put in an error. describe lists each plugin's variables so Studio can ask for them before a run.
| Plugin | Environment variables |
|---|---|
algen_agent_tools.postgres | ALGEN_TOOLS_POSTGRES_DSN |
algen_agent_tools.mysql | ALGEN_TOOLS_MYSQL_DSN |
algen_agent_tools.sqlite | ALGEN_TOOLS_SQLITE_PATH |
algen_agent_tools.smtp | ALGEN_TOOLS_SMTP_HOST, ALGEN_TOOLS_SMTP_PORT, ALGEN_TOOLS_SMTP_SECURITY, ALGEN_TOOLS_SMTP_USERNAME, ALGEN_TOOLS_SMTP_PASSWORD, ALGEN_TOOLS_SMTP_SENDER, ALGEN_TOOLS_SMTP_ALLOWED_DOMAINS |
algen_agent_tools.gmail | ALGEN_TOOLS_GMAIL_CLIENT_ID, ALGEN_TOOLS_GMAIL_CLIENT_SECRET, ALGEN_TOOLS_GMAIL_REFRESH_TOKEN, ALGEN_TOOLS_GMAIL_SENDER, ALGEN_TOOLS_GMAIL_ALLOWED_DOMAINS |
algen_agent_tools.outlook | ALGEN_TOOLS_OUTLOOK_TENANT_ID, ALGEN_TOOLS_OUTLOOK_CLIENT_ID, ALGEN_TOOLS_OUTLOOK_CLIENT_SECRET, ALGEN_TOOLS_OUTLOOK_MAILBOX, ALGEN_TOOLS_OUTLOOK_REFRESH_TOKEN, ALGEN_TOOLS_OUTLOOK_ALLOWED_DOMAINS |
algen_agent_tools.imap | ALGEN_TOOLS_IMAP_HOST, ALGEN_TOOLS_IMAP_USERNAME, ALGEN_TOOLS_IMAP_PASSWORD, ALGEN_TOOLS_IMAP_PORT, ALGEN_TOOLS_IMAP_FOLDERS, ALGEN_TOOLS_IMAP_AUTHSERV_ID |
algen_agent_tools.telegram | ALGEN_TOOLS_TELEGRAM_BOT_TOKEN, ALGEN_TOOLS_TELEGRAM_ALLOWED_CHATS |
algen_agent_tools.discord | ALGEN_TOOLS_DISCORD_BOT_TOKEN, ALGEN_TOOLS_DISCORD_GUILD_ID, ALGEN_TOOLS_DISCORD_ALLOWED_CHANNELS |
algen_agent_tools.github | ALGEN_TOOLS_GITHUB_TOKEN |
algen_agent_tools.sample | none |
To use other variable names, or two databases at once, build the toolkit yourself with your own env:// references; see Configure in code.
Configure in code#
Every connector has a toolkit factory that takes each setting as an argument. Build it in your own plugin to rename it, point it at other variables, bound recipients, or pass a fake in tests:
from algen_agent_runtime.plugins.sdk import PluginManifest, plugin
from algen_agent_tools.smtp import smtp_toolkit
notices = smtp_toolkit(
"notices", # tool notices.send_email, permission notices.write
host="env://NOTICES_SMTP_HOST",
sender="env://NOTICES_FROM",
allowed_domains=("example.com",), # refuse other recipient domains
artifact_loader=load_artifact, # adds an attachments field
)
@plugin(PluginManifest(name="acme.tools", version="1.0.0", publisher="acme"))
class AcmeTools:
def register(self, context):
notices.register(context)| Factory | Module |
|---|---|
PostgresToolkit(name, dsn=...), postgres_toolkit(...) | algen_agent_tools.postgres |
MysqlToolkit(name, dsn=...), mysql_toolkit(...) | algen_agent_tools.mysql |
SqliteToolkit(name, path=..., read_only=True), sqlite_toolkit(...) | algen_agent_tools.sqlite |
smtp_toolkit(name, host=..., sender=..., ...) | algen_agent_tools.smtp |
gmail_toolkit(name, client_id=..., client_secret=..., refresh_token=..., sender=...) | algen_agent_tools.gmail |
outlook_toolkit(name, mailbox=..., ...) | algen_agent_tools.outlook |
imap_toolkit(name, host=..., username=..., password=..., folders=...) | algen_agent_tools.imap |
telegram_toolkit(name, bot_token=..., ...), discord_toolkit(name, bot_token=..., ...) | algen_agent_tools.telegram, algen_agent_tools.discord |
github_toolkit(session_factory=None), github_overlay() | algen_agent_tools.github |
documents_toolkit(artifact_loader=..., quarantine="withhold") | algen_agent_tools.documents |
webhook_toolkit([WebhookTarget(...)]) | algen_agent_tools.webhook |
sample_toolkit(name=...) | algen_agent_tools.sample |
McpOverlay.from_file(...), overlay_plugin(...) | algen_agent_tools.mcp |
Attachments: the artifact loader is async def load_artifact(reference, context) -> bytes. The model chooses the artifact id, so the loader must check that it belongs to context.tenant_id (and, if you scope artifacts to runs, context.run_id) before returning bytes.
Saving received files: Gmail, Outlook, IMAP, Telegram and Discord offer <name>.get_attachment only when the toolkit is built with an artifact_sink, the deployment's artifact store. Each file is checked against the attachment_policy (size limit, allowed types, and leading bytes that match the declared type), and the model gets back only the artifact reference, never the bytes.
Trust your own plugin's package under security.trusted_plugin_prefixes, as you do for algen_agent_tools.
Testing#
Every connector ships an in-memory fake, so agent tests never touch the network:
| Fake | Use |
|---|---|
algen_agent_tools.postgres.FakePostgres | Answers by SQL fragment, records every call and whether it ran read-only, refuses writes inside read-only transactions. PostgresToolkit("billing", client=fake.factory) |
algen_agent_tools.mysql.FakeMysql | Answers by SQL fragment and records whether each call ran read-only, as FakePostgres does. |
algen_agent_tools.mail.FakeMailbox | Records sent messages, serves inbox pages and get_message, and from files= serves get_attachment. smtp_toolkit(client=FakeMailbox().factory()) |
algen_agent_tools.imap.FakeImapServer | Holds folders of real MIME messages and answers as imaplib does; server.commands proves a test wrote nothing. imap_toolkit(imap_factory=server.factory()) |
algen_agent_tools.messaging.FakeMessaging | A chat platform for the shared messaging tools. |
algen_agent_tools.mcp.fake.FakeMcpServer | Advertises tools and answers calls. overlay.toolkit(session_factory=fake.session_factory) |
Check your own toolkits with the same harness the package uses:
from algen_agent_tools.testing import assert_toolkit_contract, plugin_tools
def test_billing_tools_meet_the_contract():
assert_toolkit_contract(billing)
def test_the_plugin_describes_its_tools():
assert set(plugin_tools("acme.plugins:AcmeTools")) == {"billing.list_open_invoices"}Live tests in this package are marked live and run only with ALGEN_TOOLS_LIVE=1. Its end-to-end tests also run agents through the Runtime's real executor, for example to check that a context_params query's cached results stay with each caller.
Write a connector#
Open a "Connector proposal" issue first; a connector needs an accepted proposal. src/algen_agent_tools/sample is the smallest complete connector to copy.
src/algen_agent_tools/<connector>/
__init__.py # public API: the toolkit factory, its models and its fake
toolkit.py # <connector>_toolkit(...) -> Toolkit
client.py # the vendor client; optional imports happen here, lazily
fake.py # an in-memory stand-in used by tests
plugin.py # <Connector>Plugin = toolkit_plugin("<connector>", ...)- The factory takes every setting as an argument, with
env://defaults namedALGEN_TOOLS_<CONNECTOR>_<SETTING>, and aclientoverride for the fake. - Tools are defined inside the factory, so each call returns an independent toolkit.
- Nothing connects in the factory or in
register. The client resolves its secrets and imports its optional package when it opens, and raises an error naming the extra if it is missing. - The docstring is what the model reads: say what the action does and when to use it.
- Add an extra and an entry point in
pyproject.toml, a docs page, unit tests against the fake, and opt-in live tests.
Connector policy#
The package is curated. A new connector needs all of:
- Demand: at least two projects need it.
- No domain logic: it speaks to a vendor or protocol, not to one product. Hiring, payroll, text-to-SQL and similar logic belong in a domain pack.
- An owner: a named maintainer who reviews its changes and answers its security reports.
- A security review recorded in the pull request that adds it.
- MCP first: if a well-maintained MCP server covers the need, the package ships an overlay instead. A native connector is justified only when an overlay cannot give a guarantee the agent needs, such as an idempotency key or a typed action the server lacks.
Deprecations warn on use for at least one further minor release before removal, always with the replacement recorded in the changelog.
Tool reference#
Every tool the built-in plugins register, as algen-agent-runtime describe reports it:
| Tool | Plugin | Side effect | Permission | Timeout |
|---|---|---|---|---|
postgres.list_tables | algen_agent_tools.postgres | read | postgres.read | 30 s |
postgres.describe_table | algen_agent_tools.postgres | read | postgres.read | 30 s |
mysql.list_tables | algen_agent_tools.mysql | read | mysql.read | 30 s |
mysql.describe_table | algen_agent_tools.mysql | read | mysql.read | 30 s |
sqlite.list_tables | algen_agent_tools.sqlite | read | sqlite.read | 30 s |
sqlite.describe_table | algen_agent_tools.sqlite | read | sqlite.read | 30 s |
smtp.send_email | algen_agent_tools.smtp | external | smtp.write | 60 s |
gmail.list_messages | algen_agent_tools.gmail | read | gmail.read | 120 s |
gmail.get_message | algen_agent_tools.gmail | read | gmail.read | 60 s |
gmail.get_thread | algen_agent_tools.gmail | read | gmail.read | 120 s |
gmail.send_email | algen_agent_tools.gmail | external | gmail.write | 60 s |
outlook.list_messages | algen_agent_tools.outlook | read | outlook.read | 120 s |
outlook.get_message | algen_agent_tools.outlook | read | outlook.read | 60 s |
outlook.get_thread | algen_agent_tools.outlook | read | outlook.read | 120 s |
outlook.send_email | algen_agent_tools.outlook | external | outlook.write | 60 s |
imap.list_messages | algen_agent_tools.imap | read | imap.read | 120 s |
imap.list_folders | algen_agent_tools.imap | read | imap.read | 60 s |
imap.get_message | algen_agent_tools.imap | read | imap.read | 60 s |
imap.get_thread | algen_agent_tools.imap | read | imap.read | 120 s |
telegram.list_messages | algen_agent_tools.telegram | read | telegram.read | 60 s |
telegram.send_message | algen_agent_tools.telegram | external | telegram.write | 60 s |
discord.list_conversations | algen_agent_tools.discord | read | discord.read | 60 s |
discord.list_messages | algen_agent_tools.discord | read | discord.read | 60 s |
discord.send_message | algen_agent_tools.discord | external | discord.write | 60 s |
github.* (37 tools) | algen_agent_tools.github | 22 read, 13 write, 2 destructive; see GitHub | github.read, github.write | 30 s |
sample.echo | algen_agent_tools.sample | read | sample.read | 5 s |
Also registered, but only when built in code: <name>.get_attachment (read, 120 s) for Gmail, Outlook, IMAP, Telegram and Discord with an artifact_sink; <name>.read_attachment (read, 120 s) for Gmail, Outlook and IMAP with documents=DocumentReader(); documents.extract_text (read, 120 s); and one webhook.<action> (external, webhook.write) per webhook target. Typed SQL queries and your own MCP overlay tools are named by your toolkit or overlay (billing.list_open_invoices) and appear in describe the same way. The full contract of every tool is in the package's connectors.json, which each GitHub release also attaches.
Runtime compatibility#
| algen-agent-tools | Supported algen-agent-runtime |
|---|---|
| 0.2.0a10 | >=0.1.0a23,<0.2 (tested on 0.1.0a23) |
| 0.2.0a9 | >=0.1.0a23,<0.2 (tested on 0.1.0a23) |
| 0.2.0a8 | >=0.1.0a23,<0.2 (tested on 0.1.0a23) |
| 0.2.0a7 | >=0.1.0a20,<0.2 (tested on 0.1.0a20 and 0.1.0a21) |
| 0.2.0a6 | >=0.1.0a18,<0.2 |
| 0.2.0a1 to 0.2.0a5 | >=0.1.0a12,<0.2 (tested on 0.1.0a12 and 0.1.0a13) |
| 0.1.0a2 | >=0.1.0a12,<0.2 |
| 0.1.0a1 | >=0.1.0a11,<0.2 |
The package is built on the Runtime's tool authoring API (@tool, presets, Toolkit), declared plugins and describe, all introduced in Runtime 0.1.0a11. From 0.1.0a2 it needs Runtime 0.1.0a12, whose pii guardrail leaves fields declared format: email alone. From 0.2.0a6 it needs Runtime 0.1.0a18: 0.1.0a14's document inspection reads XLSX, PPTX, HTML, CSV and TSV, and 0.1.0a18's cache_context keys cached SQL results on the values context_params binds. From 0.2.0a7 it needs Runtime 0.1.0a20, which records refusals as not attempted, keeps declared format: email result fields out of pii redaction and gives tools the run's attachments. From 0.2.0a8 it needs Runtime 0.1.0a23: 0.1.0a22 and 0.1.0a23 fix two ways an approval-required workflow tool step could run without its approval, and run synchronous tools in a worker thread within their timeout. CI runs the full suite against the lowest supported Runtime and the newest release inside the range; a Runtime release that breaks a connector lowers the upper bound until it is fixed. Every plugin targets plugin core API version 1.
Migrating from the Runtime's connectors#
Runtime 0.1.0a11 and earlier shipped a few connectors in its core. They moved here; the Runtime deprecates them and removes them later.
| Runtime (deprecated) | algen-agent-tools |
|---|---|
communications.EmailMessage and the other message models | algen_agent_tools.mail (same names) |
communications.SmtpEmailSender, SmtpSettings | algen_agent_tools.smtp (same names) |
communications.email_tool(sender) | smtp_toolkit(...) or gmail_toolkit(...): tool <name>.send_email |
communications.GmailMailboxClient and its models | algen_agent_tools.gmail (same names) |
communications.gmail_inbox_tool(mailbox) | gmail_toolkit(...): tool <name>.list_messages |
tools.adapters.database_tool(name, handler) | PostgresToolkit.query(...): one typed tool per query |
The email tool changes on purpose: it is named smtp.send_email or gmail.send_email with permission <name>.write, the sender is fixed by the deployment instead of chosen by the model, custom headers are not accepted, and the timeout is 60 s. Update enabled_tools, tool_permissions and any policy that names the old tool communications.email.send. database_tool let the model send any statement; replace each statement the agent needs with a declared query.
Troubleshooting#
"Payload transformed by policy" when sending email
The pii guardrail redacted part of the message, usually an address or phone number in the subject or body, and the Runtime does not run a tool on redacted arguments. Recipients are not redacted with algen-agent-tools 0.1.0a2 and Runtime 0.1.0a12 or later, nor are the structured address fields of mail results with 0.2.0a7. Remove the detail from the message, or leave pii out of guardrail_policy.tool_policies; see Email and guardrails.
The plugin is not trusted, or is not found
Add algen_agent_tools (and your own plugin's package) to security.trusted_plugin_prefixes, and check the package is installed in the same environment as the Runtime. algen-agent-runtime describe lists the plugins it can see.
"install algen-agent-tools[...]" when a tool first runs
The connector's optional dependency is missing. Install the extra the message names, for example pip install "algen-agent-tools[postgres]".
An MCP tool is refused after a server update
The server changed the tool's input schema, or removed it. Run snapshot again, review the difference, and update the overlay's input_schema. Use on_drift: allow only when you accept unreviewed changes.
check reports an unknown tool
The name in enabled_tools does not match a registered tool. The message suggests the closest name; tool names are <toolkit>.<action>, so a renamed toolkit renames its tools.
A database read fails with a row limit error
The query returned more rows than its limit (500 by default) and raised TooManyRows. Narrow the query, or raise max_rows when declaring it; reads never truncate silently. This applies to PostgreSQL, MySQL and SQLite alike.
Every cached tool call fails with "Internal error (ValueError)"
The tool-result cache is scoped to session. On Runtime 0.1.0a18 the tool cache gets no session, so every cacheable call fails; Runtime 0.1.0a19 fixed it, and 0.2.0a7 requires 0.1.0a20 or later. On 0.1.0a18 use user or run scope instead. A context_params query is safe at any scope: its cache key includes the values it binds.
Telegram reads fail with error 409
The bot has a webhook, or another program is polling it. getUpdates works only when this connector is the bot's only reader; delete the webhook or use a separate bot.
Discord messages come back with empty text
Turn on the bot's Message Content privileged intent in the Discord developer portal. Without it Discord returns text only for messages that mention the bot.
sender_auth is always unknown on IMAP
On servers other than Gmail, set ALGEN_TOOLS_IMAP_AUTHSERV_ID to the name your receiving server writes at the start of its Authentication-Results header (for example mx.zoho.com). A header the connector can't attribute to your server proves nothing, so it is ignored.
Source and links#
- GitHub: AlgenAI/algen-agent-tools, including each connector's page under
docs/connectors. - PyPI: algen-agent-tools
- Changelog and security policy
- Algen Agent Runtime documentation and the Studio user guide