nia-mcp-server 1.0.5__py3-none-any.whl → 1.0.6__py3-none-any.whl
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.
Potentially problematic release.
This version of nia-mcp-server might be problematic. Click here for more details.
- nia_mcp_server/__init__.py +1 -1
- nia_mcp_server/api_client.py +75 -1
- nia_mcp_server/profiles.py +263 -0
- nia_mcp_server/project_init.py +193 -0
- nia_mcp_server/rule_transformer.py +363 -0
- nia_mcp_server/server.py +297 -6
- {nia_mcp_server-1.0.5.dist-info → nia_mcp_server-1.0.6.dist-info}/METADATA +1 -1
- nia_mcp_server-1.0.6.dist-info/RECORD +12 -0
- nia_mcp_server-1.0.5.dist-info/RECORD +0 -9
- {nia_mcp_server-1.0.5.dist-info → nia_mcp_server-1.0.6.dist-info}/WHEEL +0 -0
- {nia_mcp_server-1.0.5.dist-info → nia_mcp_server-1.0.6.dist-info}/entry_points.txt +0 -0
- {nia_mcp_server-1.0.5.dist-info → nia_mcp_server-1.0.6.dist-info}/licenses/LICENSE +0 -0
nia_mcp_server/__init__.py
CHANGED
nia_mcp_server/api_client.py
CHANGED
|
@@ -137,7 +137,18 @@ class NIAApiClient:
|
|
|
137
137
|
try:
|
|
138
138
|
response = await self.client.get(f"{self.base_url}/v2/repositories")
|
|
139
139
|
response.raise_for_status()
|
|
140
|
-
|
|
140
|
+
data = response.json()
|
|
141
|
+
|
|
142
|
+
# Ensure we always return a list
|
|
143
|
+
if not isinstance(data, list):
|
|
144
|
+
logger.error(f"Unexpected response type from list_repositories: {type(data)}, data: {data}")
|
|
145
|
+
# If it's a dict with an error message, raise it
|
|
146
|
+
if isinstance(data, dict) and "error" in data:
|
|
147
|
+
raise APIError(f"API returned error: {data['error']}")
|
|
148
|
+
# Otherwise return empty list
|
|
149
|
+
return []
|
|
150
|
+
|
|
151
|
+
return data
|
|
141
152
|
except httpx.HTTPStatusError as e:
|
|
142
153
|
logger.error(f"Caught HTTPStatusError in list_repositories: status={e.response.status_code}, detail={e.response.text}")
|
|
143
154
|
raise self._handle_api_error(e)
|
|
@@ -336,6 +347,53 @@ class NIAApiClient:
|
|
|
336
347
|
logger.error(f"Failed to delete repository: {e}")
|
|
337
348
|
return False
|
|
338
349
|
|
|
350
|
+
async def rename_repository(self, owner_repo: str, new_name: str) -> Dict[str, Any]:
|
|
351
|
+
"""Rename a repository's display name."""
|
|
352
|
+
try:
|
|
353
|
+
# Check if this looks like owner/repo format (contains /)
|
|
354
|
+
if '/' in owner_repo:
|
|
355
|
+
# First, get the repository ID
|
|
356
|
+
status = await self.get_repository_status(owner_repo)
|
|
357
|
+
if not status:
|
|
358
|
+
raise APIError(f"Repository {owner_repo} not found", 404)
|
|
359
|
+
|
|
360
|
+
# Extract the repository ID from status
|
|
361
|
+
repo_id = status.get("repository_id") or status.get("id")
|
|
362
|
+
if not repo_id:
|
|
363
|
+
# Try to get it from list as fallback
|
|
364
|
+
repos = await self.list_repositories()
|
|
365
|
+
for repo in repos:
|
|
366
|
+
if repo.get("repository") == owner_repo:
|
|
367
|
+
repo_id = repo.get("repository_id") or repo.get("id")
|
|
368
|
+
break
|
|
369
|
+
|
|
370
|
+
if not repo_id:
|
|
371
|
+
raise APIError(f"No repository ID found for {owner_repo}", 404)
|
|
372
|
+
|
|
373
|
+
# Rename using the ID
|
|
374
|
+
response = await self.client.patch(
|
|
375
|
+
f"{self.base_url}/v2/repositories/{repo_id}/rename",
|
|
376
|
+
json={"new_name": new_name}
|
|
377
|
+
)
|
|
378
|
+
response.raise_for_status()
|
|
379
|
+
return response.json()
|
|
380
|
+
else:
|
|
381
|
+
# Assume it's already a repository ID
|
|
382
|
+
response = await self.client.patch(
|
|
383
|
+
f"{self.base_url}/v2/repositories/{owner_repo}/rename",
|
|
384
|
+
json={"new_name": new_name}
|
|
385
|
+
)
|
|
386
|
+
response.raise_for_status()
|
|
387
|
+
return response.json()
|
|
388
|
+
|
|
389
|
+
except httpx.HTTPStatusError as e:
|
|
390
|
+
raise self._handle_api_error(e)
|
|
391
|
+
except APIError:
|
|
392
|
+
raise
|
|
393
|
+
except Exception as e:
|
|
394
|
+
logger.error(f"Failed to rename repository: {e}")
|
|
395
|
+
raise APIError(f"Failed to rename repository: {str(e)}")
|
|
396
|
+
|
|
339
397
|
# Data Source methods
|
|
340
398
|
|
|
341
399
|
async def create_data_source(
|
|
@@ -408,6 +466,22 @@ class NIAApiClient:
|
|
|
408
466
|
logger.error(f"Failed to delete data source: {e}")
|
|
409
467
|
return False
|
|
410
468
|
|
|
469
|
+
async def rename_data_source(self, source_id: str, new_name: str) -> Dict[str, Any]:
|
|
470
|
+
"""Rename a data source's display name."""
|
|
471
|
+
try:
|
|
472
|
+
response = await self.client.patch(
|
|
473
|
+
f"{self.base_url}/v2/data-sources/{source_id}/rename",
|
|
474
|
+
json={"new_name": new_name}
|
|
475
|
+
)
|
|
476
|
+
response.raise_for_status()
|
|
477
|
+
return response.json()
|
|
478
|
+
|
|
479
|
+
except httpx.HTTPStatusError as e:
|
|
480
|
+
raise self._handle_api_error(e)
|
|
481
|
+
except Exception as e:
|
|
482
|
+
logger.error(f"Failed to rename data source: {e}")
|
|
483
|
+
raise APIError(f"Failed to rename data source: {str(e)}")
|
|
484
|
+
|
|
411
485
|
async def query_unified(
|
|
412
486
|
self,
|
|
413
487
|
messages: List[Dict[str, str]],
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nia Profile Configurations
|
|
3
|
+
Defines IDE/Editor profile configurations for rule transformation
|
|
4
|
+
"""
|
|
5
|
+
from typing import Dict, Any, Optional, List
|
|
6
|
+
|
|
7
|
+
# Profile configurations define how rules are transformed and where they're placed
|
|
8
|
+
PROFILE_CONFIGS: Dict[str, Dict[str, Any]] = {
|
|
9
|
+
"cursor": {
|
|
10
|
+
"name": "Cursor",
|
|
11
|
+
"target_dir": ".cursor/rules",
|
|
12
|
+
"file_extension": ".mdc",
|
|
13
|
+
"file_map": {
|
|
14
|
+
"cursor_rules.md": "nia.mdc"
|
|
15
|
+
},
|
|
16
|
+
"mcp_config": True,
|
|
17
|
+
"format": "mdc",
|
|
18
|
+
"features": ["mcp", "composer", "inline_edits"],
|
|
19
|
+
"global_replacements": {
|
|
20
|
+
"# Nia": "# Nia for Cursor",
|
|
21
|
+
"{{IDE}}": "Cursor"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
"vscode": {
|
|
26
|
+
"name": "Visual Studio Code",
|
|
27
|
+
"target_dir": ".vscode",
|
|
28
|
+
"file_extension": ".md",
|
|
29
|
+
"file_map": {
|
|
30
|
+
"nia_rules.md": "nia-guide.md",
|
|
31
|
+
"vscode_rules.md": "nia-vscode-integration.md"
|
|
32
|
+
},
|
|
33
|
+
"mcp_config": False,
|
|
34
|
+
"format": "markdown",
|
|
35
|
+
"features": ["tasks", "snippets", "terminal_integration"],
|
|
36
|
+
"global_replacements": {
|
|
37
|
+
"# Nia": "# Nia for VSCode",
|
|
38
|
+
"{{IDE}}": "VSCode"
|
|
39
|
+
},
|
|
40
|
+
"additional_files": {
|
|
41
|
+
"tasks.json": "vscode_tasks_template",
|
|
42
|
+
"nia.code-snippets": "vscode_snippets_template"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
"claude": {
|
|
47
|
+
"name": "Claude Desktop",
|
|
48
|
+
"target_dir": ".claude",
|
|
49
|
+
"file_extension": ".md",
|
|
50
|
+
"file_map": {
|
|
51
|
+
"nia_rules.md": "nia_rules.md",
|
|
52
|
+
"claude_rules.md": "nia_claude_integration.md"
|
|
53
|
+
},
|
|
54
|
+
"mcp_config": False,
|
|
55
|
+
"format": "markdown",
|
|
56
|
+
"features": ["conversational", "context_aware", "multi_step"],
|
|
57
|
+
"global_replacements": {
|
|
58
|
+
"# Nia": "# Nia for Claude Desktop",
|
|
59
|
+
"{{IDE}}": "Claude"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
"windsurf": {
|
|
64
|
+
"name": "Windsurf",
|
|
65
|
+
"target_dir": ".windsurfrules",
|
|
66
|
+
"file_extension": ".md",
|
|
67
|
+
"file_map": {
|
|
68
|
+
"nia_rules.md": "nia_rules.md",
|
|
69
|
+
"windsurf_rules.md": "windsurf_nia_guide.md"
|
|
70
|
+
},
|
|
71
|
+
"mcp_config": True,
|
|
72
|
+
"format": "markdown",
|
|
73
|
+
"features": ["cascade", "memories", "flows"],
|
|
74
|
+
"global_replacements": {
|
|
75
|
+
"# Nia": "# Nia for Windsurf Cascade",
|
|
76
|
+
"{{IDE}}": "Windsurf"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
"cline": {
|
|
81
|
+
"name": "Cline",
|
|
82
|
+
"target_dir": ".cline",
|
|
83
|
+
"file_extension": ".md",
|
|
84
|
+
"file_map": {
|
|
85
|
+
"nia_rules.md": "nia_rules.md"
|
|
86
|
+
},
|
|
87
|
+
"mcp_config": True,
|
|
88
|
+
"format": "markdown",
|
|
89
|
+
"features": ["autonomous", "task_planning"],
|
|
90
|
+
"global_replacements": {
|
|
91
|
+
"# Nia": "# Nia for Cline",
|
|
92
|
+
"{{IDE}}": "Cline"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
"codex": {
|
|
97
|
+
"name": "OpenAI Codex",
|
|
98
|
+
"target_dir": ".codex",
|
|
99
|
+
"file_extension": ".md",
|
|
100
|
+
"file_map": {
|
|
101
|
+
"nia_rules.md": "nia_codex_guide.md"
|
|
102
|
+
},
|
|
103
|
+
"mcp_config": False,
|
|
104
|
+
"format": "markdown",
|
|
105
|
+
"features": ["completion", "generation"],
|
|
106
|
+
"global_replacements": {
|
|
107
|
+
"# Nia": "# Nia for Codex",
|
|
108
|
+
"{{IDE}}": "Codex"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
"zed": {
|
|
113
|
+
"name": "Zed",
|
|
114
|
+
"target_dir": ".zed",
|
|
115
|
+
"file_extension": ".md",
|
|
116
|
+
"file_map": {
|
|
117
|
+
"nia_rules.md": "nia_assistant.md"
|
|
118
|
+
},
|
|
119
|
+
"mcp_config": False,
|
|
120
|
+
"format": "markdown",
|
|
121
|
+
"features": ["assistant", "collaboration"],
|
|
122
|
+
"global_replacements": {
|
|
123
|
+
"# Nia": "# Nia for Zed",
|
|
124
|
+
"{{IDE}}": "Zed"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
"jetbrains": {
|
|
129
|
+
"name": "JetBrains IDEs",
|
|
130
|
+
"target_dir": ".idea/nia",
|
|
131
|
+
"file_extension": ".md",
|
|
132
|
+
"file_map": {
|
|
133
|
+
"nia_rules.md": "nia_guide.md"
|
|
134
|
+
},
|
|
135
|
+
"mcp_config": False,
|
|
136
|
+
"format": "markdown",
|
|
137
|
+
"features": ["ai_assistant", "code_completion"],
|
|
138
|
+
"global_replacements": {
|
|
139
|
+
"# Nia": "# Nia for JetBrains",
|
|
140
|
+
"{{IDE}}": "JetBrains IDE"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
"neovim": {
|
|
145
|
+
"name": "Neovim",
|
|
146
|
+
"target_dir": ".config/nvim/nia",
|
|
147
|
+
"file_extension": ".md",
|
|
148
|
+
"file_map": {
|
|
149
|
+
"nia_rules.md": "nia_guide.md"
|
|
150
|
+
},
|
|
151
|
+
"mcp_config": False,
|
|
152
|
+
"format": "markdown",
|
|
153
|
+
"features": ["copilot", "cmp"],
|
|
154
|
+
"global_replacements": {
|
|
155
|
+
"# Nia": "# Nia for Neovim",
|
|
156
|
+
"{{IDE}}": "Neovim"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
"sublime": {
|
|
161
|
+
"name": "Sublime Text",
|
|
162
|
+
"target_dir": ".sublime",
|
|
163
|
+
"file_extension": ".md",
|
|
164
|
+
"file_map": {
|
|
165
|
+
"nia_rules.md": "nia_guide.md"
|
|
166
|
+
},
|
|
167
|
+
"mcp_config": False,
|
|
168
|
+
"format": "markdown",
|
|
169
|
+
"features": ["copilot"],
|
|
170
|
+
"global_replacements": {
|
|
171
|
+
"# Nia": "# Nia for Sublime Text",
|
|
172
|
+
"{{IDE}}": "Sublime Text"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Additional template configurations for specific file types
|
|
178
|
+
TEMPLATE_CONFIGS = {
|
|
179
|
+
"vscode_tasks_template": {
|
|
180
|
+
"content": """{
|
|
181
|
+
"version": "2.0.0",
|
|
182
|
+
"tasks": [
|
|
183
|
+
{
|
|
184
|
+
"label": "Nia: Index Repository",
|
|
185
|
+
"type": "shell",
|
|
186
|
+
"command": "echo 'Run: index_repository ${input:repoUrl}'",
|
|
187
|
+
"problemMatcher": []
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"label": "Nia: Search Codebase",
|
|
191
|
+
"type": "shell",
|
|
192
|
+
"command": "echo 'Run: search_codebase \\"${input:searchQuery}\\"'",
|
|
193
|
+
"problemMatcher": []
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"label": "Nia: List Repositories",
|
|
197
|
+
"type": "shell",
|
|
198
|
+
"command": "echo 'Run: list_repositories'",
|
|
199
|
+
"problemMatcher": []
|
|
200
|
+
}
|
|
201
|
+
],
|
|
202
|
+
"inputs": [
|
|
203
|
+
{
|
|
204
|
+
"id": "repoUrl",
|
|
205
|
+
"type": "promptString",
|
|
206
|
+
"description": "GitHub repository URL"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"id": "searchQuery",
|
|
210
|
+
"type": "promptString",
|
|
211
|
+
"description": "Search query"
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
}"""
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
"vscode_snippets_template": {
|
|
218
|
+
"content": """{
|
|
219
|
+
"Nia Index": {
|
|
220
|
+
"prefix": "nia-index",
|
|
221
|
+
"body": ["index_repository ${1:repo_url}"],
|
|
222
|
+
"description": "Index a repository with Nia"
|
|
223
|
+
},
|
|
224
|
+
"Nia Search": {
|
|
225
|
+
"prefix": "nia-search",
|
|
226
|
+
"body": ["search_codebase \\"${1:query}\\""],
|
|
227
|
+
"description": "Search indexed repositories"
|
|
228
|
+
},
|
|
229
|
+
"Nia Web Search": {
|
|
230
|
+
"prefix": "nia-web",
|
|
231
|
+
"body": ["nia_web_search \\"${1:query}\\""],
|
|
232
|
+
"description": "Search the web with Nia"
|
|
233
|
+
},
|
|
234
|
+
"Nia Research": {
|
|
235
|
+
"prefix": "nia-research",
|
|
236
|
+
"body": ["nia_deep_research_agent \\"${1:query}\\""],
|
|
237
|
+
"description": "Perform deep research with Nia"
|
|
238
|
+
}
|
|
239
|
+
}"""
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def get_profile_config(profile: str) -> Optional[Dict[str, Any]]:
|
|
245
|
+
"""Get configuration for a specific profile"""
|
|
246
|
+
return PROFILE_CONFIGS.get(profile)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def get_supported_profiles() -> List[str]:
|
|
250
|
+
"""Get list of all supported profiles"""
|
|
251
|
+
return list(PROFILE_CONFIGS.keys())
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_profile_features(profile: str) -> List[str]:
|
|
255
|
+
"""Get features supported by a profile"""
|
|
256
|
+
config = get_profile_config(profile)
|
|
257
|
+
return config.get("features", []) if config else []
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def is_mcp_enabled(profile: str) -> bool:
|
|
261
|
+
"""Check if a profile supports MCP"""
|
|
262
|
+
config = get_profile_config(profile)
|
|
263
|
+
return config.get("mcp_config", False) if config else False
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nia Project Initialization Module
|
|
3
|
+
Handles creation of Nia-enabled project structures and configurations
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import logging
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import List, Dict, Optional, Any
|
|
13
|
+
from .profiles import PROFILE_CONFIGS, get_profile_config
|
|
14
|
+
from .rule_transformer import transform_rules_for_profile
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class NIAProjectInitializer:
|
|
19
|
+
"""Handles Nia project initialization with support for multiple IDE profiles"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, project_root: str):
|
|
22
|
+
self.project_root = Path(project_root).resolve()
|
|
23
|
+
self.assets_dir = Path(__file__).parent.parent.parent / "assets"
|
|
24
|
+
self.templates_dir = self.assets_dir / "templates"
|
|
25
|
+
self.rules_dir = self.assets_dir / "rules"
|
|
26
|
+
|
|
27
|
+
# Validate that required directories exist
|
|
28
|
+
if not self.assets_dir.exists():
|
|
29
|
+
raise RuntimeError(f"Assets directory not found at {self.assets_dir}. Nia MCP server may not be installed correctly.")
|
|
30
|
+
if not self.rules_dir.exists():
|
|
31
|
+
raise RuntimeError(f"Rules directory not found at {self.rules_dir}. Nia MCP server may not be installed correctly.")
|
|
32
|
+
|
|
33
|
+
def initialize_project(
|
|
34
|
+
self,
|
|
35
|
+
profiles: List[str] = ["cursor"]
|
|
36
|
+
) -> Dict[str, Any]:
|
|
37
|
+
"""
|
|
38
|
+
Initialize a Nia project with specified profiles
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
profiles: List of IDE profiles to set up (cursor, vscode, claude, etc.)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dictionary with initialization results and status
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
results = {
|
|
48
|
+
"success": True,
|
|
49
|
+
"project_root": str(self.project_root),
|
|
50
|
+
"profiles_initialized": [],
|
|
51
|
+
"files_created": [],
|
|
52
|
+
"warnings": [],
|
|
53
|
+
"next_steps": []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Validate project root
|
|
57
|
+
if not self.project_root.exists():
|
|
58
|
+
os.makedirs(self.project_root, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
# No longer creating .nia directory or config files
|
|
61
|
+
|
|
62
|
+
# Process each profile
|
|
63
|
+
for profile in profiles:
|
|
64
|
+
if profile not in PROFILE_CONFIGS:
|
|
65
|
+
results["warnings"].append(f"Unknown profile: {profile}")
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
profile_results = self._initialize_profile(profile)
|
|
69
|
+
if profile_results["success"]:
|
|
70
|
+
results["profiles_initialized"].append(profile)
|
|
71
|
+
results["files_created"].extend(profile_results["files_created"])
|
|
72
|
+
else:
|
|
73
|
+
results["warnings"].append(f"Failed to initialize {profile}: {profile_results.get('error')}")
|
|
74
|
+
|
|
75
|
+
# Generate next steps
|
|
76
|
+
results["next_steps"].extend(self._generate_next_steps(profiles))
|
|
77
|
+
|
|
78
|
+
logger.info(f"Successfully initialized Nia project at {self.project_root}")
|
|
79
|
+
return results
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Failed to initialize project: {e}")
|
|
83
|
+
return {
|
|
84
|
+
"success": False,
|
|
85
|
+
"error": str(e),
|
|
86
|
+
"project_root": str(self.project_root)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def _create_nia_directories(self):
|
|
90
|
+
"""Create the .nia directory structure"""
|
|
91
|
+
# Simplified - no unnecessary directories or files
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _initialize_profile(self, profile: str) -> Dict[str, Any]:
|
|
98
|
+
"""Initialize a specific IDE profile"""
|
|
99
|
+
try:
|
|
100
|
+
profile_config = get_profile_config(profile)
|
|
101
|
+
if not profile_config:
|
|
102
|
+
return {"success": False, "error": "Profile configuration not found"}
|
|
103
|
+
|
|
104
|
+
files_created = []
|
|
105
|
+
|
|
106
|
+
# Create profile directory
|
|
107
|
+
profile_dir = self.project_root / profile_config["target_dir"]
|
|
108
|
+
os.makedirs(profile_dir, exist_ok=True)
|
|
109
|
+
|
|
110
|
+
# Transform and copy rules
|
|
111
|
+
rule_files = transform_rules_for_profile(
|
|
112
|
+
profile,
|
|
113
|
+
self.rules_dir,
|
|
114
|
+
profile_dir,
|
|
115
|
+
self.project_root
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
for rule_file in rule_files:
|
|
119
|
+
files_created.append(str(Path(rule_file).relative_to(self.project_root)))
|
|
120
|
+
|
|
121
|
+
# Handle profile-specific setup
|
|
122
|
+
if profile == "vscode" and profile_config.get("additional_files"):
|
|
123
|
+
# VSCode gets tasks.json and snippets from additional_files
|
|
124
|
+
pass # Handled by transform_rules_for_profile
|
|
125
|
+
# No need to setup MCP config - user is already running through MCP!
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"success": True,
|
|
129
|
+
"files_created": files_created
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Failed to initialize profile {profile}: {e}")
|
|
134
|
+
return {"success": False, "error": str(e)}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _generate_next_steps(self, profiles: List[str]) -> List[str]:
|
|
139
|
+
"""Generate helpful next steps for the user"""
|
|
140
|
+
steps = []
|
|
141
|
+
|
|
142
|
+
# Check for current git repository
|
|
143
|
+
if (self.project_root / ".git").exists():
|
|
144
|
+
try:
|
|
145
|
+
# Use subprocess for safer command execution
|
|
146
|
+
result = subprocess.run(
|
|
147
|
+
["git", "remote", "get-url", "origin"],
|
|
148
|
+
cwd=self.project_root,
|
|
149
|
+
capture_output=True,
|
|
150
|
+
text=True,
|
|
151
|
+
check=False # Don't raise exception if git command fails
|
|
152
|
+
)
|
|
153
|
+
git_remote = result.stdout.strip()
|
|
154
|
+
if git_remote and "github.com" in git_remote:
|
|
155
|
+
steps.append(f"Index this repository: index_repository {git_remote}")
|
|
156
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
157
|
+
# Git might not be installed or available
|
|
158
|
+
logger.debug("Could not get git remote URL")
|
|
159
|
+
|
|
160
|
+
# Profile-specific steps
|
|
161
|
+
if "cursor" in profiles:
|
|
162
|
+
steps.append("Restart Cursor to load Nia MCP server")
|
|
163
|
+
if "vscode" in profiles:
|
|
164
|
+
steps.append("Reload VSCode window to apply settings")
|
|
165
|
+
|
|
166
|
+
# General steps
|
|
167
|
+
steps.extend([
|
|
168
|
+
"Explore available commands with list_repositories",
|
|
169
|
+
"Search for code patterns with search_codebase",
|
|
170
|
+
"Find new libraries with nia_web_search"
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
return steps
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def initialize_nia_project(
|
|
177
|
+
project_root: str,
|
|
178
|
+
profiles: List[str] = ["cursor"]
|
|
179
|
+
) -> Dict[str, Any]:
|
|
180
|
+
"""
|
|
181
|
+
Convenience function to initialize a Nia project
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
project_root: Root directory of the project
|
|
185
|
+
profiles: List of IDE profiles to set up
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dictionary with initialization results
|
|
189
|
+
"""
|
|
190
|
+
initializer = NIAProjectInitializer(project_root)
|
|
191
|
+
return initializer.initialize_project(
|
|
192
|
+
profiles=profiles
|
|
193
|
+
)
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nia Rule Transformer
|
|
3
|
+
Handles transformation of rule files for different IDE profiles
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Dict, Any, Optional
|
|
10
|
+
from .profiles import get_profile_config, TEMPLATE_CONFIGS
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def transform_rules_for_profile(
|
|
16
|
+
profile: str,
|
|
17
|
+
source_dir: Path,
|
|
18
|
+
target_dir: Path,
|
|
19
|
+
project_root: Path
|
|
20
|
+
) -> List[str]:
|
|
21
|
+
"""
|
|
22
|
+
Transform rule files for a specific profile
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
profile: Profile name (cursor, vscode, etc.)
|
|
26
|
+
source_dir: Directory containing source rule files
|
|
27
|
+
target_dir: Directory to write transformed rules
|
|
28
|
+
project_root: Project root directory for context
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
List of created file paths
|
|
32
|
+
"""
|
|
33
|
+
profile_config = get_profile_config(profile)
|
|
34
|
+
if not profile_config:
|
|
35
|
+
raise ValueError(f"Unknown profile: {profile}")
|
|
36
|
+
|
|
37
|
+
created_files = []
|
|
38
|
+
file_map = profile_config.get("file_map", {})
|
|
39
|
+
|
|
40
|
+
# Process each file in the file map
|
|
41
|
+
for source_file, target_file in file_map.items():
|
|
42
|
+
source_path = source_dir / source_file
|
|
43
|
+
|
|
44
|
+
if not source_path.exists():
|
|
45
|
+
logger.warning(f"Source file not found: {source_path}")
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# Read source content
|
|
49
|
+
content = source_path.read_text()
|
|
50
|
+
|
|
51
|
+
# Apply transformations
|
|
52
|
+
transformed_content = apply_transformations(
|
|
53
|
+
content,
|
|
54
|
+
profile_config,
|
|
55
|
+
project_root
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Write to target
|
|
59
|
+
target_path = target_dir / target_file
|
|
60
|
+
os.makedirs(target_path.parent, exist_ok=True)
|
|
61
|
+
target_path.write_text(transformed_content)
|
|
62
|
+
created_files.append(str(target_path))
|
|
63
|
+
|
|
64
|
+
logger.info(f"Created rule file: {target_path}")
|
|
65
|
+
|
|
66
|
+
# Handle additional files (like VSCode tasks.json)
|
|
67
|
+
additional_files = profile_config.get("additional_files", {})
|
|
68
|
+
for filename, template_key in additional_files.items():
|
|
69
|
+
if template_key in TEMPLATE_CONFIGS:
|
|
70
|
+
template_config = TEMPLATE_CONFIGS[template_key]
|
|
71
|
+
target_path = target_dir / filename
|
|
72
|
+
|
|
73
|
+
# Apply any necessary transformations to template
|
|
74
|
+
content = template_config["content"]
|
|
75
|
+
content = apply_template_variables(content, project_root)
|
|
76
|
+
|
|
77
|
+
target_path.write_text(content)
|
|
78
|
+
created_files.append(str(target_path))
|
|
79
|
+
logger.info(f"Created additional file: {target_path}")
|
|
80
|
+
|
|
81
|
+
return created_files
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def apply_transformations(
|
|
85
|
+
content: str,
|
|
86
|
+
profile_config: Dict[str, Any],
|
|
87
|
+
project_root: Path
|
|
88
|
+
) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Apply profile-specific transformations to content
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
content: Original content
|
|
94
|
+
profile_config: Profile configuration
|
|
95
|
+
project_root: Project root for context
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Transformed content
|
|
99
|
+
"""
|
|
100
|
+
# Apply global replacements
|
|
101
|
+
global_replacements = profile_config.get("global_replacements", {})
|
|
102
|
+
for pattern, replacement in global_replacements.items():
|
|
103
|
+
content = content.replace(pattern, replacement)
|
|
104
|
+
|
|
105
|
+
# Apply project-specific variables
|
|
106
|
+
content = apply_template_variables(content, project_root)
|
|
107
|
+
|
|
108
|
+
# Apply format-specific transformations
|
|
109
|
+
if profile_config.get("format") == "markdown":
|
|
110
|
+
content = transform_markdown_format(content, profile_config)
|
|
111
|
+
elif profile_config.get("format") == "mdc":
|
|
112
|
+
content = transform_to_mdc_format(content, profile_config)
|
|
113
|
+
|
|
114
|
+
# Apply feature-specific enhancements
|
|
115
|
+
features = profile_config.get("features", [])
|
|
116
|
+
content = enhance_for_features(content, features, profile_config)
|
|
117
|
+
|
|
118
|
+
return content
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def apply_template_variables(content: str, project_root: Path) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Replace template variables with actual values
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
content: Content with template variables
|
|
127
|
+
project_root: Project root directory
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Content with variables replaced
|
|
131
|
+
"""
|
|
132
|
+
variables = {
|
|
133
|
+
"{{PROJECT_ROOT}}": str(project_root),
|
|
134
|
+
"{{PROJECT_NAME}}": project_root.name,
|
|
135
|
+
"{{WORKSPACE_FOLDER}}": "${workspaceFolder}",
|
|
136
|
+
"{{USER_HOME}}": str(Path.home()),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for var, value in variables.items():
|
|
140
|
+
content = content.replace(var, value)
|
|
141
|
+
|
|
142
|
+
return content
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def transform_markdown_format(content: str, profile_config: Dict[str, Any]) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Apply markdown-specific transformations
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
content: Markdown content
|
|
151
|
+
profile_config: Profile configuration
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Transformed markdown
|
|
155
|
+
"""
|
|
156
|
+
# Add profile-specific header if not present
|
|
157
|
+
profile_name = profile_config.get("name", "Unknown")
|
|
158
|
+
if not content.startswith(f"# Nia Integration for {profile_name}"):
|
|
159
|
+
# Check if it starts with a generic Nia header
|
|
160
|
+
if content.startswith("# Nia"):
|
|
161
|
+
# Replace the first line
|
|
162
|
+
lines = content.split('\n')
|
|
163
|
+
lines[0] = f"# Nia Integration for {profile_name}"
|
|
164
|
+
content = '\n'.join(lines)
|
|
165
|
+
|
|
166
|
+
# Enhance code blocks with profile-specific annotations
|
|
167
|
+
if "mcp" in profile_config.get("features", []):
|
|
168
|
+
content = enhance_mcp_code_blocks(content)
|
|
169
|
+
|
|
170
|
+
return content
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def transform_to_mdc_format(content: str, profile_config: Dict[str, Any]) -> str:
|
|
174
|
+
"""
|
|
175
|
+
Transform markdown content to MDC format for Cursor
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
content: Original markdown content
|
|
179
|
+
profile_config: Profile configuration
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
MDC formatted content
|
|
183
|
+
"""
|
|
184
|
+
# Extract the first line as description
|
|
185
|
+
lines = content.split('\n')
|
|
186
|
+
description = ""
|
|
187
|
+
content_start = 0
|
|
188
|
+
|
|
189
|
+
if lines and lines[0].startswith('#'):
|
|
190
|
+
# Use the first header as description
|
|
191
|
+
description = lines[0].replace('#', '').strip()
|
|
192
|
+
content_start = 1
|
|
193
|
+
else:
|
|
194
|
+
description = "Nia Knowledge Agent Integration Rules"
|
|
195
|
+
|
|
196
|
+
# Build MDC header
|
|
197
|
+
# For Nia rules, we want them always applied since they guide AI assistant behavior
|
|
198
|
+
mdc_header = f"""---
|
|
199
|
+
description: {description}
|
|
200
|
+
alwaysApply: true
|
|
201
|
+
---
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
# Get the rest of the content
|
|
205
|
+
remaining_content = '\n'.join(lines[content_start:]).strip()
|
|
206
|
+
|
|
207
|
+
# Apply markdown transformations first
|
|
208
|
+
remaining_content = transform_markdown_format(remaining_content, profile_config)
|
|
209
|
+
|
|
210
|
+
return mdc_header + '\n' + remaining_content
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def enhance_for_features(
|
|
214
|
+
content: str,
|
|
215
|
+
features: List[str],
|
|
216
|
+
profile_config: Dict[str, Any]
|
|
217
|
+
) -> str:
|
|
218
|
+
"""
|
|
219
|
+
Enhance content based on profile features
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
content: Original content
|
|
223
|
+
features: List of features supported by profile
|
|
224
|
+
profile_config: Profile configuration
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Enhanced content
|
|
228
|
+
"""
|
|
229
|
+
# Add feature-specific sections if not present
|
|
230
|
+
enhancements = []
|
|
231
|
+
|
|
232
|
+
if "mcp" in features and "## MCP Integration" not in content:
|
|
233
|
+
enhancements.append(generate_mcp_section(profile_config))
|
|
234
|
+
|
|
235
|
+
if "composer" in features and "## Composer Usage" not in content:
|
|
236
|
+
enhancements.append(generate_composer_section())
|
|
237
|
+
|
|
238
|
+
if "tasks" in features and "## Task Automation" not in content:
|
|
239
|
+
enhancements.append(generate_tasks_section())
|
|
240
|
+
|
|
241
|
+
if "terminal_integration" in features and "## Terminal Commands" not in content:
|
|
242
|
+
enhancements.append(generate_terminal_section())
|
|
243
|
+
|
|
244
|
+
# Append enhancements to content
|
|
245
|
+
if enhancements:
|
|
246
|
+
content += "\n\n" + "\n\n".join(enhancements)
|
|
247
|
+
|
|
248
|
+
return content
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def enhance_mcp_code_blocks(content: str) -> str:
|
|
252
|
+
"""
|
|
253
|
+
Enhance code blocks for MCP-enabled profiles
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
content: Markdown content
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Enhanced content
|
|
260
|
+
"""
|
|
261
|
+
# Pattern to find code blocks with Nia commands
|
|
262
|
+
pattern = r'```(\w*)\n(.*?nia.*?)\n```'
|
|
263
|
+
|
|
264
|
+
def replacer(match):
|
|
265
|
+
lang = match.group(1) or "bash"
|
|
266
|
+
code = match.group(2)
|
|
267
|
+
|
|
268
|
+
# If it's a NIA command, add annotation
|
|
269
|
+
if any(cmd in code for cmd in ["index_repository", "search_codebase", "list_repositories"]):
|
|
270
|
+
return f'```{lang}\n# MCP Command - Run this in your AI assistant\n{code}\n```'
|
|
271
|
+
return match.group(0)
|
|
272
|
+
|
|
273
|
+
return re.sub(pattern, replacer, content, flags=re.DOTALL)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def generate_mcp_section(profile_config: Dict[str, Any]) -> str:
|
|
277
|
+
"""Generate MCP integration section"""
|
|
278
|
+
profile_name = profile_config.get("name", "IDE")
|
|
279
|
+
return f"""## MCP Integration
|
|
280
|
+
|
|
281
|
+
{profile_name} supports Nia through the Model Context Protocol (MCP). After initialization:
|
|
282
|
+
|
|
283
|
+
1. **Restart {profile_name}** to load the Nia MCP server
|
|
284
|
+
2. **Verify connection** by running: `list_repositories`
|
|
285
|
+
3. **Set API key** in your environment or {profile_name} settings
|
|
286
|
+
|
|
287
|
+
The MCP server provides direct access to all Nia commands within your AI assistant."""
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def generate_composer_section() -> str:
|
|
291
|
+
"""Generate Composer-specific section"""
|
|
292
|
+
return """## Composer Usage
|
|
293
|
+
|
|
294
|
+
When using Cursor's Composer:
|
|
295
|
+
|
|
296
|
+
1. **Start with context**: Always check indexed repositories first
|
|
297
|
+
2. **Natural language**: Use complete questions, not keywords
|
|
298
|
+
3. **Inline results**: Nia results appear directly in your code
|
|
299
|
+
4. **Multi-file**: Reference multiple files from search results
|
|
300
|
+
|
|
301
|
+
Example:
|
|
302
|
+
```
|
|
303
|
+
Composer: "How does authentication work in this Next.js app?"
|
|
304
|
+
[Nia searches indexed codebase and shows relevant files]
|
|
305
|
+
```"""
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def generate_tasks_section() -> str:
|
|
309
|
+
"""Generate tasks automation section"""
|
|
310
|
+
return """## Task Automation
|
|
311
|
+
|
|
312
|
+
Quick tasks are configured in `.vscode/tasks.json`:
|
|
313
|
+
|
|
314
|
+
- **Ctrl+Shift+P** → "Tasks: Run Task"
|
|
315
|
+
- Select "Nia: Index Repository" or other Nia tasks
|
|
316
|
+
- Follow prompts for input
|
|
317
|
+
|
|
318
|
+
Custom keyboard shortcuts can be added in keybindings.json."""
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def generate_terminal_section() -> str:
|
|
322
|
+
"""Generate terminal integration section"""
|
|
323
|
+
return """## Terminal Commands
|
|
324
|
+
|
|
325
|
+
Quick aliases for your terminal:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# Add to your shell profile (.bashrc, .zshrc, etc.)
|
|
329
|
+
alias nia-index='echo "index_repository"'
|
|
330
|
+
alias nia-search='echo "search_codebase"'
|
|
331
|
+
alias nia-list='echo "list_repositories"'
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Use these in the integrated terminal for quick access."""
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def create_profile_specific_file(
|
|
338
|
+
profile: str,
|
|
339
|
+
filename: str,
|
|
340
|
+
content: str,
|
|
341
|
+
target_dir: Path
|
|
342
|
+
) -> Optional[str]:
|
|
343
|
+
"""
|
|
344
|
+
Create a profile-specific file
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
profile: Profile name
|
|
348
|
+
filename: Target filename
|
|
349
|
+
content: File content
|
|
350
|
+
target_dir: Target directory
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Created file path or None
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
file_path = target_dir / filename
|
|
357
|
+
os.makedirs(file_path.parent, exist_ok=True)
|
|
358
|
+
file_path.write_text(content)
|
|
359
|
+
logger.info(f"Created {profile} file: {file_path}")
|
|
360
|
+
return str(file_path)
|
|
361
|
+
except Exception as e:
|
|
362
|
+
logger.error(f"Failed to create {filename} for {profile}: {e}")
|
|
363
|
+
return None
|
nia_mcp_server/server.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Nia MCP Proxy Server - Lightweight server that communicates with Nia API
|
|
3
3
|
"""
|
|
4
4
|
import os
|
|
5
5
|
import logging
|
|
@@ -12,6 +12,8 @@ from urllib.parse import urlparse
|
|
|
12
12
|
from mcp.server.fastmcp import FastMCP
|
|
13
13
|
from mcp.types import TextContent, Resource
|
|
14
14
|
from .api_client import NIAApiClient, APIError
|
|
15
|
+
from .project_init import initialize_nia_project
|
|
16
|
+
from .profiles import get_supported_profiles
|
|
15
17
|
from dotenv import load_dotenv
|
|
16
18
|
import json
|
|
17
19
|
|
|
@@ -167,7 +169,26 @@ async def search_codebase(
|
|
|
167
169
|
# Get all indexed repositories if not specified
|
|
168
170
|
if not repositories:
|
|
169
171
|
all_repos = await client.list_repositories()
|
|
170
|
-
|
|
172
|
+
|
|
173
|
+
# Ensure all_repos is a list and contains dictionaries
|
|
174
|
+
if not isinstance(all_repos, list):
|
|
175
|
+
logger.error(f"Unexpected type for all_repos: {type(all_repos)}")
|
|
176
|
+
return [TextContent(
|
|
177
|
+
type="text",
|
|
178
|
+
text="❌ Error retrieving repositories. The API returned an unexpected response."
|
|
179
|
+
)]
|
|
180
|
+
|
|
181
|
+
repositories = []
|
|
182
|
+
for repo in all_repos:
|
|
183
|
+
if isinstance(repo, dict) and repo.get("status") == "completed":
|
|
184
|
+
repo_name = repo.get("repository")
|
|
185
|
+
if repo_name:
|
|
186
|
+
repositories.append(repo_name)
|
|
187
|
+
else:
|
|
188
|
+
logger.warning(f"Repository missing 'repository' field: {repo}")
|
|
189
|
+
else:
|
|
190
|
+
logger.warning(f"Unexpected repository format: {type(repo)}, value: {repo}")
|
|
191
|
+
|
|
171
192
|
if not repositories:
|
|
172
193
|
return [TextContent(
|
|
173
194
|
type="text",
|
|
@@ -364,7 +385,17 @@ async def list_repositories() -> List[TextContent]:
|
|
|
364
385
|
|
|
365
386
|
for repo in repositories:
|
|
366
387
|
status_icon = "✅" if repo.get("status") == "completed" else "⏳"
|
|
367
|
-
|
|
388
|
+
|
|
389
|
+
# Show display name if available, otherwise show repository
|
|
390
|
+
display_name = repo.get("display_name")
|
|
391
|
+
repo_name = repo['repository']
|
|
392
|
+
|
|
393
|
+
if display_name:
|
|
394
|
+
lines.append(f"\n## {status_icon} {display_name}")
|
|
395
|
+
lines.append(f"- **Repository:** {repo_name}")
|
|
396
|
+
else:
|
|
397
|
+
lines.append(f"\n## {status_icon} {repo_name}")
|
|
398
|
+
|
|
368
399
|
lines.append(f"- **Branch:** {repo.get('branch', 'main')}")
|
|
369
400
|
lines.append(f"- **Status:** {repo.get('status', 'unknown')}")
|
|
370
401
|
if repo.get("indexed_at"):
|
|
@@ -559,7 +590,17 @@ async def list_documentation() -> List[TextContent]:
|
|
|
559
590
|
|
|
560
591
|
for source in sources:
|
|
561
592
|
status_icon = "✅" if source.get("status") == "completed" else "⏳"
|
|
562
|
-
|
|
593
|
+
|
|
594
|
+
# Show display name if available, otherwise show URL
|
|
595
|
+
display_name = source.get("display_name")
|
|
596
|
+
url = source.get('url', 'Unknown URL')
|
|
597
|
+
|
|
598
|
+
if display_name:
|
|
599
|
+
lines.append(f"\n## {status_icon} {display_name}")
|
|
600
|
+
lines.append(f"- **URL:** {url}")
|
|
601
|
+
else:
|
|
602
|
+
lines.append(f"\n## {status_icon} {url}")
|
|
603
|
+
|
|
563
604
|
lines.append(f"- **ID:** {source['id']}")
|
|
564
605
|
lines.append(f"- **Status:** {source.get('status', 'unknown')}")
|
|
565
606
|
lines.append(f"- **Type:** {source.get('source_type', 'web')}")
|
|
@@ -727,6 +768,100 @@ async def delete_repository(repository: str) -> List[TextContent]:
|
|
|
727
768
|
text=f"❌ Error deleting repository: {str(e)}"
|
|
728
769
|
)]
|
|
729
770
|
|
|
771
|
+
@mcp.tool()
|
|
772
|
+
async def rename_repository(repository: str, new_name: str) -> List[TextContent]:
|
|
773
|
+
"""
|
|
774
|
+
Rename an indexed repository for better organization.
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
repository: Repository in owner/repo format
|
|
778
|
+
new_name: New display name for the repository (1-100 characters)
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
Confirmation of rename operation
|
|
782
|
+
"""
|
|
783
|
+
try:
|
|
784
|
+
# Validate name length
|
|
785
|
+
if not new_name or len(new_name) > 100:
|
|
786
|
+
return [TextContent(
|
|
787
|
+
type="text",
|
|
788
|
+
text="❌ Display name must be between 1 and 100 characters."
|
|
789
|
+
)]
|
|
790
|
+
|
|
791
|
+
client = await ensure_api_client()
|
|
792
|
+
result = await client.rename_repository(repository, new_name)
|
|
793
|
+
|
|
794
|
+
if result.get("success"):
|
|
795
|
+
return [TextContent(
|
|
796
|
+
type="text",
|
|
797
|
+
text=f"✅ Successfully renamed repository '{repository}' to '{new_name}'"
|
|
798
|
+
)]
|
|
799
|
+
else:
|
|
800
|
+
return [TextContent(
|
|
801
|
+
type="text",
|
|
802
|
+
text=f"❌ Failed to rename repository: {result.get('message', 'Unknown error')}"
|
|
803
|
+
)]
|
|
804
|
+
|
|
805
|
+
except APIError as e:
|
|
806
|
+
logger.error(f"API Error renaming repository: {e}")
|
|
807
|
+
error_msg = f"❌ {str(e)}"
|
|
808
|
+
if e.status_code == 403 and "lifetime limit" in str(e).lower():
|
|
809
|
+
error_msg += "\n\n💡 Tip: You've reached the free tier limit. Upgrade to Pro for unlimited access."
|
|
810
|
+
return [TextContent(type="text", text=error_msg)]
|
|
811
|
+
except Exception as e:
|
|
812
|
+
logger.error(f"Error renaming repository: {e}")
|
|
813
|
+
return [TextContent(
|
|
814
|
+
type="text",
|
|
815
|
+
text=f"❌ Error renaming repository: {str(e)}"
|
|
816
|
+
)]
|
|
817
|
+
|
|
818
|
+
@mcp.tool()
|
|
819
|
+
async def rename_documentation(source_id: str, new_name: str) -> List[TextContent]:
|
|
820
|
+
"""
|
|
821
|
+
Rename a documentation source for better organization.
|
|
822
|
+
|
|
823
|
+
Args:
|
|
824
|
+
source_id: Documentation source ID
|
|
825
|
+
new_name: New display name for the documentation (1-100 characters)
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
Confirmation of rename operation
|
|
829
|
+
"""
|
|
830
|
+
try:
|
|
831
|
+
# Validate name length
|
|
832
|
+
if not new_name or len(new_name) > 100:
|
|
833
|
+
return [TextContent(
|
|
834
|
+
type="text",
|
|
835
|
+
text="❌ Display name must be between 1 and 100 characters."
|
|
836
|
+
)]
|
|
837
|
+
|
|
838
|
+
client = await ensure_api_client()
|
|
839
|
+
result = await client.rename_data_source(source_id, new_name)
|
|
840
|
+
|
|
841
|
+
if result.get("success"):
|
|
842
|
+
return [TextContent(
|
|
843
|
+
type="text",
|
|
844
|
+
text=f"✅ Successfully renamed documentation source to '{new_name}'"
|
|
845
|
+
)]
|
|
846
|
+
else:
|
|
847
|
+
return [TextContent(
|
|
848
|
+
type="text",
|
|
849
|
+
text=f"❌ Failed to rename documentation: {result.get('message', 'Unknown error')}"
|
|
850
|
+
)]
|
|
851
|
+
|
|
852
|
+
except APIError as e:
|
|
853
|
+
logger.error(f"API Error renaming documentation: {e}")
|
|
854
|
+
error_msg = f"❌ {str(e)}"
|
|
855
|
+
if e.status_code == 403 and "lifetime limit" in str(e).lower():
|
|
856
|
+
error_msg += "\n\n💡 Tip: You've reached the free tier limit. Upgrade to Pro for unlimited access."
|
|
857
|
+
return [TextContent(type="text", text=error_msg)]
|
|
858
|
+
except Exception as e:
|
|
859
|
+
logger.error(f"Error renaming documentation: {e}")
|
|
860
|
+
return [TextContent(
|
|
861
|
+
type="text",
|
|
862
|
+
text=f"❌ Error renaming documentation: {str(e)}"
|
|
863
|
+
)]
|
|
864
|
+
|
|
730
865
|
@mcp.tool()
|
|
731
866
|
async def nia_web_search(
|
|
732
867
|
query: str,
|
|
@@ -781,7 +916,7 @@ async def nia_web_search(
|
|
|
781
916
|
other_content = result.get("other_content", [])
|
|
782
917
|
|
|
783
918
|
# Format response to naturally guide next actions
|
|
784
|
-
response_text = f"## 🔍
|
|
919
|
+
response_text = f"## 🔍 Nia Web Search Results for: \"{query}\"\n\n"
|
|
785
920
|
|
|
786
921
|
if days_back:
|
|
787
922
|
response_text += f"*Showing results from the last {days_back} days*\n\n"
|
|
@@ -806,7 +941,7 @@ async def nia_web_search(
|
|
|
806
941
|
|
|
807
942
|
# Be more aggressive based on query specificity
|
|
808
943
|
if len(github_repos) == 1 or any(specific_word in query.lower() for specific_word in ["specific", "exact", "particular", "find me", "looking for"]):
|
|
809
|
-
response_text += "**🚀 RECOMMENDED ACTION - Index this repository with
|
|
944
|
+
response_text += "**🚀 RECOMMENDED ACTION - Index this repository with Nia:**\n"
|
|
810
945
|
response_text += f"```\nIndex {github_repos[0]['owner_repo']}\n```\n"
|
|
811
946
|
response_text += "✨ This will enable AI-powered code search, understanding, and analysis!\n\n"
|
|
812
947
|
else:
|
|
@@ -1075,6 +1210,162 @@ async def nia_deep_research_agent(
|
|
|
1075
1210
|
"Try simplifying your question or using the regular nia_web_search tool."
|
|
1076
1211
|
)]
|
|
1077
1212
|
|
|
1213
|
+
@mcp.tool()
|
|
1214
|
+
async def initialize_project(
|
|
1215
|
+
project_root: str,
|
|
1216
|
+
profiles: Optional[List[str]] = None
|
|
1217
|
+
) -> List[TextContent]:
|
|
1218
|
+
"""
|
|
1219
|
+
Initialize a NIA-enabled project with IDE-specific rules and configurations.
|
|
1220
|
+
|
|
1221
|
+
This tool sets up your project with NIA integration, creating configuration files
|
|
1222
|
+
and rules tailored to your IDE or editor. It enables AI assistants to better
|
|
1223
|
+
understand and work with NIA's knowledge search capabilities.
|
|
1224
|
+
|
|
1225
|
+
Args:
|
|
1226
|
+
project_root: Absolute path to the project root directory
|
|
1227
|
+
profiles: List of IDE profiles to set up (default: ["cursor"]).
|
|
1228
|
+
Options: cursor, vscode, claude, windsurf, cline, codex, zed, jetbrains, neovim, sublime
|
|
1229
|
+
|
|
1230
|
+
Returns:
|
|
1231
|
+
Status of the initialization with created files and next steps
|
|
1232
|
+
|
|
1233
|
+
Examples:
|
|
1234
|
+
- Basic: initialize_project("/path/to/project")
|
|
1235
|
+
- Multiple IDEs: initialize_project("/path/to/project", profiles=["cursor", "vscode"])
|
|
1236
|
+
- Specific IDE: initialize_project("/path/to/project", profiles=["windsurf"])
|
|
1237
|
+
"""
|
|
1238
|
+
try:
|
|
1239
|
+
# Validate project root
|
|
1240
|
+
project_path = Path(project_root)
|
|
1241
|
+
if not project_path.is_absolute():
|
|
1242
|
+
return [TextContent(
|
|
1243
|
+
type="text",
|
|
1244
|
+
text=f"❌ Error: project_root must be an absolute path. Got: {project_root}"
|
|
1245
|
+
)]
|
|
1246
|
+
|
|
1247
|
+
# Default to cursor profile if none specified
|
|
1248
|
+
if profiles is None:
|
|
1249
|
+
profiles = ["cursor"]
|
|
1250
|
+
|
|
1251
|
+
# Validate profiles
|
|
1252
|
+
supported = get_supported_profiles()
|
|
1253
|
+
invalid_profiles = [p for p in profiles if p not in supported]
|
|
1254
|
+
if invalid_profiles:
|
|
1255
|
+
return [TextContent(
|
|
1256
|
+
type="text",
|
|
1257
|
+
text=f"❌ Unknown profiles: {', '.join(invalid_profiles)}\n\n"
|
|
1258
|
+
f"Supported profiles: {', '.join(supported)}"
|
|
1259
|
+
)]
|
|
1260
|
+
|
|
1261
|
+
logger.info(f"Initializing NIA project at {project_root} with profiles: {profiles}")
|
|
1262
|
+
|
|
1263
|
+
# Initialize the project
|
|
1264
|
+
result = initialize_nia_project(
|
|
1265
|
+
project_root=project_root,
|
|
1266
|
+
profiles=profiles
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
if not result.get("success"):
|
|
1270
|
+
return [TextContent(
|
|
1271
|
+
type="text",
|
|
1272
|
+
text=f"❌ Failed to initialize project: {result.get('error', 'Unknown error')}"
|
|
1273
|
+
)]
|
|
1274
|
+
|
|
1275
|
+
# Format success response
|
|
1276
|
+
response_lines = [
|
|
1277
|
+
f"✅ Successfully initialized NIA project at: {project_root}",
|
|
1278
|
+
"",
|
|
1279
|
+
"## 📁 Created Files:",
|
|
1280
|
+
]
|
|
1281
|
+
|
|
1282
|
+
for file in result.get("files_created", []):
|
|
1283
|
+
response_lines.append(f"- {file}")
|
|
1284
|
+
|
|
1285
|
+
if result.get("profiles_initialized"):
|
|
1286
|
+
response_lines.extend([
|
|
1287
|
+
"",
|
|
1288
|
+
"## 🎨 Initialized Profiles:",
|
|
1289
|
+
])
|
|
1290
|
+
for profile in result["profiles_initialized"]:
|
|
1291
|
+
response_lines.append(f"- {profile}")
|
|
1292
|
+
|
|
1293
|
+
if result.get("warnings"):
|
|
1294
|
+
response_lines.extend([
|
|
1295
|
+
"",
|
|
1296
|
+
"## ⚠️ Warnings:",
|
|
1297
|
+
])
|
|
1298
|
+
for warning in result["warnings"]:
|
|
1299
|
+
response_lines.append(f"- {warning}")
|
|
1300
|
+
|
|
1301
|
+
if result.get("next_steps"):
|
|
1302
|
+
response_lines.extend([
|
|
1303
|
+
"",
|
|
1304
|
+
"## 🚀 Next Steps:",
|
|
1305
|
+
])
|
|
1306
|
+
for i, step in enumerate(result["next_steps"], 1):
|
|
1307
|
+
response_lines.append(f"{i}. {step}")
|
|
1308
|
+
|
|
1309
|
+
# Add profile-specific instructions
|
|
1310
|
+
response_lines.extend([
|
|
1311
|
+
"",
|
|
1312
|
+
"## 💡 Quick Start:",
|
|
1313
|
+
])
|
|
1314
|
+
|
|
1315
|
+
if "cursor" in profiles:
|
|
1316
|
+
response_lines.extend([
|
|
1317
|
+
"**For Cursor:**",
|
|
1318
|
+
"1. Restart Cursor to load the NIA MCP server",
|
|
1319
|
+
"2. Run `list_repositories` to verify connection",
|
|
1320
|
+
"3. Start indexing with `index_repository https://github.com/owner/repo`",
|
|
1321
|
+
""
|
|
1322
|
+
])
|
|
1323
|
+
|
|
1324
|
+
if "vscode" in profiles:
|
|
1325
|
+
response_lines.extend([
|
|
1326
|
+
"**For VSCode:**",
|
|
1327
|
+
"1. Reload the VSCode window (Cmd/Ctrl+R)",
|
|
1328
|
+
"2. Open command palette (Cmd/Ctrl+Shift+P)",
|
|
1329
|
+
"3. Run 'NIA: Index Repository' task",
|
|
1330
|
+
""
|
|
1331
|
+
])
|
|
1332
|
+
|
|
1333
|
+
if "claude" in profiles:
|
|
1334
|
+
response_lines.extend([
|
|
1335
|
+
"**For Claude Desktop:**",
|
|
1336
|
+
"1. The .claude directory has been created",
|
|
1337
|
+
"2. Claude will now understand NIA commands",
|
|
1338
|
+
"3. Try: 'Search for authentication patterns'",
|
|
1339
|
+
""
|
|
1340
|
+
])
|
|
1341
|
+
|
|
1342
|
+
# Add general tips
|
|
1343
|
+
response_lines.extend([
|
|
1344
|
+
"## 📚 Tips:",
|
|
1345
|
+
"- Use natural language for searches: 'How does X work?'",
|
|
1346
|
+
"- Index repositories before searching them",
|
|
1347
|
+
"- Use `nia_web_search` to discover new repositories",
|
|
1348
|
+
"- Check `list_repositories` to see what's already indexed",
|
|
1349
|
+
"",
|
|
1350
|
+
"Ready to supercharge your development with AI-powered code search! 🚀"
|
|
1351
|
+
])
|
|
1352
|
+
|
|
1353
|
+
return [TextContent(
|
|
1354
|
+
type="text",
|
|
1355
|
+
text="\n".join(response_lines)
|
|
1356
|
+
)]
|
|
1357
|
+
|
|
1358
|
+
except Exception as e:
|
|
1359
|
+
logger.error(f"Error in initialize_project tool: {e}")
|
|
1360
|
+
return [TextContent(
|
|
1361
|
+
type="text",
|
|
1362
|
+
text=f"❌ Error initializing project: {str(e)}\n\n"
|
|
1363
|
+
"Please check:\n"
|
|
1364
|
+
"- The project_root path is correct and accessible\n"
|
|
1365
|
+
"- You have write permissions to the directory\n"
|
|
1366
|
+
"- The NIA MCP server is properly installed"
|
|
1367
|
+
)]
|
|
1368
|
+
|
|
1078
1369
|
# Resources
|
|
1079
1370
|
|
|
1080
1371
|
# Note: FastMCP doesn't have list_resources or read_resource decorators
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
nia_mcp_server/__init__.py,sha256=xrM4qDGEvxPXQYqVksy08mhmfAj0wIljhJPnYZYHea8,84
|
|
2
|
+
nia_mcp_server/__main__.py,sha256=XY11ESL4hctu-BBgtPATFZyd1o-O7wE7y-UOSoNs-hw,152
|
|
3
|
+
nia_mcp_server/api_client.py,sha256=dk9FkMljuekyjIw7Ij3athsoVNnu7E2b1l2xi2XtB1Y,24255
|
|
4
|
+
nia_mcp_server/profiles.py,sha256=2DD8PFRr5Ij4IK4sPUz0mH8aKjkrEtkKLC1R0iki2bA,7221
|
|
5
|
+
nia_mcp_server/project_init.py,sha256=Mtxvlg7FfdWumc2AdMXifht3V1b6sZvX4b7USbbwMvw,7152
|
|
6
|
+
nia_mcp_server/rule_transformer.py,sha256=wCxoQ1Kl_rI9mUFnh9kG5iCXYU4QInrmFQOReZfAFVo,11000
|
|
7
|
+
nia_mcp_server/server.py,sha256=B6aLTXkX2Mq5eBqDCV_WTumqtSm4bzDf2bHPVyVsgcA,59579
|
|
8
|
+
nia_mcp_server-1.0.6.dist-info/METADATA,sha256=hN6ls8aDU9h83JuQ3AZ8VfLBsrKIVLGt-annSRXHbS8,6910
|
|
9
|
+
nia_mcp_server-1.0.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
nia_mcp_server-1.0.6.dist-info/entry_points.txt,sha256=V74FQEp48pfWxPCl7B9mihtqvIJNVjCSbRfCz4ww77I,64
|
|
11
|
+
nia_mcp_server-1.0.6.dist-info/licenses/LICENSE,sha256=5jUPBVkZEicxSAZ91jOO7i8zXEPAHS6M0w8SSf6DftI,1071
|
|
12
|
+
nia_mcp_server-1.0.6.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
nia_mcp_server/__init__.py,sha256=P3fqcVlJ9fp8ojx6xaX5lrdUNO8rkc8bGYEv8Qm3SAw,84
|
|
2
|
-
nia_mcp_server/__main__.py,sha256=XY11ESL4hctu-BBgtPATFZyd1o-O7wE7y-UOSoNs-hw,152
|
|
3
|
-
nia_mcp_server/api_client.py,sha256=E7x6W2zvFrfyR8yfk8X4z6WGF6wQk_EP4TfQhZAV_4w,20932
|
|
4
|
-
nia_mcp_server/server.py,sha256=UJaP4Q7xlZt_xW4bDD17AgYhwAZ2sZaoTZ6nf_2mNOE,48694
|
|
5
|
-
nia_mcp_server-1.0.5.dist-info/METADATA,sha256=lyWMV-006_5q1vb_aEnHWwUE8lAYcZMpbL9ypEi5kiI,6910
|
|
6
|
-
nia_mcp_server-1.0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
-
nia_mcp_server-1.0.5.dist-info/entry_points.txt,sha256=V74FQEp48pfWxPCl7B9mihtqvIJNVjCSbRfCz4ww77I,64
|
|
8
|
-
nia_mcp_server-1.0.5.dist-info/licenses/LICENSE,sha256=5jUPBVkZEicxSAZ91jOO7i8zXEPAHS6M0w8SSf6DftI,1071
|
|
9
|
-
nia_mcp_server-1.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|