nia-mcp-server 1.0.22__tar.gz → 1.0.24__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.
Potentially problematic release.
This version of nia-mcp-server might be problematic. Click here for more details.
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/PKG-INFO +1 -1
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/pyproject.toml +1 -1
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/__init__.py +2 -1
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/api_client.py +173 -2
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/server.py +674 -173
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/.gitignore +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/ARCHITECTURE.md +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/LICENSE +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/README.md +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/nia_analytics.log +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/nia_mcp_server.log +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/__main__.py +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/assets/rules/claude_rules.md +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/assets/rules/cursor_rules.md +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/assets/rules/nia_rules.md +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/assets/rules/vscode_rules.md +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/assets/rules/windsurf_rules.md +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/cli.py +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/profiles.py +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/project_init.py +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/rule_transformer.py +0 -0
- {nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/setup.py +0 -0
|
@@ -28,7 +28,7 @@ class NIAApiClient:
|
|
|
28
28
|
self.client = httpx.AsyncClient(
|
|
29
29
|
headers={
|
|
30
30
|
"Authorization": f"Bearer {api_key}",
|
|
31
|
-
"User-Agent": "nia-mcp-server/1.0.
|
|
31
|
+
"User-Agent": "nia-mcp-server/1.0.24",
|
|
32
32
|
"Content-Type": "application/json"
|
|
33
33
|
},
|
|
34
34
|
timeout=720.0 # 12 minute timeout for deep research operations
|
|
@@ -913,4 +913,175 @@ class NIAApiClient:
|
|
|
913
913
|
except httpx.HTTPStatusError as e:
|
|
914
914
|
raise self._handle_api_error(e)
|
|
915
915
|
except Exception as e:
|
|
916
|
-
raise APIError(f"Failed to read package file: {str(e)}")
|
|
916
|
+
raise APIError(f"Failed to read package file: {str(e)}")
|
|
917
|
+
|
|
918
|
+
# ========================================================================
|
|
919
|
+
# CONTEXT SHARING METHODS
|
|
920
|
+
# ========================================================================
|
|
921
|
+
|
|
922
|
+
async def save_context(
|
|
923
|
+
self,
|
|
924
|
+
title: str,
|
|
925
|
+
summary: str,
|
|
926
|
+
content: str,
|
|
927
|
+
agent_source: str,
|
|
928
|
+
tags: List[str] = None,
|
|
929
|
+
metadata: Dict[str, Any] = None,
|
|
930
|
+
nia_references: Optional[Dict[str, Any]] = None,
|
|
931
|
+
edited_files: Optional[List[Dict[str, Any]]] = None
|
|
932
|
+
) -> Dict[str, Any]:
|
|
933
|
+
"""Save a conversation context for cross-agent sharing."""
|
|
934
|
+
try:
|
|
935
|
+
payload = {
|
|
936
|
+
"title": title,
|
|
937
|
+
"summary": summary,
|
|
938
|
+
"content": content,
|
|
939
|
+
"agent_source": agent_source,
|
|
940
|
+
"tags": tags or [],
|
|
941
|
+
"metadata": metadata or {}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
# Add new structured fields if provided
|
|
945
|
+
if nia_references is not None:
|
|
946
|
+
payload["nia_references"] = nia_references
|
|
947
|
+
if edited_files is not None:
|
|
948
|
+
payload["edited_files"] = edited_files
|
|
949
|
+
|
|
950
|
+
response = await self.client.post(
|
|
951
|
+
f"{self.base_url}/v2/contexts",
|
|
952
|
+
json=payload
|
|
953
|
+
)
|
|
954
|
+
response.raise_for_status()
|
|
955
|
+
return response.json()
|
|
956
|
+
|
|
957
|
+
except httpx.HTTPStatusError as e:
|
|
958
|
+
raise self._handle_api_error(e)
|
|
959
|
+
except Exception as e:
|
|
960
|
+
raise APIError(f"Failed to save context: {str(e)}")
|
|
961
|
+
|
|
962
|
+
async def list_contexts(
|
|
963
|
+
self,
|
|
964
|
+
limit: int = 20,
|
|
965
|
+
offset: int = 0,
|
|
966
|
+
tags: Optional[str] = None,
|
|
967
|
+
agent_source: Optional[str] = None
|
|
968
|
+
) -> Dict[str, Any]:
|
|
969
|
+
"""List user's conversation contexts with pagination and filtering."""
|
|
970
|
+
try:
|
|
971
|
+
params = {
|
|
972
|
+
"limit": limit,
|
|
973
|
+
"offset": offset
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if tags:
|
|
977
|
+
params["tags"] = tags
|
|
978
|
+
if agent_source:
|
|
979
|
+
params["agent_source"] = agent_source
|
|
980
|
+
|
|
981
|
+
response = await self.client.get(
|
|
982
|
+
f"{self.base_url}/v2/contexts",
|
|
983
|
+
params=params
|
|
984
|
+
)
|
|
985
|
+
response.raise_for_status()
|
|
986
|
+
return response.json()
|
|
987
|
+
|
|
988
|
+
except httpx.HTTPStatusError as e:
|
|
989
|
+
raise self._handle_api_error(e)
|
|
990
|
+
except Exception as e:
|
|
991
|
+
raise APIError(f"Failed to list contexts: {str(e)}")
|
|
992
|
+
|
|
993
|
+
async def get_context(self, context_id: str) -> Dict[str, Any]:
|
|
994
|
+
"""Get a specific conversation context by ID."""
|
|
995
|
+
try:
|
|
996
|
+
response = await self.client.get(f"{self.base_url}/v2/contexts/{context_id}")
|
|
997
|
+
response.raise_for_status()
|
|
998
|
+
return response.json()
|
|
999
|
+
|
|
1000
|
+
except httpx.HTTPStatusError as e:
|
|
1001
|
+
if e.response.status_code == 404:
|
|
1002
|
+
return None
|
|
1003
|
+
raise self._handle_api_error(e)
|
|
1004
|
+
except Exception as e:
|
|
1005
|
+
raise APIError(f"Failed to get context: {str(e)}")
|
|
1006
|
+
|
|
1007
|
+
async def update_context(
|
|
1008
|
+
self,
|
|
1009
|
+
context_id: str,
|
|
1010
|
+
title: Optional[str] = None,
|
|
1011
|
+
summary: Optional[str] = None,
|
|
1012
|
+
content: Optional[str] = None,
|
|
1013
|
+
tags: Optional[List[str]] = None,
|
|
1014
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
1015
|
+
) -> Dict[str, Any]:
|
|
1016
|
+
"""Update an existing conversation context."""
|
|
1017
|
+
try:
|
|
1018
|
+
payload = {}
|
|
1019
|
+
|
|
1020
|
+
if title is not None:
|
|
1021
|
+
payload["title"] = title
|
|
1022
|
+
if summary is not None:
|
|
1023
|
+
payload["summary"] = summary
|
|
1024
|
+
if content is not None:
|
|
1025
|
+
payload["content"] = content
|
|
1026
|
+
if tags is not None:
|
|
1027
|
+
payload["tags"] = tags
|
|
1028
|
+
if metadata is not None:
|
|
1029
|
+
payload["metadata"] = metadata
|
|
1030
|
+
|
|
1031
|
+
response = await self.client.put(
|
|
1032
|
+
f"{self.base_url}/v2/contexts/{context_id}",
|
|
1033
|
+
json=payload
|
|
1034
|
+
)
|
|
1035
|
+
response.raise_for_status()
|
|
1036
|
+
return response.json()
|
|
1037
|
+
|
|
1038
|
+
except httpx.HTTPStatusError as e:
|
|
1039
|
+
raise self._handle_api_error(e)
|
|
1040
|
+
except Exception as e:
|
|
1041
|
+
raise APIError(f"Failed to update context: {str(e)}")
|
|
1042
|
+
|
|
1043
|
+
async def delete_context(self, context_id: str) -> bool:
|
|
1044
|
+
"""Delete a conversation context."""
|
|
1045
|
+
try:
|
|
1046
|
+
response = await self.client.delete(f"{self.base_url}/v2/contexts/{context_id}")
|
|
1047
|
+
response.raise_for_status()
|
|
1048
|
+
return True
|
|
1049
|
+
|
|
1050
|
+
except httpx.HTTPStatusError as e:
|
|
1051
|
+
if e.response.status_code == 404:
|
|
1052
|
+
return False
|
|
1053
|
+
raise self._handle_api_error(e)
|
|
1054
|
+
except Exception as e:
|
|
1055
|
+
logger.error(f"Failed to delete context: {e}")
|
|
1056
|
+
return False
|
|
1057
|
+
|
|
1058
|
+
async def search_contexts(
|
|
1059
|
+
self,
|
|
1060
|
+
query: str,
|
|
1061
|
+
limit: int = 20,
|
|
1062
|
+
tags: Optional[str] = None,
|
|
1063
|
+
agent_source: Optional[str] = None
|
|
1064
|
+
) -> Dict[str, Any]:
|
|
1065
|
+
"""Search conversation contexts by content, title, or summary."""
|
|
1066
|
+
try:
|
|
1067
|
+
params = {
|
|
1068
|
+
"q": query,
|
|
1069
|
+
"limit": limit
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if tags:
|
|
1073
|
+
params["tags"] = tags
|
|
1074
|
+
if agent_source:
|
|
1075
|
+
params["agent_source"] = agent_source
|
|
1076
|
+
|
|
1077
|
+
response = await self.client.get(
|
|
1078
|
+
f"{self.base_url}/v2/contexts/search",
|
|
1079
|
+
params=params
|
|
1080
|
+
)
|
|
1081
|
+
response.raise_for_status()
|
|
1082
|
+
return response.json()
|
|
1083
|
+
|
|
1084
|
+
except httpx.HTTPStatusError as e:
|
|
1085
|
+
raise self._handle_api_error(e)
|
|
1086
|
+
except Exception as e:
|
|
1087
|
+
raise APIError(f"Failed to search contexts: {str(e)}")
|
|
@@ -335,18 +335,19 @@ async def search_documentation(
|
|
|
335
335
|
|
|
336
336
|
Args:
|
|
337
337
|
query: Natural language search query. Don't just use keywords or unstrctured query, make a comprehensive question to get the best results possible.
|
|
338
|
-
sources: List of documentation identifiers to search.
|
|
338
|
+
sources: List of documentation identifiers to search. Preferred format is UUID, but also supports:
|
|
339
|
+
- Source UUIDs (e.g., "550e8400-e29b-41d4-a716-446655440000") - RECOMMENDED
|
|
339
340
|
- Display names (e.g., "Vercel AI SDK - Core")
|
|
340
341
|
- URLs (e.g., "https://sdk.vercel.ai/docs")
|
|
341
|
-
- Source IDs (UUID format for backwards compatibility)
|
|
342
342
|
include_sources: Whether to include source references in results
|
|
343
343
|
|
|
344
344
|
Returns:
|
|
345
345
|
Search results with relevant documentation excerpts
|
|
346
346
|
|
|
347
347
|
Important:
|
|
348
|
-
-
|
|
349
|
-
-
|
|
348
|
+
- UUIDs are the preferred identifier format for best performance
|
|
349
|
+
- Use `list_documentation` tool to see available sources and their UUIDs
|
|
350
|
+
- Display names and URLs are also supported for convenience
|
|
350
351
|
"""
|
|
351
352
|
try:
|
|
352
353
|
client = await ensure_api_client()
|
|
@@ -356,20 +357,17 @@ async def search_documentation(
|
|
|
356
357
|
return [TextContent(
|
|
357
358
|
type="text",
|
|
358
359
|
text="📚 **Please specify which documentation sources to search:**\n\n"
|
|
359
|
-
"1. Use `list_documentation` to see available sources\n"
|
|
360
|
-
"2. Then call `search_documentation(\"your query\", [\"
|
|
361
|
-
"**
|
|
360
|
+
"1. Use `list_documentation` to see available sources and their UUIDs\n"
|
|
361
|
+
"2. Then call `search_documentation(\"your query\", [\"uuid1\", \"uuid2\"])`\n\n"
|
|
362
|
+
"**Supported identifier formats (UUIDs preferred):**\n"
|
|
363
|
+
"- UUIDs: `\"550e8400-e29b-41d4-a716-446655440000\"` - RECOMMENDED\n"
|
|
362
364
|
"- Display names: `\"Vercel AI SDK - Core\"`\n"
|
|
363
|
-
"- URLs: `\"https://docs.trynia.ai/\"`\n"
|
|
364
|
-
"
|
|
365
|
-
"**Example:**\n"
|
|
365
|
+
"- URLs: `\"https://docs.trynia.ai/\"`\n\n"
|
|
366
|
+
"**Example (preferred):**\n"
|
|
366
367
|
"```\n"
|
|
367
|
-
"search_documentation(\"API reference\", [\"
|
|
368
|
+
"search_documentation(\"API reference\", [\"550e8400-e29b-41d4-a716-446655440000\"])\n"
|
|
368
369
|
"```\n\n"
|
|
369
|
-
"**📌 Tip:**
|
|
370
|
-
"```\n"
|
|
371
|
-
"search_documentation(\"query\", [\"Display Name\", \"https://docs.example.com/\"])\n"
|
|
372
|
-
"```"
|
|
370
|
+
"**📌 Tip:** UUIDs provide best performance and reliability"
|
|
373
371
|
)]
|
|
374
372
|
|
|
375
373
|
# Build messages for the query
|
|
@@ -847,7 +845,7 @@ async def rename_resource(
|
|
|
847
845
|
resource_type: Type of resource - "repository" or "documentation"
|
|
848
846
|
identifier:
|
|
849
847
|
- For repository: Repository in owner/repo format (e.g., "facebook/react")
|
|
850
|
-
- For documentation:
|
|
848
|
+
- For documentation: UUID preferred, also supports display name or URL (e.g., "550e8400-e29b-41d4-a716-446655440000", "Vercel AI SDK - Core", or "https://docs.trynia.ai/")
|
|
851
849
|
new_name: New display name for the resource (1-100 characters)
|
|
852
850
|
|
|
853
851
|
Returns:
|
|
@@ -855,7 +853,7 @@ async def rename_resource(
|
|
|
855
853
|
|
|
856
854
|
Examples:
|
|
857
855
|
- rename_resource("repository", "facebook/react", "React Framework")
|
|
858
|
-
- rename_resource("documentation", "
|
|
856
|
+
- rename_resource("documentation", "550e8400-e29b-41d4-a716-446655440000", "Python Official Docs")
|
|
859
857
|
- rename_resource("documentation", "https://docs.trynia.ai/", "NIA Documentation")
|
|
860
858
|
"""
|
|
861
859
|
try:
|
|
@@ -918,14 +916,14 @@ async def delete_resource(
|
|
|
918
916
|
resource_type: Type of resource - "repository" or "documentation"
|
|
919
917
|
identifier:
|
|
920
918
|
- For repository: Repository in owner/repo format (e.g., "facebook/react")
|
|
921
|
-
- For documentation:
|
|
919
|
+
- For documentation: UUID preferred, also supports display name or URL (e.g., "550e8400-e29b-41d4-a716-446655440000", "Vercel AI SDK - Core", or "https://docs.trynia.ai/")
|
|
922
920
|
|
|
923
921
|
Returns:
|
|
924
922
|
Confirmation of deletion
|
|
925
923
|
|
|
926
924
|
Examples:
|
|
927
925
|
- delete_resource("repository", "facebook/react")
|
|
928
|
-
- delete_resource("documentation", "
|
|
926
|
+
- delete_resource("documentation", "550e8400-e29b-41d4-a716-446655440000")
|
|
929
927
|
- delete_resource("documentation", "https://docs.trynia.ai/")
|
|
930
928
|
"""
|
|
931
929
|
try:
|
|
@@ -2828,160 +2826,6 @@ async def nia_package_search_read_file(
|
|
|
2828
2826
|
f"- The line range is valid (1-based, max 200 lines)"
|
|
2829
2827
|
)]
|
|
2830
2828
|
|
|
2831
|
-
@mcp.tool()
|
|
2832
|
-
async def visualize_codebase(
|
|
2833
|
-
repository: str
|
|
2834
|
-
) -> List[TextContent]:
|
|
2835
|
-
"""
|
|
2836
|
-
Open the graph visualization for an indexed repository in a browser.
|
|
2837
|
-
|
|
2838
|
-
This tool launches a browser with the interactive graph visualization
|
|
2839
|
-
that shows the code structure, relationships, and dependencies of
|
|
2840
|
-
the indexed codebase.
|
|
2841
|
-
|
|
2842
|
-
Args:
|
|
2843
|
-
repository: Repository in owner/repo format (e.g., "facebook/react")
|
|
2844
|
-
|
|
2845
|
-
Returns:
|
|
2846
|
-
Status message with the URL that was opened
|
|
2847
|
-
|
|
2848
|
-
Examples:
|
|
2849
|
-
- visualize_codebase("facebook/react")
|
|
2850
|
-
- visualize_codebase("langchain-ai/langchain")
|
|
2851
|
-
"""
|
|
2852
|
-
try:
|
|
2853
|
-
client = await ensure_api_client()
|
|
2854
|
-
|
|
2855
|
-
logger.info(f"Looking up repository: {repository}")
|
|
2856
|
-
|
|
2857
|
-
# List all repositories to find the matching one
|
|
2858
|
-
repositories = await client.list_repositories()
|
|
2859
|
-
|
|
2860
|
-
# Find the repository by name
|
|
2861
|
-
matching_repo = None
|
|
2862
|
-
for repo in repositories:
|
|
2863
|
-
if repo.get("repository") == repository:
|
|
2864
|
-
matching_repo = repo
|
|
2865
|
-
break
|
|
2866
|
-
|
|
2867
|
-
if not matching_repo:
|
|
2868
|
-
# Try case-insensitive match as fallback
|
|
2869
|
-
repository_lower = repository.lower()
|
|
2870
|
-
for repo in repositories:
|
|
2871
|
-
if repo.get("repository", "").lower() == repository_lower:
|
|
2872
|
-
matching_repo = repo
|
|
2873
|
-
break
|
|
2874
|
-
|
|
2875
|
-
if not matching_repo:
|
|
2876
|
-
return [TextContent(
|
|
2877
|
-
type="text",
|
|
2878
|
-
text=f"❌ Repository '{repository}' not found.\n\n"
|
|
2879
|
-
f"Available repositories:\n" +
|
|
2880
|
-
"\n".join(f"- {r.get('repository')}" for r in repositories if r.get('repository')) +
|
|
2881
|
-
"\n\nUse `list_repositories` to see all indexed repositories."
|
|
2882
|
-
)]
|
|
2883
|
-
|
|
2884
|
-
# Check if the repository is fully indexed
|
|
2885
|
-
status = matching_repo.get("status", "unknown")
|
|
2886
|
-
# Use the actual project ID if available, fall back to repository_id
|
|
2887
|
-
repository_id = matching_repo.get("id") or matching_repo.get("repository_id")
|
|
2888
|
-
|
|
2889
|
-
if not repository_id:
|
|
2890
|
-
return [TextContent(
|
|
2891
|
-
type="text",
|
|
2892
|
-
text=f"❌ No repository ID found for '{repository}'. This may be a data inconsistency."
|
|
2893
|
-
)]
|
|
2894
|
-
|
|
2895
|
-
if status != "completed":
|
|
2896
|
-
warning_msg = f"⚠️ Note: Repository '{repository}' is currently {status}.\n"
|
|
2897
|
-
if status == "indexing":
|
|
2898
|
-
warning_msg += "The visualization may show incomplete data.\n\n"
|
|
2899
|
-
elif status == "error":
|
|
2900
|
-
error_msg = matching_repo.get("error", "Unknown error")
|
|
2901
|
-
warning_msg += f"Error: {error_msg}\n\n"
|
|
2902
|
-
else:
|
|
2903
|
-
warning_msg += "The visualization may not be available.\n\n"
|
|
2904
|
-
else:
|
|
2905
|
-
warning_msg = ""
|
|
2906
|
-
|
|
2907
|
-
# Determine the base URL based on the API URL
|
|
2908
|
-
api_base_url = client.base_url
|
|
2909
|
-
if "localhost" in api_base_url or "127.0.0.1" in api_base_url:
|
|
2910
|
-
# Local development
|
|
2911
|
-
app_base_url = "http://localhost:3000"
|
|
2912
|
-
else:
|
|
2913
|
-
# Production
|
|
2914
|
-
app_base_url = "https://app.trynia.ai"
|
|
2915
|
-
|
|
2916
|
-
# Construct the visualization URL
|
|
2917
|
-
visualization_url = f"{app_base_url}/visualize/{repository_id}"
|
|
2918
|
-
|
|
2919
|
-
# Try to open the browser
|
|
2920
|
-
try:
|
|
2921
|
-
webbrowser.open(visualization_url)
|
|
2922
|
-
browser_opened = True
|
|
2923
|
-
open_msg = "✅ Opening graph visualization in your default browser..."
|
|
2924
|
-
except Exception as e:
|
|
2925
|
-
logger.warning(f"Failed to open browser: {e}")
|
|
2926
|
-
browser_opened = False
|
|
2927
|
-
open_msg = "⚠️ Could not automatically open browser."
|
|
2928
|
-
|
|
2929
|
-
# Format the response
|
|
2930
|
-
response_lines = [
|
|
2931
|
-
f"# Graph Visualization: {repository}",
|
|
2932
|
-
"",
|
|
2933
|
-
warning_msg if warning_msg else "",
|
|
2934
|
-
open_msg,
|
|
2935
|
-
"",
|
|
2936
|
-
f"**URL:** {visualization_url}",
|
|
2937
|
-
"",
|
|
2938
|
-
]
|
|
2939
|
-
|
|
2940
|
-
if matching_repo.get("display_name"):
|
|
2941
|
-
response_lines.append(f"**Display Name:** {matching_repo['display_name']}")
|
|
2942
|
-
|
|
2943
|
-
response_lines.extend([
|
|
2944
|
-
f"**Branch:** {matching_repo.get('branch', 'main')}",
|
|
2945
|
-
f"**Status:** {status}",
|
|
2946
|
-
"",
|
|
2947
|
-
"## Features Available:",
|
|
2948
|
-
"- 🔍 Interactive force-directed graph",
|
|
2949
|
-
"- 🎨 Color-coded node types (functions, classes, files, etc.)",
|
|
2950
|
-
"- 🔗 Relationship visualization (calls, imports, inherits, etc.)",
|
|
2951
|
-
"- 💬 Click on any node to chat with that specific code element",
|
|
2952
|
-
"- 🔎 Search and filter capabilities",
|
|
2953
|
-
"- 📊 Graph statistics and insights"
|
|
2954
|
-
])
|
|
2955
|
-
|
|
2956
|
-
if not browser_opened:
|
|
2957
|
-
response_lines.extend([
|
|
2958
|
-
"",
|
|
2959
|
-
"**Manual Access:**",
|
|
2960
|
-
f"Copy and paste this URL into your browser: {visualization_url}"
|
|
2961
|
-
])
|
|
2962
|
-
|
|
2963
|
-
return [TextContent(
|
|
2964
|
-
type="text",
|
|
2965
|
-
text="\n".join(response_lines)
|
|
2966
|
-
)]
|
|
2967
|
-
|
|
2968
|
-
except APIError as e:
|
|
2969
|
-
logger.error(f"API Error in visualize_codebase: {e}")
|
|
2970
|
-
if e.status_code == 403 or "free tier limit" in str(e).lower():
|
|
2971
|
-
return [TextContent(
|
|
2972
|
-
type="text",
|
|
2973
|
-
text=f"❌ {str(e)}\n\n💡 Tip: Upgrade to Pro at https://trynia.ai/billing for unlimited access."
|
|
2974
|
-
)]
|
|
2975
|
-
else:
|
|
2976
|
-
return [TextContent(type="text", text=f"❌ {str(e)}")]
|
|
2977
|
-
except Exception as e:
|
|
2978
|
-
logger.error(f"Error in visualize_codebase: {e}")
|
|
2979
|
-
return [TextContent(
|
|
2980
|
-
type="text",
|
|
2981
|
-
text=f"❌ Error opening visualization: {str(e)}"
|
|
2982
|
-
)]
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
2829
|
@mcp.tool()
|
|
2986
2830
|
async def nia_bug_report(
|
|
2987
2831
|
description: str,
|
|
@@ -3087,6 +2931,663 @@ async def nia_bug_report(
|
|
|
3087
2931
|
)
|
|
3088
2932
|
]
|
|
3089
2933
|
|
|
2934
|
+
# Context Sharing Tools
|
|
2935
|
+
|
|
2936
|
+
@mcp.tool()
|
|
2937
|
+
async def save_context(
|
|
2938
|
+
title: str,
|
|
2939
|
+
summary: str,
|
|
2940
|
+
content: str,
|
|
2941
|
+
agent_source: str,
|
|
2942
|
+
tags: Optional[List[str]] = None,
|
|
2943
|
+
metadata: Optional[dict] = None,
|
|
2944
|
+
nia_references: Optional[dict] = None,
|
|
2945
|
+
edited_files: Optional[List[dict]] = None
|
|
2946
|
+
) -> List[TextContent]:
|
|
2947
|
+
"""
|
|
2948
|
+
Save a conversation context for cross-agent sharing.
|
|
2949
|
+
|
|
2950
|
+
This tool enables agents to save conversation contexts that can be shared
|
|
2951
|
+
with other AI agents, creating seamless handoffs between different coding
|
|
2952
|
+
environments (e.g., Cursor → Claude Code).
|
|
2953
|
+
|
|
2954
|
+
Args:
|
|
2955
|
+
title: A descriptive title for the context (1-200 characters)
|
|
2956
|
+
summary: Brief summary of the conversation (10-1000 characters)
|
|
2957
|
+
content: Full conversation context - the agent should compact the conversation history but keep all important parts togethers, as well as code snippets. No excuses.
|
|
2958
|
+
agent_source: Which agent is creating this context (e.g., "cursor", "claude-code", "windsurf")
|
|
2959
|
+
tags: Optional list of searchable tags
|
|
2960
|
+
metadata: Optional metadata like file paths, repositories discussed, etc.
|
|
2961
|
+
nia_references: Structured data about NIA resources used during conversation
|
|
2962
|
+
Format: {
|
|
2963
|
+
"indexed_resources": [{"identifier": "owner/repo", "resource_type": "repository", "purpose": "Used for authentication patterns"}],
|
|
2964
|
+
"search_queries": [{"query": "JWT implementation", "query_type": "codebase", "resources_searched": ["owner/repo"], "key_findings": "Found JWT utils in auth folder"}],
|
|
2965
|
+
"session_summary": "Used NIA to explore authentication patterns and API design"
|
|
2966
|
+
}
|
|
2967
|
+
edited_files: List of files that were modified during conversation
|
|
2968
|
+
Format: [{"file_path": "src/auth.ts", "operation": "modified", "changes_description": "Added JWT validation", "key_changes": ["Added validate() function"]}]
|
|
2969
|
+
|
|
2970
|
+
Returns:
|
|
2971
|
+
Confirmation of successful context save with context ID
|
|
2972
|
+
|
|
2973
|
+
Example:
|
|
2974
|
+
save_context(
|
|
2975
|
+
title="Streaming AI SDK Implementation",
|
|
2976
|
+
summary="Planning conversation about implementing streaming responses with AI SDK",
|
|
2977
|
+
content="User asked about implementing streaming... [agent should include conversation]",
|
|
2978
|
+
agent_source="cursor",
|
|
2979
|
+
tags=["streaming", "ai-sdk", "implementation"],
|
|
2980
|
+
nia_references={
|
|
2981
|
+
"indexed_resources": [{"identifier": "vercel/ai", "resource_type": "repository", "purpose": "Reference for streaming implementation"}],
|
|
2982
|
+
"search_queries": [{"query": "streaming API", "query_type": "documentation", "key_findings": "Found useChat hook with streaming"}]
|
|
2983
|
+
},
|
|
2984
|
+
edited_files=[{"file_path": "src/chat.ts", "operation": "created", "changes_description": "Added streaming chat component"}]
|
|
2985
|
+
)
|
|
2986
|
+
"""
|
|
2987
|
+
try:
|
|
2988
|
+
# Validate input parameters
|
|
2989
|
+
if not title or not title.strip():
|
|
2990
|
+
return [TextContent(type="text", text="❌ Error: Title is required")]
|
|
2991
|
+
|
|
2992
|
+
if len(title) > 200:
|
|
2993
|
+
return [TextContent(type="text", text="❌ Error: Title must be 200 characters or less")]
|
|
2994
|
+
|
|
2995
|
+
if not summary or len(summary) < 10:
|
|
2996
|
+
return [TextContent(type="text", text="❌ Error: Summary must be at least 10 characters")]
|
|
2997
|
+
|
|
2998
|
+
if len(summary) > 1000:
|
|
2999
|
+
return [TextContent(type="text", text="❌ Error: Summary must be 1000 characters or less")]
|
|
3000
|
+
|
|
3001
|
+
if not content or len(content) < 50:
|
|
3002
|
+
return [TextContent(type="text", text="❌ Error: Content must be at least 50 characters")]
|
|
3003
|
+
|
|
3004
|
+
if not agent_source or not agent_source.strip():
|
|
3005
|
+
return [TextContent(type="text", text="❌ Error: Agent source is required")]
|
|
3006
|
+
|
|
3007
|
+
client = await ensure_api_client()
|
|
3008
|
+
|
|
3009
|
+
logger.info(f"Saving context: title='{title}', agent={agent_source}, content_length={len(content)}")
|
|
3010
|
+
|
|
3011
|
+
result = await client.save_context(
|
|
3012
|
+
title=title.strip(),
|
|
3013
|
+
summary=summary.strip(),
|
|
3014
|
+
content=content,
|
|
3015
|
+
agent_source=agent_source.strip(),
|
|
3016
|
+
tags=tags or [],
|
|
3017
|
+
metadata=metadata or {},
|
|
3018
|
+
nia_references=nia_references,
|
|
3019
|
+
edited_files=edited_files or []
|
|
3020
|
+
)
|
|
3021
|
+
|
|
3022
|
+
context_id = result.get("id")
|
|
3023
|
+
|
|
3024
|
+
return [TextContent(
|
|
3025
|
+
type="text",
|
|
3026
|
+
text=f"✅ **Context Saved Successfully!**\n\n"
|
|
3027
|
+
f"🆔 **Context ID:** `{context_id}`\n"
|
|
3028
|
+
f"📝 **Title:** {title}\n"
|
|
3029
|
+
f"🤖 **Source Agent:** {agent_source}\n"
|
|
3030
|
+
f"📊 **Content Length:** {len(content):,} characters\n"
|
|
3031
|
+
f"🏷️ **Tags:** {', '.join(tags) if tags else 'None'}\n\n"
|
|
3032
|
+
f"**Next Steps:**\n"
|
|
3033
|
+
f"• Other agents can now retrieve this context using the context ID\n"
|
|
3034
|
+
f"• Use `search_contexts` to find contexts by content or tags\n"
|
|
3035
|
+
f"• Use `list_contexts` to see all your saved contexts\n\n"
|
|
3036
|
+
f"🔗 **Share this context:** Provide the context ID `{context_id}` to other agents"
|
|
3037
|
+
)]
|
|
3038
|
+
|
|
3039
|
+
except APIError as e:
|
|
3040
|
+
logger.error(f"API Error saving context: {e}")
|
|
3041
|
+
return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
|
|
3042
|
+
except Exception as e:
|
|
3043
|
+
logger.error(f"Error saving context: {e}")
|
|
3044
|
+
return [TextContent(type="text", text=f"❌ Error saving context: {str(e)}")]
|
|
3045
|
+
|
|
3046
|
+
@mcp.tool()
|
|
3047
|
+
async def list_contexts(
|
|
3048
|
+
limit: int = 20,
|
|
3049
|
+
offset: int = 0,
|
|
3050
|
+
tags: Optional[str] = None,
|
|
3051
|
+
agent_source: Optional[str] = None
|
|
3052
|
+
) -> List[TextContent]:
|
|
3053
|
+
"""
|
|
3054
|
+
List saved conversation contexts with pagination and filtering.
|
|
3055
|
+
|
|
3056
|
+
Args:
|
|
3057
|
+
limit: Number of contexts to return (1-100, default: 20)
|
|
3058
|
+
offset: Number of contexts to skip for pagination (default: 0)
|
|
3059
|
+
tags: Comma-separated tags to filter by (optional)
|
|
3060
|
+
agent_source: Filter by specific agent source (optional)
|
|
3061
|
+
|
|
3062
|
+
Returns:
|
|
3063
|
+
List of conversation contexts with pagination info
|
|
3064
|
+
|
|
3065
|
+
Examples:
|
|
3066
|
+
- list_contexts() - List recent 20 contexts
|
|
3067
|
+
- list_contexts(limit=50) - List recent 50 contexts
|
|
3068
|
+
- list_contexts(tags="streaming,ai-sdk") - Filter by tags
|
|
3069
|
+
- list_contexts(agent_source="cursor") - Only contexts from Cursor
|
|
3070
|
+
"""
|
|
3071
|
+
try:
|
|
3072
|
+
# Validate parameters
|
|
3073
|
+
if limit < 1 or limit > 100:
|
|
3074
|
+
return [TextContent(type="text", text="❌ Error: Limit must be between 1 and 100")]
|
|
3075
|
+
|
|
3076
|
+
if offset < 0:
|
|
3077
|
+
return [TextContent(type="text", text="❌ Error: Offset must be 0 or greater")]
|
|
3078
|
+
|
|
3079
|
+
client = await ensure_api_client()
|
|
3080
|
+
|
|
3081
|
+
result = await client.list_contexts(
|
|
3082
|
+
limit=limit,
|
|
3083
|
+
offset=offset,
|
|
3084
|
+
tags=tags,
|
|
3085
|
+
agent_source=agent_source
|
|
3086
|
+
)
|
|
3087
|
+
|
|
3088
|
+
contexts = result.get("contexts", [])
|
|
3089
|
+
pagination = result.get("pagination", {})
|
|
3090
|
+
|
|
3091
|
+
if not contexts:
|
|
3092
|
+
response = "📭 **No Contexts Found**\n\n"
|
|
3093
|
+
if tags or agent_source:
|
|
3094
|
+
response += "No contexts match your filters.\n\n"
|
|
3095
|
+
else:
|
|
3096
|
+
response += "You haven't saved any contexts yet.\n\n"
|
|
3097
|
+
|
|
3098
|
+
response += "**Get started:**\n"
|
|
3099
|
+
response += "• Use `save_context` to save a conversation for cross-agent sharing\n"
|
|
3100
|
+
response += "• Perfect for handoffs between Cursor and Claude Code!"
|
|
3101
|
+
|
|
3102
|
+
return [TextContent(type="text", text=response)]
|
|
3103
|
+
|
|
3104
|
+
# Format the response
|
|
3105
|
+
response = f"📚 **Your Conversation Contexts** ({pagination.get('total', len(contexts))} total)\n\n"
|
|
3106
|
+
|
|
3107
|
+
for i, context in enumerate(contexts, offset + 1):
|
|
3108
|
+
created_at = context.get('created_at', '')
|
|
3109
|
+
if created_at:
|
|
3110
|
+
# Format datetime for better readability
|
|
3111
|
+
try:
|
|
3112
|
+
from datetime import datetime
|
|
3113
|
+
dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
|
|
3114
|
+
formatted_date = dt.strftime('%Y-%m-%d %H:%M UTC')
|
|
3115
|
+
except:
|
|
3116
|
+
formatted_date = created_at
|
|
3117
|
+
else:
|
|
3118
|
+
formatted_date = 'Unknown'
|
|
3119
|
+
|
|
3120
|
+
response += f"**{i}. {context['title']}**\n"
|
|
3121
|
+
response += f" 🆔 ID: `{context['id']}`\n"
|
|
3122
|
+
response += f" 🤖 Source: {context['agent_source']}\n"
|
|
3123
|
+
response += f" 📅 Created: {formatted_date}\n"
|
|
3124
|
+
response += f" 📝 Summary: {context['summary'][:100]}{'...' if len(context['summary']) > 100 else ''}\n"
|
|
3125
|
+
if context.get('tags'):
|
|
3126
|
+
response += f" 🏷️ Tags: {', '.join(context['tags'])}\n"
|
|
3127
|
+
response += "\n"
|
|
3128
|
+
|
|
3129
|
+
# Add pagination info
|
|
3130
|
+
if pagination.get('has_more'):
|
|
3131
|
+
next_offset = offset + limit
|
|
3132
|
+
response += f"📄 **Pagination:** Showing {offset + 1}-{offset + len(contexts)} of {pagination.get('total')}\n"
|
|
3133
|
+
response += f" Use `list_contexts(offset={next_offset})` for next page\n"
|
|
3134
|
+
|
|
3135
|
+
response += "\n**Actions:**\n"
|
|
3136
|
+
response += "• `retrieve_context(context_id)` - Get full context\n"
|
|
3137
|
+
response += "• `search_contexts(query)` - Search contexts\n"
|
|
3138
|
+
response += "• `delete_context(context_id)` - Remove context"
|
|
3139
|
+
|
|
3140
|
+
return [TextContent(type="text", text=response)]
|
|
3141
|
+
|
|
3142
|
+
except APIError as e:
|
|
3143
|
+
logger.error(f"API Error listing contexts: {e}")
|
|
3144
|
+
return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
|
|
3145
|
+
except Exception as e:
|
|
3146
|
+
logger.error(f"Error listing contexts: {e}")
|
|
3147
|
+
return [TextContent(type="text", text=f"❌ Error listing contexts: {str(e)}")]
|
|
3148
|
+
|
|
3149
|
+
@mcp.tool()
|
|
3150
|
+
async def retrieve_context(context_id: str) -> List[TextContent]:
|
|
3151
|
+
"""
|
|
3152
|
+
Retrieve a specific conversation context by ID.
|
|
3153
|
+
|
|
3154
|
+
Use this tool to get the full conversation context that was saved by
|
|
3155
|
+
another agent. Perfect for getting strategic context from Cursor
|
|
3156
|
+
when working in Claude Code.
|
|
3157
|
+
|
|
3158
|
+
Args:
|
|
3159
|
+
context_id: The unique ID of the context to retrieve
|
|
3160
|
+
|
|
3161
|
+
Returns:
|
|
3162
|
+
Full conversation context with metadata
|
|
3163
|
+
|
|
3164
|
+
Example:
|
|
3165
|
+
retrieve_context("550e8400-e29b-41d4-a716-446655440000")
|
|
3166
|
+
"""
|
|
3167
|
+
try:
|
|
3168
|
+
if not context_id or not context_id.strip():
|
|
3169
|
+
return [TextContent(type="text", text="❌ Error: Context ID is required")]
|
|
3170
|
+
|
|
3171
|
+
client = await ensure_api_client()
|
|
3172
|
+
|
|
3173
|
+
context = await client.get_context(context_id.strip())
|
|
3174
|
+
|
|
3175
|
+
if not context:
|
|
3176
|
+
return [TextContent(
|
|
3177
|
+
type="text",
|
|
3178
|
+
text=f"❌ **Context Not Found**\n\n"
|
|
3179
|
+
f"Context ID `{context_id}` was not found.\n\n"
|
|
3180
|
+
f"**Possible reasons:**\n"
|
|
3181
|
+
f"• The context ID is incorrect\n"
|
|
3182
|
+
f"• The context belongs to a different user\n"
|
|
3183
|
+
f"• The context has been deleted\n\n"
|
|
3184
|
+
f"Use `list_contexts()` to see your available contexts."
|
|
3185
|
+
)]
|
|
3186
|
+
|
|
3187
|
+
# Format the context display
|
|
3188
|
+
created_at = context.get('created_at', '')
|
|
3189
|
+
if created_at:
|
|
3190
|
+
try:
|
|
3191
|
+
from datetime import datetime
|
|
3192
|
+
dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
|
|
3193
|
+
formatted_date = dt.strftime('%Y-%m-%d %H:%M UTC')
|
|
3194
|
+
except:
|
|
3195
|
+
formatted_date = created_at
|
|
3196
|
+
else:
|
|
3197
|
+
formatted_date = 'Unknown'
|
|
3198
|
+
|
|
3199
|
+
updated_at = context.get('updated_at', '')
|
|
3200
|
+
if updated_at:
|
|
3201
|
+
try:
|
|
3202
|
+
from datetime import datetime
|
|
3203
|
+
dt = datetime.fromisoformat(updated_at.replace('Z', '+00:00'))
|
|
3204
|
+
formatted_updated = dt.strftime('%Y-%m-%d %H:%M UTC')
|
|
3205
|
+
except:
|
|
3206
|
+
formatted_updated = updated_at
|
|
3207
|
+
else:
|
|
3208
|
+
formatted_updated = None
|
|
3209
|
+
|
|
3210
|
+
response = f"📋 **Context: {context['title']}**\n\n"
|
|
3211
|
+
response += f"🆔 **ID:** `{context['id']}`\n"
|
|
3212
|
+
response += f"🤖 **Source Agent:** {context['agent_source']}\n"
|
|
3213
|
+
response += f"📅 **Created:** {formatted_date}\n"
|
|
3214
|
+
if formatted_updated:
|
|
3215
|
+
response += f"🔄 **Updated:** {formatted_updated}\n"
|
|
3216
|
+
|
|
3217
|
+
if context.get('tags'):
|
|
3218
|
+
response += f"🏷️ **Tags:** {', '.join(context['tags'])}\n"
|
|
3219
|
+
|
|
3220
|
+
response += f"\n📝 **Summary:**\n{context['summary']}\n\n"
|
|
3221
|
+
|
|
3222
|
+
# Add NIA References - CRITICAL for context handoffs
|
|
3223
|
+
nia_references = context.get('nia_references', {})
|
|
3224
|
+
if nia_references:
|
|
3225
|
+
response += "🧠 **NIA RESOURCES USED - RECOMMENDED ACTIONS:**\n"
|
|
3226
|
+
|
|
3227
|
+
indexed_resources = nia_references.get('indexed_resources', [])
|
|
3228
|
+
if indexed_resources:
|
|
3229
|
+
response += "**📦 Re-index these resources:**\n"
|
|
3230
|
+
for resource in indexed_resources:
|
|
3231
|
+
identifier = resource.get('identifier', 'Unknown')
|
|
3232
|
+
resource_type = resource.get('resource_type', 'unknown')
|
|
3233
|
+
purpose = resource.get('purpose', 'No purpose specified')
|
|
3234
|
+
|
|
3235
|
+
if resource_type == 'repository':
|
|
3236
|
+
response += f"• `Index {identifier}` - {purpose}\n"
|
|
3237
|
+
elif resource_type == 'documentation':
|
|
3238
|
+
response += f"• `Index documentation {identifier}` - {purpose}\n"
|
|
3239
|
+
else:
|
|
3240
|
+
response += f"• `Index {identifier}` ({resource_type}) - {purpose}\n"
|
|
3241
|
+
response += "\n"
|
|
3242
|
+
|
|
3243
|
+
search_queries = nia_references.get('search_queries', [])
|
|
3244
|
+
if search_queries:
|
|
3245
|
+
response += "**🔍 Useful search queries to re-run:**\n"
|
|
3246
|
+
for query in search_queries:
|
|
3247
|
+
query_text = query.get('query', 'Unknown query')
|
|
3248
|
+
query_type = query.get('query_type', 'search')
|
|
3249
|
+
key_findings = query.get('key_findings', 'No findings specified')
|
|
3250
|
+
resources_searched = query.get('resources_searched', [])
|
|
3251
|
+
|
|
3252
|
+
response += f"• **Query:** `{query_text}` ({query_type})\n"
|
|
3253
|
+
if resources_searched:
|
|
3254
|
+
response += f" **Resources:** {', '.join(resources_searched)}\n"
|
|
3255
|
+
response += f" **Key Findings:** {key_findings}\n"
|
|
3256
|
+
response += "\n"
|
|
3257
|
+
|
|
3258
|
+
session_summary = nia_references.get('session_summary')
|
|
3259
|
+
if session_summary:
|
|
3260
|
+
response += f"**📋 NIA Session Summary:** {session_summary}\n\n"
|
|
3261
|
+
|
|
3262
|
+
# Add Edited Files - CRITICAL for code handoffs
|
|
3263
|
+
edited_files = context.get('edited_files', [])
|
|
3264
|
+
if edited_files:
|
|
3265
|
+
response += "📝 **FILES MODIFIED - READ THESE TO GET UP TO SPEED:**\n"
|
|
3266
|
+
for file_info in edited_files:
|
|
3267
|
+
file_path = file_info.get('file_path', 'Unknown file')
|
|
3268
|
+
operation = file_info.get('operation', 'modified')
|
|
3269
|
+
changes_desc = file_info.get('changes_description', 'No description')
|
|
3270
|
+
key_changes = file_info.get('key_changes', [])
|
|
3271
|
+
language = file_info.get('language', '')
|
|
3272
|
+
|
|
3273
|
+
operation_emoji = {
|
|
3274
|
+
'created': '🆕',
|
|
3275
|
+
'modified': '✏️',
|
|
3276
|
+
'deleted': '🗑️'
|
|
3277
|
+
}.get(operation, '📄')
|
|
3278
|
+
|
|
3279
|
+
response += f"• {operation_emoji} **`{file_path}`** ({operation})\n"
|
|
3280
|
+
response += f" **Changes:** {changes_desc}\n"
|
|
3281
|
+
|
|
3282
|
+
if key_changes:
|
|
3283
|
+
response += f" **Key Changes:** {', '.join(key_changes)}\n"
|
|
3284
|
+
if language:
|
|
3285
|
+
response += f" **Language:** {language}\n"
|
|
3286
|
+
|
|
3287
|
+
response += f" **💡 Action:** Read this file with: `Read {file_path}`\n"
|
|
3288
|
+
response += "\n"
|
|
3289
|
+
|
|
3290
|
+
# Add metadata if available
|
|
3291
|
+
metadata = context.get('metadata', {})
|
|
3292
|
+
if metadata:
|
|
3293
|
+
response += f"📊 **Additional Metadata:**\n"
|
|
3294
|
+
for key, value in metadata.items():
|
|
3295
|
+
if isinstance(value, list):
|
|
3296
|
+
response += f"• **{key}:** {', '.join(map(str, value))}\n"
|
|
3297
|
+
else:
|
|
3298
|
+
response += f"• **{key}:** {value}\n"
|
|
3299
|
+
response += "\n"
|
|
3300
|
+
|
|
3301
|
+
response += f"📄 **Full Context:**\n\n{context['content']}\n\n"
|
|
3302
|
+
|
|
3303
|
+
response += f"---\n"
|
|
3304
|
+
response += f"🚀 **NEXT STEPS FOR SEAMLESS HANDOFF:**\n"
|
|
3305
|
+
response += f"• This context was created by **{context['agent_source']}**\n"
|
|
3306
|
+
|
|
3307
|
+
if nia_references.get('search_queries'):
|
|
3308
|
+
response += f"• **RECOMMENDED:** Re-run the search queries to get the same insights\n"
|
|
3309
|
+
if edited_files:
|
|
3310
|
+
response += f"• **ESSENTIAL:** Read the modified files above to understand code changes\n"
|
|
3311
|
+
|
|
3312
|
+
response += f"• Use the summary and full context to understand the strategic planning\n"
|
|
3313
|
+
|
|
3314
|
+
return [TextContent(type="text", text=response)]
|
|
3315
|
+
|
|
3316
|
+
except APIError as e:
|
|
3317
|
+
logger.error(f"API Error retrieving context: {e}")
|
|
3318
|
+
return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
|
|
3319
|
+
except Exception as e:
|
|
3320
|
+
logger.error(f"Error retrieving context: {e}")
|
|
3321
|
+
return [TextContent(type="text", text=f"❌ Error retrieving context: {str(e)}")]
|
|
3322
|
+
|
|
3323
|
+
@mcp.tool()
|
|
3324
|
+
async def search_contexts(
|
|
3325
|
+
query: str,
|
|
3326
|
+
limit: int = 20,
|
|
3327
|
+
tags: Optional[str] = None,
|
|
3328
|
+
agent_source: Optional[str] = None
|
|
3329
|
+
) -> List[TextContent]:
|
|
3330
|
+
"""
|
|
3331
|
+
Search conversation contexts by content, title, or summary.
|
|
3332
|
+
|
|
3333
|
+
Perfect for finding relevant contexts when you remember part of the
|
|
3334
|
+
conversation but not the exact context ID.
|
|
3335
|
+
|
|
3336
|
+
Args:
|
|
3337
|
+
query: Search query to match against title, summary, content, and tags
|
|
3338
|
+
limit: Maximum number of results to return (1-100, default: 20)
|
|
3339
|
+
tags: Comma-separated tags to filter by (optional)
|
|
3340
|
+
agent_source: Filter by specific agent source (optional)
|
|
3341
|
+
|
|
3342
|
+
Returns:
|
|
3343
|
+
Search results with matching contexts
|
|
3344
|
+
|
|
3345
|
+
Examples:
|
|
3346
|
+
- search_contexts("streaming AI SDK")
|
|
3347
|
+
- search_contexts("authentication", tags="security,implementation")
|
|
3348
|
+
- search_contexts("database", agent_source="cursor")
|
|
3349
|
+
"""
|
|
3350
|
+
try:
|
|
3351
|
+
# Validate parameters
|
|
3352
|
+
if not query or not query.strip():
|
|
3353
|
+
return [TextContent(type="text", text="❌ Error: Search query is required")]
|
|
3354
|
+
|
|
3355
|
+
if limit < 1 or limit > 100:
|
|
3356
|
+
return [TextContent(type="text", text="❌ Error: Limit must be between 1 and 100")]
|
|
3357
|
+
|
|
3358
|
+
client = await ensure_api_client()
|
|
3359
|
+
|
|
3360
|
+
result = await client.search_contexts(
|
|
3361
|
+
query=query.strip(),
|
|
3362
|
+
limit=limit,
|
|
3363
|
+
tags=tags,
|
|
3364
|
+
agent_source=agent_source
|
|
3365
|
+
)
|
|
3366
|
+
|
|
3367
|
+
contexts = result.get("contexts", [])
|
|
3368
|
+
|
|
3369
|
+
if not contexts:
|
|
3370
|
+
response = f"🔍 **No Results Found**\n\n"
|
|
3371
|
+
response += f"No contexts match your search query: \"{query}\"\n\n"
|
|
3372
|
+
|
|
3373
|
+
if tags or agent_source:
|
|
3374
|
+
response += f"**Active filters:**\n"
|
|
3375
|
+
if tags:
|
|
3376
|
+
response += f"• Tags: {tags}\n"
|
|
3377
|
+
if agent_source:
|
|
3378
|
+
response += f"• Agent: {agent_source}\n"
|
|
3379
|
+
response += "\n"
|
|
3380
|
+
|
|
3381
|
+
response += f"**Suggestions:**\n"
|
|
3382
|
+
response += f"• Try different keywords\n"
|
|
3383
|
+
response += f"• Remove filters to broaden search\n"
|
|
3384
|
+
response += f"• Use `list_contexts()` to see all contexts"
|
|
3385
|
+
|
|
3386
|
+
return [TextContent(type="text", text=response)]
|
|
3387
|
+
|
|
3388
|
+
# Format search results
|
|
3389
|
+
response = f"🔍 **Search Results for \"{query}\"** ({len(contexts)} found)\n\n"
|
|
3390
|
+
|
|
3391
|
+
for i, context in enumerate(contexts, 1):
|
|
3392
|
+
created_at = context.get('created_at', '')
|
|
3393
|
+
if created_at:
|
|
3394
|
+
try:
|
|
3395
|
+
from datetime import datetime
|
|
3396
|
+
dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
|
|
3397
|
+
formatted_date = dt.strftime('%Y-%m-%d %H:%M UTC')
|
|
3398
|
+
except:
|
|
3399
|
+
formatted_date = created_at
|
|
3400
|
+
else:
|
|
3401
|
+
formatted_date = 'Unknown'
|
|
3402
|
+
|
|
3403
|
+
response += f"**{i}. {context['title']}**\n"
|
|
3404
|
+
response += f" 🆔 ID: `{context['id']}`\n"
|
|
3405
|
+
response += f" 🤖 Source: {context['agent_source']}\n"
|
|
3406
|
+
response += f" 📅 Created: {formatted_date}\n"
|
|
3407
|
+
response += f" 📝 Summary: {context['summary'][:150]}{'...' if len(context['summary']) > 150 else ''}\n"
|
|
3408
|
+
|
|
3409
|
+
if context.get('tags'):
|
|
3410
|
+
response += f" 🏷️ Tags: {', '.join(context['tags'])}\n"
|
|
3411
|
+
|
|
3412
|
+
response += "\n"
|
|
3413
|
+
|
|
3414
|
+
response += f"**Actions:**\n"
|
|
3415
|
+
response += f"• `retrieve_context(context_id)` - Get full context\n"
|
|
3416
|
+
response += f"• Refine search with different keywords\n"
|
|
3417
|
+
response += f"• Use tags or agent filters for better results"
|
|
3418
|
+
|
|
3419
|
+
return [TextContent(type="text", text=response)]
|
|
3420
|
+
|
|
3421
|
+
except APIError as e:
|
|
3422
|
+
logger.error(f"API Error searching contexts: {e}")
|
|
3423
|
+
return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
|
|
3424
|
+
except Exception as e:
|
|
3425
|
+
logger.error(f"Error searching contexts: {e}")
|
|
3426
|
+
return [TextContent(type="text", text=f"❌ Error searching contexts: {str(e)}")]
|
|
3427
|
+
|
|
3428
|
+
@mcp.tool()
|
|
3429
|
+
async def update_context(
|
|
3430
|
+
context_id: str,
|
|
3431
|
+
title: Optional[str] = None,
|
|
3432
|
+
summary: Optional[str] = None,
|
|
3433
|
+
content: Optional[str] = None,
|
|
3434
|
+
tags: Optional[List[str]] = None,
|
|
3435
|
+
metadata: Optional[dict] = None
|
|
3436
|
+
) -> List[TextContent]:
|
|
3437
|
+
"""
|
|
3438
|
+
Update an existing conversation context.
|
|
3439
|
+
|
|
3440
|
+
Args:
|
|
3441
|
+
context_id: The unique ID of the context to update
|
|
3442
|
+
title: Updated title (optional)
|
|
3443
|
+
summary: Updated summary (optional)
|
|
3444
|
+
content: Updated content (optional)
|
|
3445
|
+
tags: Updated tags list (optional)
|
|
3446
|
+
metadata: Updated metadata (optional)
|
|
3447
|
+
|
|
3448
|
+
Returns:
|
|
3449
|
+
Confirmation of successful update
|
|
3450
|
+
|
|
3451
|
+
Example:
|
|
3452
|
+
update_context(
|
|
3453
|
+
context_id="550e8400-e29b-41d4-a716-446655440000",
|
|
3454
|
+
title="Updated: Streaming AI SDK Implementation",
|
|
3455
|
+
tags=["streaming", "ai-sdk", "completed"]
|
|
3456
|
+
)
|
|
3457
|
+
"""
|
|
3458
|
+
try:
|
|
3459
|
+
if not context_id or not context_id.strip():
|
|
3460
|
+
return [TextContent(type="text", text="❌ Error: Context ID is required")]
|
|
3461
|
+
|
|
3462
|
+
# Check that at least one field is being updated
|
|
3463
|
+
if not any([title, summary, content, tags is not None, metadata is not None]):
|
|
3464
|
+
return [TextContent(
|
|
3465
|
+
type="text",
|
|
3466
|
+
text="❌ Error: At least one field must be provided for update"
|
|
3467
|
+
)]
|
|
3468
|
+
|
|
3469
|
+
# Validate fields if provided
|
|
3470
|
+
if title is not None and (not title.strip() or len(title) > 200):
|
|
3471
|
+
return [TextContent(
|
|
3472
|
+
type="text",
|
|
3473
|
+
text="❌ Error: Title must be 1-200 characters"
|
|
3474
|
+
)]
|
|
3475
|
+
|
|
3476
|
+
if summary is not None and (len(summary) < 10 or len(summary) > 1000):
|
|
3477
|
+
return [TextContent(
|
|
3478
|
+
type="text",
|
|
3479
|
+
text="❌ Error: Summary must be 10-1000 characters"
|
|
3480
|
+
)]
|
|
3481
|
+
|
|
3482
|
+
if content is not None and len(content) < 50:
|
|
3483
|
+
return [TextContent(
|
|
3484
|
+
type="text",
|
|
3485
|
+
text="❌ Error: Content must be at least 50 characters"
|
|
3486
|
+
)]
|
|
3487
|
+
|
|
3488
|
+
if tags is not None and len(tags) > 10:
|
|
3489
|
+
return [TextContent(
|
|
3490
|
+
type="text",
|
|
3491
|
+
text="❌ Error: Maximum 10 tags allowed"
|
|
3492
|
+
)]
|
|
3493
|
+
|
|
3494
|
+
client = await ensure_api_client()
|
|
3495
|
+
|
|
3496
|
+
result = await client.update_context(
|
|
3497
|
+
context_id=context_id.strip(),
|
|
3498
|
+
title=title.strip() if title else None,
|
|
3499
|
+
summary=summary.strip() if summary else None,
|
|
3500
|
+
content=content,
|
|
3501
|
+
tags=tags,
|
|
3502
|
+
metadata=metadata
|
|
3503
|
+
)
|
|
3504
|
+
|
|
3505
|
+
if not result:
|
|
3506
|
+
return [TextContent(
|
|
3507
|
+
type="text",
|
|
3508
|
+
text=f"❌ Error: Context with ID `{context_id}` not found"
|
|
3509
|
+
)]
|
|
3510
|
+
|
|
3511
|
+
# List updated fields
|
|
3512
|
+
updated_fields = []
|
|
3513
|
+
if title is not None:
|
|
3514
|
+
updated_fields.append("title")
|
|
3515
|
+
if summary is not None:
|
|
3516
|
+
updated_fields.append("summary")
|
|
3517
|
+
if content is not None:
|
|
3518
|
+
updated_fields.append("content")
|
|
3519
|
+
if tags is not None:
|
|
3520
|
+
updated_fields.append("tags")
|
|
3521
|
+
if metadata is not None:
|
|
3522
|
+
updated_fields.append("metadata")
|
|
3523
|
+
|
|
3524
|
+
response = f"✅ **Context Updated Successfully!**\n\n"
|
|
3525
|
+
response += f"🆔 **Context ID:** `{context_id}`\n"
|
|
3526
|
+
response += f"📝 **Title:** {result['title']}\n"
|
|
3527
|
+
response += f"🔄 **Updated Fields:** {', '.join(updated_fields)}\n"
|
|
3528
|
+
response += f"🤖 **Source Agent:** {result['agent_source']}\n\n"
|
|
3529
|
+
|
|
3530
|
+
response += f"**Current Status:**\n"
|
|
3531
|
+
response += f"• **Tags:** {', '.join(result['tags']) if result.get('tags') else 'None'}\n"
|
|
3532
|
+
response += f"• **Content Length:** {len(result['content']):,} characters\n\n"
|
|
3533
|
+
|
|
3534
|
+
response += f"Use `retrieve_context('{context_id}')` to see the full updated context."
|
|
3535
|
+
|
|
3536
|
+
return [TextContent(type="text", text=response)]
|
|
3537
|
+
|
|
3538
|
+
except APIError as e:
|
|
3539
|
+
logger.error(f"API Error updating context: {e}")
|
|
3540
|
+
return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
|
|
3541
|
+
except Exception as e:
|
|
3542
|
+
logger.error(f"Error updating context: {e}")
|
|
3543
|
+
return [TextContent(type="text", text=f"❌ Error updating context: {str(e)}")]
|
|
3544
|
+
|
|
3545
|
+
@mcp.tool()
|
|
3546
|
+
async def delete_context(context_id: str) -> List[TextContent]:
|
|
3547
|
+
"""
|
|
3548
|
+
Delete a conversation context.
|
|
3549
|
+
|
|
3550
|
+
Args:
|
|
3551
|
+
context_id: The unique ID of the context to delete
|
|
3552
|
+
|
|
3553
|
+
Returns:
|
|
3554
|
+
Confirmation of successful deletion
|
|
3555
|
+
|
|
3556
|
+
Example:
|
|
3557
|
+
delete_context("550e8400-e29b-41d4-a716-446655440000")
|
|
3558
|
+
"""
|
|
3559
|
+
try:
|
|
3560
|
+
if not context_id or not context_id.strip():
|
|
3561
|
+
return [TextContent(type="text", text="❌ Error: Context ID is required")]
|
|
3562
|
+
|
|
3563
|
+
client = await ensure_api_client()
|
|
3564
|
+
|
|
3565
|
+
success = await client.delete_context(context_id.strip())
|
|
3566
|
+
|
|
3567
|
+
if success:
|
|
3568
|
+
return [TextContent(
|
|
3569
|
+
type="text",
|
|
3570
|
+
text=f"✅ **Context Deleted Successfully!**\n\n"
|
|
3571
|
+
f"🆔 **Context ID:** `{context_id}`\n\n"
|
|
3572
|
+
f"The context has been permanently removed from your account.\n"
|
|
3573
|
+
f"This action cannot be undone.\n\n"
|
|
3574
|
+
f"Use `list_contexts()` to see your remaining contexts."
|
|
3575
|
+
)]
|
|
3576
|
+
else:
|
|
3577
|
+
return [TextContent(
|
|
3578
|
+
type="text",
|
|
3579
|
+
text=f"❌ **Context Not Found**\n\n"
|
|
3580
|
+
f"Context ID `{context_id}` was not found or has already been deleted.\n\n"
|
|
3581
|
+
f"Use `list_contexts()` to see your available contexts."
|
|
3582
|
+
)]
|
|
3583
|
+
|
|
3584
|
+
except APIError as e:
|
|
3585
|
+
logger.error(f"API Error deleting context: {e}")
|
|
3586
|
+
return [TextContent(type="text", text=f"❌ API Error: {str(e)}")]
|
|
3587
|
+
except Exception as e:
|
|
3588
|
+
logger.error(f"Error deleting context: {e}")
|
|
3589
|
+
return [TextContent(type="text", text=f"❌ Error deleting context: {str(e)}")]
|
|
3590
|
+
|
|
3090
3591
|
# Resources
|
|
3091
3592
|
|
|
3092
3593
|
# Note: FastMCP doesn't have list_resources or read_resource decorators
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/assets/rules/claude_rules.md
RENAMED
|
File without changes
|
{nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/assets/rules/cursor_rules.md
RENAMED
|
File without changes
|
{nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/assets/rules/nia_rules.md
RENAMED
|
File without changes
|
{nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/assets/rules/vscode_rules.md
RENAMED
|
File without changes
|
{nia_mcp_server-1.0.22 → nia_mcp_server-1.0.24}/src/nia_mcp_server/assets/rules/windsurf_rules.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|