Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5e021e0
Initial plan
Copilot Jan 17, 2026
385de84
Add Remote MCP Server support with UI
Copilot Jan 17, 2026
f537004
Address code review feedback - fix hardcoded values and security impr…
Copilot Jan 17, 2026
2281975
Fix DAO default parameters and explicit timestamp passing
Copilot Jan 17, 2026
f0651a2
Fix MCP SSE support for Zapier MCP servers and build errors
Copilot Jan 17, 2026
4996618
Improve SSE parsing to correctly handle event format per SSE spec
Copilot Jan 17, 2026
df94f6e
Add proper Streamable HTTP transport support alongside SSE
Copilot Jan 17, 2026
a360b6b
Fix code review comments on transport documentation
Copilot Jan 17, 2026
669318b
Address code review feedback and add GitHub workflow for debug APK
Copilot Jan 17, 2026
34b3dba
Fix race condition in error auto-clearing
Copilot Jan 17, 2026
5f3ea4c
Merge pull request #1 from Godzilla675/copilot/add-remote-mcp-server-…
Godzilla675 Jan 17, 2026
aac348a
Merge branch 'Siddhesh2377:re-write' into re-write
Godzilla675 Jan 17, 2026
dd31ae5
Initial plan
Copilot Jan 17, 2026
da0af0c
Enable BuildConfig for memory-vault
Copilot Jan 17, 2026
18aea53
Merge pull request #2 from Godzilla675/copilot/fix-debug-workflow-build
Godzilla675 Jan 17, 2026
c2e79bf
Initial plan
Copilot Jan 17, 2026
148d979
Integrate MCP tools with GGUF
Copilot Jan 17, 2026
89dde9b
Refine MCP tool call execution
Copilot Jan 17, 2026
524d6eb
Merge pull request #3 from Godzilla675/copilot/test-ai-models-access
Godzilla675 Jan 17, 2026
03cfc52
Initial plan
Copilot Jan 17, 2026
d8692ff
Add MCP server integration tests
Copilot Jan 17, 2026
8872229
Fix MCP integration test input schemas to include required instructio…
Copilot Jan 17, 2026
cdb24df
Fix Streamable HTTP transport and improve MCP server tests
Copilot Jan 17, 2026
a87425a
Merge pull request #5 from Godzilla675/copilot/connect-ai-model-to-mc…
Godzilla675 Jan 17, 2026
dd3e72c
Fix MCP tool calling bugs: sanitizer, fallback parsing, error handling
Godzilla675 Feb 21, 2026
19e5004
feat: Add MCP Store and Termux integration
Godzilla675 Feb 21, 2026
0e3c723
Merge upstream re-write: resolve conflicts, integrate MCP with Plugin…
Godzilla675 Feb 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copilot Instructions for ToolNeuron

## Build & Test

```bash
# Build debug APK
./gradlew assembleDebug

# Build release APK (requires signing config in local.properties)
./gradlew assembleRelease

# Run all unit tests
./gradlew test

# Run tests for a single module
./gradlew :app:test
./gradlew :memory-vault:test
./gradlew :neuron-packet:test

# Run a single test class
./gradlew :app:testDebugUnitTest --tests "com.dark.tool_neuron.McpToolMapperTest"
```

**Requirements:** JDK 17, Android SDK 36, NDK 26.x. The `neuron-packet` module requires OpenSSL prebuilt libraries — see `neuron-packet/SETUP.md`.

## Architecture

This is an Android app (Kotlin + C++) that runs LLMs and Stable Diffusion entirely on-device. It's a multi-module Gradle project:

- **`app`** — Main application. Jetpack Compose UI, MVVM with Hilt DI, Room database. Package: `com.dark.tool_neuron`.
- **`memory-vault`** — Encrypted binary storage engine with WAL crash recovery, LZ4 compression, full-text and vector indices. Package: `com.memoryvault`. See `docs/MemoryVault.MD` for the storage format spec.
- **`neuron-packet`** — Secure data export/import with AES-256-GCM encryption. Has both Kotlin and C++ (JNI) sides. Package: `com.neuronpacket`. The C++ code lives in `neuron-packet/src/main/cpp/` and builds via CMake.

### AI inference layer

Native inference is provided by pre-built AAR libraries in `libs/`:
- `ai_gguf-release.aar` — llama.cpp bindings for text generation (GGUF models)
- `ai_sd-release.aar` — Stable Diffusion 1.5 bindings for image generation

These are wrapped by engine classes in `app/.../engine/`:
- `GGUFEngine` — loads GGUF models, generates text, supports function calling with tool grammars
- `DiffusionEngine` — loads SD models, generates images
- `EmbeddingEngine` — generates text embeddings for RAG/vector search

`LLMService` is a bound Android Service that exposes these engines via AIDL IPC.

### Data flow

`UI (Compose screens)` → `ViewModel (@HiltViewModel)` → `Repository` → `Room DAO / MemoryVault`

ViewModels expose `StateFlow` for reactive UI updates. All async work uses Kotlin Coroutines with `viewModelScope`.

## Key Conventions

- **DI:** Hilt everywhere. Activities use `@AndroidEntryPoint`, ViewModels use `@HiltViewModel`. All modules are defined in `app/.../di/HiltModules.kt` and installed in `SingletonComponent`.
- **Navigation:** Jetpack Compose NavHost in `MainActivity`. Routes are defined as a `Screen` sealed class. Uses slide + fade transitions.
- **Serialization:** `kotlinx.serialization` for JSON. Room entities live in `models/table_schema/`.
- **NDK targets:** `arm64-v8a` and `x86_64` only.
- **Build config:** Properties are read from `local.properties` or environment variables via `getProperty()` (defined in each module's `build.gradle.kts`). The `ALIAS` property is used for build config fields.
- **UI constants:** Shared sizing/padding values are in `global/Standards.kt`.
- **Version catalog:** All dependency versions are managed in `gradle/libs.versions.toml`.
35 changes: 35 additions & 0 deletions .github/workflows/build-debug-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Build Debug APK

on:
pull_request:
branches: [ main, master ]
workflow_dispatch:

jobs:
build:
name: Build Debug APK
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build Debug APK
run: ./gradlew assembleDebug --no-daemon

- name: Upload Debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug
path: app/build/outputs/apk/debug/app-debug.apk
retention-days: 14
6 changes: 5 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ dependencies {

// Debug
debugImplementation(libs.androidx.compose.ui.tooling)

// Tests
testImplementation(libs.junit)
testImplementation(libs.org.json)
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The org.json dependency is being added for testing, but this library is already included in the Android SDK. This creates redundant dependencies and potential version conflicts. Consider removing this dependency since org.json is available by default in Android.

Suggested change
testImplementation(libs.org.json)

Copilot uses AI. Check for mistakes.
}

fun getProperty(value: String): String {
Expand All @@ -163,4 +167,4 @@ fun getProperty(value: String): String {
} else {
System.getenv(value) ?: "\"sample_val\""
}
}
}
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="com.termux.permission.RUN_COMMAND" />

<!-- Required for Termux package visibility on API 30+ -->
<queries>
<package android:name="com.termux" />
</queries>

<application
android:name=".NVApplication"
Expand Down
156 changes: 156 additions & 0 deletions app/src/main/assets/mcp-registry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
[
{
"id": "brave-search",
"name": "Brave Search",
"description": "Web search using the Brave Search API. Provides real-time web results, news, and more.",
"url": "https://mcp.brave.com/sse",
"transportType": "SSE",
"category": "Search",
"requiresApiKey": true,
"requiresTermux": false,
"author": "Brave Software",
"tags": ["search", "web", "news"],
"iconName": "Search",
"setupInstructions": "Get a free API key at https://brave.com/search/api/"
},
{
"id": "github-mcp",
"name": "GitHub",
"description": "Access GitHub repositories, issues, PRs, and code search via the GitHub API.",
"url": "https://api.githubcopilot.com/mcp/",
"transportType": "STREAMABLE_HTTP",
"category": "Code",
"requiresApiKey": true,
"requiresTermux": false,
"author": "GitHub",
"tags": ["github", "code", "repositories", "issues"],
"iconName": "Code",
"setupInstructions": "Use a GitHub Personal Access Token (PAT) with appropriate scopes."
},
{
"id": "fetch-mcp",
"name": "Fetch",
"description": "Fetch and parse web pages. Converts HTML to markdown for LLM consumption.",
"url": "",
"transportType": "SSE",
"category": "Utilities",
"requiresApiKey": false,
"requiresTermux": true,
"pipPackage": "mcp-server-fetch",
"defaultPort": 8100,
"author": "Anthropic",
"tags": ["web", "fetch", "scraping", "markdown"],
"iconName": "Language",
"setupInstructions": "Requires Termux with Python installed. Will auto-install via pip."
},
{
"id": "filesystem-mcp",
"name": "Filesystem",
"description": "Read, write, and manage files on the device. Sandboxed to a configurable directory.",
"url": "",
"transportType": "SSE",
"category": "Files",
"requiresApiKey": false,
"requiresTermux": true,
"pipPackage": "mcp-server-filesystem",
"defaultPort": 8101,
"author": "Anthropic",
"tags": ["files", "filesystem", "read", "write"],
"iconName": "Folder",
"setupInstructions": "Requires Termux with Python installed. Files will be sandboxed to Termux home directory."
},
{
"id": "sqlite-mcp",
"name": "SQLite",
"description": "Query and manage SQLite databases. Execute SQL statements and explore schemas.",
"url": "",
"transportType": "SSE",
"category": "Data",
"requiresApiKey": false,
"requiresTermux": true,
"pipPackage": "mcp-server-sqlite",
"defaultPort": 8102,
"author": "Anthropic",
"tags": ["database", "sql", "sqlite", "query"],
"iconName": "Storage",
"setupInstructions": "Requires Termux with Python installed. Specify database path after installation."
},
{
"id": "memory-mcp",
"name": "Memory",
"description": "Persistent key-value memory for the AI. Stores and retrieves information across conversations.",
"url": "",
"transportType": "SSE",
"category": "AI",
"requiresApiKey": false,
"requiresTermux": true,
"pipPackage": "mcp-server-memory",
"defaultPort": 8103,
"author": "Anthropic",
"tags": ["memory", "persistence", "knowledge"],
"iconName": "Psychology",
"setupInstructions": "Requires Termux with Python installed. Memory is stored locally."
},
{
"id": "everything-mcp",
"name": "Everything",
"description": "Reference MCP server with examples of all MCP features. Great for testing your setup.",
"url": "",
"transportType": "SSE",
"category": "Utilities",
"requiresApiKey": false,
"requiresTermux": true,
"pipPackage": "mcp-server-everything",
"defaultPort": 8104,
"author": "Anthropic",
"tags": ["testing", "reference", "demo"],
"iconName": "Science",
"setupInstructions": "Requires Termux with Python installed. A demo server for testing."
},
{
"id": "duckduckgo-mcp",
"name": "DuckDuckGo Search",
"description": "Free web search using DuckDuckGo. No API key required.",
"url": "",
"transportType": "SSE",
"category": "Search",
"requiresApiKey": false,
"requiresTermux": true,
"pipPackage": "mcp-server-duckduckgo",
"defaultPort": 8105,
"author": "Community",
"tags": ["search", "web", "free", "privacy"],
"iconName": "Search",
"setupInstructions": "Requires Termux with Python installed. Free search with no API key needed."
},
{
"id": "youtube-mcp",
"name": "YouTube Transcript",
"description": "Fetch YouTube video transcripts and captions for LLM analysis.",
"url": "",
"transportType": "SSE",
"category": "Data",
"requiresApiKey": false,
"requiresTermux": true,
"pipPackage": "mcp-server-youtube-transcript",
"defaultPort": 8106,
"author": "Community",
"tags": ["youtube", "transcript", "video", "captions"],
"iconName": "OndemandVideo",
"setupInstructions": "Requires Termux with Python installed. Fetches transcripts from public YouTube videos."
},
{
"id": "custom-remote",
"name": "Custom Remote Server",
"description": "Connect to any remote MCP server by providing its URL. Supports SSE and Streamable HTTP.",
"url": "",
"transportType": "SSE",
"category": "Utilities",
"requiresApiKey": false,
"requiresTermux": false,
"author": "ToolNeuron",
"tags": ["custom", "remote", "self-hosted"],
"iconName": "Cloud",
"setupInstructions": "Enter the URL of your MCP server. It should support SSE or Streamable HTTP transport."
}
]
30 changes: 19 additions & 11 deletions app/src/main/java/com/dark/tool_neuron/activity/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import com.dark.tool_neuron.data.TermsDataStore
import com.dark.tool_neuron.di.AppContainer
import com.dark.tool_neuron.models.enums.ProviderType
import com.dark.tool_neuron.ui.screen.AiMemoryScreen
import com.dark.tool_neuron.ui.screen.McpServersScreen
import com.dark.tool_neuron.ui.screen.McpStoreScreen
import com.dark.tool_neuron.ui.screen.ModelConfigEditorScreen
import com.dark.tool_neuron.ui.screen.ModelStoreScreen
import com.dark.tool_neuron.ui.screen.PersonaEditorScreen
Expand Down Expand Up @@ -97,14 +99,9 @@ class MainActivity : ComponentActivity() {
hasModelsInstalled = hasModel

startDestination = when {
// Terms not accepted: returning user (has models) goes to terms directly, new user gets full onboarding
!termsAccepted && hasModel -> Screen.Terms.route
!termsAccepted -> Screen.Welcome.route

// Terms accepted but setup not done and no models: go to setup
!setupDone && !hasModel -> Screen.OnboardingSetup.route

// Everything done
else -> Screen.Chat.route
}
}
Expand All @@ -122,15 +119,14 @@ class MainActivity : ComponentActivity() {

override fun onDestroy() {
super.onDestroy()
// Clear password cache when app terminates
ragRepository.clearPasswordCache()
LlmModelWorker.unbindService()
AppContainer.shutdown()
}
}

sealed class Screen(val route: String) {
// Onboarding (flat routes so any can be used as startDestination)
// Onboarding
object Welcome : Screen("welcome")
object Terms : Screen("terms")
object OnboardingSetup : Screen("setup")
Expand All @@ -141,6 +137,8 @@ sealed class Screen(val route: String) {
object Editor : Screen("editor")
object Settings : Screen("settings")
object VaultManager : Screen("vault_manager")
object McpServers : Screen("mcp_servers")
object McpStore : Screen("mcp_store")
object Personas : Screen("personas")
object PersonaEditor : Screen("persona_editor/{personaId}") {
fun createRoute(personaId: String? = null) = "persona_editor/${personaId ?: "new"}"
Expand All @@ -157,7 +155,6 @@ fun AppNavigation(
val scope = rememberCoroutineScope()
val navController = rememberNavController()

// Activity-scoped ViewModels for shared state between Chat and Personas
val chatViewModel: ChatViewModel = hiltViewModel()
val llmModelViewModel: LLMModelViewModel = hiltViewModel()

Expand Down Expand Up @@ -207,12 +204,10 @@ fun AppNavigation(
termsDataStore.acceptTerms()
}
if (hasModelsInstalled) {
// Returning user: skip setup, go to chat
navController.navigate(Screen.Chat.route) {
popUpTo(0) { inclusive = true }
}
} else {
// New user: proceed to setup
navController.navigate(Screen.OnboardingSetup.route)
}
}
Expand Down Expand Up @@ -241,6 +236,9 @@ fun AppNavigation(
onVaultManagerClick = {
navController.navigate(Screen.VaultManager.route)
},
onMcpServersClick = {
navController.navigate(Screen.McpServers.route)
},
onCharacterClick = {
navController.navigate(Screen.Personas.route)
},
Expand Down Expand Up @@ -304,7 +302,6 @@ fun AppNavigation(
personaId = personaId,
onNavigateBack = { navController.popBackStack() },
onDeleted = {
// If deleted persona was active, clear selection
if (personaId != null && chatViewModel.activePersona.value?.id == personaId) {
chatViewModel.setActivePersona(null)
}
Expand All @@ -318,5 +315,16 @@ fun AppNavigation(
onNavigateBack = { navController.popBackStack() }
)
}

composable(Screen.McpServers.route) {
McpServersScreen(
onBackClick = { navController.popBackStack() },
onStoreClick = { navController.navigate(Screen.McpStore.route) }
)
}

composable(Screen.McpStore.route) {
McpStoreScreen(onBackClick = { navController.popBackStack() })
}
}
}
Loading