scope-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scope_mcp-0.1.0/LICENSE +21 -0
- scope_mcp-0.1.0/PKG-INFO +144 -0
- scope_mcp-0.1.0/README.md +120 -0
- scope_mcp-0.1.0/pyproject.toml +38 -0
- scope_mcp-0.1.0/scope/__init__.py +0 -0
- scope_mcp-0.1.0/scope/__main__.py +17 -0
- scope_mcp-0.1.0/scope/lsp_client.py +168 -0
- scope_mcp-0.1.0/scope/lsp_registry.py +58 -0
- scope_mcp-0.1.0/scope/project.py +134 -0
- scope_mcp-0.1.0/scope/server.py +683 -0
- scope_mcp-0.1.0/scope_mcp.egg-info/PKG-INFO +144 -0
- scope_mcp-0.1.0/scope_mcp.egg-info/SOURCES.txt +15 -0
- scope_mcp-0.1.0/scope_mcp.egg-info/dependency_links.txt +1 -0
- scope_mcp-0.1.0/scope_mcp.egg-info/entry_points.txt +2 -0
- scope_mcp-0.1.0/scope_mcp.egg-info/requires.txt +1 -0
- scope_mcp-0.1.0/scope_mcp.egg-info/top_level.txt +1 -0
- scope_mcp-0.1.0/setup.cfg +4 -0
scope_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Deviprasad Shetty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
scope_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scope-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Give your AI coding assistant real code understanding — powered by the same engines VS Code uses
|
|
5
|
+
Author: Deviprasad Shetty
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/deviprasadshetty-dev/scope
|
|
8
|
+
Project-URL: Repository, https://github.com/deviprasadshetty-dev/scope.git
|
|
9
|
+
Project-URL: BugTracker, https://github.com/deviprasadshetty-dev/scope/issues
|
|
10
|
+
Keywords: mcp,lsp,ai,code-navigation,claude,coding-assistant
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mcp>=1.0.0
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# scope-mcp
|
|
26
|
+
|
|
27
|
+
**A tool that gives your AI coding assistant superpowers.** Instead of guessing what your code does, it gets real answers straight from the same engines VS Code uses — so it actually understands functions, classes, types, and where things connect.
|
|
28
|
+
|
|
29
|
+
No setup. No indexing. Just works.
|
|
30
|
+
|
|
31
|
+
## What's the problem?
|
|
32
|
+
|
|
33
|
+
When you ask an AI to "find where `get_lsp` is called," most tools do a text search — like Ctrl+Shift+F. That returns *everything* named `get_lsp`: the definition, comments, imports, false positives. You have to dig through the noise.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
┌─ grep (text search) ─────────────────────┐
|
|
37
|
+
│ project.py:75 def get_lsp(self, lang) │ ← definition (not a call)
|
|
38
|
+
│ project.py:110 return await get_lsp(lang)│ ← actual call
|
|
39
|
+
│ project.py:117 lsp = await get_lsp(lang) │ ← actual call
|
|
40
|
+
│ project.py:81 cfg = LSP_REGISTRY... │ ← noise
|
|
41
|
+
│ project.py:109 async def get_lsp_for... │ ← noise
|
|
42
|
+
│ 5 results · 3 are noise │
|
|
43
|
+
└───────────────────────────────────────────┘
|
|
44
|
+
|
|
45
|
+
┌─ scope (smart search) ────────────────────┐
|
|
46
|
+
│ [call site] project.py:110 │
|
|
47
|
+
│ [call site] project.py:117 │
|
|
48
|
+
│ 2 results · 0 noise │
|
|
49
|
+
└───────────────────────────────────────────┘
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Scope asks the compiler instead. You get only what you actually asked for.
|
|
53
|
+
|
|
54
|
+
## What you can do with it
|
|
55
|
+
|
|
56
|
+
| Instead of digging through files... | Just ask scope... | You get |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| Grepping for a function name and filtering out junk | `find_references("get_lsp")` | Only the places where it's actually called — no noise |
|
|
59
|
+
| Reading a whole file to figure out what's in it | `explain_file("server.py")` | A clean summary: language, line count, every function and class |
|
|
60
|
+
| Hunting through 50 files to find where an interface is used | `implementations("IEventHandler")` | One answer with all the implementations |
|
|
61
|
+
| Tracing who calls what by hand | `call_hierarchy("validate_token")` | A tree of callers and callees, 3 levels deep |
|
|
62
|
+
| Scanning a big diff to see what changed | `changed_since("HEAD~3")` | Just the changed files and affected symbols |
|
|
63
|
+
|
|
64
|
+
## How it works
|
|
65
|
+
|
|
66
|
+
1. **Scope looks at your project** — it spots what languages you're using (Python, TypeScript, Rust, Go, C++) from files like `package.json` or `Cargo.toml`.
|
|
67
|
+
2. **It sets up the brain** — launches the same language engine your editor uses (pyright, tsserver, rust-analyzer, gopls). If missing, it installs one automatically.
|
|
68
|
+
3. **Your AI asks, scope answers** — every question hits that engine live. No stale data, no sync jobs, no waiting.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
AI Assistant (Claude, Codex, etc.) ←→ scope ←→ Language Engine
|
|
72
|
+
(pyright, tsserver, etc.)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Setup
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install scope-mcp
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then add one line to your AI client's config:
|
|
82
|
+
|
|
83
|
+
### Claude Desktop / Code
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"scope": {
|
|
89
|
+
"command": "scope",
|
|
90
|
+
"args": ["--project", "."]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Any AI coding tool
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
command: scope
|
|
100
|
+
args: ["--project", "/path/to/your/project"]
|
|
101
|
+
transport: stdio
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### From source
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git clone https://github.com/yourname/scope-mcp
|
|
108
|
+
cd scope-mcp
|
|
109
|
+
pip install -e .
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Optional extras: `rg` (ripgrep) for text searches, `git` for change tracking.
|
|
113
|
+
|
|
114
|
+
## Languages scope understands
|
|
115
|
+
|
|
116
|
+
| Language | Detected when it sees... | Scope handles setup |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| Python | `pyproject.toml` · `setup.py` · `requirements.txt` | ✅ Installs pyright automatically |
|
|
119
|
+
| TypeScript / JavaScript | `tsconfig.json` · `package.json` | ✅ Installs tsserver automatically |
|
|
120
|
+
| Rust | `Cargo.toml` | ✅ Installs rust-analyzer automatically |
|
|
121
|
+
| Go | `go.mod` | ✅ Installs gopls automatically |
|
|
122
|
+
| C / C++ | `CMakeLists.txt` · `compile_commands.json` | ⚠️ You install clangd manually |
|
|
123
|
+
|
|
124
|
+
## Project layout
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
scope/
|
|
128
|
+
├── __init__.py
|
|
129
|
+
├── __main__.py # Where scope starts — just runs `scope --project .`
|
|
130
|
+
├── server.py # All the commands (tools) your AI can call
|
|
131
|
+
├── project.py # Figures out your project and starts language engines
|
|
132
|
+
├── lsp_client.py # Talks to language engines behind the scenes
|
|
133
|
+
└── lsp_registry.py # Knows which engine to use for each language
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Requirements
|
|
137
|
+
|
|
138
|
+
- Python 3.11 or newer
|
|
139
|
+
- ripgrep (optional, for text search)
|
|
140
|
+
- git (optional, for change tracking)
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# scope-mcp
|
|
2
|
+
|
|
3
|
+
**A tool that gives your AI coding assistant superpowers.** Instead of guessing what your code does, it gets real answers straight from the same engines VS Code uses — so it actually understands functions, classes, types, and where things connect.
|
|
4
|
+
|
|
5
|
+
No setup. No indexing. Just works.
|
|
6
|
+
|
|
7
|
+
## What's the problem?
|
|
8
|
+
|
|
9
|
+
When you ask an AI to "find where `get_lsp` is called," most tools do a text search — like Ctrl+Shift+F. That returns *everything* named `get_lsp`: the definition, comments, imports, false positives. You have to dig through the noise.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌─ grep (text search) ─────────────────────┐
|
|
13
|
+
│ project.py:75 def get_lsp(self, lang) │ ← definition (not a call)
|
|
14
|
+
│ project.py:110 return await get_lsp(lang)│ ← actual call
|
|
15
|
+
│ project.py:117 lsp = await get_lsp(lang) │ ← actual call
|
|
16
|
+
│ project.py:81 cfg = LSP_REGISTRY... │ ← noise
|
|
17
|
+
│ project.py:109 async def get_lsp_for... │ ← noise
|
|
18
|
+
│ 5 results · 3 are noise │
|
|
19
|
+
└───────────────────────────────────────────┘
|
|
20
|
+
|
|
21
|
+
┌─ scope (smart search) ────────────────────┐
|
|
22
|
+
│ [call site] project.py:110 │
|
|
23
|
+
│ [call site] project.py:117 │
|
|
24
|
+
│ 2 results · 0 noise │
|
|
25
|
+
└───────────────────────────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Scope asks the compiler instead. You get only what you actually asked for.
|
|
29
|
+
|
|
30
|
+
## What you can do with it
|
|
31
|
+
|
|
32
|
+
| Instead of digging through files... | Just ask scope... | You get |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Grepping for a function name and filtering out junk | `find_references("get_lsp")` | Only the places where it's actually called — no noise |
|
|
35
|
+
| Reading a whole file to figure out what's in it | `explain_file("server.py")` | A clean summary: language, line count, every function and class |
|
|
36
|
+
| Hunting through 50 files to find where an interface is used | `implementations("IEventHandler")` | One answer with all the implementations |
|
|
37
|
+
| Tracing who calls what by hand | `call_hierarchy("validate_token")` | A tree of callers and callees, 3 levels deep |
|
|
38
|
+
| Scanning a big diff to see what changed | `changed_since("HEAD~3")` | Just the changed files and affected symbols |
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
1. **Scope looks at your project** — it spots what languages you're using (Python, TypeScript, Rust, Go, C++) from files like `package.json` or `Cargo.toml`.
|
|
43
|
+
2. **It sets up the brain** — launches the same language engine your editor uses (pyright, tsserver, rust-analyzer, gopls). If missing, it installs one automatically.
|
|
44
|
+
3. **Your AI asks, scope answers** — every question hits that engine live. No stale data, no sync jobs, no waiting.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
AI Assistant (Claude, Codex, etc.) ←→ scope ←→ Language Engine
|
|
48
|
+
(pyright, tsserver, etc.)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Setup
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install scope-mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then add one line to your AI client's config:
|
|
58
|
+
|
|
59
|
+
### Claude Desktop / Code
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"scope": {
|
|
65
|
+
"command": "scope",
|
|
66
|
+
"args": ["--project", "."]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Any AI coding tool
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
command: scope
|
|
76
|
+
args: ["--project", "/path/to/your/project"]
|
|
77
|
+
transport: stdio
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### From source
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/yourname/scope-mcp
|
|
84
|
+
cd scope-mcp
|
|
85
|
+
pip install -e .
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Optional extras: `rg` (ripgrep) for text searches, `git` for change tracking.
|
|
89
|
+
|
|
90
|
+
## Languages scope understands
|
|
91
|
+
|
|
92
|
+
| Language | Detected when it sees... | Scope handles setup |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| Python | `pyproject.toml` · `setup.py` · `requirements.txt` | ✅ Installs pyright automatically |
|
|
95
|
+
| TypeScript / JavaScript | `tsconfig.json` · `package.json` | ✅ Installs tsserver automatically |
|
|
96
|
+
| Rust | `Cargo.toml` | ✅ Installs rust-analyzer automatically |
|
|
97
|
+
| Go | `go.mod` | ✅ Installs gopls automatically |
|
|
98
|
+
| C / C++ | `CMakeLists.txt` · `compile_commands.json` | ⚠️ You install clangd manually |
|
|
99
|
+
|
|
100
|
+
## Project layout
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
scope/
|
|
104
|
+
├── __init__.py
|
|
105
|
+
├── __main__.py # Where scope starts — just runs `scope --project .`
|
|
106
|
+
├── server.py # All the commands (tools) your AI can call
|
|
107
|
+
├── project.py # Figures out your project and starts language engines
|
|
108
|
+
├── lsp_client.py # Talks to language engines behind the scenes
|
|
109
|
+
└── lsp_registry.py # Knows which engine to use for each language
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Requirements
|
|
113
|
+
|
|
114
|
+
- Python 3.11 or newer
|
|
115
|
+
- ripgrep (optional, for text search)
|
|
116
|
+
- git (optional, for change tracking)
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scope-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Give your AI coding assistant real code understanding — powered by the same engines VS Code uses"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Deviprasad Shetty" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mcp", "lsp", "ai", "code-navigation", "claude", "coding-assistant"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development",
|
|
24
|
+
"Topic :: Software Development :: Code Generators",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["mcp>=1.0.0"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/deviprasadshetty-dev/scope"
|
|
30
|
+
Repository = "https://github.com/deviprasadshetty-dev/scope.git"
|
|
31
|
+
BugTracker = "https://github.com/deviprasadshetty-dev/scope/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
scope = "scope.__main__:main_cli"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["."]
|
|
38
|
+
include = ["scope*"]
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main_cli():
|
|
6
|
+
from .server import run
|
|
7
|
+
project_path = "."
|
|
8
|
+
args = sys.argv[1:]
|
|
9
|
+
if "--project" in args:
|
|
10
|
+
i = args.index("--project")
|
|
11
|
+
if i + 1 < len(args):
|
|
12
|
+
project_path = args[i + 1]
|
|
13
|
+
asyncio.run(run(project_path))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
main_cli()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import pathlib
|
|
4
|
+
import shutil
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LSPError(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LSPClient:
|
|
13
|
+
def __init__(self, binary: str, args: list[str], root_path: str):
|
|
14
|
+
self._binary = binary
|
|
15
|
+
self._args = args
|
|
16
|
+
self._root = pathlib.Path(root_path)
|
|
17
|
+
self._proc: asyncio.subprocess.Process | None = None
|
|
18
|
+
self._pending: dict[int, asyncio.Future] = {}
|
|
19
|
+
self._seq = 0
|
|
20
|
+
self.capabilities: dict = {}
|
|
21
|
+
# path -> content hash; tracks what the LSP has open
|
|
22
|
+
self._open_files: dict[str, str] = {}
|
|
23
|
+
self._diagnostics: dict[str, list] = {}
|
|
24
|
+
self._diag_events: dict[str, asyncio.Event] = {}
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def available(binary: str) -> bool:
|
|
28
|
+
return shutil.which(binary) is not None
|
|
29
|
+
|
|
30
|
+
async def start(self) -> None:
|
|
31
|
+
self._proc = await asyncio.create_subprocess_exec(
|
|
32
|
+
self._binary, *self._args,
|
|
33
|
+
stdin=asyncio.subprocess.PIPE,
|
|
34
|
+
stdout=asyncio.subprocess.PIPE,
|
|
35
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
36
|
+
)
|
|
37
|
+
asyncio.create_task(self._read_loop())
|
|
38
|
+
|
|
39
|
+
root_uri = self._root.as_uri()
|
|
40
|
+
result = await self.request("initialize", {
|
|
41
|
+
"processId": None,
|
|
42
|
+
"rootUri": root_uri,
|
|
43
|
+
"workspaceFolders": [{"uri": root_uri, "name": self._root.name}],
|
|
44
|
+
"capabilities": {
|
|
45
|
+
"textDocument": {
|
|
46
|
+
"hover": {"contentFormat": ["plaintext", "markdown"]},
|
|
47
|
+
"references": {},
|
|
48
|
+
"documentSymbol": {"hierarchicalDocumentSymbolSupport": True},
|
|
49
|
+
"callHierarchy": {},
|
|
50
|
+
"typeHierarchy": {},
|
|
51
|
+
"implementation": {},
|
|
52
|
+
},
|
|
53
|
+
"workspace": {"symbol": {}},
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
self.capabilities = result.get("capabilities", {}) if result else {}
|
|
57
|
+
await self.notify("initialized", {})
|
|
58
|
+
|
|
59
|
+
async def request(self, method: str, params: Any, timeout: float = 30.0) -> Any:
|
|
60
|
+
self._seq += 1
|
|
61
|
+
msg_id = self._seq
|
|
62
|
+
loop = asyncio.get_event_loop()
|
|
63
|
+
fut: asyncio.Future = loop.create_future()
|
|
64
|
+
self._pending[msg_id] = fut
|
|
65
|
+
await self._send({"jsonrpc": "2.0", "id": msg_id, "method": method, "params": params})
|
|
66
|
+
try:
|
|
67
|
+
return await asyncio.wait_for(fut, timeout=timeout)
|
|
68
|
+
except TimeoutError:
|
|
69
|
+
self._pending.pop(msg_id, None)
|
|
70
|
+
raise LSPError(f"LSP timeout: {method}")
|
|
71
|
+
|
|
72
|
+
async def notify(self, method: str, params: Any) -> None:
|
|
73
|
+
await self._send({"jsonrpc": "2.0", "method": method, "params": params})
|
|
74
|
+
|
|
75
|
+
async def ensure_open(self, file_path: str, language_id: str) -> None:
|
|
76
|
+
"""Open or refresh a file in the LSP, sending didChange only if content changed."""
|
|
77
|
+
path = pathlib.Path(file_path)
|
|
78
|
+
if not path.exists():
|
|
79
|
+
return
|
|
80
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
81
|
+
content_hash = str(hash(text))
|
|
82
|
+
uri = path.as_uri()
|
|
83
|
+
|
|
84
|
+
if file_path in self._open_files:
|
|
85
|
+
if self._open_files[file_path] == content_hash:
|
|
86
|
+
return
|
|
87
|
+
await self.notify("textDocument/didChange", {
|
|
88
|
+
"textDocument": {"uri": uri, "version": 2},
|
|
89
|
+
"contentChanges": [{"text": text}],
|
|
90
|
+
})
|
|
91
|
+
else:
|
|
92
|
+
await self.notify("textDocument/didOpen", {
|
|
93
|
+
"textDocument": {"uri": uri, "languageId": language_id, "version": 1, "text": text},
|
|
94
|
+
})
|
|
95
|
+
self._open_files[file_path] = content_hash
|
|
96
|
+
|
|
97
|
+
async def get_diagnostics(self, file_path: str, timeout: float = 5.0) -> list:
|
|
98
|
+
uri = pathlib.Path(file_path).as_uri()
|
|
99
|
+
if uri not in self._diag_events:
|
|
100
|
+
self._diag_events[uri] = asyncio.Event()
|
|
101
|
+
ev = self._diag_events[uri]
|
|
102
|
+
if not ev.is_set():
|
|
103
|
+
try:
|
|
104
|
+
await asyncio.wait_for(ev.wait(), timeout=timeout)
|
|
105
|
+
except asyncio.TimeoutError:
|
|
106
|
+
pass
|
|
107
|
+
return self._diagnostics.get(uri, [])
|
|
108
|
+
|
|
109
|
+
async def shutdown(self) -> None:
|
|
110
|
+
try:
|
|
111
|
+
await self.request("shutdown", None, timeout=5)
|
|
112
|
+
await self.notify("exit", None)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
if self._proc:
|
|
116
|
+
self._proc.terminate()
|
|
117
|
+
try:
|
|
118
|
+
await asyncio.wait_for(self._proc.wait(), timeout=3)
|
|
119
|
+
except Exception:
|
|
120
|
+
self._proc.kill()
|
|
121
|
+
|
|
122
|
+
async def _send(self, msg: dict) -> None:
|
|
123
|
+
if not self._proc or self._proc.stdin.is_closing():
|
|
124
|
+
raise LSPError("LSP process not running")
|
|
125
|
+
data = json.dumps(msg).encode()
|
|
126
|
+
header = f"Content-Length: {len(data)}\r\n\r\n".encode()
|
|
127
|
+
self._proc.stdin.write(header + data)
|
|
128
|
+
await self._proc.stdin.drain()
|
|
129
|
+
|
|
130
|
+
async def _read_loop(self) -> None:
|
|
131
|
+
while self._proc and not self._proc.stdout.at_eof():
|
|
132
|
+
try:
|
|
133
|
+
content_length = None
|
|
134
|
+
while True:
|
|
135
|
+
line = await self._proc.stdout.readline()
|
|
136
|
+
if not line or line == b"\r\n":
|
|
137
|
+
break
|
|
138
|
+
if line.lower().startswith(b"content-length:"):
|
|
139
|
+
content_length = int(line.split(b":")[1].strip())
|
|
140
|
+
|
|
141
|
+
if content_length is None:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
body = await self._proc.stdout.readexactly(content_length)
|
|
145
|
+
msg = json.loads(body)
|
|
146
|
+
|
|
147
|
+
msg_id = msg.get("id")
|
|
148
|
+
if msg_id is not None and msg_id in self._pending:
|
|
149
|
+
fut = self._pending.pop(msg_id)
|
|
150
|
+
if not fut.done():
|
|
151
|
+
if "error" in msg:
|
|
152
|
+
fut.set_exception(LSPError(msg["error"].get("message", "LSP error")))
|
|
153
|
+
else:
|
|
154
|
+
fut.set_result(msg.get("result"))
|
|
155
|
+
elif msg.get("method") == "textDocument/publishDiagnostics":
|
|
156
|
+
params = msg.get("params", {})
|
|
157
|
+
uri = params.get("uri", "")
|
|
158
|
+
self._diagnostics[uri] = params.get("diagnostics", [])
|
|
159
|
+
ev = self._diag_events.get(uri)
|
|
160
|
+
if ev:
|
|
161
|
+
ev.set()
|
|
162
|
+
except Exception:
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
for fut in self._pending.values():
|
|
166
|
+
if not fut.done():
|
|
167
|
+
fut.set_exception(LSPError("LSP process disconnected"))
|
|
168
|
+
self._pending.clear()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
LSP_REGISTRY = {
|
|
2
|
+
"typescript": {
|
|
3
|
+
"binary": "typescript-language-server",
|
|
4
|
+
"args": ["--stdio"],
|
|
5
|
+
"install": "npm install -g typescript-language-server typescript",
|
|
6
|
+
"root_markers": ["tsconfig.json", "package.json"],
|
|
7
|
+
"extensions": {".ts", ".tsx", ".js", ".jsx"},
|
|
8
|
+
"language_ids": {".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact"},
|
|
9
|
+
},
|
|
10
|
+
"python": {
|
|
11
|
+
"binary": "pyright-langserver",
|
|
12
|
+
"args": ["--stdio"],
|
|
13
|
+
"install": "pip install pyright",
|
|
14
|
+
"root_markers": ["pyproject.toml", "setup.py", "requirements.txt"],
|
|
15
|
+
"extensions": {".py", ".pyi"},
|
|
16
|
+
"language_ids": {".py": "python", ".pyi": "python"},
|
|
17
|
+
},
|
|
18
|
+
"rust": {
|
|
19
|
+
"binary": "rust-analyzer",
|
|
20
|
+
"args": [],
|
|
21
|
+
"install": "rustup component add rust-analyzer",
|
|
22
|
+
"root_markers": ["Cargo.toml"],
|
|
23
|
+
"extensions": {".rs"},
|
|
24
|
+
"language_ids": {".rs": "rust"},
|
|
25
|
+
},
|
|
26
|
+
"go": {
|
|
27
|
+
"binary": "gopls",
|
|
28
|
+
"args": [],
|
|
29
|
+
"install": "go install golang.org/x/tools/gopls@latest",
|
|
30
|
+
"root_markers": ["go.mod"],
|
|
31
|
+
"extensions": {".go"},
|
|
32
|
+
"language_ids": {".go": "go"},
|
|
33
|
+
},
|
|
34
|
+
"c_cpp": {
|
|
35
|
+
"binary": "clangd",
|
|
36
|
+
"args": [],
|
|
37
|
+
"install": "apt install clangd # or: brew install llvm",
|
|
38
|
+
"root_markers": ["CMakeLists.txt", "compile_commands.json"],
|
|
39
|
+
"extensions": {".c", ".cpp", ".h", ".hpp", ".cc", ".cxx"},
|
|
40
|
+
"language_ids": {".c": "c", ".cpp": "cpp", ".h": "c", ".hpp": "cpp", ".cc": "cpp", ".cxx": "cpp"},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
SYMBOL_KINDS = {
|
|
45
|
+
1: "file", 2: "module", 3: "namespace", 4: "package", 5: "class",
|
|
46
|
+
6: "method", 7: "property", 8: "field", 9: "constructor", 10: "enum",
|
|
47
|
+
11: "interface", 12: "function", 13: "variable", 14: "constant",
|
|
48
|
+
15: "string", 16: "number", 17: "boolean", 18: "array", 19: "object",
|
|
49
|
+
20: "key", 21: "null", 22: "enum_member", 23: "struct", 24: "event",
|
|
50
|
+
25: "operator", 26: "type_parameter",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
KIND_TO_LSP = {
|
|
54
|
+
"file": 1, "module": 2, "namespace": 3, "package": 4, "class": 5,
|
|
55
|
+
"method": 6, "property": 7, "field": 8, "constructor": 9, "enum": 10,
|
|
56
|
+
"interface": 11, "function": 12, "variable": 13, "constant": 14,
|
|
57
|
+
"struct": 23, "type": 26,
|
|
58
|
+
}
|