logicaffeine_cli/mcp/
mod.rs1mod protocol;
16mod resources;
17mod sandbox;
18mod tools;
19
20use serde_json::{json, Value};
21
22use protocol::{Incoming, INVALID_PARAMS, METHOD_NOT_FOUND, NOT_INITIALIZED};
23
24pub struct McpServer {
26 initialized: bool,
27}
28
29impl McpServer {
30 pub fn new() -> Self {
32 McpServer { initialized: false }
33 }
34
35 pub fn handle_line(&mut self, line: &str) -> Option<String> {
38 let incoming = match protocol::parse_line(line) {
39 Ok(incoming) => incoming,
40 Err(response) => return Some(response),
41 };
42 let Incoming { id, method, params } = incoming;
43
44 let id = id?;
48
49 let response = match method.as_str() {
50 "initialize" => {
51 self.initialized = true;
52 let requested = params.get("protocolVersion").and_then(Value::as_str);
53 protocol::ok(
54 &id,
55 json!({
56 "protocolVersion": protocol::negotiate_version(requested),
57 "capabilities": {
58 "tools": {},
59 "resources": {},
60 "prompts": {},
61 },
62 "serverInfo": {
63 "name": "logos",
64 "version": env!("CARGO_PKG_VERSION"),
65 },
66 }),
67 )
68 }
69 "ping" => protocol::ok(&id, json!({})),
70 _ if !self.initialized => protocol::error(
71 &id,
72 NOT_INITIALIZED,
73 "server is not initialized: send initialize first",
74 ),
75 "tools/list" => protocol::ok(&id, tools::list()),
76 "tools/call" => tools::call(&id, ¶ms),
77 "resources/list" => protocol::ok(&id, resources::list()),
78 "resources/read" => resources::read(&id, ¶ms),
79 "prompts/list" => protocol::ok(&id, prompts_list()),
80 "prompts/get" => prompts_get(&id, ¶ms),
81 other => protocol::error(&id, METHOD_NOT_FOUND, &format!("unknown method '{other}'")),
82 };
83 Some(response)
84 }
85}
86
87impl Default for McpServer {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93const WRITE_LOGOS_PROMPT: &str = "\
95You are writing LOGOS, the English programming language. Before writing any \
96code, read the resource logos://quickguide — it teaches the whole language \
97on one page. The essentials: programs live in `## Main` blocks; every \
98statement is an English sentence ending with a period; `Let x be 5.` \
99declares, `Set x to 6.` mutates (requires `Let mutable`), `Show x.` prints; \
100indices are 1-based; value comparison is `is equal to` / `equals`, never \
101bare `is`. Check your work with the logos_check tool (structured \
102diagnostics with teaching explanations), then execute with logos_run.";
103
104fn prompts_list() -> Value {
106 json!({"prompts": [{
107 "name": "write-logos",
108 "description": "Orient an agent that is about to write LOGOS code",
109 "arguments": [],
110 }]})
111}
112
113fn prompts_get(id: &Value, params: &Value) -> String {
115 match params.get("name").and_then(Value::as_str) {
116 Some("write-logos") => protocol::ok(
117 id,
118 json!({
119 "description": "Orient an agent that is about to write LOGOS code",
120 "messages": [{
121 "role": "user",
122 "content": {"type": "text", "text": WRITE_LOGOS_PROMPT},
123 }],
124 }),
125 ),
126 Some(other) => protocol::error(id, INVALID_PARAMS, &format!("unknown prompt '{other}'")),
127 None => protocol::error(id, INVALID_PARAMS, "prompts/get needs a 'name'"),
128 }
129}
130
131pub fn cmd_mcp(
136 worker: Option<String>,
137 worker_args: Vec<String>,
138) -> Result<(), Box<dyn std::error::Error>> {
139 if let Some(engine) = worker {
140 return sandbox::worker(&engine, &worker_args);
141 }
142
143 use std::io::{BufRead, Write};
144
145 let stdin = std::io::stdin();
146 let stdout = std::io::stdout();
147 let mut server = McpServer::new();
148
149 for line in stdin.lock().lines() {
150 let line = line?;
151 if line.trim().is_empty() {
152 continue;
153 }
154 if let Some(response) = server.handle_line(&line) {
155 let mut out = stdout.lock();
156 out.write_all(response.as_bytes())?;
157 out.write_all(b"\n")?;
158 out.flush()?;
159 }
160 }
161 Ok(())
162}