mcp-win-stdio-tsc 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.
- mcp_win_stdio_tsc-0.1.0/.gitignore +60 -0
- mcp_win_stdio_tsc-0.1.0/PKG-INFO +70 -0
- mcp_win_stdio_tsc-0.1.0/README.md +47 -0
- mcp_win_stdio_tsc-0.1.0/pyproject.toml +40 -0
- mcp_win_stdio_tsc-0.1.0/src/mcp_win_stdio/tsc/__init__.py +5 -0
- mcp_win_stdio_tsc-0.1.0/src/mcp_win_stdio/tsc/__main__.py +8 -0
- mcp_win_stdio_tsc-0.1.0/src/mcp_win_stdio/tsc/cli.py +89 -0
- mcp_win_stdio_tsc-0.1.0/src/mcp_win_stdio/tsc/guide.py +56 -0
- mcp_win_stdio_tsc-0.1.0/src/mcp_win_stdio/tsc/server.py +474 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
.Python
|
|
6
|
+
build/
|
|
7
|
+
develop-eggs/
|
|
8
|
+
dist/
|
|
9
|
+
downloads/
|
|
10
|
+
eggs/
|
|
11
|
+
.eggs/
|
|
12
|
+
lib/
|
|
13
|
+
lib64/
|
|
14
|
+
parts/
|
|
15
|
+
sdist/
|
|
16
|
+
var/
|
|
17
|
+
wheels/
|
|
18
|
+
share/python-wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
|
|
24
|
+
*.manifest
|
|
25
|
+
*.spec
|
|
26
|
+
|
|
27
|
+
pip-log.txt
|
|
28
|
+
pip-delete-this-directory.txt
|
|
29
|
+
|
|
30
|
+
htmlcov/
|
|
31
|
+
.tox/
|
|
32
|
+
.nox/
|
|
33
|
+
.coverage
|
|
34
|
+
.coverage.*
|
|
35
|
+
.cache
|
|
36
|
+
nosetests.xml
|
|
37
|
+
coverage.xml
|
|
38
|
+
*.cover
|
|
39
|
+
*.py,cover
|
|
40
|
+
.hypothesis/
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
cover/
|
|
43
|
+
|
|
44
|
+
*.mo
|
|
45
|
+
*.pot
|
|
46
|
+
|
|
47
|
+
.env
|
|
48
|
+
.venv
|
|
49
|
+
env/
|
|
50
|
+
venv/
|
|
51
|
+
ENV/
|
|
52
|
+
env.bak/
|
|
53
|
+
venv.bak/
|
|
54
|
+
|
|
55
|
+
.idea/
|
|
56
|
+
.vscode/
|
|
57
|
+
*.swp
|
|
58
|
+
*.swo
|
|
59
|
+
|
|
60
|
+
*.log
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mcp-win-stdio-tsc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Windows-optimized TypeScript Diagnostic Watcher MCP server: background watchers and in-memory cache for instant 0ms compiler error checks.
|
|
5
|
+
Author: Mohan Kumar Indala
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: ai,claude,diagnostics,llm,mcp,tsc,typescript,watcher,windows
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: mcp>=1.2.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# mcp-win-stdio-tsc
|
|
25
|
+
|
|
26
|
+
Windows-optimized **Model Context Protocol (MCP)** server for TypeScript diagnostics.
|
|
27
|
+
|
|
28
|
+
Runs persistent background compiler watchers (`tsc --watch`) with an in-memory cache for **0ms latency** error inspection across multi-project and monorepo repositories.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🌟 Features (6 Tools)
|
|
33
|
+
|
|
34
|
+
* **0ms In-Memory Cache**: `get_tsc_errors` (all active compiler errors across watched projects, with project/tsconfig filters)
|
|
35
|
+
* **File-Specific Diagnostics**: `get_file_errors` (strictly check errors for a specific `.ts`/`.tsx`/`.js`/`.jsx` file)
|
|
36
|
+
* **Token-Safe Summary**: `get_error_summary` (total error counts, project status, and top 5 most common error codes without flooding context)
|
|
37
|
+
* **Dynamic Multi-Project Control**: `list_watched_projects`, `watch_project(path)`, and `restart_tsc_watcher`
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 📦 Installation
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
pip install mcp-win-stdio-tsc
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 🚀 Usage with Claude Desktop
|
|
50
|
+
|
|
51
|
+
Add to `%APPDATA%\Claude\claude_desktop_config.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"tsc": {
|
|
57
|
+
"command": "python",
|
|
58
|
+
"args": [
|
|
59
|
+
"-m",
|
|
60
|
+
"mcp_win_stdio.tsc"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 📜 License
|
|
70
|
+
MIT License. Copyright (c) 2026 Mohan Kumar Indala.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# mcp-win-stdio-tsc
|
|
2
|
+
|
|
3
|
+
Windows-optimized **Model Context Protocol (MCP)** server for TypeScript diagnostics.
|
|
4
|
+
|
|
5
|
+
Runs persistent background compiler watchers (`tsc --watch`) with an in-memory cache for **0ms latency** error inspection across multi-project and monorepo repositories.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🌟 Features (6 Tools)
|
|
10
|
+
|
|
11
|
+
* **0ms In-Memory Cache**: `get_tsc_errors` (all active compiler errors across watched projects, with project/tsconfig filters)
|
|
12
|
+
* **File-Specific Diagnostics**: `get_file_errors` (strictly check errors for a specific `.ts`/`.tsx`/`.js`/`.jsx` file)
|
|
13
|
+
* **Token-Safe Summary**: `get_error_summary` (total error counts, project status, and top 5 most common error codes without flooding context)
|
|
14
|
+
* **Dynamic Multi-Project Control**: `list_watched_projects`, `watch_project(path)`, and `restart_tsc_watcher`
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 📦 Installation
|
|
19
|
+
|
|
20
|
+
```powershell
|
|
21
|
+
pip install mcp-win-stdio-tsc
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🚀 Usage with Claude Desktop
|
|
27
|
+
|
|
28
|
+
Add to `%APPDATA%\Claude\claude_desktop_config.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"tsc": {
|
|
34
|
+
"command": "python",
|
|
35
|
+
"args": [
|
|
36
|
+
"-m",
|
|
37
|
+
"mcp_win_stdio.tsc"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 📜 License
|
|
47
|
+
MIT License. Copyright (c) 2026 Mohan Kumar Indala.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcp-win-stdio-tsc"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Windows-optimized TypeScript Diagnostic Watcher MCP server: background watchers and in-memory cache for instant 0ms compiler error checks."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Mohan Kumar Indala" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mcp", "claude", "typescript", "tsc", "watcher", "diagnostics", "windows", "ai", "llm"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Win32 (MS Windows)",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Topic :: Software Development :: Build Tools",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
dependencies = [
|
|
32
|
+
"mcp>=1.2.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
mcp-win-stdio-tsc = "mcp_win_stdio.tsc.cli:main"
|
|
37
|
+
mws-tsc = "mcp_win_stdio.tsc.cli:main"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/mcp_win_stdio"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for mcp-win-stdio-tsc.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from mcp_win_stdio.tsc import __version__
|
|
12
|
+
from mcp_win_stdio.tsc.guide import print_guide
|
|
13
|
+
from mcp_win_stdio.tsc.server import mcp
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cmd_run(args: argparse.Namespace) -> None:
|
|
17
|
+
"""Run TypeScript Watcher MCP server over stdio."""
|
|
18
|
+
if args.dir:
|
|
19
|
+
os.environ["TSC_WATCH_DIR"] = os.path.abspath(args.dir)
|
|
20
|
+
mcp.run()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def cmd_doctor(args: argparse.Namespace) -> None:
|
|
24
|
+
"""Check TypeScript watcher health and dependencies."""
|
|
25
|
+
print(f"\n=== mcp-win-stdio-tsc Doctor Diagnostic (v{__version__}) ===\n")
|
|
26
|
+
print(f"[OK] Python: {sys.version.split()[0]} ({sys.executable})")
|
|
27
|
+
|
|
28
|
+
for dep in ("mcp",):
|
|
29
|
+
try:
|
|
30
|
+
__import__(dep)
|
|
31
|
+
print(f"[OK] Python Dependency: {dep:<12}")
|
|
32
|
+
except ImportError:
|
|
33
|
+
print(f"[FAIL] Python Dependency: {dep:<12} (MISSING - run `pip install mcp`)")
|
|
34
|
+
|
|
35
|
+
print("\n--- TypeScript & Node.js System Tools ---")
|
|
36
|
+
node_bin = shutil.which("node")
|
|
37
|
+
if node_bin:
|
|
38
|
+
print(f"[OK] Node.js Runtime: Found ({node_bin})")
|
|
39
|
+
else:
|
|
40
|
+
print("[WARN] Node.js Runtime: Not found on PATH")
|
|
41
|
+
|
|
42
|
+
tsc_bin = shutil.which("tsc")
|
|
43
|
+
if tsc_bin:
|
|
44
|
+
print(f"[OK] Global TypeScript Compiler (tsc): Found ({tsc_bin})")
|
|
45
|
+
else:
|
|
46
|
+
print("[INFO] Global TypeScript Compiler (tsc): Not found on PATH (will check local node_modules/npx)")
|
|
47
|
+
|
|
48
|
+
npx_bin = shutil.which("npx")
|
|
49
|
+
if npx_bin:
|
|
50
|
+
print(f"[OK] NPX Runner: Found ({npx_bin})")
|
|
51
|
+
else:
|
|
52
|
+
print("[INFO] NPX Runner: Not found")
|
|
53
|
+
|
|
54
|
+
target_dir = os.environ.get("TSC_WATCH_DIR") or os.getcwd()
|
|
55
|
+
print(f"\n[INFO] Target Watch Directory: {target_dir}")
|
|
56
|
+
print("\nDiagnostic complete.\n")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> None:
|
|
60
|
+
parser = argparse.ArgumentParser(
|
|
61
|
+
prog="mcp-win-stdio-tsc",
|
|
62
|
+
description="Windows-optimized TypeScript Diagnostic Watcher MCP CLI",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument("--version", "-v", action="version", version=f"%(prog)s {__version__}")
|
|
65
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
|
66
|
+
|
|
67
|
+
sub_guide = subparsers.add_parser("guide", help="View usage guide and prompt recipes")
|
|
68
|
+
sub_guide.set_defaults(func=lambda args: print_guide())
|
|
69
|
+
|
|
70
|
+
sub_run = subparsers.add_parser("run", help="Run TypeScript Watcher MCP server over stdio")
|
|
71
|
+
sub_run.add_argument("--dir", "-d", help="Target project directory to watch")
|
|
72
|
+
sub_run.set_defaults(func=cmd_run)
|
|
73
|
+
|
|
74
|
+
sub_doctor = subparsers.add_parser("doctor", help="Check dependencies and TypeScript compiler status")
|
|
75
|
+
sub_doctor.set_defaults(func=cmd_doctor)
|
|
76
|
+
|
|
77
|
+
args = parser.parse_args()
|
|
78
|
+
if not args.command:
|
|
79
|
+
if not sys.stdin.isatty():
|
|
80
|
+
mcp.run()
|
|
81
|
+
return
|
|
82
|
+
parser.print_help()
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
|
|
85
|
+
args.func(args)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Usage guide and Claude prompt recipes for TypeScript Watcher MCP server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
def print_guide() -> None:
|
|
6
|
+
guide_text = """
|
|
7
|
+
================================================================================
|
|
8
|
+
TypeScript Diagnostic Watcher MCP Server (mcp-win-stdio.tsc)
|
|
9
|
+
================================================================================
|
|
10
|
+
|
|
11
|
+
Description:
|
|
12
|
+
Background TypeScript file watcher maintaining an in-memory diagnostic cache
|
|
13
|
+
for instant (0ms latency) TypeScript error checking across multi-tsconfig projects.
|
|
14
|
+
|
|
15
|
+
Available Tools (6 Tools):
|
|
16
|
+
--------------------------------------------------------------------------------
|
|
17
|
+
1. get_tsc_errors
|
|
18
|
+
- Returns all active TypeScript compilation errors from cache.
|
|
19
|
+
- Optional filters: project_path (str), tsconfig_path (str)
|
|
20
|
+
|
|
21
|
+
2. get_file_errors
|
|
22
|
+
- Returns TypeScript errors for a specific .ts / .tsx / .js / .jsx file.
|
|
23
|
+
- Args: file_path (str)
|
|
24
|
+
|
|
25
|
+
3. get_error_summary
|
|
26
|
+
- Returns error counts per project and top 5 most common error codes (e.g. TS2322).
|
|
27
|
+
|
|
28
|
+
4. list_watched_projects
|
|
29
|
+
- Lists all active tsconfig.json files and current watcher statuses.
|
|
30
|
+
|
|
31
|
+
5. watch_project
|
|
32
|
+
- Dynamically adds a new project directory to watch without restarting the server.
|
|
33
|
+
- Args: project_path (str)
|
|
34
|
+
|
|
35
|
+
6. restart_tsc_watcher
|
|
36
|
+
- Restarts all watchers and flushes diagnostics.
|
|
37
|
+
|
|
38
|
+
--------------------------------------------------------------------------------
|
|
39
|
+
Environment Variables:
|
|
40
|
+
--------------------------------------------------------------------------------
|
|
41
|
+
* TSC_WATCH_DIR: Base directory to scan and watch for tsconfig.json files.
|
|
42
|
+
(Defaults to current workspace directory if not specified).
|
|
43
|
+
|
|
44
|
+
--------------------------------------------------------------------------------
|
|
45
|
+
Example Prompts for Claude:
|
|
46
|
+
--------------------------------------------------------------------------------
|
|
47
|
+
* "Check if there are any TypeScript errors in my project right now."
|
|
48
|
+
* "Are there any type errors in src/components/Dashboard.tsx?"
|
|
49
|
+
* "Watch the project located at 'Z:/projects/IJITEST Main' for TypeScript diagnostics."
|
|
50
|
+
================================================================================
|
|
51
|
+
"""
|
|
52
|
+
print(guide_text)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def print_tsc_guide() -> None:
|
|
56
|
+
print_guide()
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TypeScript Diagnostic Watcher MCP Server (Python Edition).
|
|
4
|
+
Runs background tsc compiler watchers for target projects, maintaining an in-memory
|
|
5
|
+
diagnostic cache for instantaneous (0ms latency) TypeScript error inspection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import atexit
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
from mcp.server.fastmcp import FastMCP
|
|
22
|
+
|
|
23
|
+
mcp = FastMCP("tsc-mcp")
|
|
24
|
+
|
|
25
|
+
# Global in-memory cache and watcher state
|
|
26
|
+
# tsconfig_path -> { "process": Popen, "thread": Thread, "project_dir": str, "errors": list, "pending_errors": list, "status": str, "last_updated": str }
|
|
27
|
+
WATCHED_PROJECTS: Dict[str, Dict[str, Any]] = {}
|
|
28
|
+
CACHE_LOCK = threading.Lock()
|
|
29
|
+
|
|
30
|
+
# Regex to parse standard tsc compiler output line
|
|
31
|
+
# Example: src/components/App.tsx(42,15): error TS2322: Type 'string' is not assignable to type 'number'.
|
|
32
|
+
ERROR_REGEX = re.compile(r"^([^(]+)\((\d+),(\d+)\):\s*(error|warning)\s*(TS\d+):\s*(.+)$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_home_or_root_dir(p: str) -> bool:
|
|
36
|
+
"""Check if path is the user home directory, root drive, or Windows system directory."""
|
|
37
|
+
try:
|
|
38
|
+
resolved = Path(p).resolve()
|
|
39
|
+
if resolved == Path.home().resolve() or resolved.parent == resolved:
|
|
40
|
+
return True
|
|
41
|
+
windir = os.environ.get("WINDIR", "C:\\Windows")
|
|
42
|
+
if str(resolved).lower().startswith(windir.lower()):
|
|
43
|
+
return True
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_default_watch_dir() -> str:
|
|
50
|
+
"""Get project directory from env, config file, or current working directory."""
|
|
51
|
+
env_dir = os.environ.get("TSC_WATCH_DIR") or os.environ.get("PROJECT_ROOT")
|
|
52
|
+
if env_dir and os.path.isdir(env_dir):
|
|
53
|
+
return os.path.abspath(env_dir)
|
|
54
|
+
|
|
55
|
+
# Check ~/.mcp-win-stdio/config.json
|
|
56
|
+
try:
|
|
57
|
+
from mcp_win_stdio.core.config import load_config
|
|
58
|
+
cfg = load_config()
|
|
59
|
+
stored_dir = cfg.get("tsc_watch_dir")
|
|
60
|
+
if stored_dir and os.path.isdir(stored_dir):
|
|
61
|
+
return os.path.abspath(stored_dir)
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
return os.path.abspath(os.getcwd())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def normalize_path(p: str) -> str:
|
|
70
|
+
return os.path.abspath(p).replace("\\", "/")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def find_tsconfigs(base_dir: str, max_depth: int = 4) -> List[str]:
|
|
74
|
+
"""Find all tsconfig.json files skipping heavy build/vendor folders."""
|
|
75
|
+
ignore_dirs = {
|
|
76
|
+
"node_modules", ".git", ".next", ".nuxt", "dist", "build",
|
|
77
|
+
"out", ".output", "target", "bin", "obj", "__pycache__", ".cache",
|
|
78
|
+
"coverage", ".turbo"
|
|
79
|
+
}
|
|
80
|
+
configs = []
|
|
81
|
+
base_p = Path(base_dir)
|
|
82
|
+
if not base_p.exists():
|
|
83
|
+
return configs
|
|
84
|
+
|
|
85
|
+
for root, dirs, files in os.walk(base_dir):
|
|
86
|
+
# Prune ignored directories in-place
|
|
87
|
+
dirs[:] = [d for d in dirs if d.lower() not in ignore_dirs]
|
|
88
|
+
rel_depth = len(Path(root).relative_to(base_p).parts)
|
|
89
|
+
if rel_depth > max_depth:
|
|
90
|
+
dirs.clear()
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
if "tsconfig.json" in files:
|
|
94
|
+
configs.append(os.path.join(root, "tsconfig.json"))
|
|
95
|
+
|
|
96
|
+
return configs
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def resolve_tsc_command(project_dir: str) -> Optional[List[str]]:
|
|
100
|
+
"""Find local or global tsc binary."""
|
|
101
|
+
# 1. Local project node_modules/typescript/bin/tsc
|
|
102
|
+
local_tsc_js = Path(project_dir) / "node_modules" / "typescript" / "bin" / "tsc"
|
|
103
|
+
if local_tsc_js.exists():
|
|
104
|
+
node_bin = shutil.which("node") or "node"
|
|
105
|
+
return [node_bin, str(local_tsc_js)]
|
|
106
|
+
|
|
107
|
+
# 2. Local node_modules/.bin/tsc.cmd on Windows
|
|
108
|
+
local_tsc_cmd = Path(project_dir) / "node_modules" / ".bin" / "tsc.cmd"
|
|
109
|
+
if local_tsc_cmd.exists():
|
|
110
|
+
return [str(local_tsc_cmd)]
|
|
111
|
+
|
|
112
|
+
# 3. Global tsc on PATH
|
|
113
|
+
global_tsc = shutil.which("tsc")
|
|
114
|
+
if global_tsc:
|
|
115
|
+
return [global_tsc]
|
|
116
|
+
|
|
117
|
+
# 4. Fallback to npx tsc
|
|
118
|
+
npx_bin = shutil.which("npx")
|
|
119
|
+
if npx_bin:
|
|
120
|
+
return [npx_bin, "tsc"]
|
|
121
|
+
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_tsc_line(line: str, project_dir: str, root_dir: str) -> Optional[Dict[str, Any]]:
|
|
126
|
+
"""Parse single tsc diagnostic error line into structured dict."""
|
|
127
|
+
trimmed = line.strip()
|
|
128
|
+
match = ERROR_REGEX.match(trimmed)
|
|
129
|
+
if not match:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
raw_file, line_num, col_num, severity, code, message = match.groups()
|
|
133
|
+
abs_path = os.path.abspath(raw_file if os.path.isabs(raw_file) else os.path.join(project_dir, raw_file))
|
|
134
|
+
try:
|
|
135
|
+
rel_path = os.path.relpath(abs_path, root_dir)
|
|
136
|
+
except Exception:
|
|
137
|
+
rel_path = abs_path
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"file": normalize_path(abs_path),
|
|
141
|
+
"relative_path": normalize_path(rel_path),
|
|
142
|
+
"line": int(line_num),
|
|
143
|
+
"column": int(col_num),
|
|
144
|
+
"severity": severity.lower(),
|
|
145
|
+
"code": code,
|
|
146
|
+
"message": message.strip(),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _watcher_loop(tsconfig_path: str, root_dir: str) -> None:
|
|
151
|
+
"""Worker thread running tsc --noEmit --watch in background."""
|
|
152
|
+
project_dir = os.path.dirname(tsconfig_path)
|
|
153
|
+
cmd_base = resolve_tsc_command(project_dir)
|
|
154
|
+
|
|
155
|
+
if not cmd_base:
|
|
156
|
+
with CACHE_LOCK:
|
|
157
|
+
if tsconfig_path in WATCHED_PROJECTS:
|
|
158
|
+
WATCHED_PROJECTS[tsconfig_path]["status"] = "error: tsc binary not found (install typescript)"
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
full_cmd = cmd_base + ["--noEmit", "--watch", "--preserveWatchOutput", "-p", tsconfig_path]
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# Create background process without opening visible window
|
|
165
|
+
startupinfo = None
|
|
166
|
+
if os.name == "nt":
|
|
167
|
+
startupinfo = subprocess.STARTUPINFO()
|
|
168
|
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
169
|
+
|
|
170
|
+
proc = subprocess.Popen(
|
|
171
|
+
full_cmd,
|
|
172
|
+
stdout=subprocess.PIPE,
|
|
173
|
+
stderr=subprocess.STDOUT,
|
|
174
|
+
stdin=subprocess.DEVNULL,
|
|
175
|
+
text=True,
|
|
176
|
+
bufsize=1,
|
|
177
|
+
cwd=project_dir,
|
|
178
|
+
startupinfo=startupinfo,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
with CACHE_LOCK:
|
|
182
|
+
if tsconfig_path in WATCHED_PROJECTS:
|
|
183
|
+
WATCHED_PROJECTS[tsconfig_path]["process"] = proc
|
|
184
|
+
WATCHED_PROJECTS[tsconfig_path]["status"] = "watching"
|
|
185
|
+
|
|
186
|
+
pending_errors: List[Dict[str, Any]] = []
|
|
187
|
+
|
|
188
|
+
for raw_line in proc.stdout:
|
|
189
|
+
line = raw_line.rstrip()
|
|
190
|
+
if not line:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
# Check completion markers
|
|
194
|
+
if "Found 0 errors." in line or ("Found " in line and "Watching for file changes." in line):
|
|
195
|
+
with CACHE_LOCK:
|
|
196
|
+
if tsconfig_path in WATCHED_PROJECTS:
|
|
197
|
+
WATCHED_PROJECTS[tsconfig_path]["errors"] = list(pending_errors)
|
|
198
|
+
WATCHED_PROJECTS[tsconfig_path]["status"] = "watching"
|
|
199
|
+
WATCHED_PROJECTS[tsconfig_path]["last_updated"] = datetime.now(timezone.utc).isoformat()
|
|
200
|
+
pending_errors = []
|
|
201
|
+
elif "Starting compilation in watch mode..." in line or "File change detected." in line:
|
|
202
|
+
pending_errors = []
|
|
203
|
+
else:
|
|
204
|
+
err = parse_tsc_line(line, project_dir, root_dir)
|
|
205
|
+
if err:
|
|
206
|
+
pending_errors.append(err)
|
|
207
|
+
elif pending_errors and (line.startswith(" ") or line.startswith("\t")):
|
|
208
|
+
# Multi-line diagnostic message continuation
|
|
209
|
+
pending_errors[-1]["message"] += "\n" + line.strip()
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
with CACHE_LOCK:
|
|
213
|
+
if tsconfig_path in WATCHED_PROJECTS:
|
|
214
|
+
WATCHED_PROJECTS[tsconfig_path]["status"] = f"error: {str(e)}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def start_watching_project(target_dir: str) -> List[str]:
|
|
218
|
+
"""Scan and start background watchers for all tsconfig.json in target_dir."""
|
|
219
|
+
abs_root = os.path.abspath(target_dir)
|
|
220
|
+
configs = find_tsconfigs(abs_root)
|
|
221
|
+
|
|
222
|
+
for cfg in configs:
|
|
223
|
+
norm_cfg = normalize_path(cfg)
|
|
224
|
+
with CACHE_LOCK:
|
|
225
|
+
if norm_cfg in WATCHED_PROJECTS and WATCHED_PROJECTS[norm_cfg].get("process"):
|
|
226
|
+
# Already watching
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
WATCHED_PROJECTS[norm_cfg] = {
|
|
230
|
+
"project_dir": normalize_path(os.path.dirname(cfg)),
|
|
231
|
+
"tsconfig_path": norm_cfg,
|
|
232
|
+
"relative_config": normalize_path(os.path.relpath(cfg, abs_root)),
|
|
233
|
+
"errors": [],
|
|
234
|
+
"status": "initializing",
|
|
235
|
+
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
236
|
+
"process": None,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
t = threading.Thread(target=_watcher_loop, args=(norm_cfg, abs_root), daemon=True)
|
|
240
|
+
t.start()
|
|
241
|
+
|
|
242
|
+
return configs
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# Cleanup processes on exit
|
|
246
|
+
def _cleanup_watchers():
|
|
247
|
+
with CACHE_LOCK:
|
|
248
|
+
for srv in WATCHED_PROJECTS.values():
|
|
249
|
+
proc = srv.get("process")
|
|
250
|
+
if proc:
|
|
251
|
+
try:
|
|
252
|
+
proc.terminate()
|
|
253
|
+
except Exception:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
atexit.register(_cleanup_watchers)
|
|
257
|
+
|
|
258
|
+
# Initialize on import if explicitly configured or running in a project workspace
|
|
259
|
+
_initial_root = get_default_watch_dir()
|
|
260
|
+
if os.path.isdir(_initial_root):
|
|
261
|
+
is_explicit = bool(os.environ.get("TSC_WATCH_DIR") or os.environ.get("PROJECT_ROOT"))
|
|
262
|
+
if not is_explicit:
|
|
263
|
+
has_project_marker = (
|
|
264
|
+
(Path(_initial_root) / "tsconfig.json").exists()
|
|
265
|
+
or (Path(_initial_root) / "package.json").exists()
|
|
266
|
+
)
|
|
267
|
+
if not is_home_or_root_dir(_initial_root) and has_project_marker:
|
|
268
|
+
start_watching_project(_initial_root)
|
|
269
|
+
else:
|
|
270
|
+
start_watching_project(_initial_root)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ==========================================
|
|
275
|
+
# MCP TOOLS
|
|
276
|
+
# ==========================================
|
|
277
|
+
|
|
278
|
+
@mcp.tool()
|
|
279
|
+
def get_tsc_errors(project_path: Optional[str] = None, tsconfig_path: Optional[str] = None) -> Dict[str, Any]:
|
|
280
|
+
"""
|
|
281
|
+
Get all active TypeScript compiler errors across watched projects (0ms latency from memory).
|
|
282
|
+
Can filter by specific project directory or tsconfig.json path.
|
|
283
|
+
"""
|
|
284
|
+
with CACHE_LOCK:
|
|
285
|
+
if not WATCHED_PROJECTS:
|
|
286
|
+
return {
|
|
287
|
+
"total_errors": 0,
|
|
288
|
+
"projects": [],
|
|
289
|
+
"errors": [],
|
|
290
|
+
"warning": "No TypeScript projects are currently being watched. Call 'watch_project(project_path)' with your project directory first, or configure TSC_WATCH_DIR.",
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
all_errors = []
|
|
294
|
+
projects_summary = []
|
|
295
|
+
initializing_count = 0
|
|
296
|
+
|
|
297
|
+
for cfg_path, data in WATCHED_PROJECTS.items():
|
|
298
|
+
if tsconfig_path and normalize_path(tsconfig_path) != cfg_path:
|
|
299
|
+
continue
|
|
300
|
+
if project_path and normalize_path(project_path) not in data["project_dir"]:
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
errs = data.get("errors", [])
|
|
304
|
+
status = data.get("status", "unknown")
|
|
305
|
+
if status == "initializing":
|
|
306
|
+
initializing_count += 1
|
|
307
|
+
|
|
308
|
+
all_errors.extend(errs)
|
|
309
|
+
projects_summary.append({
|
|
310
|
+
"tsconfig": data["relative_config"],
|
|
311
|
+
"project_dir": data["project_dir"],
|
|
312
|
+
"status": status,
|
|
313
|
+
"error_count": len(errs),
|
|
314
|
+
"last_updated": data["last_updated"],
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
result: Dict[str, Any] = {
|
|
318
|
+
"total_errors": len(all_errors),
|
|
319
|
+
"projects": projects_summary,
|
|
320
|
+
"errors": all_errors,
|
|
321
|
+
}
|
|
322
|
+
if initializing_count > 0:
|
|
323
|
+
result["notice"] = f"{initializing_count} project(s) still compiling initial pass. Errors may update in a few seconds."
|
|
324
|
+
|
|
325
|
+
return result
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@mcp.tool()
|
|
329
|
+
def get_file_errors(file_path: str) -> Dict[str, Any]:
|
|
330
|
+
"""
|
|
331
|
+
Get TypeScript compiler errors for a specific file (.ts, .tsx, .js, .jsx).
|
|
332
|
+
"""
|
|
333
|
+
with CACHE_LOCK:
|
|
334
|
+
if not WATCHED_PROJECTS:
|
|
335
|
+
return {
|
|
336
|
+
"file": file_path,
|
|
337
|
+
"error_count": 0,
|
|
338
|
+
"has_errors": False,
|
|
339
|
+
"errors": [],
|
|
340
|
+
"warning": "No TypeScript projects are currently being watched. Call 'watch_project(project_path)' with your project directory first.",
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
norm_file = normalize_path(file_path)
|
|
344
|
+
file_errors = []
|
|
345
|
+
|
|
346
|
+
for cfg_path, data in WATCHED_PROJECTS.items():
|
|
347
|
+
for err in data.get("errors", []):
|
|
348
|
+
if err["file"] == norm_file or err["relative_path"] == norm_file or file_path.replace("\\", "/") in err["file"]:
|
|
349
|
+
file_errors.append(err)
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
"file": file_path,
|
|
353
|
+
"error_count": len(file_errors),
|
|
354
|
+
"has_errors": len(file_errors) > 0,
|
|
355
|
+
"errors": file_errors,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@mcp.tool()
|
|
360
|
+
def get_error_summary() -> Dict[str, Any]:
|
|
361
|
+
"""
|
|
362
|
+
Get high-level summary of TypeScript errors across all projects without full diagnostic lists.
|
|
363
|
+
"""
|
|
364
|
+
with CACHE_LOCK:
|
|
365
|
+
if not WATCHED_PROJECTS:
|
|
366
|
+
return {
|
|
367
|
+
"total_errors": 0,
|
|
368
|
+
"projects_count": 0,
|
|
369
|
+
"projects": {},
|
|
370
|
+
"warning": "No TypeScript projects are currently being watched. Call 'watch_project(project_path)' with your project directory first.",
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
summary = {}
|
|
374
|
+
total = 0
|
|
375
|
+
error_by_code: Dict[str, int] = {}
|
|
376
|
+
initializing_count = 0
|
|
377
|
+
|
|
378
|
+
for cfg_path, data in WATCHED_PROJECTS.items():
|
|
379
|
+
errs = data.get("errors", [])
|
|
380
|
+
count = len(errs)
|
|
381
|
+
total += count
|
|
382
|
+
status = data.get("status", "unknown")
|
|
383
|
+
if status == "initializing":
|
|
384
|
+
initializing_count += 1
|
|
385
|
+
summary[data["relative_config"]] = {
|
|
386
|
+
"project_dir": data["project_dir"],
|
|
387
|
+
"status": status,
|
|
388
|
+
"errors": count,
|
|
389
|
+
}
|
|
390
|
+
for e in errs:
|
|
391
|
+
code = e.get("code", "UNKNOWN")
|
|
392
|
+
error_by_code[code] = error_by_code.get(code, 0) + 1
|
|
393
|
+
|
|
394
|
+
result: Dict[str, Any] = {
|
|
395
|
+
"total_errors": total,
|
|
396
|
+
"projects_count": len(WATCHED_PROJECTS),
|
|
397
|
+
"projects": summary,
|
|
398
|
+
"most_common_error_codes": sorted(error_by_code.items(), key=lambda x: x[1], reverse=True)[:5],
|
|
399
|
+
}
|
|
400
|
+
if initializing_count > 0:
|
|
401
|
+
result["notice"] = f"{initializing_count} project(s) still compiling initial pass."
|
|
402
|
+
|
|
403
|
+
return result
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@mcp.tool()
|
|
407
|
+
def list_watched_projects() -> Dict[str, Any]:
|
|
408
|
+
"""
|
|
409
|
+
List all active TypeScript projects and tsconfig.json files currently being monitored.
|
|
410
|
+
"""
|
|
411
|
+
with CACHE_LOCK:
|
|
412
|
+
projects = []
|
|
413
|
+
for cfg_path, data in WATCHED_PROJECTS.items():
|
|
414
|
+
projects.append({
|
|
415
|
+
"tsconfig_path": cfg_path,
|
|
416
|
+
"relative_config": data["relative_config"],
|
|
417
|
+
"project_dir": data["project_dir"],
|
|
418
|
+
"status": data["status"],
|
|
419
|
+
"error_count": len(data.get("errors", [])),
|
|
420
|
+
"last_updated": data["last_updated"],
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
"watched_projects_count": len(projects),
|
|
425
|
+
"projects": projects,
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@mcp.tool()
|
|
430
|
+
def watch_project(project_path: str) -> Dict[str, Any]:
|
|
431
|
+
"""
|
|
432
|
+
Dynamically add and watch a new TypeScript project or directory without restarting the MCP server.
|
|
433
|
+
"""
|
|
434
|
+
if not os.path.exists(project_path):
|
|
435
|
+
return {"success": False, "error": f"Path not found: {project_path}"}
|
|
436
|
+
|
|
437
|
+
# If user or LLM passed a file path, resolve to its containing directory
|
|
438
|
+
if os.path.isfile(project_path):
|
|
439
|
+
project_path = os.path.dirname(project_path)
|
|
440
|
+
|
|
441
|
+
configs = start_watching_project(project_path)
|
|
442
|
+
if not configs:
|
|
443
|
+
return {
|
|
444
|
+
"success": False,
|
|
445
|
+
"project_path": normalize_path(project_path),
|
|
446
|
+
"found_tsconfigs": [],
|
|
447
|
+
"error": f"No tsconfig.json found in '{project_path}' (searched up to 4 directories deep). Make sure this directory contains a TypeScript project.",
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
"success": True,
|
|
452
|
+
"project_path": normalize_path(project_path),
|
|
453
|
+
"found_tsconfigs": configs,
|
|
454
|
+
"message": f"Started background TypeScript watchers for {len(configs)} configuration(s).",
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@mcp.tool()
|
|
460
|
+
def restart_tsc_watcher() -> Dict[str, Any]:
|
|
461
|
+
"""
|
|
462
|
+
Restart all active background TypeScript watchers and refresh diagnostics.
|
|
463
|
+
"""
|
|
464
|
+
_cleanup_watchers()
|
|
465
|
+
with CACHE_LOCK:
|
|
466
|
+
WATCHED_PROJECTS.clear()
|
|
467
|
+
|
|
468
|
+
default_root = get_default_watch_dir()
|
|
469
|
+
configs = start_watching_project(default_root)
|
|
470
|
+
return {
|
|
471
|
+
"success": True,
|
|
472
|
+
"restarted_configs": configs,
|
|
473
|
+
"message": f"Restarted watchers for {len(configs)} project(s).",
|
|
474
|
+
}
|