May 31, 2026

Outbound MCP client ships; vision probe-detected; news on feedparser

Chalie can now act as an MCP client against any remote MCP server (TKT-763): new mcp_client_servers table, McpClientService with CRUD/ping/sync/15-min heartbeat

Chalie can now act as an MCP client against any remote MCP server (TKT-763): new mcp_client_servers table, McpClientService with CRUD/ping/sync/15-min heartbeat, mcp_manager ability (SYSTEM+DISCOVERABLE), dual-DB find_tools search across abilities.sqlite and data/mcp_tools.sqlite, mcp_ dispatch with seeded ask/deny policy rows, and a /api/mcp-clients REST blueprint. The follow-up refactor (88f99ed) wires mcp* tool inputSchemas from mcp_tools.sqlite into find_tools so the model sees real parameters instead of brute-forcing, deletes a ~70-line duplicate _enforce_mcp_policy in favor of the unified enforce_policy, and caps the MCP score at min(0.12, abs(bm25)) so strong MCP matches sit just below the ability RRF band instead of flat-0.5-evicting abilities. TKT-785 makes add_server idempotent — resolves to a normalized endpoint key and upserts on match, purging stale mcp tool-index and policy rows on rename. The Brain MCP sidebar relabels “MCP Server” → “MCP”, the find_tools discovery hint drops its static “remote MCP tool” placeholder for the bare server-reported tool.name, and a leaked grck.lan placeholder in the Outbound form is swapped for https://mcp.example.com/mcp. An Edit button on outbound server cards now PUTs changes and silently fires ping_and_sync to re-index tools.

Vision (TKT-715) becomes a probe-detected PROVIDER capability: on provider save a synchronous 3-shapes + text PNG probe with an exact JSON prompt and a weighted score ≥0.80 sets providers.supports_vision. Multimodal send-path adds an image branch to all four LLM converters (llm_service + ollama_service) behind a thin vision_service; image-free messages convert byte-for-byte identically. vision_provider_id resolves explicit → auto → none; the Brain gains a vision badge on providers and a new Vision settings page; image_context_service.analyze() uses vision instead of OCR when a vision provider is configured (else OCR + no-vision note); the chat composer hides image upload when no vision provider is available. A legacy image-attachment code path is removed. vision-test.png is vendored forward to unblock the rc-0.9.0 merge.

News swaps its hand-rolled RSS/Atom/RDF parsing for feedparser + rapidfuzz + nh3 (TKT-733, net -109 LOC): feedparser replaces _parse_feed/_parse_rss_items/_parse_atom_entries and the image extractors and absorbs the RFC2822 date parser; rapidfuzz (C-backed Levenshtein) replaces a two-row DP title dedup; nh3 + html.unescape replace the regex + manual-entity HTML strip. _entry_to_article unifies the parse path for _parse_feed and fetch_google_news; malformed feeds now tolerate partial recovery and only return [] on zero entries. The NewsArticle 8-field contract and the public API are unchanged, so abilities/news.py, world_awareness_service.py, and the redis cache round-trip are unaffected. A separate fix (TKT-787) drops a redundant double-sort in rank_by_relevance that resolved equal-cosine-score ties oldest-first, replacing it with a single (score, published_at) reverse=True sort — ISO-8601 UTC strings order newest-first lexicographically — and a new test pins the contract.

ACT-trail pills are keyed by call_id, and Ollama was minting ids as ollama_ with the index resetting every LLM response, so the same tool at the same position across ACT iterations collided on ollama_find_tools_0 and rendered as a duplicate pill with a stuck spinner (TKT-786). Gemini had the same class of bug with a millisecond timestamp. The id is an opaque correlation key — Ollama/Gemini message replay correlate by position/name and Anthropic keeps its real block.id — so both providers now append a uuid8 suffix (ollama, gemini_), and a new test_tool_call_id_uniqueness.py pins the contract. Verified on a live rc-0.9.0 QA env: COLLISION:True → COLLISION:False.

Test cleanup deletes 8 mock-saturated documents endpoint tests that patched _get_document_service/get_data_graph_service/_process_upload and asserted on the patched return values (they passed even when the feature was broken — real coverage lives in nightly scenarios 060/062/063); the upload-unsupported-extension real 400 branch and pure-function helper tests are kept. Nine of twelve vision_probe unit tests are dropped as tautological (they mocked the model call and asserted the scorer scored its own hand-fed constants); the three _extract_json parse-robustness cases stay, with test_json_in_code_fence_is_parsed guarding the S5857 greedy-regex fix. Upload/document filename sanitization (TKT-735) is consolidated on werkzeug.secure_filename via a new services/filename_utils.py with a 255-char cap and ‘’ fallback; api/upload.py and api/documents.py drop their hand-rolled helpers and dead import re. A leftover utc_now F401 is removed from message-processor.

  • Outbound MCP client (TKT-763): connect Chalie to remote MCP servers with CRUD, heartbeat, dual-DB find_tools, mcp_ dispatch, and seeded policy; Edit UI + idempotent upsert (TKT-785); mcp* inputSchemas wired to LLM (TKT-763 follow-up)

  • Vision (TKT-715) is now probe-detected per provider: 3-shapes + text PNG probe with weighted ≥0.80 score sets supports_vision; multimodal image branch in all four converters behind vision_service; replaces OCR when configured

  • News (TKT-733) swaps hand-rolled RSS/Atom for feedparser + rapidfuzz + nh3 (net -109 LOC); TKT-787 fixes rank_by_relevance to break equal-score ties newest-first via single reverse=True sort on (score, published_at)

  • ACT-trail tool-call id collisions fixed (TKT-786): Ollama/Gemini now mint ollama_ and gemini_, removing the per-response index/timestamp reset that caused duplicate pills with stuck spinners

  • Upload/document sanitization consolidated on werkzeug.secure_filename (TKT-735) via new services/filename_utils.py with a 255-char cap and ‘’ fallback; eight mock-saturated documents endpoint tests deleted (real coverage in nightly 060/062/063)