Verfify if already possible to dynamically add tools to the tool set, by changing the configuration file or with that that the user can toggle tools on a GUI.
Injecting tools by the orchestrators strategy to list only titles, and the llm gets a tool to call the details. Making larger toolsset more token efficient.
As tools grow in number, the orchestrator should be able to paginate the tools to avoid token limits. For that the orchestartor should inject a pagination tool, when the tool set exceeds a certain number of tools.
Affects: packages/core/src/mcs/driver/core/base.py → _model_supports_native_tools()
Status: Open / Undecided
Problem:
DriverBase._model_supports_native_tools() uses a lazy import of
litellm.supports_function_calling() to check whether a model supports native
tool calls. This implicitly pulls in the entire litellm dependency (including
~2600 model entries in model_cost) into mcs-core.
Options:
- Standalone package
mcs-model-registry– references / caches thelitellm.model_costJSON and exposes a slimsupports_function_calling(model)API without the rest of litellm. - Explicit configuration – the capability is supplied from outside
(e.g. via
DriverMeta, a constructor parameter, or a pluggable registry). - Keep the status quo – lazy import with no hard dependency entry;
works without litellm (fallback
False).
Trade-offs:
- The driver should ideally not need to actively fetch anything at runtime.
- litellm itself may fetch
model_costfrom the network – requires connectivity. - Prompts are already designed to be loadable at runtime → a similar pattern could apply here.
- Not a blocking issue since the fallback (
False) works reliably.
Affects: packages/core/src/mcs/driver/core/base.py → _extract()
Status: Open
Problem:
The claim-based extraction chain distinguishes native tool-call responses from
plain text by inspecting the response shape (e.g. presence of a
"tool_calls" key). This covers >99% of practical cases, but an edge case
remains: when a native-tool-capable model is called without tools and
produces JSON in content that resembles a text-based tool call,
TextExtractionStrategy could false-positive.
Possible solutions:
- Pass
model_nametoprocess_llm_responseso the extraction chain can be context-aware (implies a signature change). - Introduce session-level state after
get_driver_context– the driver remembers whether native tools were supplied and skips text extraction accordingly. - Accept the edge case as negligible for now (models called with
toolswill always use native format; withouttoolsthe text strategy is the only sensible fallback anyway). - Setting the model name in the driver fix, or the format to choose. Maybe with the Strategy, since GPT-4.o and GPT-5 following the same pattern.
Affects: .github/workflows/
Status: Open / Planned
Problem:
Publishing to PyPI is currently a manual process (build + twine upload
for each package). With 9 independently versioned packages this is
error-prone and tedious.
Desired state: A GitHub Actions workflow that:
- Triggers on version-tag push (e.g.
mcs-driver-core/v0.3.0). - Builds the tagged package (
python -m build). - Runs the test suite for that package.
- Publishes to PyPI via
twineusing a trusted publisher (OIDC) or API token stored in GitHub secrets.
Considerations:
- Each package has its own release cadence → per-package tags are preferable over a single monorepo tag.
- A matrix build for all packages on every push to
main(lint + test only, no publish) would catch regressions early. uvcould be used in CI for faster dependency resolution.
Affects: all packages — Adapters, ToolDrivers, Drivers, Orchestrators
Status: Open
Priority: High
Problem:
Open WebUI demonstrates what happens when observability is neglected: Tool
Server specs are fetched with success logged at DEBUG only, failures are
silently swallowed with continue, and the UI gives zero feedback on whether
tools were loaded. The result is an undebuggable black box — users cannot
tell if a tool server connected, how many tools were registered, or why
nothing works.
MCS must avoid this pattern. Every layer transition (Adapter connect,
ToolDriver registration, Driver init, Orchestrator tool injection) should
produce at least one INFO-level log entry on success and a clear
WARNING/ERROR on failure — including actionable context (URL, tool count,
error reason).
Concrete requirements:
- Adapter — log on connect/disconnect with target info (URL, path, host).
- ToolDriver — log number of tools registered after adapter init
(
INFO: ToolDriver registered 12 tools from <source>). - Driver — log which tools were injected into the prompt and whether native or text-based tool calling is used.
- Orchestrator — log strategy selection, pagination state, and final tool count delivered to the driver.
- Errors — never silently
continuepast a failed connection or parse error. Always log with enough context to diagnose without a debugger.
Anti-patterns to avoid (learned from Open WebUI):
- Success on
DEBUG, failure onERRORbut swallowed → user sees nothing. - No UI/API feedback on tool registration status.
- Lazy loading without any signal that loading happened.
Affects: alle Adapter-Pakete, neuer mcs-credential-core oder Teil von mcs-driver-core
Status: Open / Design Phase
Problem:
Jeder Adapter löst Authentifizierung aktuell selbst: IMAP/SMTP nehmen
user + password, HTTP nimmt headers, LocalFS braucht nichts. Mit
OAuth2-basierten Backends (Gmail API, Microsoft Graph, Slack, GitHub, ...)
kommt ein ganz anderer Auth-Flow hinzu: Token Vault, OAuth2 Refresh,
Service Accounts, etc.
Ohne gemeinsame Abstraktion müsste jeder Adapter seinen eigenen OAuth-Code mitbringen -- oder fest an einen Anbieter (Auth0, Azure AD) gebunden sein.
Gewünschter Zustand:
Ein CredentialProvider-Protocol (structural typing, wie MailboxPort),
das Adaptern eine einheitliche Schnittstelle für Credentials bietet:
@runtime_checkable
class CredentialProvider(Protocol):
def get_access_token(self) -> str: ...Konkrete Implementierungen:
StaticCredentials(user, password)-- für IMAP, SMTP, SMBAuth0TokenVaultProvider(domain, client_id, ...)-- OAuth2 via Auth0OAuthRefreshProvider(client_id, secret, refresh_token)-- direkter OAuth2EnvCredentials(env_var)-- aus Umgebungsvariablen
Adapter akzeptieren dann optional credentials: CredentialProvider als
Alternative zu expliziten user + password Parametern.
Offene Fragen:
- Gehört das in
mcs-driver-coreoder ein eigenesmcs-credential-core? - Brauchen wir neben
get_access_token()auchget_username(),get_headers(), etc.? - Wie geht man mit Token-Refresh (Expiry, Retry) um?
- Soll der Provider synchron oder async sein?
- Zusammenspiel mit Auth0 Token Vault: Token Exchange vs. Direct Token.
Kontext:
Auth0 Hackathon "Authorized to Act" (Deadline: 2026-04-06) ist ein guter
Anlass, das Pattern in einem Beispielprojekt (mcs-examples/gmail_agent)
zu validieren, bevor es in die Core-Library wandert.
Affects: all ToolDrivers and Adapters/Connectors (especially mcs-driver-mailread, mcs-driver-mailsend)
Status: Open
Priority: Medium
Problem:
Adapters and connectors validate credentials eagerly in __init__, even though
list_tools() returns static metadata that never touches the adapter. This
means callers must provide valid credentials just to discover available tools.
Example chain (Gmail):
MailDriver.__init__
→ MailToolDriver.__init__
→ MailreadToolDriver.__init__
→ GmailMailboxConnector.__init__
→ raise ValueError("Either 'access_token' or '_credential' must be provided")
But MailreadToolDriver.list_tools() is just return list(_TOOLS) -- a static
constant that does not need the adapter at all.
Impact:
- The Skill Generator (
scripts/skill_generator.py) must inject a dummy credential to instantiate auth-aware drivers for tool discovery. - Any tooling that wants to introspect tools (inspectors, registries, UIs) hits the same problem.
- Violates Interface Segregation:
list_tools()andexecute_tool()have completely different resource requirements, but__init__validates for the latter.
Fix:
Store credential parameters in __init__ without validation. Validate on
first actual use (_get_token(), connect(), or first execute_tool() call).
# Before (eager -- blocks tool discovery)
def __init__(self, *, access_token=None, _credential=None):
if _credential is not None:
self._token = lambda: _credential.get_token("gmail")
elif access_token is not None:
self._token = access_token
else:
raise ValueError(...)
# After (lazy -- list_tools() works without credentials)
def __init__(self, *, access_token=None, _credential=None):
self._credential = _credential
self._access_token = access_token
def _get_token(self) -> str:
if self._credential is not None:
return self._credential.get_token("gmail")
if self._access_token is not None:
return self._access_token if isinstance(self._access_token, str) else self._access_token()
raise ValueError("Either 'access_token' or '_credential' must be provided")Affected files (non-exhaustive):
packages/drivers/mcs-driver-mailread/src/mcs/driver/mailread/gmail_connector.pypackages/drivers/mcs-driver-mailsend/src/mcs/driver/mailsend/gmail_sender.pypackages/adapters/mcs-adapter-imap/src/mcs/adapter/imap/adapter.pypackages/adapters/mcs-adapter-smtp/src/mcs/adapter/smtp/adapter.py- Any future adapter that validates credentials in
__init__
Related: .cursor/rules/lazy-adapter-init.mdc captures this as a design
rule for new code.
Affects: PyPI
Status: Open
Problem:
An earlier version was published under the name mcs-drivers-core (plural).
The canonical name is now mcs-driver-core (singular, consistent with the
mcs-driver-<capability> naming convention). The old package should be
yanked or updated with a deprecation notice pointing to mcs-driver-core.
Affects: mcs-driver-core -- DriverBase.process_llm_response, MCSDriver-Interface; Clients
Status: Open / Design konsolidiert -- eine offene Designfrage (Permission UX vs. Security)
Ausgangsfrage (ursprünglich):
Sollte MCS ein event-basiertes Hook-System (PreToolUse, PermissionRequest,
PostToolUse/Failure, PostToolBatch -- vgl. PIs pi.on(...)) bekommen, um das
Verhalten rund um Tool-Calls zu vereinheitlichen, statt es über Mixins,
Signale und Interfaces zu verteilen?
Befund (nach Code-Analyse): Die meisten dieser Punkte sind im Code bereits idiomatisch gelöst -- es braucht weder einen globalen Eventbus noch einen Generator-/yield-Umbau. MCS hat zwei tragende Säulen, die zusammen fast alles abdecken:
- Pull über
DriverResponse(mcs_driver_interface.py) -- ein diskriminiertes Status-Objekt:call_executed|call_failed+retry_prompt| keins (= finale Antwort). Trägt bereits einen Pre-Fehler (unbekanntes Tool kommt zurück, bevorexecute_toolläuft,base.py:100). Deckt PostToolUse + PostToolUseFailure ab. - In-band Challenge-as-result über MRO-Mixin (
mcs.auth.mixin.AuthMixin): Tool-Execution wirftAuthChallenge; der Mixin legt sich via MRO umexecute_tool, fängt sie und konvertiert sie in ein normales Tool-Ergebnis ({"auth_required": true, "url": ...}). Die interaktive Anforderung reist im Tool-Result-Kanal und wird durch den Multi-Turn-Loop des Clients aufgelöst -- kein Pausieren, kein Out-of-Band-Event. Ohne Mixin fängtDriverBase.process_llm_responsejede Exception zu sauberemcall_failed(base.py:119) -- kein roher Crash. Permission kann demselben Muster folgen (PermissionMixin, Consent-Check vorsuper().execute_tool()).
Was fehlt -- und nur das ist hier zu bauen: Reine Live-UX-Notification ("Tool startet jetzt", "Tool ist zurück"), die weder der Response noch das in-band-Muster liefern kann und die bewusst nicht durch die LLM-Konversation laufen soll. Das ist der "darf, aber muss nicht / nicht kritisch"-Fall.
Empfehlung -- Observer-Parameter, nicht Hook-Bus:
-
Optionaler, read-only
observer-Parameter anprocess_llm_response, analog zum bereits vorhandenenstreaming-kwarg imMCSDriver-Interface:def process_llm_response(self, llm_response, *, streaming=False, observer=None) -> DriverResponse: ...
Aufrufpunkte in
DriverBase.process_llm_responseumbase.py:117:on_call_started(name, args)/on_call_finished(name, result)/on_call_failed(name, err), jeweils nurif observer is not None.ToolEventObserver= schlankesProtocol. DefaultNone= heutiges Verhalten. -
Ebene = Driver (
process_llm_response), NICHTMCSToolDriver/execute_tool. Begründung: (a)execute_toolist die Ausführungs-Primitive -- "vorher/nachher" ist Orchestrierung, undtool_name/arguments/resultliegen ohnehin inprocess_llm_response; (b) ISP -- ein REST-/CSV-/FS-ToolDriver soll nicht UI-Notifications orchestrieren; (c) Orchestrator-Layer (DetailLoading, Pagination) rufenexecute_toolintern mehrfach -- die user-sichtbare "ein Call"-Grenze kennt nur die Driver-Ebene. -
Per-Call-Parameter, kein
set_observer().DriverBaseist vertraglich stateless / thread-safe (mcs_driver_interface.py:152); ein Observer als Instanz-Attribut würde das brechen. -
Kein Capability-Mixin nötig. Der Observer ist rein additiv (Client gibt ihn mit oder nicht) -- kein
isinstance-Gate wie beiSupportsDriverContext, wo der Client das Verhalten vorab kennen muss. "Grundsätzlich bereitgestellt" ergibt sich daraus, dass die Aufrufpunkte inDriverBasesitzen -- jeder Driver erbt sie. -
Progress ("Verlauf des Calls") später & separat. Als einziger der drei entsteht er innerhalb
execute_toolund bräuchte den Observer auf der Ausführungsebene (Signatur-Eingriff). Opt-in nur für ToolDriver, bei denen es Sinn ergibt -- nicht in den ersten Wurf zwingen.
Verworfen (bewusst):
- Globaler Eventbus à la PI (
pi.on(...)): PI ist eine Endanwendung und darf implizit/global sein; MCS ist eine Library -- globale, unsichtbare Hook-Magie verletzt "explizit über implizit" und die DPI-Konvention. - Generator / yield-send-Lebenszyklus: löst nur das Pausier-Problem bei interaktiven Flows -- das erledigt das in-band-Muster bereits eleganter und atomar.
- Hooks ersetzen Mixins: falsche Dichotomie. Capability-Detection
(Mixin/
isinstance, "was ist der Treiber") und Lifecycle-Notification (Observer, "was passiert um den Call") sind getrennte Achsen.
Verbleibende offene Designfrage -- entscheidet den Permission-Weg: Ist Permission ein UX-Komfort-Gate oder eine harte Sicherheitsgrenze gegen den Agenten?
- UX → in-band-Muster (
PermissionMixinanalogAuthMixin) reicht; die Freigabe läuft über die Konversation. - Security → in-band ist prompt-injection-anfällig (Freigabe im selben
Kanal wie der untrusted LLM-Output). Dann out-of-band nötig: Client-Callback
oder
plan()/execute()-Split, garantiert vom Client (nicht vom LLM) kontrolliert.
Bezug zu bestehenden TODOs:
- Observability (INFO-Logging je Layer-Übergang) -- der
ToolEventObserverist der natürliche erste Konsument. - CredentialProvider -- das in-band-Muster ist der Credential-/Auth-Pfad;
CredentialProvider.get_tokenist bereits synchron (auth/provider.py:33), passend zum synchronen Driver.
Referenz -- PI (earendil-works/pi): als Vergleich, dessen globaler Bus für
MCS bewusst nicht übernommen wird. Relevante Mappings: tool_call (kann
blocken, input mutierbar) ≈ Pre/Permission; tool_execution_start/_end ≈
Notification-Hooks; tool_result (modifizierbar) ≈ Post; turn_end ≈
PostToolBatch. PI hat keinen eigenen PermissionRequest-Event -- Permission
ist dort Deny innerhalb tool_call.
- https://pi.dev/ · https://github.com/earendil-works/pi/tree/main/packages/coding-agent
- Hook/Event-Doku:
packages/coding-agent/docs/extensions.md