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 @@ -26,6 +26,7 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego
| [`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-openai/`](ai-agent-openai/) | OpenAI-compatible inference backend for `ai-core`. Registers as `openai`; talks to OpenAI by default, or to Ollama / LM Studio / OpenRouter / `llama-server` by changing one URL. |
| [`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/
104 changes: 104 additions & 0 deletions ai-agent-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 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.
5. **Clear stored credential** — removes the stored token and headers. The token
field never shows what is stored and an empty field keeps it, so this is the
only way back to a server that needs no credential.

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**. Names
are reduced to `[a-z0-9_]` here; descriptions are flattened to one capped line
by `ai-core`, which owns that cap because it is the side that assembles the
prompt. Capping in both places only gave the two constants room to diverge, so
this plugin ships no description sanitising of its own. Nothing enforces the
pairing — the two plugins version independently — so an `ai-core` older than the
release that flattens at its `ContributedToolHandler` boundary would take raw
multi-line server text into the prompt.
- 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. A token that can no
longer be decrypted — a restored backup, an OEM Keystore reset — is reported as
exactly that, never sent as an absent one, which would surface as the server
refusing a token that is still stored and still correct.
- A token or a custom header is refused on an `http://` URL: encryption at rest
buys nothing for a credential sent in the clear.
- Redirects are never followed automatically. A 3xx is repeated only when it
resolves to the same origin, so the bearer token and the user's own headers
cannot be replayed to another host in one hop.
- Error bodies stay in logcat. The transcript gets one sentence.
187 changes: 187 additions & 0 deletions ai-agent-mcp/ai-agent-mcp.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<!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: refused on a
cleartext <code>http://</code> URL, and never replayed to another host by
an automatically followed redirect.</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 drops that call's own connection when an Agent run is
stopped, leaving any concurrent call against another server alone.</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 }
Loading
Loading