McpServer.java 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. package com.nonvioce.cal;
  2. import android.util.Log;
  3. import fi.iki.elonen.NanoHTTPD;
  4. import org.json.JSONArray;
  5. import org.json.JSONException;
  6. import org.json.JSONObject;
  7. import java.io.IOException;
  8. import java.util.ArrayList;
  9. import java.util.List;
  10. /**
  11. * MCP (Model Context Protocol) server embedded in the Android app.
  12. * Implements JSON-RPC 2.0 over HTTP on localhost, exposing Symja math
  13. * computation as MCP tools and resources.
  14. *
  15. * 与 Ijs 版本的关键区别:本工程剥离了所有插件 / JavaScript 能力(设计决策 Q18:
  16. * 插件是开发者侧的 Dart 编译期模块,MCP 不应具备插件管理与代码执行能力)。
  17. * 因此这里既没有 Rhino,也没有 PluginManager,只剩纯计算工具。
  18. */
  19. public class McpServer extends NanoHTTPD {
  20. private static final String TAG = "McpServer";
  21. private static final String PROTOCOL_VERSION = "2025-03-26";
  22. private static final String SERVER_NAME = "cal-mcp";
  23. private static final String SERVER_VERSION = "1.0";
  24. /**
  25. * 单次求值的超时(毫秒)。
  26. *
  27. * 对齐旧实现 callEvaluate 使用的 15000ms:Symja 的符号计算(积分、解方程)
  28. * 可能很慢,而 MCP 客户端本身也会超时,15s 是"够用又不至于把请求挂死"的折中。
  29. */
  30. private static final long EVAL_TIMEOUT_MS = 15000L;
  31. /** 最近 100 条求值记录,供 history://recent 资源读取。 */
  32. private final List<String> evalHistory = java.util.Collections.synchronizedList(new ArrayList<>());
  33. private volatile boolean initialized = false;
  34. /**
  35. * 监听**所有网卡**(NanoHTTPD 传 port 而不传 hostname 就是通配地址)。
  36. *
  37. * 旧实现(Ijs)绑的是 127.0.0.1,那样只有本机进程或 adb forward 能连上。
  38. * 现在改成全网卡,同网段的 AI 客户端可以直接连 `http://<手机 IP>:8734/mcp`。
  39. *
  40. * 代价要说清楚:**同网段的任何人都能调用这个计算引擎**。服务本身只读 + 求值
  41. * (没有任何写文件或执行代码的能力),但可以被拿来跑重计算(比如难解的积分)
  42. * 占住那条唯一的求值线程。要收回来就改回 super("127.0.0.1", port)。
  43. */
  44. public McpServer(int port) {
  45. super(port);
  46. }
  47. private String sessionId = java.util.UUID.randomUUID().toString();
  48. // ── HTTP routing ──────────────────────────────────────────────
  49. @Override
  50. public Response serve(IHTTPSession session) {
  51. String method = session.getMethod().name();
  52. String uri = session.getUri();
  53. // CORS preflight
  54. if ("OPTIONS".equals(method)) {
  55. Response r = newFixedLengthResponse(Response.Status.OK, "application/json", "{}");
  56. addCommonHeaders(r);
  57. return r;
  58. }
  59. // MCP Streamable HTTP: POST JSON-RPC
  60. if ("POST".equals(method) && ("/".equals(uri) || "/mcp".equals(uri) || uri.startsWith("/mcp"))) {
  61. Response r = handleJsonRpc(session);
  62. addCommonHeaders(r);
  63. r.addHeader("Mcp-Session-Id", sessionId);
  64. return r;
  65. }
  66. // DELETE /mcp terminates session
  67. if ("DELETE".equals(method) && ("/mcp".equals(uri))) {
  68. sessionId = java.util.UUID.randomUUID().toString();
  69. Response r = newFixedLengthResponse(Response.Status.OK, "application/json", "{}");
  70. addCommonHeaders(r);
  71. return r;
  72. }
  73. // GET /mcp — Streamable HTTP server check
  74. if ("GET".equals(method) && ("/mcp".equals(uri) || "/".equals(uri))) {
  75. Response r = newFixedLengthResponse(Response.Status.OK, "application/json",
  76. "{\"status\":\"ok\"}");
  77. addCommonHeaders(r);
  78. return r;
  79. }
  80. // Health check
  81. if ("GET".equals(method) && "/health".equals(uri)) {
  82. Response r = newFixedLengthResponse(Response.Status.OK, "application/json",
  83. "{\"status\":\"ok\",\"initialized\":" + initialized + "}");
  84. addCommonHeaders(r);
  85. return r;
  86. }
  87. return newFixedLengthResponse(Response.Status.NOT_FOUND, "application/json",
  88. "{\"error\":\"not found\"}");
  89. }
  90. private void addCommonHeaders(Response r) {
  91. r.addHeader("Access-Control-Allow-Origin", "*");
  92. r.addHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS");
  93. r.addHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id");
  94. r.addHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
  95. }
  96. // ── JSON-RPC dispatch ─────────────────────────────────────────
  97. private Response handleJsonRpc(IHTTPSession session) {
  98. String body;
  99. try {
  100. body = readBody(session);
  101. } catch (IOException e) {
  102. return jsonError(null, -32700, "Parse error: cannot read body");
  103. }
  104. if (body == null || body.trim().isEmpty()) {
  105. return jsonError(null, -32700, "Parse error: empty body");
  106. }
  107. JSONObject request;
  108. try {
  109. request = new JSONObject(body);
  110. } catch (JSONException e) {
  111. return jsonError(null, -32700, "Parse error: invalid JSON");
  112. }
  113. String method = null;
  114. Object id = null;
  115. try {
  116. String jsonrpc = request.optString("jsonrpc");
  117. if (!"2.0".equals(jsonrpc)) {
  118. return jsonError(null, -32600, "Invalid Request: jsonrpc must be 2.0");
  119. }
  120. method = request.optString("method");
  121. id = request.isNull("id") ? null : request.get("id");
  122. if (method.isEmpty()) {
  123. return jsonError(id, -32600, "Invalid Request: missing method");
  124. }
  125. // Notification (no id) — no response expected
  126. if (id == null) {
  127. handleNotification(method, request.optJSONObject("params"));
  128. return newFixedLengthResponse(Response.Status.NO_CONTENT, "application/json", "");
  129. }
  130. // Regular request
  131. JSONObject result = dispatch(method, request.optJSONObject("params"));
  132. return jsonResponse(id, result);
  133. } catch (JSONException e) {
  134. Log.e(TAG, "JSON-RPC error", e);
  135. return jsonError(id, -32603, "Internal error: " + e.getMessage());
  136. }
  137. }
  138. private JSONObject dispatch(String method, JSONObject params) throws JSONException {
  139. if (params == null) params = new JSONObject();
  140. switch (method) {
  141. case "initialize": return handleInitialize(params);
  142. case "tools/list": return handleToolsList();
  143. case "tools/call": return handleToolsCall(params);
  144. case "resources/list": return handleResourcesList();
  145. case "resources/read": return handleResourcesRead(params);
  146. default:
  147. throw new JSONException("Method not found: " + method);
  148. }
  149. }
  150. private void handleNotification(String method, JSONObject params) {
  151. if ("notifications/initialized".equals(method)) {
  152. initialized = true;
  153. Log.i(TAG, "MCP client initialized");
  154. }
  155. }
  156. // ── initialize ────────────────────────────────────────────────
  157. private JSONObject handleInitialize(JSONObject params) throws JSONException {
  158. JSONObject result = new JSONObject();
  159. result.put("protocolVersion", PROTOCOL_VERSION);
  160. JSONObject capabilities = new JSONObject();
  161. capabilities.put("tools", new JSONObject());
  162. capabilities.put("resources", new JSONObject());
  163. result.put("capabilities", capabilities);
  164. JSONObject serverInfo = new JSONObject();
  165. serverInfo.put("name", SERVER_NAME);
  166. serverInfo.put("version", SERVER_VERSION);
  167. result.put("serverInfo", serverInfo);
  168. Log.i(TAG, "MCP initialize handshake");
  169. return result;
  170. }
  171. // ── tools/list ────────────────────────────────────────────────
  172. private JSONObject handleToolsList() throws JSONException {
  173. JSONArray tools = new JSONArray();
  174. // --- Core computation ---
  175. tools.put(tool("evaluate",
  176. "Evaluate a Symja mathematical expression. Supports calculus, algebra, " +
  177. "linear algebra, number theory, special functions, transforms, and more. " +
  178. "Uses Wolfram/Mathematica-like syntax with square brackets for function calls, " +
  179. "e.g. Integrate[x^2, x], Solve[x^2-5*x+6==0, x], Det[{{1,2},{3,4}}].",
  180. inputSchema(
  181. props().put("expression", stringProp("The Symja expression to evaluate")),
  182. "expression")));
  183. tools.put(tool("differentiate",
  184. "Differentiate an expression with respect to a variable.",
  185. inputSchema(
  186. props()
  187. .put("expression", stringProp("Expression to differentiate, e.g. x^3*Sin[x]"))
  188. .put("variable", stringProp("Variable of differentiation, e.g. x")),
  189. "expression", "variable")));
  190. tools.put(tool("integrate",
  191. "Integrate an expression. Omit bounds for indefinite integration; " +
  192. "provide both lower and upper for definite integration.",
  193. inputSchema(
  194. props()
  195. .put("expression", stringProp("Expression to integrate"))
  196. .put("variable", stringProp("Variable of integration"))
  197. .put("lower", stringProp("Lower bound (optional, omit for indefinite integral)"))
  198. .put("upper", stringProp("Upper bound (optional, omit for indefinite integral)")),
  199. "expression", "variable")));
  200. tools.put(tool("solve",
  201. "Solve an equation or system of equations symbolically.",
  202. inputSchema(
  203. props()
  204. .put("equation", stringProp("Equation to solve, e.g. x^2-5*x+6==0"))
  205. .put("variable", stringProp("Variable to solve for, e.g. x")),
  206. "equation", "variable")));
  207. tools.put(tool("simplify",
  208. "Simplify a mathematical expression algebraically.",
  209. inputSchema(
  210. props().put("expression", stringProp("Expression to simplify, e.g. Sin[x]^2+Cos[x]^2")),
  211. "expression")));
  212. tools.put(tool("numeric",
  213. "Evaluate an expression numerically with specified precision.",
  214. inputSchema(
  215. props()
  216. .put("expression", stringProp("Expression to compute numerically, e.g. Pi"))
  217. .put("precision", integerProp("Number of significant digits (default: 16)")),
  218. "expression")));
  219. tools.put(tool("to_latex",
  220. "Convert a mathematical expression to LaTeX format string.",
  221. inputSchema(
  222. props().put("expression", stringProp("Expression to convert to LaTeX")),
  223. "expression")));
  224. tools.put(tool("session_variables",
  225. "List the currently defined global variables and their values.",
  226. inputSchema(props())));
  227. // --- Plotting ---
  228. tools.put(tool("plot",
  229. "Plot a function f[x] over a range. Returns BOTH: (1) a text block with " +
  230. "structured plot data as JSON {type:'plot', elements:[{type:'line'|'point', " +
  231. "color:'#RRGGBB', points:[[x,y],...]}], options:{...}}, and (2) an image block " +
  232. "with the rendered curve as a PNG, so you can see the plot directly. " +
  233. "Use Symja math syntax.",
  234. inputSchema(
  235. props()
  236. .put("function", stringProp("Function expression, e.g. Sin[x] or x^2"))
  237. .put("variable", stringProp("Variable name (usually x)"))
  238. .put("x_min", stringProp("Lower bound"))
  239. .put("x_max", stringProp("Upper bound")),
  240. "function", "variable", "x_min", "x_max")));
  241. tools.put(tool("parametric_plot",
  242. "Plot a parametric curve (x(t), y(t)). Returns structured plot data as JSON " +
  243. "plus a rendered PNG image (same shape as the plot tool).",
  244. inputSchema(
  245. props()
  246. .put("x_func", stringProp("x(t) expression, e.g. Sin[t]"))
  247. .put("y_func", stringProp("y(t) expression, e.g. Cos[t]"))
  248. .put("t_var", stringProp("Parameter variable (usually t)"))
  249. .put("t_min", stringProp("Lower bound for t"))
  250. .put("t_max", stringProp("Upper bound for t")),
  251. "x_func", "y_func", "t_var", "t_min", "t_max")));
  252. tools.put(tool("list_plot",
  253. "Plot a list of points as a scatter plot. Returns structured plot data as JSON " +
  254. "plus a rendered PNG image (elements are of type 'point').",
  255. inputSchema(
  256. props()
  257. .put("points", stringProp(
  258. "Point list in Symja format: {{x1,y1},{x2,y2},...}")),
  259. "points")));
  260. JSONObject result = new JSONObject();
  261. result.put("tools", tools);
  262. return result;
  263. }
  264. // ── tools/call ────────────────────────────────────────────────
  265. /** 这三个工具的返回值是绘图 JSON,可以再渲染成 PNG。 */
  266. private static boolean isPlotTool(String toolName) {
  267. return "plot".equals(toolName)
  268. || "parametric_plot".equals(toolName)
  269. || "list_plot".equals(toolName);
  270. }
  271. private JSONObject handleToolsCall(JSONObject params) throws JSONException {
  272. String name = params.getString("name");
  273. JSONObject arguments = params.optJSONObject("arguments");
  274. if (arguments == null) arguments = new JSONObject();
  275. String textResult;
  276. switch (name) {
  277. case "evaluate":
  278. textResult = callEvaluate(arguments.getString("expression"));
  279. break;
  280. case "differentiate":
  281. textResult = callDifferentiate(
  282. arguments.getString("expression"),
  283. arguments.getString("variable"));
  284. break;
  285. case "integrate":
  286. textResult = callIntegrate(
  287. arguments.getString("expression"),
  288. arguments.getString("variable"),
  289. arguments.optString("lower", null),
  290. arguments.optString("upper", null));
  291. break;
  292. case "solve":
  293. textResult = callSolve(
  294. arguments.getString("equation"),
  295. arguments.getString("variable"));
  296. break;
  297. case "simplify":
  298. textResult = callSimplify(arguments.getString("expression"));
  299. break;
  300. case "numeric":
  301. textResult = callNumeric(
  302. arguments.getString("expression"),
  303. arguments.optInt("precision", 16));
  304. break;
  305. case "to_latex":
  306. textResult = callToLatex(arguments.getString("expression"));
  307. break;
  308. case "session_variables":
  309. textResult = callSessionVariables();
  310. break;
  311. case "plot":
  312. textResult = callPlot(
  313. arguments.getString("function"),
  314. arguments.getString("variable"),
  315. arguments.getString("x_min"),
  316. arguments.getString("x_max"));
  317. break;
  318. case "parametric_plot":
  319. textResult = callParametricPlot(
  320. arguments.getString("x_func"),
  321. arguments.getString("y_func"),
  322. arguments.getString("t_var"),
  323. arguments.getString("t_min"),
  324. arguments.getString("t_max"));
  325. break;
  326. case "list_plot":
  327. textResult = callListPlot(arguments.getString("points"));
  328. break;
  329. default:
  330. throw new JSONException("Unknown tool: " + name);
  331. }
  332. JSONArray content = new JSONArray();
  333. JSONObject item = new JSONObject();
  334. item.put("type", "text");
  335. item.put("text", textResult);
  336. content.put(item);
  337. // 绘图工具额外附一张 PNG —— 客户端既能拿坐标自己处理,也能直接"看到"图。
  338. // 渲染失败就只回坐标,不影响调用成功。
  339. if (isPlotTool(name) && textResult.startsWith("{")) {
  340. String png = PlotImage.renderPngBase64(textResult, 640, 400);
  341. if (png != null) {
  342. JSONObject img = new JSONObject();
  343. img.put("type", "image");
  344. img.put("data", png);
  345. img.put("mimeType", "image/png");
  346. content.put(img);
  347. }
  348. }
  349. JSONObject result = new JSONObject();
  350. result.put("content", content);
  351. // MCP spec: isError at result top level
  352. if (textResult.startsWith("Error:") || textResult.startsWith("错误:")) {
  353. result.put("isError", true);
  354. }
  355. return result;
  356. }
  357. // ── Tool implementations ──────────────────────────────────────
  358. /**
  359. * 求值。全部计算都委托给 SymjaBridge 的单线程 worker(Symja 非线程安全,
  360. * 见 SymjaBridge 类注释)。evaluateSync 不更新 % (lastOutput),所以 MCP
  361. * 调用不会顶掉界面上用户按 ANS 拿到的东西。
  362. */
  363. private String callEvaluate(String expression) {
  364. String result = SymjaBridge.INSTANCE.evaluateSync(expression, EVAL_TIMEOUT_MS);
  365. synchronized (evalHistory) {
  366. if (evalHistory.size() >= 100) evalHistory.remove(0);
  367. evalHistory.add(expression + " → " + result);
  368. }
  369. return result;
  370. }
  371. private String callDifferentiate(String expression, String variable) {
  372. return SymjaBridge.INSTANCE.evaluateSync(
  373. "D[" + expression + ", " + variable + "]", EVAL_TIMEOUT_MS);
  374. }
  375. private String callIntegrate(String expression, String variable, String lower, String upper) {
  376. String expr;
  377. if (lower != null && !lower.isEmpty() && upper != null && !upper.isEmpty()) {
  378. expr = "Integrate[" + expression + ", {" + variable + ", " + lower + ", " + upper + "}]";
  379. } else {
  380. expr = "Integrate[" + expression + ", " + variable + "]";
  381. }
  382. return SymjaBridge.INSTANCE.evaluateSync(expr, EVAL_TIMEOUT_MS);
  383. }
  384. private String callSolve(String equation, String variable) {
  385. return SymjaBridge.INSTANCE.evaluateSync(
  386. "Solve[" + equation + ", " + variable + "]", EVAL_TIMEOUT_MS);
  387. }
  388. private String callSimplify(String expression) {
  389. return SymjaBridge.INSTANCE.evaluateSync(
  390. "Simplify[" + expression + "]", EVAL_TIMEOUT_MS);
  391. }
  392. private String callNumeric(String expression, int precision) {
  393. return SymjaBridge.INSTANCE.evaluateSync(
  394. "N[" + expression + ", " + precision + "]", EVAL_TIMEOUT_MS);
  395. }
  396. /**
  397. * 转 LaTeX。
  398. *
  399. * 保留 ##TEXFORM## 前缀:控制台据此识别并用 KaTeX 渲染,这是旧实现约定的格式。
  400. * 注意 TeXForm 若失败,结果会以 "Error: " 开头,前缀会盖住它,isError 因而
  401. * 检测不到——这与旧行为完全一致,不做改动以免破坏既有前端逻辑。
  402. */
  403. private String callToLatex(String expression) {
  404. String result = SymjaBridge.INSTANCE.evaluateSync(
  405. "TeXForm[" + expression + "]", EVAL_TIMEOUT_MS);
  406. return "##TEXFORM##" + result;
  407. }
  408. /** 当前全局变量,SymjaBridge 输出 "名字 = 值" 文本,一行一个。 */
  409. private String callSessionVariables() {
  410. return SymjaBridge.INSTANCE.variablesSync();
  411. }
  412. private String callPlot(String function, String variable, String xMin, String xMax) {
  413. String expr = "Plot[" + function + ", {" + variable + ", " + xMin + ", " + xMax + "}]";
  414. // plotSync 会把 Symja 的 Graphics 表达式树转成 {type:"plot", elements:[...]} JSON
  415. return SymjaBridge.INSTANCE.plotSync(expr, EVAL_TIMEOUT_MS);
  416. }
  417. private String callParametricPlot(String xFunc, String yFunc, String tVar, String tMin, String tMax) {
  418. String expr = "ParametricPlot[{" + xFunc + ", " + yFunc + "}, {" + tVar + ", " + tMin + ", " + tMax + "}]";
  419. return SymjaBridge.INSTANCE.plotSync(expr, EVAL_TIMEOUT_MS);
  420. }
  421. private String callListPlot(String points) {
  422. return SymjaBridge.INSTANCE.plotSync(
  423. "ListPlot[" + points + "]", EVAL_TIMEOUT_MS);
  424. }
  425. // ── resources/list ────────────────────────────────────────────
  426. private JSONObject handleResourcesList() throws JSONException {
  427. JSONArray resources = new JSONArray();
  428. resources.put(resource("variables://current",
  429. "Defined Variables",
  430. "text/plain",
  431. "Currently defined variables and their values in the Symja session"));
  432. resources.put(resource("history://recent",
  433. "Recent MCP Evaluations",
  434. "application/json",
  435. "Recent expressions evaluated via MCP and their results"));
  436. JSONObject result = new JSONObject();
  437. result.put("resources", resources);
  438. return result;
  439. }
  440. // ── resources/read ────────────────────────────────────────────
  441. private JSONObject handleResourcesRead(JSONObject params) throws JSONException {
  442. String uri = params.getString("uri");
  443. JSONArray contents = new JSONArray();
  444. switch (uri) {
  445. case "variables://current": {
  446. String vars = SymjaBridge.INSTANCE.variablesSync();
  447. contents.put(resourceContent(uri, "text/plain", vars));
  448. break;
  449. }
  450. case "history://recent": {
  451. JSONArray historyJson = new JSONArray();
  452. for (String entry : evalHistory) {
  453. historyJson.put(entry);
  454. }
  455. contents.put(resourceContent(uri, "application/json", historyJson.toString(2)));
  456. break;
  457. }
  458. default:
  459. throw new JSONException("Unknown resource URI: " + uri);
  460. }
  461. JSONObject result = new JSONObject();
  462. result.put("contents", contents);
  463. return result;
  464. }
  465. // ── HTTP helpers ──────────────────────────────────────────────
  466. private String readBody(IHTTPSession session) throws IOException {
  467. String contentLength = session.getHeaders().get("content-length");
  468. if (contentLength == null) return null;
  469. int len = Integer.parseInt(contentLength);
  470. if (len == 0) return null;
  471. byte[] buf = new byte[len];
  472. int total = 0;
  473. while (total < len) {
  474. int r = session.getInputStream().read(buf, total, len - total);
  475. if (r == -1) break;
  476. total += r;
  477. }
  478. return new String(buf, 0, total, "UTF-8");
  479. }
  480. // ── JSON builders ─────────────────────────────────────────────
  481. /** Create a complete tool object with name, description, and inputSchema. */
  482. private JSONObject tool(String name, String description, JSONObject inputSchema) throws JSONException {
  483. JSONObject t = new JSONObject();
  484. t.put("name", name);
  485. t.put("description", description);
  486. t.put("inputSchema", inputSchema);
  487. return t;
  488. }
  489. /** Build inputSchema: {type:object, properties:{...}, required:[...]} */
  490. private JSONObject inputSchema(JSONObject properties, String... required) throws JSONException {
  491. JSONObject schema = new JSONObject();
  492. schema.put("type", "object");
  493. schema.put("properties", properties);
  494. JSONArray req = new JSONArray();
  495. for (String r : required) req.put(r);
  496. schema.put("required", req);
  497. return schema;
  498. }
  499. /** Start building a properties object. Call .put() on the result. */
  500. private JSONObject props() {
  501. return new JSONObject();
  502. }
  503. private JSONObject stringProp(String description) throws JSONException {
  504. JSONObject p = new JSONObject();
  505. p.put("type", "string");
  506. p.put("description", description);
  507. return p;
  508. }
  509. private JSONObject integerProp(String description) throws JSONException {
  510. JSONObject p = new JSONObject();
  511. p.put("type", "integer");
  512. p.put("description", description);
  513. return p;
  514. }
  515. private JSONObject resource(String uri, String name, String mimeType, String description) throws JSONException {
  516. JSONObject r = new JSONObject();
  517. r.put("uri", uri);
  518. r.put("name", name);
  519. r.put("mimeType", mimeType);
  520. r.put("description", description);
  521. return r;
  522. }
  523. private JSONObject resourceContent(String uri, String mimeType, String text) throws JSONException {
  524. JSONObject c = new JSONObject();
  525. c.put("uri", uri);
  526. c.put("mimeType", mimeType);
  527. c.put("text", text);
  528. return c;
  529. }
  530. // ── Response builders ─────────────────────────────────────────
  531. private Response jsonResponse(Object id, JSONObject result) {
  532. JSONObject resp = new JSONObject();
  533. try {
  534. resp.put("jsonrpc", "2.0");
  535. resp.put("id", id);
  536. resp.put("result", result);
  537. } catch (JSONException ignored) {}
  538. return newFixedLengthResponse(Response.Status.OK, "application/json", resp.toString());
  539. }
  540. private Response jsonError(Object id, int code, String message) {
  541. JSONObject resp = new JSONObject();
  542. try {
  543. resp.put("jsonrpc", "2.0");
  544. resp.put("id", (id != null) ? id : JSONObject.NULL);
  545. JSONObject err = new JSONObject();
  546. err.put("code", code);
  547. err.put("message", message);
  548. resp.put("error", err);
  549. } catch (JSONException ignored) {}
  550. Response.Status status = (code == -32700)
  551. ? Response.Status.BAD_REQUEST
  552. : Response.Status.INTERNAL_ERROR;
  553. return newFixedLengthResponse(status, "application/json", resp.toString());
  554. }
  555. /** Gracefully stop the server. */
  556. public void stopServer() {
  557. initialized = false;
  558. stop();
  559. }
  560. }