swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates/fidget-spinner-cli/src/mcp
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fidget-spinner-cli/src/mcp')
-rw-r--r--crates/fidget-spinner-cli/src/mcp/output.rs15
-rw-r--r--crates/fidget-spinner-cli/src/mcp/projection.rs216
-rw-r--r--crates/fidget-spinner-cli/src/mcp/service.rs139
3 files changed, 285 insertions, 85 deletions
diff --git a/crates/fidget-spinner-cli/src/mcp/output.rs b/crates/fidget-spinner-cli/src/mcp/output.rs
index 2e11e20..494fe23 100644
--- a/crates/fidget-spinner-cli/src/mcp/output.rs
+++ b/crates/fidget-spinner-cli/src/mcp/output.rs
@@ -118,21 +118,6 @@ pub(crate) fn projected_tool_output(
))
}
-pub(crate) fn fallback_tool_output(
- concise: &impl Serialize,
- full: &impl Serialize,
- kind: SurfaceKind,
- stage: FaultStage,
- operation: &str,
-) -> Result<ToolOutput, FaultRecord> {
- let projection = FallbackJsonProjection::new(concise, full, kind)
- .map_err(|error| projection_fault(error, stage, operation))?;
- let concise_text = projection
- .porcelain_projection(DetailLevel::Concise)
- .map_err(|error| projection_fault(error, stage, operation))?;
- projected_tool_output(&projection, concise_text, None, stage, operation)
-}
-
pub(crate) fn fallback_detailed_tool_output(
concise: &impl Serialize,
full: &impl Serialize,
diff --git a/crates/fidget-spinner-cli/src/mcp/projection.rs b/crates/fidget-spinner-cli/src/mcp/projection.rs
index ca89af0..a36e915 100644
--- a/crates/fidget-spinner-cli/src/mcp/projection.rs
+++ b/crates/fidget-spinner-cli/src/mcp/projection.rs
@@ -2,15 +2,18 @@ use std::collections::BTreeMap;
use fidget_spinner_core::{
AttachmentTargetRef, CommandRecipe, ExperimentAnalysis, ExperimentOutcome, FrontierBrief,
- FrontierRecord, MetricValue, NonEmptyText, RunDimensionValue,
+ FrontierRecord, MetricDefinition, MetricValue, NonEmptyText, RunDimensionDefinition,
+ RunDimensionValue, TagRecord,
};
use fidget_spinner_store_sqlite::{
- ArtifactDetail, ArtifactSummary, ExperimentDetail, ExperimentSummary, FrontierOpenProjection,
- FrontierSummary, HypothesisCurrentState, HypothesisDetail, MetricBestEntry, MetricKeySummary,
- MetricObservationSummary, ProjectStore, StoreError, VertexSummary,
+ ArtifactDetail, ArtifactSummary, EntityHistoryEntry, ExperimentDetail, ExperimentSummary,
+ FrontierOpenProjection, FrontierSummary, HypothesisCurrentState, HypothesisDetail,
+ MetricBestEntry, MetricKeySummary, MetricObservationSummary, ProjectStore, StoreError,
+ VertexSummary,
};
use libmcp::{
ProjectionError, SelectorProjection, StructuredProjection, SurfaceKind, SurfacePolicy,
+ TimestampText,
};
use serde::Serialize;
use serde_json::Value;
@@ -44,7 +47,7 @@ pub(crate) struct FrontierSummaryProjection {
pub(crate) status: String,
pub(crate) active_hypothesis_count: u64,
pub(crate) open_experiment_count: u64,
- pub(crate) updated_at: String,
+ pub(crate) updated_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -55,7 +58,7 @@ pub(crate) struct FrontierBriefProjection {
pub(crate) unknowns: Vec<String>,
pub(crate) revision: u64,
#[serde(skip_serializing_if = "Option::is_none")]
- pub(crate) updated_at: Option<String>,
+ pub(crate) updated_at: Option<TimestampText>,
}
#[derive(Clone, Serialize)]
@@ -80,8 +83,8 @@ pub(crate) struct FrontierRecordProjection {
pub(crate) objective: String,
pub(crate) status: String,
pub(crate) revision: u64,
- pub(crate) created_at: String,
- pub(crate) updated_at: String,
+ pub(crate) created_at: TimestampText,
+ pub(crate) updated_at: TimestampText,
pub(crate) brief: FrontierBriefProjection,
}
@@ -127,7 +130,7 @@ pub(crate) struct HypothesisSummaryProjection {
pub(crate) open_experiment_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) latest_verdict: Option<String>,
- pub(crate) updated_at: String,
+ pub(crate) updated_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -139,8 +142,8 @@ pub(crate) struct HypothesisRecordProjection {
pub(crate) body: String,
pub(crate) tags: Vec<String>,
pub(crate) revision: u64,
- pub(crate) created_at: String,
- pub(crate) updated_at: String,
+ pub(crate) created_at: TimestampText,
+ pub(crate) updated_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -151,7 +154,7 @@ pub(crate) struct HypothesisReadRecordProjection {
pub(crate) summary: String,
pub(crate) tags: Vec<String>,
pub(crate) revision: u64,
- pub(crate) updated_at: String,
+ pub(crate) updated_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -229,9 +232,9 @@ pub(crate) struct ExperimentSummaryProjection {
pub(crate) verdict: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) primary_metric: Option<MetricObservationSummaryProjection>,
- pub(crate) updated_at: String,
+ pub(crate) updated_at: TimestampText,
#[serde(skip_serializing_if = "Option::is_none")]
- pub(crate) closed_at: Option<String>,
+ pub(crate) closed_at: Option<TimestampText>,
}
#[derive(Clone, Serialize)]
@@ -246,8 +249,8 @@ pub(crate) struct ExperimentRecordProjection {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) outcome: Option<ExperimentOutcomeProjection>,
pub(crate) revision: u64,
- pub(crate) created_at: String,
- pub(crate) updated_at: String,
+ pub(crate) created_at: TimestampText,
+ pub(crate) updated_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -262,7 +265,7 @@ pub(crate) struct ExperimentReadRecordProjection {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) verdict: Option<String>,
pub(crate) revision: u64,
- pub(crate) updated_at: String,
+ pub(crate) updated_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -329,7 +332,7 @@ pub(crate) struct ArtifactSummaryProjection {
pub(crate) locator: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) media_type: Option<String>,
- pub(crate) updated_at: String,
+ pub(crate) updated_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -343,8 +346,8 @@ pub(crate) struct ArtifactRecordProjection {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) media_type: Option<String>,
pub(crate) revision: u64,
- pub(crate) created_at: String,
- pub(crate) updated_at: String,
+ pub(crate) created_at: TimestampText,
+ pub(crate) updated_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -358,7 +361,7 @@ pub(crate) struct ArtifactReadRecordProjection {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) media_type: Option<String>,
pub(crate) revision: u64,
- pub(crate) updated_at: String,
+ pub(crate) updated_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -452,7 +455,7 @@ pub(crate) struct ExperimentOutcomeProjection {
pub(crate) rationale: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) analysis: Option<ExperimentAnalysisProjection>,
- pub(crate) closed_at: String,
+ pub(crate) closed_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -484,7 +487,7 @@ pub(crate) struct VertexSummaryProjection {
pub(crate) title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) summary: Option<String>,
- pub(crate) updated_at: String,
+ pub(crate) updated_at: TimestampText,
}
#[derive(Clone, Serialize)]
@@ -515,6 +518,82 @@ pub(crate) struct MetricBestOutput {
pub(crate) entries: Vec<MetricBestEntryProjection>,
}
+#[derive(Clone, Serialize)]
+pub(crate) struct TagRecordProjection {
+ pub(crate) name: String,
+ pub(crate) description: String,
+ pub(crate) created_at: TimestampText,
+}
+
+#[derive(Clone, Serialize, libmcp::ToolProjection)]
+#[libmcp(kind = "mutation")]
+pub(crate) struct TagRecordOutput {
+ pub(crate) record: TagRecordProjection,
+}
+
+#[derive(Clone, Serialize, libmcp::ToolProjection)]
+#[libmcp(kind = "list")]
+pub(crate) struct TagListOutput {
+ pub(crate) count: usize,
+ pub(crate) tags: Vec<TagRecordProjection>,
+}
+
+#[derive(Clone, Serialize)]
+pub(crate) struct MetricDefinitionProjection {
+ pub(crate) key: String,
+ pub(crate) unit: String,
+ pub(crate) objective: String,
+ pub(crate) visibility: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) description: Option<String>,
+ pub(crate) created_at: TimestampText,
+ pub(crate) updated_at: TimestampText,
+}
+
+#[derive(Clone, Serialize, libmcp::ToolProjection)]
+#[libmcp(kind = "mutation")]
+pub(crate) struct MetricDefinitionOutput {
+ pub(crate) record: MetricDefinitionProjection,
+}
+
+#[derive(Clone, Serialize)]
+pub(crate) struct RunDimensionDefinitionProjection {
+ pub(crate) key: String,
+ pub(crate) value_type: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) description: Option<String>,
+ pub(crate) created_at: TimestampText,
+ pub(crate) updated_at: TimestampText,
+}
+
+#[derive(Clone, Serialize, libmcp::ToolProjection)]
+#[libmcp(kind = "mutation")]
+pub(crate) struct RunDimensionDefinitionOutput {
+ pub(crate) record: RunDimensionDefinitionProjection,
+}
+
+#[derive(Clone, Serialize, libmcp::ToolProjection)]
+#[libmcp(kind = "list")]
+pub(crate) struct RunDimensionListOutput {
+ pub(crate) count: usize,
+ pub(crate) dimensions: Vec<RunDimensionDefinitionProjection>,
+}
+
+#[derive(Clone, Serialize)]
+pub(crate) struct HistoryEntryProjection {
+ pub(crate) revision: u64,
+ pub(crate) event_kind: String,
+ pub(crate) occurred_at: TimestampText,
+ pub(crate) snapshot: Value,
+}
+
+#[derive(Clone, Serialize, libmcp::ToolProjection)]
+#[libmcp(kind = "list")]
+pub(crate) struct HistoryOutput {
+ pub(crate) count: usize,
+ pub(crate) history: Vec<HistoryEntryProjection>,
+}
+
pub(crate) fn frontier_list(frontiers: &[FrontierSummary]) -> FrontierListOutput {
FrontierListOutput {
count: frontiers.len(),
@@ -784,6 +863,50 @@ pub(crate) fn metric_best(entries: &[MetricBestEntry]) -> MetricBestOutput {
}
}
+pub(crate) fn tag_record(tag: &TagRecord) -> TagRecordOutput {
+ TagRecordOutput {
+ record: tag_record_projection(tag),
+ }
+}
+
+pub(crate) fn tag_list(tags: &[TagRecord]) -> TagListOutput {
+ TagListOutput {
+ count: tags.len(),
+ tags: tags.iter().map(tag_record_projection).collect(),
+ }
+}
+
+pub(crate) fn metric_definition(metric: &MetricDefinition) -> MetricDefinitionOutput {
+ MetricDefinitionOutput {
+ record: metric_definition_projection(metric),
+ }
+}
+
+pub(crate) fn run_dimension_definition(
+ dimension: &RunDimensionDefinition,
+) -> RunDimensionDefinitionOutput {
+ RunDimensionDefinitionOutput {
+ record: run_dimension_definition_projection(dimension),
+ }
+}
+
+pub(crate) fn run_dimension_list(dimensions: &[RunDimensionDefinition]) -> RunDimensionListOutput {
+ RunDimensionListOutput {
+ count: dimensions.len(),
+ dimensions: dimensions
+ .iter()
+ .map(run_dimension_definition_projection)
+ .collect(),
+ }
+}
+
+pub(crate) fn history(history: &[EntityHistoryEntry]) -> HistoryOutput {
+ HistoryOutput {
+ count: history.len(),
+ history: history.iter().map(history_entry_projection).collect(),
+ }
+}
+
fn frontier_summary(frontier: &FrontierSummary) -> FrontierSummaryProjection {
FrontierSummaryProjection {
slug: frontier.slug.to_string(),
@@ -969,6 +1092,47 @@ fn metric_key_summary(metric: &MetricKeySummary) -> MetricKeySummaryProjection {
}
}
+fn tag_record_projection(tag: &TagRecord) -> TagRecordProjection {
+ TagRecordProjection {
+ name: tag.name.to_string(),
+ description: tag.description.to_string(),
+ created_at: timestamp_value(tag.created_at),
+ }
+}
+
+fn metric_definition_projection(metric: &MetricDefinition) -> MetricDefinitionProjection {
+ MetricDefinitionProjection {
+ key: metric.key.to_string(),
+ unit: metric.unit.as_str().to_owned(),
+ objective: metric.objective.as_str().to_owned(),
+ visibility: metric.visibility.as_str().to_owned(),
+ description: metric.description.as_ref().map(ToString::to_string),
+ created_at: timestamp_value(metric.created_at),
+ updated_at: timestamp_value(metric.updated_at),
+ }
+}
+
+fn run_dimension_definition_projection(
+ dimension: &RunDimensionDefinition,
+) -> RunDimensionDefinitionProjection {
+ RunDimensionDefinitionProjection {
+ key: dimension.key.to_string(),
+ value_type: dimension.value_type.as_str().to_owned(),
+ description: dimension.description.as_ref().map(ToString::to_string),
+ created_at: timestamp_value(dimension.created_at),
+ updated_at: timestamp_value(dimension.updated_at),
+ }
+}
+
+fn history_entry_projection(entry: &EntityHistoryEntry) -> HistoryEntryProjection {
+ HistoryEntryProjection {
+ revision: entry.revision,
+ event_kind: entry.event_kind.to_string(),
+ occurred_at: timestamp_value(entry.occurred_at),
+ snapshot: entry.snapshot.clone(),
+ }
+}
+
fn metric_best_entry(entry: &MetricBestEntry) -> MetricBestEntryProjection {
MetricBestEntryProjection {
experiment: experiment_summary(&entry.experiment),
@@ -1127,10 +1291,8 @@ fn attachment_target(
}
}
-fn timestamp_value(timestamp: time::OffsetDateTime) -> String {
- timestamp
- .format(&time::format_description::well_known::Rfc3339)
- .unwrap_or_else(|_| timestamp.unix_timestamp().to_string())
+fn timestamp_value(timestamp: time::OffsetDateTime) -> TimestampText {
+ TimestampText::from(timestamp)
}
fn store_fault(operation: &str) -> impl Fn(StoreError) -> FaultRecord + '_ {
diff --git a/crates/fidget-spinner-cli/src/mcp/service.rs b/crates/fidget-spinner-cli/src/mcp/service.rs
index 3ce68ae..7c649aa 100644
--- a/crates/fidget-spinner-cli/src/mcp/service.rs
+++ b/crates/fidget-spinner-cli/src/mcp/service.rs
@@ -26,8 +26,8 @@ use serde_json::{Map, Value, json};
use crate::mcp::fault::{FaultKind, FaultRecord, FaultStage};
use crate::mcp::output::{
- ToolOutput, fallback_detailed_tool_output, fallback_tool_output, projected_tool_output,
- split_presentation, tool_success,
+ ToolOutput, fallback_detailed_tool_output, projected_tool_output, split_presentation,
+ tool_success,
};
use crate::mcp::projection;
use crate::mcp::protocol::{TRANSIENT_ONCE_ENV, TRANSIENT_ONCE_MARKER_ENV, WorkerOperation};
@@ -103,13 +103,7 @@ impl WorkerService {
TagName::new(args.name).map_err(store_fault(&operation))?,
NonEmptyText::new(args.description).map_err(store_fault(&operation))?,
));
- fallback_tool_output(
- &tag,
- &tag,
- libmcp::SurfaceKind::Mutation,
- FaultStage::Worker,
- &operation,
- )?
+ tag_record_output(&tag, &operation)?
}
"tag.list" => tag_list_output(&lift!(self.store.list_tags()), &operation)?,
"frontier.create" => {
@@ -471,13 +465,7 @@ impl WorkerService {
.map_err(store_fault(&operation))?,
})
);
- fallback_tool_output(
- &metric,
- &metric,
- libmcp::SurfaceKind::Mutation,
- FaultStage::Worker,
- &operation,
- )?
+ metric_definition_output(&metric, &operation)?
}
"metric.keys" => {
let args = deserialize::<MetricKeysArgs>(arguments)?;
@@ -517,23 +505,11 @@ impl WorkerService {
.map_err(store_fault(&operation))?,
})
);
- fallback_tool_output(
- &dimension,
- &dimension,
- libmcp::SurfaceKind::Mutation,
- FaultStage::Worker,
- &operation,
- )?
+ run_dimension_definition_output(&dimension, &operation)?
}
"run.dimension.list" => {
let dimensions = lift!(self.store.list_run_dimensions());
- fallback_tool_output(
- &dimensions,
- &dimensions,
- libmcp::SurfaceKind::List,
- FaultStage::Worker,
- &operation,
- )?
+ run_dimension_list_output(&dimensions, &operation)?
}
other => {
return Err(FaultRecord::new(
@@ -1064,17 +1040,27 @@ fn project_status_output(
)
}
+fn tag_record_output(
+ tag: &fidget_spinner_core::TagRecord,
+ operation: &str,
+) -> Result<ToolOutput, FaultRecord> {
+ let projection = projection::tag_record(tag);
+ projected_tool_output(
+ &projection,
+ format!("tag {} — {}", tag.name, tag.description),
+ None,
+ FaultStage::Worker,
+ operation,
+ )
+}
+
fn tag_list_output(
tags: &[fidget_spinner_core::TagRecord],
operation: &str,
) -> Result<ToolOutput, FaultRecord> {
- let concise = json!({
- "count": tags.len(),
- "tags": tags,
- });
- fallback_detailed_tool_output(
- &concise,
- &concise,
+ let projection = projection::tag_list(tags);
+ projected_tool_output(
+ &projection,
if tags.is_empty() {
"no tags".to_owned()
} else {
@@ -1084,7 +1070,6 @@ fn tag_list_output(
.join("\n")
},
None,
- libmcp::SurfaceKind::List,
FaultStage::Worker,
operation,
)
@@ -1527,6 +1512,26 @@ fn metric_keys_output(
)
}
+fn metric_definition_output(
+ metric: &fidget_spinner_core::MetricDefinition,
+ operation: &str,
+) -> Result<ToolOutput, FaultRecord> {
+ let projection = projection::metric_definition(metric);
+ projected_tool_output(
+ &projection,
+ format!(
+ "metric {} [{} {} {}]",
+ metric.key,
+ metric.unit.as_str(),
+ metric.objective.as_str(),
+ metric.visibility.as_str()
+ ),
+ None,
+ FaultStage::Worker,
+ operation,
+ )
+}
+
fn metric_best_output(
entries: &[MetricBestEntry],
operation: &str,
@@ -1562,14 +1567,63 @@ fn metric_best_output(
)
}
+fn run_dimension_definition_output(
+ dimension: &fidget_spinner_core::RunDimensionDefinition,
+ operation: &str,
+) -> Result<ToolOutput, FaultRecord> {
+ let projection = projection::run_dimension_definition(dimension);
+ projected_tool_output(
+ &projection,
+ format!(
+ "dimension {} [{}]",
+ dimension.key,
+ dimension.value_type.as_str()
+ ),
+ None,
+ FaultStage::Worker,
+ operation,
+ )
+}
+
+fn run_dimension_list_output(
+ dimensions: &[fidget_spinner_core::RunDimensionDefinition],
+ operation: &str,
+) -> Result<ToolOutput, FaultRecord> {
+ let projection = projection::run_dimension_list(dimensions);
+ projected_tool_output(
+ &projection,
+ if dimensions.is_empty() {
+ "no run dimensions".to_owned()
+ } else {
+ dimensions
+ .iter()
+ .map(|dimension| {
+ format!(
+ "{} [{}]{}",
+ dimension.key,
+ dimension.value_type.as_str(),
+ dimension
+ .description
+ .as_ref()
+ .map_or_else(String::new, |description| format!(" — {description}"))
+ )
+ })
+ .collect::<Vec<_>>()
+ .join("\n")
+ },
+ None,
+ FaultStage::Worker,
+ operation,
+ )
+}
+
fn history_output(
history: &[EntityHistoryEntry],
operation: &str,
) -> Result<ToolOutput, FaultRecord> {
- let concise = json!({ "count": history.len(), "history": history });
- fallback_detailed_tool_output(
- &concise,
- &concise,
+ let projection = projection::history(history);
+ projected_tool_output(
+ &projection,
if history.is_empty() {
"no history".to_owned()
} else {
@@ -1585,7 +1639,6 @@ fn history_output(
.join("\n")
},
None,
- libmcp::SurfaceKind::List,
FaultStage::Worker,
operation,
)