diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts
index d29fa11b3..074011127 100644
--- a/__tests__/extraction.test.ts
+++ b/__tests__/extraction.test.ts
@@ -129,6 +129,7 @@ describe('Language Support', () => {
     expect(languages).toContain('swift');
     expect(languages).toContain('kotlin');
     expect(languages).toContain('dart');
+    expect(languages).toContain('groovy');
   });
 });
 
@@ -4285,6 +4286,126 @@ local count = 0
   });
 });
 
+describe('Groovy Extraction', () => {
+  describe('Language detection', () => {
+    it('should detect Groovy files', () => {
+      expect(detectLanguage('build.gradle')).toBe('groovy');
+      expect(detectLanguage('src/Main.groovy')).toBe('groovy');
+      expect(detectLanguage('utils.gvy')).toBe('groovy');
+      expect(detectLanguage('script.gy')).toBe('groovy');
+      expect(detectLanguage('helper.gsh')).toBe('groovy');
+    });
+  });
+
+  it('should extract class declarations', () => {
+    const code = `
+class UserService {
+    private repository
+
+    UserService(repository) {
+        this.repository = repository
+    }
+
+    def getUser(String id) {
+        return repository.findById(id)
+    }
+}
+`;
+    const result = extractFromSource('UserService.groovy', code);
+
+    const classNode = result.nodes.find((n) => n.kind === 'class');
+    expect(classNode).toBeDefined();
+    expect(classNode?.name).toBe('UserService');
+  });
+
+  it('should extract method declarations', () => {
+    const code = `
+class Calculator {
+    static int add(int a, int b) {
+        return a + b
+    }
+}
+`;
+    const result = extractFromSource('Calculator.groovy', code);
+
+    const methodNode = result.nodes.find((n) => n.kind === 'method' && n.name === 'add');
+    expect(methodNode).toBeDefined();
+    expect(methodNode?.isStatic).toBe(true);
+  });
+
+  it('should extract def function declarations', () => {
+    const code = `
+def greet(String name) {
+    return "Hello, \${name}"
+}
+`;
+    const result = extractFromSource('utils.groovy', code);
+
+    const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'greet');
+    expect(funcNode).toBeDefined();
+    expect(funcNode?.language).toBe('groovy');
+  });
+
+  it('should extract interface declarations', () => {
+    const code = `
+interface Repository {
+    def findById(String id)
+    def save(Object entity)
+}
+`;
+    const result = extractFromSource('Repository.groovy', code);
+
+    const ifaceNode = result.nodes.find((n) => n.kind === 'interface');
+    expect(ifaceNode).toBeDefined();
+    expect(ifaceNode?.name).toBe('Repository');
+  });
+
+  it('should extract enum declarations', () => {
+    const code = `
+enum Color {
+    RED, GREEN, BLUE
+}
+`;
+    const result = extractFromSource('Color.groovy', code);
+
+    const enumNode = result.nodes.find((n) => n.kind === 'enum');
+    expect(enumNode).toBeDefined();
+    expect(enumNode?.name).toBe('Color');
+  });
+
+  it('should extract imports', () => {
+    const code = `
+import java.util.List
+import groovy.json.JsonSlurper
+`;
+    const result = extractFromSource('App.groovy', code);
+
+    const imports = result.nodes.filter((n) => n.kind === 'import');
+    expect(imports.length).toBe(2);
+    expect(imports.map((n) => n.name)).toContain('java.util.List');
+    expect(imports.map((n) => n.name)).toContain('groovy.json.JsonSlurper');
+  });
+
+  it('should extract visibility modifiers', () => {
+    const code = `
+class Account {
+    private String secret
+    protected String internal
+    public String visible
+}
+`;
+    const result = extractFromSource('Account.groovy', code);
+
+    const fields = result.nodes.filter((n) => n.kind === 'field');
+    const secret = fields.find((n) => n.name === 'secret');
+    const internal = fields.find((n) => n.name === 'internal');
+    const visible = fields.find((n) => n.name === 'visible');
+    expect(secret?.visibility).toBe('private');
+    expect(internal?.visibility).toBe('protected');
+    expect(visible?.visibility).toBe('public');
+  });
+});
+
 // =============================================================================
 // Objective-C
 // =============================================================================
@@ -4364,11 +4485,6 @@ void helperFunction(int count) {
   });
 
   it('should reconstruct multi-keyword selectors at the call site so they resolve to the method definition', () => {
-    // Regression for the gap discovered post-#165: message_expression's
-    // multi-keyword form `[obj a:1 b:2]` was only emitting the first keyword,
-    // so calls never resolved to multi-part method definitions like
-    // `GET:parameters:headers:progress:success:failure:`. The call-site name
-    // must match the method-definition name with full keywords + trailing colons.
     const code = `
 @implementation Caller
 - (void)demo {
@@ -4452,7 +4568,6 @@ func (s Stack[T]) Len() int { return len(s.items) }
     expect(detectLanguage('mod.mts')).toBe('typescript');
     expect(detectLanguage('service.xsjs')).toBe('javascript');
 
-    // End-to-end: a .mts file is parsed as TS, a .xsjs file as JS.
     const ts = extractFromSource('mod.mts', 'export function hello(): number { return 1; }');
     expect(ts.nodes.find((n) => n.name === 'hello' && n.kind === 'function')).toBeDefined();
     const js = extractFromSource('service.xsjs', 'function handleRequest() { return 1; }');
diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts
index 576845e20..ccb4ece1e 100644
--- a/src/extraction/grammars.ts
+++ b/src/extraction/grammars.ts
@@ -37,6 +37,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
   scala: 'tree-sitter-scala.wasm',
   lua: 'tree-sitter-lua.wasm',
   luau: 'tree-sitter-luau.wasm',
+  groovy: 'tree-sitter-groovy.wasm',
   objc: 'tree-sitter-objc.wasm',
 };
 
@@ -99,6 +100,11 @@ export const EXTENSION_MAP: Record<string, Language> = {
   '.sc': 'scala',
   '.lua': 'lua',
   '.luau': 'luau',
+  '.groovy': 'groovy',
+  '.gradle': 'groovy',
+  '.gvy': 'groovy',
+  '.gy': 'groovy',
+  '.gsh': 'groovy',
   '.m': 'objc',
   '.mm': 'objc',
   // XML: file-level tracking; the MyBatis extractor matches `<mapper namespace="...">`
@@ -185,7 +191,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise<v
       // ABI-13 build that corrupts the shared WASM heap under web-tree-sitter
       // 0.25 (drops nested calls/imports on every file after the first); we
       // vendor the upstream ABI-15 wasm instead.
-      const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau')
+      const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau' || lang === 'groovy')
         ? path.join(__dirname, 'wasm', wasmFile)
         : require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
       const language = await WasmLanguage.load(wasmPath);
@@ -383,6 +389,7 @@ export function getLanguageDisplayName(language: Language): string {
     scala: 'Scala',
     lua: 'Lua',
     luau: 'Luau',
+    groovy: 'Groovy',
     objc: 'Objective-C',
     yaml: 'YAML',
     twig: 'Twig',
diff --git a/src/extraction/languages/groovy.ts b/src/extraction/languages/groovy.ts
new file mode 100644
index 000000000..b81f69794
--- /dev/null
+++ b/src/extraction/languages/groovy.ts
@@ -0,0 +1,59 @@
+import type { Node as SyntaxNode } from 'web-tree-sitter';
+import { getNodeText, getChildByField } from '../tree-sitter-helpers';
+import type { LanguageExtractor } from '../tree-sitter-types';
+
+export const groovyExtractor: LanguageExtractor = {
+  functionTypes: ['function_definition'],
+  classTypes: ['class_declaration'],
+  methodTypes: ['method_declaration', 'constructor_declaration'],
+  interfaceTypes: ['interface_declaration'],
+  structTypes: [],
+  enumTypes: ['enum_declaration'],
+  enumMemberTypes: ['enum_constant'],
+  typeAliasTypes: [],
+  importTypes: ['import_declaration'],
+  callTypes: ['method_invocation'],
+  variableTypes: ['local_variable_declaration'],
+  fieldTypes: ['field_declaration'],
+  nameField: 'name',
+  bodyField: 'body',
+  paramsField: 'parameters',
+  returnField: 'type',
+  getSignature: (node, source) => {
+    const params = getChildByField(node, 'parameters');
+    const returnType = getChildByField(node, 'type');
+    if (!params) return undefined;
+    const paramsText = getNodeText(params, source);
+    return returnType ? getNodeText(returnType, source) + ' ' + paramsText : paramsText;
+  },
+  getVisibility: (node) => {
+    for (let i = 0; i < node.childCount; i++) {
+      const child = node.child(i);
+      if (child?.type === 'modifiers') {
+        const text = child.text;
+        if (text.includes('public')) return 'public';
+        if (text.includes('private')) return 'private';
+        if (text.includes('protected')) return 'protected';
+      }
+    }
+    return undefined;
+  },
+  isStatic: (node) => {
+    for (let i = 0; i < node.childCount; i++) {
+      const child = node.child(i);
+      if (child?.type === 'modifiers' && child.text.includes('static')) {
+        return true;
+      }
+    }
+    return false;
+  },
+  extractImport: (node, source) => {
+    const importText = source.substring(node.startIndex, node.endIndex).trim();
+    const scopedId = node.namedChildren.find((c: SyntaxNode) => c.type === 'scoped_identifier');
+    if (scopedId) {
+      const moduleName = source.substring(scopedId.startIndex, scopedId.endIndex);
+      return { moduleName, signature: importText };
+    }
+    return null;
+  },
+};
diff --git a/src/extraction/languages/index.ts b/src/extraction/languages/index.ts
index 543598b8e..01d3c5782 100644
--- a/src/extraction/languages/index.ts
+++ b/src/extraction/languages/index.ts
@@ -25,6 +25,7 @@ import { pascalExtractor } from './pascal';
 import { scalaExtractor } from './scala';
 import { luaExtractor } from './lua';
 import { luauExtractor } from './luau';
+import { groovyExtractor } from './groovy';
 import { objcExtractor } from './objc';
 
 export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
@@ -48,5 +49,6 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
   scala: scalaExtractor,
   lua: luaExtractor,
   luau: luauExtractor,
+  groovy: groovyExtractor,
   objc: objcExtractor,
 };
diff --git a/src/extraction/wasm/tree-sitter-groovy.wasm b/src/extraction/wasm/tree-sitter-groovy.wasm
new file mode 100644
index 000000000..46db63d5c
Binary files /dev/null and b/src/extraction/wasm/tree-sitter-groovy.wasm differ
diff --git a/src/types.ts b/src/types.ts
index e710e31a1..ffe304774 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -87,6 +87,7 @@ export const LANGUAGES = [
   'scala',
   'lua',
   'luau',
+  'groovy',
   'objc',
   'yaml',
   'twig',
