Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3cddab7
feat(config): expose compact environment templates
ParthibanRajasekaran Sep 29, 2026
d842bd2
feat: list configuration environment variables
ParthibanRajasekaran Sep 29, 2026
52465bd
refine config-env-vars implementation per scope requirements
ParthibanRajasekaran Sep 29, 2026
b98113a
chore(configs): format code per cargo fmt standards
ParthibanRajasekaran Sep 29, 2026
5d2981a
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Sep 29, 2026
180fee6
fix(connectors): remove unused mut keyword
ParthibanRajasekaran Sep 29, 2026
7e5172a
Merge branch 'codex/issue-4322-config-env-vars' of https://github.com…
ParthibanRajasekaran Sep 29, 2026
4b88b53
refactor(configs): address CodeRabbit review findings (9 of 10)
ParthibanRajasekaran Sep 29, 2026
1beda3c
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Sep 29, 2026
78dd052
feat(configs): address CodeRabbit review findings (9 of 10)
ParthibanRajasekaran Sep 29, 2026
ccd6e8d
Merge branch 'codex/issue-4322-config-env-vars' of https://github.com…
ParthibanRajasekaran Sep 29, 2026
c2eae55
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Sep 30, 2026
b68f115
fix(connectors): handle stdout closure gracefully in print_config_env…
ParthibanRajasekaran Sep 30, 2026
20ab3dd
Merge branch 'codex/issue-4322-config-env-vars' of https://github.com…
ParthibanRajasekaran Sep 30, 2026
e9feb35
fix(configs): add missing max_elements field to EnvVarTemplate struct
ParthibanRajasekaran Sep 30, 2026
e6fcdaa
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Sep 30, 2026
23aba20
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Sep 30, 2026
8cf0a35
fix(configs_derive): set max_elements in non-Vec nested template literal
ParthibanRajasekaran Sep 30, 2026
e5ed585
fix(configs): add shared print_env_var_names helper with consistent B…
ParthibanRajasekaran Sep 30, 2026
dca4108
test(configs): assert env_templates() and env_mappings() stay in sync
ParthibanRajasekaran Sep 30, 2026
f837d0d
test(integration): move config_env_listing.rs into its own module dir…
ParthibanRajasekaran Sep 30, 2026
173d05a
test(integration): harden and extend config_env_listing coverage
ParthibanRajasekaran Sep 30, 2026
1cb8ee7
docs: fix connectors --help clap attachment, update env-var placehold…
ParthibanRajasekaran Sep 30, 2026
67f0d50
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Sep 30, 2026
cb97b2d
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Sep 30, 2026
a78e9d5
'EOF'fix: correct compilation errors and add missing ENABLED fields E…
ParthibanRajasekaran Sep 30, 2026
5b19a78
fix: resolve merge corruption and add missing ENABLED fields EOF )"
ParthibanRajasekaran Sep 30, 2026
3b2912b
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Oct 1, 2026
3062490
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Oct 1, 2026
3e4e747
fix(integration): add missing tempdir isolation to config_env_listing…
ParthibanRajasekaran Oct 1, 2026
cf9b0bf
Merge branch 'codex/issue-4322-config-env-vars' of https://github.com…
ParthibanRajasekaran Oct 1, 2026
af56297
style: apply cargo fmt formatting
ParthibanRajasekaran Oct 1, 2026
c0a6182
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Oct 1, 2026
47f56bb
fix(configs): improve env-var listing tests with detailed error context
ParthibanRajasekaran Oct 1, 2026
d874cd6
refactor(configs): make print_env_var_names testable via Write trait
ParthibanRajasekaran Oct 1, 2026
d529842
fix(configs): address remaining hubcio review comments
ParthibanRajasekaran Oct 1, 2026
d2d492a
fix(configs): address remaining hubcio review comments
ParthibanRajasekaran Oct 1, 2026
0691266
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Oct 2, 2026
dc5273e
fix(configs): fix McpRuntimeError Io variant and actually use test he…
ParthibanRajasekaran Oct 2, 2026
27503c2
Merge branch 'master' into codex/issue-4322-config-env-vars
ParthibanRajasekaran Oct 3, 2026
479cf90
fix(configs): resolve remaining simplification requests from review
ParthibanRajasekaran Oct 3, 2026
fe3e2f9
Merge branch 'codex/issue-4322-config-env-vars' of https://github.com…
ParthibanRajasekaran Oct 3, 2026
2a44277
fix(configs): complete environment variable listing
ParthibanRajasekaran Oct 3, 2026
af7e26e
Address config env listing review feedback
ParthibanRajasekaran Oct 4, 2026
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
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions core/ai/mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ systemd = ["dep:sd-notify", "dep:tokio-util"]
[dependencies]
axum = { workspace = true }
axum-server = { workspace = true }
clap = { workspace = true }
configs = { workspace = true }
configs_derive = { workspace = true }
dirs = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions core/ai/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,14 @@ The configuration file must use TOML. The default path is `core/ai/mcp/config.to

Set `IGGY_MCP_ENV_PATH` to load a particular dotenv file. Otherwise `.env` is searched for in the current directory and its parents. Existing environment variables take precedence over dotenv values.

Run `iggy-mcp --list-config-env-vars` to print the supported configuration environment variables and exit before loading dotenv or configuration files or creating the runtime.

A non-empty `iggy.token` takes precedence over username and password. It accepts a literal PAT or a `file:` reference such as `file:/run/secrets/iggy_pat`; file contents are trimmed and a leading `~/` expands to the home directory.

Set `command` to the absolute path of the built executable. This Claude Desktop example uses the development broker credentials:

`iggy-mcp` rejects unknown command-line arguments with exit code 2. Keep the client or container `args` list empty unless it contains a supported flag.

```json
{
"mcpServers": {
Expand Down
2 changes: 2 additions & 0 deletions core/ai/mcp/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ pub enum McpRuntimeError {
TokenFileReadError(String, String),
#[error("Token file is empty: {0}")]
TokenFileEmpty(String),
#[error("Failed to list config environment variables")]
ListConfigEnvVars(#[source] std::io::Error),
}
34 changes: 31 additions & 3 deletions core/ai/mcp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
// specific language governing permissions and limitations
// under the License.

use ::configs::ConfigProvider;
use ::configs::{
ConfigEnvMappings, ConfigProvider, MCP_CONFIG_PATH_ENV, MCP_ENV_PATH_ENV, MCP_RUNTIME_ENV_VARS,
print_env_var_names,
};
use clap::Parser;
use configs::{McpServerConfig, McpTransport};
use dotenvy::dotenv;
use error::McpRuntimeError;
Expand All @@ -42,7 +46,20 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");

const DEFAULT_CONFIG_PATH: &str = "core/ai/mcp/config.toml";

#[derive(Debug, Parser)]
#[command(author = "Apache Iggy", version)]
struct Args {
/// Print supported configuration environment variables and exit.
#[arg(long)]
list_config_env_vars: bool,
}

fn main() -> Result<(), McpRuntimeError> {
let args = Args::parse();
Comment thread
ParthibanRajasekaran marked this conversation as resolved.
if args.list_config_env_vars {
print_config_env_vars().map_err(McpRuntimeError::ListConfigEnvVars)?;
return Ok(());
}
let runtime = Builder::new_multi_thread()
.enable_all()
.build()
Expand All @@ -53,12 +70,23 @@ fn main() -> Result<(), McpRuntimeError> {
result
}

fn print_config_env_vars() -> std::io::Result<()> {
let mut stdout = std::io::stdout();
print_env_var_names(
McpServerConfig::env_templates()
.iter()
.map(|t| t.env_name)
.chain(MCP_RUNTIME_ENV_VARS.iter().copied()),
&mut stdout,
)
}

async fn run() -> Result<(), McpRuntimeError> {
let standard_font = FIGlet::standard().unwrap();
let figure = standard_font.convert("Iggy MCP Server");
eprintln!("{}", figure.unwrap());

if let Ok(env_path) = std::env::var("IGGY_MCP_ENV_PATH") {
if let Ok(env_path) = std::env::var(MCP_ENV_PATH_ENV) {
if dotenvy::from_path(&env_path).is_ok() {
eprintln!("Loaded environment variables from path: {env_path}");
}
Expand All @@ -70,7 +98,7 @@ async fn run() -> Result<(), McpRuntimeError> {
}

let config_path =
env::var("IGGY_MCP_CONFIG_PATH").unwrap_or_else(|_| DEFAULT_CONFIG_PATH.to_string());
env::var(MCP_CONFIG_PATH_ENV).unwrap_or_else(|_| DEFAULT_CONFIG_PATH.to_string());
eprintln!("Configuration file path: {config_path}");
let config: McpServerConfig = McpServerConfig::config_provider(config_path)
.load_config()
Expand Down
102 changes: 102 additions & 0 deletions core/configs/src/configs_impl/env_listing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

/// Writes each name in `names`, sorted and deduplicated, to the provided writer — one
/// call, one error policy. A closed writer (e.g. `iggy-server --list-config-env-vars | head -1`)
/// is expected, not a failure: on `BrokenPipe` this stops writing and returns `Ok(())`. Any
/// other I/O error is real and is propagated so the caller exits non-zero.
pub fn print_env_var_names<I, S, W>(names: I, writer: &mut W) -> std::io::Result<()>
Comment thread
ParthibanRajasekaran marked this conversation as resolved.
where
I: IntoIterator<Item = S>,
S: Into<String>,
W: std::io::Write,
{
let mut names: Vec<String> = names.into_iter().map(Into::into).collect();
names.sort_unstable();
names.dedup();

for name in names {
if let Err(err) = writeln!(writer, "{name}") {
return if err.kind() == std::io::ErrorKind::BrokenPipe {
Ok(())
} else {
Err(err)
};
}
}
Ok(())
}

pub const MCP_CONFIG_PATH_ENV: &str = "IGGY_MCP_CONFIG_PATH";
pub const MCP_ENV_PATH_ENV: &str = "IGGY_MCP_ENV_PATH";
/// Env vars `iggy-mcp --list-config-env-vars` advertises beyond the derived
/// `McpServerConfig` templates.
pub const MCP_RUNTIME_ENV_VARS: &[&str] = &[
super::file_provider::DISPLAY_CONFIG_ENV,
MCP_CONFIG_PATH_ENV,
MCP_ENV_PATH_ENV,
];

pub const CONNECTORS_CONFIG_PATH_ENV: &str = "IGGY_CONNECTORS_CONFIG_PATH";
pub const CONNECTORS_ENV_PATH_ENV: &str = "IGGY_CONNECTORS_ENV_PATH";
/// Env vars `iggy-connectors --list-config-env-vars` advertises beyond the
/// derived `ConnectorsRuntimeConfig` templates.
pub const CONNECTORS_RUNTIME_ENV_VARS: &[&str] = &[
CONNECTORS_CONFIG_PATH_ENV,
CONNECTORS_ENV_PATH_ENV,
super::file_provider::DISPLAY_CONFIG_ENV,
];

/// Segment between a connector env-var prefix and a plugin config field.
pub const PLUGIN_CONFIG_ENV_SEGMENT: &str = "PLUGIN_CONFIG_";

#[cfg(test)]
mod tests {
use super::*;
use std::io::{Error, ErrorKind, Write};

struct FailingWriter(ErrorKind);

impl Write for FailingWriter {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(Error::from(self.0))
}

fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

#[test]
fn print_env_var_names_sorts_and_deduplicates() {
let mut output = Vec::new();
print_env_var_names(["IGGY_B", "IGGY_A", "IGGY_B"], &mut output).unwrap();
assert_eq!(output, b"IGGY_A\nIGGY_B\n");
}

#[test]
fn print_env_var_names_accepts_a_closed_pipe() {
assert!(print_env_var_names(["IGGY_A"], &mut FailingWriter(ErrorKind::BrokenPipe)).is_ok());
}

#[test]
fn print_env_var_names_propagates_other_write_errors() {
let error = print_env_var_names(["IGGY_A"], &mut FailingWriter(ErrorKind::StorageFull))
.expect_err("storage-full error must be propagated");
assert_eq!(error.kind(), ErrorKind::StorageFull);
}
}
57 changes: 57 additions & 0 deletions core/configs/src/configs_impl/env_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,31 @@ pub struct EnvVarMapping {
pub is_secret: bool,
}

/// Compact representation of a configuration environment variable.
///
/// Array indices are represented by `<N>` in `env_name`. `max_elements`
/// contains the expansion limit for each placeholder, from left to right.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EnvVarTemplate {
/// Environment variable name, with `<N>` for each array index.
pub env_name: &'static str,
/// Config path, with `<N>` for each array index.
pub config_path: &'static str,
/// Whether this field contains secret data.
pub is_secret: bool,
/// Maximum element count for each `<N>` placeholder, from left to right.
pub max_elements: &'static [usize],
}

/// Trait for configuration types that provide environment variable mappings.
/// Implemented automatically by the `#[derive(ConfigEnv)]` macro.
pub trait ConfigEnvMappings {
/// Returns all environment variable mappings for this config type.
fn env_mappings() -> &'static [EnvVarMapping];

/// Returns the compact environment variable templates for this config type.
fn env_templates() -> &'static [EnvVarTemplate];

/// Finds a mapping by environment variable name.
fn find_by_env_name(env_name: &str) -> Option<&'static EnvVarMapping> {
Self::env_mappings().iter().find(|m| m.env_name == env_name)
Expand All @@ -59,3 +78,41 @@ pub trait ConfigEnvMappings {
.collect()
}
}

impl EnvVarTemplate {
fn expand(&self) -> Vec<EnvVarMapping> {
let mut results = vec![(self.env_name.to_string(), self.config_path.to_string())];

for &limit in self.max_elements {
let mut next = Vec::new();
for (env_name, config_path) in results {
for i in 0..limit {
let index = i.to_string();
next.push((
env_name.replacen("<N>", &index, 1),
config_path.replacen("<N>", &index, 1),
));
}
}
results = next;
}

results
.into_iter()
.map(|(env_name, config_path)| EnvVarMapping {
env_name: Box::leak(env_name.into_boxed_str()),
config_path: Box::leak(config_path.into_boxed_str()),
is_secret: self.is_secret,
})
.collect()
}
}

/// Expands compact templates into the mappings consumed by the env provider.
///
/// The returned mappings own leaked names and paths so generated
/// `ConfigEnvMappings` implementations can cache them in a `OnceLock` and
/// return `'static` references. Call this once per config type.
pub fn expand_env_templates(templates: &[EnvVarTemplate]) -> Vec<EnvVarMapping> {
templates.iter().flat_map(EnvVarTemplate::expand).collect()
}
11 changes: 6 additions & 5 deletions core/configs/src/configs_impl/file_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use figment::{
use std::{env, path::Path};
use tracing::{error, info, warn};

const DISPLAY_CONFIG_ENV: &str = "IGGY_DISPLAY_CONFIG";
pub(crate) const DISPLAY_CONFIG_ENV: &str = "IGGY_DISPLAY_CONFIG";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelocatedTarget {
Expand Down Expand Up @@ -448,7 +448,7 @@ mod tests {
let unknown = unknown_env_names(
names(&siblings).into_iter(),
"IGGY_",
crate::server_config::server::SERVER_PROCESS_ENV_VARS,
&crate::server_config::server::server_process_env_vars().collect::<Vec<_>>(),
crate::server_config::server::SERVER_ALLOWED_ENV_PREFIXES,
);
assert!(
Expand Down Expand Up @@ -483,12 +483,13 @@ mod tests {
}

/// The server reads these variables outside its config, so the boot check
/// must accept them. Without the `SERVER_PROCESS_ENV_VARS` chain in
/// must accept them. Without the `server_process_env_vars()` chain in
/// `ServerConfig::config_provider`, a debug build refuses to boot.
#[test]
fn given_the_server_process_variables_when_checking_then_the_server_should_boot() {
let unknown =
server_unknown_env_names(crate::server_config::server::SERVER_PROCESS_ENV_VARS);
let process_env_vars =
crate::server_config::server::server_process_env_vars().collect::<Vec<_>>();
let unknown = server_unknown_env_names(&process_env_vars);

assert!(
unknown.is_empty(),
Expand Down
8 changes: 7 additions & 1 deletion core/configs/src/configs_impl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,20 @@
//! - JSON value field handling
//! - Automatic type conversion and validation

mod env_listing;
mod env_mapping;
mod error;
mod file_provider;
mod parsing;
mod traits;
mod typed_env_provider;

pub use env_mapping::{ConfigEnvMappings, EnvVarMapping};
pub use env_listing::{
CONNECTORS_CONFIG_PATH_ENV, CONNECTORS_ENV_PATH_ENV, CONNECTORS_RUNTIME_ENV_VARS,
MCP_CONFIG_PATH_ENV, MCP_ENV_PATH_ENV, MCP_RUNTIME_ENV_VARS, PLUGIN_CONFIG_ENV_SEGMENT,
print_env_var_names,
};
pub use env_mapping::{ConfigEnvMappings, EnvVarMapping, EnvVarTemplate, expand_env_templates};
pub use error::ConfigurationError;
pub use file_provider::{FileConfigProvider, RelocatedKey, RelocatedTarget};
pub use parsing::parse_env_value_to_json;
Expand Down
Loading