trishul-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.
- trishul_mcp-0.1.0/PKG-INFO +53 -0
- trishul_mcp-0.1.0/README.md +43 -0
- trishul_mcp-0.1.0/pyproject.toml +24 -0
- trishul_mcp-0.1.0/setup.cfg +4 -0
- trishul_mcp-0.1.0/src/llmgraph_mcp/__init__.py +3 -0
- trishul_mcp-0.1.0/src/llmgraph_mcp/client.py +2 -0
- trishul_mcp-0.1.0/src/llmgraph_mcp/config.py +2 -0
- trishul_mcp-0.1.0/src/llmgraph_mcp/errors.py +2 -0
- trishul_mcp-0.1.0/src/llmgraph_mcp/schemas.py +2 -0
- trishul_mcp-0.1.0/src/llmgraph_mcp/server.py +6 -0
- trishul_mcp-0.1.0/src/trishul_mcp/__init__.py +2 -0
- trishul_mcp-0.1.0/src/trishul_mcp/client.py +145 -0
- trishul_mcp-0.1.0/src/trishul_mcp/config.py +14 -0
- trishul_mcp-0.1.0/src/trishul_mcp/errors.py +28 -0
- trishul_mcp-0.1.0/src/trishul_mcp/schemas.py +56 -0
- trishul_mcp-0.1.0/src/trishul_mcp/server.py +97 -0
- trishul_mcp-0.1.0/src/trishul_mcp.egg-info/PKG-INFO +53 -0
- trishul_mcp-0.1.0/src/trishul_mcp.egg-info/SOURCES.txt +21 -0
- trishul_mcp-0.1.0/src/trishul_mcp.egg-info/dependency_links.txt +1 -0
- trishul_mcp-0.1.0/src/trishul_mcp.egg-info/entry_points.txt +5 -0
- trishul_mcp-0.1.0/src/trishul_mcp.egg-info/requires.txt +3 -0
- trishul_mcp-0.1.0/src/trishul_mcp.egg-info/top_level.txt +2 -0
- trishul_mcp-0.1.0/tests/test_package_boundary.py +74 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trishul-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Trishul: Deterministic repository-scale code intelligence Model Context Protocol (MCP) server.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: mcp>=1.0.0
|
|
8
|
+
Requires-Dist: httpx>=0.25.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0.0
|
|
10
|
+
|
|
11
|
+
# trishul-mcp
|
|
12
|
+
|
|
13
|
+
Deterministic, repository-scale semantic code intelligence Model Context Protocol (MCP) server for AI coding agents (Claude Code, Cursor, Windsurf).
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
`trishul-mcp` connects AI coding assistants directly to a persistent, topological code intelligence graph. Instead of burning your model's context window re-reading thousands of raw files or guessing caller hierarchies with naive regex, Trishul provides deterministic answers with verified certainty:
|
|
18
|
+
|
|
19
|
+
- **Callers (`get_callers`)**: Who directly or indirectly invokes a symbol?
|
|
20
|
+
- **Blast Radius (`get_blast` / `get_impact`)**: What breaks upstream if this function or interface changes?
|
|
21
|
+
- **Execution Paths (`get_path`)**: What is the optimal, deterministic call path between Symbol A and Symbol B?
|
|
22
|
+
- **Dependency Hubs (`get_dependency_hubs`)**: What are the critical architectural bottlenecks and chokepoints?
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install trishul-mcp
|
|
28
|
+
```
|
|
29
|
+
*(Or install via `pip install llmgraph-in`)*
|
|
30
|
+
|
|
31
|
+
## MCP Client Configuration
|
|
32
|
+
|
|
33
|
+
Add to your `claude_desktop_config.json`, Cursor MCP settings, or Windsurf configuration:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"mcpServers": {
|
|
38
|
+
"trishul": {
|
|
39
|
+
"command": "trishul-mcp",
|
|
40
|
+
"env": {
|
|
41
|
+
"TRISHUL_API_KEY": "llmg_live_...",
|
|
42
|
+
"TRISHUL_API_URL": "https://api.llmgraph.in/v1"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
*Note: `LLMGRAPH_API_KEY` and `LLMGRAPH_API_URL` are also supported for backward compatibility.*
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
Apache-2.0. Developed by Brainfoods Studio.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# trishul-mcp
|
|
2
|
+
|
|
3
|
+
Deterministic, repository-scale semantic code intelligence Model Context Protocol (MCP) server for AI coding agents (Claude Code, Cursor, Windsurf).
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`trishul-mcp` connects AI coding assistants directly to a persistent, topological code intelligence graph. Instead of burning your model's context window re-reading thousands of raw files or guessing caller hierarchies with naive regex, Trishul provides deterministic answers with verified certainty:
|
|
8
|
+
|
|
9
|
+
- **Callers (`get_callers`)**: Who directly or indirectly invokes a symbol?
|
|
10
|
+
- **Blast Radius (`get_blast` / `get_impact`)**: What breaks upstream if this function or interface changes?
|
|
11
|
+
- **Execution Paths (`get_path`)**: What is the optimal, deterministic call path between Symbol A and Symbol B?
|
|
12
|
+
- **Dependency Hubs (`get_dependency_hubs`)**: What are the critical architectural bottlenecks and chokepoints?
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install trishul-mcp
|
|
18
|
+
```
|
|
19
|
+
*(Or install via `pip install llmgraph-in`)*
|
|
20
|
+
|
|
21
|
+
## MCP Client Configuration
|
|
22
|
+
|
|
23
|
+
Add to your `claude_desktop_config.json`, Cursor MCP settings, or Windsurf configuration:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"trishul": {
|
|
29
|
+
"command": "trishul-mcp",
|
|
30
|
+
"env": {
|
|
31
|
+
"TRISHUL_API_KEY": "llmg_live_...",
|
|
32
|
+
"TRISHUL_API_URL": "https://api.llmgraph.in/v1"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
*Note: `LLMGRAPH_API_KEY` and `LLMGRAPH_API_URL` are also supported for backward compatibility.*
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
Apache-2.0. Developed by Brainfoods Studio.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "trishul-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Trishul: Deterministic repository-scale code intelligence Model Context Protocol (MCP) server."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"mcp>=1.0.0",
|
|
13
|
+
"httpx>=0.25.0",
|
|
14
|
+
"pydantic>=2.0.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
trishul-mcp = "trishul_mcp.server:main"
|
|
19
|
+
trishul = "trishul_mcp.server:main"
|
|
20
|
+
llmgraph-in = "trishul_mcp.server:main"
|
|
21
|
+
llmgraph-mcp = "trishul_mcp.server:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Remote HTTP client for communicating with the Trishul / LLMGRAPH hosted API."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from .config import ClientConfig, config
|
|
6
|
+
from .errors import (
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
AuthorizationError,
|
|
9
|
+
TrishulClientError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ResourceNotFoundError,
|
|
12
|
+
ServerError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
class TrishulRemoteClient:
|
|
16
|
+
"""Lightweight HTTP client communicating with the remote code intelligence service."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, client_config: ClientConfig | None = None):
|
|
19
|
+
self.cfg = client_config or config
|
|
20
|
+
self._headers = {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
"User-Agent": "trishul-mcp/0.1.0",
|
|
23
|
+
"X-LLMGraph-Client": "mcp",
|
|
24
|
+
}
|
|
25
|
+
if self.cfg.api_key:
|
|
26
|
+
self._headers["Authorization"] = f"Bearer {self.cfg.api_key}"
|
|
27
|
+
|
|
28
|
+
def _handle_response(self, response: httpx.Response) -> dict[str, Any]:
|
|
29
|
+
if response.status_code == 200:
|
|
30
|
+
return response.json()
|
|
31
|
+
elif response.status_code == 401:
|
|
32
|
+
raise AuthenticationError("AUTH_REQUIRED", "API key (TRISHUL_API_KEY / LLMGRAPH_API_KEY) missing, invalid, or revoked.")
|
|
33
|
+
elif response.status_code == 403:
|
|
34
|
+
raise AuthorizationError("FORBIDDEN", "Access denied for this repository.")
|
|
35
|
+
elif response.status_code == 404:
|
|
36
|
+
raise ResourceNotFoundError("NOT_FOUND", "Requested repository or symbol not found.")
|
|
37
|
+
elif response.status_code == 429:
|
|
38
|
+
err_data = response.json() if response.content else {}
|
|
39
|
+
msg = err_data.get("error", {}).get("message", "Request quota or rate limit exceeded.")
|
|
40
|
+
code = err_data.get("error", {}).get("code", "RATE_LIMITED")
|
|
41
|
+
raise RateLimitError(code, msg)
|
|
42
|
+
else:
|
|
43
|
+
try:
|
|
44
|
+
err_data = response.json()
|
|
45
|
+
msg = err_data.get("error", {}).get("message", f"Remote error ({response.status_code})")
|
|
46
|
+
code = err_data.get("error", {}).get("code", "SERVER_ERROR")
|
|
47
|
+
raise ServerError(code, msg)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
if isinstance(e, ServerError):
|
|
50
|
+
raise
|
|
51
|
+
raise ServerError("SERVER_ERROR", f"Remote error (status {response.status_code})")
|
|
52
|
+
|
|
53
|
+
# --- Repository Lifecycle Operations ---
|
|
54
|
+
|
|
55
|
+
def connect_repository(self, repo_url: str) -> dict[str, Any]:
|
|
56
|
+
"""Connects a repository using locally configured Git credentials without model visibility."""
|
|
57
|
+
url = f"{self.cfg.api_url}/repositories/connect"
|
|
58
|
+
payload = {"repository_url": repo_url, "git_credential": self.cfg.github_token}
|
|
59
|
+
with httpx.Client(timeout=180.0) as client:
|
|
60
|
+
resp = client.post(url, json=payload, headers=self._headers)
|
|
61
|
+
return self._handle_response(resp)
|
|
62
|
+
|
|
63
|
+
def sync_repository(self, repo_id: str) -> dict[str, Any]:
|
|
64
|
+
"""Syncs an existing repository incrementally using local credentials."""
|
|
65
|
+
url = f"{self.cfg.api_url}/repositories/sync"
|
|
66
|
+
payload = {"repository_id": repo_id, "git_credential": self.cfg.github_token}
|
|
67
|
+
with httpx.Client(timeout=180.0) as client:
|
|
68
|
+
resp = client.post(url, json=payload, headers=self._headers)
|
|
69
|
+
return self._handle_response(resp)
|
|
70
|
+
|
|
71
|
+
def repository_status(self, repo_id: str) -> dict[str, Any]:
|
|
72
|
+
"""Checks status of an indexed repository."""
|
|
73
|
+
url = f"{self.cfg.api_url}/repositories/status"
|
|
74
|
+
payload = {"repository_id": repo_id}
|
|
75
|
+
with httpx.Client(timeout=self.cfg.timeout_sec) as client:
|
|
76
|
+
resp = client.post(url, json=payload, headers=self._headers)
|
|
77
|
+
return self._handle_response(resp)
|
|
78
|
+
|
|
79
|
+
def list_repositories(self) -> dict[str, Any]:
|
|
80
|
+
"""Lists all indexed repositories owned by the API key."""
|
|
81
|
+
url = f"{self.cfg.api_url}/repositories/list"
|
|
82
|
+
with httpx.Client(timeout=self.cfg.timeout_sec) as client:
|
|
83
|
+
resp = client.get(url, headers=self._headers)
|
|
84
|
+
return self._handle_response(resp)
|
|
85
|
+
|
|
86
|
+
def delete_repository(self, repo_id: str) -> dict[str, Any]:
|
|
87
|
+
"""Deletes a repository and releases quota."""
|
|
88
|
+
url = f"{self.cfg.api_url}/repositories/delete"
|
|
89
|
+
payload = {"repository_id": repo_id}
|
|
90
|
+
with httpx.Client(timeout=self.cfg.timeout_sec) as client:
|
|
91
|
+
resp = client.post(url, json=payload, headers=self._headers)
|
|
92
|
+
return self._handle_response(resp)
|
|
93
|
+
|
|
94
|
+
# --- Semantic Traversal Queries ---
|
|
95
|
+
|
|
96
|
+
def search_symbols(self, repo_id: str, query: str, limit: int = 10) -> dict[str, Any]:
|
|
97
|
+
url = f"{self.cfg.api_url}/symbols/search"
|
|
98
|
+
payload = {"repository_id": repo_id, "query": query, "limit": limit}
|
|
99
|
+
with httpx.Client(timeout=self.cfg.timeout_sec) as client:
|
|
100
|
+
resp = client.post(url, json=payload, headers=self._headers)
|
|
101
|
+
return self._handle_response(resp)
|
|
102
|
+
|
|
103
|
+
def get_callers(self, repo_id: str, symbol: str, min_confidence: float | None = None) -> dict[str, Any]:
|
|
104
|
+
url = f"{self.cfg.api_url}/callers"
|
|
105
|
+
payload = {"repository_id": repo_id, "symbol": symbol}
|
|
106
|
+
if min_confidence is not None:
|
|
107
|
+
payload["min_confidence"] = min_confidence
|
|
108
|
+
with httpx.Client(timeout=self.cfg.timeout_sec) as client:
|
|
109
|
+
resp = client.post(url, json=payload, headers=self._headers)
|
|
110
|
+
return self._handle_response(resp)
|
|
111
|
+
|
|
112
|
+
def get_impact(self, repo_id: str, symbol: str, depth: int = 3, min_confidence: float | None = None) -> dict[str, Any]:
|
|
113
|
+
url = f"{self.cfg.api_url}/impact"
|
|
114
|
+
payload = {"repository_id": repo_id, "symbol": symbol, "depth": depth}
|
|
115
|
+
if min_confidence is not None:
|
|
116
|
+
payload["min_confidence"] = min_confidence
|
|
117
|
+
with httpx.Client(timeout=self.cfg.timeout_sec) as client:
|
|
118
|
+
resp = client.post(url, json=payload, headers=self._headers)
|
|
119
|
+
return self._handle_response(resp)
|
|
120
|
+
|
|
121
|
+
def get_blast(self, repo_id: str, symbol: str, depth: int = 3, min_confidence: float | None = None) -> dict[str, Any]:
|
|
122
|
+
url = f"{self.cfg.api_url}/blast"
|
|
123
|
+
payload = {"repository_id": repo_id, "symbol": symbol, "depth": depth}
|
|
124
|
+
if min_confidence is not None:
|
|
125
|
+
payload["min_confidence"] = min_confidence
|
|
126
|
+
with httpx.Client(timeout=self.cfg.timeout_sec) as client:
|
|
127
|
+
resp = client.post(url, json=payload, headers=self._headers)
|
|
128
|
+
return self._handle_response(resp)
|
|
129
|
+
|
|
130
|
+
def get_path(self, repo_id: str, source: str, target: str) -> dict[str, Any]:
|
|
131
|
+
url = f"{self.cfg.api_url}/path"
|
|
132
|
+
payload = {"repository_id": repo_id, "source": source, "target": target}
|
|
133
|
+
with httpx.Client(timeout=self.cfg.timeout_sec) as client:
|
|
134
|
+
resp = client.post(url, json=payload, headers=self._headers)
|
|
135
|
+
return self._handle_response(resp)
|
|
136
|
+
|
|
137
|
+
def get_dependency_hubs(self, repo_id: str, source: str, target: str) -> dict[str, Any]:
|
|
138
|
+
url = f"{self.cfg.api_url}/chokepoints"
|
|
139
|
+
payload = {"repository_id": repo_id, "source": source, "target": target}
|
|
140
|
+
with httpx.Client(timeout=self.cfg.timeout_sec) as client:
|
|
141
|
+
resp = client.post(url, json=payload, headers=self._headers)
|
|
142
|
+
return self._handle_response(resp)
|
|
143
|
+
|
|
144
|
+
# Backward-compatible alias
|
|
145
|
+
LLMGraphRemoteClient = TrishulRemoteClient
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Client configuration and environment variable bindings for Trishul / LLMGraph MCP."""
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ClientConfig:
|
|
7
|
+
# Primary: TRISHUL_*, Fallback: LLMGRAPH_*
|
|
8
|
+
api_url: str = os.getenv("TRISHUL_API_URL") or os.getenv("LLMGRAPH_API_URL") or "https://api.llmgraph.in/v1"
|
|
9
|
+
api_key: str | None = os.getenv("TRISHUL_API_KEY") or os.getenv("LLMGRAPH_API_KEY")
|
|
10
|
+
github_token: str | None = os.getenv("TRISHUL_GITHUB_TOKEN") or os.getenv("LLMGRAPH_GITHUB_TOKEN")
|
|
11
|
+
timeout_sec: float = float(os.getenv("TRISHUL_TIMEOUT_SEC") or os.getenv("LLMGRAPH_TIMEOUT_SEC") or "30.0")
|
|
12
|
+
max_retries: int = int(os.getenv("TRISHUL_MAX_RETRIES") or os.getenv("LLMGRAPH_MAX_RETRIES") or "3")
|
|
13
|
+
|
|
14
|
+
config = ClientConfig()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Standard public error types for the remote Trishul / LLMGRAPH MCP client."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
class TrishulClientError(Exception):
|
|
5
|
+
"""Base exception for Trishul MCP client errors."""
|
|
6
|
+
def __init__(self, code: str, message: str, details: dict[str, Any] | None = None):
|
|
7
|
+
super().__init__(message)
|
|
8
|
+
self.code = code
|
|
9
|
+
self.message = message
|
|
10
|
+
self.details = details or {}
|
|
11
|
+
|
|
12
|
+
# Backward-compatible alias
|
|
13
|
+
LLMGraphClientError = TrishulClientError
|
|
14
|
+
|
|
15
|
+
class AuthenticationError(TrishulClientError):
|
|
16
|
+
"""Raised when the API key is missing, invalid, or expired."""
|
|
17
|
+
|
|
18
|
+
class AuthorizationError(TrishulClientError):
|
|
19
|
+
"""Raised when the account lacks permissions for the requested repository."""
|
|
20
|
+
|
|
21
|
+
class ResourceNotFoundError(TrishulClientError):
|
|
22
|
+
"""Raised when a symbol, file, or repository is not found."""
|
|
23
|
+
|
|
24
|
+
class RateLimitError(TrishulClientError):
|
|
25
|
+
"""Raised when the request quota or rate limit is exceeded."""
|
|
26
|
+
|
|
27
|
+
class ServerError(TrishulClientError):
|
|
28
|
+
"""Raised when the remote service encounters an error."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Public request and response schemas for Trishul / LLMGRAPH API endpoints."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
class SymbolMatch(BaseModel):
|
|
6
|
+
symbol_id: str
|
|
7
|
+
name: str
|
|
8
|
+
qualified_name: str
|
|
9
|
+
kind: str
|
|
10
|
+
file_path: str
|
|
11
|
+
line: int
|
|
12
|
+
|
|
13
|
+
class SearchResponse(BaseModel):
|
|
14
|
+
matches: list[SymbolMatch] = Field(default_factory=list)
|
|
15
|
+
did_you_mean: list[str] = Field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
class AffectedCaller(BaseModel):
|
|
18
|
+
symbol_id: str
|
|
19
|
+
qualified_name: str
|
|
20
|
+
file_path: str
|
|
21
|
+
line: int
|
|
22
|
+
distance: int
|
|
23
|
+
path_confidence: float
|
|
24
|
+
confidence_tier: str
|
|
25
|
+
call_path: str
|
|
26
|
+
|
|
27
|
+
class BlastResponse(BaseModel):
|
|
28
|
+
root_symbol: SymbolMatch
|
|
29
|
+
total_affected_count: int
|
|
30
|
+
affected_callers: list[AffectedCaller] = Field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
class PathNode(BaseModel):
|
|
33
|
+
qualified_name: str
|
|
34
|
+
file_path: str
|
|
35
|
+
line: int
|
|
36
|
+
|
|
37
|
+
class PathResponse(BaseModel):
|
|
38
|
+
source: str
|
|
39
|
+
target: str
|
|
40
|
+
hops: int
|
|
41
|
+
cumulative_confidence: float
|
|
42
|
+
weakest_link_tier: str
|
|
43
|
+
path_nodes: list[PathNode] = Field(default_factory=list)
|
|
44
|
+
path_display: str
|
|
45
|
+
|
|
46
|
+
class ChokepointSymbol(BaseModel):
|
|
47
|
+
symbol_id: str
|
|
48
|
+
qualified_name: str
|
|
49
|
+
file_path: str
|
|
50
|
+
line: int
|
|
51
|
+
is_mandatory_bottleneck: bool = True
|
|
52
|
+
|
|
53
|
+
class ChokepointsResponse(BaseModel):
|
|
54
|
+
source: str
|
|
55
|
+
target: str
|
|
56
|
+
chokepoints: list[ChokepointSymbol] = Field(default_factory=list)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""FastMCP server entry point for Trishul MCP server."""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
5
|
+
try:
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
except (ImportError, ModuleNotFoundError):
|
|
8
|
+
from mcp.server.mcpserver import MCPServer as FastMCP
|
|
9
|
+
|
|
10
|
+
from .client import TrishulRemoteClient
|
|
11
|
+
from .config import config
|
|
12
|
+
from .errors import TrishulClientError
|
|
13
|
+
|
|
14
|
+
mcp = FastMCP("trishul")
|
|
15
|
+
remote_client = TrishulRemoteClient(config)
|
|
16
|
+
|
|
17
|
+
def _safe_call(fn, *args, **kwargs) -> str:
|
|
18
|
+
try:
|
|
19
|
+
res = fn(*args, **kwargs)
|
|
20
|
+
return json.dumps(res, indent=2)
|
|
21
|
+
except TrishulClientError as e:
|
|
22
|
+
return json.dumps({"status": "error", "error": {"code": e.code, "message": e.message, "details": e.details}}, indent=2)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
return json.dumps({"status": "error", "error": {"code": "CLIENT_ERROR", "message": "Failed to communicate with Trishul code intelligence service."}}, indent=2)
|
|
25
|
+
|
|
26
|
+
# --- Repository Lifecycle Tools ---
|
|
27
|
+
|
|
28
|
+
@mcp.tool()
|
|
29
|
+
def connect_repository(repository_url: str) -> str:
|
|
30
|
+
"""Connect and index a GitHub repository using hosted static code intelligence.
|
|
31
|
+
Credentials (if any) are loaded automatically from the local MCP environment.
|
|
32
|
+
Returns an opaque repository_id to use for all subsequent graph queries."""
|
|
33
|
+
return _safe_call(remote_client.connect_repository, repo_url=repository_url)
|
|
34
|
+
|
|
35
|
+
@mcp.tool()
|
|
36
|
+
def sync_repository(repository_id: str) -> str:
|
|
37
|
+
"""Incrementally re-sync an existing repository to ingest latest code changes.
|
|
38
|
+
Maintains stable symbol identities and updates call relationships."""
|
|
39
|
+
return _safe_call(remote_client.sync_repository, repo_id=repository_id)
|
|
40
|
+
|
|
41
|
+
@mcp.tool()
|
|
42
|
+
def repository_status(repository_id: str) -> str:
|
|
43
|
+
"""Check the status, symbol count, edge count, and last sync timestamp for a repository."""
|
|
44
|
+
return _safe_call(remote_client.repository_status, repo_id=repository_id)
|
|
45
|
+
|
|
46
|
+
@mcp.tool()
|
|
47
|
+
def list_repositories() -> str:
|
|
48
|
+
"""List all indexed repositories owned by your API key with their status and symbol counts."""
|
|
49
|
+
return _safe_call(remote_client.list_repositories)
|
|
50
|
+
|
|
51
|
+
@mcp.tool()
|
|
52
|
+
def delete_repository(repository_id: str) -> str:
|
|
53
|
+
"""Delete an indexed repository graph and release repository quota."""
|
|
54
|
+
return _safe_call(remote_client.delete_repository, repo_id=repository_id)
|
|
55
|
+
|
|
56
|
+
# --- Semantic Traversal Query Tools ---
|
|
57
|
+
|
|
58
|
+
@mcp.tool()
|
|
59
|
+
def search_symbols(repository_id: str, query: str, limit: int = 10) -> str:
|
|
60
|
+
"""Search for code symbols across the indexed repository graph by name or qualified identifier."""
|
|
61
|
+
return _safe_call(remote_client.search_symbols, repo_id=repository_id, query=query, limit=limit)
|
|
62
|
+
|
|
63
|
+
@mcp.tool()
|
|
64
|
+
def get_callers(repository_id: str, symbol: str, min_confidence: float | None = None) -> str:
|
|
65
|
+
"""Get direct callers of a specific symbol with certainty tiers and relationship classifications."""
|
|
66
|
+
return _safe_call(remote_client.get_callers, repo_id=repository_id, symbol=symbol, min_confidence=min_confidence)
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
def get_impact(repository_id: str, symbol: str, depth: int = 3, min_confidence: float | None = None) -> str:
|
|
70
|
+
"""Compute the reverse change impact of all upstream callers affected if a target symbol changes."""
|
|
71
|
+
return _safe_call(remote_client.get_impact, repo_id=repository_id, symbol=symbol, depth=depth, min_confidence=min_confidence)
|
|
72
|
+
|
|
73
|
+
@mcp.tool()
|
|
74
|
+
def get_blast(repository_id: str, symbol: str, depth: int = 3, min_confidence: float | None = None) -> str:
|
|
75
|
+
"""Compute the reverse blast radius of all upstream callers affected if a target symbol changes."""
|
|
76
|
+
return _safe_call(remote_client.get_blast, repo_id=repository_id, symbol=symbol, depth=depth, min_confidence=min_confidence)
|
|
77
|
+
|
|
78
|
+
@mcp.tool()
|
|
79
|
+
def get_path(repository_id: str, source: str, target: str) -> str:
|
|
80
|
+
"""Find the single optimal execution path between two symbols using weighted certainty routing."""
|
|
81
|
+
return _safe_call(remote_client.get_path, repo_id=repository_id, source=source, target=target)
|
|
82
|
+
|
|
83
|
+
@mcp.tool()
|
|
84
|
+
def get_dependency_hubs(repository_id: str, source: str, target: str) -> str:
|
|
85
|
+
"""Find critical dependency junction symbols between source and target."""
|
|
86
|
+
return _safe_call(remote_client.get_dependency_hubs, repo_id=repository_id, source=source, target=target)
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
transport = "stdio"
|
|
90
|
+
if "--transport" in sys.argv:
|
|
91
|
+
idx = sys.argv.index("--transport")
|
|
92
|
+
if idx + 1 < len(sys.argv):
|
|
93
|
+
transport = sys.argv[idx + 1]
|
|
94
|
+
mcp.run(transport=transport)
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trishul-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Trishul: Deterministic repository-scale code intelligence Model Context Protocol (MCP) server.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: mcp>=1.0.0
|
|
8
|
+
Requires-Dist: httpx>=0.25.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0.0
|
|
10
|
+
|
|
11
|
+
# trishul-mcp
|
|
12
|
+
|
|
13
|
+
Deterministic, repository-scale semantic code intelligence Model Context Protocol (MCP) server for AI coding agents (Claude Code, Cursor, Windsurf).
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
`trishul-mcp` connects AI coding assistants directly to a persistent, topological code intelligence graph. Instead of burning your model's context window re-reading thousands of raw files or guessing caller hierarchies with naive regex, Trishul provides deterministic answers with verified certainty:
|
|
18
|
+
|
|
19
|
+
- **Callers (`get_callers`)**: Who directly or indirectly invokes a symbol?
|
|
20
|
+
- **Blast Radius (`get_blast` / `get_impact`)**: What breaks upstream if this function or interface changes?
|
|
21
|
+
- **Execution Paths (`get_path`)**: What is the optimal, deterministic call path between Symbol A and Symbol B?
|
|
22
|
+
- **Dependency Hubs (`get_dependency_hubs`)**: What are the critical architectural bottlenecks and chokepoints?
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install trishul-mcp
|
|
28
|
+
```
|
|
29
|
+
*(Or install via `pip install llmgraph-in`)*
|
|
30
|
+
|
|
31
|
+
## MCP Client Configuration
|
|
32
|
+
|
|
33
|
+
Add to your `claude_desktop_config.json`, Cursor MCP settings, or Windsurf configuration:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"mcpServers": {
|
|
38
|
+
"trishul": {
|
|
39
|
+
"command": "trishul-mcp",
|
|
40
|
+
"env": {
|
|
41
|
+
"TRISHUL_API_KEY": "llmg_live_...",
|
|
42
|
+
"TRISHUL_API_URL": "https://api.llmgraph.in/v1"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
*Note: `LLMGRAPH_API_KEY` and `LLMGRAPH_API_URL` are also supported for backward compatibility.*
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
Apache-2.0. Developed by Brainfoods Studio.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/llmgraph_mcp/__init__.py
|
|
4
|
+
src/llmgraph_mcp/client.py
|
|
5
|
+
src/llmgraph_mcp/config.py
|
|
6
|
+
src/llmgraph_mcp/errors.py
|
|
7
|
+
src/llmgraph_mcp/schemas.py
|
|
8
|
+
src/llmgraph_mcp/server.py
|
|
9
|
+
src/trishul_mcp/__init__.py
|
|
10
|
+
src/trishul_mcp/client.py
|
|
11
|
+
src/trishul_mcp/config.py
|
|
12
|
+
src/trishul_mcp/errors.py
|
|
13
|
+
src/trishul_mcp/schemas.py
|
|
14
|
+
src/trishul_mcp/server.py
|
|
15
|
+
src/trishul_mcp.egg-info/PKG-INFO
|
|
16
|
+
src/trishul_mcp.egg-info/SOURCES.txt
|
|
17
|
+
src/trishul_mcp.egg-info/dependency_links.txt
|
|
18
|
+
src/trishul_mcp.egg-info/entry_points.txt
|
|
19
|
+
src/trishul_mcp.egg-info/requires.txt
|
|
20
|
+
src/trishul_mcp.egg-info/top_level.txt
|
|
21
|
+
tests/test_package_boundary.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Crown-jewel leakage and package boundary verification tests for llmgraph-mcp."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
6
|
+
|
|
7
|
+
PROHIBITED_TERMS = [
|
|
8
|
+
"CallResolver",
|
|
9
|
+
"ResolutionEvidence",
|
|
10
|
+
"SELF_METHOD_UNIQUE",
|
|
11
|
+
"SUPER_MRO_EXACT",
|
|
12
|
+
"POLYMORPHIC_RECEIVER_SET",
|
|
13
|
+
"STATIC_RECEIVER_TYPE_EXACT",
|
|
14
|
+
"blast_symbol_id",
|
|
15
|
+
"find_shortest_path",
|
|
16
|
+
"find_top_k_paths",
|
|
17
|
+
"find_chokepoints",
|
|
18
|
+
"graph.py",
|
|
19
|
+
"docs/private",
|
|
20
|
+
"validation/",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
def test_package_contains_no_crown_jewel_terms():
|
|
24
|
+
"""Verifies that no file in the distributable client package references proprietary engine internals."""
|
|
25
|
+
leaks = []
|
|
26
|
+
scanned_files = 0
|
|
27
|
+
|
|
28
|
+
for path in REPO_ROOT.rglob("*"):
|
|
29
|
+
if path.is_file() and not any(p in path.parts for p in [".git", ".pytest_cache", "__pycache__", ".venv"]):
|
|
30
|
+
scanned_files += 1
|
|
31
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
32
|
+
# Exclude this test file itself from term search
|
|
33
|
+
if path.name == "test_package_boundary.py":
|
|
34
|
+
continue
|
|
35
|
+
for term in PROHIBITED_TERMS:
|
|
36
|
+
if term in text:
|
|
37
|
+
leaks.append(f"Leakage detected in {path.relative_to(REPO_ROOT)}: contains '{term}'")
|
|
38
|
+
|
|
39
|
+
assert scanned_files > 0, "No files scanned."
|
|
40
|
+
assert not leaks, f"Crown-jewel leakage detected:\n" + "\n".join(leaks)
|
|
41
|
+
|
|
42
|
+
def test_pyproject_contains_no_internal_dependencies():
|
|
43
|
+
"""Verifies that pyproject.toml does not depend on internal proprietary packages or Oroboros."""
|
|
44
|
+
pyproject = REPO_ROOT / "pyproject.toml"
|
|
45
|
+
assert pyproject.exists()
|
|
46
|
+
content = pyproject.read_text(encoding="utf-8")
|
|
47
|
+
assert "oroboros" not in content.lower(), "pyproject.toml directly references Oroboros repository."
|
|
48
|
+
assert "graph" not in content.lower() or "llmgraph" in content.lower(), "Unsanitized internal dependency in pyproject.toml"
|
|
49
|
+
|
|
50
|
+
def test_client_imports_are_pure_http():
|
|
51
|
+
"""Verifies that client files only import safe standard HTTP libraries."""
|
|
52
|
+
import sys
|
|
53
|
+
sys.path.insert(0, str(REPO_ROOT / "src"))
|
|
54
|
+
from trishul_mcp import client as t_client, server as t_server
|
|
55
|
+
from llmgraph_mcp import client, config, errors, schemas, server
|
|
56
|
+
assert hasattr(t_server, "connect_repository")
|
|
57
|
+
assert hasattr(t_server, "search_symbols")
|
|
58
|
+
assert hasattr(t_server, "get_callers")
|
|
59
|
+
assert hasattr(t_server, "get_impact")
|
|
60
|
+
assert hasattr(t_server, "get_blast")
|
|
61
|
+
assert hasattr(t_server, "get_path")
|
|
62
|
+
assert hasattr(t_server, "get_dependency_hubs")
|
|
63
|
+
assert hasattr(t_client, "TrishulRemoteClient")
|
|
64
|
+
assert hasattr(server, "connect_repository")
|
|
65
|
+
assert hasattr(server, "search_symbols")
|
|
66
|
+
assert hasattr(server, "get_callers")
|
|
67
|
+
assert hasattr(server, "get_impact")
|
|
68
|
+
assert hasattr(server, "get_blast")
|
|
69
|
+
assert hasattr(server, "get_path")
|
|
70
|
+
assert hasattr(server, "get_dependency_hubs")
|
|
71
|
+
assert hasattr(client, "LLMGraphRemoteClient")
|
|
72
|
+
assert hasattr(client.LLMGraphRemoteClient, "get_impact")
|
|
73
|
+
assert hasattr(client.LLMGraphRemoteClient, "get_dependency_hubs")
|
|
74
|
+
assert hasattr(client.LLMGraphRemoteClient, "get_blast")
|