diff --git a/crates/traverse-mcp/src/lib.rs b/crates/traverse-mcp/src/lib.rs
index 35c52ec7..139e4d46 100644
--- a/crates/traverse-mcp/src/lib.rs
+++ b/crates/traverse-mcp/src/lib.rs
@@ -2,6 +2,7 @@
//!
//! Governed by spec 015-capability-discovery-mcp and spec 042-mcp-library-surface
+mod prepare_cache;
mod stdio_server;
pub mod context;
@@ -9,6 +10,9 @@ pub mod error;
pub mod tools;
pub use context::McpContext;
+pub use prepare_cache::{
+ PrepareCacheError, PrepareCacheEvidence, parse_registry_ref, prepare_verified_cache,
+};
pub use stdio_server::*;
use std::collections::HashMap;
diff --git a/crates/traverse-mcp/src/main.rs b/crates/traverse-mcp/src/main.rs
index 8b627099..e56acc91 100644
--- a/crates/traverse-mcp/src/main.rs
+++ b/crates/traverse-mcp/src/main.rs
@@ -1,36 +1,145 @@
+use std::path::PathBuf;
use std::process::ExitCode;
-use traverse_mcp::run_stdio_server;
+use traverse_mcp::{prepare_verified_cache, run_stdio_server};
fn main() -> ExitCode {
run(std::env::args().skip(1))
}
+const USAGE: &str = "Usage: traverse-mcp stdio [--cache
] [--simulate-startup-failure]\n traverse-mcp prepare-cache --synced-state --cache [--ref ]... [--json]";
+
/// Testable core of [`main`]: takes the argument iterator directly instead of
/// reading `std::env::args()` so the CLI parsing branches can be exercised
/// without spawning a subprocess.
-fn run(mut args: impl Iterator- ) -> ExitCode {
+fn run(args: impl Iterator
- ) -> ExitCode {
+ match parse_command(args) {
+ Err(message) => {
+ eprintln!("{message}");
+ ExitCode::from(1)
+ }
+ Ok(Command::Stdio {
+ simulate_startup_failure,
+ cache,
+ }) => match run_stdio_server(simulate_startup_failure, cache) {
+ Ok(()) => ExitCode::SUCCESS,
+ Err(error) => {
+ eprintln!("traverse-mcp stdio server failed: {error:?}");
+ ExitCode::from(1)
+ }
+ },
+ Ok(Command::PrepareCache {
+ synced_state,
+ cache,
+ refs,
+ json_output,
+ }) => match prepare_verified_cache(&synced_state, &cache, &refs) {
+ Ok(evidence) => {
+ if json_output {
+ println!("{}", evidence.envelope());
+ } else {
+ println!("{}", evidence.render());
+ }
+ ExitCode::SUCCESS
+ }
+ Err(error) => {
+ if json_output {
+ println!("{}", error.envelope());
+ } else {
+ eprintln!("{error}");
+ }
+ ExitCode::from(1)
+ }
+ },
+ }
+}
+
+enum Command {
+ Stdio {
+ simulate_startup_failure: bool,
+ cache: Option,
+ },
+ PrepareCache {
+ synced_state: PathBuf,
+ cache: PathBuf,
+ refs: Vec,
+ json_output: bool,
+ },
+}
+
+fn parse_command(mut args: impl Iterator
- ) -> Result {
let Some(command) = args.next() else {
- eprintln!("Usage: traverse-mcp stdio [--simulate-startup-failure]");
- return ExitCode::from(1);
+ return Err(USAGE.to_string());
};
+ match command.as_str() {
+ "stdio" => parse_stdio(args),
+ "prepare-cache" => parse_prepare_cache(args),
+ other => Err(format!("Unsupported command: {other}")),
+ }
+}
- if command != "stdio" {
- eprintln!("Unsupported command: {command}");
- return ExitCode::from(1);
+fn parse_stdio(mut args: impl Iterator
- ) -> Result {
+ let mut simulate_startup_failure = false;
+ let mut cache = None;
+ while let Some(argument) = args.next() {
+ match argument.as_str() {
+ "--simulate-startup-failure" => simulate_startup_failure = true,
+ "--cache" => {
+ let value = args
+ .next()
+ .ok_or_else(|| "stdio --cache requires ".to_string())?;
+ cache = Some(PathBuf::from(value));
+ }
+ other => return Err(format!("Unsupported stdio flag: {other}")),
+ }
}
+ Ok(Command::Stdio {
+ simulate_startup_failure,
+ cache,
+ })
+}
- let simulate_startup_failure = args.any(|argument| argument == "--simulate-startup-failure");
- match run_stdio_server(simulate_startup_failure) {
- Ok(()) => ExitCode::SUCCESS,
- Err(error) => {
- eprintln!("traverse-mcp stdio server failed: {error:?}");
- ExitCode::from(1)
+fn parse_prepare_cache(mut args: impl Iterator
- ) -> Result {
+ let mut synced_state = None;
+ let mut cache = None;
+ let mut refs = Vec::new();
+ let mut json_output = false;
+ while let Some(argument) = args.next() {
+ match argument.as_str() {
+ "--synced-state" => {
+ let value = args
+ .next()
+ .ok_or_else(|| "prepare-cache --synced-state requires ".to_string())?;
+ synced_state = Some(PathBuf::from(value));
+ }
+ "--cache" => {
+ let value = args
+ .next()
+ .ok_or_else(|| "prepare-cache --cache requires ".to_string())?;
+ cache = Some(PathBuf::from(value));
+ }
+ "--ref" => {
+ let value = args.next().ok_or_else(|| {
+ "prepare-cache --ref requires ".to_string()
+ })?;
+ refs.push(value);
+ }
+ "--json" => json_output = true,
+ other => return Err(format!("Unsupported prepare-cache flag: {other}")),
}
}
+ Ok(Command::PrepareCache {
+ synced_state: synced_state
+ .ok_or_else(|| "prepare-cache requires --synced-state ".to_string())?,
+ cache: cache.ok_or_else(|| "prepare-cache requires --cache ".to_string())?,
+ refs,
+ json_output,
+ })
}
#[cfg(test)]
mod tests {
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
+
use super::*;
#[test]
@@ -54,4 +163,260 @@ mod tests {
ExitCode::from(1)
);
}
+
+ #[test]
+ fn stdio_rejects_unknown_flags() {
+ assert_eq!(
+ run(["stdio".to_string(), "--unknown".to_string()].into_iter()),
+ ExitCode::from(1)
+ );
+ }
+
+ #[test]
+ fn stdio_cache_requires_a_directory() {
+ assert_eq!(
+ run(["stdio".to_string(), "--cache".to_string()].into_iter()),
+ ExitCode::from(1)
+ );
+ }
+
+ #[test]
+ fn prepare_cache_requires_synced_state_and_cache() {
+ assert_eq!(
+ run(["prepare-cache".to_string()].into_iter()),
+ ExitCode::from(1)
+ );
+ assert_eq!(
+ run([
+ "prepare-cache".to_string(),
+ "--synced-state".to_string(),
+ "/tmp/missing-synced-state.json".to_string()
+ ]
+ .into_iter()),
+ ExitCode::from(1)
+ );
+ assert_eq!(
+ run([
+ "prepare-cache".to_string(),
+ "--cache".to_string(),
+ "/tmp/missing-mode-b-cache".to_string()
+ ]
+ .into_iter()),
+ ExitCode::from(1)
+ );
+ }
+
+ #[test]
+ fn prepare_cache_rejects_unknown_flags_and_missing_ref_value() {
+ assert_eq!(
+ run([
+ "prepare-cache".to_string(),
+ "--synced-state".to_string(),
+ "state.json".to_string(),
+ "--cache".to_string(),
+ "cache".to_string(),
+ "--bogus".to_string()
+ ]
+ .into_iter()),
+ ExitCode::from(1)
+ );
+ assert_eq!(
+ run([
+ "prepare-cache".to_string(),
+ "--synced-state".to_string(),
+ "state.json".to_string(),
+ "--cache".to_string(),
+ "cache".to_string(),
+ "--ref".to_string()
+ ]
+ .into_iter()),
+ ExitCode::from(1)
+ );
+ }
+
+ #[test]
+ fn parse_stdio_accepts_cache_flag() {
+ let command = parse_command(
+ [
+ "stdio".to_string(),
+ "--cache".to_string(),
+ "/srv/cache".to_string(),
+ ]
+ .into_iter(),
+ )
+ .expect("parse");
+ match command {
+ Command::Stdio {
+ simulate_startup_failure,
+ cache,
+ } => {
+ assert!(!simulate_startup_failure);
+ assert_eq!(cache, Some(PathBuf::from("/srv/cache")));
+ }
+ Command::PrepareCache { .. } => panic!("expected stdio"),
+ }
+ }
+
+ #[test]
+ fn prepare_cache_flag_values_are_required() {
+ assert_eq!(
+ run(["prepare-cache".to_string(), "--synced-state".to_string()].into_iter()),
+ ExitCode::from(1)
+ );
+ assert_eq!(
+ run([
+ "prepare-cache".to_string(),
+ "--synced-state".to_string(),
+ "state.json".to_string(),
+ "--cache".to_string()
+ ]
+ .into_iter()),
+ ExitCode::from(1)
+ );
+ }
+
+ #[test]
+ fn prepare_cache_run_reports_json_and_text_failures() {
+ assert_eq!(
+ run([
+ "prepare-cache".to_string(),
+ "--synced-state".to_string(),
+ "/tmp/missing-traverse-mode-b-state.json".to_string(),
+ "--cache".to_string(),
+ "/tmp/missing-traverse-mode-b-cache".to_string(),
+ "--json".to_string()
+ ]
+ .into_iter()),
+ ExitCode::from(1)
+ );
+ assert_eq!(
+ run([
+ "prepare-cache".to_string(),
+ "--synced-state".to_string(),
+ "/tmp/missing-traverse-mode-b-state.json".to_string(),
+ "--cache".to_string(),
+ "/tmp/missing-traverse-mode-b-cache".to_string()
+ ]
+ .into_iter()),
+ ExitCode::from(1)
+ );
+ }
+
+ #[test]
+ fn prepare_cache_run_prepares_a_local_file_snapshot() {
+ let root = std::env::temp_dir().join(format!(
+ "traverse-mcp-mode-b-cli-{}-{}",
+ std::process::id(),
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .expect("time")
+ .as_nanos()
+ ));
+ std::fs::create_dir_all(&root).expect("temp");
+ let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .and_then(std::path::Path::parent)
+ .expect("workspace")
+ .to_path_buf();
+ let wasm = repo.join(
+ "examples/core-normalize-participants/artifacts/core-normalize-participants.wasm",
+ );
+ let contract = repo.join("examples/core-normalize-participants/contract.json");
+ let wasm_bytes = std::fs::read(&wasm).expect("wasm");
+ let contract_bytes = std::fs::read(&contract).expect("contract");
+ let digest = |bytes: &[u8]| {
+ use sha2::{Digest, Sha256};
+ let hashed = Sha256::digest(bytes);
+ let mut out = String::from("sha256:");
+ for byte in hashed {
+ use std::fmt::Write as _;
+ let _ = write!(out, "{byte:02x}");
+ }
+ out
+ };
+ let snapshot = serde_json::json!({
+ "schema_version": "1.0.0",
+ "workspace_id": "mode-b-cli",
+ "state_scope": "public_registry_synced",
+ "source_repo": "traverse-framework/registry",
+ "release_tag": "index-v1",
+ "index_version": 1,
+ "generated_at": "2026-09-11T00:00:00Z",
+ "source_commit": null,
+ "synced_at": "2026-09-11T00:00:00Z",
+ "record_count": 1,
+ "validation_status": "valid",
+ "governing_spec": "055-registry-sync",
+ "capabilities": [{
+ "namespace": "core",
+ "id": "core.normalize-participants",
+ "version": "1.1.0",
+ "digest": digest(&wasm_bytes),
+ "artifact_url": format!("file://{}", wasm.display()),
+ "contract_digest": digest(&contract_bytes),
+ "contract_url": format!("file://{}", contract.display()),
+ "deprecated": false
+ }],
+ "events": []
+ });
+ let state = root.join("state.json");
+ std::fs::write(&state, serde_json::to_vec(&snapshot).expect("json")).expect("write");
+ let cache = root.join("cache");
+ assert_eq!(
+ run([
+ "prepare-cache".to_string(),
+ "--synced-state".to_string(),
+ state.display().to_string(),
+ "--cache".to_string(),
+ cache.display().to_string(),
+ "--json".to_string()
+ ]
+ .into_iter()),
+ ExitCode::SUCCESS
+ );
+ assert_eq!(
+ run([
+ "prepare-cache".to_string(),
+ "--synced-state".to_string(),
+ state.display().to_string(),
+ "--cache".to_string(),
+ cache.display().to_string()
+ ]
+ .into_iter()),
+ ExitCode::SUCCESS
+ );
+ let _ = std::fs::remove_dir_all(&root);
+ }
+
+ #[test]
+ fn parse_prepare_cache_accepts_refs_and_json() {
+ let command = parse_command(
+ [
+ "prepare-cache".to_string(),
+ "--synced-state".to_string(),
+ "state.json".to_string(),
+ "--cache".to_string(),
+ "cache".to_string(),
+ "--ref".to_string(),
+ "core/core.normalize-participants@=1.1.0".to_string(),
+ "--json".to_string(),
+ ]
+ .into_iter(),
+ )
+ .expect("parse");
+ match command {
+ Command::PrepareCache {
+ synced_state,
+ cache,
+ refs,
+ json_output,
+ } => {
+ assert_eq!(synced_state, PathBuf::from("state.json"));
+ assert_eq!(cache, PathBuf::from("cache"));
+ assert_eq!(refs, vec!["core/core.normalize-participants@=1.1.0"]);
+ assert!(json_output);
+ }
+ Command::Stdio { .. } => panic!("expected prepare-cache"),
+ }
+ }
}
diff --git a/crates/traverse-mcp/src/prepare_cache.rs b/crates/traverse-mcp/src/prepare_cache.rs
new file mode 100644
index 00000000..7154b6c6
--- /dev/null
+++ b/crates/traverse-mcp/src/prepare_cache.rs
@@ -0,0 +1,509 @@
+//! Mode B host CLI: prepare a Spec 080 / Spec 520 verified registry cache.
+//!
+//! Network I/O is confined to this explicit prepare step. The MCP stdio host
+//! then serves discover/validate/execute/report from the resulting cache only.
+
+use serde_json::{Value, json};
+use std::fs;
+use std::path::Path;
+use std::process::Command;
+use traverse_embedder::{
+ HostRegistryCache, RegistryArtifactFetcher, RegistryCacheError, RegistryCacheErrorCode,
+ RegistryPrepareEvidence, prepare_registry_dependency, publish_public_metadata,
+};
+use traverse_registry::{RegistryReference, SyncedPublicRegistryState};
+
+const GOVERNING_SPEC: &str = "080-embedded-registry-cache";
+
+/// Secret-free Mode B prepare-cache failure.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PrepareCacheError {
+ /// Stable Spec 080 FR-007 code (or `registry_ref_invalid` for CLI parse).
+ pub code: String,
+ /// Human-readable explanation without paths, credentials, or bytes.
+ pub message: String,
+}
+
+impl PrepareCacheError {
+ fn new(code: impl Into, message: impl Into) -> Self {
+ Self {
+ code: code.into(),
+ message: message.into(),
+ }
+ }
+
+ fn from_cache(error: RegistryCacheError) -> Self {
+ Self {
+ code: error.code.as_str().to_string(),
+ message: error.message,
+ }
+ }
+
+ /// Machine-readable error envelope for `--json` callers.
+ #[must_use]
+ pub fn envelope(&self) -> Value {
+ json!({
+ "kind": "mcp_mode_b_prepare_cache_error",
+ "governing_spec": GOVERNING_SPEC,
+ "code": self.code,
+ "message": self.message,
+ })
+ }
+}
+
+impl std::fmt::Display for PrepareCacheError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}: {}", self.code, self.message)
+ }
+}
+
+impl std::error::Error for PrepareCacheError {}
+
+/// Successful Mode B cache preparation evidence.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PrepareCacheEvidence {
+ /// One prepare result per requested (or snapshot-derived) `registry_ref`.
+ pub entries: Vec,
+}
+
+impl PrepareCacheEvidence {
+ /// Machine-readable success envelope for `--json` callers.
+ #[must_use]
+ pub fn envelope(&self) -> Value {
+ json!({
+ "kind": "mcp_mode_b_prepare_cache",
+ "governing_spec": GOVERNING_SPEC,
+ "cache_prepared": true,
+ "entry_count": self.entries.len(),
+ "entries": self.entries.iter().map(|entry| json!({
+ "namespace": entry.namespace,
+ "id": entry.id,
+ "selected_version": entry.selected_version,
+ "version_range": entry.version_range,
+ "source_release": entry.source_release,
+ "index_digest": entry.index_digest,
+ "artifact_digest": entry.artifact_digest,
+ "outcome": entry.outcome,
+ })).collect::>(),
+ })
+ }
+
+ /// Human-readable success text without cache paths.
+ #[must_use]
+ pub fn render(&self) -> String {
+ let mut lines = vec![format!(
+ "prepared {} registry_ref(s) into a verified Spec 520 cache",
+ self.entries.len()
+ )];
+ for entry in &self.entries {
+ lines.push(format!(
+ "{}@{} {}",
+ entry.id, entry.selected_version, entry.artifact_digest
+ ));
+ }
+ lines.join("\n")
+ }
+}
+
+/// Prepare one or more public `registry_ref` values into a host-owned cache.
+///
+/// When `refs` is empty, every non-deprecated snapshot capability is prepared
+/// at its exact published version (`={version}`).
+///
+/// # Errors
+///
+/// Returns a stable, secret-free code when the synced snapshot is missing or
+/// invalid, a ref cannot be parsed, or Spec 080 prepare fails.
+pub fn prepare_verified_cache(
+ synced_state_path: &Path,
+ cache_root: &Path,
+ refs: &[String],
+) -> Result {
+ let snapshot = load_synced_state(synced_state_path)?;
+ let references = if refs.is_empty() {
+ snapshot_exact_refs(&snapshot)?
+ } else {
+ refs.iter()
+ .map(|raw| parse_registry_ref(raw))
+ .collect::, _>>()?
+ };
+
+ prepare_refs(&snapshot, cache_root, &references)
+}
+
+/// Parse `--ref /@`.
+///
+/// # Errors
+///
+/// Returns `registry_ref_invalid` when the token is missing a namespace, id,
+/// or version range.
+pub fn parse_registry_ref(raw: &str) -> Result {
+ let (namespace, rest) = raw.split_once('/').ok_or_else(|| {
+ PrepareCacheError::new(
+ "registry_ref_invalid",
+ "registry_ref must be namespace/id@version_range",
+ )
+ })?;
+ let (id, version_range) = rest.split_once('@').ok_or_else(|| {
+ PrepareCacheError::new(
+ "registry_ref_invalid",
+ "registry_ref must be namespace/id@version_range",
+ )
+ })?;
+ if namespace.is_empty() || id.is_empty() || version_range.is_empty() {
+ return Err(PrepareCacheError::new(
+ "registry_ref_invalid",
+ "registry_ref must be namespace/id@version_range",
+ ));
+ }
+ Ok(RegistryReference {
+ namespace: namespace.to_string(),
+ id: id.to_string(),
+ version_range: version_range.to_string(),
+ })
+}
+
+fn load_synced_state(path: &Path) -> Result {
+ let raw = fs::read(path).map_err(|_| {
+ PrepareCacheError::new(
+ RegistryCacheErrorCode::RegistrySyncMissing.as_str(),
+ "synced registry index snapshot is missing",
+ )
+ })?;
+ serde_json::from_slice(&raw).map_err(|_| {
+ PrepareCacheError::new(
+ RegistryCacheErrorCode::RegistrySyncMissing.as_str(),
+ "synced registry index snapshot is missing or malformed",
+ )
+ })
+}
+
+fn snapshot_exact_refs(
+ snapshot: &SyncedPublicRegistryState,
+) -> Result, PrepareCacheError> {
+ let refs = snapshot
+ .capabilities
+ .iter()
+ .filter(|record| !record.deprecated)
+ .map(|record| RegistryReference {
+ namespace: record.namespace.clone(),
+ id: record.id.clone(),
+ version_range: format!("={}", record.version),
+ })
+ .collect::>();
+ if refs.is_empty() {
+ return Err(PrepareCacheError::new(
+ RegistryCacheErrorCode::RegistrySyncMissing.as_str(),
+ "synced registry index snapshot contains no preparable capabilities",
+ ));
+ }
+ Ok(refs)
+}
+
+fn prepare_refs(
+ snapshot: &SyncedPublicRegistryState,
+ cache_root: &Path,
+ references: &[RegistryReference],
+) -> Result {
+ let cache = HostRegistryCache::new(cache_root);
+ let fetcher = HostCliFetcher;
+ let mut entries = Vec::with_capacity(references.len());
+ for reference in references {
+ let evidence = prepare_registry_dependency(&cache, snapshot, reference, &fetcher)
+ .map_err(PrepareCacheError::from_cache)?;
+ entries.push(evidence);
+ }
+ publish_public_metadata(&cache, snapshot, false).map_err(PrepareCacheError::from_cache)?;
+ Ok(PrepareCacheEvidence { entries })
+}
+
+/// Host-owned fetcher: `file://` from local bytes, `http(s)://` via curl.
+///
+/// Errors are secret-free and never echo URLs, paths, or artifact bytes.
+struct HostCliFetcher;
+
+impl RegistryArtifactFetcher for HostCliFetcher {
+ fn fetch(&self, url: &str) -> Result, String> {
+ if let Some(path) = url.strip_prefix("file://") {
+ return fs::read(path).map_err(|_| "host registry artifact fetch failed".to_string());
+ }
+ if url.starts_with("https://") || url.starts_with("http://") {
+ return fetch_http(url);
+ }
+ Err("host registry artifact fetch failed: unsupported url scheme".to_string())
+ }
+}
+
+fn fetch_http(url: &str) -> Result, String> {
+ let output = Command::new("curl")
+ .args(["-fsSL", url])
+ .output()
+ .map_err(|_| "host registry artifact fetch failed".to_string())?;
+ if output.status.success() {
+ Ok(output.stdout)
+ } else {
+ Err("host registry artifact fetch failed".to_string())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
+
+ use super::*;
+ use sha2::{Digest, Sha256};
+ use std::fmt::Write as _;
+ use std::path::PathBuf;
+ use std::sync::atomic::{AtomicU64, Ordering};
+ use traverse_embedder::read_public_metadata;
+ use traverse_registry::{PublicRegistryCapabilityRecord, PublicUseCaseSummary};
+
+ const KIT_ID: &str = "core.normalize-participants";
+ const KIT_NAMESPACE: &str = "core";
+ const KIT_VERSION: &str = "1.1.0";
+
+ fn repo_root() -> PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .and_then(Path::parent)
+ .expect("workspace root")
+ .to_path_buf()
+ }
+
+ fn sha256_prefixed(bytes: &[u8]) -> String {
+ let mut hasher = Sha256::new();
+ hasher.update(bytes);
+ let mut out = String::from("sha256:");
+ for byte in hasher.finalize() {
+ write!(out, "{byte:02x}").expect("writing to a String cannot fail");
+ }
+ out
+ }
+
+ fn kit_wasm_bytes() -> Vec {
+ fs::read(repo_root().join(
+ "examples/core-normalize-participants/artifacts/core-normalize-participants.wasm",
+ ))
+ .expect("kit wasm")
+ }
+
+ fn kit_contract_bytes() -> Vec {
+ fs::read(repo_root().join("examples/core-normalize-participants/contract.json"))
+ .expect("kit contract")
+ }
+
+ fn fresh_root(tag: &str) -> PathBuf {
+ static SEQ: AtomicU64 = AtomicU64::new(0);
+ let seq = SEQ.fetch_add(1, Ordering::Relaxed);
+ let root = std::env::temp_dir().join(format!(
+ "traverse-mcp-mode-b-{tag}-{}-{seq}",
+ std::process::id()
+ ));
+ let _ = fs::remove_dir_all(&root);
+ fs::create_dir_all(&root).expect("temp root");
+ root
+ }
+
+ fn write_snapshot(dir: &Path) -> PathBuf {
+ let wasm = repo_root().join(
+ "examples/core-normalize-participants/artifacts/core-normalize-participants.wasm",
+ );
+ let contract = repo_root().join("examples/core-normalize-participants/contract.json");
+ let record = PublicRegistryCapabilityRecord {
+ namespace: KIT_NAMESPACE.to_string(),
+ id: KIT_ID.to_string(),
+ version: KIT_VERSION.to_string(),
+ digest: sha256_prefixed(&kit_wasm_bytes()),
+ artifact_url: format!("file://{}", wasm.display()),
+ contract_digest: sha256_prefixed(&kit_contract_bytes()),
+ contract_url: format!("file://{}", contract.display()),
+ deprecated: false,
+ summary: "Normalize raw participants into canonical records.".to_string(),
+ description: "Verified public kit fixture for Mode B prepare.".to_string(),
+ use_cases: vec![PublicUseCaseSummary {
+ scenario: "Resolve extracted names and emails to workspace members.".to_string(),
+ }],
+ service_type: "stateless".to_string(),
+ permitted_targets: vec!["wasm".to_string()],
+ lifecycle: "active".to_string(),
+ provenance: None,
+ };
+ let snapshot = SyncedPublicRegistryState {
+ schema_version: "1.0.0".to_string(),
+ workspace_id: "mode-b-fixture".to_string(),
+ state_scope: "public_registry_synced".to_string(),
+ source_repo: "traverse-framework/registry".to_string(),
+ release_tag: "index-v1".to_string(),
+ index_version: 1,
+ generated_at: "2026-09-11T00:00:00Z".to_string(),
+ source_commit: None,
+ synced_at: "2026-09-11T00:00:00Z".to_string(),
+ record_count: 1,
+ validation_status: "valid".to_string(),
+ governing_spec: "055-registry-sync".to_string(),
+ capabilities: vec![record],
+ events: Vec::new(),
+ };
+ let path = dir.join("synced-state.json");
+ fs::write(&path, serde_json::to_vec(&snapshot).expect("snapshot json")).expect("write");
+ path
+ }
+
+ #[test]
+ fn parse_registry_ref_accepts_namespace_id_and_range() {
+ let parsed = parse_registry_ref("core/core.normalize-participants@=1.1.0").unwrap();
+ assert_eq!(parsed.namespace, "core");
+ assert_eq!(parsed.id, KIT_ID);
+ assert_eq!(parsed.version_range, "=1.1.0");
+ }
+
+ #[test]
+ fn parse_registry_ref_rejects_malformed_tokens() {
+ for raw in ["", "core", "core/id", "/id@=1", "core/@=1", "core/id@"] {
+ let error = parse_registry_ref(raw).expect_err(raw);
+ assert_eq!(error.code, "registry_ref_invalid");
+ assert!(!error.envelope().to_string().contains(raw) || raw.is_empty());
+ }
+ }
+
+ #[test]
+ fn prepare_writes_verified_cache_and_public_metadata() {
+ let root = fresh_root("prepare");
+ let state = write_snapshot(&root);
+ let cache = root.join("cache");
+ let evidence = prepare_verified_cache(
+ &state,
+ &cache,
+ &[format!("{KIT_NAMESPACE}/{KIT_ID}@={KIT_VERSION}")],
+ )
+ .expect("prepare");
+ assert_eq!(evidence.entries.len(), 1);
+ assert_eq!(evidence.entries[0].id, KIT_ID);
+ assert_eq!(evidence.entries[0].selected_version, KIT_VERSION);
+ assert_eq!(evidence.entries[0].outcome, "prepared");
+ let envelope = evidence.envelope();
+ assert_eq!(envelope["kind"], json!("mcp_mode_b_prepare_cache"));
+ assert_eq!(envelope["governing_spec"], json!(GOVERNING_SPEC));
+ assert!(
+ !envelope
+ .to_string()
+ .contains(cache.to_string_lossy().as_ref())
+ );
+ let generation = read_public_metadata(&HostRegistryCache::new(&cache)).expect("metadata");
+ assert_eq!(generation.records.len(), 1);
+ assert_eq!(generation.records[0].id, KIT_ID);
+ let rendered = evidence.render();
+ assert!(rendered.contains(KIT_ID));
+ assert!(!rendered.contains(cache.to_string_lossy().as_ref()));
+ }
+
+ #[test]
+ fn prepare_without_refs_uses_exact_snapshot_versions() {
+ let root = fresh_root("all-refs");
+ let state = write_snapshot(&root);
+ let cache = root.join("cache");
+ let evidence = prepare_verified_cache(&state, &cache, &[]).expect("prepare all");
+ assert_eq!(evidence.entries[0].version_range, format!("={KIT_VERSION}"));
+ }
+
+ #[test]
+ fn prepare_fails_closed_for_missing_and_malformed_state() {
+ let root = fresh_root("missing");
+ let cache = root.join("cache");
+ let missing = prepare_verified_cache(&root.join("absent.json"), &cache, &[])
+ .expect_err("missing state");
+ assert_eq!(missing.code, "registry_sync_missing");
+ fs::write(root.join("bad.json"), b"{not-json").expect("bad json");
+ let malformed =
+ prepare_verified_cache(&root.join("bad.json"), &cache, &[]).expect_err("malformed");
+ assert_eq!(malformed.code, "registry_sync_missing");
+ assert_eq!(
+ malformed.envelope()["kind"],
+ json!("mcp_mode_b_prepare_cache_error")
+ );
+ }
+
+ #[test]
+ fn prepare_fails_closed_for_malformed_ref_token() {
+ let root = fresh_root("bad-ref");
+ let state = write_snapshot(&root);
+ let error = prepare_verified_cache(&state, &root.join("cache"), &["not-a-ref".to_string()])
+ .expect_err("malformed ref");
+ assert_eq!(error.code, "registry_ref_invalid");
+ }
+
+ #[test]
+ fn prepare_fails_closed_for_unknown_ref() {
+ let root = fresh_root("unknown-ref");
+ let state = write_snapshot(&root);
+ let cache = root.join("cache");
+ let error = prepare_verified_cache(
+ &state,
+ &cache,
+ &["other/unknown.capability@=1.0.0".to_string()],
+ )
+ .expect_err("unknown ref");
+ assert_eq!(error.code, "registry_version_not_found");
+ }
+
+ #[test]
+ fn prepare_error_display_is_secret_free() {
+ let error = PrepareCacheError::new("registry_sync_missing", "synced snapshot is missing");
+ assert_eq!(
+ format!("{error}"),
+ "registry_sync_missing: synced snapshot is missing"
+ );
+ assert_eq!(
+ error.to_string(),
+ "registry_sync_missing: synced snapshot is missing"
+ );
+ }
+
+ #[test]
+ fn prepare_rejects_snapshot_with_only_deprecated_capabilities() {
+ let root = fresh_root("deprecated");
+ let state = write_snapshot(&root);
+ let mut snapshot: SyncedPublicRegistryState =
+ serde_json::from_slice(&fs::read(&state).expect("read")).expect("parse");
+ snapshot.capabilities[0].deprecated = true;
+ fs::write(&state, serde_json::to_vec(&snapshot).expect("write")).expect("update");
+ let error =
+ prepare_verified_cache(&state, &root.join("cache"), &[]).expect_err("deprecated");
+ assert_eq!(error.code, "registry_sync_missing");
+ }
+
+ #[test]
+ fn host_fetcher_http_urls_fail_closed_without_echoing_the_url() {
+ let error = HostCliFetcher
+ .fetch("https://127.0.0.1:1/traverse-mode-b-missing")
+ .expect_err("http");
+ assert_eq!(error, "host registry artifact fetch failed");
+ assert!(!error.contains("127.0.0.1"));
+ let http = HostCliFetcher
+ .fetch("http://127.0.0.1:1/traverse-mode-b-missing")
+ .expect_err("http scheme");
+ assert_eq!(http, "host registry artifact fetch failed");
+ }
+
+ #[test]
+ fn host_fetcher_reads_file_urls_and_rejects_unknown_schemes() {
+ let wasm = repo_root().join(
+ "examples/core-normalize-participants/artifacts/core-normalize-participants.wasm",
+ );
+ let bytes = HostCliFetcher
+ .fetch(&format!("file://{}", wasm.display()))
+ .expect("file fetch");
+ assert_eq!(bytes, kit_wasm_bytes());
+ let error = HostCliFetcher
+ .fetch("ftp://example.test/module.wasm")
+ .expect_err("scheme");
+ assert_eq!(
+ error,
+ "host registry artifact fetch failed: unsupported url scheme"
+ );
+ let missing = HostCliFetcher
+ .fetch("file:///definitely-missing-traverse-mode-b.wasm")
+ .expect_err("missing file");
+ assert_eq!(missing, "host registry artifact fetch failed");
+ }
+}
diff --git a/crates/traverse-mcp/src/stdio_server.rs b/crates/traverse-mcp/src/stdio_server.rs
index 33478e0c..6d8e270e 100644
--- a/crates/traverse-mcp/src/stdio_server.rs
+++ b/crates/traverse-mcp/src/stdio_server.rs
@@ -1493,7 +1493,10 @@ fn observation_message_summary(message: McpObservationMessage) -> Value {
/// # Errors
///
/// Returns `catalog_load_failed` when the canonical expedition bundle cannot be loaded.
-pub fn run_stdio_server(simulate_startup_failure: bool) -> Result<(), StdioServerFailure> {
+pub fn run_stdio_server(
+ simulate_startup_failure: bool,
+ cache_root: Option,
+) -> Result<(), StdioServerFailure> {
let canonical_execution = CanonicalExecutionContext::load_canonical()?;
let catalog = McpDiscoveryCatalog::load_canonical()?;
@@ -1531,8 +1534,9 @@ pub fn run_stdio_server(simulate_startup_failure: bool) -> Result<(), StdioServe
// Loading fails closed — a missing or invalid prepared state stops startup
// with a stable error envelope rather than silently falling back to the
// expedition catalog (FR-003).
- if let Some(cache_root) = std::env::var_os(MODE_A_CACHE_ENV) {
- match ModeAContext::load(PathBuf::from(cache_root)) {
+ let cache_root = cache_root.or_else(|| std::env::var_os(MODE_A_CACHE_ENV).map(PathBuf::from));
+ if let Some(cache_root) = cache_root {
+ match ModeAContext::load(cache_root) {
Ok(context) => {
server = server.with_mode_a(Box::leak(Box::new(context)));
}
@@ -2905,7 +2909,7 @@ mod tests {
#[test]
fn run_stdio_server_reports_simulated_startup_failure() {
- let result = run_stdio_server(true);
+ let result = run_stdio_server(true, None);
assert!(result.is_err());
}
diff --git a/docs/mcp-mode-b-release-evidence.md b/docs/mcp-mode-b-release-evidence.md
new file mode 100644
index 00000000..482f6b85
--- /dev/null
+++ b/docs/mcp-mode-b-release-evidence.md
@@ -0,0 +1,64 @@
+# Traverse MCP Mode B — Release & Provenance Evidence
+
+Governed by spec [`080-embedded-registry-cache`](../specs/520-embedded-registry-cache/spec.md)
+(Spec 520 lineage). Mode B is the embedded-cache host track: the shipped
+`traverse-mcp` binary **prepares** a host-owned verified cache from public
+registry refs, then serves discover/validate/execute/report from that cache
+only. Consumers do not rewrite App-References trees via `registry materialize`.
+
+Mode A (Spec 119) remains the default Claude Desktop / Cursor path that
+**consumes** an already-prepared cache. Mode B adds the prepare host CLI.
+
+## Release form
+
+Mode B ships in the same versioned `traverse-mcp` binary as Mode A. Pin and
+verify that binary through the packaged MCP server artifact path in
+[docs/packaged-traverse-mcp-server-artifact.md](packaged-traverse-mcp-server-artifact.md)
+and [docs/mcp-mode-a-release-evidence.md](mcp-mode-a-release-evidence.md):
+
+1. Record the pinned version, e.g. `traverse-mcp 0.11.0`.
+2. Download the binary for the host target and its published `.sha256`.
+3. Recompute and compare: `shasum -a 256 traverse-mcp` must equal the published
+ digest.
+4. Confirm the provenance attestation names the same version tag and the
+ `cargo build --locked` build invocation.
+
+App-References `apps/llm-mcp-reference/mode-b/serve.sh` should invoke this
+pinned binary — not a source checkout and not a materialize rewrite.
+
+## Documented prepare → serve path
+
+Preparation is the only network-capable step. Serving is offline and
+fail-closed when the cache is missing or invalid.
+
+```bash
+traverse-mcp prepare-cache \
+ --synced-state /path/to/synced-public-registry-state.json \
+ --cache /path/to/verified-registry-cache \
+ --ref core/core.normalize-participants@=1.1.0 \
+ --json
+
+traverse-mcp stdio --cache /path/to/verified-registry-cache
+```
+
+Equivalent serve form (Mode A env, same verified-cache host):
+
+```bash
+TRAVERSE_MCP_REGISTRY_CACHE=/path/to/verified-registry-cache \
+ traverse-mcp stdio
+```
+
+`--ref` may be repeated. When omitted, every non-deprecated capability in the
+synced snapshot is prepared at its exact published version.
+
+See [docs/mcp-stdio-server.md](mcp-stdio-server.md) for the command surface.
+
+## Verification
+
+```bash
+bash scripts/ci/mcp_stdio_server_mode_b_smoke.sh
+```
+
+That smoke prepares a fixture cache from public-registry refs, drives stdio
+MCP execute against one digest-pinned capability, and asserts an unprepared
+cache fails closed.
diff --git a/docs/mcp-stdio-server.md b/docs/mcp-stdio-server.md
index 676bdda1..0c2569df 100644
--- a/docs/mcp-stdio-server.md
+++ b/docs/mcp-stdio-server.md
@@ -9,8 +9,8 @@ For the first `youaskm3` release-facing client path, use [docs/youaskm3-canonica
It is intentionally narrow:
- it stays a façade over Traverse runtime authority
-- it uses the canonical expedition registry bundle as its source of truth
-- it exposes discovery, description, validation, execution, and execution-report rendering through one stdio command surface
+- contributor `stdio` without a cache uses the canonical expedition registry bundle as its source of truth
+- Mode A / Mode B serve discover, description, validation, execution, and execution-report from a host-owned verified cache
- it is documented and runnable locally
## Supported Bootstrap Path
@@ -21,12 +21,15 @@ The supported developer bootstrap path for the dedicated MCP server is:
cargo run -p traverse-mcp -- stdio
```
-That `stdio` command is the only supported bootstrap mode in the current app-consumable release path.
+Supported host commands:
+
+- `stdio [--cache ] [--simulate-startup-failure]` — serve MCP on stdio
+- `prepare-cache --synced-state --cache [--ref ]... [--json]` — Mode B Spec 520 cache prepare
Unsupported bootstrap attempts fail loudly:
- omitting the command prints the usage line and exits non-zero
-- using any command other than `stdio` prints `Unsupported command: ` and exits non-zero
+- using any command other than `stdio` or `prepare-cache` prints `Unsupported command: ` and exits non-zero
Developers and agents should treat other bootstrap ideas as unsupported unless they are explicitly documented in this page or in the packaged artifact docs.
@@ -166,6 +169,38 @@ the checked-in verified kit fixture at
and asserts Mode A discovery + inline execute succeed and that an unprepared
cache fails closed.
+## Mode B Embedded Verified-Cache Host
+
+Governed by spec [`080-embedded-registry-cache`](../specs/520-embedded-registry-cache/spec.md).
+
+Mode B prepares a host-owned Spec 520 verified cache from public registry refs,
+then serves MCP from that cache only. App-References consumers point
+`mode-b/serve.sh` at the shipped `traverse-mcp` binary — they do not rewrite
+trees via `registry materialize`.
+
+```bash
+cargo run -p traverse-mcp -- prepare-cache \
+ --synced-state /path/to/synced-public-registry-state.json \
+ --cache /path/to/verified-registry-cache \
+ --ref core/core.normalize-participants@=1.1.0 \
+ --json
+
+cargo run -p traverse-mcp -- stdio --cache /path/to/verified-registry-cache
+```
+
+Prepare is the only network-capable step. A missing or invalid cache fails
+closed with `registry_sync_missing` / `registry_metadata_cache_invalid` /
+`registry_cache_entry_missing`. The versioned binary pin path for App-Refs
+Mode B is documented in
+[docs/mcp-mode-b-release-evidence.md](mcp-mode-b-release-evidence.md).
+
+```bash
+bash scripts/ci/mcp_stdio_server_mode_b_smoke.sh
+```
+
+The smoke prepares a fixture cache, drives stdio MCP execute against one
+digest-pinned capability, and asserts an unprepared `--cache` fails closed.
+
Run repository checks:
```bash
diff --git a/docs/releases/next.md b/docs/releases/next.md
index 3af58c2a..573fcef8 100644
--- a/docs/releases/next.md
+++ b/docs/releases/next.md
@@ -1,5 +1,13 @@
# Next Release Notes
+## Mode B embedded MCP host CLI
+
+`traverse-mcp prepare-cache` prepares a Spec 520 host-owned verified registry
+cache from public `registry_ref` values. `traverse-mcp stdio --cache `
+then serves discover/validate/execute/report from that cache only, without an
+expedition checkout or App-References materialize rewrite. Pin and verify the
+same versioned `traverse-mcp` binary documented for Mode A.
+
## ArtifactRouter WASI diagnosis
`ArtifactRouter` now forwards the concrete `WasmExecutor` failure text
diff --git a/scripts/ci/mcp_stdio_server_mode_b_smoke.sh b/scripts/ci/mcp_stdio_server_mode_b_smoke.sh
new file mode 100755
index 00000000..85427fab
--- /dev/null
+++ b/scripts/ci/mcp_stdio_server_mode_b_smoke.sh
@@ -0,0 +1,134 @@
+#!/usr/bin/env bash
+
+# Spec 520 / Spec 080 Mode B: prepare a host-owned verified cache from public
+# registry refs, then serve stdio MCP from that cache only. No expedition
+# checkout and no App-Refs materialize rewrite.
+
+set -euo pipefail
+
+repo_root=$(git rev-parse --show-toplevel)
+work_dir=$(mktemp -d)
+cache_dir="${work_dir}/verified-cache"
+state_path="${work_dir}/synced-state.json"
+stdout_log=$(mktemp)
+stderr_log=$(mktemp)
+prepare_log=$(mktemp)
+absent_stdout_log=$(mktemp)
+absent_stderr_log=$(mktemp)
+absent_cache_dir=$(mktemp -d)
+
+cleanup() {
+ rm -f "${stdout_log}" "${stderr_log}" "${prepare_log}" "${absent_stdout_log}" "${absent_stderr_log}"
+ rm -rf "${work_dir}" "${absent_cache_dir}"
+}
+trap cleanup EXIT
+
+wasm="${repo_root}/examples/core-normalize-participants/artifacts/core-normalize-participants.wasm"
+contract="${repo_root}/examples/core-normalize-participants/contract.json"
+if [[ ! -f "${wasm}" || ! -f "${contract}" ]]; then
+ echo "Missing Mode B kit fixture under examples/core-normalize-participants." >&2
+ exit 1
+fi
+
+python3 - "${state_path}" "${wasm}" "${contract}" <<'PY'
+import hashlib, json, sys
+from pathlib import Path
+
+state_path, wasm_path, contract_path = sys.argv[1], Path(sys.argv[2]), Path(sys.argv[3])
+wasm = wasm_path.read_bytes()
+contract = contract_path.read_bytes()
+
+def digest(data: bytes) -> str:
+ return "sha256:" + hashlib.sha256(data).hexdigest()
+
+record = {
+ "namespace": "core",
+ "id": "core.normalize-participants",
+ "version": "1.1.0",
+ "digest": digest(wasm),
+ "artifact_url": f"file://{wasm_path}",
+ "contract_digest": digest(contract),
+ "contract_url": f"file://{contract_path}",
+ "deprecated": False,
+ "summary": "Normalize raw participants into canonical records.",
+ "description": "Mode B prepare-cache smoke fixture.",
+ "use_cases": [{"scenario": "Resolve extracted names and emails to workspace members."}],
+ "service_type": "stateless",
+ "permitted_targets": ["wasm"],
+ "lifecycle": "active",
+ "provenance": None,
+}
+state = {
+ "schema_version": "1.0.0",
+ "workspace_id": "mode-b-smoke",
+ "state_scope": "public_registry_synced",
+ "source_repo": "traverse-framework/registry",
+ "release_tag": "index-v1",
+ "index_version": 1,
+ "generated_at": "2026-09-11T00:00:00Z",
+ "source_commit": None,
+ "synced_at": "2026-09-11T00:00:00Z",
+ "record_count": 1,
+ "validation_status": "valid",
+ "governing_spec": "055-registry-sync",
+ "capabilities": [record],
+ "events": [],
+}
+Path(state_path).write_text(json.dumps(state), encoding="utf-8")
+PY
+
+cargo run -p traverse-mcp -- prepare-cache \
+ --synced-state "${state_path}" \
+ --cache "${cache_dir}" \
+ --ref 'core/core.normalize-participants@=1.1.0' \
+ --json >"${prepare_log}"
+
+grep -q '"kind":"mcp_mode_b_prepare_cache"' "${prepare_log}"
+grep -q '"governing_spec":"080-embedded-registry-cache"' "${prepare_log}"
+grep -q '"cache_prepared":true' "${prepare_log}"
+grep -q '"id":"core.normalize-participants"' "${prepare_log}"
+if grep -q "${cache_dir}" "${prepare_log}"; then
+ echo "Mode B prepare evidence must not echo the cache path." >&2
+ exit 1
+fi
+
+kit_id="core.normalize-participants"
+kit_version="1.1.0"
+inline_request=$(python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)))' \
+ <"${repo_root}/examples/core-normalize-participants/runtime-requests/uc01-mixed-match.json")
+
+printf '%s\n' \
+ '{"command":"describe_server"}' \
+ '{"command":"list_entrypoints"}' \
+ "{\"command\":\"execute_entrypoint\",\"entrypoint_kind\":\"capability\",\"id\":\"${kit_id}\",\"version\":\"${kit_version}\",\"request\":${inline_request}}" \
+ '{"command":"shutdown"}' \
+ | cargo run -p traverse-mcp -- stdio --cache "${cache_dir}" \
+ >"${stdout_log}" 2>"${stderr_log}"
+
+grep -q '"mode":"verified_public"' "${stdout_log}"
+grep -q '"kind":"host_verified_public_registry"' "${stdout_log}"
+grep -q "\"id\":\"${kit_id}\"" "${stdout_log}"
+grep -q '"kind":"mcp_stdio_server_entrypoint_execution"' "${stdout_log}"
+grep -q '"status":"completed"' "${stdout_log}"
+grep -q '"digest_matches_public_state":true' "${stdout_log}"
+
+if grep -q 'expedition' "${stdout_log}"; then
+ echo "Mode B output must not reference the expedition catalog." >&2
+ exit 1
+fi
+
+set +e
+printf '%s\n' '{"command":"describe_server"}' \
+ | cargo run -p traverse-mcp -- stdio --cache "${absent_cache_dir}" \
+ >"${absent_stdout_log}" 2>"${absent_stderr_log}"
+absent_status=$?
+set -e
+
+if [[ ${absent_status} -eq 0 ]]; then
+ echo "Expected Mode B to fail closed without prepared verified state." >&2
+ exit 1
+fi
+grep -q '"code":"registry_sync_missing"' "${absent_stderr_log}"
+test ! -s "${absent_stdout_log}"
+
+echo "MCP stdio server Mode B smoke passed."
diff --git a/scripts/ci/repository_checks.sh b/scripts/ci/repository_checks.sh
index 023640d5..d9f0f9c1 100644
--- a/scripts/ci/repository_checks.sh
+++ b/scripts/ci/repository_checks.sh
@@ -31,6 +31,7 @@ required_files=(
"docs/mcp-consumption-validation.md"
"docs/mcp-stdio-server.md"
"docs/mcp-mode-a-release-evidence.md"
+ "docs/mcp-mode-b-release-evidence.md"
"docs/youaskm3-canonical-mcp-client-path.md"
"docs/mcp-real-agent-exercise.md"
"docs/app-consumable-release-checklist.md"
@@ -153,6 +154,7 @@ required_files=(
"scripts/ci/mcp_stdio_server_discovery_smoke.sh"
"scripts/ci/mcp_stdio_server_execution_report_smoke.sh"
"scripts/ci/mcp_stdio_server_mode_a_smoke.sh"
+ "scripts/ci/mcp_stdio_server_mode_b_smoke.sh"
"scripts/ci/mcp_real_agent_exercise_smoke.sh"
"scripts/ci/project_board_audit.sh"
"scripts/scaffold/hello_world_agent_scaffold.sh"
@@ -513,6 +515,12 @@ grep -q "119-verified-registry-mcp-mode-a" docs/mcp-stdio-server.md
grep -q "docs/mcp-mode-a-release-evidence.md" docs/mcp-stdio-server.md
grep -q "bash scripts/ci/mcp_stdio_server_mode_a_smoke.sh" docs/mcp-mode-a-release-evidence.md
grep -q "docs/mcp-stdio-server.md" docs/mcp-mode-a-release-evidence.md
+grep -q "bash scripts/ci/mcp_stdio_server_mode_b_smoke.sh" docs/mcp-stdio-server.md
+grep -q "prepare-cache" docs/mcp-stdio-server.md
+grep -q "docs/mcp-mode-b-release-evidence.md" docs/mcp-stdio-server.md
+grep -q "bash scripts/ci/mcp_stdio_server_mode_b_smoke.sh" docs/mcp-mode-b-release-evidence.md
+grep -q "docs/mcp-stdio-server.md" docs/mcp-mode-b-release-evidence.md
+grep -q "traverse-mcp prepare-cache" docs/mcp-mode-b-release-evidence.md
grep -q "render_execution_report" docs/mcp-stdio-server.md
grep -q "list_entrypoints" docs/mcp-stdio-server.md
grep -q "describe_entrypoint" docs/mcp-stdio-server.md