This is an automated email from the ASF dual-hosted git repository.

yuxia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new ee927b0  feat: Add get_server_nodes to Admin (#363)
ee927b0 is described below

commit ee927b0e9ded17fd522236641b14d203c1707599
Author: Kaiqi Dong <[email protected]>
AuthorDate: Sat Feb 28 05:11:59 2026 +0100

    feat: Add get_server_nodes to Admin (#363)
---
 bindings/cpp/include/fluss.hpp                  |  10 +++
 bindings/cpp/src/admin.cpp                      |  18 ++++
 bindings/cpp/src/lib.rs                         |  41 +++++++++
 bindings/cpp/test/test_admin.cpp                |  25 ++++++
 bindings/python/fluss/__init__.pyi              |  32 +++++++
 bindings/python/src/admin.rs                    |  81 +++++++++++++++++
 bindings/python/src/lib.rs                      |   1 +
 bindings/python/test/test_admin.py              |  17 ++++
 crates/fluss/src/client/admin.rs                |   8 ++
 crates/fluss/src/client/metadata.rs             |   2 +-
 crates/fluss/src/cluster/cluster.rs             | 112 ++++++++++++++++++++++++
 crates/fluss/src/cluster/mod.rs                 |  24 ++++-
 crates/fluss/src/lib.rs                         |   1 +
 crates/fluss/src/rpc/server_connection.rs       |   2 +-
 crates/fluss/tests/integration/admin.rs         |  43 +++++++++
 website/docs/user-guide/cpp/api-reference.md    |  16 ++++
 website/docs/user-guide/python/api-reference.md |  11 +++
 website/docs/user-guide/rust/api-reference.md   |  16 ++++
 18 files changed, 457 insertions(+), 3 deletions(-)

diff --git a/bindings/cpp/include/fluss.hpp b/bindings/cpp/include/fluss.hpp
index 0a62af9..cb06028 100644
--- a/bindings/cpp/include/fluss.hpp
+++ b/bindings/cpp/include/fluss.hpp
@@ -881,6 +881,14 @@ struct PartitionInfo {
     std::string partition_name;
 };
 
+struct ServerNode {
+    int32_t id;
+    std::string host;
+    uint32_t port;
+    std::string server_type;
+    std::string uid;
+};
+
 /// Descriptor for create_database (optional). Leave comment and properties 
empty for default.
 struct DatabaseDescriptor {
     std::string comment;
@@ -1073,6 +1081,8 @@ class Admin {
 
     Result TableExists(const TablePath& table_path, bool& out);
 
+    Result GetServerNodes(std::vector<ServerNode>& out);
+
    private:
     Result DoListOffsets(const TablePath& table_path, const 
std::vector<int32_t>& bucket_ids,
                          const OffsetSpec& offset_spec, 
std::unordered_map<int32_t, int64_t>& out,
diff --git a/bindings/cpp/src/admin.cpp b/bindings/cpp/src/admin.cpp
index 8deb182..49300c1 100644
--- a/bindings/cpp/src/admin.cpp
+++ b/bindings/cpp/src/admin.cpp
@@ -346,4 +346,22 @@ Result Admin::TableExists(const TablePath& table_path, 
bool& out) {
     return result;
 }
 
+Result Admin::GetServerNodes(std::vector<ServerNode>& out) {
+    if (!Available()) {
+        return utils::make_client_error("Admin not available");
+    }
+
+    auto ffi_result = admin_->get_server_nodes();
+    auto result = utils::from_ffi_result(ffi_result.result);
+    if (result.Ok()) {
+        out.clear();
+        out.reserve(ffi_result.server_nodes.size());
+        for (const auto& node : ffi_result.server_nodes) {
+            out.push_back({node.node_id, std::string(node.host), node.port,
+                           std::string(node.server_type), 
std::string(node.uid)});
+        }
+    }
+    return result;
+}
+
 }  // namespace fluss
diff --git a/bindings/cpp/src/lib.rs b/bindings/cpp/src/lib.rs
index 9b01d32..d26af6a 100644
--- a/bindings/cpp/src/lib.rs
+++ b/bindings/cpp/src/lib.rs
@@ -229,6 +229,19 @@ mod ffi {
         value: bool,
     }
 
+    struct FfiServerNode {
+        node_id: i32,
+        host: String,
+        port: u32,
+        server_type: String,
+        uid: String,
+    }
+
+    struct FfiServerNodesResult {
+        result: FfiResult,
+        server_nodes: Vec<FfiServerNode>,
+    }
+
     extern "Rust" {
         type Connection;
         type Admin;
@@ -319,6 +332,7 @@ mod ffi {
         fn get_database_info(self: &Admin, database_name: &str) -> 
FfiDatabaseInfoResult;
         fn list_tables(self: &Admin, database_name: &str) -> 
FfiListTablesResult;
         fn table_exists(self: &Admin, table_path: &FfiTablePath) -> 
FfiBoolResult;
+        fn get_server_nodes(self: &Admin) -> FfiServerNodesResult;
 
         // Table
         unsafe fn delete_table(table: *mut Table);
@@ -1104,6 +1118,33 @@ impl Admin {
             },
         }
     }
+
+    fn get_server_nodes(&self) -> ffi::FfiServerNodesResult {
+        let result = RUNTIME.block_on(async { 
self.inner.get_server_nodes().await });
+
+        match result {
+            Ok(nodes) => {
+                let server_nodes: Vec<ffi::FfiServerNode> = nodes
+                    .into_iter()
+                    .map(|node| ffi::FfiServerNode {
+                        node_id: node.id(),
+                        host: node.host().to_string(),
+                        port: node.port(),
+                        server_type: node.server_type().to_string(),
+                        uid: node.uid().to_string(),
+                    })
+                    .collect();
+                ffi::FfiServerNodesResult {
+                    result: ok_result(),
+                    server_nodes,
+                }
+            }
+            Err(e) => ffi::FfiServerNodesResult {
+                result: err_from_core_error(&e),
+                server_nodes: vec![],
+            },
+        }
+    }
 }
 
 // Table implementation
diff --git a/bindings/cpp/test/test_admin.cpp b/bindings/cpp/test/test_admin.cpp
index b6bb25b..99f93fc 100644
--- a/bindings/cpp/test/test_admin.cpp
+++ b/bindings/cpp/test/test_admin.cpp
@@ -285,6 +285,31 @@ TEST_F(AdminTest, ErrorTableAlreadyExist) {
     ASSERT_OK(adm.DropDatabase(db_name, true, true));
 }
 
+TEST_F(AdminTest, GetServerNodes) {
+    auto& adm = admin();
+
+    std::vector<fluss::ServerNode> nodes;
+    ASSERT_OK(adm.GetServerNodes(nodes));
+
+    ASSERT_GT(nodes.size(), 0u) << "Expected at least one server node";
+
+    bool has_coordinator = false;
+    bool has_tablet = false;
+    for (const auto& node : nodes) {
+        EXPECT_FALSE(node.host.empty()) << "Server node host should not be 
empty";
+        EXPECT_GT(node.port, 0u) << "Server node port should be > 0";
+        EXPECT_FALSE(node.uid.empty()) << "Server node uid should not be 
empty";
+
+        if (node.server_type == "CoordinatorServer") {
+            has_coordinator = true;
+        } else if (node.server_type == "TabletServer") {
+            has_tablet = true;
+        }
+    }
+    EXPECT_TRUE(has_coordinator) << "Expected a coordinator server node";
+    EXPECT_TRUE(has_tablet) << "Expected at least one tablet server node";
+}
+
 TEST_F(AdminTest, ErrorTableNotExist) {
     auto& adm = admin();
 
diff --git a/bindings/python/fluss/__init__.pyi 
b/bindings/python/fluss/__init__.pyi
index 514d011..4c2142d 100644
--- a/bindings/python/fluss/__init__.pyi
+++ b/bindings/python/fluss/__init__.pyi
@@ -185,6 +185,31 @@ class FlussConnection:
     ) -> bool: ...
     def __repr__(self) -> str: ...
 
+class ServerNode:
+    """Information about a server node in the Fluss cluster."""
+
+    @property
+    def id(self) -> int:
+        """The server node ID."""
+        ...
+    @property
+    def host(self) -> str:
+        """The hostname of the server."""
+        ...
+    @property
+    def port(self) -> int:
+        """The port number of the server."""
+        ...
+    @property
+    def server_type(self) -> str:
+        """The type of server ('CoordinatorServer' or 'TabletServer')."""
+        ...
+    @property
+    def uid(self) -> str:
+        """The unique identifier of the server (e.g. 'cs-0', 'ts-1')."""
+        ...
+    def __repr__(self) -> str: ...
+
 class FlussAdmin:
     async def create_database(
         self,
@@ -307,6 +332,13 @@ class FlussAdmin:
             List of PartitionInfo objects
         """
         ...
+    async def get_server_nodes(self) -> List[ServerNode]:
+        """Get all alive server nodes in the cluster.
+
+        Returns:
+            List of ServerNode objects (coordinator and tablet servers)
+        """
+        ...
     def __repr__(self) -> str: ...
 
 
diff --git a/bindings/python/src/admin.rs b/bindings/python/src/admin.rs
index 30db375..703b133 100644
--- a/bindings/python/src/admin.rs
+++ b/bindings/python/src/admin.rs
@@ -501,6 +501,30 @@ impl FlussAdmin {
         })
     }
 
+    /// Get all alive server nodes in the cluster.
+    ///
+    /// Returns:
+    ///     List[ServerNode]: List of server nodes (coordinator and tablet 
servers)
+    pub fn get_server_nodes<'py>(&self, py: Python<'py>) -> 
PyResult<Bound<'py, PyAny>> {
+        let admin = self.__admin.clone();
+
+        future_into_py(py, async move {
+            let nodes = admin
+                .get_server_nodes()
+                .await
+                .map_err(|e| FlussError::from_core_error(&e))?;
+
+            Python::attach(|py| {
+                let py_list = pyo3::types::PyList::empty(py);
+                for node in nodes {
+                    let py_node = ServerNode::from_core(node);
+                    py_list.append(Py::new(py, py_node)?)?;
+                }
+                Ok(py_list.unbind())
+            })
+        })
+    }
+
     fn __repr__(&self) -> String {
         "FlussAdmin()".to_string()
     }
@@ -552,3 +576,60 @@ impl PartitionInfo {
         }
     }
 }
+
+/// Information about a server node in the Fluss cluster
+#[pyclass]
+pub struct ServerNode {
+    id: i32,
+    host: String,
+    port: u32,
+    server_type: String,
+    uid: String,
+}
+
+#[pymethods]
+impl ServerNode {
+    #[getter]
+    fn id(&self) -> i32 {
+        self.id
+    }
+
+    #[getter]
+    fn host(&self) -> &str {
+        &self.host
+    }
+
+    #[getter]
+    fn port(&self) -> u32 {
+        self.port
+    }
+
+    #[getter]
+    fn server_type(&self) -> &str {
+        &self.server_type
+    }
+
+    #[getter]
+    fn uid(&self) -> &str {
+        &self.uid
+    }
+
+    fn __repr__(&self) -> String {
+        format!(
+            "ServerNode(id={}, host='{}', port={}, server_type='{}')",
+            self.id, self.host, self.port, self.server_type
+        )
+    }
+}
+
+impl ServerNode {
+    pub fn from_core(node: fcore::ServerNode) -> Self {
+        Self {
+            id: node.id(),
+            host: node.host().to_string(),
+            port: node.port(),
+            server_type: node.server_type().to_string(),
+            uid: node.uid().to_string(),
+        }
+    }
+}
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index ebc0d54..6890e08 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -125,6 +125,7 @@ fn _fluss(m: &Bound<'_, PyModule>) -> PyResult<()> {
     m.add_class::<ScanRecords>()?;
     m.add_class::<RecordBatch>()?;
     m.add_class::<PartitionInfo>()?;
+    m.add_class::<ServerNode>()?;
     m.add_class::<OffsetSpec>()?;
     m.add_class::<WriteResultHandle>()?;
     m.add_class::<DatabaseDescriptor>()?;
diff --git a/bindings/python/test/test_admin.py 
b/bindings/python/test/test_admin.py
index f203400..e2f4343 100644
--- a/bindings/python/test/test_admin.py
+++ b/bindings/python/test/test_admin.py
@@ -272,6 +272,23 @@ async def test_error_table_not_exist(admin):
     await admin.drop_table(table_path, ignore_if_not_exists=True)
 
 
+async def test_get_server_nodes(admin):
+    """Test get_server_nodes returns coordinator and tablet servers."""
+    nodes = await admin.get_server_nodes()
+
+    assert len(nodes) > 0, "Expected at least one server node"
+
+    server_types = [n.server_type for n in nodes]
+    assert "CoordinatorServer" in server_types, "Expected a coordinator server"
+    assert "TabletServer" in server_types, "Expected at least one tablet 
server"
+
+    for node in nodes:
+        assert node.host, "Server node host should not be empty"
+        assert node.port > 0, "Server node port should be > 0"
+        assert node.uid, "Server node uid should not be empty"
+        assert repr(node).startswith("ServerNode(")
+
+
 async def test_error_table_not_partitioned(admin):
     """Test error when calling partition operations on non-partitioned 
table."""
     db_name = "py_test_error_not_partitioned_db"
diff --git a/crates/fluss/src/client/admin.rs b/crates/fluss/src/client/admin.rs
index 3012f85..7a79e5e 100644
--- a/crates/fluss/src/client/admin.rs
+++ b/crates/fluss/src/client/admin.rs
@@ -16,6 +16,7 @@
 // under the License.
 
 use crate::client::metadata::Metadata;
+use crate::cluster::ServerNode;
 use crate::metadata::{
     DatabaseDescriptor, DatabaseInfo, JsonSerde, LakeSnapshot, PartitionInfo, 
PartitionSpec,
     PhysicalTablePath, TableBucket, TableDescriptor, TableInfo, TablePath,
@@ -267,6 +268,13 @@ impl FlussAdmin {
         ))
     }
 
+    /// Get all alive server nodes in the cluster, including the coordinator
+    /// and all tablet servers. Refreshes cluster metadata before returning.
+    pub async fn get_server_nodes(&self) -> Result<Vec<ServerNode>> {
+        self.metadata.reinit_cluster().await?;
+        Ok(self.metadata.get_cluster().get_server_nodes())
+    }
+
     /// Get the latest lake snapshot for a table
     pub async fn get_latest_lake_snapshot(&self, table_path: &TablePath) -> 
Result<LakeSnapshot> {
         let response = self
diff --git a/crates/fluss/src/client/metadata.rs 
b/crates/fluss/src/client/metadata.rs
index 3d8e77b..8581464 100644
--- a/crates/fluss/src/client/metadata.rs
+++ b/crates/fluss/src/client/metadata.rs
@@ -89,7 +89,7 @@ impl Metadata {
         Cluster::from_metadata_response(response, None)
     }
 
-    async fn reinit_cluster(&self) -> Result<()> {
+    pub(crate) async fn reinit_cluster(&self) -> Result<()> {
         let cluster = Self::init_cluster(&self.bootstrap, 
self.connections.clone()).await?;
         *self.cluster.write() = cluster.into();
         Ok(())
diff --git a/crates/fluss/src/cluster/cluster.rs 
b/crates/fluss/src/cluster/cluster.rs
index 5b1e083..d551870 100644
--- a/crates/fluss/src/cluster/cluster.rs
+++ b/crates/fluss/src/cluster/cluster.rs
@@ -369,6 +369,15 @@ impl Cluster {
             .unwrap_or(&EMPTY)
     }
 
+    pub fn get_server_nodes(&self) -> Vec<ServerNode> {
+        let mut nodes = Vec::new();
+        if let Some(coordinator) = &self.coordinator_server {
+            nodes.push(coordinator.clone());
+        }
+        nodes.extend(self.alive_tablet_servers.iter().cloned());
+        nodes
+    }
+
     pub fn get_one_available_server(&self) -> Option<&ServerNode> {
         if self.alive_tablet_servers.is_empty() {
             return None;
@@ -427,3 +436,106 @@ fn get_bucket_locations(
     }
     bucket_locations
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn make_coordinator() -> ServerNode {
+        ServerNode::new(
+            0,
+            "coord-host".to_string(),
+            9123,
+            ServerType::CoordinatorServer,
+        )
+    }
+
+    fn make_tablet_servers() -> HashMap<i32, ServerNode> {
+        let mut servers = HashMap::new();
+        servers.insert(
+            1,
+            ServerNode::new(1, "ts1-host".to_string(), 9124, 
ServerType::TabletServer),
+        );
+        servers.insert(
+            2,
+            ServerNode::new(2, "ts2-host".to_string(), 9125, 
ServerType::TabletServer),
+        );
+        servers
+    }
+
+    #[test]
+    fn test_server_node_getters() {
+        let node = ServerNode::new(5, "myhost".to_string(), 8080, 
ServerType::TabletServer);
+        assert_eq!(node.id(), 5);
+        assert_eq!(node.host(), "myhost");
+        assert_eq!(node.port(), 8080);
+        assert_eq!(node.server_type(), &ServerType::TabletServer);
+        assert_eq!(node.uid(), "ts-5");
+        assert_eq!(node.url(), "myhost:8080");
+    }
+
+    #[test]
+    fn test_server_type_display() {
+        assert_eq!(ServerType::TabletServer.to_string(), "TabletServer");
+        assert_eq!(
+            ServerType::CoordinatorServer.to_string(),
+            "CoordinatorServer"
+        );
+    }
+
+    #[test]
+    fn test_get_server_nodes_with_coordinator_and_tablets() {
+        let cluster = Cluster::new(
+            Some(make_coordinator()),
+            make_tablet_servers(),
+            HashMap::new(),
+            HashMap::new(),
+            HashMap::new(),
+            HashMap::new(),
+            HashMap::new(),
+        );
+
+        let nodes = cluster.get_server_nodes();
+        assert_eq!(nodes.len(), 3);
+
+        let coordinator_count = nodes
+            .iter()
+            .filter(|n| *n.server_type() == ServerType::CoordinatorServer)
+            .count();
+        assert_eq!(coordinator_count, 1);
+
+        let tablet_count = nodes
+            .iter()
+            .filter(|n| *n.server_type() == ServerType::TabletServer)
+            .count();
+        assert_eq!(tablet_count, 2);
+    }
+
+    #[test]
+    fn test_get_server_nodes_no_coordinator() {
+        let cluster = Cluster::new(
+            None,
+            make_tablet_servers(),
+            HashMap::new(),
+            HashMap::new(),
+            HashMap::new(),
+            HashMap::new(),
+            HashMap::new(),
+        );
+
+        let nodes = cluster.get_server_nodes();
+        assert_eq!(nodes.len(), 2);
+        assert!(
+            nodes
+                .iter()
+                .all(|n| *n.server_type() == ServerType::TabletServer)
+        );
+    }
+
+    #[test]
+    fn test_get_server_nodes_empty_cluster() {
+        let cluster = Cluster::default();
+        let nodes = cluster.get_server_nodes();
+        assert!(nodes.is_empty());
+    }
+}
diff --git a/crates/fluss/src/cluster/mod.rs b/crates/fluss/src/cluster/mod.rs
index 58e80c0..8b825ee 100644
--- a/crates/fluss/src/cluster/mod.rs
+++ b/crates/fluss/src/cluster/mod.rs
@@ -17,6 +17,7 @@
 
 use crate::BucketId;
 use crate::metadata::{PhysicalTablePath, TableBucket};
+use std::fmt;
 use std::sync::Arc;
 
 #[allow(clippy::module_inception)]
@@ -47,7 +48,7 @@ impl ServerNode {
         }
     }
 
-    pub fn uid(&self) -> &String {
+    pub fn uid(&self) -> &str {
         &self.uid
     }
 
@@ -58,6 +59,18 @@ impl ServerNode {
     pub fn id(&self) -> i32 {
         self.id
     }
+
+    pub fn host(&self) -> &str {
+        &self.host
+    }
+
+    pub fn port(&self) -> u32 {
+        self.port
+    }
+
+    pub fn server_type(&self) -> &ServerType {
+        &self.server_type
+    }
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -66,6 +79,15 @@ pub enum ServerType {
     CoordinatorServer,
 }
 
+impl fmt::Display for ServerType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            ServerType::TabletServer => write!(f, "TabletServer"),
+            ServerType::CoordinatorServer => write!(f, "CoordinatorServer"),
+        }
+    }
+}
+
 #[derive(Debug, Clone)]
 pub struct BucketLocation {
     pub table_bucket: TableBucket,
diff --git a/crates/fluss/src/lib.rs b/crates/fluss/src/lib.rs
index f079db2..689c37c 100644
--- a/crates/fluss/src/lib.rs
+++ b/crates/fluss/src/lib.rs
@@ -22,6 +22,7 @@ pub mod row;
 pub mod rpc;
 
 mod cluster;
+pub use cluster::{ServerNode, ServerType};
 
 pub mod config;
 pub mod error;
diff --git a/crates/fluss/src/rpc/server_connection.rs 
b/crates/fluss/src/rpc/server_connection.rs
index c8fe9ae..a345c2f 100644
--- a/crates/fluss/src/rpc/server_connection.rs
+++ b/crates/fluss/src/rpc/server_connection.rs
@@ -84,7 +84,7 @@ impl RpcClient {
                 }
             }
 
-            connections.insert(server_id.clone(), new_server.clone());
+            connections.insert(server_id.to_owned(), new_server.clone());
         }
         Ok(new_server)
     }
diff --git a/crates/fluss/tests/integration/admin.rs 
b/crates/fluss/tests/integration/admin.rs
index c0745dc..3502923 100644
--- a/crates/fluss/tests/integration/admin.rs
+++ b/crates/fluss/tests/integration/admin.rs
@@ -519,6 +519,49 @@ mod admin_test {
             .expect("drop_table with ignore_if_not_exists should succeed");
     }
 
+    #[tokio::test]
+    async fn test_get_server_nodes() {
+        let cluster = get_fluss_cluster();
+        let connection = cluster.get_fluss_connection().await;
+        let admin = connection.get_admin().await.unwrap();
+
+        let nodes = admin
+            .get_server_nodes()
+            .await
+            .expect("should get server nodes");
+
+        assert!(
+            !nodes.is_empty(),
+            "Expected at least one server node in the cluster"
+        );
+
+        let has_coordinator = nodes
+            .iter()
+            .any(|n| *n.server_type() == fluss::ServerType::CoordinatorServer);
+        assert!(has_coordinator, "Expected a coordinator server node");
+
+        let tablet_count = nodes
+            .iter()
+            .filter(|n| *n.server_type() == fluss::ServerType::TabletServer)
+            .count();
+        assert!(
+            tablet_count >= 1,
+            "Expected at least one tablet server node"
+        );
+
+        for node in &nodes {
+            assert!(
+                !node.host().is_empty(),
+                "Server node host should not be empty"
+            );
+            assert!(node.port() > 0, "Server node port should be > 0");
+            assert!(
+                !node.uid().is_empty(),
+                "Server node uid should not be empty"
+            );
+        }
+    }
+
     #[tokio::test]
     async fn test_error_table_not_partitioned() {
         let cluster = get_fluss_cluster();
diff --git a/website/docs/user-guide/cpp/api-reference.md 
b/website/docs/user-guide/cpp/api-reference.md
index 30d89a9..489f13a 100644
--- a/website/docs/user-guide/cpp/api-reference.md
+++ b/website/docs/user-guide/cpp/api-reference.md
@@ -78,6 +78,22 @@ Complete API reference for the Fluss C++ client.
 
|-----------------------------------------------------------------------------|------------------------------|
 | `GetLatestLakeSnapshot(const TablePath& path, LakeSnapshot& out) -> Result` 
| Get the latest lake snapshot |
 
+### Cluster Operations
+
+| Method                                                    | Description      
                                  |
+|-----------------------------------------------------------|----------------------------------------------------|
+| `GetServerNodes(std::vector<ServerNode>& out) -> Result`  | Get all alive 
server nodes (coordinator + tablets) |
+
+## `ServerNode`
+
+| Field         | Type          | Description                                  
            |
+|---------------|---------------|----------------------------------------------------------|
+| `id`          | `int32_t`     | Server node ID                               
            |
+| `host`        | `std::string` | Hostname of the server                       
            |
+| `port`        | `uint32_t`    | Port number                                  
            |
+| `server_type` | `std::string` | Server type (`"CoordinatorServer"` or 
`"TabletServer"`)  |
+| `uid`         | `std::string` | Unique identifier (e.g. `"cs-0"`, `"ts-1"`)  
           |
+
 ## `Table`
 
 | Method                        | Description                              |
diff --git a/website/docs/user-guide/python/api-reference.md 
b/website/docs/user-guide/python/api-reference.md
index fa62fd9..1c97066 100644
--- a/website/docs/user-guide/python/api-reference.md
+++ b/website/docs/user-guide/python/api-reference.md
@@ -50,6 +50,17 @@ Supports `with` statement (context manager).
 | `await drop_partition(table_path, partition_spec, 
ignore_if_not_exists=False)`                                        | Drop a 
partition                      |
 | `await list_partition_infos(table_path) -> list[PartitionInfo]`              
                                         | List partitions                      
 |
 | `await get_latest_lake_snapshot(table_path) -> LakeSnapshot`                 
                                         | Get latest lake snapshot             
 |
+| `await get_server_nodes() -> list[ServerNode]`                               
                                         | Get all alive server nodes           
 |
+
+## `ServerNode`
+
+| Property                 | Description                                       
         |
+|--------------------------|------------------------------------------------------------|
+| `.id -> int`             | Server node ID                                    
         |
+| `.host -> str`           | Hostname of the server                            
         |
+| `.port -> int`           | Port number                                       
         |
+| `.server_type -> str`    | Server type (`"CoordinatorServer"` or 
`"TabletServer"`)    |
+| `.uid -> str`            | Unique identifier (e.g. `"cs-0"`, `"ts-1"`)       
        |
 
 ## `FlussTable`
 
diff --git a/website/docs/user-guide/rust/api-reference.md 
b/website/docs/user-guide/rust/api-reference.md
index a38cd7d..2d149aa 100644
--- a/website/docs/user-guide/rust/api-reference.md
+++ b/website/docs/user-guide/rust/api-reference.md
@@ -71,6 +71,22 @@ Complete API reference for the Fluss Rust client.
 
|--------------------------------------------------------------------------------------------|------------------------------|
 | `async fn get_latest_lake_snapshot(&self, table_path: &TablePath) -> 
Result<LakeSnapshot>` | Get the latest lake snapshot |
 
+### Cluster Operations
+
+| Method                                                        | Description  
                                       |
+|---------------------------------------------------------------|-----------------------------------------------------|
+| `async fn get_server_nodes(&self) -> Result<Vec<ServerNode>>` | Get all 
alive server nodes (coordinator + tablets)  |
+
+## `ServerNode`
+
+| Method                            | Description                              
            |
+|-----------------------------------|------------------------------------------------------|
+| `fn id(&self) -> i32`            | Server node ID                            
           |
+| `fn host(&self) -> &str`         | Hostname of the server                    
           |
+| `fn port(&self) -> u32`          | Port number                               
           |
+| `fn server_type(&self) -> &ServerType` | Server type (`CoordinatorServer` or 
`TabletServer`) |
+| `fn uid(&self) -> &str`          | Unique identifier (e.g. `"cs-0"`, 
`"ts-1"`)         |
+
 ## `FlussTable<'a>`
 
 | Method                                        | Description                  
           |

Reply via email to