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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added
- Hidden `cortex mcp-server --verify` stdio JSON-RPC server (`cortex-verify`) so CI and agents can audit TUI chrome, lock scenes, login product copy, and API error paths offline. Remains `hide = true` until Designer sign-off.

### Changed
- Code turns default to the **Cloud** runtime for the TUI and `cortex exec` (Designer Q9 / `CLI_100_CHROME_LOCK_SIGNED`). This PC and SSH are explicit opt-in in 0.1.x (`CORTEX_COMPUTER` or `CORTEX_SSH_HOST`) and refuse a fresh session with product copy instead of blocking every first turn.
- README `docs/media/intro.gif` sits on a photographed green forest desktop (not teal blobs): Terminal chrome, a pointer that walks titlebar → composer → slash / model → Shell, and the signed lock TUI. Local CLI only — no Cortex Cloud handoff in the banner story.
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions docs/guides/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,26 @@ failure to make CI green. See [testing rules](../../.rules/testing.md).
The append regression test checks immediate visibility after Tokio 1.53.1 file
writes. An awaited `flush` finishes the pending write; it is not an `fsync`
durability guarantee. Do not replace this check with sleeps or retries.

## Verification MCP (hidden)

`cortex mcp-server --verify` is a hidden stdio JSON-RPC server (`hide = true`
until Designer sign-off). It is built on the in-tree `cortex-mcp-server` crate
as `cortex-verify` and drives the TUI through the same headless
`EventLoop` + `MockTerminal` path as `ux_contract_tests.rs`.

CI and agents add one MCP server entry:

```bash
./target/debug/Cortex mcp-server --verify
```

Tools: `tui.*`, `lock.*`, `login.run`, `api.*`, `mcp.*`, `report.finish`.
Resources: `cortex-verify://matrix`, `cortex-verify://lock/v2/<size>/<id>.txt`,
`cortex-verify://report/latest`. `report.finish` writes
`target/readiness/cli-verify/<run_id>.json` with schema `cortex-verify/1`.

Offline runs use `CORTEX_API_URL` (loopback fixture or an unreachable origin).
They must still cover chrome, legend, product-facing errors, and palette
audit. Live API checks are gated on `CORTEX_LIVE_API=1` and are not part of
default CI. Integration coverage is `src/cortex-cli/tests/mcp_server_verify.rs`.
7 changes: 7 additions & 0 deletions docs/reference/cli.commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -2744,6 +2744,13 @@
{
"about": "Run the MCP server (stdio transport)",
"arguments": [
{
"help": "Run the Cortex verification MCP over stdio JSON-RPC (`cortex-verify/1`)",
"id": "verify",
"long": "verify",
"required": false,
"short": null
},
{
"help": "Enable verbose output (same as --log-level debug)",
"id": "verbose",
Expand Down
4 changes: 3 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ help but supported.

Also hidden, and supported: `shell` (aliases `interactive`, `repl`), `dag`
(alias `tasks`), `servers`, `history`, `workspace` (alias `project`), `sandbox`
(alias `sb`), `serve`, and `mcp-server`.
(alias `sb`), `serve`, and `mcp-server`. The hidden `cortex mcp-server --verify`
flag starts the offline TUI+API verification MCP (`cortex-verify/1`); see
[Development](../guides/development.md#verification-mcp-hidden).

## See also

Expand Down
18 changes: 17 additions & 1 deletion src/cortex-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,16 @@ workspace = true
[features]
default = ["cortex-tui", "audio"]
# Use the new Cortex TUI (120 FPS, Cortex theme)
cortex-tui = ["dep:cortex-tui"]
cortex-tui = [
"dep:cortex-tui",
"dep:cortex-tui-capture",
"dep:cortex-mcp-server",
"dep:cortex-mcp-types",
"dep:sha2",
"dep:hex",
"dep:ratatui",
"dep:async-trait",
]
# Audio notifications - disabled on musl targets due to alsa-sys incompatibility
# Falls back to terminal bell when disabled
audio = ["cortex-tui?/audio"]
Expand All @@ -33,6 +42,13 @@ audio = ["cortex-tui?/audio"]
cortex-engine = { workspace = true }
cortex-protocol = { workspace = true }
cortex-tui = { workspace = true, optional = true }
cortex-tui-capture = { workspace = true, optional = true }
cortex-mcp-server = { workspace = true, optional = true }
cortex-mcp-types = { workspace = true, optional = true }
sha2 = { workspace = true, optional = true }
hex = { workspace = true, optional = true }
ratatui = { workspace = true, optional = true }
async-trait = { workspace = true, optional = true }

cortex-common = { workspace = true, features = ["cli"] }
cortex-commands = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion src/cortex-cli/src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ pub enum Commands {
/// Run the MCP server (stdio transport)
#[command(display_order = 32, hide = true)]
#[command(next_help_heading = categories::EXTENSION)]
McpServer,
McpServer(super::mcp_server::McpServerCli),

/// Start ACP server for IDE integration (e.g., Zed)
#[command(display_order = 33)]
Expand Down
6 changes: 1 addition & 5 deletions src/cortex-cli/src/cli/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@ pub async fn dispatch_command(cli: Cli) -> Result<()> {
}
Some(Commands::Mcp(mcp_cli)) => mcp_cli.run().await,
Some(Commands::Agent(agent_cli)) => agent_cli.run().await,
Some(Commands::McpServer) => {
bail!(
"MCP server mode is not yet implemented. Use 'cortex mcp' for MCP server management."
);
}
Some(Commands::McpServer(args)) => super::mcp_server::run(args).await,
Some(Commands::Completion(completion_cli)) => handle_completion(completion_cli),
Some(Commands::Sandbox(sandbox_args)) => handle_sandbox(sandbox_args).await,
Some(Commands::Resume(resume_cli)) => run_resume(resume_cli).await,
Expand Down
69 changes: 69 additions & 0 deletions src/cortex-cli/src/cli/mcp_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Hidden `cortex mcp-server` flags and dispatch.
//!
//! Kept out of [`super::args`] / [`super::handlers`] so those modules stay at
//! their source-policy line-count baseline (same split as `lock_palette`).

use anyhow::{Result, bail};
use clap::Parser;

/// Hidden `cortex mcp-server` flags. `--verify` is the offline TUI+API verifier.
#[derive(Debug, Parser)]
pub struct McpServerCli {
/// Run the Cortex verification MCP over stdio JSON-RPC (`cortex-verify/1`).
#[arg(long)]
pub verify: bool,
}

/// Run `cortex mcp-server`, including the hidden `--verify` verifier.
pub async fn run(args: McpServerCli) -> Result<()> {
if args.verify {
#[cfg(feature = "cortex-tui")]
{
return crate::verify_mcp::run().await;
}
#[cfg(not(feature = "cortex-tui"))]
{
bail!("Verification MCP requires the cortex-tui feature.");
}
}
bail!("MCP server mode is not yet implemented. Use 'cortex mcp' for MCP server management.");
}

#[cfg(test)]
mod tests {
use super::*;
use crate::cli::args::{Cli, Commands};
use clap::CommandFactory;

#[test]
fn test_mcp_server_verify_stays_hidden() {
let command = Cli::command();
let mcp = command
.find_subcommand("mcp-server")
.expect("mcp-server must exist");
assert!(mcp.is_hide_set(), "keep hide=true until Designer sign-off");
let cli = Cli::try_parse_from(["cortex", "mcp-server", "--verify"])
.expect("should parse hidden mcp-server --verify");
match cli.command {
Some(Commands::McpServer(args)) => assert!(args.verify),
_ => panic!("expected McpServer --verify"),
}
}

#[tokio::test]
async fn mcp_server_without_verify_fails_closed() {
let err = run(McpServerCli { verify: false })
.await
.expect_err("default mcp-server is not implemented");
assert!(err.to_string().contains("not yet implemented"));
}

#[tokio::test]
async fn dispatch_mcp_server_without_verify_fails_closed() {
let cli = Cli::try_parse_from(["cortex", "mcp-server"]).expect("parse mcp-server");
let err = crate::cli::handlers::dispatch_command(cli)
.await
.expect_err("dispatch must fail closed");
assert!(err.to_string().contains("not yet implemented"));
}
}
1 change: 1 addition & 0 deletions src/cortex-cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

pub mod args;
pub mod handlers;
pub mod mcp_server;
pub mod styles;

// Re-export main types
Expand Down
2 changes: 2 additions & 0 deletions src/cortex-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ pub mod stats_cmd;
pub mod styled_output;
pub mod uninstall_cmd;
pub mod upgrade_cmd;
#[cfg(feature = "cortex-tui")]
pub mod verify_mcp;
pub mod workspace_cmd;

#[cfg(not(windows))]
Expand Down
2 changes: 1 addition & 1 deletion src/cortex-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ async fn main() -> Result<()> {
let skip_auto_update = cli.interactive.debug
|| matches!(
&cli.command,
Some(Commands::Upgrade(_) | Commands::Serve(_))
Some(Commands::Upgrade(_) | Commands::Serve(_) | Commands::McpServer(_))
);
let is_tui_mode = cli.command.is_none();
if !skip_auto_update && !is_tui_mode && !is_debug_cmd {
Expand Down
76 changes: 76 additions & 0 deletions src/cortex-cli/src/mcp_cmd/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,29 @@ pub(crate) async fn run_tools(args: ToolsArgs) -> Result<()> {
Ok(())
}

/// Probe a configured MCP server and return the JSON report used by `mcp debug --json`.
pub(crate) async fn probe_named(name: &str, timeout: u64) -> Result<serde_json::Value> {
validate_server_name(name)?;
let server = get_mcp_server(name)?.ok_or_else(|| anyhow!("MCP server is not configured"))?;
match probe(name, &server, timeout).await {
Ok(info) => Ok(json!({
"name": name,
"connection": {"success": true},
"capabilities": info["capabilities"],
"tools": info["tools"],
"resources": info["resources"],
"prompts": info["prompts"],
"cached": false
})),
Err(error) => Ok(json!({
"name": name,
"connection": {"success": false},
"error": error.to_string(),
"cached": false
})),
}
}

async fn probe(name: &str, value: &toml::Value, timeout: u64) -> Result<serde_json::Value> {
if timeout == 0 {
bail!("MCP timeout must be positive");
Expand All @@ -79,6 +102,38 @@ async fn probe(name: &str, value: &toml::Value, timeout: u64) -> Result<serde_js
}
}

pub(crate) async fn call_named(
name: &str,
tool: &str,
arguments: serde_json::Value,
timeout: u64,
) -> Result<serde_json::Value> {
validate_server_name(name)?;
let server = get_mcp_server(name)?.ok_or_else(|| anyhow!("MCP server is not configured"))?;
if timeout == 0 {
bail!("MCP timeout must be positive");
}
let started = std::time::Instant::now();
let client =
McpClient::with_timeout(runtime_config(name, &server)?, Duration::from_secs(timeout));
let result = async {
client.connect().await?;
client.call_tool(tool, Some(arguments)).await
}
.await;
let closed = client.disconnect().await;
match result {
Ok(call) => {
closed?;
Ok(json!({
"result": call,
"duration_ms": started.elapsed().as_millis(),
}))
}
Err(error) => Err(error),
}
}

fn runtime_config(name: &str, server: &toml::Value) -> Result<McpServerConfig> {
if server.get("enabled").and_then(toml::Value::as_bool) == Some(false) {
bail!("MCP server is disabled");
Expand Down Expand Up @@ -175,4 +230,25 @@ mod tests {
.unwrap();
assert!(probe("fixture", &value, 1).await.is_err());
}

#[tokio::test]
async fn probe_named_rejects_empty_and_missing_servers() {
assert!(probe_named("", 1).await.is_err());
assert!(probe_named("missing-verify-peer", 1).await.is_err());
}

#[tokio::test]
async fn call_named_rejects_empty_missing_and_zero_timeout() {
assert!(call_named("", "tool", json!({}), 1).await.is_err());
assert!(
call_named("missing-verify-peer", "tool", json!({}), 1)
.await
.is_err()
);
assert!(
call_named("missing-verify-peer", "tool", json!({}), 0)
.await
.is_err()
);
}
}
2 changes: 1 addition & 1 deletion src/cortex-cli/src/mcp_cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

mod auth;
mod config;
mod debug;
pub(crate) mod debug;
mod handlers;
mod macros;
mod types;
Expand Down
Loading
Loading