Skip to content

Commit 736ce15

Browse files
Merge pull request #36 from GoodbyePlanet/feat/additional-language-parsers
feat: Add support for most popular languages
2 parents 8917250 + d591fbb commit 736ce15

52 files changed

Lines changed: 4308 additions & 51 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ build/
2424
blog.md
2525

2626
# Local config (use config.example.yaml as a template)
27-
config.yaml
27+
config.yaml
28+
29+
# Claude Code local settings
30+
.claude/settings.local.json

README.md

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,9 @@ and stale entries for deleted files are cleaned up automatically. Pass `force: t
2525

2626
Language is detected automatically from file extension or filename — no configuration needed.
2727

28-
| Language | Extracted symbols / extras |
29-
|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
30-
| **Go** | `function`, `method` (with receiver), `struct`, `interface`, `type` |
31-
| **Java** | `class`, `interface`, `enum`, `record`, `method`; Spring stereotypes (`controller`, `service`, `repository`, `component`, `configuration`, `entity`, `exception_handler`); HTTP routes from `@RequestMapping`/`@GetMapping`/etc.; Lombok annotations |
32-
| **Python** | `class`, `function`, `method`, `dataclass`, `pydantic_model`, `api_route` (FastAPI), `lifecycle_hook`; async detection |
33-
| **TypeScript / React** | `class`, `interface`, `type`, `function`, `react_component`, `react_hook`; `React.memo` detection; JSDoc preserved |
34-
| **Dockerfile** | `from`, `run`, `copy`, `env`, `expose`, `entrypoint`, `cmd` |
35-
| **Docker Compose** | `service` |
36-
| **Markdown** | `heading` (with hierarchy) |
37-
| **JSON / HTML / CSS / XML** | structural elements; XML parser is Maven/Spring-aware (`dependency`, `plugin`, `bean`) |
28+
Go, Java, Python, TypeScript / JavaScript (React), Rust, C#, C, C++, Ruby, PHP, Kotlin, Scala, Swift, Dart, Bash, SQL, Lua, R, Dockerfile, Docker Compose, Markdown, JSON, HTML, CSS, XML.
29+
30+
Most parsers are framework-aware where it matters — Spring stereotypes and HTTP routes for Java/Kotlin, FastAPI/Pydantic for Python, ASP.NET for C#, Rails for Ruby, Laravel/Symfony for PHP, React/SwiftUI/Flutter widgets, etc. See `server/parser/` for the per-language extraction details.
3831

3932
## Setup
4033

@@ -200,7 +193,7 @@ server/
200193
├── main.py # MCP server entry point + lifespan
201194
├── config.py # Settings and service configuration
202195
├── state.py # Shared store singletons
203-
├── parser/ # Tree-sitter parsers (Go, Java, Python, TypeScript, Dockerfile, Compose, Markdown, JSON, HTML, CSS, XML)
196+
├── parser/ # Tree-sitter parsers (Go, Java, Python, TypeScript, Rust, C#, C, C++, Ruby, PHP, Kotlin, Scala, Swift, Dart, Bash, SQL, Lua, R, Dockerfile, Compose, Markdown, JSON, HTML, CSS, XML)
204197
├── embeddings/ # Jina Code V2 embedding client (batched, async)
205198
├── indexer/ # GitHub fetcher, code indexing pipeline, git history pipeline
206199
├── store/ # Qdrant vector store (code_symbols and git_commits)

pyproject.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ dependencies = [
1919
"tree-sitter-css>=0.23.0",
2020
"tree-sitter-xml>=0.7.0",
2121
"tree-sitter-yaml>=0.6.0",
22+
"tree-sitter-rust>=0.24.0",
23+
"tree-sitter-c-sharp>=0.23.0",
24+
"tree-sitter-c>=0.24.0",
25+
"tree-sitter-cpp>=0.23.0",
26+
"tree-sitter-ruby>=0.23.0",
27+
"tree-sitter-php>=0.24.0",
28+
"tree-sitter-kotlin>=1.1.0",
29+
"tree-sitter-scala>=0.26.0",
30+
"tree-sitter-swift>=0.7.0",
31+
"tree-sitter-bash>=0.25.0",
32+
"tree-sitter-sql>=0.3.0",
33+
"tree-sitter-lua>=0.5.0",
34+
"tree-sitter-language-pack>=1.5.0,<1.6.0",
2235
"pydantic-settings>=2.14.0",
2336
"pyyaml>=6.0",
2437
"httpx>=0.28.0",

server/main.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from contextlib import asynccontextmanager
55
from typing import AsyncIterator
66

7+
import uvicorn
78
from mcp.server.fastmcp import FastMCP
9+
from starlette.applications import Starlette
810

911
from server.config import settings
1012
from server.embeddings.bm25 import BM25SparseProvider, close_sparse_embedding_provider
@@ -51,15 +53,29 @@ async def lifespan(_: FastMCP) -> AsyncIterator[None]:
5153
logger.info("semcode MCP server stopped.")
5254

5355

56+
# For streamable-http we drive the Starlette app's lifespan ourselves (see main),
57+
# so the per-MCP-session lifespan would re-init the store on every client connect.
5458
mcp = FastMCP(
5559
"semcode",
5660
instructions="Semantic code search across microservices codebases. Supports Go, Java, Python, and TypeScript/React.",
57-
lifespan=lifespan,
61+
lifespan=lifespan if settings.mcp_transport != "streamable-http" else None,
5862
host=settings.mcp_host,
5963
port=settings.mcp_port,
6064
)
6165

6266

67+
def _wrap_http_lifespan(app: Starlette) -> None:
68+
original = app.router.lifespan_context
69+
70+
@asynccontextmanager
71+
async def combined(scope_app: Starlette) -> AsyncIterator[None]:
72+
async with lifespan(mcp):
73+
async with original(scope_app):
74+
yield
75+
76+
app.router.lifespan_context = combined
77+
78+
6379
def main() -> None:
6480
from server.tools.search import register_search_tools
6581
from server.tools.index import register_index_tools
@@ -77,7 +93,17 @@ def main() -> None:
7793
register_system_prompts(mcp)
7894
register_http_routes(mcp)
7995

80-
mcp.run(transport=settings.mcp_transport)
96+
if settings.mcp_transport == "streamable-http":
97+
app = mcp.streamable_http_app()
98+
_wrap_http_lifespan(app)
99+
uvicorn.run(
100+
app,
101+
host=settings.mcp_host,
102+
port=settings.mcp_port,
103+
log_level=mcp.settings.log_level.lower(),
104+
)
105+
else:
106+
mcp.run(transport=settings.mcp_transport)
81107

82108

83109
if __name__ == "__main__":
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
# Shared between Java and Kotlin parsers — both target Spring/JPA on the JVM.
4+
5+
SPRING_STEREOTYPES: dict[str, str] = {
6+
"RestController": "controller",
7+
"Controller": "controller",
8+
"Service": "service",
9+
"Repository": "repository",
10+
"Component": "component",
11+
"Configuration": "configuration",
12+
"RestControllerAdvice": "exception_handler",
13+
"ControllerAdvice": "exception_handler",
14+
"Entity": "entity",
15+
"MappedSuperclass": "entity",
16+
}
17+
18+
HTTP_METHOD_ANNOTATIONS: dict[str, str | None] = {
19+
"GetMapping": "GET",
20+
"PostMapping": "POST",
21+
"PutMapping": "PUT",
22+
"DeleteMapping": "DELETE",
23+
"PatchMapping": "PATCH",
24+
"RequestMapping": None, # method determined from attributes
25+
}
26+
27+
LOMBOK_ANNOTATIONS: set[str] = {
28+
"Getter",
29+
"Setter",
30+
"Data",
31+
"Builder",
32+
"NoArgsConstructor",
33+
"AllArgsConstructor",
34+
"RequiredArgsConstructor",
35+
"Slf4j",
36+
"ToString",
37+
"EqualsAndHashCode",
38+
"Value",
39+
}

server/parser/bash.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import annotations
2+
3+
import tree_sitter_bash
4+
from tree_sitter import Language, Node, Parser
5+
6+
from server.parser.base import CodeSymbol, _node_text
7+
8+
BASH_LANGUAGE = Language(tree_sitter_bash.language())
9+
10+
11+
def _docstring(node: Node, source: bytes) -> str | None:
12+
prev = node.prev_sibling
13+
lines: list[str] = []
14+
while prev is not None:
15+
if prev.type == "comment":
16+
lines.insert(0, _node_text(prev, source))
17+
prev = prev.prev_sibling
18+
continue
19+
if prev.type in ("\n", " "):
20+
prev = prev.prev_sibling
21+
continue
22+
break
23+
return "\n".join(lines) if lines else None
24+
25+
26+
def _parse_function(node: Node, source: bytes, file_path: str) -> CodeSymbol | None:
27+
name_node = node.child_by_field_name("name")
28+
if name_node is None:
29+
for child in node.children:
30+
if child.type == "word":
31+
name_node = child
32+
break
33+
if name_node is None:
34+
return None
35+
36+
return CodeSymbol(
37+
name=_node_text(name_node, source),
38+
symbol_type="function",
39+
language="bash",
40+
source=_node_text(node, source),
41+
file_path=file_path,
42+
start_line=node.start_point[0] + 1,
43+
end_line=node.end_point[0] + 1,
44+
signature=_node_text(node, source).split("{", 1)[0].strip(),
45+
docstring=_docstring(node, source),
46+
)
47+
48+
49+
class BashParser:
50+
def __init__(self) -> None:
51+
self._parser = Parser(BASH_LANGUAGE)
52+
53+
def supported_extensions(self) -> list[str]:
54+
return [".sh", ".bash"]
55+
56+
def language(self) -> str:
57+
return "bash"
58+
59+
def parse_file(self, source: bytes, file_path: str) -> list[CodeSymbol]:
60+
tree = self._parser.parse(source)
61+
symbols: list[CodeSymbol] = []
62+
for child in tree.root_node.children:
63+
if child.type == "function_definition":
64+
sym = _parse_function(child, source, file_path)
65+
if sym:
66+
symbols.append(sym)
67+
return symbols

0 commit comments

Comments
 (0)