Slide 1

Slide 1 text

How I "Stole" PSI from Android Studio Letting AI Navigate Large Codebase Name: Jun Rong Android / Design System

Slide 2

Slide 2 text

Outline 1. Problem: AI navigating large code base 2. Solution Discovery • Kotlin LSP? • JetBrains MCP Server? 3. Kotlin PSI MCP as the solution • Architecture • Tools 4.Limitations & Challenges 5. Conclusion ●

Slide 3

Slide 3 text

Problem: AI Understanding Codebase • AI uses grep (text-based) to read codebase ‣ Wrong / best e ort result ‣ AI Model reliant ‣ Non-deterministic ‣ Name Collisions ‣ High Token Usage ‣ Slow 3 ff ●

Slide 4

Slide 4 text

Example Task: Refactoring BaseRow • find-usages to access blast radius @Composable fun BaseRow( horizontalPadding: BaseRowHorizontalPadding, verticalPadding: BaseRowVerticalPadding, Modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit, ) 4 Let’s consider an example task - let’s say we need to refactor BaseRow (Where BaseRow is a @Composable) We need to nd usages of BaseRow to access the blast radius. fi ● ● ●

Slide 5

Slide 5 text

f Let’s see how AI ind usages of BaseRow 5

Slide 6

Slide 6 text

Haiku 4.5 We can see that Haiku is using ‘grep tool’ a lot, repeatedly.

Slide 7

Slide 7 text

In the end it found 2800+ references (which is NOT accurate). The correct result is much lower (I will show later)

Slide 8

Slide 8 text

Problem with Grep • Grep includes all text occurrences with BaseRow • Partial match: BaseRowSeparator, BaseRowVariant • Comments: /* BaseRow */ • Imports: import com.mercari.BaseRow 8 AI has to do some heavy lifting and calculate the correct call sites.

Slide 9

Slide 9 text

Maybe AI Model is smart enough? • Smart AI Model can ilter out the imports? • Previous example: Haiku (low-end) model is used • What about Opus (high-end) model? 9 f

Slide 10

Slide 10 text

Let’s try with Opus!

Slide 11

Slide 11 text

Opus 5 Now we are using Opus, a very smart model. However, it’s very token hungry and expensive. We can see that it’s STILL using ‘grep tool’ A LOT.

Slide 12

Slide 12 text

Opus ilters import fi ff f What’s interesting is that, it’s actually grepping imports, and ltering them out as well. (Haiku did grep the imports too, but it didn’t subtract it o the total count) The answer from Opus is actually much closer to the correct answer.

Slide 13

Slide 13 text

Problem: AI Model Reliant Different AI Model gives us a very different result! ff ff What we can learn here is that, using `grep` is VERY AI model reliant. Once we change the model, we get di erent results. Even upgrading within the same model line, for example, Sonnet 4.5 -> Sonnet 5, might cause the result to be di erent.

Slide 14

Slide 14 text

We have seen the problem with AI inding usages How would a human do it?

Slide 15

Slide 15 text

human will use Android Studio! 15 human will of course use Android Studio!

Slide 16

Slide 16 text

● If we do this by hand: ● - we right click on BaseRow ● - then we click Find Usages

Slide 17

Slide 17 text

Android Studio Result: 452 (precise) ● Then we obtain the result, it’s 452 calls

Slide 18

Slide 18 text

Comparison We have an app module turned o ! Haiku 4.5 Opus 5 Android Studio Call site ? 455 452 Imports ? ? 249 Total 2880 ? 704 Let’s compare the result, you can see that - Haiku doesn’t give you breakdown, just a total that’s WAY o - Opus 5 provides 455 calls, which is really close, but still o - Android Studio provides 452, which is the correct result (The reason Opus 5 with Grep got extra 3 is because we have a module that is switched o !) ff ff ff ff - 6.5-7.5mins

Slide 19

Slide 19 text

Searching for a solution… To Help AI understand code semantically 19

Slide 20

Slide 20 text

Then I learned about the LSP server, it was added around winter last year. It highlights that it’s ‘real-time code intelligence!’ So I thought this must be what I needed!

Slide 21

Slide 21 text

What’s LSP ? 1. LSP stands for Language Server Protocol 2. Created by Microsoft in 2015 for VS Code 3. Why? • VS Code has to support multiple languages • 1 editor x N number of languages 4. What if we have multiple Editor / IDEs? • Eclipse, Sublime, Zed, more… • M editor x N number of languages Before jumping straight into using LSP, let’s take a second to understand the high level concept of what LSP is. 3. Why? <— “why is it created?”

Slide 22

Slide 22 text

Problem: M editor x N number of languages LSP solves this! • Write one editor client, it gets every languages • Write one Kotlin server once, every editor gets Kotlin Kotlin LSP (Language Server Protocol) https://github.com/Kotlin/kotlin-lsp 9-10mins

Slide 23

Slide 23 text

How does LSP protocol look like?

Slide 24

Slide 24 text

How does LSP protocol look like? { "jsonrpc": "2.0", Request "id": 1, @Composable fun BaseRow( "method": "textDocument/references", horizontalPadding: BaseRowHorizontalPadding, "params": { verticalPadding: BaseRowVerticalPadding, "textDocument": { "uri": “../DS/BaseRow.kt” Modifier: Modifier = Modifier, }, content: @Composable RowScope.() -> Unit, "position": { "line": 52, "character": 4 }, "context": { "includeDeclaration": true } Here’s an example of how AI can query the KotlinLSP server. It’s in jsonrpc format, and a request looks like what’s on the left. fi fi Let’s say you want to nd the usages of BaseRow. When AI makes a request, it provides the le path. Then it provides the position, e.g. Line 52, which points at the line number of BaseRow. And character 4, which is the column for ‘B’. )

Slide 25

Slide 25 text

LSP Response { "jsonrpc": "2.0", Response @Composable fun ProductScreen(…) { "id": 1, "result": [ … { "uri": BaseRow(…) “feature_discount/ProductScreen.kt”, "range": { "start": { "line": 100, "character": 4 }, "end": { "line": 100, "character": 11 } } } ] } Then now, the KotlinLSP server will reply to AI making query. It’s used in the ProductScreen.kt The line and character will be pointing at exactly where the usage is. Char 4 points at the start of BaseRow’s B, Char 11th point at the end of BaseRow’s ‘w’ That’s how the KotlinLSP replies to the AI, now AI knows the call sites of BaseRow. }

Slide 26

Slide 26 text

Can Kotlin LSP solve this? 26 Now we understand LSP a bit, let’s get back to our question!

Slide 27

Slide 27 text

Kotlin LSP Let’s install & try it! $ export ENABLE_LSP_TOOL=1 $ brew install --cask kotlin-lsp $ claude $ /plugin install kotlin-lsp@claude-plugins-official So let’s try it out.. Here are the steps to install.

Slide 28

Slide 28 text

Kotlin LSP This time, I explicitly ask it to invoke Kotlin LSP We can see that LSP tool is invoked this time.

Slide 29

Slide 29 text

Kotlin LSP 705 Note that the result is 705, only the total number is available.

Slide 30

Slide 30 text

Comparison Haiku 4.5 Opus 5 Android Studio KotlinLSP Call site ? 455 452 ? Imports ? ? Total 2880 ? ff ff Here’s the updated comparison table. We can see Android Studio and KotlinLSP are very close, o by 1. Which is expected because KotlinLSP counted declaration side 249 includes declaration, ? KotlinLSP O -by-1 is expected, it’s correct! 704 705

Slide 31

Slide 31 text

So… How is Kotlin LSP getting the same answer!? Can Kotlin LSP solve this? 31

Slide 32

Slide 32 text

Yes… but there are limitations… How? KotlinLSP is powered by a Headless IntelliJ IDE The exact same engine as Android Studio! 32

Slide 33

Slide 33 text

It increases the memory usage If Android Studio is already open, it’s wasteful 33 Read as-is

Slide 34

Slide 34 text

Headless IntelliJ 2.6G of RAM for ind-usages btop (resource monitor) f So I checked the memory usage with a terminal monitor resource tool called `btop`. You can see that the Kotlin LSP is taking up 2.6GB of RAM! Just to answer 1 question! RAM is expensive these days!

Slide 35

Slide 35 text

LSP has limited number of tools No Language speci ic features E.g. no import count! Another limitation with LSP is that, it doesn’t have much per language speci c feature, since it’s goal is to support as many editor and as many languages as possible. f fi fi (I understand there are people who don’t open Android Studio at all or want to use it on CI, in that case, LSP might be a good t)

Slide 36

Slide 36 text

Another solution… What about JetBrains MCP Server? 36 So let’s explore the next solution. Which is about JetBrains MCP Server.

Slide 37

Slide 37 text

● So this is the official JetBrains MCP server, you can look it up in the plugin marketplace.

Slide 38

Slide 38 text

The 50 tools - Project metadata (4) — get_project_modules, get_project_dependencies, get_repositories, git_status - File access & navigation (6) — read_file, create_new_file, search_file (by name), list_directory_tree, get_all_open_file_paths, open_file_in_editor - Search (3) — search_text, search_regex, search_symbol - Code intelligence / PSI (4) — get_symbol_info, analyze_calls, generate_psi_tree, get_file_problems - Edit & refactor (3) — apply_patch, reformat_file, rename_refactoring - Inspections (4) — lint_files, run_inspection_kts, generate_inspection_kts_api, generate_inspection_kts_examples - Build / run (4) — build_project, get_run_configurations, execute_run_configuration, execute_terminal_command, plus execute_too - Debugger (13) JetBrains MCP Server - Database (14) It comes with 50 tools! That is a lot of tools, more than half of them are for controlling the IDE. fi Notice how PSI related code navigation tools are only 4, it doesn’t have the nd-usages tool that we need.

Slide 39

Slide 39 text

JetBrains MCP Server? No ind-usages tool 39 f 17-18mins

Slide 40

Slide 40 text

So I wonder.. How can Android Studio have deep semantic understanding? PSI (Program Structure Interface) JetBrains’ semantic element representations

Slide 41

Slide 41 text

JetBrains’ internal model of your code: • not text • but a resolved tree of symbols that knows ‣ types, scopes, and references PSI is what powers: • ind-usages • go to de inition • rename • etc f f PSI (Program Structure Interface)

Slide 42

Slide 42 text

psiElement @Composable fun BaseRow( horizontalPadding: BaseRowHorizontalPadding, verticalPadding: BaseRowVerticalPadding, Modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit, ) For example, internally in JetBrains, this is a psiElement, this is a psiElement, this is also a psiElement… You get the idea… That’s how our code is represented internally by JetBrains

Slide 43

Slide 43 text

Can I ‘steal’ PSI and feed into AI?

Slide 44

Slide 44 text

Yes! Kotlin PSI MCP is developed! Let’s see how it’s implemented…

Slide 45

Slide 45 text

Introducing Kotlin PSI MCP PSI MCP Let me introduce the PSI MCP that I built. It’s basically a JetBrains plugin that exposes the PSI to AI agent. AI / Claude Code

Slide 46

Slide 46 text

Kotlin PSI MCP Architecture 1. Make an Android Studio Plugin 2. The Plugin should: • implement MCP protocol Next, let’s take a look at the architecure of the PSI MCP To build this, - We need to make an AS plugin, and the plugin has to implement the MCP protocol

Slide 47

Slide 47 text

Plugin HTTP Server Implementing MCP class PsiHttpServer { ... fun handle(...) { when { requestPath == "/mcp" && request.method == "POST" -> { handleMcpRequest(requestPath, request) } } } } fi Next we look inside the Plugin code to see how it works. In order to ful ll the MCP protocol, it must serve the [slash] /mcp endpoint. When a request comes in, it routes to the `handleMcpRequest()` function

Slide 48

Slide 48 text

Plugin HTTP Server Implementing MCP fun handleMcpRequest(requestPath, request) { requestPath == "/api/tools" && request.method == "GET" -> { handleListTools(response) } } private fun handleListTools(response: HttpServletResponse) { val toolSchemas = tools.map { (name, tool) -> mapOf( "name" to name, "description" to tool.getDescription(), "inputSchema" to tool.getInputSchema() ) } val responseJson = gson.toJson(toolSchemas) sendResponse(response, 200, responseJson, "application/json") } fi fi Inside the `handleMcpRequest()`, we register all the tools such as nd-usages, nd-symbols, get-containing-context, etc.

Slide 49

Slide 49 text

$ ./gradlew buildPlugin Then, we run the Gradle task to build the plugin.

Slide 50

Slide 50 text

Then, we install the plugin.

Slide 51

Slide 51 text

Enabling the MCP Server ✓ After installing the plugin, we have enable the Kotlin PSI MCP server from the Android Studio’s setting page. Since this runs on HTTP, once it’s enabled, you can curl the endpoints and obtain the PSI information for testing.

Slide 52

Slide 52 text

Claude’s MCP Con ig Con igure our PSI MCP server in Claude Con ig (Any AI tools with MCP support work) "mcpServers": { “kotlin-psi-mcp": { "type": "http", "url": "http://localhost:51234/mcp" } } fi f fi fi f fi Then we con gured the Claude’s MCP con g to add the Kotlin PSI MCP. This is an example of Claude’s con g, but any AI tools that support MCP should work, such as codex, re bender, and so on.

Slide 53

Slide 53 text

After setting up, we should see that it says “connected”, Claude is now able to talk to Android Studio. If we click into view tools…

Slide 54

Slide 54 text

fi If we click into the View tools, we can see all the tools that we have registered just now in the plugin code. If we click into the individual tools… For example, nd-usages

Slide 55

Slide 55 text

We can see the descriptions and parameters. The descriptions and the parameters will help AI to understand the tool, and to help it learn how to use the tools, so that it can invoke at the right time.

Slide 56

Slide 56 text

The MCP Plumbing is done! Let’s check the code for ind-usages tool f fi == Call Forwarding == (You might think that it’s just call forwarding, but it’s actually NOT, it’s more than that, because there’s no 1 ‘ nd-usages’ API for us to call)

Slide 57

Slide 57 text

Reuse Find-usages of BaseRow Example @Composable AI needs to provide through MCP: fun ProductScreen(…) { … BaseRow(…) Line + Column + FilePath } Let’s go back to our Find-usages of BaseRow, AI will call the MCP with the: - Line number - Column - File path (Just like how a human would do when clicking on it)

Slide 58

Slide 58 text

Inside Plugin Code: Extracting Parameters override fun execute(arg: JsonObject): String { return try { val filePath = arg.get("file_path")?.asString val line = arg.get("line")?.asInt val column = arg.get("column")?.asInt val contextLines = arg.get(“context_lines”) val limit = arg.get("limit")?.asInt gson.toJson( find(filePath, line, column, contextLines, limit, includeComments) ) } catch {...} fi fi Inside the plugin code, we need extract all the variables like lePath, line, column, etc. After that, we pass them into a function called ` nd()`.

Slide 59

Slide 59 text

Inside ` ind()` function private fun find(filePath: String, line:Int, …) { val psiFile = getPsiFile(filePath) val doc = getPsiDocument(psiFile) val offset = calculateOffset(doc, line, column) val psiElement: PsiElement = psiFile.findElementAt(offset) } ff f fi From the lePath, we get the psiFile, Then we get the doc from psiFile, Then we obtain the psiElement from the o set. Finally, we obtained the internal node of JetBrains model, the psiElement, which represents the BaseRow!

Slide 60

Slide 60 text

Find Usages of psiElement(BaseRow) val references: List = ReferencesSearch.search(psiElement).findAll() if (psiElement is KtParameter) { ReferencesSearch.search(psiElement).findAll()+ findTrailingLambdaUsages(psiElement) } else { } We can inally call ReferencesSearch.search(psiElement). indAll() on the psiElement to obtain the usages! Something interesting here is that we can actually check if psiElement is a KtParameter, if it is, we can make an extra calculation to ind the trailing lambda! f f f In this case, BaseRow is NOT a KtParameter, because it’s a function

Slide 61

Slide 61 text

Finding Usages of content Trailing Lambda • Special handling of Trailing Lambda usages: @Composable fun BaseRow( horizontalPadding: BaseRowHorizontalPadding, verticalPadding: BaseRowVerticalPadding, Modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit, ) • Github: FindUsagesTool.kt#L439 • If you try to do “Find Usage” on Android Studio on `content`, which is a trailing lambda, you’ll notice that it won’t show any trailing lambda usages, it only shows the usages of named parameter. • So special handling is required here • Find usages of the owner function composable, BaseRow • Base on the usages of BaseRow, we check the last last parameter: • “Is it the parameter in question?” Ok if yes, then • “Is it trailing lambda?” If yes, then sum them all up! • By doing this, we can obtain the ‘usages’ of trailing lambda PLUS the named parameter usages • Refer to codebase, it’s open source

Slide 62

Slide 62 text

Finally, Let’s see Kotlin PSI MCP in action!

Slide 63

Slide 63 text

Haiku with PSI MCP We can see that the number is an exact match with Android Studio now!

Slide 64

Slide 64 text

- the result matched with Android Studio

Slide 65

Slide 65 text

Recap: Comparison (No Kotlin PSI MCP) Let’s take a quick recap of the numbers before. Haiku 4.5 Opus 5 Android Studio Call site ? 455 452 Imports ? ? 249 Total 2880 ? 704

Slide 66

Slide 66 text

Comparison (with Kotlin PSI MCP) Haiku 4.5 Opus 5 Android Studio Call site 452 452 452 Imports 249 249 249 Total 704 704 704 Let’s look at the comparison table, all the numbers match regardless of the models.

Slide 67

Slide 67 text

Problem: AI Understanding Codebase • With PSI MCP ‣ Wrong / best e ort result Precise IDE-grade result ‣ AI Model reliant Not Model Reliant ‣ Non-deterministic Deterministic ‣ Name Collisions No Partial Match ‣ High Token Usage Lower Token Usage ‣ Slow Faster 67 ff ●

Slide 68

Slide 68 text

Let’s explore more tools from Kotlin PSI MCP! 30mins

Slide 69

Slide 69 text

ModuleSearch Tool { "success": true, "query": “library-ds4“ "matches": [ { "dependencies": [ { "name": ":library-ds4-theme", "type": "MODULE", "scope": "COMPILE" }, { "name": ":library-roborazzi-api", "type": "MODULE", "scope": "TEST" } ], "dependents": [ “:feature_seller", dependents ":feature_buyer", “:feature_my_page”, … dependencies This is the module search tool, it takes a query module, and returns the dependency modules and dependent modules. In this example, I query the “design system 4” module, and you can see that it has the “ds4 theme” and “roborazzi api” dependencies, and the a list of “feature modules” dependents

Slide 70

Slide 70 text

Grepping build.gradle.kts will miss injected dependencies Grep can’t see Gradle Convention Plugin plugins { id(“com.mercari.android-feature") } Grepping build.gradle.kts will miss injected dependencies Because Grep can’t see Gradle Convention Plugin For example, we have a Convention Plugin in Mercari called android-feature

Slide 71

Slide 71 text

plugins { id(“com.mercari.android-feature") } This plugin injects DS4 dependency No “DS” mentioned Grep can’t ind it fi f This plugins actually injects DS4 dependency into the module. However, there’s no “DS” word mentioned at all, so it’s not possible for Grep tool to nd it. So a proper resolution through PSI MCP would help here.

Slide 72

Slide 72 text

Get Call Hierarchy Tool Designer asked: “Which screen is AccordionRow used in?” Next, let’s take a look at a tool called Get Call Hierarchy. This can help us to trace the “Call Hierarchy”. Take for example this use case: “Which screen is AccordionRow used in?” With Get Call Hierarchy, you can trace it quickly: it’s used in ShipmentTracker, ShipmentTracker is used in OrderDetailShipment, Which is used in OrderDetailContent, which is nally used in OrderDetailScreen. fi You also get the exact line number where they are called.

Slide 73

Slide 73 text

FindDeclaration Tool @Composable fun TopCheckoutScreen(…) { Header(…) CheckoutContent(…) Footer(…) } Where is this declared? Next, let’s have a look at the FindDeclaration Tool. Take for example this simple @Composable screen which has a Header CheckoutContent and Footer. Let’s say we want to check where is CheckoutContent declared, FindDeclaration can be used.

Slide 74

Slide 74 text

Response of FindDeclaration Tool { "declaration": { "name": "CheckoutContent", "type": "function", "file": ".../checkout/CheckoutContent.kt", "line": 96, "column": 5, "packageName": "com.mercari.checkout", "signature": “ CheckoutContent( modifier: Modifier, viewModel: CheckoutViewModel, …) “, "annotations": [“@Suppress", “@Composable"] } } Here’s the response of FindDeclaration tool. The top part describes where the declaration can be found, e.g. le_path, line and column. fi The bottom part is interesting, it includes the package name, signature and annotations. What’s interesting about this is that AI doesn’t have to do a separate read tool call, and it already has all the essential information. It helps to save tokens.

Slide 75

Slide 75 text

Package name, annotation, signature @Composable fun TopCheckoutScreen(…) { SomeEffect() <— NOT a @Composable Text() <— Material3 CheckoutContent(…) <— internal @Composable } With the package name, annotations and function signature: - AI can already know: - - SomeE ect is NOT a Composable - - Text is a framework component from Material 3 - - CheckoutContent is a Mercari internal @Composable fi ff AI knows this without reading the actual declaration le.

Slide 76

Slide 76 text

Compose UI Tree Dumping - FindDeclaration combo ItemDetailScreen (L68) :global-feature-itemdetail-impl │ └── Scaffold [MATERIAL3] ├── topBar → ItemDetailTopNavigation (internal · 445L · 9 composables · 11 cond) ├── bottomBar → [C1: bottomActionButtons?.let] │ └── ItemDetailBottomActionButton (internal · 5 cond) └── Box [FOUNDATION] ├── [C2: if isUnavailableItem → top padding] │ ├── RefreshSpinner [DS4 BASE] ← [C3: isRefreshing] │ └── Content (L320, internal) │ └── LoadingContent [CROSS-MODULE: library-result-ui] │ └── [C4: placeholder | error | data] │ ├── placeholder → [C5: if isUnavailableItem] │ │ ├── ItemDetailSoldOutLoadingComponent (238L · 7 comp) │ │ └── ItemDetailLoadingComponent (328L · 8 comp) │ └── data │ ├── [C6: likeData?.let → ItemDetailSnackBar] │ ├── [C7: if isItemAvailable] │ │ └── ThumbnailComponent (389L · 6 comp · 15 cond) │ └── ItemDetailSectionList (L389) │ └── forEach sections → when(item) ★ 15-WAY SEALED │ ├── TitleWithPriceSet → TitleWithPriceSetComponent │ │ └── ItemDetailPriceComponents (465L · 31 cond) │ ├── DescriptionSectionData → ItemDetailDescriptionComponent │ ├── LanguageSwitcher → ItemDetailLanguageSwitcherComponent │ ├── SellerInfo → SellerInfoComponent (266L) │ ├── Details → ItemDetailsComponent (L549) One example use case we can do with this is to have a compose UI tree dump. It uses FindDeclaration repeatedly, without opening the declared les, it plots a UI tree. fi We can use this for analysis or for generating screenshot tests.

Slide 77

Slide 77 text

Derived work low: -> Compose UI Tree (From Kotlin PSI MCP) -> Optimal VRT Test Cases Generation f By obtaining the Compose UI Tree, we can generate an optimal VRT test cases for the feature screen 34mins+

Slide 78

Slide 78 text

Test 1 Set Cover Greedy Algorithm If we already have the various compose tree, condition branches We can use the set cover greedy algorithm to determine the optimal test cases. Basically, we test as much branches as possible in 1 test.

Slide 79

Slide 79 text

Test 2 Set Cover Greedy Algorithm In Test 2, we cover the conditional branches that are not covered in Test 1. As MUCH as we can!

Slide 80

Slide 80 text

Many other tools in PSI MCP Navigations Edit - Find-usages - Rename - Module-search: fuzzy search - Safe-delete - Find-declarations - Organize-imports - Get Call Hierarchy - Move-file - Find-implementations - Extract-interface - Find-symbols: fuzzy search - etc There are many other tools available in PSI MCP, here are some more.. In this presentation we have seen: - nd usages - module search - nd declarations - get call hierarchy And their corresponding use cases. fi fi However the use cases are not limited to what I’ve shown, it really depends on the developer’s task at hand. You can mix and match the PSI MCP tools to navigate your codebase.

Slide 81

Slide 81 text

Usage of Kotlin PSI MCP can be lexible, and you can invent your own ‘skills’.

Slide 82

Slide 82 text

Challenges • Difficulty to verify accuracy • Supporting different IDE versions are tedious • K2 update broke many things • Description and tool design have to be fine-tuned • Otherwise, AI will not invoke properly • No easy way to test this (at least I don’t know) Di culty to verify accuracy - let’s say for nd-usages, there’s no alternative way to nd the correct answer than to use Android Studio - if I use Mercari’s large code base as my testing ground, too many merges from upstream, so the answer always changes - in the end, I embedded a test-project for testing the PSI MCP tools Supporting di erent IDE versions are tedious - E.g. initially tried to support all IDEs, but relying on Kotlin analysis API will make them incompatible - The introduction of K2 API breaks many of the tools and I have to re-write a big part of them fi fi fi fi fi ff fi ffi Description and tool design have to be ne-tuned - Otherwise, AI will not invoke properly For example: 1. nd-usage vs. nd-usages (without plural AI has to actually make the call and see multiple result to understand it returns more than 1 result) 2. Find-implementations tool: “Find concrete implementations of an interface, abstract class, abstract function at a given le position.”, we have to spell it out in the description, so AI can invoke the tool when the keyword is hit.

Slide 83

Slide 83 text

• Implementations of tools and corner cases, example: • Find-usage on use side (not declaration): • Extra declaration hopping needed Use side Column { Declaration Hop! BaseRow(…) } @Composable fun BaseRow( horizontalPadding: BaseRowHorizontalPadding, verticalPadding: BaseRowVerticalPadding, Modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit, Here are some more challenges: 1. Implementations of tools requires a lot of special handling, example: • nd usage tool, if user doesn’t click on the declaration side, but the use side • We need to manually hop to the declaration side to nd usages fi fi Challenges

Slide 84

Slide 84 text

Limitations • Only 1 IDE can be connected (for now) • Project must always be sync’ed • Pagination not yet implemented • Only Android Studio and IntelliJ IDEA • IDE must be always be opened

Slide 85

Slide 85 text

Conclusion - Many tools, pick the right one for the task - Knowledge Base - Agentic Text-Based grep (This presentation) - Kotlin PSI MCP (This presentation) - KotlinLSP (This presentation) - Repo Map - Tree Sitter - SCIP - Embeddings / RAG There are many tools for AI to understand our codebase, there’s no 1 right answer. Pick the right tool for the right task. For exploration task, e.g. “study this repo”, text based grep might be good. Generally for Android Development that is interactive, where the goal is general understanding or fuzzy search, choose grep-based with a mix of Kotlin PSI MCP. — When it comes to precise semantic relationship of the code, choose Kotlin PSI MCP. You can even let your AI decide. fi There are bunch of other relevant tools when I was researching this subject, each of them suitable for various purposes. - RepoMap from Aider - TreeSitter (concept to understand AST without the cross-module semantic relationship) - SCIP from source graph - strong semantic relationship, but requires build, can run on CI, headless - RAG (who uses embeddings to nd similar meanings, Anthropic removed this in 2025 favoring agentic grep, might still be good for certain use cases)

Slide 86

Slide 86 text

PSI MCP is available on Github SpeakerDeck: SpeakerDeck Link Github: https://github.com/mercari/kotlin-psi-mcp JetBrains Plugin Marketplace: https://plugins.jetbrains.com/plugin/33755-kotlin-psi-mcp

Slide 87

Slide 87 text

Thank you!