|
| 1 | +run javascript code in v8 |
| 2 | + |
| 3 | +params: |
| 4 | +- code: the javascript code to run |
| 5 | + |
| 6 | +returns: |
| 7 | +- output: the output of the javascript code |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | +## Limitations |
| 13 | + |
| 14 | +While `mcp-v8` provides a powerful and persistent JavaScript execution environment, there are limitations to its runtime. |
| 15 | + |
| 16 | +- **No `async`/`await` or Promises**: Asynchronous JavaScript is not supported. All code must be synchronous. |
| 17 | +- **No `fetch` or network access**: There is no built-in way to make HTTP requests or access the network. |
| 18 | +- **No `console.log` or standard output**: Output from `console.log` or similar functions will not appear. To return results, ensure the value you want is the last line of your code. |
| 19 | +- **No file system access**: The runtime does not provide access to the local file system or environment variables. |
| 20 | +- **No `npm install` or external packages**: You cannot install or import npm packages. Only standard JavaScript (ECMAScript) built-ins are available. |
| 21 | +- **No timers**: Functions like `setTimeout` and `setInterval` are not available. |
| 22 | +- **No DOM or browser APIs**: This is not a browser environment; there is no access to `window`, `document`, or other browser-specific objects. |
| 23 | + |
| 24 | + |
| 25 | +The way the runtime works, is that there is no console.log. If you want the results of an execution, you must return it in the last line of code. |
| 26 | + |
| 27 | + |
| 28 | +eg: |
| 29 | + |
| 30 | +```js |
| 31 | +const result = 1 + 1; |
| 32 | +result; |
| 33 | +``` |
| 34 | + |
| 35 | +would return: |
| 36 | + |
| 37 | +``` |
| 38 | +2 |
| 39 | +``` |
| 40 | + |
| 41 | +you must also jsonify an object, and return it as a string to see its content. |
| 42 | + |
| 43 | +eg: |
| 44 | + |
| 45 | +```js |
| 46 | +const obj = { |
| 47 | + a: 1, |
| 48 | + b: 2, |
| 49 | +}; |
| 50 | +JSON.stringify(obj); |
| 51 | +``` |
| 52 | + |
| 53 | +would return: |
| 54 | + |
| 55 | +``` |
| 56 | +{"a":1,"b":2} |
| 57 | +``` |
| 58 | + |
| 59 | +the source code of the runtime is this |
| 60 | +```rust |
| 61 | +use rmcp::{ |
| 62 | + model::{ServerCapabilities, ServerInfo}, |
| 63 | + |
| 64 | + Error as McpError, RoleServer, ServerHandler, model::*, schemars, |
| 65 | + service::RequestContext, tool, |
| 66 | +}; |
| 67 | +use serde_json::json; |
| 68 | + |
| 69 | + |
| 70 | +use std::sync::Once; |
| 71 | +use v8::{self}; |
| 72 | + |
| 73 | +pub(crate) mod heap_storage; |
| 74 | +use crate::mcp::heap_storage::{HeapStorage, AnyHeapStorage}; |
| 75 | + |
| 76 | + |
| 77 | + |
| 78 | + |
| 79 | +fn eval<'s>(scope: &mut v8::HandleScope<'s>, code: &str) -> Result<v8::Local<'s, v8::Value>, String> { |
| 80 | + let scope = &mut v8::EscapableHandleScope::new(scope); |
| 81 | + let source = v8::String::new(scope, code).ok_or("Failed to create V8 string")?; |
| 82 | + let script = v8::Script::compile(scope, source, None).ok_or("Failed to compile script")?; |
| 83 | + let r = script.run(scope).ok_or("Failed to run script")?; |
| 84 | + Ok(scope.escape(r)) |
| 85 | +} |
| 86 | + |
| 87 | +static INIT: Once = Once::new(); |
| 88 | +static mut PLATFORM: Option<v8::SharedRef<v8::Platform>> = None; |
| 89 | + |
| 90 | +pub fn initialize_v8() { |
| 91 | + INIT.call_once(|| { |
| 92 | + let platform = v8::new_default_platform(0, false).make_shared(); |
| 93 | + v8::V8::initialize_platform(platform.clone()); |
| 94 | + v8::V8::initialize(); |
| 95 | + unsafe { |
| 96 | + PLATFORM = Some(platform); |
| 97 | + } |
| 98 | + }); |
| 99 | +} |
| 100 | + |
| 101 | + |
| 102 | + |
| 103 | +#[allow(dead_code)] |
| 104 | +pub trait DataService: Send + Sync + 'static { |
| 105 | + fn get_data(&self) -> String; |
| 106 | + fn set_data(&mut self, data: String); |
| 107 | +} |
| 108 | + |
| 109 | +#[derive(Clone)] |
| 110 | +pub struct GenericService { |
| 111 | + heap_storage: AnyHeapStorage, |
| 112 | +} |
| 113 | + |
| 114 | +// response to run_js |
| 115 | +#[derive(Debug, Clone)] |
| 116 | +pub struct RunJsResponse { |
| 117 | + pub output: String, |
| 118 | + pub heap: String, |
| 119 | +} |
| 120 | + |
| 121 | +impl IntoContents for RunJsResponse { |
| 122 | + fn into_contents(self) -> Vec<Content> { |
| 123 | + match Content::json(json!({ |
| 124 | + "output": self.output, |
| 125 | + "heap": self.heap, |
| 126 | + })) { |
| 127 | + Ok(content) => vec![content], |
| 128 | + Err(e) => vec![Content::text(format!("Failed to convert run_js response to content: {}", e))], |
| 129 | + } |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +#[tool(tool_box)] |
| 134 | +impl GenericService { |
| 135 | + pub async fn new( |
| 136 | + heap_storage: AnyHeapStorage, |
| 137 | + ) -> Self { |
| 138 | + Self { |
| 139 | + heap_storage, |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + #[tool(description = include_str!("run_js_tool_description.md"))] |
| 144 | + pub async fn run_js(&self, #[tool(param)] code: String, #[tool(param)] heap: String) -> RunJsResponse { |
| 145 | + let snapshot = self.heap_storage.get(&heap).await.ok(); |
| 146 | + let code_clone = code.clone(); |
| 147 | + let v8_result = tokio::task::spawn_blocking(move || { |
| 148 | + let mut snapshot_creator = match snapshot { |
| 149 | + Some(snapshot) => { |
| 150 | + eprintln!("creating isolate from snapshot..."); |
| 151 | + v8::Isolate::snapshot_creator_from_existing_snapshot(snapshot, None, None) |
| 152 | + } |
| 153 | + None => { |
| 154 | + eprintln!("snapshot not found, creating new isolate..."); |
| 155 | + v8::Isolate::snapshot_creator(Default::default(), Default::default()) |
| 156 | + } |
| 157 | + }; |
| 158 | + // Always call create_blob before dropping snapshot_creator |
| 159 | + let mut output_result: Result<String, String> = Err("Unknown error".to_string()); |
| 160 | + { |
| 161 | + let scope = &mut v8::HandleScope::new(&mut snapshot_creator); |
| 162 | + let context = v8::Context::new(scope, Default::default()); |
| 163 | + let scope = &mut v8::ContextScope::new(scope, context); |
| 164 | + let result = eval(scope, &code_clone); |
| 165 | + match result { |
| 166 | + Ok(result) => { |
| 167 | + let result_str = result |
| 168 | + .to_string(scope) |
| 169 | + .ok_or_else(|| "Failed to convert result to string".to_string()); |
| 170 | + match result_str { |
| 171 | + Ok(s) => { |
| 172 | + output_result = Ok(s.to_rust_string_lossy(scope)); |
| 173 | + } |
| 174 | + Err(e) => { |
| 175 | + output_result = Err(e); |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + Err(e) => { |
| 180 | + output_result = Err(e); |
| 181 | + } |
| 182 | + } |
| 183 | + scope.set_default_context(context); |
| 184 | + } |
| 185 | + // Always call create_blob before returning |
| 186 | + let startup_data = match snapshot_creator.create_blob(v8::FunctionCodeHandling::Clear) { |
| 187 | + Some(blob) => blob, |
| 188 | + None => return Ok::<_, std::convert::Infallible>((format!("V8 error: Failed to create V8 snapshot blob"), vec![])), |
| 189 | + }; |
| 190 | + let startup_data_vec = startup_data.to_vec(); |
| 191 | + match output_result { |
| 192 | + Ok(output) => Ok::<_, std::convert::Infallible>((output, startup_data_vec)), |
| 193 | + Err(e) => Ok::<_, std::convert::Infallible>((format!("V8 error: {}", e), startup_data_vec)), |
| 194 | + } |
| 195 | + }).await; |
| 196 | + match v8_result { |
| 197 | + Ok(Ok((output, startup_data))) => { |
| 198 | + if let Err(e) = self.heap_storage.put(&heap, &startup_data).await { |
| 199 | + return RunJsResponse { |
| 200 | + output: format!("Error saving heap: {}", e), |
| 201 | + heap, |
| 202 | + }; |
| 203 | + } |
| 204 | + RunJsResponse { output, heap } |
| 205 | + } |
| 206 | + Ok(Err(e)) => RunJsResponse { |
| 207 | + output: format!("V8 error: {}", e), |
| 208 | + heap, |
| 209 | + }, |
| 210 | + Err(e) => RunJsResponse { |
| 211 | + output: format!("Task join error: {}", e), |
| 212 | + heap, |
| 213 | + }, |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + |
| 218 | +} |
| 219 | + |
| 220 | +#[tool(tool_box)] |
| 221 | +impl ServerHandler for GenericService { |
| 222 | + fn get_info(&self) -> ServerInfo { |
| 223 | + ServerInfo { |
| 224 | + instructions: Some("generic data service".into()), |
| 225 | + capabilities: ServerCapabilities::builder().enable_tools().build(), |
| 226 | + ..Default::default() |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + |
| 231 | + async fn initialize( |
| 232 | + &self, |
| 233 | + _request: InitializeRequestParam, |
| 234 | + context: RequestContext<RoleServer>, |
| 235 | + ) -> Result<InitializeResult, McpError> { |
| 236 | + if let Some(http_request_part) = context.extensions.get::<axum::http::request::Parts>() { |
| 237 | + let initialize_headers = &http_request_part.headers; |
| 238 | + let initialize_uri = &http_request_part.uri; |
| 239 | + tracing::info!(?initialize_headers, %initialize_uri, "initialize from http server"); |
| 240 | + } |
| 241 | + Ok(self.get_info()) |
| 242 | + } |
| 243 | +} |
| 244 | +``` |
| 245 | + |
0 commit comments