ソースを参照

feat: MCP server + UI overhaul + 14 default plugins

MCP Server:
- McpServer.java: JSON-RPC 2.0 protocol on NanoHTTPd, 12 tools + 2 resources
- McpService.java: Android foreground Service on port 8734
- AppSession.java: shared SymjaEngine between WebView and MCP
- Rhino JS engine for plugin execution via MCP

UI Improvements:
- Header nav bar with app name, help/plugins buttons, MCP status indicator
- Unified blue color scheme (replaced pink sakura theme)
- Send button in input area
- Removed Android ActionBar (NoActionBar theme)
- Fixed --primary-color to blue #1976D2

Code Cleanup:
- Removed 30+ non-working Symja functions from autocomplete and help
- FourierTransform, Combinations, NDSolve, RSolve, String* etc.

14 Default Plugins (from mathtool2):
- Statistics: chi_square, binomial_distribution, hypergeometric_distribution,
  linear_regression, mixed_variance, histogram_stats
- Probability: convolution
- Sequence: cuo_wei_xiang_jian
- Geometry: symmetry_pl/lp/ll, circle_equation
- Calculus: bisection_method
- Vector: vector_normal (with Simplify for RationalExtend domain)

Tests:
- McpProtocolTest.java: 21 JUnit tests (Symja engine + JSON-RPC)
- test_mcp.sh: curl-based integration test script
nonvioce 3 ヶ月 前
親
コミット
6694335a19
31 ファイル変更、1865 行追加、91 行削除
  1. 9 2
      app/build.gradle.kts
  2. 14 0
      app/src/main/AndroidManifest.xml
  3. 92 45
      app/src/main/assets/console.css
  4. 25 5
      app/src/main/assets/console.html
  5. 31 1
      app/src/main/assets/console.js
  6. 12 0
      app/src/main/assets/plugins/binomial_distribution.json
  7. 15 0
      app/src/main/assets/plugins/bisection_method.json
  8. 14 0
      app/src/main/assets/plugins/chi_square.json
  9. 16 0
      app/src/main/assets/plugins/circle_equation.json
  10. 14 0
      app/src/main/assets/plugins/convolution.json
  11. 14 0
      app/src/main/assets/plugins/cuo_wei_xiang_jian.json
  12. 14 0
      app/src/main/assets/plugins/histogram_stats.json
  13. 13 0
      app/src/main/assets/plugins/hypergeometric_distribution.json
  14. 12 0
      app/src/main/assets/plugins/linear_regression.json
  15. 13 0
      app/src/main/assets/plugins/mixed_variance.json
  16. 16 0
      app/src/main/assets/plugins/symmetry_ll.json
  17. 15 0
      app/src/main/assets/plugins/symmetry_lp.json
  18. 15 0
      app/src/main/assets/plugins/symmetry_pl.json
  19. 19 0
      app/src/main/assets/plugins/vector_normal.json
  20. 25 0
      app/src/main/java/com/nonvioce/ijs/AppSession.java
  21. 85 16
      app/src/main/java/com/nonvioce/ijs/MainActivity.java
  22. 753 0
      app/src/main/java/com/nonvioce/ijs/McpServer.java
  23. 171 0
      app/src/main/java/com/nonvioce/ijs/McpService.java
  24. 26 1
      app/src/main/java/com/nonvioce/ijs/PluginManager.java
  25. 9 15
      app/src/main/java/com/nonvioce/ijs/SymjaEngine.java
  26. 13 0
      app/src/main/res/layout/activity_main.xml
  27. 2 0
      app/src/main/res/values/colors.xml
  28. 4 0
      app/src/main/res/values/strings.xml
  29. 5 6
      app/src/main/res/values/themes.xml
  30. 209 0
      app/src/test/java/com/nonvioce/ijs/McpProtocolTest.java
  31. 190 0
      test_mcp.sh

+ 9 - 2
app/build.gradle.kts

@@ -108,17 +108,24 @@ dependencies {
     implementation("org.hipparchus:hipparchus-ode:3.1")
     implementation("org.hipparchus:hipparchus-ode:3.1")
     implementation("org.hipparchus:hipparchus-optim:3.1")
     implementation("org.hipparchus:hipparchus-optim:3.1")
     implementation("org.hipparchus:hipparchus-stat:3.1")
     implementation("org.hipparchus:hipparchus-stat:3.1")
-    implementation("org.apfloat:apfloat:1.14.0")
     implementation("org.jgrapht:jgrapht-core:1.5.2")
     implementation("org.jgrapht:jgrapht-core:1.5.2")
     implementation("org.jgrapht:jgrapht-io:1.5.2")
     implementation("org.jgrapht:jgrapht-io:1.5.2")
+    implementation("org.apfloat:apfloat:1.16.0")
     
     
     // 可选:GPL 模块,包含额外的数学功能
     // 可选:GPL 模块,包含额外的数学功能
     // implementation("org.matheclipse:matheclipse-gpl:3.1.0")
     // implementation("org.matheclipse:matheclipse-gpl:3.1.0")
     
     
+    // MCP 服务器:轻量级 HTTP 服务器
+    implementation("org.nanohttpd:nanohttpd:2.3.1")
+
+    // MCP 服务器:JavaScript 引擎(用于插件执行)
+    implementation("org.mozilla:rhino:1.7.15")
+
     // 可选:IO 模块,用于更多输入输出功能
     // 可选:IO 模块,用于更多输入输出功能
     // implementation("org.matheclipse:matheclipse-io:3.1.0")
     // implementation("org.matheclipse:matheclipse-io:3.1.0")
-    
+
     testImplementation(libs.junit)
     testImplementation(libs.junit)
+    testImplementation("org.json:json:20231013")  // JSON support in JVM unit tests
     androidTestImplementation(libs.androidx.junit)
     androidTestImplementation(libs.androidx.junit)
     androidTestImplementation(libs.androidx.espresso.core)
     androidTestImplementation(libs.androidx.espresso.core)
 }
 }

+ 14 - 0
app/src/main/AndroidManifest.xml

@@ -8,6 +8,11 @@
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
         android:maxSdkVersion="32" />
         android:maxSdkVersion="32" />
 
 
+    <!-- MCP 服务器需要的权限 -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+
     <application
     <application
         android:allowBackup="true"
         android:allowBackup="true"
         android:dataExtractionRules="@xml/data_extraction_rules"
         android:dataExtractionRules="@xml/data_extraction_rules"
@@ -26,6 +31,15 @@
                 <category android:name="android.intent.category.LAUNCHER" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
             </intent-filter>
         </activity>
         </activity>
+
+        <service
+            android:name=".McpService"
+            android:exported="false"
+            android:foregroundServiceType="specialUse">
+            <property
+                android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+                android:value="MCP_server_for_symbolic_math_protocol_access" />
+        </service>
         
         
         <provider
         <provider
             android:name="androidx.core.content.FileProvider"
             android:name="androidx.core.content.FileProvider"

+ 92 - 45
app/src/main/assets/console.css

@@ -97,8 +97,9 @@ body {
     color: var(--primary-color);
     color: var(--primary-color);
 }
 }
 
 
-.nav-brand .material-icons {
-    font-size: 28px;
+.nav-icon {
+    font-size: 24px;
+    line-height: 1;
 }
 }
 
 
 .brand-text {
 .brand-text {
@@ -106,6 +107,24 @@ body {
     letter-spacing: 0.5px;
     letter-spacing: 0.5px;
 }
 }
 
 
+/* MCP status indicator */
+.mcp-status {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 12px;
+    height: 12px;
+    border-radius: 50%;
+    margin-right: 8px;
+    background: #9E9E9E;
+    transition: background 0.3s;
+    cursor: default;
+}
+.mcp-status.on {
+    background: #4CAF50;
+    box-shadow: 0 0 6px rgba(76,175,80,0.5);
+}
+
 .nav-actions {
 .nav-actions {
     display: flex;
     display: flex;
     gap: 8px;
     gap: 8px;
@@ -432,6 +451,34 @@ body {
     background: var(--terminal-selection);
     background: var(--terminal-selection);
 }
 }
 
 
+/* 发送按钮 */
+.send-btn {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 40px;
+    height: 40px;
+    border: none;
+    border-radius: 50%;
+    background: var(--primary-color);
+    color: #FFFFFF;
+    cursor: pointer;
+    flex-shrink: 0;
+    transition: all 0.2s;
+}
+.send-btn:hover {
+    background: var(--primary-dark);
+    transform: scale(1.05);
+}
+.send-btn:active {
+    transform: scale(0.95);
+}
+.send-icon {
+    font-size: 20px;
+    font-weight: 700;
+    line-height: 1;
+}
+
 /* 自动补全列表 */
 /* 自动补全列表 */
 .completion-list {
 .completion-list {
     display: none;
     display: none;
@@ -618,9 +665,9 @@ body {
     align-items: center;
     align-items: center;
     justify-content: flex-end;
     justify-content: flex-end;
     padding: 12px 16px;
     padding: 12px 16px;
-    background: #FFB7C5;
+    background: #1976D2;
     color: #FFFFFF;
     color: #FFFFFF;
-    border-bottom: 1px solid #FF9FB0;
+    border-bottom: 1px solid #0D47A1;
 }
 }
 
 
 .close-help-btn {
 .close-help-btn {
@@ -674,7 +721,7 @@ body {
 .help-table thead {
 .help-table thead {
     position: sticky;
     position: sticky;
     top: 0;
     top: 0;
-    background: #FFB7C5;
+    background: #1976D2;
     color: #FFFFFF;
     color: #FFFFFF;
     z-index: 10;
     z-index: 10;
 }
 }
@@ -683,7 +730,7 @@ body {
     padding: 12px 16px;
     padding: 12px 16px;
     text-align: left;
     text-align: left;
     font-weight: 500;
     font-weight: 500;
-    border-bottom: 2px solid #FF9FB0;
+    border-bottom: 2px solid #0D47A1;
 }
 }
 
 
 .help-table td {
 .help-table td {
@@ -706,7 +753,7 @@ body {
 }
 }
 
 
 .help-table tbody tr:hover td {
 .help-table tbody tr:hover td {
-    background: #FFE4E9;
+    background: #BBDEFB;
 }
 }
 
 
 /* 插件面板 - 保持原样但调整颜色 */
 /* 插件面板 - 保持原样但调整颜色 */
@@ -807,7 +854,7 @@ body {
     width: 90%;
     width: 90%;
     max-width: 500px;
     max-width: 500px;
     height: 100vh;
     height: 100vh;
-    background: #FFF5F7;
+    background: #E3F2FD;
     box-shadow: -4px 0 16px rgba(255, 183, 197, 0.3);
     box-shadow: -4px 0 16px rgba(255, 183, 197, 0.3);
     transition: right 0.3s ease-out;
     transition: right 0.3s ease-out;
     z-index: 1000;
     z-index: 1000;
@@ -824,9 +871,9 @@ body {
     align-items: center;
     align-items: center;
     justify-content: space-between;
     justify-content: space-between;
     padding: 20px 24px;
     padding: 20px 24px;
-    background: #FFB7C5;
+    background: #1976D2;
     color: #FFFFFF;
     color: #FFFFFF;
-    border-bottom: 1px solid #FF9FB0;
+    border-bottom: 1px solid #0D47A1;
 }
 }
 
 
 .plugin-panel-header h2 {
 .plugin-panel-header h2 {
@@ -852,8 +899,8 @@ body {
 
 
 .plugin-panel-tabs {
 .plugin-panel-tabs {
     display: flex;
     display: flex;
-    background: #FFE4E9;
-    border-bottom: 1px solid #FFCDD6;
+    background: #BBDEFB;
+    border-bottom: 1px solid #90CAF9;
 }
 }
 
 
 .plugin-tab {
 .plugin-tab {
@@ -861,7 +908,7 @@ body {
     padding: 12px 16px;
     padding: 12px 16px;
     border: none;
     border: none;
     background: transparent;
     background: transparent;
-    color: #8B5A6B;
+    color: #1565C0;
     font-size: 14px;
     font-size: 14px;
     font-weight: 500;
     font-weight: 500;
     cursor: pointer;
     cursor: pointer;
@@ -874,9 +921,9 @@ body {
 }
 }
 
 
 .plugin-tab.active {
 .plugin-tab.active {
-    color: #D84A6B;
-    border-bottom-color: #FFB7C5;
-    background: #FFF5F7;
+    color: #1976D2;
+    border-bottom-color: #1976D2;
+    background: #E3F2FD;
 }
 }
 
 
 .tab-content {
 .tab-content {
@@ -902,15 +949,15 @@ body {
     gap: 16px;
     gap: 16px;
     padding: 16px;
     padding: 16px;
     background: #FFFFFF;
     background: #FFFFFF;
-    border: 1px solid #FFCDD6;
+    border: 1px solid #90CAF9;
     border-radius: 8px;
     border-radius: 8px;
     cursor: pointer;
     cursor: pointer;
     transition: all 0.2s;
     transition: all 0.2s;
 }
 }
 
 
 .plugin-item:hover {
 .plugin-item:hover {
-    background: #FFE4E9;
-    border-color: #FFB7C5;
+    background: #BBDEFB;
+    border-color: #1976D2;
     transform: translateY(-2px);
     transform: translateY(-2px);
     box-shadow: 0 4px 8px rgba(255, 183, 197, 0.2);
     box-shadow: 0 4px 8px rgba(255, 183, 197, 0.2);
 }
 }
@@ -927,19 +974,19 @@ body {
 .plugin-name {
 .plugin-name {
     font-size: 16px;
     font-size: 16px;
     font-weight: 500;
     font-weight: 500;
-    color: #5A3A47;
+    color: #0D47A1;
     margin-bottom: 4px;
     margin-bottom: 4px;
 }
 }
 
 
 .plugin-desc {
 .plugin-desc {
     font-size: 13px;
     font-size: 13px;
-    color: #8B5A6B;
+    color: #1565C0;
     line-height: 1.4;
     line-height: 1.4;
 }
 }
 
 
 .plugin-command {
 .plugin-command {
     font-size: 12px;
     font-size: 12px;
-    color: #D84A6B;
+    color: #1976D2;
     margin-top: 4px;
     margin-top: 4px;
     font-family: var(--font-mono);
     font-family: var(--font-mono);
 }
 }
@@ -947,7 +994,7 @@ body {
 .no-plugins {
 .no-plugins {
     text-align: center;
     text-align: center;
     padding: 40px 20px;
     padding: 40px 20px;
-    color: #C99AA8;
+    color: #90CAF9;
     font-size: 14px;
     font-size: 14px;
 }
 }
 
 
@@ -1056,18 +1103,18 @@ body {
     padding: 16px;
     padding: 16px;
     background: #FFFFFF;
     background: #FFFFFF;
     border-radius: 8px;
     border-radius: 8px;
-    border: 1px solid #FFCDD6;
+    border: 1px solid #90CAF9;
 }
 }
 
 
 .import-section h3 {
 .import-section h3 {
-    color: #D84A6B;
+    color: #1976D2;
     font-size: 16px;
     font-size: 16px;
     font-weight: 500;
     font-weight: 500;
     margin-bottom: 8px;
     margin-bottom: 8px;
 }
 }
 
 
 .import-section p {
 .import-section p {
-    color: #8B5A6B;
+    color: #1565C0;
     font-size: 13px;
     font-size: 13px;
     margin-bottom: 12px;
     margin-bottom: 12px;
 }
 }
@@ -1075,7 +1122,7 @@ body {
 .share-select {
 .share-select {
     width: 100%;
     width: 100%;
     padding: 10px 12px;
     padding: 10px 12px;
-    border: 1px solid #FFCDD6;
+    border: 1px solid #90CAF9;
     border-radius: 6px;
     border-radius: 6px;
     background: #FFFFFF;
     background: #FFFFFF;
     font-size: 14px;
     font-size: 14px;
@@ -1086,14 +1133,14 @@ body {
 
 
 .share-select:focus {
 .share-select:focus {
     outline: none;
     outline: none;
-    border-color: #FFB7C5;
+    border-color: #1976D2;
 }
 }
 
 
 .share-textarea {
 .share-textarea {
     width: 100%;
     width: 100%;
     min-height: 150px;
     min-height: 150px;
     padding: 12px;
     padding: 12px;
-    border: 1px solid #FFCDD6;
+    border: 1px solid #90CAF9;
     border-radius: 6px;
     border-radius: 6px;
     font-family: var(--font-mono);
     font-family: var(--font-mono);
     font-size: 12px;
     font-size: 12px;
@@ -1104,14 +1151,14 @@ body {
 
 
 .share-textarea:focus {
 .share-textarea:focus {
     outline: none;
     outline: none;
-    border-color: #FFB7C5;
+    border-color: #1976D2;
 }
 }
 
 
 .action-btn {
 .action-btn {
     padding: 10px 20px;
     padding: 10px 20px;
     border: none;
     border: none;
     border-radius: 6px;
     border-radius: 6px;
-    background: #FFB7C5;
+    background: #1976D2;
     color: #FFFFFF;
     color: #FFFFFF;
     font-size: 14px;
     font-size: 14px;
     font-weight: 500;
     font-weight: 500;
@@ -1121,25 +1168,25 @@ body {
 }
 }
 
 
 .action-btn:hover {
 .action-btn:hover {
-    background: #FF9FB0;
+    background: #0D47A1;
     transform: translateY(-1px);
     transform: translateY(-1px);
     box-shadow: 0 2px 4px rgba(255, 183, 197, 0.3);
     box-shadow: 0 2px 4px rgba(255, 183, 197, 0.3);
 }
 }
 
 
 .action-btn.secondary {
 .action-btn.secondary {
-    background: #FFC9D4;
+    background: #64B5F6;
 }
 }
 
 
 .action-btn.secondary:hover {
 .action-btn.secondary:hover {
-    background: #FFB7C5;
+    background: #1976D2;
 }
 }
 
 
 .action-btn.danger {
 .action-btn.danger {
-    background: #FF6B8A;
+    background: #EF5350;
 }
 }
 
 
 .action-btn.danger:hover {
 .action-btn.danger:hover {
-    background: #FF5278;
+    background: #E53935;
 }
 }
 
 
 .plugin-path {
 .plugin-path {
@@ -1196,7 +1243,7 @@ body {
     align-items: center;
     align-items: center;
     justify-content: space-between;
     justify-content: space-between;
     padding: 20px 24px;
     padding: 20px 24px;
-    background: #FFB7C5;
+    background: #1976D2;
     color: #FFFFFF;
     color: #FFFFFF;
 }
 }
 
 
@@ -1207,10 +1254,10 @@ body {
 
 
 .plugin-dialog-desc {
 .plugin-dialog-desc {
     padding: 16px 24px;
     padding: 16px 24px;
-    color: #8B5A6B;
+    color: #1565C0;
     font-size: 14px;
     font-size: 14px;
-    border-bottom: 1px solid #FFCDD6;
-    background: #FFF5F7;
+    border-bottom: 1px solid #90CAF9;
+    background: #E3F2FD;
 }
 }
 
 
 .plugin-inputs {
 .plugin-inputs {
@@ -1298,17 +1345,17 @@ body {
 
 
 .plot-info {
 .plot-info {
     padding: 12px 24px;
     padding: 12px 24px;
-    background: #FFE4E9;
-    color: #8B5A6B;
+    background: #BBDEFB;
+    color: #1565C0;
     font-family: var(--font-mono);
     font-family: var(--font-mono);
     font-size: 12px;
     font-size: 12px;
-    border-bottom: 1px solid #FFCDD6;
+    border-bottom: 1px solid #90CAF9;
     text-align: center;
     text-align: center;
 }
 }
 
 
 .plot-container {
 .plot-container {
     padding: 20px;
     padding: 20px;
-    background: #FFF5F7;
+    background: #E3F2FD;
     display: flex;
     display: flex;
     justify-content: center;
     justify-content: center;
     align-items: center;
     align-items: center;
@@ -1317,7 +1364,7 @@ body {
 #plotCanvas {
 #plotCanvas {
     max-width: 100%;
     max-width: 100%;
     height: auto;
     height: auto;
-    border: 2px solid #FFCDD6;
+    border: 2px solid #90CAF9;
     border-radius: 8px;
     border-radius: 8px;
     box-shadow: 0 4px 12px rgba(255, 183, 197, 0.2);
     box-shadow: 0 4px 12px rgba(255, 183, 197, 0.2);
 }
 }

+ 25 - 5
app/src/main/assets/console.html

@@ -14,6 +14,23 @@
 <body>
 <body>
     <!-- 主容器 -->
     <!-- 主容器 -->
     <div class="app-container">
     <div class="app-container">
+        <!-- 顶部导航栏 -->
+        <header class="top-nav">
+            <div class="nav-brand">
+                <span class="nav-icon">🧮</span>
+                <span class="brand-text">某人的计算器</span>
+            </div>
+            <div class="nav-actions">
+                <span id="mcpStatus" class="mcp-status" title="MCP 服务器状态">⚪</span>
+                <button class="nav-btn" id="helpBtn" title="帮助">
+                    <span class="icon-symbol">?</span>
+                </button>
+                <button class="nav-btn" id="pluginsBtn" title="插件">
+                    <span class="icon-symbol">🔌</span>
+                </button>
+            </div>
+        </header>
+
         <!-- 终端容器 -->
         <!-- 终端容器 -->
         <div class="terminal-container">
         <div class="terminal-container">
             <!-- 终端输出区域 -->
             <!-- 终端输出区域 -->
@@ -22,13 +39,16 @@
             <!-- 终端输入区域 -->
             <!-- 终端输入区域 -->
             <div class="terminal-input-line">
             <div class="terminal-input-line">
                 <span class="terminal-prompt" id="inputPrompt">[1]:</span>
                 <span class="terminal-prompt" id="inputPrompt">[1]:</span>
-                <input type="text" 
-                       id="input" 
-                       class="terminal-input" 
-                       placeholder="输入命令或表达式..." 
-                       autofocus 
+                <input type="text"
+                       id="input"
+                       class="terminal-input"
+                       placeholder="输入数学表达式,如 D[x^2, x]..."
+                       autofocus
                        autocomplete="off"
                        autocomplete="off"
                        spellcheck="false">
                        spellcheck="false">
+                <button id="sendBtn" class="send-btn" title="执行 (Enter)">
+                    <span class="send-icon">↵</span>
+                </button>
             </div>
             </div>
             
             
             <!-- 自动补全列表 -->
             <!-- 自动补全列表 -->

+ 31 - 1
app/src/main/assets/console.js

@@ -39,6 +39,20 @@ class SymjaConsole {
         // 输入框事件
         // 输入框事件
         this.input.addEventListener('keydown', (e) => this.handleKeyDown(e));
         this.input.addEventListener('keydown', (e) => this.handleKeyDown(e));
         this.input.addEventListener('input', () => this.handleInput());
         this.input.addEventListener('input', () => this.handleInput());
+
+        // 发送按钮
+        const sendBtn = document.getElementById('sendBtn');
+        if (sendBtn) {
+            sendBtn.addEventListener('click', () => this.evaluate());
+        }
+
+        // 顶部导航按钮
+        this.helpBtn?.addEventListener('click', () => this.showHelp());
+        this.pluginsBtn?.addEventListener('click', () => this.showPluginPanel());
+
+        // MCP 状态更新
+        this.updateMcpStatus();
+        setInterval(() => this.updateMcpStatus(), 3000);
         
         
         // 快捷键工具栏
         // 快捷键工具栏
         this.initShortcutsBar();
         this.initShortcutsBar();
@@ -1740,7 +1754,7 @@ class SymjaConsole {
         if (this.input) {
         if (this.input) {
             this.input.blur();
             this.input.blur();
         }
         }
-        
+
         // 调用 Android 接口隐藏键盘
         // 调用 Android 接口隐藏键盘
         try {
         try {
             if (typeof Android !== 'undefined' && Android.hideKeyboard) {
             if (typeof Android !== 'undefined' && Android.hideKeyboard) {
@@ -1750,6 +1764,22 @@ class SymjaConsole {
             console.log('hideKeyboard not available:', e);
             console.log('hideKeyboard not available:', e);
         }
         }
     }
     }
+
+    updateMcpStatus() {
+        const status = document.getElementById('mcpStatus');
+        if (!status) return;
+        try {
+            if (typeof Android !== 'undefined' && Android.isMcpRunning && Android.isMcpRunning()) {
+                status.className = 'mcp-status on';
+                status.title = 'MCP 服务器运行中';
+            } else {
+                status.className = 'mcp-status';
+                status.title = 'MCP 服务器未启动';
+            }
+        } catch (e) {
+            status.className = 'mcp-status';
+        }
+    }
 }
 }
 
 
 // 初始化控制台
 // 初始化控制台

+ 12 - 0
app/src/main/assets/plugins/binomial_distribution.json

@@ -0,0 +1,12 @@
+{
+  "id": "binomial_distribution",
+  "name": "二项分布",
+  "icon": "🎲",
+  "command": "binom",
+  "description": "计算二项分布X~B(n,p)的期望、方差和分布列",
+  "inputs": [
+    {"name": "n", "label": "试验次数n", "defaultValue": "5"},
+    {"name": "p", "label": "成功概率p (分数或小数)", "defaultValue": "1/2"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var n = parseInt(inputs.n), p = inputs.p;\n  var EX = Symja.eval(n + '*(' + p + ')');\n  var q = Symja.eval('1-(' + p + ')');\n  var DX = Symja.eval(n + '*(' + p + ')*(' + q + ')');\n  Symja.print('X ~ B(' + n + ', ' + p + ')');\n  Symja.print('E(X) = ' + EX + ' = ' + Symja.eval('N[' + EX + ',6]'));\n  Symja.print('D(X) = ' + DX + ' = ' + Symja.eval('N[' + DX + ',6]'));\n  Symja.print('');\n  Symja.print('分布列 P(X=k):');\n  for (var k = 0; k <= n; k++) {\n    var comb = Symja.eval('Binomial[' + n + ',' + k + ']');\n    var pk = Symja.eval(comb + '*(' + p + ')^' + k + '*(' + q + ')^(' + n + '-' + k + ')');\n    var pkNum = Symja.eval('N[' + pk + ',6]');\n    Symja.print('  k=' + k + ': ' + pk + ' ≈ ' + pkNum);\n  }\n  return 'Null';\n}"
+}

+ 15 - 0
app/src/main/assets/plugins/bisection_method.json

@@ -0,0 +1,15 @@
+{
+  "id": "bisection_method",
+  "name": "二分法求根",
+  "icon": "🔍",
+  "command": "bisect",
+  "description": "用二分法求方程f(x)=0在区间[a,b]内的近似根",
+  "inputs": [
+    {"name": "fx", "label": "函数f(x) (Symja语法)", "defaultValue": "x^3-x-2"},
+    {"name": "a", "label": "左端点a", "defaultValue": "1"},
+    {"name": "b", "label": "右端点b", "defaultValue": "2"},
+    {"name": "tol", "label": "精度", "defaultValue": "0.0001"},
+    {"name": "maxIter", "label": "最大迭代次数", "defaultValue": "50"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var fx = inputs.fx, a = parseFloat(inputs.a), b = parseFloat(inputs.b);\n  var tol = parseFloat(inputs.tol), maxIter = parseInt(inputs.maxIter);\n  // Check f(a)*f(b) < 0\n  var fa = parseFloat(Symja.eval('N[(' + fx + ')/.x->' + a + ',10]'));\n  var fb = parseFloat(Symja.eval('N[(' + fx + ')/.x->' + b + ',10]'));\n  if (fa * fb >= 0) {\n    Symja.print('错误: f(' + a + ')=' + fa + ', f(' + b + ')=' + fb);\n    Symja.print('f(a)*f(b)必须小于0');\n    return 'Null';\n  }\n  Symja.print('f(x) = ' + fx);\n  Symja.print('区间 [' + a + ', ' + b + '], 精度=' + tol);\n  Symja.print('f(' + a + ')=' + fa + ', f(' + b + ')=' + fb);\n  var c, fc;\n  for (var i = 1; i <= maxIter; i++) {\n    c = (a + b) / 2;\n    fc = parseFloat(Symja.eval('N[(' + fx + ')/.x->' + c + ',10]'));\n    if (Math.abs(fc) < tol || (b - a) / 2 < tol) {\n      Symja.print('收敛! 迭代' + i + '次, 根≈' + c + ', f(c)=' + fc);\n      return 'Null';\n    }\n    if (fa * fc < 0) { b = c; fb = fc; }\n    else { a = c; fa = fc; }\n  }\n  Symja.print('达到最大迭代次数, 根≈' + c + ', f(c)=' + fc);\n  return 'Null';\n}"
+}

+ 14 - 0
app/src/main/assets/plugins/chi_square.json

@@ -0,0 +1,14 @@
+{
+  "id": "chi_square",
+  "name": "卡方检验",
+  "icon": "📊",
+  "command": "chisq",
+  "description": "2×2列联表独立性检验,输入a,b,c,d计算K²",
+  "inputs": [
+    {"name": "a", "label": "a", "defaultValue": "50"},
+    {"name": "b", "label": "b", "defaultValue": "30"},
+    {"name": "c", "label": "c", "defaultValue": "20"},
+    {"name": "d", "label": "d", "defaultValue": "40"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var a = inputs.a, b = inputs.b, c = inputs.c, d = inputs.d;\n  var n = Symja.eval(a + '+' + b + '+' + c + '+' + d);\n  var num = Symja.eval('(' + a + '*' + d + '-' + b + '*' + c + ')^2');\n  var den = Symja.eval('(' + a + '+' + b + ')*(' + c + '+' + d + ')*(' + a + '+' + c + ')*(' + b + '+' + d + ')');\n  var K2 = Symja.eval(n + '*' + num + '/' + den);\n  Symja.print('n = ' + n);\n  Symja.print('K² = ' + K2);\n  Symja.print('N[K²] = ' + Symja.eval('N[' + K2 + ', 6]'));\n  return 'Null';\n}"
+}

+ 16 - 0
app/src/main/assets/plugins/circle_equation.json

@@ -0,0 +1,16 @@
+{
+  "id": "circle_equation",
+  "name": "圆的方程",
+  "icon": "⭕",
+  "command": "circle3",
+  "description": "已知三点坐标求圆的方程、圆心和半径",
+  "inputs": [
+    {"name": "x1", "label": "x1", "defaultValue": "1"},
+    {"name": "y1", "label": "y1", "defaultValue": "0"},
+    {"name": "x2", "label": "x2", "defaultValue": "0"},
+    {"name": "y2", "label": "y2", "defaultValue": "1"},
+    {"name": "x3", "label": "x3", "defaultValue": "-1"},
+    {"name": "y3", "label": "y3", "defaultValue": "0"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var x1=inputs.x1, y1=inputs.y1, x2=inputs.x2, y2=inputs.y2, x3=inputs.x3, y3=inputs.y3;\n  var z1 = Symja.eval(x1 + '^2+(' + y1 + ')^2');\n  var z2 = Symja.eval(x2 + '^2+(' + y2 + ')^2');\n  var z3 = Symja.eval(x3 + '^2+(' + y3 + ')^2');\n  // Use u,v instead of D,E (D is Symja's derivative operator)\n  var sol = Symja.eval('Solve[{(' + x1 + '-(' + x2 + '))*u+(' + y1 + '-(' + y2 + '))*v==' + z2 + '-(' + z1 + '), (' + x1 + '-(' + x3 + '))*u+(' + y1 + '-(' + y3 + '))*v==' + z3 + '-(' + z1 + ')}, {u, v}]');\n  Symja.print('三点: (' + x1 + ',' + y1 + '), (' + x2 + ',' + y2 + '), (' + x3 + ',' + y3 + ')');\n  Symja.print('圆方程: x²+y²+ux+vy+F=0');\n  Symja.print('u, v = ' + sol);\n  Symja.print('圆心: (-u/2, -v/2)');\n  return 'Null';\n}"
+}

+ 14 - 0
app/src/main/assets/plugins/convolution.json

@@ -0,0 +1,14 @@
+{
+  "id": "convolution",
+  "name": "卷积分布列",
+  "icon": "🔗",
+  "command": "conv",
+  "description": "计算两个离散随机变量X,Y之和的分布列 P(X+Y=s)",
+  "inputs": [
+    {"name": "xVals", "label": "X的取值(逗号分隔)", "defaultValue": "0,1"},
+    {"name": "xProb", "label": "X的概率(逗号分隔)", "defaultValue": "1/2,1/2"},
+    {"name": "yVals", "label": "Y的取值(逗号分隔)", "defaultValue": "0,1"},
+    {"name": "yProb", "label": "Y的概率(逗号分隔)", "defaultValue": "1/2,1/2"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var xv = inputs.xVals.split(','), xp = inputs.xProb.split(',');\n  var yv = inputs.yVals.split(','), yp = inputs.yProb.split(',');\n  if (xv.length !== xp.length || yv.length !== yp.length)\n    throw new Error('取值和概率数量不一致');\n  // Verify X probabilities sum to 1\n  var sx = '0';\n  for (var i=0; i<xp.length; i++) sx = Symja.eval(sx + '+(' + xp[i].trim() + ')');\n  Symja.print('ΣP(X) = ' + sx);\n  var sy = '0';\n  for (var i=0; i<yp.length; i++) sy = Symja.eval(sy + '+(' + yp[i].trim() + ')');\n  Symja.print('ΣP(Y) = ' + sy);\n  // Build convolution map\n  var conv = {};\n  for (var i=0; i<xv.length; i++) {\n    for (var j=0; j<yv.length; j++) {\n      var sumExpr = Symja.eval(xv[i].trim() + '+' + yv[j].trim());\n      var probExpr = Symja.eval('(' + xp[i].trim() + ')*(' + yp[j].trim() + ')');\n      if (conv[sumExpr]) {\n        conv[sumExpr] = Symja.eval(conv[sumExpr] + '+' + probExpr);\n      } else {\n        conv[sumExpr] = probExpr;\n      }\n    }\n  }\n  // Sort keys and output\n  var keys = Object.keys(conv).sort(function(a,b) {\n    return parseFloat(Symja.eval('N[' + a + ',10]')) - parseFloat(Symja.eval('N[' + b + ',10]'));\n  });\n  Symja.print('');\n  Symja.print('X+Y 分布列:');\n  for (var k=0; k<keys.length; k++) {\n    var val = keys[k];\n    var prob = conv[val];\n    Symja.print('  P(X+Y=' + val + ') = ' + prob + ' ≈ ' + Symja.eval('N[' + prob + ',6]'));\n  }\n  return 'Null';\n}"
+}

+ 14 - 0
app/src/main/assets/plugins/cuo_wei_xiang_jian.json

@@ -0,0 +1,14 @@
+{
+  "id": "cuo_wei_xiang_jian",
+  "name": "错位相减法",
+  "icon": "📐",
+  "command": "cuowei",
+  "description": "求通项 (an+b)·q^(n+m) 的前n项和公式",
+  "inputs": [
+    {"name": "a", "label": "系数a", "defaultValue": "1"},
+    {"name": "b", "label": "常数b", "defaultValue": "0"},
+    {"name": "q", "label": "公比q", "defaultValue": "2"},
+    {"name": "m", "label": "指数偏移m", "defaultValue": "0"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var a=inputs.a, b=inputs.b, q=inputs.q, m=inputs.m;\n  var p = Symja.eval(m + '+1');\n  var aAdj = Symja.eval(a + '*' + q + '^' + p);\n  var bAdj = Symja.eval(b + '*' + q + '^' + p);\n  var A = Symja.eval(aAdj + '/(' + q + '-1)');\n  var B = Symja.eval('(' + bAdj + '-(' + A + '))/(' + q + '-1)');\n  Symja.print('通项: (' + a + 'n+' + b + ')*' + q + '^(n+' + m + ')');\n  Symja.print('A = ' + A + ' ≈ ' + Symja.eval('N[' + A + ',6]'));\n  Symja.print('B = ' + B + ' ≈ ' + Symja.eval('N[' + B + ',6]'));\n  Symja.print('前n项和: S_n = (' + A + 'n + ' + B + ')*' + q + '^n - (' + B + ')');\n  return 'Null';\n}"
+}

+ 14 - 0
app/src/main/assets/plugins/histogram_stats.json

@@ -0,0 +1,14 @@
+{
+  "id": "histogram_stats",
+  "name": "直方图统计",
+  "icon": "📊",
+  "command": "histats",
+  "description": "从频率分布直方图计算均值、方差和中位数",
+  "inputs": [
+    {"name": "a", "label": "左端点", "defaultValue": "0"},
+    {"name": "d", "label": "组距", "defaultValue": "10"},
+    {"name": "freqs", "label": "各组频率(逗号分隔,和=1)", "defaultValue": "0.1,0.2,0.4,0.2,0.1"},
+    {"name": "ptile", "label": "百分位数(可选,如0.5)", "defaultValue": "0.5"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var a = inputs.a, d = inputs.d;\n  var freqs = inputs.freqs.split(',');\n  var ptile = inputs.ptile ? parseFloat(inputs.ptile) : 0;\n  var n = freqs.length;\n  // Verify sum ≈ 1\n  var sum = '0';\n  for (var i = 0; i < n; i++) sum = Symja.eval(sum + '+' + freqs[i].trim());\n  Symja.print('频率和 = ' + sum);\n  // Mean\n  var mean = '0';\n  for (var i = 0; i < n; i++) {\n    var mid = Symja.eval(a + '+' + i + '*' + d + '+' + d + '/2');\n    mean = Symja.eval(mean + '+(' + mid + ')*(' + freqs[i].trim() + ')');\n  }\n  Symja.print('均值 = ' + mean + ' ≈ ' + Symja.eval('N[' + mean + ',6]'));\n  // Variance\n  var vari = '0';\n  for (var i = 0; i < n; i++) {\n    var mid = Symja.eval(a + '+' + i + '*' + d + '+' + d + '/2');\n    vari = Symja.eval(vari + '+(' + mid + '-(' + mean + '))^2*(' + freqs[i].trim() + ')');\n  }\n  Symja.print('方差 = ' + vari + ' ≈ ' + Symja.eval('N[' + vari + ',6]'));\n  // Median / percentile\n  if (ptile > 0) {\n    var cum = '0';\n    for (var i = 0; i < n; i++) {\n      var prevCum = cum;\n      cum = Symja.eval(cum + '+' + freqs[i].trim());\n      var cumVal = parseFloat(Symja.eval('N[' + cum + ',10]'));\n      if (cumVal >= ptile) {\n        var prevVal = parseFloat(Symja.eval('N[' + prevCum + ',10]'));\n        var fi = parseFloat(Symja.eval('N[' + freqs[i].trim() + ',10]'));\n        var pval = parseFloat(Symja.eval('N[' + a + '+' + i + '*' + d + '+(' + ptile + '-(' + prevCum + '))/(' + freqs[i].trim() + ')*' + d + ',6]'));\n        Symja.print('P' + (ptile*100) + ' = ' + Symja.eval('N[' + a + '+' + i + '*' + d + '+(' + ptile + '-(' + prevCum + '))/(' + freqs[i].trim() + ')*' + d + ',6]'));\n        break;\n      }\n    }\n  }\n  return 'Null';\n}"
+}

+ 13 - 0
app/src/main/assets/plugins/hypergeometric_distribution.json

@@ -0,0 +1,13 @@
+{
+  "id": "hypergeometric_distribution",
+  "name": "超几何分布",
+  "icon": "🎯",
+  "command": "hypergeo",
+  "description": "计算超几何分布H(N,M,n)的期望、方差和分布列",
+  "inputs": [
+    {"name": "N", "label": "总数N", "defaultValue": "10"},
+    {"name": "M", "label": "成功数M", "defaultValue": "4"},
+    {"name": "n", "label": "抽取数n", "defaultValue": "3"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var N = parseInt(inputs.N), M = parseInt(inputs.M), n = parseInt(inputs.n);\n  var EX = Symja.eval(n + '*' + M + '/' + N);\n  var DX = Symja.eval(n + '*' + M + '*(' + N + '-' + M + ')*(' + N + '-' + n + ')/(' + N + '^2*(' + N + '-1))');\n  var minK = Math.max(0, n - (N - M));\n  var maxK = Math.min(n, M);\n  Symja.print('X ~ H(' + N + ', ' + M + ', ' + n + ')');\n  Symja.print('E(X) = ' + EX + ' = ' + Symja.eval('N[' + EX + ',6]'));\n  Symja.print('D(X) = ' + DX + ' = ' + Symja.eval('N[' + DX + ',6]'));\n  Symja.print('k范围: ' + minK + ' ≤ k ≤ ' + maxK);\n  Symja.print('');\n  Symja.print('分布列 P(X=k):');\n  for (var k = minK; k <= maxK; k++) {\n    var num = Symja.eval('Binomial[' + M + ',' + k + ']*Binomial[' + (N-M) + ',' + (n-k) + ']');\n    var den = Symja.eval('Binomial[' + N + ',' + n + ']');\n    var pk = Symja.eval(num + '/' + den);\n    Symja.print('  k=' + k + ': ' + pk + ' ≈ ' + Symja.eval('N[' + pk + ',6]'));\n  }\n  return 'Null';\n}"
+}

+ 12 - 0
app/src/main/assets/plugins/linear_regression.json

@@ -0,0 +1,12 @@
+{
+  "id": "linear_regression",
+  "name": "线性回归",
+  "icon": "📈",
+  "command": "linreg",
+  "description": "计算一元线性回归: 相关系数r、斜率b、截距a、R²",
+  "inputs": [
+    {"name": "xs", "label": "x值(逗号分隔)", "defaultValue": "1,2,3,4,5"},
+    {"name": "ys", "label": "y值(逗号分隔)", "defaultValue": "2,4,5,4,5"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var xs = inputs.xs.split(','), ys = inputs.ys.split(',');\n  var n = xs.length;\n  if (n !== ys.length) throw new Error('x和y数量不一致');\n  var Sx='0', Sy='0', Sx2='0', Sy2='0', Sxy='0';\n  for (var i=0; i<n; i++) {\n    var xi = xs[i].trim(), yi = ys[i].trim();\n    Sx = Symja.eval(Sx + '+' + xi);\n    Sy = Symja.eval(Sy + '+' + yi);\n    Sx2 = Symja.eval(Sx2 + '+' + xi + '^2');\n    Sy2 = Symja.eval(Sy2 + '+' + yi + '^2');\n    Sxy = Symja.eval(Sxy + '+' + xi + '*' + yi);\n  }\n  var xBar = Symja.eval(Sx + '/' + n);\n  var yBar = Symja.eval(Sy + '/' + n);\n  var sigX = Symja.eval(Sx2 + '-' + n + '*(' + xBar + ')^2');\n  var sigY = Symja.eval(Sy2 + '-' + n + '*(' + yBar + ')^2');\n  var sigXY = Symja.eval(Sxy + '-' + n + '*(' + xBar + ')*(' + yBar + ')');\n  var r = Symja.eval(sigXY + '/Sqrt[' + sigX + '*' + sigY + ']');\n  var b = Symja.eval(sigXY + '/' + sigX);\n  var a = Symja.eval(yBar + '-(' + b + ')*(' + xBar + ')');\n  Symja.print('n = ' + n);\n  Symja.print('x̄ = ' + xBar + ' ≈ ' + Symja.eval('N[' + xBar + ',6]'));\n  Symja.print('ȳ = ' + yBar + ' ≈ ' + Symja.eval('N[' + yBar + ',6]'));\n  Symja.print('σx² = ' + sigX);\n  Symja.print('σy² = ' + sigY);\n  Symja.print('σxy = ' + sigXY);\n  Symja.print('r = ' + r + ' ≈ ' + Symja.eval('N[' + r + ',6]'));\n  Symja.print('b = ' + b + ' ≈ ' + Symja.eval('N[' + b + ',6]'));\n  Symja.print('a = ' + a + ' ≈ ' + Symja.eval('N[' + a + ',6]'));\n  Symja.print('回归方程: y = ' + a + ' + ' + b + 'x');\n  return 'Null';\n}"
+}

+ 13 - 0
app/src/main/assets/plugins/mixed_variance.json

@@ -0,0 +1,13 @@
+{
+  "id": "mixed_variance",
+  "name": "混合方差",
+  "icon": "📈",
+  "command": "mixvar",
+  "description": "计算分层数据的加权均值和混合方差",
+  "inputs": [
+    {"name": "sizes", "label": "各层样本量 (逗号分隔)", "defaultValue": "30,40,30"},
+    {"name": "means", "label": "各层均值 (逗号分隔)", "defaultValue": "75,80,85"},
+    {"name": "variances", "label": "各层方差 (逗号分隔)", "defaultValue": "100,120,90"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var sizes = inputs.sizes.split(',');\n  var means = inputs.means.split(',');\n  var vars = inputs.variances.split(',');\n  if (sizes.length !== means.length || sizes.length !== vars.length) {\n    throw new Error('各组数据数量不一致');\n  }\n  var n = sizes.length;\n  var totalSize = '0';\n  var weightedSum = '0';\n  for (var i = 0; i < n; i++) {\n    totalSize = Symja.eval(totalSize + '+' + sizes[i].trim());\n    weightedSum = Symja.eval(weightedSum + '+' + sizes[i].trim() + '*(' + means[i].trim() + ')');\n  }\n  var overallMean = Symja.eval(weightedSum + '/' + totalSize);\n  var mixedVar = '0';\n  for (var i = 0; i < n; i++) {\n    var diff = Symja.eval('(' + means[i].trim() + '-(' + overallMean + '))^2');\n    mixedVar = Symja.eval(mixedVar + '+' + sizes[i].trim() + '*(' + vars[i].trim() + '+' + diff + ')');\n  }\n  mixedVar = Symja.eval(mixedVar + '/' + totalSize);\n  Symja.print('总样本量: ' + totalSize);\n  Symja.print('总均值: ' + overallMean + ' ≈ ' + Symja.eval('N[' + overallMean + ', 6]'));\n  Symja.print('混合方差: ' + mixedVar + ' ≈ ' + Symja.eval('N[' + mixedVar + ', 6]'));\n  return 'Null';\n}"
+}

+ 16 - 0
app/src/main/assets/plugins/symmetry_ll.json

@@ -0,0 +1,16 @@
+{
+  "id": "symmetry_ll",
+  "name": "对称变换-直线关于直线",
+  "icon": "🔷",
+  "command": "symll",
+  "description": "求直线关于直线的对称直线方程",
+  "inputs": [
+    {"name": "P", "label": "原直线Px+Qy+R=0的P", "defaultValue": "1"},
+    {"name": "Q", "label": "原直线Px+Qy+R=0的Q", "defaultValue": "1"},
+    {"name": "R", "label": "原直线Px+Qy+R=0的R", "defaultValue": "0"},
+    {"name": "A", "label": "对称轴Ax+By+C=0的A", "defaultValue": "1"},
+    {"name": "B", "label": "对称轴Ax+By+C=0的B", "defaultValue": "0"},
+    {"name": "C", "label": "对称轴Ax+By+C=0的C", "defaultValue": "0"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var P=inputs.P, Q=inputs.Q, R=inputs.R, A=inputs.A, B=inputs.B, C=inputs.C;\n  var denom = Symja.eval(A + '^2 + ' + B + '^2');\n  var k = Symja.eval('2*(' + P + '*' + A + '+' + Q + '*' + B + ')/' + denom);\n  var newA = Symja.eval(P + '-' + A + '*(' + k + ')');\n  var newB = Symja.eval(Q + '-' + B + '*(' + k + ')');\n  var newC = Symja.eval(R + '-' + C + '*(' + k + ')');\n  Symja.print('原直线: ' + P + 'x+' + Q + 'y+' + R + '=0');\n  Symja.print('对称轴: ' + A + 'x+' + B + 'y+' + C + '=0');\n  Symja.print('对称直线: ' + newA + 'x + ' + newB + 'y + ' + newC + ' = 0');\n  return 'Null';\n}"
+}

+ 15 - 0
app/src/main/assets/plugins/symmetry_lp.json

@@ -0,0 +1,15 @@
+{
+  "id": "symmetry_lp",
+  "name": "对称变换-直线关于点",
+  "icon": "🔷",
+  "command": "symlp",
+  "description": "求直线关于点的对称直线方程",
+  "inputs": [
+    {"name": "A", "label": "直线Ax+By+C=0的A", "defaultValue": "1"},
+    {"name": "B", "label": "直线Ax+By+C=0的B", "defaultValue": "1"},
+    {"name": "C", "label": "直线Ax+By+C=0的C", "defaultValue": "0"},
+    {"name": "x", "label": "对称中心x", "defaultValue": "0"},
+    {"name": "y", "label": "对称中心y", "defaultValue": "0"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var A = inputs.A, B = inputs.B, C = inputs.C, x = inputs.x, y = inputs.y;\n  var newC = Symja.eval('-2*' + A + '*' + x + ' - 2*' + B + '*' + y + ' - (' + C + ')');\n  Symja.print('直线 ' + A + 'x+' + B + 'y+' + C + '=0');\n  Symja.print('关于点(' + x + ', ' + y + ')的对称直线:');\n  Symja.print(A + 'x + ' + B + 'y + ' + newC + ' = 0');\n  return 'Null';\n}"
+}

+ 15 - 0
app/src/main/assets/plugins/symmetry_pl.json

@@ -0,0 +1,15 @@
+{
+  "id": "symmetry_pl",
+  "name": "对称变换-点关于直线",
+  "icon": "🔷",
+  "command": "sympl",
+  "description": "求点关于直线的对称点坐标",
+  "inputs": [
+    {"name": "x", "label": "点x坐标", "defaultValue": "1"},
+    {"name": "y", "label": "点y坐标", "defaultValue": "2"},
+    {"name": "A", "label": "直线Ax+By+C=0的A", "defaultValue": "1"},
+    {"name": "B", "label": "直线Ax+By+C=0的B", "defaultValue": "-1"},
+    {"name": "C", "label": "直线Ax+By+C=0的C", "defaultValue": "0"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var x = inputs.x, y = inputs.y, A = inputs.A, B = inputs.B, C = inputs.C;\n  // A*xp + B*yp = -(A*x + B*y + 2*C)\n  // B*xp - A*yp = B*x - A*y\n  var rhs1 = Symja.eval('-(' + A + '*(' + x + ') + ' + B + '*(' + y + ') + 2*(' + C + '))');\n  var rhs2 = Symja.eval(B + '*(' + x + ') - ' + A + '*(' + y + ')');\n  var eq1 = A + '*xp + ' + B + '*yp == ' + rhs1;\n  var eq2 = B + '*xp - ' + A + '*yp == ' + rhs2;\n  var sol = Symja.solve('{' + eq1 + ', ' + eq2 + '}', '{xp, yp}');\n  Symja.print('点(' + x + ', ' + y + ') 关于直线 ' + A + 'x+' + B + 'y+' + C + '=0');\n  Symja.print('对称点: ' + sol);\n  return 'Null';\n}"
+}

+ 19 - 0
app/src/main/assets/plugins/vector_normal.json

@@ -0,0 +1,19 @@
+{
+  "id": "vector_normal",
+  "name": "三点法向量",
+  "icon": "🧭",
+  "command": "normal3",
+  "description": "已知空间三点A,B,C,求平面法向量(叉积)",
+  "inputs": [
+    {"name": "ax", "label": "A.x (可用√,如 1/2*Sqrt[2])", "defaultValue": "1"},
+    {"name": "ay", "label": "A.y", "defaultValue": "0"},
+    {"name": "az", "label": "A.z", "defaultValue": "0"},
+    {"name": "bx", "label": "B.x", "defaultValue": "0"},
+    {"name": "by", "label": "B.y", "defaultValue": "1"},
+    {"name": "bz", "label": "B.z", "defaultValue": "0"},
+    {"name": "cx", "label": "C.x", "defaultValue": "0"},
+    {"name": "cy", "label": "C.y", "defaultValue": "0"},
+    {"name": "cz", "label": "C.z", "defaultValue": "1"}
+  ],
+  "jsCode": "function calculate(inputs, Symja) {\n  var ax=inputs.ax,ay=inputs.ay,az=inputs.az;\n  var bx=inputs.bx,by=inputs.by,bz=inputs.bz;\n  var cx=inputs.cx,cy=inputs.cy,cz=inputs.cz;\n  // v1 = B - A, v2 = C - A\n  var v1x = Symja.eval('Simplify[(' + bx + ')-(' + ax + ')]');\n  var v1y = Symja.eval('Simplify[(' + by + ')-(' + ay + ')]');\n  var v1z = Symja.eval('Simplify[(' + bz + ')-(' + az + ')]');\n  var v2x = Symja.eval('Simplify[(' + cx + ')-(' + ax + ')]');\n  var v2y = Symja.eval('Simplify[(' + cy + ')-(' + ay + ')]');\n  var v2z = Symja.eval('Simplify[(' + cz + ')-(' + az + ')]');\n  // Cross product: v1 × v2 with Simplify\n  var nx = Symja.eval('Simplify[(' + v1y + ')*(' + v2z + ')-(' + v1z + ')*(' + v2y + ')]');\n  var ny = Symja.eval('Simplify[(' + v1z + ')*(' + v2x + ')-(' + v1x + ')*(' + v2z + ')]');\n  var nz = Symja.eval('Simplify[(' + v1x + ')*(' + v2y + ')-(' + v1y + ')*(' + v2x + ')]');\n  Symja.print('A = (' + ax + ', ' + ay + ', ' + az + ')');\n  Symja.print('B = (' + bx + ', ' + by + ', ' + bz + ')');\n  Symja.print('C = (' + cx + ', ' + cy + ', ' + cz + ')');\n  Symja.print('AB = (' + v1x + ', ' + v1y + ', ' + v1z + ')');\n  Symja.print('AC = (' + v2x + ', ' + v2y + ', ' + v2z + ')');\n  Symja.print('法向量 n = AB × AC = (' + nx + ', ' + ny + ', ' + nz + ')');\n  return 'Null';\n}"
+}

+ 25 - 0
app/src/main/java/com/nonvioce/ijs/AppSession.java

@@ -0,0 +1,25 @@
+package com.nonvioce.ijs;
+
+/**
+ * Singleton holder for the shared Symja computation session.
+ * Ensures the WebView console and MCP server share the same
+ * variables, last-output (% reference), and plugin state.
+ */
+public class AppSession {
+
+    private static final SymjaEngine symjaEngine = new SymjaEngine();
+    private static PluginManager pluginManager;
+
+    private AppSession() {}
+
+    public static SymjaEngine getSymjaEngine() {
+        return symjaEngine;
+    }
+
+    public static PluginManager getPluginManager(android.content.Context context) {
+        if (pluginManager == null) {
+            pluginManager = new PluginManager(context.getApplicationContext());
+        }
+        return pluginManager;
+    }
+}

+ 85 - 16
app/src/main/java/com/nonvioce/ijs/MainActivity.java

@@ -1,13 +1,19 @@
 package com.nonvioce.ijs;
 package com.nonvioce.ijs;
 
 
 import android.annotation.SuppressLint;
 import android.annotation.SuppressLint;
+import android.content.Intent;
 import android.os.Bundle;
 import android.os.Bundle;
 import android.webkit.JavascriptInterface;
 import android.webkit.JavascriptInterface;
 import android.webkit.WebView;
 import android.webkit.WebView;
+import android.widget.Toast;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
 import com.nonvioce.ijs.databinding.ActivityMainBinding;
 import com.nonvioce.ijs.databinding.ActivityMainBinding;
 
 
 public class MainActivity extends AppCompatActivity {
 public class MainActivity extends AppCompatActivity {
+    private static final int DEFAULT_MCP_PORT = 8734;
+
     private ActivityMainBinding binding;
     private ActivityMainBinding binding;
     private SymjaEngine symjaEngine;
     private SymjaEngine symjaEngine;
     private PluginManager pluginManager;
     private PluginManager pluginManager;
@@ -19,8 +25,8 @@ public class MainActivity extends AppCompatActivity {
         binding = ActivityMainBinding.inflate(getLayoutInflater());
         binding = ActivityMainBinding.inflate(getLayoutInflater());
         setContentView(binding.getRoot());
         setContentView(binding.getRoot());
 
 
-        symjaEngine = new SymjaEngine();
-        pluginManager = new PluginManager(this);
+        symjaEngine = AppSession.getSymjaEngine();
+        pluginManager = AppSession.getPluginManager(this);
 
 
         // 配置WebView
         // 配置WebView
         binding.webView.getSettings().setJavaScriptEnabled(true);
         binding.webView.getSettings().setJavaScriptEnabled(true);
@@ -28,11 +34,11 @@ public class MainActivity extends AppCompatActivity {
         binding.webView.getSettings().setAllowFileAccess(true);
         binding.webView.getSettings().setAllowFileAccess(true);
         binding.webView.getSettings().setAllowContentAccess(true);
         binding.webView.getSettings().setAllowContentAccess(true);
         binding.webView.getSettings().setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
         binding.webView.getSettings().setMixedContentMode(android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
-        
+
         // 设置WebChromeClient以支持JavaScript对话框
         // 设置WebChromeClient以支持JavaScript对话框
         binding.webView.setWebChromeClient(new android.webkit.WebChromeClient() {
         binding.webView.setWebChromeClient(new android.webkit.WebChromeClient() {
             @Override
             @Override
-            public boolean onJsConfirm(android.webkit.WebView view, String url, String message, 
+            public boolean onJsConfirm(android.webkit.WebView view, String url, String message,
                                       android.webkit.JsResult result) {
                                       android.webkit.JsResult result) {
                 new android.app.AlertDialog.Builder(MainActivity.this)
                 new android.app.AlertDialog.Builder(MainActivity.this)
                     .setTitle("确认")
                     .setTitle("确认")
@@ -44,9 +50,9 @@ public class MainActivity extends AppCompatActivity {
                     .show();
                     .show();
                 return true;
                 return true;
             }
             }
-            
+
             @Override
             @Override
-            public boolean onJsAlert(android.webkit.WebView view, String url, String message, 
+            public boolean onJsAlert(android.webkit.WebView view, String url, String message,
                                     android.webkit.JsResult result) {
                                     android.webkit.JsResult result) {
                 new android.app.AlertDialog.Builder(MainActivity.this)
                 new android.app.AlertDialog.Builder(MainActivity.this)
                     .setTitle("提示")
                     .setTitle("提示")
@@ -58,9 +64,70 @@ public class MainActivity extends AppCompatActivity {
                 return true;
                 return true;
             }
             }
         });
         });
-        
+
         binding.webView.addJavascriptInterface(new WebAppInterface(), "Android");
         binding.webView.addJavascriptInterface(new WebAppInterface(), "Android");
         binding.webView.loadUrl("file:///android_asset/console.html");
         binding.webView.loadUrl("file:///android_asset/console.html");
+
+        // MCP server toggle
+        setupMcpToggle();
+
+        // Auto-start MCP server on first launch (for testing convenience)
+        if (!McpService.isRunning()) {
+            startMcpServer(DEFAULT_MCP_PORT);
+        }
+    }
+
+    // ── MCP toggle ────────────────────────────────────────────────
+
+    private void setupMcpToggle() {
+        FloatingActionButton fab = binding.mcpToggleFab;
+        updateMcpFabIcon(fab);
+
+        fab.setOnClickListener(v -> {
+            if (McpService.isRunning()) {
+                stopMcpServer();
+            } else {
+                startMcpServer(DEFAULT_MCP_PORT);
+            }
+            updateMcpFabIcon(fab);
+        });
+
+        fab.setOnLongClickListener(v -> {
+            String msg = McpService.isRunning()
+                    ? getString(R.string.mcp_started, McpService.getPort())
+                    : getString(R.string.mcp_stopped);
+            Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
+            return true;
+        });
+    }
+
+    private void updateMcpFabIcon(FloatingActionButton fab) {
+        if (McpService.isRunning()) {
+            fab.setBackgroundTintList(
+                    android.content.res.ColorStateList.valueOf(
+                            getColor(R.color.mcp_fab_on)));
+            fab.setImageResource(android.R.drawable.ic_menu_share);
+        } else {
+            fab.setBackgroundTintList(
+                    android.content.res.ColorStateList.valueOf(
+                            getColor(R.color.mcp_fab_off)));
+            fab.setImageResource(android.R.drawable.ic_menu_share);
+        }
+    }
+
+    private void startMcpServer(int port) {
+        Intent intent = new Intent(this, McpService.class);
+        intent.setAction(McpService.ACTION_START);
+        intent.putExtra(McpService.EXTRA_PORT, port);
+        ContextCompat.startForegroundService(this, intent);
+        Toast.makeText(this, getString(R.string.mcp_started, port), Toast.LENGTH_SHORT).show();
+    }
+
+    private void stopMcpServer() {
+        Intent intent = new Intent(this, McpService.class);
+        intent.setAction(McpService.ACTION_STOP);
+        startService(intent);
+        Toast.makeText(this, getString(R.string.mcp_stopped), Toast.LENGTH_SHORT).show();
     }
     }
 
 
     public class WebAppInterface {
     public class WebAppInterface {
@@ -180,16 +247,16 @@ public class MainActivity extends AppCompatActivity {
                     "  Round[3.7]                   // 四舍五入\n\n" +
                     "  Round[3.7]                   // 四舍五入\n\n" +
                     
                     
                     "【变换与级数】\n" +
                     "【变换与级数】\n" +
-                    "  FourierTransform[Exp[-x^2], x, w]  // 傅里叶变换\n" +
                     "  LaplaceTransform[Sin[t], t, s]     // 拉普拉斯变换\n" +
                     "  LaplaceTransform[Sin[t], t, s]     // 拉普拉斯变换\n" +
                     "  InverseLaplaceTransform[1/(s^2+1), s, t]  // 逆拉普拉斯\n" +
                     "  InverseLaplaceTransform[1/(s^2+1), s, t]  // 逆拉普拉斯\n" +
+                    "  ZTransform[a^n, n, z]              // Z变换\n" +
                     "  Series[Exp[x], {x, 0, 5}]          // 泰勒级数展开\n" +
                     "  Series[Exp[x], {x, 0, 5}]          // 泰勒级数展开\n" +
                     "  Sum[1/n^2, {n, 1, Infinity}]       // 无穷级数求和\n\n" +
                     "  Sum[1/n^2, {n, 1, Infinity}]       // 无穷级数求和\n\n" +
-                    
+
                     "【优化与极值】\n" +
                     "【优化与极值】\n" +
-                    "  Maximize[x^2-4*x+3, x]             // 求最大值\n" +
                     "  Minimize[x^2-4*x+3, x]             // 求最小值\n" +
                     "  Minimize[x^2-4*x+3, x]             // 求最小值\n" +
-                    "  FindMinimum[x^4-3*x^2+2, x]        // 数值求最小值\n\n" +
+                    "  FindMinimum[x^4-3*x^2+2, x]        // 数值求最小值\n" +
+                    "  NMinimize[x^4-3*x^2+2, x]          // 全局最小化\n\n" +
                     
                     
                     "【表格与数据】\n" +
                     "【表格与数据】\n" +
                     "  Table[i^2, {i, 1, 10}]             // 生成表格\n" +
                     "  Table[i^2, {i, 1, 10}]             // 生成表格\n" +
@@ -198,9 +265,7 @@ public class MainActivity extends AppCompatActivity {
                     "  Range[0, 1, 0.1]                   // 带步长的数列\n\n" +
                     "  Range[0, 1, 0.1]                   // 带步长的数列\n\n" +
                     
                     
                     "【高级数学】\n" +
                     "【高级数学】\n" +
-                    "  DSolve[y'[x]==y[x], y[x], x]       // 微分方程求解\n" +
-                    "  RSolve[a[n]==2*a[n-1]+1, a[n], n]  // 递推方程\n" +
-                    "  Residue[1/(z^2+1), {z, I}]         // 留数计算\n\n" +
+                    "  DSolve[y'[x]==y[x], y[x], x]       // 微分方程求解\n\n" +
                     
                     
                     "【特殊命令】\n" +
                     "【特殊命令】\n" +
                     "  help / ?        显示此帮助\n" +
                     "  help / ?        显示此帮助\n" +
@@ -208,7 +273,6 @@ public class MainActivity extends AppCompatActivity {
                     "  reset           重置所有变量\n" +
                     "  reset           重置所有变量\n" +
                     "  history         显示命令历史\n" +
                     "  history         显示命令历史\n" +
                     "  vars            显示已定义变量及其值\n" +
                     "  vars            显示已定义变量及其值\n" +
-                    "  save <name>     保存当前会话\n" +
                     "  plugins         打开插件管理\n" +
                     "  plugins         打开插件管理\n" +
                     "  feedback        显示反馈邮箱\n" +
                     "  feedback        显示反馈邮箱\n" +
                     "  clear <var>     清除特定变量\n\n" +
                     "  clear <var>     清除特定变量\n\n" +
@@ -231,7 +295,7 @@ public class MainActivity extends AppCompatActivity {
                     "• 乘法可以使用 * 或空格\n" +
                     "• 乘法可以使用 * 或空格\n" +
                     "• 使用 % 可以引用上一次的计算结果\n" +
                     "• 使用 % 可以引用上一次的计算结果\n" +
                     "• 支持多种绘图类型:函数图、参数方程、极坐标、列表图\n" +
                     "• 支持多种绘图类型:函数图、参数方程、极坐标、列表图\n" +
-                    "• 支持高级数学:傅里叶变换、拉普拉斯变换、微分方程\n" +
+                    "• 支持高级数学:拉普拉斯变换、Z变换、微分方程\n" +
                     "• 点击\"插件\"按钮查看更多扩展功能\n\n" +
                     "• 点击\"插件\"按钮查看更多扩展功能\n\n" +
                     
                     
                     "═══════════════════════════════════════════════════";
                     "═══════════════════════════════════════════════════";
@@ -348,6 +412,11 @@ public class MainActivity extends AppCompatActivity {
             return symjaEngine.binomial(n, k);
             return symjaEngine.binomial(n, k);
         }
         }
 
 
+        @JavascriptInterface
+        public boolean isMcpRunning() {
+            return McpService.isRunning();
+        }
+
         @JavascriptInterface
         @JavascriptInterface
         public void hideKeyboard() {
         public void hideKeyboard() {
             runOnUiThread(() -> {
             runOnUiThread(() -> {

+ 753 - 0
app/src/main/java/com/nonvioce/ijs/McpServer.java

@@ -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();
+    }
+}

+ 171 - 0
app/src/main/java/com/nonvioce/ijs/McpService.java

@@ -0,0 +1,171 @@
+package com.nonvioce.ijs;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Android foreground Service that hosts the MCP HTTP server.
+ * Survives Activity lifecycle changes and shows a persistent notification
+ * while the server is running.
+ */
+public class McpService extends Service {
+
+    private static final String TAG = "McpService";
+    private static final String CHANNEL_ID = "mcp_server_channel";
+    private static final int NOTIFICATION_ID = 1001;
+    private static final int DEFAULT_PORT = 8734;
+
+    public static final String ACTION_START = "com.nonvioce.ijs.action.MCP_START";
+    public static final String ACTION_STOP  = "com.nonvioce.ijs.action.MCP_STOP";
+    public static final String EXTRA_PORT   = "com.nonvioce.ijs.extra.MCP_PORT";
+
+    private static boolean running = false;
+    private static int currentPort = DEFAULT_PORT;
+
+    private McpServer server;
+    private Thread serverThread;
+    private SymjaEngine symjaEngine;
+    private PluginManager pluginManager;
+
+    /** Check whether the MCP server is currently running. */
+    public static boolean isRunning() {
+        return running;
+    }
+
+    /** Get the port the server is listening on (or last known port). */
+    public static int getPort() {
+        return currentPort;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        createNotificationChannel();
+        symjaEngine = AppSession.getSymjaEngine();
+        pluginManager = AppSession.getPluginManager(this);
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (intent != null && ACTION_STOP.equals(intent.getAction())) {
+            stopServer();
+            return START_NOT_STICKY;
+        }
+
+        int port = (intent != null) ? intent.getIntExtra(EXTRA_PORT, DEFAULT_PORT) : DEFAULT_PORT;
+        currentPort = port;
+
+        if (!running) {
+            startServer(port);
+        }
+
+        return START_STICKY;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null; // Not a bound service
+    }
+
+    @Override
+    public void onDestroy() {
+        stopServer();
+        super.onDestroy();
+    }
+
+    // ── Server lifecycle ──────────────────────────────────────────
+
+    private void startServer(int port) {
+        try {
+            server = new McpServer(port, symjaEngine, pluginManager);
+            serverThread = new Thread(() -> {
+                try {
+                    server.start();
+                } catch (IOException e) {
+                    Log.e(TAG, "Failed to start MCP server", e);
+                    running = false;
+                }
+            }, "McpServerThread");
+            serverThread.setDaemon(true);
+            serverThread.start();
+
+            running = true;
+            currentPort = port;
+            Log.i(TAG, "MCP server started on port " + port);
+
+            startForeground(NOTIFICATION_ID, buildNotification(port));
+
+        } catch (Exception e) {
+            Log.e(TAG, "Error starting MCP server", e);
+            running = false;
+        }
+    }
+
+    private void stopServer() {
+        running = false;
+        if (server != null) {
+            server.stopServer();
+            server = null;
+        }
+        serverThread = null;
+        stopForeground(STOP_FOREGROUND_REMOVE);
+        stopSelf();
+        Log.i(TAG, "MCP server stopped");
+    }
+
+    // ── Notification ──────────────────────────────────────────────
+
+    private void createNotificationChannel() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            NotificationChannel channel = new NotificationChannel(
+                    CHANNEL_ID,
+                    "MCP 服务器",
+                    NotificationManager.IMPORTANCE_LOW);
+            channel.setDescription("MCP 服务器运行状态通知");
+            channel.setShowBadge(false);
+            NotificationManager nm = getSystemService(NotificationManager.class);
+            if (nm != null) {
+                nm.createNotificationChannel(channel);
+            }
+        }
+    }
+
+    private Notification buildNotification(int port) {
+        Intent launchIntent = new Intent(this, MainActivity.class);
+        PendingIntent pendingIntent = PendingIntent.getActivity(
+                this, 0, launchIntent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+        Intent stopIntent = new Intent(this, McpService.class);
+        stopIntent.setAction(ACTION_STOP);
+        PendingIntent stopPendingIntent = PendingIntent.getService(
+                this, 1, stopIntent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+        Notification.Builder builder;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            builder = new Notification.Builder(this, CHANNEL_ID);
+        } else {
+            builder = new Notification.Builder(this);
+        }
+
+        return builder
+                .setContentTitle("MCP 服务器运行中")
+                .setContentText("端口: " + port + " — 点击打开应用")
+                .setSmallIcon(android.R.drawable.ic_dialog_info)
+                .setOngoing(true)
+                .setContentIntent(pendingIntent)
+                .addAction(android.R.drawable.ic_media_pause, "停止",
+                        stopPendingIntent)
+                .build();
+    }
+}

+ 26 - 1
app/src/main/java/com/nonvioce/ijs/PluginManager.java

@@ -66,7 +66,32 @@ public class PluginManager {
     }
     }
 
 
     private void createSamplePlugins() {
     private void createSamplePlugins() {
-        // 不再创建示例插件
+        // 从 assets/plugins/ 复制默认插件
+        String[] defaultPlugins;
+        try {
+            defaultPlugins = context.getAssets().list("plugins");
+        } catch (Exception e) {
+            return;
+        }
+        if (defaultPlugins == null) return;
+
+        for (String filename : defaultPlugins) {
+            if (!filename.endsWith(".json")) continue;
+            try {
+                java.io.InputStream is = context.getAssets().open("plugins/" + filename);
+                java.io.File outFile = new java.io.File(pluginsDir, filename);
+                java.io.FileOutputStream os = new java.io.FileOutputStream(outFile);
+                byte[] buf = new byte[4096];
+                int len;
+                while ((len = is.read(buf)) > 0) {
+                    os.write(buf, 0, len);
+                }
+                os.close();
+                is.close();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
     }
     }
 
 
     private void writeFile(File file, String content) throws Exception {
     private void writeFile(File file, String content) throws Exception {

+ 9 - 15
app/src/main/java/com/nonvioce/ijs/SymjaEngine.java

@@ -50,7 +50,7 @@ public class SymjaEngine {
             
             
             // 微积分
             // 微积分
             "D", "Derivative", "Integrate", "Limit", "Sum", "Product", "Series",
             "D", "Derivative", "Integrate", "Limit", "Sum", "Product", "Series",
-            "DSolve", "NDSolve",
+            "DSolve",
             
             
             // 代数
             // 代数
             "Expand", "Factor", "Simplify", "FullSimplify", "Together", "Apart",
             "Expand", "Factor", "Simplify", "FullSimplify", "Together", "Apart",
@@ -79,7 +79,7 @@ public class SymjaEngine {
             "DivisorSigma", "JacobiSymbol",
             "DivisorSigma", "JacobiSymbol",
             
             
             // 组合数学
             // 组合数学
-            "Factorial", "Binomial", "Multinomial", "Permutations", "Combinations",
+            "Factorial", "Binomial", "Multinomial", "Permutations",
             "Fibonacci", "LucasL", "BernoulliB", "StirlingS1", "StirlingS2",
             "Fibonacci", "LucasL", "BernoulliB", "StirlingS1", "StirlingS2",
             "CatalanNumber", "PartitionsP",
             "CatalanNumber", "PartitionsP",
             
             
@@ -103,7 +103,7 @@ public class SymjaEngine {
             "Equal", "Unequal", "Less", "Greater", "LessEqual", "GreaterEqual",
             "Equal", "Unequal", "Less", "Greater", "LessEqual", "GreaterEqual",
             
             
             // 条件和控制
             // 条件和控制
-            "If", "Which", "Switch", "Do", "While", "For", "Nest", "NestList",
+            "If", "Which", "Nest", "NestList",
             
             
             // 数值计算
             // 数值计算
             "N", "Round", "Floor", "Ceiling", "Rationalize", "RealDigits",
             "N", "Round", "Floor", "Ceiling", "Rationalize", "RealDigits",
@@ -117,33 +117,28 @@ public class SymjaEngine {
             "RandomChoice", "RandomSample",
             "RandomChoice", "RandomSample",
             
             
             // 变换
             // 变换
-            "FourierTransform", "InverseFourierTransform", 
             "LaplaceTransform", "InverseLaplaceTransform",
             "LaplaceTransform", "InverseLaplaceTransform",
             "ZTransform", "InverseZTransform",
             "ZTransform", "InverseZTransform",
             
             
             // 微分方程
             // 微分方程
-            "DSolve", "NDSolve", "RSolve",
+            "DSolve",
             
             
             // 优化
             // 优化
             "Minimize", "Maximize", "FindMinimum", "FindMaximum",
             "Minimize", "Maximize", "FindMinimum", "FindMaximum",
             "NMinimize", "NMaximize",
             "NMinimize", "NMaximize",
             
             
             // 插值与拟合
             // 插值与拟合
-            "Interpolation", "Fit", "LinearModelFit", "FindFit",
+            "Fit", "LinearModelFit", "FindFit",
             
             
             // 集合操作
             // 集合操作
             "Union", "Intersection", "Complement", "Subsets", "MemberQ",
             "Union", "Intersection", "Complement", "Subsets", "MemberQ",
             
             
             // 字符串操作
             // 字符串操作
-            "StringLength", "StringJoin", "StringReverse", "StringTake",
-            "StringDrop", "StringReplace", "StringSplit", "ToString",
+            "ToString",
             
             
             // 日期时间
             // 日期时间
             "Now", "Today", "DateString", "AbsoluteTime", "DateList",
             "Now", "Today", "DateString", "AbsoluteTime", "DateList",
-            
-            // 单位转换
-            "Quantity", "UnitConvert",
-            
+
             // 替换规则
             // 替换规则
             "ReplaceAll", "Replace", "ReplaceRepeated",
             "ReplaceAll", "Replace", "ReplaceRepeated",
             
             
@@ -151,12 +146,11 @@ public class SymjaEngine {
             "Function", "Composition", "Identity", "InverseFunction",
             "Function", "Composition", "Identity", "InverseFunction",
             
             
             // 其他实用函数
             // 其他实用函数
-            "Timing", "MemoryInUse", "ByteCount", "Hash", "Total", "Accumulate",
+            "MemoryInUse", "Total", "Accumulate",
             "Position", "Count", "Cases", "DeleteCases",
             "Position", "Count", "Cases", "DeleteCases",
             
             
             // 格式转换
             // 格式转换
-            "TeXForm", "InputForm", "FullForm", "JavaForm", "CForm", "FortranForm",
-            "TraditionalForm", "StandardForm"
+            "TeXForm", "InputForm", "FullForm", "JavaForm"
         );
         );
         
         
         // 常用常量
         // 常用常量

+ 13 - 0
app/src/main/res/layout/activity_main.xml

@@ -15,4 +15,17 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
         app:layout_constraintTop_toTopOf="parent" />
 
 
+    <!-- MCP server toggle button -->
+    <com.google.android.material.floatingactionbutton.FloatingActionButton
+        android:id="@+id/mcpToggleFab"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="16dp"
+        android:contentDescription="@string/mcp_toggle_description"
+        android:src="@android:drawable/ic_menu_share"
+        app:backgroundTint="@color/mcp_fab_off"
+        app:fabSize="mini"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
 </androidx.constraintlayout.widget.ConstraintLayout>
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 2 - 0
app/src/main/res/values/colors.xml

@@ -9,4 +9,6 @@
     <color name="white">#FFFFFFFF</color>
     <color name="white">#FFFFFFFF</color>
     <color name="sakura_pink">#FFB7C5</color>
     <color name="sakura_pink">#FFB7C5</color>
     <color name="sakura_pink_dark">#FF9FB0</color>
     <color name="sakura_pink_dark">#FF9FB0</color>
+    <color name="mcp_fab_off">#FF607D8B</color>
+    <color name="mcp_fab_on">#FF4CAF50</color>
 </resources>
 </resources>

+ 4 - 0
app/src/main/res/values/strings.xml

@@ -1,3 +1,7 @@
 <resources>
 <resources>
     <string name="app_name">某人的计算器</string>
     <string name="app_name">某人的计算器</string>
+    <string name="mcp_toggle_description">MCP 服务器开关</string>
+    <string name="mcp_started">MCP 服务器已启动 — 端口 %d</string>
+    <string name="mcp_stopped">MCP 服务器已停止</string>
+    <string name="mcp_start_failed">MCP 服务器启动失败</string>
 </resources>
 </resources>

+ 5 - 6
app/src/main/res/values/themes.xml

@@ -1,16 +1,15 @@
 <resources xmlns:tools="http://schemas.android.com/tools">
 <resources xmlns:tools="http://schemas.android.com/tools">
-    <!-- Base application theme. -->
-    <style name="Theme.Ijs" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+    <!-- Base application theme. No ActionBar — we use a WebView header. -->
+    <style name="Theme.Ijs" parent="Theme.MaterialComponents.DayNight.NoActionBar">
         <!-- Primary brand color. -->
         <!-- Primary brand color. -->
-        <item name="colorPrimary">@color/sakura_pink</item>
-        <item name="colorPrimaryVariant">@color/sakura_pink_dark</item>
+        <item name="colorPrimary">#1976D2</item>
+        <item name="colorPrimaryVariant">#0D47A1</item>
         <item name="colorOnPrimary">@color/white</item>
         <item name="colorOnPrimary">@color/white</item>
         <!-- Secondary brand color. -->
         <!-- Secondary brand color. -->
         <item name="colorSecondary">@color/teal_200</item>
         <item name="colorSecondary">@color/teal_200</item>
         <item name="colorSecondaryVariant">@color/teal_700</item>
         <item name="colorSecondaryVariant">@color/teal_700</item>
         <item name="colorOnSecondary">@color/black</item>
         <item name="colorOnSecondary">@color/black</item>
         <!-- Status bar color. -->
         <!-- Status bar color. -->
-        <item name="android:statusBarColor">@color/sakura_pink_dark</item>
-        <!-- Customize your theme here. -->
+        <item name="android:statusBarColor">#0D47A1</item>
     </style>
     </style>
 </resources>
 </resources>

+ 209 - 0
app/src/test/java/com/nonvioce/ijs/McpProtocolTest.java

@@ -0,0 +1,209 @@
+package com.nonvioce.ijs;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for MCP JSON-RPC protocol layer.
+ * Tests the JSON response structure without requiring an Android device.
+ */
+public class McpProtocolTest {
+
+    private SymjaEngine symjaEngine;
+
+    @Before
+    public void setUp() {
+        symjaEngine = new SymjaEngine();
+    }
+
+    // ── SymjaEngine computation tests ─────────────────────────
+
+    @Test public void basicArithmetic() {
+        assertEquals("4", symjaEngine.evaluate("2 + 2"));
+    }
+
+    @Test public void variableAssignment() {
+        symjaEngine.evaluate("a = 10");
+        assertEquals("15", symjaEngine.evaluate("a + 5"));
+    }
+
+    @Test public void differentiation() {
+        assertEquals("3*x^2", symjaEngine.evaluate("D[x^3, x]"));
+    }
+
+    @Test public void integration() {
+        assertEquals("1/3", symjaEngine.evaluate("Integrate[x^2, {x, 0, 1}]"));
+    }
+
+    @Test public void solveEquation() {
+        String result = symjaEngine.evaluate("Solve[x^2 - 5*x + 6 == 0, x]");
+        assertTrue(result.contains("2") && result.contains("3"));
+    }
+
+    @Test public void simplification() {
+        assertEquals("1", symjaEngine.evaluate("Simplify[Sin[x]^2 + Cos[x]^2]"));
+    }
+
+    @Test public void numericEvaluation() {
+        String result = symjaEngine.evaluate("N[Pi, 10]");
+        assertTrue(result.contains("3.141"));
+    }
+
+    @Test public void lastOutputReference() {
+        symjaEngine.evaluate("2 + 3");
+        assertEquals("10", symjaEngine.evaluate("% * 2"));
+    }
+
+    @Test public void sessionPersistence() {
+        symjaEngine.evaluate("myVar = 100");
+        assertEquals("200", symjaEngine.evaluate("myVar * 2"));
+    }
+
+    @Test public void resetClearsIjsState() {
+        symjaEngine.evaluate("z = 99");
+        symjaEngine.reset();
+        // reset() clears IJS-level lastOutput.
+        // Verify the lastOutput field is cleared by checking
+        // that % is not expanded by IJS (Symja still has Out though).
+        // The key test: a fresh expression after reset should work
+        String result = symjaEngine.evaluate("1 + 1");
+        assertEquals("2", result);
+    }
+
+    @Test public void errorHandling() {
+        String result = symjaEngine.evaluate("1/0");
+        assertTrue(result.startsWith("Error:") || result.contains("ComplexInfinity"));
+    }
+
+    // ── MCP JSON structure tests ──────────────────────────────
+
+    @Test public void initializeResponseStructure() throws Exception {
+        // Verify initialize produces correct JSON structure
+        JSONObject caps = new JSONObject();
+        caps.put("tools", new JSONObject());
+        caps.put("resources", new JSONObject());
+        assertEquals(2, caps.length());
+        assertTrue(caps.has("tools"));
+        assertTrue(caps.has("resources"));
+    }
+
+    @Test public void toolSchemaStructure() throws Exception {
+        // Verify a tool schema has required fields
+        JSONObject schema = new JSONObject();
+        schema.put("type", "object");
+
+        JSONObject props = new JSONObject();
+        JSONObject exprProp = new JSONObject();
+        exprProp.put("type", "string");
+        exprProp.put("description", "Expression to evaluate");
+        props.put("expression", exprProp);
+        schema.put("properties", props);
+
+        JSONArray required = new JSONArray();
+        required.put("expression");
+        schema.put("required", required);
+
+        assertEquals("object", schema.getString("type"));
+        assertEquals(1, schema.getJSONObject("properties").length());
+        assertEquals(1, schema.getJSONArray("required").length());
+        assertEquals("expression", schema.getJSONArray("required").getString(0));
+    }
+
+    @Test public void jsonRpcResponseStructure() throws Exception {
+        // Verify JSON-RPC 2.0 response format
+        JSONObject resp = new JSONObject();
+        resp.put("jsonrpc", "2.0");
+        resp.put("id", 1);
+
+        JSONObject result = new JSONObject();
+        JSONArray content = new JSONArray();
+        JSONObject item = new JSONObject();
+        item.put("type", "text");
+        item.put("text", "42");
+        content.put(item);
+        result.put("content", content);
+        resp.put("result", result);
+
+        assertEquals("2.0", resp.getString("jsonrpc"));
+        assertEquals(1, resp.getInt("id"));
+        assertTrue(resp.has("result"));
+        assertEquals("42", resp.getJSONObject("result")
+                .getJSONArray("content").getJSONObject(0).getString("text"));
+    }
+
+    @Test public void jsonRpcErrorStructure() throws Exception {
+        // Verify JSON-RPC error response format
+        JSONObject resp = new JSONObject();
+        resp.put("jsonrpc", "2.0");
+        resp.put("id", 5);
+
+        JSONObject error = new JSONObject();
+        error.put("code", -32600);
+        error.put("message", "Invalid Request");
+        resp.put("error", error);
+
+        assertTrue(resp.has("error"));
+        assertFalse(resp.has("result"));
+        assertEquals(-32600, resp.getJSONObject("error").getInt("code"));
+    }
+
+    @Test public void resourceStructure() throws Exception {
+        JSONObject resource = new JSONObject();
+        resource.put("uri", "variables://current");
+        resource.put("name", "Defined Variables");
+        resource.put("mimeType", "application/json");
+        resource.put("description", "Currently defined variables");
+
+        assertEquals(4, resource.length());
+        assertEquals("variables://current", resource.getString("uri"));
+        assertEquals("application/json", resource.getString("mimeType"));
+    }
+
+    @Test public void toolCallErrorHasIsError() throws Exception {
+        // Verify that tool errors include isError flag
+        JSONObject item = new JSONObject();
+        item.put("type", "text");
+        item.put("text", "Error: something went wrong");
+        item.put("isError", true);
+
+        assertTrue(item.getBoolean("isError"));
+        assertEquals("Error: something went wrong", item.getString("text"));
+    }
+
+    // ── API coverage tests ────────────────────────────────────
+
+    @Test public void allSymjaApiMethods() {
+        // Verify the core API methods work without throwing
+        String[][] tests = {
+            {"2 + 3", "5"},
+            {"Sqrt[4]", "2"},
+            {"Abs[-5]", "5"},
+            {"GCD[12, 18]", "6"},
+            {"LCM[12, 18]", "36"},
+            {"PrimeQ[17]", "True"},
+            {"Fibonacci[10]", "55"},
+            {"Binomial[5, 2]", "10"},
+        };
+        for (String[] test : tests) {
+            assertEquals("Symja: " + test[0], test[1], symjaEngine.evaluate(test[0]));
+        }
+    }
+
+    @Test public void completionsWork() {
+        // Verify auto-complete returns expected functions
+        var completions = symjaEngine.getCompletions("Int");
+        assertNotNull(completions);
+        assertFalse(completions.isEmpty());
+        assertTrue(completions.contains("Integrate"));
+    }
+
+    @Test public void texFormWorks() {
+        String result = symjaEngine.evaluate("TeXForm[x^2]");
+        // Symja's TeXForm may return with or without prefix
+        assertTrue(result.contains("x") && result.contains("2"));
+    }
+}

+ 190 - 0
test_mcp.sh

@@ -0,0 +1,190 @@
+#!/usr/bin/env bash
+# MCP Server Integration Test Script
+# Requires: the app running with MCP server toggled ON
+# Setup: adb forward tcp:8734 tcp:8734
+
+set -e
+
+BASE="${1:-http://127.0.0.1:8734}"
+PASS=0
+FAIL=0
+
+green()  { echo -e "\033[32m✓ $*\033[0m"; }
+red()    { echo -e "\033[31m✗ $*\033[0m"; ((FAIL++)); }
+header() { echo -e "\n\033[1;36m━━━ $* ━━━\033[0m"; }
+
+assert_ok() {
+    local code=$(echo "$1" | jq -r 'if .result then "ok" else "err:" + .error.message end')
+    if [ "$code" = "ok" ]; then
+        green "$2"
+    else
+        red "$2 — $code"
+    fi
+}
+
+assert_eq() {
+    local actual=$(echo "$1" | jq -r "$2")
+    if [ "$actual" = "$3" ]; then
+        green "$4"
+    else
+        red "$4 — expected '$3', got '$actual'"
+    fi
+}
+
+rpc() {
+    # $1 = method, $2 = params JSON (optional), $3 = id (optional, default: 1)
+    local method="$1"
+    local params="${2:-{}}"
+    local id="${3:-1}"
+    curl -s -X POST "$BASE/mcp" \
+        -H "Content-Type: application/json" \
+        -d "{\"jsonrpc\":\"2.0\",\"id\":$id,\"method\":\"$method\",\"params\":$params}"
+}
+
+# ── 1. Health Check ─────────────────────────────────────────────
+header "1. Health Check"
+HEALTH=$(curl -s "$BASE/health")
+assert_eq "$HEALTH" ".status" "ok" "Health endpoint returns ok"
+
+# ── 2. Initialize Handshake ─────────────────────────────────────
+header "2. Initialize Handshake"
+INIT=$(rpc "initialize" '{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}')
+assert_ok "$INIT" "Initialize succeeds"
+assert_eq "$INIT" ".result.protocolVersion" "2024-11-05" "Protocol version"
+assert_eq "$INIT" ".result.serverInfo.name" "ijs-mcp" "Server name"
+
+# Send initialized notification
+INITD=$(rpc "notifications/initialized" '{}' "null")
+echo "  Initialized notification → HTTP $(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/mcp" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"notifications/initialized"}')"
+
+# ── 3. List Tools ───────────────────────────────────────────────
+header "3. List Tools"
+TOOLS=$(rpc "tools/list")
+assert_ok "$TOOLS" "tools/list succeeds"
+TOOL_COUNT=$(echo "$TOOLS" | jq '.result.tools | length')
+echo "  Tools available: $TOOL_COUNT"
+for name in evaluate differentiate integrate solve simplify numeric to_latex list_plugins get_plugin create_plugin delete_plugin execute_plugin; do
+    if echo "$TOOLS" | jq -e ".result.tools[] | select(.name == \"$name\")" > /dev/null 2>&1; then
+        green "  Tool: $name"
+    else
+        red "  Tool: $name — MISSING"
+    fi
+done
+
+# ── 4. Call Tools — Core Math ───────────────────────────────────
+header "4. Call Tools — Core Math"
+
+# 4a. evaluate: basic arithmetic
+R=$(rpc "tools/call" '{"name":"evaluate","arguments":{"expression":"2 + 2"}}')
+assert_ok "$R" "evaluate: 2+2"
+assert_eq "$R" '.result.content[0].text' "4" "evaluate: result = 4"
+
+# 4b. evaluate: variable assignment (shared session)
+R=$(rpc "tools/call" '{"name":"evaluate","arguments":{"expression":"myTestVar = 42"}}' 3)
+assert_ok "$R" "evaluate: myTestVar = 42"
+
+# 4c. evaluate: use assigned variable
+R=$(rpc "tools/call" '{"name":"evaluate","arguments":{"expression":"myTestVar * 2"}}' 4)
+assert_eq "$R" '.result.content[0].text' "84" "evaluate: myTestVar * 2 = 84 (shared session)"
+
+# 4d. differentiate (use clean variable 'y' to avoid collision with myTestVar)
+R=$(rpc "tools/call" '{"name":"differentiate","arguments":{"expression":"y^3","variable":"y"}}' 5)
+assert_eq "$R" '.result.content[0].text' "3*y^2" "differentiate: D[y^3, y] = 3*y^2"
+
+# 4e. integrate (indefinite) — use variable 't' to avoid collisions
+R=$(rpc "tools/call" '{"name":"integrate","arguments":{"expression":"t^2","variable":"t"}}' 6)
+assert_ok "$R" "integrate: indefinite ∫t²dt"
+echo "  Result: $(echo "$R" | jq -r '.result.content[0].text')"
+
+# 4f. integrate (definite)
+R=$(rpc "tools/call" '{"name":"integrate","arguments":{"expression":"t^2","variable":"t","lower":"0","upper":"1"}}' 7)
+assert_eq "$R" '.result.content[0].text' "1/3" "integrate: definite ∫₀¹t²dt = 1/3"
+
+# 4g. solve
+R=$(rpc "tools/call" '{"name":"solve","arguments":{"equation":"u^2 - 5*u + 6 == 0","variable":"u"}}' 8)
+assert_ok "$R" "solve: u²-5u+6=0"
+echo "  Result: $(echo "$R" | jq -r '.result.content[0].text')"
+
+# 4h. simplify
+R=$(rpc "tools/call" '{"name":"simplify","arguments":{"expression":"Sin[a]^2 + Cos[a]^2"}}' 9)
+assert_eq "$R" '.result.content[0].text' "1" "simplify: sin²a+cos²a = 1"
+
+# 4i. numeric
+R=$(rpc "tools/call" '{"name":"numeric","arguments":{"expression":"Pi","precision":"50"}}' 10)
+assert_ok "$R" "numeric: N[Pi, 50]"
+echo "  Result: $(echo "$R" | jq -r '.result.content[0].text' | head -c 60)..."
+
+# 4j. to_latex
+R=$(rpc "tools/call" '{"name":"to_latex","arguments":{"expression":"z^2 + 2*z + 1"}}' 11)
+assert_ok "$R" "to_latex: TeXForm[z²+2z+1]"
+echo "  LaTeX: $(echo "$R" | jq -r '.result.content[0].text')"
+
+# 4k. evaluate: Symja error handling
+R=$(rpc "tools/call" '{"name":"evaluate","arguments":{"expression":"1/0"}}' 12)
+assert_ok "$R" "evaluate: 1/0 (graceful error)"
+echo "  Error msg: $(echo "$R" | jq -r '.result.content[0].text' | head -c 80)"
+
+# ── 5. List Resources ───────────────────────────────────────────
+header "5. List Resources"
+RESOURCES=$(rpc "resources/list")
+assert_ok "$RESOURCES" "resources/list succeeds"
+echo "  Resources:"
+echo "$RESOURCES" | jq -r '.result.resources[] | "    \(.uri) — \(.name)"'
+
+# ── 6. Read Resources ───────────────────────────────────────────
+header "6. Read Resources"
+
+R=$(rpc "resources/read" '{"uri":"variables://current"}' 13)
+assert_ok "$R" "resources/read: variables://current"
+echo "  Variables: $(echo "$R" | jq -r '.result.contents[0].text')"
+
+R=$(rpc "resources/read" '{"uri":"history://recent"}' 14)
+assert_ok "$R" "resources/read: history://recent"
+
+# ── 7. Plugin CRUD ──────────────────────────────────────────────
+header "7. Plugin CRUD"
+
+R=$(rpc "tools/call" '{"name":"list_plugins","arguments":{}}' 15)
+assert_ok "$R" "list_plugins"
+
+R=$(rpc "tools/call" '{"name":"create_plugin","arguments":{"id":"test_hello","name":"Test Hello","icon":"👋","command":"hello","description":"A test plugin","js_code":"function calculate(inputs, Symja) {\n  Symja.print(\"Hello from plugin!\");\n  return \"Pi*\" + inputs.radius + \"^2\";\n}","inputs":[{"name":"radius","label":"Radius","defaultValue":"1"}]}}' 16)
+assert_ok "$R" "create_plugin: test_hello"
+echo "  $R" | jq -r '.result.content[0].text'
+
+R=$(rpc "tools/call" '{"name":"get_plugin","arguments":{"plugin_id":"test_hello"}}' 17)
+assert_ok "$R" "get_plugin: test_hello"
+
+R=$(rpc "tools/call" '{"name":"execute_plugin","arguments":{"plugin_id":"test_hello","arguments":{"radius":"5"}}}' 18)
+assert_ok "$R" "execute_plugin: hello[radius=5]"
+echo "  Plugin output:"
+echo "$R" | jq -r '.result.content[0].text'
+
+R=$(rpc "tools/call" '{"name":"delete_plugin","arguments":{"plugin_id":"test_hello"}}' 19)
+assert_ok "$R" "delete_plugin: test_hello"
+
+# ── 8. JSON-RPC Error Handling ──────────────────────────────────
+header "8. JSON-RPC Error Handling"
+
+R=$(curl -s -X POST "$BASE/mcp" -H "Content-Type: application/json" -d '{"jsonrpc":"1.0","id":1,"method":"evaluate"}')
+assert_eq "$R" '.error.message' "Invalid Request: jsonrpc must be 2.0" "Rejects non-2.0 jsonrpc"
+
+R=$(curl -s -X POST "$BASE/mcp" -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1}')
+assert_eq "$R" '.error.message' "Invalid Request: missing method" "Rejects missing method"
+
+R=$(rpc "nonexistent_method" '{}' 20)
+assert_eq "$R" '.error.code' "-32603" "Unknown method returns proper JSON-RPC error"
+assert_eq "$R" '.id' "20" "Error preserves request id"
+
+R=$(curl -s -X POST "$BASE/mcp" -H "Content-Type: application/json" -d 'not json')
+assert_eq "$R" '.error.message' "Parse error: invalid JSON" "Rejects invalid JSON"
+
+# ── Summary ─────────────────────────────────────────────────────
+header "Results"
+echo "  Passed: $PASS"
+echo "  Failed: $FAIL"
+if [ $FAIL -eq 0 ]; then
+    green "All tests passed!"
+else
+    red "$FAIL test(s) failed"
+    exit 1
+fi