Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How I Stole PSI from Android Studio - DroidKaig...

Avatar for JunRong Tan JunRong Tan
September 02, 2026

How I Stole PSI from Android Studio - DroidKaigi2026

AI coding agents read your codebase as plain text.
Even LSP-backed tools re-index your project from scratch and double your memory footprint.
Meanwhile, the IDE you already have open — Android Studio — has a far richer semantic understanding via its Program Structure Interface (PSI): the same engine that powers Find Usages, refactoring, and inheritance navigation.

What if AI agents could just ask Android Studio directly — "find usages of this method," "show me this function's signature, annotations, and visibility" — and get IDE-grade answers instead of grepping 10,000 files?

In this talk, I'll share how I built a JetBrains plugin, "Kotlin PSI MCP," that exposes PSI as a Model Context Protocol (MCP) server, letting any AI agent borrow Android Studio's brain for codebase navigation.

**GitHub Link:** https://github.com/mercari/kotlin-psi-mcp
**JetBrains Plugin Marketplace:** https://plugins.jetbrains.com/plugin/33755-kotlin-psi-mcp

## What will be covered:
- How text-based (grep) AI navigation falls short in a large codebase
- Walk through a simple example task ("Find Usages") to see how text-based navigation fails
- Existing solutions, such as KotlinLSP and the official JetBrains MCP Server
- What LSP is and what it was invented for
- Where they excel and where they fall short
- What JetBrains' PSI (Program Structure Interface) is — an internal representation of your code
- How Kotlin PSI MCP is developed
- Quick architecture overview
- Quick MCP plumbing explanation
- Plugin code walkthrough
- Exploring an example of the `find-usages` MCP tool
- Seeing Kotlin PSI MCP in action
- Exploring various Kotlin PSI MCP tools
- Example use cases in a large codebase: Find Usages, module analysis, finding declarations, drawing Compose UI trees, and derived workflows (e.g., optimal test case generation using the set cover algorithm)

Stop letting your AI guess about your codebase, and learn how to bridge AI with Android Studio.
Intended audience

**Prerequisite knowledge:**
- Basic Android development experience
- Some familiarity with using AI coding agents (Claude Code, Cursor, GitHub Copilot, etc.)
- No prior knowledge of PSI or MCP required — both will be introduced from scratch

**Who should attend:**
- Android engineers working in large or legacy codebases where AI agents frequently miss references, hallucinate function locations, or burn through context windows just grepping around.
- Developers who have tried Claude Code / Cursor / Copilot on a real production codebase and felt its limitations in navigating the codebase semantically.
- Anyone curious about Model Context Protocol (MCP) and what it looks like to build a custom MCP server that gives AI agents a capability they fundamentally lack.
- Engineers interested in IDE plugin development, JetBrains Platform internals, or how Android Studio actually understands your code under the hood.

## Problems this session aims to resolve:
- "Why does my AI agent keep missing usages of this function?"
- "Why does AI confuse two methods with the same name in different modules?"
- "Can AI accurately find the relationships between modules?"
- "Can AI understand my codebase as precisely as Android Studio?"
- "What is LSP (Language Server Protocol)? What is PSI (Program Structure Interface)?"
- "How do I write a JetBrains plugin that serves as an MCP server to talk to AI?"

Avatar for JunRong Tan

JunRong Tan

September 02, 2026

More Decks by JunRong Tan

Other Decks in Programming

Transcript

  1. How I "Stole" PSI from Android Studio Letting AI Navigate

    Large Codebase Name: Jun Rong Android / Design System
  2. 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 • <read out as-is>
  3. 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 <read as-is> ff •
  4. 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 • • •
  5. In the end it found 2800+ references (which is NOT

    accurate). The correct result is much lower (I will show later)
  6. Problem with Grep • Grep includes all text occurrences with

    BaseRow • Partial match: BaseRowSeparator, BaseRowVariant • Comments: /* BaseRow */ • Imports: import com.mercari.BaseRow 8 <read as-is> AI has to do some heavy lifting and calculate the correct call sites.
  7. 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 <read as-is>
  8. 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.
  9. 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.
  10. 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.
  11. We have seen the problem with AI inding usages How

    would a human do it? <read as-is>
  12. • If we do this by hand: • - we

    right click on BaseRow • - then we click Find Usages
  13. 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
  14. 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!
  15. 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. <read as-is starting from point 1> <extra> 3. Why? <— “why is it created?”
  16. 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 <read as is> 9-10mins
  17. 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’. )
  18. 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. }
  19. Can Kotlin LSP solve this? 26 Now we understand LSP

    a bit, let’s get back to our question! <read as is>
  20. 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.
  21. Kotlin LSP This time, I explicitly ask it to invoke

    Kotlin LSP We can see that LSP tool is invoked this time.
  22. Kotlin LSP 705 Note that the result is 705, only

    the total number is available.
  23. 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
  24. So… How is Kotlin LSP getting the same answer!? Can

    Kotlin LSP solve this? 31 <read as-is>
  25. Yes… but there are limitations… How? KotlinLSP is powered by

    a Headless IntelliJ IDE The exact same engine as Android Studio! 32 <read as-is>
  26. 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!
  27. 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)
  28. Another solution… What about JetBrains MCP Server? 36 So let’s

    explore the next solution. Which is about JetBrains MCP Server.
  29. • So this is the official JetBrains MCP server, you

    can look it up in the plugin marketplace.
  30. 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.
  31. So I wonder.. How can Android Studio have deep semantic

    understanding? PSI (Program Structure Interface) JetBrains’ semantic element representations <read as-is>
  32. 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 <read as-is> f f PSI (Program Structure Interface)
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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.
  38. 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.
  39. 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.
  40. 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…
  41. 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
  42. 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.
  43. The MCP Plumbing is done! Let’s check the code for

    ind-usages tool <read as-is> 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)
  44. 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)
  45. 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()`.
  46. 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!
  47. Find Usages of psiElement(BaseRow) val references: List<PsiReference> = 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
  48. 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
  49. Haiku with PSI MCP We can see that the number

    is an exact match with Android Studio now!
  50. 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
  51. 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.
  52. 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 <Read as-is> ff •
  53. 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
  54. 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
  55. 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.
  56. 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.
  57. 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.
  58. 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.
  59. 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.
  60. 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.
  61. 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+
  62. 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.
  63. 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!
  64. 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.
  65. Usage of Kotlin PSI MCP can be lexible, and you

    can invent your own ‘skills’. <read as-is>
  66. 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) <read points and elaborate> 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.
  67. • 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
  68. 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 <read as-is>
  69. 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)
  70. 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 <read as-is>