--- title: IA Router, Free Text and MCP Confirmation url: https://blog.guigpap.com/en/workflows/ia-router-mcp/ url_md: https://blog.guigpap.com/en/workflows/ia-router-mcp.md category: automation date: '2026-05-19' maturite: production techno: - n8n - telegram - claude application: - automation - ai --- # IA Router, Free Text and MCP Confirmation > Multi-provider intent detection, free-text reply dispatch and Telegram approval for critical MCP tool calls ## 1. What? — Definition and context Three N8N sub-workflows form the intelligent routing layer of the Telegram bot. The **IA Router** detects the intent of a free-text message and picks the target service. The **Free Text Handler** dispatches plain-text replies sent in answer to an interactive question opened by the cli-ollama API. The **MCP Confirmation Handler** triggers a Telegram approval flow before executing a critical MCP tool (create, update, delete N8N workflow). These three workflows share the same philosophy: they carry no business logic of their own. Their job is to steer the main flow towards the right handler based on context. > **Note - MCP, Model Context Protocol** > > The **Model Context Protocol** is a standard introduced by Anthropic that lets an LLM call external tools through a normalized interface. In this infrastructure the `n8n-local` MCP server exposes 20 N8N management tools (list, get, create, update, delete workflows) that the Codex agent can invoke during a conversation. ### Position in the Telegram pipeline | Workflow | ID | Type | Trigger | |----------|-----|------|---------| | **IA Router - Intent Detection** | `HWryf7aJkZa5FuUA` | wait-for-result | Execute Workflow from Orchestrator | | **Telegram Free Text Handler** | `xwGcEOpzHkvx0HTR` | fire-and-forget | Execute Workflow from Orchestrator | | **MCP Confirmation Handler** | `lyDYsxzTrDzZrfJk` | webhook | Internal POST `/webhook/mcp-confirmation` | None of these workflows is called directly by Telegram: they are internal entry points triggered by the Orchestrator or the cli-ollama gateway at the right moment. --- ## 2. Why? — Stakes and motivations Without this routing layer, the Orchestrator would have to carry the entire intent detection logic and the sensitive-action validation. The workflow size would explode, and a strategy change (adding an AI provider, changing the confirmation policy) would require touching the core of the central system. ### Problems solved | Problem | Without this layer | With this layer | |---------|-------------------|-----------------| | **Rigid intent detection** | Keyword pattern matching only | LLM with fallback chain plus regex as last resort | | **Provider overload** | Direct call without quota check | `/api/usage/summary` pre-computes the available provider | | **Orphan free-text replies** | No link between question and free-text answer | `question_mappings` ties each expected message | | **Uncontrolled critical MCP** | Agent silently executes destructive tools | Mandatory Telegram confirmation with 90s timeout | | **Telegram callback > 64 bytes** | Silent API error | `mcp_approve_{id}` / `mcp_reject_{id}` (39 chars) below limit | ### Architectural choice Why split these three workflows rather than inline them in the Orchestrator? | Approach | Pro | Con | |----------|-----|-----| | **Inline** | No Execute Workflow overhead | Orchestrator > 200 nodes, fragile editing | | **Extracted sub-workflows** | Isolated refactoring, independent evolution | API surface between parent and sub to maintain | Refactoring #273 explicitly extracted the Free Text Handler as SW-8, and #295 delivered the MCP Confirmation Handler as a standalone webhook to decouple the cli-ollama gateway from the main graph. --- ## 3. How? — Technical implementation ### Components | Workflow | ID | Nodes | Role | |----------|-----|-------|------| | IA Router - Intent Detection | `HWryf7aJkZa5FuUA` | 13 | Multi-provider intent detection with fallback | | Telegram Free Text Handler | `xwGcEOpzHkvx0HTR` | 10 | Dispatch free reply to `/api/questions/{id}/answer` | | MCP Confirmation Handler | `lyDYsxzTrDzZrfJk` | 3 | Webhook + format + Telegram send with inline keyboard | ### Lifecycle of a message ```mermaid flowchart TD Msg["Telegram message"] Orch["Orchestrator · 124 nodes"] CheckConv{"Active conversation?"} CheckAwait{"Awaiting text reply?"} subgraph IA["IA Router · 13 nodes"] direction TB Usage["GET /api/usage/summary"] Route["Switch fallback_chain[0]"] Codex["Codex · codex-yolo"] Gemini["Gemini · gemini-flash-yolo"] Keyword["Keyword regex routing"] Parse["Parse JSON response"] end subgraph FT["Free Text Handler · 10 nodes"] direction TB Lookup["DT question_mappings"] Post["POST /api/questions/{id}/answer"] Cleanup["Edit message + delete DT row"] end subgraph Conv["Conversation Agent"] direction TB Agent["Detect tool/plan/mcp blocks"] MCPCall["MCP tool call"] end subgraph MCP["MCP Confirmation Handler · 3 nodes"] direction TB Webhook["POST /webhook/mcp-confirmation"] Format["Format message + callbacks"] Send["Send Telegram + [Approve] [Reject]"] end Cb{"Button click"} CB11["SW-11 Conv Callback Handler"] Gateway["mcp-gateway:3001/confirm/{id}/respond"] Tool["Tool executes or rejected"] Msg --> Orch --> CheckAwait CheckAwait -->|yes| FT --> Post --> Cleanup CheckAwait -->|no| CheckConv CheckConv -->|yes| Conv --> Agent --> MCPCall CheckConv -->|no| IA --> Usage --> Route Route --> Codex --> Parse Route --> Gemini --> Parse Route --> Keyword --> Parse Parse --> Orch MCPCall -. critical tool .-> MCP --> Webhook --> Format --> Send Send --> Cb --> CB11 --> Gateway --> Tool ``` ### IA Router: fallback chain The workflow starts by hitting `GET http://cli-ollama:11434/api/usage/summary` which returns a pre-computed `fallback_chain` based on quotas and provider availability: ```json { "can_use": true, "fallback_chain": ["codex", "gemini"], "providers": { "codex": { "available": true, "can_use": true }, "gemini": { "available": true, "can_use": true } } } ``` The Switch node `Route by Availability` reads `fallback_chain[0]` and routes to the matching branch: | Branch | Provider | Model | Typical confidence | |--------|----------|-------|--------------------| | `codex` | OpenAI Codex via cli-ollama | `codex-yolo` | 0.85 - 0.95 | | `gemini` | Google Gemini via cli-ollama | `gemini-flash-yolo` | 0.80 - 0.90 | | `keyword` | Regex fallback (no LLM) | — | 0.6 - 0.7 | Each LLM branch calls `POST /api/generate` with a prompt that forces a strict JSON response of the form `{"intent": ..., "service": ..., "action": ..., "params": {...}, "confidence": ...}`. The Parse Response node extracts the JSON via regex, applies safe defaults, and returns everything to the parent. > **Tip - The -yolo suffix** > > The `-yolo` suffix on the model name is mandatory in this infrastructure. Without it the underlying CLI starts in interactive (approval) mode and the HTTP Request times out after 120 seconds. The suffix short-circuits approval mode for automated calls. ### Free Text Handler: from message to API When an interactive question is opened by cli-ollama (for instance: "what is the title of the new workflow?"), a row is inserted into the `question_awaiting_text` Data Table with a `qid_short`. At the next message the Orchestrator detects this state and delegates to SW-8: ``` Execute Workflow Trigger -> Handle Free Text Answer (extract qid_short, chat_id, free_text) -> Load Question for Answer (DT question_mappings by qid_short) -> POST Free Text Answer (cli-ollama /api/questions/{id}/answer) -> Cleanup Messages (prepare message_ids for edit) -> Edit Header Free Text (edit original message with answer) -> Loop Over Items [0] -> Send Confirmation "Your answer has been forwarded" [1] -> Delete Awaiting (DT question_awaiting_text) -> Delete Question Mapping (DT question_mappings) ``` Two Data Tables are involved: `question_awaiting_text` (`YYEsLEk9s3XDbo89`) stores pending inputs, `question_mappings` (`mXrPedR5PKYCUSpo`) ties a `qid_short` to the Telegram `message_id` so the message holding the question can be edited retroactively. ### MCP Confirmation Handler: approving critical tools The MCP gateway (`mcp-gateway:3001`) keeps a list of tools considered critical (any write operation against N8N: create, update, delete, activate, deactivate). When Codex CLI invokes one of these tools during a conversation, the gateway pauses execution, generates a `confirmation_id` (12 hex chars via `secrets.token_hex(6)`), and POSTs the following payload to `/webhook/mcp-confirmation`: ```json { "type": "mcp_confirmation", "confirmation_id": "mcp_a1b2c3d4e5f6", "session_id": "tg_5883063462_1234567890", "tool_name": "n8n_create_workflow", "arguments_summary": "{\"name\": \"Test Workflow\", ...}", "chatId": "5883063462", "callback_url": "http://mcp-gateway:3001/confirm/mcp_a1b2c3d4e5f6/respond", "timeout_seconds": 90 } ``` The workflow has three nodes: 1. **Webhook Trigger** — Receives the POST in `responseMode: immediately` (200 OK direct) 2. **Format Confirmation Message** (Code) — Builds the HTML text and the `mcp_approve_{id}` / `mcp_reject_{id}` callbacks 3. **Send Confirmation** (Telegram sendMessage) — Sends the message with a 2-button inline keyboard ### MCP callback convention | Pattern | Meaning | |---------|---------| | `mcp_approve_{confirmation_id}` | Approve the critical tool execution | | `mcp_reject_{confirmation_id}` | Reject the execution | The total length of a `mcp_approve_mcp_a1b2c3d4e5f6` callback is 39 characters, well under the Telegram 64-byte limit. The `confirmation_id` coming from `secrets.token_hex(6)` stays non-guessable, which prevents an attacker with access to the conversation from forging a callback for another `confirmation_id`. Callbacks flow through the normal Telegram path: Orchestrator -> Callback Router -> SW-11 (Conversation Callback Handler, `e0bLff6av97daYvi`, 87 nodes). SW-11 detects the `mcp_` prefix and routes to `Parse MCP Action`, which splits `confirm` actions (the approve/reject pair) from other MCP actions. For `confirm` two nodes are added: - **Call MCP Confirm** (HTTP Request) — POST to `http://mcp-gateway:3001/confirm/{confirmation_id}/respond` with body `{"approved": true|false}`, header `Authorization: Bearer {{ $env.N8N_MCP_AUTH_TOKEN }}` - **Edit Confirm Result** (Telegram editMessageText) — Replaces the original message with "MCP tool approved." or "MCP tool rejected." > **Danger - 90-second timeout** > > The MCP gateway holds an `asyncio.Event` on the Python side waiting for the N8N response. If nobody clicks within 90 seconds, the event times out, the gateway automatically rejects the execution, and Codex CLI receives a `-32001` error (Action denied by user). No critical action can run without explicit human validation. ### Per-conversation MCP config (#294) Each conversation carries its own MCP config stored in the `conversations` Data Table (column `mcp_config`, JSON string). The `/mcp` command opens a paginated menu (SW-11) that lets the user enable or disable each tool. The derived `mcp_enabled` flag is computed by the Orchestrator on every turn: - If no tool is enabled: `mcp_enabled = false` -> Codex CLI is launched with `-c 'mcp_servers={}'` (no MCP server loaded) - If at least one tool is enabled: `mcp_enabled = true` + `allowed_tools` whitelist -> Codex can call tools, but cli-ollama filters unauthorized calls (403 if a tool is outside the list) This per-conversation configuration allows a "read-only" conversation against N8N (list_workflows only) and an "admin" conversation on the same infrastructure, without mixing permissions. --- ## 4. What if? — Outlook and limits ### Current limits | Limit | Impact | Mitigation | |-------|--------|------------| | **IA Router latency 2-5s** | First hop on every free-text message | Acceptable, masked by Telegram latency | | **Approximate keyword fallback** | Confidence 0.6 - 0.7 when no LLM is available | Wide regex patterns, route to `general` when in doubt | | **MCP timeout fixed at 90s** | No per-tool configuration | Sufficient for human confirmation on mobile | | **Free Text not context-aware** | No N8N-side validation of the expected format | cli-ollama validates downstream, error visible if malformed | ### Evolution scenarios **If more LLM providers are needed**: - Add a case in the `Route by Availability` Switch - Add a Prepare Prompt + Call Provider + Parse Response node trio - Update `/api/usage/summary` so it exposes the new provider in `fallback_chain` **If MCP pre-approval is needed**: - Add a "whitelisted user" mode in the gateway that bypasses confirmation for pre-validated tools - Or add a per-conversation policy (auto-approve when `mcp_auto_approve = true`) **If IA Router precision needs measuring**: - Log decisions into an `intent_decisions` Data Table (text, predicted_service, actual_service after feedback) - Compute weekly precision via a scheduled workflow --- ## Related pages ### Workflows - [Telegram Orchestrator](/en/workflows/telegram-orchestrator/) — Central hub that calls the IA Router and the Free Text Handler - [Conversational System](/en/workflows/systeme-conversationnel/) — Multi-turn conversations that trigger the MCP confirmations - [Service Handlers](/en/workflows/service-handlers/) — Docker, Odoo, N8N, General handlers invoked after the IA Router - [Question Hub](/en/workflows/question-hub/) — Interactive questions whose answers flow through the Free Text Handler ### Infrastructure - [AI Stack](/en/infrastructure/ai-stack/) — cli-ollama, MCP gateway, Codex CLI ### Reference - [Glossary](/en/reference/glossary/) — MCP, Intent, Callback, Sub-workflow ## Metadonnees agent - Cet article est issu du blog GuiGPaP Lab. - Contexte global du blog: https://blog.guigpap.com/llms.txt - Contact auteur: https://odoo.guigpap.com/mon-cv - Licence: CC-BY-SA 4.0