source-context-mcp 0.1.dev1__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.
- source_context_mcp-0.1.dev1/PKG-INFO +17 -0
- source_context_mcp-0.1.dev1/README.md +1 -0
- source_context_mcp-0.1.dev1/pyproject.toml +48 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/__init__.py +5 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/__main__.py +4 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/cli.py +49 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/core/__init__.py +9 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/core/api_client.py +153 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/core/context.py +10 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/core/dependencies.py +13 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/core/settings.py +91 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/py.typed +0 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/server.py +51 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/tools/__init__.py +19 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/tools/base.py +41 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/tools/debug.py +11 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/tools/graph.py +38 -0
- source_context_mcp-0.1.dev1/src/source_context_mcp/tools/vector.py +37 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: source-context-mcp
|
|
3
|
+
Version: 0.1.dev1
|
|
4
|
+
Summary: Local Model Context Protocol (MCP) server for Source Context code intelligence.
|
|
5
|
+
Author: Le Ngoc Tu, Dang Huu Hieu, Huynh Phat Tai
|
|
6
|
+
Author-email: Le Ngoc Tu <tuleaibk@gmail.com>, Dang Huu Hieu <danghuuhieu038@gmail.com>, Huynh Phat Tai <huynhphtai@gmail.com>
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: mcp[cli]>=2.0.0
|
|
9
|
+
Requires-Dist: pydantic>=2.13.4
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.14.2
|
|
11
|
+
Requires-Dist: tomli-w>=1.2.0
|
|
12
|
+
Requires-Dist: typer>=0.27.0
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Project-URL: Repository, https://github.com/TuLe142857/source-context
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Readme for MCP Server
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Readme for MCP Server
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "source-context-mcp"
|
|
3
|
+
version = "0.1.dev1"
|
|
4
|
+
description = "Local Model Context Protocol (MCP) server for Source Context code intelligence."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Le Ngoc Tu", email = "tuleaibk@gmail.com" },
|
|
8
|
+
{ name = "Dang Huu Hieu", email = "danghuuhieu038@gmail.com" },
|
|
9
|
+
{ name = "Huynh Phat Tai", email = "huynhphtai@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.28.1",
|
|
14
|
+
"mcp[cli]>=2.0.0",
|
|
15
|
+
"pydantic>=2.13.4",
|
|
16
|
+
"pydantic-settings>=2.14.2",
|
|
17
|
+
"tomli-w>=1.2.0",
|
|
18
|
+
"typer>=0.27.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/TuLe142857/source-context"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
source-context-mcp = "source_context_mcp:run"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["uv_build>=0.11.21,<0.12.0"]
|
|
31
|
+
build-backend = "uv_build"
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
extend = "../pyproject.toml"
|
|
35
|
+
|
|
36
|
+
[tool.mypy]
|
|
37
|
+
python_version = "3.12"
|
|
38
|
+
explicit_package_bases = true
|
|
39
|
+
mypy_path = ["src"]
|
|
40
|
+
check_untyped_defs = true
|
|
41
|
+
disallow_untyped_defs = true
|
|
42
|
+
disallow_incomplete_defs = true
|
|
43
|
+
no_implicit_optional = true
|
|
44
|
+
warn_return_any = true
|
|
45
|
+
warn_unused_ignores = true
|
|
46
|
+
warn_redundant_casts = true
|
|
47
|
+
warn_unreachable = true
|
|
48
|
+
show_error_codes = true
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from .core.settings import CONFIG_FILE, get_settings, get_settings_with_overrides, write_settings
|
|
4
|
+
from .server import create_server
|
|
5
|
+
|
|
6
|
+
cli = typer.Typer()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@cli.command(name="run", help="Run the MCP server")
|
|
10
|
+
def run(
|
|
11
|
+
server_url_override: str | None = typer.Option(
|
|
12
|
+
None, "--server-url", help="Server url to override for this session"
|
|
13
|
+
),
|
|
14
|
+
pat_override: str | None = typer.Option(None, "--token", help="Token to override for this session"),
|
|
15
|
+
workspace_id: int | None = typer.Option(
|
|
16
|
+
None, "--workspace-id", help="Default workspace id to override for this session"
|
|
17
|
+
),
|
|
18
|
+
):
|
|
19
|
+
server = create_server(
|
|
20
|
+
base_url_override=server_url_override,
|
|
21
|
+
pat_override=pat_override,
|
|
22
|
+
workspace_id_override=workspace_id,
|
|
23
|
+
)
|
|
24
|
+
server.run("stdio")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cli.command(name="config", help="Configure the MCP server")
|
|
28
|
+
def config(
|
|
29
|
+
server_url: str | None = typer.Option(None, "--server-url", help="Server URL"),
|
|
30
|
+
token: str | None = typer.Option(None, "--token", help="Personal Access Token"),
|
|
31
|
+
default_workspace_id: int | None = typer.Option(None, "--workspace-id", help="Default workspace id"),
|
|
32
|
+
):
|
|
33
|
+
settings = get_settings_with_overrides(
|
|
34
|
+
server_url_override=server_url,
|
|
35
|
+
pat_override=token,
|
|
36
|
+
default_workspace_id_override=default_workspace_id,
|
|
37
|
+
)
|
|
38
|
+
write_settings(settings)
|
|
39
|
+
typer.echo(f"Config saved to {CONFIG_FILE}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@cli.command(name="show-config", help="Show the MCP server's configuration")
|
|
43
|
+
def show_config():
|
|
44
|
+
settings = get_settings()
|
|
45
|
+
settings_json = settings.model_dump_json(indent=2)
|
|
46
|
+
|
|
47
|
+
typer.echo(f"Configuration file: {CONFIG_FILE}")
|
|
48
|
+
typer.echo("Note: Secret value will be display as ***")
|
|
49
|
+
typer.echo(settings_json)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .api_client import ApiClient as ApiClient
|
|
2
|
+
from .api_client import ApiResponse as ApiResponse
|
|
3
|
+
from .context import AppContext as AppContext
|
|
4
|
+
from .dependencies import ApiClientDep as ApiClientDep
|
|
5
|
+
from .settings import CONFIG_FILE as CONFIG_FILE
|
|
6
|
+
from .settings import Settings as Settings
|
|
7
|
+
from .settings import get_settings as get_settings
|
|
8
|
+
from .settings import get_settings_with_overrides as get_settings_with_overrides
|
|
9
|
+
from .settings import write_settings as write_settings
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Literal, overload
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from pydantic import TypeAdapter, ValidationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ApiResponse[T]:
|
|
12
|
+
success: bool
|
|
13
|
+
status_code: int
|
|
14
|
+
data: T | None
|
|
15
|
+
error_code: str | None
|
|
16
|
+
message: str | None
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def ok(data: T | None, message: str | None = None, status_code: int = 200) -> ApiResponse[T]:
|
|
20
|
+
return ApiResponse(
|
|
21
|
+
success=True,
|
|
22
|
+
status_code=status_code,
|
|
23
|
+
data=data,
|
|
24
|
+
message=message,
|
|
25
|
+
error_code=None,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def error(status_code: int, error_code: str, message: str | None = None) -> ApiResponse:
|
|
30
|
+
return ApiResponse(
|
|
31
|
+
success=False,
|
|
32
|
+
data=None,
|
|
33
|
+
status_code=status_code,
|
|
34
|
+
error_code=error_code,
|
|
35
|
+
message=message,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ApiClient:
|
|
40
|
+
def __init__(self, base_url: str, token: str, max_retry: int = 1):
|
|
41
|
+
"""
|
|
42
|
+
Args:
|
|
43
|
+
base_url: server url
|
|
44
|
+
token: personal access token
|
|
45
|
+
"""
|
|
46
|
+
self.server_url = base_url
|
|
47
|
+
self.token = token
|
|
48
|
+
self.max_retry = max_retry
|
|
49
|
+
self.async_client = httpx.AsyncClient(
|
|
50
|
+
base_url=self.server_url,
|
|
51
|
+
headers={
|
|
52
|
+
"Authorization": f"Bearer {self.token}",
|
|
53
|
+
"Accept": "application/json",
|
|
54
|
+
}
|
|
55
|
+
if len(token) > 0
|
|
56
|
+
else None,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
async def close(self):
|
|
60
|
+
if self.async_client is not None:
|
|
61
|
+
await self.async_client.aclose()
|
|
62
|
+
|
|
63
|
+
def _parse_data(self, data: Any, response_model: type) -> Any:
|
|
64
|
+
"""
|
|
65
|
+
Raises:
|
|
66
|
+
ValidationError
|
|
67
|
+
"""
|
|
68
|
+
adapter = TypeAdapter(response_model)
|
|
69
|
+
return adapter.validate_python(data)
|
|
70
|
+
|
|
71
|
+
async def _request(
|
|
72
|
+
self,
|
|
73
|
+
method: Literal["GET", "PUT", "POST", "PATCH", "DELETE"],
|
|
74
|
+
path: str,
|
|
75
|
+
response_model: type | None = None,
|
|
76
|
+
**kwargs,
|
|
77
|
+
) -> ApiResponse[Any]:
|
|
78
|
+
for attempt in range(self.max_retry):
|
|
79
|
+
try:
|
|
80
|
+
resp = await self.async_client.request(method, path, **kwargs)
|
|
81
|
+
resp.raise_for_status()
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
raw_data = resp.json()
|
|
85
|
+
if isinstance(raw_data, dict) and "data" in raw_data:
|
|
86
|
+
raw_data = raw_data["data"]
|
|
87
|
+
except ValueError:
|
|
88
|
+
raw_data = resp.text
|
|
89
|
+
|
|
90
|
+
if response_model is not None:
|
|
91
|
+
try:
|
|
92
|
+
parsed_data = self._parse_data(raw_data, response_model)
|
|
93
|
+
return ApiResponse.ok(data=parsed_data)
|
|
94
|
+
except ValidationError as e:
|
|
95
|
+
return ApiResponse.error(0, str(e))
|
|
96
|
+
else:
|
|
97
|
+
return ApiResponse.ok(data=raw_data)
|
|
98
|
+
|
|
99
|
+
except httpx.TimeoutException:
|
|
100
|
+
if attempt < self.max_retry - 1:
|
|
101
|
+
continue
|
|
102
|
+
return ApiResponse.error(
|
|
103
|
+
status_code=0,
|
|
104
|
+
error_code="REQUEST_TIMEOUT",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
except httpx.ConnectError:
|
|
108
|
+
if attempt < self.max_retry - 1:
|
|
109
|
+
continue
|
|
110
|
+
return ApiResponse.error(
|
|
111
|
+
status_code=0,
|
|
112
|
+
error_code="CONNECTION_ERROR",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
except httpx.HTTPError as exc:
|
|
116
|
+
return ApiResponse.error(0, str(exc))
|
|
117
|
+
|
|
118
|
+
return ApiResponse.error(0, "UNKNOW_ERROR", f"Request failed after {self.max_retry} attempts")
|
|
119
|
+
|
|
120
|
+
@overload
|
|
121
|
+
async def get(self, path: str, params: dict | None = None) -> ApiResponse[Any]: ...
|
|
122
|
+
@overload
|
|
123
|
+
async def get[M](self, path: str, params: dict | None = None, *, response_model: type[M]) -> ApiResponse[M]: ...
|
|
124
|
+
async def get[M](self, path: str, params: dict | None = None, *, response_model: type[M] | None = None):
|
|
125
|
+
return await self._request("GET", path, response_model=response_model, params=params)
|
|
126
|
+
|
|
127
|
+
@overload
|
|
128
|
+
async def post(self, path: str, json: dict | None = None) -> ApiResponse[Any]: ...
|
|
129
|
+
@overload
|
|
130
|
+
async def post[M](self, path: str, json: dict | None = None, *, response_model: type[M]) -> ApiResponse[M]: ...
|
|
131
|
+
async def post[M](self, path: str, json: dict | None = None, *, response_model: type[M] | None = None):
|
|
132
|
+
return await self._request("POST", path, response_model=response_model, json=json)
|
|
133
|
+
|
|
134
|
+
@overload
|
|
135
|
+
async def put(self, path: str, json: dict | None = None) -> ApiResponse[Any]: ...
|
|
136
|
+
@overload
|
|
137
|
+
async def put[M](self, path: str, json: dict | None = None, *, response_model: type[M]) -> ApiResponse[M]: ...
|
|
138
|
+
async def put[M](self, path: str, json: dict | None = None, *, response_model: type[M] | None = None):
|
|
139
|
+
return await self._request("PUT", path, response_model=response_model, json=json)
|
|
140
|
+
|
|
141
|
+
@overload
|
|
142
|
+
async def patch(self, path: str, json: dict | None = None) -> ApiResponse[Any]: ...
|
|
143
|
+
@overload
|
|
144
|
+
async def patch[M](self, path: str, json: dict | None = None, *, response_model: type[M]) -> ApiResponse[M]: ...
|
|
145
|
+
async def patch[M](self, path: str, json: dict | None = None, *, response_model: type[M] | None = None):
|
|
146
|
+
return await self._request("PATCH", path, response_model=response_model, json=json)
|
|
147
|
+
|
|
148
|
+
@overload
|
|
149
|
+
async def delete(self, path: str) -> ApiResponse[Any]: ...
|
|
150
|
+
@overload
|
|
151
|
+
async def delete[M](self, path: str, *, response_model: type[M]) -> ApiResponse[M]: ...
|
|
152
|
+
async def delete[M](self, path: str, *, response_model: type[M] | None = None):
|
|
153
|
+
return await self._request("DELETE", path, response_model=response_model)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from mcp.server.mcpserver import Context, Resolve
|
|
4
|
+
|
|
5
|
+
from source_context_mcp.core import ApiClient, AppContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_api_client(ctx: Context[AppContext]) -> ApiClient:
|
|
9
|
+
app_context: AppContext = ctx.request_context.lifespan_context
|
|
10
|
+
return app_context.api_client
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ApiClientDep = Annotated[ApiClient, Resolve(get_api_client)]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import tomli_w
|
|
5
|
+
from pydantic import SecretStr
|
|
6
|
+
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource
|
|
7
|
+
|
|
8
|
+
CONFIG_DIR = Path.home() / ".source_context_mcp"
|
|
9
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Settings(BaseSettings):
|
|
13
|
+
model_config = SettingsConfigDict(
|
|
14
|
+
toml_file=str(CONFIG_FILE),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
SERVER_URL: str = "default"
|
|
18
|
+
"""Server URL. Include prefix. Example: http://localhost:8000/mcp/v1"""
|
|
19
|
+
|
|
20
|
+
PAT: SecretStr = SecretStr("Default")
|
|
21
|
+
"""Personal access token"""
|
|
22
|
+
|
|
23
|
+
DEFAULT_WORKSPACE_ID: int = 1
|
|
24
|
+
"""Default Workspace ID"""
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def settings_customise_sources(
|
|
28
|
+
cls,
|
|
29
|
+
settings_cls: type[BaseSettings],
|
|
30
|
+
init_settings: PydanticBaseSettingsSource,
|
|
31
|
+
env_settings: PydanticBaseSettingsSource,
|
|
32
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
33
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
34
|
+
):
|
|
35
|
+
return (
|
|
36
|
+
init_settings,
|
|
37
|
+
TomlConfigSettingsSource(settings_cls),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@lru_cache
|
|
42
|
+
def get_settings() -> Settings:
|
|
43
|
+
"""
|
|
44
|
+
Get default settings.
|
|
45
|
+
- See ``get_settings_with_overrides`` to see how to get settings with overrides values.
|
|
46
|
+
Returns:
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
return Settings()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_settings_with_overrides(
|
|
53
|
+
server_url_override: str | None = None,
|
|
54
|
+
pat_override: SecretStr | str | None = None,
|
|
55
|
+
default_workspace_id_override: int | None = None,
|
|
56
|
+
) -> Settings:
|
|
57
|
+
"""
|
|
58
|
+
Get settings with overrides values. Any None value will be ignored.
|
|
59
|
+
- This method does not change value of ``get_settings``
|
|
60
|
+
- This method does not change default settings in config file.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
fields_overrides = {}
|
|
64
|
+
if server_url_override is not None:
|
|
65
|
+
fields_overrides["SERVER_URL"] = server_url_override
|
|
66
|
+
if pat_override is not None:
|
|
67
|
+
fields_overrides["PAT"] = pat_override if isinstance(pat_override, SecretStr) else SecretStr(pat_override)
|
|
68
|
+
if default_workspace_id_override is not None:
|
|
69
|
+
fields_overrides["DEFAULT_WORKSPACE_ID"] = default_workspace_id_override
|
|
70
|
+
|
|
71
|
+
# Any unprovided fields will be read from config file
|
|
72
|
+
return Settings(**fields_overrides)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def write_settings(settings: Settings):
|
|
76
|
+
"""
|
|
77
|
+
Write settings to config file. Reset cache of ``get_settings()``
|
|
78
|
+
"""
|
|
79
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
data = {
|
|
82
|
+
"SERVER_URL": settings.SERVER_URL,
|
|
83
|
+
"PAT": settings.PAT.get_secret_value(),
|
|
84
|
+
"DEFAULT_WORKSPACE_ID": settings.DEFAULT_WORKSPACE_ID,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
with CONFIG_FILE.open("wb") as f:
|
|
88
|
+
tomli_w.dump(data, f)
|
|
89
|
+
|
|
90
|
+
# clear old cache
|
|
91
|
+
get_settings.cache_clear()
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
|
|
4
|
+
from mcp.server import MCPServer
|
|
5
|
+
|
|
6
|
+
from source_context_mcp.core import ApiClient, AppContext, get_settings_with_overrides
|
|
7
|
+
from source_context_mcp.tools import register_tools
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_life_span(
|
|
11
|
+
base_url_override: str | None = None, pat_override: str | None = None, workspace_id_override: int | None = None
|
|
12
|
+
):
|
|
13
|
+
@asynccontextmanager
|
|
14
|
+
async def server_lifespan(app: MCPServer) -> AsyncIterator[AppContext]:
|
|
15
|
+
settings = get_settings_with_overrides(
|
|
16
|
+
server_url_override=base_url_override,
|
|
17
|
+
pat_override=pat_override,
|
|
18
|
+
default_workspace_id_override=workspace_id_override,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
yield AppContext(
|
|
22
|
+
api_client=ApiClient(base_url=settings.SERVER_URL, token=settings.PAT.get_secret_value()), settings=settings
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return server_lifespan
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_server(
|
|
29
|
+
base_url_override: str | None = None, pat_override: str | None = None, workspace_id_override: int | None = None
|
|
30
|
+
) -> MCPServer:
|
|
31
|
+
"""
|
|
32
|
+
Create an MCPServer instance.
|
|
33
|
+
Returns:
|
|
34
|
+
MCPServer instance.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
life_span = make_life_span(
|
|
38
|
+
base_url_override=base_url_override, pat_override=pat_override, workspace_id_override=workspace_id_override
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
server = MCPServer(
|
|
42
|
+
name="SourceContextMCP",
|
|
43
|
+
lifespan=life_span,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
register_tools(server)
|
|
47
|
+
|
|
48
|
+
return server
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
mcp = create_server()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from mcp.server import MCPServer
|
|
2
|
+
|
|
3
|
+
from .base import register_tools as register_base_tools
|
|
4
|
+
from .debug import register_tools as register_debug_tools
|
|
5
|
+
from .graph import register_tools as register_graph_tools
|
|
6
|
+
from .vector import register_tools as register_vector_tools
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_tools(mcp: MCPServer) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Registry tools for MCPServer.
|
|
12
|
+
Args:
|
|
13
|
+
mcp: instance of MCPServer
|
|
14
|
+
"""
|
|
15
|
+
register_debug_tools(mcp)
|
|
16
|
+
|
|
17
|
+
register_base_tools(mcp)
|
|
18
|
+
register_graph_tools(mcp)
|
|
19
|
+
register_vector_tools(mcp)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from mcp.server import MCPServer
|
|
4
|
+
|
|
5
|
+
from source_context_mcp.core import ApiClientDep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_tools(mcp: MCPServer) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Register bases tools:
|
|
11
|
+
- list user's available workspaces
|
|
12
|
+
- list repos in workspace
|
|
13
|
+
- list branches in repo
|
|
14
|
+
- list projects in branch
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
mcp: MCPServer instance
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@mcp.tool(description="List all available workspaces that current user can access")
|
|
21
|
+
async def list_workspaces(client: ApiClientDep) -> Any:
|
|
22
|
+
res = await client.get("/general/workspaces", response_model=list)
|
|
23
|
+
return res.data
|
|
24
|
+
|
|
25
|
+
@mcp.tool(description="List all available repositories in workspace")
|
|
26
|
+
async def list_repositories(
|
|
27
|
+
client: ApiClientDep,
|
|
28
|
+
workspace_id: int,
|
|
29
|
+
) -> Any:
|
|
30
|
+
res = await client.post("/general/repositories", {"workspace_id": workspace_id})
|
|
31
|
+
return res.data
|
|
32
|
+
|
|
33
|
+
@mcp.tool(description="List all available branches in specified repository.")
|
|
34
|
+
async def list_branches(client: ApiClientDep, project_id: int) -> Any:
|
|
35
|
+
"""This tool is not implemented yet"""
|
|
36
|
+
return ["This tool is not implemented yet"]
|
|
37
|
+
|
|
38
|
+
@mcp.tool(description="List all available projects in specific branch.")
|
|
39
|
+
def list_projects(client: ApiClientDep, branch_id: int) -> Any:
|
|
40
|
+
"""This tool is not implemented yet"""
|
|
41
|
+
return ["This tool is not implemented yet"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from mcp.server import MCPServer
|
|
2
|
+
from mcp.server.mcpserver import Context
|
|
3
|
+
|
|
4
|
+
from source_context_mcp.core import AppContext, Settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register_tools(mcp: MCPServer):
|
|
8
|
+
@mcp.tool(description="This tool for development env only. Show all current config. Read source code to see more")
|
|
9
|
+
def debug(ctx: Context[AppContext]) -> Settings:
|
|
10
|
+
app_context: AppContext = ctx.request_context.lifespan_context
|
|
11
|
+
return app_context.settings
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from mcp.server import MCPServer
|
|
4
|
+
|
|
5
|
+
from source_context_mcp.core import ApiClientDep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_tools(mcp: MCPServer) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Registers all the graph tools.
|
|
11
|
+
Args:
|
|
12
|
+
mcp: MCPServer instance
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@mcp.tool(description="List all files in specified project.")
|
|
16
|
+
async def list_files_in_project(client: ApiClientDep, project_id: str) -> Any:
|
|
17
|
+
res = await client.get(f"/graph/projects/{project_id}/files")
|
|
18
|
+
return res.data
|
|
19
|
+
|
|
20
|
+
@mcp.tool(description="Inspect file structure(classes and methods")
|
|
21
|
+
async def get_file_structure(client: ApiClientDep, file_id: str) -> Any:
|
|
22
|
+
res = await client.get(f"/graph/files/{file_id}/structures")
|
|
23
|
+
return res.data
|
|
24
|
+
|
|
25
|
+
@mcp.tool(description="Read file content.")
|
|
26
|
+
async def get_file_content(client: ApiClientDep, file_id: str) -> Any:
|
|
27
|
+
res = await client.get(f"/graph/files/{file_id}/content")
|
|
28
|
+
return res.data
|
|
29
|
+
|
|
30
|
+
@mcp.tool(description="Find all nodes that call/reference to the specified node.")
|
|
31
|
+
async def find_node_usages(client: ApiClientDep, node_id: str) -> Any:
|
|
32
|
+
res = await client.get(f"/graph/nodes/{node_id}/usages")
|
|
33
|
+
return res.data
|
|
34
|
+
|
|
35
|
+
@mcp.tool(description="Find all nodes(or its children) that the specified node is calling or reference to.")
|
|
36
|
+
async def find_node_callees(client: ApiClientDep, node_id: str) -> Any:
|
|
37
|
+
res = await client.get(f"/graph/nodes/{node_id}/callees")
|
|
38
|
+
return res.data
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from mcp.server import MCPServer
|
|
4
|
+
|
|
5
|
+
from source_context_mcp.core import ApiClientDep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_tools(mcp: MCPServer) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Registers all the vector tools.
|
|
11
|
+
Args:
|
|
12
|
+
mcp: MCP server instance
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@mcp.tool()
|
|
16
|
+
async def search_in_workspace(client: ApiClientDep, query: str, workspace_id: int, top_k: int = 5) -> Any:
|
|
17
|
+
req_body = {
|
|
18
|
+
"query": query,
|
|
19
|
+
"top_k": top_k,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
res = await client.post(f"/vector/search/{workspace_id}", req_body)
|
|
23
|
+
|
|
24
|
+
return res.data
|
|
25
|
+
|
|
26
|
+
@mcp.tool()
|
|
27
|
+
async def search_in_repo_and_branch(
|
|
28
|
+
client: ApiClientDep, repository_id: int, branch_name: str, query: str, top_k: int = 5
|
|
29
|
+
) -> Any:
|
|
30
|
+
req_body = {
|
|
31
|
+
"query": query,
|
|
32
|
+
"top_k": top_k,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
res = await client.post(f"/vector/search/{repository_id}/{branch_name}", req_body)
|
|
36
|
+
|
|
37
|
+
return res.data
|