Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,13 @@ Per Decision S5 of the design spike, backend-agnostic high-level sections
only the Llama-Stack-specific synthesis controls: which baseline to start
from, an optional profile file, and a raw native_override escape hatch.

During synthesis from the default baseline or a profile, LCORE ensures the
Llama Stack MCP tool_runtime provider (`provider_id: model-context-protocol`,
`provider_type: remote::model-context-protocol`) is present so static
`mcp_servers` and dynamic MCP registration work. That ensure is skipped when
`baseline: empty` (migration / blank-slate); supply MCP via `native_override`
in that case.

Attributes:
baseline: Synthesis starting point. "default" begins from LCORE's
built-in baseline (src/data/default_run.yaml); "empty" begins from
Expand Down
7 changes: 5 additions & 2 deletions docs/deployment_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ starts from a *baseline*. By default that is LCORE's built-in baseline; a

A profile is an ordinary `run.yaml`-shaped YAML file — the same schema Llama
Stack reads natively. Everything else in the unified pipeline (enrichment,
the high-level `inference.providers` section, `native_override`) is applied
*on top* of the profile, in that order.
the high-level `inference.providers` section, ensuring the MCP tool_runtime
provider, then `native_override`) is applied *on top* of the profile, in that
order. The MCP ensure adds `provider_id: model-context-protocol` when missing
so static `mcp_servers` and dynamic MCP registration work; it is skipped only
for `baseline: empty` (use `native_override` there if you need MCP).

**Authoring a profile.** Start from one of the reference profiles shipped in
[`examples/profiles/`](https://github.com/lightspeed-core/lightspeed-stack/tree/main/examples/profiles):
Expand Down
4 changes: 4 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@
MCP_AUTH_CLIENT: Final[str] = "client"
MCP_AUTH_OAUTH: Final[str] = "oauth"

# MCP tool_runtime provider (Llama Stack run.yaml / unified synthesis)
MCP_TOOL_RUNTIME_PROVIDER_ID: Final[str] = "model-context-protocol"
MCP_TOOL_RUNTIME_PROVIDER_TYPE: Final[str] = "remote::model-context-protocol"

# Media type constants for streaming responses
MEDIA_TYPE_JSON: Final[str] = "application/json"
MEDIA_TYPE_TEXT: Final[str] = "text/plain"
Expand Down
7 changes: 6 additions & 1 deletion src/data/default_run.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
# It is intentionally thinner than the repo-root run.yaml: it carries only the
# APIs and providers needed to boot a minimal, queryable stack (inference,
# safety, vector_io, agents, tool_runtime, files) plus the storage backends
# those providers reference. Operators extend it via high-level sections or
# those providers reference. tool_runtime includes rag-runtime (file_search /
# RAG) and model-context-protocol (MCP) so those capabilities work when
# operators enable them. Operators extend it via high-level sections or
# `native_override`.
#
# NOTE: `external_providers_dir` carries a default (`:=~/.llama/providers.d`)
Expand Down Expand Up @@ -54,6 +56,9 @@ providers:
- config: {}
provider_id: rag-runtime
provider_type: inline::rag-runtime
- config: {}
provider_id: model-context-protocol
provider_type: remote::model-context-protocol
vector_io:
- config:
persistence:
Expand Down
57 changes: 53 additions & 4 deletions src/llama_stack_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,47 @@ def apply_high_level_inference(
)


def ensure_mcp_tool_runtime(ls_config: dict[str, Any]) -> None:
"""Ensure the default MCP tool_runtime provider exists in ``ls_config``.

Adds ``tool_runtime`` to ``apis`` when missing, then appends the default
``model-context-protocol`` provider under ``providers.tool_runtime`` when
no entry with that ``provider_id`` is already present. Existing entries
(including ``rag-runtime``) are left untouched.

Parameters:
ls_config: The Llama Stack configuration being synthesized (modified
in place).

Returns:
None: ``ls_config`` is modified in place.
"""
apis = ls_config.setdefault("apis", [])
if "tool_runtime" not in apis:
apis.append("tool_runtime")

providers_section = ls_config.setdefault("providers", {})
tool_runtime = providers_section.setdefault("tool_runtime", [])
for existing in tool_runtime:
if (
isinstance(existing, dict)
and existing.get("provider_id") == constants.MCP_TOOL_RUNTIME_PROVIDER_ID
):
return

tool_runtime.append(
{
"provider_id": constants.MCP_TOOL_RUNTIME_PROVIDER_ID,
"provider_type": constants.MCP_TOOL_RUNTIME_PROVIDER_TYPE,
"config": {},
}
)
logger.info(
"Added MCP tool_runtime provider provider_id=%r",
constants.MCP_TOOL_RUNTIME_PROVIDER_ID,
)


def _resolve_profile_path(profile: str, config_file_dir: Optional[str]) -> Path:
"""Resolve a ``profile:`` path against the loaded config's directory (R8).

Expand Down Expand Up @@ -812,8 +853,9 @@ def synthesize_configuration(
Implements the unified-mode synthesis pipeline: select a baseline (profile
file, empty, or the built-in default), apply the existing enrichment
(Azure Entra ID, BYOK RAG, Solr/OKP) for parity with legacy mode (R7),
expand the high-level ``inference.providers`` section, and deep-merge the
raw ``native_override`` last (R5).
expand the high-level ``inference.providers`` section, ensure the default
MCP tool_runtime provider when the baseline was not empty, and deep-merge
the raw ``native_override`` last (R5).

Parameters:
lcs_config: The full ``lightspeed-stack.yaml`` parsed into a dict.
Expand All @@ -829,13 +871,15 @@ def synthesize_configuration(
unified = llama_stack.get("config") # None when only top-level inputs are set

# 1-2. Select the baseline.
baseline_was_empty = False
if unified and unified.get("profile"):
profile_path = _resolve_profile_path(unified["profile"], config_file_dir)
logger.info("Loading synthesis baseline from profile %s", profile_path)
with open(profile_path, "r", encoding="utf-8") as file:
baseline = yaml.safe_load(file) or {}
elif unified and unified.get("baseline") == "empty":
logger.info("Synthesizing from an empty baseline")
baseline_was_empty = True
baseline = {}
else:
baseline = (
Expand All @@ -860,11 +904,16 @@ def synthesize_configuration(
if inference.get("providers"):
apply_high_level_inference(ls_config, inference)

# 6. Raw escape hatch, deep-merged last with list replacement (R5).
# 6. Ensure MCP tool_runtime for default/profile baselines (skipped for
# baseline: empty so migrate round-trips stay lossless).
if not baseline_was_empty:
ensure_mcp_tool_runtime(ls_config)

# 7. Raw escape hatch, deep-merged last with list replacement (R5).
if unified and unified.get("native_override"):
ls_config = deep_merge_list_replace(ls_config, unified["native_override"])

# 7. Dedupe again in case native_override or enrichment reintroduced dupes.
# 8. Dedupe again in case native_override or enrichment reintroduced dupes.
dedupe_providers_vector_io(ls_config)

return ls_config
Expand Down
5 changes: 4 additions & 1 deletion tests/e2e/configuration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ and the active `lightspeed-stack.yaml` is also copied to the repo root — so th
relative `profile:` path resolves to that materialized file, which the unified
synthesizer consumes as its baseline. LS behavior is identical to the legacy
two-file path (requirement R7 in the config-merge design doc); the wiring that
selects a provider config stays unchanged.
selects a provider config stays unchanged. The synthesizer also ensures the
MCP `tool_runtime` provider (`model-context-protocol`) is present on the
profile baseline when missing; many `run-*.yaml` fixtures already include it,
so the ensure is typically a no-op.

The `tests/e2e/configs/run-*.yaml` files therefore serve a dual role: in
server mode they are the run configuration of the standalone Llama Stack
Expand Down
Loading
Loading