-
Notifications
You must be signed in to change notification settings - Fork 52
Add Grok CLI connect support #1015
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -557,8 +557,8 @@ pub fn ensure_opencode_config_with_result( | |||||||||||||||||||||||||||||
| /// Configure the relaycast MCP server for any supported CLI tool. | ||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||
| /// Returns extra CLI arguments to append when spawning the agent. | ||||||||||||||||||||||||||||||
| /// For Gemini/Droid this runs a pre-spawn `mcp add` command (removing first | ||||||||||||||||||||||||||||||
| /// for idempotency). For Opencode this writes `opencode.json` on disk. | ||||||||||||||||||||||||||||||
| /// For Gemini/Droid/Grok this runs a pre-spawn `mcp add` command (removing | ||||||||||||||||||||||||||||||
| /// first for idempotency). For Opencode this writes `opencode.json` on disk. | ||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||
| /// # Parameters | ||||||||||||||||||||||||||||||
| /// Write `.cursor/mcp.json` in the given directory with the Agent Relay MCP server | ||||||||||||||||||||||||||||||
|
|
@@ -646,7 +646,7 @@ pub fn ensure_cursor_mcp_config( | |||||||||||||||||||||||||||||
| Ok(changed) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /// - `cli`: CLI tool name (e.g. "claude", "codex", "gemini", "droid", "opencode", "cursor") | ||||||||||||||||||||||||||||||
| /// - `cli`: CLI tool name (e.g. "claude", "codex", "gemini", "droid", "grok", "opencode", "cursor") | ||||||||||||||||||||||||||||||
| /// - `agent_name`: the name of the agent being spawned | ||||||||||||||||||||||||||||||
| /// - `api_key`: optional relay API key (empty or `None` means omit) | ||||||||||||||||||||||||||||||
| /// - `base_url`: optional relay base URL (empty or `None` means omit) | ||||||||||||||||||||||||||||||
|
|
@@ -723,6 +723,7 @@ pub async fn configure_relaycast_mcp_with_result( | |||||||||||||||||||||||||||||
| let is_codex = cli_lower == "codex"; | ||||||||||||||||||||||||||||||
| let is_gemini = cli_lower == "gemini"; | ||||||||||||||||||||||||||||||
| let is_droid = cli_lower == "droid"; | ||||||||||||||||||||||||||||||
| let is_grok = cli_lower == "grok"; | ||||||||||||||||||||||||||||||
| let is_opencode = cli_lower == "opencode"; | ||||||||||||||||||||||||||||||
| let is_cursor = cli_lower == "cursor" || cli_lower == "cursor-agent" || cli_lower == "agent"; // "agent" is cursor-agent's binary name | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -883,6 +884,17 @@ pub async fn configure_relaycast_mcp_with_result( | |||||||||||||||||||||||||||||
| None, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| .await?; | ||||||||||||||||||||||||||||||
| } else if is_grok { | ||||||||||||||||||||||||||||||
| configure_grok_mcp( | ||||||||||||||||||||||||||||||
| cli, | ||||||||||||||||||||||||||||||
| api_key, | ||||||||||||||||||||||||||||||
| base_url, | ||||||||||||||||||||||||||||||
| Some(agent_name), | ||||||||||||||||||||||||||||||
| agent_token, | ||||||||||||||||||||||||||||||
| workspaces_json, | ||||||||||||||||||||||||||||||
| default_workspace, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| .await?; | ||||||||||||||||||||||||||||||
| } else if is_opencode && !existing_args.iter().any(|a| a == "--agent") { | ||||||||||||||||||||||||||||||
| ensure_opencode_config_with_result( | ||||||||||||||||||||||||||||||
| cwd, | ||||||||||||||||||||||||||||||
|
|
@@ -1148,6 +1160,127 @@ async fn configure_gemini_droid_mcp( | |||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fn grok_manual_mcp_add_cmd(cli: &str) -> String { | ||||||||||||||||||||||||||||||
| format!( | ||||||||||||||||||||||||||||||
| "{cli} mcp add relaycast --command npx --args=-y --args=agent-relay --args=mcp --env RELAY_API_KEY=<key> --env RELAY_BASE_URL=<url>" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fn grok_mcp_add_args( | ||||||||||||||||||||||||||||||
| api_key: Option<&str>, | ||||||||||||||||||||||||||||||
| base_url: Option<&str>, | ||||||||||||||||||||||||||||||
| agent_name: Option<&str>, | ||||||||||||||||||||||||||||||
| agent_token: Option<&str>, | ||||||||||||||||||||||||||||||
| workspaces_json: Option<&str>, | ||||||||||||||||||||||||||||||
| default_workspace: Option<&str>, | ||||||||||||||||||||||||||||||
| ) -> Vec<String> { | ||||||||||||||||||||||||||||||
| let mut args = vec![ | ||||||||||||||||||||||||||||||
| "mcp".to_string(), | ||||||||||||||||||||||||||||||
| "add".to_string(), | ||||||||||||||||||||||||||||||
| "relaycast".to_string(), | ||||||||||||||||||||||||||||||
| "--command".to_string(), | ||||||||||||||||||||||||||||||
| "npx".to_string(), | ||||||||||||||||||||||||||||||
| "--args=-y".to_string(), | ||||||||||||||||||||||||||||||
| format!("--args={AGENT_RELAY_MCP_PACKAGE}"), | ||||||||||||||||||||||||||||||
| format!("--args={AGENT_RELAY_MCP_SUBCOMMAND}"), | ||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let mut push_env = |key: &str, value: Option<&str>| { | ||||||||||||||||||||||||||||||
| if let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) { | ||||||||||||||||||||||||||||||
| args.push("--env".to_string()); | ||||||||||||||||||||||||||||||
| args.push(format!("{key}={value}")); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| push_env("RELAY_API_KEY", api_key); | ||||||||||||||||||||||||||||||
| push_env("RELAY_BASE_URL", base_url); | ||||||||||||||||||||||||||||||
| push_env("RELAY_AGENT_NAME", agent_name); | ||||||||||||||||||||||||||||||
| push_env("RELAY_AGENT_TYPE", Some("agent")); | ||||||||||||||||||||||||||||||
| push_env("RELAY_STRICT_AGENT_NAME", Some("1")); | ||||||||||||||||||||||||||||||
| if agent_token | ||||||||||||||||||||||||||||||
| .map(str::trim) | ||||||||||||||||||||||||||||||
| .is_some_and(|value| !value.is_empty()) | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| push_env("RELAY_AGENT_TOKEN", agent_token); | ||||||||||||||||||||||||||||||
| push_env("RELAY_SKIP_BOOTSTRAP", Some("1")); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| push_env("RELAY_WORKSPACES_JSON", workspaces_json); | ||||||||||||||||||||||||||||||
| push_env("RELAY_DEFAULT_WORKSPACE", default_workspace); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| args | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| async fn configure_grok_mcp( | ||||||||||||||||||||||||||||||
| cli: &str, | ||||||||||||||||||||||||||||||
| api_key: Option<&str>, | ||||||||||||||||||||||||||||||
| base_url: Option<&str>, | ||||||||||||||||||||||||||||||
| agent_name: Option<&str>, | ||||||||||||||||||||||||||||||
| agent_token: Option<&str>, | ||||||||||||||||||||||||||||||
| workspaces_json: Option<&str>, | ||||||||||||||||||||||||||||||
| default_workspace: Option<&str>, | ||||||||||||||||||||||||||||||
| ) -> Result<()> { | ||||||||||||||||||||||||||||||
| let exe = shlex::split(cli) | ||||||||||||||||||||||||||||||
| .and_then(|parts| parts.first().cloned()) | ||||||||||||||||||||||||||||||
| .unwrap_or_else(|| cli.trim().to_string()); | ||||||||||||||||||||||||||||||
| let manual_cmd = grok_manual_mcp_add_cmd(&exe); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let _ = std::process::Command::new(&exe) | ||||||||||||||||||||||||||||||
| .args(["mcp", "remove", "relaycast"]) | ||||||||||||||||||||||||||||||
| .stdin(Stdio::null()) | ||||||||||||||||||||||||||||||
| .stdout(Stdio::null()) | ||||||||||||||||||||||||||||||
| .stderr(Stdio::null()) | ||||||||||||||||||||||||||||||
| .spawn() | ||||||||||||||||||||||||||||||
| .and_then(|mut child| child.wait()); | ||||||||||||||||||||||||||||||
|
Comment on lines
+1227
to
+1233
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In an asynchronous context, spawning a synchronous process and blocking the thread with
Suggested change
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let mut mcp_cmd = Command::new(&exe); | ||||||||||||||||||||||||||||||
| mcp_cmd.args(grok_mcp_add_args( | ||||||||||||||||||||||||||||||
| api_key, | ||||||||||||||||||||||||||||||
| base_url, | ||||||||||||||||||||||||||||||
| agent_name, | ||||||||||||||||||||||||||||||
| agent_token, | ||||||||||||||||||||||||||||||
| workspaces_json, | ||||||||||||||||||||||||||||||
| default_workspace, | ||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||
| mcp_cmd | ||||||||||||||||||||||||||||||
| .stdin(Stdio::null()) | ||||||||||||||||||||||||||||||
| .stdout(Stdio::null()) | ||||||||||||||||||||||||||||||
| .stderr(Stdio::piped()); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| match mcp_cmd.spawn() { | ||||||||||||||||||||||||||||||
| Ok(mut child) => match tokio::time::timeout(Duration::from_secs(15), child.wait()).await { | ||||||||||||||||||||||||||||||
| Ok(Ok(status)) if !status.success() => { | ||||||||||||||||||||||||||||||
| anyhow::bail!( | ||||||||||||||||||||||||||||||
| "failed to configure relaycast MCP for {cli}: `{cli} mcp add` exited with code {:?}. \ | ||||||||||||||||||||||||||||||
| Please configure the relaycast MCP server manually:\n {manual_cmd}", | ||||||||||||||||||||||||||||||
| status.code() | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Ok(Err(error)) => { | ||||||||||||||||||||||||||||||
| anyhow::bail!( | ||||||||||||||||||||||||||||||
| "failed to configure relaycast MCP for {cli}: {error}. \ | ||||||||||||||||||||||||||||||
| Please configure the relaycast MCP server manually:\n {manual_cmd}" | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Err(_) => { | ||||||||||||||||||||||||||||||
| let _ = child.kill().await; | ||||||||||||||||||||||||||||||
| anyhow::bail!( | ||||||||||||||||||||||||||||||
| "failed to configure relaycast MCP for {cli}: `{cli} mcp add` timed out after 15s. \ | ||||||||||||||||||||||||||||||
| Please configure the relaycast MCP server manually:\n {manual_cmd}" | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| _ => {} | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| Err(error) => { | ||||||||||||||||||||||||||||||
| anyhow::bail!( | ||||||||||||||||||||||||||||||
| "failed to configure relaycast MCP for {cli}: could not run `{cli} mcp add`: {error}. \ | ||||||||||||||||||||||||||||||
| Please configure the relaycast MCP server manually:\n {manual_cmd}" | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+1235
to
+1279
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Piping To prevent this and provide better error reporting, we can use let mut mcp_cmd = Command::new(&exe);
mcp_cmd.kill_on_drop(true);
mcp_cmd.args(grok_mcp_add_args(
api_key,
base_url,
agent_name,
agent_token,
workspaces_json,
default_workspace,
));
mcp_cmd
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped());
match mcp_cmd.spawn() {
Ok(child) => match tokio::time::timeout(Duration::from_secs(15), child.wait_with_output()).await {
Ok(Ok(output)) if !output.status.success() => {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"failed to configure relaycast MCP for {cli}: `{cli} mcp add` exited with code {:?}.\nStderr: {stderr}\n\
Please configure the relaycast MCP server manually:\n {manual_cmd}",
output.status.code()
);
}
Ok(Err(error)) => {
anyhow::bail!(
"failed to configure relaycast MCP for {cli}: {error}. \
Please configure the relaycast MCP server manually:\n {manual_cmd}"
);
}
Err(_) => {
anyhow::bail!(
"failed to configure relaycast MCP for {cli}: `{cli} mcp add` timed out after 15s. \
Please configure the relaycast MCP server manually:\n {manual_cmd}"
);
}
_ => {}
},
Err(error) => {
anyhow::bail!(
"failed to configure relaycast MCP for {cli}: could not run `{cli} mcp add`: {error}. \
Please configure the relaycast MCP server manually:\n {manual_cmd}"
);
}
} |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fn write_pretty_json(path: &Path, value: &Value) -> io::Result<()> { | ||||||||||||||||||||||||||||||
| let mut body = serde_json::to_string_pretty(value).map_err(|error| { | ||||||||||||||||||||||||||||||
| io::Error::other(format!("failed to serialize {}: {error}", path.display())) | ||||||||||||||||||||||||||||||
|
|
@@ -1585,6 +1718,37 @@ mod tests { | |||||||||||||||||||||||||||||
| assert!(args.contains(&"RELAY_AGENT_TOKEN=tok_droid_123".to_string())); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||
| fn grok_mcp_add_args_use_equals_form_for_dash_args() { | ||||||||||||||||||||||||||||||
| let args = super::grok_mcp_add_args( | ||||||||||||||||||||||||||||||
| Some("rk_live_xyz"), | ||||||||||||||||||||||||||||||
| Some("https://api.relaycast.dev"), | ||||||||||||||||||||||||||||||
| Some("GrokWorker"), | ||||||||||||||||||||||||||||||
| Some("tok_grok_123"), | ||||||||||||||||||||||||||||||
| Some(r#"{"workspaces":["rw_1"]}"#), | ||||||||||||||||||||||||||||||
| Some("rw_1"), | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| assert_eq!(args[0], "mcp"); | ||||||||||||||||||||||||||||||
| assert_eq!(args[1], "add"); | ||||||||||||||||||||||||||||||
| assert_eq!(args[2], "relaycast"); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"--command".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"npx".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"--args=-y".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"--args=agent-relay".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"--args=mcp".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"--env".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"RELAY_API_KEY=rk_live_xyz".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"RELAY_BASE_URL=https://api.relaycast.dev".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"RELAY_AGENT_NAME=GrokWorker".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"RELAY_AGENT_TYPE=agent".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"RELAY_STRICT_AGENT_NAME=1".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"RELAY_AGENT_TOKEN=tok_grok_123".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"RELAY_SKIP_BOOTSTRAP=1".to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&r#"RELAY_WORKSPACES_JSON={"workspaces":["rw_1"]}"#.to_string())); | ||||||||||||||||||||||||||||||
| assert!(args.contains(&"RELAY_DEFAULT_WORKSPACE=rw_1".to_string())); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||
| fn gemini_droid_mcp_add_args_omit_agent_result_env() { | ||||||||||||||||||||||||||||||
| let config = test_agent_result_config(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
configure_grok_mcpduplicates the subprocess orchestration/error-handling logic already implemented inconfigure_gemini_droid_mcp, which increases maintenance drift risk. Consider extracting a shared helper for "remove existing MCP + run mcp add with timeout + standardized error mapping".Prompt for AI agents