Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego
| [`ai-core/`](ai-core/) | The **Agent** chat (tool-calling assistant) plus the shared LLM inference **router** other plugins consume. Ships no model — install at least one backend plugin below. Mandatory for every AI feature. |
| [`ai-agent-local/`](ai-agent-local/) | On-device `.gguf` inference backend for `ai-core` (bundled llama.cpp AAR). Registers as `local`; needs no network. |
| [`ai-agent-gemini/`](ai-agent-gemini/) | Google Gemini API inference backend for `ai-core`. Registers as `gemini`; needs an API key and network access. |
| [`ai-agent-mcp/`](ai-agent-mcp/) | Connects the Agent to Model Context Protocol servers, contributing their tools through `ai-core`. Needs network access; tools are off until enabled. |
| [`flutter-template/`](flutter-template/) | Adds Flutter starter project templates (Basic, BLoC, Provider, GetX, Riverpod) to the New Project screen. |
| [`code-suggestions-plugin/`](code-suggestions-plugin/) | Inline ghost-text code completions powered by AI. |
| [`speech-to-text-plugin/`](speech-to-text-plugin/) | Voice-to-code: converts speech to code with AI generation. |
Expand Down
3 changes: 3 additions & 0 deletions ai-agent-mcp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**/.cxx/
build-output.log
**/.kotlin/
88 changes: 88 additions & 0 deletions ai-agent-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# AI Agent MCP plugin for CodeOnTheGo

Connects CodeOnTheGo's Agent to **Model Context Protocol** servers. Tools a
configured server advertises are contributed to
[`ai-core`](../ai-core/)'s agent through the host's `ToolSourceRegistry`, so they
appear beside the Agent's own tools with no change to `ai-core`.

This is a *tool* plugin, not a model backend: it adds no inference. Install
`ai-core` and at least one backend (`ai-agent-local`, `ai-agent-gemini`) as well.

## Why a separate plugin

`ai-core` declares the filesystem, shell and project permissions its own tools
need, and **no network access**. MCP is the mirror image: it declares
`network.access` and nothing else. Keeping them apart is what lets a user install
the Agent without granting it the network, or add remote tools without widening
what the Agent itself may touch.

## Building

Prerequisites: Android SDK (API 33+), JDK 17. Create `local.properties` with
`sdk.dir=...`. This plugin uses the shared wrapper at the repo root:

```bash
cd ai-agent-mcp
../gradlew assemblePlugin # release -> build/plugin/ai-agent-mcp.cgp
../gradlew assemblePluginDebug # debug variant
../gradlew testDebugUnitTest # JVM tests: framing, error classification, sanitising
```

## Using it

Install the `.cgp` through the Plugin Manager, then open **Preferences →
Configuration → MCP servers**:

1. **Add server** — name, endpoint URL, optional token.
2. **Test connection** — performs the MCP handshake and reports one sentence.
3. **Refresh tools** — lists what the server offers.
4. Switch on the tools you want. **They start off**: one popular GitHub server
advertises around ninety tools, which would fill a phone-sized context window
on its own.

Every remote tool asks for approval on every call, and there is no "always
allow" for contributed tools — they run outside the Agent's own path
containment, so the dialog is the only gate.

## Transport

MCP's **Streamable HTTP** transport over `HttpURLConnection`:

- one POST per JSON-RPC call, reading either a JSON body or the SSE stream
carrying the same document;
- `Mcp-Session-Id` echoed when the server is stateful, dropped when it is not
(the 2026-07-28 revision is stateless), with one clean re-handshake when a
session expires;
- the bearer token as an `Authorization` header, never a query string.

Two dependencies were deliberately not taken:

- **No OkHttp.** Plugins run in the host IDE's classloader, where `okhttp3`
resolves to the host's older copy; a bundled SDK crashes with
`NoSuchMethodError`.
- **No official MCP Kotlin SDK.** It is KMP with no stated Android target, ships
no HTTP engine, and required pinning Kotlin 2.4.10 when it was tried
(ADFA-5083) against the 2.3.0 these plugins standardise on.

## Layout

- `plugin/McpPlugin.kt` — entry point; registers the tool source with `ai-core`
and re-registers from a `PluginLifecycleListener`, since plugins load in
parallel with no ordering
- `transport/` — `JsonRpc` framing, `SseChunk` line parsing, `McpHttpClient`
- `client/` — `McpSession` (handshake, `tools/list`, `tools/call`),
`McpConnections` (one session per server)
- `tools/` — `McpToolSource` (the `ToolSourceRegistry.ToolSource`),
`McpToolCatalog` (what each server last advertised), `McpToolText` (sanitising)
- `settings/` — server CRUD, per-tool toggles, the settings pane
- `errors/` — HTTP and JSON-RPC failures reduced to one translated sentence
- `security/` — Keystore-backed token encryption

## Security notes

- Tool names and descriptions from a server are **untrusted remote text**. They
are reduced to `[a-z0-9_]` and to a single capped line before they can reach a
system prompt assembled inside a backend plugin.
- Tokens are encrypted with an AES/GCM key held in the Android Keystore under
this plugin's own alias; only ciphertext is written to disk.
- Error bodies stay in logcat. The transcript gets one sentence.
184 changes: 184 additions & 0 deletions ai-agent-mcp/ai-agent-mcp.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Agent MCP Plugin</title>
<style>
body {
background: #ffffff;
color: #000000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 1.25rem;
max-width: 820px;
}
h1 { font-size: 1.6rem; margin: 0 0 0.75rem; }
h2 { font-size: 1.2rem; margin: 1.75rem 0 0.5rem; border-bottom: 1px solid #e0e0e0; padding-bottom: 0.2rem; }
code {
background: #f4f4f4;
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-family: "SF Mono", Menlo, Consolas, monospace;
font-size: 0.9em;
}
ul { padding-left: 1.25rem; }
li { margin: 0.3rem 0; }
table { border-collapse: collapse; margin: 0.5rem 0; width: 100%; }
th, td { border: 1px solid #ddd; padding: 0.4rem 0.6rem; text-align: left; vertical-align: top; }
th { background: #f4f4f4; }
.note {
border-left: 3px solid #999;
padding: 0.25rem 0.75rem;
margin: 1rem 0;
background: #fafafa;
}
</style>
</head>
<body>
<h1>AI Agent MCP Plugin</h1>

<h2>Executive overview</h2>
<p><b>AI Agent MCP</b> connects CodeOnTheGo's <b>Agent</b> to <b>Model Context
Protocol</b> servers. A server you configure advertises tools — search a
documentation index, open a ticket, query an internal API — and those tools
appear in the Agent's tool list beside its built-in ones, so the assistant can
reach systems that live outside the device.</p>
<p><b>This plugin adds tools, not a model.</b> Install <b>AI Core</b> and at
least one backend (<b>AI Agent Local</b> or <b>AI Agent Gemini</b>) as well;
on its own this plugin has no assistant to contribute to. Install order does
not matter — it registers with AI Core whenever AI Core activates.</p>
<p>It is deliberately a plugin of its own rather than part of AI Core. AI Core
declares the filesystem, shell and project permissions its own tools need and
<b>no network access</b>; this plugin is the mirror image, declaring
<b>network.access and nothing else</b>. That split is what lets you run the
Agent without granting it the network, or add remote tools without widening
what the Agent itself may touch.</p>

<h2>Core functionality</h2>
<ul>
<li><b>Server management</b> — add, edit, disable and remove MCP servers from
one screen under <b>Preferences &rarr; Configuration &rarr; MCP
servers</b>.</li>
<li><b>Test connection</b> — performs the MCP handshake and reports the
result in one sentence, so a wrong URL or a stale token is caught while you
are looking at the settings rather than mid-conversation.</li>
<li><b>Tool discovery</b> — reads a server's tool catalogue, including
paginated catalogues, and remembers what it advertised.</li>
<li><b>Per-tool switches, off by default</b> — you choose which of a server's
tools the Agent may see. One popular GitHub server advertises around ninety
tools, enough to fill a phone-sized context window on its own.</li>
<li><b>Approval on every call</b> — a remote tool always asks, showing the
arguments it would send, and can never be granted "always allow".</li>
<li><b>Encrypted tokens</b> — bearer tokens are encrypted with an Android
Keystore key and sent only to the server they belong to.</li>
<li><b>Legible failures</b> — an unreachable server, a refused token or a URL
that is not an MCP endpoint each produce one plain sentence, never a raw
error body in the chat transcript.</li>
</ul>

<h2>Technical architecture</h2>
<table>
<tr><th>Component</th><th>Role</th></tr>
<tr><td><code>McpPlugin</code></td><td>Plugin entry point. Contributes the MCP
servers settings row, registers the tool source with AI Core on activation
and withdraws it on deactivation. A <code>PluginLifecycleListener</code>
re-registers if AI Core activates later, since plugins load in parallel with
no ordering.</td></tr>
<tr><td><code>McpToolSource</code></td><td>The tool source AI Core sees.
Publishes the enabled tools of the enabled servers, runs a call on its own
thread pool, and cancels in flight when an Agent run is stopped.</td></tr>
<tr><td><code>McpToolCatalog</code></td><td>What each server last advertised,
held in memory. The Agent reads its tool list on a UI-adjacent path, so that
list is answered from memory and never blocks on the network.</td></tr>
<tr><td><code>McpSession</code></td><td>One protocol conversation per server:
<code>initialize</code>, <code>tools/list</code>, <code>tools/call</code>,
session lifetime, and one clean re-handshake when a server drops a
session.</td></tr>
<tr><td><code>McpHttpClient</code> / <code>JsonRpc</code> / <code>SseChunk</code></td>
<td>The transport: one POST per JSON-RPC call, reading either a JSON body or
the server-sent-events stream carrying the same document. Connections are
closed on every path.</td></tr>
<tr><td><code>McpToolText</code></td><td>Sanitises server-supplied names and
descriptions — untrusted remote text that would otherwise land verbatim in a
prompt assembled inside a third-party backend plugin.</td></tr>
<tr><td><code>SecureTokenStore</code></td><td>AES/GCM encryption under a
Keystore alias owned by this plugin, so only ciphertext reaches disk.</td></tr>
</table>
<p>The transport is MCP's <b>Streamable HTTP</b> revision over
<code>HttpURLConnection</code>. Two dependencies were deliberately not taken:
<b>no OkHttp</b>, because plugins run in the host IDE's class loader where
<code>okhttp3</code> resolves to the host's older copy and a bundled SDK crashes
with <code>NoSuchMethodError</code>; and <b>no official MCP Kotlin SDK</b>,
which is a Kotlin Multiplatform library with no stated Android target, ships no
HTTP engine, and required a Kotlin version these plugins do not standardise
on.</p>
<p>The plugin declares <b>network.access</b> and nothing else. It holds no
filesystem, shell or project permission, so it cannot read your project;
project content reaches a server only if the model puts it in a tool argument
and you approve that call.</p>

<h2>Usage</h2>
<ol>
<li>Install <b>AI Core</b> and a backend, then install <b>AI Agent MCP</b> via
the Plugin Manager.</li>
<li>Open <b>Preferences &rarr; Configuration &rarr; MCP servers</b> and tap
<b>Add server</b>.</li>
<li>Enter a <b>name</b> — it is also the prefix on that server's tool names,
so two servers offering <code>search</code> stay distinguishable — the
<b>endpoint URL</b> (usually ending in <code>/mcp</code>), and an
<b>access token</b> if the server needs one.</li>
<li>Tap <b>Test connection</b>, then <b>Refresh tools</b> to list what the
server offers.</li>
<li>Switch on the tools you want, and save. Ask the Agent for something those
tools cover; it will request your approval before each call.</li>
</ol>
<div class="note">
New tools always arrive switched <b>off</b>, including after a refresh that
discovers them. Nothing a server adds later becomes visible to the model
without your say-so.
</div>

<h2>Key benefits</h2>
<ul>
<li><b>Reaches systems the IDE cannot</b> — issue trackers, documentation
indexes and internal APIs become things the Agent can query, without
bundling an integration for each one.</li>
<li><b>Least privilege</b> — network access is declared by this plugin alone;
installing it does not widen what the Agent's own tools may touch, and
uninstalling it removes the network capability entirely.</li>
<li><b>Context stays affordable</b> — per-tool switches, off by default, keep
a ninety-tool server from crowding out the conversation on a phone.</li>
<li><b>Nothing runs unattended</b> — every remote call is approved
individually, with its arguments visible, and no "always allow" exists for
contributed tools.</li>
<li><b>Extensible without code</b> — supporting a new service means adding its
URL in settings; neither this plugin nor AI Core changes.</li>
</ul>

<h2>Limitations</h2>
<ul>
<li>Only the <b>Streamable HTTP</b> transport is supported. A server offering
just the local <code>stdio</code> transport, or the deprecated HTTP+SSE
transport, cannot be used.</li>
<li>Only <b>tools</b> are consumed. MCP prompts, resources and sampling are
not used.</li>
<li>The tool list is answered from memory, so a server that was unreachable at
startup needs a <b>Refresh tools</b> before its tools appear.</li>
</ul>

<h2>Troubleshooting</h2>
<table>
<tr><th>Message</th><th>What to do</th></tr>
<tr><td>"has no MCP endpoint at that URL"</td><td>The path is wrong; try
adding <code>/mcp</code>.</td></tr>
<tr><td>"does not accept this request"</td><td>The server is probably not a
Streamable HTTP MCP server.</td></tr>
<tr><td>"refused the token"</td><td>Re-enter it. The field shows
<i>Stored</i> rather than the value, so typing replaces it.</td></tr>
<tr><td>Tools missing from the Agent</td><td>Check the server is enabled, at
least one tool is switched on, and AI Core is installed and active.</td></tr>
</table>
</body>
</html>
91 changes: 91 additions & 0 deletions ai-agent-mcp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.itsaky.androidide.plugins.build")
}

pluginBuilder {
pluginName = "ai-agent-mcp"
}

android {
namespace = "com.itsaky.androidide.plugins.aiagentmcp"
compileSdk = 36

defaultConfig {
applicationId = "com.itsaky.androidide.plugins.aiagentmcp"
minSdk = 33
targetSdk = 36
versionCode = 1
versionName = "1.0.0"
}

buildFeatures {
viewBinding = false
}

buildTypes {
release {
isMinifyEnabled = false
isShrinkResources = false
signingConfig = signingConfigs.getByName("debug")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}

testOptions {
unitTests.isReturnDefaultValues = true
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}

packaging {
resources {
excludes += setOf(
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/NOTICE",
"META-INF/NOTICE.txt",
"META-INF/INDEX.LIST"
)
}
}
}

dependencies {
compileOnly(files("../libs/plugin-api.jar"))

// 'implementation' (not 'compileOnly') for the androidx/Material libraries: AAPT2 needs them
// at compile time to process the settings pane's layouts, as in every CoGo plugin with XML.
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.fragment:fragment-ktx:1.8.8")
implementation("com.google.android.material:material:1.10.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")

testImplementation(files("../libs/plugin-api.jar"))
testImplementation("junit:junit:4.13.2")
testImplementation("org.json:json:20240303")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
}

// No MCP SDK dependency on purpose: the official Kotlin SDK is KMP with no stated Android target,
// ships no HTTP engine, and needed Kotlin 2.4.10 when it was tried (ADFA-5083) against the 2.3.0
// these plugins standardise on. The transport here is HttpURLConnection and org.json, both of
// which the platform already provides — and never OkHttp, which resolves to the host's older copy.

// AAR metadata checks are disabled by convention for these application-as-library plugins.
tasks.matching {
it.name.contains("checkDebugAarMetadata") ||
it.name.contains("checkReleaseAarMetadata")
}.configureEach { enabled = false }
10 changes: 10 additions & 0 deletions ai-agent-mcp/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
android.enableJetifier=false
android.jetifier.ignorelist=common-30.2.2.jar
android.nonTransitiveRClass=false
android.useAndroidX=true
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4096M -Dkotlin.daemon.jvm.options\="-Xmx4096M"
org.gradle.parallel=true

kotlin.code.style=official
Loading
Loading