Trial
0011. Client-side connector tools via the loader pattern
Trial
- Status: Trial
- Date: 2026-11-02
Context
Section titled “Context”Mistral connectors support client-side execution — the same connectors
ADR 0007 exposes as
server-side {type:"connector"} chat-request entries. Live probes
against the Mistral API established the wire facts:
- Connectors expose an MCP-compatible tool interface:
GET /v1/connectors(withfetch_user_data=true, keyset pagination viaitems+pagination.next_cursor),GET /v1/connectors/{id}/tools?pretty=false(real JSON Schema, including Mistral’sanyOf-nullable idiom;pretty=truereturns a compact summary shape), andPOST /v1/connectors/{id}/tools/{tool_name}/callwith{arguments}, returning{content: [{type:"text", text}], metadata.mcp_meta.isError}. - Unauthenticated connectors answer list_tools with HTTP 401 “No credentials found” — a usable per-connector fallback signal.
- Live-verified on this account: 17 connectors visible;
outlook(7 tools),outlook_calendar(12) andsharepoint_mcp(34) are authenticated.is_authenticatedis only populated whenfetch_user_data=true. - pi 1.4.0 supports runtime tool registration (
registerTool), passive exposure (deferred/hiddennever auto-activate), andsetActiveToolsas the documented loader pattern (“Register every tool first, keep optional tools inactive, and use pi.setActiveTools() from a loader tool”). Tools cannot be unregistered; re-registering ashiddenis the documented withdraw path.namespace: {name, aliases, description}groups tools the way MCP servers do. - 53 authenticated connector tools would bloat the prompt if registered directly active — hence the loader pattern.
- Server-tool support is per-endpoint, not per-connector (wire-verified
2026-11-03 on the chat completions endpoint, models mistral-large-4 and
zai-glm-5-3):
web_search,web_search_premiumandcode_interpreterchat-request entries return HTTP 400 code 1800 (“WebSearchTool connector is not supported” etc.), whileimage_generationand{type:"connector"}entries are accepted — Mistral’s docs mark the rejected three Conversations/Agents-API-only.
Decision
Section titled “Decision”Client-side connector tools load on demand via a loader tool
(implementation in extensions/tools/loader.ts; ADR
0007 keeps the
server-side counterpart):
- Session start (live fetch, consistent with the dynamic model
catalog): list connectors with
fetch_user_data=true, keep those withsupports_mcp !== false, and determine the authenticated ones (is_authenticated; 401 on list_tools is the per-connector fallback signal). Register ONE loader tool,mistral_connector_load, whose description dynamically lists the authenticated connectors (name + description; no tool counts at session start). mistral_connector_load(connector): fetch the connector’s tools raw-mode, register all with passive exposure (deferred, never auto-activated) under the namespacemistral_cn_<connector>(aliases: the connector name), thensetActiveTools([...current, ...new]). The result text lists each loaded tool’s name + one-line description — the running turn’s declarations were fixed before the loader ran, so the agent learns availability from the result. Original tool names are kept; name collisions are resolved session-wide (another loaded connector OR an already-registered session tool — pi built-ins, MCP tools, other extensions’ tools — disambiguate as<original>__<connector>, then_2,_3, … while the candidate is taken, so a connector tool namedreadcan never shadow pi’s built-in; re-loading the same connector stays idempotent). Loaded-tool state is cleared only when pi replaces the session (session_startreason"new", i.e./new); resume, fork and reload keep it so pending transcript tool names re-activate with stable renames.mistral_connector_unload(connector): re-register the connector’s tools ashidden(the documented withdraw path) andsetActiveToolswithout them.mistral_connector_reload: re-fetch the connector list and refresh the loader description (covers/reloadand session-resume staleness). Deliberate scope: reload refreshes the connector list (what can be loaded), not the tool schemas of already-loaded connectors — unload + load re-fetches those.- Tool calls: POST the call endpoint with
{arguments}(the tool’s inputSchema is pi’s parameter schema), fold{content:[...]}text blocks to text (non-text blocks become placeholders), propagatemetadata.mcp_meta.isError, and forward connectorannotations(readOnlyHintetc.) to pi tool annotations. - Config gating via the
connectorToolssection ofmistral-native.json(ADR 0010’s reserved section, now activated):enabled(defaulttrue;MISTRAL_CONNECTOR_TOOLSenv override, with “0”/“false”/“off” disabling; the file wins over env per the ADR 0010 precedence chain) andautoLoad(connector names auto-registered + activated at session start, without a model-facing result). - Per-endpoint filtering in the chat-completions request builder
(server-side counterpart: ADR
0007): entries are
filtered to the chat-completions-supported set (
image_generation+{type:"connector"}entries); configured-but-unsupported built-ins (web_search,web_search_premium,code_interpreter) are dropped with a once-per-session warning. Web search takes a tool-based path instead: theweb_searchpi tool issues stateless one-shot Conversations calls (configured search modelmistral-small-latest), so the model composes search results like any other pi tool — the same client-side reasoning model this ADR champions for connectors. A forthcoming sibling ADR owns the search-tool design details.
When client-side vs. server-side (ADR 0007)
Section titled “When client-side vs. server-side (ADR 0007)”The same connectors support two execution models:
| Client-side (this ADR) | Server-side (ADR 0007) | |
|---|---|---|
| Executor | pi, via the tool-call endpoint | Mistral, inside chat completions ({type:"connector"} entries) |
| Tool-schema visibility | Full — the agent sees names, descriptions, JSON Schemas | None — only the connector ref is declared |
| Provider | Model-agnostic — works with any provider, not just mistral-native chat |
Model-locked to Mistral chat |
| Results | Structured pi tool results | Opaque assistant text (ADR 0007’s forcing constraint: pi would re-execute any toolCall block client-side) |
| Prompt cost / round-trips | Managed by the loader pattern (one loader tool; extra loader round-trip) | Zero prompt declarations, zero client round-trips for execution |
| Permission gating | Permission extensions can gate on annotations | No per-tool permission gating |
| Credentials | Still live server-side at Mistral — pi never holds OAuth tokens (connection_credentials never exposed) |
Same |
Recommendation: default to client-side (loader) when the agent should reason about available tools, compose multi-step connector workflows, or the session uses non-Mistral providers; default to server-side refs for fire-and-forget retrieval-augmented answers on a Mistral model where prompt economy matters. Both can coexist in one session (different execution models, same connectors).
Consequences
Section titled “Consequences”- The prompt stays lean: one loader tool instead of 53 declarations.
- Loaded tools are visible from the next model request (pi’s
before_agent_startre-declares active tools); the loader’s result text carries the tool names for the running turn. - Name collisions resolve via the
__<connector>suffix within namespaces — checked session-wide (other loaded connectors, pi built-ins, MCP tools, other extensions’ tools), so connector tools can never shadow a built-in likeread(pi’s registry replaces same-name custom registrations); original tool names are preserved otherwise. - Unloaded tools stay registered as
hidden(pi cannot unregister) — an accepted limitation. - The loader description is a session-start snapshot;
mistral_connector_reloadrefreshes it. - Unauthenticated connectors are excluded (401 signal).
- Connector credentials stay on Mistral’s side (ADR 0004 unchanged); the cost is per-call latency — each tool call is an HTTP request from pi.