mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-08-13 08:40:35 +00:00
feat(runtime): load MCP tools list from Lean manifest JSON (shim-only)
This commit is contained in:
parent
dcd3373874
commit
466e9b68ea
1 changed files with 119 additions and 0 deletions
|
|
@ -0,0 +1,119 @@
|
||||||
|
//! Lean-first MCP manifest loader.
|
||||||
|
//!
|
||||||
|
//! This is a boundary shim: it loads the published MCP tool list from a Lean-owned
|
||||||
|
//! JSON manifest (generated by `Semantics.McpSurfaceManifest`).
|
||||||
|
//!
|
||||||
|
//! Semantics are NOT defined here: this module only parses JSON into runtime
|
||||||
|
//! `McpTool` descriptors.
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
|
use crate::mcp_stdio::McpTool;
|
||||||
|
|
||||||
|
/// Load tool descriptors from a JSON value shaped like `{ "tools": [...] }`.
|
||||||
|
pub fn tools_from_json(value: &JsonValue) -> Result<Vec<McpTool>, String> {
|
||||||
|
let tools = value
|
||||||
|
.get("tools")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.ok_or_else(|| "Lean MCP manifest missing 'tools' array".to_string())?;
|
||||||
|
|
||||||
|
let mut parsed = Vec::with_capacity(tools.len());
|
||||||
|
for tool in tools {
|
||||||
|
let name = tool
|
||||||
|
.get("name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| "Lean MCP manifest tool missing 'name'".to_string())?
|
||||||
|
.to_string();
|
||||||
|
let description = tool
|
||||||
|
.get("description")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
let input_schema = tool.get("inputSchema").cloned();
|
||||||
|
|
||||||
|
parsed.push(McpTool {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
input_schema,
|
||||||
|
annotations: None,
|
||||||
|
meta: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run an external command that prints the Lean-owned tools list JSON to stdout.
|
||||||
|
///
|
||||||
|
/// The command is provided as a vector to avoid shell injection.
|
||||||
|
pub fn tools_from_lean_command(command: &[String]) -> Result<Vec<McpTool>, String> {
|
||||||
|
if command.is_empty() {
|
||||||
|
return Err("Lean MCP manifest command is empty".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = Command::new(&command[0]);
|
||||||
|
if command.len() > 1 {
|
||||||
|
cmd.args(&command[1..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = cmd
|
||||||
|
.output()
|
||||||
|
.map_err(|error| format!("failed to spawn Lean MCP manifest command: {error}"))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"Lean MCP manifest command failed (status={}): {}",
|
||||||
|
output.status,
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json: JsonValue = serde_json::from_slice(&output.stdout)
|
||||||
|
.map_err(|error| format!("failed to parse Lean MCP manifest JSON: {error}"))?;
|
||||||
|
|
||||||
|
tools_from_json(&json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the Lean manifest using environment variables.
|
||||||
|
///
|
||||||
|
/// - `CLAW_LEAN_MCP_MANIFEST_CMD`: a JSON array of strings, e.g.
|
||||||
|
/// `["bash","-lc","cd 0-Core-Formalism/otom/tools/lean/Semantics && lake env lean -R Semantics -E 'import Semantics.McpSurfaceManifest; open Semantics.McpSurfaceManifest; #eval instanceToolsJson'"]`
|
||||||
|
///
|
||||||
|
/// This stays shim-only: the env var provides *how* to invoke Lean; the manifest
|
||||||
|
/// itself is the tool truth.
|
||||||
|
pub fn tools_from_env() -> Result<Option<Vec<McpTool>>, String> {
|
||||||
|
let cmd = std::env::var("CLAW_LEAN_MCP_MANIFEST_CMD").ok();
|
||||||
|
let Some(cmd) = cmd else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let command: Vec<String> = serde_json::from_str(&cmd)
|
||||||
|
.map_err(|error| format!("CLAW_LEAN_MCP_MANIFEST_CMD must be JSON array: {error}"))?;
|
||||||
|
|
||||||
|
Ok(Some(tools_from_lean_command(&command)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_tools_from_lean_json_shape() {
|
||||||
|
let manifest = json!({
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "connector_health",
|
||||||
|
"description": "Return connector manifest + readiness state.",
|
||||||
|
"inputSchema": {"type": "object", "properties": {}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let tools = tools_from_json(&manifest).expect("tools parse");
|
||||||
|
assert_eq!(tools.len(), 1);
|
||||||
|
assert_eq!(tools[0].name, "connector_health");
|
||||||
|
assert!(tools[0].input_schema.is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue