Premise: Expose ndarray's N-dimensional array API as MCP tools for both runtime computation and code generation. Approach: Completionist harvesting using dual-mode tools (primary), fragment tools (generics/parallel), and minimal runtime handles.
Similar to nalgebra:
- Natural JSON serialization (arrays → nested JSON)
- Synchronous operations (no async)
- Concrete methods on types (not trait-heavy)
- Clear taxonomy (creation → indexing → operations → manipulation)
Different from nalgebra:
- General N-D arrays (not just 2D matrices) — up to 6 static dimensions (Ix0-Ix6) + dynamic (IxDyn)
- Broadcasting semantics — element-wise ops across different shapes (like NumPy)
- Parallel operations — rayon integration for data parallelism
- View-based slicing — zero-copy views with arbitrary strides
- NumPy compatibility — familiar API for Python users
Wider adoption:
- Foundation for scientific computing ecosystem (ndarray-linalg, ndarray-stats, ndarray-rand)
- Used by: polars, image processing, ML libraries, scientific simulations
- NumPy mental model attracts Python → Rust migrations
ndarray uses type-level dimensions and ownership modes:
pub struct ArrayBase<S, D>
where
S: Data,
D: Dimension,
{
// S determines ownership: OwnedRepr<A>, ViewRepr<&A>, ArcArray<A>
// D determines shape: Ix0, Ix1, Ix2, ..., Ix6, IxDyn
}Type aliases:
type Array<A, D> = ArrayBase<OwnedRepr<A>, D>; // Owned
type ArrayView<'a, A, D> = ArrayBase<ViewRepr<&'a A>, D>; // Borrowed (lifetime!)
type ArcArray<A, D> = ArrayBase<ArcArray<A>, D>; // SharedCrossing the JSON boundary:
| Category | Examples | MCP Strategy |
|---|---|---|
| Fixed-dim arrays | Array1<f64>, Array2<i32>, Array3<u8> |
✅ Dual-mode (serialize as nested JSON) |
| Dynamic arrays | ArrayD<f64> (IxDyn) |
✅ Dual-mode (shape in JSON metadata) |
| Views (lifetimes) | ArrayView2<'a, f64> |
❌ Cannot serialize (use owned/Arc instead) |
| ArcArray | ArcArray2<f64> |
✅ Runtime handles (UUID → Arc) |
| Generic dim code | fn compute<D: Dimension>(arr: Array<f64, D>) |
✅ Fragment tools |
| Parallel ops | arr.par_map_inplace(f) |
✅ Fragment tools (rayon code gen) |
| Broadcasting | Automatic shape alignment | ✅ Dual-mode (runtime + emit rules) |
Tools that both execute at runtime AND emit code via CustomEmit:
- From data (15):
array_from_vec,array_from_shape_vec,array_from_elem,array_from_fn, etc. - Ranges (10):
array_range,array_linspace,array_logspace,array_geomspace, etc. - Special values (10):
array_zeros,array_ones,array_eye,array_full, etc. - Random (10):
array_rand,array_rand_uniform,array_rand_normal,array_rand_exponential, etc. - From iterators (10):
array_from_iter,array_from_iter_2d,collect_rows,collect_columns, etc. - Type conversion (5):
array_from_diag,array_from_shape_fn, etc.
- Element access (10):
array_get,array_index,array_get_mut,array_uget(unchecked), etc. - Slicing (20):
array_slice,array_slice_mut,slice_axis,slice_collapse,slice_each_axis, etc. - Views (10):
array_view,array_view_mut,into_slice,as_slice_memory_order, etc. - Iteration (10):
array_iter,iter_mut,indexed_iter,axis_iter,outer_iter, etc.
- Element-wise binary (15):
array_add,array_sub,array_mul,array_div,array_rem,array_bitand, etc. - Element-wise unary (10):
array_neg,array_abs,array_recip,array_mapv,array_mapv_inplace, etc. - Scalar ops (10):
array_add_scalar,array_mul_scalar,array_pow_scalar, etc. - Comparison (10):
array_eq,array_ne,array_lt,array_le,array_gt,array_ge, etc. - Logical (5):
array_and,array_or,array_not,array_xor, etc.
- Auto broadcast (15):
broadcast_add,broadcast_mul,broadcast_to,broadcast_to_shape, etc. - Manual broadcast (10):
insert_axis,broadcast_axis,remove_axis,expand_dims, etc. - Shape ops (5):
broadcast_with,broadcast_iter, etc.
- Full reductions (15):
sum,mean,var,std,min,max,product,all,any, etc. - Axis reductions (15):
sum_axis,mean_axis,var_axis,min_axis,max_axis, etc. - Cumulative (10):
accumulate_axis_inplace,scan_axis, etc.
- Matrix ops (15):
dot,matrix_mul,outer,inner,kron,vdot, etc. - Transpose/reshape (10):
transpose,t,reversed_axes,permuted_axes,swap_axes, etc. - Norms (10):
norm,norm_l1,norm_l2,norm_linf,norm_axis, etc. - Decompositions (5): References to ndarray-linalg (SVD, QR, etc. — separate shadow crate)
- Concatenation (10):
concatenate,stack,append,append_axis, etc. - Splitting (10):
split_at,split_axis,split_complex, etc. - Reshape (15):
reshape,into_shape,reshape_with_order,into_dyn,into_dimensionality, etc. - Axis ops (10):
insert_axis,remove_axis,merge_axes,move_axis, etc. - Flipping (10):
invert_axis,slice_each_axis_inplace,reversed_axes, etc. - Cloning (5):
clone,to_owned,into_shared,into_owned, etc.
- Math (20):
map,mapv,mapv_inplace,zip_mut_with,fold,fold_axis, etc. - Apply (10):
map_axis,map_axis_mut,accumulate_axis_inplace, etc. - Zip (10):
zip,azip,par_azip(parallel),zip_mut_with, etc.
- CSV (10):
read_csv,write_csv,from_csv_string,to_csv_string, etc. - Binary (10):
serialize,deserialize,to_bytes,from_bytes, etc. - Display (10):
to_string,fmt_table,display_shape, etc.
Code generation for generic dimensions, parallel operations, and complex patterns:
emit_array_type—Array<T, Ix2>,ArrayD<f64>, genericArray<T, D>emit_function_generic_dim— Functions withD: Dimensionparameteremit_fixed_dim_function— Functions for specific Ix1, Ix2, etc.emit_dyn_dim_function— Functions using IxDyn (dynamic dims)emit_const_dim_bounds— Where clauses for dimension constraints- And 25 more for dimension-generic patterns
emit_par_map_inplace— Parallel element-wise modificationemit_par_azip— Parallel zip macro invocationsemit_par_chunks— Parallel chunk processingemit_rayon_iterator— Rayon parallel iterator chains- And 16 more for parallel patterns
emit_broadcast_binary_op— Generate broadcasting arithmeticemit_explicit_broadcast— Manual broadcast with shape checksemit_broadcast_assign— Broadcasted assignment- And 12 more for broadcast patterns
emit_module_ndarray— Complete ndarray moduleemit_struct_with_arrays— Structs containing array fieldsemit_impl_block_array_ops— Impl blocks with array operationsemit_test_suite_ndarray— Test suite generationassemble_ndarray_binary— Full executable with ndarray- And 10 more for assembly patterns
UUID-keyed handles for persistent arrays and workflows:
| Registry | Operations | Count |
|---|---|---|
| ArrayRegistry | array_create_handle, array_get, array_set, array_delete, array_clone_handle |
15 |
| ViewRegistry | view_create, view_slice, view_reshape, view_delete |
10 |
| IteratorRegistry | iterator_create, iterator_next, iterator_collect |
10 |
| ParallelRegistry | parallel_map, parallel_reduce, parallel_zip |
5 |
Why handles?
- Long-lived arrays in agent workflows
- Chained transformations without full serialization
- Shared arrays across multiple operations (ArcArray)
- Lazy iteration and chunking
{
"type": "Array1<f64>",
"shape": [5],
"data": [1.0, 2.0, 3.0, 4.0, 5.0]
}{
"type": "Array2<f64>",
"shape": [3, 3],
"data": [
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0]
]
}{
"type": "Array3<f64>",
"shape": [2, 3, 4],
"data": [
[[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]],
[[13.0, 14.0, 15.0, 16.0], [17.0, 18.0, 19.0, 20.0], [21.0, 22.0, 23.0, 24.0]]
]
}{
"type": "ArrayD<f64>",
"ndim": 4,
"shape": [2, 3, 4, 5],
"data": [...], // Flattened row-major
"layout": "row_major" // or "column_major"
}{
"type": "ArrayView2<f64>",
"base_array_id": "uuid-of-parent",
"slice": {
"axis_0": { "start": 0, "end": 5, "step": 1 },
"axis_1": { "start": 2, "end": 8, "step": 2 }
}
}Goal: Establish dual-mode pattern for array creation and basic access.
crates/elicit_ndarray/
├── Cargo.toml
└── src/
├── lib.rs
├── creation.rs # Array creation dual-mode tools
├── indexing.rs # Indexing/slicing dual-mode tools
├── handles.rs # UUID registries
└── serde_types.rs # JSON serialization wrappers
[package]
name = "elicit_ndarray"
version = "0.1.0"
edition = "2021"
[dependencies]
elicitation = { workspace = true }
elicitation_derive.workspace = true
ndarray = { version = "0.17", features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
uuid.workspace = true
rmcp.workspace = true
tracing.workspace = true
[features]
emit = ["dep:quote", "elicitation/emit"]
rayon = ["ndarray/rayon"]use elicitation_derive::elicit_tool;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArrayZerosParams {
pub shape: Vec<usize>,
pub element_type: String, // "f64", "i32", etc.
}
#[elicit_tool(
plugin = "ndarray_creation",
name = "array_zeros",
description = "Create array filled with zeros",
emit = Auto
)]
async fn array_zeros(p: ArrayZerosParams) -> Result<CallToolResult, ErrorData> {
// Runtime: create array, serialize
let shape = IxDyn(&p.shape);
let array = Array::<f64, _>::zeros(shape);
let result = ArrayJson::from_array(&array);
Ok(CallToolResult::success(json!({ "array": result })))
}
// Auto-generated CustomEmit impl:
impl CustomEmit<ArrayZerosParams> for ArrayZerosEmit {
fn emit_code(params: &ArrayZerosParams) -> TokenStream {
let shape_dims = ¶ms.shape;
let element_type: TokenStream = params.element_type.parse().unwrap();
match params.shape.len() {
1 => quote! {
Array1::<#element_type>::zeros(#(#shape_dims),*)
},
2 => quote! {
Array2::<#element_type>::zeros((#(#shape_dims),*))
},
_ => quote! {
ArrayD::<#element_type>::zeros(IxDyn(&[#(#shape_dims),*]))
},
}
}
}#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ArrayJson {
Array1 { shape: [usize; 1], data: Vec<f64> },
Array2 { shape: [usize; 2], data: Vec<Vec<f64>> },
Array3 { shape: [usize; 3], data: Vec<Vec<Vec<f64>>> },
ArrayD { ndim: usize, shape: Vec<usize>, data: Vec<f64> },
}
impl ArrayJson {
pub fn from_array<D: Dimension>(arr: &Array<f64, D>) -> Self {
let shape = arr.shape();
match arr.ndim() {
1 => ArrayJson::Array1 {
shape: [shape[0]],
data: arr.iter().copied().collect(),
},
2 => ArrayJson::Array2 {
shape: [shape[0], shape[1]],
data: arr.outer_iter()
.map(|row| row.iter().copied().collect())
.collect(),
},
_ => {
let data = arr.iter().copied().collect();
ArrayJson::ArrayD {
ndim: arr.ndim(),
shape: shape.to_vec(),
data,
}
}
}
}
pub fn to_array_dyn(&self) -> Result<ArrayD<f64>, String> {
match self {
ArrayJson::Array1 { shape, data } => {
Ok(Array::from_vec(data.clone()).into_dyn())
}
ArrayJson::Array2 { shape, data } => {
let flat: Vec<f64> = data.iter().flatten().copied().collect();
Array::from_shape_vec(IxDyn(&[shape[0], shape[1]]), flat)
.map_err(|e| e.to_string())
}
ArrayJson::ArrayD { shape, data, .. } => {
Array::from_shape_vec(IxDyn(shape), data.clone())
.map_err(|e| e.to_string())
}
_ => todo!(),
}
}
}#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArraySliceParams {
pub array: ArrayJson,
pub slices: Vec<SliceInfo>, // Per-axis slice info
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SliceInfo {
pub start: Option<isize>,
pub end: Option<isize>,
pub step: isize,
}
#[elicit_tool(
plugin = "ndarray_indexing",
name = "array_slice",
description = "Slice array along axes",
emit = Auto
)]
async fn array_slice(p: ArraySliceParams) -> Result<CallToolResult, ErrorData> {
let array = p.array.to_array_dyn()?;
// Build slice info for each axis
let sliced = array.slice(s![..;2, 1..5]); // Example
let result = ArrayJson::from_array(&sliced.to_owned());
Ok(CallToolResult::success(json!({ "array": result })))
}
impl CustomEmit<ArraySliceParams> for ArraySliceEmit {
fn emit_code(params: &ArraySliceParams) -> TokenStream {
let array_code = emit_array_literal(¶ms.array);
let slice_args = params.slices.iter().map(emit_slice_arg);
quote! {
{
let arr = #array_code;
arr.slice(s![#(#slice_args),*]).to_owned()
}
}
}
}
fn emit_slice_arg(info: &SliceInfo) -> TokenStream {
match (info.start, info.end, info.step) {
(None, None, 1) => quote! { .. },
(Some(s), None, 1) => quote! { #s.. },
(None, Some(e), 1) => quote! { ..#e },
(Some(s), Some(e), 1) => quote! { #s..#e },
(Some(s), Some(e), step) => quote! { #s..#e; #step },
_ => panic!("Invalid slice info"),
}
}Goal: Element-wise operations and broadcasting semantics.
#[elicit_tool(
plugin = "ndarray_arithmetic",
name = "array_add",
description = "Element-wise addition with broadcasting",
emit = Auto
)]
async fn array_add(p: BinaryOpParams) -> Result<CallToolResult, ErrorData> {
let lhs = p.lhs.to_array_dyn()?;
let rhs = p.rhs.to_array_dyn()?;
// Broadcasting happens automatically via ndarray
let result = &lhs + &rhs;
let result_json = ArrayJson::from_array(&result);
Ok(CallToolResult::success(json!({ "result": result_json })))
}
impl CustomEmit<BinaryOpParams> for ArrayAddEmit {
fn emit_code(params: &BinaryOpParams) -> TokenStream {
let lhs_code = emit_array_literal(¶ms.lhs);
let rhs_code = emit_array_literal(¶ms.rhs);
quote! {
{
let lhs = #lhs_code;
let rhs = #rhs_code;
&lhs + &rhs
}
}
}
}#[elicit_tool(
plugin = "ndarray_broadcasting",
name = "broadcast_to_shape",
description = "Broadcast array to target shape",
emit = Auto
)]
async fn broadcast_to_shape(p: BroadcastParams) -> Result<CallToolResult, ErrorData> {
let array = p.array.to_array_dyn()?;
let target_shape = IxDyn(&p.target_shape);
let broadcasted = array.broadcast(target_shape)
.ok_or_else(|| ErrorData::new("Cannot broadcast to target shape"))?;
let result = ArrayJson::from_array(&broadcasted.to_owned());
Ok(CallToolResult::success(json!({ "result": result })))
}Goal: Reductions, matrix multiplication, norms.
#[elicit_tool(
plugin = "ndarray_aggregation",
name = "array_sum",
description = "Sum all elements",
emit = Auto
)]
async fn array_sum(p: ArraySumParams) -> Result<CallToolResult, ErrorData> {
let array = p.array.to_array_dyn()?;
let sum: f64 = array.sum();
Ok(CallToolResult::success(json!({ "sum": sum })))
}
#[elicit_tool(
plugin = "ndarray_aggregation",
name = "array_sum_axis",
description = "Sum along specific axis",
emit = Auto
)]
async fn array_sum_axis(p: ArraySumAxisParams) -> Result<CallToolResult, ErrorData> {
let array = p.array.to_array_dyn()?;
let result = array.sum_axis(Axis(p.axis));
let result_json = ArrayJson::from_array(&result);
Ok(CallToolResult::success(json!({ "result": result_json })))
}#[elicit_tool(
plugin = "ndarray_linalg",
name = "array_dot",
description = "Matrix/vector dot product",
emit = Auto
)]
async fn array_dot(p: DotParams) -> Result<CallToolResult, ErrorData> {
let lhs = p.lhs.to_array_dyn()?;
let rhs = p.rhs.to_array_dyn()?;
// Use general_mat_mul or dot depending on dimensions
let result = lhs.dot(&rhs);
let result_json = ArrayJson::from_array(&result);
Ok(CallToolResult::success(json!({ "result": result_json })))
}
#[elicit_tool(
plugin = "ndarray_linalg",
name = "array_transpose",
description = "Transpose (reverse axes)",
emit = Auto
)]
async fn array_transpose(p: TransposeParams) -> Result<CallToolResult, ErrorData> {
let array = p.array.to_array_dyn()?;
let result = array.t().to_owned();
let result_json = ArrayJson::from_array(&result);
Ok(CallToolResult::success(json!({ "result": result_json })))
}Goal: Concatenation, reshaping, serialization.
#[elicit_tool(
plugin = "ndarray_manipulation",
name = "array_concatenate",
description = "Concatenate arrays along axis",
emit = Auto
)]
async fn array_concatenate(p: ConcatenateParams) -> Result<CallToolResult, ErrorData> {
let arrays: Vec<ArrayD<f64>> = p.arrays.iter()
.map(|a| a.to_array_dyn())
.collect::<Result<_, _>>()?;
let views: Vec<_> = arrays.iter().map(|a| a.view()).collect();
let result = ndarray::concatenate(Axis(p.axis), &views)
.map_err(|e| ErrorData::new(e.to_string()))?;
let result_json = ArrayJson::from_array(&result);
Ok(CallToolResult::success(json!({ "result": result_json })))
}#[elicit_tool(
plugin = "ndarray_manipulation",
name = "array_reshape",
description = "Reshape array to new dimensions",
emit = Auto
)]
async fn array_reshape(p: ReshapeParams) -> Result<CallToolResult, ErrorData> {
let array = p.array.to_array_dyn()?;
let new_shape = IxDyn(&p.new_shape);
let reshaped = array.into_shape(new_shape)
.map_err(|e| ErrorData::new(e.to_string()))?;
let result_json = ArrayJson::from_array(&reshaped);
Ok(CallToolResult::success(json!({ "result": result_json })))
}Goal: Generate code with generic dimensions and parallel operations.
#[elicit_tool(
plugin = "ndarray_fragments",
name = "emit_array_type",
description = "Emit array type with generic or fixed dimensions",
emit = Auto
)]
async fn emit_array_type(p: EmitArrayTypeParams) -> Result<CallToolResult, ErrorData> {
let code = match p.dim_type.as_str() {
"Array1" => format!("Array1<{}>", p.element_type),
"Array2" => format!("Array2<{}>", p.element_type),
"ArrayD" => format!("ArrayD<{}>", p.element_type),
"generic" => format!("Array<{}, D> where D: Dimension", p.element_type),
_ => return Err(ErrorData::new("Unknown dimension type")),
};
Ok(CallToolResult::success(Content::text(code)))
}
#[elicit_tool(
plugin = "ndarray_fragments",
name = "emit_function_generic_dim",
description = "Emit function with Dimension bound",
emit = Auto
)]
async fn emit_function_generic_dim(p: EmitGenericDimParams) -> Result<CallToolResult, ErrorData> {
let code = format!(
r#"fn {}<D: Dimension>(arr: Array<f64, D>) -> Array<f64, D> {{
{}
}}"#,
p.function_name,
p.body
);
Ok(CallToolResult::success(Content::text(code)))
}#[elicit_tool(
plugin = "ndarray_fragments",
name = "emit_par_map_inplace",
description = "Emit parallel element-wise modification code",
emit = Auto
)]
async fn emit_par_map_inplace(p: EmitParMapParams) -> Result<CallToolResult, ErrorData> {
let code = format!(
r#"arr.par_map_inplace(|x| {});"#,
p.closure_body
);
Ok(CallToolResult::success(Content::text(code)))
}
#[elicit_tool(
plugin = "ndarray_fragments",
name = "emit_par_azip",
description = "Emit parallel azip! macro invocation",
emit = Auto
)]
async fn emit_par_azip(p: EmitParAzipParams) -> Result<CallToolResult, ErrorData> {
let bindings = p.bindings.iter()
.map(|b| format!("{} {}", b.mode, b.name))
.collect::<Vec<_>>()
.join(", ");
let code = format!(
r#"par_azip!(({}) {});"#,
bindings,
p.body
);
Ok(CallToolResult::success(Content::text(code)))
}#[elicit_tool(
plugin = "ndarray_fragments",
name = "assemble_ndarray_binary",
description = "Generate complete executable with ndarray computations",
emit = Auto
)]
async fn assemble_ndarray_binary(p: AssembleParams) -> Result<CallToolResult, ErrorData> {
let cargo_toml = generate_cargo_toml(&p);
let main_rs = generate_main_with_arrays(&p);
Ok(CallToolResult::success(json!({
"Cargo.toml": cargo_toml,
"src/main.rs": main_rs,
"description": "Complete ndarray binary project"
})))
}
fn generate_main_with_arrays(p: &AssembleParams) -> String {
let features = if p.use_rayon {
r#"ndarray = { version = "0.17", features = ["rayon", "serde"] }"#
} else {
r#"ndarray = { version = "0.17", features = ["serde"] }"#
};
format!(
r#"use ndarray::{{Array, Array1, Array2, ArrayD, IxDyn, Axis, s}};
{}
fn main() {{
{}
}}
{}
"#,
if p.use_rayon { "use ndarray::parallel::prelude::*;" } else { "" },
p.main_body,
p.helper_functions.join("\n\n")
)
}Goal: Persistent array handles for stateful workflows.
pub struct NdarrayPlugin {
arrays: Arc<Mutex<HashMap<Uuid, ArcArray<f64, IxDyn>>>>,
views: Arc<Mutex<HashMap<Uuid, ViewHandle>>>,
iterators: Arc<Mutex<HashMap<Uuid, IteratorHandle>>>,
}
#[elicit_tool(
plugin = "ndarray_handles",
name = "array_create_handle",
description = "Create persistent array handle (ArcArray for sharing)"
)]
async fn array_create_handle(p: ArrayCreateParams) -> Result<CallToolResult, ErrorData> {
let array = p.array.to_array_dyn()?;
let arc_array = array.into_shared();
let id = Uuid::new_v4();
let plugin = get_plugin();
plugin.arrays.lock().unwrap().insert(id, arc_array);
Ok(CallToolResult::success(json!({ "array_id": id })))
}
#[elicit_tool(
plugin = "ndarray_handles",
name = "array_get_handle",
description = "Retrieve array by handle"
)]
async fn array_get_handle(p: ArrayGetParams) -> Result<CallToolResult, ErrorData> {
let plugin = get_plugin();
let arrays = plugin.arrays.lock().unwrap();
let array = arrays.get(&p.array_id)
.ok_or_else(|| ErrorData::new("Array not found"))?;
let result = ArrayJson::from_array(&array.view());
Ok(CallToolResult::success(json!({ "array": result })))
}
#[elicit_tool(
plugin = "ndarray_handles",
name = "array_compose_handles",
description = "Perform operation on two array handles, store result"
)]
async fn array_compose_handles(p: ArrayComposeParams) -> Result<CallToolResult, ErrorData> {
let plugin = get_plugin();
let arrays = plugin.arrays.lock().unwrap();
let lhs = arrays.get(&p.lhs_id).ok_or_else(|| ErrorData::new("LHS not found"))?;
let rhs = arrays.get(&p.rhs_id).ok_or_else(|| ErrorData::new("RHS not found"))?;
let result = match p.operation.as_str() {
"add" => (lhs.view() + rhs.view()).into_shared(),
"mul" => (lhs.view() * rhs.view()).into_shared(),
"dot" => lhs.dot(&rhs.view()).into_shared(),
_ => return Err(ErrorData::new("Unknown operation")),
};
let result_id = Uuid::new_v4();
drop(arrays);
plugin.arrays.lock().unwrap().insert(result_id, result);
Ok(CallToolResult::success(json!({ "result_id": result_id })))
}pub enum ViewHandle {
Slice { parent_id: Uuid, slice_info: Vec<SliceInfo> },
Transpose { parent_id: Uuid },
Reshape { parent_id: Uuid, shape: Vec<usize> },
}
#[elicit_tool(
plugin = "ndarray_handles",
name = "view_create_slice",
description = "Create zero-copy slice view of array"
)]
async fn view_create_slice(p: ViewSliceParams) -> Result<CallToolResult, ErrorData> {
let plugin = get_plugin();
let arrays = plugin.arrays.lock().unwrap();
// Verify parent exists
arrays.get(&p.parent_id)
.ok_or_else(|| ErrorData::new("Parent array not found"))?;
let view_handle = ViewHandle::Slice {
parent_id: p.parent_id,
slice_info: p.slices,
};
let view_id = Uuid::new_v4();
drop(arrays);
plugin.views.lock().unwrap().insert(view_id, view_handle);
Ok(CallToolResult::success(json!({ "view_id": view_id })))
}ndarray-linalg is a separate crate providing advanced linear algebra (LAPACK/OpenBLAS bindings):
- SVD, QR, LU, Cholesky decompositions
- Eigenvalue/eigenvector computations
- Matrix inverse, determinant, rank
Strategy:
- elicit_ndarray focuses on core array operations (this plan)
- elicit_ndarray_linalg would be a separate shadow crate (future plan)
- Reference ndarray-linalg in Phase 3 tools, but defer advanced decompositions
Example reference tool:
#[elicit_tool(
plugin = "ndarray_linalg",
name = "array_svd",
description = "Compute SVD (requires elicit_ndarray_linalg plugin)"
)]
async fn array_svd(p: SvdParams) -> Result<CallToolResult, ErrorData> {
Err(ErrorData::new(
"SVD requires elicit_ndarray_linalg plugin. \
Use `nalgebra` or install `elicit_ndarray_linalg` for advanced linear algebra."
))
}- Phase 1a — Crate scaffold:
Cargo.toml,lib.rs,serde_types.rs - Phase 1b — Creation dual-mode tools:
creation.rs(60 tools) - Phase 1c — Indexing dual-mode tools:
indexing.rs(50 tools) - Phase 1d —
just check elicit_ndarray; fix compilation - Phase 2a — Arithmetic dual-mode tools: element-wise ops (50 tools)
- Phase 2b — Broadcasting dual-mode tools: (30 tools)
- Phase 2c —
just check elicit_ndarray - Phase 3a — Aggregation dual-mode tools: (40 tools)
- Phase 3b — Linear algebra dual-mode tools: (40 tools)
- Phase 3c —
just check elicit_ndarray - Phase 4a — Manipulation dual-mode tools: (60 tools)
- Phase 4b — I/O dual-mode tools: (30 tools)
- Phase 4c —
just check elicit_ndarray - Phase 5a — Fragment tools: generic dimensions (30 tools)
- Phase 5b — Fragment tools: parallel ops (20 tools)
- Phase 5c — Fragment tools: assembly (15 tools)
- Phase 5d —
just check elicit_ndarray - Phase 6a — UUID handles: ArrayRegistry, ViewRegistry (40 tools)
- Phase 6b —
just check elicit_ndarray - Phase 7 — Wire into
elicit_serveremit chain
| Category | Count | Implementation Strategy |
|---|---|---|
| Dual-Mode Creation | 60 | emit = Auto + CustomEmit |
| Dual-Mode Indexing/Slicing | 50 | emit = Auto + CustomEmit |
| Dual-Mode Arithmetic | 50 | emit = Auto + CustomEmit |
| Dual-Mode Broadcasting | 30 | emit = Auto + CustomEmit |
| Dual-Mode Aggregations | 40 | emit = Auto + CustomEmit |
| Dual-Mode Linear Algebra | 40 | emit = Auto + CustomEmit |
| Dual-Mode Manipulation | 60 | emit = Auto + CustomEmit |
| Dual-Mode I/O | 30 | emit = Auto + CustomEmit |
| Fragment Generic Dims | 30 | Code generation only |
| Fragment Parallel Ops | 20 | Code generation only |
| Fragment Assembly | 15 | Code generation only |
| Fragment Broadcasting | 15 | Code generation only |
| Runtime Handles | 40 | UUID registries |
| Total | 520 |
- NumPy Familiarity: Python users recognize the API immediately
- Natural Serialization: N-D arrays → nested JSON, shape metadata
- Dual-Mode Dominance: 400/520 tools (77%) are dual-mode
- Broadcasting Support: Element-wise ops automatically broadcast (like NumPy)
- Zero-Copy Views: UUID handles enable efficient slicing workflows
- Parallel Ready: Fragment tools generate rayon parallel code
- Generic Dimensions: Support both static (Ix2) and dynamic (IxDyn) arrays
- Wide Ecosystem: Foundation for ndarray-linalg, ndarray-stats, ndarray-rand
| Aspect | nalgebra | ndarray |
|---|---|---|
| Focus | Linear algebra + geometry | General N-D arrays |
| Use case | Graphics, physics, robotics | Scientific computing, data analysis |
| Dimensions | 2D matrices primary | N-D arrays (up to 6+ dimensions) |
| Special types | Rotations, quaternions, transforms | Slicing, broadcasting, parallel ops |
| Tool count | 480 tools | 520 tools |
| Dual-mode % | 73% | 77% |
| Key feature | Geometric types + decompositions | Broadcasting + NumPy compatibility |
Both are "straightforward" because:
- Natural JSON serialization
- Synchronous operations
- Concrete methods (not trait-heavy)
- Clear API taxonomy