Create mcp_server.rs
This commit is contained in:
@@ -0,0 +1,159 @@
|
|||||||
|
use rmcp::{
|
||||||
|
ServerHandler,
|
||||||
|
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||||
|
model::{ServerCapabilities, ServerInfo},
|
||||||
|
schemars, tool, tool_handler, tool_router,
|
||||||
|
transport::stdio,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::ast::{environment::Environment, rtl};
|
||||||
|
|
||||||
|
const LANGUAGE_SPEC: &str = include_str!("../../docs/BNF.md");
|
||||||
|
|
||||||
|
/// Input struct for tools that take a Myc source code string.
|
||||||
|
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||||
|
pub struct CodeInput {
|
||||||
|
/// The Myc source code to process.
|
||||||
|
pub code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MCP server exposing the Myc compiler as a set of tools for LLMs.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MycMcpServer {
|
||||||
|
// Used by the generated `ServerHandler` impl from `#[tool_router]`.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
tool_router: ToolRouter<Self>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MycMcpServer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tool_router: Self::tool_router(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a fresh, fully initialized Myc environment.
|
||||||
|
/// A new environment is created per tool call because `Environment` uses
|
||||||
|
/// `Rc<RefCell<...>>` internally and is intentionally single-threaded.
|
||||||
|
fn make_env() -> Environment {
|
||||||
|
let mut env = Environment::new();
|
||||||
|
env.optimization = true;
|
||||||
|
env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MycMcpServer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool_router]
|
||||||
|
impl MycMcpServer {
|
||||||
|
/// Evaluates a Myc expression and returns the result as a string.
|
||||||
|
#[tool(description = "Evaluate a Myc expression or script and return the result. \
|
||||||
|
The Myc language is a Lisp-like DSL for financial analysis with first-class ASTs, \
|
||||||
|
closures, and series (time-series queues). \
|
||||||
|
Use get_language_spec to learn the syntax first.")]
|
||||||
|
fn eval_myc(&self, Parameters(CodeInput { code }): Parameters<CodeInput>) -> String {
|
||||||
|
let env = Self::make_env();
|
||||||
|
match env.run_script(&code) {
|
||||||
|
Ok(value) => format!("{}", value),
|
||||||
|
Err(e) => format!("Error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dumps the compiled and optimized AST for a Myc expression.
|
||||||
|
#[tool(description = "Compile a Myc expression and return a human-readable dump of \
|
||||||
|
the compiled AST. Useful for understanding how the compiler transforms code \
|
||||||
|
through its optimization and lowering passes.")]
|
||||||
|
fn dump_ast(&self, Parameters(CodeInput { code }): Parameters<CodeInput>) -> String {
|
||||||
|
let env = Self::make_env();
|
||||||
|
match env.dump_ast(&code) {
|
||||||
|
Ok(dump) => dump,
|
||||||
|
Err(e) => format!("Error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the syntax and types of a Myc expression without executing it.
|
||||||
|
#[tool(description = "Check the syntax and types of a Myc expression without running it. \
|
||||||
|
Returns 'OK' if the code is valid, or a list of compiler diagnostics on error. \
|
||||||
|
Note: accepts a single expression only. Wrap multiple expressions in (do ...): \
|
||||||
|
e.g. (do (def x 42) (+ x 1)).")]
|
||||||
|
fn check_syntax(&self, Parameters(CodeInput { code }): Parameters<CodeInput>) -> String {
|
||||||
|
let env = Self::make_env();
|
||||||
|
let result = env.compile(&code);
|
||||||
|
if result.diagnostics.has_errors() {
|
||||||
|
let errors: Vec<String> = result
|
||||||
|
.diagnostics
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.map(|d| format!("{:?}: {}", d.level, d.message))
|
||||||
|
.collect();
|
||||||
|
errors.join("\n")
|
||||||
|
} else {
|
||||||
|
"OK".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists all built-in functions and constants available in the Myc RTL.
|
||||||
|
#[tool(description = "List all built-in functions and constants available in the Myc \
|
||||||
|
runtime library (RTL). Returns a sorted list of all registered identifiers \
|
||||||
|
such as +, -, push, series, print, etc.")]
|
||||||
|
fn list_builtins(&self) -> String {
|
||||||
|
let env = Self::make_env();
|
||||||
|
// Only RTL bindings are in the fixed scope (scope 0), which is what
|
||||||
|
// list_bindings() returns. This excludes user-defined symbols.
|
||||||
|
let names = env.list_bindings();
|
||||||
|
names.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the full Myc language specification (BNF grammar and semantics).
|
||||||
|
#[tool(description = "Return the complete Myc language specification, including BNF grammar, \
|
||||||
|
core semantics, data types, special forms, macro system, and standard library overview. \
|
||||||
|
Read this first to understand how to write valid Myc code.")]
|
||||||
|
fn get_language_spec(&self) -> String {
|
||||||
|
LANGUAGE_SPEC.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool_handler]
|
||||||
|
impl ServerHandler for MycMcpServer {
|
||||||
|
fn get_info(&self) -> ServerInfo {
|
||||||
|
ServerInfo::new(
|
||||||
|
ServerCapabilities::builder()
|
||||||
|
.enable_tools()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.with_instructions(
|
||||||
|
"This server exposes the Myc compiler — a Lisp-like DSL for financial analysis. \
|
||||||
|
Use `get_language_spec` to learn the language syntax and semantics, \
|
||||||
|
`list_builtins` to see all available built-in functions, \
|
||||||
|
`check_syntax` to validate code, \
|
||||||
|
`eval_myc` to run expressions, and \
|
||||||
|
`dump_ast` to inspect the compiled AST.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts the MCP server on stdio (JSON-RPC over stdin/stdout).
|
||||||
|
///
|
||||||
|
/// Uses a single-threaded Tokio runtime because `Environment` relies on
|
||||||
|
/// `Rc<RefCell<...>>` and is not `Send`. The server handles one request
|
||||||
|
/// at a time, which matches the single-threaded design of the Myc runtime.
|
||||||
|
pub fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
// Register RTL once just to warm the prelude path — the actual environment
|
||||||
|
// used per tool call is created fresh in `make_env()`.
|
||||||
|
// This also validates that the embedded system library compiles correctly.
|
||||||
|
let _ = rtl::register as fn(&Environment);
|
||||||
|
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?
|
||||||
|
.block_on(async {
|
||||||
|
let server = MycMcpServer::new();
|
||||||
|
let service = rmcp::serve_server(server, stdio()).await?;
|
||||||
|
service.waiting().await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user