diff --git a/crates/cli/src/subcommands/db_arg_resolution.rs b/crates/cli/src/subcommands/db_arg_resolution.rs
index 7661d2258ab..2d5ccc3e137 100644
--- a/crates/cli/src/subcommands/db_arg_resolution.rs
+++ b/crates/cli/src/subcommands/db_arg_resolution.rs
@@ -13,6 +13,10 @@
// | subscribe | resolve_optional_database_parts | No (variable args) | No |
// | describe | resolve_database_with_optional_parts | No (optional args)| No |
//
+// Commands that address a server rather than a database (`list`, `rename`, `mcp`) have no
+// database argument to resolve, so they take the project's server from the config directly
+// via `resolve_config_server`.
+//
// "Auto-fallthrough" means: if the provided database name doesn't match any config target,
// treat it as an ad-hoc database outside the project (equivalent to --no-config for that arg).
//
@@ -21,7 +25,7 @@
// so 2+ positional args means the first must be a database. For `call`/`subscribe`/`describe`,
// the first positional could be a non-database argument, so we must error to avoid misinterpreting it.
-use crate::spacetime_config::find_and_load_with_env;
+use crate::spacetime_config::{find_and_load_with_env, SpacetimeConfig};
use itertools::Itertools;
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -81,6 +85,31 @@ pub(crate) fn load_config_db_targets(no_config: bool) -> anyhow::Result anyhow::Result > {
+ if no_config {
+ return Ok(None);
+ }
+ Ok(find_and_load_with_env(None)?.and_then(|loaded| single_config_server(&loaded.config)))
+}
+
+/// The one server a config points at, or `None` if it names zero or several.
+fn single_config_server(config: &SpacetimeConfig) -> Option {
+ config
+ .collect_all_targets_with_inheritance()
+ .iter()
+ .filter_map(|target| target.fields.get("server").and_then(|v| v.as_str()))
+ .unique()
+ .exactly_one()
+ .ok()
+ .map(str::to_string)
+}
+
pub(crate) fn resolve_optional_database_parts(
raw_parts: &[String],
config_targets: Option<&[ConfigDbTarget]>,
@@ -262,8 +291,44 @@ pub(crate) fn resolve_database_with_optional_parts(
#[cfg(test)]
mod tests {
use super::{
- resolve_database_arg, resolve_database_with_optional_parts, resolve_optional_database_parts, ConfigDbTarget,
+ resolve_database_arg, resolve_database_with_optional_parts, resolve_optional_database_parts,
+ single_config_server, ConfigDbTarget,
};
+ use crate::spacetime_config::SpacetimeConfig;
+
+ fn parse_config(json: &str) -> SpacetimeConfig {
+ serde_json::from_str(json).unwrap()
+ }
+
+ #[test]
+ fn root_server_is_used_when_config_has_no_database() {
+ let config = parse_config(r#"{ "server": "local", "module-path": "./spacetimedb" }"#);
+ assert_eq!(single_config_server(&config).as_deref(), Some("local"));
+ }
+
+ #[test]
+ fn children_inherit_the_root_server() {
+ let config =
+ parse_config(r#"{ "server": "local", "children": [{ "database": "foo" }, { "database": "bar" }] }"#);
+ assert_eq!(single_config_server(&config).as_deref(), Some("local"));
+ }
+
+ #[test]
+ fn no_server_when_config_omits_it() {
+ let config = parse_config(r#"{ "database": "foo" }"#);
+ assert_eq!(single_config_server(&config), None);
+ }
+
+ #[test]
+ fn no_server_when_children_disagree() {
+ let config = parse_config(
+ r#"{ "children": [
+ { "database": "foo", "server": "local" },
+ { "database": "bar", "server": "maincloud" }
+ ] }"#,
+ );
+ assert_eq!(single_config_server(&config), None);
+ }
#[test]
fn single_db_infers_database() {
diff --git a/crates/cli/src/subcommands/dns.rs b/crates/cli/src/subcommands/dns.rs
index 59e049b038b..1a7a4e67ad9 100644
--- a/crates/cli/src/subcommands/dns.rs
+++ b/crates/cli/src/subcommands/dns.rs
@@ -1,5 +1,6 @@
use crate::common_args;
use crate::config::Config;
+use crate::subcommands::db_arg_resolution::resolve_config_server;
use crate::util::{add_auth_header_opt, get_auth_header, ResponseExt};
use clap::ArgMatches;
use clap::{Arg, Command};
@@ -22,14 +23,28 @@ pub fn cli() -> Command {
)
.arg(common_args::server().help("The nickname, host name or URL of the server on which to set the name"))
.arg(common_args::yes())
+ .arg(
+ Arg::new("no_config")
+ .long("no-config")
+ .action(clap::ArgAction::SetTrue)
+ .help("Ignore spacetime.json configuration"),
+ )
.after_help("Run `spacetime rename --help` for more detailed information.\n")
}
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let domain = args.get_one::("new-name").unwrap();
let database_identity = args.get_one::("database-identity").unwrap();
- let server = args.get_one::("server").map(|s| s.as_ref());
let force = args.get_flag("force");
+ let no_config = args.get_flag("no_config");
+
+ // `rename` addresses the database by identity, so there is no database name to
+ // resolve against the config -- take the project's server directly.
+ let config_server = resolve_config_server(no_config)?;
+ let server = args
+ .get_one::("server")
+ .map(|s| s.as_str())
+ .or(config_server.as_deref());
let auth_header = get_auth_header(&mut config, false, server, !force).await?;
let domain: DomainName = domain.parse()?;
diff --git a/crates/cli/src/subcommands/list.rs b/crates/cli/src/subcommands/list.rs
index 889b6a93346..99e896e5739 100644
--- a/crates/cli/src/subcommands/list.rs
+++ b/crates/cli/src/subcommands/list.rs
@@ -1,11 +1,12 @@
use crate::common_args;
+use crate::subcommands::db_arg_resolution::resolve_config_server;
use crate::util;
use crate::util::get_login_token_or_log_in;
use crate::util::ResponseExt;
use crate::util::UNSTABLE_WARNING;
use crate::Config;
use anyhow::Context;
-use clap::{ArgMatches, Command};
+use clap::{Arg, ArgMatches, Command};
use futures::future::join_all;
use serde::Deserialize;
use spacetimedb_lib::Identity;
@@ -21,6 +22,12 @@ pub fn cli() -> Command {
))
.arg(common_args::server().help("The nickname, host name or URL of the server from which to list databases"))
.arg(common_args::yes())
+ .arg(
+ Arg::new("no_config")
+ .long("no-config")
+ .action(clap::ArgAction::SetTrue)
+ .help("Ignore spacetime.json configuration"),
+ )
}
#[derive(Deserialize)]
@@ -39,8 +46,17 @@ struct DatabaseRow {
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
eprintln!("{UNSTABLE_WARNING}\n");
- let server = args.get_one::("server").map(|s| s.as_ref());
let force = args.get_flag("force");
+ let no_config = args.get_flag("no_config");
+
+ // `list` has no database argument, so unlike `call`/`logs`/`sql` it picks up the
+ // project's server directly from spacetime.json rather than from a database target.
+ let config_server = resolve_config_server(no_config)?;
+ let server = args
+ .get_one::("server")
+ .map(|s| s.as_str())
+ .or(config_server.as_deref());
+
let token = get_login_token_or_log_in(&mut config, server, !force).await?;
let identity = util::decode_identity(&token)?;
diff --git a/crates/cli/src/subcommands/mcp.rs b/crates/cli/src/subcommands/mcp.rs
index 7d679c121c7..b66658479ec 100644
--- a/crates/cli/src/subcommands/mcp.rs
+++ b/crates/cli/src/subcommands/mcp.rs
@@ -1,6 +1,7 @@
use crate::api::{build_client, Connection};
use crate::common_args;
use crate::config::Config;
+use crate::subcommands::db_arg_resolution::resolve_config_server;
use crate::util::{auth_header_from_saved_token, database_identity, ResponseExt, UNSTABLE_WARNING};
use anyhow::Context;
use clap::{Arg, ArgMatches};
@@ -17,6 +18,12 @@ pub fn cli() -> clap::Command {
))
.arg(common_args::server().help("The nickname, host name or URL of the server hosting the database"))
.arg(common_args::anonymous())
+ .arg(
+ Arg::new("no_config")
+ .long("no-config")
+ .action(clap::ArgAction::SetTrue)
+ .help("Ignore spacetime.json configuration"),
+ )
.after_help("Run `spacetime help mcp` for more detailed information.\n")
}
@@ -24,8 +31,16 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
eprintln!("{UNSTABLE_WARNING}\n");
let database = args.get_one::("database");
- let server = args.get_one::("server").map(|s| s.as_ref());
let anon_identity = args.get_flag("anon_identity");
+ let no_config = args.get_flag("no_config");
+
+ // The database arg stays as given (omitting it deliberately serves the whole
+ // server), but the server it is served from follows the project's config.
+ let config_server = resolve_config_server(no_config)?;
+ let server = args
+ .get_one::("server")
+ .map(|s| s.as_str())
+ .or(config_server.as_deref());
let conn = Connection {
host: config.get_host_url(server)?,