8 · Optional Capabilities
MCS keeps the base contract tiny. Optional behavior is declared by the Supports… contract a driver implements and mirrored as capability flags in DriverMeta. Consumers feature-detect before invoking an optional method: ask the object (isinstance(driver, SupportsX)) when you hold it, or read meta.capabilities when you only have the metadata. The client-side pattern below shows both.
Extend via capabilities, e.g.:
| Capability | Flag | Suggested Mix-in / Interface | Description |
|---|---|---|---|
| Health check | healthcheck | abstract class SupportsHealthcheck { abstract healthcheck() -> dict } | Returns status info, e.g., {"status": "OK"}. |
| Resource preload | cache | abstract class SupportsCache { abstract warmup() -> void } | Preloads resources for faster execution. |
| Status & metrics | status | abstract class SupportsStatus { abstract get_status() -> dict } | Provides runtime metrics or detailed status. |
| Autostart | autostart | abstract class SupportsAutostart { abstract autostart(kwargs: dict) -> void } | Launches required infrastructure (e.g., containers). |
| Native tools | native_tools | abstract class SupportsNativeTools { abstract get_native_tool_context() -> NativeToolContext } | Provides structured tool definitions for native tool-calling APIs (see below). |
Rule of Thumb: For easy use cases, name the mixin class Supports<CapabilityName> with the method named <capabilityName>. This convention simplifies dynamic invocation but is not mandatory. SDKs may define their own standards for common capabilities.
How DriverMeta carries capabilities
A capability has two halves, and they answer two different questions at two different moments:
| Question | Moment | Answer |
|---|---|---|
| "Which driver should I pick?" | before you hold one | DriverMeta — the data sheet |
| "Can this object do X?" | you hold the driver | isinstance(driver, SupportsX) — the contract |
The contract is the truth. A driver that implements SupportsHealthcheck
is an instance of it, so the client can simply ask the object. Since nothing
wraps a driver any more, that answer is always available and
always current.
The data sheet is a plain, serializable record — a registry entry, a package
index, a line of JSON. It exists for the consumer who has metadata but no
driver object, and cannot call isinstance on anything. That is the original
purpose of DriverMeta: finding the right driver among many.
Two fields serve that search, and their similar names should not mislead:
bindings[].capabilityis the driver's subject matter —"rest","csv","mailread". This is the primary selector: "give me a driver formailoverimap." NoSupports…contract corresponds to it, and none should.capabilitieslists the optional contracts the driver class satisfies, as theirCAPABILITYflags: roles ("standalone","orchestratable") and optional features ("healthcheck","streaming","native_tools").
DriverMeta offers exactly two operations over that second field — one writing,
one reading:
- Projection —
meta.derive_capabilities(cls)returns a copy with everyCAPABILITYflag thatclscarries added (idempotent, scanning its MRO). Because the flags are derived from the contracts rather than typed out beside them, the data sheet cannot drift away from the object: it is a serializable shadow of the truth, not a second copy of it. - Catalog read —
meta.has_capability(Contract)is a pure read over those flags, keeping the flag together with its contract instead of forcing string literals on the caller.
The data sheet stays strictly static — it describes the driver class, and must be reproducible as a JSON or XML record without instantiating anything. Whatever a particular instance was configured with at runtime, notably the middleware a client hung into it, therefore stays out of it.
There is deliberately no resolution operation. Capabilities that intervene
in how MCS handles a call (authentication, permission, lifecycle hooks) are
middleware inside the driver, not wrappers around it — so the
driver keeps its identity and nothing hides its interfaces from an isinstance
check. An orchestrator is opaque: it advertises only what it provides
itself, never the capabilities of the drivers it holds (see
Middleware → two axes of composition).
Using a capability from the client's side
When you hold the driver, ask the object — that is the whole sequence, and the route a client should take:
# `driver` may be a plain driver or an orchestrator — the client does not care.
if isinstance(driver, SupportsHealthcheck):
result = driver.healthcheck()
Reach for the flags only where there is no object to ask — selecting from metadata alone:
# a registry picking a driver it has not instantiated yet
candidates = [m for m in registry if m.has_capability(SupportsStreaming)]
Why on DriverMeta and not on the driver interface? So the core MCSDriver
contract stays minimal — it gains no methods for this. Projection and catalog
read are pure metadata operations, and they stay on the metadata. The payoff:
the driver author stays simple — a plain driver needs none of this — and the
client treats every MCSDriver the same, whether it is a plain driver or an
orchestrator, no matter what was injected.
NativeToolContext: Native Tool-Calling Support
The core MCS contract is text-centric: get_driver_system_message() returns a string, process_llm_response() accepts a string or dict. This works universally -- every LLM can consume text prompts.
However, some LLMs (notably OpenAI's GPT family and Anthropic's Claude) offer native tool-calling APIs where tools are passed as structured objects alongside the system message, rather than embedded in the prompt text. The LLM then returns structured tool_calls objects instead of text that needs parsing.
The NativeToolContext capability bridges this gap. A driver that supports it can provide its tools in the structured format that native tool-calling APIs expect:
struct NativeToolContext {
system_message: string // the system prompt (may exclude tool descriptions)
tools: array[dict] // tools in the LLM provider's native format (e.g. OpenAI function schema)
}
abstract class SupportsNativeTools {
abstract get_native_tool_context(model_name?: string) -> NativeToolContext
}
How it works
When using native tool-calling:
- The client calls
get_native_tool_context()instead ofget_driver_system_message(). - The returned
system_messagecontains only the behavioral prompt (usage instructions, formatting guidance) -- tool descriptions are not inlined, since they are provided separately intools. - The
toolsarray contains tool definitions in the provider's native schema (e.g. OpenAI's{"type": "function", "function": {"name": ..., "parameters": ...}}format). - The client passes
system_messageas the system prompt andtoolsas thetoolsparameter to the LLM API. - The LLM responds with structured
tool_callsobjects. The client passes these toprocess_llm_response(), which accepts dicts via theExtractionStrategychain (see Section 11).
Why this is optional
Native tool-calling is not universally supported. Many models (open-source, local, or older commercial models) only work with text prompts. The text-based path (get_driver_system_message() + text parsing) remains the universal default. NativeToolContext is an optimization for clients that target specific providers.
Relationship to BaseDriver
In the Python SDK, BaseDriver implements SupportsNativeTools by default. It derives the tools array from the MCSToolDriver.list_tools() output, converting each Tool into the OpenAI function-calling schema. The system_message is generated from the PromptStrategy but without inlined tool descriptions. This means any driver that inherits from BaseDriver automatically supports native tool-calling without additional code.