|
|
@@ -0,0 +1,753 @@
|
|
|
+package com.nonvioce.ijs;
|
|
|
+
|
|
|
+import android.util.Log;
|
|
|
+
|
|
|
+import fi.iki.elonen.NanoHTTPD;
|
|
|
+
|
|
|
+import org.json.JSONArray;
|
|
|
+import org.json.JSONException;
|
|
|
+import org.json.JSONObject;
|
|
|
+import org.mozilla.javascript.Context;
|
|
|
+import org.mozilla.javascript.Scriptable;
|
|
|
+import org.mozilla.javascript.ScriptableObject;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * MCP (Model Context Protocol) server embedded in the Android app.
|
|
|
+ * Implements JSON-RPC 2.0 over HTTP on localhost, exposing Symja math
|
|
|
+ * computation as MCP tools and resources.
|
|
|
+ */
|
|
|
+public class McpServer extends NanoHTTPD {
|
|
|
+
|
|
|
+ private static final String TAG = "McpServer";
|
|
|
+ private static final String PROTOCOL_VERSION = "2024-11-05";
|
|
|
+ private static final String SERVER_NAME = "ijs-mcp";
|
|
|
+ private static final String SERVER_VERSION = "1.0";
|
|
|
+
|
|
|
+ private final SymjaEngine symjaEngine;
|
|
|
+ private final PluginManager pluginManager;
|
|
|
+ private final List<String> evalHistory = new ArrayList<>();
|
|
|
+ private boolean initialized = false;
|
|
|
+
|
|
|
+ public McpServer(int port, SymjaEngine symjaEngine, PluginManager pluginManager) {
|
|
|
+ super("127.0.0.1", port);
|
|
|
+ this.symjaEngine = symjaEngine;
|
|
|
+ this.pluginManager = pluginManager;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── HTTP routing ──────────────────────────────────────────────
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Response serve(IHTTPSession session) {
|
|
|
+ String method = session.getMethod().name();
|
|
|
+ String uri = session.getUri();
|
|
|
+
|
|
|
+ // CORS preflight
|
|
|
+ if ("OPTIONS".equals(method)) {
|
|
|
+ Response r = newFixedLengthResponse(Response.Status.OK, "text/plain", "");
|
|
|
+ addCors(r);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ // MCP JSON-RPC endpoint
|
|
|
+ if ("POST".equals(method) && ("/".equals(uri) || "/mcp".equals(uri))) {
|
|
|
+ Response r = handleJsonRpc(session);
|
|
|
+ addCors(r);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Health check
|
|
|
+ if ("GET".equals(method) && "/health".equals(uri)) {
|
|
|
+ Response r = newFixedLengthResponse(Response.Status.OK, "application/json",
|
|
|
+ "{\"status\":\"ok\",\"initialized\":" + initialized + "}");
|
|
|
+ addCors(r);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Not Found");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addCors(Response r) {
|
|
|
+ r.addHeader("Access-Control-Allow-Origin", "*");
|
|
|
+ r.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
|
|
+ r.addHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── JSON-RPC dispatch ─────────────────────────────────────────
|
|
|
+
|
|
|
+ private Response handleJsonRpc(IHTTPSession session) {
|
|
|
+ String body;
|
|
|
+ try {
|
|
|
+ body = readBody(session);
|
|
|
+ } catch (IOException e) {
|
|
|
+ return jsonError(null, -32700, "Parse error: cannot read body");
|
|
|
+ }
|
|
|
+ if (body == null || body.trim().isEmpty()) {
|
|
|
+ return jsonError(null, -32700, "Parse error: empty body");
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject request;
|
|
|
+ try {
|
|
|
+ request = new JSONObject(body);
|
|
|
+ } catch (JSONException e) {
|
|
|
+ return jsonError(null, -32700, "Parse error: invalid JSON");
|
|
|
+ }
|
|
|
+
|
|
|
+ String method = null;
|
|
|
+ Object id = null;
|
|
|
+ try {
|
|
|
+ String jsonrpc = request.optString("jsonrpc");
|
|
|
+ if (!"2.0".equals(jsonrpc)) {
|
|
|
+ return jsonError(null, -32600, "Invalid Request: jsonrpc must be 2.0");
|
|
|
+ }
|
|
|
+
|
|
|
+ method = request.optString("method");
|
|
|
+ id = request.isNull("id") ? null : request.get("id");
|
|
|
+
|
|
|
+ if (method.isEmpty()) {
|
|
|
+ return jsonError(id, -32600, "Invalid Request: missing method");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Notification (no id) — no response expected
|
|
|
+ if (id == null) {
|
|
|
+ handleNotification(method, request.optJSONObject("params"));
|
|
|
+ return newFixedLengthResponse(Response.Status.NO_CONTENT, "text/plain", "");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Regular request
|
|
|
+ JSONObject result = dispatch(method, request.optJSONObject("params"));
|
|
|
+ return jsonResponse(id, result);
|
|
|
+
|
|
|
+ } catch (JSONException e) {
|
|
|
+ Log.e(TAG, "JSON-RPC error", e);
|
|
|
+ return jsonError(id, -32603, "Internal error: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject dispatch(String method, JSONObject params) throws JSONException {
|
|
|
+ if (params == null) params = new JSONObject();
|
|
|
+
|
|
|
+ switch (method) {
|
|
|
+ case "initialize": return handleInitialize(params);
|
|
|
+ case "tools/list": return handleToolsList();
|
|
|
+ case "tools/call": return handleToolsCall(params);
|
|
|
+ case "resources/list": return handleResourcesList();
|
|
|
+ case "resources/read": return handleResourcesRead(params);
|
|
|
+ default:
|
|
|
+ throw new JSONException("Method not found: " + method);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void handleNotification(String method, JSONObject params) {
|
|
|
+ if ("notifications/initialized".equals(method)) {
|
|
|
+ initialized = true;
|
|
|
+ Log.i(TAG, "MCP client initialized");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── initialize ────────────────────────────────────────────────
|
|
|
+
|
|
|
+ private JSONObject handleInitialize(JSONObject params) throws JSONException {
|
|
|
+ JSONObject result = new JSONObject();
|
|
|
+ result.put("protocolVersion", PROTOCOL_VERSION);
|
|
|
+
|
|
|
+ JSONObject capabilities = new JSONObject();
|
|
|
+ capabilities.put("tools", new JSONObject());
|
|
|
+ capabilities.put("resources", new JSONObject());
|
|
|
+ result.put("capabilities", capabilities);
|
|
|
+
|
|
|
+ JSONObject serverInfo = new JSONObject();
|
|
|
+ serverInfo.put("name", SERVER_NAME);
|
|
|
+ serverInfo.put("version", SERVER_VERSION);
|
|
|
+ result.put("serverInfo", serverInfo);
|
|
|
+
|
|
|
+ Log.i(TAG, "MCP initialize handshake");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── tools/list ────────────────────────────────────────────────
|
|
|
+
|
|
|
+ private JSONObject handleToolsList() throws JSONException {
|
|
|
+ JSONArray tools = new JSONArray();
|
|
|
+
|
|
|
+ // --- Core computation ---
|
|
|
+ tools.put(tool("evaluate",
|
|
|
+ "Evaluate a Symja mathematical expression. Supports calculus, algebra, " +
|
|
|
+ "linear algebra, number theory, special functions, transforms, and more. " +
|
|
|
+ "Uses Wolfram/Mathematica-like syntax with square brackets for function calls, " +
|
|
|
+ "e.g. Integrate[x^2, x], Solve[x^2-5*x+6==0, x], Det[{{1,2},{3,4}}].",
|
|
|
+ inputSchema(
|
|
|
+ props().put("expression", stringProp("The Symja expression to evaluate")),
|
|
|
+ "expression")));
|
|
|
+
|
|
|
+ tools.put(tool("differentiate",
|
|
|
+ "Differentiate an expression with respect to a variable.",
|
|
|
+ inputSchema(
|
|
|
+ props()
|
|
|
+ .put("expression", stringProp("Expression to differentiate, e.g. x^3*Sin[x]"))
|
|
|
+ .put("variable", stringProp("Variable of differentiation, e.g. x")),
|
|
|
+ "expression", "variable")));
|
|
|
+
|
|
|
+ tools.put(tool("integrate",
|
|
|
+ "Integrate an expression. Omit bounds for indefinite integration; " +
|
|
|
+ "provide both lower and upper for definite integration.",
|
|
|
+ inputSchema(
|
|
|
+ props()
|
|
|
+ .put("expression", stringProp("Expression to integrate"))
|
|
|
+ .put("variable", stringProp("Variable of integration"))
|
|
|
+ .put("lower", stringProp("Lower bound (optional, omit for indefinite integral)"))
|
|
|
+ .put("upper", stringProp("Upper bound (optional, omit for indefinite integral)")),
|
|
|
+ "expression", "variable")));
|
|
|
+
|
|
|
+ tools.put(tool("solve",
|
|
|
+ "Solve an equation or system of equations symbolically.",
|
|
|
+ inputSchema(
|
|
|
+ props()
|
|
|
+ .put("equation", stringProp("Equation to solve, e.g. x^2-5*x+6==0"))
|
|
|
+ .put("variable", stringProp("Variable to solve for, e.g. x")),
|
|
|
+ "equation", "variable")));
|
|
|
+
|
|
|
+ tools.put(tool("simplify",
|
|
|
+ "Simplify a mathematical expression algebraically.",
|
|
|
+ inputSchema(
|
|
|
+ props().put("expression", stringProp("Expression to simplify, e.g. Sin[x]^2+Cos[x]^2")),
|
|
|
+ "expression")));
|
|
|
+
|
|
|
+ tools.put(tool("numeric",
|
|
|
+ "Evaluate an expression numerically with specified precision.",
|
|
|
+ inputSchema(
|
|
|
+ props()
|
|
|
+ .put("expression", stringProp("Expression to compute numerically, e.g. Pi"))
|
|
|
+ .put("precision", integerProp("Number of significant digits (default: 16)")),
|
|
|
+ "expression")));
|
|
|
+
|
|
|
+ tools.put(tool("to_latex",
|
|
|
+ "Convert a mathematical expression to LaTeX format string.",
|
|
|
+ inputSchema(
|
|
|
+ props().put("expression", stringProp("Expression to convert to LaTeX")),
|
|
|
+ "expression")));
|
|
|
+
|
|
|
+ // --- Plugin management ---
|
|
|
+ tools.put(tool("list_plugins",
|
|
|
+ "List all installed plugins with their metadata.",
|
|
|
+ inputSchema(props())));
|
|
|
+
|
|
|
+ tools.put(tool("get_plugin",
|
|
|
+ "Get a single plugin's full details including its JavaScript code.",
|
|
|
+ inputSchema(
|
|
|
+ props().put("plugin_id", stringProp("The plugin's unique identifier")),
|
|
|
+ "plugin_id")));
|
|
|
+
|
|
|
+ tools.put(tool("create_plugin",
|
|
|
+ "Create a new plugin or update an existing one. Plugins are JavaScript functions " +
|
|
|
+ "that take inputs and a Symja API object, and return a Symja expression string.",
|
|
|
+ inputSchema(
|
|
|
+ props()
|
|
|
+ .put("id", stringProp("Unique plugin identifier (e.g. 'circle_area')"))
|
|
|
+ .put("name", stringProp("Display name (e.g. 'Circle Area Calculator')"))
|
|
|
+ .put("js_code", stringProp(
|
|
|
+ "JavaScript code containing a calculate(inputs, Symja) function that " +
|
|
|
+ "returns a Symja expression string"))
|
|
|
+ .put("icon", stringProp("Emoji icon (optional, default: 🔌)"))
|
|
|
+ .put("command", stringProp("Short command name (optional, e.g. 'circle')"))
|
|
|
+ .put("description", stringProp("Plugin description (optional)"))
|
|
|
+ .put("inputs", stringProp(
|
|
|
+ "JSON array of input definitions (optional). " +
|
|
|
+ "Each: {\"name\":\"x\",\"label\":\"X Value\",\"defaultValue\":\"1\"}")),
|
|
|
+ "id", "name", "js_code")));
|
|
|
+
|
|
|
+ tools.put(tool("delete_plugin",
|
|
|
+ "Delete a plugin by its ID.",
|
|
|
+ inputSchema(
|
|
|
+ props().put("plugin_id", stringProp("Plugin ID to delete")),
|
|
|
+ "plugin_id")));
|
|
|
+
|
|
|
+ tools.put(tool("execute_plugin",
|
|
|
+ "Execute a plugin with the given arguments. " +
|
|
|
+ "Runs the plugin's JavaScript code using Rhino and returns the computed result.",
|
|
|
+ inputSchema(
|
|
|
+ props()
|
|
|
+ .put("plugin_id", stringProp("Plugin ID to execute"))
|
|
|
+ .put("arguments",
|
|
|
+ stringProp("JSON object of input arguments, e.g. {\"radius\":\"5\"}")),
|
|
|
+ "plugin_id")));
|
|
|
+
|
|
|
+ JSONObject result = new JSONObject();
|
|
|
+ result.put("tools", tools);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── tools/call ────────────────────────────────────────────────
|
|
|
+
|
|
|
+ private JSONObject handleToolsCall(JSONObject params) throws JSONException {
|
|
|
+ String name = params.getString("name");
|
|
|
+ JSONObject arguments = params.optJSONObject("arguments");
|
|
|
+ if (arguments == null) arguments = new JSONObject();
|
|
|
+
|
|
|
+ String textResult;
|
|
|
+ switch (name) {
|
|
|
+ case "evaluate":
|
|
|
+ textResult = callEvaluate(arguments.getString("expression"));
|
|
|
+ break;
|
|
|
+ case "differentiate":
|
|
|
+ textResult = callDifferentiate(
|
|
|
+ arguments.getString("expression"),
|
|
|
+ arguments.getString("variable"));
|
|
|
+ break;
|
|
|
+ case "integrate":
|
|
|
+ textResult = callIntegrate(
|
|
|
+ arguments.getString("expression"),
|
|
|
+ arguments.getString("variable"),
|
|
|
+ arguments.optString("lower", null),
|
|
|
+ arguments.optString("upper", null));
|
|
|
+ break;
|
|
|
+ case "solve":
|
|
|
+ textResult = callSolve(
|
|
|
+ arguments.getString("equation"),
|
|
|
+ arguments.getString("variable"));
|
|
|
+ break;
|
|
|
+ case "simplify":
|
|
|
+ textResult = callSimplify(arguments.getString("expression"));
|
|
|
+ break;
|
|
|
+ case "numeric":
|
|
|
+ textResult = callNumeric(
|
|
|
+ arguments.getString("expression"),
|
|
|
+ arguments.optInt("precision", 16));
|
|
|
+ break;
|
|
|
+ case "to_latex":
|
|
|
+ textResult = callToLatex(arguments.getString("expression"));
|
|
|
+ break;
|
|
|
+ case "list_plugins":
|
|
|
+ textResult = callListPlugins();
|
|
|
+ break;
|
|
|
+ case "get_plugin":
|
|
|
+ textResult = callGetPlugin(arguments.getString("plugin_id"));
|
|
|
+ break;
|
|
|
+ case "create_plugin":
|
|
|
+ textResult = callCreatePlugin(arguments);
|
|
|
+ break;
|
|
|
+ case "delete_plugin":
|
|
|
+ textResult = callDeletePlugin(arguments.getString("plugin_id"));
|
|
|
+ break;
|
|
|
+ case "execute_plugin":
|
|
|
+ textResult = callExecutePlugin(
|
|
|
+ arguments.getString("plugin_id"),
|
|
|
+ arguments.optJSONObject("arguments"));
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ throw new JSONException("Unknown tool: " + name);
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONArray content = new JSONArray();
|
|
|
+ JSONObject item = new JSONObject();
|
|
|
+ item.put("type", "text");
|
|
|
+ item.put("text", textResult);
|
|
|
+ if (textResult.startsWith("Error:")) {
|
|
|
+ item.put("isError", true);
|
|
|
+ }
|
|
|
+ content.put(item);
|
|
|
+
|
|
|
+ JSONObject result = new JSONObject();
|
|
|
+ result.put("content", content);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── Tool implementations ──────────────────────────────────────
|
|
|
+
|
|
|
+ private String callEvaluate(String expression) {
|
|
|
+ String result = symjaEngine.evaluate(expression);
|
|
|
+ if (evalHistory.size() >= 100) evalHistory.remove(0);
|
|
|
+ evalHistory.add(expression + " → " + result);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callDifferentiate(String expression, String variable) {
|
|
|
+ return symjaEngine.evaluate("D[" + expression + ", " + variable + "]");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callIntegrate(String expression, String variable, String lower, String upper) {
|
|
|
+ String expr;
|
|
|
+ if (lower != null && !lower.isEmpty() && upper != null && !upper.isEmpty()) {
|
|
|
+ expr = "Integrate[" + expression + ", {" + variable + ", " + lower + ", " + upper + "}]";
|
|
|
+ } else {
|
|
|
+ expr = "Integrate[" + expression + ", " + variable + "]";
|
|
|
+ }
|
|
|
+ return symjaEngine.evaluate(expr);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callSolve(String equation, String variable) {
|
|
|
+ return symjaEngine.evaluate("Solve[" + equation + ", " + variable + "]");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callSimplify(String expression) {
|
|
|
+ return symjaEngine.evaluate("Simplify[" + expression + "]");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callNumeric(String expression, int precision) {
|
|
|
+ return symjaEngine.numericEval(expression, precision);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callToLatex(String expression) {
|
|
|
+ return symjaEngine.toTeX(expression);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callListPlugins() {
|
|
|
+ return pluginManager.getPluginsJson();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callGetPlugin(String pluginId) {
|
|
|
+ pluginManager.reloadPlugins();
|
|
|
+ String allJson = pluginManager.getPluginsJson();
|
|
|
+ try {
|
|
|
+ JSONArray plugins = new JSONArray(allJson);
|
|
|
+ for (int i = 0; i < plugins.length(); i++) {
|
|
|
+ JSONObject p = plugins.getJSONObject(i);
|
|
|
+ if (pluginId.equals(p.getString("id"))) {
|
|
|
+ return p.toString(2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return "Error: Plugin not found: " + pluginId;
|
|
|
+ } catch (JSONException e) {
|
|
|
+ return "Error: Failed to read plugins: " + e.getMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callCreatePlugin(JSONObject args) throws JSONException {
|
|
|
+ JSONObject plugin = new JSONObject();
|
|
|
+ plugin.put("id", args.getString("id"));
|
|
|
+ plugin.put("name", args.getString("name"));
|
|
|
+ plugin.put("icon", args.optString("icon", "🔌"));
|
|
|
+ plugin.put("command", args.optString("command", ""));
|
|
|
+ plugin.put("description", args.optString("description", ""));
|
|
|
+
|
|
|
+ // js_code from MCP params → jsCode in plugin JSON
|
|
|
+ if (args.has("js_code")) {
|
|
|
+ plugin.put("jsCode", args.getString("js_code"));
|
|
|
+ } else if (args.has("jsCode")) {
|
|
|
+ plugin.put("jsCode", args.getString("jsCode"));
|
|
|
+ } else {
|
|
|
+ throw new JSONException("Missing required field: js_code");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Parse inputs — could be a JSON string or already an array
|
|
|
+ JSONArray inputs;
|
|
|
+ if (args.has("inputs")) {
|
|
|
+ Object val = args.get("inputs");
|
|
|
+ if (val instanceof JSONArray) {
|
|
|
+ inputs = (JSONArray) val;
|
|
|
+ } else if (val instanceof String) {
|
|
|
+ inputs = new JSONArray((String) val);
|
|
|
+ } else {
|
|
|
+ inputs = new JSONArray();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ inputs = new JSONArray();
|
|
|
+ }
|
|
|
+ plugin.put("inputs", inputs);
|
|
|
+
|
|
|
+ return pluginManager.savePlugin(plugin.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callDeletePlugin(String pluginId) {
|
|
|
+ return pluginManager.deletePlugin(pluginId);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String callExecutePlugin(String pluginId, JSONObject argsObj) {
|
|
|
+ JSONObject targetPlugin = null;
|
|
|
+ try {
|
|
|
+ JSONArray plugins = new JSONArray(pluginManager.getPluginsJson());
|
|
|
+ for (int i = 0; i < plugins.length(); i++) {
|
|
|
+ JSONObject p = plugins.getJSONObject(i);
|
|
|
+ if (pluginId.equals(p.getString("id"))) {
|
|
|
+ targetPlugin = p;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (JSONException e) {
|
|
|
+ return "Error: Failed to load plugins: " + e.getMessage();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (targetPlugin == null) {
|
|
|
+ return "Error: Plugin not found: " + pluginId;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String jsCode = targetPlugin.getString("jsCode");
|
|
|
+ JSONArray inputsDef = targetPlugin.optJSONArray("inputs");
|
|
|
+
|
|
|
+ Map<String, Object> inputValues = new HashMap<>();
|
|
|
+ if (inputsDef != null) {
|
|
|
+ for (int i = 0; i < inputsDef.length(); i++) {
|
|
|
+ JSONObject def = inputsDef.getJSONObject(i);
|
|
|
+ String name = def.getString("name");
|
|
|
+ String defaultValue = def.optString("defaultValue", "");
|
|
|
+ String value = (argsObj != null) ? argsObj.optString(name, defaultValue) : defaultValue;
|
|
|
+ inputValues.put(name, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return executePluginJS(jsCode, inputValues);
|
|
|
+ } catch (JSONException e) {
|
|
|
+ return "Error: Malformed plugin data: " + e.getMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── Rhino JS execution ────────────────────────────────────────
|
|
|
+
|
|
|
+ private String executePluginJS(String jsCode, Map<String, Object> inputs) {
|
|
|
+ List<String> consoleOutput = new ArrayList<>();
|
|
|
+ SymjaJsApi symjaJsApi = new SymjaJsApi(consoleOutput);
|
|
|
+
|
|
|
+ Context rhinoCtx = Context.enter();
|
|
|
+ try {
|
|
|
+ rhinoCtx.setOptimizationLevel(-1);
|
|
|
+
|
|
|
+ Scriptable scope = rhinoCtx.initStandardObjects();
|
|
|
+
|
|
|
+ // Build the inputs object for JavaScript
|
|
|
+ Scriptable jsInputs = rhinoCtx.newObject(scope);
|
|
|
+ for (Map.Entry<String, Object> e : inputs.entrySet()) {
|
|
|
+ jsInputs.put(e.getKey(), jsInputs, e.getValue());
|
|
|
+ }
|
|
|
+
|
|
|
+ ScriptableObject.putProperty(scope, "inputs", jsInputs);
|
|
|
+ ScriptableObject.putProperty(scope, "Symja",
|
|
|
+ Context.javaToJS(symjaJsApi, scope));
|
|
|
+
|
|
|
+ // Execute and call calculate()
|
|
|
+ String wrapped = jsCode + "\n" +
|
|
|
+ "var __ijs_result = calculate(inputs, Symja);\n" +
|
|
|
+ "__ijs_result;";
|
|
|
+ Object result = rhinoCtx.evaluateString(scope, wrapped, "plugin.js", 1, null);
|
|
|
+
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ if (!consoleOutput.isEmpty()) {
|
|
|
+ for (String line : consoleOutput) {
|
|
|
+ sb.append(line).append("\n");
|
|
|
+ }
|
|
|
+ sb.append("---\n");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (result != null && result != Context.getUndefinedValue()) {
|
|
|
+ String exprStr = Context.toString(result);
|
|
|
+ // Evaluate the returned expression through Symja (same as WebView console)
|
|
|
+ String evaluated = symjaEngine.evaluate(exprStr);
|
|
|
+ sb.append(evaluated);
|
|
|
+ }
|
|
|
+
|
|
|
+ String output = sb.toString().trim();
|
|
|
+ return output.isEmpty() ? "(plugin returned nothing)" : output;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ return "Error executing plugin: " + e.getMessage();
|
|
|
+ } finally {
|
|
|
+ Context.exit();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Symja API object exposed to plugin JavaScript code via Rhino.
|
|
|
+ * Matches the API provided by console.js's createSymjaAPI().
|
|
|
+ */
|
|
|
+ @SuppressWarnings("unused")
|
|
|
+ public class SymjaJsApi {
|
|
|
+ private final List<String> consoleOutput;
|
|
|
+
|
|
|
+ SymjaJsApi(List<String> consoleOutput) {
|
|
|
+ this.consoleOutput = consoleOutput;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void print(String msg) { consoleOutput.add(String.valueOf(msg)); }
|
|
|
+
|
|
|
+ public String eval(String expr) { return symjaEngine.evaluate(expr); }
|
|
|
+ public String differentiate(String expr, String var) { return eval("D[" + expr + ", " + var + "]"); }
|
|
|
+ public String integrate(String expr, String var) { return eval("Integrate[" + expr + ", " + var + "]"); }
|
|
|
+ public String solve(String eq, String var) { return eval("Solve[" + eq + ", " + var + "]"); }
|
|
|
+ public String simplify(String expr) { return eval("Simplify[" + expr + "]"); }
|
|
|
+ public String expand(String expr) { return eval("Expand[" + expr + "]"); }
|
|
|
+ public String factor(String expr) { return eval("Factor[" + expr + "]"); }
|
|
|
+ public String numeric(String expr, int precision) { return symjaEngine.numericEval(expr, precision); }
|
|
|
+ public String toTeX(String expr) { return symjaEngine.toTeX(expr); }
|
|
|
+ public String gcd(String a, String b) { return symjaEngine.gcd(a, b); }
|
|
|
+ public String lcm(String a, String b) { return symjaEngine.lcm(a, b); }
|
|
|
+ public String isPrime(String n) { return symjaEngine.isPrime(n); }
|
|
|
+ public String factorInteger(String n) { return symjaEngine.factorInteger(n); }
|
|
|
+ public String fibonacci(int n) { return symjaEngine.fibonacci(n); }
|
|
|
+ public String binomial(String n, String k) { return symjaEngine.binomial(n, k); }
|
|
|
+ public String sum(String expr, String var, String start, String end) { return symjaEngine.sum(expr, var, start, end); }
|
|
|
+ public String product(String expr, String var, String start, String end) { return symjaEngine.product(expr, var, start, end); }
|
|
|
+ public String limit(String expr, String var, String point) { return symjaEngine.limit(expr, var, point, ""); }
|
|
|
+ public String trigSimplify(String expr) { return symjaEngine.trigSimplify(expr); }
|
|
|
+ public String apart(String expr, String var) { return symjaEngine.apart(expr, var); }
|
|
|
+ public String together(String expr) { return symjaEngine.together(expr); }
|
|
|
+
|
|
|
+ public String det(String matrix) { return symjaEngine.matrixOperation("Det", matrix); }
|
|
|
+ public String inverse(String matrix) { return symjaEngine.matrixOperation("Inverse", matrix); }
|
|
|
+ public String transpose(String matrix) { return symjaEngine.matrixOperation("Transpose", matrix); }
|
|
|
+ public String eigenvalues(String matrix) { return symjaEngine.matrixOperation("Eigenvalues", matrix); }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── resources/list ────────────────────────────────────────────
|
|
|
+
|
|
|
+ private JSONObject handleResourcesList() throws JSONException {
|
|
|
+ JSONArray resources = new JSONArray();
|
|
|
+
|
|
|
+ resources.put(resource("variables://current",
|
|
|
+ "Defined Variables",
|
|
|
+ "application/json",
|
|
|
+ "Currently defined variables and their values in the Symja session"));
|
|
|
+
|
|
|
+ resources.put(resource("history://recent",
|
|
|
+ "Recent MCP Evaluations",
|
|
|
+ "application/json",
|
|
|
+ "Recent expressions evaluated via MCP and their results"));
|
|
|
+
|
|
|
+ JSONObject result = new JSONObject();
|
|
|
+ result.put("resources", resources);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── resources/read ────────────────────────────────────────────
|
|
|
+
|
|
|
+ private JSONObject handleResourcesRead(JSONObject params) throws JSONException {
|
|
|
+ String uri = params.getString("uri");
|
|
|
+
|
|
|
+ JSONArray contents = new JSONArray();
|
|
|
+
|
|
|
+ switch (uri) {
|
|
|
+ case "variables://current": {
|
|
|
+ String varsJson = symjaEngine.getDefinedVariables();
|
|
|
+ contents.put(resourceContent(uri, "application/json", varsJson));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case "history://recent": {
|
|
|
+ JSONArray historyJson = new JSONArray();
|
|
|
+ for (String entry : evalHistory) {
|
|
|
+ historyJson.put(entry);
|
|
|
+ }
|
|
|
+ contents.put(resourceContent(uri, "application/json", historyJson.toString(2)));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ throw new JSONException("Unknown resource URI: " + uri);
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject result = new JSONObject();
|
|
|
+ result.put("contents", contents);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── HTTP helpers ──────────────────────────────────────────────
|
|
|
+
|
|
|
+ private String readBody(IHTTPSession session) throws IOException {
|
|
|
+ String contentLength = session.getHeaders().get("content-length");
|
|
|
+ if (contentLength == null) return null;
|
|
|
+ int len = Integer.parseInt(contentLength);
|
|
|
+ if (len == 0) return null;
|
|
|
+ byte[] buf = new byte[len];
|
|
|
+ int total = 0;
|
|
|
+ while (total < len) {
|
|
|
+ int r = session.getInputStream().read(buf, total, len - total);
|
|
|
+ if (r == -1) break;
|
|
|
+ total += r;
|
|
|
+ }
|
|
|
+ return new String(buf, 0, total, "UTF-8");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── JSON builders ─────────────────────────────────────────────
|
|
|
+
|
|
|
+ /** Create a complete tool object with name, description, and inputSchema. */
|
|
|
+ private JSONObject tool(String name, String description, JSONObject inputSchema) throws JSONException {
|
|
|
+ JSONObject t = new JSONObject();
|
|
|
+ t.put("name", name);
|
|
|
+ t.put("description", description);
|
|
|
+ t.put("inputSchema", inputSchema);
|
|
|
+ return t;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Build inputSchema: {type:object, properties:{...}, required:[...]} */
|
|
|
+ private JSONObject inputSchema(JSONObject properties, String... required) throws JSONException {
|
|
|
+ JSONObject schema = new JSONObject();
|
|
|
+ schema.put("type", "object");
|
|
|
+ schema.put("properties", properties);
|
|
|
+ JSONArray req = new JSONArray();
|
|
|
+ for (String r : required) req.put(r);
|
|
|
+ schema.put("required", req);
|
|
|
+ return schema;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Start building a properties object. Call .put() on the result. */
|
|
|
+ private JSONObject props() {
|
|
|
+ return new JSONObject();
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject stringProp(String description) throws JSONException {
|
|
|
+ JSONObject p = new JSONObject();
|
|
|
+ p.put("type", "string");
|
|
|
+ p.put("description", description);
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject integerProp(String description) throws JSONException {
|
|
|
+ JSONObject p = new JSONObject();
|
|
|
+ p.put("type", "integer");
|
|
|
+ p.put("description", description);
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject resource(String uri, String name, String mimeType, String description) throws JSONException {
|
|
|
+ JSONObject r = new JSONObject();
|
|
|
+ r.put("uri", uri);
|
|
|
+ r.put("name", name);
|
|
|
+ r.put("mimeType", mimeType);
|
|
|
+ r.put("description", description);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject resourceContent(String uri, String mimeType, String text) throws JSONException {
|
|
|
+ JSONObject c = new JSONObject();
|
|
|
+ c.put("uri", uri);
|
|
|
+ c.put("mimeType", mimeType);
|
|
|
+ c.put("text", text);
|
|
|
+ return c;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── Response builders ─────────────────────────────────────────
|
|
|
+
|
|
|
+ private Response jsonResponse(Object id, JSONObject result) {
|
|
|
+ JSONObject resp = new JSONObject();
|
|
|
+ try {
|
|
|
+ resp.put("jsonrpc", "2.0");
|
|
|
+ resp.put("id", id);
|
|
|
+ resp.put("result", result);
|
|
|
+ } catch (JSONException ignored) {}
|
|
|
+ return newFixedLengthResponse(Response.Status.OK, "application/json", resp.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ private Response jsonError(Object id, int code, String message) {
|
|
|
+ JSONObject resp = new JSONObject();
|
|
|
+ try {
|
|
|
+ resp.put("jsonrpc", "2.0");
|
|
|
+ resp.put("id", (id != null) ? id : JSONObject.NULL);
|
|
|
+ JSONObject err = new JSONObject();
|
|
|
+ err.put("code", code);
|
|
|
+ err.put("message", message);
|
|
|
+ resp.put("error", err);
|
|
|
+ } catch (JSONException ignored) {}
|
|
|
+ Response.Status status = (code == -32700)
|
|
|
+ ? Response.Status.BAD_REQUEST
|
|
|
+ : Response.Status.INTERNAL_ERROR;
|
|
|
+ return newFixedLengthResponse(status, "application/json", resp.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Gracefully stop the server. */
|
|
|
+ public void stopServer() {
|
|
|
+ initialized = false;
|
|
|
+ stop();
|
|
|
+ }
|
|
|
+}
|